component · v0.1.0

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.

<link rel="stylesheet" href="https://assets.gf.cx/op-panel/op-panel.css"> <script src="https://assets.gf.cx/op-panel/op-panel.js" defer></script>

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.

Scheduled jobs

Passing
7
of 9 jobs
Failing
1
needs attention
Recovered
1
last 24h
JobStatusRan
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

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>
Single source of truth. The generic core — panel chrome, sort segment, view toggle, collapse shell, card-view reflow, verdict rails, and the [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.