Outage providers: licence-gated registry, ODIN adapter, /outages API, admin tab (Refs #184)
Part 1 of 2 for server-26#184 (map layer + incident cross-match follow in batch 3). - app/internal/outages/: same gate as the camera registry -- settings doc config/outage_providers, every provider OFF by default, unreadable settings fail closed, stubs never enable, gated licences need acknowledge_licence stored with who/when plus an audit_log write as a precondition, and the acknowledgement is bound to the licence status it was given for. Reuses cameras.base Licence/LicenceInfo/get_json/safe_error/require_parsed. - ODIN adapter (Licence.UNVERIFIED, gated): public county-level API verified first-hand against https://odin.ornl.gov/v3/api-docs -- /odi?format=JSON (counts, start, ETR), /odi/map?scale=COUNTY (county name, bbox, label point), /odi/status (utility freshness). Schema canary on all three. metersServed is utility-wide, so customers_served stays None. - Stubs: poweroutage_us (paid), coned_oru / nyseg_rge / central_hudson storm centers and eagle_i_live (agreement required). - GET /outages (signed-in) returns areas + per-provider credit/health and a status (ok / none_enabled / no_source / error) so an empty layer is never ambiguous. /admin/outage-providers GET + PUT, admin only, 409 with requires_acknowledgement like cameras. - Frontend: c2api getOutages/listOutageProviders/setOutageProvider, types, Admin -> Outage providers tab (deep link /admin#outages). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a75149b2eb
commit
ebeabe342e
@@ -4,6 +4,7 @@ import { useAuth } from "@/components/AuthProvider";
|
||||
import { c2api } from "@/lib/c2api";
|
||||
import { ReplayTab } from "@/components/admin/ReplayTab";
|
||||
import { CameraProvidersTab } from "@/components/admin/CameraProvidersTab";
|
||||
import { OutageProvidersTab } from "@/components/admin/OutageProvidersTab";
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { UserRecord, AuditEntry, UserRole, CallRecord } from "@/lib/types";
|
||||
@@ -1268,11 +1269,12 @@ function SttEvalTab() {
|
||||
// Main admin page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type AdminTab = "features" | "cameras" | "correlation" | "replay" | "users" | "audit" | "calls" | "eval";
|
||||
type AdminTab = "features" | "cameras" | "outages" | "correlation" | "replay" | "users" | "audit" | "calls" | "eval";
|
||||
|
||||
const TAB_LABELS: { key: AdminTab; label: string }[] = [
|
||||
{ key: "features", label: "AI Features" },
|
||||
{ key: "cameras", label: "Camera providers" },
|
||||
{ key: "outages", label: "Outage providers" },
|
||||
{ key: "correlation", label: "Correlation Debug" },
|
||||
{ key: "replay", label: "Replay" },
|
||||
{ key: "calls", label: "Calls" },
|
||||
@@ -1285,7 +1287,7 @@ export default function AdminPage() {
|
||||
const { user, isAdmin, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [tab, setTab] = useState<AdminTab>("features");
|
||||
// Deep link to a tab (/admin#cameras), e.g. from the live wall's empty state.
|
||||
// Deep link to a tab (/admin#cameras, /admin#outages), e.g. from a map layer's empty state.
|
||||
useEffect(() => {
|
||||
const h = window.location.hash.slice(1);
|
||||
if (TAB_LABELS.some((t) => t.key === h)) setTab(h as AdminTab);
|
||||
@@ -1304,7 +1306,7 @@ export default function AdminPage() {
|
||||
if (!isAdmin) return null;
|
||||
|
||||
// Users/Audit tabs benefit from full width; everything else is narrow
|
||||
const wide = tab === "users" || tab === "audit" || tab === "replay" || tab === "cameras";
|
||||
const wide = tab === "users" || tab === "audit" || tab === "replay" || tab === "cameras" || tab === "outages";
|
||||
|
||||
return (
|
||||
<div className={`space-y-6 ${wide ? "" : "max-w-2xl"}`}>
|
||||
@@ -1326,6 +1328,7 @@ export default function AdminPage() {
|
||||
|
||||
{tab === "features" && <FeaturesTab />}
|
||||
{tab === "cameras" && <CameraProvidersTab />}
|
||||
{tab === "outages" && <OutageProvidersTab />}
|
||||
{tab === "correlation" && <CorrelationDebugTab />}
|
||||
{tab === "replay" && <ReplayTab />}
|
||||
{tab === "calls" && <StaleCallsTab />}
|
||||
|
||||
Reference in New Issue
Block a user