Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
b81bda6ce8 | |||
9b3f5c458d | |||
3ba47f9a71 | |||
2ab2e30336 | |||
8ce6c88d58 | |||
facae93e4b | |||
0eb4963247 | |||
02dd3c77b5 | |||
93995d5031 |
16
package.json
16
package.json
@ -12,15 +12,16 @@
|
||||
"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/tsbundle": "^2.4.0",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.1",
|
||||
"@git.zone/tswatch": "^2.0.1",
|
||||
"@types/node": "^22.15.30",
|
||||
"@types/node": "^22.0.0",
|
||||
"node-forge": "^1.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -29,8 +30,8 @@
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@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.8.20",
|
||||
"@design.estate/dees-element": "^2.0.44",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
"@push.rocks/smartacme": "^8.0.0",
|
||||
@ -41,14 +42,15 @@
|
||||
"@push.rocks/smartjwt": "^2.2.1",
|
||||
"@push.rocks/smartlog": "^3.1.8",
|
||||
"@push.rocks/smartmail": "^2.1.0",
|
||||
"@push.rocks/smartmetrics": "^2.0.10",
|
||||
"@push.rocks/smartnetwork": "^4.0.2",
|
||||
"@push.rocks/smartpath": "^5.0.5",
|
||||
"@push.rocks/smartpromise": "^4.0.3",
|
||||
"@push.rocks/smartproxy": "^19.5.26",
|
||||
"@push.rocks/smartproxy": "^19.6.6",
|
||||
"@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.20",
|
||||
"@push.rocks/smartunique": "^3.0.9",
|
||||
"@serve.zone/interfaces": "^5.0.4",
|
||||
"@tsclass/tsclass": "^9.2.0",
|
||||
|
1018
pnpm-lock.yaml
generated
1018
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -903,4 +903,45 @@ The DNS functionality has been refactored from UnifiedEmailServer to a dedicated
|
||||
- DNS functionality is now easily discoverable in DnsManager
|
||||
- Clear separation between DNS management and email server logic
|
||||
- UnifiedEmailServer is simpler and more focused
|
||||
- All DNS-related tests pass successfully
|
||||
- All DNS-related tests pass successfully
|
||||
|
||||
## SmartMetrics Integration (2025-06-12) - COMPLETED
|
||||
|
||||
### Overview
|
||||
Fixed the UI metrics display to show accurate CPU and memory data from SmartMetrics.
|
||||
|
||||
### Key Findings
|
||||
1. **CPU Metrics:**
|
||||
- SmartMetrics provides `cpuUsageText` as a string percentage
|
||||
- MetricsManager parses it as `cpuUsage.user` (system is always 0)
|
||||
- UI was incorrectly dividing by 2, showing half the actual CPU usage
|
||||
|
||||
2. **Memory Metrics:**
|
||||
- SmartMetrics calculates `maxMemoryMB` as minimum of:
|
||||
- V8 heap size limit
|
||||
- System total memory
|
||||
- Docker memory limit (if available)
|
||||
- Provides `memoryUsageBytes` (total process memory including children)
|
||||
- Provides `memoryPercentage` (pre-calculated percentage)
|
||||
- UI was only showing heap usage, missing actual memory constraints
|
||||
|
||||
### Changes Made
|
||||
1. **MetricsManager Enhanced:**
|
||||
- Added `maxMemoryMB` from SmartMetrics instance
|
||||
- Added `actualUsageBytes` from SmartMetrics data
|
||||
- Added `actualUsagePercentage` from SmartMetrics data
|
||||
- Kept existing memory fields for compatibility
|
||||
|
||||
2. **Interface Updated:**
|
||||
- Added optional fields to `IServerStats.memoryUsage`
|
||||
- Fields are optional to maintain backward compatibility
|
||||
|
||||
3. **UI Fixed:**
|
||||
- Removed incorrect CPU division by 2
|
||||
- Uses `actualUsagePercentage` when available (falls back to heap percentage)
|
||||
- Shows actual memory usage vs max memory limit (not just heap)
|
||||
|
||||
### Result
|
||||
- CPU now shows accurate usage percentage
|
||||
- Memory shows percentage of actual constraints (Docker/system/V8 limits)
|
||||
- Better monitoring for containerized environments
|
@ -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.*
|
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
|
@ -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:');
|
||||
@ -566,6 +580,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(),
|
||||
|
||||
|
@ -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
|
||||
this.activeConnections.delete(socket);
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
288
ts/monitoring/classes.metricsmanager.ts
Normal file
288
ts/monitoring/classes.metricsmanager.ts
Normal file
@ -0,0 +1,288 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import { DcRouter } from '../classes.dcrouter.js';
|
||||
|
||||
export class MetricsManager {
|
||||
private logger: plugins.smartlog.Smartlog;
|
||||
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
||||
private dcRouter: DcRouter;
|
||||
private resetInterval?: NodeJS.Timeout;
|
||||
|
||||
// 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(),
|
||||
};
|
||||
|
||||
// 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(),
|
||||
};
|
||||
|
||||
// Track security-specific metrics
|
||||
private securityMetrics = {
|
||||
blockedIPs: 0,
|
||||
authFailures: 0,
|
||||
spamDetected: 0,
|
||||
malwareDetected: 0,
|
||||
phishingDetected: 0,
|
||||
lastResetDate: new Date().toDateString(),
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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.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.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.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() {
|
||||
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStats() : 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.getActiveConnections() : 0,
|
||||
totalConnections: proxyStats ? proxyStats.getTotalConnections() : 0,
|
||||
requestsPerSecond: proxyStats ? proxyStats.getRequestsPerSecond() : 0,
|
||||
throughput: proxyStats ? proxyStats.getThroughput() : { bytesIn: 0, bytesOut: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// Get email metrics
|
||||
public async getEmailStats() {
|
||||
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: 0, // TODO: Implement when delivery tracking is added
|
||||
topRecipients: [], // TODO: Implement recipient tracking
|
||||
recentActivity: [], // TODO: Implement activity log
|
||||
};
|
||||
}
|
||||
|
||||
// Get DNS metrics
|
||||
public async getDnsStats() {
|
||||
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 }));
|
||||
|
||||
return {
|
||||
queriesPerSecond: 0, // TODO: Calculate based on time window
|
||||
totalQueries: this.dnsMetrics.totalQueries,
|
||||
cacheHits: this.dnsMetrics.cacheHits,
|
||||
cacheMisses: this.dnsMetrics.cacheMisses,
|
||||
cacheHitRate: cacheHitRate,
|
||||
topDomains: topDomains,
|
||||
queryTypes: this.dnsMetrics.queryTypes,
|
||||
averageResponseTime: 0, // TODO: Implement response time tracking
|
||||
activeDomains: this.dnsMetrics.topDomains.size,
|
||||
};
|
||||
}
|
||||
|
||||
// Get security metrics
|
||||
public async getSecurityStats() {
|
||||
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: [], // TODO: Implement incident logging
|
||||
};
|
||||
}
|
||||
|
||||
// Get connection info from SmartProxy
|
||||
public async getConnectionInfo() {
|
||||
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStats() : null;
|
||||
|
||||
if (!proxyStats) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const connectionsByRoute = proxyStats.getConnectionsByRoute();
|
||||
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(): void {
|
||||
this.emailMetrics.sentToday++;
|
||||
}
|
||||
|
||||
public trackEmailReceived(): void {
|
||||
this.emailMetrics.receivedToday++;
|
||||
}
|
||||
|
||||
public trackEmailFailed(): void {
|
||||
this.emailMetrics.failedToday++;
|
||||
}
|
||||
|
||||
public trackEmailBounced(): void {
|
||||
this.emailMetrics.bouncedToday++;
|
||||
}
|
||||
|
||||
public updateQueueSize(size: number): void {
|
||||
this.emailMetrics.queueSize = size;
|
||||
}
|
||||
|
||||
// DNS event tracking methods
|
||||
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean): void {
|
||||
this.dnsMetrics.totalQueries++;
|
||||
|
||||
if (cacheHit) {
|
||||
this.dnsMetrics.cacheHits++;
|
||||
} else {
|
||||
this.dnsMetrics.cacheMisses++;
|
||||
}
|
||||
|
||||
// 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(): void {
|
||||
this.securityMetrics.blockedIPs++;
|
||||
}
|
||||
|
||||
public trackAuthFailure(): void {
|
||||
this.securityMetrics.authFailures++;
|
||||
}
|
||||
|
||||
public trackSpamDetected(): void {
|
||||
this.securityMetrics.spamDetected++;
|
||||
}
|
||||
|
||||
public trackMalwareDetected(): void {
|
||||
this.securityMetrics.malwareDetected++;
|
||||
}
|
||||
|
||||
public trackPhishingDetected(): void {
|
||||
this.securityMetrics.phishingDetected++;
|
||||
}
|
||||
}
|
1
ts/monitoring/index.ts
Normal file
1
ts/monitoring/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './classes.metricsmanager.js';
|
@ -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();
|
||||
@ -120,7 +121,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 +201,31 @@ 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 from MetricsManager if available
|
||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||
|
||||
// Map connection info to detailed format
|
||||
// Note: Some fields will be placeholder values until more detailed tracking is implemented
|
||||
connectionInfo.forEach((info, index) => {
|
||||
connections.push({
|
||||
id: `conn-${index}`,
|
||||
type: 'http', // TODO: Determine from source/protocol
|
||||
source: {
|
||||
ip: '0.0.0.0', // TODO: Track actual source IPs
|
||||
port: 0,
|
||||
},
|
||||
destination: {
|
||||
ip: '0.0.0.0',
|
||||
port: 443,
|
||||
service: info.source,
|
||||
},
|
||||
startTime: info.lastActivity.getTime(),
|
||||
bytesTransferred: 0, // TODO: Track bytes per connection
|
||||
status: 'active',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
@ -178,25 +179,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 +210,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 +225,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 +260,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';
|
||||
|
@ -6,6 +6,10 @@ export interface IServerStats {
|
||||
heapTotal: number;
|
||||
external: number;
|
||||
rss: number;
|
||||
// SmartMetrics memory data
|
||||
maxMemoryMB?: number;
|
||||
actualUsageBytes?: number;
|
||||
actualUsagePercentage?: number;
|
||||
};
|
||||
cpuUsage: {
|
||||
user: number;
|
||||
|
@ -82,7 +82,7 @@ export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||
activeView: 'dashboard',
|
||||
sidebarCollapsed: false,
|
||||
autoRefresh: true,
|
||||
refreshInterval: 30000, // 30 seconds
|
||||
refreshInterval: 1000, // 1 second
|
||||
theme: 'light',
|
||||
},
|
||||
);
|
||||
|
@ -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';
|
||||
@ -84,8 +85,12 @@ export class OpsDashboard extends DeesElement {
|
||||
element: OpsViewOverview,
|
||||
},
|
||||
{
|
||||
name: 'Statistics',
|
||||
element: OpsViewStats,
|
||||
name: 'Network',
|
||||
element: OpsViewNetwork,
|
||||
},
|
||||
{
|
||||
name: 'Emails',
|
||||
element: OpsViewEmails,
|
||||
},
|
||||
{
|
||||
name: 'Logs',
|
||||
|
@ -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')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
681
ts_web/elements/ops-view-emails.ts
Normal file
681
ts_web/elements/ops-view-emails.ts
Normal file
@ -0,0 +1,681 @@
|
||||
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 = '';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.loadEmails();
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
await DeesModal.createAndShow({
|
||||
heading: forward ? 'Forward Email' : replyTo ? 'Reply to Email' : 'New Email',
|
||||
content: html`
|
||||
<div style="width: 700px; max-width: 90vw;">
|
||||
<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?.());
|
||||
}}>
|
||||
<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-editor
|
||||
key="body"
|
||||
label="Message"
|
||||
.mode=${'markdown'}
|
||||
.height=${400}
|
||||
.value=${replyTo && !forward ? `\n\n---\nOn ${new Date(replyTo.date).toLocaleString()}, ${replyTo.from} wrote:\n\n${replyTo.body}` : replyTo && forward ? replyTo.body : ''}
|
||||
></dees-editor>
|
||||
|
||||
<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 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)
|
||||
const newEmail: IEmail = {
|
||||
id: `email-${Date.now()}`,
|
||||
from: 'me@dcrouter.local',
|
||||
to: formData.to || [],
|
||||
cc: formData.cc || [],
|
||||
bcc: formData.bcc || [],
|
||||
subject: formData.subject,
|
||||
body: formData.body,
|
||||
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;
|
||||
}
|
||||
|
503
ts_web/elements/ops-view-network.ts
Normal file
503
ts_web/elements/ops-view-network.ts
Normal file
@ -0,0 +1,503 @@
|
||||
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 selectedTimeRange: '1m' | '5m' | '15m' | '1h' | '24h' = '5m';
|
||||
|
||||
@state()
|
||||
private selectedProtocol: 'all' | 'http' | 'https' | 'smtp' | 'dns' = 'all';
|
||||
|
||||
@state()
|
||||
private networkRequests: INetworkRequest[] = [];
|
||||
|
||||
@state()
|
||||
private trafficData: Array<{ x: number; y: number }> = [];
|
||||
|
||||
@state()
|
||||
private isLoading = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.subscribeToStateParts();
|
||||
this.generateMockData(); // TODO: Replace with real data from metrics
|
||||
}
|
||||
|
||||
private subscribeToStateParts() {
|
||||
appstate.statsStatePart.state.subscribe((state) => {
|
||||
this.statsState = state;
|
||||
this.updateNetworkData();
|
||||
});
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.networkContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.controlBar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.controlGroup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controlLabel {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
dees-statsgrid {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-chart-area {
|
||||
margin-bottom: 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">
|
||||
<!-- Control Bar -->
|
||||
<div class="controlBar">
|
||||
<div class="controlGroup">
|
||||
<span class="controlLabel">Time Range:</span>
|
||||
<dees-button-group>
|
||||
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
|
||||
<dees-button
|
||||
@click=${() => this.selectedTimeRange = range}
|
||||
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
|
||||
>
|
||||
${range}
|
||||
</dees-button>
|
||||
`)}
|
||||
</dees-button-group>
|
||||
</div>
|
||||
|
||||
<div class="controlGroup">
|
||||
<span class="controlLabel">Protocol:</span>
|
||||
<dees-input-dropdown
|
||||
.options=${[
|
||||
{ key: 'all', label: 'All Protocols' },
|
||||
{ key: 'http', label: 'HTTP' },
|
||||
{ key: 'https', label: 'HTTPS' },
|
||||
{ key: 'smtp', label: 'SMTP' },
|
||||
{ key: 'dns', label: 'DNS' },
|
||||
]}
|
||||
.selectedOption=${{ key: this.selectedProtocol, label: this.getProtocolLabel(this.selectedProtocol) }}
|
||||
@selectedOption=${(e: CustomEvent) => this.selectedProtocol = e.detail.key}
|
||||
></dees-input-dropdown>
|
||||
</div>
|
||||
|
||||
<div style="margin-left: auto;">
|
||||
<dees-button
|
||||
@click=${() => this.refreshData()}
|
||||
.disabled=${this.isLoading}
|
||||
>
|
||||
${this.isLoading ? html`<dees-spinner size="small"></dees-spinner>` : 'Refresh'}
|
||||
</dees-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
${this.renderNetworkStats()}
|
||||
|
||||
<!-- Traffic Chart -->
|
||||
<dees-chart-area
|
||||
.label=${'Network Traffic'}
|
||||
.series=${[
|
||||
{
|
||||
name: 'Requests/min',
|
||||
data: this.trafficData,
|
||||
}
|
||||
]}
|
||||
></dees-chart-area>
|
||||
|
||||
<!-- Requests Table -->
|
||||
<dees-table
|
||||
.data=${this.getFilteredRequests()}
|
||||
.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="Last ${this.selectedTimeRange} of 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);
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Request ID copied to clipboard');
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
private getFilteredRequests(): INetworkRequest[] {
|
||||
if (this.selectedProtocol === 'all') {
|
||||
return this.networkRequests;
|
||||
}
|
||||
|
||||
// Map protocol filter to actual protocol values
|
||||
const protocolMap: Record<string, string[]> = {
|
||||
'http': ['http'],
|
||||
'https': ['https'],
|
||||
'smtp': ['tcp'], // SMTP runs over TCP
|
||||
'dns': ['udp'], // DNS typically runs over UDP
|
||||
};
|
||||
|
||||
const allowedProtocols = protocolMap[this.selectedProtocol] || [this.selectedProtocol];
|
||||
return this.networkRequests.filter(req => allowedProtocols.includes(req.protocol));
|
||||
}
|
||||
|
||||
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 getProtocolLabel(protocol: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
'all': 'All Protocols',
|
||||
'http': 'HTTP',
|
||||
'https': 'HTTPS',
|
||||
'smtp': 'SMTP',
|
||||
'dns': 'DNS',
|
||||
};
|
||||
return labels[protocol] || protocol.toUpperCase();
|
||||
}
|
||||
|
||||
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 calculateRequestsPerSecond(): number {
|
||||
// TODO: Calculate from real data based on connection metrics
|
||||
// For now, return a calculated value based on active connections
|
||||
return Math.floor((this.statsState.serverStats?.activeConnections || 0) * 0.8);
|
||||
}
|
||||
|
||||
private calculateThroughput(): { in: number; out: number } {
|
||||
// TODO: Calculate from real connection data
|
||||
// For now, return estimated values
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
return {
|
||||
in: activeConnections * 1024 * 10, // 10KB per connection estimate
|
||||
out: activeConnections * 1024 * 50, // 50KB per connection estimate
|
||||
};
|
||||
}
|
||||
|
||||
private renderNetworkStats(): TemplateResult {
|
||||
const reqPerSec = this.calculateRequestsPerSecond();
|
||||
const throughput = this.calculateThroughput();
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
|
||||
// Generate trend data for requests per second
|
||||
const trendData = Array.from({ length: 20 }, (_, i) =>
|
||||
Math.max(0, reqPerSec + (Math.random() - 0.5) * 10)
|
||||
);
|
||||
|
||||
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 () => {
|
||||
// TODO: Show connection details
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'requests',
|
||||
title: 'Requests/sec',
|
||||
value: reqPerSec,
|
||||
type: 'trend',
|
||||
icon: 'chartLine',
|
||||
color: '#3b82f6',
|
||||
trendData: trendData,
|
||||
description: `${this.formatNumber(reqPerSec)} req/s`,
|
||||
},
|
||||
{
|
||||
id: 'throughputIn',
|
||||
title: 'Throughput In',
|
||||
value: this.formatBytes(throughput.in),
|
||||
unit: '/s',
|
||||
type: 'number',
|
||||
icon: 'download',
|
||||
color: '#22c55e',
|
||||
},
|
||||
{
|
||||
id: 'throughputOut',
|
||||
title: 'Throughput Out',
|
||||
value: this.formatBytes(throughput.out),
|
||||
unit: '/s',
|
||||
type: 'number',
|
||||
icon: 'upload',
|
||||
color: '#8b5cf6',
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<dees-statsgrid
|
||||
.tiles=${tiles}
|
||||
.minTileWidth=${200}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Export Data',
|
||||
iconName: 'fileExport',
|
||||
action: async () => {
|
||||
// TODO: Export network data
|
||||
// TODO: Implement toast notification when DeesToast.show is available
|
||||
console.log('Export feature coming soon');
|
||||
},
|
||||
},
|
||||
]}
|
||||
></dees-statsgrid>
|
||||
`;
|
||||
}
|
||||
|
||||
private async refreshData() {
|
||||
this.isLoading = true;
|
||||
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
|
||||
await this.updateNetworkData();
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
private async updateNetworkData() {
|
||||
// TODO: Fetch real network data from the server
|
||||
// For now, using mock data
|
||||
this.generateMockData();
|
||||
}
|
||||
|
||||
private generateMockData() {
|
||||
// Generate mock network requests
|
||||
const now = Date.now();
|
||||
const protocols: Array<'http' | 'https' | 'tcp' | 'udp'> = ['http', 'https', 'tcp', 'udp'];
|
||||
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'];
|
||||
const hosts = ['api.example.com', 'app.local', 'mail.server.com', 'dns.resolver.net'];
|
||||
|
||||
this.networkRequests = Array.from({ length: 100 }, (_, i) => ({
|
||||
id: `req-${i}`,
|
||||
timestamp: now - (i * 5000), // 5 seconds apart
|
||||
method: methods[Math.floor(Math.random() * methods.length)],
|
||||
url: `/api/v1/resource/${Math.floor(Math.random() * 100)}`,
|
||||
hostname: hosts[Math.floor(Math.random() * hosts.length)],
|
||||
port: Math.random() > 0.5 ? 443 : 80,
|
||||
protocol: protocols[Math.floor(Math.random() * protocols.length)],
|
||||
statusCode: Math.random() > 0.8 ? 404 : 200,
|
||||
duration: Math.floor(Math.random() * 500),
|
||||
bytesIn: Math.floor(Math.random() * 10000),
|
||||
bytesOut: Math.floor(Math.random() * 50000),
|
||||
remoteIp: `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
||||
route: 'main-route',
|
||||
}));
|
||||
|
||||
// Generate traffic data for chart
|
||||
this.trafficData = Array.from({ length: 60 }, (_, i) => ({
|
||||
x: now - (i * 60000), // 1 minute intervals
|
||||
y: Math.floor(Math.random() * 100) + 50,
|
||||
})).reverse();
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
||||
<div class="statCard">
|
||||
<h3>Connections</h3>
|
||||
<div class="statValue">${this.statsState.serverStats.activeConnections}</div>
|
||||
<div class="statLabel">Active connections</div>
|
||||
</div>
|
||||
${this.renderServerStats()}
|
||||
|
||||
<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>
|
||||
${this.renderEmailStats()}
|
||||
|
||||
<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