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 wired dashboards mapping.

  • 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 by dev_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 via emit_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=True carries 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

make_app(*, dashboards[, dev_log, ...])

Build a Starlette ASGI app serving multiple dashboards.

Classes

ServedDashboard(tree_app, sheet, title, ...)

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: object

One 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

tree App node owning the analysis the sheet lives in. Internal IDs are resolved on first emit (idempotent).

sheet

tree Sheet rendered at /dashboards/{id}. Must belong to tree_app.analysis.sheets.

title

human-readable name for the /dashboards listing.

data_fetcher

per-dashboard fetcher invoked on every GET to the visual data path. Returns d3-shaped chart data.

theme

per-dashboard ThemePreset injected as CSS variables in the page shell. None falls back to DEFAULT_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
sheet: Sheet
theme: ThemePreset | None = None
title: str
tree_app: App
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 /docs unmounted. The standalone docs apply / docs serve / docs export CLI 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 + a POST /log route 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 sets Cache-Control: no-store on 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=N on visual-data responses. URL == cache key (X.2.b’s GET-shape contract), so any (visual, filter-set) tuple stays cacheable for N seconds at the edge / browser. Conservative default of 60s since matviews refresh on ETL cycles (minutes-to- hours); production can dial up. Ignored when dev_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 default GET / /dashboards redirect (since the Studio mount owns GET / as its landing page). None is the Dashboards-only mount: keep the redirect, no Studio routes. cli.dashboards always passes None; cli.studio passes the list make_studio_routes built. Severability contract: make_app does not import _studio_routes — Studio routes are constructed externally and injected.

Return type:

Starlette

Returns:

A starlette.Starlette ASGI application.