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 ca5c758ba..614d2fcd0 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -185,7 +185,8 @@ export class SettingsComponent this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR || this.systemStatus.tasks.sanity_check_status === - SystemStatusItemStatus.ERROR + SystemStatusItemStatus.ERROR || + this.systemStatus.websocket_connected === 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 e3b09ee7e..99fddbf2c 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 @@ -254,6 +254,18 @@
Error:
{{status.tasks.sanity_check_error}} } +
WebSocket Connection
+
+ + @if (status.websocket_connected === 'OK') { + OK + + } @else { + Error + + } + +
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 f9d8b4d68..1785459f4 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 @@ -24,7 +24,7 @@ import { } from '@angular/core/testing' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' -import { of, throwError } from 'rxjs' +import { Subject, of, throwError } from 'rxjs' import { PaperlessTaskName } from 'src/app/data/paperless-task' import { InstallType, @@ -34,6 +34,7 @@ import { import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' import { ToastService } from 'src/app/services/toast.service' +import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { SystemStatusDialogComponent } from './system-status-dialog.component' const status: SystemStatus = { @@ -77,6 +78,8 @@ describe('SystemStatusDialogComponent', () => { let tasksService: TasksService let systemStatusService: SystemStatusService let toastService: ToastService + let websocketStatusService: WebsocketStatusService + let websocketSubject: Subject = new Subject() beforeEach(async () => { await TestBed.configureTestingModule({ @@ -98,6 +101,12 @@ describe('SystemStatusDialogComponent', () => { tasksService = TestBed.inject(TasksService) systemStatusService = TestBed.inject(SystemStatusService) toastService = TestBed.inject(ToastService) + websocketStatusService = TestBed.inject(WebsocketStatusService) + jest + .spyOn(websocketStatusService, 'onConnectionStatus') + .mockImplementation(() => { + return websocketSubject.asObservable() + }) fixture.detectChanges() }) @@ -168,4 +177,19 @@ describe('SystemStatusDialogComponent', () => { component.ngOnInit() expect(component.versionMismatch).toBeFalsy() }) + + it('should update websocket connection status', () => { + websocketSubject.next(true) + expect(component.status.websocket_connected).toEqual( + SystemStatusItemStatus.OK + ) + websocketSubject.next(false) + expect(component.status.websocket_connected).toEqual( + SystemStatusItemStatus.ERROR + ) + websocketSubject.next(true) + expect(component.status.websocket_connected).toEqual( + SystemStatusItemStatus.OK + ) + }) }) 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 bc027ebbf..f88d56ff6 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,5 +1,5 @@ import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard' -import { Component, OnInit, inject } from '@angular/core' +import { Component, OnDestroy, OnInit, inject } from '@angular/core' import { NgbActiveModal, NgbModalModule, @@ -7,6 +7,7 @@ import { NgbProgressbarModule, } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' +import { Subject, takeUntil } from 'rxjs' import { PaperlessTaskName } from 'src/app/data/paperless-task' import { SystemStatus, @@ -18,6 +19,7 @@ import { PermissionsService } from 'src/app/services/permissions.service' import { SystemStatusService } from 'src/app/services/system-status.service' import { TasksService } from 'src/app/services/tasks.service' import { ToastService } from 'src/app/services/toast.service' +import { WebsocketStatusService } from 'src/app/services/websocket-status.service' import { environment } from 'src/environments/environment' @Component({ @@ -34,13 +36,14 @@ import { environment } from 'src/environments/environment' NgxBootstrapIconsModule, ], }) -export class SystemStatusDialogComponent implements OnInit { +export class SystemStatusDialogComponent implements OnInit, OnDestroy { activeModal = inject(NgbActiveModal) private clipboard = inject(Clipboard) private systemStatusService = inject(SystemStatusService) private tasksService = inject(TasksService) private toastService = inject(ToastService) private permissionsService = inject(PermissionsService) + private websocketStatusService = inject(WebsocketStatusService) public SystemStatusItemStatus = SystemStatusItemStatus public PaperlessTaskName = PaperlessTaskName @@ -51,6 +54,7 @@ export class SystemStatusDialogComponent implements OnInit { public copied: boolean = false private runningTasks: Set = new Set() + private unsubscribeNotifier: Subject = new Subject() get currentUserIsSuperUser(): boolean { return this.permissionsService.isSuperUser() @@ -65,6 +69,17 @@ export class SystemStatusDialogComponent implements OnInit { if (this.versionMismatch) { this.status.pngx_version = `${this.status.pngx_version} (frontend: ${this.frontendVersion})` } + this.status.websocket_connected = this.websocketStatusService.isConnected() + ? SystemStatusItemStatus.OK + : SystemStatusItemStatus.ERROR + this.websocketStatusService + .onConnectionStatus() + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe((connected) => { + this.status.websocket_connected = connected + ? SystemStatusItemStatus.OK + : SystemStatusItemStatus.ERROR + }) } public close() { @@ -97,7 +112,7 @@ export class SystemStatusDialogComponent implements OnInit { this.runningTasks.delete(taskName) this.systemStatusService.get().subscribe({ next: (status) => { - this.status = status + Object.assign(this.status, status) }, }) }, @@ -110,4 +125,9 @@ export class SystemStatusDialogComponent implements OnInit { }, }) } + + ngOnDestroy(): void { + this.unsubscribeNotifier.next(this) + this.unsubscribeNotifier.complete() + } } diff --git a/src-ui/src/app/data/system-status.ts b/src-ui/src/app/data/system-status.ts index 698382154..334dc54f8 100644 --- a/src-ui/src/app/data/system-status.ts +++ b/src-ui/src/app/data/system-status.ts @@ -44,4 +44,5 @@ export interface SystemStatus { sanity_check_last_run: string // ISO date string sanity_check_error: string } + websocket_connected?: SystemStatusItemStatus // added client-side } diff --git a/src-ui/src/app/services/websocket-status.service.ts b/src-ui/src/app/services/websocket-status.service.ts index 1809e96f7..f9084c88c 100644 --- a/src-ui/src/app/services/websocket-status.service.ts +++ b/src-ui/src/app/services/websocket-status.service.ts @@ -103,6 +103,7 @@ export class WebsocketStatusService { private documentConsumptionFinishedSubject = new Subject() private documentConsumptionFailedSubject = new Subject() private documentDeletedSubject = new Subject() + private connectionStatusSubject = new Subject() private get(taskId: string, filename?: string) { let status = @@ -153,6 +154,15 @@ export class WebsocketStatusService { this.statusWebSocket = new WebSocket( `${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/` ) + this.statusWebSocket.onopen = () => { + this.connectionStatusSubject.next(true) + } + this.statusWebSocket.onclose = () => { + this.connectionStatusSubject.next(false) + } + this.statusWebSocket.onerror = () => { + this.connectionStatusSubject.next(false) + } this.statusWebSocket.onmessage = (ev: MessageEvent) => { const { type, @@ -286,4 +296,12 @@ export class WebsocketStatusService { onDocumentDeleted() { return this.documentDeletedSubject } + + onConnectionStatus() { + return this.connectionStatusSubject.asObservable() + } + + isConnected(): boolean { + return this.statusWebSocket?.readyState === WebSocket.OPEN + } }