diff --git a/public/observers.js b/public/observers.js index 43b72d1b..d0dfdda4 100644 --- a/public/observers.js +++ b/public/observers.js @@ -320,6 +320,16 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo return timeAgo(o.last_packet_at); } + // #1789 — Firmware strings can be long, e.g. + // "v1.16.0-07a3ca9 Build: 2025-..."; truncate the "Build:" suffix for the + // display string, keeping the full value in the cell's title= attr. + function truncateBuildSuffix(s) { + if (!s) return ''; + const idx = s.indexOf('Build:'); + if (idx < 0) return s; + return s.slice(0, idx).replace(/[\s,;-]+$/, ''); + } + function uptimeStr(firstSeen) { if (!firstSeen) return '—'; const ms = Date.now() - new Date(firstSeen).getTime(); @@ -374,7 +384,7 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo Observer status and statistics StatusNameRegionLast StatusLast Packet - Packet HealthTotal PacketsPackets/HourClock OffsetUptime + Packet HealthTotal PacketsPackets/HourClock OffsetUptimeFirmwareClient Select for compare ${filtered.map(o => { @@ -416,6 +426,8 @@ window.preserveCompareSelection = function preserveCompareSelection(prevIds, tbo return renderSkewBadge(sev, sk.offsetSec) + ' (' + sk.samples + ')'; })()} ${uptimeStr(o.first_seen)} + ${o.firmware ? escapeHtml(truncateBuildSuffix(String(o.firmware))) : ''} + ${o.client_version ? escapeHtml(String(o.client_version)) : ''} { + const thead = extractBlock(src, //, /<\/tr><\/thead>/); + assert.ok( + /]*data-priority="4"[^>]*data-sort-key="firmware"[^>]*>\s*Firmware\s*<\/th>/.test(thead) || + /]*data-sort-key="firmware"[^>]*data-priority="4"[^>]*>\s*Firmware\s*<\/th>/.test(thead), + 'Firmware with data-priority="4" and data-sort-key="firmware" not found in thead' + ); +}); + +test('thead contains Client header with data-priority=4 and data-sort-key=client_version', () => { + const thead = extractBlock(src, //, /<\/tr><\/thead>/); + assert.ok( + /]*data-priority="4"[^>]*data-sort-key="client_version"[^>]*>\s*Client\s*<\/th>/.test(thead) || + /]*data-sort-key="client_version"[^>]*data-priority="4"[^>]*>\s*Client\s*<\/th>/.test(thead), + 'Client with data-priority="4" and data-sort-key="client_version" not found in thead' + ); +}); + +test('tbody row template contains a firmware with data-value and class="mono"', () => { + const tbodyStart = src.indexOf(''); + assert.ok(tbodyStart > 0, ' not found in observers.js'); + const after = src.slice(tbodyStart); + const trOpen = after.search(/`', trOpen); + const row = after.slice(trOpen, rowEnd); + assert.ok( + /]*class="mono"[^>]*data-value="\$\{[^}]*o\.firmware[^}]*\}"|]*data-value="\$\{[^}]*o\.firmware[^}]*\}"[^>]*class="mono"/.test(row), + 'firmware with data-value reading o.firmware and class="mono" not found in row template' + ); +}); + +test('tbody row template contains a client_version with data-value and class="mono"', () => { + const tbodyStart = src.indexOf(''); + const after = src.slice(tbodyStart); + const trOpen = after.search(/`', trOpen); + const row = after.slice(trOpen, rowEnd); + assert.ok( + /]*class="mono"[^>]*data-value="\$\{[^}]*o\.client_version[^}]*\}"|]*data-value="\$\{[^}]*o\.client_version[^}]*\}"[^>]*class="mono"/.test(row), + 'client_version with data-value reading o.client_version and class="mono" not found in row template' + ); +}); + +test('firmware display truncates "Build:" suffix and preserves full string in title=', () => { + // Look for any helper or inline expression that strips Build:... when + // rendering, AND a title= attribute carrying the unmodified firmware string. + assert.ok( + /Build:/.test(src), + 'firmware truncation marker "Build:" not referenced anywhere in observers.js' + ); + assert.ok( + /title="\$\{[^}]*escapeHtml\([^)]*o\.firmware[^)]*\)[^}]*\}"/.test(src) || + /title="\$\{[^}]*o\.firmware[^}]*\}"/.test(src), + 'firmware cell does not carry a title= with full o.firmware string' + ); +}); + +test('thead+tbody column counts stay balanced after adding new columns', () => { + const thead = extractBlock(src, //, /<\/tr><\/thead>/); + const thCount = (thead.match(/'); + const after = src.slice(tbodyStart); + const trOpen = after.search(/`', trOpen); + const row = after.slice(trOpen, rowEnd); + const tdCount = (row.match(/ vs ${tdCount} `); +}); + +console.log(`\n${passed} passed, ${failed} failed`); +process.exit(failed === 0 ? 0 : 1); diff --git a/test-observers-headings.js b/test-observers-headings.js index 305b6af7..f980c002 100644 --- a/test-observers-headings.js +++ b/test-observers-headings.js @@ -58,7 +58,8 @@ test('expected headings present and ordered', () => { let m; while ((m = re.exec(thead)) !== null) labels.push(m[1].trim()); const expected = ['Status', 'Name', 'Region', 'Last Status', 'Last Packet', - 'Packet Health', 'Total Packets', 'Packets/Hour', 'Clock Offset', 'Uptime']; + 'Packet Health', 'Total Packets', 'Packets/Hour', 'Clock Offset', 'Uptime', + 'Firmware', 'Client']; assert.deepStrictEqual(labels, expected, `Headings out of sync.\nGot: ${JSON.stringify(labels)}\nExpected: ${JSON.stringify(expected)}`); });