← back to Norma
app/api/cron/dispatch-scheduler/route.ts
133 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { auditLog } from '@/lib/audit';
import { verifyCronAuth, buildInternalAuthHeader } from '@/lib/cron-auth';
/**
* POST /api/cron/dispatch-scheduler
* Called every 5 minutes by system cron. Dispatches scheduled items.
*/
export async function POST(request: NextRequest) {
const auth = verifyCronAuth(request);
if (auth instanceof NextResponse) return auth;
try {
// Find due dispatches
const { rows: due } = await query(
`SELECT * FROM scheduled_dispatches
WHERE status = 'scheduled' AND scheduled_at <= NOW()
ORDER BY scheduled_at ASC
LIMIT 20`,
);
if (due.length === 0) {
// Return 204 No Content — nothing to log
return new NextResponse(null, { status: 204 });
}
const results = [];
for (const dispatch of due) {
try {
// Call Pulse agent to dispatch to the target platform agent
const pulseRes = await fetch('http://127.0.0.1:9845/api/pulse/dispatch', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': buildInternalAuthHeader(),
},
body: JSON.stringify({
petition_id: dispatch.pipeline_id || dispatch.statement_id,
title: dispatch.title,
body: dispatch.body,
target_agents: [dispatch.platform],
}),
});
const pulseData = await pulseRes.json();
const dispatched = pulseData?.summary?.dispatched > 0;
// Update scheduled_dispatches row
await query(
`UPDATE scheduled_dispatches
SET status = $1,
dispatched_at = NOW(),
dispatch_result = $2
WHERE id = $3`,
[
dispatched ? 'dispatched' : 'failed',
JSON.stringify(pulseData),
dispatch.id,
],
);
// Update source table status after dispatch
if (dispatch.pipeline_id) {
await query(
`UPDATE petition_pipeline
SET status = CASE WHEN status = 'approved' THEN 'posting' ELSE status END,
updated_at = NOW()
WHERE id = $1`,
[dispatch.pipeline_id],
);
}
if (dispatch.petition_id) {
await query(
`UPDATE petitions SET updated_at = NOW() WHERE id = $1`,
[dispatch.petition_id],
);
}
if (dispatch.grant_id) {
await query(
`UPDATE grants SET updated_at = NOW() WHERE id = $1`,
[dispatch.grant_id],
);
}
await auditLog(
dispatched ? 'scheduled.dispatched' : 'scheduled.failed',
'scheduled_dispatches',
dispatch.id,
{ platform: dispatch.platform, pulse_result: pulseData },
);
results.push({
id: dispatch.id,
platform: dispatch.platform,
status: dispatched ? 'dispatched' : 'failed',
title: dispatch.title.slice(0, 60),
});
} catch (err) {
// Mark as failed
await query(
`UPDATE scheduled_dispatches
SET status = 'failed',
error_message = $1,
dispatched_at = NOW()
WHERE id = $2`,
[(err as Error).message, dispatch.id],
);
results.push({
id: dispatch.id,
platform: dispatch.platform,
status: 'failed',
error: (err as Error).message,
});
}
}
console.log(`[cron/dispatch-scheduler] Processed ${results.length} dispatches: ${results.filter(r => r.status === 'dispatched').length} dispatched, ${results.filter(r => r.status === 'failed').length} failed`);
return NextResponse.json({
processed: results.length,
dispatched: results.filter(r => r.status === 'dispatched').length,
failed: results.filter(r => r.status === 'failed').length,
results,
});
} catch (err) {
console.error('[cron/dispatch-scheduler] error:', (err as Error).message);
return NextResponse.json({ error: 'Scheduler failed' }, { status: 500 });
}
}