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:
Juergen Kunz
2025-06-08 12:03:17 +00:00
parent 5faca8c1b6
commit f1fb4c8495
13 changed files with 2064 additions and 47 deletions

View File

@ -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
---

View File

@ -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();
})();

View File

@ -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';

View File

@ -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>
`;
}
}

View 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);
}
}
}

View 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');
}
}

View 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]}`;
}
}

View 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');
}
}

View 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();
}
}

View 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;
}
`;

View File

@ -0,0 +1,2 @@
export * from './css.js';
export * from './ops-sectionheading.js';

View 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>
`;
}
}

View File

@ -6,3 +6,6 @@ export {
deesElement,
deesCatalog
}
// domtools gives us TypedRequest and other utilities
export const domtools = deesElement.domtools;