recon_gen.common.html

App2 — HTML renderer for the dashboard tree.

emit_html(app, sheet, *, dashboard_id) projects a tree Sheet to a complete HTML page (title, description, one <section> per visual carrying title + subtitle + a swap target).

Interactive surface:

  • HTMX + d3 + d3-sankey from CDN, plus a bootstrap script that hydrates data-visual-kind fragments after every swap.

  • All-GET REST data path (X.2.b): the date-range form’s Refresh button + the in-chart click handlers both hx-get against /dashboards/{dashboard_id}/sheets/{sheet_id}/visuals/{visual_id}/data with filter values in the query string. URL == cache key == bookmark.

  • emit_visual_data_fragment(visual_id, data) produces the swap fragment the Starlette server returns from the data endpoint.

  • server.make_app(...) wires Starlette routes around a pluggable data fetcher (mock or real DB).

Per the X.2 design constraint: rendering functions take tree nodes as parameters — never load from disk inside. This preserves the X.4 stateful editor future where the tree lives in a per-session in-memory object.

Out of scope here: authentication (deferred to backlog), embedding, server-side caching (Cache-Control headers in X.2.b.4 push caching to edge / browser).

class recon_gen.common.html.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.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.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.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.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.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
recon_gen.common.html.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.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.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.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)

recon_gen.common.html.make_filter_specs_for_sheet(sheet)[source]

Return the App2 filter-form specs auto-derived from sheet’s parameter-control nodes.

Order follows the sheet’s parameter_controls order so the filter bar matches the QuickSight control layout. Sheets with no such control return [] (the form is then date-pickers-only, and is suppressed entirely for text-box-only sheets per render’s existing logic).

Coverage:

  • MULTI_SELECT + StaticValuesParameterMultiSelectSpec with inlined options (Y.2.app2.cde.l2ft-wiring.b — pre-AA.A.3 L2FT Rail / Status / Bundle + L1 Account-Role / Transfer-Type / Rail enums; AA.A.3 flipped these to SINGLE_SELECT, so the MULTI case handles only future genuinely-multi keepers).

  • MULTI_SELECT + LinkedValuesParameterMultiSelectSpec carrying options_dataset / options_column (X.2.u.4.b — pre-AA.A.3 L1 Account / Transfer / Status / Origin data-value dropdowns; same AA.A.3 flip caveat as above).

  • SINGLE_SELECT + StaticValuesParameterDropdownSpec with inlined options (AA.A.3 — post-flip L1 + L2FT enum dropdowns: Rail / Status / Bundle / Completion / Transfer-Type / Account-Role / Check-Type / Supersede-Reason; also the L2FT metadata-cascade key dropdowns picked up a widget in App2 as a side benefit).

  • SINGLE_SELECT + LinkedValuesParameterDropdownSpec likewise (X.2.u.4.b — Daily Statement’s Account picker; AA.A.3 — post-flip L1 data-value dropdowns for Account / Transfer / Status / Origin).

  • ``ParameterSlider``ParameterNumberSpec (X.2.u.4.e — Investigation’s σ / max-hops / min-amount threshold knobs); the number input is the wire element, with a one-handle noUiSlider over it. Initial value = the bound parameter’s analysis-level default if declared (so it matches the dataset SQL’s static-default literal), else the slider minimum.

Still skipped: ParameterTextField controls (a different shape — text-field parity not yet needed); add_parameter_datetime_picker controls (date-control parity is the universal date range, handled separately). Skipped controls just don’t get a widget.

Return type:

list[ParameterDropdownSpec | CategoryFilterSpec | NumericRangeSpec | ParameterNumberSpec | ParameterMultiSelectSpec | ParameterDateSpec]

Parameters:

sheet (Sheet)

Modules

render

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

server

Starlette ASGI server for the App2 (HTMX) dashboard renderer.