feat(ops-server): implement TypedRouter integration and modular handler classes
This commit is contained in:
@ -70,14 +70,14 @@ ts_interfaces/
|
||||
- **Health Check**
|
||||
- [x] `IReq_GetHealthStatus` - Service health monitoring
|
||||
|
||||
### Phase 2: Backend Implementation
|
||||
### Phase 2: Backend Implementation ✓
|
||||
|
||||
#### 2.1 Enhance OpsServer (`ts/opsserver/classes.opsserver.ts`)
|
||||
#### 2.1 Enhance OpsServer (`ts/opsserver/classes.opsserver.ts`) ✓
|
||||
|
||||
- [ ] Add TypedRouter initialization
|
||||
- [ ] Use TypedServer's built-in typedrouter
|
||||
- [ ] CORS is already handled by TypedServer
|
||||
- [ ] Add handler registration method
|
||||
- [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
|
||||
@ -122,15 +122,15 @@ TypedServer (built-in typedrouter at /typedrequest)
|
||||
|
||||
This allows clean separation of concerns while keeping all handlers accessible through the single `/typedrequest` endpoint.
|
||||
|
||||
#### 2.2 Create Handler Classes
|
||||
#### 2.2 Create Handler Classes ✓
|
||||
|
||||
Create modular handlers in `ts/opsserver/handlers/`:
|
||||
|
||||
- [ ] `stats.handler.ts` - Server and performance statistics
|
||||
- [ ] `email.handler.ts` - Email-related operations
|
||||
- [ ] `dns.handler.ts` - DNS management statistics
|
||||
- [ ] `security.handler.ts` - Security and reputation metrics
|
||||
- [ ] `config.handler.ts` - Configuration management
|
||||
- [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
|
||||
@ -298,10 +298,17 @@ Create modular components in `ts_web/elements/components/`:
|
||||
- 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
|
||||
|
||||
### Next Steps
|
||||
- Phase 2: Backend Implementation - Enhance OpsServer and create handlers
|
||||
- Phase 3: Frontend State Management - Set up Smartstate
|
||||
- Phase 4: Frontend Integration - Create API clients and update dashboard
|
||||
- Phase 5: Create modular UI components
|
||||
|
||||
---
|
||||
|
||||
|
83
test/test.opsserver-api.ts
Normal file
83
test/test.opsserver-api.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DcRouter } from '../ts/index.js';
|
||||
import { TypedRequest } from '@api.global/typedrequest';
|
||||
import * as interfaces from '../ts_interfaces/index.js';
|
||||
|
||||
let testDcRouter: DcRouter;
|
||||
|
||||
tap.test('should start DCRouter with OpsServer', async () => {
|
||||
testDcRouter = new DcRouter({
|
||||
// Minimal config for testing
|
||||
});
|
||||
|
||||
await testDcRouter.start();
|
||||
expect(testDcRouter.opsServer).toBeInstanceOf(Object);
|
||||
});
|
||||
|
||||
tap.test('should respond to health status request', async () => {
|
||||
const healthRequest = new TypedRequest<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'getHealthStatus'
|
||||
);
|
||||
|
||||
const response = await healthRequest.fire({
|
||||
detailed: false
|
||||
});
|
||||
|
||||
expect(response).toHaveProperty('health');
|
||||
expect(response.health.healthy).toBeTrue();
|
||||
expect(response.health.services).toHaveProperty('OpsServer');
|
||||
});
|
||||
|
||||
tap.test('should respond to server statistics request', async () => {
|
||||
const statsRequest = new TypedRequest<interfaces.requests.IReq_GetServerStatistics>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'getServerStatistics'
|
||||
);
|
||||
|
||||
const response = await statsRequest.fire({
|
||||
includeHistory: false
|
||||
});
|
||||
|
||||
expect(response).toHaveProperty('stats');
|
||||
expect(response.stats).toHaveProperty('uptime');
|
||||
expect(response.stats).toHaveProperty('cpuUsage');
|
||||
expect(response.stats).toHaveProperty('memoryUsage');
|
||||
});
|
||||
|
||||
tap.test('should respond to configuration request', async () => {
|
||||
const configRequest = new TypedRequest<interfaces.requests.IReq_GetConfiguration>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'getConfiguration'
|
||||
);
|
||||
|
||||
const response = await configRequest.fire({});
|
||||
|
||||
expect(response).toHaveProperty('config');
|
||||
expect(response.config).toHaveProperty('email');
|
||||
expect(response.config).toHaveProperty('dns');
|
||||
expect(response.config).toHaveProperty('proxy');
|
||||
expect(response.config).toHaveProperty('security');
|
||||
});
|
||||
|
||||
tap.test('should handle log retrieval request', async () => {
|
||||
const logsRequest = new TypedRequest<interfaces.requests.IReq_GetRecentLogs>(
|
||||
'http://localhost:3000/typedrequest',
|
||||
'getRecentLogs'
|
||||
);
|
||||
|
||||
const response = await logsRequest.fire({
|
||||
limit: 10
|
||||
});
|
||||
|
||||
expect(response).toHaveProperty('logs');
|
||||
expect(response).toHaveProperty('total');
|
||||
expect(response).toHaveProperty('hasMore');
|
||||
expect(response.logs).toBeArray();
|
||||
});
|
||||
|
||||
tap.test('should stop DCRouter', async () => {
|
||||
await testDcRouter.stop();
|
||||
});
|
||||
|
||||
export default tap.start();
|
@ -134,6 +134,8 @@ export class DcRouter {
|
||||
public storageManager: StorageManager;
|
||||
public opsServer: OpsServer;
|
||||
|
||||
// TypedRouter for API endpoints
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
// Environment access
|
||||
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
||||
|
@ -1,13 +1,27 @@
|
||||
import type DcRouter from '../classes.dcrouter.js';
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import * as handlers from './handlers/index.js';
|
||||
|
||||
export class OpsServer {
|
||||
public dcRouterRef: DcRouter;
|
||||
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
|
||||
|
||||
// TypedRouter for OpsServer-specific handlers
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
// Handler instances
|
||||
private adminHandler: handlers.AdminHandler;
|
||||
private configHandler: handlers.ConfigHandler;
|
||||
private logsHandler: handlers.LogsHandler;
|
||||
private securityHandler: handlers.SecurityHandler;
|
||||
private statsHandler: handlers.StatsHandler;
|
||||
|
||||
constructor(dcRouterRefArg: DcRouter) {
|
||||
this.dcRouterRef = dcRouterRefArg;
|
||||
|
||||
// Add our typedrouter to the dcRouter's main typedrouter
|
||||
this.dcRouterRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
}
|
||||
|
||||
public async start() {
|
||||
@ -17,8 +31,33 @@ export class OpsServer {
|
||||
serveDir: paths.distServe,
|
||||
});
|
||||
|
||||
// The server has a built-in typedrouter at /typedrequest
|
||||
// Add the main dcRouter typedrouter to the server's typedrouter
|
||||
this.server.typedrouter.addTypedRouter(this.dcRouterRef.typedrouter);
|
||||
|
||||
// Set up handlers
|
||||
this.setupHandlers();
|
||||
|
||||
await this.server.start(3000);
|
||||
}
|
||||
|
||||
public async stop() {}
|
||||
/**
|
||||
* Set up all TypedRequest handlers
|
||||
*/
|
||||
private setupHandlers(): void {
|
||||
// Instantiate all handlers - they self-register with the typedrouter
|
||||
this.adminHandler = new handlers.AdminHandler(this);
|
||||
this.configHandler = new handlers.ConfigHandler(this);
|
||||
this.logsHandler = new handlers.LogsHandler(this);
|
||||
this.securityHandler = new handlers.SecurityHandler(this);
|
||||
this.statsHandler = new handlers.StatsHandler(this);
|
||||
|
||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
if (this.server) {
|
||||
await this.server.stop();
|
||||
}
|
||||
}
|
||||
}
|
148
ts/opsserver/handlers/admin.handler.ts
Normal file
148
ts/opsserver/handlers/admin.handler.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class AdminHandler {
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
|
||||
// Simple in-memory session storage (in production, use proper session management)
|
||||
private sessions = new Map<string, {
|
||||
identity: interfaces.data.IIdentity;
|
||||
createdAt: number;
|
||||
lastAccess: number;
|
||||
}>();
|
||||
|
||||
constructor(private opsServerRef: OpsServer) {
|
||||
// Add this handler's router to the parent
|
||||
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||
this.registerHandlers();
|
||||
}
|
||||
|
||||
private registerHandlers(): void {
|
||||
// Admin Login Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLoginWithUsernameAndPassword>(
|
||||
'adminLoginWithUsernameAndPassword',
|
||||
async (dataArg, toolsArg) => {
|
||||
try {
|
||||
// TODO: Implement proper authentication
|
||||
// For now, use a simple hardcoded check
|
||||
if (dataArg.username === 'admin' && dataArg.password === 'admin') {
|
||||
const token = plugins.uuid.v4();
|
||||
const identity: interfaces.data.IIdentity = {
|
||||
token,
|
||||
expiresAt: Date.now() + 24 * 60 * 60 * 1000, // 24 hours
|
||||
permissions: ['admin'],
|
||||
};
|
||||
|
||||
// Store session
|
||||
this.sessions.set(token, {
|
||||
identity,
|
||||
createdAt: Date.now(),
|
||||
lastAccess: Date.now(),
|
||||
});
|
||||
|
||||
// Clean up old sessions
|
||||
this.cleanupSessions();
|
||||
|
||||
return {
|
||||
identity,
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Admin Logout Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AdminLogout>(
|
||||
'adminLogout',
|
||||
async (dataArg, toolsArg) => {
|
||||
if (dataArg.identity?.token && this.sessions.has(dataArg.identity.token)) {
|
||||
this.sessions.delete(dataArg.identity.token);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Verify Identity Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyIdentity>(
|
||||
'verifyIdentity',
|
||||
async (dataArg, toolsArg) => {
|
||||
if (!dataArg.identity?.token) {
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
|
||||
const session = this.sessions.get(dataArg.identity.token);
|
||||
if (session && session.identity.expiresAt > Date.now()) {
|
||||
// Update last access
|
||||
session.lastAccess = Date.now();
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
identity: session.identity,
|
||||
};
|
||||
} else {
|
||||
// Clean up expired session
|
||||
if (session) {
|
||||
this.sessions.delete(dataArg.identity.token);
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up expired sessions (older than 24 hours)
|
||||
*/
|
||||
private cleanupSessions(): void {
|
||||
const now = Date.now();
|
||||
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
for (const [token, session] of this.sessions.entries()) {
|
||||
if (now - session.lastAccess > maxAge) {
|
||||
this.sessions.delete(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a guard for authentication
|
||||
* This can be used by other handlers to protect endpoints
|
||||
*/
|
||||
public createAuthGuard() {
|
||||
return async (dataArg: { identity?: interfaces.data.IIdentity }) => {
|
||||
if (!dataArg.identity?.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const session = this.sessions.get(dataArg.identity.token);
|
||||
if (session && session.identity.expiresAt > Date.now()) {
|
||||
// Update last access
|
||||
session.lastAccess = Date.now();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}
|
143
ts/opsserver/handlers/config.handler.ts
Normal file
143
ts/opsserver/handlers/config.handler.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class ConfigHandler {
|
||||
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(): void {
|
||||
// Get Configuration Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetConfiguration>(
|
||||
'getConfiguration',
|
||||
async (dataArg, toolsArg) => {
|
||||
const config = await this.getConfiguration(dataArg.section);
|
||||
return {
|
||||
config,
|
||||
section: dataArg.section,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Update Configuration Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateConfiguration>(
|
||||
'updateConfiguration',
|
||||
async (dataArg, toolsArg) => {
|
||||
try {
|
||||
const updatedConfig = await this.updateConfiguration(dataArg.section, dataArg.config);
|
||||
return {
|
||||
updated: true,
|
||||
config: updatedConfig,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
updated: false,
|
||||
config: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async getConfiguration(section?: string): Promise<{
|
||||
email: {
|
||||
enabled: boolean;
|
||||
ports: number[];
|
||||
maxMessageSize: number;
|
||||
rateLimits: {
|
||||
perMinute: number;
|
||||
perHour: number;
|
||||
perDay: number;
|
||||
};
|
||||
};
|
||||
dns: {
|
||||
enabled: boolean;
|
||||
port: number;
|
||||
nameservers: string[];
|
||||
caching: boolean;
|
||||
ttl: number;
|
||||
};
|
||||
proxy: {
|
||||
enabled: boolean;
|
||||
httpPort: number;
|
||||
httpsPort: number;
|
||||
maxConnections: number;
|
||||
};
|
||||
security: {
|
||||
blockList: string[];
|
||||
rateLimit: boolean;
|
||||
spamDetection: boolean;
|
||||
tlsRequired: boolean;
|
||||
};
|
||||
}> {
|
||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||
|
||||
return {
|
||||
email: {
|
||||
enabled: !!dcRouter.emailServer,
|
||||
ports: dcRouter.emailServer ? [25, 465, 587, 2525] : [],
|
||||
maxMessageSize: 10 * 1024 * 1024, // 10MB default
|
||||
rateLimits: {
|
||||
perMinute: 10,
|
||||
perHour: 100,
|
||||
perDay: 1000,
|
||||
},
|
||||
},
|
||||
dns: {
|
||||
enabled: !!dcRouter.dnsServer,
|
||||
port: 53,
|
||||
nameservers: dcRouter.options.dnsNsDomains || [],
|
||||
caching: true,
|
||||
ttl: 300,
|
||||
},
|
||||
proxy: {
|
||||
enabled: !!dcRouter.smartProxy,
|
||||
httpPort: 80,
|
||||
httpsPort: 443,
|
||||
maxConnections: 1000,
|
||||
},
|
||||
security: {
|
||||
blockList: [],
|
||||
rateLimit: true,
|
||||
spamDetection: true,
|
||||
tlsRequired: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async updateConfiguration(section: string, config: any): Promise<any> {
|
||||
// TODO: Implement actual configuration updates
|
||||
// This would involve:
|
||||
// 1. Validating the configuration changes
|
||||
// 2. Applying them to the running services
|
||||
// 3. Persisting them to storage
|
||||
// 4. Potentially restarting affected services
|
||||
|
||||
// For now, just validate and return the config
|
||||
if (section === 'email' && config.maxMessageSize && config.maxMessageSize < 1024) {
|
||||
throw new Error('Maximum message size must be at least 1KB');
|
||||
}
|
||||
|
||||
if (section === 'dns' && config.ttl && (config.ttl < 0 || config.ttl > 86400)) {
|
||||
throw new Error('DNS TTL must be between 0 and 86400 seconds');
|
||||
}
|
||||
|
||||
if (section === 'proxy' && config.maxConnections && config.maxConnections < 1) {
|
||||
throw new Error('Maximum connections must be at least 1');
|
||||
}
|
||||
|
||||
// In a real implementation, apply the changes here
|
||||
// For now, return the current configuration
|
||||
const currentConfig = await this.getConfiguration(section);
|
||||
return currentConfig;
|
||||
}
|
||||
}
|
5
ts/opsserver/handlers/index.ts
Normal file
5
ts/opsserver/handlers/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './admin.handler.js';
|
||||
export * from './config.handler.js';
|
||||
export * from './logs.handler.js';
|
||||
export * from './security.handler.js';
|
||||
export * from './stats.handler.js';
|
195
ts/opsserver/handlers/logs.handler.ts
Normal file
195
ts/opsserver/handlers/logs.handler.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class LogsHandler {
|
||||
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(): void {
|
||||
// Get Recent Logs Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRecentLogs>(
|
||||
'getRecentLogs',
|
||||
async (dataArg, toolsArg) => {
|
||||
const logs = await this.getRecentLogs(
|
||||
dataArg.level,
|
||||
dataArg.category,
|
||||
dataArg.limit || 100,
|
||||
dataArg.offset || 0,
|
||||
dataArg.search,
|
||||
dataArg.timeRange
|
||||
);
|
||||
|
||||
return {
|
||||
logs,
|
||||
total: logs.length, // TODO: Implement proper total count
|
||||
hasMore: false, // TODO: Implement proper pagination
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Get Log Stream Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetLogStream>(
|
||||
'getLogStream',
|
||||
async (dataArg, toolsArg) => {
|
||||
// Create a virtual stream for log streaming
|
||||
const virtualStream = new plugins.typedrequest.VirtualStream<Uint8Array>();
|
||||
|
||||
// Set up log streaming
|
||||
const streamLogs = this.setupLogStream(
|
||||
virtualStream,
|
||||
dataArg.filters?.level,
|
||||
dataArg.filters?.category,
|
||||
dataArg.follow
|
||||
);
|
||||
|
||||
// Start streaming
|
||||
streamLogs.start();
|
||||
|
||||
// VirtualStream handles cleanup automatically
|
||||
|
||||
return {
|
||||
logStream: virtualStream as any, // Cast to IVirtualStream interface
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async getRecentLogs(
|
||||
level?: 'error' | 'warn' | 'info' | 'debug',
|
||||
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email',
|
||||
limit: number = 100,
|
||||
offset: number = 0,
|
||||
search?: string,
|
||||
timeRange?: '1h' | '6h' | '24h' | '7d' | '30d'
|
||||
): Promise<Array<{
|
||||
timestamp: number;
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
message: string;
|
||||
metadata?: any;
|
||||
}>> {
|
||||
// TODO: Implement actual log retrieval from storage or logger
|
||||
// For now, return mock data
|
||||
const mockLogs: Array<{
|
||||
timestamp: number;
|
||||
level: 'debug' | 'info' | 'warn' | 'error';
|
||||
category: 'smtp' | 'dns' | 'security' | 'system' | 'email';
|
||||
message: string;
|
||||
metadata?: any;
|
||||
}> = [];
|
||||
|
||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
||||
const now = Date.now();
|
||||
|
||||
// Generate some mock log entries
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
||||
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
||||
|
||||
// Filter by requested criteria
|
||||
if (level && mockLevel !== level) continue;
|
||||
if (category && mockCategory !== category) continue;
|
||||
|
||||
mockLogs.push({
|
||||
timestamp: now - (i * 60000), // 1 minute apart
|
||||
level: mockLevel,
|
||||
category: mockCategory,
|
||||
message: `Sample log message ${i} from ${mockCategory}`,
|
||||
metadata: {
|
||||
requestId: plugins.uuid.v4(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
return mockLogs.slice(offset, offset + limit);
|
||||
}
|
||||
|
||||
private setupLogStream(
|
||||
virtualStream: plugins.typedrequest.VirtualStream<Uint8Array>,
|
||||
levelFilter?: string[],
|
||||
categoryFilter?: string[],
|
||||
follow: boolean = true
|
||||
): {
|
||||
start: () => void;
|
||||
stop: () => void;
|
||||
} {
|
||||
let intervalId: NodeJS.Timeout | null = null;
|
||||
let logIndex = 0;
|
||||
|
||||
const start = () => {
|
||||
if (!follow) {
|
||||
// Send existing logs and close
|
||||
this.getRecentLogs(
|
||||
levelFilter?.[0] as any,
|
||||
categoryFilter?.[0] as any,
|
||||
100,
|
||||
0
|
||||
).then(logs => {
|
||||
logs.forEach(log => {
|
||||
const logData = JSON.stringify(log);
|
||||
const encoder = new TextEncoder();
|
||||
virtualStream.sendData(encoder.encode(logData));
|
||||
});
|
||||
// VirtualStream doesn't have end() method - it closes automatically
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// For follow mode, simulate real-time log streaming
|
||||
intervalId = setInterval(() => {
|
||||
const categories: Array<'smtp' | 'dns' | 'security' | 'system' | 'email'> = ['smtp', 'dns', 'security', 'system', 'email'];
|
||||
const levels: Array<'debug' | 'info' | 'warn' | 'error'> = ['info', 'warn', 'error', 'debug'];
|
||||
|
||||
const mockCategory = categories[Math.floor(Math.random() * categories.length)];
|
||||
const mockLevel = levels[Math.floor(Math.random() * levels.length)];
|
||||
|
||||
// Filter by requested criteria
|
||||
if (levelFilter && !levelFilter.includes(mockLevel)) return;
|
||||
if (categoryFilter && !categoryFilter.includes(mockCategory)) return;
|
||||
|
||||
const logEntry = {
|
||||
timestamp: Date.now(),
|
||||
level: mockLevel,
|
||||
category: mockCategory,
|
||||
message: `Real-time log ${logIndex++} from ${mockCategory}`,
|
||||
metadata: {
|
||||
requestId: plugins.uuid.v4(),
|
||||
},
|
||||
};
|
||||
|
||||
const logData = JSON.stringify(logEntry);
|
||||
const encoder = new TextEncoder();
|
||||
virtualStream.sendData(encoder.encode(logData));
|
||||
}, 2000); // Send a log every 2 seconds
|
||||
|
||||
// TODO: Hook into actual logger events
|
||||
// logger.on('log', (logEntry) => {
|
||||
// if (matchesCriteria(logEntry, level, service)) {
|
||||
// virtualStream.sendData(formatLogEntry(logEntry));
|
||||
// }
|
||||
// });
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
// TODO: Unhook from logger events
|
||||
};
|
||||
|
||||
return { start, stop };
|
||||
}
|
||||
}
|
208
ts/opsserver/handlers/security.handler.ts
Normal file
208
ts/opsserver/handlers/security.handler.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
export class SecurityHandler {
|
||||
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(): void {
|
||||
// Security Metrics Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetSecurityMetrics>(
|
||||
'getSecurityMetrics',
|
||||
async (dataArg, toolsArg) => {
|
||||
const metrics = await this.collectSecurityMetrics();
|
||||
return {
|
||||
metrics: {
|
||||
blockedIPs: metrics.blockedIPs,
|
||||
reputationScores: metrics.reputationScores,
|
||||
spamDetected: metrics.spamDetection.detected,
|
||||
malwareDetected: metrics.malwareDetected,
|
||||
phishingDetected: metrics.phishingDetected,
|
||||
authenticationFailures: metrics.authFailures,
|
||||
suspiciousActivities: metrics.suspiciousActivities,
|
||||
},
|
||||
trends: dataArg.includeDetails ? {
|
||||
spam: metrics.trends.spam,
|
||||
malware: metrics.trends.malware,
|
||||
phishing: metrics.trends.phishing,
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Active Connections Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetActiveConnections>(
|
||||
'getActiveConnections',
|
||||
async (dataArg, toolsArg) => {
|
||||
const connections = await this.getActiveConnections(dataArg.protocol, dataArg.state);
|
||||
const connectionInfos: interfaces.data.IConnectionInfo[] = connections.map(conn => ({
|
||||
id: conn.id,
|
||||
remoteAddress: conn.source.ip,
|
||||
localAddress: conn.destination.ip,
|
||||
startTime: conn.startTime,
|
||||
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
||||
state: conn.status as any,
|
||||
bytesReceived: Math.floor(conn.bytesTransferred / 2),
|
||||
bytesSent: Math.floor(conn.bytesTransferred / 2),
|
||||
}));
|
||||
|
||||
const summary = {
|
||||
total: connectionInfos.length,
|
||||
byProtocol: connectionInfos.reduce((acc, conn) => {
|
||||
acc[conn.protocol] = (acc[conn.protocol] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as { [protocol: string]: number }),
|
||||
byState: connectionInfos.reduce((acc, conn) => {
|
||||
acc[conn.state] = (acc[conn.state] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as { [state: string]: number }),
|
||||
};
|
||||
|
||||
return {
|
||||
connections: connectionInfos,
|
||||
summary,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Rate Limit Status Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRateLimitStatus>(
|
||||
'getRateLimitStatus',
|
||||
async (dataArg, toolsArg) => {
|
||||
const status = await this.getRateLimitStatus(dataArg.domain, dataArg.ip);
|
||||
const limits: interfaces.data.IRateLimitInfo[] = status.limits.map(limit => ({
|
||||
domain: limit.identifier,
|
||||
currentRate: limit.current,
|
||||
limit: limit.limit,
|
||||
remaining: limit.limit - limit.current,
|
||||
resetTime: limit.resetAt,
|
||||
blocked: limit.status === 'limited',
|
||||
}));
|
||||
|
||||
return {
|
||||
limits,
|
||||
globalLimit: dataArg.includeBlocked ? {
|
||||
current: limits.reduce((sum, l) => sum + l.currentRate, 0),
|
||||
limit: 1000, // Global limit
|
||||
remaining: 1000 - limits.reduce((sum, l) => sum + l.currentRate, 0),
|
||||
} : undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async collectSecurityMetrics(): Promise<{
|
||||
blockedIPs: string[];
|
||||
reputationScores: { [domain: string]: number };
|
||||
spamDetection: {
|
||||
detected: number;
|
||||
falsePositives: number;
|
||||
};
|
||||
malwareDetected: number;
|
||||
phishingDetected: number;
|
||||
authFailures: number;
|
||||
suspiciousActivities: number;
|
||||
trends: {
|
||||
spam: Array<{ timestamp: number; value: number }>;
|
||||
malware: Array<{ timestamp: number; value: number }>;
|
||||
phishing: Array<{ timestamp: number; value: number }>;
|
||||
};
|
||||
}> {
|
||||
// TODO: Implement actual security metrics collection
|
||||
return {
|
||||
blockedIPs: [],
|
||||
reputationScores: {},
|
||||
spamDetection: {
|
||||
detected: 0,
|
||||
falsePositives: 0,
|
||||
},
|
||||
malwareDetected: 0,
|
||||
phishingDetected: 0,
|
||||
authFailures: 0,
|
||||
suspiciousActivities: 0,
|
||||
trends: {
|
||||
spam: [],
|
||||
malware: [],
|
||||
phishing: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getActiveConnections(
|
||||
protocol?: 'http' | 'https' | 'smtp' | 'smtps',
|
||||
state?: string
|
||||
): Promise<Array<{
|
||||
id: string;
|
||||
type: 'http' | 'smtp' | 'dns';
|
||||
source: {
|
||||
ip: string;
|
||||
port: number;
|
||||
country?: string;
|
||||
};
|
||||
destination: {
|
||||
ip: string;
|
||||
port: number;
|
||||
service?: string;
|
||||
};
|
||||
startTime: number;
|
||||
bytesTransferred: number;
|
||||
status: 'active' | 'idle' | 'closing';
|
||||
}>> {
|
||||
const connections: Array<{
|
||||
id: string;
|
||||
type: 'http' | 'smtp' | 'dns';
|
||||
source: {
|
||||
ip: string;
|
||||
port: number;
|
||||
country?: string;
|
||||
};
|
||||
destination: {
|
||||
ip: string;
|
||||
port: number;
|
||||
service?: string;
|
||||
};
|
||||
startTime: number;
|
||||
bytesTransferred: number;
|
||||
status: 'active' | 'idle' | 'closing';
|
||||
}> = [];
|
||||
|
||||
// TODO: Implement actual connection tracking
|
||||
// This would collect from:
|
||||
// - SmartProxy connections
|
||||
// - Email server connections
|
||||
// - DNS server connections
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
private async getRateLimitStatus(
|
||||
domain?: string,
|
||||
ip?: string
|
||||
): Promise<{
|
||||
limits: Array<{
|
||||
identifier: string;
|
||||
type: 'ip' | 'domain' | 'email';
|
||||
limit: number;
|
||||
current: number;
|
||||
resetAt: number;
|
||||
status: 'ok' | 'warning' | 'limited';
|
||||
}>;
|
||||
}> {
|
||||
// TODO: Implement actual rate limit status collection
|
||||
return {
|
||||
limits: [],
|
||||
};
|
||||
}
|
||||
}
|
344
ts/opsserver/handlers/stats.handler.ts
Normal file
344
ts/opsserver/handlers/stats.handler.ts
Normal file
@ -0,0 +1,344 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { OpsServer } from '../classes.opsserver.js';
|
||||
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||
|
||||
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(): void {
|
||||
// Server Statistics Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetServerStatistics>(
|
||||
'getServerStatistics',
|
||||
async (dataArg, toolsArg) => {
|
||||
const stats = await this.collectServerStats();
|
||||
return {
|
||||
stats: {
|
||||
uptime: stats.uptime,
|
||||
startTime: Date.now() - (stats.uptime * 1000),
|
||||
memoryUsage: stats.memoryUsage,
|
||||
cpuUsage: stats.cpuUsage,
|
||||
activeConnections: stats.activeConnections,
|
||||
totalConnections: stats.totalConnections,
|
||||
},
|
||||
history: dataArg.includeHistory ? stats.history : undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Email Statistics Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetEmailStatistics>(
|
||||
'getEmailStatistics',
|
||||
async (dataArg, toolsArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
if (!emailServer) {
|
||||
return {
|
||||
stats: {
|
||||
sent: 0,
|
||||
received: 0,
|
||||
bounced: 0,
|
||||
queued: 0,
|
||||
failed: 0,
|
||||
averageDeliveryTime: 0,
|
||||
deliveryRate: 0,
|
||||
bounceRate: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await this.collectEmailStats();
|
||||
return {
|
||||
stats: {
|
||||
sent: stats.sentToday,
|
||||
received: stats.receivedToday,
|
||||
bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
|
||||
queued: stats.queueSize,
|
||||
failed: 0,
|
||||
averageDeliveryTime: 0,
|
||||
deliveryRate: stats.deliveryRate,
|
||||
bounceRate: stats.bounceRate,
|
||||
},
|
||||
domainBreakdown: dataArg.includeDetails ? stats.domainBreakdown : undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// DNS Statistics Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsStatistics>(
|
||||
'getDnsStatistics',
|
||||
async (dataArg, toolsArg) => {
|
||||
const dnsServer = this.opsServerRef.dcRouterRef.dnsServer;
|
||||
if (!dnsServer) {
|
||||
return {
|
||||
stats: {
|
||||
totalQueries: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0,
|
||||
cacheHitRate: 0,
|
||||
activeDomains: 0,
|
||||
averageResponseTime: 0,
|
||||
queryTypes: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const stats = await this.collectDnsStats();
|
||||
return {
|
||||
stats: {
|
||||
totalQueries: stats.totalQueries,
|
||||
cacheHits: stats.cacheHits,
|
||||
cacheMisses: stats.cacheMisses,
|
||||
cacheHitRate: stats.cacheHitRate,
|
||||
activeDomains: stats.topDomains.length,
|
||||
averageResponseTime: 0,
|
||||
queryTypes: stats.queryTypes,
|
||||
},
|
||||
domainBreakdown: dataArg.includeQueryTypes ? stats.domainBreakdown : undefined,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Queue Status Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetQueueStatus>(
|
||||
'getQueueStatus',
|
||||
async (dataArg, toolsArg) => {
|
||||
const emailServer = this.opsServerRef.dcRouterRef.emailServer;
|
||||
const queues: interfaces.data.IQueueStatus[] = [];
|
||||
|
||||
if (emailServer) {
|
||||
const status = await this.getQueueStatus();
|
||||
queues.push({
|
||||
name: dataArg.queueName || 'default',
|
||||
size: status.pending,
|
||||
processing: status.active,
|
||||
failed: status.failed,
|
||||
retrying: status.retrying,
|
||||
averageProcessingTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
queues,
|
||||
totalItems: queues.reduce((sum, q) => sum + q.size + q.processing + q.failed + q.retrying, 0),
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
// Health Status Handler
|
||||
this.typedrouter.addTypedHandler(
|
||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetHealthStatus>(
|
||||
'getHealthStatus',
|
||||
async (dataArg, toolsArg) => {
|
||||
const health = await this.checkHealthStatus();
|
||||
return {
|
||||
health: {
|
||||
healthy: health.healthy,
|
||||
uptime: process.uptime(),
|
||||
services: health.services.reduce((acc, service) => {
|
||||
acc[service.name] = {
|
||||
status: service.status,
|
||||
message: service.message,
|
||||
lastCheck: Date.now(),
|
||||
};
|
||||
return acc;
|
||||
}, {} as any),
|
||||
version: '2.12.0', // TODO: Get from package.json
|
||||
},
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private async collectServerStats(): Promise<{
|
||||
uptime: number;
|
||||
cpuUsage: {
|
||||
user: number;
|
||||
system: number;
|
||||
};
|
||||
memoryUsage: interfaces.data.IServerStats['memoryUsage'];
|
||||
requestsPerSecond: number;
|
||||
activeConnections: number;
|
||||
totalConnections: number;
|
||||
history: Array<{
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}>;
|
||||
}> {
|
||||
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
|
||||
},
|
||||
memoryUsage: {
|
||||
heapUsed: memUsage.heapUsed,
|
||||
heapTotal: memUsage.heapTotal,
|
||||
external: memUsage.external,
|
||||
rss: memUsage.rss,
|
||||
},
|
||||
requestsPerSecond,
|
||||
activeConnections,
|
||||
totalConnections,
|
||||
history: [], // TODO: Implement history tracking
|
||||
};
|
||||
}
|
||||
|
||||
private async collectEmailStats(): Promise<{
|
||||
sentToday: number;
|
||||
receivedToday: number;
|
||||
bounceRate: number;
|
||||
deliveryRate: number;
|
||||
queueSize: number;
|
||||
domainBreakdown?: { [domain: string]: interfaces.data.IEmailStats };
|
||||
}> {
|
||||
// TODO: Implement actual email statistics collection
|
||||
return {
|
||||
sentToday: 0,
|
||||
receivedToday: 0,
|
||||
bounceRate: 0,
|
||||
deliveryRate: 100,
|
||||
queueSize: 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async collectDnsStats(): Promise<{
|
||||
queriesPerSecond: number;
|
||||
totalQueries: number;
|
||||
cacheHits: number;
|
||||
cacheMisses: number;
|
||||
cacheHitRate: number;
|
||||
topDomains: Array<{
|
||||
domain: string;
|
||||
count: number;
|
||||
}>;
|
||||
queryTypes: { [key: string]: number };
|
||||
domainBreakdown?: { [domain: string]: interfaces.data.IDnsStats };
|
||||
}> {
|
||||
// TODO: Implement actual DNS statistics collection
|
||||
return {
|
||||
queriesPerSecond: 0,
|
||||
totalQueries: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0,
|
||||
cacheHitRate: 0,
|
||||
topDomains: [],
|
||||
queryTypes: {},
|
||||
};
|
||||
}
|
||||
|
||||
private async getQueueStatus(): Promise<{
|
||||
pending: number;
|
||||
active: number;
|
||||
failed: number;
|
||||
retrying: number;
|
||||
items: Array<{
|
||||
id: string;
|
||||
recipient: string;
|
||||
subject: string;
|
||||
status: string;
|
||||
attempts: number;
|
||||
nextRetry?: number;
|
||||
}>;
|
||||
}> {
|
||||
// TODO: Implement actual queue status collection
|
||||
return {
|
||||
pending: 0,
|
||||
active: 0,
|
||||
failed: 0,
|
||||
retrying: 0,
|
||||
items: [],
|
||||
};
|
||||
}
|
||||
|
||||
private async checkHealthStatus(): Promise<{
|
||||
healthy: boolean;
|
||||
services: Array<{
|
||||
name: string;
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
message?: string;
|
||||
}>;
|
||||
checks: Array<{
|
||||
name: string;
|
||||
passed: boolean;
|
||||
message?: string;
|
||||
}>;
|
||||
}> {
|
||||
const services: Array<{
|
||||
name: string;
|
||||
status: 'healthy' | 'degraded' | 'unhealthy';
|
||||
message?: string;
|
||||
}> = [];
|
||||
|
||||
// Check HTTP Proxy
|
||||
if (this.opsServerRef.dcRouterRef.smartProxy) {
|
||||
services.push({
|
||||
name: 'HTTP/HTTPS Proxy',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check Email Server
|
||||
if (this.opsServerRef.dcRouterRef.emailServer) {
|
||||
services.push({
|
||||
name: 'Email Server',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check DNS Server
|
||||
if (this.opsServerRef.dcRouterRef.dnsServer) {
|
||||
services.push({
|
||||
name: 'DNS Server',
|
||||
status: 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
// Check OpsServer
|
||||
services.push({
|
||||
name: 'OpsServer',
|
||||
status: 'healthy',
|
||||
});
|
||||
|
||||
const healthy = services.every(s => s.status === 'healthy');
|
||||
|
||||
return {
|
||||
healthy,
|
||||
services,
|
||||
checks: [
|
||||
{
|
||||
name: 'Memory Usage',
|
||||
passed: process.memoryUsage().heapUsed < (plugins.os.totalmem() * 0.9),
|
||||
message: 'Memory usage within limits',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user