| |
|
|
| 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<string, number>; |
| by_type: Record<string, number>; |
| by_status: Record<string, number>; |
| 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<ReportResponse> { |
| 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<Cluster[]> { |
| 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<ClusterDetail> { |
| 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<Cluster> { |
| 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<void> { |
| const r = await fetch(`${API_BASE}/hazards/${id}`, { method: "DELETE" }); |
| if (!r.ok) throw new Error(`dismiss ${r.status}`); |
| } |
|
|
| export async function getDashboardSummary(): Promise<DashboardSummary> { |
| 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<HazardType, "other">; |
| 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<FleetTelemetryResponse> { |
| 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<Severity, string> = { |
| low: "#16a34a", |
| medium: "#f59e0b", |
| high: "#dc2626", |
| }; |
|
|
| export const HAZARD_LABEL: Record<HazardType, string> = { |
| pothole: "Pothole", |
| crack: "Crack", |
| water_logging: "Water logging", |
| construction_zone: "Construction zone", |
| traffic_block: "Traffic block", |
| other: "Other", |
| }; |
|
|