diff --git a/archivebox/core/views.py b/archivebox/core/views.py index 4a104b45..f0410846 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -565,6 +565,29 @@ def live_progress_view(request): archiveresults_succeeded = ArchiveResult.objects.filter(status=ArchiveResult.StatusChoices.SUCCEEDED).count() archiveresults_failed = ArchiveResult.objects.filter(status=ArchiveResult.StatusChoices.FAILED).count() + # Get recently completed ArchiveResults with thumbnails (last 20 succeeded results) + recent_thumbnails = [] + recent_results = ArchiveResult.objects.filter( + status=ArchiveResult.StatusChoices.SUCCEEDED, + ).select_related('snapshot').order_by('-end_ts')[:20] + + for ar in recent_results: + embed = ar.embed_path() + if embed: + # Only include results with embeddable image/media files + ext = embed.lower().split('.')[-1] if '.' in embed else '' + is_embeddable = ext in ('png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'pdf', 'html') + if is_embeddable or ar.plugin in ('screenshot', 'favicon', 'dom'): + recent_thumbnails.append({ + 'id': str(ar.id), + 'plugin': ar.plugin, + 'snapshot_id': str(ar.snapshot_id), + 'snapshot_url': ar.snapshot.url[:60] if ar.snapshot else '', + 'embed_path': embed, + 'archive_path': f'/archive/{ar.snapshot.timestamp}/{embed}' if ar.snapshot else '', + 'end_ts': ar.end_ts.isoformat() if ar.end_ts else None, + }) + # Build hierarchical active crawls with nested snapshots and archive results from django.db.models import Prefetch @@ -689,6 +712,7 @@ def live_progress_view(request): 'archiveresults_succeeded': archiveresults_succeeded, 'archiveresults_failed': archiveresults_failed, 'active_crawls': active_crawls, + 'recent_thumbnails': recent_thumbnails, 'server_time': timezone.now().isoformat(), }) except Exception as e: @@ -708,6 +732,7 @@ def live_progress_view(request): 'archiveresults_succeeded': 0, 'archiveresults_failed': 0, 'active_crawls': [], + 'recent_thumbnails': [], 'server_time': timezone.now().isoformat(), }, status=500) diff --git a/archivebox/templates/admin/progress_monitor.html b/archivebox/templates/admin/progress_monitor.html index acc7ebdf..266afb70 100644 --- a/archivebox/templates/admin/progress_monitor.html +++ b/archivebox/templates/admin/progress_monitor.html @@ -423,6 +423,102 @@ color: #6e7681; } + /* Thumbnail Strip */ + #progress-monitor .thumbnail-strip { + display: flex; + gap: 8px; + padding: 10px 16px; + background: rgba(0,0,0,0.15); + border-top: 1px solid #21262d; + overflow-x: auto; + scrollbar-width: thin; + scrollbar-color: #30363d #0d1117; + } + #progress-monitor .thumbnail-strip::-webkit-scrollbar { + height: 6px; + } + #progress-monitor .thumbnail-strip::-webkit-scrollbar-track { + background: #0d1117; + } + #progress-monitor .thumbnail-strip::-webkit-scrollbar-thumb { + background: #30363d; + border-radius: 3px; + } + #progress-monitor .thumbnail-strip::-webkit-scrollbar-thumb:hover { + background: #484f58; + } + #progress-monitor .thumbnail-strip.empty { + display: none; + } + #progress-monitor .thumbnail-item { + flex-shrink: 0; + position: relative; + width: 64px; + height: 48px; + border-radius: 4px; + overflow: hidden; + border: 1px solid #30363d; + background: #161b22; + cursor: pointer; + transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s; + } + #progress-monitor .thumbnail-item:hover { + transform: scale(1.1); + border-color: #58a6ff; + box-shadow: 0 0 12px rgba(88, 166, 255, 0.3); + z-index: 10; + } + #progress-monitor .thumbnail-item.new { + animation: thumbnail-pop 0.4s ease-out; + } + @keyframes thumbnail-pop { + 0% { transform: scale(0.5); opacity: 0; } + 50% { transform: scale(1.15); } + 100% { transform: scale(1); opacity: 1; } + } + #progress-monitor .thumbnail-item img { + width: 100%; + height: 100%; + object-fit: cover; + } + #progress-monitor .thumbnail-item .thumbnail-fallback { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + color: #8b949e; + background: linear-gradient(135deg, #21262d 0%, #161b22 100%); + } + #progress-monitor .thumbnail-item .thumbnail-plugin { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 2px 4px; + font-size: 8px; + font-weight: 600; + text-transform: uppercase; + color: #fff; + background: rgba(0,0,0,0.7); + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + #progress-monitor .thumbnail-label { + display: flex; + align-items: center; + gap: 6px; + padding: 0 4px; + color: #8b949e; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + } +
@@ -456,6 +552,10 @@
+
+ Recent: +
+
No active crawls
@@ -469,9 +569,11 @@ const treeContainer = document.getElementById('tree-container'); const crawlTree = document.getElementById('crawl-tree'); const idleMessage = document.getElementById('idle-message'); + const thumbnailStrip = document.getElementById('thumbnail-strip'); let pollInterval = null; let isCollapsed = localStorage.getItem('progress-monitor-collapsed') === 'true'; + let knownThumbnailIds = new Set(); // Baselines for resettable counters let succeededBaseline = parseInt(localStorage.getItem('progress-succeeded-baseline') || '0'); @@ -501,6 +603,82 @@ } } + function getPluginIcon(plugin) { + const icons = { + 'screenshot': '📷', + 'favicon': '⭐', + 'dom': '📄', + 'pdf': '🗎', + 'title': '📝', + 'headers': '📋', + 'singlefile': '📦', + 'readability': '📖', + 'mercury': '⚜', + 'wget': '📥', + 'media': '🎥', + }; + return icons[plugin] || '📄'; + } + + function renderThumbnail(thumb, isNew) { + const ext = (thumb.embed_path || '').toLowerCase().split('.').pop(); + const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico'].includes(ext); + + const item = document.createElement('a'); + item.className = 'thumbnail-item' + (isNew ? ' new' : ''); + item.href = `/admin/core/snapshot/${thumb.snapshot_id}/change/`; + item.title = `${thumb.plugin}: ${thumb.snapshot_url}`; + item.dataset.id = thumb.id; + + if (isImage && thumb.archive_path) { + item.innerHTML = ` + ${thumb.plugin} + ${thumb.plugin} + `; + } else { + item.innerHTML = ` +
${getPluginIcon(thumb.plugin)}
+ ${thumb.plugin} + `; + } + + return item; + } + + function updateThumbnails(thumbnails) { + if (!thumbnails || thumbnails.length === 0) { + thumbnailStrip.classList.add('empty'); + return; + } + + thumbnailStrip.classList.remove('empty'); + + // Find new thumbnails (ones we haven't seen before) + const newThumbs = thumbnails.filter(t => !knownThumbnailIds.has(t.id)); + + // Add new thumbnails to the beginning (after the label) + const label = thumbnailStrip.querySelector('.thumbnail-label'); + newThumbs.reverse().forEach(thumb => { + const item = renderThumbnail(thumb, true); + if (label.nextSibling) { + thumbnailStrip.insertBefore(item, label.nextSibling); + } else { + thumbnailStrip.appendChild(item); + } + knownThumbnailIds.add(thumb.id); + }); + + // Limit to 20 thumbnails (remove old ones) + const items = thumbnailStrip.querySelectorAll('.thumbnail-item'); + if (items.length > 20) { + for (let i = 20; i < items.length; i++) { + const id = items[i].dataset.id; + knownThumbnailIds.delete(id); + items[i].remove(); + } + } + } + function renderExtractor(extractor) { const icon = extractor.status === 'started' ? '↻' : extractor.status === 'succeeded' ? '✓' : @@ -705,6 +883,9 @@ idleMessage.innerHTML = `No active crawls (${data.crawls_pending || 0} pending, ${data.crawls_started || 0} started, ${data.crawls_recent || 0} recent)`; crawlTree.innerHTML = ''; } + + // Update thumbnail strip with recently completed results + updateThumbnails(data.recent_thumbnails || []); } function fetchProgress() {