diff --git a/public/analytics.js b/public/analytics.js index cbf36502..0b9160a9 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -5558,17 +5558,29 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf ''; } - function sendersHtml(senders, totalMessages) { + var TOP_N_LIMIT = 10; + + // Shared "Show all N / Show fewer" toggle trigger for any collapsible + // table below. The click handler itself is wired generically by + // wireExpandToggle, keyed on the data-wd-toggle attribute. + function topNToggleHtml(total, expanded, noun) { + if (total <= TOP_N_LIMIT) return ''; + var label = expanded ? 'Show fewer' : 'Show all ' + total.toLocaleString() + ' ' + noun; + return '
'; + } + + function sendersHtml(senders, totalMessages, expanded) { if (!senders || senders.length === 0) { return 'No wardriving messages in this window.
'; } - var rows = senders.map(function(s) { + var shown = expanded ? senders : senders.slice(0, TOP_N_LIMIT); + var rows = shown.map(function(s) { return '| Sender | Messages | % of Total |
|---|
No observer has heard wardriving traffic in this window.
'; } + // % of Observations stays relative to ALL observers, not just the + // shown top-N slice, so the numbers stay meaningful when collapsed. var totalObsCount = observers.reduce(function(sum, o) { return sum + o.observationCount; }, 0); - var rows = observers.map(function(o) { + var shown = expanded ? observers : observers.slice(0, TOP_N_LIMIT); + var rows = shown.map(function(o) { var loc = (o.lat != null && o.lon != null) ? (o.lat.toFixed(2) + ', ' + o.lon.toFixed(2)) : '—'; return '| Observer | Region | Lat, Lon | Observations | % of Observations | Distinct Messages |
|---|
Average SNR and RSSI across every observation of wardriving traffic in each time bucket — a rough proxy for link quality, not tied to any one observer.
' + + 'Who\'s actively wardriving in this window, by message count.
' + - 'Each sender\'s messages grouped into distinct runs — a gap of more than 15 minutes starts a new session.
' + 'Which observer stations actually heard wardriving traffic — observers sit at fixed, known locations, so this is the reliable half of "how far did it reach."
' + - 'Average SNR and RSSI across every observation of wardriving traffic in each time bucket — a rough proxy for link quality, not tied to any one observer.
' + - 'Senders whose client appended their own position after the standard token — a deliberate choice by that sender to share their location, not something inferred from the anonymous token itself.
' + ''; @@ -5932,6 +5968,10 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf body; attachWindowButtons(); attachSenderDrilldown(); + if (d) { + wireExpandToggle('wardrivingSenders', function(exp) { return sendersHtml(d.topSenders, d.totalMessages, exp); }); + wireExpandToggle('wardrivingObservers', function(exp) { return observersHtml(d.observers, exp); }); + } } await load(selectedWindow); diff --git a/test-analytics-wardriving-tab.js b/test-analytics-wardriving-tab.js index f8499820..b8717089 100644 --- a/test-analytics-wardriving-tab.js +++ b/test-analytics-wardriving-tab.js @@ -221,6 +221,36 @@ function makeApiStub(wardrivingResp, resolveHopsResp) { assert.ok(el.innerHTML.includes('66.7%'), 'Alice row should show 66.7% of total messages'); }); + await testAsync('Signal Quality Trends renders above Top Senders (moved up per layout request)', async () => { + const ctx = makeAnalyticsSandbox(makeApiStub(makeWardrivingResponse())); + const el = fakeEl(); + await ctx.window._analyticsRenderWardrivingTab(el); + const idxSignal = el.innerHTML.indexOf('Signal Quality Trends'); + const idxSenders = el.innerHTML.indexOf('>Top Senders<'); + assert.ok(idxSignal > -1 && idxSenders > -1, 'both sections should render'); + assert.ok(idxSignal < idxSenders, 'Signal Quality Trends should render before Top Senders'); + }); + + await testAsync('Top Senders and Coverage by Observer collapse to top 10 with a "Show all" toggle', async () => { + const manySenders = []; + for (let i = 0; i < 15; i++) manySenders.push({ sender: 'Sender' + i, count: 15 - i }); + const manyObservers = []; + for (let i = 0; i < 12; i++) manyObservers.push({ observerId: String(i), observerName: 'Observer' + i, observationCount: 12 - i, messageCount: 1 }); + const ctx = makeAnalyticsSandbox(makeApiStub(makeWardrivingResponse({ topSenders: manySenders, observers: manyObservers }))); + const el = fakeEl(); + await ctx.window._analyticsRenderWardrivingTab(el); + + const sendersSection = el.innerHTML.slice(el.innerHTML.indexOf('id="wardrivingSenders"'), el.innerHTML.indexOf('id="wardrivingSessions"')); + assert.ok(sendersSection.includes('Sender9'), 'the 10th sender (index 9) should be shown'); + assert.ok(!sendersSection.includes('Sender10'), 'the 11th sender should be collapsed away by default'); + assert.ok(sendersSection.includes('Show all 15 senders'), 'a "Show all" toggle should appear when there are more than 10 senders'); + + const observersSection = el.innerHTML.slice(el.innerHTML.indexOf('id="wardrivingObservers"'), el.innerHTML.indexOf('id="wardrivingGPSShares"')); + assert.ok(observersSection.includes('Observer9'), 'the 10th observer (index 9) should be shown'); + assert.ok(!observersSection.includes('Observer10'), 'the 11th observer should be collapsed away by default'); + assert.ok(observersSection.includes('Show all 12 observers'), 'a "Show all" toggle should appear when there are more than 10 observers'); + }); + await testAsync('Top Senders names are clickable drill-down triggers (whole-window, no since/until)', async () => { const ctx = makeAnalyticsSandbox(makeApiStub(makeWardrivingResponse())); const el = fakeEl();