mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-05-15 06:30:26 +02:00
improve queue
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"core.min.js": "core.3db744fc07.min.js",
|
||||
"vendor.min.js": "vendor.9df222182a.min.js",
|
||||
"vendor.min.js": "vendor.9aa24967d7.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.8d9fbb7ca6.min.css"
|
||||
"all.min.css": "all.f79970ad3b.min.css"
|
||||
}
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+224
-12
@@ -3289,6 +3289,35 @@ code {
|
||||
color: var(--color);
|
||||
}
|
||||
.dark-mode .paper-error-card { border-left-color: #FF8B7B; }
|
||||
|
||||
.paper-ratelimit-card {
|
||||
margin-top: 18px;
|
||||
padding: 20px 22px;
|
||||
background: var(--paper-bg-alt);
|
||||
border: 1px solid var(--border-color);
|
||||
border-left: 3px solid #D69E2E;
|
||||
border-radius: 10px;
|
||||
color: var(--color);
|
||||
}
|
||||
.dark-mode .paper-ratelimit-card { border-left-color: #F6E05E; }
|
||||
.paper-ratelimit-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
.paper-ratelimit-head > i {
|
||||
font-size: 18px;
|
||||
color: #D69E2E;
|
||||
margin-top: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dark-mode .paper-ratelimit-head > i { color: #F6E05E; }
|
||||
.paper-ratelimit-card strong { color: var(--color); }
|
||||
|
||||
.status-pill-ratelimit { border-color: #D69E2E; color: #D69E2E; }
|
||||
.dark-mode .status-pill-ratelimit { border-color: #F6E05E; color: #F6E05E; }
|
||||
.status-ratelimit { background: #D69E2E; }
|
||||
.dark-mode .status-ratelimit { background: #F6E05E; }
|
||||
.paper-error-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -4681,10 +4710,45 @@ textarea::selection {
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
.q-chart-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.q-throughput canvas {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
.q-chart-tooltip {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: var(--paper-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
z-index: 10;
|
||||
}
|
||||
.q-chart-tooltip .q-tip-time {
|
||||
color: var(--ink-muted);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.q-chart-tooltip .q-tip-completed { color: #3B4AD6; }
|
||||
.q-chart-tooltip .q-tip-failed { color: #B42318; }
|
||||
.dark-mode .q-chart-tooltip .q-tip-completed { color: #A7B2FF; }
|
||||
.dark-mode .q-chart-tooltip .q-tip-failed { color: #F08A82; }
|
||||
.q-chart-crosshair {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: var(--ink-muted);
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
z-index: 9;
|
||||
}
|
||||
.q-stats-panel {
|
||||
background: var(--paper-card);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -4708,6 +4772,14 @@ textarea::selection {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.q-legend-completed { color: #3B4AD6; }
|
||||
.q-legend-failed { color: #B42318; margin-left: 8px; }
|
||||
.q-legend-exec { color: #B8860B; margin-left: 8px; letter-spacing: 2px; }
|
||||
.dark-mode .q-legend-completed { color: #A7B2FF; }
|
||||
.dark-mode .q-legend-failed { color: #F08A82; }
|
||||
.dark-mode .q-legend-exec { color: #F5C842; }
|
||||
.q-chart-tooltip .q-tip-exec { color: #B8860B; }
|
||||
.dark-mode .q-chart-tooltip .q-tip-exec { color: #F5C842; }
|
||||
.q-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -4761,25 +4833,67 @@ textarea::selection {
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.q-tabs {
|
||||
/* State filter toggles */
|
||||
.q-state-filters {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.q-tab {
|
||||
.q-state-toggle {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.q-state-toggle input { display: none; }
|
||||
.q-state-chip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
padding: 5px 14px;
|
||||
border-radius: 8px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--paper-card);
|
||||
color: var(--ink-muted);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
transition: opacity 0.15s, background 0.15s;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.q-tab:hover { background: var(--paper-bg-alt); }
|
||||
.q-tab.active {
|
||||
background: var(--color);
|
||||
color: var(--paper-card);
|
||||
.q-state-toggle input:checked + .q-state-chip {
|
||||
opacity: 1;
|
||||
}
|
||||
.q-state-toggle input:not(:checked) + .q-state-chip {
|
||||
opacity: 0.4;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* State badge colors */
|
||||
.q-state-badge, .q-state-chip {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.q-state-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.q-state-active { background: #dbeafe; color: #1d4ed8; }
|
||||
.q-state-waiting { background: #fef3c7; color: #92400e; }
|
||||
.q-state-delayed { background: #e0e7ff; color: #4338ca; }
|
||||
.q-state-failed { background: #fee2e2; color: #b91c1c; }
|
||||
.q-state-completed { background: #dcfce7; color: #166534; }
|
||||
.dark-mode .q-state-active { background: rgba(59,130,246,0.2); color: #93bbfd; }
|
||||
.dark-mode .q-state-waiting { background: rgba(251,191,36,0.15); color: #fbbf24; }
|
||||
.dark-mode .q-state-delayed { background: rgba(99,102,241,0.2); color: #a5b4fc; }
|
||||
.dark-mode .q-state-failed { background: rgba(239,68,68,0.2); color: #fca5a5; }
|
||||
.dark-mode .q-state-completed { background: rgba(34,197,94,0.15); color: #86efac; }
|
||||
.q-cell-state { width: 120px; }
|
||||
.q-delay-hint {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: var(--ink-muted);
|
||||
margin-top: 2px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.q-search-row {
|
||||
display: flex;
|
||||
@@ -4835,6 +4949,20 @@ textarea::selection {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.q-cell-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.q-chevron {
|
||||
font-size: 9px;
|
||||
color: var(--ink-muted);
|
||||
transition: transform 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.q-chevron-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.q-cell-id a { color: var(--color); }
|
||||
.q-cell-payload {
|
||||
font-family: var(--font-mono);
|
||||
@@ -4901,11 +5029,94 @@ textarea::selection {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.dark-mode .q-error-reason { color: #F08A82; }
|
||||
/* Error toast */
|
||||
.q-toast-error {
|
||||
background: #FBE7E7;
|
||||
color: #B42318;
|
||||
border: 1px solid #F3C7C7;
|
||||
border-radius: 8px;
|
||||
padding: 10px 16px;
|
||||
margin-bottom: 12px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.dark-mode .q-toast-error {
|
||||
background: rgba(240,138,130,0.1);
|
||||
color: #F08A82;
|
||||
border-color: rgba(240,138,130,0.25);
|
||||
}
|
||||
|
||||
.q-error-stack {
|
||||
font-size: 0.78rem;
|
||||
max-height: 80px;
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
margin: 4px 0 0;
|
||||
background: var(--paper-bg-alt);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
/* Expanded job detail row */
|
||||
.q-row-expanded > td { border-bottom-color: transparent; }
|
||||
.q-detail-row > td {
|
||||
padding: 0 10px 14px !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.q-job-detail {
|
||||
background: var(--paper-bg-alt);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.q-job-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
gap: 10px 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.q-job-detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.q-job-detail-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
color: var(--ink-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.q-job-detail-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
color: var(--color);
|
||||
word-break: break-all;
|
||||
}
|
||||
.q-job-detail-value a { color: var(--color); }
|
||||
.q-job-detail-error {
|
||||
margin: 8px 0;
|
||||
}
|
||||
.q-job-detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
.q-job-detail-actions .btn {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--paper-card);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -4918,6 +5129,7 @@ textarea::selection {
|
||||
.q-search-row .form-control { max-width: 100%; flex: 1; }
|
||||
.q-table { font-size: 12px; }
|
||||
.q-header { flex-direction: column; }
|
||||
.q-job-detail-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"repoUrl_not_defined": "The repository URL needs to be defined.",
|
||||
"source_not_provided": "A repository source must be provided.",
|
||||
"github_rate_limit_exceeded": "GitHub temporarily blocked the request because we hit its API rate limit. Wait a few minutes and try again. If the problem persists and you contact GitHub Support, include the request ID and timestamp shown in GitHub's response (the 'X-GitHub-Request-Id' header).",
|
||||
"rate_limited": "GitHub API rate limit reached. The repository will be available shortly — please wait a moment and refresh.",
|
||||
"repoId_already_used": "The repository ID is already used.",
|
||||
"invalid_repoId": "The format of the repository ID is invalid.",
|
||||
"unsupported_source": "The repository source type is not supported.",
|
||||
|
||||
@@ -44,8 +44,12 @@
|
||||
<!-- Detail: throughput chart + stats panel -->
|
||||
<div class="q-detail" ng-if="selectedStats">
|
||||
<div class="q-throughput">
|
||||
<div class="q-section-label">{{selectedQueue}}·throughput <span class="q-section-right">COMPLETED / MIN · {{range | uppercase}}</span></div>
|
||||
<canvas id="q-throughput-chart" height="180"></canvas>
|
||||
<div class="q-section-label">{{selectedQueue}}·throughput <span class="q-section-right"><span class="q-legend-completed">●</span> completed <span class="q-legend-failed">●</span> failed <span class="q-legend-exec">- -</span> avg time · {{range | uppercase}}</span></div>
|
||||
<div class="q-chart-wrap">
|
||||
<canvas id="q-throughput-chart" height="180"></canvas>
|
||||
<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">{{selectedQueue}}·stats</div>
|
||||
@@ -83,16 +87,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-toast-error" ng-if="actionError" ng-click="actionError = null">
|
||||
<i class="fas fa-exclamation-circle"></i> {{actionError}}
|
||||
</div>
|
||||
|
||||
<!-- Jobs table -->
|
||||
<div class="q-jobs">
|
||||
<div class="q-jobs-header">
|
||||
<div class="q-section-label">{{query.state | uppercase}} JOBS · {{selectedQueue | uppercase}}</div>
|
||||
<div class="q-tabs">
|
||||
<button class="q-tab" ng-class="{active: query.state == 'active'}" ng-click="query.state = 'active'">Active</button>
|
||||
<button class="q-tab" ng-class="{active: query.state == 'waiting'}" ng-click="query.state = 'waiting'">Waiting</button>
|
||||
<button class="q-tab" ng-class="{active: query.state == 'completed'}" ng-click="query.state = 'completed'">Completed</button>
|
||||
<button class="q-tab" ng-class="{active: query.state == 'failed'}" ng-click="query.state = 'failed'">Failed</button>
|
||||
<button class="q-tab" ng-class="{active: query.state == 'delayed'}" ng-click="query.state = 'delayed'">Delayed</button>
|
||||
<div class="q-section-label">ALL JOBS · {{selectedQueue | uppercase}}</div>
|
||||
<div class="q-state-filters">
|
||||
<label class="q-state-toggle" ng-repeat="s in allStates">
|
||||
<input type="checkbox" ng-model="stateFilter[s]" />
|
||||
<span class="q-state-chip q-state-{{s}}">{{s}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,9 +112,10 @@
|
||||
<button class="btn btn-sm" type="button" ng-click="refreshNow()" title="Refresh now"><i class="fas fa-sync"></i></button>
|
||||
</div>
|
||||
|
||||
<table class="q-table" ng-if="jobs.length > 0">
|
||||
<table class="q-table" ng-if="filteredJobs().length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>STATE</th>
|
||||
<th>JOB ID</th>
|
||||
<th>PAYLOAD</th>
|
||||
<th>ATTEMPTS</th>
|
||||
@@ -116,14 +124,19 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="job in jobs" ng-class="{'q-row-failed': job.failedReason || job.stacktrace.length}">
|
||||
<tbody ng-repeat="job in filteredJobs()">
|
||||
<tr ng-class="{'q-row-failed': job._state == 'failed', 'q-row-expanded': expanded[job.id]}" ng-click="toggleJob(job)" style="cursor:pointer;">
|
||||
<td class="q-cell-state">
|
||||
<span class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span>
|
||||
<span class="q-delay-hint" ng-if="job._state == 'delayed' && job.delayUntil" ng-bind="delayCountdown(job.delayUntil)"></span>
|
||||
</td>
|
||||
<td class="q-cell-id">
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-bind="'job:' + (job.id | limitTo:6)"></a>
|
||||
<i class="fas fa-chevron-right q-chevron" ng-class="{'q-chevron-open': expanded[job.id]}"></i>
|
||||
<a target="_blank" ng-href="/r/{{job.id}}" ng-click="$event.stopPropagation()" ng-bind="'job:' + (job.id | limitTo:6)"></a>
|
||||
</td>
|
||||
<td class="q-cell-payload">
|
||||
<span ng-bind="job.name || 'anonymize'"></span>
|
||||
<span class="q-payload-detail" ng-if="job.data.repoId"> · {{job.data.repoId}}</span>
|
||||
<span class="q-payload-detail" ng-if="job.data.repoId && job.data.repoId !== job.name"> · {{job.data.repoId}}</span>
|
||||
</td>
|
||||
<td class="q-cell-num" ng-bind="job.attemptsMade || 1"></td>
|
||||
<td class="q-cell-num" ng-bind="jobDuration(job)"></td>
|
||||
@@ -134,22 +147,77 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="q-cell-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob(job)" title="Retry"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(job)" title="Remove"><i class="fas fa-trash-alt"></i></button>
|
||||
<button class="btn btn-sm" ng-click="retryJob(job); $event.stopPropagation()" title="Retry" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(job); $event.stopPropagation()" title="Remove"><i class="fas fa-trash-alt"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="q-detail-row" ng-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" ng-href="/r/{{job.id}}" ng-bind="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 class="q-state-badge q-state-{{job._state}}" ng-bind="job._state"></span></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.data.repoId">
|
||||
<span class="q-job-detail-label">REPO ID</span>
|
||||
<span class="q-job-detail-value" ng-bind="job.data.repoId"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.timestamp">
|
||||
<span class="q-job-detail-label">CREATED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.timestamp)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-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" ng-if="job.processedOn">
|
||||
<span class="q-job-detail-label">PROCESSED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.processedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.finishedOn">
|
||||
<span class="q-job-detail-label">FINISHED</span>
|
||||
<span class="q-job-detail-value" ng-bind="humanTime(job.finishedOn)"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.attemptsMade">
|
||||
<span class="q-job-detail-label">ATTEMPTS</span>
|
||||
<span class="q-job-detail-value" ng-bind="job.attemptsMade"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="job.progress && job.progress.status">
|
||||
<span class="q-job-detail-label">STATUS</span>
|
||||
<span class="q-job-detail-value" ng-bind="job.progress.status"></span>
|
||||
</div>
|
||||
<div class="q-job-detail-item" ng-if="jobProgressPct(job) !== null">
|
||||
<span class="q-job-detail-label">PROGRESS</span>
|
||||
<span class="q-job-detail-value" ng-bind="jobProgressPct(job) + '%'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="job.failedReason" class="q-job-detail-error">
|
||||
<span class="q-job-detail-label">ERROR</span>
|
||||
<div class="q-error-reason" ng-bind="job.failedReason"></div>
|
||||
</div>
|
||||
<div ng-if="job.stacktrace.length">
|
||||
<span class="q-job-detail-label">STACKTRACE</span>
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" class="q-error-stack"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
<div class="q-job-detail-actions">
|
||||
<button class="btn btn-sm" ng-click="retryJob(job)" ng-if="job._state == 'failed'"><i class="fas fa-sync"></i> Retry</button>
|
||||
<button class="btn btn-sm" ng-click="removeJob(job)"><i class="fas fa-trash-alt"></i> Remove</button>
|
||||
<a class="btn btn-sm" target="_blank" ng-href="/r/{{job.id}}"><i class="fas fa-external-link-alt"></i> View repo</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- Expanded error detail -->
|
||||
<div ng-repeat="job in jobs" ng-if="job.failedReason || job.stacktrace.length" class="q-error-detail" style="display:none;">
|
||||
<div ng-if="job.failedReason" class="q-error-reason" ng-bind="job.failedReason"></div>
|
||||
<pre ng-repeat="stack in job.stacktrace track by $index" class="q-error-stack"><code ng-bind="stack"></code></pre>
|
||||
</div>
|
||||
|
||||
<div class="paper-table-empty" ng-if="jobs.length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<div class="paper-table-empty" ng-if="filteredJobs().length == 0" style="border:1px solid var(--border-color);border-radius:10px;background:var(--paper-card);">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span ng-if="!query.search">No {{query.state}} jobs in the {{selectedQueue}} queue.</span>
|
||||
<span ng-if="!query.search">No jobs in the {{selectedQueue}} queue.</span>
|
||||
<span ng-if="query.search">No jobs match the current filters.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span>
|
||||
<span ng-bind="repo.status | title"></span>
|
||||
</span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage"></span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::repo.pageView || 0 | number"></div>
|
||||
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div>
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
<span class="status-dot" ng-class="{'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-preparing': repo.status == 'preparing'}"></span>
|
||||
<span ng-bind="repo.status | title"></span>
|
||||
</span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage"></span>
|
||||
<span class="status-sub" ng-if="repo.statusMessage" title="{{repo.statusMessage}}" ng-bind="repo.statusMessage | statusMsg"></span>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="::repo.pageView || 0 | number"></div>
|
||||
<div class="cell-expires" role="cell" ng-bind="repo.anonymizeDate | humanTime"></div>
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
></span>
|
||||
<span ng-bind="item.status | title"></span>
|
||||
</div>
|
||||
<div class="status-sub status-sub-error" ng-if="item.status == 'error' && item.statusMessage" title="{{item.statusMessage}}" ng-bind="item.statusMessage"></div>
|
||||
<div class="status-sub status-sub-error" ng-if="item.status == 'error' && item.statusMessage" title="{{item.statusMessage}}" ng-bind="item.statusMessage | statusMsg"></div>
|
||||
<div class="status-sub" ng-if="item.status != 'error' && item.anonymizeDate" title="Last anonymized {{item.anonymizeDate | humanTime}}" ng-bind="item.anonymizeDate | humanTime"></div>
|
||||
</div>
|
||||
<div class="cell-views num" role="cell" ng-bind="item.pageView | number"></div>
|
||||
|
||||
@@ -8,6 +8,18 @@
|
||||
</div>
|
||||
<div ng-if="type == 'audio'"><audio controls="controls"><source ng-src="{{url}}" /></audio></div>
|
||||
<div ng-if="type == 'IPython'"><notebook file="url"></notebook></div>
|
||||
<div ng-if="type == 'rate_limited'" class="file-error container d-flex h-100">
|
||||
<div class="paper-ratelimit-card m-auto" style="max-width:520px;">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="paper-error-msg">This repository will be available in <strong>{{rateLimitCountdown}}</strong>. The page will reload automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="type == 'error'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto" translate="ERRORS.{{content}}">Error</h1></div>
|
||||
<div ng-if="type == 'loading' && !error" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Loading…</h1></div>
|
||||
<div ng-if="type == 'empty'" class="file-error container d-flex h-100"><h1 class="paper-empty-title m-auto">Empty <em>repository</em>.</h1></div>
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
<h1 class="paper-page-title">Status of <em>{{repoId}}</em></h1>
|
||||
<p class="paper-page-lede">Track progress as your anonymization is prepared.</p>
|
||||
</div>
|
||||
<span class="status-pill" ng-class="{'status-pill-ready': repo.status == 'ready', 'status-pill-error': repo.status == 'error', 'status-pill-removed': repo.status == 'removed' || repo.status == 'expired'}">
|
||||
<span class="status-dot" ng-class="{'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-removed': repo.status == 'removed' || repo.status == 'expired'}"></span>
|
||||
<span ng-bind="repo.status | title"></span>
|
||||
<span class="status-pill" ng-class="{'status-pill-ready': repo.status == 'ready', 'status-pill-error': repo.status == 'error', 'status-pill-removed': repo.status == 'removed' || repo.status == 'expired', 'status-pill-ratelimit': rateLimitResetAt}">
|
||||
<span class="status-dot" ng-class="{'status-ready': repo.status == 'ready', 'status-error': repo.status == 'error', 'status-removed': repo.status == 'removed' || repo.status == 'expired', 'status-ratelimit': rateLimitResetAt}"></span>
|
||||
<span ng-if="!rateLimitResetAt" ng-bind="repo.status | title"></span>
|
||||
<span ng-if="rateLimitResetAt">Rate limited</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +20,18 @@
|
||||
size. Visit the <a href="/faq">FAQ</a> for more information.
|
||||
</p>
|
||||
|
||||
<div class="paper-progress" ng-if="repo.status != 'error'" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="100" ng-class="{'paper-progress-ready': repo.status == 'ready'}">
|
||||
<div class="paper-ratelimit-card" ng-if="rateLimitResetAt" role="status">
|
||||
<div class="paper-ratelimit-head">
|
||||
<i class="fas fa-hourglass-half"></i>
|
||||
<div>
|
||||
<div class="paper-error-eyebrow">Temporarily paused</div>
|
||||
<div class="paper-error-title">GitHub API rate limit reached</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="paper-error-msg">Anonymization will resume automatically in <strong>{{rateLimitCountdown}}</strong>. No action needed — the job is queued and will continue where it left off.</p>
|
||||
</div>
|
||||
|
||||
<div class="paper-progress" ng-if="repo.status != 'error' && !rateLimitResetAt" role="progressbar" aria-valuenow="{{progress}}" aria-valuemin="0" aria-valuemax="100" ng-class="{'paper-progress-ready': repo.status == 'ready'}">
|
||||
<div class="paper-progress-bar" style="width: {{progress}}%;"></div>
|
||||
<div class="paper-progress-label">
|
||||
<span ng-bind="repo.status | title"></span><span ng-if="repo.statusMessage"> · <span ng-bind="repo.statusMessage"></span></span>
|
||||
@@ -27,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="paper-error-card" ng-if="repo.status == 'error'" role="alert">
|
||||
<div class="paper-error-card" ng-if="repo.status == 'error' && !rateLimitResetAt" role="alert">
|
||||
<div class="paper-error-head">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<div>
|
||||
|
||||
+276
-73
@@ -919,12 +919,17 @@ angular
|
||||
$scope.selectedQueue = "download";
|
||||
$scope.selectedStats = null;
|
||||
$scope.range = "1h";
|
||||
$scope.allStates = ["active", "waiting", "delayed", "failed", "completed"];
|
||||
$scope.stateFilter = { active: true, waiting: true, delayed: true, failed: true, completed: true };
|
||||
$scope.query = {
|
||||
search: "",
|
||||
state: "active",
|
||||
autoRefresh: true,
|
||||
};
|
||||
|
||||
$scope.filteredJobs = () => {
|
||||
return ($scope.jobs || []).filter((j) => $scope.stateFilter[j._state]);
|
||||
};
|
||||
|
||||
$scope.jobProgressPct = (job) => {
|
||||
if (job && job.progress && typeof job.progress === "object" && typeof job.progress.percent === "number") {
|
||||
return Math.max(0, Math.min(100, Math.round(job.progress.percent)));
|
||||
@@ -943,20 +948,22 @@ angular
|
||||
return (ms / 1000).toFixed(1) + "s";
|
||||
};
|
||||
|
||||
$scope.metricsPoints = [];
|
||||
|
||||
$scope.selectQueue = (key) => {
|
||||
$scope.selectedQueue = key;
|
||||
getQueues();
|
||||
getMetrics();
|
||||
};
|
||||
|
||||
$scope.setRange = (r) => {
|
||||
$scope.range = r;
|
||||
getQueues();
|
||||
getMetrics();
|
||||
};
|
||||
|
||||
function getQueues() {
|
||||
const params = {
|
||||
queue: $scope.selectedQueue,
|
||||
state: $scope.query.state,
|
||||
search: $scope.query.search,
|
||||
};
|
||||
$http.get("/api/admin/queues", { params }).then(
|
||||
@@ -964,26 +971,50 @@ angular
|
||||
$scope.queueList = res.data.queues || [];
|
||||
$scope.jobs = res.data.jobs || [];
|
||||
$scope.selectedStats = $scope.queueList.find((q) => q.key === $scope.selectedQueue) || $scope.queueList[0] || null;
|
||||
},
|
||||
(err) => console.error(err)
|
||||
);
|
||||
}
|
||||
|
||||
function getMetrics() {
|
||||
$http.get("/api/admin/queues/metrics", {
|
||||
params: { queue: $scope.selectedQueue, range: $scope.range }
|
||||
}).then(
|
||||
(res) => {
|
||||
$scope.metricsPoints = res.data.points || [];
|
||||
$timeout(drawChart, 0);
|
||||
},
|
||||
(err) => console.error(err)
|
||||
);
|
||||
}
|
||||
getQueues();
|
||||
getMetrics();
|
||||
|
||||
const stop = $interval(() => {
|
||||
if ($scope.query.autoRefresh) getQueues();
|
||||
if ($scope.query.autoRefresh) {
|
||||
getQueues();
|
||||
getMetrics();
|
||||
}
|
||||
}, 5000);
|
||||
$scope.$on("$destroy", () => $interval.cancel(stop));
|
||||
|
||||
$scope.refreshNow = getQueues;
|
||||
$scope.refreshNow = function () { getQueues(); getMetrics(); };
|
||||
|
||||
function apiError(err) {
|
||||
const msg = (err && err.data && (err.data.message || err.data.error)) || "Request failed";
|
||||
$scope.actionError = msg;
|
||||
$timeout(() => { $scope.actionError = null; }, 5000);
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
$scope.actionError = null;
|
||||
|
||||
$scope.removeJob = (job) => {
|
||||
$http.delete(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, (err) => console.error(err));
|
||||
$http.delete(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, apiError);
|
||||
};
|
||||
|
||||
$scope.retryJob = (job) => {
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, (err) => console.error(err));
|
||||
$http.post(`/api/admin/queue/${$scope.selectedQueue}/${job.id}`).then(getQueues, apiError);
|
||||
};
|
||||
|
||||
$scope.retryFailed = () => {
|
||||
@@ -1016,88 +1047,260 @@ angular
|
||||
clearTimeout(searchClear);
|
||||
searchClear = setTimeout(getQueues, 350);
|
||||
});
|
||||
$scope.$watch("query.state", getQueues);
|
||||
$scope.expanded = {};
|
||||
$scope.toggleJob = (job) => {
|
||||
$scope.expanded[job.id] = !$scope.expanded[job.id];
|
||||
};
|
||||
|
||||
$scope.humanTime = (ts) => {
|
||||
if (!ts) return "";
|
||||
const d = new Date(ts);
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false })
|
||||
+ " " + d.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
};
|
||||
|
||||
$scope.delayCountdown = (ts) => {
|
||||
if (!ts) return "";
|
||||
var remaining = Math.max(0, Math.ceil((ts - Date.now()) / 1000));
|
||||
if (remaining <= 0) return "resuming soon";
|
||||
var min = Math.floor(remaining / 60);
|
||||
var sec = remaining % 60;
|
||||
return "in " + (min > 0 ? min + "m " + sec + "s" : sec + "s");
|
||||
};
|
||||
|
||||
function niceScale(max) {
|
||||
if (max <= 0) return { ticks: [0], niceMax: 1 };
|
||||
const mag = Math.pow(10, Math.floor(Math.log10(max)));
|
||||
let step = mag;
|
||||
if (max / step < 2) step = mag / 2;
|
||||
else if (max / step > 5) step = mag * 2;
|
||||
const niceMax = Math.ceil(max / step) * step;
|
||||
const ticks = [];
|
||||
for (let v = 0; v <= niceMax; v += step) ticks.push(v);
|
||||
return { ticks, niceMax };
|
||||
}
|
||||
|
||||
function drawChart() {
|
||||
const canvas = document.getElementById("q-throughput-chart");
|
||||
if (!canvas || !$scope.selectedStats) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
const w = rect.width - 40;
|
||||
const h = 160;
|
||||
canvas.width = w * dpr;
|
||||
canvas.height = h * dpr;
|
||||
canvas.style.width = w + "px";
|
||||
canvas.style.height = h + "px";
|
||||
var canvas = document.getElementById("q-throughput-chart");
|
||||
if (!canvas) return;
|
||||
var ctx = canvas.getContext("2d");
|
||||
var dpr = window.devicePixelRatio || 1;
|
||||
var rect = canvas.parentElement.getBoundingClientRect();
|
||||
var marginLeft = 44;
|
||||
var marginRight = 50;
|
||||
var marginBottom = 20;
|
||||
var totalW = rect.width - 40;
|
||||
var totalH = 180;
|
||||
var w = totalW - marginLeft - marginRight;
|
||||
var h = totalH - marginBottom;
|
||||
canvas.width = totalW * dpr;
|
||||
canvas.height = totalH * dpr;
|
||||
canvas.style.width = totalW + "px";
|
||||
canvas.style.height = totalH + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
const data = ($scope.selectedStats.throughput || []).slice().reverse();
|
||||
if (data.length === 0) {
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue("--ink-muted").trim() || "#8A857C";
|
||||
ctx.font = "12px var(--font-mono)";
|
||||
var isDark = document.body.classList.contains("dark-mode");
|
||||
var labelColor = "#8A857C";
|
||||
var gridColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
|
||||
var completedColor = isDark ? "#A7B2FF" : "#3B4AD6";
|
||||
var completedFill = isDark ? "rgba(167,178,255,0.12)" : "rgba(59,74,214,0.08)";
|
||||
var failedColor = isDark ? "#F08A82" : "#B42318";
|
||||
var failedFill = isDark ? "rgba(240,138,130,0.08)" : "rgba(180,35,24,0.06)";
|
||||
var execColor = isDark ? "#F5C842" : "#B8860B";
|
||||
|
||||
var pts = $scope.metricsPoints || [];
|
||||
if (pts.length === 0) {
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.font = "12px monospace";
|
||||
ctx.textAlign = "center";
|
||||
ctx.fillText("No throughput data yet", w / 2, h / 2);
|
||||
ctx.fillText("No metrics data yet", totalW / 2, totalH / 2);
|
||||
chartState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const rangePoints = { "1h": 60, "6h": 120, "24h": 120, "7d": 120 };
|
||||
const pts = data.slice(0, rangePoints[$scope.range] || 60);
|
||||
const max = Math.max(1, ...pts);
|
||||
const step = w / (pts.length - 1 || 1);
|
||||
// Data is oldest→newest from the API; chart shows newest on the right
|
||||
var completedPts = pts.map(function (p) { return p.completed; });
|
||||
var failedPts = pts.map(function (p) { return p.failed; });
|
||||
var execPts = pts.map(function (p) { return p.avgMs; });
|
||||
var maxLen = pts.length;
|
||||
var step = w / (maxLen - 1 || 1);
|
||||
|
||||
const isDark = document.body.classList.contains("dark-mode");
|
||||
const lineColor = isDark ? "#A7B2FF" : "#3B4AD6";
|
||||
const fillColor = isDark ? "rgba(167,178,255,0.12)" : "rgba(59,74,214,0.08)";
|
||||
const gridColor = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)";
|
||||
// Left Y-axis: jobs/min
|
||||
var rawMax = Math.max(1, Math.max.apply(null, completedPts), Math.max.apply(null, failedPts));
|
||||
var left = niceScale(rawMax);
|
||||
|
||||
// grid
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const y = (h / 4) * i;
|
||||
// Right Y-axis: avg exec time (ms)
|
||||
var execMax = Math.max.apply(null, execPts);
|
||||
var right = execMax > 0 ? niceScale(execMax) : { ticks: [0], niceMax: 1 };
|
||||
|
||||
var toY = function (v) { return h - (v / left.niceMax) * (h - 10); };
|
||||
var toYr = function (v) { return h - (v / right.niceMax) * (h - 10); };
|
||||
var toX = function (i) { return marginLeft + i * step; };
|
||||
|
||||
// Grid + left Y-axis labels (jobs/min)
|
||||
ctx.textAlign = "right";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = "10px monospace";
|
||||
left.ticks.forEach(function (v) {
|
||||
var y = toY(v);
|
||||
ctx.strokeStyle = gridColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(w, y);
|
||||
ctx.moveTo(marginLeft, y);
|
||||
ctx.lineTo(totalW - marginRight, y);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.fillText(v >= 1000 ? (v / 1000).toFixed(1) + "k" : String(v), marginLeft - 6, y);
|
||||
});
|
||||
|
||||
// Right Y-axis labels (ms)
|
||||
if (execMax > 0) {
|
||||
ctx.textAlign = "left";
|
||||
right.ticks.forEach(function (v) {
|
||||
var y = toYr(v);
|
||||
ctx.fillStyle = execColor;
|
||||
ctx.fillText(v >= 1000 ? (v / 1000).toFixed(1) + "s" : v + "ms", totalW - marginRight + 6, y);
|
||||
});
|
||||
}
|
||||
|
||||
// X-axis time labels using actual timestamps
|
||||
var now = Date.now();
|
||||
var xLabelCount = Math.min(6, maxLen);
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "top";
|
||||
for (var i = 0; i < xLabelCount; i++) {
|
||||
var idx = Math.round((i / (xLabelCount - 1)) * (maxLen - 1));
|
||||
var minsAgo = Math.round((now - pts[idx].ts) / 60000);
|
||||
var x = toX(idx);
|
||||
var lbl;
|
||||
if (minsAgo <= 0) lbl = "now";
|
||||
else if (minsAgo < 60) lbl = minsAgo + "m";
|
||||
else if (minsAgo < 1440) lbl = Math.round(minsAgo / 60) + "h";
|
||||
else lbl = Math.round(minsAgo / 1440) + "d";
|
||||
ctx.fillStyle = labelColor;
|
||||
ctx.fillText(lbl, x, h + 4);
|
||||
}
|
||||
|
||||
function drawArea(data, yFn, fillStyle, strokeStyle) {
|
||||
if (data.length === 0) return;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toX(0), h);
|
||||
data.forEach(function (v, i) {
|
||||
var x = toX(i), y = yFn(v);
|
||||
if (i === 0) ctx.lineTo(x, y);
|
||||
else {
|
||||
var cx = (toX(i - 1) + x) / 2;
|
||||
ctx.bezierCurveTo(cx, yFn(data[i - 1]), cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.lineTo(toX(data.length - 1), h);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fillStyle;
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
data.forEach(function (v, i) {
|
||||
var x = toX(i), y = yFn(v);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else {
|
||||
var cx = (toX(i - 1) + x) / 2;
|
||||
ctx.bezierCurveTo(cx, yFn(data[i - 1]), cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.strokeStyle = strokeStyle;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// area fill
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h);
|
||||
pts.forEach((v, i) => {
|
||||
const x = i * step;
|
||||
const y = h - (v / max) * (h - 10);
|
||||
if (i === 0) ctx.lineTo(x, y);
|
||||
else {
|
||||
const px = (i - 1) * step;
|
||||
const py = h - (pts[i - 1] / max) * (h - 10);
|
||||
const cx = (px + x) / 2;
|
||||
ctx.bezierCurveTo(cx, py, cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.lineTo(w, h);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fill();
|
||||
drawArea(completedPts, toY, completedFill, completedColor);
|
||||
drawArea(failedPts, toY, failedFill, failedColor);
|
||||
// Exec time as a line only (no fill) on the right axis
|
||||
if (execMax > 0) {
|
||||
ctx.beginPath();
|
||||
execPts.forEach(function (v, i) {
|
||||
var x = toX(i), y = toYr(v);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else {
|
||||
var cx = (toX(i - 1) + x) / 2;
|
||||
ctx.bezierCurveTo(cx, toYr(execPts[i - 1]), cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.strokeStyle = execColor;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 3]);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
|
||||
// line
|
||||
ctx.beginPath();
|
||||
pts.forEach((v, i) => {
|
||||
const x = i * step;
|
||||
const y = h - (v / max) * (h - 10);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else {
|
||||
const px = (i - 1) * step;
|
||||
const py = h - (pts[i - 1] / max) * (h - 10);
|
||||
const cx = (px + x) / 2;
|
||||
ctx.bezierCurveTo(cx, py, cx, y, x, y);
|
||||
}
|
||||
});
|
||||
ctx.strokeStyle = lineColor;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
chartState = { pts: pts, maxLen: maxLen, marginLeft: marginLeft, step: step, totalW: totalW, toX: toX };
|
||||
}
|
||||
|
||||
var chartState = null;
|
||||
|
||||
function setupTooltip() {
|
||||
var canvas = document.getElementById("q-throughput-chart");
|
||||
if (!canvas || canvas._tipBound) return;
|
||||
canvas._tipBound = true;
|
||||
|
||||
var tooltip = document.getElementById("q-chart-tooltip");
|
||||
var crosshair = document.getElementById("q-chart-crosshair");
|
||||
|
||||
canvas.addEventListener("mousemove", function (e) {
|
||||
if (!chartState || !tooltip || !crosshair) return;
|
||||
var cs = chartState;
|
||||
var rect = canvas.getBoundingClientRect();
|
||||
var mx = e.clientX - rect.left;
|
||||
|
||||
var idx = Math.round((mx - cs.marginLeft) / cs.step);
|
||||
if (idx < 0 || idx >= cs.maxLen) {
|
||||
tooltip.style.display = "none";
|
||||
crosshair.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
var p = cs.pts[idx];
|
||||
var now = Date.now();
|
||||
var minsAgo = Math.round((now - p.ts) / 60000);
|
||||
var timeLabel;
|
||||
if (minsAgo <= 0) timeLabel = "now";
|
||||
else if (minsAgo < 60) timeLabel = minsAgo + "m ago";
|
||||
else if (minsAgo < 1440) {
|
||||
var hrs = Math.floor(minsAgo / 60);
|
||||
var mins = minsAgo % 60;
|
||||
timeLabel = hrs + "h" + (mins ? " " + mins + "m" : "") + " ago";
|
||||
} else timeLabel = Math.round(minsAgo / 1440) + "d ago";
|
||||
|
||||
var html =
|
||||
'<div class="q-tip-time">' + timeLabel + '</div>' +
|
||||
'<div class="q-tip-completed">● completed: ' + p.completed + '/min</div>' +
|
||||
'<div class="q-tip-failed">● failed: ' + p.failed + '/min</div>';
|
||||
if (p.avgMs > 0) {
|
||||
var dur = p.avgMs >= 1000 ? (p.avgMs / 1000).toFixed(1) + "s" : p.avgMs + "ms";
|
||||
html += '<div class="q-tip-exec">● avg time: ' + dur + '</div>';
|
||||
}
|
||||
tooltip.innerHTML = html;
|
||||
|
||||
var xPos = cs.toX(idx);
|
||||
var tipW = tooltip.offsetWidth;
|
||||
var tipLeft = xPos + 10;
|
||||
if (tipLeft + tipW > cs.totalW) tipLeft = xPos - tipW - 10;
|
||||
|
||||
tooltip.style.display = "block";
|
||||
tooltip.style.left = tipLeft + "px";
|
||||
tooltip.style.top = "8px";
|
||||
|
||||
crosshair.style.display = "block";
|
||||
crosshair.style.left = xPos + "px";
|
||||
});
|
||||
|
||||
canvas.addEventListener("mouseleave", function () {
|
||||
if (tooltip) tooltip.style.display = "none";
|
||||
if (crosshair) crosshair.style.display = "none";
|
||||
});
|
||||
}
|
||||
|
||||
$scope.$watch("metricsPoints", function () {
|
||||
$timeout(setupTooltip, 50);
|
||||
});
|
||||
},
|
||||
])
|
||||
.controller("errorsAdminController", [
|
||||
|
||||
+96
-7
@@ -226,6 +226,20 @@ angular
|
||||
return capitalized.join(" ");
|
||||
};
|
||||
})
|
||||
.filter("statusMsg", function () {
|
||||
return function (msg) {
|
||||
if (!msg) return msg;
|
||||
var m = msg.match(/^rate_limited:(\d+)$/);
|
||||
if (m) {
|
||||
var remaining = Math.max(0, Math.ceil((parseInt(m[1], 10) - Date.now()) / 1000));
|
||||
if (remaining <= 0) return "Rate limited — resuming soon";
|
||||
var min = Math.floor(remaining / 60);
|
||||
var sec = remaining % 60;
|
||||
return "Rate limited — retrying in " + (min > 0 ? min + "m " + sec + "s" : sec + "s");
|
||||
}
|
||||
return msg;
|
||||
};
|
||||
})
|
||||
.filter("diff", [
|
||||
"$sce",
|
||||
function ($sce) {
|
||||
@@ -1520,6 +1534,47 @@ angular
|
||||
$scope.repoId = $routeParams.repoId;
|
||||
$scope.repo = null;
|
||||
$scope.progress = 0;
|
||||
$scope.rateLimitResetAt = 0;
|
||||
$scope.rateLimitCountdown = "";
|
||||
|
||||
var countdownTimer = null;
|
||||
function startRateLimitCountdown(resetAt) {
|
||||
$scope.rateLimitResetAt = resetAt;
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
function tick() {
|
||||
var remaining = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000));
|
||||
if (remaining <= 0) {
|
||||
$scope.rateLimitCountdown = "";
|
||||
$scope.rateLimitResetAt = 0;
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
} else {
|
||||
var min = Math.floor(remaining / 60);
|
||||
var sec = remaining % 60;
|
||||
$scope.rateLimitCountdown = min > 0
|
||||
? min + "m " + sec + "s"
|
||||
: sec + "s";
|
||||
}
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
tick();
|
||||
countdownTimer = setInterval(tick, 1000);
|
||||
}
|
||||
$scope.$on("$destroy", function () {
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
});
|
||||
|
||||
function parseStatusMessage(msg) {
|
||||
if (!msg) return msg;
|
||||
var m = msg.match(/^rate_limited:(\d+)$/);
|
||||
if (m) {
|
||||
startRateLimitCountdown(parseInt(m[1], 10));
|
||||
return null;
|
||||
}
|
||||
$scope.rateLimitResetAt = 0;
|
||||
return msg;
|
||||
}
|
||||
|
||||
$scope.getStatus = () => {
|
||||
$http
|
||||
.get("/api/repo/" + $scope.repoId, {
|
||||
@@ -1529,6 +1584,11 @@ angular
|
||||
.then(
|
||||
(res) => {
|
||||
$scope.repo = res.data;
|
||||
if (res.data.rateLimitResetAt) {
|
||||
startRateLimitCountdown(res.data.rateLimitResetAt);
|
||||
} else {
|
||||
$scope.repo.statusMessage = parseStatusMessage($scope.repo.statusMessage);
|
||||
}
|
||||
if ($scope.repo.status == "ready") {
|
||||
$scope.progress = 100;
|
||||
} else if ($scope.repo.status == "queue") {
|
||||
@@ -1542,10 +1602,11 @@ angular
|
||||
} else if ($scope.repo.status == "anonymizing") {
|
||||
$scope.progress = 75;
|
||||
}
|
||||
if (
|
||||
$scope.repo.status != "ready" &&
|
||||
$scope.repo.status != "error"
|
||||
) {
|
||||
var shouldPoll = $scope.repo.status != "ready";
|
||||
if ($scope.repo.status == "error" && !$scope.rateLimitResetAt) {
|
||||
shouldPoll = false;
|
||||
}
|
||||
if (shouldPoll) {
|
||||
setTimeout($scope.getStatus, 2000);
|
||||
}
|
||||
},
|
||||
@@ -2598,12 +2659,14 @@ angular
|
||||
)[0];
|
||||
}
|
||||
|
||||
var rlCountdownTimer = null;
|
||||
$scope.$on("$destroy", function () { if (rlCountdownTimer) clearInterval(rlCountdownTimer); });
|
||||
|
||||
function getOptions(callback) {
|
||||
$http.get(`/api/repo/${$scope.repoId}/options`).then(
|
||||
(res) => {
|
||||
$scope.options = res.data;
|
||||
if ($scope.options.url) {
|
||||
// the repository is expired with redirect option
|
||||
window.location = $scope.options.url;
|
||||
return;
|
||||
}
|
||||
@@ -2612,8 +2675,34 @@ angular
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
$scope.type = "error";
|
||||
$scope.content = err.data.error;
|
||||
var data = err.data || {};
|
||||
if (data.error === "rate_limited" && data.resetAt) {
|
||||
$scope.type = "rate_limited";
|
||||
$scope.rateLimitResetAt = data.resetAt;
|
||||
if (rlCountdownTimer) clearInterval(rlCountdownTimer);
|
||||
function rlTick() {
|
||||
var remaining = Math.max(0, Math.ceil(($scope.rateLimitResetAt - Date.now()) / 1000));
|
||||
if (remaining <= 0) {
|
||||
$scope.rateLimitCountdown = "";
|
||||
$scope.rateLimitResetAt = 0;
|
||||
if (rlCountdownTimer) { clearInterval(rlCountdownTimer); rlCountdownTimer = null; }
|
||||
getOptions(callback);
|
||||
} else {
|
||||
var min = Math.floor(remaining / 60);
|
||||
var sec = remaining % 60;
|
||||
$scope.rateLimitCountdown = min > 0 ? min + "m " + sec + "s" : sec + "s";
|
||||
}
|
||||
$scope.$applyAsync();
|
||||
}
|
||||
rlTick();
|
||||
rlCountdownTimer = setInterval(rlTick, 1000);
|
||||
} else if (data.error === "repository_not_ready") {
|
||||
$scope.type = "loading";
|
||||
setTimeout(function () { getOptions(callback); }, 3000);
|
||||
} else {
|
||||
$scope.type = "error";
|
||||
$scope.content = data.error;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user