op-panel
An operational status panel. At rest it shows a roll-up of stat-card tiles and a + marker; expand it and the full detail — a sortable job table — is revealed. One shared sort segment (failing-first / A–Z / recent) and a list ↔ cards view toggle drive every panel on the hub at once, from a handful of data-attributes. Lifted from status.gf.cx.
This demo imports the module itself — the panel below is driven by the same op-panel.css + op-panel.js any surface would load. Single source of truth: the shelf. The status-hub renderer imports these files rather than inlining a copy.
Live demo
Click + to expand. The sort segment and view toggle are wired to the real comparators; your choice persists in localStorage just as it does on the hub.
Passing7of 9 jobs
Failing1needs attention
Recovered1last 24h
| Job | Status | Ran |
|---|---|---|
| bunny-noir-mirror | ✗ rsync.net leg failed | 2h ago |
| fleet-seed | ⚠ recovered | 5h ago |
| anthropic-editorial | ✓ passing | 1h ago |
| gcp-budget-audit | ✓ passing | 9h ago |
| weather-refresh | · not yet run | — |
Three behaviours, from data-attributes
1 · Collapse to a roll-up
A native <details> holds the controls + table. At rest the <summary> shows only the heading, a + marker, and a grid of card--stat tiles — the verdict at a glance. Expand for the full table. (Dan: “displayed more simply for top-level viewing … I don't think we need to see all the scripts unless we click the +.”)
2 · One shared sort model
Every row carries data-rank (verdict order), data-label (name) and optional data-age (hours). Three comparators reorder any [data-sortlist] container — a table tbody or a card grid, same code — and the choice is page-level: click A–Z on one panel and every panel re-sorts.
// the three comparators (verbatim shape from the hub)
status: (a,b) => (+a.dataset.rank||0) - (+b.dataset.rank||0)
|| a.dataset.label.localeCompare(b.dataset.label)
name: (a,b) => a.dataset.label.localeCompare(b.dataset.label)
recent: (a,b) => (b.dataset.age||-Infinity) - (a.dataset.age||-Infinity)
3 · List ↔ cards, one row set
The view toggle flips data-view on the panel. In cards the same <tr> rows reflow to a grid — the header row is hidden and each <td>'s data-th becomes its label. No duplicated markup; CSS does the reshaping.
The green-flash event model — four lessons baked in
- Delegate on the stable parent. The shared panel module rebuilds the toggle buttons on init, so direct listeners orphan — use capture-phase delegation on the panel, not the buttons.
- Defer the pulse past the restyle. The view switch flips
data-viewand thedisplaychange cancels a synchronously-started animation — start the flash in asetTimeout(…, 0). - Scope by a shared key. A
data-connfilter pulses only the matching rows (click Dropbox → only Dropbox flashes); clear any in-flight.is-flashingfirst so all→one never strands a row. - Headless gotchas.
rAFis throttled in headless Chrome andhttp.servercaches — usesetTimeoutand a?v=Nbuster when testing with Playwright.
Anatomy
<!-- panel: the hooks the shared script binds to -->
<section class="op-panel op-panel--collapsible"
data-panel data-view="list" data-default-sort="status">
<details class="op-collapse" open>
<summary class="op-collapse__summary">
<div class="op-collapse__bar"><h2>Scheduled jobs</h2>
<span class="op-collapse__marker">+</span></div>
<div class="op-rollup"><!-- card--stat tiles --></div>
</summary>
<div class="op-collapse__body">
<div class="op-controls"><!-- .op-sort + .view-toggle --></div>
<table class="scheduled-jobs"><tbody data-sortlist>
<tr class="sched-row" data-rank="0" data-label="…" data-age="2">
<td class="sched-label" data-th="Job">…</td> … </tr>
</tbody></table>
</div>
</details>
</section>
[data-panel] client script — lives ONCE here, in op-panel.css + op-panel.js. The status-hub renderer (gfcx_status_hub_render.py) <link>/<script src>-imports them instead of inlining a copy, the way made-with and the favicon control-plane already do. Surface-specific extensions that layer on top — transit sparklines, the sort-by-entity .card--pin, per-surface green-flash — stay in the host page by design; they aren't part of the module.
In use: status.gf.cx (every operational panel) · Source: github · Renderer: xlab-co-toolkit/gfcx_status_hub_render.py