Files
anonymous_github/public/partials/admin/errors.htm
T
Thomas DurieuxandClaude Opus 5.5 c685d833ab Merge origin/main into feat/ui-redesign
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>
2026-11-08 14:07:33 +00:00

200 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="container paper-page admin-page errors-page">
<div class="paper-crumbs">Admin &nbsp;/&nbsp; <span class="here">Errors</span></div>
<header class="errors-header">
<h1 class="paper-page-title">Errors</h1>
<div class="errors-actions">
<button class="btn btn-sm" type="button" @click="exportCsv()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-export"></use></svg> Export CSV</button>
<button class="btn btn-sm btn-outline-danger" type="button" @click="clearAll()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Clear all</button>
</div>
</header>
<nav class="admin-nav">
<a href="/admin/"><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" class="active"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg> Errors</a>
</nav>
<section class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Occurrences · 24h</div>
<div class="kpi-value">{{ fmt?.number(stats?.last24h ?? 0) }}</div>
<div class="kpi-sub" :class="{up: stats?.delta &gt; 0, down: stats?.delta &lt; 0}">
<span v-if="stats?.prev24h &gt;= 20">{{ (stats?.last24h ?? 0) - (stats?.prev24h ?? 0) &gt;= 0 ? '+' : '' }}{{ Math.round(((stats?.last24h ?? 0) - (stats?.prev24h ?? 0)) / stats.prev24h * 100) }}% vs yesterday</span><span v-else-if="stats?.prev24h !== undefined">{{ fmt?.number(stats?.prev24h ?? 0) }} in prior 24h</span>
<span v-if="!stats?.prev24h">no prior baseline</span>
</div>
</div>
<div class="kpi-card kpi-error kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'error' }" :aria-pressed="query?.bucket === 'error'" :title="'Show only 5xx errors'" @click="setBucket(query?.bucket === 'error' ? '' : 'error')" @keydown.enter.prevent="setBucket(query?.bucket === 'error' ? '' : 'error')">
<div class="kpi-label">Errors (5xx)</div>
<div class="kpi-value">{{ fmt?.number(stats?.severity?.error ?? 0) }}</div>
<div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
</div>
<div class="kpi-card kpi-warn kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'warn' }" :aria-pressed="query?.bucket === 'warn'" :title="'Show only 4xx responses'" @click="setBucket(query?.bucket === 'warn' ? '' : 'warn')" @keydown.enter.prevent="setBucket(query?.bucket === 'warn' ? '' : 'warn')">
<div class="kpi-label">4xx responses</div>
<div class="kpi-value">{{ fmt?.number(stats?.severity?.warn ?? 0) }}</div>
<div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
</div>
<div class="kpi-card kpi-info kpi-filter" role="button" tabindex="0" :class="{ 'is-selected': query?.bucket === 'info' }" :aria-pressed="query?.bucket === 'info'" :title="'Show only info events'" @click="setBucket(query?.bucket === 'info' ? '' : 'info')" @keydown.enter.prevent="setBucket(query?.bucket === 'info' ? '' : 'info')">
<div class="kpi-label">Info (auth, 404)</div>
<div class="kpi-value">{{ fmt?.number(stats?.severity?.info ?? 0) }}</div>
<div class="kpi-sub">{{ stats?.unique?.info }} unique</div>
</div>
<div class="kpi-card" :class="{&#x27;kpi-error&#x27;: stats?.dropped &gt; 0}">
<div class="kpi-label">Distinct errors stored</div>
<div class="kpi-value">{{ fmt?.number(total) }}</div>
<div class="kpi-sub">
all time · cap {{ fmt?.number(cap) }} · {{ available ? 'live' : 'redis off' }}
<span class="dropped-warn" v-if="stats?.dropped &gt; 0"> · {{ stats?.dropped }} dropped</span>
</div>
</div>
</section>
<section class="volume-chart">
<div class="volume-head">
<span class="volume-title">Volume · 24h · 1h buckets</span>
<span class="volume-legend">
<span v-if="stats?.severity?.error"><span class="dot dot-error"></span>error</span>
<span v-if="stats?.severity?.warn"><span class="dot dot-warn"></span>warn</span>
<span v-if="stats?.severity?.info"><span class="dot dot-info"></span>info</span>
</span>
</div>
<div class="volume-bars">
<div class="volume-bar" v-for="(b, index) in stats?.buckets" :key="index" :title="(bucketTitle(b))">
<span class="seg seg-error" :style="{height: barPx(b, &#x27;error&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-warn" :style="{height: barPx(b, &#x27;warn&#x27;) + &#x27;px&#x27;}"></span>
<span class="seg seg-info" :style="{height: barPx(b, &#x27;info&#x27;) + &#x27;px&#x27;}"></span>
</div>
</div>
<div class="volume-axis" aria-hidden="true"><span>24h ago</span><span>12h ago</span><span>now</span></div>
</section>
<form class="errors-toolbar" aria-label="Error filters" v-form="viewState">
<div class="seg-tabs">
<button type="button" :class="{active: query?.bucket === &#x27;&#x27;}" @click="setBucket(&#x27;&#x27;)">All</button>
<button type="button" :class="{active: query?.bucket === &#x27;error&#x27;}" @click="setBucket(&#x27;error&#x27;)">5xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;warn&#x27;}" @click="setBucket(&#x27;warn&#x27;)">4xx</button>
<button type="button" :class="{active: query?.bucket === &#x27;info&#x27;}" @click="setBucket(&#x27;info&#x27;)">Info</button>
</div>
<div class="search-wrap">
<svg class="ui-icon search-icon" aria-hidden="true"><use href="#ui-icon-search"></use></svg>
<input type="search" class="form-control" placeholder="Search code, module, status…" title="Syntax: code:repo_not_found module:route status:&gt;=400" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<span class="filter-count" v-if="parsedFilterCount">{{ parsedFilterCount }} filter{{ parsedFilterCount > 1 ? 's' : '' }}</span>
</div>
<div class="select-wrap">
<label>Sort</label>
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.sort = value }, value: query?.sort, form: null, options: {} }">
<option value="recent">Most recent</option>
<option value="count">Most frequent</option>
</select>
</div>
<div class="select-wrap">
<label>Group</label>
<select class="form-control form-control-sm" v-field="{ state: viewState, set: value =&gt; { query.group = value }, value: query?.group, form: null, options: {} }">
<option value="">Off</option>
<option value="code">By code</option>
<option value="module">By module</option>
</select>
</div>
<label class="autoref">
<input type="checkbox" v-field="{ state: viewState, set: value =&gt; { query.autoRefresh = value }, value: query?.autoRefresh, form: null, options: {} }">
<span class="auto-refresh-label">Auto-refresh</span>
</label>
<button class="btn btn-sm btn-icon" type="button" title="Refresh now" @click="refreshNow()" aria-label="Refresh"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
</form>
<div class="admin-empty" v-if="!visible?.length">No errors captured.</div>
<div class="errors-pager" v-if="canLoadMore() &amp;&amp; visible?.length">
<span>{{ query?.group &amp;&amp; visible?.length !== entries?.length ? fmt?.number(visible?.length) + (visible?.length === 1 ? ' group' : ' groups') + ' from ' : '' }}{{ fmt?.number(entries?.length) }} of {{ fmt?.number(total) }} stored errors loaded</span>
<button class="btn btn-sm" type="button" @click="loadMore()">Load 250 older</button>
</div>
<div class="errors-list" v-if="visible?.length">
<div class="errors-list-head">
<span class="col-when">When</span>
<span class="col-sev">Severity</span>
<span class="col-mod">Module</span>
<span class="col-msg">Message</span>
<span class="col-count" title="Total occurrences since first seen">Total</span>
<span class="col-status">Status</span>
</div>
<div class="errors-row" v-for="(row, index) in visible" :key="row?._key" :class="{open: expanded[row?._key]}">
<div class="errors-row-main" @click="toggle(row)">
<div class="col-when">
<div class="when-rel">{{ relTime(row?.ts) }}</div>
<div class="when-abs">{{ absTimeShort(row?.ts) }}</div>
</div>
<div class="col-sev">
<span class="sev-dot" :class="&#x27;sev-&#x27; + row?._bucket"></span>
<span class="sev-label">{{ fmt?.title(row?._bucket) }}</span>
</div>
<div class="col-mod"><span class="pill pill-module">{{ row?.module }}</span></div>
<div class="col-msg">
<strong class="msg-code">{{ row?.displayMessage }}</strong>
<span class="msg-context" v-if="row?.displayContext &amp;&amp; row?.displayContext !== row?.displayMessage">{{ row?.displayContext }}</span>
<span class="msg-detail" v-if="row?._detail">{{ row?._detail }}</span>
<div class="msg-url" v-if="row?._url">{{ row?._url }}</div>
</div>
<div class="col-count">
<span class="count-pill" v-if="row?.count &gt; 1">×{{ row?.count }}</span>
<span class="count-pill count-pill-muted" v-if="row?.count === 1">×1</span>
</div>
<div class="col-status">
<span class="status-pill" v-if="row?._status" :class="String(row?._status).match(/^[23]/) ? 'status-neutral' : (&#x27;status-&#x27; + row?._bucket)" :title="String(row?._status).match(/^2/) &amp;&amp; row?._bucket === 'warn' ? 'Client closed the connection' : null">{{ String(row?._status).match(/^2/) &amp;&amp; row?._bucket === 'warn' ? '—' : row?._status }}</span>
</div>
</div>
<div class="errors-row-detail" v-if="expanded[row?._key]">
<div class="detail-tabs">
<button type="button" :class="{active: detailTab[row?._key] === &#x27;raw&#x27; || !detailTab[row?._key]}" @click="detailTab[row._key] = &#x27;raw&#x27;">Raw</button>
<button type="button" v-if="row?._stack" :class="{active: detailTab[row?._key] === &#x27;stack&#x27;}" @click="detailTab[row._key] = &#x27;stack&#x27;">Stack</button>
<button type="button" v-if="row?.count &gt; 1" :class="{active: detailTab[row?._key] === &#x27;related&#x27;}" @click="detailTab[row._key] = &#x27;related&#x27;">Related ({{ row?.count }})</button>
</div>
<div class="detail-body">
<div class="detail-main">
<pre v-if="(detailTab[row?._key] || &#x27;raw&#x27;) === &#x27;raw&#x27;">{{ row?._detailJson }}</pre>
<pre class="stack-pre" v-if="detailTab[row?._key] === &#x27;stack&#x27;">{{ row?._stack }}</pre>
<div class="related-list" v-if="detailTab[row?._key] === &#x27;related&#x27;">
<div class="related-row" v-for="(r, index) in row?._related" :key="index">
<span class="when-abs">{{ absTimeShort(r?.ts) }}</span>
<span class="msg-url">{{ r?._url }}</span>
<span class="status-pill" v-if="r?._status" :class="&#x27;status-&#x27; + r?._bucket">{{ r?._status }}</span>
</div>
</div>
<div class="detail-actions">
<button class="btn btn-sm" type="button" title="Copy a curl that reproduces the request" @click="copyCurl(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-code"></use></svg> Copy curl</button>
<button class="btn btn-sm" type="button" @click="copyJson(row)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-clipboard"></use></svg> Copy JSON</button>
<span class="copy-hint" v-if="copyHint">{{ copyHint }}</span>
</div>
</div>
<aside class="detail-aside">
<div class="aside-block">
<div class="aside-label">First seen</div>
<div class="aside-value" :title="(absTime(row?._firstSeen))">{{ relTime(row?._firstSeen) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Last seen</div>
<div class="aside-value" :title="(absTime(row?.ts))">{{ relTime(row?.ts) }}</div>
</div>
<div class="aside-block">
<div class="aside-label">Occurrences</div>
<div class="aside-value">{{ row?.count }}<span class="aside-sub" v-if="row?._lastHourCount"> · {{ row?._lastHourCount }} this hour</span></div>
</div>
<div class="aside-block" v-if="row?._repoId">
<div class="aside-label">Repository</div>
<a class="aside-value" :href="safeUrl(&quot;/r/&quot; + (row?._repoId))">{{ row?._repoId }}</a>
</div>
<div class="aside-block" v-if="row?._url">
<div class="aside-label">URL</div>
<div class="aside-value mono">{{ row?._url }}</div>
</div>
</aside>
</div>
</div>
</div>
</div>
</div>