Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 58 additions & 4 deletions docs/regions-observed.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,58 @@ and scopes. Every truncation is exposed. Its retained-history floor and loading
state are captured with the relay evidence, rather than taken from a newer
store state when a request arrives.

## Region activity health and traffic trends

**Health & trends** is a separate view, leaving Latest's rolling-window map
and Timeline's playback unchanged. It uses the same bounded history snapshot
as Timeline, completed UTC hours, interval selection and scope filter. The
view, selected hour, period, scope and comparison checkbox are shareable in
the URL, for example `#/regions?view=health&window=24h&scope=%23example`.
Switching between Health and Timeline preserves the selected hour. Health
does not play automatically and does not render the map.

The summary shows distinct relay nodes observed in the selected interval,
relay appearances and the most recent observed relay timestamp **within that
scope and interval**. Nodes without usable positions are included. A node
seen in several hours or scopes remains one node in the interval total;
hourly node counts must not be added to obtain that total. A missing last
relay timestamp means not observed in the interval, not an outage.

The traffic chart uses disjoint one-hour bins: one, 24 or 168 bins for the
1h, 24h and 7d periods. These are UTC hours, not local days or extrapolated
daily totals. The exact-values table provides the same timestamps and counts
as the chart. A zero is shown only for a fully retained, uncapped hour.
Partial, unknown, loading or capped evidence produces a gap, not a fabricated
zero; retained observations can still be disclosed explicitly as incomplete
evidence. The mean appearances/hour is available only when the entire
selected interval is complete and uncapped. Partial counts are never divided
by the full requested duration.

The optional comparison reuses Timeline's immediately preceding equal
interval and its completeness checks. A complete selected period may have
an hourly mean even when the reference is incomplete, but changes, percentages
and new/not-observed classifications still require **both** periods to be
complete and uncapped. A zero reference has no defined percentage increase.
The expected set for "not observed" is the preceding interval, not all nodes
ever present in the catalogue.

Health describes observed activity and evidence availability, not service
availability or radio quality. No healthy/degraded/offline score or alarm
threshold is inferred. More traffic can reflect spam, changed routing or
observer reception; it does not establish improved network health. The
snapshot timestamp and retained-history limitations remain visible.

The selected scope's hourly activity is indexed once per payload or scope
change into bounded arrays spanning the existing 504-hour horizon. Selecting
an interval reads at most 168 hourly bins and reuses the existing interval
comparison; it does not rescan raw packets, fetch each node or request a new
payload while scrubbing. No new endpoint, persistence, retention setting,
dependency or operator configuration is introduced. Any future health
thresholds must be evidence-based and exposed through the customizer.

## Following milestones

1. **Region health:** add active relay counts, last relay and traffic trends
over comparable periods. Quiet relays should be labelled as not observed in
the period; lack of reception alone does not establish that a node is offline.
2. **Connections between regions:** add observed paths with direction and
1. **Connections between regions:** add observed paths with direction and
distinct-transmission counts. Preserve scope transitions, distinguish
ambiguous hop identities and avoid interpreting shared scope membership as
proof of a direct radio link. Route changes belong to this path-evidence
Expand All @@ -113,3 +159,11 @@ stable map bounds, keyboard operation, no per-frame requests, and playback
cleanup on manual changes, hidden documents and navigation. Performance checks
cover background snapshot construction, bounded retained history and warm bulk
responses without acquiring the packet-store lock.

Health tests additionally cover cross-scope and cross-hour node deduplication,
scoped interval-specific last relay timestamps, complete-zero versus missing
bins, all coverage/truncation states, hourly means, comparison parity, fixed
UTC boundaries and no sparse-cell rescans after activity indexing. Browser
checks include Health deep links, local filter/time changes, Health/Timeline
transitions, missing-data charts, exact values, themes, teardown and narrow
mobile/desktop layouts.
51 changes: 50 additions & 1 deletion public/regions-history.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,56 @@
percentChange: comparable && previousAppearances > 0 ? (currentAppearances - previousAppearances) / previousAppearances * 100 : null };
}

const api = { indexHistory, frameIndex, frameTime, interval, selectFrame };
// Build one selected-scope activity index per payload/scope change. Dense
// arrays are bounded to the 504-hour snapshot, never one array per scope.
// A node can carry several scopes in an hour but remains one observed relay.
function indexActivity(history, scope) {
const appearances = new Float64Array(MAX_BUCKETS), nodes = new Uint16Array(MAX_BUCKETS);
const times = new Array(MAX_BUCKETS).fill(''), seen = new Uint16Array(MAX_BUCKETS);
let generation = 0;
for (const node of history.nodes) {
generation++;
for (const entry of node.scopes) {
if (scope && entry.name !== scope) continue;
for (let i = 0; i < entry.indices.length; i++) {
const index = entry.indices[i];
appearances[index] += entry.prefix[i + 1] - entry.prefix[i];
if (seen[index] !== generation) { nodes[index]++; seen[index] = generation; }
if (entry.times[i] > times[index]) times[index] = entry.times[i];
}
}
}
return { history, scope: scope || '', appearances, nodes, times };
}

// Playback reads only bounded hourly arrays. Partial/unknown history has
// observed evidence, but no chart value or whole-window activity rate.
function selectActivity(activity, history, frame) {
if (!activity || activity.history !== history) throw new Error('Activity index does not match history');
const start = millis(frame.interval.start), end = millis(frame.interval.end);
const hours = (end - start) / HOUR;
if (![1, 24, 168].includes(hours) || start % HOUR || end % HOUR || start < history.start || end > history.end) {
throw new Error('Invalid activity interval');
}
const first = (start - history.start) / HOUR;
const bins = [];
let lastRelayed = '', total = 0;
for (let i = 0; i < hours; i++) {
const index = first + i, binStart = start + i * HOUR;
const coverage = coverageState(history, binStart, binStart + HOUR);
const observedAppearances = activity.appearances[index], observedNodes = activity.nodes[index];
const complete = coverage === 'complete' && !history.limited;
bins.push({ start: new Date(binStart).toISOString(), end: new Date(binStart + HOUR).toISOString(), coverage,
observedAppearances, observedNodes, appearances: complete ? observedAppearances : null,
nodes: complete ? observedNodes : null });
total += observedAppearances;
if (activity.times[index] > lastRelayed) lastRelayed = activity.times[index];
}
return { bins, lastRelayed,
ratePerHour: frame.currentCoverage === 'complete' && !history.limited ? total / hours : null };
}

const api = { indexHistory, frameIndex, frameTime, interval, selectFrame, indexActivity, selectActivity };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
root.RegionsHistory = api;
})(typeof window === 'undefined' ? globalThis : window);
Loading
Loading