- 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.
149 lines
4.6 KiB
TypeScript
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,
|
|
};
|
|
});
|