Files
dcrouter/ts_web/appstate.ts
Juergen Kunz 2f46b3c9f3 update
2025-07-02 11:33:50 +00:00

548 lines
16 KiB
TypeScript

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<ILoginState>(
'login',
{
identity: null,
isLoggedIn: false,
},
'soft' // Login state persists across sessions
);
export const statsStatePart = await appState.getStatePart<IStatsState>(
'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<IConfigState>(
'config',
{
config: null,
isLoading: false,
error: null,
}
);
export const uiStatePart = await appState.getStatePart<IUiState>(
'ui',
{
activeView: 'overview',
sidebarCollapsed: false,
autoRefresh: true,
refreshInterval: 1000, // 1 second
theme: 'light',
},
);
export const logStatePart = await appState.getStatePart<ILogState>(
'logs',
{
recentLogs: [],
isStreaming: false,
filters: {},
},
'soft'
);
export const networkStatePart = await appState.getStatePart<INetworkState>(
'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<string>(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();
})();