|
@@ -3,15 +3,14 @@
|
|
|
*
|
|
*
|
|
|
* Opens a persistent Socket.io connection to NestJS /monitor namespace.
|
|
* Opens a persistent Socket.io connection to NestJS /monitor namespace.
|
|
|
* Emits monitor:subscribe on connect and keeps the tunnel alive indefinitely.
|
|
* Emits monitor:subscribe on connect and keeps the tunnel alive indefinitely.
|
|
|
- * Exposes a signal with the latest MonitorPayload[] for the PerformanceHUD.
|
|
|
|
|
|
|
+ * Exposes signals for the PerformanceHUD and chatbot status indicators.
|
|
|
*
|
|
*
|
|
|
- * Connection is opened once at construction and NEVER closed — zombie
|
|
|
|
|
- * socket deliberately held open (Lego 06 "Permanent State" logic).
|
|
|
|
|
|
|
+ * n8nStatus is driven by monitor:status events emitted by NestJS every time
|
|
|
|
|
+ * its server-side webhook probe result changes (probed every 10 s). This is
|
|
|
|
|
+ * truth-based: NestJS POSTs to the actual webhook, not just a port check.
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import { Injectable, signal, computed, OnDestroy } from '@angular/core';
|
|
import { Injectable, signal, computed, OnDestroy } from '@angular/core';
|
|
|
-import { HttpClient } from '@angular/common/http';
|
|
|
|
|
-import { firstValueFrom } from 'rxjs';
|
|
|
|
|
import { io, Socket } from 'socket.io-client';
|
|
import { io, Socket } from 'socket.io-client';
|
|
|
import { environment } from '../../environments/environment';
|
|
import { environment } from '../../environments/environment';
|
|
|
|
|
|
|
@@ -23,6 +22,11 @@ export interface MonitorPayload {
|
|
|
timestamp: string;
|
|
timestamp: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+export interface MonitorStatus {
|
|
|
|
|
+ n8nWebhookReady: boolean;
|
|
|
|
|
+ timestamp: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
@Injectable({ providedIn: 'root' })
|
|
@Injectable({ providedIn: 'root' })
|
|
|
export class SurveillanceService implements OnDestroy {
|
|
export class SurveillanceService implements OnDestroy {
|
|
|
|
|
|
|
@@ -35,12 +39,15 @@ export class SurveillanceService implements OnDestroy {
|
|
|
this.connected() ? 'ONLINE' : 'OFFLINE'
|
|
this.connected() ? 'ONLINE' : 'OFFLINE'
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- /** n8n reachability: untested → online → offline */
|
|
|
|
|
- readonly n8nStatus = signal<'untested' | 'online' | 'offline'>('untested');
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * n8n agent readiness — driven by NestJS webhook probe via monitor:status.
|
|
|
|
|
+ * 'checking' until the first probe result arrives after connect.
|
|
|
|
|
+ */
|
|
|
|
|
+ readonly n8nStatus = signal<'checking' | 'ready' | 'not-ready'>('checking');
|
|
|
|
|
|
|
|
private socket: Socket;
|
|
private socket: Socket;
|
|
|
|
|
|
|
|
- constructor(private http: HttpClient) {
|
|
|
|
|
|
|
+ constructor() {
|
|
|
// Persistent tunnel — opened on service construction (app boot)
|
|
// Persistent tunnel — opened on service construction (app boot)
|
|
|
this.socket = io(`${environment.nestWsUrl}/monitor`, {
|
|
this.socket = io(`${environment.nestWsUrl}/monitor`, {
|
|
|
transports: ['websocket'],
|
|
transports: ['websocket'],
|
|
@@ -50,54 +57,31 @@ export class SurveillanceService implements OnDestroy {
|
|
|
|
|
|
|
|
this.socket.on('connect', () => {
|
|
this.socket.on('connect', () => {
|
|
|
this.connected.set(true);
|
|
this.connected.set(true);
|
|
|
|
|
+ this.n8nStatus.set('checking');
|
|
|
// Lego 11: emit monitor:subscribe to start the data stream
|
|
// Lego 11: emit monitor:subscribe to start the data stream
|
|
|
this.socket.emit('monitor:subscribe');
|
|
this.socket.emit('monitor:subscribe');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
this.socket.on('disconnect', () => {
|
|
this.socket.on('disconnect', () => {
|
|
|
this.connected.set(false);
|
|
this.connected.set(false);
|
|
|
|
|
+ this.n8nStatus.set('not-ready');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Every 500ms tick from NestJS SurveillanceService
|
|
// Every 500ms tick from NestJS SurveillanceService
|
|
|
this.socket.on('monitor:data', (payload: MonitorPayload[]) => {
|
|
this.socket.on('monitor:data', (payload: MonitorPayload[]) => {
|
|
|
this.metrics.set(payload);
|
|
this.metrics.set(payload);
|
|
|
});
|
|
});
|
|
|
|
|
+
|
|
|
|
|
+ // Webhook probe result — emitted immediately on connect and on every change
|
|
|
|
|
+ this.socket.on('monitor:status', (status: MonitorStatus) => {
|
|
|
|
|
+ this.n8nStatus.set(status.n8nWebhookReady ? 'ready' : 'not-ready');
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnDestroy() {
|
|
ngOnDestroy() {
|
|
|
- // NOTE: intentionally NOT calling socket.disconnect() during normal app
|
|
|
|
|
- // operation — the socket stays alive (Lego 06 zombie connection mandate).
|
|
|
|
|
- // This method only runs on Angular DI teardown (full app destroy).
|
|
|
|
|
this.socket.disconnect();
|
|
this.socket.disconnect();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Lightweight OPTIONS request to test if the n8n webhook URL is reachable.
|
|
|
|
|
- * 405 (Method Not Allowed) is treated as reachable — server responded.
|
|
|
|
|
- * Any network error or non-2xx/non-405 marks it offline.
|
|
|
|
|
- */
|
|
|
|
|
- async checkN8nStatus(url: string): Promise<void> {
|
|
|
|
|
- if (!url || !url.trim()) {
|
|
|
|
|
- this.n8nStatus.set('offline');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- try {
|
|
|
|
|
- const response = await firstValueFrom(
|
|
|
|
|
- this.http.options(url, { observe: 'response' })
|
|
|
|
|
- );
|
|
|
|
|
- this.n8nStatus.set(
|
|
|
|
|
- response.ok || response.status === 405 ? 'online' : 'offline'
|
|
|
|
|
- );
|
|
|
|
|
- } catch (err: any) {
|
|
|
|
|
- // 405 arrives as an HTTP error in Angular's HttpClient
|
|
|
|
|
- if (err?.status === 405 || err?.status === 200) {
|
|
|
|
|
- this.n8nStatus.set('online');
|
|
|
|
|
- } else {
|
|
|
|
|
- this.n8nStatus.set('offline');
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
formatBytes(bytes: number): string {
|
|
formatBytes(bytes: number): string {
|
|
|
if (bytes === 0) return '0 B';
|
|
if (bytes === 0) return '0 B';
|
|
|
const mb = bytes / (1024 * 1024);
|
|
const mb = bytes / (1024 * 1024);
|