Updates
CI / test (push) Successful in 1m6s
Release rolling / release (push) Successful in 7m30s

This commit is contained in:
Raven Scott
2026-07-18 21:15:49 -04:00
parent 700e621882
commit 41623bae68
3 changed files with 133 additions and 29 deletions
+10 -10
View File
@@ -80,11 +80,11 @@
<!-- Overview -->
<section id="overview-view" class="view">
<header class="page-header">
<header class="page-header overview-header">
<div>
<p class="dash-kicker">Live</p>
<h1 class="dash-title">Overview</h1>
<p class="page-subtitle">Real-time host metrics from the active agent</p>
<p class="page-subtitle">Active agent at a glance</p>
</div>
<div class="page-actions">
<button type="button" id="btn-refresh-meta" class="ghost">Refresh</button>
@@ -122,13 +122,13 @@
<ul id="fleet-children" class="fleet-children"></ul>
</section>
<section class="charts-grid">
<section class="charts-grid overview-charts">
<article class="dash-card chart-panel" data-chart="system.cpu" id="panel-cpu">
<header>
<h3>CPU</h3>
<span class="muted" id="chart-cpu-label">system.cpu</span>
</header>
<canvas id="chart-cpu" height="160"></canvas>
<canvas id="chart-cpu" height="110"></canvas>
<div class="chart-legend" id="legend-cpu"></div>
</article>
<article class="dash-card chart-panel" data-chart="mem.available" id="panel-ram">
@@ -136,28 +136,28 @@
<h3>Memory</h3>
<span class="muted">system.ram</span>
</header>
<canvas id="chart-ram" height="160"></canvas>
<canvas id="chart-ram" height="110"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.net" id="panel-net">
<header>
<h3>Network</h3>
<span class="muted">system.net</span>
</header>
<canvas id="chart-net" height="160"></canvas>
<canvas id="chart-net" height="110"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.io" id="panel-io">
<header>
<h3>Disk I/O</h3>
<span class="muted">system.io</span>
</header>
<canvas id="chart-io" height="160"></canvas>
<canvas id="chart-io" height="110"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="system.load" id="panel-load">
<header>
<h3>Load</h3>
<span class="muted">system.load</span>
</header>
<canvas id="chart-load" height="160"></canvas>
<canvas id="chart-load" height="110"></canvas>
</article>
<article class="dash-card chart-panel" data-chart="explore" id="panel-explore">
<header>
@@ -168,7 +168,7 @@
</select>
</label>
</header>
<canvas id="chart-explore" height="160"></canvas>
<canvas id="chart-explore" height="110"></canvas>
</article>
</section>
</section>
@@ -179,7 +179,7 @@
<div>
<p class="dash-kicker">Metrics</p>
<h1 class="dash-title">Charts</h1>
<p class="page-subtitle">Sectioned wall · shared time · <kbd>Space</kbd> · <kbd>/</kbd> · <kbd>15</kbd></p>
<p class="page-subtitle">Scroll freely · <kbd></kbd>/<kbd></kbd>+scroll zooms · <kbd>Space</kbd> · <kbd>/</kbd> · <kbd>15</kbd></p>
</div>
</header>
<div class="metrics-toolbar" id="metrics-timebar">
+48 -11
View File
@@ -111,7 +111,7 @@ export function createMetricsDashboard(opts) {
observer: /** @type {IntersectionObserver|null} */ (null),
built: false,
refetchTimer: /** @type {ReturnType<typeof setTimeout>|null} */ (null),
pan: /** @type {{ active: boolean, startX: number, startOffset: number }|null} */ (null),
pan: /** @type {{ active: boolean, pending?: boolean, pointerId?: number, startX: number, startY?: number, startOffset: number }|null} */ (null),
/** @type {Map<string, number>} in-flight generation per chart */
fetchGen: new Map(),
/** Seconds of history available across catalog (0 = unknown). */
@@ -792,6 +792,8 @@ export function createMetricsDashboard(opts) {
}
function bindCanvasInteractions(canvas, chartId) {
canvas.title = 'Scroll to browse · Ctrl/⌘ or Shift + scroll to zoom · drag horizontally to pan'
canvas.addEventListener('mousemove', (ev) => {
if (state.pan?.active) return
const idx = hoverIndexFromEvent(canvas, ev.clientX, {
@@ -809,31 +811,67 @@ export function createMetricsDashboard(opts) {
updateHoverReadout()
for (const id of state.visible) paintCard(id)
})
// Natural wall scroll by default. Zoom only with an intentional modifier
// (otherwise every chart canvas traps the wheel and scrolling feels stuck).
canvas.addEventListener(
'wheel',
(ev) => {
const card = state.cards.get(chartId)
if (normalizeChartMode(card?.mode) === 'pie') return
const zoomIntent = ev.ctrlKey || ev.metaKey || ev.shiftKey
if (!zoomIntent) return // let .metrics-wall / #content scroll
ev.preventDefault()
const factor = ev.deltaY > 0 ? 1.25 : 0.8
setWindow(Math.round(state.afterSeconds * factor), state.endOffset)
},
{ passive: false }
)
canvas.addEventListener('pointerdown', (ev) => {
if (ev.button !== 0) return
if (state.forcePlay) return
const card = state.cards.get(chartId)
if (normalizeChartMode(card?.mode) === 'pie') return
// Drag to pan time (pause + shift window end)
// Defer pan until a clear horizontal drag — avoids fighting trackpad scroll.
state.pan = {
active: false,
pending: true,
pointerId: ev.pointerId,
startX: ev.clientX,
startY: ev.clientY,
startOffset: state.endOffset,
}
try {
canvas.setPointerCapture(ev.pointerId)
state.pan = { active: true, startX: ev.clientX, startOffset: state.endOffset }
setPlaying(false)
canvas.classList.add('panning')
} catch {
// ignore
}
})
canvas.addEventListener('pointermove', (ev) => {
if (!state.pan?.active) return
if (!state.pan) return
const dx = ev.clientX - state.pan.startX
const dy = ev.clientY - state.pan.startY
if (state.pan.pending) {
if (Math.hypot(dx, dy) < 8) return
// Vertical-dominant gesture → cancel pan so scroll/selection feels natural
if (Math.abs(dy) > Math.abs(dx) * 1.15) {
state.pan = null
try {
canvas.releasePointerCapture(ev.pointerId)
} catch {
// ignore
}
return
}
state.pan.pending = false
state.pan.active = true
setPlaying(false)
canvas.classList.add('panning')
}
if (!state.pan.active) return
const pxPerSec = Math.max(2, canvas.clientWidth / state.afterSeconds)
const deltaSec = Math.round(-dx / pxPerSec)
state.endOffset = Math.max(0, state.pan.startOffset + deltaSec)
@@ -843,20 +881,19 @@ export function createMetricsDashboard(opts) {
updateHoverReadout()
})
const endPan = (ev) => {
if (!state.pan?.active) return
state.pan.active = false
if (!state.pan) return
const wasActive = state.pan.active
state.pan = null
canvas.classList.remove('panning')
try {
canvas.releasePointerCapture(ev.pointerId)
} catch {
// ignore
}
scheduleRefetchVisible()
state.pan = null
if (wasActive) scheduleRefetchVisible()
}
canvas.addEventListener('pointerup', endPan)
canvas.addEventListener('pointercancel', endPan)
void chartId
}
function render() {
+74 -7
View File
@@ -669,33 +669,83 @@ body.is-offline .offline-banner:not(.hidden) {
box-shadow: var(--shadow-sm);
}
/* Overview — compact home that fits the viewport */
#overview-view .overview-header {
margin-bottom: var(--space);
align-items: center;
}
#overview-view .dash-title {
font-size: clamp(1.35rem, 2vw, 1.7rem);
}
#overview-view .dash-kpis {
margin-bottom: var(--space-sm);
}
#overview-view .dash-kpi {
min-height: 64px;
padding: 10px 12px;
}
#overview-view .dash-kpi strong {
font-size: clamp(1.15rem, 1.6vw, 1.4rem);
}
#overview-view .fleet-strip {
margin-bottom: var(--space-sm);
padding: 10px 12px;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space);
}
.overview-charts {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
#overview-view .chart-panel {
padding: 10px 12px 8px;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 0;
}
.chart-panel header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
margin-bottom: 6px;
flex-shrink: 0;
}
.chart-panel h3 {
margin: 0;
font-size: 14px;
font-size: 13px;
font-weight: 650;
}
.chart-panel canvas {
width: 100%;
display: block;
border-radius: 10px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.18);
}
/* Lock overview spark heights — width:100% alone was stretching them huge */
#overview-view .chart-panel canvas {
height: 100px !important;
max-height: 100px;
flex: 0 0 100px;
}
html[data-theme='light'] .chart-panel canvas {
background: rgba(15, 23, 42, 0.03);
}
@@ -703,10 +753,12 @@ html[data-theme='light'] .chart-panel canvas {
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
margin-top: 8px;
font-size: 11px;
gap: 6px 10px;
margin-top: 6px;
font-size: 10px;
color: var(--text-muted);
max-height: 28px;
overflow: hidden;
}
.chart-legend span::before {
@@ -1349,6 +1401,8 @@ button.metrics-tf:disabled {
display: block;
border-radius: 8px;
background: rgba(0, 0, 0, 0.18);
/* Let vertical trackpad/mouse wheel scroll the wall; pan is horizontal-drag only */
touch-action: pan-y;
}
html[data-theme='light'] .metric-card canvas {
@@ -2130,11 +2184,16 @@ code {
}
/* ─── Responsive ─── */
@media (max-width: 1280px) {
.overview-charts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 1100px) {
.dash-kpis {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.charts-grid,
.charts-browser,
.metrics-shell {
grid-template-columns: 1fr;
@@ -2155,6 +2214,11 @@ code {
.metrics-toolbar-right {
margin-left: 0;
}
#overview-view .chart-panel canvas {
height: 96px !important;
max-height: 96px;
flex-basis: 96px;
}
}
@media (max-width: 720px) {
@@ -2170,6 +2234,9 @@ code {
.dash-kpis {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.overview-charts {
grid-template-columns: 1fr;
}
.fleet-summary-bar {
grid-template-columns: 1fr 1fr;
}