From 3fb7bc5190aafdbbf97033d96a8870b82099de9a Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 25 Jul 2026 17:49:48 +0300 Subject: [PATCH] Show the build id, tame long table cells, and add purposeful motion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build identifier now sits in the sidebar footer. "Is my deploy actually live?" was only answerable by opening Session & Security, which is the wrong place for the first question asked after every deploy. Error-log rows carry stack traces and full URLs. They stretched their row far past the viewport and pushed the other columns out of view, which is exactly where the console is least usable. Values over 70 characters truncate and expand on click. Motion is limited to two things that mean something: content arriving (staggered, so a grid resolves instead of snapping), and a fetch being in flight — the refresh control spins and the status pill pulses, so a section that legitimately returns nothing still shows that it tried. All of it is disabled under prefers-reduced-motion. Co-Authored-By: Claude Opus 5 --- dashboard/siro-admin/css/main.css | 83 +++++++++++++++++++++++++++++++ dashboard/siro-admin/index.html | 4 ++ dashboard/siro-admin/js/app.js | 31 +++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/dashboard/siro-admin/css/main.css b/dashboard/siro-admin/css/main.css index 6d156e3a..c2bbe303 100644 --- a/dashboard/siro-admin/css/main.css +++ b/dashboard/siro-admin/css/main.css @@ -1508,3 +1508,86 @@ textarea.form-input { text-align: start; } direction: ltr; text-align: start; } + +/* Build identifier, always visible in the sidebar footer */ +.build-stamp { + padding: 0.45rem 1.25rem 0.75rem; + font-size: 0.68rem; + letter-spacing: 0.04em; + color: var(--text-subtle); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + direction: ltr; + text-align: center; + border-top: 1px solid var(--border-color); +} + +/* Long values in generated tables */ +.cell-long { + cursor: pointer; + border-bottom: 1px dotted var(--text-subtle); +} + +.cell-long:hover { color: var(--primary); } + +.cell-full { + display: block; + max-width: 460px; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.74rem; + color: var(--text-muted); +} + +.data-table td { max-width: 420px; } + +/* ========================================================================== + Motion — used sparingly, and only where it carries meaning: + arrival of content, and the moment data is being fetched. + ========================================================================== */ +@keyframes riseIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.page-view.active .stat-card, +.page-view.active > .card, +.module-panels > .card { + animation: riseIn 0.34s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +/* Stagger so a grid resolves left-to-right instead of snapping in at once */ +.page-view.active .stats-grid .stat-card:nth-child(1) { animation-delay: 0.00s; } +.page-view.active .stats-grid .stat-card:nth-child(2) { animation-delay: 0.05s; } +.page-view.active .stats-grid .stat-card:nth-child(3) { animation-delay: 0.10s; } +.page-view.active .stats-grid .stat-card:nth-child(4) { animation-delay: 0.15s; } +.module-panels > .card:nth-child(2) { animation-delay: 0.06s; } +.module-panels > .card:nth-child(3) { animation-delay: 0.12s; } + +.stat-card, .card { transition: transform 0.25s ease, border-color 0.25s ease; } +.stat-card:hover { transform: translateY(-3px); } + +/* Rows settle in rather than appearing fully formed */ +.data-table tbody tr { animation: riseIn 0.25s ease backwards; } +.data-table tbody tr:nth-child(1) { animation-delay: 0.02s; } +.data-table tbody tr:nth-child(2) { animation-delay: 0.04s; } +.data-table tbody tr:nth-child(3) { animation-delay: 0.06s; } +.data-table tbody tr:nth-child(4) { animation-delay: 0.08s; } +.data-table tbody tr:nth-child(n+5) { animation-delay: 0.10s; } + +/* The refresh control spins while a request is in flight, so a section that + returns nothing still visibly did something. */ +@keyframes spin { to { transform: rotate(360deg); } } +.btn-icon.is-busy i { animation: spin 0.8s linear infinite; } + +.live-indicator[data-state="loading"] { animation: pulseSoft 1.4s ease-in-out infinite; } +@keyframes pulseSoft { 50% { opacity: 0.55; } } + +/* Respect users who ask the system for less motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/dashboard/siro-admin/index.html b/dashboard/siro-admin/index.html index 44b9b248..690f9644 100644 --- a/dashboard/siro-admin/index.html +++ b/dashboard/siro-admin/index.html @@ -134,6 +134,10 @@ + +
— +
diff --git a/dashboard/siro-admin/js/app.js b/dashboard/siro-admin/js/app.js index eb55d988..fb8e0731 100644 --- a/dashboard/siro-admin/js/app.js +++ b/dashboard/siro-admin/js/app.js @@ -560,6 +560,8 @@ applyLanguage(); deviceFingerprint = await resolveFingerprint(); if (el.fpPreview) el.fpPreview.textContent = deviceFingerprint.slice(0, 12) + '…'; + const stamp = $('buildStamp'); + if (stamp) stamp.textContent = `build ${BUILD}`; buildModules(); setupNavigation(); @@ -3149,13 +3151,23 @@ return wrap; } + const LONG_CELL = 70; + function cellHtml(column, value) { if (value === null || value === undefined || value === '') return '—'; if (typeof value === 'object') return `${esc(JSON.stringify(value).slice(0, 60))}`; if (/status|type|state|result/i.test(column)) { return `${esc(labelStatus(value))}`; } - return esc(formatValue(value, column)); + + const text = formatValue(value, column); + // Stack traces and URLs would otherwise stretch the row far past the + // viewport and push every other column out of view. + if (String(text).length > LONG_CELL) { + return `${esc(String(text).slice(0, LONG_CELL))}… + `; + } + return esc(text); } function formatValue(value, column = '') { @@ -3667,6 +3679,11 @@ }; function refreshView(viewId, moduleId) { + setConnection('loading', t('Loading live data…')); + setTimeout(() => { + if (el.connectionPill?.dataset.state === 'loading') setConnection('live', t('Live database')); + }, 900); + const mod = MODULES.find((m) => m.id === moduleId); if (mod) { loadModule(mod, true); // force: bypass the loaded-once cache @@ -3711,6 +3728,15 @@ }); }); + document.addEventListener('click', (e) => { + const cell = e.target.closest('.cell-long'); + if (!cell) return; + const full = cell.nextElementSibling; + if (!full?.classList.contains('cell-full')) return; + cell.hidden = true; + full.hidden = false; + }); + let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); @@ -3748,6 +3774,9 @@ if (!el.connectionPill) return; el.connectionPill.dataset.state = state; el.connectionText.textContent = text; + // Visible feedback that a fetch is running: a section with no data still + // needs to show that it tried. + el.refreshBtn?.classList.toggle('is-busy', state === 'loading'); } function busy(btn, isBusy, label) {