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
@@ -19,6 +19,7 @@ from app.internal.auth import (
|
||||
)
|
||||
from app.routers import nodes, systems, calls, upload, tokens, incidents, alerts, admin, trips, places, links, users
|
||||
from app.routers import enrollment, media, org, waitlist, telemetry, replay, traffic, cameras
|
||||
from app.routers import outages
|
||||
from app.routers import discord as discord_router
|
||||
from app.routers import discord_link as discord_link_router
|
||||
from app.internal import dynsec
|
||||
@@ -146,6 +147,8 @@ app.include_router(places.router, dependencies=[Depends(require_service_or_fi
|
||||
app.include_router(traffic.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(cameras.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(cameras.admin_router) # auth: admin only, per endpoint (needs the principal for audit)
|
||||
app.include_router(outages.router, dependencies=[Depends(require_service_or_firebase_token)])
|
||||
app.include_router(outages.admin_router) # auth: admin only, per endpoint (needs the principal for audit)
|
||||
app.include_router(upload.router) # auth is per-node, handled inline
|
||||
app.include_router(admin.router) # auth is per-endpoint (read: firebase, write: admin)
|
||||
app.include_router(replay.router) # auth: admin only (every route spends or reads a replay run)
|
||||
|
||||
Reference in New Issue
Block a user