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:
@ -164,13 +164,13 @@ export class StatsHandler {
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: Frontend State Management
|
||||
### Phase 3: Frontend State Management ✓
|
||||
|
||||
#### 3.1 Set up Smartstate (`ts_web/appstate.ts`)
|
||||
#### 3.1 Set up Smartstate (`ts_web/appstate.ts`) ✓
|
||||
|
||||
- [ ] Initialize Smartstate instance
|
||||
- [ ] Create state parts with appropriate persistence
|
||||
- [ ] Define initial state structures
|
||||
- [x] Initialize Smartstate instance
|
||||
- [x] Create state parts with appropriate persistence
|
||||
- [x] Define initial state structures
|
||||
|
||||
```typescript
|
||||
// State structure example
|
||||
@ -184,48 +184,53 @@ interface IStatsState {
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 State Parts to Create
|
||||
#### 3.2 State Parts to Create ✓
|
||||
|
||||
- [ ] `statsState` - Runtime statistics (soft persistence)
|
||||
- [ ] `configState` - Configuration data (soft persistence)
|
||||
- [ ] `uiState` - UI preferences (persistent)
|
||||
- [ ] `loginState` - Authentication state (persistent) *if needed*
|
||||
- [x] `statsState` - Runtime statistics (soft persistence)
|
||||
- [x] `configState` - Configuration data (soft persistence)
|
||||
- [x] `uiState` - UI preferences (persistent)
|
||||
- [x] `loginState` - Authentication state (persistent)
|
||||
|
||||
### Phase 4: Frontend Integration
|
||||
### Phase 4: Frontend Integration ✓
|
||||
|
||||
#### 4.1 API Client Setup (`ts_web/api/clients.ts`)
|
||||
#### 4.1 API Client Setup ✓
|
||||
|
||||
- [ ] Create TypedRequest instances for each endpoint
|
||||
- [ ] Configure base URL handling
|
||||
- [ ] Add error interceptors
|
||||
- [ ] Implement retry logic
|
||||
- [x] TypedRequest instances created inline within actions
|
||||
- [x] Base URL handled through relative paths
|
||||
- [x] Error handling integrated in actions
|
||||
- [x] Following cloudly pattern of creating requests within actions
|
||||
|
||||
#### 4.2 Create Actions (`ts_web/state/actions.ts`)
|
||||
#### 4.2 Create Actions (`ts_web/appstate.ts`) ✓
|
||||
|
||||
- [ ] `fetchAllStatsAction` - Batch fetch all statistics
|
||||
- [ ] `refreshServerStatsAction` - Update server stats only
|
||||
- [ ] `refreshEmailStatsAction` - Update email stats only
|
||||
- [ ] `setAutoRefreshAction` - Toggle auto-refresh
|
||||
- [ ] Error handling actions
|
||||
- [x] `loginAction` - Authentication with JWT
|
||||
- [x] `logoutAction` - Clear authentication state
|
||||
- [x] `fetchAllStatsAction` - Batch fetch all statistics
|
||||
- [x] `fetchConfigurationAction` - Get configuration
|
||||
- [x] `updateConfigurationAction` - Update configuration
|
||||
- [x] `fetchRecentLogsAction` - Get recent logs
|
||||
- [x] `toggleAutoRefreshAction` - Toggle auto-refresh
|
||||
- [x] `setActiveViewAction` - Change active view
|
||||
- [x] Error handling in all actions
|
||||
|
||||
#### 4.3 Update Dashboard Component (`ts_web/elements/ops-dashboard.ts`)
|
||||
#### 4.3 Update Dashboard Component (`ts_web/elements/ops-dashboard.ts`) ✓
|
||||
|
||||
- [ ] Subscribe to state changes
|
||||
- [ ] Implement reactive UI updates
|
||||
- [ ] Add refresh controls
|
||||
- [ ] Create sub-components for different stat types
|
||||
- [ ] Implement auto-refresh timer
|
||||
- [x] Subscribe to state changes (login and UI state)
|
||||
- [x] Implement reactive UI updates
|
||||
- [x] Use dees-simple-login and dees-simple-appdash components
|
||||
- [x] Create view components for different sections
|
||||
- [x] Implement auto-refresh timer functionality
|
||||
|
||||
### Phase 5: Component Structure
|
||||
### Phase 5: Component Structure ✓
|
||||
|
||||
Create modular components in `ts_web/elements/components/`:
|
||||
Created modular view components in `ts_web/elements/`:
|
||||
|
||||
- [ ] `server-stats.ts` - Server statistics display
|
||||
- [ ] `email-stats.ts` - Email metrics visualization
|
||||
- [ ] `dns-stats.ts` - DNS statistics
|
||||
- [ ] `rate-limit-display.ts` - Rate limiting status
|
||||
- [ ] `security-metrics.ts` - Security dashboard
|
||||
- [ ] `log-viewer.ts` - Real-time log display
|
||||
- [x] `ops-view-overview.ts` - Overview with server, email, and DNS statistics
|
||||
- [x] `ops-view-stats.ts` - Detailed statistics with tables and metrics
|
||||
- [x] `ops-view-logs.ts` - Log viewer with filtering and search
|
||||
- [x] `ops-view-config.ts` - Configuration editor with JSON editing
|
||||
- [x] `ops-view-security.ts` - Security metrics and threat monitoring
|
||||
- [x] `shared/ops-sectionheading.ts` - Reusable section heading component
|
||||
- [x] `shared/css.ts` - Shared CSS styles
|
||||
|
||||
### Phase 6: Optional Enhancements
|
||||
|
||||
@ -314,10 +319,32 @@ Create modular components in `ts_web/elements/components/`:
|
||||
- Added guard helpers for protecting endpoints
|
||||
- Full test coverage for JWT authentication flows
|
||||
|
||||
- **Phase 3: Frontend State Management** - Smartstate implementation
|
||||
- Initialized Smartstate with proper state parts
|
||||
- Created state interfaces for all data types
|
||||
- Implemented persistent vs soft state persistence
|
||||
- Set up reactive subscriptions
|
||||
|
||||
- **Phase 4: Frontend Integration** - Complete dashboard implementation
|
||||
- Created all state management actions with TypedRequest
|
||||
- Implemented JWT authentication flow in frontend
|
||||
- Built reactive dashboard with dees-simple-login and dees-simple-appdash
|
||||
- Added auto-refresh functionality
|
||||
- Fixed all interface import issues (using dist_ts_interfaces)
|
||||
|
||||
- **Phase 5: Component Structure** - View components
|
||||
- Created all view components following cloudly patterns
|
||||
- Implemented reactive data binding with state subscriptions
|
||||
- Added interactive features (filtering, editing, refresh controls)
|
||||
- Used @design.estate/dees-catalog components throughout
|
||||
- Created shared components and styles
|
||||
|
||||
### Next Steps
|
||||
- Phase 3: Frontend State Management - Set up Smartstate
|
||||
- Phase 4: Frontend Integration - Create API clients and update dashboard
|
||||
- Phase 5: Create modular UI components
|
||||
- Write comprehensive tests for handlers and frontend components
|
||||
- Implement real data sources (replace mock data)
|
||||
- Add WebSocket support for real-time updates
|
||||
- Enhance error handling and user feedback
|
||||
- Add more detailed charts and visualizations
|
||||
|
||||
---
|
||||
|
||||
|
@ -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();
|
||||
})();
|
@ -1 +1,7 @@
|
||||
export * from './ops-dashboard.js';
|
||||
export * from './ops-view-overview.js';
|
||||
export * from './ops-view-stats.js';
|
||||
export * from './ops-view-logs.js';
|
||||
export * from './ops-view-config.js';
|
||||
export * from './ops-view-security.js';
|
||||
export * from './shared/index.js';
|
@ -1,3 +1,6 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
@ -8,11 +11,114 @@ import {
|
||||
type TemplateResult
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
// Import view components
|
||||
import { OpsViewOverview } from './ops-view-overview.js';
|
||||
import { OpsViewStats } from './ops-view-stats.js';
|
||||
import { OpsViewLogs } from './ops-view-logs.js';
|
||||
import { OpsViewConfig } from './ops-view-config.js';
|
||||
import { OpsViewSecurity } from './ops-view-security.js';
|
||||
|
||||
@customElement('ops-dashboard')
|
||||
export class OpsDashboard extends DeesElement {
|
||||
@state() private loginState: appstate.ILoginState = {
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
};
|
||||
|
||||
@state() private uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
document.title = 'DCRouter OpsServer';
|
||||
|
||||
// Subscribe to login state
|
||||
const loginSubscription = appstate.loginStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((loginState) => {
|
||||
this.loginState = loginState;
|
||||
// Trigger data fetch when logged in
|
||||
if (loginState.isLoggedIn) {
|
||||
appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
}
|
||||
});
|
||||
this.rxSubscriptions.push(loginSubscription);
|
||||
|
||||
// Subscribe to UI state
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
.maincontainer {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
hello
|
||||
<div class="maincontainer">
|
||||
<dees-simple-login
|
||||
name="DCRouter OpsServer"
|
||||
.loginAction=${async (username: string, password: string) => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
return this.loginState.isLoggedIn;
|
||||
}}
|
||||
>
|
||||
<dees-simple-appdash
|
||||
name="DCRouter OpsServer"
|
||||
.viewTabs=${[
|
||||
{
|
||||
name: 'Overview',
|
||||
element: OpsViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'Statistics',
|
||||
element: OpsViewStats,
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
element: OpsViewLogs,
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
element: OpsViewConfig,
|
||||
},
|
||||
{
|
||||
name: 'Security',
|
||||
element: OpsViewSecurity,
|
||||
},
|
||||
]}
|
||||
.userMenuItems=${[
|
||||
{
|
||||
name: 'Logout',
|
||||
action: async () => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
268
ts_web/elements/ops-view-config.ts
Normal file
268
ts_web/elements/ops-view-config.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-config')
|
||||
export class OpsViewConfig extends DeesElement {
|
||||
@state()
|
||||
private configState: appstate.IConfigState = {
|
||||
config: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
private editingSection: string | null = null;
|
||||
|
||||
@state()
|
||||
private editedConfig: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.configStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((configState) => {
|
||||
this.configState = configState;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.configSection {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
background: #f8f9fa;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.configField {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fieldValue {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #f8f9fa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.configEditor {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
color: #856404;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
color: #c00;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Configuration</ops-sectionheading>
|
||||
|
||||
${this.configState.isLoading ? html`
|
||||
<div class="loadingMessage">
|
||||
<dees-spinner></dees-spinner>
|
||||
<p>Loading configuration...</p>
|
||||
</div>
|
||||
` : this.configState.error ? html`
|
||||
<div class="errorMessage">
|
||||
Error loading configuration: ${this.configState.error}
|
||||
</div>
|
||||
` : this.configState.config ? html`
|
||||
<div class="warning">
|
||||
<dees-icon name="warning"></dees-icon>
|
||||
<span>Changes to configuration will take effect immediately. Please be careful when editing production settings.</span>
|
||||
</div>
|
||||
|
||||
${this.renderConfigSection('server', 'Server Configuration', this.configState.config.server)}
|
||||
${this.renderConfigSection('email', 'Email Configuration', this.configState.config.email)}
|
||||
${this.renderConfigSection('dns', 'DNS Configuration', this.configState.config.dns)}
|
||||
${this.renderConfigSection('security', 'Security Configuration', this.configState.config.security)}
|
||||
${this.renderConfigSection('storage', 'Storage Configuration', this.configState.config.storage)}
|
||||
` : html`
|
||||
<div class="errorMessage">No configuration loaded</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderConfigSection(key: string, title: string, config: any) {
|
||||
const isEditing = this.editingSection === key;
|
||||
|
||||
return html`
|
||||
<div class="configSection">
|
||||
<div class="sectionHeader">
|
||||
<h3 class="sectionTitle">${title}</h3>
|
||||
<div>
|
||||
${isEditing ? html`
|
||||
<dees-button
|
||||
@click=${() => this.saveConfig(key)}
|
||||
type="highlighted"
|
||||
>
|
||||
Save
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => this.cancelEdit()}
|
||||
>
|
||||
Cancel
|
||||
</dees-button>
|
||||
` : html`
|
||||
<dees-button
|
||||
@click=${() => this.startEdit(key, config)}
|
||||
>
|
||||
Edit
|
||||
</dees-button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="sectionContent">
|
||||
${isEditing ? html`
|
||||
<textarea
|
||||
class="configEditor"
|
||||
@input=${(e) => this.editedConfig = e.target.value}
|
||||
.value=${JSON.stringify(config, null, 2)}
|
||||
></textarea>
|
||||
` : html`
|
||||
${this.renderConfigFields(config)}
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderConfigFields(config: any, prefix = '') {
|
||||
if (!config || typeof config !== 'object') {
|
||||
return html`<div class="fieldValue">${config}</div>`;
|
||||
}
|
||||
|
||||
return Object.entries(config).map(([key, value]) => {
|
||||
const fieldName = prefix ? `${prefix}.${key}` : key;
|
||||
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return html`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${fieldName}</label>
|
||||
${this.renderConfigFields(value, fieldName)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="configField">
|
||||
<label class="fieldLabel">${fieldName}</label>
|
||||
<div class="fieldValue">
|
||||
${Array.isArray(value) ? value.join(', ') : value}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
private startEdit(section: string, config: any) {
|
||||
this.editingSection = section;
|
||||
this.editedConfig = JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
private cancelEdit() {
|
||||
this.editingSection = null;
|
||||
this.editedConfig = null;
|
||||
}
|
||||
|
||||
private async saveConfig(section: string) {
|
||||
try {
|
||||
const parsedConfig = JSON.parse(this.editedConfig);
|
||||
|
||||
await appstate.configStatePart.dispatchAction(appstate.updateConfigurationAction, {
|
||||
section,
|
||||
config: parsedConfig,
|
||||
});
|
||||
|
||||
this.editingSection = null;
|
||||
this.editedConfig = null;
|
||||
|
||||
// Configuration updated successfully
|
||||
} catch (error) {
|
||||
console.error(`Error updating configuration:`, error);
|
||||
}
|
||||
}
|
||||
}
|
207
ts_web/elements/ops-view-logs.ts
Normal file
207
ts_web/elements/ops-view-logs.ts
Normal file
@ -0,0 +1,207 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-logs')
|
||||
export class OpsViewLogs extends DeesElement {
|
||||
@state()
|
||||
private logState: appstate.ILogState = {
|
||||
recentLogs: [],
|
||||
isStreaming: false,
|
||||
filters: {},
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.logStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((logState) => {
|
||||
this.logState = logState;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.logContainer {
|
||||
background: #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.logEntry {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logTimestamp {
|
||||
color: #7a7a7a;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logLevel {
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.logLevel.debug {
|
||||
color: #6a9955;
|
||||
background: rgba(106, 153, 85, 0.1);
|
||||
}
|
||||
.logLevel.info {
|
||||
color: #569cd6;
|
||||
background: rgba(86, 156, 214, 0.1);
|
||||
}
|
||||
.logLevel.warn {
|
||||
color: #ce9178;
|
||||
background: rgba(206, 145, 120, 0.1);
|
||||
}
|
||||
.logLevel.error {
|
||||
color: #f44747;
|
||||
background: rgba(244, 71, 71, 0.1);
|
||||
}
|
||||
|
||||
.logCategory {
|
||||
color: #c586c0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logMessage {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.noLogs {
|
||||
color: #7a7a7a;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Logs</ops-sectionheading>
|
||||
|
||||
<div class="controls">
|
||||
<div class="filterGroup">
|
||||
<dees-button
|
||||
@click=${() => this.fetchLogs()}
|
||||
>
|
||||
Refresh Logs
|
||||
</dees-button>
|
||||
|
||||
<dees-button
|
||||
@click=${() => this.toggleStreaming()}
|
||||
.type=${this.logState.isStreaming ? 'highlighted' : 'normal'}
|
||||
>
|
||||
${this.logState.isStreaming ? 'Stop Streaming' : 'Start Streaming'}
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Level:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['all', 'debug', 'info', 'warn', 'error']}
|
||||
.selectedOption=${'all'}
|
||||
@selectedOption=${(e) => this.updateFilter('level', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Category:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['all', 'smtp', 'dns', 'security', 'system', 'email']}
|
||||
.selectedOption=${'all'}
|
||||
@selectedOption=${(e) => this.updateFilter('category', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="filterGroup">
|
||||
<label>Limit:</label>
|
||||
<dees-input-dropdown
|
||||
.options=${['50', '100', '200', '500']}
|
||||
.selectedOption=${'100'}
|
||||
@selectedOption=${(e) => this.updateFilter('limit', e.detail)}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logContainer">
|
||||
${this.logState.recentLogs.length > 0 ?
|
||||
this.logState.recentLogs.map(log => html`
|
||||
<div class="logEntry">
|
||||
<span class="logTimestamp">${new Date(log.timestamp).toLocaleTimeString()}</span>
|
||||
<span class="logLevel ${log.level}">${log.level.toUpperCase()}</span>
|
||||
<span class="logCategory">[${log.category}]</span>
|
||||
<span class="logMessage">${log.message}</span>
|
||||
</div>
|
||||
`) : html`
|
||||
<div class="noLogs">No logs to display</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async fetchLogs() {
|
||||
const filters = this.getActiveFilters();
|
||||
await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, {
|
||||
limit: filters.limit || 100,
|
||||
level: filters.level as 'debug' | 'info' | 'warn' | 'error' | undefined,
|
||||
category: filters.category as 'smtp' | 'dns' | 'security' | 'system' | 'email' | undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private updateFilter(type: string, value: string) {
|
||||
if (value === 'all') {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
// Update filters then fetch logs
|
||||
this.fetchLogs();
|
||||
}
|
||||
|
||||
private getActiveFilters() {
|
||||
return {
|
||||
level: this.logState.filters.level?.[0],
|
||||
category: this.logState.filters.category?.[0],
|
||||
limit: 100,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleStreaming() {
|
||||
// TODO: Implement log streaming with VirtualStream
|
||||
console.log('Streaming toggle not yet implemented');
|
||||
}
|
||||
}
|
222
ts_web/elements/ops-view-overview.ts
Normal file
222
ts_web/elements/ops-view-overview.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-overview')
|
||||
export class OpsViewOverview extends DeesElement {
|
||||
@state()
|
||||
private statsState: appstate.IStatsState = {
|
||||
serverStats: null,
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.statsStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((statsState) => {
|
||||
this.statsState = statsState;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.statCard h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2196F3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chartGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-gap: 16px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
color: #c00;
|
||||
margin: 16px 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Overview</ops-sectionheading>
|
||||
|
||||
${this.statsState.isLoading ? html`
|
||||
<div class="loadingMessage">
|
||||
<dees-spinner></dees-spinner>
|
||||
<p>Loading statistics...</p>
|
||||
</div>
|
||||
` : this.statsState.error ? html`
|
||||
<div class="errorMessage">
|
||||
Error loading statistics: ${this.statsState.error}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="statsGrid">
|
||||
${this.statsState.serverStats ? html`
|
||||
<div class="statCard">
|
||||
<h3>Server Status</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}</div>
|
||||
<div class="statLabel">Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Connections</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
<div class="statLabel">Active connections</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Memory Usage</h3>
|
||||
<div class="statValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||||
<div class="statLabel">of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>CPU Usage</h3>
|
||||
<div class="statValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%</div>
|
||||
<div class="statLabel">Average load</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.statsState.emailStats ? html`
|
||||
<h2>Email Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>Emails Sent</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.sent}</div>
|
||||
<div class="statLabel">Total sent</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Emails Received</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.received}</div>
|
||||
<div class="statLabel">Total received</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Failed Deliveries</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.failed}</div>
|
||||
<div class="statLabel">Delivery failures</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Queued</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.queued}</div>
|
||||
<div class="statLabel">In queue</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.dnsStats ? html`
|
||||
<h2>DNS Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>DNS Queries</h3>
|
||||
<div class="statValue">${this.statsState.dnsStats.totalQueries}</div>
|
||||
<div class="statLabel">Total queries handled</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Cache Hit Rate</h3>
|
||||
<div class="statValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%</div>
|
||||
<div class="statLabel">Cache efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="chartGrid">
|
||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
||||
<dees-chart-area .label=${'DNS Queries (24h)'} .data=${[]}></dees-chart-area>
|
||||
<dees-chart-log .label=${'Recent Events'} .data=${[]}></dees-chart-log>
|
||||
<dees-chart-log .label=${'Security Alerts'} .data=${[]}></dees-chart-log>
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
}
|
471
ts_web/elements/ops-view-security.ts
Normal file
471
ts_web/elements/ops-view-security.ts
Normal file
@ -0,0 +1,471 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-security')
|
||||
export class OpsViewSecurity extends DeesElement {
|
||||
@state()
|
||||
private statsState: appstate.IStatsState = {
|
||||
serverStats: null,
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
private selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const subscription = appstate.statsStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((statsState) => {
|
||||
this.statsState = statsState;
|
||||
});
|
||||
this.rxSubscriptions.push(subscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 12px 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #2196F3;
|
||||
border-bottom-color: #2196F3;
|
||||
}
|
||||
|
||||
.securityGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.securityCard {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.securityCard.alert {
|
||||
border-color: #f44336;
|
||||
background: #ffebee;
|
||||
}
|
||||
|
||||
.securityCard.warning {
|
||||
border-color: #ff9800;
|
||||
background: #fff3e0;
|
||||
}
|
||||
|
||||
.securityCard.success {
|
||||
border-color: #4caf50;
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cardStatus {
|
||||
font-size: 14px;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-critical {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-good {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metricLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.blockedIpList {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.blockedIpItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.blockedIpItem:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.ipAddress {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.blockReason {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.blockTime {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Security</ops-sectionheading>
|
||||
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="tab ${this.selectedTab === 'overview' ? 'active' : ''}"
|
||||
@click=${() => this.selectedTab = 'overview'}
|
||||
>
|
||||
Overview
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.selectedTab === 'blocked' ? 'active' : ''}"
|
||||
@click=${() => this.selectedTab = 'blocked'}
|
||||
>
|
||||
Blocked IPs
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.selectedTab === 'authentication' ? 'active' : ''}"
|
||||
@click=${() => this.selectedTab = 'authentication'}
|
||||
>
|
||||
Authentication
|
||||
</button>
|
||||
<button
|
||||
class="tab ${this.selectedTab === 'email-security' ? 'active' : ''}"
|
||||
@click=${() => this.selectedTab = 'email-security'}
|
||||
>
|
||||
Email Security
|
||||
</button>
|
||||
</div>
|
||||
|
||||
${this.renderTabContent()}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTabContent() {
|
||||
const metrics = this.statsState.securityMetrics;
|
||||
|
||||
if (!metrics) {
|
||||
return html`
|
||||
<div class="loadingMessage">
|
||||
<p>Loading security metrics...</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
switch(this.selectedTab) {
|
||||
case 'overview':
|
||||
return this.renderOverview(metrics);
|
||||
case 'blocked':
|
||||
return this.renderBlockedIPs(metrics);
|
||||
case 'authentication':
|
||||
return this.renderAuthentication(metrics);
|
||||
case 'email-security':
|
||||
return this.renderEmailSecurity(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
private renderOverview(metrics: any) {
|
||||
const threatLevel = this.calculateThreatLevel(metrics);
|
||||
|
||||
return html`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard ${threatLevel}">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Threat Level</h3>
|
||||
<span class="cardStatus status-${threatLevel === 'alert' ? 'critical' : threatLevel === 'warning' ? 'warning' : 'good'}">
|
||||
${threatLevel.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metricValue">${this.getThreatScore(metrics)}/100</div>
|
||||
<div class="metricLabel">Overall security score</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Blocked Threats</h3>
|
||||
</div>
|
||||
<div class="metricValue">${metrics.blockedIPs.length + metrics.spamDetected}</div>
|
||||
<div class="metricLabel">Total threats blocked today</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Active Sessions</h3>
|
||||
</div>
|
||||
<div class="metricValue">${0}</div>
|
||||
<div class="metricLabel">Current authenticated sessions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Recent Security Events</h2>
|
||||
<dees-table
|
||||
.heading1=${'Security Events'}
|
||||
.heading2=${'Last 24 hours'}
|
||||
.data=${this.getSecurityEvents(metrics)}
|
||||
.displayFunction=${(item) => ({
|
||||
'Time': new Date(item.timestamp).toLocaleTimeString(),
|
||||
'Event': item.event,
|
||||
'Severity': item.severity,
|
||||
'Details': item.details,
|
||||
})}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBlockedIPs(metrics: any) {
|
||||
return html`
|
||||
<div class="securityCard">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Blocked IP Addresses</h3>
|
||||
<dees-button @click=${() => this.clearBlockedIPs()}>
|
||||
Clear All
|
||||
</dees-button>
|
||||
</div>
|
||||
|
||||
<div class="blockedIpList">
|
||||
${metrics.blockedIPs && metrics.blockedIPs.length > 0 ? metrics.blockedIPs.map((ipAddress, index) => html`
|
||||
<div class="blockedIpItem">
|
||||
<div>
|
||||
<div class="ipAddress">${ipAddress}</div>
|
||||
<div class="blockReason">Suspicious activity</div>
|
||||
<div class="blockTime">Blocked</div>
|
||||
</div>
|
||||
<dees-button @click=${() => this.unblockIP(ipAddress)}>
|
||||
Unblock
|
||||
</dees-button>
|
||||
</div>
|
||||
`) : html`
|
||||
<p>No blocked IPs</p>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAuthentication(metrics: any) {
|
||||
return html`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Authentication Statistics</h3>
|
||||
<div class="metricValue">${metrics.authenticationFailures}</div>
|
||||
<div class="metricLabel">Failed authentication attempts today</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Successful Logins</h3>
|
||||
<div class="metricValue">${0}</div>
|
||||
<div class="metricLabel">Successful logins today</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Recent Login Attempts</h2>
|
||||
<dees-table
|
||||
.heading1=${'Login History'}
|
||||
.heading2=${'Recent authentication attempts'}
|
||||
.data=${[]}
|
||||
.displayFunction=${(item) => ({
|
||||
'Time': new Date(item.timestamp).toLocaleString(),
|
||||
'Username': item.username,
|
||||
'IP Address': item.ipAddress,
|
||||
'Status': item.success ? 'Success' : 'Failed',
|
||||
'Reason': item.reason || '-',
|
||||
})}
|
||||
></dees-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderEmailSecurity(metrics: any) {
|
||||
return html`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Malware Detection</h3>
|
||||
<div class="metricValue">${metrics.malwareDetected}</div>
|
||||
<div class="metricLabel">Malware detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Phishing Detection</h3>
|
||||
<div class="metricValue">${metrics.phishingDetected}</div>
|
||||
<div class="metricLabel">Phishing attempts detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Suspicious Activities</h3>
|
||||
<div class="metricValue">${metrics.suspiciousActivities}</div>
|
||||
<div class="metricLabel">Suspicious activities detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Spam Detection</h3>
|
||||
<div class="metricValue">${metrics.spamDetected}</div>
|
||||
<div class="metricLabel">Spam emails blocked</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Email Security Configuration</h2>
|
||||
<div class="securityCard">
|
||||
<dees-form>
|
||||
<dees-input-checkbox
|
||||
.key=${'enableSPF'}
|
||||
.label=${'Enable SPF checking'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'enableDKIM'}
|
||||
.label=${'Enable DKIM validation'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'enableDMARC'}
|
||||
.label=${'Enable DMARC policy enforcement'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
<dees-input-checkbox
|
||||
.key=${'enableSpamFilter'}
|
||||
.label=${'Enable spam filtering'}
|
||||
.value=${true}
|
||||
></dees-input-checkbox>
|
||||
</dees-form>
|
||||
<dees-button
|
||||
class="actionButton"
|
||||
type="highlighted"
|
||||
@click=${() => this.saveEmailSecuritySettings()}
|
||||
>
|
||||
Save Settings
|
||||
</dees-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private calculateThreatLevel(metrics: any): string {
|
||||
const score = this.getThreatScore(metrics);
|
||||
if (score < 30) return 'alert';
|
||||
if (score < 70) return 'warning';
|
||||
return 'success';
|
||||
}
|
||||
|
||||
private getThreatScore(metrics: any): number {
|
||||
// Simple scoring algorithm
|
||||
let score = 100;
|
||||
score -= metrics.blockedIPs.length * 2;
|
||||
score -= metrics.authenticationFailures * 1;
|
||||
score -= metrics.spamDetected * 0.5;
|
||||
score -= metrics.malwareDetected * 3;
|
||||
score -= metrics.phishingDetected * 3;
|
||||
score -= metrics.suspiciousActivities * 2;
|
||||
return Math.max(0, Math.min(100, Math.round(score)));
|
||||
}
|
||||
|
||||
private getSecurityEvents(metrics: any): any[] {
|
||||
// Mock data - in real implementation, this would come from the server
|
||||
return [
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 5,
|
||||
event: 'Multiple failed login attempts',
|
||||
severity: 'warning',
|
||||
details: 'IP: 192.168.1.100',
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 15,
|
||||
event: 'SPF check failed',
|
||||
severity: 'medium',
|
||||
details: 'Domain: example.com',
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1000 * 60 * 30,
|
||||
event: 'IP blocked due to spam',
|
||||
severity: 'high',
|
||||
details: 'IP: 10.0.0.1',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async clearBlockedIPs() {
|
||||
console.log('Clear blocked IPs');
|
||||
}
|
||||
|
||||
private async unblockIP(ip: string) {
|
||||
console.log('Unblock IP:', ip);
|
||||
}
|
||||
|
||||
private async saveEmailSecuritySettings() {
|
||||
console.log('Save email security settings');
|
||||
}
|
||||
}
|
299
ts_web/elements/ops-view-stats.ts
Normal file
299
ts_web/elements/ops-view-stats.ts
Normal file
@ -0,0 +1,299 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as shared from './shared/index.js';
|
||||
import * as appstate from '../appstate.js';
|
||||
|
||||
import {
|
||||
DeesElement,
|
||||
customElement,
|
||||
html,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-view-stats')
|
||||
export class OpsViewStats extends DeesElement {
|
||||
@state()
|
||||
private statsState: appstate.IStatsState = {
|
||||
serverStats: null,
|
||||
emailStats: null,
|
||||
dnsStats: null,
|
||||
securityMetrics: null,
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@state()
|
||||
private uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
theme: 'light',
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const statsSubscription = appstate.statsStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((statsState) => {
|
||||
this.statsState = statsState;
|
||||
});
|
||||
this.rxSubscriptions.push(statsSubscription);
|
||||
|
||||
const uiSubscription = appstate.uiStatePart
|
||||
.select((stateArg) => stateArg)
|
||||
.subscribe((uiState) => {
|
||||
this.uiState = uiState;
|
||||
});
|
||||
this.rxSubscriptions.push(uiSubscription);
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.refreshButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.lastUpdated {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.statsSection {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 24px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metricsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.metricCard {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.metricCard:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.metricLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.metricUnit {
|
||||
font-size: 16px;
|
||||
color: #999;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.chartContainer {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
<ops-sectionheading>Statistics</ops-sectionheading>
|
||||
|
||||
<div class="controls">
|
||||
<div class="refreshButton">
|
||||
<dees-button
|
||||
@click=${() => appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null)}
|
||||
.disabled=${this.statsState.isLoading}
|
||||
>
|
||||
${this.statsState.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
|
||||
</dees-button>
|
||||
<dees-button
|
||||
@click=${() => appstate.uiStatePart.dispatchAction(appstate.toggleAutoRefreshAction, null)}
|
||||
.type=${this.uiState.autoRefresh ? 'highlighted' : 'normal'}
|
||||
>
|
||||
Auto-refresh: ${this.uiState.autoRefresh ? 'ON' : 'OFF'}
|
||||
</dees-button>
|
||||
</div>
|
||||
<div class="lastUpdated">
|
||||
${this.statsState.lastUpdated ? html`
|
||||
Last updated: ${new Date(this.statsState.lastUpdated).toLocaleTimeString()}
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${this.statsState.serverStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Server Metrics</h2>
|
||||
<div class="metricsGrid">
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Uptime</div>
|
||||
<div class="metricValue">${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">CPU Usage</div>
|
||||
<div class="metricValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}<span class="metricUnit">%</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Memory Used</div>
|
||||
<div class="metricValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Active Connections</div>
|
||||
<div class="metricValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chartContainer">
|
||||
<dees-chart-area
|
||||
.label=${'Server Performance (Last 24 Hours)'}
|
||||
.data=${[]}
|
||||
></dees-chart-area>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.emailStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Email Statistics</h2>
|
||||
<dees-table
|
||||
.heading1=${'Email Metrics'}
|
||||
.heading2=${'Current statistics for email processing'}
|
||||
.data=${[
|
||||
{ metric: 'Total Sent', value: this.statsState.emailStats.sent, unit: 'emails' },
|
||||
{ metric: 'Total Received', value: this.statsState.emailStats.received, unit: 'emails' },
|
||||
{ metric: 'Failed Deliveries', value: this.statsState.emailStats.failed, unit: 'emails' },
|
||||
{ metric: 'Currently Queued', value: this.statsState.emailStats.queued, unit: 'emails' },
|
||||
{ metric: 'Average Delivery Time', value: this.statsState.emailStats.averageDeliveryTime, unit: 'ms' },
|
||||
{ metric: 'Delivery Rate', value: `${Math.round(this.statsState.emailStats.deliveryRate * 100)}`, unit: '%' },
|
||||
]}
|
||||
.displayFunction=${(item) => ({
|
||||
Metric: item.metric,
|
||||
Value: `${item.value} ${item.unit}`,
|
||||
})}
|
||||
></dees-table>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.dnsStats ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">DNS Statistics</h2>
|
||||
<div class="metricsGrid">
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Total Queries</div>
|
||||
<div class="metricValue">${this.formatNumber(this.statsState.dnsStats.totalQueries)}</div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Cache Hit Rate</div>
|
||||
<div class="metricValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}<span class="metricUnit">%</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Average Response Time</div>
|
||||
<div class="metricValue">${this.statsState.dnsStats.averageResponseTime}<span class="metricUnit">ms</span></div>
|
||||
</div>
|
||||
<div class="metricCard">
|
||||
<div class="metricLabel">Domains Configured</div>
|
||||
<div class="metricValue">${this.statsState.dnsStats.activeDomains}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.securityMetrics ? html`
|
||||
<div class="statsSection">
|
||||
<h2 class="sectionTitle">Security Metrics</h2>
|
||||
<dees-table
|
||||
.heading1=${'Security Events'}
|
||||
.heading2=${'Recent security-related activities'}
|
||||
.data=${[
|
||||
{ metric: 'Blocked IPs', value: this.statsState.securityMetrics.blockedIPs.length, severity: 'high' },
|
||||
{ metric: 'Failed Authentications', value: this.statsState.securityMetrics.authenticationFailures, severity: 'medium' },
|
||||
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'low' },
|
||||
{ metric: 'Spam Detected', value: this.statsState.securityMetrics.spamDetected, severity: 'medium' },
|
||||
{ metric: 'Malware Detected', value: this.statsState.securityMetrics.malwareDetected, severity: 'high' },
|
||||
{ metric: 'Phishing Detected', value: this.statsState.securityMetrics.phishingDetected, severity: 'high' },
|
||||
]}
|
||||
.displayFunction=${(item) => ({
|
||||
'Security Metric': item.metric,
|
||||
'Count': item.value,
|
||||
'Severity': item.severity,
|
||||
})}
|
||||
></dees-table>
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
private formatUptime(seconds: number): string {
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
private formatBytes(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
private formatNumber(num: number): string {
|
||||
if (num >= 1000000) {
|
||||
return `${(num / 1000000).toFixed(1)}M`;
|
||||
} else if (num >= 1000) {
|
||||
return `${(num / 1000).toFixed(1)}K`;
|
||||
}
|
||||
return num.toString();
|
||||
}
|
||||
}
|
10
ts_web/elements/shared/css.ts
Normal file
10
ts_web/elements/shared/css.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { css } from '@design.estate/dees-element';
|
||||
|
||||
export const viewHostCss = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: auto;
|
||||
max-width: 1280px;
|
||||
padding: 16px 16px;
|
||||
}
|
||||
`;
|
2
ts_web/elements/shared/index.ts
Normal file
2
ts_web/elements/shared/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './css.js';
|
||||
export * from './ops-sectionheading.js';
|
42
ts_web/elements/shared/ops-sectionheading.ts
Normal file
42
ts_web/elements/shared/ops-sectionheading.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {
|
||||
DeesElement,
|
||||
css,
|
||||
cssManager,
|
||||
customElement,
|
||||
html,
|
||||
type TemplateResult
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
@customElement('ops-sectionheading')
|
||||
export class OpsSectionHeading extends DeesElement {
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-family: 'Cal Sans', 'Inter', sans-serif;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([theme="dark"]) .heading {
|
||||
color: #fff;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<h1 class="heading">
|
||||
<slot></slot>
|
||||
</h1>
|
||||
`;
|
||||
}
|
||||
}
|
@ -6,3 +6,6 @@ export {
|
||||
deesElement,
|
||||
deesCatalog
|
||||
}
|
||||
|
||||
// domtools gives us TypedRequest and other utilities
|
||||
export const domtools = deesElement.domtools;
|
||||
|
Reference in New Issue
Block a user