Skip to content

feat: live network throughput card — subscription test balloon (0.1.40-beta1) - #200

Merged
nofuturekid merged 9 commits into
mainfrom
feat/live-network-graph
May 29, 2026
Merged

nofuturekid merged 9 commits into
mainfrom
feat/live-network-graph

Conversation

@nofuturekid

Copy link
Copy Markdown
Owner

What shipped

Live network throughput (rx/tx bytes per second) is now displayed in the Overview Network card via a GraphQL WebSocket subscription (systemMetricsNetwork), replacing the hardcoded-zero scaffold that shipped in earlier versions. The card shows real ~1 Hz rates for the primary NIC with no UI changes — the sparkline and StatCard were already wired, only the data source was missing.

Why this matters / ADR-0042

This is a scoped test balloon re-validating the WebSocket subscription transport per ADR-0042. The prior graphql-transport-ws pilot (ADR-0026) was provisionally reverted pending two triggers:

  1. The API now exposes network throughput — systemMetricsNetwork subscription delivering rxBytesPerSec/txBytesPerSec per interface at ~1 Hz. Live-verified 2026-05-29 against the real server.
  2. Direct server validation is now possible — collapsing the multi-beta relay loop that caused the ADR-0026 revert.

Both triggers fired. This beta ships the narrowest possible balloon: network throughput only. Metrics (CPU/Mem/Temp), docker stats, and array subscription remain on their existing poll paths.

Key implementation points:

  • ApolloClientFactory.buildWs(...) reuses the ADR-0041 self-signed TLS pin path — no new trust surface.
  • Generic subscriptionStream<T> substrate mirrors domainStream (ADR-0017); networkThroughputStream is its only consumer in this beta.
  • MainViewModel.networkThroughputState gated with overviewOnly() — WS socket lives only while Overview is the foreground tab.
  • No poll fallback (there is no query path for byte rates; "unavailable" on WS failure is accurate).
  • No new dependency — Apollo 5.0.0's WebSocketNetworkTransport is already on the classpath.

Spec / plan

Beta-first

This PR targets 0.1.40-beta1 (versionCode 108). Promotion to stable requires the maintainer's on-device acceptance gate (ADR-0027 Tier 3): the Network card must show live rates on real hardware. If the card shows "unavailable" after accepting the beta, stop — do not promote. The WS transport or TLS wiring needs diagnosis first.

Network throughput data (systemMetricsNetwork) is newly available upstream — this is not a reimplementation of a prior feature.


🤖 Generated with Claude Code

nofuturekid and others added 9 commits May 29, 2026 23:51
Brainstormed design for feeding live systemMetricsNetwork throughput into
the existing Overview Network card via a GraphQL WS subscription. Scoped
test balloon re-validating the subscription transport (ADR-0026 revisit;
network throughput is newly available upstream). Ships beta-first.

Also gitignore .superpowers/ brainstorm artifacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task-by-task TDD plan (pure seams for unit-testable logic given no local
toolchain; CI as the build/test authority). Co-Authored-By trailer below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@nofuturekid
nofuturekid merged commit 2fc3194 into main May 29, 2026
13 checks passed
nofuturekid added a commit that referenced this pull request May 30, 2026
…riptions (0.1.40-beta2) (#201)

* docs(adr-0042): adopt WS subscription transport for network throughput pilot

* release: 0.1.40-beta1 — live network throughput card (versionCode 108)

* docs(spec): 0.1.40 live metrics & docker stats via subscriptions

Approved brainstorm design for the multi-domain subscription rollout
(CPU+Mem swap w/ poll-fallback, new Temperature card, Docker stats
stream-join). Builds on the device-verified ADR-0042 network pilot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(plan): 0.1.40 live metrics & docker stats implementation plan

8 dependency-ordered TDD tasks (schema/ops -> subscriptionOrPoll substrate
-> models -> mappers -> repository streams -> viewmodel -> UI -> release tail).
Generated via survey/synthesize/critic/revise workflow; self-reviewed for
spec coverage, type consistency, and the nullable systemMetricsTemperature trap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(graphql): vendor cpu/mem/temp/docker subscriptions + extend GetMetrics

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(repo): subscriptionOrPoll fallback combinator + grace const

New substrate capability over the ADR-0042 bare subscriptionStream: when a
live sub Errors past SUB_FALLBACK_GRACE_MS (or never delivers a first frame),
degrade to the HTTP poll; return to the sub on recovery. NoServer is forwarded
immediately. Poll is collected only while in fallback (no double-collect).
ADR-0026 mandate: no blank card.

Tests pin the degrade/recover/cold-start/no-server transitions through the
real production combinator on the virtual scheduler: transient error within
grace keeps the last frame; sustained error or silent cold-start past grace
uses the poll; recovery returns to the sub (the recovered frame emitted
directly) and stops the poll (no lingering double-collect); NoServer is
forwarded immediately; the grace const encodes its 3x-poll-interval intent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(model): Temperature (+UNKNOWN sentinel, pure toTemperature) + ContainerLiveStats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(model): null-safe toTemperature smart-cast (sample!!.let)

The `sample?.average ?: return` Elvis guard does NOT smart-cast `sample`
to non-null (the null check is on the property, not the receiver), so the
subsequent `sample.unit` / `sample.hottestName` accesses would not compile.
Assert non-null via `sample!!.let { it -> ... }` — control flow proves it,
since a null `sample` triggers the Elvis return. Also correct the same
defect in the plan spec so future tasks reference a compilable pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(mapper): combineLiveMetrics (poll+live) + cpu/mem/temp/docker frame mappers

Shared combineLiveMetrics holds the byte->GiB + null-buffcache math once;
toLiveMetrics now delegates to it so the GetMetrics poll and the live
cpu+mem subscription combine are provably byte-identical (Rule 9). Adds the
thin Apollo frame extractors (cpuPercentTotal, toMemoryTriple), the shared
TemperatureSummary->Temperature mapper for both the subscription and the
extended GetMetrics poll, the G->domain TemperatureUnit mapping, and the
dockerContainerStats frame->(id, ContainerLiveStats) mapper (netIO/blockIO
dropped, out of scope). Tests pin the GiB divisor, null-buffcache->0, and
the null-root->UNKNOWN / unit-carrying temperature behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(repo): live cpu+mem combine (subscriptionOrPoll), temperatureStream, dockerStatsStream

Wire the three live streams for Task 5:
- liveMetricsStream() combines systemMetricsCpu + systemMetricsMemory via the
  companion combineMetricsStates fold (Content only when both legs Content with
  matching server URL); metricsStream() now wraps it in subscriptionOrPoll with
  the existing GetMetrics poll as fallback, keeping its exact signature.
- temperatureStream() = subscriptionOrPoll(tempSub, GetMetrics temp poll).
- dockerStatsStream() scans frames into a growing immutable map (no fallback).
- Companion test seams metricsCombineForTest (drives the real combineMetricsStates
  over DomainState legs) and dockerStatsStreamForTest/accumulateStats.
- MetricsCombineTest + DockerStatsAccumulationTest pin the precedence fold and
  accumulate/update-by-id semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(vm): gated temperatureState + dockerStatsState + dockerLiveStats overlay + joinContainerStats

Adds the Overview-gated temperatureState and the dockerGate-gated
dockerStatsState (so the stats socket and the docker poll come and go
together), plus the derived dockerLiveStats StateFlow that unwraps the
overlay to a plain Map<String, ContainerLiveStats> for the Docker UI
(empty unless Content). dockerState stays byte-identical — the overlay is
a SEPARATE flow (spec §C / Rule 2-3 surgical).

joinContainerStats is the single LEFT-join overlay seam (companion):
every polled container is kept in its original order; a container with no
stats frame yet gets null stats (no zero noise), never dropped. Pinned by
ContainerStatsJoinTest (keeps-without-frame, preserves polled order,
attaches by id). It is called by DockerContent over dockerLiveStats
(Task 7), so it is load-bearing, not dead code.

CPU/Mem need no ViewModel change — the transport swap is invisible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui): Overview Temperature StatCard + live per-container Docker rows (joinContainerStats)

Thread temperatureState and dockerLiveStats from MainViewModel through
MainScreen into OverviewTab and DockerTab.

OverviewTab gains a Temperature StatCard with M3 accent escalation
(t.danger for critical, t.warn for warning, else t.accent), a rolling
tempSeries sparkline fed only when a reading is available, and a
unitSymbol helper carrying the server-reported unit. Unavailable reads
show an em-dash and 'live unavailable'.

DockerTab overlays live per-container cpu%/mem onto the polled list via
the single MainViewModel.joinContainerStats seam, threading the stats
through all four ContainerRow call sites (List + Grouped); the Grid
layout stays stats-free. Live numbers render only for Running
containers (guards stale entries after a stop). dockerState's poll
transport stays byte-for-byte unchanged (separate-map design, spec C).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* release: 0.1.40-beta2 — live CPU/mem/temperature + per-container docker stats (versionCode 108)

Rolls the ADR-0042 WebSocket subscription substrate out to four more
domains — CPU, memory, temperature, and per-container docker stats
(ADR-0043). CPU/Mem/Temp degrade to the existing 2 s GetMetrics poll via
the new subscriptionOrPoll fallback combinator (ADR-0026 mandate, finally
implemented); docker stats and the temperature card are new surfaces with
no poll fallback. ADR-0026 status updated to note the supersede.

versionCode stays 108: 0.1.39 stable = 107; the broken v0.1.40-beta1 tag
was built at a commit reading 107 (the #200 merge omitted the release
commit) and never validly shipped as 108; this branch carries 108 but it
has never been merged/tagged/distributed. beta2 reuses 108 (>107,
never-shipped). The broken v0.1.40-beta1 tag is left untouched in history.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@nofuturekid
nofuturekid deleted the feat/live-network-graph branch August 29, 2026 14:55
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