Browse Source

full working update

Dr-Swopt 1 day ago
parent
commit
bc35af4de1

+ 1 - 1
dependencies/dp-ui

@@ -1 +1 @@
-Subproject commit 4da9f0b20acb765efc0fdf6c3f6ef7040daaf691
+Subproject commit 092e0a6523cb89dacb216bca7b96499d070c5c2c

+ 1 - 1
public/config/config.json

@@ -1,6 +1,6 @@
 {
 {
   "connection": {
   "connection": {
-    "uacp_ws": "http://localhost:3000/vision",
+    "uacp_ws": "wss://localhost:3000",
     "uacpEmulation": "off"
     "uacpEmulation": "off"
   }
   }
 }
 }

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

@@ -15,7 +15,7 @@
 
 
         <!-- Message list -->
         <!-- Message list -->
         <div class="message-list" #messageList>
         <div class="message-list" #messageList>
-          @for (msg of chatSocket.messages(); track msg.timestamp) {
+          @for (msg of messages(); track msg.timestamp) {
             <div class="message" [class]="'message--' + msg.role">
             <div class="message" [class]="'message--' + msg.role">
               <div class="message-bubble">
               <div class="message-bubble">
                 <p class="message-text">{{ msg.text }}</p>
                 <p class="message-text">{{ msg.text }}</p>
@@ -29,7 +29,7 @@
             </div>
             </div>
           }
           }
 
 
-          @if (chatSocket.loading()) {
+          @if (loading()) {
             <div class="message message--bot">
             <div class="message message--bot">
               <div class="message-bubble">
               <div class="message-bubble">
                 <div class="thinking-dots">
                 <div class="thinking-dots">
@@ -56,13 +56,13 @@
             (keydown)="onEnter($event)"
             (keydown)="onEnter($event)"
             placeholder="Ask about palm oil ripeness, site data, operational reports..."
             placeholder="Ask about palm oil ripeness, site data, operational reports..."
             rows="2"
             rows="2"
-            [disabled]="chatSocket.loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
+            [disabled]="loading() || sending() || visionSocket.nestStatus() === 'OFFLINE'">
           </textarea>
           </textarea>
           <button
           <button
             class="btn btn-primary send-btn"
             class="btn btn-primary send-btn"
             (click)="sendMessage()"
             (click)="sendMessage()"
-            [disabled]="!inputText.trim() || chatSocket.loading() || chatSocket.sending() || visionSocket.nestStatus() === 'OFFLINE'">
-            {{ chatSocket.sending() ? '...' : 'Send' }}
+            [disabled]="!inputText.trim() || loading() || sending() || visionSocket.nestStatus() === 'OFFLINE'">
+            {{ sending() ? '...' : 'Send' }}
           </button>
           </button>
         </div>
         </div>
 
 

+ 67 - 18
src/app/components/chatbot/chatbot.component.ts

@@ -1,22 +1,26 @@
-/**
- * Lego 03 / Lego 14 — n8n RAG Chatbot Tab (Intelligence)
- *
- * Thin view layer — all chat state (messages, loading) lives in ChatSocketService
- * so it survives tab navigation. This component only owns scroll behaviour.
- */
-
 import {
 import {
   Component,
   Component,
   ViewChild,
   ViewChild,
   ElementRef,
   ElementRef,
   AfterViewChecked,
   AfterViewChecked,
-  OnInit,
+  signal,
   inject,
   inject,
 } from '@angular/core';
 } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { FormsModule } from '@angular/forms';
-import { VisionSocketService } from '../../services/vision-socket.service';
-import { ChatSocketService } from '../../services/chat-socket.service';
+import { Store } from '@ngxs/store';
+import { firstValueFrom } from 'rxjs';
+import { BaseComponent } from 'angularlib/base.component';
+import { DpService } from 'dp-ui/dp.service';
+import { FisAppMessage } from 'dp-ui/fisappmessage/apprequestmessagetype';
+import { NgxSocketService } from 'dp-ui/socket/ngxSocket.service';
+
+export interface ChatMessage {
+  role: 'user' | 'bot';
+  text: string;
+  timestamp: Date;
+  durationMs?: number;
+}
 
 
 @Component({
 @Component({
   selector: 'app-chatbot',
   selector: 'app-chatbot',
@@ -25,16 +29,25 @@ import { ChatSocketService } from '../../services/chat-socket.service';
   templateUrl: './chatbot.component.html',
   templateUrl: './chatbot.component.html',
   styleUrls: ['./chatbot.component.scss'],
   styleUrls: ['./chatbot.component.scss'],
 })
 })
-export class ChatbotComponent implements OnInit, AfterViewChecked {
-  readonly chatSocket = inject(ChatSocketService);
-  readonly visionSocket = inject(VisionSocketService);
+export class ChatbotComponent extends BaseComponent implements AfterViewChecked {
+  private readonly dpService = inject(DpService);
+  private readonly _ngxSocket = inject(NgxSocketService);
+  readonly visionSocket = { nestStatus: () => this._ngxSocket.status === 'online' ? 'ONLINE' as const : 'OFFLINE' as const };
 
 
   @ViewChild('messageList') messageListRef!: ElementRef<HTMLDivElement>;
   @ViewChild('messageList') messageListRef!: ElementRef<HTMLDivElement>;
 
 
+  readonly messages = signal<ChatMessage[]>([]);
+  readonly loading = signal(false);
+  readonly sending = signal(false);
   inputText = '';
   inputText = '';
   private shouldScrollToBottom = false;
   private shouldScrollToBottom = false;
 
 
-  ngOnInit(): void {
+  constructor() {
+    super(inject(Store));
+  }
+
+  override ngOnInit(): void {
+    super.ngOnInit();
     this.shouldScrollToBottom = true;
     this.shouldScrollToBottom = true;
   }
   }
 
 
@@ -47,11 +60,31 @@ export class ChatbotComponent implements OnInit, AfterViewChecked {
 
 
   async sendMessage(): Promise<void> {
   async sendMessage(): Promise<void> {
     const text = this.inputText.trim();
     const text = this.inputText.trim();
-    if (!text || this.chatSocket.loading() || this.chatSocket.sending()) return;
+    if (!text || this.loading() || this.sending()) return;
     this.inputText = '';
     this.inputText = '';
     this.shouldScrollToBottom = true;
     this.shouldScrollToBottom = true;
-    await this.chatSocket.sendMessage(text);
-    this.shouldScrollToBottom = true;
+    this.messages.update(msgs => [...msgs, { role: 'user', text, timestamp: new Date() }]);
+    this.sending.set(true);
+
+    const start = performance.now();
+    try {
+      const response = await firstValueFrom(
+        this.dpService.stream(this.buildFisMessage(text))
+      );
+      const durationMs = Math.round(performance.now() - start);
+      const content =
+        response?.output ?? response?.answer ?? response?.response ?? response?.text ?? JSON.stringify(response);
+      this.messages.update(msgs => [...msgs, { role: 'bot', text: content, timestamp: new Date(), durationMs }]);
+    } catch (err: any) {
+      this.messages.update(msgs => [...msgs, {
+        role: 'bot',
+        text: `Error: ${err?.message ?? 'Failed to send message'}`,
+        timestamp: new Date(),
+      }]);
+    } finally {
+      this.sending.set(false);
+      this.shouldScrollToBottom = true;
+    }
   }
   }
 
 
   onEnter(event: KeyboardEvent): void {
   onEnter(event: KeyboardEvent): void {
@@ -62,7 +95,23 @@ export class ChatbotComponent implements OnInit, AfterViewChecked {
   }
   }
 
 
   onClearChat(): void {
   onClearChat(): void {
-    this.chatSocket.clearChat();
+    this.messages.set([]);
+  }
+
+  private buildFisMessage(text: string): FisAppMessage {
+    return {
+      header: {
+        messageType: 'Command' as any,
+        messageID: crypto.randomUUID(),
+        messageName: 'send',
+        dateCreated: new Date().toISOString(),
+        isAggregate: false,
+        serviceId: 'Chat',
+        messageProducerInformation: { producerName: 'palm-oil-ai' } as any,
+        security: { ucpId: 'palm-oil-ai' },
+      },
+      data: { message: text },
+    };
   }
   }
 
 
   private scrollToBottom(): void {
   private scrollToBottom(): void {

+ 97 - 44
src/app/components/history/history.component.ts

@@ -1,9 +1,12 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, inject } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { CommonModule } from '@angular/common';
+import { Store } from '@ngxs/store';
+import { firstValueFrom } from 'rxjs';
+import { BaseComponent } from 'angularlib/base.component';
+import { DpService } from 'dp-ui/dp.service';
+import { FisAppMessage } from 'dp-ui/fisappmessage/apprequestmessagetype';
 import { LocalHistoryService } from '../../services/local-history.service';
 import { LocalHistoryService } from '../../services/local-history.service';
-import { RemoteInferenceService } from '../../core/services/remote-inference.service';
-import { VisionSocketService } from '../../services/vision-socket.service';
-import { environment } from '../../../environments/environment';
+import { NgxSocketService } from 'dp-ui/socket/ngxSocket.service';
 
 
 const GRADE_COLORS: Record<string, string> = {
 const GRADE_COLORS: Record<string, string> = {
   'Empty_Bunch': '#6C757D', 'Underripe': '#F9A825', 'Abnormal': '#DC3545',
   'Empty_Bunch': '#6C757D', 'Underripe': '#F9A825', 'Abnormal': '#DC3545',
@@ -12,7 +15,7 @@ const GRADE_COLORS: Record<string, string> = {
 
 
 export interface BatchSessionGroup {
 export interface BatchSessionGroup {
   batch_id: string;
   batch_id: string;
-  label: string;         // e.g. "Batch Session · April 20"
+  label: string;
   count: number;
   count: number;
   timestamp: string;
   timestamp: string;
   records: any[];
   records: any[];
@@ -26,54 +29,61 @@ export interface BatchSessionGroup {
   templateUrl: './history.component.html',
   templateUrl: './history.component.html',
   styleUrls: ['./history.component.scss']
   styleUrls: ['./history.component.scss']
 })
 })
-export class HistoryComponent implements OnInit {
+export class HistoryComponent extends BaseComponent {
+  private readonly dpService = inject(DpService);
+  private readonly localHistory = inject(LocalHistoryService);
+  private readonly _ngxSocket = inject(NgxSocketService);
+  readonly visionSocket = { nestStatus: () => this._ngxSocket.status === 'online' ? 'ONLINE' as const : 'OFFLINE' as const };
+
   localHistoryRecords: any[] = [];
   localHistoryRecords: any[] = [];
   remoteHistoryRecords: any[] = [];
   remoteHistoryRecords: any[] = [];
-  /** Remote records grouped by batch_id for the Industrial Cloud tab */
   batchGroups: BatchSessionGroup[] = [];
   batchGroups: BatchSessionGroup[] = [];
 
 
   viewMode: 'local' | 'remote' = 'local';
   viewMode: 'local' | 'remote' = 'local';
-  loading = true;
+  loading = false;
   expandedId: string | null = null;
   expandedId: string | null = null;
 
 
-  constructor(
-    private localHistory: LocalHistoryService,
-    private remoteInference: RemoteInferenceService,
-    public visionSocket: VisionSocketService,
-  ) {}
+  constructor() {
+    super(inject(Store));
+  }
 
 
-  ngOnInit(): void {
+  override ngOnInit(): void {
+    super.ngOnInit();
     this.loadLocalHistory();
     this.loadLocalHistory();
   }
   }
 
 
+  override ngOnDestroy(): void {
+    this.batchGroups.forEach(g => g.records.forEach(r => { r.imageData = null; }));
+    super.ngOnDestroy();
+  }
+
   loadLocalHistory(): void {
   loadLocalHistory(): void {
     this.loading = true;
     this.loading = true;
     this.localHistoryRecords = this.localHistory.getRecords();
     this.localHistoryRecords = this.localHistory.getRecords();
     this.loading = false;
     this.loading = false;
   }
   }
 
 
-  loadRemoteHistory(): void {
+  async loadRemoteHistory(): Promise<void> {
     this.loading = true;
     this.loading = true;
-    this.remoteInference.getHistory().subscribe({
-      next: (data) => {
-        this.remoteHistoryRecords = data.map(record => ({
-          ...record,
-          timestamp: new Date(record.created_at).toLocaleString(),
-          engine: 'API AI',
-          isNormalized: false,
-          imageData: `${environment.apiUrl}/palm-oil/archive/${record.archive_id}`
-        }));
-        this.batchGroups = this.groupByBatch(this.remoteHistoryRecords);
-        this.loading = false;
-      },
-      error: (err) => {
-        console.error('Failed to load remote history:', err);
-        this.loading = false;
-      }
-    });
+    try {
+      const data: any = await firstValueFrom(
+        this.dpService.stream(this.buildFisMessage('History', 'getAll', {}))
+      );
+      const records = Array.isArray(data) ? data : [];
+      this.remoteHistoryRecords = records.map((record: any) => ({
+        ...record,
+        timestamp: new Date(record.created_at).toLocaleString(),
+        engine: 'Socket AI',
+        imageData: null,
+      }));
+      this.batchGroups = this.groupByBatch(this.remoteHistoryRecords);
+    } catch (err) {
+      console.error('Failed to load remote history:', err);
+    } finally {
+      this.loading = false;
+    }
   }
   }
 
 
-  /** Group remote records by batch_id. Records without a batch_id each form a singleton group. */
   private groupByBatch(records: any[]): BatchSessionGroup[] {
   private groupByBatch(records: any[]): BatchSessionGroup[] {
     const map = new Map<string, any[]>();
     const map = new Map<string, any[]>();
     records.forEach(r => {
     records.forEach(r => {
@@ -110,8 +120,27 @@ export class HistoryComponent implements OnInit {
     }
     }
   }
   }
 
 
-  toggleBatchGroup(group: BatchSessionGroup): void {
+  async toggleBatchGroup(group: BatchSessionGroup): Promise<void> {
     group.expanded = !group.expanded;
     group.expanded = !group.expanded;
+    if (group.expanded) {
+      await this.loadImagesForGroup(group);
+    }
+  }
+
+  private async loadImagesForGroup(group: BatchSessionGroup): Promise<void> {
+    for (const record of group.records) {
+      // null = not fetched; false = failed; string = loaded — skip anything already attempted
+      if (record.imageData !== null) continue;
+      if (!record.archive_id) { record.imageData = false; continue; }
+      try {
+        const res: any = await firstValueFrom(
+          this.dpService.stream(this.buildFisMessage('PalmHistory', 'GetImage', { archiveId: record.archive_id }))
+        );
+        record.imageData = res?.image_data ?? false;
+      } catch {
+        record.imageData = false;
+      }
+    }
   }
   }
 
 
   // ── Delete actions ────────────────────────────────────────────────────────
   // ── Delete actions ────────────────────────────────────────────────────────
@@ -129,20 +158,30 @@ export class HistoryComponent implements OnInit {
     this.loadLocalHistory();
     this.loadLocalHistory();
   }
   }
 
 
-  deleteRemoteRecord(record: any, event: Event): void {
+  async deleteRemoteRecord(record: any, event: Event): Promise<void> {
     event.stopPropagation();
     event.stopPropagation();
     if (!record.archive_id) return;
     if (!record.archive_id) return;
-    this.remoteInference.deleteRecord(record.archive_id).subscribe(() => {
-      this.loadRemoteHistory();
-    });
+    try {
+      await firstValueFrom(
+        this.dpService.stream(this.buildFisMessage('History', 'delete', { archiveId: record.archive_id }))
+      );
+      await this.loadRemoteHistory();
+    } catch (err) {
+      console.error('Failed to delete record:', err);
+    }
   }
   }
 
 
-  clearRemoteHistory(event: Event): void {
+  async clearRemoteHistory(event: Event): Promise<void> {
     event.stopPropagation();
     event.stopPropagation();
     if (!confirm('Delete ALL industrial cloud records from the server? This also removes archived images from disk.')) return;
     if (!confirm('Delete ALL industrial cloud records from the server? This also removes archived images from disk.')) return;
-    this.remoteInference.clearAll().subscribe(() => {
-      this.loadRemoteHistory();
-    });
+    try {
+      await firstValueFrom(
+        this.dpService.stream(this.buildFisMessage('History', 'clearAll', {}))
+      );
+      await this.loadRemoteHistory();
+    } catch (err) {
+      console.error('Failed to clear history:', err);
+    }
   }
   }
 
 
   get currentHistory(): any[] {
   get currentHistory(): any[] {
@@ -186,12 +225,10 @@ export class HistoryComponent implements OnInit {
     };
     };
   }
   }
 
 
-  /** CSS border color for a detection box by ripeness class */
   getDetectionColor(det: any): string {
   getDetectionColor(det: any): string {
     return GRADE_COLORS[det.class] ?? '#00A651';
     return GRADE_COLORS[det.class] ?? '#00A651';
   }
   }
 
 
-  /** Aggregate industrial_summary for a batch group into display badges */
   getGroupSummary(group: BatchSessionGroup): string[] {
   getGroupSummary(group: BatchSessionGroup): string[] {
     const totals: Record<string, number> = {};
     const totals: Record<string, number> = {};
     group.records.forEach(r => {
     group.records.forEach(r => {
@@ -206,4 +243,20 @@ export class HistoryComponent implements OnInit {
       .filter(([, n]) => n > 0)
       .filter(([, n]) => n > 0)
       .map(([cls, n]) => `${cls}: ${n}`);
       .map(([cls, n]) => `${cls}: ${n}`);
   }
   }
+
+  private buildFisMessage(serviceId: string, messageName: string, data: any): FisAppMessage {
+    return {
+      header: {
+        messageType: 'Command' as any,
+        messageID: crypto.randomUUID(),
+        messageName,
+        dateCreated: new Date().toISOString(),
+        isAggregate: false,
+        serviceId,
+        messageProducerInformation: { producerName: 'palm-oil-ai' } as any,
+        security: { ucpId: 'palm-oil-ai' },
+      },
+      data,
+    };
+  }
 }
 }

+ 10 - 10
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 -->
   <!-- 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'">
   <button class="hud-toggle" cdkDragHandle (click)="toggle()" [title]="collapsed() ? 'Expand HUD' : 'Collapse HUD'">
     <span class="hud-toggle-icon">{{ collapsed() ? '📊' : '✕' }}</span>
     <span class="hud-toggle-icon">{{ collapsed() ? '📊' : '✕' }}</span>
-    <span class="hud-status-dot" [class.connected]="visionSocket.connected()"></span>
+    <span class="hud-status-dot" [class.connected]="connected()"></span>
   </button>
   </button>
 
 
   <!-- Expanded panel -->
   <!-- Expanded panel -->
@@ -13,17 +13,17 @@
         <span class="hud-drag-grip">⠿</span>
         <span class="hud-drag-grip">⠿</span>
         <span class="hud-title">SYSTEM MONITOR</span>
         <span class="hud-title">SYSTEM MONITOR</span>
         <div class="hud-header-leds">
         <div class="hud-header-leds">
-          <span class="hud-led-group" title="NestJS Socket: {{ visionSocket.connected() ? 'Connected' : 'Offline' }}">
+          <span class="hud-led-group" title="NestJS Socket: {{ connected() ? 'Connected' : 'Offline' }}">
             <span class="hud-led-dot"
             <span class="hud-led-dot"
-                  [class.led-green]="visionSocket.connected()"
-                  [class.led-red]="!visionSocket.connected()">
+                  [class.led-green]="connected()"
+                  [class.led-red]="!connected()">
             </span>
             </span>
             <span class="hud-led-label">Nest</span>
             <span class="hud-led-label">Nest</span>
           </span>
           </span>
         </div>
         </div>
       </div>
       </div>
 
 
-      @if (!visionSocket.connected()) {
+      @if (!connected()) {
         <div class="hud-waiting hud-offline">
         <div class="hud-waiting hud-offline">
           <span class="hud-offline-icon">⚠</span>
           <span class="hud-offline-icon">⚠</span>
           <span>Backend Offline</span>
           <span>Backend Offline</span>
@@ -109,13 +109,13 @@
         <div class="hud-svc-row">
         <div class="hud-svc-row">
           <span class="hud-svc-label">NestJS</span>
           <span class="hud-svc-label">NestJS</span>
           <span class="hud-svc-dot"
           <span class="hud-svc-dot"
-                [class.dot-green]="visionSocket.connected()"
-                [class.dot-red]="!visionSocket.connected()">
+                [class.dot-green]="connected()"
+                [class.dot-red]="!connected()">
           </span>
           </span>
           <span class="hud-svc-state"
           <span class="hud-svc-state"
-                [class.state-online]="visionSocket.connected()"
-                [class.state-offline]="!visionSocket.connected()">
-            {{ visionSocket.connected() ? 'Connected' : 'Offline — Socket features disabled' }}
+                [class.state-online]="connected()"
+                [class.state-offline]="!connected()">
+            {{ connected() ? 'Connected' : 'Offline — Socket features disabled' }}
           </span>
           </span>
         </div>
         </div>
       </div>
       </div>

+ 34 - 21
src/app/components/performance-hud/performance-hud.component.ts

@@ -1,25 +1,17 @@
-/**
- * Lego 09 / Lego 04 — Performance HUD
- *
- * Floating, draggable, collapsible overlay showing live CPU/RAM for all
- * 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, inject } from '@angular/core';
 import { Component, signal, inject } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { CommonModule } from '@angular/common';
 import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
 import { CdkDrag, CdkDragHandle } from '@angular/cdk/drag-drop';
-import { toSignal } from '@angular/core/rxjs-interop';
-import { VisionSocketService } from '../../services/vision-socket.service';
-import { ServiceStatus, ServiceStatusType } from '../../core/interfaces/system-metrics.interface';
+import { Store } from '@ngxs/store';
+import { interval } from 'rxjs';
+import { filter } from 'rxjs/operators';
+import { BaseComponent, untilDestroy } from 'angularlib/base.component';
+import { NgxSocketService } from 'dp-ui/socket/ngxSocket.service';
+import { SystemMetrics, ServiceStatus, ServiceStatusType } from '../../core/interfaces/system-metrics.interface';
 
 
 export interface StatusConfig {
 export interface StatusConfig {
-  color:   string;  // CSS color for the status pill and bar
-  label:   string;  // Human-readable state label
-  showBar: boolean; // Whether to render the CPU progress bar at all
+  color:   string;
+  label:   string;
+  showBar: boolean;
 }
 }
 
 
 const STATUS_CONFIG: Record<ServiceStatusType, StatusConfig> = {
 const STATUS_CONFIG: Record<ServiceStatusType, StatusConfig> = {
@@ -35,17 +27,38 @@ const STATUS_CONFIG: Record<ServiceStatusType, StatusConfig> = {
   templateUrl: './performance-hud.component.html',
   templateUrl: './performance-hud.component.html',
   styleUrls: ['./performance-hud.component.scss'],
   styleUrls: ['./performance-hud.component.scss'],
 })
 })
-export class PerformanceHudComponent {
-  readonly visionSocket = inject(VisionSocketService);
+export class PerformanceHudComponent extends BaseComponent {
+  private readonly ngxSocket = inject(NgxSocketService);
 
 
+  readonly connected = signal(false);
+  readonly metrics = signal<SystemMetrics | null>(null);
   collapsed = signal<boolean>(false);
   collapsed = signal<boolean>(false);
-  metrics = toSignal(this.visionSocket.metrics$);
+
+  constructor() {
+    super(inject(Store));
+  }
+
+  override ngOnInit(): void {
+    super.ngOnInit();
+
+    // Track socket connection status via public status property
+    interval(1000).pipe(untilDestroy(this)).subscribe(() => {
+      this.connected.set(this.ngxSocket.status === 'online');
+    });
+
+    // Intercept Surveillance metrics frames from the shared multiplexed stream
+    this.ngxSocket.responses$.pipe(
+      untilDestroy(this),
+      filter((msg: any) => msg?.serviceId === 'Surveillance' && msg?.operation === 'metricsUpdate'),
+    ).subscribe((msg: any) => {
+      this.metrics.set(msg.payload as SystemMetrics);
+    });
+  }
 
 
   toggle(): void {
   toggle(): void {
     this.collapsed.update(v => !v);
     this.collapsed.update(v => !v);
   }
   }
 
 
-  /** Returns the full rendering config for a service status — no branching in the template. */
   statusConfig(status: ServiceStatusType): StatusConfig {
   statusConfig(status: ServiceStatusType): StatusConfig {
     return STATUS_CONFIG[status];
     return STATUS_CONFIG[status];
   }
   }

+ 0 - 12
src/app/core/services/remote-inference.service.ts

@@ -15,16 +15,4 @@ export class RemoteInferenceService {
     formData.append('image', blob, 'capture.jpg');
     formData.append('image', blob, 'capture.jpg');
     return this.http.post<AnalysisResponse>(`${this.base}/palm-oil/analyze`, formData);
     return this.http.post<AnalysisResponse>(`${this.base}/palm-oil/analyze`, formData);
   }
   }
-
-  getHistory(): Observable<any[]> {
-    return this.http.get<any[]>(`${this.base}/palm-oil/history`);
-  }
-
-  deleteRecord(archiveId: string): Observable<any> {
-    return this.http.delete<any>(`${this.base}/palm-oil/history/${archiveId}`);
-  }
-
-  clearAll(): Observable<any> {
-    return this.http.delete<any>(`${this.base}/palm-oil/history`);
-  }
 }
 }

+ 0 - 67
src/app/services/chat-socket.service.ts

@@ -1,67 +0,0 @@
-import { Injectable, signal } from '@angular/core';
-import { firstValueFrom } from 'rxjs';
-import { DpService } from 'dp-ui/dp.service';
-import { FisAppMessage } from 'dp-ui/fisappmessage/apprequestmessagetype';
-
-export interface ChatMessage {
-  role: 'user' | 'bot';
-  text: string;
-  timestamp: Date;
-  durationMs?: number;
-}
-
-@Injectable({ providedIn: 'root' })
-export class ChatSocketService {
-  readonly messages = signal<ChatMessage[]>([]);
-  readonly loading = signal<boolean>(false);
-  readonly sending = signal<boolean>(false);
-
-  constructor(private dpService: DpService) {}
-
-  private buildMessage(serviceId: string, operation: string, data: unknown): FisAppMessage {
-    return {
-      header: {
-        messageType: 'Command' as any,
-        messageID: crypto.randomUUID(),
-        messageName: operation,
-        dateCreated: new Date().toISOString(),
-        isAggregate: false,
-        serviceId,
-        messageProducerInformation: { producerName: 'palm-oil-ai' } as any,
-        security: { ucpId: 'palm-oil-ai' },
-      },
-      data,
-    };
-  }
-
-  async sendMessage(text: string): Promise<void> {
-    const userMsg: ChatMessage = { role: 'user', text, timestamp: new Date() };
-    this.messages.update(msgs => [...msgs, userMsg]);
-    this.sending.set(true);
-
-    const start = performance.now();
-    try {
-      const response = await firstValueFrom(
-        this.dpService.stream(this.buildMessage('Chat', 'send', { message: text }))
-      );
-      const durationMs = Math.round(performance.now() - start);
-      const content =
-        response?.output ?? response?.answer ?? response?.response ?? response?.text ?? JSON.stringify(response);
-      const botMsg: ChatMessage = { role: 'bot', text: content, timestamp: new Date(), durationMs };
-      this.messages.update(msgs => [...msgs, botMsg]);
-    } catch (err: any) {
-      const errorMsg: ChatMessage = {
-        role: 'bot',
-        text: `Error: ${err?.message ?? 'Failed to send message'}`,
-        timestamp: new Date(),
-      };
-      this.messages.update(msgs => [...msgs, errorMsg]);
-    } finally {
-      this.sending.set(false);
-    }
-  }
-
-  clearChat(): void {
-    this.messages.set([]);
-  }
-}

+ 0 - 28
src/app/services/vision-socket.service.ts

@@ -1,28 +0,0 @@
-import { Injectable, signal, computed } from '@angular/core';
-import { Observable, filter, map, Subject } from 'rxjs';
-import { interval } from 'rxjs';
-import { NgxSocketService } from 'dp-ui/socket/ngxSocket.service';
-import { SystemMetrics } from '../core/interfaces/system-metrics.interface';
-
-@Injectable({ providedIn: 'root' })
-export class VisionSocketService {
-  private readonly _nestStatus = signal<'ONLINE' | 'OFFLINE'>('OFFLINE');
-
-  readonly nestStatus = this._nestStatus.asReadonly();
-  readonly connected = computed(() => this._nestStatus() === 'ONLINE');
-  readonly metrics$: Observable<SystemMetrics>;
-
-  constructor(private ngxSocket: NgxSocketService) {
-    interval(1000).subscribe(() => {
-      const live = ngxSocket.status === 'online' ? 'ONLINE' : 'OFFLINE';
-      if (this._nestStatus() !== live) this._nestStatus.set(live);
-    });
-
-    // responseSubject is private in the submodule — access via cast to avoid modifying protected code
-    const allResponses$ = (ngxSocket as any).responseSubject as Subject<any>;
-    this.metrics$ = allResponses$.pipe(
-      filter((msg: any) => msg?.serviceId === 'Surveillance' && msg?.operation === 'metricsUpdate'),
-      map((msg: any) => msg.payload as SystemMetrics),
-    );
-  }
-}