Files
anonymous_github/public/partials/admin/queues.htm
T
Thomas DurieuxandClaude Opus 5.5 c044b83541 feat(ui): one consistent design across author, reviewer and admin screens
The UI was uneven: the home page looked finished, but app pages used three
different display typefaces, the anonymize flow changed frame between steps,
and the dashboard, explorer and admin screens each had their own controls,
status colours and edge-case handling.

Make every screen follow one system: shared page header, grid, buttons,
filters and status colours; a repository picker and step trail on anonymize;
a "What reviewers will see" preview of the actual reviewer page; consistent
file headers and viewers for every file type; truthful admin charts and
tables. Fold the accumulated override blocks back into style.css (22%
smaller, no visual change).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 02:22:45 +00:00

225 lines
16 KiB
HTML

<div class="container paper-page admin-page">
<div class="paper-crumbs">Admin &nbsp;/&nbsp; <span class="here">Queues</span></div>
<div class="q-header">
<h1 class="paper-page-title">Queues</h1>
<div class="q-header-actions">
<button class="btn btn-sm" @click="pauseAll()">Pause all queues</button>
</div>
</div>
<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" class="active"><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>
<!-- Queue overview cards -->
<div class="q-cards">
<div class="q-card" role="button" tabindex="0" :aria-pressed="selectedQueue === q.key" @keydown.enter.prevent="selectQueue(q.key)" @keydown.space.prevent="selectQueue(q.key)" v-for="(q, index) in queueList" :class="{selected: selectedQueue == q?.key, paused: q?.paused}" @click="selectQueue(q.key)">
<div class="q-card-head">
<span class="q-dot" :title="q?.paused ? 'Paused' : q?.counts?.failed &gt; 0 ? q.counts.failed + ' failed jobs' : (q?.counts?.active || q?.counts?.waiting) ? 'Running' : 'Idle'" :class="{&#x27;q-dot-red&#x27;: q?.paused || q?.counts?.failed &gt; 0, &#x27;q-dot-idle&#x27;: !q?.counts?.active &amp;&amp; !q?.counts?.waiting}"></span>
<span class="q-card-name" v-text="q?.label"></span>
</div>
<div class="q-card-count" v-text="(q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0)"></div>
<div class="q-card-sub">
<span>waiting &middot; {{ q?.counts?.active || 0 }} active<template v-if="q?.counts?.failed"> &middot; <b class="q-card-failed">{{ q.counts.failed }} failed</b></template><template v-if="q?.paused"> &middot; <b class="q-card-failed">paused</b></template></span>
<div class="q-card-bar" v-if="q?.counts?.active">
<div class="q-card-bar-fill" :style="{width: (q?.counts?.active / ((q?.counts?.waiting || 0) + (q?.counts?.active || 0) + (q?.counts?.delayed || 0) || 1) * 100) + &#x27;%&#x27;}"></div>
</div>
</div>
</div>
</div>
<!-- Detail: throughput chart + stats panel -->
<div class="q-detail" v-if="selectedStats">
<div class="q-throughput">
<div class="q-range-btns">
<button class="btn btn-sm" :class="{active: range == &#x27;1h&#x27;}" @click="setRange(&#x27;1h&#x27;)">1h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;6h&#x27;}" @click="setRange(&#x27;6h&#x27;)">6h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;24h&#x27;}" @click="setRange(&#x27;24h&#x27;)">24h</button>
<button class="btn btn-sm" :class="{active: range == &#x27;7d&#x27;}" @click="setRange(&#x27;7d&#x27;)">7d</button>
</div>
<div class="q-section-label">Throughput · {{ selectedQueue }} <span class="q-section-right" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) &gt; 0"><span class="q-legend-completed">&#9679;</span> Completed <span class="q-legend-failed">&#9679;</span> Failed <template v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) &gt; 0"><span class="q-legend-exec" aria-hidden="true"></span> Avg time &middot; {{ range }}</template></span></div>
<div class="q-chart-wrap">
<canvas id="q-throughput-chart" height="180"></canvas>
<div class="q-chart-empty" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) === 0"><strong>No jobs processed in this window</strong><span>Completed and failed jobs will chart here.</span></div>
<div id="q-chart-tooltip" class="q-chart-tooltip" style="display:none;"></div>
<div id="q-chart-crosshair" class="q-chart-crosshair" style="display:none;"></div>
</div>
</div>
<div class="q-stats-panel">
<div class="q-section-label">Stats · {{ selectedQueue }}</div>
<div class="q-stats-grid">
<div class="q-stat">
<div class="q-stat-label">Waiting</div>
<div class="q-stat-value" v-text="selectedStats?.counts?.waiting || 0"></div>
</div>
<div class="q-stat">
<div class="q-stat-label">Active</div>
<div class="q-stat-value" v-text="selectedStats?.counts?.active || 0"></div>
</div>
<div class="q-stat">
<div class="q-stat-label">Completed (24h)</div>
<div class="q-stat-value" v-text="fmt?.number(selectedStats?.completed24h)"></div>
</div>
<div class="q-stat">
<div class="q-stat-label">Failed (24h)</div>
<div class="q-stat-value" v-text="selectedStats?.failed24h || 0"></div>
</div>
<div class="q-stat">
<div class="q-stat-label">Delayed</div>
<div class="q-stat-value" v-text="selectedStats?.counts?.delayed || 0"></div>
</div>
<div class="q-stat">
<div class="q-stat-label">Workers</div>
<div class="q-stat-value" v-text="selectedStats?.workers || 0"></div>
</div>
</div>
<div class="q-stats-actions">
<button class="btn btn-sm" @click="togglePause()">{{ selectedStats?.paused ? 'Resume ' + selectedQueue : 'Pause ' + selectedQueue }}</button>
<button class="btn btn-sm" @click="retryFailed()" :disabled="!selectedStats?.counts?.failed">Retry failed</button>
<span class="q-danger-group"><button class="btn btn-sm btn-outline-danger" @click="drainSelected()">Drain</button><button class="btn btn-sm btn-outline-danger" @click="emptyQueue()">Empty</button></span>
</div>
</div>
</div>
<div class="q-toast-error" v-if="actionError" @click="actionError = null">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-info"></use></svg> {{ actionError }}
</div>
<!-- Jobs table -->
<div class="q-jobs">
<div class="q-jobs-header">
<div class="q-section-label">All jobs · {{ selectedQueue }}</div>
<div class="seg-filter" role="radiogroup" aria-label="Filter jobs by state">
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'all' }" :aria-checked="segView() === 'all'" @click="setSegView('all')">All <span class="count">{{ fmt?.number(segCount('all')) }}</span></button>
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'active', 'is-empty': !segCount('active') }" :aria-checked="segView() === 'active'" @click="setSegView('active')"><span class="seg-dot ok" aria-hidden="true"></span>Active <span class="count">{{ fmt?.number(segCount('active')) }}</span></button>
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'waiting', 'is-empty': !segCount('waiting') }" :aria-checked="segView() === 'waiting'" @click="setSegView('waiting')"><span class="seg-dot warn" aria-hidden="true"></span>Waiting <span class="count">{{ fmt?.number(segCount('waiting')) }}</span></button>
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'delayed', 'is-empty': !segCount('delayed') }" :aria-checked="segView() === 'delayed'" @click="setSegView('delayed')"><span class="seg-dot warn" aria-hidden="true"></span>Delayed <span class="count">{{ fmt?.number(segCount('delayed')) }}</span></button>
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'failed', 'is-empty': !segCount('failed') }" :aria-checked="segView() === 'failed'" @click="setSegView('failed')"><span class="seg-dot bad" aria-hidden="true"></span>Failed <span class="count">{{ fmt?.number(segCount('failed')) }}</span></button>
<button type="button" role="radio" class="seg-chip" :class="{ active: segView() === 'completed', 'is-empty': !segCount('completed') }" :aria-checked="segView() === 'completed'" @click="setSegView('completed')"><span class="seg-dot done" aria-hidden="true"></span>Completed <span class="count">{{ fmt?.number(segCount('completed')) }}</span></button>
</div>
</div>
<div class="q-search-row">
<input type="search" class="form-control" placeholder="Search by job/repo id…" autocomplete="off" v-field="{ state: viewState, set: value =&gt; { query.search = value }, value: query?.search, form: null, options: {} }">
<label class="q-auto-refresh">
<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>
</div>
<table class="q-table" v-if="filteredJobs().length &gt; 0">
<thead>
<tr>
<th>State</th>
<th>Job</th>
<th>Detail</th>
<th>Attempts</th>
<th class="q-th-num">Duration</th>
<th>Progress</th>
<th></th>
</tr>
</thead>
<tbody v-for="(job, index) in filteredJobs()">
<tr style="cursor:pointer;" :class="{&#x27;q-row-failed&#x27;: job?._state == &#x27;failed&#x27;, &#x27;q-row-expanded&#x27;: expanded[job?.id]}" @click="toggleJob(job)">
<td class="q-cell-state">
<span v-text="job?._state" :class="&quot;q-state-badge q-state-&quot; + (job?._state)"></span>
<span class="q-delay-hint" v-if="job?._state == &#x27;delayed&#x27; &amp;&amp; job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
</td>
<td class="q-cell-id" :title="'job:' + job?.id + (job?.name &amp;&amp; job?.name !== job?.id ? ' · ' + job.name : '') + (job?.data?.repoId &amp;&amp; job?.data?.repoId !== job?.id ? ' · ' + job.data.repoId : '')">
<svg class="ui-icon q-chevron" :class="{&#x27;q-chevron-open&#x27;: expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
<a target="_blank" class="q-job-link" v-text="job?.data?.repoId || job?.id" @click="$event.stopPropagation()" :href="safeUrl(&quot;/r/&quot; + (job?.data?.repoId || job?.id))"></a>
</td>
<td class="q-cell-detail" :class="{'is-error': job?._state == 'failed'}" :title="job?._state == 'failed' ? job?.failedReason : ''" v-text="jobDetail(job)"></td>
<td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
<td class="q-cell-num q-cell-duration" v-text="jobDuration(job)"></td>
<td class="q-cell-progress">
<div class="q-progress-wrap" :class="{'is-indeterminate': jobProgressPct(job) === null}" :aria-label="jobProgressPct(job) === null ? 'Progress not reported' : jobProgressPct(job) + '% done'">
<div class="q-progress-bar" :style="{&#x27;--pct&#x27;: (jobProgressPct(job) || 0) + &#x27;%&#x27;}"></div>
<span class="q-progress-label" v-text="jobProgressPct(job) === null ? '—' : jobProgressPct(job) + &#x27;%&#x27;"></span>
</div>
</td>
<td class="q-cell-actions">
<button class="btn btn-sm btn-icon" title="Retry" v-if="job?._state == &#x27;failed&#x27;" @click="retryJob(job); $event.stopPropagation()" aria-label="Retry job"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg></button>
<button class="btn btn-sm btn-icon" title="Remove" aria-label="Remove job" @click="removeJob(job); $event.stopPropagation()"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg></button>
</td>
</tr>
<tr class="q-detail-row" v-if="expanded[job?.id]">
<td colspan="7">
<div class="q-job-detail">
<div class="q-job-detail-grid">
<div class="q-job-detail-item">
<span class="q-job-detail-label">Job id</span>
<span class="q-job-detail-value"><a target="_blank" v-text="job?.id" :href="safeUrl(&quot;/r/&quot; + (job?.id))"></a></span>
</div>
<div class="q-job-detail-item">
<span class="q-job-detail-label">State</span>
<span class="q-job-detail-value"><span v-text="job?._state" :class="&quot;q-state-badge q-state-&quot; + (job?._state)"></span></span>
</div>
<div class="q-job-detail-item" v-if="job?.data?.repoId">
<span class="q-job-detail-label">Repo ID</span>
<span class="q-job-detail-value" v-text="job?.data?.repoId"></span>
</div>
<div class="q-job-detail-item" v-if="job?.timestamp">
<span class="q-job-detail-label">Created</span>
<span class="q-job-detail-value" v-text="humanTime(job?.timestamp)"></span>
</div>
<div class="q-job-detail-item" v-if="job?._state == &#x27;delayed&#x27; &amp;&amp; job?.delayUntil">
<span class="q-job-detail-label">Retry at</span>
<span class="q-job-detail-value">{{ humanTime(job?.delayUntil) }} ({{ delayCountdown(job?.delayUntil) }})</span>
</div>
<div class="q-job-detail-item" v-if="job?.processedOn">
<span class="q-job-detail-label">Processed</span>
<span class="q-job-detail-value" v-text="humanTime(job?.processedOn)"></span>
</div>
<div class="q-job-detail-item" v-if="job?.finishedOn">
<span class="q-job-detail-label">Finished</span>
<span class="q-job-detail-value" v-text="humanTime(job?.finishedOn)"></span>
</div>
<div class="q-job-detail-item" v-if="job?.attemptsMade">
<span class="q-job-detail-label">Attempts</span>
<span class="q-job-detail-value" v-text="job?.attemptsMade"></span>
</div>
<div class="q-job-detail-item" v-if="job?.progress &amp;&amp; job?.progress?.status">
<span class="q-job-detail-label">Status</span>
<span class="q-job-detail-value" v-text="job?.progress?.status"></span>
</div>
<div class="q-job-detail-item" v-if="jobProgressPct(job) !== null">
<span class="q-job-detail-label">Progress</span>
<span class="q-job-detail-value" v-text="jobProgressPct(job) + &#x27;%&#x27;"></span>
</div>
</div>
<div class="q-job-detail-error" v-if="job?.failedReason">
<span class="q-job-detail-label">Error</span>
<div class="q-error-reason" v-text="job?.failedReason"></div>
</div>
<div v-if="job?.stacktrace?.length">
<span class="q-job-detail-label">Stacktrace</span>
<pre class="q-error-stack" v-for="(stack, index) in job?.stacktrace" :key="index"><code v-text="stack"></code></pre>
</div>
<div class="q-job-detail-actions">
<button class="btn btn-sm" v-if="job?._state == &#x27;failed&#x27;" @click="retryJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-refresh"></use></svg> Retry</button>
<button class="btn btn-sm" @click="removeJob(job)"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-trash"></use></svg> Remove</button>
<a class="btn btn-sm" target="_blank" :href="safeUrl(&quot;/r/&quot; + (job?.id))"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-external-link"></use></svg> View repo</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
<div class="paper-table-empty" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);" v-if="filteredJobs().length == 0">
<svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-check-circle"></use></svg>
<span v-if="!query?.search">No jobs in the {{ selectedQueue }} queue.</span>
<span v-if="query?.search">No jobs match the current filters.</span>
</div>
</div>
</div>