← back to Norma
app/api/geo/topic-grid/route.ts
118 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
/**
* GET /api/geo/topic-grid
*
* Returns a matrix of topic x state heat values suitable for a grid heatmap.
*
* Response shape:
* {
* topics: [{ id, name, category, urgency }],
* states: ['CA', 'TX', ...],
* matrix: [[value, ...], ...], // matrix[topicIdx][stateIdx]
* sentiments: [[value, ...], ...], // parallel sentiment matrix
* articles: [[value, ...], ...], // parallel article-count matrix
* maxValue: number // global max for color scale
* }
*/
export async function GET(_request: NextRequest) {
const auth = requireRole(_request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
// 1) Fetch all topics
const topicsResult = await query<{
id: string;
name: string;
category: string;
urgency: string;
}>(
`SELECT id, name, category, urgency
FROM pulse_topics
ORDER BY
CASE urgency
WHEN 'critical' THEN 0
WHEN 'high' THEN 1
WHEN 'medium' THEN 2
ELSE 3
END,
name`,
);
const topics = topicsResult.rows;
// 2) Fetch all heat data in a single query
const heatResult = await query<{
topic_id: string;
state: string;
heat_score: number;
article_count: number;
sentiment_avg: number;
}>(
`SELECT topic_id, state, heat_score, article_count, sentiment_avg
FROM pulse_geo_heat
ORDER BY topic_id, state`,
);
// 3) Build ordered list of unique states
const stateSet = new Set<string>();
for (const row of heatResult.rows) {
stateSet.add(row.state);
}
const states = Array.from(stateSet).sort();
// 4) Build lookup: topicId → { state → row }
const lookup = new Map<string, Map<string, (typeof heatResult.rows)[0]>>();
for (const row of heatResult.rows) {
if (!lookup.has(row.topic_id)) {
lookup.set(row.topic_id, new Map());
}
lookup.get(row.topic_id)!.set(row.state, row);
}
// 5) Build matrices (topics = rows, states = columns)
let maxValue = 0;
const matrix: number[][] = [];
const sentiments: number[][] = [];
const articles: number[][] = [];
for (const topic of topics) {
const heatRow: number[] = [];
const sentRow: number[] = [];
const artRow: number[] = [];
const topicMap = lookup.get(topic.id);
for (const st of states) {
const cell = topicMap?.get(st);
const heat = cell ? Number(cell.heat_score) : 0;
heatRow.push(heat);
sentRow.push(cell ? Number(cell.sentiment_avg) : 0);
artRow.push(cell ? Number(cell.article_count) : 0);
if (heat > maxValue) maxValue = heat;
}
matrix.push(heatRow);
sentiments.push(sentRow);
articles.push(artRow);
}
return NextResponse.json({
topics: topics.map((t) => ({
id: t.id,
name: t.name,
category: t.category,
urgency: t.urgency,
})),
states,
matrix,
sentiments,
articles,
maxValue,
});
} catch (err) {
console.error('[topic-grid] Error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch topic grid data' }, { status: 500 });
}
}