mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-01-04 01:46:54 +10:00
747 lines
25 KiB
HTML
747 lines
25 KiB
HTML
<style>
|
|
/* Progress Monitor Container */
|
|
#progress-monitor {
|
|
background: linear-gradient(135deg, #0d1117 0%, #161b22 100%);
|
|
color: #c9d1d9;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
|
|
font-size: 12px;
|
|
border-bottom: 1px solid #30363d;
|
|
position: relative;
|
|
z-index: 100;
|
|
}
|
|
#progress-monitor.hidden {
|
|
display: none;
|
|
}
|
|
#progress-monitor .tree-container {
|
|
max-height: 350px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Header Bar */
|
|
#progress-monitor .header-bar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 16px;
|
|
background: rgba(0,0,0,0.2);
|
|
border-bottom: 1px solid #30363d;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
#progress-monitor .header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
#progress-monitor .header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* Orchestrator Status */
|
|
#progress-monitor .orchestrator-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
#progress-monitor .status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
#progress-monitor .status-dot.running {
|
|
background: #3fb950;
|
|
box-shadow: 0 0 8px #3fb950;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
#progress-monitor .status-dot.idle {
|
|
background: #d29922;
|
|
box-shadow: 0 0 4px #d29922;
|
|
}
|
|
#progress-monitor .status-dot.stopped {
|
|
background: #6e7681;
|
|
}
|
|
#progress-monitor .status-dot.flash {
|
|
animation: flash 0.3s ease-out;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; box-shadow: 0 0 8px #3fb950; }
|
|
50% { opacity: 0.6; box-shadow: 0 0 4px #3fb950; }
|
|
}
|
|
@keyframes flash {
|
|
0% { transform: scale(1.5); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
/* Stats */
|
|
#progress-monitor .stats {
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
#progress-monitor .stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
#progress-monitor .stat-label {
|
|
color: #8b949e;
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
#progress-monitor .stat-value {
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
#progress-monitor .stat-value.success { color: #3fb950; }
|
|
#progress-monitor .stat-value.error { color: #f85149; }
|
|
#progress-monitor .stat-value.warning { color: #d29922; }
|
|
#progress-monitor .stat-value.info { color: #58a6ff; }
|
|
#progress-monitor .stat.clickable {
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
margin: -2px -6px;
|
|
border-radius: 4px;
|
|
transition: background 0.2s;
|
|
}
|
|
#progress-monitor .stat.clickable:hover {
|
|
background: rgba(255,255,255,0.1);
|
|
}
|
|
#progress-monitor .stat.clickable:active {
|
|
background: rgba(255,255,255,0.2);
|
|
}
|
|
|
|
/* Toggle Button */
|
|
#progress-monitor .toggle-btn {
|
|
background: transparent;
|
|
border: 1px solid #30363d;
|
|
color: #8b949e;
|
|
cursor: pointer;
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
transition: all 0.2s;
|
|
}
|
|
#progress-monitor .toggle-btn:hover {
|
|
background: #21262d;
|
|
color: #c9d1d9;
|
|
border-color: #8b949e;
|
|
}
|
|
|
|
/* Tree Container */
|
|
#progress-monitor .tree-container {
|
|
padding: 12px 16px;
|
|
}
|
|
#progress-monitor.collapsed .tree-container {
|
|
display: none;
|
|
}
|
|
|
|
/* Idle Message */
|
|
#progress-monitor .idle-message {
|
|
color: #8b949e;
|
|
font-style: italic;
|
|
padding: 8px 0;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Crawl Item */
|
|
#progress-monitor .crawl-item {
|
|
background: #161b22;
|
|
border: 1px solid #30363d;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
overflow: hidden;
|
|
}
|
|
#progress-monitor .crawl-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 14px;
|
|
background: rgba(0,0,0,0.2);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
#progress-monitor .crawl-header:hover {
|
|
background: rgba(88, 166, 255, 0.1);
|
|
}
|
|
#progress-monitor a.crawl-header:visited {
|
|
color: inherit;
|
|
}
|
|
#progress-monitor .crawl-icon {
|
|
font-size: 16px;
|
|
width: 20px;
|
|
text-align: center;
|
|
}
|
|
#progress-monitor .crawl-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
#progress-monitor .crawl-label {
|
|
font-weight: 600;
|
|
color: #58a6ff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
#progress-monitor .crawl-meta {
|
|
font-size: 11px;
|
|
color: #8b949e;
|
|
margin-top: 2px;
|
|
}
|
|
#progress-monitor .crawl-stats {
|
|
display: flex;
|
|
gap: 12px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
#progress-monitor .progress-bar-container {
|
|
height: 4px;
|
|
background: #21262d;
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
#progress-monitor .progress-bar {
|
|
height: 100%;
|
|
border-radius: 2px;
|
|
transition: width 0.5s ease-out;
|
|
position: relative;
|
|
}
|
|
#progress-monitor .progress-bar.crawl {
|
|
background: linear-gradient(90deg, #238636 0%, #3fb950 100%);
|
|
}
|
|
#progress-monitor .progress-bar.snapshot {
|
|
background: linear-gradient(90deg, #1f6feb 0%, #58a6ff 100%);
|
|
}
|
|
#progress-monitor .progress-bar.extractor {
|
|
background: linear-gradient(90deg, #8957e5 0%, #a371f7 100%);
|
|
}
|
|
#progress-monitor .progress-bar.indeterminate {
|
|
background: linear-gradient(90deg, transparent 0%, #58a6ff 50%, transparent 100%);
|
|
animation: indeterminate 1.5s infinite linear;
|
|
width: 30% !important;
|
|
}
|
|
@keyframes indeterminate {
|
|
0% { transform: translateX(-100%); }
|
|
100% { transform: translateX(400%); }
|
|
}
|
|
|
|
/* Crawl Body */
|
|
#progress-monitor .crawl-body {
|
|
padding: 0 14px 14px;
|
|
}
|
|
#progress-monitor .crawl-progress {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid #21262d;
|
|
}
|
|
|
|
/* Snapshot List */
|
|
#progress-monitor .snapshot-list {
|
|
margin-top: 8px;
|
|
}
|
|
#progress-monitor .snapshot-item {
|
|
background: #0d1117;
|
|
border: 1px solid #21262d;
|
|
border-radius: 6px;
|
|
margin-bottom: 8px;
|
|
overflow: hidden;
|
|
}
|
|
#progress-monitor .snapshot-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
#progress-monitor .snapshot-header:hover {
|
|
background: rgba(88, 166, 255, 0.05);
|
|
}
|
|
#progress-monitor a.snapshot-header:visited {
|
|
color: inherit;
|
|
}
|
|
#progress-monitor .snapshot-icon {
|
|
font-size: 14px;
|
|
width: 18px;
|
|
text-align: center;
|
|
color: #58a6ff;
|
|
}
|
|
#progress-monitor .snapshot-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
#progress-monitor .snapshot-url {
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 11px;
|
|
color: #c9d1d9;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
#progress-monitor .snapshot-meta {
|
|
font-size: 10px;
|
|
color: #8b949e;
|
|
margin-top: 2px;
|
|
}
|
|
#progress-monitor .snapshot-progress {
|
|
padding: 0 12px 8px;
|
|
}
|
|
|
|
/* Extractor List - Compact Badge Layout */
|
|
#progress-monitor .extractor-list {
|
|
padding: 8px 12px;
|
|
background: rgba(0,0,0,0.2);
|
|
border-top: 1px solid #21262d;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
}
|
|
#progress-monitor .extractor-badge {
|
|
position: relative;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 3px 8px;
|
|
border-radius: 4px;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 10px;
|
|
background: #21262d;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
#progress-monitor .extractor-badge .progress-fill {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
bottom: 0;
|
|
z-index: 0;
|
|
transition: width 0.3s ease-out;
|
|
}
|
|
#progress-monitor .extractor-badge .badge-content {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
#progress-monitor .extractor-badge.queued {
|
|
color: #8b949e;
|
|
}
|
|
#progress-monitor .extractor-badge.queued .progress-fill {
|
|
background: rgba(110, 118, 129, 0.2);
|
|
width: 0%;
|
|
}
|
|
#progress-monitor .extractor-badge.started {
|
|
color: #d29922;
|
|
}
|
|
#progress-monitor .extractor-badge.started .progress-fill {
|
|
background: rgba(210, 153, 34, 0.3);
|
|
width: 50%;
|
|
animation: progress-pulse 1.5s ease-in-out infinite;
|
|
}
|
|
@keyframes progress-pulse {
|
|
0%, 100% { opacity: 0.5; }
|
|
50% { opacity: 1; }
|
|
}
|
|
#progress-monitor .extractor-badge.succeeded {
|
|
color: #3fb950;
|
|
}
|
|
#progress-monitor .extractor-badge.succeeded .progress-fill {
|
|
background: rgba(63, 185, 80, 0.25);
|
|
width: 100%;
|
|
}
|
|
#progress-monitor .extractor-badge.failed {
|
|
color: #f85149;
|
|
}
|
|
#progress-monitor .extractor-badge.failed .progress-fill {
|
|
background: rgba(248, 81, 73, 0.25);
|
|
width: 100%;
|
|
}
|
|
#progress-monitor .extractor-badge .badge-icon {
|
|
font-size: 10px;
|
|
}
|
|
#progress-monitor .extractor-badge.started .badge-icon {
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Status Badge */
|
|
#progress-monitor .status-badge {
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
#progress-monitor .status-badge.queued {
|
|
background: #21262d;
|
|
color: #8b949e;
|
|
}
|
|
#progress-monitor .status-badge.started {
|
|
background: rgba(210, 153, 34, 0.2);
|
|
color: #d29922;
|
|
}
|
|
#progress-monitor .status-badge.sealed,
|
|
#progress-monitor .status-badge.succeeded {
|
|
background: rgba(63, 185, 80, 0.2);
|
|
color: #3fb950;
|
|
}
|
|
#progress-monitor .status-badge.failed {
|
|
background: rgba(248, 81, 73, 0.2);
|
|
color: #f85149;
|
|
}
|
|
|
|
</style>
|
|
|
|
<div id="progress-monitor">
|
|
<div class="header-bar">
|
|
<div class="header-left">
|
|
<div class="orchestrator-status">
|
|
<span class="status-dot stopped" id="orchestrator-dot"></span>
|
|
<span id="orchestrator-text">Stopped</span>
|
|
</div>
|
|
<div class="stats">
|
|
<div class="stat">
|
|
<span class="stat-label">Workers</span>
|
|
<span class="stat-value info" id="worker-count">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Queued</span>
|
|
<span class="stat-value warning" id="total-queued">0</span>
|
|
</div>
|
|
<div class="stat clickable" id="stat-succeeded" title="Click to reset counter">
|
|
<span class="stat-label">Done</span>
|
|
<span class="stat-value success" id="total-succeeded">0</span>
|
|
</div>
|
|
<div class="stat clickable" id="stat-failed" title="Click to reset counter">
|
|
<span class="stat-label">Failed</span>
|
|
<span class="stat-value error" id="total-failed">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<button class="toggle-btn" id="progress-collapse" title="Toggle details">Details</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tree-container" id="tree-container">
|
|
<div class="idle-message" id="idle-message">No active crawls</div>
|
|
<div id="crawl-tree"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
const monitor = document.getElementById('progress-monitor');
|
|
const collapseBtn = document.getElementById('progress-collapse');
|
|
const treeContainer = document.getElementById('tree-container');
|
|
const crawlTree = document.getElementById('crawl-tree');
|
|
const idleMessage = document.getElementById('idle-message');
|
|
|
|
let pollInterval = null;
|
|
let isCollapsed = localStorage.getItem('progress-monitor-collapsed') === 'true';
|
|
|
|
// Baselines for resettable counters
|
|
let succeededBaseline = parseInt(localStorage.getItem('progress-succeeded-baseline') || '0');
|
|
let failedBaseline = parseInt(localStorage.getItem('progress-failed-baseline') || '0');
|
|
let lastSucceeded = 0;
|
|
let lastFailed = 0;
|
|
|
|
// Click handlers for resetting counters
|
|
document.getElementById('stat-succeeded').addEventListener('click', function() {
|
|
succeededBaseline = lastSucceeded;
|
|
localStorage.setItem('progress-succeeded-baseline', succeededBaseline);
|
|
document.getElementById('total-succeeded').textContent = '0';
|
|
});
|
|
document.getElementById('stat-failed').addEventListener('click', function() {
|
|
failedBaseline = lastFailed;
|
|
localStorage.setItem('progress-failed-baseline', failedBaseline);
|
|
document.getElementById('total-failed').textContent = '0';
|
|
});
|
|
|
|
function formatUrl(url) {
|
|
try {
|
|
const u = new URL(url);
|
|
return u.hostname + u.pathname.substring(0, 30) + (u.pathname.length > 30 ? '...' : '');
|
|
} catch {
|
|
return url.substring(0, 50) + (url.length > 50 ? '...' : '');
|
|
}
|
|
}
|
|
|
|
function renderExtractor(extractor) {
|
|
const icon = extractor.status === 'started' ? '↻' :
|
|
extractor.status === 'succeeded' ? '✓' :
|
|
extractor.status === 'failed' ? '✗' : '○';
|
|
|
|
return `
|
|
<span class="extractor-badge ${extractor.status}">
|
|
<span class="progress-fill"></span>
|
|
<span class="badge-content">
|
|
<span class="badge-icon">${icon}</span>
|
|
<span>${extractor.extractor}</span>
|
|
</span>
|
|
</span>
|
|
`;
|
|
}
|
|
|
|
function renderSnapshot(snapshot, crawlId) {
|
|
const statusIcon = snapshot.status === 'started' ? '↻' : '📄';
|
|
const adminUrl = `/admin/core/snapshot/${snapshot.id}/change/`;
|
|
|
|
let extractorHtml = '';
|
|
if (snapshot.all_extractors && snapshot.all_extractors.length > 0) {
|
|
// Sort extractors alphabetically by name to prevent reordering on updates
|
|
const sortedExtractors = [...snapshot.all_extractors].sort((a, b) =>
|
|
a.extractor.localeCompare(b.extractor)
|
|
);
|
|
extractorHtml = `
|
|
<div class="extractor-list">
|
|
${sortedExtractors.map(e => renderExtractor(e)).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return `
|
|
<div class="snapshot-item">
|
|
<a class="snapshot-header" href="${adminUrl}">
|
|
<span class="snapshot-icon">${statusIcon}</span>
|
|
<div class="snapshot-info">
|
|
<div class="snapshot-url">${formatUrl(snapshot.url)}</div>
|
|
<div class="snapshot-meta">
|
|
${snapshot.completed_extractors}/${snapshot.total_extractors} extractors
|
|
${snapshot.failed_extractors > 0 ? `<span style="color:#f85149">(${snapshot.failed_extractors} failed)</span>` : ''}
|
|
</div>
|
|
</div>
|
|
<span class="status-badge ${snapshot.status}">${snapshot.status}</span>
|
|
</a>
|
|
<div class="snapshot-progress">
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar snapshot ${snapshot.status === 'started' && snapshot.progress === 0 ? 'indeterminate' : ''}"
|
|
style="width: ${snapshot.progress}%"></div>
|
|
</div>
|
|
</div>
|
|
${extractorHtml}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function renderCrawl(crawl) {
|
|
const statusIcon = crawl.status === 'started' ? '↻' : '🔍';
|
|
const adminUrl = `/admin/crawls/crawl/${crawl.id}/change/`;
|
|
|
|
let snapshotsHtml = '';
|
|
if (crawl.active_snapshots && crawl.active_snapshots.length > 0) {
|
|
snapshotsHtml = crawl.active_snapshots.map(s => renderSnapshot(s, crawl.id)).join('');
|
|
}
|
|
|
|
// Show warning if crawl is stuck (queued but can't start)
|
|
let warningHtml = '';
|
|
if (crawl.status === 'queued' && !crawl.can_start) {
|
|
warningHtml = `
|
|
<div style="padding: 8px 14px; background: rgba(248, 81, 73, 0.1); border-top: 1px solid #f85149; color: #f85149; font-size: 11px;">
|
|
⚠️ Crawl cannot start: ${crawl.urls_preview ? 'unknown error' : 'no URLs'}
|
|
</div>
|
|
`;
|
|
} else if (crawl.status === 'queued' && crawl.retry_at_future) {
|
|
// Queued but retry_at is in future (was claimed by worker, will retry)
|
|
warningHtml = `
|
|
<div style="padding: 8px 14px; background: rgba(88, 166, 255, 0.1); border-top: 1px solid #58a6ff; color: #58a6ff; font-size: 11px;">
|
|
🔄 Retrying in ${crawl.seconds_until_retry}s...${crawl.urls_preview ? ` (${crawl.urls_preview})` : ''}
|
|
</div>
|
|
`;
|
|
} else if (crawl.status === 'queued' && crawl.total_snapshots === 0) {
|
|
// Queued and waiting to be picked up by worker
|
|
warningHtml = `
|
|
<div style="padding: 8px 14px; background: rgba(210, 153, 34, 0.1); border-top: 1px solid #d29922; color: #d29922; font-size: 11px;">
|
|
⏳ Waiting for worker to pick up...${crawl.urls_preview ? ` (${crawl.urls_preview})` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Show snapshot info or URL count if no snapshots yet
|
|
let metaText = `depth: ${crawl.max_depth}`;
|
|
if (crawl.total_snapshots > 0) {
|
|
metaText += ` | ${crawl.total_snapshots} snapshots`;
|
|
} else if (crawl.urls_count > 0) {
|
|
metaText += ` | ${crawl.urls_count} URLs`;
|
|
} else if (crawl.urls_preview) {
|
|
metaText += ` | ${crawl.urls_preview.substring(0, 40)}${crawl.urls_preview.length > 40 ? '...' : ''}`;
|
|
}
|
|
|
|
return `
|
|
<div class="crawl-item" data-crawl-id="${crawl.id}">
|
|
<a class="crawl-header" href="${adminUrl}">
|
|
<span class="crawl-icon">${statusIcon}</span>
|
|
<div class="crawl-info">
|
|
<div class="crawl-label">${crawl.label}</div>
|
|
<div class="crawl-meta">${metaText}</div>
|
|
</div>
|
|
<div class="crawl-stats">
|
|
<span style="color:#3fb950">${crawl.completed_snapshots} done</span>
|
|
<span style="color:#d29922">${crawl.started_snapshots || 0} active</span>
|
|
<span style="color:#8b949e">${crawl.pending_snapshots} pending</span>
|
|
</div>
|
|
<span class="status-badge ${crawl.status}">${crawl.status}</span>
|
|
</a>
|
|
<div class="crawl-progress">
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar crawl ${crawl.status === 'started' && crawl.progress === 0 ? 'indeterminate' : ''}"
|
|
style="width: ${crawl.progress}%"></div>
|
|
</div>
|
|
</div>
|
|
${warningHtml}
|
|
<div class="crawl-body">
|
|
<div class="snapshot-list">
|
|
${snapshotsHtml}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
|
|
function updateProgress(data) {
|
|
// Calculate if there's activity
|
|
const hasActivity = data.active_crawls.length > 0 ||
|
|
data.crawls_pending > 0 || data.crawls_started > 0 ||
|
|
data.snapshots_pending > 0 || data.snapshots_started > 0 ||
|
|
data.archiveresults_pending > 0 || data.archiveresults_started > 0;
|
|
|
|
// Update orchestrator status - show "Running" only when there's actual activity
|
|
// Don't distinguish between "Stopped" and "Idle" since orchestrator starts/stops frequently
|
|
const dot = document.getElementById('orchestrator-dot');
|
|
const text = document.getElementById('orchestrator-text');
|
|
const hasWorkers = data.total_workers > 0;
|
|
|
|
if (hasWorkers || hasActivity) {
|
|
dot.classList.remove('stopped', 'idle');
|
|
dot.classList.add('running');
|
|
text.textContent = 'Running';
|
|
} else {
|
|
// No activity - show as idle (whether orchestrator process exists or not)
|
|
dot.classList.remove('stopped', 'running');
|
|
dot.classList.add('idle');
|
|
text.textContent = 'Idle';
|
|
}
|
|
|
|
// Pulse the dot to show we got fresh data
|
|
dot.classList.add('flash');
|
|
setTimeout(() => dot.classList.remove('flash'), 300);
|
|
|
|
// Update stats
|
|
document.getElementById('worker-count').textContent = data.total_workers;
|
|
document.getElementById('total-queued').textContent =
|
|
data.crawls_pending + data.snapshots_pending + data.archiveresults_pending;
|
|
|
|
// Store raw values and display relative to baseline
|
|
lastSucceeded = data.archiveresults_succeeded;
|
|
lastFailed = data.archiveresults_failed;
|
|
|
|
// If baseline is higher than current (e.g. after DB reset), reset baseline
|
|
if (succeededBaseline > lastSucceeded) {
|
|
succeededBaseline = 0;
|
|
localStorage.setItem('progress-succeeded-baseline', '0');
|
|
}
|
|
if (failedBaseline > lastFailed) {
|
|
failedBaseline = 0;
|
|
localStorage.setItem('progress-failed-baseline', '0');
|
|
}
|
|
|
|
document.getElementById('total-succeeded').textContent = lastSucceeded - succeededBaseline;
|
|
document.getElementById('total-failed').textContent = lastFailed - failedBaseline;
|
|
|
|
// Render crawl tree
|
|
if (data.active_crawls.length > 0) {
|
|
idleMessage.style.display = 'none';
|
|
crawlTree.innerHTML = data.active_crawls.map(c => renderCrawl(c)).join('');
|
|
} else if (hasActivity) {
|
|
idleMessage.style.display = 'none';
|
|
crawlTree.innerHTML = `
|
|
<div class="idle-message">
|
|
${data.snapshots_started} snapshots processing, ${data.archiveresults_started} extractors running
|
|
</div>
|
|
`;
|
|
} else {
|
|
idleMessage.style.display = '';
|
|
// Build the URL for recent crawls (last 24 hours)
|
|
var yesterday = new Date(Date.now() - 24*60*60*1000).toISOString().split('T')[0];
|
|
var recentUrl = '/admin/crawls/crawl/?created_at__gte=' + yesterday + '&o=-1';
|
|
idleMessage.innerHTML = `No active crawls (${data.crawls_pending} pending, ${data.crawls_started} started, <a href="${recentUrl}" style="color: #58a6ff;">${data.crawls_recent} recent</a>)`;
|
|
crawlTree.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
function fetchProgress() {
|
|
fetch('/admin/live-progress/')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
console.error('Progress API error:', data.error, data.traceback);
|
|
idleMessage.textContent = 'API Error: ' + data.error;
|
|
idleMessage.style.color = '#f85149';
|
|
}
|
|
updateProgress(data);
|
|
})
|
|
.catch(error => {
|
|
console.error('Progress fetch error:', error);
|
|
idleMessage.textContent = 'Fetch Error: ' + error.message;
|
|
idleMessage.style.color = '#f85149';
|
|
});
|
|
}
|
|
|
|
function startPolling() {
|
|
if (pollInterval) return;
|
|
fetchProgress();
|
|
pollInterval = setInterval(fetchProgress, 1000); // Poll every 1 second
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (pollInterval) {
|
|
clearInterval(pollInterval);
|
|
pollInterval = null;
|
|
}
|
|
}
|
|
|
|
// Collapse toggle
|
|
collapseBtn.addEventListener('click', function() {
|
|
isCollapsed = !isCollapsed;
|
|
localStorage.setItem('progress-monitor-collapsed', isCollapsed);
|
|
if (isCollapsed) {
|
|
monitor.classList.add('collapsed');
|
|
collapseBtn.textContent = 'Expand';
|
|
} else {
|
|
monitor.classList.remove('collapsed');
|
|
collapseBtn.textContent = 'Details';
|
|
}
|
|
});
|
|
|
|
// Apply initial state
|
|
if (isCollapsed) {
|
|
monitor.classList.add('collapsed');
|
|
collapseBtn.textContent = 'Expand';
|
|
}
|
|
|
|
// Start polling when page loads
|
|
startPolling();
|
|
|
|
// Pause polling when tab is hidden
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (document.hidden) {
|
|
stopPolling();
|
|
} else {
|
|
startPolling();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|