feat: Add operations view components for logs, overview, security, and stats
- Implemented `ops-view-logs` for displaying and filtering logs with streaming capabilities. - Created `ops-view-overview` to show server, email, DNS statistics, and charts. - Developed `ops-view-security` for monitoring security metrics, blocked IPs, and authentication attempts. - Added `ops-view-stats` to present comprehensive statistics on server, email, DNS, and security metrics. - Introduced shared styles and components including `ops-sectionheading` for consistent UI.
This commit is contained in:
@ -1,9 +1,363 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as interfaces from '../dist_ts_interfaces/index.js';
|
||||
|
||||
const appState = new plugins.deesElement.domtools.plugins.smartstate.Smartstate();
|
||||
// Create main app state instance
|
||||
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
||||
|
||||
export interface IDcRouterState {}
|
||||
export const loginStatePart: plugins.deesElement.domtools.plugins.smartstate.StatePart<
|
||||
unknown,
|
||||
IDcRouterState
|
||||
> = await appState.getStatePart<IDcRouterState>('login', { identity: null }, 'persistent');
|
||||
// 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[];
|
||||
};
|
||||
}
|
||||
|
||||
// Create state parts with appropriate persistence
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
{
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'persistent' // 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,
|
||||
},
|
||||
'soft'
|
||||
);
|
||||
|
||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
'ui',
|
||||
{
|
||||
activeView: 'dashboard',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000, // 30 seconds
|
||||
theme: 'light',
|
||||
},
|
||||
'persistent' // UI preferences persist
|
||||
);
|
||||
|
||||
export const logStatePart = await appState.getStatePart<ILogState>(
|
||||
'logs',
|
||||
{
|
||||
recentLogs: [],
|
||||
isStreaming: false,
|
||||
filters: {},
|
||||
},
|
||||
'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
|
||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
// Fetch server stats
|
||||
const serverStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServerStatistics
|
||||
>('/typedrequest', 'getServerStatistics');
|
||||
|
||||
const serverStatsResponse = await serverStatsRequest.fire({
|
||||
identity: context.identity,
|
||||
includeHistory: false,
|
||||
});
|
||||
|
||||
// Fetch email stats
|
||||
const emailStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetEmailStatistics
|
||||
>('/typedrequest', 'getEmailStatistics');
|
||||
|
||||
const emailStatsResponse = await emailStatsRequest.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
|
||||
// Fetch DNS stats
|
||||
const dnsStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetDnsStatistics
|
||||
>('/typedrequest', 'getDnsStatistics');
|
||||
|
||||
const dnsStatsResponse = await dnsStatsRequest.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
|
||||
// Fetch security metrics
|
||||
const securityRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSecurityMetrics
|
||||
>('/typedrequest', 'getSecurityMetrics');
|
||||
|
||||
const securityResponse = await securityRequest.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
|
||||
// Update state with all stats
|
||||
return {
|
||||
serverStats: serverStatsResponse.stats,
|
||||
emailStats: emailStatsResponse.stats,
|
||||
dnsStats: dnsStatsResponse.stats,
|
||||
securityMetrics: securityResponse.metrics,
|
||||
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();
|
||||
return {
|
||||
...currentState,
|
||||
activeView: viewName,
|
||||
};
|
||||
});
|
||||
|
||||
// Initialize auto-refresh
|
||||
let refreshInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
// Initialize auto-refresh when UI state is ready
|
||||
(() => {
|
||||
const startAutoRefresh = () => {
|
||||
const uiState = uiStatePart.getState();
|
||||
if (uiState.autoRefresh && loginStatePart.getState().isLoggedIn) {
|
||||
refreshInterval = setInterval(() => {
|
||||
statsStatePart.dispatchAction(fetchAllStatsAction, null);
|
||||
}, uiState.refreshInterval);
|
||||
}
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes
|
||||
uiStatePart.state.subscribe(() => {
|
||||
stopAutoRefresh();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
loginStatePart.state.subscribe(() => {
|
||||
stopAutoRefresh();
|
||||
startAutoRefresh();
|
||||
});
|
||||
|
||||
// Initial start
|
||||
startAutoRefresh();
|
||||
})();
|
Reference in New Issue
Block a user