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>
226 lines
14 KiB
HTML
226 lines
14 KiB
HTML
<div class="container paper-page admin-page">
|
|
<div class="paper-crumbs">Admin / <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" :class="{'q-dot-red': q?.paused || q?.counts?.failed > 0, 'q-dot-idle': !q?.counts?.active && !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 · {{ q?.counts?.active || 0 }} active</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) + '%'}"></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 == '1h'}" @click="setRange('1h')">1h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '6h'}" @click="setRange('6h')">6h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '24h'}" @click="setRange('24h')">24h</button>
|
|
<button class="btn btn-sm" :class="{active: range == '7d'}" @click="setRange('7d')">7d</button>
|
|
</div>
|
|
<div class="q-section-label">Throughput · {{ selectedQueue }} <span class="q-section-right" v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <template v-if="(selectedStats?.completed24h || 0) + (selectedStats?.failed24h || 0) > 0"><span class="q-legend-exec" aria-hidden="true"></span> avg time · {{ 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="q-state-filters">
|
|
<label class="q-state-toggle" v-for="(s, index) in allStates">
|
|
<input type="checkbox" v-field="{ state: viewState, set: value => { stateFilter[s] = value }, value: stateFilter[s], form: null, options: {} }">
|
|
<span :class=""q-state-chip q-state-" + (s)">{{ s }}</span>
|
|
</label>
|
|
</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 => { query.search = value }, value: query?.search, form: null, options: {} }">
|
|
<label class="q-auto-refresh">
|
|
<input type="checkbox" v-field="{ state: viewState, set: value => { 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 > 0">
|
|
<thead>
|
|
<tr>
|
|
<th>State</th>
|
|
<th>Job ID</th>
|
|
<th>Payload</th>
|
|
<th>Attempts</th>
|
|
<th>Duration</th>
|
|
<th>Progress</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody v-for="(job, index) in filteredJobs()">
|
|
<tr style="cursor:pointer;" :class="{'q-row-failed': job?._state == 'failed', 'q-row-expanded': expanded[job?.id]}" @click="toggleJob(job)">
|
|
<td class="q-cell-state">
|
|
<span v-text="job?._state" :class=""q-state-badge q-state-" + (job?._state)"></span>
|
|
<span class="q-delay-hint" v-if="job?._state == 'delayed' && job?.delayUntil" v-text="delayCountdown(job?.delayUntil)"></span>
|
|
</td>
|
|
<td class="q-cell-id">
|
|
<svg class="ui-icon q-chevron" :class="{'q-chevron-open': expanded[job?.id]}" aria-hidden="true"><use href="#ui-icon-chevron-right"></use></svg>
|
|
<a target="_blank" v-text="'job:' + fmt.limitTo(job?.id, 6)" @click="$event.stopPropagation()" :href="safeUrl("/r/" + (job?.id))"></a>
|
|
</td>
|
|
<td class="q-cell-payload">
|
|
<span v-text="job?.name || 'anonymize'"></span>
|
|
<span class="q-payload-detail" v-if="job?.data?.repoId && job?.data?.repoId !== job?.name"> · {{ job?.data?.repoId }}</span>
|
|
</td>
|
|
<td class="q-cell-num" v-text="job?.attemptsMade || 1"></td>
|
|
<td class="q-cell-num" v-text="jobDuration(job)"></td>
|
|
<td class="q-cell-progress">
|
|
<div class="q-progress-wrap" v-if="jobProgressPct(job) !== null">
|
|
<div class="q-progress-bar" :style="{'--pct': jobProgressPct(job) + '%'}"></div>
|
|
<span class="q-progress-label" v-text="jobProgressPct(job) + '%'"></span>
|
|
</div>
|
|
</td>
|
|
<td class="q-cell-actions">
|
|
<button class="btn btn-sm btn-icon" title="Retry" v-if="job?._state == 'failed'" @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("/r/" + (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=""q-state-badge q-state-" + (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 == 'delayed' && 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 && 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) + '%'"></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 == 'failed'" @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("/r/" + (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>
|