Swadhin Biswas feat(api): add runner, analytics, packages, and security services · 3/6/2026
28f7954
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { type APIRoute } from "astro";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { and, desc, eq, or, isNull } from "drizzle-orm";
import { getDatabase, schema } from "@/db";
import { notifications } from "@/db/schema";
import { getUserFromRequest } from "@/lib/auth";
import { scoreNotificationPriority } from "@/lib/notification-priority";
import { getRoutingDecision } from "@/lib/notification-routing";
import { success, unauthorized, serverError } from "@/lib/api";
const BLOCKING_NOTIFICATION_TYPES = [
"ci_failed",
"review_request",
"security_alert",
"merge_conflict",
"merge_blocked",
];
const BLOCKING_NOTIFICATION_REASONS = [
"ci_failed",
"review_requested",
"security_alert",
"merge_conflict",
"merge_blocked",
];
export const GET: APIRoute = async ({ request }) => {
try {
const tokenPayload = await getUserFromRequest(request);
if (!tokenPayload) return unauthorized();
const db = getDatabase() as NodePgDatabase<typeof schema>;
const blocking = await db.query.notifications.findMany({
where: and(
eq(notifications.userId, tokenPayload.userId),
eq(notifications.isArchived, false),
or(
...BLOCKING_NOTIFICATION_TYPES.map((type) => eq(notifications.type, type)),
...BLOCKING_NOTIFICATION_REASONS.map((reason) => eq(notifications.reason, reason))
)!
),
orderBy: [desc(notifications.createdAt)],
limit: 100,
with: {
repository: {
columns: { id: true, name: true, slug: true },
},
},
});
const scored = blocking.map((item) => {
const priority = scoreNotificationPriority(item);
return {
...item,
priority: priority.priority,
priorityScore: priority.score,
isBlocking: priority.isBlocking,
};
});
scored.sort((a, b) => {
if (b.priorityScore !== a.priorityScore) return b.priorityScore - a.priorityScore;
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
});
const countsByType: Record<string, number> = {};
const countsByReason: Record<string, number> = {};
const countsByPrimaryChannel: Record<string, number> = {};
const nowMs = Date.now();
const staleThresholdHours = 4;
let staleBlockingCount = 0;
const prefs = db.query.notificationPreferences
? await db.query.notificationPreferences.findMany({
where: and(
eq(schema.notificationPreferences.userId, tokenPayload.userId),
isNull(schema.notificationPreferences.repositoryId)
),
})
: [];
for (const item of scored) {
const type = item.type || "unknown";
const reason = item.reason || "unknown";
countsByType[type] = (countsByType[type] || 0) + 1;
countsByReason[reason] = (countsByReason[reason] || 0) + 1;
const routing = getRoutingDecision(item.type, prefs as any);
countsByPrimaryChannel[routing.primaryChannel] = (countsByPrimaryChannel[routing.primaryChannel] || 0) + 1;
const ageHours = (nowMs - new Date(item.createdAt).getTime()) / (1000 * 60 * 60);
if (!item.isRead && ageHours >= staleThresholdHours) {
staleBlockingCount++;
}
}
return success({
totalBlocking: scored.length,
unreadBlockingCount: scored.filter((item) => !item.isRead).length,
staleBlockingCount,
countsByType,
countsByReason,
countsByPrimaryChannel,
topPriority: scored[0]?.priority || null,
items: scored.slice(0, 20),
});
} catch (error) {
return serverError("Failed to load blocking notification summary");
}
};