@qwickapps/server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +321 -0
  3. package/dist/core/control-panel.d.ts +21 -0
  4. package/dist/core/control-panel.d.ts.map +1 -0
  5. package/dist/core/control-panel.js +416 -0
  6. package/dist/core/control-panel.js.map +1 -0
  7. package/dist/core/gateway.d.ts +133 -0
  8. package/dist/core/gateway.d.ts.map +1 -0
  9. package/dist/core/gateway.js +270 -0
  10. package/dist/core/gateway.js.map +1 -0
  11. package/dist/core/health-manager.d.ts +52 -0
  12. package/dist/core/health-manager.d.ts.map +1 -0
  13. package/dist/core/health-manager.js +192 -0
  14. package/dist/core/health-manager.js.map +1 -0
  15. package/dist/core/index.d.ts +10 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +8 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/logging.d.ts +83 -0
  20. package/dist/core/logging.d.ts.map +1 -0
  21. package/dist/core/logging.js +191 -0
  22. package/dist/core/logging.js.map +1 -0
  23. package/dist/core/types.d.ts +195 -0
  24. package/dist/core/types.d.ts.map +1 -0
  25. package/dist/core/types.js +7 -0
  26. package/dist/core/types.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +17 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/plugins/config-plugin.d.ts +15 -0
  32. package/dist/plugins/config-plugin.d.ts.map +1 -0
  33. package/dist/plugins/config-plugin.js +96 -0
  34. package/dist/plugins/config-plugin.js.map +1 -0
  35. package/dist/plugins/diagnostics-plugin.d.ts +29 -0
  36. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
  37. package/dist/plugins/diagnostics-plugin.js +142 -0
  38. package/dist/plugins/diagnostics-plugin.js.map +1 -0
  39. package/dist/plugins/health-plugin.d.ts +17 -0
  40. package/dist/plugins/health-plugin.d.ts.map +1 -0
  41. package/dist/plugins/health-plugin.js +25 -0
  42. package/dist/plugins/health-plugin.js.map +1 -0
  43. package/dist/plugins/index.d.ts +14 -0
  44. package/dist/plugins/index.d.ts.map +1 -0
  45. package/dist/plugins/index.js +10 -0
  46. package/dist/plugins/index.js.map +1 -0
  47. package/dist/plugins/logs-plugin.d.ts +22 -0
  48. package/dist/plugins/logs-plugin.d.ts.map +1 -0
  49. package/dist/plugins/logs-plugin.js +242 -0
  50. package/dist/plugins/logs-plugin.js.map +1 -0
  51. package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
  52. package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
  53. package/dist-ui/assets/index-CiizQQnb.css +1 -0
  54. package/dist-ui/index.html +13 -0
  55. package/package.json +98 -0
  56. package/src/core/control-panel.ts +493 -0
  57. package/src/core/gateway.ts +421 -0
  58. package/src/core/health-manager.ts +227 -0
  59. package/src/core/index.ts +25 -0
  60. package/src/core/logging.ts +234 -0
  61. package/src/core/types.ts +218 -0
  62. package/src/index.ts +55 -0
  63. package/src/plugins/config-plugin.ts +117 -0
  64. package/src/plugins/diagnostics-plugin.ts +178 -0
  65. package/src/plugins/health-plugin.ts +35 -0
  66. package/src/plugins/index.ts +17 -0
  67. package/src/plugins/logs-plugin.ts +314 -0
  68. package/ui/index.html +12 -0
  69. package/ui/src/App.tsx +65 -0
  70. package/ui/src/api/controlPanelApi.ts +148 -0
  71. package/ui/src/config/AppConfig.ts +18 -0
  72. package/ui/src/index.css +29 -0
  73. package/ui/src/index.tsx +11 -0
  74. package/ui/src/pages/ConfigPage.tsx +199 -0
  75. package/ui/src/pages/DashboardPage.tsx +264 -0
  76. package/ui/src/pages/DiagnosticsPage.tsx +315 -0
  77. package/ui/src/pages/HealthPage.tsx +204 -0
  78. package/ui/src/pages/LogsPage.tsx +267 -0
  79. package/ui/src/pages/NotFoundPage.tsx +41 -0
  80. package/ui/tsconfig.json +19 -0
  81. package/ui/vite.config.ts +21 -0
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Gateway Server for @qwickapps/server
3
+ *
4
+ * Provides a production-ready gateway pattern that:
5
+ * 1. Serves the control panel UI (always responsive)
6
+ * 2. Proxies API requests to an internal service
7
+ * 3. Provides health and diagnostics endpoints
8
+ *
9
+ * Architecture:
10
+ * Internet → Gateway (GATEWAY_PORT, public) → Service (SERVICE_PORT, internal)
11
+ *
12
+ * The gateway is always responsive even if the internal service is down,
13
+ * allowing diagnostics and error visibility.
14
+ *
15
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
16
+ */
17
+
18
+ import type { Application, Request, Response, NextFunction } from 'express';
19
+ import type { IncomingMessage, ServerResponse } from 'http';
20
+ import type { Socket } from 'net';
21
+ import type { Server } from 'http';
22
+ import type { ControlPanelConfig, ControlPanelPlugin, Logger } from './types.js';
23
+ import { createControlPanel } from './control-panel.js';
24
+ import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
25
+ import { createProxyMiddleware, type Options } from 'http-proxy-middleware';
26
+ import { randomBytes } from 'crypto';
27
+ import express from 'express';
28
+ import { existsSync } from 'fs';
29
+ import { resolve } from 'path';
30
+
31
+ /**
32
+ * Gateway configuration
33
+ */
34
+ export interface GatewayConfig {
35
+ /** Port for the gateway (public-facing). Defaults to GATEWAY_PORT env or 3101 */
36
+ gatewayPort?: number;
37
+
38
+ /** Port for the internal service. Defaults to SERVICE_PORT env or 3100 */
39
+ servicePort?: number;
40
+
41
+ /** Product name for the control panel */
42
+ productName: string;
43
+
44
+ /** Product version */
45
+ version?: string;
46
+
47
+ /** Branding configuration */
48
+ branding?: ControlPanelConfig['branding'];
49
+
50
+ /** CORS origins */
51
+ corsOrigins?: string[];
52
+
53
+ /** Control panel plugins */
54
+ plugins?: ControlPanelPlugin[];
55
+
56
+ /** Quick links for the control panel */
57
+ links?: ControlPanelConfig['links'];
58
+
59
+ /** Path to custom React UI dist folder */
60
+ customUiPath?: string;
61
+
62
+ /**
63
+ * API paths to proxy to the internal service.
64
+ * Defaults to ['/api/v1'] if not specified.
65
+ * The gateway always proxies /health to the internal service.
66
+ */
67
+ proxyPaths?: string[];
68
+
69
+ /**
70
+ * Authentication mode for the control panel (not the API).
71
+ * - 'none': No authentication (not recommended for production)
72
+ * - 'basic': HTTP Basic Auth with username/password
73
+ * - 'auto': Auto-generate password on startup (default)
74
+ */
75
+ authMode?: 'none' | 'basic' | 'auto';
76
+
77
+ /** Basic auth username (defaults to 'admin') */
78
+ basicAuthUser?: string;
79
+
80
+ /** Basic auth password (required if authMode is 'basic') */
81
+ basicAuthPassword?: string;
82
+
83
+ /** Logger instance (deprecated: use logging config instead) */
84
+ logger?: Logger;
85
+
86
+ /** Logging configuration */
87
+ logging?: LoggingConfig;
88
+ }
89
+
90
+ /**
91
+ * Service factory function type
92
+ * Called with the service port, should return an object with:
93
+ * - app: Express application (or compatible)
94
+ * - server: HTTP server (created by calling listen)
95
+ * - shutdown: Async function to gracefully shut down the service
96
+ */
97
+ export interface ServiceFactory {
98
+ (port: number): Promise<{
99
+ app: Application;
100
+ server: Server;
101
+ shutdown: () => Promise<void>;
102
+ }>;
103
+ }
104
+
105
+ /**
106
+ * Gateway instance returned by createGateway
107
+ */
108
+ export interface GatewayInstance {
109
+ /** The control panel instance */
110
+ controlPanel: ReturnType<typeof createControlPanel>;
111
+
112
+ /** The internal service (if started) */
113
+ service: {
114
+ app: Application;
115
+ server: Server;
116
+ shutdown: () => Promise<void>;
117
+ } | null;
118
+
119
+ /** Start the gateway and internal service */
120
+ start: () => Promise<void>;
121
+
122
+ /** Stop everything gracefully */
123
+ stop: () => Promise<void>;
124
+
125
+ /** Gateway port */
126
+ gatewayPort: number;
127
+
128
+ /** Service port */
129
+ servicePort: number;
130
+ }
131
+
132
+
133
+ /**
134
+ * Basic auth middleware for gateway protection (control panel only)
135
+ * - Skips localhost requests
136
+ * - Skips API routes (/api/v1/*) - they have their own service auth
137
+ * - Skips health endpoints - these should be public
138
+ * - Requires valid credentials for non-localhost control panel access
139
+ */
140
+ function createBasicAuthMiddleware(
141
+ username: string,
142
+ password: string,
143
+ apiPaths: string[]
144
+ ) {
145
+ const expectedAuth = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
146
+
147
+ return (req: Request, res: Response, next: NextFunction) => {
148
+ const path = req.path;
149
+
150
+ // Skip auth for API routes - they use their own authentication
151
+ for (const apiPath of apiPaths) {
152
+ if (path.startsWith(apiPath)) {
153
+ return next();
154
+ }
155
+ }
156
+
157
+ // Skip auth for health endpoints - these should be publicly accessible
158
+ if (path === '/health' || path === '/api/health') {
159
+ return next();
160
+ }
161
+
162
+ // Allow localhost without auth
163
+ const remoteAddress = req.ip || req.socket?.remoteAddress || '';
164
+ const host = req.hostname || req.headers.host || '';
165
+ const isLocalhost =
166
+ host === 'localhost' ||
167
+ host === '127.0.0.1' ||
168
+ host.startsWith('localhost:') ||
169
+ host.startsWith('127.0.0.1:') ||
170
+ remoteAddress === '127.0.0.1' ||
171
+ remoteAddress === '::1' ||
172
+ remoteAddress === '::ffff:127.0.0.1';
173
+
174
+ if (isLocalhost) {
175
+ return next();
176
+ }
177
+
178
+ // Check for valid basic auth
179
+ const authHeader = req.headers.authorization;
180
+ if (authHeader === expectedAuth) {
181
+ return next();
182
+ }
183
+
184
+ // Request authentication
185
+ res.setHeader('WWW-Authenticate', 'Basic realm="Control Panel"');
186
+ res.status(401).json({
187
+ error: 'Unauthorized',
188
+ message: 'Authentication required.',
189
+ });
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Create a gateway that proxies to an internal service
195
+ *
196
+ * @param config - Gateway configuration
197
+ * @param serviceFactory - Factory function to create the internal service
198
+ * @returns Gateway instance
199
+ *
200
+ * @example
201
+ * ```typescript
202
+ * import { createGateway } from '@qwickapps/server';
203
+ *
204
+ * const gateway = createGateway(
205
+ * {
206
+ * productName: 'My Service',
207
+ * gatewayPort: 3101,
208
+ * servicePort: 3100,
209
+ * },
210
+ * async (port) => {
211
+ * const app = createMyApp();
212
+ * const server = app.listen(port);
213
+ * return {
214
+ * app,
215
+ * server,
216
+ * shutdown: async () => { server.close(); },
217
+ * };
218
+ * }
219
+ * );
220
+ *
221
+ * await gateway.start();
222
+ * ```
223
+ */
224
+ export function createGateway(
225
+ config: GatewayConfig,
226
+ serviceFactory: ServiceFactory
227
+ ): GatewayInstance {
228
+ // Initialize logging subsystem first
229
+ const loggingSubsystem = initializeLogging({
230
+ namespace: config.productName,
231
+ ...config.logging,
232
+ });
233
+
234
+ // Use provided logger or get one from the logging subsystem
235
+ const logger = config.logger || getControlPanelLogger('Gateway');
236
+
237
+ const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
238
+ const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
239
+ const nodeEnv = process.env.NODE_ENV || 'development';
240
+
241
+ // Auth configuration
242
+ const authMode = config.authMode || 'auto';
243
+ const basicAuthUser = config.basicAuthUser || process.env.BASIC_AUTH_USER || 'admin';
244
+ const providedPassword = config.basicAuthPassword || process.env.BASIC_AUTH_PASSWORD;
245
+ const basicAuthPassword = providedPassword || (authMode === 'auto' ? randomBytes(16).toString('base64url') : '');
246
+ const isPasswordAutoGenerated = !providedPassword && authMode === 'auto';
247
+
248
+ // API paths to proxy
249
+ const proxyPaths = config.proxyPaths || ['/api/v1'];
250
+
251
+ let service: GatewayInstance['service'] = null;
252
+
253
+ // Create control panel
254
+ const controlPanel = createControlPanel({
255
+ config: {
256
+ productName: config.productName,
257
+ port: gatewayPort,
258
+ version: config.version || process.env.npm_package_version || '1.0.0',
259
+ branding: config.branding,
260
+ cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
261
+ // Skip body parsing for proxied paths
262
+ skipBodyParserPaths: [...proxyPaths, '/health'],
263
+ // Disable built-in dashboard if custom UI is provided
264
+ disableDashboard: !!config.customUiPath,
265
+ links: config.links,
266
+ },
267
+ plugins: config.plugins || [],
268
+ logger,
269
+ });
270
+
271
+ // Add basic auth middleware if enabled
272
+ if (authMode === 'basic' || authMode === 'auto') {
273
+ controlPanel.app.use(createBasicAuthMiddleware(basicAuthUser, basicAuthPassword, proxyPaths));
274
+ }
275
+
276
+ // Setup proxy middleware for API paths
277
+ const setupProxyMiddleware = () => {
278
+ const target = `http://localhost:${servicePort}`;
279
+
280
+ // Proxy each API path
281
+ for (const apiPath of proxyPaths) {
282
+ const proxyOptions: Options = {
283
+ target,
284
+ changeOrigin: false,
285
+ pathFilter: `${apiPath}/**`,
286
+ on: {
287
+ error: (err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
288
+ logger.error('Proxy error', { error: err.message, path: apiPath });
289
+ if (res && 'writeHead' in res && !res.headersSent) {
290
+ res.writeHead(503, { 'Content-Type': 'application/json' });
291
+ res.end(
292
+ JSON.stringify({
293
+ error: 'Service Unavailable',
294
+ message: 'The service is currently unavailable. Please try again later.',
295
+ details: nodeEnv === 'development' ? err.message : undefined,
296
+ })
297
+ );
298
+ }
299
+ },
300
+ },
301
+ };
302
+ controlPanel.app.use(createProxyMiddleware(proxyOptions));
303
+ }
304
+
305
+ // Proxy /health endpoint to internal service
306
+ const healthProxyOptions: Options = {
307
+ target,
308
+ changeOrigin: false,
309
+ pathFilter: '/health',
310
+ on: {
311
+ error: (_err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
312
+ if (res && 'writeHead' in res && !res.headersSent) {
313
+ res.writeHead(503, { 'Content-Type': 'application/json' });
314
+ res.end(
315
+ JSON.stringify({
316
+ status: 'unhealthy',
317
+ error: 'Service unavailable',
318
+ gateway: 'healthy',
319
+ })
320
+ );
321
+ }
322
+ },
323
+ },
324
+ };
325
+ controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
326
+ };
327
+
328
+ // Serve custom React UI if provided
329
+ const setupCustomUI = () => {
330
+ if (config.customUiPath && existsSync(config.customUiPath)) {
331
+ logger.info(`Serving custom UI from ${config.customUiPath}`);
332
+ controlPanel.app.use(express.static(config.customUiPath));
333
+
334
+ // SPA fallback
335
+ controlPanel.app.get('*', (req, res, next) => {
336
+ if (req.path.startsWith('/api/') || req.path === '/api') {
337
+ return next();
338
+ }
339
+ res.sendFile(resolve(config.customUiPath!, 'index.html'));
340
+ });
341
+ }
342
+ };
343
+
344
+ const start = async (): Promise<void> => {
345
+ logger.info('Starting gateway...');
346
+
347
+ // 1. Start internal service
348
+ logger.info(`Starting internal service on port ${servicePort}...`);
349
+ service = await serviceFactory(servicePort);
350
+ logger.info(`Internal service started on port ${servicePort}`);
351
+
352
+ // 2. Setup proxy middleware (after service is started)
353
+ setupProxyMiddleware();
354
+
355
+ // 3. Setup custom UI (after proxy middleware)
356
+ setupCustomUI();
357
+
358
+ // 4. Start control panel gateway
359
+ await controlPanel.start();
360
+
361
+ // Log startup info
362
+ logger.info('');
363
+ logger.info('========================================');
364
+ logger.info(` ${config.productName} Gateway`);
365
+ logger.info('========================================');
366
+ logger.info('');
367
+ logger.info(` Gateway Port: ${gatewayPort} (public)`);
368
+ logger.info(` Service Port: ${servicePort} (internal)`);
369
+ logger.info('');
370
+
371
+ if (authMode === 'basic' || authMode === 'auto') {
372
+ logger.info(' Control Panel Auth: HTTP Basic Auth');
373
+ logger.info(' ----------------------------------------');
374
+ logger.info(` Username: ${basicAuthUser}`);
375
+ if (isPasswordAutoGenerated) {
376
+ logger.info(` Password: ${basicAuthPassword}`);
377
+ logger.info(' (auto-generated, set BASIC_AUTH_PASSWORD to use a fixed password)');
378
+ } else {
379
+ logger.info(' Password: ********** (from environment)');
380
+ }
381
+ logger.info(' ----------------------------------------');
382
+ } else {
383
+ logger.info(' Control Panel Auth: None (not recommended)');
384
+ }
385
+
386
+ logger.info('');
387
+ logger.info(' Endpoints:');
388
+ logger.info(` GET / - Control Panel UI`);
389
+ logger.info(` GET /api/health - Gateway health`);
390
+ logger.info(` GET /health - Service health (proxied)`);
391
+ for (const apiPath of proxyPaths) {
392
+ logger.info(` * ${apiPath}/* - Service API (proxied)`);
393
+ }
394
+ logger.info('========================================');
395
+ logger.info('');
396
+ };
397
+
398
+ const stop = async (): Promise<void> => {
399
+ logger.info('Shutting down gateway...');
400
+
401
+ // Stop control panel
402
+ await controlPanel.stop();
403
+
404
+ // Stop internal service
405
+ if (service) {
406
+ await service.shutdown();
407
+ service.server.close();
408
+ }
409
+
410
+ logger.info('Gateway shutdown complete');
411
+ };
412
+
413
+ return {
414
+ controlPanel,
415
+ service,
416
+ start,
417
+ stop,
418
+ gatewayPort,
419
+ servicePort,
420
+ };
421
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Health Check Manager
3
+ *
4
+ * Manages health checks for various services and provides aggregated status
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ import type { HealthCheck, HealthCheckResult, HealthStatus, Logger } from './types.js';
10
+
11
+ export class HealthManager {
12
+ private checks: Map<string, HealthCheck> = new Map();
13
+ private results: Map<string, HealthCheckResult> = new Map();
14
+ private intervals: Map<string, ReturnType<typeof setInterval>> = new Map();
15
+ private logger: Logger;
16
+
17
+ constructor(logger: Logger) {
18
+ this.logger = logger;
19
+ }
20
+
21
+ /**
22
+ * Register a health check
23
+ */
24
+ register(check: HealthCheck): void {
25
+ this.checks.set(check.name, check);
26
+
27
+ // Initialize result
28
+ this.results.set(check.name, {
29
+ status: 'unknown',
30
+ lastChecked: new Date(),
31
+ });
32
+
33
+ // Start periodic check
34
+ const interval = check.interval || 30000; // Default 30 seconds
35
+ this.runCheck(check.name);
36
+
37
+ const timer = setInterval(() => {
38
+ this.runCheck(check.name);
39
+ }, interval);
40
+
41
+ this.intervals.set(check.name, timer);
42
+
43
+ this.logger.info(`[HealthManager] Registered health check: ${check.name}`, {
44
+ type: check.type,
45
+ interval,
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Run a health check
51
+ */
52
+ private async runCheck(name: string): Promise<void> {
53
+ const check = this.checks.get(name);
54
+ if (!check) return;
55
+
56
+ const startTime = Date.now();
57
+ const timeout = check.timeout || 5000;
58
+
59
+ try {
60
+ let result: { healthy: boolean; latency?: number; details?: Record<string, unknown> };
61
+
62
+ switch (check.type) {
63
+ case 'http':
64
+ result = await this.runHttpCheck(check, timeout);
65
+ break;
66
+ case 'tcp':
67
+ result = await this.runTcpCheck(check, timeout);
68
+ break;
69
+ case 'custom':
70
+ if (check.check) {
71
+ result = await Promise.race([
72
+ check.check(),
73
+ new Promise<never>((_, reject) =>
74
+ setTimeout(() => reject(new Error('Timeout')), timeout)
75
+ ),
76
+ ]);
77
+ } else {
78
+ result = { healthy: false, details: { error: 'No check function provided' } };
79
+ }
80
+ break;
81
+ default:
82
+ result = { healthy: false, details: { error: 'Unknown check type' } };
83
+ }
84
+
85
+ const latency = result.latency || Date.now() - startTime;
86
+
87
+ this.results.set(name, {
88
+ status: result.healthy ? 'healthy' : 'unhealthy',
89
+ latency,
90
+ lastChecked: new Date(),
91
+ details: result.details,
92
+ });
93
+ } catch (error) {
94
+ const latency = Date.now() - startTime;
95
+ const message = error instanceof Error ? error.message : String(error);
96
+
97
+ this.results.set(name, {
98
+ status: 'unhealthy',
99
+ latency,
100
+ message,
101
+ lastChecked: new Date(),
102
+ });
103
+
104
+ this.logger.warn(`[HealthManager] Health check failed: ${name}`, {
105
+ error: message,
106
+ latency,
107
+ });
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Run HTTP health check
113
+ */
114
+ private async runHttpCheck(
115
+ check: HealthCheck,
116
+ timeout: number
117
+ ): Promise<{ healthy: boolean; latency?: number; details?: Record<string, unknown> }> {
118
+ if (!check.url) {
119
+ return { healthy: false, details: { error: 'No URL provided' } };
120
+ }
121
+
122
+ const controller = new AbortController();
123
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
124
+
125
+ try {
126
+ const startTime = Date.now();
127
+ const response = await fetch(check.url, {
128
+ method: 'GET',
129
+ signal: controller.signal,
130
+ });
131
+ const latency = Date.now() - startTime;
132
+
133
+ clearTimeout(timeoutId);
134
+
135
+ return {
136
+ healthy: response.ok,
137
+ latency,
138
+ details: {
139
+ status: response.status,
140
+ statusText: response.statusText,
141
+ },
142
+ };
143
+ } catch (error) {
144
+ clearTimeout(timeoutId);
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Run TCP health check (simplified - just tries to connect)
151
+ */
152
+ private async runTcpCheck(
153
+ check: HealthCheck,
154
+ timeout: number
155
+ ): Promise<{ healthy: boolean; latency?: number; details?: Record<string, unknown> }> {
156
+ if (!check.host || !check.port) {
157
+ return { healthy: false, details: { error: 'Host and port required for TCP check' } };
158
+ }
159
+
160
+ // Use HTTP check as a proxy for TCP (simplified for browser compatibility)
161
+ // In a real Node.js environment, you'd use net.createConnection
162
+ const url = `http://${check.host}:${check.port}`;
163
+
164
+ try {
165
+ return await this.runHttpCheck({ ...check, url }, timeout);
166
+ } catch {
167
+ // TCP check failed
168
+ return {
169
+ healthy: false,
170
+ details: { error: `Cannot connect to ${check.host}:${check.port}` },
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Get all health check results
177
+ */
178
+ getResults(): Record<string, HealthCheckResult> {
179
+ return Object.fromEntries(this.results);
180
+ }
181
+
182
+ /**
183
+ * Get specific health check result
184
+ */
185
+ getResult(name: string): HealthCheckResult | undefined {
186
+ return this.results.get(name);
187
+ }
188
+
189
+ /**
190
+ * Get aggregated status
191
+ */
192
+ getAggregatedStatus(): HealthStatus {
193
+ const results = Array.from(this.results.values());
194
+
195
+ if (results.length === 0) return 'unknown';
196
+
197
+ const unhealthyCount = results.filter((r) => r.status === 'unhealthy').length;
198
+ const degradedCount = results.filter((r) => r.status === 'degraded').length;
199
+
200
+ if (unhealthyCount > 0) return 'unhealthy';
201
+ if (degradedCount > 0) return 'degraded';
202
+
203
+ const hasUnknown = results.some((r) => r.status === 'unknown');
204
+ if (hasUnknown) return 'unknown';
205
+
206
+ return 'healthy';
207
+ }
208
+
209
+ /**
210
+ * Force run all checks
211
+ */
212
+ async checkAll(): Promise<void> {
213
+ const promises = Array.from(this.checks.keys()).map((name) => this.runCheck(name));
214
+ await Promise.all(promises);
215
+ }
216
+
217
+ /**
218
+ * Shutdown - clear all intervals
219
+ */
220
+ shutdown(): void {
221
+ for (const timer of this.intervals.values()) {
222
+ clearInterval(timer);
223
+ }
224
+ this.intervals.clear();
225
+ this.logger.info('[HealthManager] Shutdown complete');
226
+ }
227
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Core exports for @qwickapps/server
3
+ *
4
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
5
+ */
6
+
7
+ export { createControlPanel } from './control-panel.js';
8
+ export type { CreateControlPanelOptions } from './control-panel.js';
9
+
10
+ export { HealthManager } from './health-manager.js';
11
+
12
+ export type {
13
+ ControlPanelConfig,
14
+ ControlPanelPlugin,
15
+ ControlPanelInstance,
16
+ PluginContext,
17
+ HealthCheck,
18
+ HealthCheckType,
19
+ HealthStatus,
20
+ HealthCheckResult,
21
+ LogSource,
22
+ ConfigDisplayOptions,
23
+ Logger,
24
+ DiagnosticsReport,
25
+ } from './types.js';