Files
anonymous_github/public/partials/admin/errors.htm
T
tdurieuxandClaude Opus 5.5 fd1bce74b4 fix: clean up the admin queue controls
The range picker drew a border around every segment inside the group's
border, and Drain used a filled pill that made the most destructive
action the loudest. Segments are now borderless inside one outline with
a tinted selection, all header controls share one height, and Drain and
Clear all use a red outline. Queue labels drop the mono caps. ADMIN=1
signs the mock server in as an administrator.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 14:46:01 +02:00

199 lines
11 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-danger-soft" 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">Last 24h</div>
<div class="kpi-value">{{ stats?.last24h }}</div>
<div class="kpi-sub" :class="{up: stats?.delta &gt; 0, down: stats?.delta &lt; 0}">
<span v-if="stats?.prev24h">{{ stats?.delta > 0 ? '+' : '' }}{{ stats?.delta }}% vs yesterday</span>
<span v-if="!stats?.prev24h">no prior baseline</span>
</div>
</div>
<div class="kpi-card kpi-error">
<div class="kpi-label">Errors (5xx)</div>
<div class="kpi-value">{{ stats?.severity?.error }}</div>
<div class="kpi-sub">{{ stats?.unique?.error }} unique</div>
</div>
<div class="kpi-card kpi-warn">
<div class="kpi-label">Warnings (4xx)</div>
<div class="kpi-value">{{ stats?.severity?.warn }}</div>
<div class="kpi-sub">{{ stats?.unique?.warn }} unique</div>
</div>
<div class="kpi-card kpi-info">
<div class="kpi-label">Info (auth, 404)</div>
<div class="kpi-value">{{ stats?.severity?.info }}</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">Captured</div>
<div class="kpi-value">{{ total }}</div>
<div class="kpi-sub">
cap {{ 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 class="dot dot-error"></span>error
<span class="dot dot-warn"></span>warn
<span class="dot dot-info"></span>info
</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>
</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="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: {} }">
Auto-refresh
</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>Showing {{ entries?.length }} of {{ total }} captured</span>
<button class="btn btn-sm" type="button" @click="loadMore()">Load 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">Count</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?.uppercase(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="&#x27;status-&#x27; + row?._bucket">{{ 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>