mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 01:04:09 +00:00
Show build age next to commit hash in UI (#311)
## Summary - show relative build age next to the commit hash in the nav stats version badge (e.g. `abc1234 (3h ago)`) - use `stats.buildTime` from `/api/stats` and existing `timeAgo()` formatting in `public/app.js` - keep behavior unchanged when `buildTime` is missing/unknown ## What changed - updated `formatVersionBadge()` signature to accept `buildTime` - appended a `build-age` span after the commit link when `buildTime` is valid - passed `stats.buildTime` from `updateNavStats()` - updated frontend helper tests for the new function signature - added regression tests for build-age rendering/skip behavior - bumped cache busters in `public/index.html` ## API check - verified Go server already exposes `buildTime` on `/api/stats` and `/api/health` via `cmd/server/routes.go` - no backend API changes required ## Tests - `node test-frontend-helpers.js` - `node test-packet-filter.js` - `node test-aging.js` All passed locally. ## Browser validation - Not run in this environment (no browser session available). Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Kpa-clawbot
Copilot
parent
90db37e07f
commit
114b6eea1f
@@ -121,6 +121,9 @@ func TestHealthEndpoint(t *testing.T) {
|
||||
if _, ok := body["commit"]; !ok {
|
||||
t.Error("expected commit field in health response")
|
||||
}
|
||||
if bt, ok := body["buildTime"]; !ok || bt == nil {
|
||||
t.Error("expected non-nil buildTime field in health response")
|
||||
}
|
||||
|
||||
// Verify memory has spec-defined fields (no heapMB or goRuntime per api-spec.md)
|
||||
mem, ok := body["memory"].(map[string]interface{})
|
||||
@@ -201,6 +204,9 @@ func TestStatsEndpoint(t *testing.T) {
|
||||
if _, ok := body["commit"]; !ok {
|
||||
t.Error("expected commit field in stats response")
|
||||
}
|
||||
if bt, ok := body["buildTime"]; !ok || bt == nil {
|
||||
t.Error("expected non-nil buildTime field in stats response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketsEndpoint(t *testing.T) {
|
||||
|
||||
+8
-3
@@ -210,8 +210,13 @@ function formatEngineBadge(engine) {
|
||||
return ` <span class="engine-badge">${engine}</span>`;
|
||||
}
|
||||
|
||||
function formatVersionBadge(version, commit, engine) {
|
||||
function formatVersionBadge(version, commit, engine, buildTime) {
|
||||
if (!version && !commit && !engine) return '';
|
||||
var buildAge = '';
|
||||
if (buildTime && buildTime !== 'unknown') {
|
||||
var age = timeAgo(buildTime);
|
||||
if (age && age !== '—') buildAge = ' <span class="build-age">(' + age + ')</span>';
|
||||
}
|
||||
var port = (typeof location !== 'undefined' && location.port) || '';
|
||||
var isProd = !port || port === '80' || port === '443';
|
||||
var GH = 'https://github.com/Kpa-clawbot/corescope';
|
||||
@@ -222,7 +227,7 @@ function formatVersionBadge(version, commit, engine) {
|
||||
}
|
||||
if (commit && commit !== 'unknown') {
|
||||
var short = commit.length > 7 ? commit.slice(0, 7) : commit;
|
||||
parts.push('<a href="' + GH + '/commit/' + commit + '" target="_blank" rel="noopener">' + short + '</a>');
|
||||
parts.push('<a href="' + GH + '/commit/' + commit + '" target="_blank" rel="noopener">' + short + '</a>' + buildAge);
|
||||
}
|
||||
if (engine) parts.push('<span class="engine-badge">' + engine + '</span>');
|
||||
if (parts.length === 0) return '';
|
||||
@@ -698,7 +703,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
const stats = await api('/stats', { ttl: CLIENT_TTL.stats });
|
||||
const el = document.getElementById('navStats');
|
||||
if (el) {
|
||||
el.innerHTML = `<span class="stat-val">${stats.totalPackets}</span> pkts · <span class="stat-val">${stats.totalNodes}</span> nodes · <span class="stat-val">${stats.totalObservers}</span> obs${formatVersionBadge(stats.version, stats.commit, stats.engine)}`;
|
||||
el.innerHTML = `<span class="stat-val">${stats.totalPackets}</span> pkts · <span class="stat-val">${stats.totalNodes}</span> nodes · <span class="stat-val">${stats.totalObservers}</span> obs${formatVersionBadge(stats.version, stats.commit, stats.engine, stats.buildTime)}`;
|
||||
el.querySelectorAll('.stat-val').forEach(s => s.classList.add('updated'));
|
||||
setTimeout(() => { el.querySelectorAll('.stat-val').forEach(s => s.classList.remove('updated')); }, 600);
|
||||
}
|
||||
|
||||
+28
-28
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="CoreScope">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/corescope/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774926567">
|
||||
<link rel="stylesheet" href="home.css?v=1774926567">
|
||||
<link rel="stylesheet" href="live.css?v=1774926567">
|
||||
<link rel="stylesheet" href="style.css?v=1774928028">
|
||||
<link rel="stylesheet" href="home.css?v=1774928028">
|
||||
<link rel="stylesheet" href="live.css?v=1774928028">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,31 +81,31 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774926567"></script>
|
||||
<script src="customize.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774926567"></script>
|
||||
<script src="hop-resolver.js?v=1774926567"></script>
|
||||
<script src="hop-display.js?v=1774926567"></script>
|
||||
<script src="app.js?v=1774926567"></script>
|
||||
<script src="home.js?v=1774926567"></script>
|
||||
<script src="packet-filter.js?v=1774926567"></script>
|
||||
<script src="packets.js?v=1774926567"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774926567"></script>
|
||||
<script src="map.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774926567" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774928028"></script>
|
||||
<script src="customize.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774928028"></script>
|
||||
<script src="hop-resolver.js?v=1774928028"></script>
|
||||
<script src="hop-display.js?v=1774928028"></script>
|
||||
<script src="app.js?v=1774928028"></script>
|
||||
<script src="home.js?v=1774928028"></script>
|
||||
<script src="packet-filter.js?v=1774928028"></script>
|
||||
<script src="packets.js?v=1774928028"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774928028"></script>
|
||||
<script src="map.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774928028" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
+41
-13
@@ -1388,7 +1388,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
// --- Prod tests (no port / port 80 / port 443) ---
|
||||
test('prod: shows version + commit + engine with links', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'node');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'node', null);
|
||||
assert.ok(result.includes('version-badge'), 'should have version-badge class');
|
||||
assert.ok(result.includes(`href="${GH}/releases/tag/v2.6.0"`), 'version links to release');
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'version text has v prefix');
|
||||
@@ -1398,17 +1398,17 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
});
|
||||
test('prod port 80: shows version', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('80');
|
||||
const result = formatVersionBadge('2.6.0', null, 'node');
|
||||
const result = formatVersionBadge('2.6.0', null, 'node', null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'port 80 is prod — shows version');
|
||||
});
|
||||
test('prod port 443: shows version', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('443');
|
||||
const result = formatVersionBadge('2.6.0', null, 'node');
|
||||
const result = formatVersionBadge('2.6.0', null, 'node', null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'port 443 is prod — shows version');
|
||||
});
|
||||
test('prod: version already has v prefix', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('v2.6.0', null, null);
|
||||
const result = formatVersionBadge('v2.6.0', null, null, null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should not double the v prefix');
|
||||
assert.ok(!result.includes('vv'), 'should not have vv');
|
||||
});
|
||||
@@ -1416,7 +1416,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
// --- Staging tests (non-standard port) ---
|
||||
test('staging: hides version, shows commit + engine', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('3000');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go', null);
|
||||
assert.ok(!result.includes('v2.6.0'), 'staging should NOT show version');
|
||||
assert.ok(result.includes('>abc1234</a>'), 'should show commit hash');
|
||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit is linked');
|
||||
@@ -1424,7 +1424,7 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
});
|
||||
test('staging port 81: hides version', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('81');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234', 'go');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234', 'go', null);
|
||||
assert.ok(!result.includes('v2.6.0'), 'port 81 is staging — no version');
|
||||
assert.ok(result.includes('>abc1234</a>'), 'commit shown');
|
||||
});
|
||||
@@ -1432,46 +1432,74 @@ console.log('\n=== app.js: formatVersionBadge ===');
|
||||
// --- Shared behavior ---
|
||||
test('commit link uses full hash', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge(null, 'abc1234def567890123456789abcdef012345678', 'node');
|
||||
const result = formatVersionBadge(null, 'abc1234def567890123456789abcdef012345678', 'node', null);
|
||||
assert.ok(result.includes(`href="${GH}/commit/abc1234def567890123456789abcdef012345678"`), 'link uses full hash');
|
||||
assert.ok(result.includes('>abc1234</a>'), 'display is truncated to 7');
|
||||
});
|
||||
test('skips commit when "unknown"', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'unknown', 'node');
|
||||
const result = formatVersionBadge('2.6.0', 'unknown', 'node', null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||
assert.ok(!result.includes('unknown'), 'should not show unknown commit');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>node<'), 'should show engine name');
|
||||
});
|
||||
test('skips commit when missing', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go', null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||
});
|
||||
test('shows only engine when version/commit missing', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('3000');
|
||||
const result = formatVersionBadge(null, null, 'go');
|
||||
const result = formatVersionBadge(null, null, 'go', null);
|
||||
assert.ok(result.includes('engine-badge'), 'should show engine badge'); assert.ok(result.includes('>go<'), 'should show engine name');
|
||||
assert.ok(result.includes('version-badge'), 'should use version-badge class');
|
||||
});
|
||||
test('short commit not truncated in display', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('1.0.0', 'abc1234', 'node');
|
||||
const result = formatVersionBadge('1.0.0', 'abc1234', 'node', null);
|
||||
assert.ok(result.includes('>abc1234</a>'), 'should show full short commit');
|
||||
});
|
||||
test('version only on prod', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', null, null);
|
||||
const result = formatVersionBadge('2.6.0', null, null, null);
|
||||
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
|
||||
assert.ok(!result.includes('·'), 'should not have separator for single part');
|
||||
});
|
||||
test('staging: only engine when no commit', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('8080');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go');
|
||||
const result = formatVersionBadge('2.6.0', null, 'go', null);
|
||||
assert.ok(!result.includes('2.6.0'), 'no version on staging');
|
||||
assert.ok(result.includes('engine-badge'), 'engine badge shown'); assert.ok(result.includes('>go<'), 'engine name shown');
|
||||
});
|
||||
test('shows build age next to commit when buildTime is valid', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const recent = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go', recent);
|
||||
assert.ok(result.includes('>abc1234</a>'), 'commit shown');
|
||||
assert.ok(result.includes('build-age'), 'build age span shown');
|
||||
assert.ok(result.includes('(3h ago)'), 'build age text shown');
|
||||
});
|
||||
test('does not show build age for unknown buildTime', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go', 'unknown');
|
||||
assert.ok(!result.includes('build-age'), 'no build age for unknown buildTime');
|
||||
});
|
||||
test('does not show build age for null buildTime', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go', null);
|
||||
assert.ok(!result.includes('build-age'), 'no build age for null buildTime');
|
||||
});
|
||||
test('does not show build age for undefined buildTime', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go');
|
||||
assert.ok(!result.includes('build-age'), 'no build age for undefined buildTime');
|
||||
});
|
||||
test('does not show build age for invalid buildTime', () => {
|
||||
const { formatVersionBadge } = makeBadgeSandbox('');
|
||||
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go', 'not-a-date');
|
||||
assert.ok(!result.includes('build-age'), 'no build age for invalid buildTime');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== CSS: version-badge link contrast (issue #139) =====
|
||||
|
||||
Reference in New Issue
Block a user