← back to Norma
app/api/geo/orchestrate/route.ts
91 lines
/**
* POST /api/geo/orchestrate
*
* Master orchestrator that runs all geo-profiling sub-agents in sequence:
* 1. buildZipProfiles() — create composite zip profiles
* 2. detectHotspots() — flag statistical outliers
* 3. computeAdvocacyScores() — score zips for campaign targeting
*
* Returns combined results with timing for each step.
* Requires authentication.
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireRole } from '@/lib/require-role';
import {
buildZipProfiles,
detectHotspots,
computeAdvocacyScores,
type ProfilerResult,
} from '@/lib/geo-profiler';
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
const overallStart = Date.now();
const results: Record<string, ProfilerResult> = {};
const stepTimings: { step: string; elapsed: string; status: string }[] = [];
try {
// ── Step 1: Build zip profiles ──
const profileResult = await buildZipProfiles();
results.buildZipProfiles = profileResult;
stepTimings.push({
step: 'buildZipProfiles',
elapsed: profileResult.elapsed,
status: profileResult.errors === 0 ? 'success' : 'partial',
});
// ── Step 2: Detect hotspots ──
const hotspotResult = await detectHotspots();
results.detectHotspots = hotspotResult;
stepTimings.push({
step: 'detectHotspots',
elapsed: hotspotResult.elapsed,
status: hotspotResult.errors === 0 ? 'success' : 'partial',
});
// ── Step 3: Compute advocacy scores ──
const advocacyResult = await computeAdvocacyScores();
results.computeAdvocacyScores = advocacyResult;
stepTimings.push({
step: 'computeAdvocacyScores',
elapsed: advocacyResult.elapsed,
status: advocacyResult.errors === 0 ? 'success' : 'partial',
});
const overallElapsed = `${((Date.now() - overallStart) / 1000).toFixed(1)}s`;
const totalProcessed = Object.values(results).reduce((sum, r) => sum + r.processed, 0);
const totalErrors = Object.values(results).reduce((sum, r) => sum + r.errors, 0);
return NextResponse.json({
success: true,
overall: {
total_processed: totalProcessed,
total_errors: totalErrors,
elapsed: overallElapsed,
status: totalErrors === 0 ? 'success' : 'partial',
},
steps: stepTimings,
details: results,
triggered_by: auth.username,
completed_at: new Date().toISOString(),
});
} catch (err) {
const overallElapsed = `${((Date.now() - overallStart) / 1000).toFixed(1)}s`;
console.error('[geo/orchestrate] Pipeline error:', (err as Error).message);
return NextResponse.json(
{
success: false,
error: `Pipeline failed: ${(err as Error).message}`,
elapsed: overallElapsed,
steps: stepTimings,
details: results,
},
{ status: 500 },
);
}
}