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
@@ -0,0 +1,52 @@
|
||||
"""Power-outage endpoints (map layer, server-26#184).
|
||||
|
||||
``GET /outages`` -- any signed-in user (auth applied at include time in main.py):
|
||||
outage areas inside a bbox from every provider an admin has enabled, each with
|
||||
the provider's attribution, disclaimer, coverage note and feed freshness, plus
|
||||
a ``status`` that says why an empty list is empty.
|
||||
|
||||
``/admin/outage-providers`` -- platform admin only: list every catalogued
|
||||
provider with its licence status and health, and toggle it. Gated licences
|
||||
need ``acknowledge_licence: true``; that acknowledgement is stored with who and
|
||||
when and written to audit_log (see app/internal/outages/registry.py).
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.internal.auth import describe_actor, require_admin_token
|
||||
from app.internal.outages import registry
|
||||
|
||||
router = APIRouter(prefix="/outages", tags=["outages"])
|
||||
admin_router = APIRouter(prefix="/admin/outage-providers", tags=["admin"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def get_outages(
|
||||
south: float = Query(..., ge=-90, le=90),
|
||||
west: float = Query(..., ge=-180, le=180),
|
||||
north: float = Query(..., ge=-90, le=90),
|
||||
east: float = Query(..., ge=-180, le=180),
|
||||
):
|
||||
if south > north or west > east:
|
||||
raise HTTPException(400, "bbox must satisfy south<=north and west<=east")
|
||||
return await registry.outages_in_bbox((south, west, north, east))
|
||||
|
||||
|
||||
class ProviderToggle(BaseModel):
|
||||
enabled: bool
|
||||
acknowledge_licence: bool = False
|
||||
|
||||
|
||||
@admin_router.get("")
|
||||
async def list_outage_providers(_=Depends(require_admin_token)):
|
||||
return {"providers": await registry.list_providers()}
|
||||
|
||||
|
||||
@admin_router.put("/{provider_id}")
|
||||
async def set_outage_provider(provider_id: str, body: ProviderToggle, principal: dict = Depends(require_admin_token)):
|
||||
try:
|
||||
return await registry.set_enabled(provider_id, body.enabled, describe_actor(principal),
|
||||
acknowledge_licence=body.acknowledge_licence)
|
||||
except registry.ProviderError as e:
|
||||
return JSONResponse(status_code=e.code, content={"detail": e.message, **e.extra})
|
||||
Reference in New Issue
Block a user