refine version badge: clickable links, version only on prod

- Commit hash is now an <a> linking to GitHub commit (full hash in URL, 7-char display)
- Version tag only shown on prod (port 80/443 or no port), linked to GitHub release
- Staging (non-standard port) shows commit + engine only, no version noise
- Detect prod vs staging via location.port
- Updated tests: 16 cases covering prod/staging/links/edge cases
- Bumped cache busters

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbotandCopilot committed 2026-03-27 09:54:53 -07:00
1 parent a7a280801a
commit b92e71fa0e
3 files changed
+111 -47

No files matched your search

+11 -2
View File
@@ -107,9 +107,18 @@ function formatEngineBadge(engine) {
function formatVersionBadge(version, commit, engine) {
if (!version && !commit && !engine) return '';
var port = (typeof location !== 'undefined' && location.port) || '';
var isProd = !port || port === '80' || port === '443';
var GH = 'https://github.com/Kpa-clawbot/meshcore-analyzer';
var parts = [];
if (version) parts.push(version.charAt(0) === 'v' ? version : 'v' + version);
if (commit && commit !== 'unknown') parts.push(commit.length > 7 ? commit.slice(0, 7) : commit);
if (version && isProd) {
var vTag = version.charAt(0) === 'v' ? version : 'v' + version;
parts.push('<a href="' + GH + '/releases/tag/' + vTag + '" target="_blank" rel="noopener">' + vTag + '</a>');
}
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>');
}
if (engine) parts.push('[' + engine + ']');
if (parts.length === 0) return '';
return ' <span class="version-badge">' + parts.join(' · ') + '</span>';
+27 -27
View File
@@ -22,9 +22,9 @@
<meta name="twitter:title" content="MeshCore Analyzer">
<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/meshcore-analyzer/master/public/og-image.png">
<link rel="stylesheet" href="style.css?v=1774630314">
<link rel="stylesheet" href="home.css?v=1774630314">
<link rel="stylesheet" href="live.css?v=1774630314">
<link rel="stylesheet" href="style.css?v=1774630476">
<link rel="stylesheet" href="home.css?v=1774630476">
<link rel="stylesheet" href="live.css?v=1774630476">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin="anonymous">
@@ -81,29 +81,29 @@
<main id="app" role="main"></main>
<script src="vendor/qrcode.js"></script>
<script src="roles.js?v=1774630314"></script>
<script src="customize.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774630314"></script>
<script src="hop-resolver.js?v=1774630314"></script>
<script src="hop-display.js?v=1774630314"></script>
<script src="app.js?v=1774630314"></script>
<script src="home.js?v=1774630314"></script>
<script src="packet-filter.js?v=1774630314"></script>
<script src="packets.js?v=1774630314"></script>
<script src="map.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774630314" onerror="console.error('Failed to load:', this.src)"></script>
<script src="roles.js?v=1774630476"></script>
<script src="customize.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="region-filter.js?v=1774630476"></script>
<script src="hop-resolver.js?v=1774630476"></script>
<script src="hop-display.js?v=1774630476"></script>
<script src="app.js?v=1774630476"></script>
<script src="home.js?v=1774630476"></script>
<script src="packet-filter.js?v=1774630476"></script>
<script src="packets.js?v=1774630476"></script>
<script src="map.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="channels.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="nodes.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="traces.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="analytics.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v1-constellation.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-v2-constellation.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="audio-lab.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="live.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observers.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="observer-detail.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="compare.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="node-analytics.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
<script src="perf.js?v=1774630476" onerror="console.error('Failed to load:', this.src)"></script>
</body>
</html>
+73 -18
View File
@@ -1166,54 +1166,109 @@ console.log('\n=== app.js: formatEngineBadge ===');
// ===== APP.JS: formatVersionBadge =====
console.log('\n=== app.js: formatVersionBadge ===');
{
const ctx = makeSandbox();
loadInCtx(ctx, 'public/roles.js');
loadInCtx(ctx, 'public/app.js');
const formatVersionBadge = ctx.formatVersionBadge;
function makeBadgeSandbox(port) {
const ctx = makeSandbox();
ctx.location.port = port || '';
loadInCtx(ctx, 'public/roles.js');
loadInCtx(ctx, 'public/app.js');
return ctx;
}
const GH = 'https://github.com/Kpa-clawbot/meshcore-analyzer';
test('returns empty string when all args missing', () => {
const { formatVersionBadge } = makeBadgeSandbox('');
assert.strictEqual(formatVersionBadge(null, null, null), '');
assert.strictEqual(formatVersionBadge(undefined, undefined, undefined), '');
assert.strictEqual(formatVersionBadge('', '', ''), '');
});
test('shows version + commit + engine', () => {
const result = formatVersionBadge('2.6.0', 'abc1234def', 'go');
// --- 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');
assert.ok(result.includes('version-badge'), 'should have version-badge class');
assert.ok(result.includes('v2.6.0'), 'should show version with v prefix');
assert.ok(result.includes('abc1234'), 'should show truncated commit');
assert.ok(!result.includes('abc1234d'), 'should truncate commit to 7 chars');
assert.ok(result.includes('[go]'), 'should show engine in brackets');
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');
assert.ok(result.includes(`href="${GH}/commit/abc1234def5678"`), 'commit links to full hash');
assert.ok(result.includes('>abc1234</a>'), 'commit display is truncated to 7');
assert.ok(result.includes('[node]'), 'should show engine');
});
test('version already has v prefix', () => {
test('prod port 80: shows version', () => {
const { formatVersionBadge } = makeBadgeSandbox('80');
const result = formatVersionBadge('2.6.0', null, 'node');
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');
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);
assert.ok(result.includes('v2.6.0'), 'should not double the v prefix');
assert.ok(result.includes('>v2.6.0</a>'), 'should not double the v prefix');
assert.ok(!result.includes('vv'), 'should not have vv');
});
// --- Staging tests (non-standard port) ---
test('staging: hides version, shows commit + engine', () => {
const { formatVersionBadge } = makeBadgeSandbox('3000');
const result = formatVersionBadge('2.6.0', 'abc1234def5678', 'go');
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');
assert.ok(result.includes('[go]'), 'should show engine');
});
test('staging port 81: hides version', () => {
const { formatVersionBadge } = makeBadgeSandbox('81');
const result = formatVersionBadge('2.6.0', 'abc1234', 'go');
assert.ok(!result.includes('v2.6.0'), 'port 81 is staging — no version');
assert.ok(result.includes('>abc1234</a>'), 'commit shown');
});
// --- Shared behavior ---
test('commit link uses full hash', () => {
const { formatVersionBadge } = makeBadgeSandbox('');
const result = formatVersionBadge(null, 'abc1234def567890123456789abcdef012345678', 'node');
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');
assert.ok(result.includes('v2.6.0'), 'should show version');
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('[node]'), 'should show engine');
});
test('skips commit when missing', () => {
const { formatVersionBadge } = makeBadgeSandbox('');
const result = formatVersionBadge('2.6.0', null, 'go');
assert.ok(result.includes('v2.6.0'), 'should show version');
assert.ok(result.includes('>v2.6.0</a>'), 'should show version');
assert.ok(result.includes('[go]'), 'should show engine');
});
test('shows only engine when version/commit missing', () => {
const { formatVersionBadge } = makeBadgeSandbox('3000');
const result = formatVersionBadge(null, null, 'go');
assert.ok(result.includes('[go]'), 'should show engine');
assert.ok(result.includes('version-badge'), 'should use version-badge class');
});
test('short commit not truncated', () => {
test('short commit not truncated in display', () => {
const { formatVersionBadge } = makeBadgeSandbox('');
const result = formatVersionBadge('1.0.0', 'abc1234', 'node');
assert.ok(result.includes('abc1234'), 'should show full short commit');
assert.ok(result.includes('>abc1234</a>'), 'should show full short commit');
});
test('version only', () => {
test('version only on prod', () => {
const { formatVersionBadge } = makeBadgeSandbox('');
const result = formatVersionBadge('2.6.0', null, null);
assert.ok(result.includes('v2.6.0'), 'should show version');
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');
assert.ok(!result.includes('2.6.0'), 'no version on staging');
assert.ok(result.includes('[go]'), 'engine shown');
});
}
// ===== SUMMARY =====