From 9e02f84081d3ea7d8d10f87acc2c6ff369e5acb0 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Thu, 21 May 2026 14:42:57 +0300 Subject: [PATCH 1/4] fix(ui): lock dashboard timestamps to locale-independent format --- core/ui/static/modules/actions.js | 2 +- core/ui/static/modules/aether.js | 3 ++- core/ui/static/modules/decisions.js | 4 ++- core/ui/static/modules/processes.js | 2 +- core/ui/static/modules/ui-updates.js | 5 ++-- core/ui/static/modules/utils.js | 5 ++-- .../src/Dotbot.Server/wwwroot/js/dashboard.js | 27 +++++++++++++++++-- 7 files changed, 38 insertions(+), 10 deletions(-) diff --git a/core/ui/static/modules/actions.js b/core/ui/static/modules/actions.js index 6edb7286..4ad3e55a 100644 --- a/core/ui/static/modules/actions.js +++ b/core/ui/static/modules/actions.js @@ -548,7 +548,7 @@ function renderReviewItem(item) { if (item.commit_sha) parts.push(`commit ${escapeHtml(item.commit_sha.substring(0, 8))}`); if (item.review_requested_at) { const d = new Date(item.review_requested_at); - const label = isNaN(d) ? item.review_requested_at : d.toLocaleString(); + const label = isNaN(d) ? item.review_requested_at : formatFriendlyDate(item.review_requested_at); parts.push(`${escapeHtml(label)}`); } return parts.length ? `
${parts.join('')}
` : ''; diff --git a/core/ui/static/modules/aether.js b/core/ui/static/modules/aether.js index 9e811bc0..add56927 100644 --- a/core/ui/static/modules/aether.js +++ b/core/ui/static/modules/aether.js @@ -836,7 +836,8 @@ const Aether = (function() { if (errorEl) errorEl.textContent = _stats.errors; if (lastEventEl && _lastEvent) { - const timeStr = _lastEvent.time.toLocaleTimeString(); + const _t = _lastEvent.time; + const timeStr = `${_t.getHours().toString().padStart(2,'0')}:${_t.getMinutes().toString().padStart(2,'0')}`; const colorVar = _lastEvent.type === 'START' ? '--color-success' : _lastEvent.type === 'COMPLETE' ? '--color-secondary' : '--color-primary'; lastEventEl.innerHTML = `${_lastEvent.type}${timeStr}`; diff --git a/core/ui/static/modules/decisions.js b/core/ui/static/modules/decisions.js index 5cc9ff02..f6dafd45 100644 --- a/core/ui/static/modules/decisions.js +++ b/core/ui/static/modules/decisions.js @@ -190,7 +190,9 @@ function _renderList() { function _friendlyDate(iso) { if (!iso) return ''; try { - return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }); + const d = new Date(iso); + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } catch { return iso; } } diff --git a/core/ui/static/modules/processes.js b/core/ui/static/modules/processes.js index 28d66079..6d2005aa 100644 --- a/core/ui/static/modules/processes.js +++ b/core/ui/static/modules/processes.js @@ -295,7 +295,7 @@ function toggleProcessExpand(processId) { function renderOutputHtml(events) { let html = ''; for (const evt of events) { - const ts = evt.timestamp ? new Date(evt.timestamp).toLocaleTimeString() : ''; + const ts = evt.timestamp ? formatCompactTime(evt.timestamp) : ''; const typeClass = evt.type === 'rate_limit' ? 'warning' : (evt.type === 'text' ? 'text' : 'tool'); const messageText = stripConsoleSequences(evt.message || ''); html += `
`; diff --git a/core/ui/static/modules/ui-updates.js b/core/ui/static/modules/ui-updates.js index d95ab2cb..a7e8ee93 100644 --- a/core/ui/static/modules/ui-updates.js +++ b/core/ui/static/modules/ui-updates.js @@ -64,7 +64,8 @@ function updateUI(state) { * Update timestamp display */ function updateTimestamp(instanceId) { - setElementText('last-update', new Date().toLocaleTimeString()); + const _now = new Date(); + setElementText('last-update', `${_now.getHours().toString().padStart(2,'0')}:${_now.getMinutes().toString().padStart(2,'0')}`); setElementText('instance-id', instanceId || '--'); // Update footer mission on each poll to ensure it's current updateFooterMission(); @@ -143,7 +144,7 @@ function updateSessionInfo(session) { if (session.started_at) { const started = new Date(session.started_at); - setElementText('session-started', started.toLocaleTimeString()); + setElementText('session-started', `${started.getHours().toString().padStart(2,'0')}:${started.getMinutes().toString().padStart(2,'0')}`); sessionStartTime = started; } else { setElementText('session-started', '--'); diff --git a/core/ui/static/modules/utils.js b/core/ui/static/modules/utils.js index 8eed31ee..73dd9ab1 100644 --- a/core/ui/static/modules/utils.js +++ b/core/ui/static/modules/utils.js @@ -90,7 +90,7 @@ function formatCompactDate(isoString) { /** * Format ISO date string to human-friendly format with day of week * @param {string} isoString - ISO date string - * @returns {string} Formatted date like "Fri Dec 15 14:30" + * @returns {string} Formatted date like "Fri, Dec 15 2026 14:30" */ function formatFriendlyDate(isoString) { if (!isoString) return ''; @@ -101,9 +101,10 @@ function formatFriendlyDate(isoString) { const dayOfWeek = days[date.getDay()]; const month = months[date.getMonth()]; const dayNum = date.getDate(); + const year = date.getFullYear(); const hours = date.getHours().toString().padStart(2, '0'); const mins = date.getMinutes().toString().padStart(2, '0'); - return `${dayOfWeek} ${month} ${dayNum} ${hours}:${mins}`; + return `${dayOfWeek}, ${month} ${dayNum} ${year} ${hours}:${mins}`; } catch (e) { return ''; } diff --git a/server/src/Dotbot.Server/wwwroot/js/dashboard.js b/server/src/Dotbot.Server/wwwroot/js/dashboard.js index 374f205d..044785d4 100644 --- a/server/src/Dotbot.Server/wwwroot/js/dashboard.js +++ b/server/src/Dotbot.Server/wwwroot/js/dashboard.js @@ -16,6 +16,29 @@ let nudgeCooldowns = {}; // key -> timestamp // --- Helpers --- + function formatDashboardDateTime(date) { + try { + const d = date instanceof Date ? date : new Date(date); + const parts = new Intl.DateTimeFormat('en-US', { + weekday: 'short', month: 'short', day: 'numeric', + year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false + }).formatToParts(d); + const get = (t) => (parts.find(p => p.type === t) || {}).value || ''; + return `${get('weekday')}, ${get('month')} ${get('day')} ${get('year')} ${get('hour')}:${get('minute')}`; + } catch (e) { return String(date); } + } + + function formatDashboardTime(date) { + try { + const d = date instanceof Date ? date : new Date(date); + const parts = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', minute: '2-digit', hour12: false + }).formatToParts(d); + const get = (t) => (parts.find(p => p.type === t) || {}).value || ''; + return `${get('hour')}:${get('minute')}`; + } catch (e) { return ''; } + } + function buildRecipientPills(recipients, max) { if (!recipients || recipients.length === 0) return ''; // Sort: non-responders first, then responders @@ -286,7 +309,7 @@
Created - ${inst.createdAt ? new Date(inst.createdAt).toLocaleString() : '-'} + ${inst.createdAt ? formatDashboardDateTime(inst.createdAt) : '-'}
Created By @@ -814,6 +837,6 @@ function updateRefreshTime() { const el = document.getElementById('last-refresh'); - el.textContent = `Last refresh: ${new Date().toLocaleTimeString()}`; + el.textContent = `Last refresh: ${formatDashboardTime(new Date())}`; } })(); From 724569fdf9eaa807b9e29c7e1b5a354eb75a3dc7 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Thu, 21 May 2026 15:14:17 +0300 Subject: [PATCH 2/4] fix: restore seconds in time-only timestamp displays --- core/ui/static/modules/aether.js | 2 +- core/ui/static/modules/ui-updates.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/ui/static/modules/aether.js b/core/ui/static/modules/aether.js index add56927..76829614 100644 --- a/core/ui/static/modules/aether.js +++ b/core/ui/static/modules/aether.js @@ -837,7 +837,7 @@ const Aether = (function() { if (lastEventEl && _lastEvent) { const _t = _lastEvent.time; - const timeStr = `${_t.getHours().toString().padStart(2,'0')}:${_t.getMinutes().toString().padStart(2,'0')}`; + const timeStr = `${_t.getHours().toString().padStart(2,'0')}:${_t.getMinutes().toString().padStart(2,'0')}:${_t.getSeconds().toString().padStart(2,'0')}`; const colorVar = _lastEvent.type === 'START' ? '--color-success' : _lastEvent.type === 'COMPLETE' ? '--color-secondary' : '--color-primary'; lastEventEl.innerHTML = `${_lastEvent.type}${timeStr}`; diff --git a/core/ui/static/modules/ui-updates.js b/core/ui/static/modules/ui-updates.js index a7e8ee93..7c7ec94d 100644 --- a/core/ui/static/modules/ui-updates.js +++ b/core/ui/static/modules/ui-updates.js @@ -65,7 +65,7 @@ function updateUI(state) { */ function updateTimestamp(instanceId) { const _now = new Date(); - setElementText('last-update', `${_now.getHours().toString().padStart(2,'0')}:${_now.getMinutes().toString().padStart(2,'0')}`); + setElementText('last-update', `${_now.getHours().toString().padStart(2,'0')}:${_now.getMinutes().toString().padStart(2,'0')}:${_now.getSeconds().toString().padStart(2,'0')}`); setElementText('instance-id', instanceId || '--'); // Update footer mission on each poll to ensure it's current updateFooterMission(); @@ -144,7 +144,7 @@ function updateSessionInfo(session) { if (session.started_at) { const started = new Date(session.started_at); - setElementText('session-started', `${started.getHours().toString().padStart(2,'0')}:${started.getMinutes().toString().padStart(2,'0')}`); + setElementText('session-started', `${started.getHours().toString().padStart(2,'0')}:${started.getMinutes().toString().padStart(2,'0')}:${started.getSeconds().toString().padStart(2,'0')}`); sessionStartTime = started; } else { setElementText('session-started', '--'); From 4b62720af0e64203885d713a32ceff7f8d454172 Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Thu, 28 May 2026 11:59:49 +0300 Subject: [PATCH 3/4] fix: address copilot review on pr #439 --- core/ui/static/modules/decisions.js | 1 + core/ui/static/modules/utils.js | 1 + .../src/Dotbot.Server/wwwroot/js/dashboard.js | 20 ++++++++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/core/ui/static/modules/decisions.js b/core/ui/static/modules/decisions.js index f6dafd45..41203595 100644 --- a/core/ui/static/modules/decisions.js +++ b/core/ui/static/modules/decisions.js @@ -191,6 +191,7 @@ function _friendlyDate(iso) { if (!iso) return ''; try { const d = new Date(iso); + if (isNaN(d.getTime())) return iso; const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`; } catch { return iso; } diff --git a/core/ui/static/modules/utils.js b/core/ui/static/modules/utils.js index 73dd9ab1..bf7ae9a2 100644 --- a/core/ui/static/modules/utils.js +++ b/core/ui/static/modules/utils.js @@ -96,6 +96,7 @@ function formatFriendlyDate(isoString) { if (!isoString) return ''; try { const date = new Date(isoString); + if (isNaN(date.getTime())) return ''; const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; const dayOfWeek = days[date.getDay()]; diff --git a/server/src/Dotbot.Server/wwwroot/js/dashboard.js b/server/src/Dotbot.Server/wwwroot/js/dashboard.js index 044785d4..5948633d 100644 --- a/server/src/Dotbot.Server/wwwroot/js/dashboard.js +++ b/server/src/Dotbot.Server/wwwroot/js/dashboard.js @@ -5,6 +5,15 @@ (function () { 'use strict'; + // --- Formatters --- + const _dtFormatter = new Intl.DateTimeFormat('en-US', { + weekday: 'short', month: 'short', day: 'numeric', + year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false + }); + const _timeFormatter = new Intl.DateTimeFormat('en-US', { + hour: '2-digit', minute: '2-digit', hour12: false + }); + // --- State --- let allInstances = []; let byPerson = {}; // email -> [{ instance, recipient, response }] @@ -19,10 +28,8 @@ function formatDashboardDateTime(date) { try { const d = date instanceof Date ? date : new Date(date); - const parts = new Intl.DateTimeFormat('en-US', { - weekday: 'short', month: 'short', day: 'numeric', - year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false - }).formatToParts(d); + if (isNaN(d.getTime())) return String(date); + const parts = _dtFormatter.formatToParts(d); const get = (t) => (parts.find(p => p.type === t) || {}).value || ''; return `${get('weekday')}, ${get('month')} ${get('day')} ${get('year')} ${get('hour')}:${get('minute')}`; } catch (e) { return String(date); } @@ -31,9 +38,8 @@ function formatDashboardTime(date) { try { const d = date instanceof Date ? date : new Date(date); - const parts = new Intl.DateTimeFormat('en-US', { - hour: '2-digit', minute: '2-digit', hour12: false - }).formatToParts(d); + if (isNaN(d.getTime())) return ''; + const parts = _timeFormatter.formatToParts(d); const get = (t) => (parts.find(p => p.type === t) || {}).value || ''; return `${get('hour')}:${get('minute')}`; } catch (e) { return ''; } From 96fe4014606c3852f8994f1a1bfca8d69f3e322a Mon Sep 17 00:00:00 2001 From: Ivan Bondarenko Date: Thu, 28 May 2026 12:35:34 +0300 Subject: [PATCH 4/4] fix(dashboard): widen instance detail modal to fit datetime --- server/src/Dotbot.Server/wwwroot/css/dashboard.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/Dotbot.Server/wwwroot/css/dashboard.css b/server/src/Dotbot.Server/wwwroot/css/dashboard.css index 9cdc0cb1..286be4ee 100644 --- a/server/src/Dotbot.Server/wwwroot/css/dashboard.css +++ b/server/src/Dotbot.Server/wwwroot/css/dashboard.css @@ -914,7 +914,7 @@ select.filter-input option { background: var(--bg-panel); border: 1px solid var(--bezel-edge); border-radius: 6px; - max-width: 900px; + max-width: 920px; width: 95%; max-height: 80vh; overflow: hidden;