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) { + + } @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))"', )