Baseline before the clean-up (PD-13): the PD-11 reader setup and the PD-12 app dashboards as they ran on the local stack, uncommitted until now. Not deployed, not independently reviewed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
583 lines
48 KiB
Markdown
583 lines
48 KiB
Markdown
# ATOMA dashboard: Fleet map and Site details
|
||
|
||
**Status: built and tested on a local Superset (P2, 24 Oct 2026); merged into one dashboard with
|
||
the fleet-map view on 28 Oct 2026 (§9); not imported live.**
|
||
- `build_bundle.py` generates the dashboard, every dataset and every chart from this
|
||
specification, as one Superset import bundle; `import_bundle.py` imports it (§6).
|
||
- The local tests changed parts of this design. Each change is marked *(P2)* and §8 lists them.
|
||
- The merge of 28 Oct (owner's decisions PD-5 and PD-6, `Petrolist/.context/design-decisions.md`)
|
||
replaced the Sites Map dashboard by a Fleet tab holding the fleet-map view of petrolyst-platform.
|
||
Changed parts are marked *(28 Oct)*; §9 lists the results.
|
||
- Nothing is imported into the live Superset without the owner's go-ahead (phase P3).
|
||
|
||
**Datasets.** The `public.v_*` views of
|
||
`stacks/fuel-monitoring/fuel-db/migrations/0003_bi_views.sql`, read through Trino as
|
||
`fuel.public`.
|
||
|
||
**Design and rationale** live in the fuel-monitoring corpus, in
|
||
`.context/plans/2026-10-24-fuel-db-baseline-dashboard.md`:
|
||
- its §6 is this specification;
|
||
- the site-specific facts left out here (positions, distances, pilot sites) are there too;
|
||
- Q-numbers refer to its §7, and phases P0–P5 to its §8.
|
||
|
||
One dashboard (two until 28 Oct) replaces the earlier tab-per-site design; nothing in it is
|
||
specific to a site.
|
||
|
||
## 1. Capabilities of the installed Superset 6.1.0
|
||
|
||
Source: read-only inspection of `config.py`, `superset_config.py` and the frontend bundle on
|
||
24 Oct 2026. The former *(local test)* items were tested in P2 (§8); rows marked *(P2)* changed as a result.
|
||
|
||
| Requirement | What 6.1.0 provides | Choice |
|
||
|---|---|---|
|
||
| Map chart | `deck_scatter` (deck.gl Scatterplot): a modern plugin with the InteractiveChart behaviour. Also: deck_multi, deck_screengrid / hex / grid, country_map, world_map, cartodiagram, legacy `mapbox` | **deck.gl Scatterplot** |
|
||
| Basemap | `DECKGL_BASE_MAP` unset → OSM raster (`tile.openstreetmap.org`), OSM Swiss topo, Mapbox styles. `MAPBOX_API_KEY` is empty, so the Mapbox styles fail. CSP `connect-src` already allows the OSM host; Traefik sets no CSP | set `DECKGL_BASE_MAP` to **OSM only**; "© OpenStreetMap contributors" in a Markdown under the map |
|
||
| OSM terms | no key, no cost; tile usage policy: light use, attribution, no bulk download, a valid Referer (ours sends the origin) | fine for an internal user base; viewers' browsers reveal the viewed area (tile coordinates) and their IP to OSMF, not our data. Self-hosted tiles later if needed |
|
||
| Tooltip | "Tooltip contents" (drag columns) plus "Customize tooltips template" (Handlebars). `js_tooltip` exists but `ENABLE_JAVASCRIPT_CONTROLS` is False. The contents show column names, not labels, and leave out the colour column | **contents only** *(P2)*: short tooltip-only calculated columns (`code`, `name`, `l_per_h`, `method`, `status`, `validity`, `latest_record`); no template, no JavaScript |
|
||
| Marker colour | "Categorical color" by a dimension, from the colour scheme; a fixed colour per label through the dashboard's `label_colors` (confirmed in P2) | colour by `baseline_status_label` |
|
||
| Marker size | fixed radius, with minimum = maximum pixel radius. No outline: the layer is drawn with `stroked: false` | uniform, no outline *(P2)* |
|
||
| Text cards | The Handlebars chart compiles its template with `eval`; Superset's own CSP (`script-src 'self' 'strict-dynamic'` + nonce) blocks that, so the chart shows a CSP error instead *(P2)* | **no Handlebars**: one-row tables. The CSP stays as it is |
|
||
| Chart descriptions | "Show description" (`expanded_slices`) never renders in 6.1: the dashboard loads `description_markeddown`, the chart holder reads `description_markdown` *(P2)* | descriptions stay on the charts; the visible reading notes are a Markdown block at the top of each tab |
|
||
| Viewport | `autozoom` fits the filtered points | on |
|
||
| Clustering | none in deck.gl; only the legacy `mapbox` chart (`clustering_radius`, needs a Mapbox key) | **no clustering** (§3) |
|
||
| Marker click | emits a **cross-filter** on the latitude and longitude columns: the exact point, or a box around the click that shrinks as you zoom in. It applies to other charts on the same dashboard | cross-filters the Sites list |
|
||
| Navigation | **no chart-to-dashboard navigation.** A dashboard reads `?native_filters=<rison>` into preselected native filter values (also `native_filters_key`). Table cells render sanitized HTML (`allow_render_html`) | an "Open site detail" link column in the Sites list |
|
||
| Native filters | Select (single or multi, required, searchable, "first value by default"), Time range; scoping per chart and tab; cross-filter scoping per chart | §4, §5 |
|
||
| Markdown iframe *(28 Oct)* | `HTML_SANITIZATION` is on and strips an iframe from a Markdown block; `HTML_SANITIZATION_SCHEMA_EXTENSIONS` lets named tags and attributes through. The CSP (`default-src 'self'`, no `frame-src`) allows frames from this origin only | allow `iframe` with `src`, `title`, `loading`, `style`; the fleet-map view is served on the same origin under `/views` (§3) |
|
||
| Deep link into a tab *(28 Oct)* | `#TAB-<id>` after the URL opens that tab; it combines with `?native_filters=` (tested 28 Oct) | the "Site details" button opens the Site details tab with the site preset |
|
||
|
||
*(28 Oct)* The map moved out of Superset into the fleet-map view (§3). The rows Map chart,
|
||
Basemap, Tooltip, Marker colour, Marker size, Viewport, Clustering and Marker click describe the
|
||
deck.gl option the dashboard used until then; the view's choices are in §3. The Navigation row
|
||
still holds: the view builds the same `?native_filters=` link.
|
||
|
||
No custom frontend and no patch are needed.
|
||
|
||
## 2. Navigation flow
|
||
|
||
*(28 Oct)* One dashboard, two top-level tabs. *(29 Oct)* They are named **Map** (`TAB-fleet`) and
|
||
**Insights** (`TAB-site`), the dashboard's lenses; earlier text in this document calls them Fleet and
|
||
Site details. Readers see them as a lens switch, centred in the navbar (§6).
|
||
|
||
```
|
||
ATOMA (slug atoma)
|
||
filters, in Superset's left column: Site (exactly one, searchable) · Viewing period
|
||
— both out of scope on Map; readers have no column there (§6)
|
||
[Map] the fleet-map view (iframe, /views/fleet-map/), the page's full width
|
||
towers fitted to the placed sites ──hover or click──► popup
|
||
"Find a site" (code or name) ──► the map flies to it ──► its popup
|
||
└─"Site details"──► /superset/dashboard/atoma/?native_filters=(site preset)#TAB-site
|
||
sites without an accepted location: counted in the map's key ("Choose them in Insights",
|
||
the view's tab_label parameter); chosen in the Site filter
|
||
[Insights] D0 site header, then the tabs
|
||
Summary · Fuel & Deliveries · Runtime & Electricity · Baseline & Evidence
|
||
(a row of section buttons, not a second row of tabs: dashboard.css)
|
||
```
|
||
|
||
The filters sit in Superset's left column (`filter_bar_orientation: VERTICAL`) *(owner,
|
||
29 Oct; `build_bundle.py` checks it)*. A filter shows in the column on the tabs where it is in
|
||
scope; the others are listed under "Filters out of scope" (the Viewing period on Summary, for
|
||
example). No filter applies to the Fleet tab, so readers have no column there and the map takes
|
||
the full width (`reader.css`, §6); admins keep the column on both tabs. The column opens
|
||
collapsed, a 32 px strip with an expand button (`FILTERBAR_CLOSED_BY_DEFAULT`,
|
||
`superset_config.py`; owner, 29 Oct); the selected site shows in the Site table above the
|
||
sections. So for a reader the tabs move sideways by 32 px between Fleet and Site details, or by
|
||
the column's width once it is expanded; the owner accepted that.
|
||
|
||
From 28 to 29 Oct the filters sat in a row above the tabs (`HORIZONTAL`). On the Fleet tab that
|
||
row held only an empty "More filters", and hiding it there made the tabs jump down on Site
|
||
details. Superset 6.1 has no per-tab setting for the filter bar; it hides it only per link
|
||
(`?show_filters=0`).
|
||
|
||
Until 28 Oct there were two dashboards: "ATOMA - Sites" (slug `atoma-sites`: a deck.gl map, five
|
||
filters, a sites list) and "ATOMA - Site Detail" (slug `atoma-site-detail`), linked by "Open site
|
||
detail" and "← Sites map".
|
||
|
||
## 3. Fleet tab: the fleet-map view *(28 Oct)*
|
||
|
||
The Fleet tab holds one Markdown block with an iframe of the fleet-map view, a page of
|
||
petrolyst-platform (`views/src/fleet-map/`) served under `/views/` on Superset's own origin. The
|
||
view reads the read service's `site_status` operation (`/views/api/v1/site-status`, from
|
||
`public.v_site`). It is not a Superset chart: it has no dataset and no native filter, and both
|
||
filters are out of scope on this tab.
|
||
|
||
```
|
||
[Fleet tab: MARKDOWN-fleet-map, w12 h100]
|
||
<iframe src="/views/fleet-map/?detail=/superset/dashboard/atoma/&filter=NATIVE_FILTER-site&tab=TAB-site">
|
||
```
|
||
|
||
| Part | Content | Missing or insufficient |
|
||
|---|---|---|
|
||
| Map | MapLibre on OpenFreeMap Positron: OpenStreetMap data, English and Dari place names (chosen by the owner on 28 Oct after a side-by-side with OSM standard tiles, which carry the same data). Fitted to the placed sites, clear of the key | no placed site: Afghanistan, and the key says so |
|
||
| Towers | one per site with an accepted location: a pin holding a lattice tower, in the site's baseline status colour (below); a site that is not an active pilot site faded; uniform size. The site's identity is `site_uid`, since two sites can share a `site_code` (an alias cut to 20 characters) | sites without an accepted location are not drawn |
|
||
| Popup | opens 120 ms after hover; stays while the pointer is on the tower or the popup; closes 250 ms after it leaves both. A click, tap or keyboard focus pins it; Esc or a click on the map closes it. It springs out of the tower (a fade only with reduced motion) and takes the side with room; when neither side has room for all of it, the side with more room, where its figures scroll while the header, the status with any validity note, and **Site details** stay in view. Content: code, name; status as `baseline_status_text` ("not approved" for Eligible and Provisional); any `baseline_validity_note` right beside the status and the L/h; L/h with method ("not available" when NULL); L/day, "whole record, not per running hour"; latest record; pilot status by name (active, reference, candidate, withdrawn, or not in the pilot); other sites whose towers cover this one (the same or nearly the same position), each a button that switches the popup to it; **Site details** | an approved estimate whose inputs changed is never shown without its validity note |
|
||
| Site details | a link (`target="_top"`) to this dashboard with the site preset in the Site filter, on the Site details tab: the `?native_filters=` value of §5 plus `#TAB-site`. The iframe's address names the dashboard, the filter and the tab, and `build_bundle.py` checks them | no link when that address is missing or malformed: the view accepts only a dashboard path and filter and tab ids of a fixed shape |
|
||
| Search | "Find a site: code or name", top left. Matches ignore case: code prefixes first, then codes containing the text, then names containing it; at most 8, each with its status dot and status. Arrow keys and Enter, or the pointer, choose one; the map flies to it at zoom 14 or closer (past the clusters) and pins its popup. Esc clears the field | sites without an accepted location are not searched (owner's choice, 28 Oct): the key counts them and the Site filter finds them; "No site matches" when nothing does |
|
||
| Operator's logo | ATOMA's logo in its own colours, bottom right above the map's credits, on frosted glass (*owner, 29 Oct*). The dashboard passes it in the iframe's address (`logo=/static/assets/branding/atoma_logo.svg`, `logo_alt=ATOMA`; `build_bundle.py` checks the file exists); the view takes only an image from that folder on its own origin, so it carries no operator's branding | no `logo` in the address: no logo |
|
||
| Key | the four statuses; "Faded: not an active pilot site"; "Colours show evidence status, not performance"; the number of sites without an accepted location, with "Choose them in Site details" | — |
|
||
| Keyboard, screen readers | a hidden list of the sites with the same links; focusing one opens its popup | — |
|
||
|
||
**Baseline status and tower colours.** Colours are neutral categorical hues, with no red or green
|
||
and no reference to L/h:
|
||
|
||
| Status | Definition (`fm.v_site_baseline_status`) | Colour |
|
||
|---|---|---|
|
||
| Approved baseline | an approved `baseline_version` with a decision reference. The **only** state that says "approved" | teal #009688 |
|
||
| Eligible – not yet approved | no approval; the latest proposed KPI version has `meets_policy` (BP-1: ≥ 2 eligible G1 inputs, ≥ 30 matched days, no conditions, one population and metric) | blue #3987e5 |
|
||
| Provisional | no approval; a proposed version below BP-1, **or** usable intervals with no version yet (no estimate shown, method "no baseline version proposed") | amber #c98500 |
|
||
| Insufficient evidence | no usable interval. **Every pilot site while no runtime evidence exists** | slate #64748B |
|
||
|
||
**Nearby sites.** Towers that would overlap cluster into a teal circle with their count (radius
|
||
36 px, up to zoom 13); a click on it zooms in until they separate. The pilot's sites, a few
|
||
kilometres apart, separate at the zoom the map opens with. The towers are one symbol layer, so
|
||
thousands of sites stay fast.
|
||
|
||
**Sites without an accepted location** are not plotted. The key counts them, and the Site filter
|
||
in Site details lists every site, placed or not.
|
||
|
||
## 4. Site details tab (any site)
|
||
|
||
**Filters**
|
||
- **Site** (`NATIVE_FILTER-site`):
|
||
- Select on `v_site.site_label`, single value, **required**, searchable,
|
||
**first value by default** with `requiredFirst` (the §4 fallback, adopted in P2);
|
||
- scope: every chart on every tab;
|
||
- every detail dataset carries `site_label` from `v_site`, so this one filter drives all of
|
||
them (confirmed in P2: every chart request carries the selected site).
|
||
- **Viewing period** (`NATIVE_FILTER-period`):
|
||
- Time range, default no filter;
|
||
- scope: the time-series charts on Fuel & Deliveries and Runtime & Electricity only;
|
||
- Summary and Baseline & Evidence are outside its scope, so a viewing range never changes a
|
||
baseline's fixed window.
|
||
|
||
**Exactly one site, and never network totals:**
|
||
1. A required, single-value filter. *P2: "required" alone does not stop the charts: with no
|
||
value they queried every site.*
|
||
2. Therefore "Select first filter value by default", with `requiredFirst`, as the filter editor
|
||
writes it: the charts wait for the filter, the first site is applied without "Apply filters",
|
||
and "Clear all" stays disabled. Without `requiredFirst` the first value waits behind "Apply
|
||
filters" while the charts have already queried every site.
|
||
3. No Big Number or aggregate tile for site figures. They are computed per site in `v_site`,
|
||
never aggregated by Superset. *(29 Oct, PD-12: the app's Overview, §10, adds up recorded
|
||
amounts across sites; per-site rates are still never averaged.)*
|
||
4. The Handlebars "Select one site" guard is gone with Handlebars (§1); with (2) the filter is
|
||
never empty.
|
||
|
||
```
|
||
[D0 Site: one-row table, code · name · province · pilot status · baseline status · location] (28 Oct: inside Site details, no link)
|
||
every tab [Reading notes (Markdown) w12] (P2)
|
||
Summary [S1 Fuel per running hour: one-row table w12] (P2)
|
||
[S2 Supporting measures w12]
|
||
[S3 Evidence coverage timeline (Gantt) w12]
|
||
Fuel & Deliveries [F1 Deliveries w6][F2 Recorded stock w6]
|
||
[F5 Deliveries table w12] (P2)
|
||
[F3 Fuel per calendar day by interval w12]
|
||
[F4 Interval table w12]
|
||
Runtime & Electricity[R1 Runtime per day by meter interval w6][R2 Hour-meter evidence w6]
|
||
[R1T Runtime pairs and their validity w12] (P2)
|
||
[R3 Grid energy, kWh w6][R4 Electricity cost, AFN: bar over table w6]
|
||
Baseline & Evidence [B1 Baseline versions w6][B2 Inputs of each version w6]
|
||
[B3 All intervals: eligibility and exclusions w12]
|
||
[B4 Interval L/h against baseline w12]
|
||
[B5 Assumptions w6][B6 Missing data and quality checks w6]
|
||
[B7 Evidence references w6][B8 Workbook calculations, assessed w6]
|
||
[B9 Stock against engine-measured fuel w6][B10 Equipment specifications w6]
|
||
[B11 Modelled L/h under an assumed runtime w12]
|
||
```
|
||
|
||
| # | Question it answers | Type | Dataset | Dims / measures (units) | Grain | When data are missing or insufficient |
|
||
|---|---|---|---|---|---|---|
|
||
| D0 | Which site am I looking at? | Table, one row *(P2)* | v_site | code, name, province, pilot status, baseline status, location status *(28 Oct: the link "← Sites map" is gone; the Fleet tab is one click away)* | site | the Site filter is never empty (§4) |
|
||
| S1 | What is the site's L/h, by which method, over which window, how strong? | Table, one row *(P2)* | v_site | `lph_estimate`, `lph_method`, `baseline_status_label`, `baseline_validity_note`, window, decision; L/day whole record | site | Keeps its position. **In v1 every site shows "Insufficient runtime evidence: no hour-meter readings"**, plus L/day labelled "per calendar day, not per running hour". "Eligible" and "Provisional" carry "not approved" |
|
||
| S2 | What supports it? | Table | v_kpi_measures | measure, value, unit, `value_kind` (recorded · calculated · assumed · not established), window. Two L/day rows: the whole record (moves with new records) and the L/day reference version (fixed window, its status and any input drift in the note) | site × measure | Rows stay and show "not established" |
|
||
| S3 | Which periods are covered by which evidence, and where are the gaps? | Gantt | coverage_detail *(P2: virtual dataset over v_coverage)* | lane name without the view's sort number; start/end; colour = status; tooltip: litres delivered in a stock interval, or a bill's AFN amount | span | Nothing is interpolated. *(P2)* The view emits a lane only where spans exist, so the tab's notes name the four lanes and say that a missing lane means no span could be formed: no evidence of that kind, or too few readings (F2, R2 show single readings) |
|
||
| F1 | When and how much was delivered? | Bar (time axis, **day** grain) | v_delivery (one row per delivery event) | `delivery_date`; **SUM(`delivered_l`)**, titled "Litres delivered per day (all deliveries that day)" | day | Bars only on delivery dates; "deliveries are not consumption" in the tab's notes. *(P2)* The count and the individual deliveries are in **F5**, a table on v_delivery (date, visit of the day, event, litres, price, sources, review), which also lists a delivery of unknown quantity; its search box shows the number of records |
|
||
| F2 | What stock was recorded? | Scatter (no lines) | v_stock_reading | `observed_on`; litres; series = reading type × `value_kind`, split by visit when a day has several *(P2)* | observation | Points only; nothing between readings |
|
||
| F3 | How much fuel was used per calendar day in each observed interval? | Bar (categorical x, ordered by start) | v_interval | `interval_label`; MAX(`fuel_l_per_day`) (L/d); colour = `stock_status` | stock interval | Intervals that are not `ok` greyed (*P2*: fixed grey per status in Site Detail's `label_colors`); the status is the series name, so legend and tooltip give the reason; no monthly spreading |
|
||
| F4 | Interval detail | Table | v_interval | dates, opening, deliveries, closing, used, L/d, status | interval | Shows the reasons |
|
||
| R1 | How many hours did each generator run per day? | Bar (categorical) | v_runtime | interval × meter; MAX(h/day). h/day is NULL for an invalid pair (`pair_valid` false: two epochs, a decreasing timer or over 24 h/day); such pairs show no bar and are listed with their flags in **R1T**, a pair table with `pair_valid`, `same_epoch`, `monotonic`, `over_24h_per_day` *(P2: the bar tooltip cannot show them)* | meter interval | **v1: empty**, with "No hour-meter readings" in the tab's notes |
|
||
| R2 | What runtime evidence exists? | Table | v_hour_meter_evidence | meter, mapped set, date, hours, state, epoch | reading | **v1: no rows**, "No hour-meter readings" |
|
||
| R3 | How much grid energy did the site receive? | Table, the kWh row *(P2)*; a bar once kWh exist | v_kpi_measures (kWh row) | kWh per bill month | month | "No kWh records; bills are cost only and are not converted to fuel or hours" |
|
||
| R4 | What did grid electricity cost? | Bar (month) + table | v_electricity_cost | bill month; amount (AFN, inferred) | month | Own chart, never on a fuel axis. A disputed bill is listed without amount or kWh |
|
||
| B1 | Which baseline versions exist, and who approved them? | Table | v_baseline_version | code, status, value, window, grade, decision | version | "No baseline defined" |
|
||
| B2 | What went into each version? | Table | v_baseline_input | interval, included, reason, fuel, runtime, drift | version × interval | empty until a version exists |
|
||
| B3 | Which intervals are usable, and why not? | Table | v_interval | eligibility, exclusion reasons, grade, conditions, in current baseline | interval | v1: every interval excluded, "no timer reading at both ends" (and "a generator has neither a counter nor an idle assumption") |
|
||
| B4 | What L/h did each interval give, against the approved baseline? | Mixed: bars + line | v_interval | x `interval_label`; A: MAX(`l_per_h`) where eligibility ≠ excluded; B: MAX(`current_baseline_l_per_h`) (approved value only, and only beside intervals of the same metric and population, `baseline_comparable`) | interval | v1: no bars; description points to S1 |
|
||
| B5 | Which assumptions apply? | Table | v_assumption | code, topic, value and range, basis, status, rationale | assumption | "None recorded" |
|
||
| B6 | What is missing or inconsistent? | Table | v_data_quality | check, status, detail | check | Lists the gaps: no runtime, placeholder rows, no kWh, no transfer records, location awaiting acceptance |
|
||
| B7 | Which documents do the figures rest on? | Table | v_evidence | title, kind, issuer, date, used for | document | — |
|
||
| B8 | What did the operator workbook calculate, and is it usable? | Table | v_imported_calculation | label, formula, value, our reading, use, reason, `claim_status` | cell | Shows "excluded" with the reason for "actual L/h" and "CP L/h". A disputed transcription shows its status, without formula or value |
|
||
| B9 | Does stock depletion match engine-measured fuel? | Table | v_reconciliation | engine L of this generator, `covered_h` of `interval_h`; site-wide: stock L, engine L of all generators, `all_generators_metered`, difference L and % *(P2: both engine totals shown, so the difference reconciles)*. The window runs from the opening visit's departure to the closing visit's arrival. The difference is shown only for an `ok` stock interval with both visit times recorded, in which every generator is placed at the site throughout and covered for the whole window | interval × generator | "No flow-meter data yet"; partial coverage, unknown visit times or a mid-interval placement show the hours covered and no difference |
|
||
| B10 | What does the equipment specification say? | Table | v_spec_reference | attribute, value, unit, basis, applicability, `claim_status` | spec | Titled "Manufacturer and nameplate data, not site performance". A disputed specification shows its status, without value |
|
||
| B11 | What would L/h be under the stated runtime? | Table | v_modelled_lph | assumption code, status, basis; h/day with low–high; window, `matched_days`; L/day; modelled L/h with low–high; `value_kind` | site × assumption | Titled "Modelled from an assumed runtime, not measured". "No runtime assumption recorded" |
|
||
|
||
Modelled L/h (B11) is in v1 (Q9, decided 24 Oct 2026). It pools only the usable intervals inside
|
||
the runtime assumption's dates, and shows that window and its days. It never reaches S1 or the
|
||
fleet map.
|
||
|
||
## 5. Datasets and filter mapping
|
||
|
||
Physical datasets, one per `public.v_*` view on Trino `fuel.public`, plus one virtual dataset
|
||
*(P2)*. `coverage_detail` adds two columns to `v_coverage`:
|
||
- `lane_name`, the lane without its sort number, which read as a count;
|
||
- `amount`, a stock interval's delivered litres (from its opening visit up to its closing visit,
|
||
so the opening refill is included) or a bill's AFN amount.
|
||
|
||
Both use only `v_coverage`, `v_delivery` and `v_electricity_cost`. Every dataset has the same
|
||
four site columns from `v_site`: `site_uid`, `site_code`, `site_name`, `site_label`.
|
||
|
||
| Dataset | Grain | Used by | Native filters that apply |
|
||
|---|---|---|---|
|
||
| v_site | site | D0, S1; the fleet-map view through the read service *(28 Oct)* | Site |
|
||
| v_kpi_measures | site × measure | S2, R3 | Site |
|
||
| v_coverage | site × lane × span | — (S3 reads `coverage_detail`) | Site |
|
||
| coverage_detail *(P2)* | site × lane × span | S3 | Site |
|
||
| v_delivery | delivery | F1 | Site, Viewing period |
|
||
| v_stock_reading | observation | F2 | Site, Viewing period |
|
||
| v_interval | stock interval | F3, F4, B3, B4 | Site; Viewing period on F3 and F4 only |
|
||
| v_runtime | meter interval | R1 | Site, Viewing period |
|
||
| v_hour_meter_evidence | reading | R2 | Site, Viewing period |
|
||
| v_electricity_cost | bill month | R4 | Site, Viewing period |
|
||
| v_baseline_version, v_baseline_input | version (× interval) | B1, B2 | Site |
|
||
| v_assumption, v_data_quality, v_evidence, v_imported_calculation, v_reconciliation, v_spec_reference, v_modelled_lph | per view | B5–B11 | Site |
|
||
|
||
**Metrics.** Saved metrics enforce pooling, with the same eligibility filter on numerator and
|
||
denominator, e.g. `pooled_l_per_h = SUM(CASE WHEN eligibility <> 'excluded' THEN fuel_used_l END)
|
||
/ NULLIF(SUM(CASE WHEN eligibility <> 'excluded' THEN runtime_h END), 0)`. A chart that uses it
|
||
is grouped or filtered by `metric` and `population_key` (both in `v_interval`), so it never pools
|
||
across methods or generator sets. The site's L/h figure itself comes only from a baseline version
|
||
(`v_site`), never from this metric. There is no AVG metric on ratio columns.
|
||
|
||
**Site details link** *(28 Oct)*. The fleet-map view builds it (petrolyst-platform
|
||
`views/src/shared/deepLink.ts`) from its iframe address: the dashboard path, the Site filter's id
|
||
and the tab. The rison shape is the one the sites list used until 28 Oct, confirmed in P2, and the
|
||
view's tests compare it with that list's Trino output:
|
||
|
||
```
|
||
/superset/dashboard/atoma/?native_filters=<url-encoded:>
|
||
(NATIVE_FILTER-site:(id:NATIVE_FILTER-site,extraFormData:(filters:!((col:site_label,op:IN,val:!(q)))),
|
||
filterState:(value:!(q)),ownState:()))
|
||
#TAB-site
|
||
-- q = the site label in rison quotes: 'label', with ! written !! and ' written !'
|
||
```
|
||
|
||
|
||
## 6. Build as code, scale, access
|
||
|
||
**Build**
|
||
- `build_bundle.py --database-uuid <uuid>` writes one import bundle containing the dashboard,
|
||
every dataset and every chart:
|
||
- stable UUIDs (uuid5), so a re-import updates instead of duplicating; the zip is
|
||
byte-identical for the same inputs;
|
||
- fixed native-filter ids (`NATIVE_FILTER-site`, `NATIVE_FILTER-period`) and tab ids
|
||
(`TAB-fleet`, `TAB-site`, `TAB-summary`, …);
|
||
- `label_colors` for the four statuses;
|
||
- every view column, from `columns.tsv` (a snapshot of Trino's `information_schema`);
|
||
- the existing "Fuel (Trino)" database UUID, which differs per instance:
|
||
`import_bundle.py --show-database` prints it.
|
||
- Before writing, it checks that every column a chart or filter uses exists in its dataset,
|
||
that every chart in a Site filter's scope has `site_label`, that the fleet map's iframe
|
||
address names this dashboard, its Site filter and the Site details tab *(28 Oct)*, and that
|
||
no row is wider than 12.
|
||
- Import: `import_bundle.py <zip> --user <username>`, inside the Superset container (*P2*: not
|
||
`superset import-dashboards`, which leaves existing datasets and charts unchanged on a
|
||
re-import). It runs the dataset, chart and dashboard importers with overwrite on, never creates
|
||
or changes the database, and reports columns that differ between a dataset and its view.
|
||
- A re-import replaces edits made in the Superset UI to these objects.
|
||
|
||
**Adding a site**
|
||
- Import its `site.yaml` and its records. Nothing in the dashboard or the fleet-map view names a
|
||
site.
|
||
- A new site appears in the Site filter, and on the map once its location is accepted; until
|
||
then the map's key counts it.
|
||
|
||
**Hundreds of sites**
|
||
- `v_site` has one row per site; the fleet map draws its towers as one symbol layer and
|
||
clusters overlapping ones, so thousands of sites stay fast.
|
||
- The Site filter loads the distinct labels; with thousands of sites, check its value limit
|
||
(open point 6 of the design decisions).
|
||
|
||
**Superset configuration changes** (in `superset_config.py`; image rebuild in P3):
|
||
- `DECKGL_BASE_MAP = [["tile://https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap"]]`
|
||
(no deck.gl chart since 28 Oct; kept for charts made by hand);
|
||
- *(28 Oct)* `HTML_SANITIZATION_SCHEMA_EXTENSIONS` lets an `iframe` (`src`, `title`, `loading`,
|
||
`style`) through a Markdown block. The fleet-map view must be served on the same origin under
|
||
`/views` (petrolyst-infra routing, not part of this step).
|
||
|
||
The CSP needs no change. P2: the tiles load and the map causes no CSP violation. One violation
|
||
appears on every Superset page, including the stock dashboard list: a capability probe
|
||
(`Function("")`) in the 6.1 frontend bundle, caught by the bundle itself.
|
||
|
||
**Access, internal first.** The dashboard for Admin and Alpha only; the read service behind the
|
||
same sign-in *(28 Oct: not built yet; locally it has none)*. No Gamma role until the
|
||
ATOMA-facing edit.
|
||
|
||
**ATOMA's readers** *(owner, 29 Oct)*. The Superset role **"ATOMA Reader"** (Keycloak client role
|
||
`atoma-reader`) reads the ATOMA dashboard and nothing else.
|
||
- **The role** (`petrolyst-insights/roles/atoma_reader.py`): exactly 12 permissions, set by
|
||
`superset-init` whenever the image changes, and reset by hand with the command in the README.
|
||
- Dashboard and Chart `can_read`;
|
||
- Superset `can_dashboard`, `can_dashboard_permalink`, `can_log`;
|
||
- Api `can_time_range`;
|
||
- the filter state (`can_read` and `can_write`), permalinks (`can_read`), the current user,
|
||
the CSRF token and the menu.
|
||
|
||
It has no menus, no write rights, no Explore, CSV, sharing, drill or query view. The bundle
|
||
import attaches the role to the dashboard (DASHBOARD_RBAC).
|
||
- **Rules a role cannot express** (`petrolyst_reader.py`, in both security managers):
|
||
- readers own nothing, and reach a dashboard only through its roles (published, role match);
|
||
- the dashboard list shows them only those;
|
||
- their chart-data responses carry no SQL (`patches/fix_redact_query_for_readers.py`;
|
||
Superset itself drops it only for embedded guests).
|
||
- **What they see:**
|
||
- every page they open leads to the ATOMA dashboard;
|
||
- a stylesheet loaded only for them (`assets/branding/reader.css`) hides the dashboard's
|
||
title row, the theme switcher, and Info and About in Settings, leaving Logout;
|
||
- the same stylesheet hides the filter column on the Fleet tab, and while the dashboard
|
||
loads. It sizes the map to the window: the full width at once (Superset measures widths
|
||
again only about 0.3 s after loading), and down to 32 px above the window's bottom edge
|
||
instead of the layout's 800 px, at least 560 px. `build_bundle.py` checks that it names the
|
||
Fleet tab and that the map spans all 12 columns;
|
||
- the same stylesheet turns the top-level tabs into a lens switch, centred in the navbar, with
|
||
an icon each; the map's block then grows by the tab row's height. A scroll-linked animation
|
||
moves the switch up with the navbar, which scrolls away. Without support for scroll-linked
|
||
animations, or narrower than 900 px, the tab row stays in the page. From 1100 px the switch
|
||
shows a third lens, AI, as a teaser: greyed, tagged PRO in green, drawn by the tab list
|
||
(not a tab, not clickable; the layout keeps Map and Insights). Narrower, it would cover the
|
||
logo. `build_bundle.py` checks that every layout id the stylesheet names exists;
|
||
- the stylesheet's address carries a hash of its content
|
||
(`petrolyst_reader.register`): Superset lets browsers keep static files for a year, so a
|
||
changed file would otherwise not reach readers who loaded the old one;
|
||
- Superset already hides the menus, "+" and editing from a role without them.
|
||
- **One refused request remains, harmless:** Superset stores a dashboard's label colours when
|
||
they changed (`PUT /api/v1/dashboard/<id>/colors`). A reader may not, so it is refused until
|
||
someone with edit rights opens the dashboard; the colours show either way.
|
||
- A reader is someone whose only roles are "ATOMA Reader" (and Public). Anyone with Admin, Alpha
|
||
or Gamma as well sees everything as before.
|
||
|
||
## 7. Acceptance checks
|
||
|
||
Run in P2 against a local Superset, and repeated as a smoke test in P3.
|
||
|
||
1. **New site, no new objects.** Count dashboards, charts and datasets in the metadata database.
|
||
Then import a synthetic fourth site with records and an accepted location, and a fifth with no
|
||
location. The counts are unchanged; site 4 appears on the fleet map and in the Site filter;
|
||
site 5 appears in the Site filter, and the map's count of sites without an accepted location
|
||
goes up by 1 *(28 Oct)*.
|
||
2. **Site switching.** In Site Detail, select pilot site A, then site B. D0 shows the new code and
|
||
name; every chart's query (View query, or chart-data API) carries `site_label = …<site B>…`; no
|
||
chart keeps site A's rows.
|
||
3. **Clearing the selection.** With the Site filter cleared, no chart shows multi-site data; D0 and
|
||
S1 show "Select one site". If not, switch to the §4 fallback and re-test.
|
||
4. **Unavailable L/h.** For each pilot site:
|
||
- the fleet map's popup says "not available" with the method "no runtime evidence" *(28 Oct)*;
|
||
- S1 shows the insufficient-evidence state plus L/day;
|
||
- B3 shows every interval excluded, with its reason;
|
||
- B11 shows a modelled range labelled "not measured", and that figure appears nowhere else.
|
||
5. **Missing coordinates.** A site without an accepted location is not plotted, is counted in the
|
||
map's key, and can be chosen in the Site filter *(28 Oct)*.
|
||
6. **Filter propagation.**
|
||
- *(28 Oct)* Both filters are out of scope on the Fleet tab; the fleet map shows every placed
|
||
site, sites that are not active pilot sites faded.
|
||
- In Site details, the Viewing period changes F1–F5, R1, R1T, R2, R4 and R4T, but not S1 or
|
||
B1–B4.
|
||
7. **Detail links** *(28 Oct)*. For each placed site, "Site details" in its popup opens the Site
|
||
details tab with exactly that site selected and Summary open; D0 matches, and every chart's
|
||
request carries that site.
|
||
8. **Colours.**
|
||
- Fixtures in each status show the four colours.
|
||
- "Eligible" never renders as "Approved" unless the fixture has an approved version with a
|
||
decision reference.
|
||
- Marker size is identical across sites.
|
||
- *(28 Oct)* An approved site whose inputs changed shows its validity note beside the status
|
||
and the L/h in the popup.
|
||
9. **Basemap.** The fleet map's tiles load, the attribution is visible, and neither the view nor
|
||
the dashboard adds a CSP violation (Superset's own probe, §6, stays) *(28 Oct)*.
|
||
|
||
## 8. P2 results (local, 24 Oct 2026)
|
||
|
||
Local Superset 6.1.0 image built from this repository, Trino 484, fuel-db with migrations
|
||
0001–0003 and the corpus data loaded; browser checks in headless Chrome.
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| 1 New site | pass: with a synthetic site with an accepted location and one without, the object counts stay unchanged (2 dashboards, 28 charts before R1T was added, 18 datasets); the first is in M1, M3 and the Site filter, the second in M5, and M4 goes from 0 to 1. Fixtures removed afterwards |
|
||
| 2 Site switching | pass: for each active pilot site, every chart request on the four tabs carries that site's `site_label`, all HTTP 200 (25 per site; F5, added later, re-checked for one site) |
|
||
| 3 Clearing | pass with the fallback (§4): "required" alone let the charts query every site |
|
||
| 4 Unavailable L/h | pass: tooltip and M3 "not available", method "Not available: no runtime evidence"; S1 the same plus L/day; B3 every interval excluded with its reason; B11 the modelled range, labelled "not measured", nowhere else |
|
||
| 5 Missing coordinates | pass (synthetic site, check 1): not plotted, in M4 and M5, its link opens its detail |
|
||
| 6 Filter propagation | pass: the default "active" reaches M1, M3, M4, M5; a marker click re-queries M3 only, with a latitude/longitude box; the Viewing period reaches F1–F5, R1, R1T, R2, R4 and the bill table, and no Summary or Baseline & Evidence chart |
|
||
| 7 Detail links | pass: from M3 for each active pilot site, and from M5 |
|
||
| 8 Colours | pass for the mapping: a throwaway fixture with one point per status shows the four colours and equal marker sizes. Not tested by fixture: that the database never labels an unapproved version "Approved baseline" |
|
||
| 9 Basemap | pass: OSM tiles load, attribution visible, no CSP violation from the map |
|
||
|
||
Changes forced by the tests and by two Codex inspection rounds of 24 Oct (F2 visits, R1T, B9
|
||
totals, coverage lanes; then F5, grey stock statuses, lane wording), all marked *(P2)* above.
|
||
The second round's fixes were not inspected again. The grey colour for a non-ok stock interval
|
||
is configured but not shown by the current data, which has no such interval.
|
||
- Handlebars charts (D0, S1, R3) became one-row tables.
|
||
- The Site filter uses "first value by default" with `requiredFirst`.
|
||
- Reading notes moved into a Markdown block per tab.
|
||
- The map tooltip uses short tooltip-only columns.
|
||
- The markers have no outline.
|
||
|
||
The marker palette is unchanged from §3. The dataviz palette check flags slate as reading
|
||
grey, which is intended for "insufficient evidence", and amber at 2.99:1 contrast. The label is
|
||
always also shown as text, in the legend, the tooltip and M3.
|
||
|
||
## 9. Fleet map results (local, 28 Oct 2026)
|
||
|
||
Local Superset 6.1.0 rebuilt with the iframe setting, behind a local Traefik that serves Superset
|
||
and the petrolyst-platform read service on one origin (as on the server); the fuel-db of P2.
|
||
Browser checks with Playwright in Chrome (`petrolyst-platform/views/e2e/`).
|
||
|
||
**Spike, before building**
|
||
- A Markdown iframe survives the sanitiser with the setting of §6.
|
||
- A `target="_top"` link inside it navigates the whole dashboard page.
|
||
- `?native_filters=…#TAB-…` selects the site and opens that tab.
|
||
- Basemaps compared at zoom 11, 14 and 16 over the pilot area: OpenFreeMap Positron and OSM
|
||
standard tiles show the same OpenStreetMap data; the owner chose Positron.
|
||
|
||
**Bundle:** 1 dashboard, 26 charts, 19 datasets; two builds are byte-identical; the import reports
|
||
column drift 0. The two earlier dashboards and the charts M1, M3, M4 and M5 were deleted from the
|
||
local Superset through its API. The live Superset never had them (P3 not done).
|
||
|
||
| Check | Result |
|
||
|---|---|
|
||
| 2 Site switching | pass: choosing a second site in the Site filter moves every chart request to it |
|
||
| 3 Clearing | pass: after "Clear all" every chart request still carries exactly one site |
|
||
| 4 Unavailable L/h | pass for the popup ("not available" and the method); S1, B3 and B11 are unchanged by this step and were not re-run |
|
||
| 5 Missing coordinates | pass for the count: the map's key shows the 2 sites without an accepted location |
|
||
| 7 Detail links | pass: for each of the 3 placed sites, hover, then "Site details", opens the Site details tab with Summary active, D0 shows the site, and every chart request carries exactly its `site_label` |
|
||
| 8 Colours | the validity note beside an approved status and its L/h, and "(not approved)" for Provisional, pass as component tests on fixtures. Live, every site is "Insufficient evidence", so only slate was seen; the other three colours were checked by reading the code against §3, not on screen |
|
||
| 9 Basemap | pass: tiles load, attribution visible; no CSP violation from the view. Superset's own `script-src eval` probe appears on every Superset page, as in P2 (§6), and nothing else. The test's watcher was checked with a deliberately disallowed image inside the view |
|
||
|
||
Also passed: the popup opens on hover, stays while the pointer moves onto it, closes after the
|
||
pointer leaves; a click pins it; Esc and a click on the map close it; with reduced motion it only
|
||
fades and nothing pulses. The view's minimum size is 480 × 560 px (`catalogue.json`): there, and
|
||
also at 480 × 360, every site's popup stays inside the map, and an approved estimate whose inputs
|
||
changed is never in view without its warning, checked by scrolling the card. At 480 × 560 every
|
||
site's L/h can be read. A single placed site sits clear of the key. Two sites at the same
|
||
position can each be opened with the pointer. The states the scratch data lacks (approved with a
|
||
changed input, one site, two at one spot) are made in the browser by changing the read service's
|
||
answer; the database is unchanged.
|
||
|
||
**Filters on top and the map search** *(owner, later on 28 Oct)*: the filter row was checked on
|
||
screen first: the Fleet tab shows only "More filters", and Site details shows Site and Viewing
|
||
period in the row. Browser checks:
|
||
- the map takes the page's width;
|
||
- the search finds a site by part of its name and by its code, and its popup's "Site details"
|
||
opens exactly that site;
|
||
- a site inside a cluster is taken out of it;
|
||
- site switching and clearing (checks 2 and 3) still pass with the filters in the row.
|
||
|
||
**ATOMA readers and the operator's logo** *(owner, 29 Oct)*: the plan was reviewed by Codex
|
||
(approved after three rounds: deploy inputs, ownership and no-dataset paths, SQL in responses).
|
||
Browser checks as the local user `reader`:
|
||
- every page leads to the dashboard;
|
||
- no title row and no menus; Settings shows only Logout;
|
||
- the whole fleet-map suite passes as the reader;
|
||
- reading needs nothing the role lacks (only the colours PUT above is refused);
|
||
- no chart-data response carries SQL, fresh, cached or as `result_type=query` (an admin's
|
||
does);
|
||
- CSV and editing answer 403;
|
||
- two probe dashboards (Markdown only without roles, and one the reader owns) stay out of
|
||
reach: page, API and permalink.
|
||
|
||
Also checked:
|
||
- the role script removes a permission added by hand;
|
||
- every new file changes the image tag;
|
||
- the patch's `--verify` fails on unpatched Superset and on a tampered line.
|
||
|
||
Found on the way: under many parallel requests the local proxy returned 502s. gunicorn closes
|
||
idle connections after 2 s (`--keep-alive 2`) and Traefik reused them. The local proxy now drops
|
||
idle connections after 1 s. **The server has the same pairing** (Traefik in front of gunicorn
|
||
with the default keep-alive): open item.
|
||
|
||
**Final inspection:** two fresh Codex sessions reviewed the build.
|
||
- The first found four defects, all fixed and re-checked:
|
||
- the popup could leave a small map;
|
||
- two sites sharing a `site_code` could open each other's details;
|
||
- every non-active pilot status was called "reference";
|
||
- a failed database connection gave a 500 instead of the generic 503.
|
||
- The second confirmed those fixes and found three more:
|
||
- in a short card the validity warning could scroll away from the L/h;
|
||
- a single site was centred under the key;
|
||
- sites at one position hid each other.
|
||
|
||
All three are fixed and tested. Those last fixes, and the filter row and the map search, have
|
||
not had an independent inspection.
|
||
|
||
**Filters back on the left, the readers' Fleet tab, the navbar logo, section buttons** *(owner,
|
||
29 Oct)*:
|
||
checked in the browser as `reader` and `admin` at 1400 × 1000, 1920 × 1080, 1440 × 760,
|
||
1280 × 600 and 1024 wide:
|
||
- a reader's Fleet tab has no filter column and no resize handle, and the map is as wide as
|
||
the page and reaches 32 px above the window's bottom edge (no page scroll), at once rather
|
||
than after Superset measures again; at 600 px high it keeps 560 px and the page scrolls;
|
||
- Site details shows the column collapsed (32 px, for readers and admins, also after the
|
||
map's "Site details" link); expanded, it has the Site filter; switching back hides it again;
|
||
- an admin has the column on both tabs;
|
||
- the column no longer flashes up while the Fleet tab loads (one frame, about 20 ms, remains:
|
||
the grid exists before its tabs do);
|
||
- after a switch to Site details its charts are too wide for one or two frames before
|
||
Superset measures again; not changed;
|
||
- a reader's page links `reader.css?v=<hash>`, an admin's none;
|
||
- the navbar shows the wordmark at 24 px for readers, admins and the login page, white in
|
||
dark mode;
|
||
- Site details' inner tabs are a row of section buttons (`dashboard.css`), lined up with the
|
||
content, all four visible and switching, for readers and admins, light and dark; a
|
||
vertical list was tried first and dropped: Superset sizes the charts for the full width, so
|
||
they were cut off on the right;
|
||
- `build_bundle.py` stops when the orientation is not VERTICAL, when reader.css does not name
|
||
the Fleet tab, when the map spans fewer than 12 columns, or when `dashboard.css` names an
|
||
id that is not in the layout.
|
||
|
||
- *(lenses)* a reader at 1400 px: the switch centred in the navbar (Map | Insights), the map
|
||
735 px high in an 860 px window and 32 px above its bottom edge; scrolled by 30, 61 and
|
||
400 px the switch moves up by exactly as much and then stays out of view; at 820 px the tab
|
||
row stays in the page; an admin keeps the tab row, named Map and Insights; the map's key
|
||
says "Choose them in Insights".
|
||
- *(AI teaser)* a reader at 1400 px: the switch reads Map | Insights | AI PRO, still centred,
|
||
44 px high; a click on AI changes nothing; at 900 px the switch keeps Map | Insights.
|
||
|
||
These changes have not had an independent inspection (the owner skipped Codex).
|
||
|
||
**Not run:** check 1 (a synthetic fourth and fifth site), check 6 (the Viewing period's scope is
|
||
unchanged), sign-in through Keycloak, the server's routing of `/views`, sign-in for the read
|
||
service, thousands of sites, and dark mode.
|
||
|
||
## 10. The Petrolyst app's dashboards *(PD-12, local trial, 29 Oct 2026)*
|
||
|
||
The Petrolyst app (petrolyst-platform `app/`, PD-12) shows Superset dashboards inside its
|
||
Insights lens, with guest passes from its app service. `build_bundle.py` builds them beside the
|
||
unchanged ATOMA dashboard:
|
||
|
||
| Slug | Content | Filters (set by the app; Superset's filter column is hidden) |
|
||
|---|---|---|
|
||
| `atoma-overview` | O1–O6: sites by pilot status and by baseline status, litres delivered, fuel cost by currency, electricity billed by currency and basis, litres delivered per month | Viewing period on O3–O6 |
|
||
| `atoma-site` | the four §4 detail tabs as Superset's own tabs, the same rows and charts; no D0 (the app shows the site's header) | Site (all charts), Viewing period (as §4: fuel and runtime charts only) |
|
||
|
||
- **One site dashboard with tabs** *(owner, 29 Oct: "move it back inside the dashboard and make
|
||
the tab as they were")*: replaces the four one-tab dashboards `atoma-site-summary`, `-fuel`,
|
||
`-runtime`, `-baseline` and the app's per-section pages. Switching tabs needs no reload. The
|
||
app opens a site on its first tab; a new viewing period reloads the dashboard on the tab that
|
||
was open (the app reads it from Superset and names it in the address, `#TAB-fuel`). Delete
|
||
the four old dashboards by hand on an instance that has them.
|
||
|
||
- **Overview rule** (changes §4 rule 3 for the Overview only): only sums and counts of
|
||
recorded values across sites (`OVERVIEW_METRICS`); per-site rates (L/h, L/day) are never
|
||
averaged; money is grouped by `currency_text`, a missing currency is its own row, and no
|
||
total crosses currencies. `build_bundle.py` checks both.
|
||
- They carry no CSS; `dashboard.css` and `reader.css` are checked against the ATOMA dashboard
|
||
only.
|
||
- `import_bundle.py --embedded-out FILE --embed-domain URL` makes them embeddable from the app's
|
||
origin and writes `{slug: embedding id}` for the app service; the reader role stays on the
|
||
ATOMA dashboard only.
|
||
- Embedding is switched on in the local stack only (`.local/fmdb-scratch`,
|
||
`superset_config_local.py`); production keeps it off until PD-12's open points are settled.
|
||
- **Checked (local):** a self-signed guest pass opens its own dashboard and nothing else (another
|
||
embedded dashboard and ATOMA: 404; no pass or a wrong audience: 401); the embedded page answers
|
||
only to the app's referrer (403 otherwise); chart-data responses to guests carry no SQL.
|
||
|