Files
ims/app/api/incidents/[id]/ai/triage-suggest/route.ts
T
adminandClaude Sonnet 4.6 c07bdb77ad fix: RLS guard in match_incidents + try/catch around AI/embed calls
- Add new migration 20260711000013_match_incidents_auth_guard.sql that
  replaces match_incidents with an inline auth guard: callers without
  hse/admin role receive PGRST301 Forbidden, closing the SECURITY
  DEFINER RLS bypass.
- Wrap anthropic.messages.create() in try/catch returning 503 in all
  four AI routes: quality-check, triage-suggest, rca-draft, similar.
- Wrap JSON.parse(inc.embedding) and embedText() in similar/route.ts
  in a shared try/catch returning 503 Embedding service unavailable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
2026-07-11 16:48:20 +08:00

111 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const dynamic = 'force-dynamic'
import { NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { anthropic } from '@/lib/claude/client'
export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
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'].includes(profile.role))
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
const { data: incident } = await supabase
.from('incidents')
.select('id, incident_type, description, injury_involved, asset_involved, medical_status')
.eq('id', id)
.single()
if (!incident) return NextResponse.json({ error: 'Not found' }, { status: 404 })
const inc = incident as {
incident_type: string
description: string
injury_involved: boolean
asset_involved: boolean
medical_status: string | null
}
let message: Awaited<ReturnType<typeof anthropic.messages.create>>
try {
message = await anthropic.messages.create({
model: 'claude-opus-4-8',
thinking: { type: 'adaptive' },
max_tokens: 1024,
tools: [{
name: 'suggest_triage',
description: 'Suggest severity rating and NADOPOD 2004 DOSH classification for a warehouse incident',
input_schema: {
type: 'object' as const,
properties: {
severity: { type: 'number', description: '1=minor, 2=low, 3=moderate, 4=serious, 5=critical/fatality' },
is_fatality: { type: 'boolean' },
is_serious_bodily_injury: { type: 'boolean', description: 'Fracture, amputation, blindness, serious burn, or similar' },
is_dangerous_occurrence: { type: 'boolean', description: 'Structural collapse, explosion, fire, scaffold collapse, etc.' },
is_occupational_disease: { type: 'boolean', description: 'Disease arising from workplace exposure' },
rationale: { type: 'string', description: 'One-sentence rationale citing NADOPOD 2004 where applicable' },
},
required: [
'severity', 'is_fatality', 'is_serious_bodily_injury',
'is_dangerous_occurrence', 'is_occupational_disease', 'rationale',
],
},
}],
tool_choice: { type: 'tool', name: 'suggest_triage' },
messages: [{
role: 'user',
content: `You are an HSE triage specialist for a Malaysian 3PL warehouse. Assess this incident under NADOPOD 2004.
Incident type: ${inc.incident_type}
Description: ${inc.description}
Injury involved: ${inc.injury_involved ? 'yes' : 'no'}
Medical status: ${inc.medical_status ?? 'N/A'}
Asset/equipment involved: ${inc.asset_involved ? 'yes' : 'no'}
Suggest severity (15) and tick the appropriate NADOPOD 2004 flags. Give a one-sentence rationale.`,
}],
})
} 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 {
severity?: unknown
is_fatality?: unknown
is_serious_bodily_injury?: unknown
is_dangerous_occurrence?: unknown
is_occupational_disease?: unknown
rationale?: unknown
}
if (
typeof input.severity !== 'number' ||
typeof input.is_fatality !== 'boolean' ||
typeof input.is_serious_bodily_injury !== 'boolean' ||
typeof input.is_dangerous_occurrence !== 'boolean' ||
typeof input.is_occupational_disease !== 'boolean' ||
typeof input.rationale !== 'string'
) {
return NextResponse.json({ error: 'AI returned unexpected structure' }, { status: 500 })
}
await supabase.rpc('write_audit_log', {
p_table_name: 'incidents',
p_record_id: id,
p_action: 'ai_triage_suggest',
p_new_value: { suggestion: toolBlock.input, model: 'claude-opus-4-8' } as never,
})
return NextResponse.json(toolBlock.input)
}