Переглянути джерело

refactor: update n8n status indicators and remove unused webhook configuration

Dr-Swopt 6 днів тому
батько
коміт
e0e1779577

+ 20 - 57
src/app/components/chatbot/chatbot.component.html

@@ -19,38 +19,24 @@
           <span>n8n → Angular Response</span>
         </div>
 
-        <!-- Live webhook URL display -->
-        <div class="sidebar-endpoint">
-          <div class="endpoint-label">Webhook Target</div>
-          <div class="endpoint-url">{{ webhookUrl() }}</div>
-        </div>
-
-        <!-- n8n status indicator -->
+        <!-- Agent readiness indicator (driven by NestJS webhook probe) -->
         <div class="n8n-status-row">
           <span class="n8n-status-dot"
-                [class.dot-green]="surveillance.n8nStatus() === 'online'"
-                [class.dot-yellow]="surveillance.n8nStatus() === 'untested'"
-                [class.dot-red]="surveillance.n8nStatus() === 'offline'">
+                [class.dot-green]="surveillance.n8nStatus() === 'ready'"
+                [class.dot-yellow]="surveillance.n8nStatus() === 'checking'"
+                [class.dot-red]="surveillance.n8nStatus() === 'not-ready'">
           </span>
           <span class="n8n-status-label"
-                [class.state-online]="surveillance.n8nStatus() === 'online'"
-                [class.state-warn]="surveillance.n8nStatus() === 'untested'"
-                [class.state-offline]="surveillance.n8nStatus() === 'offline'">
-            n8n:
-            {{ surveillance.n8nStatus() === 'online' ? 'Reachable'
-             : surveillance.n8nStatus() === 'offline' ? 'Unreachable'
-             : 'Untested' }}
+                [class.state-online]="surveillance.n8nStatus() === 'ready'"
+                [class.state-warn]="surveillance.n8nStatus() === 'checking'"
+                [class.state-offline]="surveillance.n8nStatus() === 'not-ready'">
+            Agent:
+            {{ surveillance.n8nStatus() === 'ready' ? 'Ready'
+             : surveillance.n8nStatus() === 'not-ready' ? 'Not Ready'
+             : 'Checking...' }}
           </span>
         </div>
 
-        <button class="btn btn-outline sidebar-clear"
-                (click)="testConnection()"
-                [disabled]="testingConnection() || !webhookUrl()">
-          {{ testingConnection() ? 'Testing...' : '🔍 Test Connection' }}
-        </button>
-        <button class="btn btn-outline sidebar-clear" (click)="toggleWebhookConfig()">
-          ⚙ Change URL
-        </button>
         <button class="btn btn-outline sidebar-clear" (click)="clearChat()">
           Clear Chat
         </button>
@@ -63,36 +49,6 @@
           <span class="chat-lego">Lego 03 · 06</span>
         </div>
 
-        <!-- Inline webhook URL reconfigure overlay -->
-        @if (showWebhookConfig()) {
-          <div class="webhook-config">
-            <div class="webhook-config__label-row">
-              <span class="webhook-config__label">n8n Webhook URL</span>
-              <span class="webhook-config__status-dot"
-                    [class.dot-green]="surveillance.n8nStatus() === 'online'"
-                    [class.dot-yellow]="surveillance.n8nStatus() === 'untested'"
-                    [class.dot-red]="surveillance.n8nStatus() === 'offline'">
-              </span>
-            </div>
-            <div class="webhook-config__row">
-              <input
-                class="webhook-config__input"
-                type="text"
-                [(ngModel)]="webhookInputDraft"
-                placeholder="http://localhost:5678/webhook/..."
-              />
-              <button class="btn btn-outline webhook-config__verify"
-                      (click)="testConnection()"
-                      [disabled]="testingConnection() || !webhookUrl()">
-                {{ testingConnection() ? '...' : '🔍 Verify' }}
-              </button>
-              <button class="btn btn-primary webhook-config__save" (click)="saveWebhookUrl()">Save</button>
-              <button class="btn btn-outline webhook-config__cancel" (click)="toggleWebhookConfig()">✕</button>
-            </div>
-            <div class="webhook-config__hint">Saved to localStorage. Survives page refresh.</div>
-          </div>
-        }
-
         <!-- Message list -->
         <div class="message-list" #messageList>
           @for (msg of messages(); track msg.timestamp) {
@@ -128,6 +84,13 @@
           </div>
         }
 
+        <!-- Agent not ready banner — NestJS cannot reach n8n webhook -->
+        @if (surveillance.nestStatus() === 'ONLINE' && surveillance.n8nStatus() === 'not-ready') {
+          <div class="n8n-offline-banner">
+            🟡 LLM agent not ready — NestJS cannot reach the n8n webhook. Check that n8n is running and the workflow is active.
+          </div>
+        }
+
         <!-- Input row -->
         <div class="chat-input-row">
           <textarea
@@ -136,12 +99,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() || surveillance.nestStatus() === 'OFFLINE' || surveillance.n8nStatus() !== 'ready'">
           </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() || surveillance.nestStatus() === 'OFFLINE' || surveillance.n8nStatus() !== 'ready'">
             {{ chatSocket.sending() ? '...' : 'Send' }}
           </button>
         </div>

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

@@ -1,13 +1,9 @@
 /**
- * Lego 03 / Lego 06 / Lego 14 — n8n RAG Chatbot Tab (Intelligence)
+ * Lego 03 / Lego 14 — n8n RAG Chatbot Tab (Intelligence)
  *
  * Chat messages are sent to NestJS via Socket.io (chat:send).
  * NestJS proxies them server-to-server to n8n — no CORS constraint.
  * The n8n webhook URL lives in NestJS .env (N8N_WEBHOOK_URL).
- *
- * Lego 06: The optional webhook URL in localStorage is kept only for the
- * SurveillanceService pulse-check ("Test Connection"). It is no longer
- * required for sending messages — the proxy handles routing.
  */
 
 import {
@@ -22,8 +18,6 @@ import { FormsModule } from '@angular/forms';
 import { SurveillanceService } from '../../services/surveillance.service';
 import { ChatSocketService } from '../../services/chat-socket.service';
 
-const WEBHOOK_STORAGE_KEY = 'n8n_webhook_url';
-
 export interface ChatMessage {
   role: 'user' | 'bot' | 'error';
   text: string;
@@ -52,22 +46,12 @@ export class ChatbotComponent implements AfterViewChecked {
   inputText = '';
   loading = signal<boolean>(false);
 
-  // Lego 06 — Optional URL stored locally for pulse-check only (not used for sending)
-  webhookUrl = signal<string>(localStorage.getItem(WEBHOOK_STORAGE_KEY) ?? '');
-  webhookInputDraft = '';
-  showWebhookConfig = signal<boolean>(false);
-
   private shouldScrollToBottom = false;
 
-  /** True while the "Test Connection" OPTIONS request is in flight */
-  testingConnection = signal<boolean>(false);
-
   constructor(
     public chatSocket: ChatSocketService,
     public surveillance: SurveillanceService,
-  ) {
-    this.webhookInputDraft = this.webhookUrl();
-  }
+  ) {}
 
   ngAfterViewChecked(): void {
     if (this.shouldScrollToBottom) {
@@ -76,15 +60,6 @@ export class ChatbotComponent implements AfterViewChecked {
     }
   }
 
-  /** Fire a lightweight OPTIONS request to check if the n8n URL is alive */
-  async testConnection(): Promise<void> {
-    const url = this.webhookUrl().trim();
-    if (!url) return;
-    this.testingConnection.set(true);
-    await this.surveillance.checkN8nStatus(url);
-    this.testingConnection.set(false);
-  }
-
   async sendMessage(): Promise<void> {
     const text = this.inputText.trim();
     if (!text || this.loading() || this.chatSocket.sending()) return;
@@ -137,22 +112,6 @@ export class ChatbotComponent implements AfterViewChecked {
     ]);
   }
 
-  // Lego 06 — Webhook URL config (localStorage persistence)
-  toggleWebhookConfig(): void {
-    this.webhookInputDraft = this.webhookUrl();
-    this.showWebhookConfig.update(v => !v);
-  }
-
-  saveWebhookUrl(): void {
-    const url = this.webhookInputDraft.trim();
-    if (!url) return;
-    localStorage.setItem(WEBHOOK_STORAGE_KEY, url);
-    this.webhookUrl.set(url);
-    this.showWebhookConfig.set(false);
-    // Immediately pulse-check the saved URL
-    this.surveillance.checkN8nStatus(url);
-  }
-
   private pushMessage(msg: ChatMessage): void {
     this.messages.update(msgs => [...msgs, msg]);
     this.shouldScrollToBottom = true;

+ 13 - 13
src/app/components/performance-hud/performance-hud.component.html

@@ -21,11 +21,11 @@
             </span>
             <span class="hud-led-label">Nest</span>
           </span>
-          <span class="hud-led-group" title="n8n Webhook: {{ surveillance.n8nStatus() }}">
+          <span class="hud-led-group" title="n8n Agent: {{ surveillance.n8nStatus() }}">
             <span class="hud-led-dot"
-                  [class.led-green]="surveillance.n8nStatus() === 'online'"
-                  [class.led-yellow]="surveillance.n8nStatus() === 'untested'"
-                  [class.led-red]="surveillance.n8nStatus() === 'offline'">
+                  [class.led-green]="surveillance.n8nStatus() === 'ready'"
+                  [class.led-yellow]="surveillance.n8nStatus() === 'checking'"
+                  [class.led-red]="surveillance.n8nStatus() === 'not-ready'">
             </span>
             <span class="hud-led-label">n8n</span>
           </span>
@@ -84,17 +84,17 @@
         <div class="hud-svc-row">
           <span class="hud-svc-label">n8n</span>
           <span class="hud-svc-dot"
-                [class.dot-green]="surveillance.n8nStatus() === 'online'"
-                [class.dot-yellow]="surveillance.n8nStatus() === 'untested'"
-                [class.dot-red]="surveillance.n8nStatus() === 'offline'">
+                [class.dot-green]="surveillance.n8nStatus() === 'ready'"
+                [class.dot-yellow]="surveillance.n8nStatus() === 'checking'"
+                [class.dot-red]="surveillance.n8nStatus() === 'not-ready'">
           </span>
           <span class="hud-svc-state"
-                [class.state-online]="surveillance.n8nStatus() === 'online'"
-                [class.state-warn]="surveillance.n8nStatus() === 'untested'"
-                [class.state-offline]="surveillance.n8nStatus() === 'offline'">
-            {{ surveillance.n8nStatus() === 'online' ? 'Reachable'
-             : surveillance.n8nStatus() === 'offline' ? 'Last request failed'
-             : 'Untested' }}
+                [class.state-online]="surveillance.n8nStatus() === 'ready'"
+                [class.state-warn]="surveillance.n8nStatus() === 'checking'"
+                [class.state-offline]="surveillance.n8nStatus() === 'not-ready'">
+            {{ surveillance.n8nStatus() === 'ready' ? 'Agent Ready'
+             : surveillance.n8nStatus() === 'not-ready' ? 'Agent Not Ready'
+             : 'Checking...' }}
           </span>
         </div>
       </div>

+ 22 - 38
src/app/services/surveillance.service.ts

@@ -3,15 +3,14 @@
  *
  * Opens a persistent Socket.io connection to NestJS /monitor namespace.
  * 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 { HttpClient } from '@angular/common/http';
-import { firstValueFrom } from 'rxjs';
 import { io, Socket } from 'socket.io-client';
 import { environment } from '../../environments/environment';
 
@@ -23,6 +22,11 @@ export interface MonitorPayload {
   timestamp: string;
 }
 
+export interface MonitorStatus {
+  n8nWebhookReady: boolean;
+  timestamp: string;
+}
+
 @Injectable({ providedIn: 'root' })
 export class SurveillanceService implements OnDestroy {
 
@@ -35,12 +39,15 @@ export class SurveillanceService implements OnDestroy {
     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;
 
-  constructor(private http: HttpClient) {
+  constructor() {
     // Persistent tunnel — opened on service construction (app boot)
     this.socket = io(`${environment.nestWsUrl}/monitor`, {
       transports: ['websocket'],
@@ -50,54 +57,31 @@ export class SurveillanceService implements OnDestroy {
 
     this.socket.on('connect', () => {
       this.connected.set(true);
+      this.n8nStatus.set('checking');
       // Lego 11: emit monitor:subscribe to start the data stream
       this.socket.emit('monitor:subscribe');
     });
 
     this.socket.on('disconnect', () => {
       this.connected.set(false);
+      this.n8nStatus.set('not-ready');
     });
 
     // Every 500ms tick from NestJS SurveillanceService
     this.socket.on('monitor:data', (payload: MonitorPayload[]) => {
       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() {
-    // 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();
   }
 
-  /**
-   * 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 {
     if (bytes === 0) return '0 B';
     const mb = bytes / (1024 * 1024);