// Typed client for the RoadRecon backend. export const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? "http://localhost:8000"; export type Severity = "low" | "medium" | "high"; export type HazardType = | "pothole" | "crack" | "water_logging" | "construction_zone" | "traffic_block" | "other"; export type Status = "pending_review" | "reported" | "acknowledged" | "fixed"; export type GeoSource = "exif" | "device" | "manual"; export interface Detection { hazard_type: HazardType; confidence: number; bbox: number[]; severity: Severity | null; } export interface ReportResponse { status: "ok" | "rejected"; rejected_reason: string | null; report_ids: string[]; cluster_ids: string[]; hazard_type: HazardType | null; severity: Severity | null; detections: Detection[]; lat: number | null; lon: number | null; geo_source: GeoSource | null; annotated_image_b64: string | null; can_report_anyway: boolean; pending_review: boolean; } export interface Cluster { id: string; centroid_lat: number; centroid_lon: number; hazard_type: HazardType; severity: Severity; report_count: number; first_seen: string; last_seen: string; status: Status; resolved_at: string | null; fleet_report_count: number; citizen_report_count: number; avg_device_speed: number | null; } export interface StatusEvent { from_status: Status | null; to_status: Status; at: string; } export interface ReportOut { id: string; created_at: string; observed_at: string | null; source: string; hazard_type: HazardType; severity: Severity; confidence: number; lat: number; lon: number; geo_source: GeoSource; media_path: string; thumbnail_path: string | null; bbox: number[]; bbox_area_frac: number | null; device_speed: number | null; client_event_id: string | null; status: Status; cluster_id: string | null; } export interface ClusterDetail extends Cluster { reports: ReportOut[]; history: StatusEvent[]; } export interface DashboardSummary { total_active: number; high_severity: number; fixed_total: number; pending_review: number; avg_resolution_hours: number | null; by_severity: Record; by_type: Record; by_status: Record; reports_over_time: { date: string; count: number }[]; top_priority: Cluster[]; } export interface Bbox { minLat: number; minLon: number; maxLat: number; maxLon: number; } function bboxParam(b: Bbox): string { return `${b.minLat},${b.minLon},${b.maxLat},${b.maxLon}`; } export function mediaUrl(path: string): string { return `${API_BASE}/${path}`; } export async function health(): Promise<{ ok: boolean; service: string }> { const r = await fetch(`${API_BASE}/health`); if (!r.ok) throw new Error(`health ${r.status}`); return r.json(); } export async function submitReport(opts: { file: File; source?: "citizen" | "fleet_stream"; deviceLat?: number; deviceLon?: number; force?: boolean; }): Promise { const fd = new FormData(); fd.append("file", opts.file); fd.append("source", opts.source ?? "citizen"); if (opts.deviceLat != null) fd.append("device_lat", String(opts.deviceLat)); if (opts.deviceLon != null) fd.append("device_lon", String(opts.deviceLon)); fd.append("annotate", "true"); if (opts.force) fd.append("force", "true"); const r = await fetch(`${API_BASE}/report`, { method: "POST", body: fd }); if (!r.ok) throw new Error(`report ${r.status}`); return r.json(); } export async function getHazards( b: Bbox, filters?: { status?: Status; type?: HazardType }, ): Promise { const p = new URLSearchParams({ bbox: bboxParam(b) }); if (filters?.status) p.set("status", filters.status); if (filters?.type) p.set("type", filters.type); const r = await fetch(`${API_BASE}/hazards?${p.toString()}`); if (!r.ok) throw new Error(`hazards ${r.status}`); return r.json(); } export async function getClusterDetail(id: string): Promise { const r = await fetch(`${API_BASE}/hazards/${id}`); if (!r.ok) throw new Error(`cluster ${r.status}`); return r.json(); } export async function updateClusterStatus(id: string, status: Status): Promise { const r = await fetch(`${API_BASE}/hazards/${id}/status`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); if (!r.ok) throw new Error(`status ${r.status}`); return r.json(); } export async function dismissCluster(id: string): Promise { const r = await fetch(`${API_BASE}/hazards/${id}`, { method: "DELETE" }); if (!r.ok) throw new Error(`dismiss ${r.status}`); } export async function getDashboardSummary(): Promise { const r = await fetch(`${API_BASE}/dashboard/summary`); if (!r.ok) throw new Error(`summary ${r.status}`); return r.json(); } export interface FleetTelemetryPayload { lat: number; lon: number; hazard_type: Exclude; confidence: number; timestamp: string; speed?: number; bbox?: [number, number, number, number]; bbox_area_frac?: number; thumbnail_b64?: string; client_event_id?: string; } export interface FleetTelemetryResponse { status: "accepted"; report_id: string; queued_for_cluster: boolean; } export async function submitFleetTelemetry( payload: FleetTelemetryPayload, ): Promise { const r = await fetch(`${API_BASE}/fleet/telemetry`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!r.ok) throw new Error(`fleet telemetry ${r.status}`); return r.json(); } export const SEVERITY_COLOR: Record = { low: "#16a34a", medium: "#f59e0b", high: "#dc2626", }; export const HAZARD_LABEL: Record = { pothole: "Pothole", crack: "Crack", water_logging: "Water logging", construction_zone: "Construction zone", traffic_block: "Traffic block", other: "Other", };