Date: 2026-07-19
Proposed
The dashboard shows live/idle state and a per-peer last-seen timestamp, but no history. There is no way to answer "was this peer actually connected during its scheduled window" or "which peers haven't shown up in weeks" without history over time.
ActivityPoller (peer/ActivityPoller.java) already samples wg show <iface> dump every 30 s and writes lastSeenAt / lastSeenEndpoint / cumulative rx-tx bytes back onto the Peer row — one row per peer, overwritten every tick, no history retained by design (deliberately minimal in v1, per the class-level comment). Issue #32 asks for a peers-×-days activity matrix, colored like a GitHub contribution graph. That needs some form of history; the question this ADR answers is what shape that history takes in the database.
The two poles are: keep every sample forever (maximum fidelity, unbounded growth), or throw away the timestamp entirely and keep only a coarse daily bucket (bounded growth, coarse fidelity). The heatmap's actual consumption pattern — a colored cell per peer per day — never needs sub-day resolution to render, which is the lever this decision turns on.
Aggregate into one row per peer per day — peer_daily_activity(peer_id, day, sample_hits) — upserted incrementally by the existing ActivityPoller tick (increment sample_hits for the current day when a peer reports a non-null handshake, same "seen" rule already used for lastSeenAt). No raw per-sample table. Retention is a configurable settings value (default 180 days, following the ADR-0008 runtime-settings pattern), enforced by a daily cleanup job.
peer_count × retention_days — flat, not growing with poll frequency.ActivityPoller.poll(), no new scheduled job, no new adapter. A raw series would need the same upsert logic plus a separate retention/downsampling job to keep the table bounded, which is strictly more code for a benefit (sub-day resolution) the UI doesn't use.GROUP BY -free read: peers × days already matches the table's own shape, so the query is a plain range-scan on (peer_id, day), no aggregation step at render time. A raw series would need a COUNT/bucket-by-day query on every render (or a materialized rollup — which is this same aggregate table, built the hard way).Baseline: A — daily aggregate upserted by the existing poller (the decision).
| Criterion (weight) | A: daily aggregate (baseline) | B: raw per-sample time series | C: embedded TSDB / RRD (rrdtool-style) | D: no persistence, render from live wg show only |
|---|---|---|---|---|
| Data size / bounded growth (5) | 0 | −1 | 0 | +1 |
| Implementation effort (4) | 0 | −1 | −1 | +1 |
| Matches the heatmap's actual render need (4) | 0 | 0 | 0 | −1 |
| No new component/dependency (3) | 0 | 0 | −1 | 0 |
| Query simplicity at render time (2) | 0 | −1 | 0 | 0 |
| Room for a future higher-resolution feature (1) | 0 | +1 | +1 | −1 |
| Weighted total | 0 | −10 | −7 | +2* |
* D's positive score is misleading: it scores well because it has almost no cost, but it cannot satisfy the requirement at all (see below) — it is disqualified on capability, not weighed down on cost.
wg show only, no persistence) cannot answer the question at all — wg show has no memory past the current handshake state, so "was this peer connected yesterday" is unanswerable without some stored history. Included in the matrix for completeness; disqualified rather than genuinely competing.A wins by matching the actual consumption shape (one cell per peer-day) with the cheapest implementation and the tightest storage bound, at the cost of resolution nothing in scope currently needs.
Positive
peers × retention_days), independent of poll-interval tuning.Risks created
sample_hits is an approximation, not exact connected-time. It counts poll ticks with a non-null handshake, not measured session duration; a peer that connects for 10 minutes once looks similar to one that connects for 10 minutes three times, within the same day's bucket. Acceptable for a heatmap (relative intensity, not a billing metric); would need to be named explicitly if ever surfaced as a precise number.Accepted trade-offs
sample_hits is a tick-count proxy for connected time, not a duration measurement — accepted for the same reason.