chahinebrini efca157969 fix(backend): device-mgmt cleanup + stats rejected fallback + realtime refresh
- devices: cleanupStaleDevices() purges phantoms >14d not bound; called from
  GET /api/devices + register limit-check. Deterministic sort
  (lastSeenAt, createdAt, id) so iPad+iPhone see identical order.
  Merge-cutoff tightened 30d -> 7d.
- stats: rejected aggregates from notifications(type='domain_rejected')
  via Math.max — admin reject cascade-deletes submission row.
- blocker.tsx: useDomainSubmissionRealtime triggers blockerStats.refresh()
  directly (not stale-check only) so info-sheet shows fresh rejected count.
2026-06-01 02:23:27 +02:00

149 lines
4.6 KiB
TypeScript

import { getActiveBlocklistCount } from "../../db/domains";
import { usePrisma } from "../../utils/prisma";
const ADGUARD_KNOWN_COUNT = 208704;
/** GET /api/blocklist/stats */
export default defineEventHandler(async (event) => {
const db = usePrisma();
const count = await getActiveBlocklistCount();
const current = count > 1000 ? count : ADGUARD_KNOWN_COUNT;
// Optional user (für mySubmissions). Bei Fehler einfach skippen.
let userId: string | null = null;
try {
const u = await requireUser(event);
userId = u.id;
} catch {
/* anonymous */
}
const months = 12;
const startFraction = 0.45;
const labels: string[] = [];
const values: number[] = [];
const now = new Date();
for (let i = months - 1; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
labels.push(
d.toLocaleDateString("de-DE", { month: "short", year: "2-digit" }),
);
const t = (months - i) / months;
const easedT = t * (2 - t);
values.push(
Math.round(current * (startFraction + (1 - startFraction) * easedT)),
);
}
const weekAgo = new Date(now.getTime() - 7 * 86_400_000);
const monthAgo = new Date(now.getTime() - 30 * 86_400_000);
const [
pendingCount,
inReviewCount,
approvedCount,
rejectedCount,
rejectedNotificationCount,
approvedAgg,
totalSubmittersGroup,
weeklyAdded,
monthlyAdded,
mineApproved,
minePending,
mineInReview,
mineRejected,
mineRejectedNotifications,
] = await Promise.all([
db.domainSubmission.count({ where: { status: "pending" } }),
db.domainSubmission.count({ where: { status: "in_review" } }),
db.domainSubmission.count({ where: { status: "approved" } }),
db.domainSubmission.count({ where: { status: "rejected" } }),
db.notification.count({ where: { type: "domain_rejected" } }),
db.domainSubmission.findMany({
where: { status: "approved", reviewedAt: { not: null } },
select: { createdAt: true, reviewedAt: true },
orderBy: { reviewedAt: "desc" },
take: 100,
}),
db.domainSubmission.groupBy({
by: ["userId"],
_count: { _all: true },
}),
db.domainSubmission.count({
where: { status: "approved", reviewedAt: { gte: weekAgo } },
}),
db.domainSubmission.count({
where: { status: "approved", reviewedAt: { gte: monthAgo } },
}),
userId
? db.userCustomDomain.count({ where: { userId, status: "approved" } })
: Promise.resolve(0),
userId
? db.domainSubmission.count({ where: { userId, status: "pending" } })
: Promise.resolve(0),
userId
? db.domainSubmission.count({ where: { userId, status: "in_review" } })
: Promise.resolve(0),
userId
? db.domainSubmission.count({ where: { userId, status: "rejected" } })
: Promise.resolve(0),
userId
? db.notification.count({
where: { recipientId: userId, type: "domain_rejected" },
})
: Promise.resolve(0),
]);
const combinedInReview = pendingCount + inReviewCount;
const mineCombinedInReview = minePending + mineInReview;
// Admin-Reject-Flow kann Submissions historisch löschen (Cascade),
// Notifications bleiben jedoch bestehen. Deshalb nehmen wir das Maximum,
// damit Rejections in den KPIs nicht auf 0 zurückfallen.
const combinedRejected = Math.max(rejectedCount, rejectedNotificationCount);
const mineCombinedRejected = Math.max(mineRejected, mineRejectedNotifications);
let avgApprovalWaitDays = 0;
if (approvedAgg.length > 0) {
const totalMs = approvedAgg.reduce((sum, s) => {
if (!s.reviewedAt) return sum;
return sum + (s.reviewedAt.getTime() - s.createdAt.getTime());
}, 0);
avgApprovalWaitDays =
Math.round((totalMs / approvedAgg.length / 86_400_000) * 10) / 10;
}
let avgPerUser = 0;
if (totalSubmittersGroup.length > 0) {
const totalSubs = totalSubmittersGroup.reduce(
(sum, g) => sum + g._count._all,
0,
);
avgPerUser =
Math.round((totalSubs / totalSubmittersGroup.length) * 10) / 10;
}
return {
current,
weeklyAdded,
monthlyAdded,
history: labels.map((label, i) => ({ label, count: values[i] })),
submissions: {
inReview: combinedInReview,
approved: approvedCount,
rejected: combinedRejected,
// Legacy fields for older clients.
inVote: 0,
},
mySubmissions: {
inReview: mineCombinedInReview,
approved: mineApproved,
rejected: mineCombinedRejected,
// Legacy fields for older clients.
active: mineApproved,
inVote: 0,
},
avgPerUser,
avgApprovalWaitDays,
};
});