Add GatewayActivityCard: is this gateway actually hearing anything? - #2
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.StatusWritermaintains at/run/<app>/status.json(snstac/pytak#107). Only thecolumnsdiffer per gateway, because a tail number and an ACARS label matter for one and an MMSI matters for another.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_ttoDate.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_tis 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:
livequietstalemissingdegradedwrite_errorsA 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_sare 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