mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 03:03:38 +00:00
fix: Areas tab Position-Fix Coverage Gaps table was overwhelming -- sort worst-first, collapse to top 10
dborup: the full 36-row dump buried the handful of areas with an actual gap behind dozens already fully GPS-mapped (0% estimated). Sort by % estimated descending and collapse to top 10 with a "Show all" toggle, matching the existing pattern from the Wardriving tab's Top Senders / Coverage by Observer sections. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1e01d04c8a
commit
b096f73662
+53
-15
@@ -6586,20 +6586,42 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
'</tr></thead><tbody>' + bridgeRows + '</tbody></table>' :
|
||||
'<p class="text-muted" style="font-size:0.85em">No packet-derived neighbor edges cross between two different areas yet.</p>';
|
||||
|
||||
var gapRows = positionGaps.map(function (g) {
|
||||
var total = g.realFix + g.approximated;
|
||||
return '<tr>' +
|
||||
'<td>' + esc(g.label) + '</td>' +
|
||||
'<td style="text-align:right">' + g.realFix.toLocaleString() + '</td>' +
|
||||
'<td style="text-align:right">' + g.approximated.toLocaleString() + '</td>' +
|
||||
'<td style="text-align:right">' + pct(g.approximated, total) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
var gapHtml = positionGaps.length ?
|
||||
'<table class="analytics-table"><thead><tr>' +
|
||||
// Sorted worst-coverage-first (highest % estimated) rather than the
|
||||
// API's realFix-desc order — most areas have 0% estimated (fully
|
||||
// GPS-mapped already), so a plain dump buried the handful of areas
|
||||
// that actually have a gap worth looking at. Collapses to the top
|
||||
// 10 by default, same "Show all N" pattern as the Wardriving tab's
|
||||
// Top Senders/Coverage by Observer sections.
|
||||
var AREAS_TOP_N = 10;
|
||||
var sortedGaps = positionGaps.slice().sort(function (a, b) {
|
||||
var totalA = a.realFix + a.approximated, totalB = b.realFix + b.approximated;
|
||||
var pctA = totalA ? a.approximated / totalA : 0;
|
||||
var pctB = totalB ? b.approximated / totalB : 0;
|
||||
return pctB - pctA;
|
||||
});
|
||||
function gapsToggleHtml(expanded) {
|
||||
if (sortedGaps.length <= AREAS_TOP_N) return '';
|
||||
var label = expanded ? 'Show fewer' : 'Show all ' + sortedGaps.length.toLocaleString() + ' areas';
|
||||
return '<div style="margin-top:6px;text-align:right"><button type="button" data-areas-gaps-toggle class="btn-link" style="font-size:12px;cursor:pointer;background:none;border:none;color:var(--link-color);padding:0">' + label + '</button></div>';
|
||||
}
|
||||
function gapsTableHtml(expanded) {
|
||||
if (!sortedGaps.length) {
|
||||
return '<p class="text-muted" style="font-size:0.85em">No unpositioned node could be placed in any area, even approximately.</p>';
|
||||
}
|
||||
var shown = expanded ? sortedGaps : sortedGaps.slice(0, AREAS_TOP_N);
|
||||
var rows = shown.map(function (g) {
|
||||
var total = g.realFix + g.approximated;
|
||||
return '<tr>' +
|
||||
'<td>' + esc(g.label) + '</td>' +
|
||||
'<td style="text-align:right">' + g.realFix.toLocaleString() + '</td>' +
|
||||
'<td style="text-align:right">' + g.approximated.toLocaleString() + '</td>' +
|
||||
'<td style="text-align:right">' + pct(g.approximated, total) + '</td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
return '<table class="analytics-table"><thead><tr>' +
|
||||
'<th scope="col">Area</th><th scope="col">Real GPS Fix</th><th scope="col">Estimated (via Neighbors)</th><th scope="col">% Estimated</th>' +
|
||||
'</tr></thead><tbody>' + gapRows + '</tbody></table>' :
|
||||
'<p class="text-muted" style="font-size:0.85em">No unpositioned node could be placed in any area, even approximately.</p>';
|
||||
'</tr></thead><tbody>' + rows + '</tbody></table>' + gapsToggleHtml(expanded);
|
||||
}
|
||||
var unpositionedNote = '<p class="text-muted" style="margin:8px 0 0;font-size:0.85em">' +
|
||||
(d.unpositionedTotal || 0).toLocaleString() + ' node' + (d.unpositionedTotal === 1 ? '' : 's') + ' network-wide have no real GPS fix' +
|
||||
(d.unpositionedNoNeighborFix ? ', of which ' + d.unpositionedNoNeighborFix.toLocaleString() + ' also have no positioned neighbor to estimate from — those can\'t be placed anywhere, not even approximately, so they\'re absent from the table above entirely.' : '.') +
|
||||
@@ -6618,10 +6640,26 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _stopForeignTraf
|
||||
'</div>' +
|
||||
'<div class="analytics-card" style="margin-top:16px">' +
|
||||
'<h3>Position-Fix Coverage Gaps by Area</h3>' +
|
||||
'<p class="text-muted" style="margin:0 0 8px;font-size:0.85em">How many of each area\'s nodes have an actual reported GPS position vs. how many were only placeable via a neighbor-based estimate (same technique used for View Path\'s approximate markers).</p>' +
|
||||
gapHtml +
|
||||
'<p class="text-muted" style="margin:0 0 8px;font-size:0.85em">How many of each area\'s nodes have an actual reported GPS position vs. how many were only placeable via a neighbor-based estimate (same technique used for View Path\'s approximate markers). Sorted worst-coverage-first.</p>' +
|
||||
'<div id="areasPositionGaps">' + gapsTableHtml(false) + '</div>' +
|
||||
unpositionedNote +
|
||||
'</div>';
|
||||
|
||||
var gapsExpanded = false;
|
||||
var gapsContainer = document.getElementById('areasPositionGaps');
|
||||
function attachGapsToggle() {
|
||||
var container = document.getElementById('areasPositionGaps');
|
||||
if (!container) return;
|
||||
var btn = container.querySelector('[data-areas-gaps-toggle]');
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
gapsExpanded = !gapsExpanded;
|
||||
container.innerHTML = gapsTableHtml(gapsExpanded);
|
||||
attachGapsToggle();
|
||||
});
|
||||
}
|
||||
}
|
||||
if (gapsContainer) attachGapsToggle();
|
||||
} catch (e) {
|
||||
el.innerHTML = '<div class="text-center" style="color:var(--status-red);padding:20px">Failed to load area analytics: ' + esc(String(e)) + '</div>';
|
||||
}
|
||||
|
||||
@@ -164,6 +164,46 @@ function makeApiStub(resp) {
|
||||
assert.ok(el.innerHTML.includes('20.0%'), 'ODE row should show 20.0% estimated');
|
||||
});
|
||||
|
||||
await testAsync('Position-Fix Coverage Gaps sorts worst-coverage-first, not the API\'s realFix-desc order', async () => {
|
||||
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse({
|
||||
positionGaps: [
|
||||
{ areaKey: 'FULL', label: 'FullyMapped', realFix: 100, approximated: 0 }, // 0% estimated, biggest realFix
|
||||
{ areaKey: 'WORST', label: 'WorstCoverage', realFix: 2, approximated: 8 }, // 80% estimated, smallest realFix
|
||||
{ areaKey: 'MID', label: 'MidCoverage', realFix: 10, approximated: 5 }, // ~33% estimated
|
||||
],
|
||||
})));
|
||||
const el = fakeEl();
|
||||
await ctx.window._analyticsRenderAreasTab(el);
|
||||
const startIdx = el.innerHTML.indexOf('id="areasPositionGaps"');
|
||||
const section = el.innerHTML.slice(startIdx);
|
||||
const idxWorst = section.indexOf('WorstCoverage');
|
||||
const idxMid = section.indexOf('MidCoverage');
|
||||
const idxFull = section.indexOf('FullyMapped');
|
||||
assert.ok(idxWorst > -1 && idxMid > -1 && idxFull > -1, 'all three areas should render');
|
||||
assert.ok(idxWorst < idxMid && idxMid < idxFull, 'rows should be ordered worst-% -> best-%, not by realFix');
|
||||
});
|
||||
|
||||
await testAsync('Position-Fix Coverage Gaps collapses to top 10 with a "Show all" toggle', async () => {
|
||||
const manyGaps = [];
|
||||
for (let i = 0; i < 15; i++) manyGaps.push({ areaKey: 'A' + i, label: 'Area' + i, realFix: 10, approximated: i });
|
||||
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse({ positionGaps: manyGaps })));
|
||||
const el = fakeEl();
|
||||
await ctx.window._analyticsRenderAreasTab(el);
|
||||
const startIdx = el.innerHTML.indexOf('id="areasPositionGaps"');
|
||||
const section = el.innerHTML.slice(startIdx, el.innerHTML.indexOf('nodes network-wide have no real GPS fix'));
|
||||
const tbody = section.slice(section.indexOf('<tbody>'), section.indexOf('</tbody>'));
|
||||
const rowCount = (tbody.match(/<tr>/g) || []).length;
|
||||
assert.strictEqual(rowCount, 10, 'only 10 rows should render by default');
|
||||
assert.ok(section.includes('Show all 15 areas'), 'a "Show all" toggle should appear when there are more than 10 areas');
|
||||
});
|
||||
|
||||
await testAsync('no "Show all" toggle on the Position-Fix Coverage Gaps table when there are 10 or fewer areas', async () => {
|
||||
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse()));
|
||||
const el = fakeEl();
|
||||
await ctx.window._analyticsRenderAreasTab(el);
|
||||
assert.ok(!el.innerHTML.includes('Show all'), 'no toggle should render with only 1 area in positionGaps');
|
||||
});
|
||||
|
||||
await testAsync('renders the unpositioned-nodes summary note including the no-neighbor-fix subset', async () => {
|
||||
const ctx = makeAnalyticsSandbox(makeApiStub(makeAreasResponse()));
|
||||
const el = fakeEl();
|
||||
|
||||
Reference in New Issue
Block a user