diff --git a/archivebox/core/admin_snapshots.py b/archivebox/core/admin_snapshots.py index e5f972da..0af36faf 100644 --- a/archivebox/core/admin_snapshots.py +++ b/archivebox/core/admin_snapshots.py @@ -117,7 +117,7 @@ class SnapshotAdminForm(forms.ModelForm): class SnapshotAdmin(SearchResultsAdminMixin, ConfigEditorMixin, BaseModelAdmin): form = SnapshotAdminForm - list_display = ('created_at', 'title_str', 'status', 'files', 'size', 'url_str') + list_display = ('created_at', 'title_str', 'status_with_progress', 'files', 'size_with_stats', 'url_str') sort_fields = ('title_str', 'url_str', 'created_at', 'status', 'crawl') readonly_fields = ('admin_actions', 'status_info', 'imported_timestamp', 'created_at', 'modified_at', 'downloaded_at', 'output_dir', 'archiveresults_list') search_fields = ('id', 'url', 'timestamp', 'title', 'tags__name') @@ -376,6 +376,106 @@ class SnapshotAdmin(SearchResultsAdminMixin, ConfigEditorMixin, BaseModelAdmin): size_txt, ) + @admin.display( + description='Status', + ordering='status', + ) + def status_with_progress(self, obj): + """Show status with progress bar for in-progress snapshots.""" + stats = obj.get_progress_stats() + + # Status badge colors + status_colors = { + 'queued': ('#f59e0b', '#fef3c7'), # amber + 'started': ('#3b82f6', '#dbeafe'), # blue + 'sealed': ('#10b981', '#d1fae5'), # green + 'succeeded': ('#10b981', '#d1fae5'), # green + 'failed': ('#ef4444', '#fee2e2'), # red + 'backoff': ('#f59e0b', '#fef3c7'), # amber + 'skipped': ('#6b7280', '#f3f4f6'), # gray + } + fg_color, bg_color = status_colors.get(obj.status, ('#6b7280', '#f3f4f6')) + + # For started snapshots, show progress bar + if obj.status == 'started' and stats['total'] > 0: + percent = stats['percent'] + running = stats['running'] + succeeded = stats['succeeded'] + failed = stats['failed'] + + return format_html( + '''
+
+ + {}/{} hooks +
+
+
+
+
+ ✓{} ✗{} ⏳{} +
+
''', + succeeded + failed + stats['skipped'], + stats['total'], + int(succeeded / stats['total'] * 100) if stats['total'] else 0, + int(succeeded / stats['total'] * 100) if stats['total'] else 0, + int((succeeded + failed) / stats['total'] * 100) if stats['total'] else 0, + int((succeeded + failed) / stats['total'] * 100) if stats['total'] else 0, + percent, + succeeded, + failed, + running, + ) + + # For other statuses, show simple badge + return format_html( + '{}', + bg_color, + fg_color, + obj.status.upper(), + ) + + @admin.display( + description='Size', + ) + def size_with_stats(self, obj): + """Show archive size with output size from archive results.""" + stats = obj.get_progress_stats() + + # Use output_size from archive results if available, fallback to disk size + output_size = stats['output_size'] + archive_size = os.access(Path(obj.output_dir) / 'index.html', os.F_OK) and obj.archive_size + + size_bytes = output_size or archive_size or 0 + + if size_bytes: + size_txt = printable_filesize(size_bytes) + if size_bytes > 52428800: # 50MB + size_txt = mark_safe(f'{size_txt}') + else: + size_txt = mark_safe('...') + + # Show hook statistics + if stats['total'] > 0: + return format_html( + '' + '{}' + '
' + '{}/{} hooks
', + obj.archive_path, + size_txt, + stats['succeeded'], + stats['total'], + ) + + return format_html( + '{}', + obj.archive_path, + size_txt, + ) @admin.display( description='Original URL', diff --git a/archivebox/core/models.py b/archivebox/core/models.py index f7b45ba9..b4cf9045 100755 --- a/archivebox/core/models.py +++ b/archivebox/core/models.py @@ -1712,6 +1712,56 @@ class Snapshot(ModelWithOutputDir, ModelWithConfig, ModelWithNotes, ModelWithHea # otherwise archiveresults exist and are all finished, so it's finished return True + def get_progress_stats(self) -> dict: + """ + Get progress statistics for this snapshot's archiving process. + + Returns dict with: + - total: Total number of archive results + - succeeded: Number of succeeded results + - failed: Number of failed results + - running: Number of currently running results + - pending: Number of pending/queued results + - percent: Completion percentage (0-100) + - output_size: Total output size in bytes + - is_sealed: Whether the snapshot is in a final state + """ + from django.db.models import Sum + + results = self.archiveresult_set.all() + + # Count by status + succeeded = results.filter(status='succeeded').count() + failed = results.filter(status='failed').count() + running = results.filter(status='started').count() + skipped = results.filter(status='skipped').count() + total = results.count() + pending = total - succeeded - failed - running - skipped + + # Calculate percentage (succeeded + failed + skipped as completed) + completed = succeeded + failed + skipped + percent = int((completed / total * 100) if total > 0 else 0) + + # Sum output sizes + output_size = results.filter(status='succeeded').aggregate( + total_size=Sum('output_size') + )['total_size'] or 0 + + # Check if sealed + is_sealed = self.status in (self.StatusChoices.SEALED, self.StatusChoices.FAILED, self.StatusChoices.BACKOFF) + + return { + 'total': total, + 'succeeded': succeeded, + 'failed': failed, + 'running': running, + 'pending': pending, + 'skipped': skipped, + 'percent': percent, + 'output_size': output_size, + 'is_sealed': is_sealed, + } + def retry_failed_archiveresults(self, retry_at: Optional['timezone.datetime'] = None) -> int: """ Reset failed/skipped ArchiveResults to queued for retry. diff --git a/archivebox/templates/admin/base.html b/archivebox/templates/admin/base.html index bde628a4..c6270ed9 100644 --- a/archivebox/templates/admin/base.html +++ b/archivebox/templates/admin/base.html @@ -1346,10 +1346,16 @@
{% if opts.model_name == 'snapshot' and cl %} - - | - ⣿⣿ - +
+ + + List + + + + Grid + +
{% endif %} {% block pretitle %}{% endblock %} {% block content_title %}{# {% if title %}

{{ title }}

{% endif %} #}{% endblock %} @@ -1500,10 +1506,20 @@ $("#snapshot-view-list").click(selectSnapshotListView) $("#snapshot-view-grid").click(selectSnapshotGridView) + // Set active class based on current view + const isGridView = window.location.pathname === "{% url 'admin:grid' %}" + if (isGridView) { + $("#snapshot-view-grid").addClass('active') + $("#snapshot-view-list").removeClass('active') + } else { + $("#snapshot-view-list").addClass('active') + $("#snapshot-view-grid").removeClass('active') + } + $('#changelist-form .card input:checkbox').change(function() { if ($(this).is(':checked')) $(this).parents('.card').addClass('selected-card') - else + else $(this).parents('.card').removeClass('selected-card') }) }; diff --git a/archivebox/templates/admin/snapshots_grid.html b/archivebox/templates/admin/snapshots_grid.html index 54de082d..bf115e8e 100644 --- a/archivebox/templates/admin/snapshots_grid.html +++ b/archivebox/templates/admin/snapshots_grid.html @@ -126,6 +126,21 @@ .cards .card .card-info .timestamp { font-weight: 600; } + .cards .card .card-progress { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 0; + } + .cards .card .card-progress .progress-text { + font-size: 11px; + color: #3b82f6; + font-weight: 500; + } + .cards .card.archiving { + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); + } .cards .card .card-footer code { display: inline-block; width: 100%; @@ -145,14 +160,21 @@ {% block content %}
{% for obj in results %} -
+
{{obj.bookmarked_at}} -
- {{ obj.icons|safe }} -
+ {% if obj.status == 'started' %} +
+ + Archiving... +
+ {% else %} +
+ {{ obj.icons|safe }} +
+ {% endif %}