← 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 });
  }
}