Updates
This commit is contained in:
+10
-10
@@ -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>1–5</kbd></p>
|
||||
<p class="page-subtitle">Scroll freely · <kbd>⌃</kbd>/<kbd>⇧</kbd>+scroll zooms · <kbd>Space</kbd> · <kbd>/</kbd> · <kbd>1–5</kbd></p>
|
||||
</div>
|
||||
</header>
|
||||
<div class="metrics-toolbar" id="metrics-timebar">
|
||||
|
||||
+49
-12
@@ -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)
|
||||
canvas.setPointerCapture(ev.pointerId)
|
||||
state.pan = { active: true, startX: ev.clientX, startOffset: state.endOffset }
|
||||
setPlaying(false)
|
||||
canvas.classList.add('panning')
|
||||
// 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)
|
||||
} 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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user