mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
Backend, scripts, docs and their tests take main's newer code where both sides changed the same lines (the shared blob cache, GitHubStream and markdown fixes landed on main after the Go migration branch was cut). The Dockerfile keeps the Go build from the migration, plus main's owner-alert-protocol label; .gitignore keeps both sides' entries. Frontend keeps the redesign and folds in main's features: Stripe billing in the New anonymization submit bar, GitHub access repair, redaction syntax help, the preview expand toggle, configurable redaction mask, folder counts, reviewer-route header rules, link expiry (now in the viewer's snapshot details) and the explorer error card. main's narrow-only wrap logic is dropped because the redesign wraps on desktop and scrolls sideways on phones. Tests: 1534 passing with SESSION_SECRET set (as in CI). Three main tests were updated for the agreed viewer design (no site header in the viewer, no repository-wide file count). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
413 lines
32 KiB
HTML
413 lines
32 KiB
HTML
<div class="container paper-page admin-page overview-page">
|
|
<div class="paper-crumbs"><a href="/admin/">Admin</a> / <span class="here">Overview</span></div>
|
|
|
|
<div class="ov-header">
|
|
<h1 class="paper-page-title">Overview</h1>
|
|
<span class="ov-live"><span class="ov-live-dot" aria-hidden="true"></span>Live<span class="ov-live-extra"> · refreshes every 30s</span></span>
|
|
</div>
|
|
|
|
<nav class="admin-nav">
|
|
<a href="/admin/" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-chart"></use></svg> Overview</a>
|
|
<a href="/admin/repositories"><svg class="ui-icon" aria-hidden="true"><use href="#project-type-repo"></use></svg> Repositories</a>
|
|
<a href="/admin/users"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-users"></use></svg> Users</a>
|
|
<a href="/admin/conferences"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-conference"></use></svg> Conferences</a>
|
|
<a href="/admin/queues"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-list"></use></svg> Queues</a>
|
|
<a href="/admin/errors"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
|
|
</nav>
|
|
|
|
<div class="admin-empty" v-if="loading">Loading overview…</div>
|
|
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ error }}</div>
|
|
|
|
|
|
<div v-if="data">
|
|
|
|
<div class="ov-health" role="status" v-if="data?.system?.memPercent > 90 || data?.system?.diskPercent > 90">
|
|
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
|
<span><b>Server needs attention.</b> Memory or disk is running high (see the cards below). New anonymizations may fail until it drops.</span>
|
|
</div>
|
|
|
|
<!-- ── Top KPI row ──────────────────────────────────────────── -->
|
|
<section class="ov-kpi-row">
|
|
<div class="ov-kpi-card">
|
|
<div class="ov-kpi-label">Live repositories</div>
|
|
<div class="ov-kpi-value">{{ humanNum((data?.repos?.statusBreakdown || []).filter(s => s._id !== 'removed' && s._id !== 'expired').reduce((a, s) => a + (s.count || 0), 0)) }}</div>
|
|
<div class="ov-kpi-sub">{{ humanNum(((data?.repos?.statusBreakdown || []).find(s => s._id === 'removed') || {}).count || 0) }} removed · {{ humanNum(data?.repos?.total) }} total</div>
|
|
</div>
|
|
<div class="ov-kpi-card">
|
|
<div class="ov-kpi-label">CPU</div>
|
|
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.cpuPercent > 80}">{{ data?.system?.cpuPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.cpuPercent > 80, crit: data?.system?.cpuPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.cpuPercent || 0) + '%' }"></span></div>
|
|
<div class="ov-kpi-sub">{{ data?.system?.cpuCount }} cores · load {{ data?.system?.loadAvg?.[0]?.toFixed(1) }}</div>
|
|
</div>
|
|
<div class="ov-kpi-card">
|
|
<div class="ov-kpi-label">Memory</div>
|
|
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.memPercent > 85}">{{ data?.system?.memPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.memPercent > 85, crit: data?.system?.memPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.memPercent || 0) + '%' }"></span></div>
|
|
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.memPercent > 90 }" v-if="data?.system?.memPercent > 85">{{ data?.system?.memPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.memUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.memTotal) }}</div>
|
|
</div>
|
|
<div class="ov-kpi-card">
|
|
<div class="ov-kpi-label">Disk</div>
|
|
<div class="ov-kpi-value" :class="{'ov-val-warn': data?.system?.diskPercent > 85}">{{ data?.system?.diskPercent }}%</div><div class="ov-meter" :class="{ warn: data?.system?.diskPercent > 85, crit: data?.system?.diskPercent > 90 }"><span :style="{ width: Math.min(100, data?.system?.diskPercent || 0) + '%' }"></span></div>
|
|
<div class="ov-kpi-sub"><b class="ov-flag" :class="{ critical: data?.system?.diskPercent > 90 }" v-if="data?.system?.diskPercent > 85">{{ data?.system?.diskPercent > 90 ? 'Critical' : 'High' }} · </b>{{ humanBytes(data?.system?.diskUsed).replace(/\s*GB$/, '') }} / {{ humanBytes(data?.system?.diskTotal) }}<template v-if="data?.system?.diskMount && data.system.diskMount !== '/'"> · {{ data?.system?.diskMount }}</template></div>
|
|
</div>
|
|
<div class="ov-kpi-card">
|
|
<div class="ov-kpi-label">Uptime</div>
|
|
<div class="ov-kpi-value">{{ humanDuration(data?.system?.uptime) }}</div>
|
|
<div class="ov-kpi-sub">{{ data?.system?.nodeVersion }} · {{ data?.system?.platform }}</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Daily activity highlights ───────────────────────────── -->
|
|
<section class="ov-daily-row">
|
|
<div class="ov-daily-card">
|
|
<div class="ov-daily-label">New repos today</div>
|
|
<div class="ov-daily-value">{{ fmt?.number(data?.daily?.today?.repositories || 0) }}</div>
|
|
<div class="ov-daily-sub">so far today</div>
|
|
</div>
|
|
<div class="ov-daily-card">
|
|
<div class="ov-daily-label">New users today</div>
|
|
<div class="ov-daily-value">{{ fmt?.number(data?.daily?.today?.users || 0) }}</div>
|
|
<div class="ov-daily-sub">{{ fmt?.number(data?.users?.total) }} total users</div>
|
|
</div>
|
|
<div class="ov-daily-card">
|
|
<div class="ov-daily-label">Page views today</div>
|
|
<div class="ov-daily-value">{{ fmt?.number(data?.daily?.today?.pageViews || 0) }}</div>
|
|
<div class="ov-daily-sub">so far today</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Daily charts ─────────────────────────────────────────── -->
|
|
<section class="ov-chart-row">
|
|
<div class="ov-chart-card">
|
|
<div class="ov-chart-head">
|
|
<span class="ov-chart-title">Daily page views · 30d</span>
|
|
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-accent"></span>views/day · peak {{ fmt?.number(historyMax('dailyPageViews')) }}</span>
|
|
</div>
|
|
<div class="ov-spark-bars" v-if="data?.history?.length">
|
|
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyPageViews)) + " views"">
|
|
<span class="ov-spark-fill" :style="{height: historyBarH(d, 'dailyPageViews') + 'px'}"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-spark-x" v-if="data?.history?.length">
|
|
<span>{{ historyLabel(data?.history[0]) }}</span>
|
|
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
|
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-chart-card">
|
|
<div class="ov-chart-head">
|
|
<span class="ov-chart-title">New repos · 30d</span>
|
|
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-ok-fill"></span>repos/day · peak {{ fmt?.number(historyMax('dailyRepositories')) }}</span>
|
|
</div>
|
|
<div class="ov-spark-bars" v-if="data?.history?.length">
|
|
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyRepositories)) + " repos"">
|
|
<span class="ov-spark-fill ov-spark-fill-alt" :style="{height: historyBarH(d, 'dailyRepositories') + 'px'}"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-spark-x" v-if="data?.history?.length">
|
|
<span>{{ historyLabel(data?.history[0]) }}</span>
|
|
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
|
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-chart-card">
|
|
<div class="ov-chart-head">
|
|
<span class="ov-chart-title">Users · 30d</span>
|
|
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-user-fill"></span>total users · {{ fmt?.number(historyRange('nbUsers').min) }}–{{ fmt?.number(historyRange('nbUsers').max) }}</span>
|
|
</div>
|
|
<div class="ov-spark-bars" v-if="data?.history?.length">
|
|
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"" :aria-label="(historyLabel(d)) + ": " + (fmt?.number(d?.nbUsers)) + " users"">
|
|
<span class="ov-spark-fill ov-spark-fill-user" :style="{height: historyRangeH(d, 'nbUsers') + 'px'}"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-spark-x" v-if="data?.history?.length">
|
|
<span>{{ historyLabel(data?.history[0]) }}</span>
|
|
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
|
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-chart-card">
|
|
<div class="ov-chart-head">
|
|
<span class="ov-chart-title">New users · 30d</span>
|
|
<span class="ov-chart-legend"><span class="ov-dot-legend ov-dot-new-user-fill"></span>users/day · peak {{ fmt?.number(historyMax('dailyUsers')) }}</span>
|
|
</div>
|
|
<div class="ov-spark-bars" v-if="data?.history?.length">
|
|
<div class="ov-spark-col has-tip" v-for="(d, index) in data?.history" :key="index" :data-tip="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"" :aria-label="(historyLabel(d)) + ": +" + (fmt?.number(d?.dailyUsers)) + " users"">
|
|
<span class="ov-spark-fill ov-spark-fill-new-user" :style="{height: historyBarH(d, 'dailyUsers') + 'px'}"></span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-spark-x" v-if="data?.history?.length">
|
|
<span>{{ historyLabel(data?.history[0]) }}</span>
|
|
<span>{{ historyLabel(data?.history[Math?.floor(data?.history?.length/2)]) }}</span>
|
|
<span>{{ historyLabel(data?.history[data?.history?.length - 1]) }}</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- ── Three-panel row: Status / Errors / Queues ────────────── -->
|
|
<section class="ov-triple-row">
|
|
|
|
<!-- Repo status breakdown -->
|
|
<div class="ov-panel-card">
|
|
<div class="ov-panel-head">
|
|
<span class="ov-panel-title">Repo status</span>
|
|
<span class="ov-panel-meta">{{ fmt?.number(data?.repos?.total) }} total</span>
|
|
</div>
|
|
<div class="ov-stacked-bar">
|
|
<span class="ov-bar-seg ov-bar-ready" title="Ready" :style="{width: barPct('ready') + '%'}"></span>
|
|
<span class="ov-bar-seg ov-bar-preparing" title="Preparing" :style="{width: barPct('preparing') + '%'}"></span>
|
|
<span class="ov-bar-seg ov-bar-error" title="Error" :style="{width: barPct('error') + '%'}"></span>
|
|
<span class="ov-bar-seg ov-bar-expired" title="Expired" :style="{width: barPct('expired') + '%'}"></span>
|
|
<span class="ov-bar-seg ov-bar-removed" title="Removed" :style="{width: barPct('removed') + '%'}"></span>
|
|
</div>
|
|
<div class="ov-bar-legend">
|
|
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-ready"></span> ready <span class="ov-legend-n">{{ fmt?.number(statusCount('ready')) }}</span></a>
|
|
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-preparing"></span> preparing <span class="ov-legend-n">{{ fmt?.number(statusCount('preparing') + statusCount('download')) }}</span></a>
|
|
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-error"></span> error <span class="ov-legend-n">{{ fmt?.number(statusCount('error')) }}</span></a>
|
|
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-expired"></span> expired <span class="ov-legend-n">{{ fmt?.number(statusCount('expired') + statusCount('expiring')) }}</span></a>
|
|
<a href="/admin/repositories" class="ov-legend-item"><span class="ov-swatch ov-bar-removed"></span> removed <span class="ov-legend-n">{{ fmt?.number(statusCount('removed') + statusCount('removing')) }}</span></a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error breakdown -->
|
|
<div class="ov-panel-card">
|
|
<div class="ov-panel-head">
|
|
<span class="ov-panel-title">Errors · 24h</span>
|
|
<span class="ov-panel-meta">{{ data?.errors?.last24h }} total</span>
|
|
</div>
|
|
<div class="ov-error-bars">
|
|
<div class="ov-ebar-row">
|
|
<span class="ov-ebar-label">5xx</span>
|
|
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-error" :style="{width: errPct('error') + '%'}"></span></span>
|
|
<span class="ov-ebar-n">{{ data?.errors?.severity?.error }}</span>
|
|
</div>
|
|
<div class="ov-ebar-row">
|
|
<span class="ov-ebar-label">4xx</span>
|
|
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-warn" :style="{width: errPct('warn') + '%'}"></span></span>
|
|
<span class="ov-ebar-n">{{ data?.errors?.severity?.warn }}</span>
|
|
</div>
|
|
<div class="ov-ebar-row">
|
|
<span class="ov-ebar-label">Info</span>
|
|
<span class="ov-ebar-track"><span class="ov-ebar-fill ov-ebar-info" :style="{width: errPct('info') + '%'}"></span></span>
|
|
<span class="ov-ebar-n">{{ data?.errors?.severity?.info }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-panel-foot" v-if="data?.repos?.recentErrors24h">
|
|
<a href="/admin/errors">{{ data?.repos?.recentErrors24h }} repos in error state →</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Top routes / queues -->
|
|
<div class="ov-panel-card">
|
|
<div class="ov-panel-head">
|
|
<span class="ov-panel-title">Queues</span>
|
|
<span class="ov-panel-meta">by state</span>
|
|
</div>
|
|
<div class="ov-routes-table">
|
|
<div class="ov-route-row ov-route-head">
|
|
<span class="ov-route-name">Queue</span>
|
|
<span class="ov-route-n">Active</span>
|
|
<span class="ov-route-n">Wait</span>
|
|
<span class="ov-route-n ov-route-lat">Failed</span>
|
|
</div>
|
|
<a class="ov-route-row" href="/admin/queues">
|
|
<span class="ov-route-name">download</span>
|
|
<span class="ov-route-n">{{ data?.queues?.download?.active }}</span>
|
|
<span class="ov-route-n">{{ data?.queues?.download?.waiting }}</span>
|
|
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.download?.failed > 0}">{{ data?.queues?.download?.failed }}</span>
|
|
</a>
|
|
<a class="ov-route-row" href="/admin/queues">
|
|
<span class="ov-route-name">remove</span>
|
|
<span class="ov-route-n">{{ data?.queues?.remove?.active }}</span>
|
|
<span class="ov-route-n">{{ data?.queues?.remove?.waiting }}</span>
|
|
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.remove?.failed > 0}">{{ data?.queues?.remove?.failed }}</span>
|
|
</a>
|
|
<a class="ov-route-row" href="/admin/queues">
|
|
<span class="ov-route-name">cache</span>
|
|
<span class="ov-route-n">{{ data?.queues?.cache?.active }}</span>
|
|
<span class="ov-route-n">{{ data?.queues?.cache?.waiting }}</span>
|
|
<span class="ov-route-n ov-route-lat" :class="{'ov-n-bad': data?.queues?.cache?.failed > 0}">{{ data?.queues?.cache?.failed }}</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</section>
|
|
|
|
<!-- ── Services bar ─────────────────────────────────────────── -->
|
|
<section class="ov-services-card">
|
|
<div class="ov-services-head">
|
|
<span class="ov-panel-title">Services</span>
|
|
<span class="ov-panel-meta">{{ fmt?.number(data?.users?.total) }} users · {{ data?.conferences?.total }} conferences</span>
|
|
</div>
|
|
<div class="ov-services-grid">
|
|
<div class="ov-svc">
|
|
<span class="ov-svc-dot ov-dot-ok"></span>
|
|
<div class="ov-svc-info">
|
|
<span class="ov-svc-name">web</span>
|
|
<span class="ov-svc-meta">{{ data?.system?.nodeVersion }}</span>
|
|
</div>
|
|
<span class="ov-svc-detail">uptime {{ humanDuration(data?.system?.uptime) }}</span>
|
|
</div>
|
|
<div class="ov-svc">
|
|
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.download) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
|
<div class="ov-svc-info">
|
|
<span class="ov-svc-name">download</span>
|
|
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.download)">{{ queueTotal(data?.queues?.download) }} jobs</span>
|
|
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.download)">idle</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-svc">
|
|
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.cache) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
|
<div class="ov-svc-info">
|
|
<span class="ov-svc-name">cache</span>
|
|
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.cache)">{{ queueTotal(data?.queues?.cache) }} jobs</span>
|
|
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.cache)">idle</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-svc">
|
|
<span class="ov-svc-dot" :class="queueTotal(data?.queues?.remove) > 0 ? 'ov-dot-ok' : 'ov-dot-idle'"></span>
|
|
<div class="ov-svc-info">
|
|
<span class="ov-svc-name">remove</span>
|
|
<span class="ov-svc-meta" v-if="queueTotal(data?.queues?.remove)">{{ queueTotal(data?.queues?.remove) }} jobs</span>
|
|
<span class="ov-svc-meta" v-if="!queueTotal(data?.queues?.remove)">idle</span>
|
|
</div>
|
|
</div>
|
|
<div class="ov-svc">
|
|
<span class="ov-svc-dot" :class="data?.repos?.recentErrors24h > 10 ? 'ov-dot-warn' : 'ov-dot-ok'"></span>
|
|
<div class="ov-svc-info">
|
|
<span class="ov-svc-name">errors</span>
|
|
<span class="ov-svc-meta">{{ data?.errors?.last24h }} / 24h</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
|
|
<section class="ov-panel-card ov-cache" aria-labelledby="cache-heading">
|
|
<div class="ov-panel-head ov-performance-head">
|
|
<div><h2 class="ov-panel-title" id="cache-heading">Cache</h2>
|
|
<p class="ov-performance-note">Shared storage, repeated reads and anonymization work · refreshes every 30 seconds</p></div>
|
|
<div class="ov-performance-window">
|
|
<label for="cache-window">Time range</label>
|
|
<select id="cache-window" class="form-control form-control-sm" v-model.number="cacheMinutes" @change="loadCache()">
|
|
<option :value="15">Last 15 minutes</option><option :value="60">Last hour</option>
|
|
<option :value="1440">Last 24 hours</option><option :value="10080">Last 7 days</option>
|
|
</select>
|
|
<label for="cache-traffic">Traffic</label>
|
|
<select id="cache-traffic" class="form-control form-control-sm" v-model="cacheTraffic">
|
|
<option value="all">All traffic</option><option value="file">File reads</option><option value="website">Website rendering</option>
|
|
<option value="zip">ZIP downloads</option><option value="monitor">Known monitors</option><option value="background">Background work</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div v-if="cacheError" class="ov-performance-message ov-performance-warning" role="alert">
|
|
<span>{{ cacheError }}</span><button type="button" class="btn btn-sm" @click="loadCache()">Retry</button>
|
|
</div>
|
|
<p v-if="!cacheReport && !cacheError" class="ov-performance-empty" role="status">Loading cache monitoring…</p>
|
|
<template v-if="cacheReport?.available">
|
|
<p v-if="!cacheReport.enabled" class="ov-performance-note">Shared blob storage is disabled. Inspection and legacy-cache activity are still measured.</p>
|
|
<p v-if="!cacheReport.filesystemSupported" class="ov-performance-note">Shared blobs require a local filesystem. The configured storage continues to use its existing cache.</p>
|
|
<p v-if="cacheReport.dropped" class="ov-performance-message ov-performance-warning" role="status">{{ cacheReport.dropped }} telemetry events were dropped. Totals may be incomplete.</p>
|
|
<div class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Cache storage budgets" v-if="cacheReport.source">
|
|
<table class="ov-metrics-table">
|
|
<caption class="sr-only">Current shared-cache storage and byte budgets</caption>
|
|
<thead><tr><th scope="col">Pool</th><th scope="col">Allocated / budget</th><th scope="col">Reserved for fills</th><th scope="col">Original bytes</th><th scope="col">Blobs</th><th scope="col">Active readers</th></tr></thead>
|
|
<tbody><tr v-for="pool in [{name: 'Source blobs', data: cacheReport.source}, {name: 'Transformed output', data: cacheReport.results}]" :key="pool.name" v-show="pool.data">
|
|
<th scope="row">{{ pool.name }}</th><td>{{ humanBytes(pool.data?.allocatedBytes) }} / {{ humanBytes(pool.data?.budgetBytes) }}</td>
|
|
<td>{{ humanBytes(pool.data?.reservedBytes) }}</td><td>{{ humanBytes(pool.data?.originalBytes) }}</td>
|
|
<td>{{ pool.data?.blobs }}</td><td>{{ pool.data?.activeReaders }}</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
<p class="ov-performance-note" v-if="cacheReport.source">Source request hit rate: {{ cacheRate('source', 'count') }} · Source byte hit rate: {{ cacheRate('source', 'bytes') }} · Decision hit rate: {{ cacheRate('decision', 'count') }}</p>
|
|
<p class="ov-performance-note">Reuse counts measure consecutive reads of the same blob within each interval. Codec timings are elapsed stream time, not CPU time; eviction times measure how long a blob was idle. Known monitors are identified by request metadata; other automation may remain in file reads.</p>
|
|
<div v-if="cacheRows().length" class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Cache events and processing durations">
|
|
<table class="ov-metrics-table">
|
|
<caption class="sr-only">Cache events for the selected time range and traffic</caption>
|
|
<thead><tr><th scope="col">Event</th><th scope="col">Traffic</th><th scope="col">Count</th><th scope="col">Bytes</th><th scope="col">Mean measured time</th><th scope="col">p95 bound</th></tr></thead>
|
|
<tbody><tr v-for="row in cacheRows()" :key="row.name + ':' + row.traffic">
|
|
<th scope="row">{{ cacheEventName(row.name) }}</th><td>{{ row.traffic }}</td><td>{{ row.count }}</td><td>{{ humanBytes(row.bytes) }}</td>
|
|
<td>{{ row.ms ? (row.ms / row.count).toFixed(2) + " ms" : "Not timed" }}</td><td>{{ cacheP95(row) }}</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
<p v-else class="ov-performance-empty" role="status">No cache events in this time range for the selected traffic.</p>
|
|
<p class="ov-performance-note">Updated {{ sampleAge(cacheReport) }} seconds ago. Minute history is retained for 48 hours; hourly history for 30 days.</p>
|
|
</template>
|
|
</section>
|
|
|
|
<section class="ov-panel-card ov-performance" aria-labelledby="performance-heading">
|
|
<div class="ov-panel-head ov-performance-head">
|
|
<div>
|
|
<h2 class="ov-panel-title" id="performance-heading">Request performance</h2>
|
|
<p class="ov-performance-note">Request latency and process health · refreshes every 30 seconds</p>
|
|
</div>
|
|
<div class="ov-performance-window">
|
|
<label for="performance-window">Time range</label>
|
|
<select id="performance-window" class="form-control form-control-sm" v-model.number="performanceMinutes" @change="loadPerformance()" aria-label="Performance time range">
|
|
<option :value="15">Last 15 minutes</option><option :value="60">Last hour</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div v-if="performanceError" class="ov-performance-message ov-performance-warning" role="alert">
|
|
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg>
|
|
<span>{{ performanceError }}</span>
|
|
<button type="button" class="btn btn-sm" @click="loadPerformance()">Retry</button>
|
|
</div>
|
|
<p v-if="!performance && !performanceError" class="ov-performance-empty" role="status">Loading performance monitoring…</p>
|
|
<div v-if="performance?.available">
|
|
<div class="paper-tabs ov-performance-tabs" role="tablist" aria-label="Performance metrics">
|
|
<button id="performance-routes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'routes'}" :aria-selected="performanceTab === 'routes'" :tabindex="performanceTab === 'routes' ? 0 : -1" aria-controls="performance-routes" @click="performanceTab = 'routes'" @keydown="previewTabKeydown($event)">Routes</button>
|
|
<button id="performance-processes-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'processes'}" :aria-selected="performanceTab === 'processes'" :tabindex="performanceTab === 'processes' ? 0 : -1" aria-controls="performance-processes" @click="performanceTab = 'processes'" @keydown="previewTabKeydown($event)">Processes</button>
|
|
<button id="performance-stages-tab" type="button" class="paper-tab" role="tab" :class="{active: performanceTab === 'stages'}" :aria-selected="performanceTab === 'stages'" :tabindex="performanceTab === 'stages' ? 0 : -1" aria-controls="performance-stages" @click="performanceTab = 'stages'" @keydown="previewTabKeydown($event)">Work stages</button>
|
|
</div>
|
|
<div id="performance-routes" role="tabpanel" aria-labelledby="performance-routes-tab" v-show="performanceTab === 'routes'">
|
|
<div class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Route latency metrics, scroll for more columns" v-if="performance.routes?.length">
|
|
<table class="ov-metrics-table">
|
|
<caption class="sr-only">Route latency for the selected time range</caption>
|
|
<thead><tr><th scope="col">Route</th><th scope="col" class="num">Requests</th><th scope="col" class="num">Average</th><th scope="col" class="num">First byte p95</th><th scope="col" class="num">p95</th><th scope="col" class="num">p99</th><th scope="col" class="num">Interrupted</th><th scope="col" class="num">5xx</th><th scope="col" class="num">Slow</th></tr></thead>
|
|
<tbody><tr v-for="row in performance.routes" :key="row.service + row.method + row.route">
|
|
<th scope="row" class="ov-metric-name"><span>{{ row.route }}</span><small>{{ row.service }} · {{ row.method }}</small></th>
|
|
<td class="num">{{ fmt.number(row.count) }}</td><td class="num">{{ formatMs(row.avgMs) }}</td>
|
|
<td class="num">{{ firstByteBound(row) }}</td><td class="num">{{ latencyBound(row, 'p95UpperMs') }}</td><td class="num">{{ latencyBound(row, 'p99UpperMs') }}</td>
|
|
<td class="num" :class="{'ov-metric-warn': row.aborted > 0}">{{ fmt.number(row.aborted) }}</td><td class="num" :class="{'ov-metric-warn': row.errors > 0}">{{ fmt.number(row.errors) }}</td><td class="num" :class="{'ov-metric-warn': row.slow > 0}">{{ fmt.number(row.slow) }}</td>
|
|
</tr></tbody>
|
|
</table>
|
|
</div>
|
|
<p v-else class="ov-performance-empty">No recorded requests in this time range yet.</p>
|
|
</div>
|
|
<div id="performance-processes" role="tabpanel" aria-labelledby="performance-processes-tab" v-show="performanceTab === 'processes'">
|
|
<p class="ov-performance-note">API and streamer processes · latest sample per instance</p>
|
|
<div v-if="!performance.instances?.length" class="ov-performance-message ov-performance-warning" role="status">No recent process samples. Monitoring coverage is incomplete.</div>
|
|
<div v-else class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Process resource metrics, scroll for more columns">
|
|
<table class="ov-metrics-table">
|
|
<caption class="sr-only">API and streamer process resource usage</caption>
|
|
<thead><tr><th scope="col">Process</th><th scope="col" class="num">RSS / limit</th><th scope="col" class="num">CPU</th><th scope="col" class="num">Event loop p95</th><th scope="col" class="num">Active requests</th><th scope="col" class="num">Sockets / CLOSE_WAIT</th><th scope="col" class="num">Workers / waiting</th><th scope="col" class="num">Sample age</th><th scope="col" class="num">Dropped batches / metrics</th></tr></thead>
|
|
<tbody><tr v-for="sample in performance.instances" :key="sample.instance" :class="{'ov-metric-warning-row': performanceWarning(sample)}">
|
|
<th scope="row" class="ov-metric-name"><span>{{ sample.instance }}</span><small class="ov-metric-warn" v-if="performanceWarning(sample)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> Needs attention</small></th>
|
|
<td class="num">{{ humanBytes(sample.memory.rss) }} / {{ humanBytes(sample.memoryLimitBytes) }}<small>Peak {{ humanBytes(peakRss(sample)) }}</small></td>
|
|
<td class="num">{{ sample.cpuPercent == null ? 'Unavailable' : sample.cpuPercent.toFixed(1) + '%' }}</td><td class="num">{{ sample.eventLoop?.p95Ms == null ? 'Not applicable' : sample.eventLoop.p95Ms.toFixed(1) + ' ms' }}</td>
|
|
<td class="num">{{ sample.activeRequests }}</td><td class="num">{{ sample.sockets?.descriptors ?? 'Unavailable' }} / {{ sample.sockets?.closeWait ?? 'Unavailable' }}</td>
|
|
<td class="num">{{ sample.workers?.running ?? 0 }} / {{ sample.workers?.waiting ?? 0 }}</td><td class="num">{{ sampleAge(sample) }} s</td>
|
|
<td class="num">{{ sample.droppedBatches }} / {{ sample.droppedMetrics }}</td>
|
|
</tr></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div id="performance-stages" role="tabpanel" aria-labelledby="performance-stages-tab" v-show="performanceTab === 'stages'">
|
|
<div v-if="performance.stages?.some(row => row.metric !== 'first_byte')" class="ov-metrics-scroll" tabindex="0" role="region" aria-label="Work stage metrics, scroll for more columns">
|
|
<table class="ov-metrics-table">
|
|
<caption class="sr-only">Work stages and cache results</caption>
|
|
<thead><tr><th scope="col">Work stage</th><th scope="col" class="num">Samples</th><th scope="col" class="num">Average</th><th scope="col" class="num">p95</th></tr></thead>
|
|
<tbody><tr v-for="row in performance.stages.filter(row => row.metric !== 'first_byte')" :key="row.service + row.metric">
|
|
<th scope="row" class="ov-metric-name"><span>{{ row.metric }}</span><small>{{ row.service }}</small></th><td class="num">{{ fmt.number(row.count) }}</td><td class="num">{{ formatMs(row.avgMs) }}</td><td class="num">{{ latencyBound(row, 'p95UpperMs') }}</td>
|
|
</tr></tbody>
|
|
</table>
|
|
</div>
|
|
<p v-else class="ov-performance-empty">No recorded work stages or cache results in this time range yet.</p>
|
|
</div>
|
|
<details class="ov-performance-help">
|
|
<summary>How to read these metrics</summary>
|
|
<p>Latency includes upstream waits and response transfer. Percentiles show histogram bucket upper bounds, not exact timings. Process rows marked “Needs attention” have stale samples, dropped monitoring data, CLOSE_WAIT sockets, high event loop delay, or high memory usage.</p>
|
|
</details>
|
|
</div>
|
|
</section>
|
|
</div>
|