Skip to content

Add GatewayActivityCard: is this gateway actually hearing anything? - #2

Merged
ampledata merged 6 commits into
mainfrom
feat/gateway-activity
Aug 1, 2026
Merged

Add GatewayActivityCard: is this gateway actually hearing anything?#2
ampledata merged 6 commits into
mainfrom
feat/gateway-activity

Conversation

@ampledata

Copy link
Copy Markdown
Contributor

Every pytak gateway plugin can now render the same activity panel — decode rate, trend sparkline, health, and a feed of recent contacts — reading the status file pytak.StatusWriter maintains at /run/<app>/status.json (snstac/pytak#107). Only the columns differ per gateway, because a tail number and an ACARS label matter for one and an MMSI matters for another.

<GatewayActivityCard
  appName="acarscot" title="ACARS Activity" noun="message"
  counterLabels={{ rx: 'received', emitted: 'plotted' }}
  columns={[
    { key: 'tail', label: 'Tail' },
    { key: 'flight', label: 'Flight' },
    { key: 'label', label: 'Label' },
    { key: 'freq', label: 'MHz' },
  ]}
/>

The hard part isn't reading the file

It's deciding whether what it says is still true.

Staleness is measured client-side, as time since we observed the file change. The obvious check — compare wall_t to Date.now() — is wrong: Cockpit runs in a browser that may be on a different machine from the gateway, so those are two different clocks. A laptop ten minutes off would show every healthy gateway as stale, or worse, a dead one as live. wall_t is still read, but only to detect and report that the clocks disagree — itself worth surfacing on a box meant to be GNSS-disciplined.

A timer drives re-render, because the interesting event is the absence of a change. A watcher alone would leave the last document on screen forever — precisely the failure this card exists to prevent.

Five states, because "no contacts" has several very different causes:

State Meaning
live file changing, contacts arriving
quiet writing, but hearing nothing
stale stopped writing — may be wedged
missing no file at all — stopped, or pytak too old
degraded running but reporting write_errors

A missing file says so explicitly rather than rendering as "working but quiet". And the banner always says when — "Receiving" with no timestamp is the claim this card exists to stop being taken on faith.

trendDirection masks pre-start buckets

Buckets older than the gateway's uptime_s are empty because the service wasn't running, not because it heard nothing. Counting them reported a dramatic rise after every restart.

A genuine gap — up for an hour, silent for twenty minutes, now hearing traffic — is still correctly reported as rising, since that's a real change an operator wants to see. Both cases are tested; the restart case was found by a test failing against my first implementation.

Sparkline

Inline SVG, no charting dependency — these plugins ship to a Pi over a field link. Baseline is always zero: scaling to [min..max] would make a flat line at 3 contacts/min look identical to one at 300. All-zero data draws a flat line on the floor rather than an empty box — silence is a real reading, and it's what a disconnected antenna looks like.

Also

Verified every --sns-*/--aos-* variable the new styles reference actually resolves. The first draft referenced four that don't exist (--sns-ink-console, --sns-muted-console, --sns-rule-color-console, --sns-font-narrow), which CSS would have failed silently.

29 tests pass; typecheck and eslint clean. Version → 1.3.0.

🤖 Generated with Claude Code

https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM

gba and others added 6 commits July 29, 2026 23:05
Eleven cockpit-* plugins each carried their own 143-308 line stylesheet
defining an identical --aos-* palette. cockpit-dronecot and cockpit-lincot
were BYTE-FOR-BYTE identical; charontak was a 308-line variant of the same
thing; four more repos also shared an identical 230-line pkg/lib/page.scss.
None of them referenced the AryaOS Product Suite design tokens, so the
console had drifted from the kit and from aryaos.org:

  accent     #35a58f teal  -> kit says Signal Orange #E4610F
  ok         #74d8a4       -> kit says #3F7D57
  danger     #ff8a7a       -> kit says #A32820
  body font  RedHatText    -> kit says Archivo
  mono       ui-monospace  -> kit says JetBrains Mono
  cards      8px radius + 0 10px 30px shadow -> kit says radius 0, no shadows

What the plugins already had RIGHT, and this preserves: the dark console
theme, 48px (3rem) targets for gloved use, and one 760px breakpoint, with
no stray breakpoints anywhere. That part of the 2026-07-14 fleet sweep held.

--aos-* names are kept as ALIASES onto the kit tokens, deliberately: every
plugin rule already says var(--aos-panel), so aliasing makes those rules
kit-correct without rewriting hundreds of declarations. A plugin migrates by
deleting its palette block and importing this.

The fonts are BUNDLED, not just named. Cockpit renders in the operator's
browser, not on the Pi, so installing Archivo on the box would do nothing --
measured on aryaos-4f11, fc-list finds 0 Archivo and 0 JetBrains faces, and
a bare `font-family: Archivo` would have silently fallen back to sans-serif.
A rule that looks applied and is not. Per-plugin webfonts are already the
established pattern: PatternFly ships ~22 woff2 into every plugin and
@font-face-s them relatively (264 across the fleet). Ours add ~85 KB.
OFL-1.1 licence and notices travel with them.

Two documented deviations from the kit:

  1. --sns-target is 48px, not 44px. Operator surfaces are used with gloves.
  2. Status colours are lightened for the console, because the kit specifies
     them against Paper. Measured WCAG contrast of the kit values as text on
     #0B1512:
         ok    #3F7D57  3.79:1  passes UI 3:1, FAILS body text 4.5:1
         warn  #A8811C  5.15:1  passes -- would have been fine as-is
         fault #A32820  2.55:1  FAILS outright
     The hue stays anchored to the kit token and the console variant is
     derived with color-mix, so there is no second hardcoded palette to
     drift. Percentages put all three in one narrow band, since a triad
     lightened unevenly reads as mismatched:
         ok 65% -> #7EA589 6.75:1, warn 75% -> #BA9C4F 7.04:1,
         fault 50% -> #CA8C84 6.72:1

radius-0 and no-shadow are scoped to our own surfaces plus the PatternFly
components we actually render, NOT a blanket `*` rule: PF uses radius
meaningfully in switches, spinners and progress bars. .aos-dot keeps its 50%
and says why -- a 2px square status dot reads as a defect, not a state.

Compiles to 4839 bytes. Asserted on the compiled output: radius 0, target
48px, #e4610f, Archivo, JetBrains Mono, box-shadow none, 760px, and the
woff2 URL all present; the only non-zero radius emitted is the documented
dot. Tests 6 passed, typecheck clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
I said this theme had the same orange-vs-danger collision as the AryaOS
portal. Measured on THIS surface, it does not:

  console panel #12211A      as text   min deltaE vs status
  Signal Orange #E4610F      4.78:1    51
  Okabe-Ito blue #56B4E9     7.23:1    46

The portal collision was orange sitting deltaE 5.3 from a saturated
vermillion danger colour. The console's status set is lightened pastels
(#7EA589 / #BA9C4F / #CA8C84), so orange is comfortably clear of it -- in
fact further than blue is. I asserted the collision without measuring this
surface.

The split is still made, on legibility rather than confusion: 4.78:1 is a
floor-scraping value for body text sitting next to 7.23:1, and an operator
reading a console for a whole shift should not have to work at it. Keeping
the two roles apart on every surface also means one rule to remember instead
of a per-surface exception.

  --sns-brand   Signal Orange: fills, rules, the mark. Never a label.
  --sns-accent  Okabe-Ito blue: interactive text and links.

--aos-accent keeps pointing at the interactive colour, because that is what
the plugins' existing rules use it for; fills ask for --aos-brand.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
Every pytak gateway plugin can now render the same activity panel -- decode
rate, trend sparkline, health, and a feed of recent contacts -- reading the
status file pytak's StatusWriter maintains at /run/<app>/status.json
(snstac/pytak#107). Only the columns differ per gateway, because a tail number
and an ACARS label matter for one and an MMSI matters for another.

The hard part is not reading the file, it is deciding whether what it says is
still TRUE.

  * Staleness is measured CLIENT-side, as time since we observed the file
    change. The obvious check -- compare wall_t to Date.now() -- is wrong:
    Cockpit runs in a browser that may be on a different machine from the
    gateway, so those are two different clocks. A laptop ten minutes off would
    show every healthy gateway as stale, or worse, a dead one as live. wall_t
    is still read, but only to detect and report that the clocks disagree.

  * A timer drives re-render, because the interesting event is the ABSENCE of
    a change. A watcher alone would leave the last document on screen forever,
    which is precisely the failure this card exists to prevent.

  * Five distinct states, because "no contacts" has several very different
    causes: live, quiet (writing but hearing nothing), stale (stopped
    writing), missing (no file at all), degraded (running but reporting
    write_errors). A missing file says so explicitly rather than rendering as
    "working but quiet".

  * The banner always says WHEN. "Receiving" with no timestamp is the claim
    this card exists to stop being taken on faith.

trendDirection() masks buckets older than the gateway's uptime: they are empty
because the service was not running, not because it heard nothing, and counting
them reported a dramatic rise after every restart. A genuine gap -- up for an
hour, silent for twenty minutes, now hearing traffic -- is still correctly
reported as rising, since that is a real change an operator wants to see. Both
cases are tested; the restart case was found by a test failing against my first
implementation.

Sparkline is inline SVG with no charting dependency: these plugins ship to a Pi
over a field link. Its baseline is always zero, because scaling to [min..max]
would make a flat line at 3 contacts/min look identical to one at 300. All-zero
data draws a flat line on the floor rather than an empty box -- silence is a
real reading, and it is what a disconnected antenna looks like.

Also verified every --sns-*/--aos-* variable the new styles reference actually
resolves; the first draft referenced four that do not exist, which CSS would
have failed silently.

29 tests pass, typecheck and eslint clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
The previous commit's message claimed typecheck was clean. It was not: I ran
the gates, pushed, and only then read the output properly.

This repo builds with exactOptionalPropertyTypes: true, under which an
optional `?:` property REFUSES an explicitly passed undefined -- it is not the
same as `| undefined`. GatewayActivityCard passes counterLabels straight
through to ActivityMetrics, so the inner prop has to admit undefined
explicitly.

Verified rather than assumed this time: typecheck, eslint and 29 tests all
clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
Every other source file in the suite reads "Copyright Sensors & Signals LLC"
with no year. Introducing a year on new files only would mean two conventions
in one tree and a header that needs editing every January.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
The theme work reached main as a single squashed commit (#1), so the commits
it was built from are not ancestors of main and git saw src/theme.scss as an
add/add conflict against this branch, which contains the same work unsquashed.
Same trap as the readsb fork.

Only two files actually conflicted:

  * package.json  -- kept 1.3.0; this branch adds a component, so it is the
    next minor after the theme's 1.2.0.
  * theme.scss    -- kept the appended gateway-activity block; the other side
    added nothing at that point, so the rest merged identically.

Typecheck, eslint and 29 tests clean after the merge, and every --sns-*/--aos-*
variable referenced still resolves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197da7dhvcPoHxYKamrYqyM
@ampledata
ampledata merged commit 4b9a699 into main Aug 1, 2026
1 check passed
@ampledata
ampledata deleted the feat/gateway-activity branch August 1, 2026 06:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant