Enhancement: websocket status

This commit is contained in:
shamoon
2025-09-04 03:42:18 -07:00
parent b2703b4605
commit 0e0419d010
6 changed files with 81 additions and 5 deletions

View File

@@ -185,7 +185,8 @@ export class SettingsComponent
this.systemStatus.tasks.classifier_status === this.systemStatus.tasks.classifier_status ===
SystemStatusItemStatus.ERROR || SystemStatusItemStatus.ERROR ||
this.systemStatus.tasks.sanity_check_status === this.systemStatus.tasks.sanity_check_status ===
SystemStatusItemStatus.ERROR SystemStatusItemStatus.ERROR ||
this.systemStatus.websocket_connected === SystemStatusItemStatus.ERROR
) )
} }

View File

@@ -254,6 +254,18 @@
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span> <h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
} }
</ng-template> </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> </dl>
</div> </div>
</div> </div>

View File

@@ -24,7 +24,7 @@ import {
} from '@angular/core/testing' } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' 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 { PaperlessTaskName } from 'src/app/data/paperless-task'
import { import {
InstallType, InstallType,
@@ -34,6 +34,7 @@ import {
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { TasksService } from 'src/app/services/tasks.service' import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.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' import { SystemStatusDialogComponent } from './system-status-dialog.component'
const status: SystemStatus = { const status: SystemStatus = {
@@ -77,6 +78,8 @@ describe('SystemStatusDialogComponent', () => {
let tasksService: TasksService let tasksService: TasksService
let systemStatusService: SystemStatusService let systemStatusService: SystemStatusService
let toastService: ToastService let toastService: ToastService
let websocketStatusService: WebsocketStatusService
let websocketSubject: Subject<boolean> = new Subject<boolean>()
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
@@ -98,6 +101,12 @@ describe('SystemStatusDialogComponent', () => {
tasksService = TestBed.inject(TasksService) tasksService = TestBed.inject(TasksService)
systemStatusService = TestBed.inject(SystemStatusService) systemStatusService = TestBed.inject(SystemStatusService)
toastService = TestBed.inject(ToastService) toastService = TestBed.inject(ToastService)
websocketStatusService = TestBed.inject(WebsocketStatusService)
jest
.spyOn(websocketStatusService, 'onConnectionStatus')
.mockImplementation(() => {
return websocketSubject.asObservable()
})
fixture.detectChanges() fixture.detectChanges()
}) })
@@ -168,4 +177,19 @@ describe('SystemStatusDialogComponent', () => {
component.ngOnInit() component.ngOnInit()
expect(component.versionMismatch).toBeFalsy() 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
)
})
}) })

View File

@@ -1,5 +1,5 @@
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard' import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
import { Component, OnInit, inject } from '@angular/core' import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { import {
NgbActiveModal, NgbActiveModal,
NgbModalModule, NgbModalModule,
@@ -7,6 +7,7 @@ import {
NgbProgressbarModule, NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap' } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, takeUntil } from 'rxjs'
import { PaperlessTaskName } from 'src/app/data/paperless-task' import { PaperlessTaskName } from 'src/app/data/paperless-task'
import { import {
SystemStatus, SystemStatus,
@@ -18,6 +19,7 @@ import { PermissionsService } from 'src/app/services/permissions.service'
import { SystemStatusService } from 'src/app/services/system-status.service' import { SystemStatusService } from 'src/app/services/system-status.service'
import { TasksService } from 'src/app/services/tasks.service' import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service' import { ToastService } from 'src/app/services/toast.service'
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
@Component({ @Component({
@@ -34,13 +36,14 @@ import { environment } from 'src/environments/environment'
NgxBootstrapIconsModule, NgxBootstrapIconsModule,
], ],
}) })
export class SystemStatusDialogComponent implements OnInit { export class SystemStatusDialogComponent implements OnInit, OnDestroy {
activeModal = inject(NgbActiveModal) activeModal = inject(NgbActiveModal)
private clipboard = inject(Clipboard) private clipboard = inject(Clipboard)
private systemStatusService = inject(SystemStatusService) private systemStatusService = inject(SystemStatusService)
private tasksService = inject(TasksService) private tasksService = inject(TasksService)
private toastService = inject(ToastService) private toastService = inject(ToastService)
private permissionsService = inject(PermissionsService) private permissionsService = inject(PermissionsService)
private websocketStatusService = inject(WebsocketStatusService)
public SystemStatusItemStatus = SystemStatusItemStatus public SystemStatusItemStatus = SystemStatusItemStatus
public PaperlessTaskName = PaperlessTaskName public PaperlessTaskName = PaperlessTaskName
@@ -51,6 +54,7 @@ export class SystemStatusDialogComponent implements OnInit {
public copied: boolean = false public copied: boolean = false
private runningTasks: Set<PaperlessTaskName> = new Set() private runningTasks: Set<PaperlessTaskName> = new Set()
private unsubscribeNotifier: Subject<any> = new Subject()
get currentUserIsSuperUser(): boolean { get currentUserIsSuperUser(): boolean {
return this.permissionsService.isSuperUser() return this.permissionsService.isSuperUser()
@@ -65,6 +69,17 @@ export class SystemStatusDialogComponent implements OnInit {
if (this.versionMismatch) { if (this.versionMismatch) {
this.status.pngx_version = `${this.status.pngx_version} (frontend: ${this.frontendVersion})` 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() { public close() {
@@ -97,7 +112,7 @@ export class SystemStatusDialogComponent implements OnInit {
this.runningTasks.delete(taskName) this.runningTasks.delete(taskName)
this.systemStatusService.get().subscribe({ this.systemStatusService.get().subscribe({
next: (status) => { 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()
}
} }

View File

@@ -44,4 +44,5 @@ export interface SystemStatus {
sanity_check_last_run: string // ISO date string sanity_check_last_run: string // ISO date string
sanity_check_error: string sanity_check_error: string
} }
websocket_connected?: SystemStatusItemStatus // added client-side
} }

View File

@@ -103,6 +103,7 @@ export class WebsocketStatusService {
private documentConsumptionFinishedSubject = new Subject<FileStatus>() private documentConsumptionFinishedSubject = new Subject<FileStatus>()
private documentConsumptionFailedSubject = new Subject<FileStatus>() private documentConsumptionFailedSubject = new Subject<FileStatus>()
private documentDeletedSubject = new Subject<boolean>() private documentDeletedSubject = new Subject<boolean>()
private connectionStatusSubject = new Subject<boolean>()
private get(taskId: string, filename?: string) { private get(taskId: string, filename?: string) {
let status = let status =
@@ -153,6 +154,15 @@ export class WebsocketStatusService {
this.statusWebSocket = new WebSocket( this.statusWebSocket = new WebSocket(
`${environment.webSocketProtocol}//${environment.webSocketHost}${environment.webSocketBaseUrl}status/` `${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) => { this.statusWebSocket.onmessage = (ev: MessageEvent) => {
const { const {
type, type,
@@ -286,4 +296,12 @@ export class WebsocketStatusService {
onDocumentDeleted() { onDocumentDeleted() {
return this.documentDeletedSubject return this.documentDeletedSubject
} }
onConnectionStatus() {
return this.connectionStatusSubject.asObservable()
}
isConnected(): boolean {
return this.statusWebSocket?.readyState === WebSocket.OPEN
}
} }