diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 338eeda43..a62059c21 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -1540,7 +1540,7 @@
Error retrieving users
src/app/components/admin/settings/settings.component.ts
- 182
+ 185
src/app/components/admin/users-groups/users-groups.component.ts
@@ -1551,7 +1551,7 @@
Error retrieving groups
src/app/components/admin/settings/settings.component.ts
- 201
+ 204
src/app/components/admin/users-groups/users-groups.component.ts
@@ -1562,35 +1562,35 @@
Saved view "" deleted.
src/app/components/admin/settings/settings.component.ts
- 414
+ 417
Settings were saved successfully.
src/app/components/admin/settings/settings.component.ts
- 540
+ 543
Settings were saved successfully. Reload is required to apply some changes.
src/app/components/admin/settings/settings.component.ts
- 544
+ 547
Reload now
src/app/components/admin/settings/settings.component.ts
- 545
+ 548
An error occurred while saving settings.
src/app/components/admin/settings/settings.component.ts
- 555
+ 558
src/app/components/app-frame/app-frame.component.ts
@@ -1601,7 +1601,7 @@
Error while storing settings on server.
src/app/components/admin/settings/settings.component.ts
- 589
+ 592
@@ -4160,7 +4160,7 @@
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 124
+ 144
@@ -4449,18 +4449,11 @@
41
-
- URL
-
- src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 47
-
-
Status
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 49
+ 47
src/app/components/common/toasts/toasts.component.html
@@ -4475,49 +4468,67 @@
Migration Status
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 58
+ 56
Latest Migration
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 66
+ 64
Pending Migrations
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 68
+ 66
Tasks
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 85
-
-
-
- Redis URL
-
- src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 89
+ 83
Redis Status
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 91
+ 87
Celery Status
src/app/components/common/system-status-dialog/system-status-dialog.component.html
- 100
+ 96
+
+
+
+ Index Status
+
+ src/app/components/common/system-status-dialog/system-status-dialog.component.html
+ 105
+
+
+
+ Last Updated
+
+ src/app/components/common/system-status-dialog/system-status-dialog.component.html
+ 115
+
+
+ src/app/components/common/system-status-dialog/system-status-dialog.component.html
+ 127
+
+
+
+ Classifier Status
+
+ src/app/components/common/system-status-dialog/system-status-dialog.component.html
+ 117
diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts
index 8fdaa626c..4c336a754 100644
--- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts
+++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts
@@ -44,9 +44,9 @@ import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-butt
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
- PaperlessSystemStatus,
- PaperlessInstallType,
- PaperlessConnectionStatus,
+ SystemStatus,
+ InstallType,
+ SystemStatusItemStatus,
} from 'src/app/data/system-status'
const savedViews = [
@@ -388,15 +388,15 @@ describe('SettingsComponent', () => {
})
it('should load system status on initialize, show errors if needed', () => {
- const status: PaperlessSystemStatus = {
+ const status: SystemStatus = {
pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit',
- install_type: PaperlessInstallType.BareMetal,
+ install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 },
database: {
type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3',
- status: PaperlessConnectionStatus.ERROR,
+ status: SystemStatusItemStatus.ERROR,
error: null,
migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
@@ -405,10 +405,16 @@ describe('SettingsComponent', () => {
},
tasks: {
redis_url: 'redis://localhost:6379',
- redis_status: PaperlessConnectionStatus.ERROR,
+ redis_status: SystemStatusItemStatus.ERROR,
redis_error:
'Error 61 connecting to localhost:6379. Connection refused.',
- celery_status: PaperlessConnectionStatus.ERROR,
+ celery_status: SystemStatusItemStatus.ERROR,
+ index_status: SystemStatusItemStatus.OK,
+ index_last_modified: new Date(),
+ index_error: null,
+ classifier_status: SystemStatusItemStatus.OK,
+ classifier_last_modified: new Date(),
+ classifier_error: null,
},
}
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
@@ -416,9 +422,9 @@ describe('SettingsComponent', () => {
expect(component['systemStatus']).toEqual(status) // private
expect(component.systemStatusHasErrors).toBeTruthy()
// coverage
- component['systemStatus'].database.status = PaperlessConnectionStatus.OK
- component['systemStatus'].tasks.redis_status = PaperlessConnectionStatus.OK
- component['systemStatus'].tasks.celery_status = PaperlessConnectionStatus.OK
+ component['systemStatus'].database.status = SystemStatusItemStatus.OK
+ component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
+ component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
expect(component.systemStatusHasErrors).toBeFalsy()
})
diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts
index 7fe5320e3..f04af2f9d 100644
--- a/src-ui/src/app/components/admin/settings/settings.component.ts
+++ b/src-ui/src/app/components/admin/settings/settings.component.ts
@@ -47,8 +47,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service'
import {
- PaperlessConnectionStatus,
- PaperlessSystemStatus,
+ SystemStatusItemStatus,
+ SystemStatus,
} from 'src/app/data/system-status'
enum SettingsNavIDs {
@@ -121,14 +121,15 @@ export class SettingsComponent
users: User[]
groups: Group[]
- private systemStatus: PaperlessSystemStatus
+ private systemStatus: SystemStatus
get systemStatusHasErrors(): boolean {
return (
- this.systemStatus.database.status === PaperlessConnectionStatus.ERROR ||
- this.systemStatus.tasks.redis_status ===
- PaperlessConnectionStatus.ERROR ||
- this.systemStatus.tasks.celery_status === PaperlessConnectionStatus.ERROR
+ this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
+ this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
+ this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
+ this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
+ this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
)
}
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 df32d9fdc..8edfbc9c1 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
@@ -44,15 +44,13 @@
- Type
- {{status.database.type}}
- - URL
- - {{status.database.url}}
- Status
-
{{status.database.status}}
@if (status.database.status === 'OK') {
-
+
} @else {
-
+
}
- Migration Status
@@ -86,15 +84,13 @@
- - Redis URL
- - {{status.tasks.redis_url}}
- Redis Status
-
{{status.tasks.redis_status}}
@if (status.tasks.redis_status === 'OK') {
-
+
} @else {
-
+
}
- Celery Status
@@ -106,6 +102,30 @@
}
+ - Index Status
+ -
+ {{status.tasks.index_status}}
+ @if (status.tasks.index_status === 'OK') {
+
+ } @else {
+
+ }
+
+
+ Last Updated:
{{status.tasks.index_last_modified}}
+
+ - Classifier Status
+ -
+ {{status.tasks.classifier_status}}
+ @if (status.tasks.classifier_status === 'OK') {
+
+ } @else {
+
+ }
+
+
+ Last Updated:
{{status.tasks.classifier_last_modified}}
+
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 413f34be1..72dc5e7e4 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
@@ -8,29 +8,28 @@ import {
NgbActiveModal,
NgbModalModule,
NgbPopoverModule,
+ NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
-import { SystemStatusService } from 'src/app/services/system-status.service'
import { SystemStatusDialogComponent } from './system-status-dialog.component'
-import { of } from 'rxjs'
import {
- PaperlessConnectionStatus,
- PaperlessInstallType,
- PaperlessSystemStatus,
+ SystemStatusItemStatus,
+ InstallType,
+ SystemStatus,
} from 'src/app/data/system-status'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NgxFilesizeModule } from 'ngx-filesize'
-const status: PaperlessSystemStatus = {
+const status: SystemStatus = {
pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit',
- install_type: PaperlessInstallType.BareMetal,
+ install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 },
database: {
type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3',
- status: PaperlessConnectionStatus.ERROR,
+ status: SystemStatusItemStatus.ERROR,
error: null,
migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
@@ -39,9 +38,15 @@ const status: PaperlessSystemStatus = {
},
tasks: {
redis_url: 'redis://localhost:6379',
- redis_status: PaperlessConnectionStatus.ERROR,
+ redis_status: SystemStatusItemStatus.ERROR,
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
- celery_status: PaperlessConnectionStatus.ERROR,
+ celery_status: SystemStatusItemStatus.ERROR,
+ index_status: SystemStatusItemStatus.OK,
+ index_last_modified: new Date(),
+ index_error: null,
+ classifier_status: SystemStatusItemStatus.OK,
+ classifier_last_modified: new Date(),
+ classifier_error: null,
},
}
@@ -61,6 +66,7 @@ describe('SystemStatusDialogComponent', () => {
NgxBootstrapIconsModule.pick(allIcons),
NgxFilesizeModule,
NgbPopoverModule,
+ NgbProgressbarModule,
],
}).compileComponents()
diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts
index 26948aa3f..f0d3372c6 100644
--- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts
+++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts
@@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
-import { PaperlessSystemStatus } from 'src/app/data/system-status'
+import { SystemStatus } from 'src/app/data/system-status'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { Clipboard } from '@angular/cdk/clipboard'
@@ -10,7 +10,7 @@ import { Clipboard } from '@angular/cdk/clipboard'
styleUrl: './system-status-dialog.component.scss',
})
export class SystemStatusDialogComponent {
- public status: PaperlessSystemStatus
+ public status: SystemStatus
public copied: boolean = false
diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts
index f3953ed05..6b131ce1c 100644
--- a/src-ui/src/app/data/system-status.ts
+++ b/src-ui/src/app/data/system-status.ts
@@ -1,17 +1,17 @@
-export enum PaperlessInstallType {
+export enum InstallType {
Containerized = 'containerized',
BareMetal = 'bare-metal',
}
-export enum PaperlessConnectionStatus {
+export enum SystemStatusItemStatus {
OK = 'OK',
ERROR = 'ERROR',
}
-export interface PaperlessSystemStatus {
+export interface SystemStatus {
pngx_version: string
server_os: string
- install_type: PaperlessInstallType
+ install_type: InstallType
storage: {
total: number
available: number
@@ -19,7 +19,7 @@ export interface PaperlessSystemStatus {
database: {
type: string
url: string
- status: PaperlessConnectionStatus
+ status: SystemStatusItemStatus
error?: string
migration_status: {
latest_migration: string
@@ -28,8 +28,14 @@ export interface PaperlessSystemStatus {
}
tasks: {
redis_url: string
- redis_status: PaperlessConnectionStatus
+ redis_status: SystemStatusItemStatus
redis_error: string
- celery_status: PaperlessConnectionStatus
+ celery_status: SystemStatusItemStatus
+ index_status: SystemStatusItemStatus
+ index_last_modified: Date
+ index_error: string
+ classifier_status: SystemStatusItemStatus
+ classifier_last_modified: Date
+ classifier_error: string
}
}
diff --git a/src-ui/src/app/services/system-status.service.spec.ts b/src-ui/src/app/services/system-status.service.spec.ts
index 236d21a8e..dd0eb3a88 100644
--- a/src-ui/src/app/services/system-status.service.spec.ts
+++ b/src-ui/src/app/services/system-status.service.spec.ts
@@ -25,7 +25,7 @@ describe('SystemStatusService', () => {
httpTestingController.verify()
})
- it('calls get statys endpoint', () => {
+ it('calls get status endpoint', () => {
service.get().subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}status/`
diff --git a/src-ui/src/app/services/system-status.service.ts b/src-ui/src/app/services/system-status.service.ts
index 2a538658f..ae6c5a91c 100644
--- a/src-ui/src/app/services/system-status.service.ts
+++ b/src-ui/src/app/services/system-status.service.ts
@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
-import { PaperlessSystemStatus } from '../data/system-status'
+import { SystemStatus } from '../data/system-status'
import { environment } from 'src/environments/environment'
@Injectable({
@@ -12,8 +12,8 @@ export class SystemStatusService {
constructor(private http: HttpClient) {}
- get(): Observable {
- return this.http.get(
+ get(): Observable {
+ return this.http.get(
`${environment.apiBaseUrl}${this.endpoint}/`
)
}
diff --git a/src/documents/tests/test_api_status.py b/src/documents/tests/test_api_status.py
index 9a1859585..a64c8c1b4 100644
--- a/src/documents/tests/test_api_status.py
+++ b/src/documents/tests/test_api_status.py
@@ -1,14 +1,16 @@
import os
from unittest import mock
+from django.conf import settings
from django.contrib.auth.models import User
+from django.test import override_settings
from rest_framework import status
from rest_framework.test import APITestCase
from paperless import version
-class TestSystemStatusView(APITestCase):
+class TestSystemStatus(APITestCase):
ENDPOINT = "/api/status/"
def setUp(self):
@@ -67,3 +69,46 @@ class TestSystemStatusView(APITestCase):
response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["celery_status"], "OK")
+
+ @override_settings(INDEX_DIR="/tmp/index")
+ @mock.patch("whoosh.index.FileIndex.last_modified")
+ def test_system_status_index_ok(self, mock_last_modified):
+ mock_last_modified.return_value = 1707839087
+ 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"]["index_status"], "OK")
+ self.assertIsNotNone(response.data["tasks"]["index_last_modified"])
+
+ @override_settings(INDEX_DIR="/tmp/index/")
+ @mock.patch("documents.index.open_index", autospec=True)
+ def test_system_status_index_error(self, mock_open_index):
+ mock_open_index.return_value = None
+ mock_open_index.side_effect = Exception("Index error")
+ self.client.force_login(self.user)
+ response = self.client.get(self.ENDPOINT)
+ mock_open_index.assert_called_once()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
+ self.assertIsNotNone(response.data["tasks"]["index_error"])
+
+ @override_settings(MODEL_FILE="/tmp/does_not_exist")
+ def test_system_status_classifier_ok(self):
+ with open(settings.MODEL_FILE, "w") as f:
+ f.write("test")
+ f.close()
+ 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"]["classifier_status"], "OK")
+ self.assertIsNone(response.data["tasks"]["classifier_error"])
+
+ @override_settings(MODEL_FILE="/tmp/does_not_exist")
+ @mock.patch("documents.classifier.load_classifier")
+ def test_system_status_classifier_error(self, mock_load_classifier):
+ mock_load_classifier.side_effect = Exception("Classifier error")
+ 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"]["classifier_status"], "ERROR")
+ self.assertIsNotNone(response.data["tasks"]["classifier_error"])
diff --git a/src/documents/views.py b/src/documents/views.py
index 559d167ed..18d022b34 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -35,6 +35,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.decorators import method_decorator
+from django.utils.timezone import make_aware
from django.utils.translation import get_language
from django.views import View
from django.views.decorators.cache import cache_control
@@ -66,6 +67,7 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from rest_framework.viewsets import ViewSet
from documents import bulk_edit
+from documents import index
from documents.bulk_download import ArchiveOnlyStrategy
from documents.bulk_download import OriginalAndArchiveStrategy
from documents.bulk_download import OriginalsOnlyStrategy
@@ -1595,13 +1597,39 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
redis_error = "Error connecting to redis, check logs for more detail."
try:
- ping = celery_app.control.inspect().ping()
- first_worker_ping = ping[next(iter(ping.keys()))]
+ celery_ping = celery_app.control.inspect().ping()
+ first_worker_ping = celery_ping[next(iter(celery_ping.keys()))]
if first_worker_ping["ok"] == "pong":
celery_active = "OK"
except Exception:
celery_active = "ERROR"
+ index_error = None
+ try:
+ ix = index.open_index()
+ index_status = "OK"
+ index_last_modified = make_aware(
+ datetime.fromtimestamp(ix.last_modified()),
+ )
+ except Exception as e:
+ index_status = "ERROR"
+ index_error = "Error opening index, check logs for more detail."
+ logger.exception(f"System status error opening index: {e}")
+ index_last_modified = None
+
+ classifier_error = None
+ try:
+ load_classifier()
+ classifier_status = "OK"
+ classifier_last_modified = make_aware(
+ datetime.fromtimestamp(os.path.getmtime(settings.MODEL_FILE)),
+ )
+ except Exception as e:
+ classifier_status = "ERROR"
+ classifier_last_modified = None
+ classifier_error = "Error loading classifier, check logs for more detail."
+ logger.exception(f"System status error loading classifier: {e}")
+
return Response(
{
"pngx_version": current_version,
@@ -1628,6 +1656,12 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
"redis_status": redis_status,
"redis_error": redis_error,
"celery_status": celery_active,
+ "index_status": index_status,
+ "index_last_modified": index_last_modified,
+ "index_error": index_error,
+ "classifier_status": classifier_status,
+ "classifier_last_modified": classifier_last_modified,
+ "classifier_error": classifier_error,
},
},
)