]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Enhancement: report websocket status (#10777)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 5 Sep 2025 18:09:42 +0000 (11:09 -0700)
committerGitHub <noreply@github.com>
Fri, 5 Sep 2025 18:09:42 +0000 (11:09 -0700)
src-ui/src/app/components/admin/settings/settings.component.ts
src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html
src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.spec.ts
src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts
src-ui/src/app/data/system-status.ts
src-ui/src/app/services/websocket-status.service.ts

index ca5c758bacd0b609a5a636f533a3da59be91c679..614d2fcd0f37fe5fa6bc634ffb1fac3683ac9d36 100644 (file)
@@ -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
     )
   }
 
index e3b09ee7e6f540d7c899e71282b373f05939a9bd..99fddbf2c3e6cbc6a0c1c43caa644b561fd4677b 100644 (file)
                   <h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
                 }
               </ng-template>
+              <dt i18n>WebSocket Connection</dt>
+              <dd>
+                <span class="btn btn-sm pe-none align-items-center btn-dark text-uppercase small">
+                  @if (status.websocket_connected === 'OK') {
+                    <ng-container i18n>OK</ng-container>
+                    <i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
+                  } @else {
+                    <ng-container i18n>Error</ng-container>
+                    <i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
+                  }
+                </span>
+              </dd>
             </dl>
           </div>
         </div>
index f9d8b4d68354e413ddf028a32f947f2b7ee7c6a8..1785459f464522d67df0edc8838b35b0d4e64a54 100644 (file)
@@ -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<boolean> = new Subject<boolean>()
 
   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
+    )
+  })
 })
index bc027ebbf070be91c29359ba1dec078c9da175b9..f88d56ff6e997082fb11233998f42397340a4acb 100644 (file)
@@ -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<PaperlessTaskName> = new Set()
+  private unsubscribeNotifier: Subject<any> = 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()
+  }
 }
index 698382154e6d3425abe923c15374c784528ccdae..334dc54f87c896a67dd0c34f35fbc6d2f059b822 100644 (file)
@@ -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
 }
index 1809e96f798a8f2a56900b2e90c86d3840830604..f9084c88cf98011e88ef4e7960bf81a24d9ae8f0 100644 (file)
@@ -103,6 +103,7 @@ export class WebsocketStatusService {
   private documentConsumptionFinishedSubject = new Subject<FileStatus>()
   private documentConsumptionFailedSubject = new Subject<FileStatus>()
   private documentDeletedSubject = new Subject<boolean>()
+  private connectionStatusSubject = new Subject<boolean>()
 
   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
+  }
 }