diff --git a/public/analytics.js b/public/analytics.js index 36fe90c6..3a79f30a 100644 --- a/public/analytics.js +++ b/public/analytics.js @@ -1732,8 +1732,8 @@
⏱️ Timeline
-
First seen: ${data.firstSeen ? new Date(data.firstSeen).toLocaleString() : '—'}
-
Last seen: ${data.lastSeen ? new Date(data.lastSeen).toLocaleString() : '—'}
+
First seen: ${data.firstSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.firstSeen) : new Date(data.firstSeen).toLocaleString()) : '—'}
+
Last seen: ${data.lastSeen ? (typeof formatAbsoluteTimestamp === 'function' ? formatAbsoluteTimestamp(data.lastSeen) : new Date(data.lastSeen).toLocaleString()) : '—'}
${data.observers.length ? ` @@ -2660,7 +2660,7 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _ const name = esc(n.name || n.public_key.slice(0, 12)); const role = n.role ? `${esc(n.role)}` : ''; const hs = n.hash_size ? ` ${n.hash_size}B hash` : ''; - const when = n.last_seen ? ` ${new Date(n.last_seen).toLocaleDateString()}` : ''; + const when = n.last_seen ? ` ${(typeof formatAbsoluteTimestamp === 'function') ? formatAbsoluteTimestamp(n.last_seen) : new Date(n.last_seen).toLocaleDateString()}` : ''; return `
${name} ${role}${hs}${when}
`; } @@ -3158,7 +3158,7 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _ const t = new Date(d.t); const x = sx(t.getTime()); const y = sy(d.v); - const ts = t.toISOString().replace('T', ' ').replace(/\.\d+Z/, ' UTC'); + const ts = (typeof formatAbsoluteTimestamp === 'function') ? formatAbsoluteTimestamp(d.t) : t.toISOString().replace('T', ' ').replace(/\.\d+Z/, ' UTC'); const tip = `${label}: ${formatV(d.v)}${unit}\n${ts}`; svg += `${tip}`; }); @@ -3172,7 +3172,7 @@ function destroy() { _analyticsData = {}; _channelData = null; if (_ngState && _ const idx = Math.floor(i * (data.length - 1) / Math.max(xTicks - 1, 1)); const t = new Date(data[idx].t); const x = sx(t.getTime()); - const label = t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const label = (typeof formatChartAxisLabel === 'function') ? formatChartAxisLabel(t, true) : t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); svg += `${label}`; } return svg; diff --git a/public/app.js b/public/app.js index 135a57b0..e9091ea9 100644 --- a/public/app.js +++ b/public/app.js @@ -309,6 +309,39 @@ function formatTimestampWithTooltip(isoString, mode) { return { text, tooltip, isFuture }; } +// Format a Date for chart axis labels, respecting customizer timestamp settings. +// shortForm: true = time only (for intra-day), false = date+time (multi-day). +function formatChartAxisLabel(d, shortForm) { + if (!(d instanceof Date) || !isFinite(d.getTime())) return '—'; + var timezone = (typeof getTimestampTimezone === 'function') ? getTimestampTimezone() : 'local'; + var preset = (typeof getTimestampFormatPreset === 'function') ? getTimestampFormatPreset() : 'iso'; + var useUtc = timezone === 'utc'; + + if (preset === 'locale') { + if (shortForm) { + var opts = { hour: '2-digit', minute: '2-digit' }; + if (useUtc) opts.timeZone = 'UTC'; + return d.toLocaleTimeString([], opts); + } + var opts2 = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; + if (useUtc) opts2.timeZone = 'UTC'; + return d.toLocaleString([], opts2); + } + + // ISO-style (iso or iso-seconds) + var hour = useUtc ? d.getUTCHours() : d.getHours(); + var minute = useUtc ? d.getUTCMinutes() : d.getMinutes(); + var timeStr = pad2(hour) + ':' + pad2(minute); + if (preset === 'iso-seconds') { + var sec = useUtc ? d.getUTCSeconds() : d.getSeconds(); + timeStr += ':' + pad2(sec); + } + if (shortForm) return timeStr; + var month = useUtc ? d.getUTCMonth() + 1 : d.getMonth() + 1; + var day = useUtc ? d.getUTCDate() : d.getDate(); + return pad2(month) + '-' + pad2(day) + ' ' + timeStr; +} + function truncate(str, len) { if (!str) return ''; return str.length > len ? str.slice(0, len) + '…' : str; diff --git a/public/node-analytics.js b/public/node-analytics.js index 52eefd3c..3babda9a 100644 --- a/public/node-analytics.js +++ b/public/node-analytics.js @@ -170,7 +170,7 @@ data: { labels: tl.map(b => { const d = new Date(b.bucket); - return currentDays <= 3 ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : d.toLocaleDateString([], { month: 'short', day: 'numeric' }); + return (typeof formatChartAxisLabel === 'function') ? formatChartAxisLabel(d, currentDays <= 3) : (currentDays <= 3 ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : d.toLocaleDateString([], { month: 'short', day: 'numeric' })); }), datasets: [{ label: 'Packets', data: tl.map(b => b.count), backgroundColor: 'rgba(74,158,255,0.5)', borderColor: '#4a9eff', borderWidth: 1 }] }, @@ -197,7 +197,7 @@ const longestObs = Object.values(byObs).sort((a, b) => b.points.length - a.points.length)[0]; const labels = longestObs ? longestObs.points.map(p => { const d = p.x; - return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return (typeof formatChartAxisLabel === 'function') ? formatChartAxisLabel(d, false) : d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }) : []; const c = new Chart(ctx, { type: 'line', diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index af076c29..134531e7 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -6451,6 +6451,73 @@ console.log('\n=== analytics.js: renderCollisionsFromServer collision table ===' }); } +// ===== APP.JS: formatChartAxisLabel ===== +console.log('\n=== app.js: formatChartAxisLabel ==='); +{ + const ctx = makeSandbox(); + loadInCtx(ctx, 'public/roles.js'); + loadInCtx(ctx, 'public/app.js'); + const formatChartAxisLabel = ctx.formatChartAxisLabel; + + test('formatChartAxisLabel returns dash for invalid date', () => { + assert.strictEqual(formatChartAxisLabel(new Date('invalid'), true), '—'); + }); + + test('formatChartAxisLabel returns dash for non-Date', () => { + assert.strictEqual(formatChartAxisLabel('not a date', true), '—'); + }); + + test('formatChartAxisLabel ISO short form returns HH:MM', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'iso'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:00Z'); + assert.strictEqual(formatChartAxisLabel(d, true), '14:30'); + }); + + test('formatChartAxisLabel ISO long form returns MM-DD HH:MM', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'iso'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:00Z'); + assert.strictEqual(formatChartAxisLabel(d, false), '06-15 14:30'); + }); + + test('formatChartAxisLabel ISO-seconds short form includes seconds', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'iso-seconds'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:05Z'); + assert.strictEqual(formatChartAxisLabel(d, true), '14:30:05'); + }); + + test('formatChartAxisLabel ISO-seconds long form includes seconds', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'iso-seconds'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:05Z'); + assert.strictEqual(formatChartAxisLabel(d, false), '06-15 14:30:05'); + }); + + test('formatChartAxisLabel locale short form returns localized time', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'locale'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:00Z'); + const result = formatChartAxisLabel(d, true); + // Locale output varies by env, but should contain hour digits + assert.ok(result.includes('14') || result.includes('2:'), 'short locale should contain hour: ' + result); + }); + + test('formatChartAxisLabel locale long form returns date+time', () => { + ctx.localStorage.setItem('meshcore-timestamp-format', 'locale'); + ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc'); + const d = new Date('2024-06-15T14:30:00Z'); + const result = formatChartAxisLabel(d, false); + // Should contain day reference and time + assert.ok(result.length > 5, 'long locale should be non-trivial: ' + result); + }); + + // Clean up + ctx.localStorage.removeItem('meshcore-timestamp-format'); + ctx.localStorage.removeItem('meshcore-timestamp-timezone'); +} + // ===== SUMMARY ===== Promise.allSettled(pendingTests).then(() => { console.log(`\n${'═'.repeat(40)}`);