recon_gen.common.html.server
Starlette ASGI server for the App2 (HTMX) dashboard renderer.
X.2.b shape — all-GET REST surface (no POSTs except dev-log):
GET /— 302 redirect to/dashboards. The dashboards list IS the canonical entry;/is convenience.GET /dashboards— landing page listing every dashboard the server is wired to serve. One link per dashboard, bookmarkable per entry.GET /dashboards/{dashboard_id}— dashboard chrome + the served Sheet inline. 404 if the dashboard_id isn’t in the wireddashboardsmapping.GET /dashboards/{dashboard_id}/sheets/{sheet_id}/visuals/{visual_id}/data— chart data fragment for HTMX swap. Filter values arrive as query string. GET-not-POST means every (visual, filter-set) tuple is a bookmarkable URL.POST /log(dev-only, gated bydev_log=True) — the only POST route. Receives forwarded HTMX + d3 click events from the browser for live debugging.
X.2.b.3: make_app takes a dashboards mapping so one server
can host multiple apps. Each value is a ServedDashboard carrying
its own tree, sheet, title, and data fetcher (different apps query
different matviews via different fetchers). X.2.g wires the four
QS apps (Executives / Investigation / L2 Flow Tracing / L1
Dashboard) into this mapping from one L2 instance.
Error handling (X.2.m)
Two exception handlers wrap the app so production deploys never return a Starlette default error page:
HTTPException(404)(raised by route handlers when a dashboard_id / sheet_id slug doesn’t resolve) renders a themed “Not found” page viaemit_error_page.Generic
Exception(anything uncaught from a route handler — fetcher SQL crash, render-time bug, DB unreachable) returns 500 with a themed “Something went wrong” page.dev_log=Truecarries the traceback inside a collapsible<details>; production hides it.
The HTMX htmx:responseError event in bootstrap.js surfaces
a transient toast for 4xx / 5xx responses to swap targets so a
failed visual data fetch shows context instead of a blank panel.
Pluggable data fetcher
Each ServedDashboard owns a DataFetcher callable so the
spike + tests can run without a database:
- def stub(visual_id: VisualId, params: Mapping[str, list[str]]) -> Any:
return {“nodes”: […], “links”: […]}
- app = make_app(dashboards={
- “smoke”: ServedDashboard(
tree_app=app, sheet=money_trail, title=”Smoke”, data_fetcher=stub,
),
})
Production deploys wire the same callable to a DB-backed factory
(see _db_fetcher.make_db_fetcher).
Stateless on purpose
No sessions, no auth, no in-process caching. Each GET executes the fetcher fresh. Cache-Control headers (X.2.b.4) push caching to edge / browser layers — the URL IS the cache key, by design.
Functions
|
Build a Starlette ASGI app serving multiple dashboards. |
Classes
|
One dashboard's wiring for the App2 server. |
- class recon_gen.common.html.server.ServedDashboard(tree_app, sheet, title, data_fetcher, theme=None, filter_specs=(), options_fetcher=None)[source]
Bases:
objectOne dashboard’s wiring for the App2 server.
Each App2 server holds a mapping
{dashboard_id: ServedDashboard}so one process can serve multiple apps from one L2 instance (X.2.g wires Executives + Investigation + L2FT + L1 from one L2). Per-dashboard fetcher means apps that query different matviews don’t have to share a routing layer.- Parameters:
tree_app (App)
sheet (Sheet)
title (str)
data_fetcher (Callable[[VisualId, Mapping[str, list[str]]], Awaitable[Any]] | Callable[[VisualId, Mapping[str, list[str]]], Any])
theme (ThemePreset | None)
filter_specs (tuple[ParameterDropdownSpec | CategoryFilterSpec | NumericRangeSpec | ParameterNumberSpec | ParameterMultiSelectSpec | ParameterDateSpec, ...])
options_fetcher (Callable[[str, str, Mapping[str, list[str]]], Awaitable[tuple[str, ...]]] | None)
- tree_app
tree
Appnode owning the analysis the sheet lives in. Internal IDs are resolved on first emit (idempotent).
- sheet
tree
Sheetrendered at/dashboards/{id}. Must belong totree_app.analysis.sheets.
- title
human-readable name for the
/dashboardslisting.
- data_fetcher
per-dashboard fetcher invoked on every GET to the visual data path. Returns d3-shaped chart data.
- theme
per-dashboard
ThemePresetinjected as CSS variables in the page shell.Nonefalls back toDEFAULT_PRESET(silent-fallback per N.4.k, mirrors QS dialect’s CLASSIC fallback). Multi-dashboard servers usually share a single theme since the listing page renders one palette across all entries.
- data_fetcher: Callable[[VisualId, Mapping[str, list[str]]], Awaitable[Any]] | Callable[[VisualId, Mapping[str, list[str]]], Any]
- filter_specs: tuple[ParameterDropdownSpec | CategoryFilterSpec | NumericRangeSpec | ParameterNumberSpec | ParameterMultiSelectSpec | ParameterDateSpec, ...] = ()
- options_fetcher: Callable[[str, str, Mapping[str, list[str]]], Awaitable[tuple[str, ...]]] | None = None
- theme: ThemePreset | None = None
- title: str
- recon_gen.common.html.server.make_app(*, dashboards, dev_log=False, visual_data_cache_max_age_s=60, docs_dir=None, studio_routes=None)[source]
Build a Starlette ASGI app serving multiple dashboards.
- Parameters:
dashboards (
Mapping[str,ServedDashboard]) –{dashboard_id: ServedDashboard}mapping. One entry per dashboard. The server validates inbound path slugs against this mapping; unknown ids 404.docs_dir (
Path|None) – when set, the built mkdocs handbook site at this path is mounted at/docs(StaticFiles(html=True)—/docs/handbook/l1/→…/index.html) and the dashboards-list page links it (X.2.i — “one process, one place”: docs + dashboards together).None(default) leaves/docsunmounted. The standalonedocs apply/docs serve/docs exportCLI is unchanged either way — embedding here is purely additive.dev_log (
bool) – when True, the page emits a<meta name="dev-log">tag that activates the client-side event forwarder + aPOST /logroute is registered that prints each forwarded event to stderr. Off by default — keeps production deploys silent and zero- overhead. The developer tool / smoke server enables it. Also setsCache-Control: no-storeon visual data responses so dev iteration sees fresh data on every reload (no surprise stale fragments).visual_data_cache_max_age_s (
int) –Cache-Control: public, max-age=Non visual-data responses. URL == cache key (X.2.b’s GET-shape contract), so any (visual, filter-set) tuple stays cacheable forNseconds at the edge / browser. Conservative default of 60s since matviews refresh on ETL cycles (minutes-to- hours); production can dial up. Ignored whendev_log=True(cache is bypassed for dev runs).studio_routes (
Sequence[Route|Mount] |None) – X.4.a.4 — when set, splice these routes in BEFORE the Dashboards routes, AND skip the defaultGET / → /dashboardsredirect (since the Studio mount ownsGET /as its landing page).Noneis the Dashboards-only mount: keep the redirect, no Studio routes.cli.dashboardsalways passesNone;cli.studiopasses the listmake_studio_routesbuilt. Severability contract:make_appdoes not import_studio_routes— Studio routes are constructed externally and injected.
- Return type:
Starlette- Returns:
A
starlette.StarletteASGI application.