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
+ }
}