import * as plugins from './plugins.js'; import * as interfaces from '../dist_ts_interfaces/index.js'; // Create main app state instance export const appState = new plugins.domtools.plugins.smartstate.Smartstate(); // Define state interfaces export interface ILoginState { identity: interfaces.data.IIdentity | null; isLoggedIn: boolean; } export interface IStatsState { serverStats: interfaces.data.IServerStats | null; emailStats: interfaces.data.IEmailStats | null; dnsStats: interfaces.data.IDnsStats | null; securityMetrics: interfaces.data.ISecurityMetrics | null; lastUpdated: number; isLoading: boolean; error: string | null; } export interface IConfigState { config: any | null; isLoading: boolean; error: string | null; } export interface IUiState { activeView: string; sidebarCollapsed: boolean; autoRefresh: boolean; refreshInterval: number; // milliseconds theme: 'light' | 'dark'; } export interface ILogState { recentLogs: interfaces.data.ILogEntry[]; isStreaming: boolean; filters: { level?: string[]; category?: string[]; }; } export interface INetworkState { connections: interfaces.data.IConnectionInfo[]; connectionsByIP: { [ip: string]: number }; throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; topIPs: Array<{ ip: string; count: number }>; lastUpdated: number; isLoading: boolean; error: string | null; } // Create state parts with appropriate persistence export const loginStatePart = await appState.getStatePart( 'login', { identity: null, isLoggedIn: false, }, 'soft' // Login state persists across sessions ); export const statsStatePart = await appState.getStatePart( 'stats', { serverStats: null, emailStats: null, dnsStats: null, securityMetrics: null, lastUpdated: 0, isLoading: false, error: null, }, 'soft' // Stats are cached but not persisted ); export const configStatePart = await appState.getStatePart( 'config', { config: null, isLoading: false, error: null, } ); export const uiStatePart = await appState.getStatePart( 'ui', { activeView: 'overview', sidebarCollapsed: false, autoRefresh: true, refreshInterval: 1000, // 1 second theme: 'light', }, ); export const logStatePart = await appState.getStatePart( 'logs', { recentLogs: [], isStreaming: false, filters: {}, }, 'soft' ); export const networkStatePart = await appState.getStatePart( 'network', { connections: [], connectionsByIP: {}, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [], lastUpdated: 0, isLoading: false, error: null, }, 'soft' ); // Actions for state management interface IActionContext { identity: interfaces.data.IIdentity | null; } const getActionContext = (): IActionContext => { return { identity: loginStatePart.getState().identity, }; }; // Login Action export const loginAction = loginStatePart.createAction<{ username: string; password: string; }>(async (statePartArg, dataArg) => { const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_AdminLoginWithUsernameAndPassword >('/typedrequest', 'adminLoginWithUsernameAndPassword'); try { const response = await typedRequest.fire({ username: dataArg.username, password: dataArg.password, }); if (response.identity) { return { identity: response.identity, isLoggedIn: true, }; } return statePartArg.getState(); } catch (error) { console.error('Login failed:', error); return statePartArg.getState(); } }); // Logout Action export const logoutAction = loginStatePart.createAction(async (statePartArg) => { const context = getActionContext(); if (!context.identity) return statePartArg.getState(); const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_AdminLogout >('/typedrequest', 'adminLogout'); try { await typedRequest.fire({ identity: context.identity, }); } catch (error) { console.error('Logout error:', error); } // Clear login state regardless return { identity: null, isLoggedIn: false, }; }); // Fetch All Stats Action - Using combined endpoint for efficiency export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => { const context = getActionContext(); const currentState = statePartArg.getState(); try { // Use combined metrics endpoint - single request instead of 4 const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetCombinedMetrics >('/typedrequest', 'getCombinedMetrics'); const combinedResponse = await combinedRequest.fire({ identity: context.identity, sections: { server: true, email: true, dns: true, security: true, network: false, // Network is fetched separately for the network view }, }); // Update state with all stats from combined response return { serverStats: combinedResponse.metrics.server || currentState.serverStats, emailStats: combinedResponse.metrics.email || currentState.emailStats, dnsStats: combinedResponse.metrics.dns || currentState.dnsStats, securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics, lastUpdated: Date.now(), isLoading: false, error: null, }; } catch (error) { return { ...currentState, isLoading: false, error: error.message || 'Failed to fetch statistics', }; } }); // Fetch Configuration Action export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => { const context = getActionContext(); const currentState = statePartArg.getState(); try { const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetConfiguration >('/typedrequest', 'getConfiguration'); const response = await configRequest.fire({ identity: context.identity, }); return { config: response.config, isLoading: false, error: null, }; } catch (error) { return { ...currentState, isLoading: false, error: error.message || 'Failed to fetch configuration', }; } }); // Update Configuration Action export const updateConfigurationAction = configStatePart.createAction<{ section: string; config: any; }>(async (statePartArg, dataArg) => { const context = getActionContext(); if (!context.identity) { throw new Error('Must be logged in to update configuration'); } const updateRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_UpdateConfiguration >('/typedrequest', 'updateConfiguration'); const response = await updateRequest.fire({ identity: context.identity, section: dataArg.section, config: dataArg.config, }); if (response.updated) { // Refresh configuration await configStatePart.dispatchAction(fetchConfigurationAction, null); return statePartArg.getState(); } return statePartArg.getState(); }); // Fetch Recent Logs Action export const fetchRecentLogsAction = logStatePart.createAction<{ limit?: number; level?: 'debug' | 'info' | 'warn' | 'error'; category?: 'smtp' | 'dns' | 'security' | 'system' | 'email'; }>(async (statePartArg, dataArg) => { const context = getActionContext(); const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetRecentLogs >('/typedrequest', 'getRecentLogs'); const response = await logsRequest.fire({ identity: context.identity, limit: dataArg.limit || 100, level: dataArg.level, category: dataArg.category, }); return { ...statePartArg.getState(), recentLogs: response.logs, }; }); // Toggle Auto Refresh Action export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg) => { const currentState = statePartArg.getState(); return { ...currentState, autoRefresh: !currentState.autoRefresh, }; }); // Set Active View Action export const setActiveViewAction = uiStatePart.createAction(async (statePartArg, viewName) => { const currentState = statePartArg.getState(); // If switching to network view, ensure we fetch network data if (viewName === 'network' && currentState.activeView !== 'network') { setTimeout(() => { networkStatePart.dispatchAction(fetchNetworkStatsAction, null); }, 100); } return { ...currentState, activeView: viewName, }; }); // Fetch Network Stats Action export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => { const context = getActionContext(); const currentState = statePartArg.getState(); try { // Fetch active connections using the existing endpoint const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetActiveConnections >('/typedrequest', 'getActiveConnections'); const connectionsResponse = await connectionsRequest.fire({ identity: context.identity, }); // Get network stats for throughput and IP data const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest( '/typedrequest', 'getNetworkStats' ); const networkStatsResponse = await networkStatsRequest.fire({ identity: context.identity, }) as any; // Use the connections data for the connection list // and network stats for throughput and IP analytics const connectionsByIP: { [ip: string]: number } = {}; // Build connectionsByIP from network stats if available if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) { networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => { connectionsByIP[item.ip] = item.count; }); } else { // Fallback: calculate from connections connectionsResponse.connections.forEach(conn => { const ip = conn.remoteAddress; connectionsByIP[ip] = (connectionsByIP[ip] || 0) + 1; }); } return { connections: connectionsResponse.connections, connectionsByIP, throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: networkStatsResponse.topIPs || [], lastUpdated: Date.now(), isLoading: false, error: null, }; } catch (error) { console.error('Failed to fetch network stats:', error); return { ...currentState, isLoading: false, error: error instanceof Error ? error.message : 'Failed to fetch network stats', }; } }); // Combined refresh action for efficient polling async function dispatchCombinedRefreshAction() { const context = getActionContext(); const currentView = uiStatePart.getState().activeView; try { // Always fetch basic stats for dashboard widgets const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetCombinedMetrics >('/typedrequest', 'getCombinedMetrics'); const combinedResponse = await combinedRequest.fire({ identity: context.identity, sections: { server: true, email: true, dns: true, security: true, network: currentView === 'network' || currentView === 'Network', // Only fetch network if on network view }, }); // Update all stats from combined response statsStatePart.setState({ ...statsStatePart.getState(), serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats, emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats, dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats, securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics, lastUpdated: Date.now(), isLoading: false, error: null, }); // Update network stats if included if (combinedResponse.metrics.network && (currentView === 'network' || currentView === 'Network')) { const network = combinedResponse.metrics.network; const connectionsByIP: { [ip: string]: number } = {}; // Convert connection details to IP counts network.connectionDetails.forEach(conn => { connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1; }); // Fetch detailed connections for the network view try { const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest< interfaces.requests.IReq_GetActiveConnections >('/typedrequest', 'getActiveConnections'); const connectionsResponse = await connectionsRequest.fire({ identity: context.identity, }); networkStatePart.setState({ ...networkStatePart.getState(), connections: connectionsResponse.connections, connectionsByIP, throughputRate: { bytesInPerSecond: network.totalBandwidth.in, bytesOutPerSecond: network.totalBandwidth.out }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), lastUpdated: Date.now(), isLoading: false, error: null, }); } catch (error) { console.error('Failed to fetch connections:', error); networkStatePart.setState({ ...networkStatePart.getState(), connections: [], connectionsByIP, throughputRate: { bytesInPerSecond: network.totalBandwidth.in, bytesOutPerSecond: network.totalBandwidth.out }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), lastUpdated: Date.now(), isLoading: false, error: null, }); } } } catch (error) { console.error('Combined refresh failed:', error); } } // Initialize auto-refresh let refreshInterval: NodeJS.Timeout | null = null; let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts // Initialize auto-refresh when UI state is ready (() => { const startAutoRefresh = () => { const uiState = uiStatePart.getState(); const loginState = loginStatePart.getState(); // Only start if conditions are met and not already running at the same rate if (uiState.autoRefresh && loginState.isLoggedIn) { // Check if we need to restart the interval (rate changed or not running) if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) { stopAutoRefresh(); currentRefreshRate = uiState.refreshInterval; refreshInterval = setInterval(() => { // Use combined refresh action for efficiency dispatchCombinedRefreshAction(); }, uiState.refreshInterval); } } else { stopAutoRefresh(); } }; const stopAutoRefresh = () => { if (refreshInterval) { clearInterval(refreshInterval); refreshInterval = null; currentRefreshRate = 0; } }; // Watch for relevant changes only let previousAutoRefresh = uiStatePart.getState().autoRefresh; let previousRefreshInterval = uiStatePart.getState().refreshInterval; let previousIsLoggedIn = loginStatePart.getState().isLoggedIn; uiStatePart.state.subscribe((state) => { // Only restart if relevant values changed if (state.autoRefresh !== previousAutoRefresh || state.refreshInterval !== previousRefreshInterval) { previousAutoRefresh = state.autoRefresh; previousRefreshInterval = state.refreshInterval; startAutoRefresh(); } }); loginStatePart.state.subscribe((state) => { // Only restart if login state changed if (state.isLoggedIn !== previousIsLoggedIn) { previousIsLoggedIn = state.isLoggedIn; startAutoRefresh(); } }); // Initial start startAutoRefresh(); })();