export const dynamic = 'force-dynamic' import { NextResponse } from 'next/server' import { createClient } from '@/lib/supabase/server' import { createAnthropicClient } from '@/lib/claude/client' import { getApiKey } from '@/lib/settings' type ZoneAggregate = { zone: string site: string total: number near_miss: number hazard: number injury: number avg_severity: number | null first_half: number second_half: number } export async function POST() { const supabase = await createClient() const { data: { user }, error: authError } = await supabase.auth.getUser() if (authError || !user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) const { data: profile } = await supabase.from('users').select('role').eq('id', user.id).single() if (!profile || !['hse', 'admin', 'management'].includes(profile.role)) return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) const now = new Date() const ninetyDaysAgo = new Date(now) ninetyDaysAgo.setDate(now.getDate() - 90) const midpoint = new Date(now) midpoint.setDate(now.getDate() - 45) const { data: incidents } = await supabase .from('incidents') .select('incident_type, severity, reported_at, zones (name), sites (name)') .gte('reported_at', ninetyDaysAgo.toISOString()) const rows = incidents ?? [] if (rows.length === 0) return NextResponse.json({ flags: [], summary: 'No incidents in the last 90 days.' }) const zoneMap = new Map() for (const r of rows) { const zone = (r.zones as unknown as { name: string } | null)?.name ?? 'Unknown zone' const site = (r.sites as unknown as { name: string } | null)?.name ?? 'Unknown site' const key = `${site}|${zone}` let agg = zoneMap.get(key) if (!agg) { agg = { zone, site, total: 0, near_miss: 0, hazard: 0, injury: 0, avg_severity: null, first_half: 0, second_half: 0, severitySum: 0, severityCount: 0, } zoneMap.set(key, agg) } agg.total++ if (r.incident_type === 'near_miss') agg.near_miss++ if (r.incident_type === 'hazard') agg.hazard++ if (r.incident_type === 'injury') agg.injury++ if (typeof r.severity === 'number') { agg.severitySum += r.severity agg.severityCount++ } if (new Date(r.reported_at as string) < midpoint) agg.first_half++ else agg.second_half++ } const aggregates: ZoneAggregate[] = [...zoneMap.values()].map(a => ({ zone: a.zone, site: a.site, total: a.total, near_miss: a.near_miss, hazard: a.hazard, injury: a.injury, avg_severity: a.severityCount > 0 ? Math.round((a.severitySum / a.severityCount) * 10) / 10 : null, first_half: a.first_half, second_half: a.second_half, })) // Rate limit: 1 AI call per 60s per user (checked via audit_log) const { data: lastCall } = await supabase .from('audit_log') .select('changed_at') .eq('changed_by', user.id) .eq('action', 'ai_risk_flags') .order('changed_at', { ascending: false }) .limit(1) .single() if (lastCall && Date.now() - new Date(lastCall.changed_at).getTime() < 60_000) { return NextResponse.json({ error: 'Rate limit: wait 60 seconds between AI requests' }, { status: 429 }) } const anthropicKey = await getApiKey(supabase, 'ANTHROPIC_API_KEY') const anthropic = createAnthropicClient(anthropicKey) let message: Awaited> try { message = await anthropic.messages.create({ model: 'claude-opus-4-8', thinking: { type: 'adaptive' }, max_tokens: 2048, tools: [{ name: 'flag_rising_risk', description: 'Flag warehouse zones showing rising safety risk from 90-day incident aggregates', input_schema: { type: 'object' as const, properties: { flags: { type: 'array', items: { type: 'object', properties: { zone: { type: 'string' }, site: { type: 'string' }, risk_level: { type: 'string', enum: ['low', 'medium', 'high'] }, rationale: { type: 'string', description: 'One or two sentences citing the numbers' }, recommended_action: { type: 'string', description: 'One concrete preventive action' }, }, required: ['zone', 'site', 'risk_level', 'rationale', 'recommended_action'], }, }, summary: { type: 'string', description: 'Two-sentence overall risk picture' }, }, required: ['flags', 'summary'], }, }], tool_choice: { type: 'tool', name: 'flag_rising_risk' }, messages: [{ role: 'user', content: `You are an HSE risk analyst for a Malaysian 3PL warehouse operator. Below are 90-day incident aggregates per zone. "first_half" is incidents in days 90-46, "second_half" is days 45-0 — a rising second_half means worsening trend. Near-miss and hazard reports are leading indicators; injuries are lagging. Flag zones with rising or elevated risk (at most 5 flags; do not flag healthy zones). Base every rationale strictly on the numbers given. ${JSON.stringify(aggregates, null, 2)} `, }], }) } catch { return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 }) } const toolBlock = message.content.find(b => b.type === 'tool_use') if (!toolBlock || toolBlock.type !== 'tool_use') return NextResponse.json({ error: 'AI suggestion failed' }, { status: 500 }) const input = toolBlock.input as { flags?: unknown; summary?: unknown } if (!Array.isArray(input.flags) || typeof input.summary !== 'string') return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 }) const flags = input.flags.filter( (f: unknown): f is { zone: string; site: string; risk_level: string; rationale: string; recommended_action: string } => typeof f === 'object' && f !== null && typeof (f as Record).zone === 'string' && typeof (f as Record).site === 'string' && ['low', 'medium', 'high'].includes((f as Record).risk_level as string) && typeof (f as Record).rationale === 'string' && typeof (f as Record).recommended_action === 'string', ) await supabase.rpc('write_audit_log', { p_table_name: 'incidents', p_record_id: user.id, p_action: 'ai_risk_flags', p_new_value: { flags, summary: input.summary, model: 'claude-opus-4-8' } as never, }) return NextResponse.json({ flags, summary: input.summary }) }