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-kindfragments 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-getagainst/dashboards/{dashboard_id}/sheets/{sheet_id}/visuals/{visual_id}/datawith 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:
objectMulti-select check group for a column.
Renders as a checkbox group plus a hidden
<input name="filter_<column>">thatwireCategoryFilters(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:
objectMin/max range filter for a numeric column.
Renders two
<input type="number">namedmin_<column>+max_<column>(empty = open-ended on that side); URL keys on submit:?min_<column>=N&max_<column>=M. Whenlo+hibounds are supplied, a noUiSlider two-handle widget is also emitted over the inputs (X.2.l.4) —stepis 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:
objectSingle DATE value bound to a named parameter — the App2 counterpart of a tree
ParameterDateTimePicker(AO.2). Renders a Flatpickr single-date input feeding a hiddenparam_<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 —
placeholderdescribes 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-open1900-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.selectedis filled from a?param_<name>=<v>page-URL key byserver.py::_apply_url_param_overridesso 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:
objectSingle-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 treeParameterDropdown(type="SINGLE_SELECT", selectable_values=LinkedValues(...))), the spec carries the source instead of inlinedoptions(which stays()until the server resolves it — server.py::_resolve_linked_options runsSELECT DISTINCT <col> FROM (<dataset SQL>)and replacesoptionsbefore rendering). Whenoptions_datasetis None the staticoptionsare 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_overridessetsselectedto that value, so_render_parameter_dropdownpre-marks the matching<option>. Because every visual loads viahx-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:
objectMulti-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 ownparam_<name>=<value>pair, so HTMX submits?param_<name>=A¶m_<name>=B(repeated key, NOT comma-joined — that’s the shape_sql_executor’s multi-valued dataset-param expansion consumes). Nothing selected → noparam_<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 treeParameterDropdown( type="MULTI_SELECT", selectable_values=StaticValues(...) | LinkedValues(...))node — derived from the tree bymake_filter_specs_for_sheet.options_dataset/options_column(X.2.u.4.b): for aLinkedValuessource the spec carries the dataset identifier + column instead of an inlinedoptionslist (which stays()untilserver.py::_resolve_linked_optionsrunsSELECT DISTINCT <col> FROM (<dataset SQL>)and replacesoptionspre-render).selected(u.4.e.4): the repeated?param_<name>=A¶m_<name>=Bkeys from the sheet page URL —server.py::_apply_url_param_overridesfills 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:
objectSingle numeric value bound to a named parameter — the App2 counterpart of a tree
ParameterSlidernode (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_executortranslates to a:param_<name>scalar bind for a<<$pName>>placeholder; NOT the repeated-key shapeParameterMultiSelectSpecproduces) plus a one-handle noUiSlider over it (wireNoUiSlider’s single-handle mode — driven bydata-value-inputrather than the two-handledata-min-input/data-max-input). The widget’s initial value isdefaultwhen set, elseminimum— 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_overridesoverwritesdefaultfrom 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
/dashboardslanding page.dashboardsis 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).themecontrols the CSS-variable values injected into the page shell. WhenNone,DEFAULT_PRESETwins 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_appso 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_login the server today). Production callers passNoneso internals don’t leak.theme (
ThemePreset|None) – sameThemePresetcontract asemit_html—Nonefalls back toDEFAULT_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
Sheetas a standalone HTML page.Page shell pulls HTMX + d3 + d3-sankey, optionally emits a sheet tab strip (when
all_sheetscarries more than one), then a filter form (date-range plus anyfilter_specscontrols) 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-renderedsheetis highlighted as active. Passapp.analysis.sheetsto wire every sheet in the analysis as a tab. Default()keeps single-sheet behavior for callers that haven’t migrated.- Parameters:
app (
App) – treeAppnode owning the analysis the sheet lives in.app.resolve_auto_ids()is called before render (idempotent).sheet (
Sheet) – treeSheetnode. Must be one ofapp.analysis.sheets; raisesValueErrorif not.dashboard_id (
str) – URL slug for this dashboard. Embedded in every visual’s data-fetch URL + the Refresh button’shx-getso the path matches whatserver.make_appwired its route for.theme (
ThemePreset|None) –ThemePresetto inject as CSS variables. WhenNone, falls back toDEFAULT_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 bydata-visual-kindon the parent section.The fragment is the script tag ALONE (no wrapper div). HTMX’s default
hx-swap="innerHTML"drops it inside thevisual-data-<id>placeholder — wrapping in another div with the same id would create duplicate IDs after the swap.The
visual_idargument 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 adata-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 renderedparam_*andfilter_*/ date entries (everything that influences the SQL bind) get stamped asdata-bound-params='<json>'on the script tag. That makes failure-capturedom.htmlself- 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_controlsorder 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 perrender’s existing logic).Coverage:
MULTI_SELECT + StaticValues →
ParameterMultiSelectSpecwith inlinedoptions(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 + LinkedValues →
ParameterMultiSelectSpeccarryingoptions_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 + StaticValues →
ParameterDropdownSpecwith inlinedoptions(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 + LinkedValues →
ParameterDropdownSpeclikewise (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:
ParameterTextFieldcontrols (a different shape — text-field parity not yet needed);add_parameter_datetime_pickercontrols (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
HTML renderer for App2 — projects a tree |
|
Starlette ASGI server for the App2 (HTMX) dashboard renderer. |