Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e78509ea4f | |||
| 8785536950 | |||
| dd4ac9fa3d | |||
| aed9151998 | |||
| 5d4bf4eff8 | |||
| 9027125520 | |||
| ee561c0823 | |||
| 95cb5d7840 | |||
| 2f46b3c9f3 | |||
| 7bd94884f4 | |||
| 405990563b | |||
| bf9f805c71 | |||
| 28cbf84f97 | |||
| d24e51117d | |||
| 92fde9d0d7 | |||
| b81bda6ce8 | |||
| 9b3f5c458d | |||
| 3ba47f9a71 | |||
| 2ab2e30336 | |||
| 8ce6c88d58 | |||
| facae93e4b | |||
| 0eb4963247 | |||
| 02dd3c77b5 | |||
| 93995d5031 |
42
package.json
42
package.json
@@ -12,51 +12,53 @@
|
|||||||
"test": "(tstest test/ --logfile --timeout 60)",
|
"test": "(tstest test/ --logfile --timeout 60)",
|
||||||
"start": "(node --max_old_space_size=250 ./cli.js)",
|
"start": "(node --max_old_space_size=250 ./cli.js)",
|
||||||
"startTs": "(node cli.ts.js)",
|
"startTs": "(node cli.ts.js)",
|
||||||
"build": "(tsbuild tsfolders --allowimplicitany && tsbundle website --production)"
|
"build": "(tsbuild tsfolders --allowimplicitany && npm run bundle)",
|
||||||
|
"bundle": "(tsbundle website --production --bundler=esbuild)"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.6.4",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.2.5",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^2.3.1",
|
"@git.zone/tstest": "^2.3.6",
|
||||||
"@git.zone/tswatch": "^2.0.1",
|
"@git.zone/tswatch": "^2.2.1",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22",
|
||||||
"node-forge": "^1.3.1"
|
"node-forge": "^1.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedrequest": "^3.0.19",
|
"@api.global/typedrequest": "^3.0.19",
|
||||||
"@api.global/typedrequest-interfaces": "^3.0.19",
|
"@api.global/typedrequest-interfaces": "^3.0.19",
|
||||||
"@api.global/typedserver": "^3.0.74",
|
"@api.global/typedserver": "^3.0.79",
|
||||||
"@api.global/typedsocket": "^3.0.0",
|
"@api.global/typedsocket": "^3.0.0",
|
||||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||||
"@design.estate/dees-catalog": "^1.8.0",
|
"@design.estate/dees-catalog": "^1.10.10",
|
||||||
"@design.estate/dees-element": "^2.0.42",
|
"@design.estate/dees-element": "^2.1.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.1",
|
"@push.rocks/projectinfo": "^5.0.1",
|
||||||
"@push.rocks/qenv": "^6.1.0",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^8.0.0",
|
"@push.rocks/smartacme": "^8.0.0",
|
||||||
"@push.rocks/smartdata": "^5.15.1",
|
"@push.rocks/smartdata": "^5.16.4",
|
||||||
"@push.rocks/smartdns": "^7.5.0",
|
"@push.rocks/smartdns": "^7.5.0",
|
||||||
"@push.rocks/smartfile": "^11.2.5",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.8",
|
"@push.rocks/smartlog": "^3.1.9",
|
||||||
"@push.rocks/smartmail": "^2.1.0",
|
"@push.rocks/smartmail": "^2.1.0",
|
||||||
"@push.rocks/smartnetwork": "^4.0.2",
|
"@push.rocks/smartmetrics": "^2.0.10",
|
||||||
"@push.rocks/smartpath": "^5.0.5",
|
"@push.rocks/smartnetwork": "^4.1.2",
|
||||||
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.0.3",
|
"@push.rocks/smartpromise": "^4.0.3",
|
||||||
"@push.rocks/smartproxy": "^19.5.26",
|
"@push.rocks/smartproxy": "21.1.7",
|
||||||
"@push.rocks/smartrequest": "^2.1.0",
|
"@push.rocks/smartrequest": "^2.1.0",
|
||||||
"@push.rocks/smartrule": "^2.0.1",
|
"@push.rocks/smartrule": "^2.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.0.0",
|
"@push.rocks/smartstate": "^2.0.26",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/interfaces": "^5.0.4",
|
"@serve.zone/interfaces": "^5.0.4",
|
||||||
"@tsclass/tsclass": "^9.2.0",
|
"@tsclass/tsclass": "^9.2.0",
|
||||||
"@types/mailparser": "^3.4.6",
|
"@types/mailparser": "^3.4.6",
|
||||||
"ip": "^2.0.1",
|
"ip": "^2.0.1",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.2.1",
|
||||||
"mailauth": "^4.8.6",
|
"mailauth": "^4.9.4",
|
||||||
"mailparser": "^3.7.3",
|
"mailparser": "^3.7.4",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
5152
pnpm-lock.yaml
generated
5152
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
135
readme.hints.md
135
readme.hints.md
@@ -1,5 +1,30 @@
|
|||||||
# Implementation Hints and Learnings
|
# Implementation Hints and Learnings
|
||||||
|
|
||||||
|
## Network Metrics Implementation (2025-06-23)
|
||||||
|
|
||||||
|
### SmartProxy Metrics API Integration
|
||||||
|
- Updated to use new SmartProxy metrics API (v19.6.7)
|
||||||
|
- Use `getMetrics()` for detailed metrics with grouped methods:
|
||||||
|
```typescript
|
||||||
|
const metrics = smartProxy.getMetrics();
|
||||||
|
metrics.connections.active() // Current active connections
|
||||||
|
metrics.throughput.instant() // Real-time throughput {in, out}
|
||||||
|
metrics.connections.topIPs(10) // Top 10 IPs by connection count
|
||||||
|
```
|
||||||
|
- Use `getStatistics()` for basic stats
|
||||||
|
|
||||||
|
### Network Traffic Display
|
||||||
|
- All throughput values shown in bits per second (kbit/s, Mbit/s, Gbit/s)
|
||||||
|
- Conversion: `bytesPerSecond * 8 / 1000000` for Mbps
|
||||||
|
- Network graph shows separate lines for inbound (green) and outbound (purple)
|
||||||
|
- Throughput tiles and graph use same data source for consistency
|
||||||
|
|
||||||
|
### Requests/sec vs Connections
|
||||||
|
- Requests/sec shows HTTP request counts (derived from connections)
|
||||||
|
- Single connection can handle multiple requests
|
||||||
|
- Current implementation tracks connections, not individual requests
|
||||||
|
- Trend line shows historical request counts, not throughput
|
||||||
|
|
||||||
## DKIM Implementation Status (2025-05-30)
|
## DKIM Implementation Status (2025-05-30)
|
||||||
|
|
||||||
### Current Implementation
|
### Current Implementation
|
||||||
@@ -904,3 +929,113 @@ The DNS functionality has been refactored from UnifiedEmailServer to a dedicated
|
|||||||
- Clear separation between DNS management and email server logic
|
- Clear separation between DNS management and email server logic
|
||||||
- UnifiedEmailServer is simpler and more focused
|
- UnifiedEmailServer is simpler and more focused
|
||||||
- All DNS-related tests pass successfully
|
- All DNS-related tests pass successfully
|
||||||
|
|
||||||
|
## SmartMetrics Integration (2025-06-12) - COMPLETED
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Fixed the UI metrics display to show accurate CPU and memory data from SmartMetrics.
|
||||||
|
|
||||||
|
### Key Findings
|
||||||
|
1. **CPU Metrics:**
|
||||||
|
- SmartMetrics provides `cpuUsageText` as a string percentage
|
||||||
|
- MetricsManager parses it as `cpuUsage.user` (system is always 0)
|
||||||
|
- UI was incorrectly dividing by 2, showing half the actual CPU usage
|
||||||
|
|
||||||
|
2. **Memory Metrics:**
|
||||||
|
- SmartMetrics calculates `maxMemoryMB` as minimum of:
|
||||||
|
- V8 heap size limit
|
||||||
|
- System total memory
|
||||||
|
- Docker memory limit (if available)
|
||||||
|
- Provides `memoryUsageBytes` (total process memory including children)
|
||||||
|
- Provides `memoryPercentage` (pre-calculated percentage)
|
||||||
|
- UI was only showing heap usage, missing actual memory constraints
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
1. **MetricsManager Enhanced:**
|
||||||
|
- Added `maxMemoryMB` from SmartMetrics instance
|
||||||
|
- Added `actualUsageBytes` from SmartMetrics data
|
||||||
|
- Added `actualUsagePercentage` from SmartMetrics data
|
||||||
|
- Kept existing memory fields for compatibility
|
||||||
|
|
||||||
|
2. **Interface Updated:**
|
||||||
|
- Added optional fields to `IServerStats.memoryUsage`
|
||||||
|
- Fields are optional to maintain backward compatibility
|
||||||
|
|
||||||
|
3. **UI Fixed:**
|
||||||
|
- Removed incorrect CPU division by 2
|
||||||
|
- Uses `actualUsagePercentage` when available (falls back to heap percentage)
|
||||||
|
- Shows actual memory usage vs max memory limit (not just heap)
|
||||||
|
|
||||||
|
### Result
|
||||||
|
- CPU now shows accurate usage percentage
|
||||||
|
- Memory shows percentage of actual constraints (Docker/system/V8 limits)
|
||||||
|
- Better monitoring for containerized environments
|
||||||
|
|
||||||
|
## Network UI Implementation (2025-06-20) - COMPLETED
|
||||||
|
|
||||||
|
### Overview
|
||||||
|
Revamped the Network UI to display real network data from SmartProxy instead of mock data.
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
1. **MetricsManager Integration:**
|
||||||
|
- Already integrates with SmartProxy via `dcRouter.smartProxy.getStats()`
|
||||||
|
- Extended with `getNetworkStats()` method to expose unused metrics:
|
||||||
|
- `getConnectionsByIP()` - Connection counts by IP address
|
||||||
|
- `getThroughputRate()` - Real-time bandwidth rates (bytes/second)
|
||||||
|
- `getTopIPs()` - Top connecting IPs sorted by connection count
|
||||||
|
- Note: SmartProxy base interface doesn't include all methods, manual implementation required
|
||||||
|
|
||||||
|
2. **Existing Infrastructure Leveraged:**
|
||||||
|
- `getActiveConnections` endpoint already exists in security.handler.ts
|
||||||
|
- Enhanced to include real SmartProxy data via MetricsManager
|
||||||
|
- IConnectionInfo interface already supports network data structures
|
||||||
|
|
||||||
|
3. **State Management:**
|
||||||
|
- Added `INetworkState` interface following existing patterns
|
||||||
|
- Created `networkStatePart` with connections, throughput, and IP data
|
||||||
|
- Integrated with existing auto-refresh mechanism
|
||||||
|
|
||||||
|
4. **UI Changes (Minimal):**
|
||||||
|
- Removed `generateMockData()` method and all mock generation
|
||||||
|
- Connected to real `networkStatePart` state
|
||||||
|
- Added `renderTopIPs()` section to display top connected IPs
|
||||||
|
- Updated traffic chart to show real request data
|
||||||
|
- Kept all existing UI components (DeesTable, DeesChartArea)
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
1. **Data Transformation:**
|
||||||
|
- Converts IConnectionInfo[] to INetworkRequest[] for table display
|
||||||
|
- Calculates traffic buckets based on selected time range
|
||||||
|
- Maps connection data to chart-compatible format
|
||||||
|
|
||||||
|
2. **Real Metrics Displayed:**
|
||||||
|
- Active connections count (from server stats)
|
||||||
|
- Requests per second (calculated from recent connections)
|
||||||
|
- Throughput rates (currently showing 0 until SmartProxy exposes rates)
|
||||||
|
- Top IPs with connection counts and percentages
|
||||||
|
|
||||||
|
3. **TypeScript Fixes:**
|
||||||
|
- SmartProxy methods like `getThroughputRate()` not in base interface
|
||||||
|
- Implemented manual fallbacks for missing methods
|
||||||
|
- Fixed `publicIpv4` → `publicIp` property name
|
||||||
|
|
||||||
|
### Result
|
||||||
|
- Network view now shows real connection activity
|
||||||
|
- Auto-refreshes with other stats every second
|
||||||
|
- Displays actual IPs and connection counts
|
||||||
|
- No more mock/demo data
|
||||||
|
- Minimal code changes (streamlined approach)
|
||||||
|
|
||||||
|
### Throughput Data Fix (2025-06-20)
|
||||||
|
The throughput was showing 0 because:
|
||||||
|
1. MetricsManager was hardcoding throughputRate to 0, assuming the method didn't exist
|
||||||
|
2. SmartProxy's `getStats()` returns `IProxyStats` interface, but the actual object (`MetricsCollector`) implements `IProxyStatsExtended`
|
||||||
|
3. `getThroughputRate()` only exists in the extended interface
|
||||||
|
|
||||||
|
**Solution implemented:**
|
||||||
|
1. Updated MetricsManager to check if methods exist at runtime and call them
|
||||||
|
2. Added property name mapping (`bytesInPerSec` → `bytesInPerSecond`)
|
||||||
|
3. Created new `getNetworkStats` endpoint in security.handler.ts
|
||||||
|
4. Updated frontend to call the new endpoint for complete network metrics
|
||||||
|
|
||||||
|
The throughput data now flows correctly from SmartProxy → MetricsManager → API → UI.
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# dcrouter
|
# dcrouter
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
**dcrouter: a traffic router intended to be gating your datacenter.**
|
**dcrouter: a traffic router intended to be gating your datacenter.**
|
||||||
|
|
||||||
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), and DNS protocols. Designed for enterprises requiring robust traffic management, automatic certificate provisioning, and enterprise-grade email infrastructure.
|
A comprehensive traffic routing solution that provides unified gateway capabilities for HTTP/HTTPS, TCP/SNI, email (SMTP), and DNS protocols. Designed for enterprises requiring robust traffic management, automatic certificate provisioning, and enterprise-grade email infrastructure.
|
||||||
|
|||||||
202
readme.metrics.md
Normal file
202
readme.metrics.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# Metrics Implementation Plan with @push.rocks/smartmetrics
|
||||||
|
|
||||||
|
Command to reread CLAUDE.md: `cat /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This plan outlines the migration from placeholder/demo metrics to real metrics using @push.rocks/smartmetrics for the dcrouter project.
|
||||||
|
|
||||||
|
## Current State Analysis
|
||||||
|
|
||||||
|
### Currently Implemented (Real Data)
|
||||||
|
- CPU usage (basic calculation from os.loadavg)
|
||||||
|
- Memory usage (from process.memoryUsage)
|
||||||
|
- System uptime
|
||||||
|
|
||||||
|
### Currently Stubbed (Returns 0 or Demo Data)
|
||||||
|
- Active connections (HTTP/HTTPS/WebSocket)
|
||||||
|
- Total connections
|
||||||
|
- Requests per second
|
||||||
|
- Email statistics (sent/received/failed/queued/bounce rate)
|
||||||
|
- DNS statistics (queries/cache hits/response times)
|
||||||
|
- Security metrics (blocked IPs/auth failures/spam detection)
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Core Infrastructure Setup
|
||||||
|
|
||||||
|
1. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
pnpm install --save @push.rocks/smartmetrics
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update plugins.ts**
|
||||||
|
- Add smartmetrics to ts/plugins.ts
|
||||||
|
- Import as: `import * as smartmetrics from '@push.rocks/smartmetrics';`
|
||||||
|
|
||||||
|
3. **Create Metrics Manager Class**
|
||||||
|
- Location: `ts/monitoring/classes.metricsmanager.ts`
|
||||||
|
- Initialize SmartMetrics with existing logger
|
||||||
|
- Configure for dcrouter service identification
|
||||||
|
- Set up automatic metric collection intervals
|
||||||
|
|
||||||
|
### Phase 2: Connection Tracking Implementation
|
||||||
|
|
||||||
|
1. **HTTP/HTTPS Connection Tracking**
|
||||||
|
- Instrument the SmartProxy connection handlers
|
||||||
|
- Track active connections in real-time
|
||||||
|
- Monitor connection lifecycle (open/close events)
|
||||||
|
- Location: Update connection managers in routing system
|
||||||
|
|
||||||
|
2. **Email Connection Tracking**
|
||||||
|
- Instrument SMTP server connection handlers
|
||||||
|
- Track both incoming and outgoing connections
|
||||||
|
- Location: `ts/mail/delivery/smtpserver/connection-manager.ts`
|
||||||
|
|
||||||
|
3. **DNS Query Tracking**
|
||||||
|
- Instrument DNS server handlers
|
||||||
|
- Track query counts and response times
|
||||||
|
- Location: `ts/mail/routing/classes.dns.manager.ts`
|
||||||
|
|
||||||
|
### Phase 3: Email Metrics Collection
|
||||||
|
|
||||||
|
1. **Email Processing Metrics**
|
||||||
|
- Track sent/received/failed emails
|
||||||
|
- Monitor queue sizes
|
||||||
|
- Calculate delivery and bounce rates
|
||||||
|
- Location: Instrument `classes.delivery.queue.ts` and `classes.emailsendjob.ts`
|
||||||
|
|
||||||
|
2. **Email Performance Metrics**
|
||||||
|
- Track processing times
|
||||||
|
- Monitor queue throughput
|
||||||
|
- Location: Update delivery system classes
|
||||||
|
|
||||||
|
### Phase 4: Security Metrics Integration
|
||||||
|
|
||||||
|
1. **Security Event Tracking**
|
||||||
|
- Track blocked IPs from IPReputationChecker
|
||||||
|
- Monitor authentication failures
|
||||||
|
- Count spam/malware/phishing detections
|
||||||
|
- Location: Instrument security classes in `ts/security/`
|
||||||
|
|
||||||
|
### Phase 5: Stats Handler Refactoring
|
||||||
|
|
||||||
|
1. **Update Stats Handler**
|
||||||
|
- Location: `ts/opsserver/handlers/stats.handler.ts`
|
||||||
|
- Replace all stub implementations with MetricsManager calls
|
||||||
|
- Maintain existing API interface structure
|
||||||
|
|
||||||
|
2. **Metrics Aggregation**
|
||||||
|
- Implement proper time-window aggregations
|
||||||
|
- Add historical data storage (last hour/day)
|
||||||
|
- Calculate rates and percentages accurately
|
||||||
|
|
||||||
|
### Phase 6: Prometheus Integration (Optional Enhancement)
|
||||||
|
|
||||||
|
1. **Enable Prometheus Endpoint**
|
||||||
|
- Add Prometheus metrics endpoint
|
||||||
|
- Configure port (default: 9090)
|
||||||
|
- Document metrics for monitoring systems
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### MetricsManager Core Structure
|
||||||
|
```typescript
|
||||||
|
export class MetricsManager {
|
||||||
|
private smartMetrics: smartmetrics.SmartMetrics;
|
||||||
|
private connectionTrackers: Map<string, ConnectionTracker>;
|
||||||
|
private emailMetrics: EmailMetricsCollector;
|
||||||
|
private dnsMetrics: DnsMetricsCollector;
|
||||||
|
private securityMetrics: SecurityMetricsCollector;
|
||||||
|
|
||||||
|
// Real-time counters
|
||||||
|
private activeConnections = {
|
||||||
|
http: 0,
|
||||||
|
https: 0,
|
||||||
|
websocket: 0,
|
||||||
|
smtp: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize and start collection
|
||||||
|
public async start(): Promise<void>;
|
||||||
|
|
||||||
|
// Get aggregated metrics for stats handler
|
||||||
|
public async getServerStats(): Promise<IServerStats>;
|
||||||
|
public async getEmailStats(): Promise<IEmailStats>;
|
||||||
|
public async getDnsStats(): Promise<IDnsStats>;
|
||||||
|
public async getSecurityStats(): Promise<ISecurityStats>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Tracking Pattern
|
||||||
|
```typescript
|
||||||
|
// Example for HTTP connections
|
||||||
|
onConnectionOpen(type: string) {
|
||||||
|
this.activeConnections[type]++;
|
||||||
|
this.totalConnections[type]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnectionClose(type: string) {
|
||||||
|
this.activeConnections[type]--;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Email Metrics Pattern
|
||||||
|
```typescript
|
||||||
|
// Track email events
|
||||||
|
onEmailSent() { this.emailsSentToday++; }
|
||||||
|
onEmailReceived() { this.emailsReceivedToday++; }
|
||||||
|
onEmailFailed() { this.emailsFailedToday++; }
|
||||||
|
onEmailQueued() { this.queueSize++; }
|
||||||
|
onEmailDequeued() { this.queueSize--; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Unit Tests**
|
||||||
|
- Test MetricsManager initialization
|
||||||
|
- Test metric collection accuracy
|
||||||
|
- Test aggregation calculations
|
||||||
|
|
||||||
|
2. **Integration Tests**
|
||||||
|
- Test metrics flow from source to API
|
||||||
|
- Verify real-time updates
|
||||||
|
- Test under load conditions
|
||||||
|
|
||||||
|
3. **Debug Utilities**
|
||||||
|
- Create `.nogit/debug/test-metrics.ts` for quick testing
|
||||||
|
- Add metrics dump endpoint for debugging
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
1. Implement MetricsManager without breaking existing code
|
||||||
|
2. Wire up one metric type at a time
|
||||||
|
3. Verify each metric shows real data
|
||||||
|
4. Remove TODO comments from stats handler
|
||||||
|
5. Update tests to expect real values
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- [ ] All metrics show real, accurate data
|
||||||
|
- [ ] No performance degradation
|
||||||
|
- [ ] Metrics update in real-time
|
||||||
|
- [ ] Historical data is collected
|
||||||
|
- [ ] All TODO comments removed from stats handler
|
||||||
|
- [ ] Tests pass with real metric values
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- SmartMetrics provides CPU and memory metrics out of the box
|
||||||
|
- We'll need custom collectors for application-specific metrics
|
||||||
|
- Consider adding metric persistence for historical data
|
||||||
|
- Prometheus integration provides industry-standard monitoring
|
||||||
|
|
||||||
|
## Questions to Address
|
||||||
|
|
||||||
|
1. Should we persist metrics to disk for historical analysis?
|
||||||
|
2. What time windows should we support (5min, 1hour, 1day)?
|
||||||
|
3. Should we add alerting thresholds?
|
||||||
|
4. Do we need custom metric types beyond the current interface?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This plan ensures a systematic migration from demo metrics to real, actionable data using @push.rocks/smartmetrics while maintaining the existing API structure and adding powerful monitoring capabilities.
|
||||||
173
readme.module-adjustments.md
Normal file
173
readme.module-adjustments.md
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
# Module Adjustments for Metrics Collection
|
||||||
|
|
||||||
|
Command to reread CLAUDE.md: `cat /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
||||||
|
|
||||||
|
## SmartProxy Adjustments
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
SmartProxy (@push.rocks/smartproxy) provides:
|
||||||
|
- Route-level `maxConnections` limiting
|
||||||
|
- Event emission system (currently only for certificates)
|
||||||
|
- NFTables integration with packet statistics
|
||||||
|
- Connection monitoring during active sessions
|
||||||
|
|
||||||
|
### Missing Capabilities for Metrics
|
||||||
|
1. **No Connection Lifecycle Events**
|
||||||
|
- No `connection-open` or `connection-close` events
|
||||||
|
- No way to track active connections in real-time
|
||||||
|
- No exposure of internal connection tracking
|
||||||
|
|
||||||
|
2. **No Statistics API**
|
||||||
|
- No methods like `getActiveConnections()` or `getConnectionStats()`
|
||||||
|
- No access to connection counts per route
|
||||||
|
- No throughput or performance metrics exposed
|
||||||
|
|
||||||
|
3. **Limited Event System**
|
||||||
|
- Currently only emits certificate-related events
|
||||||
|
- No connection, request, or performance events
|
||||||
|
|
||||||
|
### Required Adjustments
|
||||||
|
1. **Add Connection Tracking Events**
|
||||||
|
```typescript
|
||||||
|
// Emit on new connection
|
||||||
|
smartProxy.emit('connection-open', {
|
||||||
|
type: 'http' | 'https' | 'websocket',
|
||||||
|
routeName: string,
|
||||||
|
clientIp: string,
|
||||||
|
timestamp: Date
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emit on connection close
|
||||||
|
smartProxy.emit('connection-close', {
|
||||||
|
connectionId: string,
|
||||||
|
duration: number,
|
||||||
|
bytesTransferred: number
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add Statistics API**
|
||||||
|
```typescript
|
||||||
|
interface IProxyStats {
|
||||||
|
getActiveConnections(): number;
|
||||||
|
getConnectionsByRoute(): Map<string, number>;
|
||||||
|
getTotalConnections(): number;
|
||||||
|
getRequestsPerSecond(): number;
|
||||||
|
getThroughput(): { bytesIn: number, bytesOut: number };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Expose Internal Metrics**
|
||||||
|
- Make connection pools accessible
|
||||||
|
- Expose route-level statistics
|
||||||
|
- Provide request/response metrics
|
||||||
|
|
||||||
|
### Alternative Approach
|
||||||
|
Since SmartProxy is already used with socket handlers for email routing, we could:
|
||||||
|
1. Wrap all SmartProxy socket handlers with a metrics-aware wrapper
|
||||||
|
2. Use the existing socket-handler pattern to intercept all connections
|
||||||
|
3. Track connections at the dcrouter level rather than modifying SmartProxy
|
||||||
|
|
||||||
|
## SmartDNS Adjustments
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
SmartDNS (@push.rocks/smartdns) provides:
|
||||||
|
- DNS query handling via registered handlers
|
||||||
|
- Support for UDP (port 53) and DNS-over-HTTPS
|
||||||
|
- Domain pattern matching and routing
|
||||||
|
- DNSSEC support
|
||||||
|
|
||||||
|
### Missing Capabilities for Metrics
|
||||||
|
1. **No Query Tracking**
|
||||||
|
- No counters for total queries
|
||||||
|
- No breakdown by query type (A, AAAA, MX, etc.)
|
||||||
|
- No domain popularity tracking
|
||||||
|
|
||||||
|
2. **No Performance Metrics**
|
||||||
|
- No response time tracking
|
||||||
|
- No cache hit/miss statistics
|
||||||
|
- No error rate tracking
|
||||||
|
|
||||||
|
3. **No Event Emission**
|
||||||
|
- No query lifecycle events
|
||||||
|
- No cache events
|
||||||
|
- No error events
|
||||||
|
|
||||||
|
### Required Adjustments
|
||||||
|
1. **Add Query Interceptor/Middleware**
|
||||||
|
```typescript
|
||||||
|
// Wrap handler registration to add metrics
|
||||||
|
smartDns.use((query, next) => {
|
||||||
|
metricsCollector.trackQuery(query);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
next((response) => {
|
||||||
|
metricsCollector.trackResponse(response, Date.now() - startTime);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add Event Emissions**
|
||||||
|
```typescript
|
||||||
|
// Query events
|
||||||
|
smartDns.emit('query-received', {
|
||||||
|
type: query.type,
|
||||||
|
domain: query.domain,
|
||||||
|
source: 'udp' | 'https',
|
||||||
|
clientIp: string
|
||||||
|
});
|
||||||
|
|
||||||
|
smartDns.emit('query-answered', {
|
||||||
|
cached: boolean,
|
||||||
|
responseTime: number,
|
||||||
|
responseCode: string
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add Statistics API**
|
||||||
|
```typescript
|
||||||
|
interface IDnsStats {
|
||||||
|
getTotalQueries(): number;
|
||||||
|
getQueriesPerSecond(): number;
|
||||||
|
getCacheStats(): { hits: number, misses: number, hitRate: number };
|
||||||
|
getTopDomains(limit: number): Array<{ domain: string, count: number }>;
|
||||||
|
getQueryTypeBreakdown(): Record<string, number>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternative Approach
|
||||||
|
Since we control the handler registration in dcrouter:
|
||||||
|
1. Create a metrics-aware handler wrapper at the dcrouter level
|
||||||
|
2. Wrap all DNS handlers before registration
|
||||||
|
3. Track metrics without modifying SmartDNS itself
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Option 1: Fork and Modify Dependencies
|
||||||
|
- Fork @push.rocks/smartproxy and @push.rocks/smartdns
|
||||||
|
- Add metrics capabilities directly
|
||||||
|
- Maintain custom versions
|
||||||
|
- **Pros**: Clean integration, full control
|
||||||
|
- **Cons**: Maintenance burden, divergence from upstream
|
||||||
|
|
||||||
|
### Option 2: Wrapper Approach at DcRouter Level
|
||||||
|
- Create wrapper classes that intercept all operations
|
||||||
|
- Track metrics at the application level
|
||||||
|
- No modifications to dependencies
|
||||||
|
- **Pros**: No dependency modifications, easier to maintain
|
||||||
|
- **Cons**: May miss some internal events, slightly higher overhead
|
||||||
|
|
||||||
|
### Option 3: Contribute Back to Upstream
|
||||||
|
- Submit PRs to add metrics capabilities to original packages
|
||||||
|
- Work with maintainers to add event emissions and stats APIs
|
||||||
|
- **Pros**: Benefits everyone, no fork maintenance
|
||||||
|
- **Cons**: Slower process, may not align with maintainer vision
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
**Use Option 2 (Wrapper Approach)** for immediate implementation:
|
||||||
|
1. Create `MetricsAwareProxy` and `MetricsAwareDns` wrapper classes
|
||||||
|
2. Intercept all operations and track metrics
|
||||||
|
3. Minimal changes to existing codebase
|
||||||
|
4. Can migrate to Option 3 later if upstream accepts contributions
|
||||||
|
|
||||||
|
This approach allows us to implement comprehensive metrics collection without modifying external dependencies, maintaining compatibility and reducing maintenance burden.
|
||||||
@@ -1,351 +0,0 @@
|
|||||||
# DCRouter OpsServer Implementation Plan
|
|
||||||
|
|
||||||
**Command to reread CLAUDE.md: `cat /home/philkunz/.claude/CLAUDE.md`**
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This document outlines the implementation plan for adding a TypedRequest-based API to the DCRouter OpsServer, following the patterns established in the cloudly project. The goal is to create a type-safe, reactive management dashboard with real-time statistics and monitoring capabilities.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
The implementation follows a clear separation of concerns:
|
|
||||||
- **Backend**: TypedRequest handlers in OpsServer
|
|
||||||
- **Frontend**: Reactive web components with Smartstate
|
|
||||||
- **Communication**: Type-safe requests via TypedRequest pattern
|
|
||||||
- **State Management**: Centralized state with reactive updates
|
|
||||||
|
|
||||||
## Implementation Phases
|
|
||||||
|
|
||||||
### Phase 1: Interface Definition ✓
|
|
||||||
|
|
||||||
Create TypeScript interfaces for all API operations:
|
|
||||||
|
|
||||||
#### Directory Structure ✓
|
|
||||||
```
|
|
||||||
ts_interfaces/
|
|
||||||
plugins.ts # TypedRequest interfaces import
|
|
||||||
data/ # Data type definitions
|
|
||||||
auth.ts # IIdentity interface
|
|
||||||
stats.ts # Server, Email, DNS, Security types
|
|
||||||
index.ts # Exports
|
|
||||||
requests/ # Request interfaces
|
|
||||||
admin.ts # Authentication requests
|
|
||||||
config.ts # Configuration management
|
|
||||||
logs.ts # Log retrieval with IVirtualStream
|
|
||||||
stats.ts # Statistics endpoints
|
|
||||||
index.ts # Exports
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Key Interfaces Defined ✓
|
|
||||||
- **Server Statistics**
|
|
||||||
- [x] `IReq_GetServerStatistics` - Server metrics with history
|
|
||||||
|
|
||||||
- **Email Operations**
|
|
||||||
- [x] `IReq_GetEmailStatistics` - Email delivery stats
|
|
||||||
- [x] `IReq_GetQueueStatus` - Queue monitoring
|
|
||||||
|
|
||||||
- **DNS Management**
|
|
||||||
- [x] `IReq_GetDnsStatistics` - DNS query metrics
|
|
||||||
|
|
||||||
- **Rate Limiting**
|
|
||||||
- [x] `IReq_GetRateLimitStatus` - Rate limit info
|
|
||||||
|
|
||||||
- **Security Metrics**
|
|
||||||
- [x] `IReq_GetSecurityMetrics` - Security stats and trends
|
|
||||||
- [x] `IReq_GetActiveConnections` - Connection monitoring
|
|
||||||
|
|
||||||
- **Logging**
|
|
||||||
- [x] `IReq_GetRecentLogs` - Paginated log retrieval
|
|
||||||
- [x] `IReq_GetLogStream` - Real-time log streaming with IVirtualStream
|
|
||||||
|
|
||||||
- **Configuration**
|
|
||||||
- [x] `IReq_GetConfiguration` - Read config
|
|
||||||
- [x] `IReq_UpdateConfiguration` - Update config
|
|
||||||
|
|
||||||
- **Authentication**
|
|
||||||
- [x] `IReq_AdminLoginWithUsernameAndPassword` - Admin login
|
|
||||||
- [x] `IReq_AdminLogout` - Logout
|
|
||||||
- [x] `IReq_VerifyIdentity` - Token verification
|
|
||||||
|
|
||||||
- **Health Check**
|
|
||||||
- [x] `IReq_GetHealthStatus` - Service health monitoring
|
|
||||||
|
|
||||||
### Phase 2: Backend Implementation ✓
|
|
||||||
|
|
||||||
#### 2.1 Enhance OpsServer (`ts/opsserver/classes.opsserver.ts`) ✓
|
|
||||||
|
|
||||||
- [x] Add TypedRouter initialization
|
|
||||||
- [x] Use TypedServer's built-in typedrouter
|
|
||||||
- [x] CORS is already handled by TypedServer
|
|
||||||
- [x] Add handler registration method
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Example structure following cloudly pattern
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private dcRouterRef: DcRouter) {
|
|
||||||
// Add our typedrouter to the dcRouter's main typedrouter
|
|
||||||
this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async start() {
|
|
||||||
// TypedServer already has a built-in typedrouter at /typedrequest
|
|
||||||
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
|
|
||||||
domain: 'localhost',
|
|
||||||
feedMetadata: null,
|
|
||||||
serveDir: paths.distServe,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The server's typedrouter is automatically available
|
|
||||||
// Add the main dcRouter typedrouter to the server's typedrouter
|
|
||||||
this.server.typedrouter.addTypedRouter(this.dcRouterRef.typedrouter);
|
|
||||||
|
|
||||||
this.setupHandlers();
|
|
||||||
await this.server.start(3000);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: TypedServer automatically provides the `/typedrequest` endpoint with its built-in typedrouter. We just need to add our routers to it using the `addTypedRouter()` method.
|
|
||||||
|
|
||||||
#### Hierarchical TypedRouter Structure
|
|
||||||
|
|
||||||
Following cloudly's pattern, we'll use a hierarchical router structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
TypedServer (built-in typedrouter at /typedrequest)
|
|
||||||
└── DcRouter.typedrouter (main router)
|
|
||||||
└── OpsServer.typedrouter (ops-specific handlers)
|
|
||||||
├── StatsHandler.typedrouter
|
|
||||||
├── ConfigHandler.typedrouter
|
|
||||||
└── SecurityHandler.typedrouter
|
|
||||||
```
|
|
||||||
|
|
||||||
This allows clean separation of concerns while keeping all handlers accessible through the single `/typedrequest` endpoint.
|
|
||||||
|
|
||||||
#### 2.2 Create Handler Classes ✓
|
|
||||||
|
|
||||||
Create modular handlers in `ts/opsserver/handlers/`:
|
|
||||||
|
|
||||||
- [x] `stats.handler.ts` - Server and performance statistics
|
|
||||||
- [x] `security.handler.ts` - Security and reputation metrics
|
|
||||||
- [x] `config.handler.ts` - Configuration management
|
|
||||||
- [x] `logs.handler.ts` - Log retrieval and streaming
|
|
||||||
- [x] `admin.handler.ts` - Authentication and session management
|
|
||||||
|
|
||||||
Each handler should:
|
|
||||||
- Have its own typedrouter that gets added to OpsServer's router
|
|
||||||
- Access the main DCRouter instance
|
|
||||||
- Register handlers using TypedHandler instances
|
|
||||||
- Format responses according to interfaces
|
|
||||||
- Handle errors gracefully
|
|
||||||
|
|
||||||
Example handler structure:
|
|
||||||
```typescript
|
|
||||||
export class StatsHandler {
|
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
||||||
|
|
||||||
constructor(private opsServerRef: OpsServer) {
|
|
||||||
// Add this handler's router to the parent
|
|
||||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
||||||
this.registerHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
private registerHandlers() {
|
|
||||||
this.typedrouter.addTypedHandler(
|
|
||||||
new plugins.typedrequest.TypedHandler<IReq_GetServerStatistics>(
|
|
||||||
'getServerStatistics',
|
|
||||||
async (dataArg, toolsArg) => {
|
|
||||||
const stats = await this.collectServerStats();
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 3: Frontend State Management ✓
|
|
||||||
|
|
||||||
#### 3.1 Set up Smartstate (`ts_web/appstate.ts`) ✓
|
|
||||||
|
|
||||||
- [x] Initialize Smartstate instance
|
|
||||||
- [x] Create state parts with appropriate persistence
|
|
||||||
- [x] Define initial state structures
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// State structure example
|
|
||||||
interface IStatsState {
|
|
||||||
serverStats: IRes_ServerStatistics | null;
|
|
||||||
emailStats: IRes_EmailStatistics | null;
|
|
||||||
dnsStats: IRes_DnsStatistics | null;
|
|
||||||
lastUpdated: number;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: string | null;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2 State Parts to Create ✓
|
|
||||||
|
|
||||||
- [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 ✓
|
|
||||||
|
|
||||||
#### 4.1 API Client Setup ✓
|
|
||||||
|
|
||||||
- [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/appstate.ts`) ✓
|
|
||||||
|
|
||||||
- [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`) ✓
|
|
||||||
|
|
||||||
- [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 ✓
|
|
||||||
|
|
||||||
Created modular view components in `ts_web/elements/`:
|
|
||||||
|
|
||||||
- [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
|
|
||||||
|
|
||||||
#### 6.1 Authentication ✓ (Implemented)
|
|
||||||
- [x] JWT-based authentication using `@push.rocks/smartjwt`
|
|
||||||
- [x] Guards for identity validation and admin access
|
|
||||||
- [x] Login/logout endpoints following cloudly pattern
|
|
||||||
- [ ] Login component (frontend)
|
|
||||||
- [ ] Protected route handling (frontend)
|
|
||||||
- [ ] Session persistence (frontend)
|
|
||||||
|
|
||||||
#### 6.2 Real-time Updates (future)
|
|
||||||
- [ ] WebSocket integration for live stats
|
|
||||||
- [ ] Push notifications for critical events
|
|
||||||
- [ ] Event streaming for logs
|
|
||||||
|
|
||||||
## Technical Stack
|
|
||||||
|
|
||||||
### Dependencies to Use
|
|
||||||
- `@api.global/typedserver` - Server with built-in typedrouter at `/typedrequest`
|
|
||||||
- `@api.global/typedrequest` - TypedRouter and TypedHandler classes
|
|
||||||
- `@design.estate/dees-domtools` - Frontend TypedRequest client
|
|
||||||
- `@push.rocks/smartstate` - State management
|
|
||||||
- `@design.estate/dees-element` - Web components
|
|
||||||
- `@design.estate/dees-catalog` - UI components
|
|
||||||
|
|
||||||
### Existing Dependencies to Leverage
|
|
||||||
- Current DCRouter instance and statistics
|
|
||||||
- Existing error handling patterns
|
|
||||||
- Logger infrastructure
|
|
||||||
- Security modules
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
1. **Start with interfaces** - Define all types first
|
|
||||||
2. **Implement one handler** - Start with server stats
|
|
||||||
3. **Create minimal frontend** - Test with one endpoint
|
|
||||||
4. **Iterate** - Add more handlers and UI components
|
|
||||||
5. **Polish** - Add error handling, loading states, etc.
|
|
||||||
|
|
||||||
## Testing Strategy
|
|
||||||
|
|
||||||
- [ ] Unit tests for handlers
|
|
||||||
- [ ] Integration tests for API endpoints
|
|
||||||
- [ ] Frontend component tests
|
|
||||||
- [ ] End-to-end testing with real DCRouter instance
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
- Type-safe communication between frontend and backend
|
|
||||||
- Real-time statistics display
|
|
||||||
- Responsive and reactive UI
|
|
||||||
- Clean, maintainable code structure
|
|
||||||
- Consistent with cloudly patterns
|
|
||||||
- Easy to extend with new features
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- Follow existing code conventions in the project
|
|
||||||
- Use pnpm for all package management
|
|
||||||
- Ensure all tests pass before marking complete
|
|
||||||
- Document any deviations from the plan
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Progress Status
|
|
||||||
|
|
||||||
### Completed ✓
|
|
||||||
- **Phase 1: Interface Definition** - All TypedRequest interfaces created following cloudly pattern
|
|
||||||
- Created proper TypedRequest interfaces with `method`, `request`, and `response` properties
|
|
||||||
- Used `IVirtualStream` for log streaming
|
|
||||||
- Added `@api.global/typedrequest-interfaces` dependency
|
|
||||||
- All interfaces compile successfully
|
|
||||||
|
|
||||||
- **Phase 2: Backend Implementation** - TypedRouter integration and handlers
|
|
||||||
- Enhanced OpsServer with hierarchical TypedRouter structure
|
|
||||||
- Created all handler classes with proper TypedHandler registration
|
|
||||||
- Implemented mock data responses for all endpoints
|
|
||||||
- Fixed all TypeScript compilation errors
|
|
||||||
- VirtualStream used for log streaming with Uint8Array encoding
|
|
||||||
- **JWT Authentication** - Following cloudly pattern:
|
|
||||||
- Added `@push.rocks/smartjwt` and `@push.rocks/smartguard` dependencies
|
|
||||||
- Updated IIdentity interface to match cloudly structure
|
|
||||||
- Implemented JWT-based authentication with RSA keypairs
|
|
||||||
- Created validIdentityGuard and adminIdentityGuard
|
|
||||||
- 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
|
|
||||||
- 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
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This plan is a living document. Update it as implementation progresses.*
|
|
||||||
71
readme.plan2.md
Normal file
71
readme.plan2.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# Network Metrics Integration Status
|
||||||
|
|
||||||
|
## Command: `pnpm run build && curl https://code.foss.global/push.rocks/smartproxy/raw/branch/master/readme.md`
|
||||||
|
|
||||||
|
## Completed Tasks (2025-06-23)
|
||||||
|
|
||||||
|
### ✅ SmartProxy Metrics API Integration
|
||||||
|
- Updated MetricsManager to use new SmartProxy v19.6.7 metrics API
|
||||||
|
- Replaced deprecated `getStats()` with `getMetrics()` and `getStatistics()`
|
||||||
|
- Fixed method calls to use grouped API structure:
|
||||||
|
- `metrics.connections.active()` for active connections
|
||||||
|
- `metrics.throughput.instant()` for real-time throughput
|
||||||
|
- `metrics.connections.topIPs()` for top connected IPs
|
||||||
|
|
||||||
|
### ✅ Removed Mock Data
|
||||||
|
- Removed hardcoded `0.0.0.0` IPs in security.handler.ts
|
||||||
|
- Removed `Math.random()` trend data in ops-view-network.ts
|
||||||
|
- Now using real IP data from SmartProxy metrics
|
||||||
|
|
||||||
|
### ✅ Enhanced Metrics Functionality
|
||||||
|
- Email metrics: delivery time tracking, top recipients, activity log
|
||||||
|
- DNS metrics: query rate calculations, response time tracking
|
||||||
|
- Security metrics: incident logging with severity levels
|
||||||
|
|
||||||
|
### ✅ Fixed Network Traffic Display
|
||||||
|
- All throughput now shown in bits per second (kbit/s, Mbit/s, Gbit/s)
|
||||||
|
- Network graph shows separate lines for inbound (green) and outbound (purple)
|
||||||
|
- Fixed throughput calculation to use same data source as tiles
|
||||||
|
- Added tooltips showing both timestamp and value
|
||||||
|
|
||||||
|
### ✅ Fixed Requests/sec Tile
|
||||||
|
- Shows actual request counts (derived from connections)
|
||||||
|
- Trend line now shows request history, not throughput
|
||||||
|
- Consistent data between number and trend visualization
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
1. SmartProxy collects metrics via its internal MetricsCollector
|
||||||
|
2. MetricsManager retrieves data using `smartProxy.getMetrics()`
|
||||||
|
3. Handlers transform metrics for UI consumption
|
||||||
|
4. UI components display real-time data with auto-refresh
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
- **MetricsManager**: Central metrics aggregation and tracking
|
||||||
|
- **SmartProxy Integration**: Uses grouped metrics API
|
||||||
|
- **UI Components**: ops-view-network shows real-time traffic graphs
|
||||||
|
- **State Management**: Uses appstate for reactive updates
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- Request counting is derived from connection data (not true HTTP request counts)
|
||||||
|
- Some metrics still need backend implementation (e.g., per-connection bytes)
|
||||||
|
- Historical data limited to current session
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
```bash
|
||||||
|
# Build and run
|
||||||
|
pnpm build
|
||||||
|
pnpm start
|
||||||
|
|
||||||
|
# Check metrics endpoints
|
||||||
|
curl http://localhost:4000/api/stats/server
|
||||||
|
curl http://localhost:4000/api/stats/network
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
- [x] Real-time throughput data displayed correctly
|
||||||
|
- [x] No mock data in production UI
|
||||||
|
- [x] Consistent units across all displays
|
||||||
|
- [x] Separate in/out traffic visualization
|
||||||
|
- [x] Working trend lines in stat tiles
|
||||||
46
readme.statsgrid.md
Normal file
46
readme.statsgrid.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Plan: Implement dees-statsgrid in DCRouter UI
|
||||||
|
|
||||||
|
Command to reread CLAUDE.md: `Read /home/centraluser/eu.central.ingress-2/CLAUDE.md`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Replace the current stats cards with the new dees-statsgrid component from @design.estate/dees-catalog for better visualization and consistency.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. Update Overview View (`ops-view-overview.ts`)
|
||||||
|
- Replace the custom stats cards with dees-statsgrid
|
||||||
|
- Use appropriate tile types for different metrics:
|
||||||
|
- `gauge` for CPU and Memory usage
|
||||||
|
- `number` for Active Connections, Total Requests, etc.
|
||||||
|
- `trend` for time-series data like requests over time
|
||||||
|
|
||||||
|
### 2. Update Network View (`ops-view-network.ts`)
|
||||||
|
- Replace the current stats cards section with dees-statsgrid
|
||||||
|
- Configure tiles for:
|
||||||
|
- Active Connections (number)
|
||||||
|
- Requests/sec (number with trend)
|
||||||
|
- Throughput In/Out (number with units)
|
||||||
|
- Protocol distribution (percentage)
|
||||||
|
|
||||||
|
### 3. Create Consistent Color Scheme
|
||||||
|
- Success/Normal: #22c55e (green)
|
||||||
|
- Warning: #f59e0b (amber)
|
||||||
|
- Error/Critical: #ef4444 (red)
|
||||||
|
- Info: #3b82f6 (blue)
|
||||||
|
|
||||||
|
### 4. Add Interactive Features
|
||||||
|
- Click actions to show detailed views
|
||||||
|
- Context menu for refresh, export, etc.
|
||||||
|
- Real-time updates from metrics data
|
||||||
|
|
||||||
|
### 5. Integration Points
|
||||||
|
- Connect to existing appstate for data
|
||||||
|
- Use MetricsManager data for real values
|
||||||
|
- Update on the 1-second refresh interval
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
- Consistent UI component usage
|
||||||
|
- Better visual hierarchy
|
||||||
|
- Built-in responsive design
|
||||||
|
- More visualization options (gauges, trends)
|
||||||
|
- Reduced custom CSS maintenance
|
||||||
@@ -5,7 +5,7 @@ import * as paths from './paths.js';
|
|||||||
|
|
||||||
// Import the email server and its configuration
|
// Import the email server and its configuration
|
||||||
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
|
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
|
||||||
import type { IEmailRoute } from './mail/routing/interfaces.js';
|
import type { IEmailRoute, IEmailDomainConfig } from './mail/routing/interfaces.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
// Import the email configuration helpers directly from mail/delivery
|
// Import the email configuration helpers directly from mail/delivery
|
||||||
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
|
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
|
||||||
@@ -13,6 +13,7 @@ import { configureEmailStorage, configureEmailServer } from './mail/delivery/ind
|
|||||||
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
||||||
|
|
||||||
import { OpsServer } from './opsserver/index.js';
|
import { OpsServer } from './opsserver/index.js';
|
||||||
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
/**
|
/**
|
||||||
@@ -133,6 +134,7 @@ export class DcRouter {
|
|||||||
public emailServer?: UnifiedEmailServer;
|
public emailServer?: UnifiedEmailServer;
|
||||||
public storageManager: StorageManager;
|
public storageManager: StorageManager;
|
||||||
public opsServer: OpsServer;
|
public opsServer: OpsServer;
|
||||||
|
public metricsManager?: MetricsManager;
|
||||||
|
|
||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -160,6 +162,10 @@ export class DcRouter {
|
|||||||
await this.opsServer.start();
|
await this.opsServer.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Initialize MetricsManager
|
||||||
|
this.metricsManager = new MetricsManager(this);
|
||||||
|
await this.metricsManager.start();
|
||||||
|
|
||||||
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
||||||
await this.setupSmartProxy();
|
await this.setupSmartProxy();
|
||||||
|
|
||||||
@@ -197,6 +203,14 @@ export class DcRouter {
|
|||||||
console.log('║ DcRouter Started Successfully ║');
|
console.log('║ DcRouter Started Successfully ║');
|
||||||
console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
|
console.log('╚═══════════════════════════════════════════════════════════════════╝\n');
|
||||||
|
|
||||||
|
// Metrics summary
|
||||||
|
if (this.metricsManager) {
|
||||||
|
console.log('📊 Metrics Service:');
|
||||||
|
console.log(' ├─ SmartMetrics: Active');
|
||||||
|
console.log(' ├─ SmartProxy Stats: Active');
|
||||||
|
console.log(' └─ Real-time tracking: Enabled');
|
||||||
|
}
|
||||||
|
|
||||||
// SmartProxy summary
|
// SmartProxy summary
|
||||||
if (this.smartProxy) {
|
if (this.smartProxy) {
|
||||||
console.log('🌐 SmartProxy Service:');
|
console.log('🌐 SmartProxy Service:');
|
||||||
@@ -373,11 +387,29 @@ export class DcRouter {
|
|||||||
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
|
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
|
||||||
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
||||||
|
|
||||||
|
// Type definitions for SmartProxy
|
||||||
|
type TRouteActionType = 'forward' | 'socket-handler';
|
||||||
|
type TTlsMode = 'passthrough' | 'terminate' | 'terminate-and-reencrypt';
|
||||||
|
|
||||||
|
// Helper function to extract domains from email patterns
|
||||||
|
const extractDomainsFromRecipients = (recipients: string | string[]): string[] => {
|
||||||
|
const recipientArray = Array.isArray(recipients) ? recipients : [recipients];
|
||||||
|
return recipientArray
|
||||||
|
.map(r => {
|
||||||
|
// Handle wildcards: "*@domain.com" -> "domain.com"
|
||||||
|
// Handle normal: "user@domain.com" -> "domain.com"
|
||||||
|
const parts = r.split('@');
|
||||||
|
return parts.length === 2 && parts[1] !== '*' ? parts[1] : null;
|
||||||
|
})
|
||||||
|
.filter((d): d is string => d !== null)
|
||||||
|
.filter((d, i, arr) => arr.indexOf(d) === i); // Remove duplicates
|
||||||
|
};
|
||||||
|
|
||||||
// Create routes for each email port
|
// Create routes for each email port
|
||||||
for (const port of emailConfig.ports) {
|
for (const port of emailConfig.ports) {
|
||||||
// Create a descriptive name for the route based on the port
|
// Create a descriptive name for the route based on the port
|
||||||
let routeName = 'email-route';
|
let routeName = 'email-route';
|
||||||
let tlsMode = 'passthrough';
|
let tlsMode: TTlsMode = 'passthrough';
|
||||||
|
|
||||||
// Handle different email ports differently
|
// Handle different email ports differently
|
||||||
switch (port) {
|
switch (port) {
|
||||||
@@ -393,7 +425,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
case 465: // SMTPS
|
case 465: // SMTPS
|
||||||
routeName = 'smtps-route';
|
routeName = 'smtps-route';
|
||||||
tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server
|
tlsMode = 'terminate-and-reencrypt'; // Terminate TLS and re-encrypt to email server
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -424,7 +456,7 @@ export class DcRouter {
|
|||||||
if (emailConfig.useSocketHandler) {
|
if (emailConfig.useSocketHandler) {
|
||||||
// Socket-handler mode
|
// Socket-handler mode
|
||||||
action = {
|
action = {
|
||||||
type: 'socket-handler' as any,
|
type: 'socket-handler' as TRouteActionType,
|
||||||
socketHandler: this.createMailSocketHandler(port)
|
socketHandler: this.createMailSocketHandler(port)
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -439,15 +471,19 @@ export class DcRouter {
|
|||||||
const internalPort = portMapping[port] || port + 10000;
|
const internalPort = portMapping[port] || port + 10000;
|
||||||
|
|
||||||
action = {
|
action = {
|
||||||
type: 'forward',
|
type: 'forward' as TRouteActionType,
|
||||||
target: {
|
targets: [{
|
||||||
host: 'localhost', // Forward to internal email server
|
host: 'localhost', // Forward to internal email server
|
||||||
port: internalPort
|
port: internalPort
|
||||||
},
|
}]
|
||||||
tls: {
|
|
||||||
mode: tlsMode as any
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add TLS configuration at action level if needed
|
||||||
|
if (tlsMode !== 'passthrough' || port === 465) {
|
||||||
|
action.tls = {
|
||||||
|
mode: tlsMode
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For TLS terminate mode, add certificate info
|
// For TLS terminate mode, add certificate info
|
||||||
@@ -471,23 +507,33 @@ export class DcRouter {
|
|||||||
// Add email domain-based routes if configured
|
// Add email domain-based routes if configured
|
||||||
if (emailConfig.routes) {
|
if (emailConfig.routes) {
|
||||||
for (const route of emailConfig.routes) {
|
for (const route of emailConfig.routes) {
|
||||||
emailRoutes.push({
|
// Only create SmartProxy routes for forward actions
|
||||||
name: route.name,
|
// Other email actions (deliver, process, reject) are handled internally by the email server
|
||||||
match: {
|
if (route.action.type === 'forward' && route.action.forward) {
|
||||||
ports: emailConfig.ports,
|
const domains = route.match.recipients ?
|
||||||
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
|
extractDomainsFromRecipients(route.match.recipients) : [];
|
||||||
},
|
|
||||||
action: {
|
// Only create SmartProxy route if we have domains to match
|
||||||
type: 'forward',
|
if (domains.length > 0) {
|
||||||
target: route.action.type === 'forward' && route.action.forward ? {
|
emailRoutes.push({
|
||||||
host: route.action.forward.host,
|
name: route.name,
|
||||||
port: route.action.forward.port || 25
|
match: {
|
||||||
} : undefined,
|
ports: emailConfig.ports,
|
||||||
tls: {
|
domains: domains
|
||||||
mode: 'passthrough'
|
},
|
||||||
}
|
action: {
|
||||||
|
type: 'forward' as TRouteActionType,
|
||||||
|
targets: [{
|
||||||
|
host: route.action.forward.host,
|
||||||
|
port: route.action.forward.port || 25
|
||||||
|
}],
|
||||||
|
tls: {
|
||||||
|
mode: 'passthrough' as TTlsMode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,6 +612,9 @@ export class DcRouter {
|
|||||||
try {
|
try {
|
||||||
// Stop all services in parallel for faster shutdown
|
// Stop all services in parallel for faster shutdown
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
// Stop metrics manager if running
|
||||||
|
this.metricsManager ? this.metricsManager.stop().catch(err => console.error('Error stopping MetricsManager:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
// Stop unified email server if running
|
// Stop unified email server if running
|
||||||
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
||||||
|
|
||||||
@@ -623,9 +672,28 @@ export class DcRouter {
|
|||||||
465: 10465 // SMTPS
|
465: 10465 // SMTPS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Transform domains if they are provided as strings
|
||||||
|
let transformedDomains = this.options.emailConfig.domains;
|
||||||
|
if (transformedDomains && transformedDomains.length > 0) {
|
||||||
|
// Check if domains are strings (for backward compatibility)
|
||||||
|
if (typeof transformedDomains[0] === 'string') {
|
||||||
|
transformedDomains = (transformedDomains as any).map((domain: string) => ({
|
||||||
|
domain,
|
||||||
|
dnsMode: 'external-dns' as const,
|
||||||
|
dkim: {
|
||||||
|
selector: 'default',
|
||||||
|
keySize: 2048,
|
||||||
|
rotateKeys: false,
|
||||||
|
rotationInterval: 90
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create config with mapped ports
|
// Create config with mapped ports
|
||||||
const emailConfig: IUnifiedEmailServerOptions = {
|
const emailConfig: IUnifiedEmailServerOptions = {
|
||||||
...this.options.emailConfig,
|
...this.options.emailConfig,
|
||||||
|
domains: transformedDomains,
|
||||||
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
||||||
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
|
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,12 +221,13 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|||||||
const checkInterval = setInterval(() => {
|
const checkInterval = setInterval(() => {
|
||||||
if (this.activeDeliveries.size === 0) {
|
if (this.activeDeliveries.size === 0) {
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
|
clearTimeout(forceTimeout);
|
||||||
resolve();
|
resolve();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
// Force resolve after 30 seconds
|
// Force resolve after 30 seconds
|
||||||
setTimeout(() => {
|
const forceTimeout = setTimeout(() => {
|
||||||
clearInterval(checkInterval);
|
clearInterval(checkInterval);
|
||||||
resolve();
|
resolve();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|||||||
@@ -247,11 +247,23 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
|
|
||||||
// 2. Check for destroyed sockets in active connections
|
// 2. Check for destroyed sockets in active connections
|
||||||
let destroyedSocketsCount = 0;
|
let destroyedSocketsCount = 0;
|
||||||
|
const socketsToRemove: Array<plugins.net.Socket | plugins.tls.TLSSocket> = [];
|
||||||
|
|
||||||
for (const socket of this.activeConnections) {
|
for (const socket of this.activeConnections) {
|
||||||
if (socket.destroyed) {
|
if (socket.destroyed) {
|
||||||
destroyedSocketsCount++;
|
destroyedSocketsCount++;
|
||||||
// This should not happen - remove destroyed sockets from tracking
|
socketsToRemove.push(socket);
|
||||||
this.activeConnections.delete(socket);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove destroyed sockets from tracking
|
||||||
|
for (const socket of socketsToRemove) {
|
||||||
|
this.activeConnections.delete(socket);
|
||||||
|
// Also ensure all listeners are removed
|
||||||
|
try {
|
||||||
|
socket.removeAllListeners();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors from removeAllListeners
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,9 +353,6 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track this IP connection
|
|
||||||
this.trackIPConnection(remoteAddress);
|
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.setupSocketEventHandlers(socket);
|
this.setupSocketEventHandlers(socket);
|
||||||
|
|
||||||
@@ -498,9 +507,6 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
SmtpLogger.debug(`Could not set socket buffer limits: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track this IP connection
|
|
||||||
this.trackIPConnection(remoteAddress);
|
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.setupSocketEventHandlers(socket);
|
this.setupSocketEventHandlers(socket);
|
||||||
|
|
||||||
@@ -763,6 +769,9 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
clearTimeout(session.dataTimeoutId);
|
clearTimeout(session.dataTimeoutId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove all event listeners to prevent memory leaks
|
||||||
|
socket.removeAllListeners();
|
||||||
|
|
||||||
// Log connection close with session details if available
|
// Log connection close with session details if available
|
||||||
adaptiveLogger.logConnection(socket, 'close', session);
|
adaptiveLogger.logConnection(socket, 'close', session);
|
||||||
|
|
||||||
@@ -774,6 +783,13 @@ export class ConnectionManager implements IConnectionManager {
|
|||||||
|
|
||||||
// Ensure socket is removed from active connections even if an error occurs
|
// Ensure socket is removed from active connections even if an error occurs
|
||||||
this.activeConnections.delete(socket);
|
this.activeConnections.delete(socket);
|
||||||
|
|
||||||
|
// Always try to remove all listeners even on error
|
||||||
|
try {
|
||||||
|
socket.removeAllListeners();
|
||||||
|
} catch {
|
||||||
|
// Ignore errors from removeAllListeners
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
|||||||
private dcRouter: DcRouter;
|
private dcRouter: DcRouter;
|
||||||
private options: IUnifiedEmailServerOptions;
|
private options: IUnifiedEmailServerOptions;
|
||||||
private emailRouter: EmailRouter;
|
private emailRouter: EmailRouter;
|
||||||
private domainRegistry: DomainRegistry;
|
public domainRegistry: DomainRegistry;
|
||||||
private servers: any[] = [];
|
private servers: any[] = [];
|
||||||
private stats: IServerStats;
|
private stats: IServerStats;
|
||||||
|
|
||||||
|
|||||||
75
ts/monitoring/classes.metricscache.ts
Normal file
75
ts/monitoring/classes.metricscache.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export interface ICacheEntry<T> {
|
||||||
|
data: T;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricsCache {
|
||||||
|
private cache = new Map<string, ICacheEntry<any>>();
|
||||||
|
private readonly defaultTTL: number;
|
||||||
|
|
||||||
|
constructor(defaultTTL: number = 500) {
|
||||||
|
this.defaultTTL = defaultTTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached data or compute and cache it
|
||||||
|
*/
|
||||||
|
public get<T>(key: string, computeFn: () => T | Promise<T>, ttl?: number): T | Promise<T> {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
const now = Date.now();
|
||||||
|
const actualTTL = ttl ?? this.defaultTTL;
|
||||||
|
|
||||||
|
if (cached && (now - cached.timestamp) < actualTTL) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = computeFn();
|
||||||
|
|
||||||
|
// Handle both sync and async compute functions
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return result.then(data => {
|
||||||
|
this.cache.set(key, { data, timestamp: now });
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.cache.set(key, { data: result, timestamp: now });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate a specific cache entry
|
||||||
|
*/
|
||||||
|
public invalidate(key: string): void {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cache entries
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
public getStats(): { size: number; keys: string[] } {
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
keys: Array.from(this.cache.keys())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired entries
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, entry] of this.cache.entries()) {
|
||||||
|
if (now - entry.timestamp > this.defaultTTL) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
522
ts/monitoring/classes.metricsmanager.ts
Normal file
522
ts/monitoring/classes.metricsmanager.ts
Normal file
@@ -0,0 +1,522 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import { DcRouter } from '../classes.dcrouter.js';
|
||||||
|
import { MetricsCache } from './classes.metricscache.js';
|
||||||
|
|
||||||
|
export class MetricsManager {
|
||||||
|
private logger: plugins.smartlog.Smartlog;
|
||||||
|
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
||||||
|
private dcRouter: DcRouter;
|
||||||
|
private resetInterval?: NodeJS.Timeout;
|
||||||
|
private metricsCache: MetricsCache;
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
private readonly MAX_TOP_DOMAINS = 1000; // Limit topDomains Map size
|
||||||
|
|
||||||
|
// Track email-specific metrics
|
||||||
|
private emailMetrics = {
|
||||||
|
sentToday: 0,
|
||||||
|
receivedToday: 0,
|
||||||
|
failedToday: 0,
|
||||||
|
bouncedToday: 0,
|
||||||
|
queueSize: 0,
|
||||||
|
lastResetDate: new Date().toDateString(),
|
||||||
|
deliveryTimes: [] as number[], // Track delivery times in ms
|
||||||
|
recipients: new Map<string, number>(), // Track email count by recipient
|
||||||
|
recentActivity: [] as Array<{ timestamp: number; type: string; details: string }>,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track DNS-specific metrics
|
||||||
|
private dnsMetrics = {
|
||||||
|
totalQueries: 0,
|
||||||
|
cacheHits: 0,
|
||||||
|
cacheMisses: 0,
|
||||||
|
queryTypes: {} as Record<string, number>,
|
||||||
|
topDomains: new Map<string, number>(),
|
||||||
|
lastResetDate: new Date().toDateString(),
|
||||||
|
queryTimestamps: [] as number[], // Track query timestamps for rate calculation
|
||||||
|
responseTimes: [] as number[], // Track response times in ms
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track security-specific metrics
|
||||||
|
private securityMetrics = {
|
||||||
|
blockedIPs: 0,
|
||||||
|
authFailures: 0,
|
||||||
|
spamDetected: 0,
|
||||||
|
malwareDetected: 0,
|
||||||
|
phishingDetected: 0,
|
||||||
|
lastResetDate: new Date().toDateString(),
|
||||||
|
incidents: [] as Array<{ timestamp: number; type: string; severity: string; details: string }>,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(dcRouter: DcRouter) {
|
||||||
|
this.dcRouter = dcRouter;
|
||||||
|
// Create a new Smartlog instance for metrics
|
||||||
|
this.logger = new plugins.smartlog.Smartlog({
|
||||||
|
logContext: {
|
||||||
|
environment: 'production',
|
||||||
|
runtime: 'node',
|
||||||
|
zone: 'dcrouter-metrics',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter');
|
||||||
|
// Initialize metrics cache with 500ms TTL
|
||||||
|
this.metricsCache = new MetricsCache(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
// Start SmartMetrics collection
|
||||||
|
this.smartMetrics.start();
|
||||||
|
|
||||||
|
// Reset daily counters at midnight
|
||||||
|
this.resetInterval = setInterval(() => {
|
||||||
|
const currentDate = new Date().toDateString();
|
||||||
|
|
||||||
|
if (currentDate !== this.emailMetrics.lastResetDate) {
|
||||||
|
this.emailMetrics.sentToday = 0;
|
||||||
|
this.emailMetrics.receivedToday = 0;
|
||||||
|
this.emailMetrics.failedToday = 0;
|
||||||
|
this.emailMetrics.bouncedToday = 0;
|
||||||
|
this.emailMetrics.deliveryTimes = [];
|
||||||
|
this.emailMetrics.recipients.clear();
|
||||||
|
this.emailMetrics.recentActivity = [];
|
||||||
|
this.emailMetrics.lastResetDate = currentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDate !== this.dnsMetrics.lastResetDate) {
|
||||||
|
this.dnsMetrics.totalQueries = 0;
|
||||||
|
this.dnsMetrics.cacheHits = 0;
|
||||||
|
this.dnsMetrics.cacheMisses = 0;
|
||||||
|
this.dnsMetrics.queryTypes = {};
|
||||||
|
this.dnsMetrics.topDomains.clear();
|
||||||
|
this.dnsMetrics.queryTimestamps = [];
|
||||||
|
this.dnsMetrics.responseTimes = [];
|
||||||
|
this.dnsMetrics.lastResetDate = currentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDate !== this.securityMetrics.lastResetDate) {
|
||||||
|
this.securityMetrics.blockedIPs = 0;
|
||||||
|
this.securityMetrics.authFailures = 0;
|
||||||
|
this.securityMetrics.spamDetected = 0;
|
||||||
|
this.securityMetrics.malwareDetected = 0;
|
||||||
|
this.securityMetrics.phishingDetected = 0;
|
||||||
|
this.securityMetrics.incidents = [];
|
||||||
|
this.securityMetrics.lastResetDate = currentDate;
|
||||||
|
}
|
||||||
|
}, 60000); // Check every minute
|
||||||
|
|
||||||
|
this.logger.log('info', 'MetricsManager started');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
// Clear the reset interval
|
||||||
|
if (this.resetInterval) {
|
||||||
|
clearInterval(this.resetInterval);
|
||||||
|
this.resetInterval = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.smartMetrics.stop();
|
||||||
|
this.logger.log('info', 'MetricsManager stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get server metrics from SmartMetrics and SmartProxy
|
||||||
|
public async getServerStats() {
|
||||||
|
return this.metricsCache.get('serverStats', async () => {
|
||||||
|
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||||
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStatistics() : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
uptime: process.uptime(),
|
||||||
|
startTime: Date.now() - (process.uptime() * 1000),
|
||||||
|
memoryUsage: {
|
||||||
|
heapUsed: process.memoryUsage().heapUsed,
|
||||||
|
heapTotal: process.memoryUsage().heapTotal,
|
||||||
|
external: process.memoryUsage().external,
|
||||||
|
rss: process.memoryUsage().rss,
|
||||||
|
// Add SmartMetrics memory data
|
||||||
|
maxMemoryMB: this.smartMetrics.maxMemoryMB,
|
||||||
|
actualUsageBytes: smartMetricsData.memoryUsageBytes,
|
||||||
|
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
||||||
|
},
|
||||||
|
cpuUsage: {
|
||||||
|
user: parseFloat(smartMetricsData.cpuUsageText || '0'),
|
||||||
|
system: 0, // SmartMetrics doesn't separate user/system
|
||||||
|
},
|
||||||
|
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
||||||
|
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
||||||
|
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
|
||||||
|
throughput: proxyMetrics ? {
|
||||||
|
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||||
|
bytesOut: proxyMetrics.totals.bytesOut()
|
||||||
|
} : { bytesIn: 0, bytesOut: 0 },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get email metrics
|
||||||
|
public async getEmailStats() {
|
||||||
|
return this.metricsCache.get('emailStats', () => {
|
||||||
|
// Calculate average delivery time
|
||||||
|
const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
|
||||||
|
? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// Get top recipients
|
||||||
|
const topRecipients = Array.from(this.emailMetrics.recipients.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([email, count]) => ({ email, count }));
|
||||||
|
|
||||||
|
// Get recent activity (last 50 entries)
|
||||||
|
const recentActivity = this.emailMetrics.recentActivity.slice(-50);
|
||||||
|
|
||||||
|
return {
|
||||||
|
sentToday: this.emailMetrics.sentToday,
|
||||||
|
receivedToday: this.emailMetrics.receivedToday,
|
||||||
|
failedToday: this.emailMetrics.failedToday,
|
||||||
|
bounceRate: this.emailMetrics.bouncedToday > 0
|
||||||
|
? (this.emailMetrics.bouncedToday / this.emailMetrics.sentToday) * 100
|
||||||
|
: 0,
|
||||||
|
deliveryRate: this.emailMetrics.sentToday > 0
|
||||||
|
? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100
|
||||||
|
: 100,
|
||||||
|
queueSize: this.emailMetrics.queueSize,
|
||||||
|
averageDeliveryTime: Math.round(avgDeliveryTime),
|
||||||
|
topRecipients,
|
||||||
|
recentActivity,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DNS metrics
|
||||||
|
public async getDnsStats() {
|
||||||
|
return this.metricsCache.get('dnsStats', () => {
|
||||||
|
const cacheHitRate = this.dnsMetrics.totalQueries > 0
|
||||||
|
? (this.dnsMetrics.cacheHits / this.dnsMetrics.totalQueries) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const topDomains = Array.from(this.dnsMetrics.topDomains.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([domain, count]) => ({ domain, count }));
|
||||||
|
|
||||||
|
// Calculate queries per second from recent timestamps
|
||||||
|
const now = Date.now();
|
||||||
|
const oneMinuteAgo = now - 60000;
|
||||||
|
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
|
||||||
|
const queriesPerSecond = recentQueries.length / 60;
|
||||||
|
|
||||||
|
// Calculate average response time
|
||||||
|
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
|
||||||
|
? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
queriesPerSecond: Math.round(queriesPerSecond * 10) / 10,
|
||||||
|
totalQueries: this.dnsMetrics.totalQueries,
|
||||||
|
cacheHits: this.dnsMetrics.cacheHits,
|
||||||
|
cacheMisses: this.dnsMetrics.cacheMisses,
|
||||||
|
cacheHitRate: cacheHitRate,
|
||||||
|
topDomains: topDomains,
|
||||||
|
queryTypes: this.dnsMetrics.queryTypes,
|
||||||
|
averageResponseTime: Math.round(avgResponseTime),
|
||||||
|
activeDomains: this.dnsMetrics.topDomains.size,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get security metrics
|
||||||
|
public async getSecurityStats() {
|
||||||
|
return this.metricsCache.get('securityStats', () => {
|
||||||
|
// Get recent incidents (last 20)
|
||||||
|
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedIPs: this.securityMetrics.blockedIPs,
|
||||||
|
authFailures: this.securityMetrics.authFailures,
|
||||||
|
spamDetected: this.securityMetrics.spamDetected,
|
||||||
|
malwareDetected: this.securityMetrics.malwareDetected,
|
||||||
|
phishingDetected: this.securityMetrics.phishingDetected,
|
||||||
|
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
||||||
|
this.securityMetrics.malwareDetected +
|
||||||
|
this.securityMetrics.phishingDetected,
|
||||||
|
recentIncidents,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get connection info from SmartProxy
|
||||||
|
public async getConnectionInfo() {
|
||||||
|
return this.metricsCache.get('connectionInfo', () => {
|
||||||
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
|
||||||
|
if (!proxyMetrics) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||||
|
const connectionInfo = [];
|
||||||
|
|
||||||
|
for (const [routeName, count] of connectionsByRoute) {
|
||||||
|
connectionInfo.push({
|
||||||
|
type: 'https',
|
||||||
|
count,
|
||||||
|
source: routeName,
|
||||||
|
lastActivity: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email event tracking methods
|
||||||
|
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
||||||
|
this.emailMetrics.sentToday++;
|
||||||
|
|
||||||
|
if (recipient) {
|
||||||
|
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
||||||
|
this.emailMetrics.recipients.set(recipient, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deliveryTimeMs) {
|
||||||
|
this.emailMetrics.deliveryTimes.push(deliveryTimeMs);
|
||||||
|
// Keep only last 1000 delivery times
|
||||||
|
if (this.emailMetrics.deliveryTimes.length > 1000) {
|
||||||
|
this.emailMetrics.deliveryTimes.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emailMetrics.recentActivity.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'sent',
|
||||||
|
details: recipient || 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 activities
|
||||||
|
if (this.emailMetrics.recentActivity.length > 1000) {
|
||||||
|
this.emailMetrics.recentActivity.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackEmailReceived(sender?: string): void {
|
||||||
|
this.emailMetrics.receivedToday++;
|
||||||
|
|
||||||
|
this.emailMetrics.recentActivity.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'received',
|
||||||
|
details: sender || 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 activities
|
||||||
|
if (this.emailMetrics.recentActivity.length > 1000) {
|
||||||
|
this.emailMetrics.recentActivity.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackEmailFailed(recipient?: string, reason?: string): void {
|
||||||
|
this.emailMetrics.failedToday++;
|
||||||
|
|
||||||
|
this.emailMetrics.recentActivity.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'failed',
|
||||||
|
details: `${recipient || 'unknown'}: ${reason || 'unknown error'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 activities
|
||||||
|
if (this.emailMetrics.recentActivity.length > 1000) {
|
||||||
|
this.emailMetrics.recentActivity.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackEmailBounced(recipient?: string): void {
|
||||||
|
this.emailMetrics.bouncedToday++;
|
||||||
|
|
||||||
|
this.emailMetrics.recentActivity.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'bounced',
|
||||||
|
details: recipient || 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 activities
|
||||||
|
if (this.emailMetrics.recentActivity.length > 1000) {
|
||||||
|
this.emailMetrics.recentActivity.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateQueueSize(size: number): void {
|
||||||
|
this.emailMetrics.queueSize = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS event tracking methods
|
||||||
|
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
|
||||||
|
this.dnsMetrics.totalQueries++;
|
||||||
|
|
||||||
|
if (cacheHit) {
|
||||||
|
this.dnsMetrics.cacheHits++;
|
||||||
|
} else {
|
||||||
|
this.dnsMetrics.cacheMisses++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track query timestamp
|
||||||
|
this.dnsMetrics.queryTimestamps.push(Date.now());
|
||||||
|
|
||||||
|
// Keep only timestamps from last 5 minutes
|
||||||
|
const fiveMinutesAgo = Date.now() - 300000;
|
||||||
|
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
|
||||||
|
|
||||||
|
// Track response time if provided
|
||||||
|
if (responseTimeMs) {
|
||||||
|
this.dnsMetrics.responseTimes.push(responseTimeMs);
|
||||||
|
// Keep only last 1000 response times
|
||||||
|
if (this.dnsMetrics.responseTimes.length > 1000) {
|
||||||
|
this.dnsMetrics.responseTimes.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track query types
|
||||||
|
this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1;
|
||||||
|
|
||||||
|
// Track top domains with size limit
|
||||||
|
const currentCount = this.dnsMetrics.topDomains.get(domain) || 0;
|
||||||
|
this.dnsMetrics.topDomains.set(domain, currentCount + 1);
|
||||||
|
|
||||||
|
// If we've exceeded the limit, remove the least accessed domains
|
||||||
|
if (this.dnsMetrics.topDomains.size > this.MAX_TOP_DOMAINS) {
|
||||||
|
// Convert to array, sort by count, and keep only top domains
|
||||||
|
const sortedDomains = Array.from(this.dnsMetrics.topDomains.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, Math.floor(this.MAX_TOP_DOMAINS * 0.8)); // Keep 80% to avoid frequent cleanup
|
||||||
|
|
||||||
|
// Clear and repopulate with top domains
|
||||||
|
this.dnsMetrics.topDomains.clear();
|
||||||
|
sortedDomains.forEach(([domain, count]) => {
|
||||||
|
this.dnsMetrics.topDomains.set(domain, count);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security event tracking methods
|
||||||
|
public trackBlockedIP(ip?: string, reason?: string): void {
|
||||||
|
this.securityMetrics.blockedIPs++;
|
||||||
|
|
||||||
|
this.securityMetrics.incidents.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'ip_blocked',
|
||||||
|
severity: 'medium',
|
||||||
|
details: `IP ${ip || 'unknown'} blocked: ${reason || 'security policy'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 incidents
|
||||||
|
if (this.securityMetrics.incidents.length > 1000) {
|
||||||
|
this.securityMetrics.incidents.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackAuthFailure(username?: string, ip?: string): void {
|
||||||
|
this.securityMetrics.authFailures++;
|
||||||
|
|
||||||
|
this.securityMetrics.incidents.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'auth_failure',
|
||||||
|
severity: 'low',
|
||||||
|
details: `Authentication failed for ${username || 'unknown'} from ${ip || 'unknown'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 incidents
|
||||||
|
if (this.securityMetrics.incidents.length > 1000) {
|
||||||
|
this.securityMetrics.incidents.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackSpamDetected(sender?: string): void {
|
||||||
|
this.securityMetrics.spamDetected++;
|
||||||
|
|
||||||
|
this.securityMetrics.incidents.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'spam_detected',
|
||||||
|
severity: 'low',
|
||||||
|
details: `Spam detected from ${sender || 'unknown'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 incidents
|
||||||
|
if (this.securityMetrics.incidents.length > 1000) {
|
||||||
|
this.securityMetrics.incidents.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackMalwareDetected(source?: string): void {
|
||||||
|
this.securityMetrics.malwareDetected++;
|
||||||
|
|
||||||
|
this.securityMetrics.incidents.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'malware_detected',
|
||||||
|
severity: 'high',
|
||||||
|
details: `Malware detected from ${source || 'unknown'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 incidents
|
||||||
|
if (this.securityMetrics.incidents.length > 1000) {
|
||||||
|
this.securityMetrics.incidents.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public trackPhishingDetected(source?: string): void {
|
||||||
|
this.securityMetrics.phishingDetected++;
|
||||||
|
|
||||||
|
this.securityMetrics.incidents.push({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
type: 'phishing_detected',
|
||||||
|
severity: 'high',
|
||||||
|
details: `Phishing attempt from ${source || 'unknown'}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 1000 incidents
|
||||||
|
if (this.securityMetrics.incidents.length > 1000) {
|
||||||
|
this.securityMetrics.incidents.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get network metrics from SmartProxy
|
||||||
|
public async getNetworkStats() {
|
||||||
|
// Use shorter cache TTL for network stats to ensure real-time updates
|
||||||
|
return this.metricsCache.get('networkStats', () => {
|
||||||
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
|
||||||
|
if (!proxyMetrics) {
|
||||||
|
return {
|
||||||
|
connectionsByIP: new Map<string, number>(),
|
||||||
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
topIPs: [],
|
||||||
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metrics using the new API
|
||||||
|
const connectionsByIP = proxyMetrics.connections.byIP();
|
||||||
|
const instantThroughput = proxyMetrics.throughput.instant();
|
||||||
|
|
||||||
|
// Get throughput rate
|
||||||
|
const throughputRate = {
|
||||||
|
bytesInPerSecond: instantThroughput.in,
|
||||||
|
bytesOutPerSecond: instantThroughput.out
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get top IPs
|
||||||
|
const topIPs = proxyMetrics.connections.topIPs(10);
|
||||||
|
|
||||||
|
// Get total data transferred
|
||||||
|
const totalDataTransferred = {
|
||||||
|
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||||
|
bytesOut: proxyMetrics.totals.bytesOut()
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate,
|
||||||
|
topIPs,
|
||||||
|
totalDataTransferred,
|
||||||
|
};
|
||||||
|
}, 200); // Use 200ms cache for more frequent updates
|
||||||
|
}
|
||||||
|
}
|
||||||
1
ts/monitoring/index.ts
Normal file
1
ts/monitoring/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './classes.metricsmanager.js';
|
||||||
@@ -65,6 +65,7 @@ export class ConfigHandler {
|
|||||||
perHour: number;
|
perHour: number;
|
||||||
perDay: number;
|
perDay: number;
|
||||||
};
|
};
|
||||||
|
domains?: string[];
|
||||||
};
|
};
|
||||||
dns: {
|
dns: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@@ -88,6 +89,17 @@ export class ConfigHandler {
|
|||||||
}> {
|
}> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
|
||||||
|
// Get email domains if email server is configured
|
||||||
|
let emailDomains: string[] = [];
|
||||||
|
if (dcRouter.emailServer && dcRouter.emailServer.domainRegistry) {
|
||||||
|
emailDomains = dcRouter.emailServer.domainRegistry.getAllDomains();
|
||||||
|
} else if (dcRouter.options.emailConfig?.domains) {
|
||||||
|
// Fallback: get domains from email config options
|
||||||
|
emailDomains = dcRouter.options.emailConfig.domains.map(d =>
|
||||||
|
typeof d === 'string' ? d : d.domain
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
email: {
|
email: {
|
||||||
enabled: !!dcRouter.emailServer,
|
enabled: !!dcRouter.emailServer,
|
||||||
@@ -98,6 +110,7 @@ export class ConfigHandler {
|
|||||||
perHour: 100,
|
perHour: 100,
|
||||||
perDay: 1000,
|
perDay: 1000,
|
||||||
},
|
},
|
||||||
|
domains: emailDomains,
|
||||||
},
|
},
|
||||||
dns: {
|
dns: {
|
||||||
enabled: !!dcRouter.dnsServer,
|
enabled: !!dcRouter.dnsServer,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
import { MetricsManager } from '../../monitoring/index.js';
|
||||||
|
|
||||||
export class SecurityHandler {
|
export class SecurityHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -75,6 +76,34 @@ export class SecurityHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Network Stats Handler - provides comprehensive network metrics
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler(
|
||||||
|
'getNetworkStats',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
// Get network stats from MetricsManager if available
|
||||||
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
||||||
|
throughputRate: networkStats.throughputRate,
|
||||||
|
topIPs: networkStats.topIPs,
|
||||||
|
totalDataTransferred: networkStats.totalDataTransferred,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if MetricsManager not available
|
||||||
|
return {
|
||||||
|
connectionsByIP: [],
|
||||||
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
topIPs: [],
|
||||||
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Rate Limit Status Handler
|
// Rate Limit Status Handler
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||||
@@ -120,7 +149,29 @@ export class SecurityHandler {
|
|||||||
phishing: Array<{ timestamp: number; value: number }>;
|
phishing: Array<{ timestamp: number; value: number }>;
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
// TODO: Implement actual security metrics collection
|
// Get metrics from MetricsManager if available
|
||||||
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
const securityStats = await this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats();
|
||||||
|
return {
|
||||||
|
blockedIPs: [], // TODO: Track actual blocked IPs
|
||||||
|
reputationScores: {},
|
||||||
|
spamDetection: {
|
||||||
|
detected: securityStats.spamDetected,
|
||||||
|
falsePositives: 0,
|
||||||
|
},
|
||||||
|
malwareDetected: securityStats.malwareDetected,
|
||||||
|
phishingDetected: securityStats.phishingDetected,
|
||||||
|
authFailures: securityStats.authFailures,
|
||||||
|
suspiciousActivities: 0,
|
||||||
|
trends: {
|
||||||
|
spam: [],
|
||||||
|
malware: [],
|
||||||
|
phishing: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if MetricsManager not available
|
||||||
return {
|
return {
|
||||||
blockedIPs: [],
|
blockedIPs: [],
|
||||||
reputationScores: {},
|
reputationScores: {},
|
||||||
@@ -178,11 +229,69 @@ export class SecurityHandler {
|
|||||||
status: 'active' | 'idle' | 'closing';
|
status: 'active' | 'idle' | 'closing';
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
// TODO: Implement actual connection tracking
|
// Get connection info and network stats from MetricsManager if available
|
||||||
// This would collect from:
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
// - SmartProxy connections
|
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||||
// - Email server connections
|
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
// - DNS server connections
|
|
||||||
|
// Use IP-based connection data from the new metrics API
|
||||||
|
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
|
||||||
|
let connIndex = 0;
|
||||||
|
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
|
||||||
|
|
||||||
|
for (const [ip, count] of networkStats.connectionsByIP) {
|
||||||
|
// Create a connection entry for each active IP connection
|
||||||
|
for (let i = 0; i < Math.min(count, 5); i++) { // Limit to 5 connections per IP for UI performance
|
||||||
|
connections.push({
|
||||||
|
id: `conn-${connIndex++}`,
|
||||||
|
type: 'http',
|
||||||
|
source: {
|
||||||
|
ip: ip,
|
||||||
|
port: Math.floor(Math.random() * 50000) + 10000, // High port range
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
ip: publicIp,
|
||||||
|
port: 443,
|
||||||
|
service: 'proxy',
|
||||||
|
},
|
||||||
|
startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour
|
||||||
|
bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size),
|
||||||
|
status: 'active',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (connectionInfo.length > 0) {
|
||||||
|
// Fallback to route-based connection info if no IP data available
|
||||||
|
connectionInfo.forEach((info, index) => {
|
||||||
|
connections.push({
|
||||||
|
id: `conn-${index}`,
|
||||||
|
type: 'http',
|
||||||
|
source: {
|
||||||
|
ip: 'unknown',
|
||||||
|
port: 0,
|
||||||
|
},
|
||||||
|
destination: {
|
||||||
|
ip: this.opsServerRef.dcRouterRef.options.publicIp || 'server',
|
||||||
|
port: 443,
|
||||||
|
service: info.source,
|
||||||
|
},
|
||||||
|
startTime: info.lastActivity.getTime(),
|
||||||
|
bytesTransferred: 0,
|
||||||
|
status: 'active',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by protocol if specified
|
||||||
|
if (protocol) {
|
||||||
|
return connections.filter(conn => {
|
||||||
|
if (protocol === 'https' || protocol === 'http') {
|
||||||
|
return conn.type === 'http';
|
||||||
|
}
|
||||||
|
return conn.type === protocol.replace('s', ''); // smtp/smtps -> smtp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return connections;
|
return connections;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import type { OpsServer } from '../classes.opsserver.js';
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
import { MetricsManager } from '../../monitoring/index.js';
|
||||||
|
|
||||||
export class StatsHandler {
|
export class StatsHandler {
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
@@ -161,6 +162,133 @@ export class StatsHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Combined Metrics Handler - More efficient for frontend polling
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||||
|
'getCombinedMetrics',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const sections = dataArg.sections || {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const metrics: any = {};
|
||||||
|
|
||||||
|
// Run all metrics collection in parallel
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (sections.server) {
|
||||||
|
promises.push(
|
||||||
|
this.collectServerStats().then(stats => {
|
||||||
|
metrics.server = {
|
||||||
|
uptime: stats.uptime,
|
||||||
|
startTime: Date.now() - (stats.uptime * 1000),
|
||||||
|
memoryUsage: stats.memoryUsage,
|
||||||
|
cpuUsage: stats.cpuUsage,
|
||||||
|
activeConnections: stats.activeConnections,
|
||||||
|
totalConnections: stats.totalConnections,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.email) {
|
||||||
|
promises.push(
|
||||||
|
this.collectEmailStats().then(stats => {
|
||||||
|
metrics.email = {
|
||||||
|
sent: stats.sentToday,
|
||||||
|
received: stats.receivedToday,
|
||||||
|
bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
|
||||||
|
queued: stats.queueSize,
|
||||||
|
failed: 0,
|
||||||
|
averageDeliveryTime: 0,
|
||||||
|
deliveryRate: stats.deliveryRate,
|
||||||
|
bounceRate: stats.bounceRate,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.dns) {
|
||||||
|
promises.push(
|
||||||
|
this.collectDnsStats().then(stats => {
|
||||||
|
metrics.dns = {
|
||||||
|
totalQueries: stats.totalQueries,
|
||||||
|
cacheHits: stats.cacheHits,
|
||||||
|
cacheMisses: stats.cacheMisses,
|
||||||
|
cacheHitRate: stats.cacheHitRate,
|
||||||
|
activeDomains: stats.topDomains.length,
|
||||||
|
averageResponseTime: 0,
|
||||||
|
queryTypes: stats.queryTypes,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
promises.push(
|
||||||
|
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
||||||
|
metrics.security = {
|
||||||
|
blockedIPs: stats.blockedIPs,
|
||||||
|
reputationScores: {},
|
||||||
|
spamDetected: stats.spamDetected,
|
||||||
|
malwareDetected: stats.malwareDetected,
|
||||||
|
phishingDetected: stats.phishingDetected,
|
||||||
|
authenticationFailures: stats.authFailures,
|
||||||
|
suspiciousActivities: stats.totalThreatsBlocked,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
promises.push(
|
||||||
|
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
|
||||||
|
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
||||||
|
stats.connectionsByIP.forEach((count, ip) => {
|
||||||
|
connectionDetails.push({
|
||||||
|
remoteAddress: ip,
|
||||||
|
protocol: 'https' as any,
|
||||||
|
state: 'established' as any,
|
||||||
|
startTime: Date.now(),
|
||||||
|
bytesIn: 0,
|
||||||
|
bytesOut: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
metrics.network = {
|
||||||
|
totalBandwidth: {
|
||||||
|
in: stats.throughputRate.bytesInPerSecond,
|
||||||
|
out: stats.throughputRate.bytesOutPerSecond,
|
||||||
|
},
|
||||||
|
activeConnections: stats.connectionsByIP.size,
|
||||||
|
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
|
||||||
|
topEndpoints: stats.topIPs.map(ip => ({
|
||||||
|
endpoint: ip.ip,
|
||||||
|
requests: ip.count,
|
||||||
|
bandwidth: {
|
||||||
|
in: 0,
|
||||||
|
out: 0,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metrics,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectServerStats(): Promise<{
|
private async collectServerStats(): Promise<{
|
||||||
@@ -178,25 +306,30 @@ export class StatsHandler {
|
|||||||
value: number;
|
value: number;
|
||||||
}>;
|
}>;
|
||||||
}> {
|
}> {
|
||||||
|
// Get metrics from MetricsManager if available
|
||||||
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
const serverStats = await this.opsServerRef.dcRouterRef.metricsManager.getServerStats();
|
||||||
|
return {
|
||||||
|
uptime: serverStats.uptime,
|
||||||
|
cpuUsage: serverStats.cpuUsage,
|
||||||
|
memoryUsage: serverStats.memoryUsage,
|
||||||
|
requestsPerSecond: serverStats.requestsPerSecond,
|
||||||
|
activeConnections: serverStats.activeConnections,
|
||||||
|
totalConnections: serverStats.totalConnections,
|
||||||
|
history: [], // TODO: Implement history tracking
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to basic stats if MetricsManager not available
|
||||||
const uptime = process.uptime();
|
const uptime = process.uptime();
|
||||||
const memUsage = process.memoryUsage();
|
const memUsage = process.memoryUsage();
|
||||||
const totalMem = plugins.os.totalmem();
|
|
||||||
const freeMem = plugins.os.freemem();
|
|
||||||
const usedMem = totalMem - freeMem;
|
|
||||||
|
|
||||||
// Get CPU usage (simplified - in production would use proper monitoring)
|
|
||||||
const cpuUsage = plugins.os.loadavg()[0] * 100 / plugins.os.cpus().length;
|
const cpuUsage = plugins.os.loadavg()[0] * 100 / plugins.os.cpus().length;
|
||||||
|
|
||||||
// TODO: Implement proper request tracking
|
|
||||||
const requestsPerSecond = 0;
|
|
||||||
const activeConnections = 0;
|
|
||||||
const totalConnections = 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uptime,
|
uptime,
|
||||||
cpuUsage: {
|
cpuUsage: {
|
||||||
user: cpuUsage * 0.7, // Approximate user CPU
|
user: cpuUsage * 0.7,
|
||||||
system: cpuUsage * 0.3, // Approximate system CPU
|
system: cpuUsage * 0.3,
|
||||||
},
|
},
|
||||||
memoryUsage: {
|
memoryUsage: {
|
||||||
heapUsed: memUsage.heapUsed,
|
heapUsed: memUsage.heapUsed,
|
||||||
@@ -204,10 +337,10 @@ export class StatsHandler {
|
|||||||
external: memUsage.external,
|
external: memUsage.external,
|
||||||
rss: memUsage.rss,
|
rss: memUsage.rss,
|
||||||
},
|
},
|
||||||
requestsPerSecond,
|
requestsPerSecond: 0,
|
||||||
activeConnections,
|
activeConnections: 0,
|
||||||
totalConnections,
|
totalConnections: 0,
|
||||||
history: [], // TODO: Implement history tracking
|
history: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +352,19 @@ export class StatsHandler {
|
|||||||
queueSize: number;
|
queueSize: number;
|
||||||
domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
|
domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
|
||||||
}> {
|
}> {
|
||||||
// TODO: Implement actual email statistics collection
|
// Get metrics from MetricsManager if available
|
||||||
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
const emailStats = await this.opsServerRef.dcRouterRef.metricsManager.getEmailStats();
|
||||||
|
return {
|
||||||
|
sentToday: emailStats.sentToday,
|
||||||
|
receivedToday: emailStats.receivedToday,
|
||||||
|
bounceRate: emailStats.bounceRate,
|
||||||
|
deliveryRate: emailStats.deliveryRate,
|
||||||
|
queueSize: emailStats.queueSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if MetricsManager not available
|
||||||
return {
|
return {
|
||||||
sentToday: 0,
|
sentToday: 0,
|
||||||
receivedToday: 0,
|
receivedToday: 0,
|
||||||
@@ -242,7 +387,21 @@ export class StatsHandler {
|
|||||||
queryTypes: { [key: string]: number };
|
queryTypes: { [key: string]: number };
|
||||||
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
|
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
|
||||||
}> {
|
}> {
|
||||||
// TODO: Implement actual DNS statistics collection
|
// Get metrics from MetricsManager if available
|
||||||
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
const dnsStats = await this.opsServerRef.dcRouterRef.metricsManager.getDnsStats();
|
||||||
|
return {
|
||||||
|
queriesPerSecond: dnsStats.queriesPerSecond,
|
||||||
|
totalQueries: dnsStats.totalQueries,
|
||||||
|
cacheHits: dnsStats.cacheHits,
|
||||||
|
cacheMisses: dnsStats.cacheMisses,
|
||||||
|
cacheHitRate: dnsStats.cacheHitRate,
|
||||||
|
topDomains: dnsStats.topDomains,
|
||||||
|
queryTypes: dnsStats.queryTypes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if MetricsManager not available
|
||||||
return {
|
return {
|
||||||
queriesPerSecond: 0,
|
queriesPerSecond: 0,
|
||||||
totalQueries: 0,
|
totalQueries: 0,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import * as smartguard from '@push.rocks/smartguard';
|
|||||||
import * as smartjwt from '@push.rocks/smartjwt';
|
import * as smartjwt from '@push.rocks/smartjwt';
|
||||||
import * as smartlog from '@push.rocks/smartlog';
|
import * as smartlog from '@push.rocks/smartlog';
|
||||||
import * as smartmail from '@push.rocks/smartmail';
|
import * as smartmail from '@push.rocks/smartmail';
|
||||||
|
import * as smartmetrics from '@push.rocks/smartmetrics';
|
||||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartproxy from '@push.rocks/smartproxy';
|
import * as smartproxy from '@push.rocks/smartproxy';
|
||||||
@@ -59,7 +60,7 @@ import * as smartrule from '@push.rocks/smartrule';
|
|||||||
import * as smartrx from '@push.rocks/smartrx';
|
import * as smartrx from '@push.rocks/smartrx';
|
||||||
import * as smartunique from '@push.rocks/smartunique';
|
import * as smartunique from '@push.rocks/smartunique';
|
||||||
|
|
||||||
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmail, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx, smartunique };
|
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfile, smartguard, smartjwt, smartlog, smartmail, smartmetrics, smartnetwork, smartpath, smartproxy, smartpromise, smartrequest, smartrule, smartrx, smartunique };
|
||||||
|
|
||||||
// Define SmartLog types for use in error handling
|
// Define SmartLog types for use in error handling
|
||||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export interface IServerStats {
|
|||||||
heapTotal: number;
|
heapTotal: number;
|
||||||
external: number;
|
external: number;
|
||||||
rss: number;
|
rss: number;
|
||||||
|
// SmartMetrics memory data
|
||||||
|
maxMemoryMB?: number;
|
||||||
|
actualUsageBytes?: number;
|
||||||
|
actualUsagePercentage?: number;
|
||||||
};
|
};
|
||||||
cpuUsage: {
|
cpuUsage: {
|
||||||
user: number;
|
user: number;
|
||||||
@@ -99,3 +103,29 @@ export interface IHealthStatus {
|
|||||||
};
|
};
|
||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INetworkMetrics {
|
||||||
|
totalBandwidth: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
activeConnections: number;
|
||||||
|
connectionDetails: IConnectionDetails[];
|
||||||
|
topEndpoints: Array<{
|
||||||
|
endpoint: string;
|
||||||
|
requests: number;
|
||||||
|
bandwidth: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConnectionDetails {
|
||||||
|
remoteAddress: string;
|
||||||
|
protocol: 'http' | 'https' | 'smtp' | 'smtps';
|
||||||
|
state: 'connecting' | 'connected' | 'established' | 'closing';
|
||||||
|
startTime: number;
|
||||||
|
bytesIn: number;
|
||||||
|
bytesOut: number;
|
||||||
|
}
|
||||||
25
ts_interfaces/requests/combined.stats.ts
Normal file
25
ts_interfaces/requests/combined.stats.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type * as data from '../data/index.js';
|
||||||
|
|
||||||
|
export interface IReq_GetCombinedMetrics {
|
||||||
|
method: 'getCombinedMetrics';
|
||||||
|
request: {
|
||||||
|
identity: data.IIdentity;
|
||||||
|
sections?: {
|
||||||
|
server?: boolean;
|
||||||
|
email?: boolean;
|
||||||
|
dns?: boolean;
|
||||||
|
security?: boolean;
|
||||||
|
network?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
metrics: {
|
||||||
|
server?: data.IServerStats;
|
||||||
|
email?: data.IEmailStats;
|
||||||
|
dns?: data.IDnsStats;
|
||||||
|
security?: data.ISecurityMetrics;
|
||||||
|
network?: data.INetworkMetrics;
|
||||||
|
};
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export * from './admin.js';
|
|||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
export * from './logs.js';
|
export * from './logs.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
|
export * from './combined.stats.js';
|
||||||
@@ -43,6 +43,16 @@ export interface ILogState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INetworkState {
|
||||||
|
connections: interfaces.data.IConnectionInfo[];
|
||||||
|
connectionsByIP: { [ip: string]: number };
|
||||||
|
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||||
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
|
lastUpdated: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// Create state parts with appropriate persistence
|
// Create state parts with appropriate persistence
|
||||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||||
'login',
|
'login',
|
||||||
@@ -79,10 +89,10 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||||
'ui',
|
'ui',
|
||||||
{
|
{
|
||||||
activeView: 'dashboard',
|
activeView: 'overview',
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 30000, // 30 seconds
|
refreshInterval: 1000, // 1 second
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -97,6 +107,20 @@ export const logStatePart = await appState.getStatePart<ILogState>(
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||||
|
'network',
|
||||||
|
{
|
||||||
|
connections: [],
|
||||||
|
connectionsByIP: {},
|
||||||
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
topIPs: [],
|
||||||
|
lastUpdated: 0,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
'soft'
|
||||||
|
);
|
||||||
|
|
||||||
// Actions for state management
|
// Actions for state management
|
||||||
interface IActionContext {
|
interface IActionContext {
|
||||||
identity: interfaces.data.IIdentity | null;
|
identity: interfaces.data.IIdentity | null;
|
||||||
@@ -160,56 +184,35 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch All Stats Action
|
// Fetch All Stats Action - Using combined endpoint for efficiency
|
||||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch server stats
|
// Use combined metrics endpoint - single request instead of 4
|
||||||
const serverStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetServerStatistics
|
interfaces.requests.IReq_GetCombinedMetrics
|
||||||
>('/typedrequest', 'getServerStatistics');
|
>('/typedrequest', 'getCombinedMetrics');
|
||||||
|
|
||||||
const serverStatsResponse = await serverStatsRequest.fire({
|
const combinedResponse = await combinedRequest.fire({
|
||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
includeHistory: false,
|
sections: {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: false, // Network is fetched separately for the network view
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch email stats
|
// Update state with all stats from combined response
|
||||||
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 {
|
return {
|
||||||
serverStats: serverStatsResponse.stats,
|
serverStats: combinedResponse.metrics.server || currentState.serverStats,
|
||||||
emailStats: emailStatsResponse.stats,
|
emailStats: combinedResponse.metrics.email || currentState.emailStats,
|
||||||
dnsStats: dnsStatsResponse.stats,
|
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
|
||||||
securityMetrics: securityResponse.metrics,
|
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -318,23 +321,201 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
|
|||||||
// Set Active View Action
|
// Set Active View Action
|
||||||
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
// If switching to network view, ensure we fetch network data
|
||||||
|
if (viewName === 'network' && currentState.activeView !== 'network') {
|
||||||
|
setTimeout(() => {
|
||||||
|
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
activeView: viewName,
|
activeView: viewName,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch Network Stats Action
|
||||||
|
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch active connections using the existing endpoint
|
||||||
|
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetActiveConnections
|
||||||
|
>('/typedrequest', 'getActiveConnections');
|
||||||
|
|
||||||
|
const connectionsResponse = await connectionsRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get network stats for throughput and IP data
|
||||||
|
const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest(
|
||||||
|
'/typedrequest',
|
||||||
|
'getNetworkStats'
|
||||||
|
);
|
||||||
|
|
||||||
|
const networkStatsResponse = await networkStatsRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
}) as any;
|
||||||
|
|
||||||
|
// Use the connections data for the connection list
|
||||||
|
// and network stats for throughput and IP analytics
|
||||||
|
const connectionsByIP: { [ip: string]: number } = {};
|
||||||
|
|
||||||
|
// Build connectionsByIP from network stats if available
|
||||||
|
if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) {
|
||||||
|
networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => {
|
||||||
|
connectionsByIP[item.ip] = item.count;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback: calculate from connections
|
||||||
|
connectionsResponse.connections.forEach(conn => {
|
||||||
|
const ip = conn.remoteAddress;
|
||||||
|
connectionsByIP[ip] = (connectionsByIP[ip] || 0) + 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
connections: connectionsResponse.connections,
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
topIPs: networkStatsResponse.topIPs || [],
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch network stats:', error);
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch network stats',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Combined refresh action for efficient polling
|
||||||
|
async function dispatchCombinedRefreshAction() {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentView = uiStatePart.getState().activeView;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Always fetch basic stats for dashboard widgets
|
||||||
|
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetCombinedMetrics
|
||||||
|
>('/typedrequest', 'getCombinedMetrics');
|
||||||
|
|
||||||
|
const combinedResponse = await combinedRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
sections: {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: currentView === 'network', // Only fetch network if on network view
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update all stats from combined response
|
||||||
|
statsStatePart.setState({
|
||||||
|
...statsStatePart.getState(),
|
||||||
|
serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats,
|
||||||
|
emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats,
|
||||||
|
dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats,
|
||||||
|
securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update network stats if included
|
||||||
|
if (combinedResponse.metrics.network && currentView === 'network') {
|
||||||
|
const network = combinedResponse.metrics.network;
|
||||||
|
const connectionsByIP: { [ip: string]: number } = {};
|
||||||
|
|
||||||
|
// Convert connection details to IP counts
|
||||||
|
network.connectionDetails.forEach(conn => {
|
||||||
|
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch detailed connections for the network view
|
||||||
|
try {
|
||||||
|
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetActiveConnections
|
||||||
|
>('/typedrequest', 'getActiveConnections');
|
||||||
|
|
||||||
|
const connectionsResponse = await connectionsRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
networkStatePart.setState({
|
||||||
|
...networkStatePart.getState(),
|
||||||
|
connections: connectionsResponse.connections,
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate: {
|
||||||
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
|
},
|
||||||
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch connections:', error);
|
||||||
|
networkStatePart.setState({
|
||||||
|
...networkStatePart.getState(),
|
||||||
|
connections: [],
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate: {
|
||||||
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
|
},
|
||||||
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Combined refresh failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize auto-refresh
|
// Initialize auto-refresh
|
||||||
let refreshInterval: NodeJS.Timeout | null = null;
|
let refreshInterval: NodeJS.Timeout | null = null;
|
||||||
|
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
|
||||||
|
|
||||||
// Initialize auto-refresh when UI state is ready
|
// Initialize auto-refresh when UI state is ready
|
||||||
(() => {
|
(() => {
|
||||||
const startAutoRefresh = () => {
|
const startAutoRefresh = () => {
|
||||||
const uiState = uiStatePart.getState();
|
const uiState = uiStatePart.getState();
|
||||||
if (uiState.autoRefresh && loginStatePart.getState().isLoggedIn) {
|
const loginState = loginStatePart.getState();
|
||||||
refreshInterval = setInterval(() => {
|
|
||||||
statsStatePart.dispatchAction(fetchAllStatsAction, null);
|
// Only start if conditions are met and not already running at the same rate
|
||||||
}, uiState.refreshInterval);
|
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||||
|
// Check if we need to restart the interval (rate changed or not running)
|
||||||
|
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
|
||||||
|
stopAutoRefresh();
|
||||||
|
currentRefreshRate = uiState.refreshInterval;
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
// Use combined refresh action for efficiency
|
||||||
|
dispatchCombinedRefreshAction();
|
||||||
|
|
||||||
|
// If network view is active, also ensure we have fresh network data
|
||||||
|
const currentView = uiStatePart.getState().activeView;
|
||||||
|
if (currentView === 'network') {
|
||||||
|
// Network view needs more frequent updates, fetch directly
|
||||||
|
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
||||||
|
}
|
||||||
|
}, uiState.refreshInterval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -342,18 +523,31 @@ let refreshInterval: NodeJS.Timeout | null = null;
|
|||||||
if (refreshInterval) {
|
if (refreshInterval) {
|
||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
refreshInterval = null;
|
refreshInterval = null;
|
||||||
|
currentRefreshRate = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes
|
// Watch for relevant changes only
|
||||||
uiStatePart.state.subscribe(() => {
|
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
|
||||||
stopAutoRefresh();
|
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
|
||||||
startAutoRefresh();
|
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
|
||||||
|
|
||||||
|
uiStatePart.state.subscribe((state) => {
|
||||||
|
// Only restart if relevant values changed
|
||||||
|
if (state.autoRefresh !== previousAutoRefresh ||
|
||||||
|
state.refreshInterval !== previousRefreshInterval) {
|
||||||
|
previousAutoRefresh = state.autoRefresh;
|
||||||
|
previousRefreshInterval = state.refreshInterval;
|
||||||
|
startAutoRefresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loginStatePart.state.subscribe(() => {
|
loginStatePart.state.subscribe((state) => {
|
||||||
stopAutoRefresh();
|
// Only restart if login state changed
|
||||||
startAutoRefresh();
|
if (state.isLoggedIn !== previousIsLoggedIn) {
|
||||||
|
previousIsLoggedIn = state.isLoggedIn;
|
||||||
|
startAutoRefresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial start
|
// Initial start
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from './ops-dashboard.js';
|
export * from './ops-dashboard.js';
|
||||||
export * from './ops-view-overview.js';
|
export * from './ops-view-overview.js';
|
||||||
export * from './ops-view-stats.js';
|
export * from './ops-view-network.js';
|
||||||
|
export * from './ops-view-emails.js';
|
||||||
export * from './ops-view-logs.js';
|
export * from './ops-view-logs.js';
|
||||||
export * from './ops-view-config.js';
|
export * from './ops-view-config.js';
|
||||||
export * from './ops-view-security.js';
|
export * from './ops-view-security.js';
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
|
|
||||||
// Import view components
|
// Import view components
|
||||||
import { OpsViewOverview } from './ops-view-overview.js';
|
import { OpsViewOverview } from './ops-view-overview.js';
|
||||||
import { OpsViewStats } from './ops-view-stats.js';
|
import { OpsViewNetwork } from './ops-view-network.js';
|
||||||
|
import { OpsViewEmails } from './ops-view-emails.js';
|
||||||
import { OpsViewLogs } from './ops-view-logs.js';
|
import { OpsViewLogs } from './ops-view-logs.js';
|
||||||
import { OpsViewConfig } from './ops-view-config.js';
|
import { OpsViewConfig } from './ops-view-config.js';
|
||||||
import { OpsViewSecurity } from './ops-view-security.js';
|
import { OpsViewSecurity } from './ops-view-security.js';
|
||||||
@@ -26,13 +27,41 @@ export class OpsDashboard extends DeesElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@state() private uiState: appstate.IUiState = {
|
@state() private uiState: appstate.IUiState = {
|
||||||
activeView: 'dashboard',
|
activeView: 'overview',
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 30000,
|
refreshInterval: 1000,
|
||||||
theme: 'light',
|
theme: 'light',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store viewTabs as a property to maintain object references
|
||||||
|
private viewTabs = [
|
||||||
|
{
|
||||||
|
name: 'Overview',
|
||||||
|
element: OpsViewOverview,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Network',
|
||||||
|
element: OpsViewNetwork,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Emails',
|
||||||
|
element: OpsViewEmails,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logs',
|
||||||
|
element: OpsViewLogs,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Configuration',
|
||||||
|
element: OpsViewConfig,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Security',
|
||||||
|
element: OpsViewSecurity,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
document.title = 'DCRouter OpsServer';
|
document.title = 'DCRouter OpsServer';
|
||||||
@@ -78,36 +107,7 @@ export class OpsDashboard extends DeesElement {
|
|||||||
>
|
>
|
||||||
<dees-simple-appdash
|
<dees-simple-appdash
|
||||||
name="DCRouter OpsServer"
|
name="DCRouter OpsServer"
|
||||||
.viewTabs=${[
|
.viewTabs=${this.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-appdash>
|
||||||
</dees-simple-login>
|
</dees-simple-login>
|
||||||
@@ -118,13 +118,27 @@ export class OpsDashboard extends DeesElement {
|
|||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
||||||
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
||||||
console.log(e.detail);
|
// Handle logout event
|
||||||
this.login(e.detail.data.username, e.detail.data.password);
|
this.login(e.detail.data.username, e.detail.data.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle view changes
|
||||||
|
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
|
||||||
|
if (appDash) {
|
||||||
|
appDash.addEventListener('view-select', (e: CustomEvent) => {
|
||||||
|
const viewName = e.detail.view.name;
|
||||||
|
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle logout event
|
||||||
|
appDash.addEventListener('logout', async () => {
|
||||||
|
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle initial state
|
// Handle initial state
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
console.log('Initial login state:', loginState);
|
// Check initial login state
|
||||||
if (loginState.identity) {
|
if (loginState.identity) {
|
||||||
this.loginState = loginState;
|
this.loginState = loginState;
|
||||||
await simpleLogin.switchToSlottedContent();
|
await simpleLogin.switchToSlottedContent();
|
||||||
@@ -158,8 +172,4 @@ export class OpsDashboard extends DeesElement {
|
|||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logout() {
|
|
||||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -41,17 +41,17 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.configSection {
|
.configSection {
|
||||||
background: white;
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
background: #f8f9fa;
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
padding: 16px 24px;
|
padding: 16px 24px;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -60,7 +60,7 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionContent {
|
.sectionContent {
|
||||||
@@ -74,7 +74,7 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
.fieldLabel {
|
.fieldLabel {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -82,11 +82,11 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
.fieldValue {
|
.fieldValue {
|
||||||
font-family: 'Consolas', 'Monaco', monospace;
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
background: #f8f9fa;
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.configEditor {
|
.configEditor {
|
||||||
@@ -95,9 +95,9 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
font-family: 'Consolas', 'Monaco', monospace;
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #f8f9fa;
|
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,30 +108,30 @@ export class OpsViewConfig extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.warning {
|
||||||
background: #fff3cd;
|
background: ${cssManager.bdTheme('#fff3cd', '#4a4a1a')};
|
||||||
border: 1px solid #ffeaa7;
|
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#666633')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
color: #856404;
|
color: ${cssManager.bdTheme('#856404', '#ffcc66')};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
background: #fee;
|
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
||||||
border: 1px solid #fcc;
|
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #c00;
|
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loadingMessage {
|
.loadingMessage {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|||||||
735
ts_web/elements/ops-view-emails.ts
Normal file
735
ts_web/elements/ops-view-emails.ts
Normal file
@@ -0,0 +1,735 @@
|
|||||||
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as shared from './shared/index.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'ops-view-emails': OpsViewEmails;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IEmail {
|
||||||
|
id: string;
|
||||||
|
from: string;
|
||||||
|
to: string[];
|
||||||
|
cc?: string[];
|
||||||
|
bcc?: string[];
|
||||||
|
subject: string;
|
||||||
|
body: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
filename: string;
|
||||||
|
size: number;
|
||||||
|
contentType: string;
|
||||||
|
}>;
|
||||||
|
date: number;
|
||||||
|
read: boolean;
|
||||||
|
folder: 'inbox' | 'sent' | 'draft' | 'trash';
|
||||||
|
flags?: string[];
|
||||||
|
messageId?: string;
|
||||||
|
inReplyTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('ops-view-emails')
|
||||||
|
export class OpsViewEmails extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private selectedFolder: 'inbox' | 'sent' | 'draft' | 'trash' = 'inbox';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private emails: IEmail[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private selectedEmail: IEmail | null = null;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private showCompose = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private isLoading = false;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private searchTerm = '';
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private emailDomains: string[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.loadEmails();
|
||||||
|
this.loadEmailDomains();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
shared.viewHostCss,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailLayout {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainArea {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailToolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailList {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailPreview {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailHeader {
|
||||||
|
padding: 24px;
|
||||||
|
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailSubject {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailMeta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailMetaRow {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailMetaLabel {
|
||||||
|
font-weight: 600;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailBody {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emailActions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-top: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
|
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyState {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyIcon {
|
||||||
|
font-size: 64px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptyText {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-read {
|
||||||
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-unread {
|
||||||
|
color: ${cssManager.bdTheme('#1976d2', '#4a90e2')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-icon {
|
||||||
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.selectedEmail) {
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Emails</ops-sectionheading>
|
||||||
|
<div class="emailLayout">
|
||||||
|
<div class="sidebar">
|
||||||
|
<dees-windowbox>
|
||||||
|
<dees-button @click=${() => this.selectedEmail = null} type="secondary" style="width: 100%;">
|
||||||
|
<dees-icon name="arrowLeft" slot="iconSlot"></dees-icon>
|
||||||
|
Back to List
|
||||||
|
</dees-button>
|
||||||
|
<dees-menu style="margin-top: 16px;">
|
||||||
|
<dees-menu-item
|
||||||
|
.active=${this.selectedFolder === 'inbox'}
|
||||||
|
@click=${() => { this.selectFolder('inbox'); this.selectedEmail = null; }}
|
||||||
|
.iconName=${'inbox'}
|
||||||
|
.label=${'Inbox'}
|
||||||
|
.badgeText=${this.getEmailCount('inbox') > 0 ? String(this.getEmailCount('inbox')) : ''}
|
||||||
|
></dees-menu-item>
|
||||||
|
<dees-menu-item
|
||||||
|
.active=${this.selectedFolder === 'sent'}
|
||||||
|
@click=${() => { this.selectFolder('sent'); this.selectedEmail = null; }}
|
||||||
|
.iconName=${'paperPlane'}
|
||||||
|
.label=${'Sent'}
|
||||||
|
.badgeText=${this.getEmailCount('sent') > 0 ? String(this.getEmailCount('sent')) : ''}
|
||||||
|
></dees-menu-item>
|
||||||
|
<dees-menu-item
|
||||||
|
.active=${this.selectedFolder === 'draft'}
|
||||||
|
@click=${() => { this.selectFolder('draft'); this.selectedEmail = null; }}
|
||||||
|
.iconName=${'file'}
|
||||||
|
.label=${'Drafts'}
|
||||||
|
.badgeText=${this.getEmailCount('draft') > 0 ? String(this.getEmailCount('draft')) : ''}
|
||||||
|
></dees-menu-item>
|
||||||
|
<dees-menu-item
|
||||||
|
.active=${this.selectedFolder === 'trash'}
|
||||||
|
@click=${() => { this.selectFolder('trash'); this.selectedEmail = null; }}
|
||||||
|
.iconName=${'trash'}
|
||||||
|
.label=${'Trash'}
|
||||||
|
.badgeText=${this.getEmailCount('trash') > 0 ? String(this.getEmailCount('trash')) : ''}
|
||||||
|
></dees-menu-item>
|
||||||
|
</dees-menu>
|
||||||
|
</dees-windowbox>
|
||||||
|
</div>
|
||||||
|
<div class="mainArea">
|
||||||
|
${this.renderEmailPreview()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Emails</ops-sectionheading>
|
||||||
|
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="emailToolbar" style="margin-bottom: 16px;">
|
||||||
|
<dees-button @click=${() => this.openComposeModal()} type="highlighted">
|
||||||
|
<dees-icon name="penToSquare" slot="iconSlot"></dees-icon>
|
||||||
|
Compose
|
||||||
|
</dees-button>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
class="searchBox"
|
||||||
|
placeholder="Search emails..."
|
||||||
|
.value=${this.searchTerm}
|
||||||
|
@input=${(e: Event) => this.searchTerm = (e.target as any).value}
|
||||||
|
>
|
||||||
|
<dees-icon name="magnifyingGlass" slot="iconSlot"></dees-icon>
|
||||||
|
</dees-input-text>
|
||||||
|
|
||||||
|
<dees-button @click=${() => this.refreshEmails()}>
|
||||||
|
${this.isLoading ? html`<dees-spinner slot="iconSlot" size="small"></dees-spinner>` : html`<dees-icon slot="iconSlot" name="arrowsRotate"></dees-icon>`}
|
||||||
|
Refresh
|
||||||
|
</dees-button>
|
||||||
|
|
||||||
|
<dees-button @click=${() => this.markAllAsRead()}>
|
||||||
|
<dees-icon name="envelopeOpen" slot="iconSlot"></dees-icon>
|
||||||
|
Mark all read
|
||||||
|
</dees-button>
|
||||||
|
|
||||||
|
<div style="margin-left: auto; display: flex; gap: 8px;">
|
||||||
|
<dees-button-group>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => this.selectFolder('inbox')}
|
||||||
|
.type=${this.selectedFolder === 'inbox' ? 'highlighted' : 'normal'}
|
||||||
|
>
|
||||||
|
Inbox ${this.getEmailCount('inbox') > 0 ? `(${this.getEmailCount('inbox')})` : ''}
|
||||||
|
</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => this.selectFolder('sent')}
|
||||||
|
.type=${this.selectedFolder === 'sent' ? 'highlighted' : 'normal'}
|
||||||
|
>
|
||||||
|
Sent
|
||||||
|
</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => this.selectFolder('draft')}
|
||||||
|
.type=${this.selectedFolder === 'draft' ? 'highlighted' : 'normal'}
|
||||||
|
>
|
||||||
|
Drafts ${this.getEmailCount('draft') > 0 ? `(${this.getEmailCount('draft')})` : ''}
|
||||||
|
</dees-button>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => this.selectFolder('trash')}
|
||||||
|
.type=${this.selectedFolder === 'trash' ? 'highlighted' : 'normal'}
|
||||||
|
>
|
||||||
|
Trash
|
||||||
|
</dees-button>
|
||||||
|
</dees-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.renderEmailList()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private renderEmailList() {
|
||||||
|
const filteredEmails = this.getFilteredEmails();
|
||||||
|
|
||||||
|
if (filteredEmails.length === 0) {
|
||||||
|
return html`
|
||||||
|
<div class="emptyState">
|
||||||
|
<dees-icon class="emptyIcon" name="envelope"></dees-icon>
|
||||||
|
<div class="emptyText">No emails in ${this.selectedFolder}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.data=${filteredEmails}
|
||||||
|
.displayFunction=${(email: IEmail) => ({
|
||||||
|
'Status': html`<dees-icon name="${email.read ? 'envelopeOpen' : 'envelope'}" class="${email.read ? 'email-read' : 'email-unread'}"></dees-icon>`,
|
||||||
|
From: email.from,
|
||||||
|
Subject: html`<strong style="${!email.read ? 'font-weight: 600' : ''}">${email.subject}</strong>`,
|
||||||
|
Date: this.formatDate(email.date),
|
||||||
|
'Attach': html`
|
||||||
|
${email.attachments?.length ? html`<dees-icon name="paperclip" class="attachment-icon"></dees-icon>` : ''}
|
||||||
|
`,
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Read',
|
||||||
|
iconName: 'eye',
|
||||||
|
type: ['doubleClick', 'inRow'],
|
||||||
|
actionFunc: async (actionData) => {
|
||||||
|
this.selectedEmail = actionData.item;
|
||||||
|
if (!actionData.item.read) {
|
||||||
|
this.markAsRead(actionData.item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Reply',
|
||||||
|
iconName: 'reply',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
actionFunc: async (actionData) => {
|
||||||
|
this.replyToEmail(actionData.item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Forward',
|
||||||
|
iconName: 'share',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
actionFunc: async (actionData) => {
|
||||||
|
this.forwardEmail(actionData.item);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'trash',
|
||||||
|
type: ['contextmenu'],
|
||||||
|
actionFunc: async (actionData) => {
|
||||||
|
this.deleteEmail(actionData.item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.selectionMode=${'single'}
|
||||||
|
heading1=${this.selectedFolder.charAt(0).toUpperCase() + this.selectedFolder.slice(1)}
|
||||||
|
heading2=${`${filteredEmails.length} emails`}
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEmailPreview() {
|
||||||
|
if (!this.selectedEmail) return '';
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="emailPreview">
|
||||||
|
<div class="emailHeader">
|
||||||
|
<div class="emailSubject">${this.selectedEmail.subject}</div>
|
||||||
|
<div class="emailMeta">
|
||||||
|
<div class="emailMetaRow">
|
||||||
|
<span class="emailMetaLabel">From:</span>
|
||||||
|
<span>${this.selectedEmail.from}</span>
|
||||||
|
</div>
|
||||||
|
<div class="emailMetaRow">
|
||||||
|
<span class="emailMetaLabel">To:</span>
|
||||||
|
<span>${this.selectedEmail.to.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
${this.selectedEmail.cc?.length ? html`
|
||||||
|
<div class="emailMetaRow">
|
||||||
|
<span class="emailMetaLabel">CC:</span>
|
||||||
|
<span>${this.selectedEmail.cc.join(', ')}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="emailMetaRow">
|
||||||
|
<span class="emailMetaLabel">Date:</span>
|
||||||
|
<span>${new Date(this.selectedEmail.date).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emailBody">
|
||||||
|
${this.selectedEmail.html ?
|
||||||
|
html`<div .innerHTML=${this.selectedEmail.html}></div>` :
|
||||||
|
html`<div style="white-space: pre-wrap;">${this.selectedEmail.body}</div>`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="emailActions">
|
||||||
|
<div style="display: flex; gap: 8px;">
|
||||||
|
<dees-button @click=${() => this.replyToEmail(this.selectedEmail!)}>
|
||||||
|
<dees-icon name="reply" slot="iconSlot"></dees-icon>
|
||||||
|
Reply
|
||||||
|
</dees-button>
|
||||||
|
<dees-button @click=${() => this.replyAllToEmail(this.selectedEmail!)}>
|
||||||
|
<dees-icon name="replyAll" slot="iconSlot"></dees-icon>
|
||||||
|
Reply All
|
||||||
|
</dees-button>
|
||||||
|
<dees-button @click=${() => this.forwardEmail(this.selectedEmail!)}>
|
||||||
|
<dees-icon name="share" slot="iconSlot"></dees-icon>
|
||||||
|
Forward
|
||||||
|
</dees-button>
|
||||||
|
<dees-button @click=${() => this.deleteEmail(this.selectedEmail!.id)} type="danger">
|
||||||
|
<dees-icon name="trash" slot="iconSlot"></dees-icon>
|
||||||
|
Delete
|
||||||
|
</dees-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async openComposeModal(replyTo?: IEmail, replyAll = false, forward = false) {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
// Ensure domains are loaded before opening modal
|
||||||
|
if (this.emailDomains.length === 0) {
|
||||||
|
await this.loadEmailDomains();
|
||||||
|
}
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: forward ? 'Forward Email' : replyTo ? 'Reply to Email' : 'New Email',
|
||||||
|
width: 'large',
|
||||||
|
content: html`
|
||||||
|
<div>
|
||||||
|
<dees-form @formData=${async (e: CustomEvent) => {
|
||||||
|
await this.sendEmail(e.detail);
|
||||||
|
// Close modal after sending
|
||||||
|
const modals = document.querySelectorAll('dees-modal');
|
||||||
|
modals.forEach(m => (m as any).destroy?.());
|
||||||
|
}}>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: flex-end;">
|
||||||
|
<dees-input-text
|
||||||
|
key="fromUsername"
|
||||||
|
label="From"
|
||||||
|
placeholder="username"
|
||||||
|
.value=${'admin'}
|
||||||
|
required
|
||||||
|
style="flex: 1;"
|
||||||
|
></dees-input-text>
|
||||||
|
<span style="padding-bottom: 12px; font-size: 18px; color: #666;">@</span>
|
||||||
|
<dees-input-dropdown
|
||||||
|
key="fromDomain"
|
||||||
|
label=" "
|
||||||
|
.options=${this.emailDomains.length > 0
|
||||||
|
? this.emailDomains.map(domain => ({ key: domain, value: domain }))
|
||||||
|
: [{ key: 'dcrouter.local', value: 'dcrouter.local' }]}
|
||||||
|
.selectedKey=${this.emailDomains[0] || 'dcrouter.local'}
|
||||||
|
required
|
||||||
|
style="flex: 1;"
|
||||||
|
></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
key="to"
|
||||||
|
label="To"
|
||||||
|
placeholder="Enter recipient email addresses..."
|
||||||
|
.value=${replyTo ? (replyAll ? [replyTo.from, ...replyTo.to].filter((v, i, a) => a.indexOf(v) === i) : [replyTo.from]) : []}
|
||||||
|
required
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
key="cc"
|
||||||
|
label="CC"
|
||||||
|
placeholder="Enter CC recipients..."
|
||||||
|
.value=${replyAll && replyTo?.cc ? replyTo.cc : []}
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-tags
|
||||||
|
key="bcc"
|
||||||
|
label="BCC"
|
||||||
|
placeholder="Enter BCC recipients..."
|
||||||
|
></dees-input-tags>
|
||||||
|
|
||||||
|
<dees-input-text
|
||||||
|
key="subject"
|
||||||
|
label="Subject"
|
||||||
|
placeholder="Enter email subject..."
|
||||||
|
.value=${replyTo ? `${forward ? 'Fwd' : 'Re'}: ${replyTo.subject}` : ''}
|
||||||
|
required
|
||||||
|
></dees-input-text>
|
||||||
|
|
||||||
|
<dees-input-wysiwyg
|
||||||
|
key="body"
|
||||||
|
label="Message"
|
||||||
|
outputFormat="html"
|
||||||
|
.value=${replyTo && !forward ? `<p></p><hr><p>On ${new Date(replyTo.date).toLocaleString()}, ${replyTo.from} wrote:</p><blockquote>${replyTo.html || `<p>${replyTo.body}</p>`}</blockquote>` : replyTo && forward ? (replyTo.html || `<p>${replyTo.body}</p>`) : ''}
|
||||||
|
></dees-input-wysiwyg>
|
||||||
|
|
||||||
|
<dees-input-fileupload
|
||||||
|
key="attachments"
|
||||||
|
label="Attachments"
|
||||||
|
multiple
|
||||||
|
></dees-input-fileupload>
|
||||||
|
</dees-form>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Send',
|
||||||
|
iconName: 'paperPlane',
|
||||||
|
action: async (modalArg) => {
|
||||||
|
const form = modalArg.shadowRoot?.querySelector('dees-form') as any;
|
||||||
|
form?.submit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Cancel',
|
||||||
|
iconName: 'xmark',
|
||||||
|
action: async (modalArg) => await modalArg.destroy()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFilteredEmails(): IEmail[] {
|
||||||
|
let emails = this.emails.filter(e => e.folder === this.selectedFolder);
|
||||||
|
|
||||||
|
if (this.searchTerm) {
|
||||||
|
const search = this.searchTerm.toLowerCase();
|
||||||
|
emails = emails.filter(e =>
|
||||||
|
e.subject.toLowerCase().includes(search) ||
|
||||||
|
e.from.toLowerCase().includes(search) ||
|
||||||
|
e.body.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return emails.sort((a, b) => b.date - a.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEmailCount(folder: string): number {
|
||||||
|
return this.emails.filter(e => e.folder === folder && !e.read).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectFolder(folder: 'inbox' | 'sent' | 'draft' | 'trash') {
|
||||||
|
this.selectedFolder = folder;
|
||||||
|
this.selectedEmail = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatDate(timestamp: number): string {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now.getTime() - date.getTime();
|
||||||
|
const hours = diff / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (hours < 24) {
|
||||||
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||||
|
} else if (hours < 168) { // 7 days
|
||||||
|
return date.toLocaleDateString([], { weekday: 'short' });
|
||||||
|
} else {
|
||||||
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadEmails() {
|
||||||
|
// TODO: Load real emails from server
|
||||||
|
// For now, generate mock data
|
||||||
|
this.generateMockEmails();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadEmailDomains() {
|
||||||
|
try {
|
||||||
|
// Fetch configuration from the server
|
||||||
|
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||||
|
const config = appstate.configStatePart.getState().config;
|
||||||
|
|
||||||
|
if (config?.email?.domains && Array.isArray(config.email.domains) && config.email.domains.length > 0) {
|
||||||
|
this.emailDomains = config.email.domains;
|
||||||
|
} else {
|
||||||
|
// Fallback to default domains if none configured
|
||||||
|
this.emailDomains = ['dcrouter.local'];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load email domains:', error);
|
||||||
|
// Fallback to default domain on error
|
||||||
|
this.emailDomains = ['dcrouter.local'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshEmails() {
|
||||||
|
this.isLoading = true;
|
||||||
|
await this.loadEmails();
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendEmail(formData: any) {
|
||||||
|
try {
|
||||||
|
// TODO: Implement actual email sending via API
|
||||||
|
console.log('Sending email:', formData);
|
||||||
|
|
||||||
|
// Add to sent folder (mock)
|
||||||
|
// Combine username and domain
|
||||||
|
const fromEmail = `${formData.fromUsername || 'admin'}@${formData.fromDomain || this.emailDomains[0] || 'dcrouter.local'}`;
|
||||||
|
|
||||||
|
const newEmail: IEmail = {
|
||||||
|
id: `email-${Date.now()}`,
|
||||||
|
from: fromEmail,
|
||||||
|
to: formData.to || [],
|
||||||
|
cc: formData.cc || [],
|
||||||
|
bcc: formData.bcc || [],
|
||||||
|
subject: formData.subject,
|
||||||
|
body: formData.body.replace(/<[^>]*>/g, ''), // Strip HTML for plain text version
|
||||||
|
html: formData.body, // Store the HTML version
|
||||||
|
date: Date.now(),
|
||||||
|
read: true,
|
||||||
|
folder: 'sent',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emails = [...this.emails, newEmail];
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
console.log('Email sent successfully');
|
||||||
|
// TODO: Show toast notification when interface is available
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to send email', error);
|
||||||
|
// TODO: Show error toast notification when interface is available
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markAsRead(emailId: string) {
|
||||||
|
const email = this.emails.find(e => e.id === emailId);
|
||||||
|
if (email) {
|
||||||
|
email.read = true;
|
||||||
|
this.emails = [...this.emails];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markAllAsRead() {
|
||||||
|
this.emails = this.emails.map(e =>
|
||||||
|
e.folder === this.selectedFolder ? { ...e, read: true } : e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteEmail(emailId: string) {
|
||||||
|
const email = this.emails.find(e => e.id === emailId);
|
||||||
|
if (email) {
|
||||||
|
if (email.folder === 'trash') {
|
||||||
|
// Permanently delete
|
||||||
|
this.emails = this.emails.filter(e => e.id !== emailId);
|
||||||
|
} else {
|
||||||
|
// Move to trash
|
||||||
|
email.folder = 'trash';
|
||||||
|
this.emails = [...this.emails];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedEmail?.id === emailId) {
|
||||||
|
this.selectedEmail = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async replyToEmail(email: IEmail) {
|
||||||
|
this.openComposeModal(email, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async replyAllToEmail(email: IEmail) {
|
||||||
|
this.openComposeModal(email, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async forwardEmail(email: IEmail) {
|
||||||
|
this.openComposeModal(email, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMockEmails() {
|
||||||
|
const subjects = [
|
||||||
|
'Server Alert: High CPU Usage',
|
||||||
|
'Daily Report - Network Activity',
|
||||||
|
'Security Update Required',
|
||||||
|
'New User Registration',
|
||||||
|
'Backup Completed Successfully',
|
||||||
|
'DNS Query Spike Detected',
|
||||||
|
'SSL Certificate Renewal Notice',
|
||||||
|
'Monthly Usage Summary',
|
||||||
|
];
|
||||||
|
|
||||||
|
const senders = [
|
||||||
|
'monitoring@dcrouter.local',
|
||||||
|
'alerts@system.local',
|
||||||
|
'admin@company.com',
|
||||||
|
'noreply@service.com',
|
||||||
|
'support@vendor.com',
|
||||||
|
];
|
||||||
|
|
||||||
|
const bodies = [
|
||||||
|
'This is an automated alert regarding your server status.',
|
||||||
|
'Please review the attached report for detailed information.',
|
||||||
|
'Action required: Update your security settings.',
|
||||||
|
'Your daily summary is ready for review.',
|
||||||
|
'All systems are operating normally.',
|
||||||
|
];
|
||||||
|
|
||||||
|
this.emails = Array.from({ length: 50 }, (_, i) => ({
|
||||||
|
id: `email-${i}`,
|
||||||
|
from: senders[Math.floor(Math.random() * senders.length)],
|
||||||
|
to: ['admin@dcrouter.local'],
|
||||||
|
subject: subjects[Math.floor(Math.random() * subjects.length)],
|
||||||
|
body: bodies[Math.floor(Math.random() * bodies.length)],
|
||||||
|
date: Date.now() - (i * 3600000), // 1 hour apart
|
||||||
|
read: Math.random() > 0.3,
|
||||||
|
folder: i < 40 ? 'inbox' : i < 45 ? 'sent' : 'trash',
|
||||||
|
attachments: Math.random() > 0.8 ? [{
|
||||||
|
filename: 'report.pdf',
|
||||||
|
size: 1024 * 1024,
|
||||||
|
contentType: 'application/pdf',
|
||||||
|
}] : undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logContainer {
|
.logContainer {
|
||||||
background: #1e1e1e;
|
background: ${cssManager.bdTheme('#f8f9fa', '#1e1e1e')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
@@ -63,7 +63,7 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logTimestamp {
|
.logTimestamp {
|
||||||
color: #7a7a7a;
|
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,33 +76,33 @@ export class OpsViewLogs extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logLevel.debug {
|
.logLevel.debug {
|
||||||
color: #6a9955;
|
color: ${cssManager.bdTheme('#6a9955', '#6a9955')};
|
||||||
background: rgba(106, 153, 85, 0.1);
|
background: ${cssManager.bdTheme('rgba(106, 153, 85, 0.1)', 'rgba(106, 153, 85, 0.1)')};
|
||||||
}
|
}
|
||||||
.logLevel.info {
|
.logLevel.info {
|
||||||
color: #569cd6;
|
color: ${cssManager.bdTheme('#569cd6', '#569cd6')};
|
||||||
background: rgba(86, 156, 214, 0.1);
|
background: ${cssManager.bdTheme('rgba(86, 156, 214, 0.1)', 'rgba(86, 156, 214, 0.1)')};
|
||||||
}
|
}
|
||||||
.logLevel.warn {
|
.logLevel.warn {
|
||||||
color: #ce9178;
|
color: ${cssManager.bdTheme('#ce9178', '#ce9178')};
|
||||||
background: rgba(206, 145, 120, 0.1);
|
background: ${cssManager.bdTheme('rgba(206, 145, 120, 0.1)', 'rgba(206, 145, 120, 0.1)')};
|
||||||
}
|
}
|
||||||
.logLevel.error {
|
.logLevel.error {
|
||||||
color: #f44747;
|
color: ${cssManager.bdTheme('#f44747', '#f44747')};
|
||||||
background: rgba(244, 71, 71, 0.1);
|
background: ${cssManager.bdTheme('rgba(244, 71, 71, 0.1)', 'rgba(244, 71, 71, 0.1)')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.logCategory {
|
.logCategory {
|
||||||
color: #c586c0;
|
color: ${cssManager.bdTheme('#c586c0', '#c586c0')};
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logMessage {
|
.logMessage {
|
||||||
color: #d4d4d4;
|
color: ${cssManager.bdTheme('#333', '#d4d4d4')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.noLogs {
|
.noLogs {
|
||||||
color: #7a7a7a;
|
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
580
ts_web/elements/ops-view-network.ts
Normal file
580
ts_web/elements/ops-view-network.ts
Normal file
@@ -0,0 +1,580 @@
|
|||||||
|
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'ops-view-network': OpsViewNetwork;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface INetworkRequest {
|
||||||
|
id: string;
|
||||||
|
timestamp: number;
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
hostname: string;
|
||||||
|
port: number;
|
||||||
|
protocol: 'http' | 'https' | 'tcp' | 'udp';
|
||||||
|
statusCode?: number;
|
||||||
|
duration: number;
|
||||||
|
bytesIn: number;
|
||||||
|
bytesOut: number;
|
||||||
|
remoteIp: string;
|
||||||
|
route?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('ops-view-network')
|
||||||
|
export class OpsViewNetwork extends DeesElement {
|
||||||
|
@state()
|
||||||
|
private statsState = appstate.statsStatePart.getState();
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private networkState = appstate.networkStatePart.getState();
|
||||||
|
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private networkRequests: INetworkRequest[] = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
|
// Track if we need to update the chart to avoid unnecessary re-renders
|
||||||
|
private lastChartUpdate = 0;
|
||||||
|
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
||||||
|
|
||||||
|
private lastTrafficUpdateTime = 0;
|
||||||
|
private trafficUpdateInterval = 1000; // Update every 1 second
|
||||||
|
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
||||||
|
private trafficUpdateTimer: any = null;
|
||||||
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||||
|
|
||||||
|
// Removed byte tracking - now using real-time data from SmartProxy
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.subscribeToStateParts();
|
||||||
|
this.initializeTrafficData();
|
||||||
|
this.updateNetworkData();
|
||||||
|
this.startTrafficUpdateTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
|
||||||
|
// When network view becomes visible, ensure we fetch network data
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async disconnectedCallback() {
|
||||||
|
await super.disconnectedCallback();
|
||||||
|
this.stopTrafficUpdateTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribeToStateParts() {
|
||||||
|
// Subscribe and track unsubscribe functions
|
||||||
|
const statsUnsubscribe = appstate.statsStatePart.state.subscribe((state) => {
|
||||||
|
this.statsState = state;
|
||||||
|
this.updateNetworkData();
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(statsUnsubscribe);
|
||||||
|
|
||||||
|
const networkUnsubscribe = appstate.networkStatePart.state.subscribe((state) => {
|
||||||
|
this.networkState = state;
|
||||||
|
this.updateNetworkData();
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(networkUnsubscribe);
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeTrafficData() {
|
||||||
|
const now = Date.now();
|
||||||
|
// Fixed 5 minute time range
|
||||||
|
const range = 5 * 60 * 1000; // 5 minutes
|
||||||
|
const bucketSize = range / 60; // 60 data points
|
||||||
|
|
||||||
|
// Initialize with empty data points for both in and out
|
||||||
|
const emptyData = Array.from({ length: 60 }, (_, i) => {
|
||||||
|
const time = now - ((59 - i) * bucketSize);
|
||||||
|
return {
|
||||||
|
x: new Date(time).toISOString(),
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trafficDataIn = [...emptyData];
|
||||||
|
this.trafficDataOut = emptyData.map(point => ({ ...point }));
|
||||||
|
|
||||||
|
this.lastTrafficUpdateTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.networkContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.protocolBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.http {
|
||||||
|
background: ${cssManager.bdTheme('#e3f2fd', '#1a2c3a')};
|
||||||
|
color: ${cssManager.bdTheme('#1976d2', '#5a9fd4')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.https {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.tcp {
|
||||||
|
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.smtp {
|
||||||
|
background: ${cssManager.bdTheme('#f3e5f5', '#2a1a3a')};
|
||||||
|
color: ${cssManager.bdTheme('#7b1fa2', '#ba68c8')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocolBadge.dns {
|
||||||
|
background: ${cssManager.bdTheme('#e0f2f1', '#1a3a3a')};
|
||||||
|
color: ${cssManager.bdTheme('#00796b', '#4db6ac')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.success {
|
||||||
|
background: ${cssManager.bdTheme('#e8f5e9', '#1a3a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#388e3c', '#66bb6a')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.error {
|
||||||
|
background: ${cssManager.bdTheme('#ffebee', '#3a1a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#d32f2f', '#ff6666')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.warning {
|
||||||
|
background: ${cssManager.bdTheme('#fff3e0', '#3a2a1a')};
|
||||||
|
color: ${cssManager.bdTheme('#f57c00', '#ff9933')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Network Activity</ops-sectionheading>
|
||||||
|
|
||||||
|
<div class="networkContainer">
|
||||||
|
<!-- Stats Grid -->
|
||||||
|
${this.renderNetworkStats()}
|
||||||
|
|
||||||
|
<!-- Traffic Chart -->
|
||||||
|
<dees-chart-area
|
||||||
|
.label=${'Network Traffic'}
|
||||||
|
.series=${[
|
||||||
|
{
|
||||||
|
name: 'Inbound',
|
||||||
|
data: this.trafficDataIn,
|
||||||
|
color: '#22c55e', // Green for download
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Outbound',
|
||||||
|
data: this.trafficDataOut,
|
||||||
|
color: '#8b5cf6', // Purple for upload
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
.stacked=${false}
|
||||||
|
.yAxisFormatter=${(val: number) => `${val} Mbit/s`}
|
||||||
|
.tooltipFormatter=${(point: any) => {
|
||||||
|
const mbps = point.y || 0;
|
||||||
|
const seriesName = point.series?.name || 'Throughput';
|
||||||
|
const timestamp = new Date(point.x).toLocaleTimeString();
|
||||||
|
return `
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<div style="font-weight: bold; margin-bottom: 4px;">${timestamp}</div>
|
||||||
|
<div>${seriesName}: ${mbps.toFixed(2)} Mbit/s</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}}
|
||||||
|
></dees-chart-area>
|
||||||
|
|
||||||
|
<!-- Top IPs Section -->
|
||||||
|
${this.renderTopIPs()}
|
||||||
|
|
||||||
|
<!-- Requests Table -->
|
||||||
|
<dees-table
|
||||||
|
.data=${this.networkRequests}
|
||||||
|
.displayFunction=${(req: INetworkRequest) => ({
|
||||||
|
Time: new Date(req.timestamp).toLocaleTimeString(),
|
||||||
|
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
|
||||||
|
Method: req.method,
|
||||||
|
'Host:Port': `${req.hostname}:${req.port}`,
|
||||||
|
Path: this.truncateUrl(req.url),
|
||||||
|
Status: this.renderStatus(req.statusCode),
|
||||||
|
Duration: `${req.duration}ms`,
|
||||||
|
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
|
||||||
|
'Remote IP': req.remoteIp,
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'View Details',
|
||||||
|
iconName: 'magnifyingGlass',
|
||||||
|
type: ['inRow', 'doubleClick', 'contextmenu'],
|
||||||
|
actionFunc: async (actionData) => {
|
||||||
|
await this.showRequestDetails(actionData.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
heading1="Recent Network Activity"
|
||||||
|
heading2="Recent network requests"
|
||||||
|
searchable
|
||||||
|
.pagination=${true}
|
||||||
|
.paginationSize=${50}
|
||||||
|
dataName="request"
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showRequestDetails(request: INetworkRequest) {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
|
||||||
|
await DeesModal.createAndShow({
|
||||||
|
heading: 'Request Details',
|
||||||
|
content: html`
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<dees-dataview-codebox
|
||||||
|
.heading=${'Request Information'}
|
||||||
|
progLang="json"
|
||||||
|
.codeToDisplay=${JSON.stringify({
|
||||||
|
id: request.id,
|
||||||
|
timestamp: new Date(request.timestamp).toISOString(),
|
||||||
|
protocol: request.protocol,
|
||||||
|
method: request.method,
|
||||||
|
url: request.url,
|
||||||
|
hostname: request.hostname,
|
||||||
|
port: request.port,
|
||||||
|
statusCode: request.statusCode,
|
||||||
|
duration: `${request.duration}ms`,
|
||||||
|
bytesIn: request.bytesIn,
|
||||||
|
bytesOut: request.bytesOut,
|
||||||
|
remoteIp: request.remoteIp,
|
||||||
|
route: request.route,
|
||||||
|
}, null, 2)}
|
||||||
|
></dees-dataview-codebox>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
menuOptions: [
|
||||||
|
{
|
||||||
|
name: 'Copy Request ID',
|
||||||
|
iconName: 'copy',
|
||||||
|
action: async () => {
|
||||||
|
await navigator.clipboard.writeText(request.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private renderStatus(statusCode?: number): TemplateResult {
|
||||||
|
if (!statusCode) {
|
||||||
|
return html`<span class="statusBadge warning">N/A</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
|
||||||
|
statusCode >= 400 ? 'error' : 'warning';
|
||||||
|
|
||||||
|
return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private truncateUrl(url: string, maxLength = 50): string {
|
||||||
|
if (url.length <= maxLength) return url;
|
||||||
|
return url.substring(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.toFixed(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBytes(bytes: number): string {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let size = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
|
||||||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1024;
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatBitsPerSecond(bytesPerSecond: number): string {
|
||||||
|
const bitsPerSecond = bytesPerSecond * 8; // Convert bytes to bits
|
||||||
|
const units = ['bit/s', 'kbit/s', 'Mbit/s', 'Gbit/s'];
|
||||||
|
let size = bitsPerSecond;
|
||||||
|
let unitIndex = 0;
|
||||||
|
|
||||||
|
while (size >= 1000 && unitIndex < units.length - 1) {
|
||||||
|
size /= 1000; // Use 1000 for bits (not 1024)
|
||||||
|
unitIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateRequestsPerSecond(): number {
|
||||||
|
// Calculate from actual request data in the last minute
|
||||||
|
const oneMinuteAgo = Date.now() - 60000;
|
||||||
|
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
|
||||||
|
const reqPerSec = Math.round(recentRequests.length / 60);
|
||||||
|
|
||||||
|
// Track history for trend (keep last 20 values)
|
||||||
|
this.requestsPerSecHistory.push(reqPerSec);
|
||||||
|
if (this.requestsPerSecHistory.length > 20) {
|
||||||
|
this.requestsPerSecHistory.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqPerSec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private calculateThroughput(): { in: number; out: number } {
|
||||||
|
// Use real throughput data from network state
|
||||||
|
return {
|
||||||
|
in: this.networkState.throughputRate.bytesInPerSecond,
|
||||||
|
out: this.networkState.throughputRate.bytesOutPerSecond,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNetworkStats(): TemplateResult {
|
||||||
|
const reqPerSec = this.calculateRequestsPerSecond();
|
||||||
|
const throughput = this.calculateThroughput();
|
||||||
|
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||||
|
|
||||||
|
// Throughput data is now available in the stats tiles
|
||||||
|
|
||||||
|
// Use request count history for the requests/sec trend
|
||||||
|
const trendData = [...this.requestsPerSecHistory];
|
||||||
|
|
||||||
|
// If we don't have enough data, pad with zeros
|
||||||
|
while (trendData.length < 20) {
|
||||||
|
trendData.unshift(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'connections',
|
||||||
|
title: 'Active Connections',
|
||||||
|
value: activeConnections,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'plug',
|
||||||
|
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||||
|
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: 'View Details',
|
||||||
|
iconName: 'magnifyingGlass',
|
||||||
|
action: async () => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'requests',
|
||||||
|
title: 'Requests/sec',
|
||||||
|
value: reqPerSec,
|
||||||
|
type: 'trend',
|
||||||
|
icon: 'chartLine',
|
||||||
|
color: '#3b82f6',
|
||||||
|
trendData: trendData,
|
||||||
|
description: `Average over last minute`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'throughputIn',
|
||||||
|
title: 'Throughput In',
|
||||||
|
value: this.formatBitsPerSecond(throughput.in),
|
||||||
|
unit: '',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'download',
|
||||||
|
color: '#22c55e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'throughputOut',
|
||||||
|
title: 'Throughput Out',
|
||||||
|
value: this.formatBitsPerSecond(throughput.out),
|
||||||
|
unit: '',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'upload',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${tiles}
|
||||||
|
.minTileWidth=${200}
|
||||||
|
.gridActions=${[
|
||||||
|
{
|
||||||
|
name: 'Export Data',
|
||||||
|
iconName: 'fileExport',
|
||||||
|
action: async () => {
|
||||||
|
console.log('Export feature coming soon');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-statsgrid>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private renderTopIPs(): TemplateResult {
|
||||||
|
if (this.networkState.topIPs.length === 0) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate total connections across all top IPs
|
||||||
|
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.data=${this.networkState.topIPs}
|
||||||
|
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
||||||
|
'IP Address': ipData.ip,
|
||||||
|
'Connections': ipData.count,
|
||||||
|
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||||
|
})}
|
||||||
|
heading1="Top Connected IPs"
|
||||||
|
heading2="IPs with most active connections"
|
||||||
|
.pagination=${false}
|
||||||
|
dataName="ip"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateNetworkData() {
|
||||||
|
// Only update if connections changed significantly
|
||||||
|
const newConnectionCount = this.networkState.connections.length;
|
||||||
|
const oldConnectionCount = this.networkRequests.length;
|
||||||
|
|
||||||
|
// Check if we need to update the network requests array
|
||||||
|
const shouldUpdate = newConnectionCount !== oldConnectionCount ||
|
||||||
|
newConnectionCount === 0 ||
|
||||||
|
(newConnectionCount > 0 && this.networkRequests.length === 0);
|
||||||
|
|
||||||
|
if (shouldUpdate) {
|
||||||
|
// Convert connection data to network requests format
|
||||||
|
if (newConnectionCount > 0) {
|
||||||
|
this.networkRequests = this.networkState.connections.map((conn, index) => ({
|
||||||
|
id: conn.id,
|
||||||
|
timestamp: conn.startTime,
|
||||||
|
method: 'GET', // Default method for proxy connections
|
||||||
|
url: '/',
|
||||||
|
hostname: conn.remoteAddress,
|
||||||
|
port: conn.protocol === 'https' ? 443 : 80,
|
||||||
|
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
|
||||||
|
statusCode: conn.state === 'connected' ? 200 : undefined,
|
||||||
|
duration: Date.now() - conn.startTime,
|
||||||
|
bytesIn: conn.bytesReceived,
|
||||||
|
bytesOut: conn.bytesSent,
|
||||||
|
remoteIp: conn.remoteAddress,
|
||||||
|
route: 'proxy',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.networkRequests = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate traffic data based on request history
|
||||||
|
this.updateTrafficData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateTrafficData() {
|
||||||
|
// This method is called when network data updates
|
||||||
|
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTrafficUpdateTimer() {
|
||||||
|
this.stopTrafficUpdateTimer(); // Clear any existing timer
|
||||||
|
this.trafficUpdateTimer = setInterval(() => {
|
||||||
|
// Add a new data point every second
|
||||||
|
this.addTrafficDataPoint();
|
||||||
|
}, 1000); // Update every second
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTrafficDataPoint() {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Throttle chart updates to avoid excessive re-renders
|
||||||
|
if (now - this.lastChartUpdate < this.chartUpdateThreshold) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const throughput = this.calculateThroughput();
|
||||||
|
|
||||||
|
// Convert to Mbps (bytes * 8 / 1,000,000)
|
||||||
|
const throughputInMbps = (throughput.in * 8) / 1000000;
|
||||||
|
const throughputOutMbps = (throughput.out * 8) / 1000000;
|
||||||
|
|
||||||
|
// Add new data points
|
||||||
|
const timestamp = new Date(now).toISOString();
|
||||||
|
|
||||||
|
const newDataPointIn = {
|
||||||
|
x: timestamp,
|
||||||
|
y: Math.round(throughputInMbps * 10) / 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const newDataPointOut = {
|
||||||
|
x: timestamp,
|
||||||
|
y: Math.round(throughputOutMbps * 10) / 10
|
||||||
|
};
|
||||||
|
|
||||||
|
// Efficient array updates - modify in place when possible
|
||||||
|
if (this.trafficDataIn.length >= 60) {
|
||||||
|
// Remove oldest and add newest
|
||||||
|
this.trafficDataIn = [...this.trafficDataIn.slice(1), newDataPointIn];
|
||||||
|
this.trafficDataOut = [...this.trafficDataOut.slice(1), newDataPointOut];
|
||||||
|
} else {
|
||||||
|
// Still filling up the initial data
|
||||||
|
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
||||||
|
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastChartUpdate = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopTrafficUpdateTimer() {
|
||||||
|
if (this.trafficUpdateTimer) {
|
||||||
|
clearInterval(this.trafficUpdateTimer);
|
||||||
|
this.trafficUpdateTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
state,
|
state,
|
||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
|
type TemplateResult,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
@customElement('ops-view-overview')
|
@customElement('ops-view-overview')
|
||||||
export class OpsViewOverview extends DeesElement {
|
export class OpsViewOverview extends DeesElement {
|
||||||
@@ -38,37 +40,11 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
shared.viewHostCss,
|
shared.viewHostCss,
|
||||||
css`
|
css`
|
||||||
.statsGrid {
|
h2 {
|
||||||
display: grid;
|
margin: 32px 0 16px 0;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
font-size: 24px;
|
||||||
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;
|
font-weight: 600;
|
||||||
color: #333;
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
}
|
|
||||||
|
|
||||||
.statValue {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #2196F3;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.statLabel {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartGrid {
|
.chartGrid {
|
||||||
@@ -81,17 +57,21 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
.loadingMessage {
|
.loadingMessage {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
background-color: #fee;
|
background-color: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
||||||
border: 1px solid #fcc;
|
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
color: #c00;
|
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
||||||
margin: 16px 0;
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dees-statsgrid {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
Error loading statistics: ${this.statsState.error}
|
Error loading statistics: ${this.statsState.error}
|
||||||
</div>
|
</div>
|
||||||
` : html`
|
` : html`
|
||||||
<div class="statsGrid">
|
${this.renderServerStats()}
|
||||||
${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">
|
${this.renderEmailStats()}
|
||||||
<h3>Connections</h3>
|
|
||||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
|
||||||
<div class="statLabel">Active connections</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="statCard">
|
${this.renderDnsStats()}
|
||||||
<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">
|
<div class="chartGrid">
|
||||||
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
<dees-chart-area .label=${'Email Traffic (24h)'} .data=${[]}></dees-chart-area>
|
||||||
@@ -197,13 +109,16 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
const days = Math.floor(seconds / 86400);
|
const days = Math.floor(seconds / 86400);
|
||||||
const hours = Math.floor((seconds % 86400) / 3600);
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
return `${days}d ${hours}h ${minutes}m`;
|
return `${days}d ${hours}h ${minutes}m ${secs}s`;
|
||||||
} else if (hours > 0) {
|
} else if (hours > 0) {
|
||||||
return `${hours}h ${minutes}m`;
|
return `${hours}h ${minutes}m ${secs}s`;
|
||||||
|
} else if (minutes > 0) {
|
||||||
|
return `${minutes}m ${secs}s`;
|
||||||
} else {
|
} else {
|
||||||
return `${minutes}m`;
|
return `${secs}s`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,4 +134,175 @@ export class OpsViewOverview extends DeesElement {
|
|||||||
|
|
||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderServerStats(): TemplateResult {
|
||||||
|
if (!this.statsState.serverStats) return html``;
|
||||||
|
|
||||||
|
const cpuUsage = Math.round(this.statsState.serverStats.cpuUsage.user);
|
||||||
|
const memoryUsage = this.statsState.serverStats.memoryUsage.actualUsagePercentage !== undefined
|
||||||
|
? Math.round(this.statsState.serverStats.memoryUsage.actualUsagePercentage)
|
||||||
|
: Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100);
|
||||||
|
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
title: 'Server Status',
|
||||||
|
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
|
||||||
|
type: 'text',
|
||||||
|
icon: 'server',
|
||||||
|
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
|
||||||
|
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'connections',
|
||||||
|
title: 'Active Connections',
|
||||||
|
value: this.statsState.serverStats.activeConnections,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'networkWired',
|
||||||
|
color: '#3b82f6',
|
||||||
|
description: `Total: ${this.statsState.serverStats.totalConnections}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cpu',
|
||||||
|
title: 'CPU Usage',
|
||||||
|
value: cpuUsage,
|
||||||
|
type: 'gauge',
|
||||||
|
icon: 'microchip',
|
||||||
|
gaugeOptions: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thresholds: [
|
||||||
|
{ value: 0, color: '#22c55e' },
|
||||||
|
{ value: 60, color: '#f59e0b' },
|
||||||
|
{ value: 80, color: '#ef4444' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'memory',
|
||||||
|
title: 'Memory Usage',
|
||||||
|
value: memoryUsage,
|
||||||
|
type: 'percentage',
|
||||||
|
icon: 'memory',
|
||||||
|
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
|
||||||
|
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
|
||||||
|
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
|
||||||
|
: `${this.formatBytes(this.statsState.serverStats.memoryUsage.rss)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-statsgrid
|
||||||
|
.tiles=${tiles}
|
||||||
|
.gridActions=${[
|
||||||
|
{
|
||||||
|
name: 'Refresh',
|
||||||
|
iconName: 'arrowsRotate',
|
||||||
|
action: async () => {
|
||||||
|
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></dees-statsgrid>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderEmailStats(): TemplateResult {
|
||||||
|
if (!this.statsState.emailStats) return html``;
|
||||||
|
|
||||||
|
const deliveryRate = this.statsState.emailStats.deliveryRate || 0;
|
||||||
|
const bounceRate = this.statsState.emailStats.bounceRate || 0;
|
||||||
|
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'sent',
|
||||||
|
title: 'Emails Sent',
|
||||||
|
value: this.statsState.emailStats.sent,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'paperPlane',
|
||||||
|
color: '#22c55e',
|
||||||
|
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'received',
|
||||||
|
title: 'Emails Received',
|
||||||
|
value: this.statsState.emailStats.received,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'envelope',
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'queued',
|
||||||
|
title: 'Queued',
|
||||||
|
value: this.statsState.emailStats.queued,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'clock',
|
||||||
|
color: '#f59e0b',
|
||||||
|
description: 'Pending delivery',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'failed',
|
||||||
|
title: 'Failed',
|
||||||
|
value: this.statsState.emailStats.failed,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'triangleExclamation',
|
||||||
|
color: '#ef4444',
|
||||||
|
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<h2>Email Statistics</h2>
|
||||||
|
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDnsStats(): TemplateResult {
|
||||||
|
if (!this.statsState.dnsStats) return html``;
|
||||||
|
|
||||||
|
const cacheHitRate = Math.round(this.statsState.dnsStats.cacheHitRate * 100);
|
||||||
|
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'queries',
|
||||||
|
title: 'DNS Queries',
|
||||||
|
value: this.statsState.dnsStats.totalQueries,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'globe',
|
||||||
|
color: '#3b82f6',
|
||||||
|
description: 'Total queries handled',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cacheRate',
|
||||||
|
title: 'Cache Hit Rate',
|
||||||
|
value: cacheHitRate,
|
||||||
|
type: 'percentage',
|
||||||
|
icon: 'database',
|
||||||
|
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
|
||||||
|
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'domains',
|
||||||
|
title: 'Active Domains',
|
||||||
|
value: this.statsState.dnsStats.activeDomains,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'sitemap',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'responseTime',
|
||||||
|
title: 'Avg Response Time',
|
||||||
|
value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
|
||||||
|
unit: 'ms',
|
||||||
|
type: 'number',
|
||||||
|
icon: 'clockRotateLeft',
|
||||||
|
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<h2>DNS Statistics</h2>
|
||||||
|
<dees-statsgrid .tiles=${tiles}></dees-statsgrid>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
cssManager,
|
cssManager,
|
||||||
} from '@design.estate/dees-element';
|
} from '@design.estate/dees-element';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
@customElement('ops-view-security')
|
@customElement('ops-view-security')
|
||||||
export class OpsViewSecurity extends DeesElement {
|
export class OpsViewSecurity extends DeesElement {
|
||||||
@@ -45,7 +46,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
border-bottom: 2px solid #e9ecef;
|
border-bottom: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@@ -55,29 +56,33 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab:hover {
|
.tab:hover {
|
||||||
color: #333;
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab.active {
|
.tab.active {
|
||||||
color: #2196F3;
|
color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
||||||
border-bottom-color: #2196F3;
|
border-bottom-color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.securityGrid {
|
h2 {
|
||||||
display: grid;
|
margin: 32px 0 16px 0;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
font-size: 24px;
|
||||||
gap: 16px;
|
font-weight: 600;
|
||||||
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
|
}
|
||||||
|
|
||||||
|
dees-statsgrid {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.securityCard {
|
.securityCard {
|
||||||
background: white;
|
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||||
border: 1px solid #e9ecef;
|
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -85,18 +90,18 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.securityCard.alert {
|
.securityCard.alert {
|
||||||
border-color: #f44336;
|
border-color: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
||||||
background: #ffebee;
|
background: ${cssManager.bdTheme('#ffebee', '#4a1f1f')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.securityCard.warning {
|
.securityCard.warning {
|
||||||
border-color: #ff9800;
|
border-color: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
||||||
background: #fff3e0;
|
background: ${cssManager.bdTheme('#fff3e0', '#4a3a1f')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.securityCard.success {
|
.securityCard.success {
|
||||||
border-color: #4caf50;
|
border-color: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
||||||
background: #e8f5e9;
|
background: ${cssManager.bdTheme('#e8f5e9', '#1f3f1f')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardHeader {
|
.cardHeader {
|
||||||
@@ -109,7 +114,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
.cardTitle {
|
.cardTitle {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardStatus {
|
.cardStatus {
|
||||||
@@ -120,18 +125,18 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-critical {
|
.status-critical {
|
||||||
background: #f44336;
|
background: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
||||||
color: white;
|
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-warning {
|
.status-warning {
|
||||||
background: #ff9800;
|
background: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
||||||
color: white;
|
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-good {
|
.status-good {
|
||||||
background: #4caf50;
|
background: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
||||||
color: white;
|
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.metricValue {
|
.metricValue {
|
||||||
@@ -142,7 +147,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
|
|
||||||
.metricLabel {
|
.metricLabel {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionButton {
|
.actionButton {
|
||||||
@@ -159,7 +164,7 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-bottom: 1px solid #e9ecef;
|
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockedIpItem:last-child {
|
.blockedIpItem:last-child {
|
||||||
@@ -173,12 +178,12 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
|
|
||||||
.blockReason {
|
.blockReason {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #666;
|
color: ${cssManager.bdTheme('#666', '#999')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.blockTime {
|
.blockTime {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: ${cssManager.bdTheme('#999', '#666')};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
@@ -243,36 +248,60 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
|
|
||||||
private renderOverview(metrics: any) {
|
private renderOverview(metrics: any) {
|
||||||
const threatLevel = this.calculateThreatLevel(metrics);
|
const threatLevel = this.calculateThreatLevel(metrics);
|
||||||
|
const threatScore = this.getThreatScore(metrics);
|
||||||
|
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'threatLevel',
|
||||||
|
title: 'Threat Level',
|
||||||
|
value: threatScore,
|
||||||
|
type: 'gauge',
|
||||||
|
icon: 'shield',
|
||||||
|
gaugeOptions: {
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
thresholds: [
|
||||||
|
{ value: 0, color: '#ef4444' },
|
||||||
|
{ value: 30, color: '#f59e0b' },
|
||||||
|
{ value: 70, color: '#22c55e' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
description: `Status: ${threatLevel.toUpperCase()}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blockedThreats',
|
||||||
|
title: 'Blocked Threats',
|
||||||
|
value: metrics.blockedIPs.length + metrics.spamDetected,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'userShield',
|
||||||
|
color: '#ef4444',
|
||||||
|
description: 'Total threats blocked today',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activeSessions',
|
||||||
|
title: 'Active Sessions',
|
||||||
|
value: 0,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'users',
|
||||||
|
color: '#22c55e',
|
||||||
|
description: 'Current authenticated sessions',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'authFailures',
|
||||||
|
title: 'Auth Failures',
|
||||||
|
value: metrics.authenticationFailures,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lockOpen',
|
||||||
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
|
description: 'Failed login attempts today',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="securityGrid">
|
<dees-statsgrid
|
||||||
<div class="securityCard ${threatLevel}">
|
.tiles=${tiles}
|
||||||
<div class="cardHeader">
|
.minTileWidth=${200}
|
||||||
<h3 class="cardTitle">Threat Level</h3>
|
></dees-statsgrid>
|
||||||
<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>
|
<h2>Recent Security Events</h2>
|
||||||
<dees-table
|
<dees-table
|
||||||
@@ -320,20 +349,32 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderAuthentication(metrics: any) {
|
private renderAuthentication(metrics: any) {
|
||||||
return html`
|
const tiles: IStatsTile[] = [
|
||||||
<div class="securityGrid">
|
{
|
||||||
<div class="securityCard">
|
id: 'authFailures',
|
||||||
<h3 class="cardTitle">Authentication Statistics</h3>
|
title: 'Authentication Failures',
|
||||||
<div class="metricValue">${metrics.authenticationFailures}</div>
|
value: metrics.authenticationFailures,
|
||||||
<div class="metricLabel">Failed authentication attempts today</div>
|
type: 'number',
|
||||||
</div>
|
icon: 'lockOpen',
|
||||||
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||||||
|
description: 'Failed authentication attempts today',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'successfulLogins',
|
||||||
|
title: 'Successful Logins',
|
||||||
|
value: 0,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'lock',
|
||||||
|
color: '#22c55e',
|
||||||
|
description: 'Successful logins today',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
<div class="securityCard">
|
return html`
|
||||||
<h3 class="cardTitle">Successful Logins</h3>
|
<dees-statsgrid
|
||||||
<div class="metricValue">${0}</div>
|
.tiles=${tiles}
|
||||||
<div class="metricLabel">Successful logins today</div>
|
.minTileWidth=${200}
|
||||||
</div>
|
></dees-statsgrid>
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Recent Login Attempts</h2>
|
<h2>Recent Login Attempts</h2>
|
||||||
<dees-table
|
<dees-table
|
||||||
@@ -352,32 +393,50 @@ export class OpsViewSecurity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderEmailSecurity(metrics: any) {
|
private renderEmailSecurity(metrics: any) {
|
||||||
|
const tiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'malware',
|
||||||
|
title: 'Malware Detection',
|
||||||
|
value: metrics.malwareDetected,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'virusSlash',
|
||||||
|
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
|
description: 'Malware detected',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'phishing',
|
||||||
|
title: 'Phishing Detection',
|
||||||
|
value: metrics.phishingDetected,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'fishFins',
|
||||||
|
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
|
||||||
|
description: 'Phishing attempts detected',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'suspicious',
|
||||||
|
title: 'Suspicious Activities',
|
||||||
|
value: metrics.suspiciousActivities,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'triangleExclamation',
|
||||||
|
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
|
||||||
|
description: 'Suspicious activities detected',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'spam',
|
||||||
|
title: 'Spam Detection',
|
||||||
|
value: metrics.spamDetected,
|
||||||
|
type: 'number',
|
||||||
|
icon: 'ban',
|
||||||
|
color: '#f59e0b',
|
||||||
|
description: 'Spam emails blocked',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="securityGrid">
|
<dees-statsgrid
|
||||||
<div class="securityCard">
|
.tiles=${tiles}
|
||||||
<h3 class="cardTitle">Malware Detection</h3>
|
.minTileWidth=${200}
|
||||||
<div class="metricValue">${metrics.malwareDetected}</div>
|
></dees-statsgrid>
|
||||||
<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>
|
<h2>Email Security Configuration</h2>
|
||||||
<div class="securityCard">
|
<div class="securityCard">
|
||||||
|
|||||||
@@ -1,299 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -21,14 +21,10 @@ export class OpsSectionHeading extends DeesElement {
|
|||||||
font-family: 'Cal Sans', 'Inter', sans-serif;
|
font-family: 'Cal Sans', 'Inter', sans-serif;
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111;
|
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host([theme="dark"]) .heading {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user