summary history files

frontend/src/lib/logs.ts
// lib/logs.ts
import { KubernetesCluster } from '../types/kubernetes';
import { isPod, isDeployment } from './resources';

interface LogsOptions {
  selectedResource: any;
  selectedCluster: KubernetesCluster;
  selectedContainer: string;
  tailLines: number;
  stripColors?: boolean;
}

interface LogsConnection {
  ws: WebSocket | null;
  connecting: boolean;
  closeTimeout: number | null;
  error: string;
  logs: string;
  onMessage?: (logs: string) => void;
  onError?: (error: string) => void;
  onOpen?: () => void;
  onClose?: (code: number, reason: string) => void;
}

export class LogsService {
  private connection: LogsConnection = {
    ws: null,
    connecting: false,
    closeTimeout: null,
    error: '',
    logs: ''
  };

  /**
   * Connect to logs WebSocket
   */
  connectLogsWebSocket(options: LogsOptions, callbacks?: {
    onMessage?: (logs: string) => void;
    onError?: (error: string) => void;
    onOpen?: () => void;
    onClose?: (code: number, reason: string) => void;
  }): void {
    const { selectedResource, selectedCluster, selectedContainer, tailLines, stripColors = true } = options;

    // Store callbacks
    if (callbacks) {
      this.connection.onMessage = callbacks.onMessage;
      this.connection.onError = callbacks.onError;
      this.connection.onOpen = callbacks.onOpen;
      this.connection.onClose = callbacks.onClose;
    }

    // If we're already connecting, don't try to connect again
    if (this.connection.connecting) {
      console.log('Already connecting to logs WebSocket, ignoring duplicate request');
      return;
    }

    if (!selectedResource || !selectedCluster) {
      this.connection.logs = 'No resource selected';
      return;
    }

    this.connection.connecting = true;
    this.connection.logs = 'Connecting to logs...';
    this.connection.error = '';

    try {
      // Determine the WebSocket URL based on resource type
      let wsUrl = '';

      if (isPod(selectedResource)) {
        wsUrl = `ws://127.0.0.1:8081/api/pods/logs?` + new URLSearchParams({
          contextName: selectedCluster.contextName,
          namespace: selectedResource.metadata.namespace,
          name: selectedResource.metadata.name,
          container: selectedContainer || '',
          tailLines: tailLines.toString(),
          stripColors: stripColors ? 'true' : 'false'
        }).toString();
      } else if (isDeployment(selectedResource)) {
        wsUrl = `ws://127.0.0.1:8081/api/deployments/logs?` + new URLSearchParams({
          contextName: selectedCluster.contextName,
          namespace: selectedResource.metadata.namespace,
          name: selectedResource.metadata.name,
          tailLines: tailLines.toString(),
          stripColors: stripColors ? 'true' : 'false'
        }).toString();
      } else {
        this.connection.logs = 'Logs not available for this resource type';
        this.connection.connecting = false;
        return;
      }

      console.log(`Connecting to logs WebSocket: ${wsUrl}`);

      // Create WebSocket connection
      this.connection.ws = new WebSocket(wsUrl);

      // Set up event handlers
      this.connection.ws.onopen = () => {
        console.log('Logs WebSocket connection established');
        this.connection.logs = '';
        this.connection.connecting = false;
        this.connection.error = '';

        if (this.connection.onOpen) {
          this.connection.onOpen();
        }
      };

      this.connection.ws.onmessage = (event) => {
        // Append new log lines to the existing logs
        this.connection.logs += event.data;

        if (this.connection.onMessage) {
          this.connection.onMessage(event.data);
        }
      };

      this.connection.ws.onerror = (error) => {
        console.error('WebSocket error:', error);
        this.connection.error = 'Error connecting to logs';
        this.connection.connecting = false;

        if (this.connection.onError) {
          this.connection.onError(this.connection.error);
        }
      };

      this.connection.ws.onclose = (event) => {
        console.log(`Logs WebSocket connection closed: ${event.code} ${event.reason}`);
        this.connection.connecting = false;

        // Only set error if it wasn't a normal closure and we don't already have an error
        if (event.code !== 1000 && event.code !== 1001 && !this.connection.error) {
          // Don't show error for code 1006 (abnormal closure) as it's common when navigating away
          if (event.code !== 1006) {
            this.connection.error = `Connection closed: ${event.reason || 'Server disconnected'}`;

            if (this.connection.onError) {
              this.connection.onError(this.connection.error);
            }
          }
        }

        // Clear the WebSocket reference
        this.connection.ws = null;

        // Clear any pending close timeout
        if (this.connection.closeTimeout !== null) {
          clearTimeout(this.connection.closeTimeout);
          this.connection.closeTimeout = null;
        }

        if (this.connection.onClose) {
          this.connection.onClose(event.code, event.reason);
        }
      };
    } catch (error) {
      console.error('Error setting up WebSocket:', error);
      this.connection.error = `Error setting up WebSocket: ${error}`;
      this.connection.logs = 'Failed to connect to logs';
      this.connection.connecting = false;

      if (this.connection.onError) {
        this.connection.onError(this.connection.error);
      }
    }
  }

  /**
   * Close logs WebSocket connection
   */
  closeLogsWebSocket(): Promise<void> {
    return new Promise<void>((resolve) => {
      // Clear any existing timeout
      if (this.connection.closeTimeout !== null) {
        clearTimeout(this.connection.closeTimeout);
        this.connection.closeTimeout = null;
      }

      if (this.connection.ws) {
        console.log('Closing logs WebSocket connection');

        // Set a flag to track if onclose was called
        let onCloseCalled = false;

        // Create a temporary onclose handler to know when the connection is fully closed
        const originalOnClose = this.connection.ws.onclose;
        this.connection.ws.onclose = (event) => {
          // Mark that onclose was called
          onCloseCalled = true;

          // Call the original handler if it exists
          if (originalOnClose) {
            originalOnClose.call(this.connection.ws, event);
          }

          resolve();
        };

        // Try to close the connection gracefully
        try {
          this.connection.ws.close(1000, 'User navigated away');
        } catch (error) {
          console.error('Error closing WebSocket:', error);
        }

        // Set a timeout in case the onclose event doesn't fire
        this.connection.closeTimeout = window.setTimeout(() => {
          if (!onCloseCalled) {
            console.log('WebSocket close timed out, forcing cleanup');

            // If we have an original onclose handler, call it with a simulated event
            if (originalOnClose && this.connection.ws) {
              try {
                originalOnClose.call(this.connection.ws, {
                  type: 'close',
                  code: 1006,
                  reason: 'Timeout while closing',
                  wasClean: false
                } as CloseEvent);
              } catch (error) {
                console.error('Error calling original onclose handler:', error);
              }
            }

            // Clear the WebSocket reference
            this.connection.ws = null;
            this.connection.connecting = false;
          }

          this.connection.closeTimeout = null;
          resolve();
        }, 300); // Shorter timeout to reduce waiting time
      } else {
        // No WebSocket to close
        this.connection.connecting = false;
        resolve();
      }
    });
  }

  /**
   * Reconnect logs WebSocket
   */
  async reconnectLogsWebSocket(options: LogsOptions, callbacks?: {
    onMessage?: (logs: string) => void;
    onError?: (error: string) => void;
    onOpen?: () => void;
    onClose?: (code: number, reason: string) => void;
  }): Promise<void> {
    try {
      // Ensure we close any existing connection first
      await this.closeLogsWebSocket();

      // Wait a short time to ensure the previous connection is fully closed
      await new Promise(resolve => setTimeout(resolve, 100));

      // Now connect the new WebSocket
      this.connectLogsWebSocket(options, callbacks);
    } catch (error) {
      console.error('Error reconnecting WebSocket:', error);
      this.connection.error = 'Error reconnecting to logs';

      if (callbacks?.onError) {
        callbacks.onError(this.connection.error);
      }
    }
  }

  /**
   * Get current logs
   */
  getLogs(): string {
    return this.connection.logs;
  }

  /**
   * Clear logs
   */
  clearLogs(): void {
    this.connection.logs = '';
  }

  /**
   * Get current error
   */
  getError(): string {
    return this.connection.error;
  }


  /**
   * Extract container names from resource
   */
  extractContainers(resource: any): string[] {
    if (!resource || !resource.spec || !resource.spec.containers) {
      return [];
    }

    return resource.spec.containers.map((c: any) => c.name);
  }
}

// Create a singleton instance
export const logsService = new LogsService();