Merge pull request 'Outage providers: licence-gated registry, ODIN adapter, /outages API, admin tab (Refs #184)' (#255) from feat/184-outage-providers into main
This commit was merged in pull request #255.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { auth } from "@/lib/firebase";
|
||||
import type { AreaContext, CameraProviderAdmin, CamerasResponse, Ny511Response, TalkgroupPending } from "@/lib/types";
|
||||
import type {
|
||||
AreaContext, CameraProviderAdmin, CamerasResponse, Ny511Response, OutageProviderAdmin, OutagesResponse, TalkgroupPending,
|
||||
} from "@/lib/types";
|
||||
import type { DiscordAdminOverview, DiscordStream, RadioBot, RadioBotAddResult } from "@/lib/discord";
|
||||
|
||||
const BASE = process.env.NEXT_PUBLIC_C2_URL ?? "http://localhost:8000";
|
||||
@@ -514,6 +516,22 @@ export const c2api = {
|
||||
body: JSON.stringify({ enabled, acknowledge_licence: acknowledgeLicence }),
|
||||
}),
|
||||
|
||||
// Power outages from every admin-enabled provider inside a bbox (server-26#184).
|
||||
// `status` explains an empty list: none_enabled / no_source / error / ok.
|
||||
getOutages: (bbox: { south: number; west: number; north: number; east: number }) =>
|
||||
request<OutagesResponse>(`/outages?${new URLSearchParams({
|
||||
south: String(bbox.south), west: String(bbox.west), north: String(bbox.north), east: String(bbox.east),
|
||||
})}`),
|
||||
|
||||
// Admin → Outage providers. Same licence gate as cameras: a gated provider
|
||||
// answers 409 with `requires_acknowledgement` until `acknowledge_licence` is sent.
|
||||
listOutageProviders: () => request<{ providers: OutageProviderAdmin[] }>("/admin/outage-providers"),
|
||||
setOutageProvider: (id: string, enabled: boolean, acknowledgeLicence = false) =>
|
||||
request<OutageProviderAdmin>(`/admin/outage-providers/${encodeURIComponent(id)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ enabled, acknowledge_licence: acknowledgeLicence }),
|
||||
}),
|
||||
|
||||
// Public waitlist — no auth, see routers/waitlist.py
|
||||
joinWaitlist: (body: { email: string; org_name?: string; note?: string }) =>
|
||||
request<{ ok: boolean }>("/waitlist", { method: "POST", body: JSON.stringify(body) }),
|
||||
|
||||
@@ -217,6 +217,74 @@ export interface CameraProviderAdmin {
|
||||
health: { fetched_at: number | null; last_attempt_at: number | null; error: string | null; count: number } | null;
|
||||
}
|
||||
|
||||
// Power outages (server-26#184) — GET /outages, cached server-side, not Firestore.
|
||||
/**
|
||||
* One county with active outages from an enabled provider. `id` is
|
||||
* provider-prefixed ("odin:36067"). Counts are utility-reported meters, used
|
||||
* as customers. `lat`/`lon` is the centre of the county's bounding box — a
|
||||
* label point, not an outage location. Times are ISO-8601 UTC.
|
||||
*/
|
||||
export interface OutageArea {
|
||||
id: string;
|
||||
provider: string;
|
||||
fips: string;
|
||||
county: string | null;
|
||||
state: string | null;
|
||||
customers_out: number; // live events only
|
||||
/** Meters on events the source still lists whose own ETR passed >24h ago (ODIN keeps months-old rows). */
|
||||
stale_customers_out: number;
|
||||
stale: boolean; // every event in the county is stale: dim or hide, never show as live
|
||||
customers_served: number | null; // null: no county-level denominator from the source
|
||||
etr: string | null; // latest estimated restoration across the county's outages
|
||||
since: string | null; // earliest reported start
|
||||
updated_at: string | null; // newest time a reporting utility here last sent data
|
||||
lat: number | null;
|
||||
lon: number | null;
|
||||
bbox: [number, number, number, number] | null; // south, west, north, east
|
||||
utilities: { name: string | null; eia_id: string | null; customers_out: number; stale_customers_out: number }[];
|
||||
}
|
||||
|
||||
export interface OutageProviderFeed extends CameraCredit {
|
||||
id: string;
|
||||
name: string;
|
||||
granularity: string;
|
||||
coverage_note: string | null;
|
||||
covers_bbox: boolean;
|
||||
fetched_at: number | null; // epoch s of the provider's last successful fetch
|
||||
error: string | null;
|
||||
total_in_bbox: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface OutagesResponse {
|
||||
areas: OutageArea[];
|
||||
providers: OutageProviderFeed[];
|
||||
serving_count: number;
|
||||
/** Why the list is what it is — an empty layer is never ambiguous. */
|
||||
status: "ok" | "none_enabled" | "no_source" | "error";
|
||||
message: string | null;
|
||||
}
|
||||
|
||||
/** Admin → Outage providers (GET /admin/outage-providers). */
|
||||
export interface OutageProviderAdmin {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
granularity: string;
|
||||
implemented: boolean;
|
||||
notes: string;
|
||||
homepage: string | null;
|
||||
licence: CameraProviderAdmin["licence"];
|
||||
enabled: boolean;
|
||||
serving: boolean;
|
||||
not_serving_reason: string | null;
|
||||
acknowledgement: CameraProviderAdmin["acknowledgement"];
|
||||
updated_at: string | null;
|
||||
updated_by: { uid: string; email: string } | null;
|
||||
coverage_note: string | null;
|
||||
health: CameraProviderAdmin["health"];
|
||||
}
|
||||
|
||||
export interface Ny511Event {
|
||||
id: string;
|
||||
lat: number;
|
||||
|
||||
Reference in New Issue
Block a user