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>
48 KiB
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.pygenerates the dashboard, every dataset and every chart from this specification, as one Superset import bundle;import_bundle.pyimports 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 withrequiredFirst(the §4 fallback, adopted in P2); - scope: every chart on every tab;
- every detail dataset carries
site_labelfromv_site, so this one filter drives all of them (confirmed in P2: every chart request carries the selected site).
- Select on
- 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:
- A required, single-value filter. P2: "required" alone does not stop the charts: with no value they queried every site.
- 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. WithoutrequiredFirstthe first value waits behind "Apply filters" while the charts have already queried every site. - 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.) - 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_colorsfor the four statuses;- every view column, from
columns.tsv(a snapshot of Trino'sinformation_schema); - the existing "Fuel (Trino)" database UUID, which differs per instance:
import_bundle.py --show-databaseprints 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: notsuperset 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.yamland 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_sitehas 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_EXTENSIONSlets aniframe(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 bysuperset-initwhenever 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_readandcan_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).
- Dashboard and Chart
-
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.pychecks 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.pychecks 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.
- 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).
- 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. - 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.
- 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.
- 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).
- 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.
- 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.
- 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.
- 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
--verifyfails 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_codecould 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.pystops when the orientation is not VERTICAL, when reader.css does not name the Fleet tab, when the map spans fewer than 12 columns, or whendashboard.cssnames 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,-baselineand 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 bycurrency_text, a missing currency is its own row, and no total crosses currencies.build_bundle.pychecks both. -
They carry no CSS;
dashboard.cssandreader.cssare checked against the ATOMA dashboard only. -
import_bundle.py --embedded-out FILE --embed-domain URLmakes 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.