@qwickapps/server 1.1.6 → 1.1.9
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.
- package/README.md +1 -1
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +5 -8
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +5 -0
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +390 -28
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/health-manager.d.ts.map +1 -1
- package/dist/core/health-manager.js +3 -9
- package/dist/core/health-manager.js.map +1 -1
- package/dist/core/logging.d.ts.map +1 -1
- package/dist/core/logging.js +2 -6
- package/dist/core/logging.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/cache-plugin.d.ts +219 -0
- package/dist/plugins/cache-plugin.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.js +326 -0
- package/dist/plugins/cache-plugin.js.map +1 -0
- package/dist/plugins/cache-plugin.test.d.ts +8 -0
- package/dist/plugins/cache-plugin.test.d.ts.map +1 -0
- package/dist/plugins/cache-plugin.test.js +188 -0
- package/dist/plugins/cache-plugin.test.js.map +1 -0
- package/dist/plugins/config-plugin.js +1 -1
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +1 -1
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.js +1 -1
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +6 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +1 -3
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +155 -0
- package/dist/plugins/postgres-plugin.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.js +244 -0
- package/dist/plugins/postgres-plugin.js.map +1 -0
- package/dist/plugins/postgres-plugin.test.d.ts +8 -0
- package/dist/plugins/postgres-plugin.test.d.ts.map +1 -0
- package/dist/plugins/postgres-plugin.test.js +165 -0
- package/dist/plugins/postgres-plugin.test.js.map +1 -0
- package/dist-ui/assets/{index-Bk7ypbI4.js → index-CW1BviRn.js} +2 -2
- package/dist-ui/assets/{index-Bk7ypbI4.js.map → index-CW1BviRn.js.map} +1 -1
- package/dist-ui/index.html +1 -1
- package/package.json +13 -2
- package/src/core/control-panel.ts +5 -8
- package/src/core/gateway.ts +412 -30
- package/src/core/health-manager.ts +3 -9
- package/src/core/logging.ts +2 -6
- package/src/index.ts +22 -0
- package/src/plugins/cache-plugin.test.ts +241 -0
- package/src/plugins/cache-plugin.ts +503 -0
- package/src/plugins/config-plugin.ts +1 -1
- package/src/plugins/diagnostics-plugin.ts +1 -1
- package/src/plugins/health-plugin.ts +1 -1
- package/src/plugins/index.ts +10 -0
- package/src/plugins/logs-plugin.ts +1 -3
- package/src/plugins/postgres-plugin.test.ts +213 -0
- package/src/plugins/postgres-plugin.ts +345 -0
- package/ui/src/api/controlPanelApi.ts +1 -1
- package/ui/src/pages/LogsPage.tsx +6 -10
package/dist-ui/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Control Panel</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CW1BviRn.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-CiizQQnb.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qwickapps/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
4
4
|
"description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -55,7 +55,10 @@
|
|
|
55
55
|
"@types/cors": "^2.8.17",
|
|
56
56
|
"@types/express": "^4.17.21",
|
|
57
57
|
"@types/node": "^20.10.5",
|
|
58
|
+
"@types/pg": "^8.11.0",
|
|
58
59
|
"@types/react": "^18.2.0",
|
|
60
|
+
"ioredis": "^5.4.0",
|
|
61
|
+
"pg": "^8.13.0",
|
|
59
62
|
"@types/react-dom": "^18.2.0",
|
|
60
63
|
"@vitejs/plugin-react": "^4.3.4",
|
|
61
64
|
"express-openid-connect": "^2.19.3",
|
|
@@ -68,7 +71,9 @@
|
|
|
68
71
|
},
|
|
69
72
|
"peerDependencies": {
|
|
70
73
|
"@qwickapps/react-framework": ">=1.0.0",
|
|
71
|
-
"express-openid-connect": ">=2.0.0"
|
|
74
|
+
"express-openid-connect": ">=2.0.0",
|
|
75
|
+
"ioredis": ">=5.0.0",
|
|
76
|
+
"pg": ">=8.0.0"
|
|
72
77
|
},
|
|
73
78
|
"peerDependenciesMeta": {
|
|
74
79
|
"@qwickapps/react-framework": {
|
|
@@ -76,6 +81,12 @@
|
|
|
76
81
|
},
|
|
77
82
|
"express-openid-connect": {
|
|
78
83
|
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"ioredis": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"pg": {
|
|
89
|
+
"optional": true
|
|
79
90
|
}
|
|
80
91
|
},
|
|
81
92
|
"keywords": [
|
|
@@ -169,7 +169,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
169
169
|
logger.debug(`Dashboard config: mountPath=${mountPath}, effectiveUiPath=${effectiveUiPath}, hasRichUI=${hasRichUI}, useRichUI=${useRichUI}`);
|
|
170
170
|
|
|
171
171
|
if (useRichUI) {
|
|
172
|
-
logger.
|
|
172
|
+
logger.debug(`Serving React UI from ${effectiveUiPath}`);
|
|
173
173
|
// Serve static assets from dist-ui at the mount path
|
|
174
174
|
app.use(mountPath, express.static(effectiveUiPath));
|
|
175
175
|
|
|
@@ -189,7 +189,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
189
189
|
});
|
|
190
190
|
}
|
|
191
191
|
} else {
|
|
192
|
-
logger.
|
|
192
|
+
logger.debug(`Serving basic HTML dashboard`);
|
|
193
193
|
const dashboardPath = mountPath === '/' ? '/' : mountPath;
|
|
194
194
|
app.get(dashboardPath, (_req: Request, res: Response) => {
|
|
195
195
|
const html = generateDashboardHtml(config, healthManager.getResults(), mountPath);
|
|
@@ -209,7 +209,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
209
209
|
|
|
210
210
|
// Register plugin
|
|
211
211
|
const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
|
|
212
|
-
logger.
|
|
212
|
+
logger.debug(`Registering plugin: ${plugin.name}`);
|
|
213
213
|
|
|
214
214
|
// Register routes
|
|
215
215
|
if (plugin.routes) {
|
|
@@ -238,7 +238,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
registeredPlugins.push(plugin);
|
|
241
|
-
logger.
|
|
241
|
+
logger.debug(`Plugin registered: ${plugin.name}`);
|
|
242
242
|
};
|
|
243
243
|
|
|
244
244
|
// Get diagnostics report
|
|
@@ -276,10 +276,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
|
|
|
276
276
|
|
|
277
277
|
return new Promise((resolve) => {
|
|
278
278
|
server = app.listen(config.port, () => {
|
|
279
|
-
logger.info(`Control panel
|
|
280
|
-
logger.info(`Dashboard: http://localhost:${config.port}${mountPath}`);
|
|
281
|
-
logger.info(`Health: http://localhost:${config.port}${apiBasePath}/health`);
|
|
282
|
-
logger.info(`Diagnostics: http://localhost:${config.port}${apiBasePath}/diagnostics`);
|
|
279
|
+
logger.info(`Control panel listening on port ${config.port}`);
|
|
283
280
|
resolve();
|
|
284
281
|
});
|
|
285
282
|
});
|
package/src/core/gateway.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface GatewayConfig {
|
|
|
43
43
|
/** Product version */
|
|
44
44
|
version?: string;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* URL to the product logo image (SVG, PNG, etc.).
|
|
48
|
+
* Used on the default landing page when no frontend app is configured.
|
|
49
|
+
*/
|
|
50
|
+
logoUrl?: string;
|
|
51
|
+
|
|
46
52
|
/** Branding configuration */
|
|
47
53
|
branding?: ControlPanelConfig['branding'];
|
|
48
54
|
|
|
@@ -248,6 +254,381 @@ function generateLandingPageHtml(
|
|
|
248
254
|
</html>`;
|
|
249
255
|
}
|
|
250
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Generate default landing page HTML when no frontend app is configured
|
|
259
|
+
* Shows system status with animated background
|
|
260
|
+
*/
|
|
261
|
+
function generateDefaultLandingPageHtml(
|
|
262
|
+
productName: string,
|
|
263
|
+
controlPanelPath: string,
|
|
264
|
+
apiBasePath: string,
|
|
265
|
+
version: string,
|
|
266
|
+
logoUrl?: string
|
|
267
|
+
): string {
|
|
268
|
+
return `<!DOCTYPE html>
|
|
269
|
+
<html lang="en">
|
|
270
|
+
<head>
|
|
271
|
+
<meta charset="UTF-8">
|
|
272
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
273
|
+
<title>${productName}</title>
|
|
274
|
+
<style>
|
|
275
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
276
|
+
|
|
277
|
+
:root {
|
|
278
|
+
--primary: #6366f1;
|
|
279
|
+
--primary-glow: rgba(99, 102, 241, 0.4);
|
|
280
|
+
--success: #22c55e;
|
|
281
|
+
--warning: #f59e0b;
|
|
282
|
+
--error: #ef4444;
|
|
283
|
+
--bg-dark: #0a0a0f;
|
|
284
|
+
--bg-card: rgba(255, 255, 255, 0.03);
|
|
285
|
+
--text-primary: #f1f5f9;
|
|
286
|
+
--text-secondary: #94a3b8;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
body {
|
|
290
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
|
291
|
+
background: var(--bg-dark);
|
|
292
|
+
color: var(--text-primary);
|
|
293
|
+
min-height: 100vh;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: center;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Animated gradient background */
|
|
301
|
+
.bg-gradient {
|
|
302
|
+
position: fixed;
|
|
303
|
+
top: 0;
|
|
304
|
+
left: 0;
|
|
305
|
+
right: 0;
|
|
306
|
+
bottom: 0;
|
|
307
|
+
background:
|
|
308
|
+
radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
|
309
|
+
radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
|
|
310
|
+
radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.05) 0%, transparent 70%);
|
|
311
|
+
animation: gradientShift 15s ease-in-out infinite;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@keyframes gradientShift {
|
|
315
|
+
0%, 100% {
|
|
316
|
+
background-position: 0% 0%, 100% 100%, 50% 50%;
|
|
317
|
+
opacity: 1;
|
|
318
|
+
}
|
|
319
|
+
50% {
|
|
320
|
+
background-position: 100% 0%, 0% 100%, 50% 50%;
|
|
321
|
+
opacity: 0.8;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/* Floating particles */
|
|
326
|
+
.particles {
|
|
327
|
+
position: fixed;
|
|
328
|
+
top: 0;
|
|
329
|
+
left: 0;
|
|
330
|
+
right: 0;
|
|
331
|
+
bottom: 0;
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
pointer-events: none;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.particle {
|
|
337
|
+
position: absolute;
|
|
338
|
+
width: 4px;
|
|
339
|
+
height: 4px;
|
|
340
|
+
background: var(--primary);
|
|
341
|
+
border-radius: 50%;
|
|
342
|
+
opacity: 0.3;
|
|
343
|
+
animation: float 20s infinite;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.particle:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
|
347
|
+
.particle:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 20s; }
|
|
348
|
+
.particle:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 28s; }
|
|
349
|
+
.particle:nth-child(4) { left: 40%; animation-delay: 1s; animation-duration: 22s; }
|
|
350
|
+
.particle:nth-child(5) { left: 50%; animation-delay: 3s; animation-duration: 24s; }
|
|
351
|
+
.particle:nth-child(6) { left: 60%; animation-delay: 5s; animation-duration: 26s; }
|
|
352
|
+
.particle:nth-child(7) { left: 70%; animation-delay: 2s; animation-duration: 21s; }
|
|
353
|
+
.particle:nth-child(8) { left: 80%; animation-delay: 4s; animation-duration: 23s; }
|
|
354
|
+
.particle:nth-child(9) { left: 90%; animation-delay: 1s; animation-duration: 27s; }
|
|
355
|
+
|
|
356
|
+
@keyframes float {
|
|
357
|
+
0% { transform: translateY(100vh) scale(0); opacity: 0; }
|
|
358
|
+
10% { opacity: 0.3; }
|
|
359
|
+
90% { opacity: 0.3; }
|
|
360
|
+
100% { transform: translateY(-100vh) scale(1); opacity: 0; }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Grid pattern overlay */
|
|
364
|
+
.grid-overlay {
|
|
365
|
+
position: fixed;
|
|
366
|
+
top: 0;
|
|
367
|
+
left: 0;
|
|
368
|
+
right: 0;
|
|
369
|
+
bottom: 0;
|
|
370
|
+
background-image:
|
|
371
|
+
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
372
|
+
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
373
|
+
background-size: 60px 60px;
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.container {
|
|
378
|
+
position: relative;
|
|
379
|
+
z-index: 10;
|
|
380
|
+
text-align: center;
|
|
381
|
+
max-width: 500px;
|
|
382
|
+
padding: 3rem 2rem;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.logo {
|
|
386
|
+
width: 80px;
|
|
387
|
+
height: 80px;
|
|
388
|
+
margin: 0 auto 2rem;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: center;
|
|
392
|
+
animation: logoFloat 6s ease-in-out infinite;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.logo.default {
|
|
396
|
+
background: linear-gradient(135deg, var(--primary) 0%, #8b5cf6 100%);
|
|
397
|
+
border-radius: 20px;
|
|
398
|
+
box-shadow: 0 20px 40px var(--primary-glow);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.logo.custom {
|
|
402
|
+
filter: drop-shadow(0 20px 40px var(--primary-glow));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@keyframes logoFloat {
|
|
406
|
+
0%, 100% { transform: translateY(0); }
|
|
407
|
+
50% { transform: translateY(-10px); }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.logo svg {
|
|
411
|
+
width: 48px;
|
|
412
|
+
height: 48px;
|
|
413
|
+
fill: white;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.logo img {
|
|
417
|
+
width: 80px;
|
|
418
|
+
height: 80px;
|
|
419
|
+
object-fit: contain;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
h1 {
|
|
423
|
+
font-size: 2.5rem;
|
|
424
|
+
font-weight: 700;
|
|
425
|
+
margin-bottom: 0.5rem;
|
|
426
|
+
background: linear-gradient(135deg, var(--text-primary) 0%, var(--primary) 100%);
|
|
427
|
+
-webkit-background-clip: text;
|
|
428
|
+
-webkit-text-fill-color: transparent;
|
|
429
|
+
background-clip: text;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.status-badge {
|
|
433
|
+
display: inline-flex;
|
|
434
|
+
align-items: center;
|
|
435
|
+
gap: 0.5rem;
|
|
436
|
+
padding: 0.75rem 1.5rem;
|
|
437
|
+
background: var(--bg-card);
|
|
438
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
439
|
+
border-radius: 100px;
|
|
440
|
+
margin: 1.5rem 0 2rem;
|
|
441
|
+
backdrop-filter: blur(10px);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.status-dot {
|
|
445
|
+
width: 10px;
|
|
446
|
+
height: 10px;
|
|
447
|
+
border-radius: 50%;
|
|
448
|
+
background: var(--success);
|
|
449
|
+
box-shadow: 0 0 10px var(--success);
|
|
450
|
+
animation: pulse 2s ease-in-out infinite;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.status-dot.degraded {
|
|
454
|
+
background: var(--warning);
|
|
455
|
+
box-shadow: 0 0 10px var(--warning);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.status-dot.unhealthy {
|
|
459
|
+
background: var(--error);
|
|
460
|
+
box-shadow: 0 0 10px var(--error);
|
|
461
|
+
animation: none;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
@keyframes pulse {
|
|
465
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
466
|
+
50% { opacity: 0.7; transform: scale(1.1); }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.status-text {
|
|
470
|
+
font-size: 0.95rem;
|
|
471
|
+
font-weight: 500;
|
|
472
|
+
color: var(--text-primary);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.description {
|
|
476
|
+
color: var(--text-secondary);
|
|
477
|
+
font-size: 1rem;
|
|
478
|
+
line-height: 1.6;
|
|
479
|
+
margin-bottom: 2rem;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.links {
|
|
483
|
+
display: flex;
|
|
484
|
+
flex-wrap: wrap;
|
|
485
|
+
gap: 1rem;
|
|
486
|
+
justify-content: center;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.link {
|
|
490
|
+
display: inline-flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
gap: 0.5rem;
|
|
493
|
+
padding: 0.875rem 1.75rem;
|
|
494
|
+
background: var(--primary);
|
|
495
|
+
color: white;
|
|
496
|
+
text-decoration: none;
|
|
497
|
+
border-radius: 12px;
|
|
498
|
+
font-weight: 500;
|
|
499
|
+
font-size: 0.95rem;
|
|
500
|
+
transition: all 0.3s ease;
|
|
501
|
+
box-shadow: 0 4px 15px var(--primary-glow);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.link:hover {
|
|
505
|
+
transform: translateY(-2px);
|
|
506
|
+
box-shadow: 0 8px 25px var(--primary-glow);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.footer {
|
|
510
|
+
position: fixed;
|
|
511
|
+
bottom: 1.5rem;
|
|
512
|
+
left: 0;
|
|
513
|
+
right: 0;
|
|
514
|
+
text-align: center;
|
|
515
|
+
color: var(--text-secondary);
|
|
516
|
+
font-size: 0.85rem;
|
|
517
|
+
z-index: 10;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.footer a {
|
|
521
|
+
color: var(--primary);
|
|
522
|
+
text-decoration: none;
|
|
523
|
+
font-weight: 500;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.footer a:hover {
|
|
527
|
+
text-decoration: underline;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/* Loading state */
|
|
531
|
+
.loading .status-dot {
|
|
532
|
+
background: var(--text-secondary);
|
|
533
|
+
box-shadow: none;
|
|
534
|
+
animation: none;
|
|
535
|
+
}
|
|
536
|
+
</style>
|
|
537
|
+
</head>
|
|
538
|
+
<body>
|
|
539
|
+
<div class="bg-gradient"></div>
|
|
540
|
+
<div class="particles">
|
|
541
|
+
<div class="particle"></div>
|
|
542
|
+
<div class="particle"></div>
|
|
543
|
+
<div class="particle"></div>
|
|
544
|
+
<div class="particle"></div>
|
|
545
|
+
<div class="particle"></div>
|
|
546
|
+
<div class="particle"></div>
|
|
547
|
+
<div class="particle"></div>
|
|
548
|
+
<div class="particle"></div>
|
|
549
|
+
<div class="particle"></div>
|
|
550
|
+
</div>
|
|
551
|
+
<div class="grid-overlay"></div>
|
|
552
|
+
|
|
553
|
+
<div class="container">
|
|
554
|
+
${logoUrl
|
|
555
|
+
? `<div class="logo custom"><img src="${logoUrl}" alt="${productName} logo" /></div>`
|
|
556
|
+
: `<div class="logo default">
|
|
557
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
558
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
559
|
+
</svg>
|
|
560
|
+
</div>`}
|
|
561
|
+
|
|
562
|
+
<h1>${productName}</h1>
|
|
563
|
+
|
|
564
|
+
<div class="status-badge loading" id="status-badge">
|
|
565
|
+
<div class="status-dot" id="status-dot"></div>
|
|
566
|
+
<span class="status-text" id="status-text">Checking status...</span>
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<p class="description" id="description">
|
|
570
|
+
Enterprise-grade service powered by QwickApps
|
|
571
|
+
</p>
|
|
572
|
+
|
|
573
|
+
<div class="links">
|
|
574
|
+
<a href="${controlPanelPath}" class="link">
|
|
575
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
576
|
+
<rect x="3" y="3" width="7" height="7"></rect>
|
|
577
|
+
<rect x="14" y="3" width="7" height="7"></rect>
|
|
578
|
+
<rect x="14" y="14" width="7" height="7"></rect>
|
|
579
|
+
<rect x="3" y="14" width="7" height="7"></rect>
|
|
580
|
+
</svg>
|
|
581
|
+
Control Panel
|
|
582
|
+
</a>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div class="footer">
|
|
587
|
+
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps Server</a> - <a href="https://github.com/qwickapps/server" target="_blank">Version ${version}</a>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
<script>
|
|
591
|
+
async function checkStatus() {
|
|
592
|
+
const badge = document.getElementById('status-badge');
|
|
593
|
+
const dot = document.getElementById('status-dot');
|
|
594
|
+
const text = document.getElementById('status-text');
|
|
595
|
+
const desc = document.getElementById('description');
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
const res = await fetch('${apiBasePath}/health');
|
|
599
|
+
const data = await res.json();
|
|
600
|
+
|
|
601
|
+
badge.classList.remove('loading');
|
|
602
|
+
|
|
603
|
+
if (data.status === 'healthy') {
|
|
604
|
+
dot.className = 'status-dot';
|
|
605
|
+
text.textContent = 'All systems operational';
|
|
606
|
+
desc.textContent = 'The service is running smoothly and ready to handle requests.';
|
|
607
|
+
} else if (data.status === 'degraded') {
|
|
608
|
+
dot.className = 'status-dot degraded';
|
|
609
|
+
text.textContent = 'Degraded performance';
|
|
610
|
+
desc.textContent = 'Some services may be experiencing issues. Core functionality remains available.';
|
|
611
|
+
} else {
|
|
612
|
+
dot.className = 'status-dot unhealthy';
|
|
613
|
+
text.textContent = 'System maintenance';
|
|
614
|
+
desc.textContent = 'The service is currently undergoing maintenance. Please check back shortly.';
|
|
615
|
+
}
|
|
616
|
+
} catch (e) {
|
|
617
|
+
badge.classList.remove('loading');
|
|
618
|
+
dot.className = 'status-dot unhealthy';
|
|
619
|
+
text.textContent = 'Unable to connect';
|
|
620
|
+
desc.textContent = 'Could not reach the service. Please try again later.';
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Check status on load and every 30 seconds
|
|
625
|
+
checkStatus();
|
|
626
|
+
setInterval(checkStatus, 30000);
|
|
627
|
+
</script>
|
|
628
|
+
</body>
|
|
629
|
+
</html>`;
|
|
630
|
+
}
|
|
631
|
+
|
|
251
632
|
/**
|
|
252
633
|
* Create a gateway that proxies to an internal service
|
|
253
634
|
*
|
|
@@ -305,6 +686,9 @@ export function createGateway(
|
|
|
305
686
|
// API paths to proxy
|
|
306
687
|
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
307
688
|
|
|
689
|
+
// Version for display
|
|
690
|
+
const version = config.version || process.env.npm_package_version || '1.0.0';
|
|
691
|
+
|
|
308
692
|
let service: GatewayInstance['service'] = null;
|
|
309
693
|
|
|
310
694
|
// Create control panel
|
|
@@ -312,7 +696,7 @@ export function createGateway(
|
|
|
312
696
|
config: {
|
|
313
697
|
productName: config.productName,
|
|
314
698
|
port: gatewayPort,
|
|
315
|
-
version
|
|
699
|
+
version,
|
|
316
700
|
branding: config.branding,
|
|
317
701
|
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
318
702
|
// Skip body parsing for proxied paths
|
|
@@ -381,9 +765,24 @@ export function createGateway(
|
|
|
381
765
|
controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
|
|
382
766
|
};
|
|
383
767
|
|
|
768
|
+
// Calculate API base path for landing page
|
|
769
|
+
const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
|
|
770
|
+
|
|
384
771
|
// Setup frontend app at root path
|
|
385
772
|
const setupFrontendApp = () => {
|
|
773
|
+
// If no frontend app configured, serve default landing page with status
|
|
386
774
|
if (!config.frontendApp) {
|
|
775
|
+
logger.info('Frontend app: Serving default landing page');
|
|
776
|
+
controlPanel.app.get('/', (_req, res) => {
|
|
777
|
+
const html = generateDefaultLandingPageHtml(
|
|
778
|
+
config.productName,
|
|
779
|
+
controlPanelPath,
|
|
780
|
+
apiBasePath,
|
|
781
|
+
version,
|
|
782
|
+
config.logoUrl
|
|
783
|
+
);
|
|
784
|
+
res.type('html').send(html);
|
|
785
|
+
});
|
|
387
786
|
return;
|
|
388
787
|
}
|
|
389
788
|
|
|
@@ -437,43 +836,26 @@ export function createGateway(
|
|
|
437
836
|
// 4. Start control panel gateway
|
|
438
837
|
await controlPanel.start();
|
|
439
838
|
|
|
440
|
-
// Calculate API base path
|
|
441
|
-
const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
|
|
442
|
-
|
|
443
839
|
// Log startup info
|
|
444
|
-
logger.info(
|
|
445
|
-
logger.info(
|
|
446
|
-
logger.info(` ${
|
|
447
|
-
|
|
448
|
-
logger.info('');
|
|
449
|
-
logger.info(` Gateway Port: ${gatewayPort} (public)`);
|
|
450
|
-
logger.info(` Service Port: ${servicePort} (internal)`);
|
|
451
|
-
logger.info('');
|
|
452
|
-
|
|
840
|
+
logger.info(`${config.productName} Gateway`);
|
|
841
|
+
logger.info(`Gateway Port: ${gatewayPort} (public)`);
|
|
842
|
+
logger.info(`Service Port: ${servicePort} (internal)`);
|
|
843
|
+
|
|
453
844
|
if (guardConfig && guardConfig.type === 'basic') {
|
|
454
|
-
logger.info(
|
|
455
|
-
logger.info(' ----------------------------------------');
|
|
456
|
-
logger.info(` Username: ${guardConfig.username}`);
|
|
457
|
-
logger.info(' ----------------------------------------');
|
|
845
|
+
logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
|
|
458
846
|
} else if (guardConfig && guardConfig.type !== 'none') {
|
|
459
|
-
logger.info(`
|
|
847
|
+
logger.info(`Control Panel Auth: ${guardConfig.type}`);
|
|
460
848
|
} else {
|
|
461
|
-
logger.info('
|
|
849
|
+
logger.info('Control Panel Auth: None (not recommended)');
|
|
462
850
|
}
|
|
463
851
|
|
|
464
|
-
logger.info(
|
|
465
|
-
logger.info(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
}
|
|
469
|
-
logger.info(` GET ${controlPanelPath.padEnd(20)} - Control Panel UI`);
|
|
470
|
-
logger.info(` GET ${apiBasePath}/health - Gateway health`);
|
|
471
|
-
logger.info(` GET /health - Service health (proxied)`);
|
|
852
|
+
logger.info(`Frontend App: GET /`);
|
|
853
|
+
logger.info(`Control Panel UI: GET ${controlPanelPath.padEnd(20)}`);
|
|
854
|
+
logger.info(`Gateway Health: GET ${apiBasePath}/health`);
|
|
855
|
+
logger.info(`Service Health: GET /health`);
|
|
472
856
|
for (const apiPath of proxyPaths) {
|
|
473
|
-
logger.info(`
|
|
857
|
+
logger.info(`Service API: * ${apiPath}/*`);
|
|
474
858
|
}
|
|
475
|
-
logger.info('========================================');
|
|
476
|
-
logger.info('');
|
|
477
859
|
};
|
|
478
860
|
|
|
479
861
|
const stop = async (): Promise<void> => {
|
|
@@ -40,10 +40,7 @@ export class HealthManager {
|
|
|
40
40
|
|
|
41
41
|
this.intervals.set(check.name, timer);
|
|
42
42
|
|
|
43
|
-
this.logger.
|
|
44
|
-
type: check.type,
|
|
45
|
-
interval,
|
|
46
|
-
});
|
|
43
|
+
this.logger.debug(`Health check registered: ${check.name} (${check.type}, ${interval}ms)`)
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
/**
|
|
@@ -101,10 +98,7 @@ export class HealthManager {
|
|
|
101
98
|
lastChecked: new Date(),
|
|
102
99
|
});
|
|
103
100
|
|
|
104
|
-
this.logger.warn(`
|
|
105
|
-
error: message,
|
|
106
|
-
latency,
|
|
107
|
-
});
|
|
101
|
+
this.logger.warn(`Health check failed: ${name} - ${message}`);
|
|
108
102
|
}
|
|
109
103
|
}
|
|
110
104
|
|
|
@@ -222,6 +216,6 @@ export class HealthManager {
|
|
|
222
216
|
clearInterval(timer);
|
|
223
217
|
}
|
|
224
218
|
this.intervals.clear();
|
|
225
|
-
this.logger.
|
|
219
|
+
this.logger.debug('Health manager shutdown complete');
|
|
226
220
|
}
|
|
227
221
|
}
|
package/src/core/logging.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface LoggingConfig {
|
|
|
34
34
|
|
|
35
35
|
// Default configuration
|
|
36
36
|
const DEFAULT_CONFIG: Required<LoggingConfig> = {
|
|
37
|
-
namespace: '
|
|
37
|
+
namespace: 'App',
|
|
38
38
|
level: (process.env.LOG_LEVEL as LogLevel) || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
|
|
39
39
|
logDir: process.env.LOG_DIR || './logs',
|
|
40
40
|
fileLogging: process.env.LOG_FILE !== 'false',
|
|
@@ -135,12 +135,8 @@ class LoggingSubsystem {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
this.initialized = true;
|
|
138
|
-
this.rootLogger.
|
|
139
|
-
logDir: this.config.logDir,
|
|
138
|
+
this.rootLogger.debug('Logging initialized', {
|
|
140
139
|
level: this.config.level,
|
|
141
|
-
fileLogging: this.config.fileLogging,
|
|
142
|
-
consoleOutput: this.config.consoleOutput,
|
|
143
|
-
usingPino: this.rootLogger.isUsingPino(),
|
|
144
140
|
});
|
|
145
141
|
}
|
|
146
142
|
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,18 @@ export {
|
|
|
63
63
|
createConfigPlugin,
|
|
64
64
|
createDiagnosticsPlugin,
|
|
65
65
|
createFrontendAppPlugin,
|
|
66
|
+
// Database plugins
|
|
67
|
+
createPostgresPlugin,
|
|
68
|
+
getPostgres,
|
|
69
|
+
hasPostgres,
|
|
70
|
+
// Backward compatibility aliases (deprecated)
|
|
71
|
+
createPostgresPlugin as createDatabasePlugin,
|
|
72
|
+
getPostgres as getDatabase,
|
|
73
|
+
hasPostgres as hasDatabase,
|
|
74
|
+
// Cache plugins
|
|
75
|
+
createCachePlugin,
|
|
76
|
+
getCache,
|
|
77
|
+
hasCache,
|
|
66
78
|
} from './plugins/index.js';
|
|
67
79
|
export type {
|
|
68
80
|
HealthPluginConfig,
|
|
@@ -70,4 +82,14 @@ export type {
|
|
|
70
82
|
ConfigPluginConfig,
|
|
71
83
|
DiagnosticsPluginConfig,
|
|
72
84
|
FrontendAppPluginConfig,
|
|
85
|
+
// Database plugin types
|
|
86
|
+
PostgresPluginConfig,
|
|
87
|
+
PostgresInstance,
|
|
88
|
+
TransactionCallback,
|
|
89
|
+
// Backward compatibility aliases (deprecated)
|
|
90
|
+
PostgresPluginConfig as DatabasePluginConfig,
|
|
91
|
+
PostgresInstance as DatabaseInstance,
|
|
92
|
+
// Cache plugin types
|
|
93
|
+
CachePluginConfig,
|
|
94
|
+
CacheInstance,
|
|
73
95
|
} from './plugins/index.js';
|