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 %}
-
- ☰ |
- ⣿⣿
-
+
{% 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 %}