feat(dashboard): make the gauge-sites bulk import reachable (#149)

* fix(dashboard): register gauge_sites_import router

The gauge-sites bulk-import endpoint (CSV / NWS-AHPS) was fully built and
tested but never wired into the app -- server.py never called
include_router() for it, so POST /api/gauge-sites/import 404'd in
production. Only test_tail_followups.py exercised it, via a raw
TestClient bypassing the real app.

* test(dashboard): assert gauge_sites import route is reachable via real app

Guards against the router silently going unregistered again: builds the
actual dashboard app via create_app() and drives a real CSV import
through it end-to-end (POST + GET round-trip), instead of only hitting
the router in isolation.

* feat(dashboard): add gauge-sites bulk import UI

Wires a UI onto the now-registered POST /api/gauge-sites/import endpoint,
inside the existing GaugeSites tab (no new nav entries/pages). Adds an
Import toggle next to Add site, with CSV (paste or file-load into a
textarea) and NWS-AHPS (WFO code list) modes. Required/optional CSV
columns are documented inline so an operator isn't guessing. Result
counts (inserted/updated/skipped/detail_fetched) and any partial-failure
errors from the AHPS scrape are always shown; a pending spinner covers
the AHPS path's live water.weather.gov calls so a slow response never
reads as "imported 0 sites". The list refreshes only after a successful
import that actually changed rows.

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
This commit is contained in:
malice 2026-07-17 14:07:09 -06:00 committed by GitHub
commit b0d1a76e70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 225 additions and 7 deletions

View file

@ -230,6 +230,42 @@ def test_ahps_detail_extracts_thresholds():
assert parsed["flood_major_ft"] == 14.5
# ============================================================================
# Item 3b -- gauge_sites import route is registered on the real app
#
# The endpoint logic above is fully covered, but until server.create_app()
# calls app.include_router(gauge_sites_import_router, ...) the route is
# unreachable in production -- the raw-router `client` fixture above can't
# catch that because it builds its own bare FastAPI app. This test drives
# the actual dashboard app the server process serves.
# ============================================================================
def test_gauge_sites_import_reachable_through_real_app():
from meshai.dashboard.server import create_app
app = create_app()
real_client = TestClient(app)
res = real_client.post(
"/api/gauge-sites/import",
json={
"format": "csv",
"data": "site_id,gauge_name,lat,lon\nUSGS-REAL1,Real App Creek,43.5,-114.5\n",
},
)
assert res.status_code == 200, res.text
body = res.json()
assert body["inserted"] == 1
assert body["updated"] == 0
# And it actually landed -- round-trip through the sibling curation
# router (also mounted on the real app) to prove the 200 reflects a
# real DB write through the real app, not a false positive.
listed = real_client.get("/api/gauge-sites").json()
assert any(r["site_id"] == "USGS-REAL1" for r in listed)
# ============================================================================
# Item 4 -- WFIGS tombstone column + reminder behavior
# ============================================================================