Browse Source

enhanced/fixedx monitoring system

Dr-Swopt 7 giờ trước cách đây
mục cha
commit
e1cdcc16d0

+ 3 - 3
src/app/components/analyzer/analyzer.component.html

@@ -8,8 +8,8 @@
   </button>
   <button class="engine-tab"
           [class.active]="engine() === 'socket'"
-          [class.disabled]="surveillance.nestStatus() === 'OFFLINE'"
-          [disabled]="surveillance.nestStatus() === 'OFFLINE'"
+          [class.disabled]="visionSocket.nestStatus() === 'OFFLINE'"
+          [disabled]="visionSocket.nestStatus() === 'OFFLINE'"
           (click)="onEngineChange('socket')">
     🔌 NestJS Socket
     <span class="socket-status" [class.live]="visionSocket.connected()">
@@ -19,7 +19,7 @@
 </div>
 
 <!-- ── NestJS Offline Banner ─────────────────────────────────────────────────── -->
-@if (surveillance.nestStatus() === 'OFFLINE') {
+@if (visionSocket.nestStatus() === 'OFFLINE') {
   <div class="container nest-offline-banner">
     ⚠ Backend Inference Engine Offline — Local TFLite active. Socket engine disabled.
   </div>

+ 1 - 3
src/app/components/analyzer/analyzer.component.ts

@@ -39,7 +39,6 @@ import { FormsModule } from '@angular/forms';
 import { LocalHistoryService } from '../../services/local-history.service';
 import { InferenceService, LocalEngine } from '../../core/services/inference.service';
 import { VisionSocketService } from '../../services/vision-socket.service';
-import { SurveillanceService } from '../../services/surveillance.service';
 import {
   BatchResult,
   FullSessionReport,
@@ -120,11 +119,10 @@ export class AnalyzerComponent implements OnInit, OnDestroy {
     public inferenceService: InferenceService,
     private localHistory: LocalHistoryService,
     public visionSocket: VisionSocketService,
-    public surveillance: SurveillanceService,
   ) {
     // When NestJS goes offline, force the engine back to a local mode
     effect(() => {
-      if (surveillance.nestStatus() === 'OFFLINE' && this.engine() === 'socket') {
+      if (visionSocket.nestStatus() === 'OFFLINE' && this.engine() === 'socket') {
         this.engine.set('onnx');
         this.stopWebcam();
       }

+ 3 - 3
src/app/components/chatbot/chatbot.component.html

@@ -42,7 +42,7 @@
         </div>
 
         <!-- NestJS offline banner — socket proxy unavailable -->
-        @if (surveillance.nestStatus() === 'OFFLINE') {
+        @if (visionSocket.nestStatus() === 'OFFLINE') {
           <div class="n8n-offline-banner">
             🔴 NestJS offline — chat proxy unavailable. Restore the backend to send messages.
           </div>
@@ -56,12 +56,12 @@
             (keydown)="onEnter($event)"
             placeholder="Ask about palm oil ripeness, site data, operational reports..."
             rows="2"
-            [disabled]="loading() || chatSocket.sending() || surveillance.nestStatus() === 'OFFLINE'">
+            [disabled]="loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
           </textarea>
           <button
             class="btn btn-primary send-btn"
             (click)="sendMessage()"
-            [disabled]="!inputText.trim() || loading() || chatSocket.sending() || surveillance.nestStatus() === 'OFFLINE'">
+            [disabled]="!inputText.trim() || loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
             {{ chatSocket.sending() ? '...' : 'Send' }}
           </button>
         </div>

+ 2 - 2
src/app/components/chatbot/chatbot.component.ts

@@ -16,7 +16,7 @@ import {
 } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
-import { SurveillanceService } from '../../services/surveillance.service';
+import { VisionSocketService } from '../../services/vision-socket.service';
 import { ChatSocketService } from '../../services/chat-socket.service';
 
 export interface ChatMessage {
@@ -51,7 +51,7 @@ export class ChatbotComponent implements OnInit, AfterViewChecked {
 
   constructor(
     public chatSocket: ChatSocketService,
-    public surveillance: SurveillanceService,
+    public visionSocket: VisionSocketService,
   ) {}
 
   private loadMessages(): ChatMessage[] {

+ 6 - 6
src/app/components/history/history.component.html

@@ -11,14 +11,14 @@
         <button
           class="tab-btn"
           [class.active]="viewMode === 'remote'"
-          [class.tab-disabled]="surveillance.nestStatus() === 'OFFLINE'"
-          [disabled]="surveillance.nestStatus() === 'OFFLINE'"
+          [class.tab-disabled]="visionSocket.nestStatus() === 'OFFLINE'"
+          [disabled]="visionSocket.nestStatus() === 'OFFLINE'"
           (click)="switchTab('remote')"
-          [title]="surveillance.nestStatus() === 'OFFLINE' ? 'NestJS offline — Industrial Cloud unavailable' : ''">
+          [title]="visionSocket.nestStatus() === 'OFFLINE' ? 'NestJS offline — Industrial Cloud unavailable' : ''">
           <span class="icon">☁️</span> Industrial Cloud (API)
           <span class="nest-dot"
-                [class.dot-live]="surveillance.nestStatus() === 'ONLINE'"
-                [class.dot-off]="surveillance.nestStatus() === 'OFFLINE'">
+                [class.dot-live]="visionSocket.nestStatus() === 'ONLINE'"
+                [class.dot-off]="visionSocket.nestStatus() === 'OFFLINE'">
           </span>
         </button>
       </div>
@@ -31,7 +31,7 @@
       }
     </div>
 
-    @if (surveillance.nestStatus() === 'OFFLINE') {
+    @if (visionSocket.nestStatus() === 'OFFLINE') {
       <div class="cloud-offline-banner">
         ⚠ NestJS Offline — Industrial Cloud (API) is unavailable. Browser Cache only.
       </div>

+ 3 - 3
src/app/components/history/history.component.ts

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { LocalHistoryService } from '../../services/local-history.service';
 import { RemoteInferenceService } from '../../core/services/remote-inference.service';
-import { SurveillanceService } from '../../services/surveillance.service';
+import { VisionSocketService } from '../../services/vision-socket.service';
 import { environment } from '../../../environments/environment';
 
 const GRADE_COLORS: Record<string, string> = {
@@ -39,7 +39,7 @@ export class HistoryComponent implements OnInit {
   constructor(
     private localHistory: LocalHistoryService,
     private remoteInference: RemoteInferenceService,
-    public surveillance: SurveillanceService,
+    public visionSocket: VisionSocketService,
   ) {}
 
   ngOnInit(): void {
@@ -100,7 +100,7 @@ export class HistoryComponent implements OnInit {
   }
 
   switchTab(mode: 'local' | 'remote'): void {
-    if (mode === 'remote' && this.surveillance.nestStatus() === 'OFFLINE') return;
+    if (mode === 'remote' && this.visionSocket.nestStatus() === 'OFFLINE') return;
     this.viewMode = mode;
     this.expandedId = null;
     if (mode === 'local') {

+ 56 - 25
src/app/components/performance-hud/performance-hud.component.html

@@ -3,7 +3,7 @@
   <!-- Toggle button — always visible, also a drag handle when panel is collapsed -->
   <button class="hud-toggle" cdkDragHandle (click)="toggle()" [title]="collapsed() ? 'Expand HUD' : 'Collapse HUD'">
     <span class="hud-toggle-icon">{{ collapsed() ? '📊' : '✕' }}</span>
-    <span class="hud-status-dot" [class.connected]="surveillance.connected()"></span>
+    <span class="hud-status-dot" [class.connected]="visionSocket.connected()"></span>
   </button>
 
   <!-- Expanded panel -->
@@ -12,64 +12,95 @@
       <div class="hud-header" cdkDragHandle>
         <span class="hud-drag-grip">⠿</span>
         <span class="hud-title">SYSTEM MONITOR</span>
-        <!-- Status LEDs — Nest + n8n always visible in the header -->
         <div class="hud-header-leds">
-          <span class="hud-led-group" title="NestJS Socket: {{ surveillance.connected() ? 'Connected' : 'Offline' }}">
+          <span class="hud-led-group" title="NestJS Socket: {{ visionSocket.connected() ? 'Connected' : 'Offline' }}">
             <span class="hud-led-dot"
-                  [class.led-green]="surveillance.connected()"
-                  [class.led-red]="!surveillance.connected()">
+                  [class.led-green]="visionSocket.connected()"
+                  [class.led-red]="!visionSocket.connected()">
             </span>
             <span class="hud-led-label">Nest</span>
           </span>
         </div>
       </div>
 
-      @if (!surveillance.connected()) {
+      @if (!visionSocket.connected()) {
         <div class="hud-waiting hud-offline">
           <span class="hud-offline-icon">⚠</span>
           <span>Backend Offline</span>
         </div>
-      } @else if (surveillance.metrics().length === 0) {
+      } @else if (!metrics()) {
         <div class="hud-waiting">
-          <span>No processes detected</span>
+          <span>Waiting for metrics...</span>
         </div>
-      }
-
-      @for (metric of surveillance.metrics(); track trackByService($index, metric)) {
-        <div class="hud-row">
-          <div class="hud-service-name">{{ metric.service }}</div>
+      } @else {
 
+        <!-- Global PC stats -->
+        <div class="hud-row hud-global">
+          <div class="hud-service-name">Total CPU</div>
           <div class="hud-metric-block">
-            <div class="hud-metric-label">CPU</div>
             <div class="hud-bar-track">
               <div
                 class="hud-bar-fill"
-                [style.width]="cpuBarWidth(metric.cpu)"
-                [style.background]="cpuColor(metric.cpu)">
+                [style.width]="cpuBarWidth(metrics()!.cpuLoad)"
+                [style.background]="cpuColor(metrics()!.cpuLoad)">
               </div>
             </div>
-            <div class="hud-metric-value">{{ metric.cpu.toFixed(1) }}%</div>
+            <div class="hud-metric-value">{{ metrics()!.cpuLoad.toFixed(1) }}%</div>
           </div>
+        </div>
 
+        <div class="hud-row hud-global">
+          <div class="hud-service-name">Total RAM</div>
           <div class="hud-metric-block">
-            <div class="hud-metric-label">RAM</div>
-            <div class="hud-ram-value">{{ formatBytes(metric.memory) }}</div>
+            <div class="hud-bar-track">
+              <div
+                class="hud-bar-fill"
+                [style.width]="cpuBarWidth((metrics()!.memUsed / metrics()!.memTotal) * 100)"
+                [style.background]="cpuColor((metrics()!.memUsed / metrics()!.memTotal) * 100)">
+              </div>
+            </div>
+            <div class="hud-metric-value">{{ formatBytes(metrics()!.memUsed) }} / {{ formatBytes(metrics()!.memTotal) }}</div>
           </div>
         </div>
+
+        <!-- Per-service rows -->
+        @for (svc of metrics()!.services; track trackByService($index, svc)) {
+          <div class="hud-row">
+            <div class="hud-service-name">{{ svc.service }}</div>
+
+            <div class="hud-metric-block">
+              <div class="hud-metric-label">CPU</div>
+              <div class="hud-bar-track">
+                <div
+                  class="hud-bar-fill"
+                  [style.width]="cpuBarWidth(svc.cpu)"
+                  [style.background]="cpuColor(svc.cpu)">
+                </div>
+              </div>
+              <div class="hud-metric-value">{{ svc.cpu.toFixed(1) }}%</div>
+            </div>
+
+            <div class="hud-metric-block">
+              <div class="hud-metric-label">RAM</div>
+              <div class="hud-ram-value">{{ svc.online ? formatBytes(svc.memory) : '—' }}</div>
+            </div>
+          </div>
+        }
+
       }
 
-      <!-- Service status indicators -->
+      <!-- Socket connection status -->
       <div class="hud-service-status">
         <div class="hud-svc-row">
           <span class="hud-svc-label">NestJS</span>
           <span class="hud-svc-dot"
-                [class.dot-green]="surveillance.connected()"
-                [class.dot-red]="!surveillance.connected()">
+                [class.dot-green]="visionSocket.connected()"
+                [class.dot-red]="!visionSocket.connected()">
           </span>
           <span class="hud-svc-state"
-                [class.state-online]="surveillance.connected()"
-                [class.state-offline]="!surveillance.connected()">
-            {{ surveillance.connected() ? 'Connected' : 'Offline — Socket features disabled' }}
+                [class.state-online]="visionSocket.connected()"
+                [class.state-offline]="!visionSocket.connected()">
+            {{ visionSocket.connected() ? 'Connected' : 'Offline — Socket features disabled' }}
           </span>
         </div>
       </div>

+ 14 - 9
src/app/components/performance-hud/performance-hud.component.ts

@@ -2,17 +2,19 @@
  * Lego 09 / Lego 04 — Performance HUD
  *
  * Floating, draggable, collapsible overlay showing live CPU/RAM for all
- * backend processes (NestJS, n8n, Ollama). Reads from SurveillanceService
- * signals. Renders regardless of active route — mounted in app.html root.
+ * backend processes (NestJS, n8n, Ollama) plus total PC CPU/RAM.
+ * Reads from VisionSocketService signals and metrics$ observable.
  *
  * CDK DragDrop: user can reposition the HUD anywhere on screen.
  * The zombie socket feeding this HUD is never closed (Lego 06).
  */
 
-import { Component, signal } from '@angular/core';
+import { Component, signal, inject } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
-import { SurveillanceService, MonitorPayload } from '../../services/surveillance.service';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { VisionSocketService } from '../../services/vision-socket.service';
+import { ServiceStatus } from '../../core/interfaces/system-metrics.interface';
 
 @Component({
   selector: 'app-performance-hud',
@@ -22,16 +24,19 @@ import { SurveillanceService, MonitorPayload } from '../../services/surveillance
   styleUrls: ['./performance-hud.component.scss'],
 })
 export class PerformanceHudComponent {
-  collapsed = signal<boolean>(false);
+  readonly visionSocket = inject(VisionSocketService);
 
-  constructor(public surveillance: SurveillanceService) {}
+  collapsed = signal<boolean>(false);
+  metrics = toSignal(this.visionSocket.metrics$);
 
   toggle(): void {
     this.collapsed.update(v => !v);
   }
 
   formatBytes(bytes: number): string {
-    return this.surveillance.formatBytes(bytes);
+    if (bytes === 0) return '0 B';
+    const mb = bytes / (1024 * 1024);
+    return `${mb.toFixed(1)} MB`;
   }
 
   cpuBarWidth(cpu: number): string {
@@ -44,7 +49,7 @@ export class PerformanceHudComponent {
     return '#00a651';
   }
 
-  trackByService(_: number, m: MonitorPayload): string {
-    return m.service;
+  trackByService(_: number, s: ServiceStatus): string {
+    return s.service;
   }
 }

+ 16 - 0
src/app/core/interfaces/system-metrics.interface.ts

@@ -0,0 +1,16 @@
+export interface ServiceStatus {
+  service: string;
+  pid: number | null;
+  online: boolean;
+  cpu: number;    // %
+  memory: number; // bytes
+}
+
+export interface SystemMetrics {
+  cpuLoad: number;
+  memUsed: number;
+  memTotal: number;
+  uptime: number;
+  services: ServiceStatus[];
+  timestamp: string | Date;
+}

+ 0 - 71
src/app/services/surveillance.service.ts

@@ -1,71 +0,0 @@
-/**
- * Lego 09 / Lego 11 — Surveillance HUD Service
- *
- * Opens a persistent Socket.io connection to NestJS /monitor namespace.
- * Emits monitor:subscribe on connect and keeps the tunnel alive indefinitely.
- * Exposes signals for the PerformanceHUD and chatbot status indicators.
- *
- * Exposes nestStatus (ONLINE/OFFLINE) derived from socket connection state.
- */
-
-import { Injectable, signal, computed, OnDestroy } from '@angular/core';
-import { io, Socket } from 'socket.io-client';
-import { environment } from '../../environments/environment';
-
-export interface MonitorPayload {
-  service: string;
-  pid: number;
-  cpu: number;
-  memory: number;   // bytes
-  timestamp: string;
-}
-
-
-@Injectable({ providedIn: 'root' })
-export class SurveillanceService implements OnDestroy {
-
-  /** Live metrics — HUD reads this signal directly */
-  readonly metrics = signal<MonitorPayload[]>([]);
-  readonly connected = signal<boolean>(false);
-
-  /** Derived status string — consumed by Scanner and HUD */
-  readonly nestStatus = computed<'ONLINE' | 'OFFLINE'>(() =>
-    this.connected() ? 'ONLINE' : 'OFFLINE'
-  );
-
-  private socket: Socket;
-
-  constructor() {
-    // Persistent tunnel — opened on service construction (app boot)
-    this.socket = io(`${environment.nestWsUrl}/monitor`, {
-      transports: ['websocket'],
-      reconnection: true,
-      reconnectionDelay: 1000,
-      secure: true,
-      rejectUnauthorized: false,
-    });
-
-    this.socket.on('connect', () => {
-      this.connected.set(true);
-      this.socket.emit('monitor:subscribe');
-    });
-
-    this.socket.on('disconnect', () => {
-      this.connected.set(false);
-    });
-
-    this.socket.on('monitor:data', (payload: MonitorPayload[]) => {
-      this.metrics.set(payload);
-    });
-  }
-
-  ngOnDestroy() {
-    this.socket.disconnect();
-  }
-
-  formatBytes(bytes: number): string {
-    if (bytes === 0) return '0 B';
-    const mb = bytes / (1024 * 1024);
-    return `${mb.toFixed(1)} MB`;
-  }
-}

+ 14 - 1
src/app/services/vision-socket.service.ts

@@ -16,10 +16,12 @@
  * on each snap intentionally — this is the I/O overhead being measured.
  */
 
-import { Injectable, signal, OnDestroy } from '@angular/core';
+import { Injectable, signal, computed, OnDestroy } from '@angular/core';
+import { BehaviorSubject } from 'rxjs';
 import { io, Socket } from 'socket.io-client';
 import { environment } from '../../environments/environment';
 import { AnalysisResponse } from '../core/interfaces/palm-analysis.interface';
+import { SystemMetrics } from '../core/interfaces/system-metrics.interface';
 
 @Injectable({ providedIn: 'root' })
 export class VisionSocketService implements OnDestroy {
@@ -29,6 +31,13 @@ export class VisionSocketService implements OnDestroy {
   readonly lastResult = signal<AnalysisResponse | null>(null);
   readonly lastError = signal<string | null>(null);
 
+  readonly nestStatus = computed<'ONLINE' | 'OFFLINE'>(() =>
+    this.connected() ? 'ONLINE' : 'OFFLINE'
+  );
+
+  private readonly metricsSubject = new BehaviorSubject<SystemMetrics | null>(null);
+  readonly metrics$ = this.metricsSubject.asObservable();
+
   private socket: Socket;
 
   // Offscreen canvas used to capture a single frame from the video element
@@ -62,6 +71,10 @@ export class VisionSocketService implements OnDestroy {
       this.lastError.set(err.message);
       this.analyzing.set(false);
     });
+
+    this.socket.on('monitor:update', (data: SystemMetrics) => {
+      this.metricsSubject.next(data);
+    });
   }
 
   /**