From 17029ba8b8c3d1b405d3d0905506b0063c89dea6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 31 Dec 2025 20:38:55 +0000 Subject: [PATCH] Add thumbnail strip to live progress monitor Show small thumbnails of recently completed ArchiveResult content in the progress header. The thumbnail strip appears below the stats bar and shows the last 20 successfully archived items with embeddable content (screenshots, favicons, DOM snapshots, etc.). Features: - API returns recent_thumbnails with embed paths for succeeded results - Thumbnails display with plugin-specific icons as fallback - New thumbnails animate in with a pop effect - Clicking a thumbnail navigates to the snapshot admin page - Horizontal scrollable strip with custom scrollbar styling --- archivebox/core/views.py | 25 +++ .../templates/admin/progress_monitor.html | 181 ++++++++++++++++++ 2 files changed, 206 insertions(+) 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() {