diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html
index d57485214..d9194fd2c 100644
--- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html
+++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html
@@ -142,6 +142,31 @@
}
+
Recent Task Activity ({{status.tasks.summary.days}} days)
+
+ @if (status.tasks.summary.total_count > 0) {
+
+ -
+ Total:
+ {{status.tasks.summary.total_count}}
+
+ -
+ Successful:
+ {{status.tasks.summary.success_count}}
+
+ -
+ Failed:
+ {{status.tasks.summary.failure_count}}
+
+ -
+ Pending:
+ {{status.tasks.summary.pending_count}}
+
+
+ } @else {
+ No recent tasks
+ }
+
diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts
index 29bad431e..4c69feed4 100644
--- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts
+++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts
@@ -71,6 +71,13 @@ const status: SystemStatus = {
llmindex_status: SystemStatusItemStatus.OK,
llmindex_last_modified: new Date().toISOString(),
llmindex_error: null,
+ summary: {
+ days: 30,
+ total_count: 12,
+ pending_count: 1,
+ success_count: 10,
+ failure_count: 1,
+ },
},
}
diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts
index 7dcbffa20..41cc738cc 100644
--- a/src-ui/src/app/data/system-status.ts
+++ b/src-ui/src/app/data/system-status.ts
@@ -47,6 +47,13 @@ export interface SystemStatus {
llmindex_status: SystemStatusItemStatus
llmindex_last_modified: string // ISO date string
llmindex_error: string
+ summary: {
+ days: number
+ total_count: number
+ pending_count: number
+ success_count: number
+ failure_count: number
+ }
}
websocket_connected?: SystemStatusItemStatus // added client-side
}
diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py
index bb0039e67..72f1e269b 100644
--- a/src/documents/tests/test_api_status.py
+++ b/src/documents/tests/test_api_status.py
@@ -1,12 +1,14 @@
import os
import shutil
import tempfile
+from datetime import timedelta
from pathlib import Path
from unittest import mock
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from django.test import override_settings
+from django.utils import timezone
from rest_framework import status
from rest_framework.test import APITestCase
@@ -76,6 +78,11 @@ class TestSystemStatus(APITestCase):
self.assertEqual(response.data["tasks"]["redis_url"], "redis://localhost:6379")
self.assertEqual(response.data["tasks"]["redis_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["redis_error"])
+ self.assertEqual(response.data["tasks"]["summary"]["days"], 30)
+ self.assertEqual(response.data["tasks"]["summary"]["total_count"], 0)
+ self.assertEqual(response.data["tasks"]["summary"]["success_count"], 0)
+ self.assertEqual(response.data["tasks"]["summary"]["failure_count"], 0)
+ self.assertEqual(response.data["tasks"]["summary"]["pending_count"], 0)
def test_system_status_insufficient_permissions(self) -> None:
"""
@@ -436,3 +443,32 @@ class TestSystemStatus(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["llmindex_status"], "ERROR")
self.assertIsNotNone(response.data["tasks"]["llmindex_error"])
+
+ def test_system_status_includes_recent_task_summary(self) -> None:
+ PaperlessTaskFactory(
+ task_type=PaperlessTask.TaskType.CONSUME_FILE,
+ status=PaperlessTask.Status.SUCCESS,
+ )
+ PaperlessTaskFactory(
+ task_type=PaperlessTask.TaskType.CONSUME_FILE,
+ status=PaperlessTask.Status.FAILURE,
+ )
+ PaperlessTaskFactory(
+ task_type=PaperlessTask.TaskType.SANITY_CHECK,
+ status=PaperlessTask.Status.PENDING,
+ )
+ PaperlessTaskFactory(
+ task_type=PaperlessTask.TaskType.MAIL_FETCH,
+ status=PaperlessTask.Status.SUCCESS,
+ date_created=timezone.now() - timedelta(days=45),
+ )
+
+ self.client.force_login(self.user)
+ response = self.client.get(self.ENDPOINT)
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["tasks"]["summary"]["days"], 30)
+ self.assertEqual(response.data["tasks"]["summary"]["total_count"], 3)
+ self.assertEqual(response.data["tasks"]["summary"]["success_count"], 1)
+ self.assertEqual(response.data["tasks"]["summary"]["failure_count"], 1)
+ self.assertEqual(response.data["tasks"]["summary"]["pending_count"], 1)
diff --git a/src/documents/views.py b/src/documents/views.py
index eae343357..54c8f1ae6 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -4609,6 +4609,16 @@ class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Custom
"redis_status": serializers.CharField(),
"redis_error": serializers.CharField(),
"celery_status": serializers.CharField(),
+ "summary": inline_serializer(
+ name="TasksSummaryOverview",
+ fields={
+ "days": serializers.IntegerField(),
+ "total_count": serializers.IntegerField(),
+ "pending_count": serializers.IntegerField(),
+ "success_count": serializers.IntegerField(),
+ "failure_count": serializers.IntegerField(),
+ },
+ ),
},
),
"index": inline_serializer(
@@ -4642,6 +4652,7 @@ class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Custom
)
class SystemStatusView(PassUserMixin):
permission_classes = (IsAuthenticated,)
+ TASK_SUMMARY_DAYS = 30
def get(self, request, format=None):
if not has_system_status_permission(request.user):
@@ -4808,6 +4819,29 @@ class SystemStatusView(PassUserMixin):
last_llmindex_update.date_done if last_llmindex_update else None
)
+ summary_cutoff = timezone.now() - timedelta(days=self.TASK_SUMMARY_DAYS)
+ task_summary_agg = PaperlessTask.objects.filter(
+ date_created__gte=summary_cutoff,
+ ).aggregate(
+ total_count=Count("id"),
+ pending_count=Count(
+ "id",
+ filter=Q(status=PaperlessTask.Status.PENDING),
+ ),
+ success_count=Count(
+ "id",
+ filter=Q(status=PaperlessTask.Status.SUCCESS),
+ ),
+ failure_count=Count(
+ "id",
+ filter=Q(status=PaperlessTask.Status.FAILURE),
+ ),
+ )
+ task_summary = {
+ "days": self.TASK_SUMMARY_DAYS,
+ **task_summary_agg,
+ }
+
return Response(
{
"pngx_version": current_version,
@@ -4848,6 +4882,7 @@ class SystemStatusView(PassUserMixin):
"llmindex_status": llmindex_status,
"llmindex_last_modified": llmindex_last_modified,
"llmindex_error": llmindex_error,
+ "summary": task_summary,
},
},
)
diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po
index 34c40481c..1199b4dc8 100644
--- a/src/locale/en_US/LC_MESSAGES/django.po
+++ b/src/locale/en_US/LC_MESSAGES/django.po
@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2026-04-22 20:49+0000\n"
+"POT-Creation-Date: 2026-04-23 16:12+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1918,151 +1918,151 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
-#: paperless/settings/__init__.py:531
+#: paperless/settings/__init__.py:532
msgid "English (US)"
msgstr ""
-#: paperless/settings/__init__.py:532
+#: paperless/settings/__init__.py:533
msgid "Arabic"
msgstr ""
-#: paperless/settings/__init__.py:533
+#: paperless/settings/__init__.py:534
msgid "Afrikaans"
msgstr ""
-#: paperless/settings/__init__.py:534
+#: paperless/settings/__init__.py:535
msgid "Belarusian"
msgstr ""
-#: paperless/settings/__init__.py:535
+#: paperless/settings/__init__.py:536
msgid "Bulgarian"
msgstr ""
-#: paperless/settings/__init__.py:536
+#: paperless/settings/__init__.py:537
msgid "Catalan"
msgstr ""
-#: paperless/settings/__init__.py:537
+#: paperless/settings/__init__.py:538
msgid "Czech"
msgstr ""
-#: paperless/settings/__init__.py:538
+#: paperless/settings/__init__.py:539
msgid "Danish"
msgstr ""
-#: paperless/settings/__init__.py:539
+#: paperless/settings/__init__.py:540
msgid "German"
msgstr ""
-#: paperless/settings/__init__.py:540
+#: paperless/settings/__init__.py:541
msgid "Greek"
msgstr ""
-#: paperless/settings/__init__.py:541
+#: paperless/settings/__init__.py:542
msgid "English (GB)"
msgstr ""
-#: paperless/settings/__init__.py:542
+#: paperless/settings/__init__.py:543
msgid "Spanish"
msgstr ""
-#: paperless/settings/__init__.py:543
+#: paperless/settings/__init__.py:544
msgid "Persian"
msgstr ""
-#: paperless/settings/__init__.py:544
+#: paperless/settings/__init__.py:545
msgid "Finnish"
msgstr ""
-#: paperless/settings/__init__.py:545
+#: paperless/settings/__init__.py:546
msgid "French"
msgstr ""
-#: paperless/settings/__init__.py:546
+#: paperless/settings/__init__.py:547
msgid "Hungarian"
msgstr ""
-#: paperless/settings/__init__.py:547
+#: paperless/settings/__init__.py:548
msgid "Indonesian"
msgstr ""
-#: paperless/settings/__init__.py:548
+#: paperless/settings/__init__.py:549
msgid "Italian"
msgstr ""
-#: paperless/settings/__init__.py:549
+#: paperless/settings/__init__.py:550
msgid "Japanese"
msgstr ""
-#: paperless/settings/__init__.py:550
+#: paperless/settings/__init__.py:551
msgid "Korean"
msgstr ""
-#: paperless/settings/__init__.py:551
+#: paperless/settings/__init__.py:552
msgid "Luxembourgish"
msgstr ""
-#: paperless/settings/__init__.py:552
+#: paperless/settings/__init__.py:553
msgid "Norwegian"
msgstr ""
-#: paperless/settings/__init__.py:553
+#: paperless/settings/__init__.py:554
msgid "Dutch"
msgstr ""
-#: paperless/settings/__init__.py:554
+#: paperless/settings/__init__.py:555
msgid "Polish"
msgstr ""
-#: paperless/settings/__init__.py:555
+#: paperless/settings/__init__.py:556
msgid "Portuguese (Brazil)"
msgstr ""
-#: paperless/settings/__init__.py:556
+#: paperless/settings/__init__.py:557
msgid "Portuguese"
msgstr ""
-#: paperless/settings/__init__.py:557
+#: paperless/settings/__init__.py:558
msgid "Romanian"
msgstr ""
-#: paperless/settings/__init__.py:558
+#: paperless/settings/__init__.py:559
msgid "Russian"
msgstr ""
-#: paperless/settings/__init__.py:559
+#: paperless/settings/__init__.py:560
msgid "Slovak"
msgstr ""
-#: paperless/settings/__init__.py:560
+#: paperless/settings/__init__.py:561
msgid "Slovenian"
msgstr ""
-#: paperless/settings/__init__.py:561
+#: paperless/settings/__init__.py:562
msgid "Serbian"
msgstr ""
-#: paperless/settings/__init__.py:562
+#: paperless/settings/__init__.py:563
msgid "Swedish"
msgstr ""
-#: paperless/settings/__init__.py:563
+#: paperless/settings/__init__.py:564
msgid "Turkish"
msgstr ""
-#: paperless/settings/__init__.py:564
+#: paperless/settings/__init__.py:565
msgid "Ukrainian"
msgstr ""
-#: paperless/settings/__init__.py:565
+#: paperless/settings/__init__.py:566
msgid "Vietnamese"
msgstr ""
-#: paperless/settings/__init__.py:566
+#: paperless/settings/__init__.py:567
msgid "Chinese Simplified"
msgstr ""
-#: paperless/settings/__init__.py:567
+#: paperless/settings/__init__.py:568
msgid "Chinese Traditional"
msgstr ""
diff --git a/src/paperless/settings/__init__.py b/src/paperless/settings/__init__.py
index 6f76d3499..79dd98ee3 100644
--- a/src/paperless/settings/__init__.py
+++ b/src/paperless/settings/__init__.py
@@ -463,10 +463,11 @@ SECURE_PROXY_SSL_HEADER = (
else None
)
-SECRET_KEY = os.getenv("PAPERLESS_SECRET_KEY", "")
-if not SECRET_KEY: # pragma: no cover
+SECRET_KEY = os.getenv("PAPERLESS_SECRET_KEY")
+_INSECURE_SECRET_KEYS = {None, "", "change-me"}
+if not DEBUG and SECRET_KEY in _INSECURE_SECRET_KEYS: # pragma: no cover
raise ImproperlyConfigured(
- "PAPERLESS_SECRET_KEY is not set. "
+ "PAPERLESS_SECRET_KEY is not set or is the default 'change-me' value. "
"A unique, secret key is required for secure operation. "
'Generate one with: python3 -c "import secrets; print(secrets.token_urlsafe(64))"',
)