Add index and classifier status

This commit is contained in:
shamoon 2024-02-13 19:28:48 -08:00
parent d651e3132f
commit d56eb2eed5
11 changed files with 211 additions and 82 deletions

View File

@ -1540,7 +1540,7 @@
<source>Error retrieving users</source> <source>Error retrieving users</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">182</context> <context context-type="linenumber">185</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
@ -1551,7 +1551,7 @@
<source>Error retrieving groups</source> <source>Error retrieving groups</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">201</context> <context context-type="linenumber">204</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
@ -1562,35 +1562,35 @@
<source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source> <source>Saved view &quot;<x id="PH" equiv-text="savedView.name"/>&quot; deleted.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">414</context> <context context-type="linenumber">417</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7217000812750597833" datatype="html"> <trans-unit id="7217000812750597833" datatype="html">
<source>Settings were saved successfully.</source> <source>Settings were saved successfully.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">540</context> <context context-type="linenumber">543</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="525012668859298131" datatype="html"> <trans-unit id="525012668859298131" datatype="html">
<source>Settings were saved successfully. Reload is required to apply some changes.</source> <source>Settings were saved successfully. Reload is required to apply some changes.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">544</context> <context context-type="linenumber">547</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8491974984518503778" datatype="html"> <trans-unit id="8491974984518503778" datatype="html">
<source>Reload now</source> <source>Reload now</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">545</context> <context context-type="linenumber">548</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3011185103048412841" datatype="html"> <trans-unit id="3011185103048412841" datatype="html">
<source>An error occurred while saving settings.</source> <source>An error occurred while saving settings.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">555</context> <context context-type="linenumber">558</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
@ -1601,7 +1601,7 @@
<source>Error while storing settings on server.</source> <source>Error while storing settings on server.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context> <context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">589</context> <context context-type="linenumber">592</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2991443309752293110" datatype="html"> <trans-unit id="2991443309752293110" datatype="html">
@ -4160,7 +4160,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">124</context> <context context-type="linenumber">144</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="595732867213154214" datatype="html"> <trans-unit id="595732867213154214" datatype="html">
@ -4449,18 +4449,11 @@
<context context-type="linenumber">41</context> <context context-type="linenumber">41</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2375260419993138758" datatype="html">
<source>URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
</trans-unit>
<trans-unit id="5611592591303869712" datatype="html"> <trans-unit id="5611592591303869712" datatype="html">
<source>Status</source> <source>Status</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">47</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context> <context context-type="sourcefile">src/app/components/common/toasts/toasts.component.html</context>
@ -4475,49 +4468,67 @@
<source>Migration Status</source> <source>Migration Status</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">58</context> <context context-type="linenumber">56</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7881311375431899727" datatype="html"> <trans-unit id="7881311375431899727" datatype="html">
<source>Latest Migration</source> <source>Latest Migration</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">66</context> <context context-type="linenumber">64</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4632965004151576238" datatype="html"> <trans-unit id="4632965004151576238" datatype="html">
<source>Pending Migrations</source> <source>Pending Migrations</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6904866445262015585" datatype="html"> <trans-unit id="6904866445262015585" datatype="html">
<source>Tasks</source> <source>Tasks</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">85</context> <context context-type="linenumber">83</context>
</context-group>
</trans-unit>
<trans-unit id="1044837289640087179" datatype="html">
<source>Redis URL</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">89</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6911698235105017958" datatype="html"> <trans-unit id="6911698235105017958" datatype="html">
<source>Redis Status</source> <source>Redis Status</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">91</context> <context context-type="linenumber">87</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5349496739889768589" datatype="html"> <trans-unit id="5349496739889768589" datatype="html">
<source>Celery Status</source> <source>Celery Status</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">100</context> <context context-type="linenumber">96</context>
</context-group>
</trans-unit>
<trans-unit id="1086740373716043695" datatype="html">
<source>Index Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">105</context>
</context-group>
</trans-unit>
<trans-unit id="4089509911694721896" datatype="html">
<source>Last Updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">115</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">127</context>
</context-group>
</trans-unit>
<trans-unit id="5232507269140097134" datatype="html">
<source>Classifier Status</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">117</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6732151329960766506" datatype="html"> <trans-unit id="6732151329960766506" datatype="html">

View File

@ -44,9 +44,9 @@ import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-butt
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { import {
PaperlessSystemStatus, SystemStatus,
PaperlessInstallType, InstallType,
PaperlessConnectionStatus, SystemStatusItemStatus,
} from 'src/app/data/system-status' } from 'src/app/data/system-status'
const savedViews = [ const savedViews = [
@ -388,15 +388,15 @@ describe('SettingsComponent', () => {
}) })
it('should load system status on initialize, show errors if needed', () => { it('should load system status on initialize, show errors if needed', () => {
const status: PaperlessSystemStatus = { const status: SystemStatus = {
pngx_version: '2.4.3', pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit', server_os: 'macOS-14.1.1-arm64-arm-64bit',
install_type: PaperlessInstallType.BareMetal, install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 }, storage: { total: 494384795648, available: 13573525504 },
database: { database: {
type: 'sqlite', type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3', url: '/paperless-ngx/data/db.sqlite3',
status: PaperlessConnectionStatus.ERROR, status: SystemStatusItemStatus.ERROR,
error: null, error: null,
migration_status: { migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data', latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
@ -405,10 +405,16 @@ describe('SettingsComponent', () => {
}, },
tasks: { tasks: {
redis_url: 'redis://localhost:6379', redis_url: 'redis://localhost:6379',
redis_status: PaperlessConnectionStatus.ERROR, redis_status: SystemStatusItemStatus.ERROR,
redis_error: redis_error:
'Error 61 connecting to localhost:6379. Connection refused.', '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)) jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
@ -416,9 +422,9 @@ describe('SettingsComponent', () => {
expect(component['systemStatus']).toEqual(status) // private expect(component['systemStatus']).toEqual(status) // private
expect(component.systemStatusHasErrors).toBeTruthy() expect(component.systemStatusHasErrors).toBeTruthy()
// coverage // coverage
component['systemStatus'].database.status = PaperlessConnectionStatus.OK component['systemStatus'].database.status = SystemStatusItemStatus.OK
component['systemStatus'].tasks.redis_status = PaperlessConnectionStatus.OK component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
component['systemStatus'].tasks.celery_status = PaperlessConnectionStatus.OK component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
expect(component.systemStatusHasErrors).toBeFalsy() expect(component.systemStatusHasErrors).toBeFalsy()
}) })

View File

@ -47,8 +47,8 @@ import { ComponentWithPermissions } from '../../with-permissions/with-permission
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component' import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { import {
PaperlessConnectionStatus, SystemStatusItemStatus,
PaperlessSystemStatus, SystemStatus,
} from 'src/app/data/system-status' } from 'src/app/data/system-status'
enum SettingsNavIDs { enum SettingsNavIDs {
@ -121,14 +121,15 @@ export class SettingsComponent
users: User[] users: User[]
groups: Group[] groups: Group[]
private systemStatus: PaperlessSystemStatus private systemStatus: SystemStatus
get systemStatusHasErrors(): boolean { get systemStatusHasErrors(): boolean {
return ( return (
this.systemStatus.database.status === PaperlessConnectionStatus.ERROR || this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.redis_status === this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
PaperlessConnectionStatus.ERROR || this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.celery_status === PaperlessConnectionStatus.ERROR this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
) )
} }

View File

@ -44,15 +44,13 @@
<dl class="card-text"> <dl class="card-text">
<dt i18n>Type</dt> <dt i18n>Type</dt>
<dd>{{status.database.type}}</dd> <dd>{{status.database.type}}</dd>
<dt i18n>URL</dt>
<dd>{{status.database.url}}</dd>
<dt i18n>Status</dt> <dt i18n>Status</dt>
<dd> <dd>
{{status.database.status}} {{status.database.status}}
@if (status.database.status === 'OK') { @if (status.database.status === 'OK') {
<i-bs name="check-circle-fill" class="text-success ms-1"></i-bs> <i-bs name="check-circle-fill" class="text-success ms-1" ngbPopover="{{status.database.url}}" triggers="mouseenter:mouseleave"></i-bs>
} @else { } @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs> <i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.database.url}}: {{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
} }
</dd> </dd>
<dt i18n>Migration Status</dt> <dt i18n>Migration Status</dt>
@ -86,15 +84,13 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="card-text"> <dl class="card-text">
<dt i18n>Redis URL</dt>
<dd>{{status.tasks.redis_url}}</dd>
<dt i18n>Redis Status</dt> <dt i18n>Redis Status</dt>
<dd> <dd>
{{status.tasks.redis_status}} {{status.tasks.redis_status}}
@if (status.tasks.redis_status === 'OK') { @if (status.tasks.redis_status === 'OK') {
<i-bs name="check-circle-fill" class="text-success ms-1"></i-bs> <i-bs name="check-circle-fill" class="text-success ms-1" ngbPopover="{{status.tasks.redis_url}}" triggers="mouseenter:mouseleave"></i-bs>
} @else { } @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs> <i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.tasks.redis_url}}: {{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
} }
</dd> </dd>
<dt i18n>Celery Status</dt> <dt i18n>Celery Status</dt>
@ -106,6 +102,30 @@
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1"></i-bs> <i-bs name="exclamation-triangle-fill" class="text-danger ms-1"></i-bs>
} }
</dd> </dd>
<dt i18n>Index Status</dt>
<dd>
{{status.tasks.index_status}}
@if (status.tasks.index_status === 'OK') {
<i-bs name="check-circle-fill" class="text-success ms-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.tasks.index_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<ng-template #indexStatus>
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified}}</span>
</ng-template>
<dt i18n>Classifier Status</dt>
<dd>
{{status.tasks.classifier_status}}
@if (status.tasks.classifier_status === 'OK') {
<i-bs name="check-circle-fill" class="text-success ms-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
} @else {
<i-bs name="exclamation-triangle-fill" class="text-danger ms-1" ngbPopover="{{status.tasks.classifier_error}}" triggers="mouseenter:mouseleave"></i-bs>
}
</dd>
<ng-template #classifierStatus>
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_modified}}</span>
</ng-template>
</dl> </dl>
</div> </div>
</div> </div>

View File

@ -8,29 +8,28 @@ import {
NgbActiveModal, NgbActiveModal,
NgbModalModule, NgbModalModule,
NgbPopoverModule, NgbPopoverModule,
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard' import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { SystemStatusDialogComponent } from './system-status-dialog.component' import { SystemStatusDialogComponent } from './system-status-dialog.component'
import { of } from 'rxjs'
import { import {
PaperlessConnectionStatus, SystemStatusItemStatus,
PaperlessInstallType, InstallType,
PaperlessSystemStatus, SystemStatus,
} from 'src/app/data/system-status' } from 'src/app/data/system-status'
import { HttpClientTestingModule } from '@angular/common/http/testing' import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NgxFilesizeModule } from 'ngx-filesize' import { NgxFilesizeModule } from 'ngx-filesize'
const status: PaperlessSystemStatus = { const status: SystemStatus = {
pngx_version: '2.4.3', pngx_version: '2.4.3',
server_os: 'macOS-14.1.1-arm64-arm-64bit', server_os: 'macOS-14.1.1-arm64-arm-64bit',
install_type: PaperlessInstallType.BareMetal, install_type: InstallType.BareMetal,
storage: { total: 494384795648, available: 13573525504 }, storage: { total: 494384795648, available: 13573525504 },
database: { database: {
type: 'sqlite', type: 'sqlite',
url: '/paperless-ngx/data/db.sqlite3', url: '/paperless-ngx/data/db.sqlite3',
status: PaperlessConnectionStatus.ERROR, status: SystemStatusItemStatus.ERROR,
error: null, error: null,
migration_status: { migration_status: {
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data', latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
@ -39,9 +38,15 @@ const status: PaperlessSystemStatus = {
}, },
tasks: { tasks: {
redis_url: 'redis://localhost:6379', redis_url: 'redis://localhost:6379',
redis_status: PaperlessConnectionStatus.ERROR, redis_status: SystemStatusItemStatus.ERROR,
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.', 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), NgxBootstrapIconsModule.pick(allIcons),
NgxFilesizeModule, NgxFilesizeModule,
NgbPopoverModule, NgbPopoverModule,
NgbProgressbarModule,
], ],
}).compileComponents() }).compileComponents()

View File

@ -1,6 +1,6 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 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 { SystemStatusService } from 'src/app/services/system-status.service'
import { Clipboard } from '@angular/cdk/clipboard' import { Clipboard } from '@angular/cdk/clipboard'
@ -10,7 +10,7 @@ import { Clipboard } from '@angular/cdk/clipboard'
styleUrl: './system-status-dialog.component.scss', styleUrl: './system-status-dialog.component.scss',
}) })
export class SystemStatusDialogComponent { export class SystemStatusDialogComponent {
public status: PaperlessSystemStatus public status: SystemStatus
public copied: boolean = false public copied: boolean = false

View File

@ -1,17 +1,17 @@
export enum PaperlessInstallType { export enum InstallType {
Containerized = 'containerized', Containerized = 'containerized',
BareMetal = 'bare-metal', BareMetal = 'bare-metal',
} }
export enum PaperlessConnectionStatus { export enum SystemStatusItemStatus {
OK = 'OK', OK = 'OK',
ERROR = 'ERROR', ERROR = 'ERROR',
} }
export interface PaperlessSystemStatus { export interface SystemStatus {
pngx_version: string pngx_version: string
server_os: string server_os: string
install_type: PaperlessInstallType install_type: InstallType
storage: { storage: {
total: number total: number
available: number available: number
@ -19,7 +19,7 @@ export interface PaperlessSystemStatus {
database: { database: {
type: string type: string
url: string url: string
status: PaperlessConnectionStatus status: SystemStatusItemStatus
error?: string error?: string
migration_status: { migration_status: {
latest_migration: string latest_migration: string
@ -28,8 +28,14 @@ export interface PaperlessSystemStatus {
} }
tasks: { tasks: {
redis_url: string redis_url: string
redis_status: PaperlessConnectionStatus redis_status: SystemStatusItemStatus
redis_error: string 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
} }
} }

View File

@ -25,7 +25,7 @@ describe('SystemStatusService', () => {
httpTestingController.verify() httpTestingController.verify()
}) })
it('calls get statys endpoint', () => { it('calls get status endpoint', () => {
service.get().subscribe() service.get().subscribe()
const req = httpTestingController.expectOne( const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}status/` `${environment.apiBaseUrl}status/`

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { PaperlessSystemStatus } from '../data/system-status' import { SystemStatus } from '../data/system-status'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@Injectable({ @Injectable({
@ -12,8 +12,8 @@ export class SystemStatusService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
get(): Observable<PaperlessSystemStatus> { get(): Observable<SystemStatus> {
return this.http.get<PaperlessSystemStatus>( return this.http.get<SystemStatus>(
`${environment.apiBaseUrl}${this.endpoint}/` `${environment.apiBaseUrl}${this.endpoint}/`
) )
} }

View File

@ -1,14 +1,16 @@
import os import os
from unittest import mock from unittest import mock
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from paperless import version from paperless import version
class TestSystemStatusView(APITestCase): class TestSystemStatus(APITestCase):
ENDPOINT = "/api/status/" ENDPOINT = "/api/status/"
def setUp(self): def setUp(self):
@ -67,3 +69,46 @@ class TestSystemStatusView(APITestCase):
response = self.client.get(self.ENDPOINT) response = self.client.get(self.ENDPOINT)
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["tasks"]["celery_status"], "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"])

View File

@ -35,6 +35,7 @@ from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import make_aware
from django.utils.translation import get_language from django.utils.translation import get_language
from django.views import View from django.views import View
from django.views.decorators.cache import cache_control 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 rest_framework.viewsets import ViewSet
from documents import bulk_edit from documents import bulk_edit
from documents import index
from documents.bulk_download import ArchiveOnlyStrategy from documents.bulk_download import ArchiveOnlyStrategy
from documents.bulk_download import OriginalAndArchiveStrategy from documents.bulk_download import OriginalAndArchiveStrategy
from documents.bulk_download import OriginalsOnlyStrategy 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." redis_error = "Error connecting to redis, check logs for more detail."
try: try:
ping = celery_app.control.inspect().ping() celery_ping = celery_app.control.inspect().ping()
first_worker_ping = ping[next(iter(ping.keys()))] first_worker_ping = celery_ping[next(iter(celery_ping.keys()))]
if first_worker_ping["ok"] == "pong": if first_worker_ping["ok"] == "pong":
celery_active = "OK" celery_active = "OK"
except Exception: except Exception:
celery_active = "ERROR" 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( return Response(
{ {
"pngx_version": current_version, "pngx_version": current_version,
@ -1628,6 +1656,12 @@ class SystemStatusView(GenericAPIView, PassUserMixin):
"redis_status": redis_status, "redis_status": redis_status,
"redis_error": redis_error, "redis_error": redis_error,
"celery_status": celery_active, "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,
}, },
}, },
) )