@qwickapps/server 1.1.7 → 1.2.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.
@@ -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,382 @@ 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="/logo.svg" 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
+ // Use /health (public, proxied from service) instead of control panel API
599
+ const res = await fetch('/health');
600
+ const data = await res.json();
601
+
602
+ badge.classList.remove('loading');
603
+
604
+ if (data.status === 'healthy') {
605
+ dot.className = 'status-dot';
606
+ text.textContent = 'All systems operational';
607
+ desc.textContent = 'The service is running smoothly and ready to handle requests.';
608
+ } else if (data.status === 'degraded') {
609
+ dot.className = 'status-dot degraded';
610
+ text.textContent = 'Degraded performance';
611
+ desc.textContent = 'Some services may be experiencing issues. Core functionality remains available.';
612
+ } else {
613
+ dot.className = 'status-dot unhealthy';
614
+ text.textContent = 'System maintenance';
615
+ desc.textContent = 'The service is currently undergoing maintenance. Please check back shortly.';
616
+ }
617
+ } catch (e) {
618
+ badge.classList.remove('loading');
619
+ dot.className = 'status-dot unhealthy';
620
+ text.textContent = 'Unable to connect';
621
+ desc.textContent = 'Could not reach the service. Please try again later.';
622
+ }
623
+ }
624
+
625
+ // Check status on load and every 30 seconds
626
+ checkStatus();
627
+ setInterval(checkStatus, 30000);
628
+ </script>
629
+ </body>
630
+ </html>`;
631
+ }
632
+
251
633
  /**
252
634
  * Create a gateway that proxies to an internal service
253
635
  *
@@ -305,6 +687,9 @@ export function createGateway(
305
687
  // API paths to proxy
306
688
  const proxyPaths = config.proxyPaths || ['/api/v1'];
307
689
 
690
+ // Version for display
691
+ const version = config.version || process.env.npm_package_version || '1.0.0';
692
+
308
693
  let service: GatewayInstance['service'] = null;
309
694
 
310
695
  // Create control panel
@@ -312,7 +697,7 @@ export function createGateway(
312
697
  config: {
313
698
  productName: config.productName,
314
699
  port: gatewayPort,
315
- version: config.version || process.env.npm_package_version || '1.0.0',
700
+ version,
316
701
  branding: config.branding,
317
702
  cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
318
703
  // Skip body parsing for proxied paths
@@ -381,9 +766,35 @@ export function createGateway(
381
766
  controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
382
767
  };
383
768
 
769
+ // Calculate API base path for landing page
770
+ const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
771
+
384
772
  // Setup frontend app at root path
385
773
  const setupFrontendApp = () => {
774
+ // Serve logo at /logo.svg if logoUrl is configured and customUiPath exists
775
+ if (config.logoUrl && config.customUiPath) {
776
+ const logoPath = resolve(config.customUiPath, 'logo.svg');
777
+ if (existsSync(logoPath)) {
778
+ controlPanel.app.get('/logo.svg', (_req, res) => {
779
+ res.sendFile(logoPath);
780
+ });
781
+ logger.debug('Frontend app: Serving logo at /logo.svg');
782
+ }
783
+ }
784
+
785
+ // If no frontend app configured, serve default landing page with status
386
786
  if (!config.frontendApp) {
787
+ logger.debug('Frontend app: Serving default landing page');
788
+ controlPanel.app.get('/', (_req, res) => {
789
+ const html = generateDefaultLandingPageHtml(
790
+ config.productName,
791
+ controlPanelPath,
792
+ apiBasePath,
793
+ version,
794
+ config.logoUrl
795
+ );
796
+ res.type('html').send(html);
797
+ });
387
798
  return;
388
799
  }
389
800
 
@@ -391,7 +802,7 @@ export function createGateway(
391
802
 
392
803
  // Priority 1: Redirect
393
804
  if (redirectUrl) {
394
- logger.info(`Frontend app: Redirecting / to ${redirectUrl}`);
805
+ logger.debug(`Frontend app: Redirecting / to ${redirectUrl}`);
395
806
  controlPanel.app.get('/', (_req, res) => {
396
807
  res.redirect(redirectUrl);
397
808
  });
@@ -400,7 +811,7 @@ export function createGateway(
400
811
 
401
812
  // Priority 2: Serve static files
402
813
  if (staticPath && existsSync(staticPath)) {
403
- logger.info(`Frontend app: Serving static files from ${staticPath}`);
814
+ logger.debug(`Frontend app: Serving static files from ${staticPath}`);
404
815
  controlPanel.app.use('/', express.static(staticPath));
405
816
 
406
817
  // SPA fallback for root
@@ -412,7 +823,7 @@ export function createGateway(
412
823
 
413
824
  // Priority 3: Landing page
414
825
  if (landingPage) {
415
- logger.info(`Frontend app: Serving landing page`);
826
+ logger.debug(`Frontend app: Serving landing page`);
416
827
  controlPanel.app.get('/', (_req, res) => {
417
828
  const html = generateLandingPageHtml(landingPage, controlPanelPath);
418
829
  res.type('html').send(html);
@@ -421,12 +832,12 @@ export function createGateway(
421
832
  };
422
833
 
423
834
  const start = async (): Promise<void> => {
424
- logger.info('Starting gateway...');
835
+ logger.debug('Starting gateway...');
425
836
 
426
837
  // 1. Start internal service
427
- logger.info(`Starting internal service on port ${servicePort}...`);
838
+ logger.debug(`Starting internal service on port ${servicePort}...`);
428
839
  service = await serviceFactory(servicePort);
429
- logger.info(`Internal service started on port ${servicePort}`);
840
+ logger.debug(`Internal service started on port ${servicePort}`);
430
841
 
431
842
  // 2. Setup proxy middleware (after service is started)
432
843
  setupProxyMiddleware();
@@ -437,35 +848,29 @@ export function createGateway(
437
848
  // 4. Start control panel gateway
438
849
  await controlPanel.start();
439
850
 
440
- // Calculate API base path
441
- const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
442
-
443
- // Log startup info
444
- logger.info(`${config.productName} Gateway`);
445
- logger.info(`Gateway Port: ${gatewayPort} (public)`);
446
- logger.info(`Service Port: ${servicePort} (internal)`);
447
-
448
- if (guardConfig && guardConfig.type === 'basic') {
449
- logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
450
- } else if (guardConfig && guardConfig.type !== 'none') {
451
- logger.info(`Control Panel Auth: ${guardConfig.type}`);
452
- } else {
453
- logger.info('Control Panel Auth: None (not recommended)');
454
- }
455
-
456
- if (config.frontendApp) {
457
- logger.info(`Frontend App: GET /`);
458
- }
459
- logger.info(`Control Panel UI: GET ${controlPanelPath.padEnd(20)}`);
460
- logger.info(`Gateway Health: GET ${apiBasePath}/health`);
461
- logger.info(`Service Health: GET /health`);
851
+ // Log concise startup info
852
+ const authInfo = guardConfig?.type === 'basic'
853
+ ? `(auth: ${guardConfig.username})`
854
+ : guardConfig?.type && guardConfig.type !== 'none'
855
+ ? `(auth: ${guardConfig.type})`
856
+ : '(no auth)';
857
+
858
+ logger.info(`${config.productName} started on port ${gatewayPort} ${authInfo}`);
859
+
860
+ // Log detailed route info at debug level
861
+ logger.debug(`Gateway Port: ${gatewayPort} (public)`);
862
+ logger.debug(`Service Port: ${servicePort} (internal)`);
863
+ logger.debug(`Frontend App: GET /`);
864
+ logger.debug(`Control Panel UI: GET ${controlPanelPath}`);
865
+ logger.debug(`Gateway Health: GET ${apiBasePath}/health`);
866
+ logger.debug(`Service Health: GET /health`);
462
867
  for (const apiPath of proxyPaths) {
463
- logger.info(`Service API: * ${apiPath}/*`);
868
+ logger.debug(`Service API: * ${apiPath}/*`);
464
869
  }
465
870
  };
466
871
 
467
872
  const stop = async (): Promise<void> => {
468
- logger.info('Shutting down gateway...');
873
+ logger.debug('Shutting down gateway...');
469
874
 
470
875
  // Stop control panel
471
876
  await controlPanel.stop();
@@ -476,7 +881,7 @@ export function createGateway(
476
881
  service.server.close();
477
882
  }
478
883
 
479
- logger.info('Gateway shutdown complete');
884
+ logger.debug('Gateway shutdown complete');
480
885
  };
481
886
 
482
887
  return {
@@ -34,7 +34,7 @@ export interface LoggingConfig {
34
34
 
35
35
  // Default configuration
36
36
  const DEFAULT_CONFIG: Required<LoggingConfig> = {
37
- namespace: 'ControlPanel',
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',