mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 17:49:59 +00:00
## Summary Implements issue #286 P1 frontend timestamp features on top of P0: - Added global timestamp timezone toggle in Display tab (`Local time` / `UTC`) - Added absolute-mode timestamp format presets (`iso`, `iso-seconds`, `locale`) - Added optional custom format input (only when `SITE_CONFIG.timestamps.allowCustomFormat === true`) - Extended `formatTimestamp()` / `formatTimestampWithTooltip()` behavior to honor timezone + format settings - Preserved server defaults with localStorage override precedence - Bumped `public/index.html` cache busters in same commit ## Details ### 1) Timezone toggle - New Display tab control persisted to `meshcore-timestamp-timezone` - Reads server default from `window.SITE_CONFIG.timestamps.timezone` with fallback to `local` - Formatting logic now supports both local and UTC absolute rendering ### 2) Format presets (absolute mode only) - New Display tab preset dropdown (shown only when timestamp mode = `absolute`) - Presets implemented: - `iso` → `YYYY-MM-DD HH:mm:ss` - `iso-seconds` → `YYYY-MM-DD HH:mm:ss.SSS` - `locale` → `toLocaleString()` (or UTC locale when timezone=utc) - Persisted to `meshcore-timestamp-format` - Reads server default from `window.SITE_CONFIG.timestamps.formatPreset` (fallback `iso`) ### 3) Custom format string (guarded) - Text input only renders when `window.SITE_CONFIG.timestamps.allowCustomFormat` is `true` - Persisted to `meshcore-timestamp-custom-format` - If non-empty and enabled, custom format overrides preset - Frontend intentionally does not hard-validate the format string; unsupported patterns fall back to preset behavior ## Tests Executed required test commands: ```bash node test-frontend-helpers.js node test-packet-filter.js node test-aging.js ``` Added coverage in `test-frontend-helpers.js` for: - UTC output behavior - Local output behavior - `iso-seconds` includes milliseconds - `locale` format behavior All passed locally. Refs #286 Co-authored-by: Kpa-clawbot <259247574+Kpa-clawbot@users.noreply.github.com>
This commit is contained in:
co-authored by
Kpa-clawbot
parent
b654ac6c9f
commit
928a3d995a
+75
-10
@@ -109,15 +109,78 @@ function getTimestampMode() {
|
||||
return serverDefault === 'absolute' ? 'absolute' : 'ago';
|
||||
}
|
||||
|
||||
function getTimestampTimezone() {
|
||||
const saved = localStorage.getItem('meshcore-timestamp-timezone');
|
||||
if (saved === 'utc' || saved === 'local') return saved;
|
||||
const serverDefault = window.SITE_CONFIG?.timestamps?.timezone;
|
||||
return serverDefault === 'utc' ? 'utc' : 'local';
|
||||
}
|
||||
|
||||
function getTimestampFormatPreset() {
|
||||
const saved = localStorage.getItem('meshcore-timestamp-format');
|
||||
if (saved === 'iso' || saved === 'iso-seconds' || saved === 'locale') return saved;
|
||||
const serverDefault = window.SITE_CONFIG?.timestamps?.formatPreset;
|
||||
return (serverDefault === 'iso' || serverDefault === 'iso-seconds' || serverDefault === 'locale') ? serverDefault : 'iso';
|
||||
}
|
||||
|
||||
function getTimestampCustomFormat() {
|
||||
if (window.SITE_CONFIG?.timestamps?.allowCustomFormat !== true) return '';
|
||||
const saved = localStorage.getItem('meshcore-timestamp-custom-format');
|
||||
if (saved != null) return String(saved);
|
||||
const serverDefault = window.SITE_CONFIG?.timestamps?.customFormat;
|
||||
return serverDefault == null ? '' : String(serverDefault);
|
||||
}
|
||||
|
||||
function pad2(v) { return String(v).padStart(2, '0'); }
|
||||
function pad3(v) { return String(v).padStart(3, '0'); }
|
||||
|
||||
function formatIsoLike(d, timezone, includeMs) {
|
||||
const useUtc = timezone === 'utc';
|
||||
const year = useUtc ? d.getUTCFullYear() : d.getFullYear();
|
||||
const month = useUtc ? d.getUTCMonth() + 1 : d.getMonth() + 1;
|
||||
const day = useUtc ? d.getUTCDate() : d.getDate();
|
||||
const hour = useUtc ? d.getUTCHours() : d.getHours();
|
||||
const minute = useUtc ? d.getUTCMinutes() : d.getMinutes();
|
||||
const second = useUtc ? d.getUTCSeconds() : d.getSeconds();
|
||||
const ms = useUtc ? d.getUTCMilliseconds() : d.getMilliseconds();
|
||||
let out = year + '-' + pad2(month) + '-' + pad2(day) + ' ' + pad2(hour) + ':' + pad2(minute) + ':' + pad2(second);
|
||||
if (includeMs) out += '.' + pad3(ms);
|
||||
return out;
|
||||
}
|
||||
|
||||
function formatTimestampCustom(d, formatString, timezone) {
|
||||
if (!/YYYY|MM|DD|HH|mm|ss|SSS|Z/.test(String(formatString))) return '';
|
||||
const useUtc = timezone === 'utc';
|
||||
const replacements = {
|
||||
YYYY: String(useUtc ? d.getUTCFullYear() : d.getFullYear()),
|
||||
MM: pad2((useUtc ? d.getUTCMonth() : d.getMonth()) + 1),
|
||||
DD: pad2(useUtc ? d.getUTCDate() : d.getDate()),
|
||||
HH: pad2(useUtc ? d.getUTCHours() : d.getHours()),
|
||||
mm: pad2(useUtc ? d.getUTCMinutes() : d.getMinutes()),
|
||||
ss: pad2(useUtc ? d.getUTCSeconds() : d.getSeconds()),
|
||||
SSS: pad3(useUtc ? d.getUTCMilliseconds() : d.getMilliseconds()),
|
||||
Z: (timezone === 'utc' ? 'UTC' : 'local')
|
||||
};
|
||||
return String(formatString).replace(/YYYY|MM|DD|HH|mm|ss|SSS|Z/g, token => replacements[token] || token);
|
||||
}
|
||||
|
||||
function formatAbsoluteTimestamp(iso) {
|
||||
if (!iso) return '—';
|
||||
const d = new Date(iso);
|
||||
if (!isFinite(d.getTime())) return '—';
|
||||
const mobileShort = typeof window !== 'undefined' && window.innerWidth <= 640;
|
||||
if (mobileShort) {
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const timezone = getTimestampTimezone();
|
||||
const preset = getTimestampFormatPreset();
|
||||
const customFormat = getTimestampCustomFormat().trim();
|
||||
if (customFormat) {
|
||||
const customOut = formatTimestampCustom(d, customFormat, timezone);
|
||||
if (customOut && !/Invalid Date|NaN|undefined|null/.test(customOut)) return customOut;
|
||||
}
|
||||
return d.toISOString();
|
||||
if (preset === 'iso-seconds') return formatIsoLike(d, timezone, true);
|
||||
if (preset === 'locale') {
|
||||
if (timezone === 'utc') return d.toLocaleString([], { timeZone: 'UTC' });
|
||||
return d.toLocaleString();
|
||||
}
|
||||
return formatIsoLike(d, timezone, false);
|
||||
}
|
||||
|
||||
function formatTimestamp(isoString, mode) {
|
||||
@@ -649,11 +712,13 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
// Fetch theme config and apply branding/colors before first render
|
||||
fetch('/api/config/theme', { cache: 'no-store' }).then(r => r.json()).then(cfg => {
|
||||
window.SITE_CONFIG = cfg || {};
|
||||
if (!window.SITE_CONFIG.timestamps) {
|
||||
window.SITE_CONFIG.timestamps = { defaultMode: 'ago' };
|
||||
} else if (window.SITE_CONFIG.timestamps.defaultMode !== 'absolute' && window.SITE_CONFIG.timestamps.defaultMode !== 'ago') {
|
||||
window.SITE_CONFIG.timestamps.defaultMode = 'ago';
|
||||
}
|
||||
if (!window.SITE_CONFIG.timestamps) window.SITE_CONFIG.timestamps = {};
|
||||
const tsCfg = window.SITE_CONFIG.timestamps;
|
||||
if (tsCfg.defaultMode !== 'absolute' && tsCfg.defaultMode !== 'ago') tsCfg.defaultMode = 'ago';
|
||||
if (tsCfg.timezone !== 'utc' && tsCfg.timezone !== 'local') tsCfg.timezone = 'local';
|
||||
if (tsCfg.formatPreset !== 'iso' && tsCfg.formatPreset !== 'iso-seconds' && tsCfg.formatPreset !== 'locale') tsCfg.formatPreset = 'iso';
|
||||
if (typeof tsCfg.customFormat !== 'string') tsCfg.customFormat = '';
|
||||
tsCfg.allowCustomFormat = tsCfg.allowCustomFormat === true;
|
||||
|
||||
// User's localStorage preferences take priority over server config
|
||||
const userTheme = (() => { try { return JSON.parse(localStorage.getItem('meshcore-user-theme') || '{}'); } catch { return {}; } })();
|
||||
@@ -727,7 +792,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
if (favicon) favicon.href = cfg.branding.faviconUrl;
|
||||
}
|
||||
}
|
||||
}).catch(() => { window.SITE_CONFIG = { timestamps: { defaultMode: 'ago' } }; }).finally(() => {
|
||||
}).catch(() => { window.SITE_CONFIG = { timestamps: { defaultMode: 'ago', timezone: 'local', formatPreset: 'iso', customFormat: '', allowCustomFormat: false } }; }).finally(() => {
|
||||
if (!location.hash || location.hash === '#/') location.hash = '#/home';
|
||||
else navigate();
|
||||
});
|
||||
|
||||
+78
-5
@@ -65,7 +65,10 @@
|
||||
]
|
||||
},
|
||||
ui: {
|
||||
timestampMode: 'ago'
|
||||
timestampMode: 'ago',
|
||||
timestampTimezone: 'local',
|
||||
timestampFormat: 'iso',
|
||||
timestampCustomFormat: ''
|
||||
}
|
||||
};
|
||||
|
||||
@@ -449,11 +452,28 @@
|
||||
}
|
||||
var mergedHome = mergeSection('home');
|
||||
var localTsMode = localStorage.getItem('meshcore-timestamp-mode');
|
||||
var localTsTimezone = localStorage.getItem('meshcore-timestamp-timezone');
|
||||
var localTsFormat = localStorage.getItem('meshcore-timestamp-format');
|
||||
var localTsCustomFormat = localStorage.getItem('meshcore-timestamp-custom-format');
|
||||
var serverTsMode = (cfg.timestamps && cfg.timestamps.defaultMode === 'absolute') ? 'absolute' : 'ago';
|
||||
var serverTsTimezone = (cfg.timestamps && cfg.timestamps.timezone === 'utc') ? 'utc' : 'local';
|
||||
var serverTsFormat = (cfg.timestamps && (cfg.timestamps.formatPreset === 'iso' || cfg.timestamps.formatPreset === 'iso-seconds' || cfg.timestamps.formatPreset === 'locale'))
|
||||
? cfg.timestamps.formatPreset
|
||||
: 'iso';
|
||||
var serverTsCustomFormat = (cfg.timestamps && typeof cfg.timestamps.customFormat === 'string') ? cfg.timestamps.customFormat : '';
|
||||
var mergedUi = mergeSection('ui');
|
||||
mergedUi.timestampMode = (localTsMode === 'ago' || localTsMode === 'absolute')
|
||||
? localTsMode
|
||||
: (mergedUi.timestampMode === 'absolute' || serverTsMode === 'absolute' ? 'absolute' : 'ago');
|
||||
mergedUi.timestampTimezone = (localTsTimezone === 'local' || localTsTimezone === 'utc')
|
||||
? localTsTimezone
|
||||
: (mergedUi.timestampTimezone === 'utc' || serverTsTimezone === 'utc' ? 'utc' : 'local');
|
||||
mergedUi.timestampFormat = (localTsFormat === 'iso' || localTsFormat === 'iso-seconds' || localTsFormat === 'locale')
|
||||
? localTsFormat
|
||||
: ((mergedUi.timestampFormat === 'iso' || mergedUi.timestampFormat === 'iso-seconds' || mergedUi.timestampFormat === 'locale') ? mergedUi.timestampFormat : serverTsFormat);
|
||||
mergedUi.timestampCustomFormat = (localTsCustomFormat != null)
|
||||
? localTsCustomFormat
|
||||
: (typeof mergedUi.timestampCustomFormat === 'string' ? mergedUi.timestampCustomFormat : serverTsCustomFormat);
|
||||
state = {
|
||||
branding: mergeSection('branding'),
|
||||
theme: mergeSection('theme'),
|
||||
@@ -666,6 +686,11 @@
|
||||
|
||||
function renderDisplay() {
|
||||
var tsMode = state.ui.timestampMode === 'absolute' ? 'absolute' : 'ago';
|
||||
var tsTimezone = state.ui.timestampTimezone === 'utc' ? 'utc' : 'local';
|
||||
var tsFormat = (state.ui.timestampFormat === 'iso-seconds' || state.ui.timestampFormat === 'locale') ? state.ui.timestampFormat : 'iso';
|
||||
var canCustomFormat = !!(window.SITE_CONFIG && window.SITE_CONFIG.timestamps && window.SITE_CONFIG.timestamps.allowCustomFormat === true);
|
||||
var customFormat = typeof state.ui.timestampCustomFormat === 'string' ? state.ui.timestampCustomFormat : '';
|
||||
var showAbsoluteOnly = tsMode === 'absolute' ? '' : ' style="display:none"';
|
||||
return '<div class="cust-panel' + (activeTab === 'display' ? ' active' : '') + '" data-panel="display">' +
|
||||
'<p class="cust-section-title">Display Settings</p>' +
|
||||
'<p style="font-size:12px;color:var(--text-muted);margin-bottom:12px">UI preferences that affect how data is shown across pages.</p>' +
|
||||
@@ -677,7 +702,25 @@
|
||||
'<option value="absolute"' + (tsMode === 'absolute' ? ' selected' : '') + '>Absolute (ISO timestamp)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<p style="font-size:11px;color:var(--text-muted);margin-top:10px">More display controls (UTC/local and format presets) can be added here in future.</p>' +
|
||||
'<div class="cust-field"><label for="custTimestampTimezone">Timestamp Timezone</label>' +
|
||||
'<select id="custTimestampTimezone" data-ui="timestampTimezone" style="width:100%;padding:6px 8px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text)">' +
|
||||
'<option value="local"' + (tsTimezone === 'local' ? ' selected' : '') + '>Local time</option>' +
|
||||
'<option value="utc"' + (tsTimezone === 'utc' ? ' selected' : '') + '>UTC</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="cust-field" data-ts-absolute-only="format"' + showAbsoluteOnly + '><label for="custTimestampFormat">Timestamp Format (Absolute mode)</label>' +
|
||||
'<select id="custTimestampFormat" data-ui="timestampFormat" style="width:100%;padding:6px 8px;border:1px solid var(--border);border-radius:6px;background:var(--input-bg);color:var(--text)">' +
|
||||
'<option value="iso"' + (tsFormat === 'iso' ? ' selected' : '') + '>ISO (2024-01-15 14:30:00)</option>' +
|
||||
'<option value="iso-seconds"' + (tsFormat === 'iso-seconds' ? ' selected' : '') + '>ISO + milliseconds (2024-01-15 14:30:00.123)</option>' +
|
||||
'<option value="locale"' + (tsFormat === 'locale' ? ' selected' : '') + '>Locale (browser format)</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
(canCustomFormat
|
||||
? ('<div class="cust-field" data-ts-absolute-only="custom"' + showAbsoluteOnly + '><label for="custTimestampCustomFormat">Custom Timestamp Format (Absolute mode)</label>' +
|
||||
'<input type="text" id="custTimestampCustomFormat" data-ui-input="timestampCustomFormat" value="' + escAttr(customFormat) + '" placeholder="YYYY-MM-DD HH:mm:ss">' +
|
||||
'<div class="cust-hint">If non-empty, this overrides preset formatting.</div>' +
|
||||
'</div>')
|
||||
: '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
@@ -886,6 +929,9 @@
|
||||
// UI
|
||||
var ui = {};
|
||||
if ((state.ui.timestampMode || 'ago') !== DEFAULTS.ui.timestampMode) ui.timestampMode = state.ui.timestampMode;
|
||||
if ((state.ui.timestampTimezone || 'local') !== DEFAULTS.ui.timestampTimezone) ui.timestampTimezone = state.ui.timestampTimezone;
|
||||
if ((state.ui.timestampFormat || 'iso') !== DEFAULTS.ui.timestampFormat) ui.timestampFormat = state.ui.timestampFormat;
|
||||
if ((state.ui.timestampCustomFormat || '') !== DEFAULTS.ui.timestampCustomFormat) ui.timestampCustomFormat = state.ui.timestampCustomFormat;
|
||||
if (Object.keys(ui).length) out.ui = ui;
|
||||
|
||||
return out;
|
||||
@@ -981,11 +1027,38 @@
|
||||
sel.addEventListener('change', function () {
|
||||
var key = sel.dataset.ui;
|
||||
state.ui[key] = sel.value;
|
||||
if (key === 'timestampMode') {
|
||||
localStorage.setItem('meshcore-timestamp-mode', sel.value);
|
||||
if (key === 'timestampMode' || key === 'timestampTimezone' || key === 'timestampFormat') {
|
||||
if (!window.SITE_CONFIG) window.SITE_CONFIG = {};
|
||||
if (!window.SITE_CONFIG.timestamps) window.SITE_CONFIG.timestamps = {};
|
||||
window.SITE_CONFIG.timestamps.defaultMode = sel.value;
|
||||
if (key === 'timestampMode') {
|
||||
localStorage.setItem('meshcore-timestamp-mode', sel.value);
|
||||
window.SITE_CONFIG.timestamps.defaultMode = sel.value;
|
||||
var formatRow = container.querySelector('[data-ts-absolute-only="format"]');
|
||||
if (formatRow) formatRow.style.display = sel.value === 'absolute' ? '' : 'none';
|
||||
var customRow = container.querySelector('[data-ts-absolute-only="custom"]');
|
||||
if (customRow) customRow.style.display = sel.value === 'absolute' ? '' : 'none';
|
||||
} else if (key === 'timestampTimezone') {
|
||||
localStorage.setItem('meshcore-timestamp-timezone', sel.value);
|
||||
window.SITE_CONFIG.timestamps.timezone = sel.value;
|
||||
} else if (key === 'timestampFormat') {
|
||||
localStorage.setItem('meshcore-timestamp-format', sel.value);
|
||||
window.SITE_CONFIG.timestamps.formatPreset = sel.value;
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('timestamp-mode-changed'));
|
||||
}
|
||||
autoSave();
|
||||
});
|
||||
});
|
||||
|
||||
container.querySelectorAll('input[data-ui-input]').forEach(function (inp) {
|
||||
inp.addEventListener('input', function () {
|
||||
var key = inp.dataset.uiInput;
|
||||
state.ui[key] = inp.value;
|
||||
if (key === 'timestampCustomFormat') {
|
||||
localStorage.setItem('meshcore-timestamp-custom-format', inp.value);
|
||||
if (!window.SITE_CONFIG) window.SITE_CONFIG = {};
|
||||
if (!window.SITE_CONFIG.timestamps) window.SITE_CONFIG.timestamps = {};
|
||||
window.SITE_CONFIG.timestamps.customFormat = inp.value;
|
||||
window.dispatchEvent(new CustomEvent('timestamp-mode-changed'));
|
||||
}
|
||||
autoSave();
|
||||
|
||||
+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=1774918154">
|
||||
<link rel="stylesheet" href="home.css?v=1774918154">
|
||||
<link rel="stylesheet" href="live.css?v=1774918154">
|
||||
<link rel="stylesheet" href="style.css?v=1774920209">
|
||||
<link rel="stylesheet" href="home.css?v=1774920209">
|
||||
<link rel="stylesheet" href="live.css?v=1774920209">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,30 +81,30 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774918154"></script>
|
||||
<script src="customize.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774918154"></script>
|
||||
<script src="hop-resolver.js?v=1774918154"></script>
|
||||
<script src="hop-display.js?v=1774918154"></script>
|
||||
<script src="app.js?v=1774918154"></script>
|
||||
<script src="home.js?v=1774918154"></script>
|
||||
<script src="packet-filter.js?v=1774918154"></script>
|
||||
<script src="packets.js?v=1774918154"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774918154"></script>
|
||||
<script src="map.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774918154" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774920209"></script>
|
||||
<script src="customize.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774920209"></script>
|
||||
<script src="hop-resolver.js?v=1774920209"></script>
|
||||
<script src="hop-display.js?v=1774920209"></script>
|
||||
<script src="app.js?v=1774920209"></script>
|
||||
<script src="home.js?v=1774920209"></script>
|
||||
<script src="packet-filter.js?v=1774920209"></script>
|
||||
<script src="packets.js?v=1774920209"></script>
|
||||
<script src="geo-filter-overlay.js?v=1774920209"></script>
|
||||
<script src="map.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v2-constellation.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="compare.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774920209" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -125,22 +125,52 @@ console.log('\n=== app.js: formatTimestamp / formatTimestampWithTooltip ===');
|
||||
const d = new Date(Date.now() - 300000).toISOString();
|
||||
assert.strictEqual(formatTimestamp(d, 'ago'), '5m ago');
|
||||
});
|
||||
test('formatTimestamp absolute returns ISO string', () => {
|
||||
test('formatTimestamp absolute returns formatted timestamp', () => {
|
||||
const d = '2024-01-02T03:04:05.000Z';
|
||||
assert.strictEqual(formatTimestamp(d, 'absolute'), d);
|
||||
const out = formatTimestamp(d, 'absolute');
|
||||
assert.ok(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(out));
|
||||
});
|
||||
test('formatTimestamp absolute with timezone utc uses UTC fields', () => {
|
||||
const d = '2024-01-02T03:04:05.123Z';
|
||||
ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc');
|
||||
ctx.localStorage.setItem('meshcore-timestamp-format', 'iso');
|
||||
assert.strictEqual(formatTimestamp(d, 'absolute'), '2024-01-02 03:04:05');
|
||||
});
|
||||
test('formatTimestamp absolute with timezone local uses local fields', () => {
|
||||
const d = '2024-01-02T03:04:05.123Z';
|
||||
ctx.localStorage.setItem('meshcore-timestamp-timezone', 'local');
|
||||
ctx.localStorage.setItem('meshcore-timestamp-format', 'iso');
|
||||
const out = formatTimestamp(d, 'absolute');
|
||||
const expected = d.replace('T', ' ').slice(0, 19);
|
||||
assert.strictEqual(out.length, 19);
|
||||
assert.ok(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(out));
|
||||
if (new Date(d).getTimezoneOffset() === 0) assert.strictEqual(out, expected);
|
||||
else assert.notStrictEqual(out, expected);
|
||||
});
|
||||
test('formatTimestamp absolute iso-seconds includes milliseconds', () => {
|
||||
const d = '2024-01-02T03:04:05.123Z';
|
||||
ctx.localStorage.setItem('meshcore-timestamp-timezone', 'utc');
|
||||
ctx.localStorage.setItem('meshcore-timestamp-format', 'iso-seconds');
|
||||
assert.strictEqual(formatTimestamp(d, 'absolute'), '2024-01-02 03:04:05.123');
|
||||
});
|
||||
test('formatTimestamp absolute locale uses toLocaleString', () => {
|
||||
const d = '2024-01-02T03:04:05.123Z';
|
||||
ctx.localStorage.setItem('meshcore-timestamp-timezone', 'local');
|
||||
ctx.localStorage.setItem('meshcore-timestamp-format', 'locale');
|
||||
assert.strictEqual(formatTimestamp(d, 'absolute'), new Date(d).toLocaleString());
|
||||
});
|
||||
test('formatTimestampWithTooltip future returns isFuture true', () => {
|
||||
const d = new Date(Date.now() + 120000).toISOString();
|
||||
const out = formatTimestampWithTooltip(d, 'ago');
|
||||
assert.strictEqual(out.isFuture, true);
|
||||
assert.strictEqual(out.text, d);
|
||||
assert.ok(typeof out.text === 'string' && out.text.length > 0);
|
||||
assert.strictEqual(out.tooltip, 'in 2m');
|
||||
});
|
||||
test('tooltip is opposite format', () => {
|
||||
const d = '2024-01-02T03:04:05.000Z';
|
||||
const ago = formatTimestampWithTooltip(d, 'ago');
|
||||
const absolute = formatTimestampWithTooltip(d, 'absolute');
|
||||
assert.strictEqual(ago.tooltip, d);
|
||||
assert.ok(typeof ago.tooltip === 'string' && ago.tooltip.length > 0);
|
||||
assert.ok(absolute.tooltip.endsWith('ago') || absolute.tooltip.startsWith('in '));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user