@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,493 @@
1
+ /**
2
+ * Control Panel Core
3
+ *
4
+ * Creates and manages the control panel Express application
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ import express, { type Application, type Router, type Request, type Response } from 'express';
10
+ import helmet from 'helmet';
11
+ import cors from 'cors';
12
+ import compression from 'compression';
13
+ import { existsSync } from 'node:fs';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { dirname, join } from 'node:path';
16
+ import { HealthManager } from './health-manager.js';
17
+ import { initializeLogging, getControlPanelLogger, getLoggingSubsystem, type LoggingConfig } from './logging.js';
18
+ import type {
19
+ ControlPanelConfig,
20
+ ControlPanelPlugin,
21
+ ControlPanelInstance,
22
+ PluginContext,
23
+ DiagnosticsReport,
24
+ HealthCheck,
25
+ Logger,
26
+ } from './types.js';
27
+
28
+ // Get the package root directory for serving UI assets
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = dirname(__filename);
31
+ // Handle both src/core and dist/core paths - go up to find package root
32
+ const packageRoot = __dirname.includes('/src/')
33
+ ? join(__dirname, '..', '..')
34
+ : join(__dirname, '..', '..');
35
+ const uiDistPath = join(packageRoot, 'dist-ui');
36
+
37
+ export interface CreateControlPanelOptions {
38
+ config: ControlPanelConfig;
39
+ plugins?: ControlPanelPlugin[];
40
+ logger?: Logger;
41
+ /** Logging configuration */
42
+ logging?: LoggingConfig;
43
+ }
44
+
45
+ /**
46
+ * Create a control panel instance
47
+ */
48
+ export function createControlPanel(options: CreateControlPanelOptions): ControlPanelInstance {
49
+ const { config, plugins = [], logging: loggingConfig } = options;
50
+
51
+ // Initialize logging subsystem
52
+ const loggingSubsystem = initializeLogging({
53
+ namespace: config.productName,
54
+ ...loggingConfig,
55
+ });
56
+
57
+ // Use provided logger or get one from the logging subsystem
58
+ const logger = options.logger || loggingSubsystem.getRootLogger();
59
+
60
+ const app: Application = express();
61
+ const router: Router = express.Router();
62
+ const healthManager = new HealthManager(logger);
63
+ const registeredPlugins: ControlPanelPlugin[] = [];
64
+ let server: ReturnType<typeof app.listen> | null = null;
65
+ const startTime = Date.now();
66
+
67
+ // Security middleware
68
+ app.use(
69
+ helmet({
70
+ contentSecurityPolicy: false, // Allow inline scripts for simple UI
71
+ })
72
+ );
73
+
74
+ // CORS
75
+ app.use(
76
+ cors({
77
+ origin: config.cors?.origins || '*',
78
+ credentials: true,
79
+ })
80
+ );
81
+
82
+ // Body parsing (skip for specified paths to allow proxy middleware to work)
83
+ const skipBodyParserPaths = config.skipBodyParserPaths || [];
84
+ if (skipBodyParserPaths.length > 0) {
85
+ app.use((req, res, next) => {
86
+ // Skip body parsing for specified paths (useful for proxy middleware)
87
+ if (skipBodyParserPaths.some(path => req.path.startsWith(path))) {
88
+ return next();
89
+ }
90
+ express.json()(req, res, next);
91
+ });
92
+ } else {
93
+ app.use(express.json());
94
+ }
95
+ app.use(compression());
96
+
97
+ // Basic auth middleware if configured
98
+ if (config.auth?.enabled && config.auth.provider === 'basic' && config.auth.users) {
99
+ app.use((req, res, next) => {
100
+ const authHeader = req.headers.authorization;
101
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
102
+ res.set('WWW-Authenticate', 'Basic realm="Control Panel"');
103
+ return res.status(401).json({ error: 'Authentication required' });
104
+ }
105
+
106
+ const base64Credentials = authHeader.substring(6);
107
+ const credentials = Buffer.from(base64Credentials, 'base64').toString('utf-8');
108
+ const [username, password] = credentials.split(':');
109
+
110
+ const validUser = config.auth!.users!.find(
111
+ (u) => u.username === username && u.password === password
112
+ );
113
+
114
+ if (!validUser) {
115
+ return res.status(401).json({ error: 'Invalid credentials' });
116
+ }
117
+
118
+ next();
119
+ });
120
+ }
121
+
122
+ // Custom auth middleware
123
+ if (config.auth?.enabled && config.auth.provider === 'custom' && config.auth.customMiddleware) {
124
+ app.use(config.auth.customMiddleware);
125
+ }
126
+
127
+ // Request logging
128
+ app.use((req, _res, next) => {
129
+ logger.debug(`${req.method} ${req.path}`);
130
+ next();
131
+ });
132
+
133
+ // Mount router
134
+ app.use('/api', router);
135
+
136
+ // Built-in routes
137
+
138
+ /**
139
+ * GET /api/health - Aggregated health status
140
+ */
141
+ router.get('/health', (_req: Request, res: Response) => {
142
+ const results = healthManager.getResults();
143
+ const status = healthManager.getAggregatedStatus();
144
+ const uptime = Date.now() - startTime;
145
+
146
+ res.status(status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503).json({
147
+ status,
148
+ timestamp: new Date().toISOString(),
149
+ uptime,
150
+ checks: results,
151
+ });
152
+ });
153
+
154
+ /**
155
+ * GET /api/info - Product information
156
+ */
157
+ router.get('/info', (_req: Request, res: Response) => {
158
+ res.json({
159
+ product: config.productName,
160
+ version: config.version || 'unknown',
161
+ uptime: Date.now() - startTime,
162
+ links: config.links || [],
163
+ branding: config.branding || {},
164
+ });
165
+ });
166
+
167
+ /**
168
+ * GET /api/diagnostics - Full diagnostics for AI agents
169
+ */
170
+ router.get('/diagnostics', (_req: Request, res: Response) => {
171
+ const report = getDiagnostics();
172
+ res.json(report);
173
+ });
174
+
175
+ /**
176
+ * Serve dashboard UI
177
+ *
178
+ * Priority:
179
+ * 1. If useRichUI is true and dist-ui exists, serve React SPA
180
+ * 2. Otherwise serve simple static HTML dashboard
181
+ */
182
+ if (!config.disableDashboard) {
183
+ // Use customUiPath if provided, otherwise fall back to package's dist-ui
184
+ const effectiveUiPath = config.customUiPath || uiDistPath;
185
+ const hasRichUI = existsSync(effectiveUiPath);
186
+ const useRichUI = config.useRichUI !== false && hasRichUI;
187
+
188
+ logger.debug(`Dashboard config: __dirname=${__dirname}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
189
+
190
+ if (useRichUI) {
191
+ logger.info(`Serving rich React UI from ${effectiveUiPath}`);
192
+ // Serve static assets from dist-ui
193
+ app.use(express.static(effectiveUiPath));
194
+
195
+ // SPA fallback - serve index.html for all non-API routes
196
+ app.get('*', (req: Request, res: Response, next) => {
197
+ // Skip API routes (must check for /api/ with trailing slash to avoid matching /api-keys etc)
198
+ if (req.path.startsWith('/api/') || req.path === '/api') {
199
+ return next();
200
+ }
201
+ res.sendFile(join(effectiveUiPath, 'index.html'));
202
+ });
203
+ } else {
204
+ logger.info('Serving basic HTML dashboard');
205
+ app.get('/', (_req: Request, res: Response) => {
206
+ const html = generateDashboardHtml(config, healthManager.getResults());
207
+ res.type('html').send(html);
208
+ });
209
+ }
210
+ }
211
+
212
+ // Plugin context factory - creates context with plugin-specific logger
213
+ const createPluginContext = (pluginName: string): PluginContext => ({
214
+ config,
215
+ app,
216
+ router,
217
+ logger: getControlPanelLogger(pluginName),
218
+ registerHealthCheck: (check: HealthCheck) => healthManager.register(check),
219
+ });
220
+
221
+ // Register plugin
222
+ const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
223
+ logger.info(`Registering plugin: ${plugin.name}`);
224
+
225
+ // Register routes
226
+ if (plugin.routes) {
227
+ for (const route of plugin.routes) {
228
+ switch (route.method) {
229
+ case 'get':
230
+ router.get(route.path, route.handler);
231
+ break;
232
+ case 'post':
233
+ router.post(route.path, route.handler);
234
+ break;
235
+ case 'put':
236
+ router.put(route.path, route.handler);
237
+ break;
238
+ case 'delete':
239
+ router.delete(route.path, route.handler);
240
+ break;
241
+ }
242
+ logger.debug(`Registered route: ${route.method.toUpperCase()} ${route.path}`);
243
+ }
244
+ }
245
+
246
+ // Initialize plugin with plugin-specific logger
247
+ if (plugin.onInit) {
248
+ await plugin.onInit(createPluginContext(plugin.name));
249
+ }
250
+
251
+ registeredPlugins.push(plugin);
252
+ logger.info(`Plugin registered: ${plugin.name}`);
253
+ };
254
+
255
+ // Get diagnostics report
256
+ const getDiagnostics = (): DiagnosticsReport => {
257
+ const memUsage = process.memoryUsage();
258
+
259
+ return {
260
+ timestamp: new Date().toISOString(),
261
+ product: config.productName,
262
+ version: config.version,
263
+ uptime: Date.now() - startTime,
264
+ health: healthManager.getResults(),
265
+ system: {
266
+ nodeVersion: process.version,
267
+ platform: process.platform,
268
+ arch: process.arch,
269
+ memory: {
270
+ total: memUsage.heapTotal,
271
+ used: memUsage.heapUsed,
272
+ free: memUsage.heapTotal - memUsage.heapUsed,
273
+ },
274
+ cpu: {
275
+ usage: 0, // Would need os.cpus() for real measurement
276
+ },
277
+ },
278
+ };
279
+ };
280
+
281
+ // Start server
282
+ const start = async (): Promise<void> => {
283
+ // Register initial plugins
284
+ for (const plugin of plugins) {
285
+ await registerPlugin(plugin);
286
+ }
287
+
288
+ return new Promise((resolve) => {
289
+ server = app.listen(config.port, () => {
290
+ logger.info(`Control panel started on port ${config.port}`);
291
+ logger.info(`Dashboard: http://localhost:${config.port}`);
292
+ logger.info(`Health: http://localhost:${config.port}/api/health`);
293
+ logger.info(`Diagnostics: http://localhost:${config.port}/api/diagnostics`);
294
+ resolve();
295
+ });
296
+ });
297
+ };
298
+
299
+ // Stop server
300
+ const stop = async (): Promise<void> => {
301
+ // Shutdown plugins
302
+ for (const plugin of registeredPlugins) {
303
+ if (plugin.onShutdown) {
304
+ await plugin.onShutdown();
305
+ }
306
+ }
307
+
308
+ // Shutdown health manager
309
+ healthManager.shutdown();
310
+
311
+ // Close server
312
+ if (server) {
313
+ return new Promise((resolve) => {
314
+ server!.close(() => {
315
+ logger.info('Control panel stopped');
316
+ resolve();
317
+ });
318
+ });
319
+ }
320
+ };
321
+
322
+ return {
323
+ app,
324
+ start,
325
+ stop,
326
+ registerPlugin,
327
+ getHealthStatus: () => healthManager.getResults(),
328
+ getDiagnostics,
329
+ };
330
+ }
331
+
332
+ /**
333
+ * Generate simple dashboard HTML
334
+ */
335
+ function generateDashboardHtml(
336
+ config: ControlPanelConfig,
337
+ health: Record<string, { status: string; latency?: number; lastChecked: Date }>
338
+ ): string {
339
+ const healthEntries = Object.entries(health);
340
+ const overallStatus = healthEntries.every((e) => e[1].status === 'healthy')
341
+ ? 'healthy'
342
+ : healthEntries.some((e) => e[1].status === 'unhealthy')
343
+ ? 'unhealthy'
344
+ : 'degraded';
345
+
346
+ const statusColor =
347
+ overallStatus === 'healthy' ? '#22c55e' : overallStatus === 'degraded' ? '#f59e0b' : '#ef4444';
348
+
349
+ const linksHtml = (config.links || [])
350
+ .map(
351
+ (link) =>
352
+ `<a href="${link.url}" ${link.external ? 'target="_blank"' : ''} class="link">${link.label}</a>`
353
+ )
354
+ .join('');
355
+
356
+ const healthHtml = healthEntries
357
+ .map(
358
+ ([name, result]) => `
359
+ <div class="health-item">
360
+ <span class="status-dot" style="background-color: ${
361
+ result.status === 'healthy' ? '#22c55e' : result.status === 'degraded' ? '#f59e0b' : '#ef4444'
362
+ }"></span>
363
+ <span class="name">${name}</span>
364
+ <span class="latency">${result.latency ? `${result.latency}ms` : '-'}</span>
365
+ </div>
366
+ `
367
+ )
368
+ .join('');
369
+
370
+ return `<!DOCTYPE html>
371
+ <html lang="en">
372
+ <head>
373
+ <meta charset="UTF-8">
374
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
375
+ <title>${config.productName} - Control Panel</title>
376
+ <style>
377
+ * { margin: 0; padding: 0; box-sizing: border-box; }
378
+ body {
379
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
380
+ background: #0f172a;
381
+ color: #e2e8f0;
382
+ min-height: 100vh;
383
+ padding: 2rem;
384
+ }
385
+ .container { max-width: 1200px; margin: 0 auto; }
386
+ header {
387
+ display: flex;
388
+ justify-content: space-between;
389
+ align-items: center;
390
+ margin-bottom: 2rem;
391
+ padding-bottom: 1rem;
392
+ border-bottom: 1px solid #334155;
393
+ }
394
+ h1 { font-size: 1.5rem; color: ${config.branding?.primaryColor || '#6366f1'}; }
395
+ .status-badge {
396
+ display: inline-flex;
397
+ align-items: center;
398
+ gap: 0.5rem;
399
+ padding: 0.5rem 1rem;
400
+ border-radius: 9999px;
401
+ background: ${statusColor}20;
402
+ color: ${statusColor};
403
+ font-weight: 600;
404
+ }
405
+ .status-dot {
406
+ width: 8px;
407
+ height: 8px;
408
+ border-radius: 50%;
409
+ background: currentColor;
410
+ }
411
+ .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 1.5rem; }
412
+ .card {
413
+ background: #1e293b;
414
+ border-radius: 0.75rem;
415
+ padding: 1.5rem;
416
+ border: 1px solid #334155;
417
+ }
418
+ .card h2 {
419
+ font-size: 0.875rem;
420
+ text-transform: uppercase;
421
+ letter-spacing: 0.05em;
422
+ color: #94a3b8;
423
+ margin-bottom: 1rem;
424
+ }
425
+ .health-item {
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 0.75rem;
429
+ padding: 0.75rem 0;
430
+ border-bottom: 1px solid #334155;
431
+ }
432
+ .health-item:last-child { border-bottom: none; }
433
+ .health-item .name { flex: 1; }
434
+ .health-item .latency { color: #64748b; font-size: 0.875rem; }
435
+ .links { display: flex; flex-wrap: wrap; gap: 0.75rem; }
436
+ .link {
437
+ display: inline-block;
438
+ padding: 0.75rem 1.5rem;
439
+ background: #334155;
440
+ color: #e2e8f0;
441
+ text-decoration: none;
442
+ border-radius: 0.5rem;
443
+ transition: background 0.2s;
444
+ }
445
+ .link:hover { background: #475569; }
446
+ .api-links { margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #334155; }
447
+ .api-links a { color: #6366f1; margin-right: 1.5rem; text-decoration: none; }
448
+ .api-links a:hover { text-decoration: underline; }
449
+ </style>
450
+ </head>
451
+ <body>
452
+ <div class="container">
453
+ <header>
454
+ <h1>${config.productName} Control Panel</h1>
455
+ <div class="status-badge">
456
+ <span class="status-dot"></span>
457
+ ${overallStatus.charAt(0).toUpperCase() + overallStatus.slice(1)}
458
+ </div>
459
+ </header>
460
+
461
+ <div class="cards">
462
+ <div class="card">
463
+ <h2>Health Checks</h2>
464
+ ${healthHtml || '<p style="color: #64748b;">No health checks configured</p>'}
465
+ </div>
466
+
467
+ ${
468
+ (config.links || []).length > 0
469
+ ? `
470
+ <div class="card">
471
+ <h2>Quick Links</h2>
472
+ <div class="links">${linksHtml}</div>
473
+ </div>
474
+ `
475
+ : ''
476
+ }
477
+ </div>
478
+
479
+ <div class="api-links">
480
+ <strong>API:</strong>
481
+ <a href="/api/health">Health</a>
482
+ <a href="/api/info">Info</a>
483
+ <a href="/api/diagnostics">Diagnostics</a>
484
+ </div>
485
+ </div>
486
+
487
+ <script>
488
+ // Auto-refresh health status every 10 seconds
489
+ setTimeout(() => location.reload(), 10000);
490
+ </script>
491
+ </body>
492
+ </html>`;
493
+ }