recon_gen.common.html.render

HTML renderer for App2 — projects a tree Sheet to a complete HTML page (HTMX + d3 dialect).

Page shell pulls HTMX + d3 + d3-sankey from CDNs; bootstrap JS hydrates data-visual-kind fragments after every HTMX swap; the per-visual data fetch path is GET (X.2.b) with all filter state as query params.

Theme integration (X.2.l)

The page shell injects an inline <style>:root { --color-accent: ...; ... }</style> block carrying CSS-variable values from the served L2 instance’s ThemePreset (or DEFAULT_PRESET when the L2 has no theme — silent-fallback contract from N.4.k). The Tailwind @theme block in assets/input.css declares the same token names with default values (build-time); the runtime override at :root wins via the cascade. Net effect: every bg-accent / text-accent utility resolves per-instance without rebuilding Tailwind.

Error pages (X.2.m)

emit_error_page renders a themed error shell for 4xx / 5xx responses using the same _emit_theme_style injection — so Starlette’s exception handlers can return a styled “Something went wrong” / “Not found” page instead of the framework default. In dev mode the page can carry a traceback inside a collapsible <details> block; production mode hides it (operators look at the server log).

Visual rendering shape:

<section data-visual-kind=”<kind>” data-visual-id=”<id>”

data-fetch-url=”/dashboards/…/visuals/<id>/data”>

<h2>{title}</h2> <p class=”subtitle”>{subtitle}</p> <div id=”visual-data-<id>” class=”visual-data”>

<!– HTMX swap target; server emits <script type=”application/json”>

holding the chart data, bootstrap JS hydrates via d3 –>

</div>

</section>

Functions

build_top_nav_entries(dashboards, *, ...)

Phase BS.3 — assemble the flat top-nav entries list from the App2 binary's deployed-state args.

emit_dashboards_list(dashboards, *[, theme, ...])

Render the /dashboards landing page.

emit_error_page(*, status_code, headline, ...)

Render a themed error page for 4xx / 5xx responses (X.2.m).

emit_html(app, sheet, *, dashboard_id[, ...])

Render a tree Sheet as a standalone HTML page.

emit_top_nav(*, entries[, active_href])

Phase BS.2 (D1 lock) — render the always-on flat top-nav for App2.

emit_visual_data_fragment(visual_id, data, *)

Server-side fragment HTMX swaps into #visual-data-<visual_id>.

Classes

CategoryFilterSpec(column, label, options)

Multi-select check group for a column.

NumericRangeSpec(column, label[, lo, hi, step])

Min/max range filter for a numeric column.

ParameterDateSpec(name, label[, selected, ...])

Single DATE value bound to a named parameter — the App2 counterpart of a tree ParameterDateTimePicker (AO.2).

ParameterDropdownSpec(name, label, options)

Single-select dropdown for a named parameter.

ParameterMultiSelectSpec(name, label, options)

Multi-select dropdown for a MULTI_VALUED named parameter (Y.2.app2.cde.l2ft-wiring.b).

ParameterNumberSpec(name, label, minimum, ...)

Single numeric value bound to a named parameter — the App2 counterpart of a tree ParameterSlider node (X.2.u.4.e).

TopNavEntry(label, href[, group])

Phase BS.2/BS.3 (D1+D2 nav contract) — single nav entry.

class recon_gen.common.html.render.CategoryFilterSpec(column, label, options)[source]

Bases: object

Multi-select check group for a column.

Renders as a checkbox group plus a hidden <input name="filter_<column>"> that wireCategoryFilters (in bootstrap.js) keeps in sync with the joined-by-comma value. HTMX serializes the hidden input — checkboxes themselves aren’t named so they never reach the wire. URL key on submit: ?filter_<column>=v1,v2,v3.

Parameters:
  • column (str)

  • label (str)

  • options (tuple[str, ...])

column: str
label: str
options: tuple[str, ...]
class recon_gen.common.html.render.NumericRangeSpec(column, label, lo=None, hi=None, step=None)[source]

Bases: object

Min/max range filter for a numeric column.

Renders two <input type="number"> named min_<column> + max_<column> (empty = open-ended on that side); URL keys on submit: ?min_<column>=N&max_<column>=M. When lo + hi bounds are supplied, a noUiSlider two-handle widget is also emitted over the inputs (X.2.l.4) — step is the optional snap interval. Bounds are caller-supplied (a column min/max query, or sensible defaults); without them the widget is number-inputs-only since a slider needs a range.

Parameters:
  • column (str)

  • label (str)

  • lo (float | None)

  • hi (float | None)

  • step (float | None)

column: str
hi: float | None = None
label: str
lo: float | None = None
step: float | None = None
class recon_gen.common.html.render.ParameterDateSpec(name, label, selected='', placeholder='Latest day')[source]

Bases: object

Single DATE value bound to a named parameter — the App2 counterpart of a tree ParameterDateTimePicker (AO.2). Renders a Flatpickr single-date input feeding a hidden param_<name>; URL key on submit ?param_<name>=YYYY-MM-DD. Empty → no key → the executor’s static-default fallback (the dataset param’s sentinel default).

BO.10 — placeholder describes what happens when the picker is empty: "Latest day" for single-day pickers (Daily Statement — the SQL sentinel default resolves to the latest available day), "Earliest day" / "Latest day" for the Phase BM Date From / Date To range pickers (the dataset-param defaults are wide-open 1900-01-01 / 2099-12-31, so an empty range picker really does include the entire data window — the placeholder names the end of that window the picker is anchored to). The pre-BO.10 hardcoded "Latest day" placeholder mis-signalled the BM-shape range pickers as “pinned to one day,” producing the cold-read F15 “five-digit row count with Date From/To = Latest day” surprise.

selected is filled from a ?param_<name>=<v> page-URL key by server.py::_apply_url_param_overrides so a bookmark / drill lands on that day with the visuals’ load-fetch already narrowed.

Parameters:
  • name (str)

  • label (str)

  • selected (str)

  • placeholder (str)

label: str
name: str
placeholder: str = 'Latest day'
selected: str = ''
class recon_gen.common.html.render.ParameterDropdownSpec(name, label, options, options_dataset=None, options_column=None, selected='', cascade_source_param=None)[source]

Bases: object

Single-select dropdown for a named parameter.

Renders as a <select name="param_<name>"> with a blank leading option (the empty string round-trips as “no selection”). URL key on submit: ?param_<name>=<value>.

options_dataset / options_column (X.2.u.4.b): when the option universe is a dataset column rather than a static list (a tree ParameterDropdown(type="SINGLE_SELECT", selectable_values=LinkedValues(...))), the spec carries the source instead of inlined options (which stays () until the server resolves it — server.py::_resolve_linked_options runs SELECT DISTINCT <col> FROM (<dataset SQL>) and replaces options before rendering). When options_dataset is None the static options are authoritative.

selected (u.4.e.4): when the sheet page URL carries ?param_<name>=<v> — a cross-sheet drill that walked an anchor, or a bookmarked filter state — server.py::_apply_url_param_overrides sets selected to that value, so _render_parameter_dropdown pre-marks the matching <option>. Because every visual loads via hx-include="#filter-form", a pre-marked option makes the initial fetch already narrowed (the destination renders filtered, no manual re-pick). Empty string (the default) = blank leading option active.

Parameters:
  • name (str)

  • label (str)

  • options (tuple[str, ...])

  • options_dataset (str | None)

  • options_column (str | None)

  • selected (str)

  • cascade_source_param (str | None)

cascade_source_param: str | None = None
label: str
name: str
options: tuple[str, ...]
options_column: str | None = None
options_dataset: str | None = None
selected: str = ''
class recon_gen.common.html.render.ParameterMultiSelectSpec(name, label, options, options_dataset=None, options_column=None, selected=())[source]

Bases: object

Multi-select dropdown for a MULTI_VALUED named parameter (Y.2.app2.cde.l2ft-wiring.b).

Renders as a <select multiple name="param_<name>"> — each selected <option> serializes as its own param_<name>=<value> pair, so HTMX submits ?param_<name>=A&param_<name>=B (repeated key, NOT comma-joined — that’s the shape _sql_executor’s multi-valued dataset-param expansion consumes). Nothing selected → no param_<name> key at all → the executor falls back to the dataset param’s declared-value default (= no narrowing), matching QuickSight’s “empty the dropdown reverts to default” behaviour (Y.2.c.0). This is the App2 counterpart of a tree ParameterDropdown( type="MULTI_SELECT", selectable_values=StaticValues(...) | LinkedValues(...)) node — derived from the tree by make_filter_specs_for_sheet.

options_dataset / options_column (X.2.u.4.b): for a LinkedValues source the spec carries the dataset identifier + column instead of an inlined options list (which stays () until server.py::_resolve_linked_options runs SELECT DISTINCT <col> FROM (<dataset SQL>) and replaces options pre-render).

selected (u.4.e.4): the repeated ?param_<name>=A&param_<name>=B keys from the sheet page URL — server.py::_apply_url_param_overrides fills it so the matching <option>``s render pre-selected and the visuals' ``hx-include="#filter-form" load fetch is already narrowed. () (the default) = nothing pre-selected (= the executor’s static-default fallback = no narrowing).

Parameters:
  • name (str)

  • label (str)

  • options (tuple[str, ...])

  • options_dataset (str | None)

  • options_column (str | None)

  • selected (tuple[str, ...])

label: str
name: str
options: tuple[str, ...]
options_column: str | None = None
options_dataset: str | None = None
selected: tuple[str, ...] = ()
class recon_gen.common.html.render.ParameterNumberSpec(name, label, minimum, maximum, step, default=None)[source]

Bases: object

Single numeric value bound to a named parameter — the App2 counterpart of a tree ParameterSlider node (X.2.u.4.e).

Renders an <input type="number" name="param_<name>"> (the wire element — submits a single ?param_<name>=<value> key, which _sql_executor translates to a :param_<name> scalar bind for a <<$pName>> placeholder; NOT the repeated-key shape ParameterMultiSelectSpec produces) plus a one-handle noUiSlider over it (wireNoUiSlider’s single-handle mode — driven by data-value-input rather than the two-handle data-min-input / data-max-input). The widget’s initial value is default when set, else minimum — which for the Investigation thresholds is the no-narrowing position (σ≥1 / hop≥$0). Empty → no key → the executor’s static-default fallback (= the dataset param’s declared default), mirroring QuickSight’s “slider untouched ⇒ analysis default”. server.py::_apply_url_param_overrides overwrites default from a ?param_<name>=<v> page-URL key (u.4.e.4) so a drill / bookmark lands on the right slider position.

Parameters:
  • name (str)

  • label (str)

  • minimum (float)

  • maximum (float)

  • step (float)

  • default (float | None)

default: float | None = None
label: str
maximum: float
minimum: float
name: str
step: float
class recon_gen.common.html.render.TopNavEntry(label, href, group='viewing')[source]

Bases: object

Phase BS.2/BS.3 (D1+D2 nav contract) — single nav entry.

A flat top-nav across the App2 binary’s surfaces. group lets callers signal which role-bucket the entry belongs to (authoring / viewing / reading) so the renderer can place visual dividers between groups when the operator asked for them. Per BS.0 Lock 2, the lock-in shipped is <hr> separator between EVERY entry — group is reserved for future styling, currently unused.

Parameters:
  • label (str)

  • href (str)

  • group (str)

group: str = 'viewing'
href: str
label: str
recon_gen.common.html.render.build_top_nav_entries(dashboards, *, studio_enabled, docs_url)[source]

Phase BS.3 — assemble the flat top-nav entries list from the App2 binary’s deployed-state args.

Order per BS.0 Lock 2: Studio entries first (authoring), then dashboards (viewing), then Docs (reading). Studio entries only when studio_enabled=True; Docs entry only when handbook is embedded (caller passes docs_url or None).

Callers pass the result to emit_top_nav with active_href matching the current page’s path.

Return type:

list[TopNavEntry]

Parameters:
  • dashboards (list[tuple[str, str]])

  • studio_enabled (bool)

  • docs_url (str | None)

recon_gen.common.html.render.emit_dashboards_list(dashboards, *, theme=None, docs_url=None, studio_enabled=False)[source]

Render the /dashboards landing page.

dashboards is a list of (dashboard_id, title) tuples in display order. Each entry renders as a link to /dashboards/{dashboard_id} so the URL surface stays the bookmarkable layer (no JS required to navigate the list).

theme controls the CSS-variable values injected into the page shell. When None, DEFAULT_PRESET wins via silent-fallback (N.4.k). Multi-dashboard servers should pass a single shared theme — the listing is one page, one palette.

docs_url — when the server has the handbook embedded (X.2.i, make_app(docs_dir=...)), pass the mount path ("/docs/"). The Docs entry appears in the shared top-nav (BS.3). None ⇒ no Docs entry.

studio_enabled (BS.2 / BS.3) — when True, the shared top-nav surfaces the three Studio entries (L2 Editor / ETL Support / Training). Default False = dashboards-only deploy.

Return type:

str

Parameters:
  • dashboards (list[tuple[str, str]])

  • theme (ThemePreset | None)

  • docs_url (str | None)

  • studio_enabled (bool)

recon_gen.common.html.render.emit_error_page(*, status_code, headline, subtitle, traceback_text=None, theme=None)[source]

Render a themed error page for 4xx / 5xx responses (X.2.m).

Used by Starlette exception handlers in server.make_app so every error surface — sheet-not-found, dashboard-not-found, uncaught render exception, DB unreachable — lands on a page that fits the rest of the app: same theme tokens, same shell, link back to the dashboards listing.

Parameters:
  • status_code (int) – HTTP status this page is being returned with. Shown in small text under the headline (so operators on the phone with a user can confirm “yes that’s the 500 page” without dev tools).

  • headline (str) – short error label (e.g. “Something went wrong”, “Not found”). HTML-escaped.

  • subtitle (str) – longer human-readable explanation + next-step guidance. HTML-escaped.

  • traceback_text (str | None) – when set, included inside a collapsible <details> block. Caller is expected to gate this on whatever dev-mode flag governs traceback exposure (dev_log in the server today). Production callers pass None so internals don’t leak.

  • theme (ThemePreset | None) – same ThemePreset contract as emit_htmlNone falls back to DEFAULT_PRESET.

Return type:

str

Returns:

A complete HTML document as a string.

recon_gen.common.html.render.emit_html(app, sheet, *, dashboard_id, dev_log=False, theme=None, filter_specs=(), all_sheets=(), data_generation_id=None, top_nav='', prefix_override=None)[source]

Render a tree Sheet as a standalone HTML page.

Page shell pulls HTMX + d3 + d3-sankey, optionally emits a sheet tab strip (when all_sheets carries more than one), then a filter form (date-range plus any filter_specs controls) at the top of the body that GETs each visual’s data endpoint on Refresh, plus one <section> per visual carrying its title, subtitle, and the swap-target div.

all_sheets (X.2.e): when provided and longer than one, the page renders sheet tabs at the top. Each tab links to /dashboards/{dashboard_id}/sheets/{sheet_id} (plain anchors — full page reload). The currently-rendered sheet is highlighted as active. Pass app.analysis.sheets to wire every sheet in the analysis as a tab. Default () keeps single-sheet behavior for callers that haven’t migrated.

Parameters:
  • app (App) – tree App node owning the analysis the sheet lives in. app.resolve_auto_ids() is called before render (idempotent).

  • sheet (Sheet) – tree Sheet node. Must be one of app.analysis.sheets; raises ValueError if not.

  • dashboard_id (str) – URL slug for this dashboard. Embedded in every visual’s data-fetch URL + the Refresh button’s hx-get so the path matches what server.make_app wired its route for.

  • theme (ThemePreset | None) – ThemePreset to inject as CSS variables. When None, falls back to DEFAULT_PRESET (silent- fallback per N.4.k, mirrors the QS dialect’s CLASSIC fallback).

  • dev_log (bool)

  • filter_specs (Sequence[ParameterDropdownSpec | CategoryFilterSpec | NumericRangeSpec | ParameterNumberSpec | ParameterMultiSelectSpec | ParameterDateSpec])

  • all_sheets (Sequence[Sheet])

  • data_generation_id (int | None)

  • top_nav (str)

  • prefix_override (str | None)

Return type:

str

Returns:

A complete, well-formed HTML document as a string. Title + body content are HTML-escaped at the leaf level.

recon_gen.common.html.render.emit_top_nav(*, entries, active_href=None)[source]

Phase BS.2 (D1 lock) — render the always-on flat top-nav for App2.

Per BS.0 Lock 2: the Studio container intermediate goes away; Studio’s three modes (when studio_enabled=true) sit as first-class nav entries alongside Dashboards + Docs. Divider style is <hr> vertical separator between every entry.

Callers build their entries list per their cfg+deploy state:

  • Studio entries (L2 Editor / ETL Support / Training) only when cfg.studio_enabled is True.

  • One entry per deployed dashboard.

  • Docs entry only when the docs site is embedded.

active_href highlights the entry whose href the current page matches (typically by exact match on the URL path the route was hit at). None = no active highlight (e.g., a 404 page).

Returns an HTML <nav> element with inline styling matching the ThemePreset’s accent + surface tokens. Empty entries list returns an empty string (a single-surface deploy doesn’t need a nav per BS.0 Lock 1 — caller filters to that case).

Return type:

str

Parameters:
recon_gen.common.html.render.emit_visual_data_fragment(visual_id, data, *, url_params=None)[source]

Server-side fragment HTMX swaps into #visual-data-<visual_id>.

Carries the d3-shaped chart data as a <script type="application/json" class="chart-data">. The bootstrap JS finds the script after swap, parses, and dispatches to the right d3 renderer by data-visual-kind on the parent section.

The fragment is the script tag ALONE (no wrapper div). HTMX’s default hx-swap="innerHTML" drops it inside the visual-data-<id> placeholder — wrapping in another div with the same id would create duplicate IDs after the swap.

The visual_id argument is currently used only for log / debug context (it doesn’t appear in the rendered fragment), but kept in the signature so future callers can attach it as a data- attribute on the script tag if needed.

url_params (AA.B.5.followon.diag) — the URL query params that drove this fetch. When supplied, the rendered param_* and filter_* / date entries (everything that influences the SQL bind) get stamped as data-bound-params='<json>' on the script tag. That makes failure-capture dom.html self- describing: instead of inferring “did the right param reach the server?” from the network log (which already showed 200s with empty rows looking identical to no-request-fired), the test artifact carries the actual params each visual was queried with. The same flow that returns 0 rows for “user picked something that matches nothing” can be told apart from “the picked value never arrived” by reading one attribute on the script tag.

JSON serialization uses json.dumps — caller is responsible for shaping the payload (d3-sankey wants {"nodes": [...], "links": [...]}; a future TimeSeries kind would shape differently).

Return type:

str

Parameters:
  • visual_id (str)

  • data (Any)

  • url_params (Mapping[str, list[str]] | None)