Skip to content

feat: live CPU/mem/temperature + per-container docker stats via subscriptions (0.1.40-beta2) - #201

Merged
nofuturekid merged 13 commits into
mainfrom
feat/live-metrics-subscriptions
May 30, 2026
Merged

nofuturekid merged 13 commits into
mainfrom
feat/live-metrics-subscriptions

Conversation

@nofuturekid

Copy link
Copy Markdown
Owner

Rolls the ADR-0042 WebSocket subscription substrate out to four more domains, building directly on the device-verified 0.1.40-beta1 network-throughput pilot (ADR-0043).

Live domains

  • CPU and Memory — the dashboard cards now update live (~1 Hz push) instead of polling every 2 s.
  • CPU temperature — a new Overview card showing the current temperature with the hottest sensor highlighted, escalating to the M3 warning/critical roles when a sensor crosses a threshold.
  • Per-container docker stats — the Docker tab shows live CPU and memory usage per running container, left-joined onto the polled list by id without mutating Container.

Fallback + nullable handling

  • New generic subscriptionOrPoll combinator implements the long-standing ADR-0026 fallback mandate: CPU/Mem/Temp degrade to the existing 2 s GetMetrics poll on sustained live failure (grace SUB_FALLBACK_GRACE_MS = 6 s, 3× the metrics poll) and return to the live stream on recovery, so the cards never go blank. Docker stats have no query path and so have no fallback (same as network).
  • systemMetricsTemperature has a nullable root — a null frame is treated as "no reading yet" via an UNKNOWN sentinel, never an error.

Version bookkeeping

Ships as 0.1.40-beta2, versionCode 108. 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, so beta2 reuses 108. The broken tag is left untouched — this PR also corrects the stranded 0.1.40 version bookkeeping.

Beta-first (ADR-0006); on-device acceptance (ADR-0027 Tier 3) is the promotion-to-stable gate, which stays maintainer-only.

References

🤖 Generated with Claude Code

nofuturekid and others added 13 commits May 30, 2026 10:05
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>
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>
…etrics

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
…ntainerLiveStats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
…ame 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>
…eam, 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>
… 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>
…ows (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>
…er 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>
@nofuturekid
nofuturekid merged commit f3ed17b into main May 30, 2026
8 checks passed
@nofuturekid
nofuturekid deleted the feat/live-metrics-subscriptions branch May 30, 2026 10:08
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