From b9bc3469eb7dc849937162fc807b79a1fad445a6 Mon Sep 17 00:00:00 2001 From: wagesj45 Date: Tue, 13 Oct 2026 22:44:10 -0500 Subject: [PATCH] Improve request timestamps and dashboard metric styling --- app/static/index.html | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/app/static/index.html b/app/static/index.html index 81dffc9..fc924fb 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -11,6 +11,8 @@ .navbar, .card { background: var(--bulma-scheme-main); } .chart-wrap { height: 390px; position: relative; } .metric-value { font-size: 1.35rem; font-weight: 700; white-space: nowrap; } + #updated { background: var(--bulma-scheme-main-ter); color: var(--bulma-text-strong); } + .metric-wide .metric-value { white-space: normal; overflow-wrap: anywhere; } .metric-label { color: var(--bulma-text-weak); font-size: .75rem; text-transform: uppercase; letter-spacing: .05em; } .host-list { max-height: 185px; overflow-y: auto; } .legend-dot { width: .65rem; height: .65rem; display: inline-block; border-radius: 50%; margin-right: .4rem; } @@ -38,12 +40,12 @@

-

Throughput

Waiting for data
+

Throughput

Waiting for data

Top 10 Requests

Completed requests in the selected window, ranked by their larger inbound or outbound size. Select a row for details.

- +
RankDirectionHostClient addressSize
RankDirectionHostClient addressSizeCompleted

No completed requests in this window.

@@ -73,9 +75,17 @@ let selected = new Set(), latest = null, source; const $ = id => document.getElementById(id); function bytesPerSecond(value) { const bits = value * 8; if (bits >= 1e9) return (bits/1e9).toFixed(1)+' Gbit/s'; if (bits >= 1e6) return (bits/1e6).toFixed(1)+' Mbit/s'; if (bits >= 1e3) return (bits/1e3).toFixed(1)+' Kbit/s'; return Math.round(bits)+' bit/s'; } function duration(v) { return v == null ? '—' : v.toFixed(3)+' s'; } +function formatTimestamp(timestamp) { + return new Date(timestamp*1000).toLocaleString(undefined, {year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit', second:'2-digit', timeZoneName:'short'}); +} +function timeAgo(timestamp, now = Date.now()) { + const seconds = Math.max(0, Math.floor((now - timestamp*1000)/1000)); + const [value, unit] = seconds < 60 ? [seconds, 'second'] : seconds < 3600 ? [Math.floor(seconds/60), 'minute'] : seconds < 86400 ? [Math.floor(seconds/3600), 'hour'] : [Math.floor(seconds/86400), 'day']; + return `${value} ${unit}${value === 1 ? '' : 's'} ago`; +} function applyTheme(value) { const actual = value === 'system' ? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : value; document.documentElement.dataset.theme = actual; document.documentElement.style.colorScheme = actual; if (typeof trafficChart !== 'undefined') { trafficChart.options.scales.x.grid.color = getComputedStyle(document.documentElement).getPropertyValue('--chart-grid'); hostChart.options.scales.x.grid.color = trafficChart.options.scales.x.grid.color; trafficChart.update(); hostChart.update(); } } -function metric(label, value) { return `
${label}
${value}
`; } -function renderMetrics(s) { const status = s.status.reduce((a,b)=>a+b,0); const errors = status ? ((s.status[2]+s.status[3])/status*100).toFixed(1)+'%' : '0.0%'; $('metrics').innerHTML = metric('Inbound', bytesPerSecond(s.incoming))+metric('Outbound', bytesPerSecond(s.outgoing))+metric('Requests', s.requests.toFixed(1)+'/sec')+metric('Transferred', `${formatBytes(s.total_incoming+s.total_outgoing)}`)+metric('Avg latency', duration(s.latency))+metric('p95 latency', duration(s.p95))+metric('Upstream avg', duration(s.upstream))+metric('Errors (4xx/5xx)', errors)+metric('Active hosts', s.active_hosts)+metric('Busiest host', escapeHtml(s.busiest_host || '—')); } +function metric(label, value, wide = false) { return `
${label}
${value}
`; } +function renderMetrics(s) { const status = s.status.reduce((a,b)=>a+b,0); const errors = status ? ((s.status[2]+s.status[3])/status*100).toFixed(1)+'%' : '0.0%'; $('metrics').innerHTML = metric('Inbound', bytesPerSecond(s.incoming))+metric('Outbound', bytesPerSecond(s.outgoing))+metric('Requests', s.requests.toFixed(1)+'/sec')+metric('Transferred', `${formatBytes(s.total_incoming+s.total_outgoing)}`)+metric('Avg latency', duration(s.latency))+metric('p95 latency', duration(s.p95))+metric('Upstream avg', duration(s.upstream))+metric('Errors (4xx/5xx)', errors)+metric('Active hosts', s.active_hosts)+metric('Busiest host', escapeHtml(s.busiest_host || '—'), true); } function formatBytes(n) { const units=['B','KB','MB','GB','TB']; let i=0; while(n>=1000 && ibytesPerSecond(v)}}}, plugins:{legend:{display:true}}}}); } const trafficChart = makeChart('traffic-chart',[{label:'Inbound',data:[],borderColor:'#3273dc',backgroundColor:'#3273dc',tension:.25,pointRadius:0},{label:'Outbound',data:[],borderColor:'#23d160',backgroundColor:'#23d160',tension:.25,pointRadius:0}]); @@ -87,7 +97,7 @@ function escapeHtml(s) { return s.replace(/[&<>'"]/g,c=>({'&':'&','<':'<' let openedRequest = null, openerId = null, modalGeneration = 0; function requestFields(record) { return [ - ['Request ID', String(record.id)], ['Completed at', new Date(record.timestamp*1000).toISOString()], + ['Request ID', String(record.id)], ['Completed at', formatTimestamp(record.timestamp)], ['Host', record.host], ['Client address', record.client_address || 'Unavailable'], ['Method', record.method], ['Path', record.path || 'Unavailable'], ['Status', String(record.status)], ['Direction', record.direction === 'inbound' ? 'Inbound' : 'Outbound'], @@ -131,6 +141,12 @@ function renderRequests(data) { } row.lastElementChild.title = `${record.size} bytes`; row.lastElementChild.className = 'request-size'; + const completed = document.createElement('td'); + completed.dataset.timestamp = String(record.timestamp); + completed.textContent = timeAgo(record.timestamp); + completed.title = formatTimestamp(record.timestamp); + completed.style.whiteSpace = 'nowrap'; + row.append(completed); row.onclick = () => openRequest(record); row.onkeydown = event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openRequest(record); } }; return row; @@ -141,6 +157,12 @@ function renderRequests(data) { (rows.find(row => row.dataset.requestId === focusedId) || $('top-requests-title')).focus(); } } +setInterval(() => { + const now = Date.now(); + document.querySelectorAll('#request-rows [data-timestamp]').forEach(cell => { + cell.textContent = timeAgo(Number(cell.dataset.timestamp), now); + }); +}, 1000); $('request-close').onclick = () => $('request-dialog').close(); $('request-dialog').addEventListener('click', event => { const bounds = $('request-dialog').getBoundingClientRect();