roadrecon / frontend /src /lib /api.ts
Deeraj
Feat: add fleet mode and inference telemetry worker
f17ffc5
Raw
History Blame Contribute Delete
6.07 kB
// 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<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",
};