Compare commits
30 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 | |||
| 554d245c0c | |||
| e3cb35a036 | |||
| 3a95ea9f4e | |||
| 99f57dba76 | |||
| 415e28038d | |||
| 7bda406624 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -19,5 +19,5 @@ dist_*/
|
||||
|
||||
# custom
|
||||
**/.claude/settings.local.json
|
||||
data/
|
||||
.nogit/data/
|
||||
readme.plan.md
|
||||
|
||||
44
package.json
44
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "2.12.1",
|
||||
"version": "2.12.4",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
@@ -12,51 +12,53 @@
|
||||
"test": "(tstest test/ --logfile --timeout 60)",
|
||||
"start": "(node --max_old_space_size=250 ./cli.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": {
|
||||
"@git.zone/tsbuild": "^2.6.4",
|
||||
"@git.zone/tsbundle": "^2.2.5",
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.0.1",
|
||||
"@types/node": "^22.15.30",
|
||||
"@git.zone/tstest": "^2.3.6",
|
||||
"@git.zone/tswatch": "^2.2.1",
|
||||
"@types/node": "^22",
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@api.global/typedrequest": "^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",
|
||||
"@apiclient.xyz/cloudflare": "^6.4.1",
|
||||
"@design.estate/dees-catalog": "^1.8.0",
|
||||
"@design.estate/dees-element": "^2.0.42",
|
||||
"@design.estate/dees-catalog": "^1.10.10",
|
||||
"@design.estate/dees-element": "^2.1.2",
|
||||
"@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/smartdata": "^5.15.1",
|
||||
"@push.rocks/smartdata": "^5.16.4",
|
||||
"@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/smartjwt": "^2.2.1",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartlog": "^3.1.9",
|
||||
"@push.rocks/smartmail": "^2.1.0",
|
||||
"@push.rocks/smartnetwork": "^4.0.2",
|
||||
"@push.rocks/smartpath": "^5.0.5",
|
||||
"@push.rocks/smartmetrics": "^2.0.10",
|
||||
"@push.rocks/smartnetwork": "^4.1.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartproxy": "^19.5.25",
|
||||
"@push.rocks/smartproxy": "21.1.7",
|
||||
"@push.rocks/smartrequest": "^2.1.0",
|
||||
"@push.rocks/smartrule": "^2.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"@push.rocks/smartstate": "^2.0.0",
|
||||
"@push.rocks/smartstate": "^2.0.26",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@serve.zone/interfaces": "^5.0.4",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
"@types/mailparser": "^3.4.6",
|
||||
"ip": "^2.0.1",
|
||||
"lru-cache": "^11.1.0",
|
||||
"mailauth": "^4.8.6",
|
||||
"mailparser": "^3.7.3",
|
||||
"lru-cache": "^11.2.1",
|
||||
"mailauth": "^4.9.4",
|
||||
"mailparser": "^3.7.4",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"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
|
||||
|
||||
## 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)
|
||||
|
||||
### 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
|
||||
- UnifiedEmailServer is simpler and more focused
|
||||
- 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: 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.
|
||||
|
||||
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 { 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 the email configuration helpers directly from mail/delivery
|
||||
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 { OpsServer } from './opsserver/index.js';
|
||||
import { MetricsManager } from './monitoring/index.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/**
|
||||
@@ -133,6 +134,7 @@ export class DcRouter {
|
||||
public emailServer?: UnifiedEmailServer;
|
||||
public storageManager: StorageManager;
|
||||
public opsServer: OpsServer;
|
||||
public metricsManager?: MetricsManager;
|
||||
|
||||
// TypedRouter for API endpoints
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
@@ -160,6 +162,10 @@ export class DcRouter {
|
||||
await this.opsServer.start();
|
||||
|
||||
try {
|
||||
// Initialize MetricsManager
|
||||
this.metricsManager = new MetricsManager(this);
|
||||
await this.metricsManager.start();
|
||||
|
||||
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
||||
await this.setupSmartProxy();
|
||||
|
||||
@@ -197,6 +203,14 @@ export class DcRouter {
|
||||
console.log('║ DcRouter Started Successfully ║');
|
||||
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
|
||||
if (this.smartProxy) {
|
||||
console.log('🌐 SmartProxy Service:');
|
||||
@@ -373,11 +387,29 @@ export class DcRouter {
|
||||
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): 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
|
||||
for (const port of emailConfig.ports) {
|
||||
// Create a descriptive name for the route based on the port
|
||||
let routeName = 'email-route';
|
||||
let tlsMode = 'passthrough';
|
||||
let tlsMode: TTlsMode = 'passthrough';
|
||||
|
||||
// Handle different email ports differently
|
||||
switch (port) {
|
||||
@@ -393,7 +425,7 @@ export class DcRouter {
|
||||
|
||||
case 465: // SMTPS
|
||||
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;
|
||||
|
||||
default:
|
||||
@@ -424,7 +456,7 @@ export class DcRouter {
|
||||
if (emailConfig.useSocketHandler) {
|
||||
// Socket-handler mode
|
||||
action = {
|
||||
type: 'socket-handler' as any,
|
||||
type: 'socket-handler' as TRouteActionType,
|
||||
socketHandler: this.createMailSocketHandler(port)
|
||||
};
|
||||
} else {
|
||||
@@ -439,15 +471,19 @@ export class DcRouter {
|
||||
const internalPort = portMapping[port] || port + 10000;
|
||||
|
||||
action = {
|
||||
type: 'forward',
|
||||
target: {
|
||||
type: 'forward' as TRouteActionType,
|
||||
targets: [{
|
||||
host: 'localhost', // Forward to internal email server
|
||||
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
|
||||
@@ -471,25 +507,35 @@ export class DcRouter {
|
||||
// Add email domain-based routes if configured
|
||||
if (emailConfig.routes) {
|
||||
for (const route of emailConfig.routes) {
|
||||
// Only create SmartProxy routes for forward actions
|
||||
// Other email actions (deliver, process, reject) are handled internally by the email server
|
||||
if (route.action.type === 'forward' && route.action.forward) {
|
||||
const domains = route.match.recipients ?
|
||||
extractDomainsFromRecipients(route.match.recipients) : [];
|
||||
|
||||
// Only create SmartProxy route if we have domains to match
|
||||
if (domains.length > 0) {
|
||||
emailRoutes.push({
|
||||
name: route.name,
|
||||
match: {
|
||||
ports: emailConfig.ports,
|
||||
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
|
||||
domains: domains
|
||||
},
|
||||
action: {
|
||||
type: 'forward',
|
||||
target: route.action.type === 'forward' && route.action.forward ? {
|
||||
type: 'forward' as TRouteActionType,
|
||||
targets: [{
|
||||
host: route.action.forward.host,
|
||||
port: route.action.forward.port || 25
|
||||
} : undefined,
|
||||
}],
|
||||
tls: {
|
||||
mode: 'passthrough'
|
||||
mode: 'passthrough' as TTlsMode
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return emailRoutes;
|
||||
}
|
||||
@@ -566,6 +612,9 @@ export class DcRouter {
|
||||
try {
|
||||
// Stop all services in parallel for faster shutdown
|
||||
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
|
||||
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
|
||||
};
|
||||
|
||||
// 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
|
||||
const emailConfig: IUnifiedEmailServerOptions = {
|
||||
...this.options.emailConfig,
|
||||
domains: transformedDomains,
|
||||
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
||||
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
|
||||
};
|
||||
|
||||
@@ -221,12 +221,13 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
const checkInterval = setInterval(() => {
|
||||
if (this.activeDeliveries.size === 0) {
|
||||
clearInterval(checkInterval);
|
||||
clearTimeout(forceTimeout);
|
||||
resolve();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Force resolve after 30 seconds
|
||||
setTimeout(() => {
|
||||
const forceTimeout = setTimeout(() => {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
}, 30000);
|
||||
|
||||
@@ -247,11 +247,23 @@ export class ConnectionManager implements IConnectionManager {
|
||||
|
||||
// 2. Check for destroyed sockets in active connections
|
||||
let destroyedSocketsCount = 0;
|
||||
const socketsToRemove: Array<plugins.net.Socket | plugins.tls.TLSSocket> = [];
|
||||
|
||||
for (const socket of this.activeConnections) {
|
||||
if (socket.destroyed) {
|
||||
destroyedSocketsCount++;
|
||||
// This should not happen - remove destroyed sockets from tracking
|
||||
socketsToRemove.push(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)}`);
|
||||
}
|
||||
|
||||
// Track this IP connection
|
||||
this.trackIPConnection(remoteAddress);
|
||||
|
||||
// Set up event handlers
|
||||
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)}`);
|
||||
}
|
||||
|
||||
// Track this IP connection
|
||||
this.trackIPConnection(remoteAddress);
|
||||
|
||||
// Set up event handlers
|
||||
this.setupSocketEventHandlers(socket);
|
||||
|
||||
@@ -763,6 +769,9 @@ export class ConnectionManager implements IConnectionManager {
|
||||
clearTimeout(session.dataTimeoutId);
|
||||
}
|
||||
|
||||
// Remove all event listeners to prevent memory leaks
|
||||
socket.removeAllListeners();
|
||||
|
||||
// Log connection close with session details if available
|
||||
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
|
||||
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 options: IUnifiedEmailServerOptions;
|
||||
private emailRouter: EmailRouter;
|
||||
private domainRegistry: DomainRegistry;
|
||||
public domainRegistry: DomainRegistry;
|
||||
private servers: any[] = [];
|
||||
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;
|
||||
perDay: number;
|
||||
};
|
||||
domains?: string[];
|
||||
};
|
||||
dns: {
|
||||
enabled: boolean;
|
||||
@@ -88,6 +89,17 @@ export class ConfigHandler {
|
||||
}> {
|
||||
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 {
|
||||
email: {
|
||||
enabled: !!dcRouter.emailServer,
|
||||
@@ -98,6 +110,7 @@ export class ConfigHandler {
|
||||
perHour: 100,
|
||||
perDay: 1000,
|
||||
},
|
||||
domains: emailDomains,
|
||||
},
|
||||
dns: {
|
||||
enabled: !!dcRouter.dnsServer,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { MetricsManager } from '../../monitoring/index.js';
|
||||
|
||||
export class SecurityHandler {
|
||||
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
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||
@@ -120,7 +149,29 @@ export class SecurityHandler {
|
||||
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 {
|
||||
blockedIPs: [],
|
||||
reputationScores: {},
|
||||
@@ -178,11 +229,69 @@ export class SecurityHandler {
|
||||
status: 'active' | 'idle' | 'closing';
|
||||
}> = [];
|
||||
|
||||
// TODO: Implement actual connection tracking
|
||||
// This would collect from:
|
||||
// - SmartProxy connections
|
||||
// - Email server connections
|
||||
// - DNS server connections
|
||||
// Get connection info and network stats from MetricsManager if available
|
||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
import { MetricsManager } from '../../monitoring/index.js';
|
||||
|
||||
export class StatsHandler {
|
||||
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<{
|
||||
@@ -178,25 +306,30 @@ export class StatsHandler {
|
||||
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 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;
|
||||
|
||||
// TODO: Implement proper request tracking
|
||||
const requestsPerSecond = 0;
|
||||
const activeConnections = 0;
|
||||
const totalConnections = 0;
|
||||
|
||||
return {
|
||||
uptime,
|
||||
cpuUsage: {
|
||||
user: cpuUsage * 0.7, // Approximate user CPU
|
||||
system: cpuUsage * 0.3, // Approximate system CPU
|
||||
user: cpuUsage * 0.7,
|
||||
system: cpuUsage * 0.3,
|
||||
},
|
||||
memoryUsage: {
|
||||
heapUsed: memUsage.heapUsed,
|
||||
@@ -204,10 +337,10 @@ export class StatsHandler {
|
||||
external: memUsage.external,
|
||||
rss: memUsage.rss,
|
||||
},
|
||||
requestsPerSecond,
|
||||
activeConnections,
|
||||
totalConnections,
|
||||
history: [], // TODO: Implement history tracking
|
||||
requestsPerSecond: 0,
|
||||
activeConnections: 0,
|
||||
totalConnections: 0,
|
||||
history: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,7 +352,19 @@ export class StatsHandler {
|
||||
queueSize: number;
|
||||
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 {
|
||||
sentToday: 0,
|
||||
receivedToday: 0,
|
||||
@@ -242,7 +387,21 @@ export class StatsHandler {
|
||||
queryTypes: { [key: string]: number };
|
||||
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 {
|
||||
queriesPerSecond: 0,
|
||||
totalQueries: 0,
|
||||
|
||||
@@ -50,6 +50,7 @@ import * as smartguard from '@push.rocks/smartguard';
|
||||
import * as smartjwt from '@push.rocks/smartjwt';
|
||||
import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartmail from '@push.rocks/smartmail';
|
||||
import * as smartmetrics from '@push.rocks/smartmetrics';
|
||||
import * as smartnetwork from '@push.rocks/smartnetwork';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
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 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
|
||||
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
||||
|
||||
8
ts_interfaces/data/auth.ts
Normal file
8
ts_interfaces/data/auth.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface IIdentity {
|
||||
jwt: string;
|
||||
userId: string;
|
||||
name: string;
|
||||
expiresAt: number;
|
||||
role?: string;
|
||||
type?: string;
|
||||
}
|
||||
2
ts_interfaces/data/index.ts
Normal file
2
ts_interfaces/data/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './auth.js';
|
||||
export * from './stats.js';
|
||||
131
ts_interfaces/data/stats.ts
Normal file
131
ts_interfaces/data/stats.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
export interface IServerStats {
|
||||
uptime: number;
|
||||
startTime: number;
|
||||
memoryUsage: {
|
||||
heapUsed: number;
|
||||
heapTotal: number;
|
||||
external: number;
|
||||
rss: number;
|
||||
// SmartMetrics memory data
|
||||
maxMemoryMB?: number;
|
||||
actualUsageBytes?: number;
|
||||
actualUsagePercentage?: number;
|
||||
};
|
||||
cpuUsage: {
|
||||
user: number;
|
||||
system: number;
|
||||
};
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
}
|
||||
|
||||
export interface IEmailStats {
|
||||
sent: number;
|
||||
received: number;
|
||||
bounced: number;
|
||||
queued: number;
|
||||
failed: number;
|
||||
averageDeliveryTime: number;
|
||||
deliveryRate: number;
|
||||
bounceRate: number;
|
||||
}
|
||||
|
||||
export interface IDnsStats {
|
||||
totalQueries: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
cacheHitRate: number;
|
||||
activeDomains: number;
|
||||
averageResponseTime: number;
|
||||
queryTypes: {
|
||||
[key: string]: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRateLimitInfo {
|
||||
domain: string;
|
||||
currentRate: number;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
resetTime: number;
|
||||
blocked: boolean;
|
||||
}
|
||||
|
||||
export interface ISecurityMetrics {
|
||||
blockedIPs: string[];
|
||||
reputationScores: {
|
||||
[domain: string]: number;
|
||||
};
|
||||
spamDetected: number;
|
||||
malwareDetected: number;
|
||||
phishingDetected: number;
|
||||
authenticationFailures: number;
|
||||
suspiciousActivities: number;
|
||||
}
|
||||
|
||||
export interface ILogEntry {
|
||||
timestamp: number;
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
message: string;
|
||||
metadata?: any;
|
||||
}
|
||||
|
||||
export interface IConnectionInfo {
|
||||
id: string;
|
||||
remoteAddress: string;
|
||||
localAddress: string;
|
||||
startTime: number;
|
||||
protocol: 'smtp' | 'smtps' | 'http' | 'https';
|
||||
state: 'connecting' | 'connected' | 'authenticated' | 'transmitting' | 'closing';
|
||||
bytesReceived: number;
|
||||
bytesSent: number;
|
||||
}
|
||||
|
||||
export interface IQueueStatus {
|
||||
name: string;
|
||||
size: number;
|
||||
processing: number;
|
||||
failed: number;
|
||||
retrying: number;
|
||||
averageProcessingTime: number;
|
||||
}
|
||||
|
||||
export interface IHealthStatus {
|
||||
healthy: boolean;
|
||||
uptime: number;
|
||||
services: {
|
||||
[service: string]: {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
message?: string;
|
||||
lastCheck: number;
|
||||
};
|
||||
};
|
||||
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 './logs.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
|
||||
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
'login',
|
||||
@@ -50,7 +60,7 @@ export const loginStatePart = await appState.getStatePart<ILoginState>(
|
||||
identity: null,
|
||||
isLoggedIn: false,
|
||||
},
|
||||
'persistent' // Login state persists across sessions
|
||||
'soft' // Login state persists across sessions
|
||||
);
|
||||
|
||||
export const statsStatePart = await appState.getStatePart<IStatsState>(
|
||||
@@ -73,20 +83,18 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
||||
config: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
'soft'
|
||||
}
|
||||
);
|
||||
|
||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
'ui',
|
||||
{
|
||||
activeView: 'dashboard',
|
||||
activeView: 'overview',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000, // 30 seconds
|
||||
refreshInterval: 1000, // 1 second
|
||||
theme: 'light',
|
||||
},
|
||||
'persistent' // UI preferences persist
|
||||
);
|
||||
|
||||
export const logStatePart = await appState.getStatePart<ILogState>(
|
||||
@@ -99,6 +107,20 @@ export const logStatePart = await appState.getStatePart<ILogState>(
|
||||
'soft'
|
||||
);
|
||||
|
||||
export const networkStatePart = await appState.getStatePart<INetworkState>(
|
||||
'network',
|
||||
{
|
||||
connections: [],
|
||||
connectionsByIP: {},
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
lastUpdated: 0,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
'soft'
|
||||
);
|
||||
|
||||
// Actions for state management
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
@@ -162,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) => {
|
||||
const context = getActionContext();
|
||||
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
// Fetch server stats
|
||||
const serverStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetServerStatistics
|
||||
>('/typedrequest', 'getServerStatistics');
|
||||
// Use combined metrics endpoint - single request instead of 4
|
||||
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCombinedMetrics
|
||||
>('/typedrequest', 'getCombinedMetrics');
|
||||
|
||||
const serverStatsResponse = await serverStatsRequest.fire({
|
||||
const combinedResponse = await combinedRequest.fire({
|
||||
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
|
||||
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
|
||||
// Update state with all stats from combined response
|
||||
return {
|
||||
serverStats: serverStatsResponse.stats,
|
||||
emailStats: emailStatsResponse.stats,
|
||||
dnsStats: dnsStatsResponse.stats,
|
||||
securityMetrics: securityResponse.metrics,
|
||||
serverStats: combinedResponse.metrics.server || currentState.serverStats,
|
||||
emailStats: combinedResponse.metrics.email || currentState.emailStats,
|
||||
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
|
||||
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
@@ -320,42 +321,233 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
|
||||
// Set Active View Action
|
||||
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
// If switching to network view, ensure we fetch network data
|
||||
if (viewName === 'network' && currentState.activeView !== 'network') {
|
||||
setTimeout(() => {
|
||||
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
activeView: viewName,
|
||||
};
|
||||
});
|
||||
|
||||
// Fetch Network Stats Action
|
||||
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
// Fetch active connections using the existing endpoint
|
||||
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetActiveConnections
|
||||
>('/typedrequest', 'getActiveConnections');
|
||||
|
||||
const connectionsResponse = await connectionsRequest.fire({
|
||||
identity: context.identity,
|
||||
});
|
||||
|
||||
// Get network stats for throughput and IP data
|
||||
const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest(
|
||||
'/typedrequest',
|
||||
'getNetworkStats'
|
||||
);
|
||||
|
||||
const networkStatsResponse = await networkStatsRequest.fire({
|
||||
identity: context.identity,
|
||||
}) as any;
|
||||
|
||||
// Use the connections data for the connection list
|
||||
// and network stats for throughput and IP analytics
|
||||
const connectionsByIP: { [ip: string]: number } = {};
|
||||
|
||||
// Build connectionsByIP from network stats if available
|
||||
if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) {
|
||||
networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => {
|
||||
connectionsByIP[item.ip] = item.count;
|
||||
});
|
||||
} else {
|
||||
// Fallback: calculate from connections
|
||||
connectionsResponse.connections.forEach(conn => {
|
||||
const ip = conn.remoteAddress;
|
||||
connectionsByIP[ip] = (connectionsByIP[ip] || 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
connections: connectionsResponse.connections,
|
||||
connectionsByIP,
|
||||
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: networkStatsResponse.topIPs || [],
|
||||
lastUpdated: Date.now(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch network stats:', error);
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch network stats',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
const currentView = uiStatePart.getState().activeView;
|
||||
|
||||
try {
|
||||
// Always fetch basic stats for dashboard widgets
|
||||
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetCombinedMetrics
|
||||
>('/typedrequest', 'getCombinedMetrics');
|
||||
|
||||
const combinedResponse = await combinedRequest.fire({
|
||||
identity: context.identity,
|
||||
sections: {
|
||||
server: true,
|
||||
email: true,
|
||||
dns: true,
|
||||
security: true,
|
||||
network: currentView === 'network', // 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
|
||||
let refreshInterval: NodeJS.Timeout | null = null;
|
||||
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
|
||||
|
||||
// Initialize auto-refresh when UI state is ready
|
||||
(() => {
|
||||
const startAutoRefresh = () => {
|
||||
const uiState = uiStatePart.getState();
|
||||
if (uiState.autoRefresh && loginStatePart.getState().isLoggedIn) {
|
||||
const loginState = loginStatePart.getState();
|
||||
|
||||
// Only start if conditions are met and not already running at the same rate
|
||||
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||
// Check if we need to restart the interval (rate changed or not running)
|
||||
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
|
||||
stopAutoRefresh();
|
||||
currentRefreshRate = uiState.refreshInterval;
|
||||
refreshInterval = setInterval(() => {
|
||||
statsStatePart.dispatchAction(fetchAllStatsAction, null);
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
refreshInterval = null;
|
||||
currentRefreshRate = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes
|
||||
uiStatePart.state.subscribe(() => {
|
||||
stopAutoRefresh();
|
||||
// Watch for relevant changes only
|
||||
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
|
||||
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
|
||||
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
|
||||
|
||||
uiStatePart.state.subscribe((state) => {
|
||||
// Only restart if relevant values changed
|
||||
if (state.autoRefresh !== previousAutoRefresh ||
|
||||
state.refreshInterval !== previousRefreshInterval) {
|
||||
previousAutoRefresh = state.autoRefresh;
|
||||
previousRefreshInterval = state.refreshInterval;
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
loginStatePart.state.subscribe(() => {
|
||||
stopAutoRefresh();
|
||||
loginStatePart.state.subscribe((state) => {
|
||||
// Only restart if login state changed
|
||||
if (state.isLoggedIn !== previousIsLoggedIn) {
|
||||
previousIsLoggedIn = state.isLoggedIn;
|
||||
startAutoRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial start
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './ops-dashboard.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-config.js';
|
||||
export * from './ops-view-security.js';
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
|
||||
// Import view components
|
||||
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 { OpsViewConfig } from './ops-view-config.js';
|
||||
import { OpsViewSecurity } from './ops-view-security.js';
|
||||
@@ -26,13 +27,41 @@ export class OpsDashboard extends DeesElement {
|
||||
};
|
||||
|
||||
@state() private uiState: appstate.IUiState = {
|
||||
activeView: 'dashboard',
|
||||
activeView: 'overview',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000,
|
||||
refreshInterval: 1000,
|
||||
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() {
|
||||
super();
|
||||
document.title = 'DCRouter OpsServer';
|
||||
@@ -75,50 +104,72 @@ export class OpsDashboard extends DeesElement {
|
||||
<div class="maincontainer">
|
||||
<dees-simple-login
|
||||
name="DCRouter OpsServer"
|
||||
.loginAction=${async (username: string, password: string) => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
return this.loginState.isLoggedIn;
|
||||
}}
|
||||
>
|
||||
<dees-simple-appdash
|
||||
name="DCRouter OpsServer"
|
||||
.viewTabs=${[
|
||||
{
|
||||
name: 'Overview',
|
||||
element: OpsViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'Statistics',
|
||||
element: OpsViewStats,
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
element: OpsViewLogs,
|
||||
},
|
||||
{
|
||||
name: 'Configuration',
|
||||
element: OpsViewConfig,
|
||||
},
|
||||
{
|
||||
name: 'Security',
|
||||
element: OpsViewSecurity,
|
||||
},
|
||||
]}
|
||||
.userMenuItems=${[
|
||||
{
|
||||
name: 'Logout',
|
||||
action: async () => {
|
||||
await appstate.loginStatePart.dispatchAction(appstate.logoutAction, null);
|
||||
},
|
||||
},
|
||||
]}
|
||||
.viewTabs=${this.viewTabs}
|
||||
>
|
||||
</dees-simple-appdash>
|
||||
</dees-simple-login>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
||||
simpleLogin.addEventListener('login', (e: CustomEvent) => {
|
||||
// Handle logout event
|
||||
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
|
||||
const loginState = appstate.loginStatePart.getState();
|
||||
// Check initial login state
|
||||
if (loginState.identity) {
|
||||
this.loginState = loginState;
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
}
|
||||
}
|
||||
|
||||
private async login(username: string, password: string) {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
console.log(`Attempting to login...`);
|
||||
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
|
||||
const form = simpleLogin.shadowRoot.querySelector('dees-form');
|
||||
form.setStatus('pending', 'Logging in...');
|
||||
|
||||
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
if (state.identity) {
|
||||
console.log('Login successful');
|
||||
this.loginState = state;
|
||||
form.setStatus('success', 'Logged in!');
|
||||
await simpleLogin.switchToSlottedContent();
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
|
||||
} else {
|
||||
form.setStatus('error', 'Login failed!');
|
||||
await domtools.convenience.smartdelay.delayFor(2000);
|
||||
form.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,17 +41,17 @@ export class OpsViewConfig extends DeesElement {
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.configSection {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sectionHeader {
|
||||
background: #f8f9fa;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -60,7 +60,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
.sectionTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.sectionContent {
|
||||
@@ -74,7 +74,7 @@ export class OpsViewConfig extends DeesElement {
|
||||
.fieldLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
@@ -82,11 +82,11 @@ export class OpsViewConfig extends DeesElement {
|
||||
.fieldValue {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
background: #f8f9fa;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e9ecef;
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
}
|
||||
|
||||
.configEditor {
|
||||
@@ -95,9 +95,9 @@ export class OpsViewConfig extends DeesElement {
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e9ecef;
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
border-radius: 4px;
|
||||
background: #f8f9fa;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
@@ -108,30 +108,30 @@ export class OpsViewConfig extends DeesElement {
|
||||
}
|
||||
|
||||
.warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
background: ${cssManager.bdTheme('#fff3cd', '#4a4a1a')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ffeaa7', '#666633')};
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
color: #856404;
|
||||
color: ${cssManager.bdTheme('#856404', '#ffcc66')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background: #fee;
|
||||
border: 1px solid #fcc;
|
||||
background: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
||||
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
color: #c00;
|
||||
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.loadingMessage {
|
||||
text-align: center;
|
||||
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 {
|
||||
background: #1e1e1e;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#1e1e1e')};
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
max-height: 600px;
|
||||
@@ -63,7 +63,7 @@ export class OpsViewLogs extends DeesElement {
|
||||
}
|
||||
|
||||
.logTimestamp {
|
||||
color: #7a7a7a;
|
||||
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
@@ -76,33 +76,33 @@ export class OpsViewLogs extends DeesElement {
|
||||
}
|
||||
|
||||
.logLevel.debug {
|
||||
color: #6a9955;
|
||||
background: rgba(106, 153, 85, 0.1);
|
||||
color: ${cssManager.bdTheme('#6a9955', '#6a9955')};
|
||||
background: ${cssManager.bdTheme('rgba(106, 153, 85, 0.1)', 'rgba(106, 153, 85, 0.1)')};
|
||||
}
|
||||
.logLevel.info {
|
||||
color: #569cd6;
|
||||
background: rgba(86, 156, 214, 0.1);
|
||||
color: ${cssManager.bdTheme('#569cd6', '#569cd6')};
|
||||
background: ${cssManager.bdTheme('rgba(86, 156, 214, 0.1)', 'rgba(86, 156, 214, 0.1)')};
|
||||
}
|
||||
.logLevel.warn {
|
||||
color: #ce9178;
|
||||
background: rgba(206, 145, 120, 0.1);
|
||||
color: ${cssManager.bdTheme('#ce9178', '#ce9178')};
|
||||
background: ${cssManager.bdTheme('rgba(206, 145, 120, 0.1)', 'rgba(206, 145, 120, 0.1)')};
|
||||
}
|
||||
.logLevel.error {
|
||||
color: #f44747;
|
||||
background: rgba(244, 71, 71, 0.1);
|
||||
color: ${cssManager.bdTheme('#f44747', '#f44747')};
|
||||
background: ${cssManager.bdTheme('rgba(244, 71, 71, 0.1)', 'rgba(244, 71, 71, 0.1)')};
|
||||
}
|
||||
|
||||
.logCategory {
|
||||
color: #c586c0;
|
||||
color: ${cssManager.bdTheme('#c586c0', '#c586c0')};
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.logMessage {
|
||||
color: #d4d4d4;
|
||||
color: ${cssManager.bdTheme('#333', '#d4d4d4')};
|
||||
}
|
||||
|
||||
.noLogs {
|
||||
color: #7a7a7a;
|
||||
color: ${cssManager.bdTheme('#7a7a7a', '#7a7a7a')};
|
||||
text-align: center;
|
||||
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,
|
||||
css,
|
||||
cssManager,
|
||||
type TemplateResult,
|
||||
} from '@design.estate/dees-element';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('ops-view-overview')
|
||||
export class OpsViewOverview extends DeesElement {
|
||||
@@ -38,37 +40,11 @@ export class OpsViewOverview extends DeesElement {
|
||||
cssManager.defaultStyles,
|
||||
shared.viewHostCss,
|
||||
css`
|
||||
.statsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-gap: 16px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.statCard {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.statCard h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
h2 {
|
||||
margin: 32px 0 16px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #2196F3;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.chartGrid {
|
||||
@@ -81,17 +57,21 @@ export class OpsViewOverview extends DeesElement {
|
||||
.loadingMessage {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
background-color: #fee;
|
||||
border: 1px solid #fcc;
|
||||
background-color: ${cssManager.bdTheme('#fee', '#4a1f1f')};
|
||||
border: 1px solid ${cssManager.bdTheme('#fcc', '#6a2f2f')};
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
color: #c00;
|
||||
color: ${cssManager.bdTheme('#c00', '#ff6666')};
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -109,79 +89,11 @@ export class OpsViewOverview extends DeesElement {
|
||||
Error loading statistics: ${this.statsState.error}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="statsGrid">
|
||||
${this.statsState.serverStats ? html`
|
||||
<div class="statCard">
|
||||
<h3>Server Status</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.uptime ? 'Online' : 'Offline'}</div>
|
||||
<div class="statLabel">Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}</div>
|
||||
</div>
|
||||
${this.renderServerStats()}
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Connections</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
<div class="statLabel">Active connections</div>
|
||||
</div>
|
||||
${this.renderEmailStats()}
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Memory Usage</h3>
|
||||
<div class="statValue">${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)}</div>
|
||||
<div class="statLabel">of ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>CPU Usage</h3>
|
||||
<div class="statValue">${Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2)}%</div>
|
||||
<div class="statLabel">Average load</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${this.statsState.emailStats ? html`
|
||||
<h2>Email Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>Emails Sent</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.sent}</div>
|
||||
<div class="statLabel">Total sent</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Emails Received</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.received}</div>
|
||||
<div class="statLabel">Total received</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Failed Deliveries</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.failed}</div>
|
||||
<div class="statLabel">Delivery failures</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Queued</h3>
|
||||
<div class="statValue">${this.statsState.emailStats.queued}</div>
|
||||
<div class="statLabel">In queue</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${this.statsState.dnsStats ? html`
|
||||
<h2>DNS Statistics</h2>
|
||||
<div class="statsGrid">
|
||||
<div class="statCard">
|
||||
<h3>DNS Queries</h3>
|
||||
<div class="statValue">${this.statsState.dnsStats.totalQueries}</div>
|
||||
<div class="statLabel">Total queries handled</div>
|
||||
</div>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Cache Hit Rate</h3>
|
||||
<div class="statValue">${Math.round(this.statsState.dnsStats.cacheHitRate * 100)}%</div>
|
||||
<div class="statLabel">Cache efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.renderDnsStats()}
|
||||
|
||||
<div class="chartGrid">
|
||||
<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 hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
return `${days}d ${hours}h ${minutes}m ${secs}s`;
|
||||
} 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 {
|
||||
return `${minutes}m`;
|
||||
return `${secs}s`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,4 +134,175 @@ export class OpsViewOverview extends DeesElement {
|
||||
|
||||
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,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||
|
||||
@customElement('ops-view-security')
|
||||
export class OpsViewSecurity extends DeesElement {
|
||||
@@ -45,7 +46,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
border-bottom: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
}
|
||||
|
||||
.tab {
|
||||
@@ -55,29 +56,33 @@ export class OpsViewSecurity extends DeesElement {
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: #333;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #2196F3;
|
||||
border-bottom-color: #2196F3;
|
||||
color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
||||
border-bottom-color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
|
||||
}
|
||||
|
||||
.securityGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
h2 {
|
||||
margin: 32px 0 16px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.securityCard {
|
||||
background: white;
|
||||
border: 1px solid #e9ecef;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
@@ -85,18 +90,18 @@ export class OpsViewSecurity extends DeesElement {
|
||||
}
|
||||
|
||||
.securityCard.alert {
|
||||
border-color: #f44336;
|
||||
background: #ffebee;
|
||||
border-color: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
||||
background: ${cssManager.bdTheme('#ffebee', '#4a1f1f')};
|
||||
}
|
||||
|
||||
.securityCard.warning {
|
||||
border-color: #ff9800;
|
||||
background: #fff3e0;
|
||||
border-color: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
||||
background: ${cssManager.bdTheme('#fff3e0', '#4a3a1f')};
|
||||
}
|
||||
|
||||
.securityCard.success {
|
||||
border-color: #4caf50;
|
||||
background: #e8f5e9;
|
||||
border-color: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
||||
background: ${cssManager.bdTheme('#e8f5e9', '#1f3f1f')};
|
||||
}
|
||||
|
||||
.cardHeader {
|
||||
@@ -109,7 +114,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
.cardTitle {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||||
}
|
||||
|
||||
.cardStatus {
|
||||
@@ -120,18 +125,18 @@ export class OpsViewSecurity extends DeesElement {
|
||||
}
|
||||
|
||||
.status-critical {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
background: ${cssManager.bdTheme('#f44336', '#ff6666')};
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
background: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
}
|
||||
|
||||
.status-good {
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
background: ${cssManager.bdTheme('#4caf50', '#66cc66')};
|
||||
color: ${cssManager.bdTheme('#fff', '#fff')};
|
||||
}
|
||||
|
||||
.metricValue {
|
||||
@@ -142,7 +147,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
|
||||
.metricLabel {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
@@ -159,7 +164,7 @@ export class OpsViewSecurity extends DeesElement {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
|
||||
}
|
||||
|
||||
.blockedIpItem:last-child {
|
||||
@@ -173,12 +178,12 @@ export class OpsViewSecurity extends DeesElement {
|
||||
|
||||
.blockReason {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.blockTime {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -243,36 +248,60 @@ export class OpsViewSecurity extends DeesElement {
|
||||
|
||||
private renderOverview(metrics: any) {
|
||||
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`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard ${threatLevel}">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Threat Level</h3>
|
||||
<span class="cardStatus status-${threatLevel === 'alert' ? 'critical' : threatLevel === 'warning' ? 'warning' : 'good'}">
|
||||
${threatLevel.toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metricValue">${this.getThreatScore(metrics)}/100</div>
|
||||
<div class="metricLabel">Overall security score</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Blocked Threats</h3>
|
||||
</div>
|
||||
<div class="metricValue">${metrics.blockedIPs.length + metrics.spamDetected}</div>
|
||||
<div class="metricLabel">Total threats blocked today</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<div class="cardHeader">
|
||||
<h3 class="cardTitle">Active Sessions</h3>
|
||||
</div>
|
||||
<div class="metricValue">${0}</div>
|
||||
<div class="metricLabel">Current authenticated sessions</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
></dees-statsgrid>
|
||||
|
||||
<h2>Recent Security Events</h2>
|
||||
<dees-table
|
||||
@@ -320,20 +349,32 @@ export class OpsViewSecurity extends DeesElement {
|
||||
}
|
||||
|
||||
private renderAuthentication(metrics: any) {
|
||||
return html`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Authentication Statistics</h3>
|
||||
<div class="metricValue">${metrics.authenticationFailures}</div>
|
||||
<div class="metricLabel">Failed authentication attempts today</div>
|
||||
</div>
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
id: 'authFailures',
|
||||
title: 'Authentication Failures',
|
||||
value: metrics.authenticationFailures,
|
||||
type: 'number',
|
||||
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">
|
||||
<h3 class="cardTitle">Successful Logins</h3>
|
||||
<div class="metricValue">${0}</div>
|
||||
<div class="metricLabel">Successful logins today</div>
|
||||
</div>
|
||||
</div>
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
></dees-statsgrid>
|
||||
|
||||
<h2>Recent Login Attempts</h2>
|
||||
<dees-table
|
||||
@@ -352,32 +393,50 @@ export class OpsViewSecurity extends DeesElement {
|
||||
}
|
||||
|
||||
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`
|
||||
<div class="securityGrid">
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Malware Detection</h3>
|
||||
<div class="metricValue">${metrics.malwareDetected}</div>
|
||||
<div class="metricLabel">Malware detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Phishing Detection</h3>
|
||||
<div class="metricValue">${metrics.phishingDetected}</div>
|
||||
<div class="metricLabel">Phishing attempts detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Suspicious Activities</h3>
|
||||
<div class="metricValue">${metrics.suspiciousActivities}</div>
|
||||
<div class="metricLabel">Suspicious activities detected</div>
|
||||
</div>
|
||||
|
||||
<div class="securityCard">
|
||||
<h3 class="cardTitle">Spam Detection</h3>
|
||||
<div class="metricValue">${metrics.spamDetected}</div>
|
||||
<div class="metricLabel">Spam emails blocked</div>
|
||||
</div>
|
||||
</div>
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
></dees-statsgrid>
|
||||
|
||||
<h2>Email Security Configuration</h2>
|
||||
<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-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
color: ${cssManager.bdTheme('#111', '#fff')};
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:host([theme="dark"]) .heading {
|
||||
color: #fff;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user