feat: external status monitor for registratura (Primaria Cluj-Napoca)

- Add ExternalStatusTracking types + ExternalDocStatus semantic states
- Authority catalog with Primaria Cluj-Napoca (POST scraper + HTML parser)
- Status check service: batch + single entry, change detection via hash
- API routes: cron-triggered batch (/api/registratura/status-check) +
  user-triggered single (/api/registratura/status-check/single)
- Add "status-change" notification type with instant email on change
- Table badge: Radio icon color-coded by status (amber/blue/green/red)
- Detail panel: full monitoring section with status, history, manual check
- Auto-detection: prompt when recipient matches known authority
- Activation dialog: configure petitioner name + confirm registration data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-11 14:42:21 +02:00
parent 1c51236c31
commit d7bd1a7f5d
10 changed files with 1201 additions and 6 deletions
@@ -12,6 +12,7 @@ const VALID_TYPES: NotificationType[] = [
"deadline-urgent",
"deadline-overdue",
"document-expiry",
"status-change",
];
type SessionUser = {
@@ -0,0 +1,38 @@
/**
* External Status Check — Cron-triggered batch endpoint.
*
* N8N cron: POST with Bearer token, 4x/day (9, 12, 15, 17) weekdays.
* ?test=true for dry-run (check but don't save/email).
*/
import { NextResponse } from "next/server";
import { runStatusCheck } from "@/modules/registratura/services/status-check-service";
const CRON_SECRET =
process.env.STATUS_CHECK_CRON_SECRET ??
process.env.NOTIFICATION_CRON_SECRET;
export async function POST(request: Request) {
if (!CRON_SECRET) {
return NextResponse.json(
{ error: "STATUS_CHECK_CRON_SECRET not configured" },
{ status: 500 },
);
}
const authHeader = request.headers.get("Authorization");
const token = authHeader?.replace("Bearer ", "");
if (token !== CRON_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const url = new URL(request.url);
const dryRun = url.searchParams.get("test") === "true";
const result = await runStatusCheck(dryRun);
return NextResponse.json(result, {
status: result.success ? 200 : 500,
});
}
@@ -0,0 +1,34 @@
/**
* Single Entry Status Check — User-triggered from UI.
*
* POST with session auth, body: { entryId: string }
*/
import { NextRequest, NextResponse } from "next/server";
import { getAuthSession } from "@/core/auth";
import { checkSingleEntry } from "@/modules/registratura/services/status-check-service";
export async function POST(req: NextRequest) {
const session = await getAuthSession();
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const body = await req.json();
const { entryId } = body as { entryId: string };
if (!entryId) {
return NextResponse.json(
{ error: "Missing entryId" },
{ status: 400 },
);
}
const result = await checkSingleEntry(entryId);
return NextResponse.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : "Internal error";
return NextResponse.json({ error: message }, { status: 500 });
}
}