Displaying Dexes on the Web
How to put TerraPulse's dexes in front of a website viewer — and why the way the data is already sorted does most of the work.
Status: design note / proposal. Nothing here is built yet (see
What it takes to build). It exists so that when we do build a
public dex surface, the shape is decided once and stays consistent across all six
families. Companion to docs/dex-data-model.md (the model) and docs/design-handoff.md
(the house look).
The one idea
A dex is already a website. Every dex is an indexed list of slots, and each slot holds exactly one thing — one earthquake, one year of dairy, one tide station, one asteroid. That is the same structure every good data site converges on by hand:
- a roster you can scan (the list of things), and
- a detail page you open for one thing (everything known about it).
We don't have to design that split — the storehouse already built it. Each kind keeps a
storehouse_index.json (the roster) rebuilt from a directory of per-slot dossiers (the
detail records). The roster row is already a summary card: it carries the slot's name,
how many observations it has accreted, how many sensor hits it drew, and its time span.
The dossier is already a detail page's worth of content: identity, intrinsic
properties, the full time-series the slot accreted, and cited provenance.
So the job isn't "figure out how to present this data." The job is "point the site's existing visual vocabulary at a structure that was built to be walked."
Two views fall out for free
| Storehouse artifact | Becomes | Existing site analogue |
|---|---|---|
storehouse_index.json roster (array of slot summaries, sorted by last_fix_utc) |
Roster view — a scannable list/grid of every slot in a kind | web/src/pages/articles/index.astro (dotted-leader list) + the features.ts card grid |
One per-slot dossier (data/<family>_storehouse/<kind>/<slot_id>.json) |
Slot view — one page per thing, the fat record | a single web/src/pages/articles/<slug>.astro |
That is the whole architecture. Everything below is how to dress those two views in the house style.
Map the data model to the page
The dossier is layered (spelled out in docs/celestialdex-framework.md and
docs/movingplatformdex-framework.md). Each layer has an obvious home on the page:
| Dossier layer | What it is | Where it goes on the page |
|---|---|---|
| Identity | event_id, name, designation, catalog id, kind |
Hero title + eyebrow. This is the <h1> and the "TerraPulse Research — |
| Intrinsic properties | what the thing is — orbital elements, cutoff rigidity, EF rating, station altitude | The 4-big-stat strip (the site's signature grid-cols-2 md:grid-cols-4 band). Estimated values get a soft badge. |
| The time-series array | the ordered series the slot accreted — close approaches, decay track, flux history, a platform's track | The star of the page. One chart / map / timeline. This is what makes a dex page more than a fact sheet. |
| Provenance / sources | the cited sources folded into this one slot | A source-chip row + a footer credit. Every datum traces back — see [feedback_provenance_every_datum]. |
| Provisional / soft flags | WARN:-level data-quality notes, has_series=false, provisional=true |
Small honest badges ("registry-only", "ingestion in progress"), never hidden. |
The roster row is the same content, pre-summarized. event_storehouse._index_entry()
already computes per slot: name, n_fixes, total_sensor_hits, first_fix_utc,
last_fix_utc. That is a card without writing a card — a title, two big numbers, and a
date range.
The roster view
A page per kind: every slot, scannable. Two ready-made layouts, both already on the site:
- Dotted-leader list (like
articles/index.astro) — name … date range, with the count and a one-line descriptor beneath. Best for text-heavy kinds and long rosters. - Card grid (like the
features.tsfeature cards) — each slot a tile with its name, its headline stat (n_fixesor the intrinsic marquee number), and a span. Best for kinds where each slot has a strong single number.
What the roster gives the viewer that a paper never could: the whole population at once. All 16,377 catalogued objects. All 154 magnetic observatories. All 431 gravitational-wave chirps. Sorted, counted, and each one a door. Controls to add, in rough priority:
- Sort — by
last_fix_utc(default, "what moved most recently"), by name, by the marquee stat. - Search / filter — by name, by
kind_subtype, by a property band (magnitude, rigidity, year). - A roster headline — the site's 4-stat strip applied to the kind: total slots,
date span, most-recent update, source count. The
kindscounts dict in the index gives you the totals for free.
The slot view (dossier detail)
One page per slot, and it mirrors an article page exactly:
- Hero — identity. Big serif
<h1>with the slot name, the pulsing-dot eyebrow, the byline (kind · span · sources). - Stat strip — the intrinsic properties as four big numbers. This is where the
StatCardcomponent earns its keep. - The series — the accreted time-series, visualized. A decaying satellite's altitude falling over 72 days; a station's flux history; an asteroid's close approaches on a timeline. This is the payload; give it the width.
- Provenance footer — the cited sources, the DuckDB/PostgreSQL lineage, the honest flags.
The reason this is compelling and not just tidy: a slot page shows one real thing
accumulating measured reality over time. That's a narrative the reader can hold, and
it's true by construction — the dex only ever organizes measured data
([feedback_measured_reality_only]).
Per-family display idioms
The two-view architecture is universal; the primary visualization differs by what a slot is. Lead with the idiom that matches the family:
| Family | A slot is | Primary visual | Roster shines as |
|---|---|---|---|
Eventdex (eq, tor, gw) |
one discrete event | point on a map + a moment on a timeline | a time-sorted feed — "what happened, when" |
Yeardex (dairy, landuse) |
one year of a subject | the value on a year axis; small-multiples across years | a settled historical series — frozen, complete |
Locationdex (neutron_monitor, tide_gauge) |
one fixed place | a map of pins; the station's series on click | a network map — the single most obviously compelling roster |
CelestialObjectDex (neo, satcat, satellite_decay) |
one celestial object | orbit / decay track (no Earth map — lat/lon are null) |
a catalogue you can page through by property |
YearLocationdex (drought, water-quality) |
one place × period cell | a choropleth or place×time heatmap | a grid you can slice by year or by place |
MovingPlatformDex (mobile_radiosonde) |
one moving platform + its track | the track drawn on a map | a fleet of paths |
Locationdex is the recommended first build: "154 stations as pins on a map, click one to see its measured series" is instantly legible, needs no explanation, and shows off the provenance model (each pin cites its network).
Live vs frozen: the velocity badge
docs/paper-taxonomy.md splits data by velocity — fast-moving live streams vs
slow-moving frozen aggregates. The dex framework draws the same line ("events are live,
years are frozen") and every live kind carries a refresh_and_store() "live edge" on a
scheduler cadence. Surface it:
- Live-edge kinds get the site's existing pulsing accent dot and a "live" badge — the roster is a standing monitor, updating on its cadence.
- Frozen kinds get a quiet "complete through
<year>" stamp — the roster is a closed historical record.
This one badge tells the viewer whether they're looking at a heartbeat or an archive, which is exactly the expectation-setting the taxonomy exists to provide.
Design constraints to inherit (non-negotiable)
Any dex surface must adopt the house rules from docs/design-handoff.md:
- Chonky / high-contrast is intentional. A primary user has low vision. Big type, the
4-stat idiom, WCAG AA (≥ 4.5:1 text, ≥ 3:1 large). When in doubt, bolder and larger,
never finer or denser. Do not regress this for the sake of fitting more slots on
screen — paginate instead. (See [
feedback_chonky_accessibility].) - The type system: Libre Baskerville display serif, Montserrat body, JetBrains Mono for all data and timestamps — slot ids, coordinates, counts, and UTC times render in the mono face.
- Provenance on every datum. No number appears without a traceable source; the slot
view's footer is the natural home for the lineage
([
feedback_provenance_every_datum]). - Measured reality only. If a kind ever carries a modelled field (e.g. a CME's
WSA-ENLIL arrival forecast), it must be visibly flagged as model context, never shown
as a measured value ([
feedback_measured_reality_only]).
What it takes to build
Today there is no HTTP surface for dexes. The only read path is the in-process
Python library src/terrapulse/monitor/dex_reader.py (read_slots, read_aggregate,
read_archive), built for the paper repoint campaign. The website's only research
surface is the hand-authored /articles Astro pages driven by features.ts. So the
enabling work, smallest-first:
- A roster endpoint —
GET /api/v1/dexes/<kind>returning the index roster (thin wrapper overdex_reader.read_slots(kind), projected to the summary columns). This is the single unlock; everything visual sits on top of it. - A slot endpoint —
GET /api/v1/dexes/<kind>/<slot_id>returning one dossier. - Two Astro page templates — a roster template and a slot template, per the two
views above, reusing
FeatureEyebrow,StatCard, and the hero/stat-strip vocabulary. - Static vs live generation — frozen kinds can be static-generated at build time (they don't change); live-edge kinds fetch the roster at request time so the "live" badge tells the truth.
Suggested rollout
Prove the pattern on one Locationdex kind (neutron_monitor, 64 stations) — a map of
pins backed by the roster endpoint, each pin opening a slot page with its measured flux
series. It's small, it's visually self-explanatory, and it exercises every piece
(roster + slot + provenance + the live badge) without committing the whole platform. Once
that page feels right, the same two templates carry every other kind — because every
other kind is already sorted the same way.
References
docs/dex-data-model.md— the slot model and the six families.docs/celestialdex-framework.md,docs/movingplatformdex-framework.md— the layered dossier shape.docs/paper-taxonomy.md— data velocity × window type (the live/frozen badge).docs/design-handoff.md— palette, type, and the accessibility floor.src/terrapulse/monitor/dex_reader.py— the read library a roster API would wrap.src/terrapulse/monitor/event_storehouse.py—_index_entry(), the roster-row fields.