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:
@@ -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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user