feat(gateway): enhance gateway process management with auto-reconnection
Improve Gateway lifecycle management with the following features: - Add exponential backoff reconnection (1s-30s delay, max 10 attempts) - Add health check monitoring every 30 seconds - Add proper restart method with graceful shutdown - Handle server-initiated notifications (channel status, chat messages) - Add 'reconnecting' state for better UI feedback - Enhance IPC handlers with isConnected and health check endpoints - Update preload script with new event channels - Improve type safety and error handling throughout Also fixes several TypeScript errors and unused variable warnings.
This commit is contained in:
@@ -28,8 +28,11 @@ function App() {
|
||||
|
||||
// Listen for navigation events from main process
|
||||
useEffect(() => {
|
||||
const handleNavigate = (path: string) => {
|
||||
navigate(path);
|
||||
const handleNavigate = (...args: unknown[]) => {
|
||||
const path = args[0];
|
||||
if (typeof path === 'string') {
|
||||
navigate(path);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = window.electron.ipcRenderer.on('navigate', handleNavigate);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting';
|
||||
export type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting' | 'reconnecting';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: Status;
|
||||
@@ -20,6 +20,7 @@ const statusConfig: Record<Status, { label: string; variant: 'success' | 'second
|
||||
stopped: { label: 'Stopped', variant: 'secondary' },
|
||||
connecting: { label: 'Connecting', variant: 'warning' },
|
||||
starting: { label: 'Starting', variant: 'warning' },
|
||||
reconnecting: { label: 'Reconnecting', variant: 'warning' },
|
||||
error: { label: 'Error', variant: 'destructive' },
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Top navigation bar with search and actions
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Search, Bell, Moon, Sun, Monitor } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -21,7 +21,6 @@ const pageTitles: Record<string, string> = {
|
||||
|
||||
export function Header() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useSettingsStore((state) => state.theme);
|
||||
const setTheme = useSettingsStore((state) => state.setTheme);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Navigation sidebar with menu items
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
Home,
|
||||
MessageSquare,
|
||||
@@ -62,7 +62,6 @@ function NavItem({ to, icon, label, badge, collapsed }: NavItemProps) {
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Cron Page
|
||||
* Manage scheduled tasks
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Plus, Clock, Play, Pause, Trash2, Edit, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
Clock,
|
||||
Settings,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -22,7 +20,6 @@ import { useGatewayStore } from '@/stores/gateway';
|
||||
import { useChannelsStore } from '@/stores/channels';
|
||||
import { useSkillsStore } from '@/stores/skills';
|
||||
import { StatusBadge } from '@/components/common/StatusBadge';
|
||||
import { formatRelativeTime } from '@/lib/utils';
|
||||
|
||||
export function Dashboard() {
|
||||
const gatewayStatus = useGatewayStore((state) => state.status);
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Loader2,
|
||||
Terminal,
|
||||
ExternalLink,
|
||||
Info,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
@@ -20,7 +20,7 @@ interface CronState {
|
||||
setJobs: (jobs: CronJob[]) => void;
|
||||
}
|
||||
|
||||
export const useCronStore = create<CronState>((set, get) => ({
|
||||
export const useCronStore = create<CronState>((set) => ({
|
||||
jobs: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
/**
|
||||
* Gateway State Store
|
||||
* Manages Gateway connection state
|
||||
* Manages Gateway connection state and communication
|
||||
*/
|
||||
import { create } from 'zustand';
|
||||
import type { GatewayStatus } from '../types/gateway';
|
||||
|
||||
interface GatewayHealth {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
uptime?: number;
|
||||
}
|
||||
|
||||
interface GatewayState {
|
||||
status: GatewayStatus;
|
||||
health: GatewayHealth | null;
|
||||
isInitialized: boolean;
|
||||
lastError: string | null;
|
||||
|
||||
// Actions
|
||||
init: () => Promise<void>;
|
||||
start: () => Promise<void>;
|
||||
stop: () => Promise<void>;
|
||||
restart: () => Promise<void>;
|
||||
checkHealth: () => Promise<GatewayHealth>;
|
||||
rpc: <T>(method: string, params?: unknown, timeoutMs?: number) => Promise<T>;
|
||||
setStatus: (status: GatewayStatus) => void;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
@@ -22,7 +33,9 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
state: 'stopped',
|
||||
port: 18789,
|
||||
},
|
||||
health: null,
|
||||
isInitialized: false,
|
||||
lastError: null,
|
||||
|
||||
init: async () => {
|
||||
if (get().isInitialized) return;
|
||||
@@ -36,38 +49,111 @@ export const useGatewayStore = create<GatewayState>((set, get) => ({
|
||||
window.electron.ipcRenderer.on('gateway:status-changed', (newStatus) => {
|
||||
set({ status: newStatus as GatewayStatus });
|
||||
});
|
||||
|
||||
// Listen for errors
|
||||
window.electron.ipcRenderer.on('gateway:error', (error) => {
|
||||
set({ lastError: String(error) });
|
||||
});
|
||||
|
||||
// Listen for notifications
|
||||
window.electron.ipcRenderer.on('gateway:notification', (notification) => {
|
||||
console.log('Gateway notification:', notification);
|
||||
// Could dispatch to other stores based on notification type
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Gateway:', error);
|
||||
set({ lastError: String(error) });
|
||||
}
|
||||
},
|
||||
|
||||
start: async () => {
|
||||
try {
|
||||
set({ status: { ...get().status, state: 'starting' } });
|
||||
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||
const result = await window.electron.ipcRenderer.invoke('gateway:start') as { success: boolean; error?: string };
|
||||
|
||||
if (!result.success) {
|
||||
set({ status: { ...get().status, state: 'error', error: result.error } });
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: result.error },
|
||||
lastError: result.error || 'Failed to start Gateway'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
set({ status: { ...get().status, state: 'error', error: String(error) } });
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: String(error) },
|
||||
lastError: String(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
stop: async () => {
|
||||
try {
|
||||
await window.electron.ipcRenderer.invoke('gateway:stop');
|
||||
set({ status: { ...get().status, state: 'stopped' } });
|
||||
set({ status: { ...get().status, state: 'stopped' }, lastError: null });
|
||||
} catch (error) {
|
||||
console.error('Failed to stop Gateway:', error);
|
||||
set({ lastError: String(error) });
|
||||
}
|
||||
},
|
||||
|
||||
restart: async () => {
|
||||
const { stop, start } = get();
|
||||
await stop();
|
||||
await start();
|
||||
try {
|
||||
set({ status: { ...get().status, state: 'starting' }, lastError: null });
|
||||
const result = await window.electron.ipcRenderer.invoke('gateway:restart') as { success: boolean; error?: string };
|
||||
|
||||
if (!result.success) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: result.error },
|
||||
lastError: result.error || 'Failed to restart Gateway'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
set({
|
||||
status: { ...get().status, state: 'error', error: String(error) },
|
||||
lastError: String(error)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
checkHealth: async () => {
|
||||
try {
|
||||
const result = await window.electron.ipcRenderer.invoke('gateway:health') as {
|
||||
success: boolean;
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
uptime?: number
|
||||
};
|
||||
|
||||
const health: GatewayHealth = {
|
||||
ok: result.ok,
|
||||
error: result.error,
|
||||
uptime: result.uptime,
|
||||
};
|
||||
|
||||
set({ health });
|
||||
return health;
|
||||
} catch (error) {
|
||||
const health: GatewayHealth = { ok: false, error: String(error) };
|
||||
set({ health });
|
||||
return health;
|
||||
}
|
||||
},
|
||||
|
||||
rpc: async <T>(method: string, params?: unknown, timeoutMs?: number): Promise<T> => {
|
||||
const result = await window.electron.ipcRenderer.invoke('gateway:rpc', method, params, timeoutMs) as {
|
||||
success: boolean;
|
||||
result?: T;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || `RPC call failed: ${method}`);
|
||||
}
|
||||
|
||||
return result.result as T;
|
||||
},
|
||||
|
||||
setStatus: (status) => set({ status }),
|
||||
|
||||
clearError: () => set({ lastError: null }),
|
||||
}));
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
* Gateway connection status
|
||||
*/
|
||||
export interface GatewayStatus {
|
||||
state: 'stopped' | 'starting' | 'running' | 'error';
|
||||
state: 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
|
||||
port: number;
|
||||
pid?: number;
|
||||
uptime?: number;
|
||||
error?: string;
|
||||
connectedAt?: number;
|
||||
version?: string;
|
||||
reconnectAttempts?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,3 +25,34 @@ export interface GatewayRpcResponse<T = unknown> {
|
||||
result?: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway health check response
|
||||
*/
|
||||
export interface GatewayHealth {
|
||||
ok: boolean;
|
||||
error?: string;
|
||||
uptime?: number;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway notification (server-initiated event)
|
||||
*/
|
||||
export interface GatewayNotification {
|
||||
method: string;
|
||||
params?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider configuration
|
||||
*/
|
||||
export interface ProviderConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'openai' | 'anthropic' | 'ollama' | 'custom';
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
model?: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user