@qwickapps/server 1.2.0 → 1.3.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 (240) hide show
  1. package/README.md +238 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +92 -54
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +679 -319
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +114 -61
  180. package/src/core/gateway.ts +863 -403
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -2,83 +2,193 @@
2
2
  * Gateway Server for @qwickapps/server
3
3
  *
4
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
5
+ * 1. Proxies multiple apps mounted at configurable paths
6
+ * 2. Each app runs at `/` on its own internal port
7
+ * 3. Gateway handles path rewriting automatically
8
+ * 4. Provides health and diagnostics endpoints
8
9
  *
9
10
  * Architecture:
10
- * Internet → Gateway (GATEWAY_PORT, public) → Service (SERVICE_PORT, internal)
11
+ * Internet → Gateway (:3000) → [Control Panel (:3001), Admin (:3002), API (:3003), ...]
11
12
  *
12
- * The gateway is always responsive even if the internal service is down,
13
- * allowing diagnostics and error visibility.
13
+ * Port Scheme:
14
+ * - 3000: Gateway (public)
15
+ * - 3001: Control Panel
16
+ * - 3002+: Additional apps
17
+ *
18
+ * Each app is isolated and can be served from any mount path without rebuilding.
14
19
  *
15
20
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
16
21
  */
17
22
 
18
- import type { Application, Request, Response, NextFunction } from 'express';
19
- import type { IncomingMessage, ServerResponse } from 'http';
23
+ import type { Application } from 'express';
24
+ import type { IncomingMessage, ServerResponse, Server } from 'http';
20
25
  import type { Socket } from 'net';
21
- import type { Server } from 'http';
22
- import type { ControlPanelConfig, ControlPanelPlugin, Logger } from './types.js';
26
+ import type { Duplex } from 'stream';
27
+ import type { ControlPanelConfig, Logger } from './types.js';
28
+ import type { Plugin, PluginConfig } from './plugin-registry.js';
23
29
  import { createControlPanel } from './control-panel.js';
24
30
  import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './logging.js';
25
31
  import { createProxyMiddleware, type Options } from 'http-proxy-middleware';
26
32
  import express from 'express';
27
- import { existsSync } from 'fs';
28
- import { resolve } from 'path';
33
+ import { existsSync, readFileSync } from 'fs';
34
+ import { resolve, join, dirname } from 'path';
35
+ import { fileURLToPath } from 'url';
36
+
37
+ // Get QwickApps Server version from package.json
38
+ const __filename = fileURLToPath(import.meta.url);
39
+ const __dirname = dirname(__filename);
40
+ const serverPackageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
41
+ const QWICKAPPS_SERVER_VERSION = serverPackageJson.version || '1.0.0';
42
+
43
+ /**
44
+ * Maintenance mode configuration for a mounted app
45
+ */
46
+ export interface MaintenanceConfig {
47
+ /** Enable maintenance mode - blocks all requests with maintenance page */
48
+ enabled: boolean;
49
+ /** Custom page title (default: "Under Maintenance") */
50
+ title?: string;
51
+ /** Custom message to display */
52
+ message?: string;
53
+ /**
54
+ * Expected time when service will be back.
55
+ * Can be ISO date string, relative time ("2 hours", "30 minutes"), or "soon"
56
+ */
57
+ expectedBackAt?: string;
58
+ /** URL for support/contact (shows "Contact Support" link) */
59
+ contactUrl?: string;
60
+ /** Allow specific paths to bypass maintenance (e.g., ["/api/health", "/api/status"]) */
61
+ bypassPaths?: string[];
62
+ }
63
+
64
+ /**
65
+ * Fallback page configuration when a service is unavailable (proxy error)
66
+ */
67
+ export interface FallbackConfig {
68
+ /** Custom page title (default: "Service Unavailable") */
69
+ title?: string;
70
+ /** Custom message (default: "This service is temporarily unavailable") */
71
+ message?: string;
72
+ /** Show "Try Again" button (default: true) */
73
+ showRetry?: boolean;
74
+ /** Auto-refresh interval in seconds (0 = disabled, default: 30) */
75
+ autoRefresh?: number;
76
+ }
77
+
78
+ /**
79
+ * Configuration for a mounted app
80
+ */
81
+ export interface MountedAppConfig {
82
+ /** Mount path (e.g., '/admin', '/api') */
83
+ path: string;
84
+
85
+ /** Display name for the app (used in status pages) */
86
+ name?: string;
87
+
88
+ /** App source configuration */
89
+ source:
90
+ | {
91
+ /** Proxy to an internal service */
92
+ type: 'proxy';
93
+ /** Target URL (e.g., 'http://localhost:3002') */
94
+ target: string;
95
+ /** Enable WebSocket proxying */
96
+ ws?: boolean;
97
+ }
98
+ | {
99
+ /** Serve static files */
100
+ type: 'static';
101
+ /** Path to static files directory */
102
+ directory: string;
103
+ /** Enable SPA mode (serve index.html for all routes) */
104
+ spa?: boolean;
105
+ };
106
+
107
+ /** Whether to strip the mount path prefix when proxying (default: true) */
108
+ stripPrefix?: boolean;
109
+
110
+ /** Route guard for this app */
111
+ guard?: ControlPanelConfig['guard'];
112
+
113
+ /**
114
+ * Maintenance mode configuration.
115
+ * When enabled, all requests to this app show a maintenance page.
116
+ */
117
+ maintenance?: MaintenanceConfig;
118
+
119
+ /**
120
+ * Fallback page configuration for when the service is unavailable.
121
+ * Shown when proxy encounters connection errors (service down/crashed).
122
+ */
123
+ fallback?: FallbackConfig;
124
+ }
29
125
 
30
126
  /**
31
127
  * Gateway configuration
32
128
  */
33
129
  export interface GatewayConfig {
34
- /** Port for the gateway (public-facing). Defaults to GATEWAY_PORT env or 3101 */
35
- gatewayPort?: number;
130
+ /** Port for the gateway (public-facing). Defaults to GATEWAY_PORT env or 3000 */
131
+ port?: number;
36
132
 
37
- /** Port for the internal service. Defaults to SERVICE_PORT env or 3100 */
38
- servicePort?: number;
39
-
40
- /** Product name for the control panel */
133
+ /** Product name for the gateway */
41
134
  productName: string;
42
135
 
43
136
  /** Product version */
44
137
  version?: string;
45
138
 
46
139
  /**
47
- * URL to the product logo image (SVG, PNG, etc.).
48
- * Used on the default landing page when no frontend app is configured.
140
+ * URL path to the product logo icon (SVG, PNG, etc.).
141
+ * Used on landing pages and passed to the control panel React UI.
142
+ * Example: '/cpanel/logo.svg'
49
143
  */
50
- logoUrl?: string;
144
+ logoIconUrl?: string;
51
145
 
52
- /** Branding configuration */
146
+ /** Branding configuration (primaryColor, favicon) */
53
147
  branding?: ControlPanelConfig['branding'];
54
148
 
55
149
  /** CORS origins */
56
150
  corsOrigins?: string[];
57
151
 
58
- /** Control panel plugins */
59
- plugins?: ControlPanelPlugin[];
60
-
61
- /** Quick links for the control panel */
62
- links?: ControlPanelConfig['links'];
63
-
64
- /** Path to custom React UI dist folder for the control panel */
65
- customUiPath?: string;
66
-
67
152
  /**
68
- * Mount path for the control panel.
69
- * Defaults to '/cpanel'.
153
+ * Mounted apps configuration.
154
+ * Each app runs at `/` on its own port, gateway proxies to it at the configured path.
155
+ *
156
+ * @example
157
+ * ```typescript
158
+ * apps: [
159
+ * { path: '/cpanel', source: { type: 'proxy', target: 'http://localhost:3001' } },
160
+ * { path: '/admin', source: { type: 'proxy', target: 'http://localhost:3002', ws: true } },
161
+ * { path: '/api', source: { type: 'proxy', target: 'http://localhost:3003' } },
162
+ * { path: '/docs', source: { type: 'static', directory: './dist-docs', spa: true } },
163
+ * ]
164
+ * ```
70
165
  */
71
- controlPanelPath?: string;
166
+ apps?: MountedAppConfig[];
72
167
 
73
168
  /**
74
- * Route guard for the control panel.
75
- * Defaults to auto-generated basic auth.
169
+ * Control panel configuration.
170
+ * The control panel is a special built-in app that can be enabled/disabled.
76
171
  */
77
- controlPanelGuard?: ControlPanelConfig['guard'];
172
+ controlPanel?: {
173
+ /** Enable the built-in control panel (default: true) */
174
+ enabled?: boolean;
175
+ /** Mount path for control panel (default: '/cpanel') */
176
+ path?: string;
177
+ /** Port for internal control panel server (default: 3001) */
178
+ port?: number;
179
+ /** Control panel plugins */
180
+ plugins?: Array<{ plugin: Plugin; config?: PluginConfig }>;
181
+ /** Quick links */
182
+ links?: ControlPanelConfig['links'];
183
+ /** Custom UI path */
184
+ customUiPath?: string;
185
+ /** Route guard */
186
+ guard?: ControlPanelConfig['guard'];
187
+ };
78
188
 
79
189
  /**
80
190
  * Frontend app configuration for the root path (/).
81
- * If not provided, root path is not handled by the gateway.
191
+ * If not provided, shows a default landing page with system status.
82
192
  */
83
193
  frontendApp?: {
84
194
  /** Redirect to another URL */
@@ -91,16 +201,12 @@ export interface GatewayConfig {
91
201
  heading?: string;
92
202
  description?: string;
93
203
  links?: Array<{ label: string; url: string }>;
204
+ branding?: {
205
+ primaryColor?: string;
206
+ };
94
207
  };
95
208
  };
96
209
 
97
- /**
98
- * API paths to proxy to the internal service.
99
- * Defaults to ['/api/v1'] if not specified.
100
- * The gateway always proxies /health to the internal service.
101
- */
102
- proxyPaths?: string[];
103
-
104
210
  /** Logger instance */
105
211
  logger?: Logger;
106
212
 
@@ -108,49 +214,37 @@ export interface GatewayConfig {
108
214
  logging?: LoggingConfig;
109
215
  }
110
216
 
111
- /**
112
- * Service factory function type
113
- * Called with the service port, should return an object with:
114
- * - app: Express application (or compatible)
115
- * - server: HTTP server (created by calling listen)
116
- * - shutdown: Async function to gracefully shut down the service
117
- */
118
- export interface ServiceFactory {
119
- (port: number): Promise<{
120
- app: Application;
121
- server: Server;
122
- shutdown: () => Promise<void>;
123
- }>;
124
- }
125
217
 
126
218
  /**
127
219
  * Gateway instance returned by createGateway
128
220
  */
129
221
  export interface GatewayInstance {
130
- /** The control panel instance */
131
- controlPanel: ReturnType<typeof createControlPanel>;
222
+ /** The gateway Express app */
223
+ app: Application;
224
+
225
+ /** HTTP server */
226
+ server: Server | null;
132
227
 
133
- /** The internal service (if started) */
134
- service: {
135
- app: Application;
136
- server: Server;
137
- shutdown: () => Promise<void>;
138
- } | null;
228
+ /** The internal control panel (if enabled) */
229
+ controlPanel: ReturnType<typeof createControlPanel> | null;
139
230
 
140
- /** Start the gateway and internal service */
231
+ /** Mounted apps information */
232
+ mountedApps: Array<{
233
+ path: string;
234
+ type: 'proxy' | 'static';
235
+ target?: string;
236
+ }>;
237
+
238
+ /** Start the gateway */
141
239
  start: () => Promise<void>;
142
240
 
143
241
  /** Stop everything gracefully */
144
242
  stop: () => Promise<void>;
145
243
 
146
244
  /** Gateway port */
147
- gatewayPort: number;
148
-
149
- /** Service port */
150
- servicePort: number;
245
+ port: number;
151
246
  }
152
247
 
153
-
154
248
  /**
155
249
  * Generate landing page HTML for the frontend app
156
250
  */
@@ -160,7 +254,7 @@ function generateLandingPageHtml(
160
254
  ): string {
161
255
  if (!config) return '';
162
256
 
163
- const primaryColor = '#6366f1';
257
+ const primaryColor = config.branding?.primaryColor || '#6366f1';
164
258
 
165
259
  const links = config.links || [
166
260
  { label: 'Control Panel', url: controlPanelPath },
@@ -256,14 +350,11 @@ function generateLandingPageHtml(
256
350
 
257
351
  /**
258
352
  * Generate default landing page HTML when no frontend app is configured
259
- * Shows system status with animated background
260
353
  */
261
354
  function generateDefaultLandingPageHtml(
262
355
  productName: string,
263
356
  controlPanelPath: string,
264
- apiBasePath: string,
265
- version: string,
266
- logoUrl?: string
357
+ logoIconUrl?: string
267
358
  ): string {
268
359
  return `<!DOCTYPE html>
269
360
  <html lang="en">
@@ -297,81 +388,18 @@ function generateDefaultLandingPageHtml(
297
388
  justify-content: center;
298
389
  }
299
390
 
300
- /* Animated gradient background */
301
391
  .bg-gradient {
302
392
  position: fixed;
303
- top: 0;
304
- left: 0;
305
- right: 0;
306
- bottom: 0;
393
+ top: 0; left: 0; right: 0; bottom: 0;
307
394
  background:
308
395
  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%);
396
+ radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
311
397
  animation: gradientShift 15s ease-in-out infinite;
312
398
  }
313
399
 
314
400
  @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;
401
+ 0%, 100% { opacity: 1; }
402
+ 50% { opacity: 0.8; }
375
403
  }
376
404
 
377
405
  .container {
@@ -398,10 +426,6 @@ function generateDefaultLandingPageHtml(
398
426
  box-shadow: 0 20px 40px var(--primary-glow);
399
427
  }
400
428
 
401
- .logo.custom {
402
- filter: drop-shadow(0 20px 40px var(--primary-glow));
403
- }
404
-
405
429
  @keyframes logoFloat {
406
430
  0%, 100% { transform: translateY(0); }
407
431
  50% { transform: translateY(-10px); }
@@ -414,8 +438,8 @@ function generateDefaultLandingPageHtml(
414
438
  }
415
439
 
416
440
  .logo img {
417
- width: 80px;
418
- height: 80px;
441
+ width: 64px;
442
+ height: 64px;
419
443
  object-fit: contain;
420
444
  }
421
445
 
@@ -450,17 +474,6 @@ function generateDefaultLandingPageHtml(
450
474
  animation: pulse 2s ease-in-out infinite;
451
475
  }
452
476
 
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
477
  @keyframes pulse {
465
478
  0%, 100% { opacity: 1; transform: scale(1); }
466
479
  50% { opacity: 0.7; transform: scale(1.1); }
@@ -469,7 +482,6 @@ function generateDefaultLandingPageHtml(
469
482
  .status-text {
470
483
  font-size: 0.95rem;
471
484
  font-weight: 500;
472
- color: var(--text-primary);
473
485
  }
474
486
 
475
487
  .description {
@@ -522,54 +534,28 @@ function generateDefaultLandingPageHtml(
522
534
  text-decoration: none;
523
535
  font-weight: 500;
524
536
  }
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
537
  </style>
537
538
  </head>
538
539
  <body>
539
540
  <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
541
 
553
542
  <div class="container">
554
- ${logoUrl
555
- ? `<div class="logo custom"><img src="/logo.svg" alt="${productName} logo" /></div>`
543
+ ${logoIconUrl
544
+ ? `<div class="logo"><img src="${logoIconUrl}" alt="${productName} logo"></div>`
556
545
  : `<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>`}
546
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
547
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
548
+ </svg>
549
+ </div>`
550
+ }
561
551
 
562
552
  <h1>${productName}</h1>
563
553
 
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>
554
+ <div class="status-badge">
555
+ <div class="status-dot"></div>
556
+ <span class="status-text">Gateway Online</span>
567
557
  </div>
568
558
 
569
- <p class="description" id="description">
570
- Enterprise-grade service powered by QwickApps
571
- </p>
572
-
573
559
  <div class="links">
574
560
  <a href="${controlPanelPath}" class="link">
575
561
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -584,238 +570,669 @@ function generateDefaultLandingPageHtml(
584
570
  </div>
585
571
 
586
572
  <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>
573
+ Enterprise Services Powered by <a href="https://qwickapps.com" target="_blank">QwickApps Server</a> - <a href="https://github.com/qwickapps/server" target="_blank">Version ${QWICKAPPS_SERVER_VERSION}</a>
588
574
  </div>
575
+ </body>
576
+ </html>`;
577
+ }
589
578
 
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
- }
579
+ /**
580
+ * Shared CSS styles for status pages (maintenance, service unavailable)
581
+ */
582
+ const statusPageStyles = `
583
+ * { margin: 0; padding: 0; box-sizing: border-box; }
584
+
585
+ :root {
586
+ --bg-dark: #0a0a0f;
587
+ --bg-card: rgba(255, 255, 255, 0.03);
588
+ --text-primary: #f1f5f9;
589
+ --text-secondary: #94a3b8;
590
+ --border-color: rgba(255, 255, 255, 0.08);
591
+ }
592
+
593
+ body {
594
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
595
+ background: var(--bg-dark);
596
+ color: var(--text-primary);
597
+ min-height: 100vh;
598
+ display: flex;
599
+ align-items: center;
600
+ justify-content: center;
601
+ overflow: hidden;
602
+ }
603
+
604
+ .bg-gradient {
605
+ position: fixed;
606
+ top: 0; left: 0; right: 0; bottom: 0;
607
+ animation: gradientShift 15s ease-in-out infinite;
608
+ }
609
+
610
+ @keyframes gradientShift {
611
+ 0%, 100% { opacity: 1; }
612
+ 50% { opacity: 0.8; }
613
+ }
614
+
615
+ .container {
616
+ position: relative;
617
+ z-index: 10;
618
+ text-align: center;
619
+ max-width: 480px;
620
+ padding: 3rem 2rem;
621
+ }
622
+
623
+ .icon-wrapper {
624
+ width: 100px;
625
+ height: 100px;
626
+ margin: 0 auto 2rem;
627
+ border-radius: 50%;
628
+ display: flex;
629
+ align-items: center;
630
+ justify-content: center;
631
+ animation: iconPulse 3s ease-in-out infinite;
632
+ }
633
+
634
+ @keyframes iconPulse {
635
+ 0%, 100% { transform: scale(1); }
636
+ 50% { transform: scale(1.05); }
637
+ }
638
+
639
+ .icon-wrapper svg {
640
+ width: 48px;
641
+ height: 48px;
642
+ }
643
+
644
+ h1 {
645
+ font-size: 2rem;
646
+ font-weight: 700;
647
+ margin-bottom: 0.75rem;
648
+ }
649
+
650
+ .subtitle {
651
+ color: var(--text-secondary);
652
+ font-size: 1.05rem;
653
+ line-height: 1.6;
654
+ margin-bottom: 2rem;
655
+ }
656
+
657
+ .status-card {
658
+ background: var(--bg-card);
659
+ border: 1px solid var(--border-color);
660
+ border-radius: 16px;
661
+ padding: 1.5rem;
662
+ margin-bottom: 2rem;
663
+ backdrop-filter: blur(10px);
664
+ }
665
+
666
+ .status-row {
667
+ display: flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ gap: 0.75rem;
671
+ color: var(--text-secondary);
672
+ font-size: 0.95rem;
673
+ }
674
+
675
+ .status-row svg {
676
+ width: 20px;
677
+ height: 20px;
678
+ flex-shrink: 0;
679
+ }
680
+
681
+ .eta-badge {
682
+ display: inline-flex;
683
+ align-items: center;
684
+ gap: 0.5rem;
685
+ padding: 0.75rem 1.25rem;
686
+ background: var(--bg-card);
687
+ border: 1px solid var(--border-color);
688
+ border-radius: 100px;
689
+ font-size: 0.9rem;
690
+ color: var(--text-secondary);
691
+ }
692
+
693
+ .actions {
694
+ display: flex;
695
+ flex-wrap: wrap;
696
+ gap: 1rem;
697
+ justify-content: center;
698
+ margin-top: 2rem;
699
+ }
700
+
701
+ .btn {
702
+ display: inline-flex;
703
+ align-items: center;
704
+ gap: 0.5rem;
705
+ padding: 0.875rem 1.5rem;
706
+ border-radius: 12px;
707
+ font-weight: 500;
708
+ font-size: 0.95rem;
709
+ text-decoration: none;
710
+ transition: all 0.3s ease;
711
+ cursor: pointer;
712
+ border: none;
713
+ }
714
+
715
+ .btn-primary {
716
+ background: var(--accent-color);
717
+ color: white;
718
+ box-shadow: 0 4px 15px var(--accent-glow);
719
+ }
720
+
721
+ .btn-primary:hover {
722
+ transform: translateY(-2px);
723
+ box-shadow: 0 8px 25px var(--accent-glow);
623
724
  }
624
725
 
625
- // Check status on load and every 30 seconds
626
- checkStatus();
627
- setInterval(checkStatus, 30000);
628
- </script>
726
+ .btn-secondary {
727
+ background: var(--bg-card);
728
+ border: 1px solid var(--border-color);
729
+ color: var(--text-primary);
730
+ }
731
+
732
+ .btn-secondary:hover {
733
+ background: rgba(255, 255, 255, 0.08);
734
+ }
735
+
736
+ .footer {
737
+ position: fixed;
738
+ bottom: 1.5rem;
739
+ left: 0;
740
+ right: 0;
741
+ text-align: center;
742
+ color: var(--text-secondary);
743
+ font-size: 0.85rem;
744
+ z-index: 10;
745
+ }
746
+
747
+ @media (max-width: 480px) {
748
+ .container { padding: 2rem 1.5rem; }
749
+ h1 { font-size: 1.5rem; }
750
+ .icon-wrapper { width: 80px; height: 80px; }
751
+ .icon-wrapper svg { width: 40px; height: 40px; }
752
+ }
753
+ `;
754
+
755
+ /**
756
+ * Generate a maintenance page HTML
757
+ */
758
+ function generateMaintenancePageHtml(
759
+ appName: string,
760
+ config: MaintenanceConfig,
761
+ productName: string
762
+ ): string {
763
+ const title = config.title || 'Under Maintenance';
764
+ const message = config.message || `${appName} is currently undergoing scheduled maintenance.`;
765
+
766
+ let etaHtml = '';
767
+ if (config.expectedBackAt) {
768
+ const eta = config.expectedBackAt;
769
+ if (eta === 'soon') {
770
+ etaHtml = `
771
+ <div class="eta-badge">
772
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
773
+ <circle cx="12" cy="12" r="10"/>
774
+ <path d="M12 6v6l4 2"/>
775
+ </svg>
776
+ Back online soon
777
+ </div>`;
778
+ } else if (eta.includes('hour') || eta.includes('minute')) {
779
+ etaHtml = `
780
+ <div class="eta-badge">
781
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
782
+ <circle cx="12" cy="12" r="10"/>
783
+ <path d="M12 6v6l4 2"/>
784
+ </svg>
785
+ Expected back in ${eta}
786
+ </div>`;
787
+ } else {
788
+ // ISO date string
789
+ etaHtml = `
790
+ <div class="eta-badge">
791
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
792
+ <circle cx="12" cy="12" r="10"/>
793
+ <path d="M12 6v6l4 2"/>
794
+ </svg>
795
+ <span id="eta-countdown">Calculating...</span>
796
+ </div>
797
+ <script>
798
+ (function() {
799
+ const target = new Date('${eta}');
800
+ const el = document.getElementById('eta-countdown');
801
+ function update() {
802
+ const now = new Date();
803
+ const diff = target - now;
804
+ if (diff <= 0) {
805
+ el.textContent = 'Should be back now';
806
+ setTimeout(() => location.reload(), 5000);
807
+ return;
808
+ }
809
+ const hours = Math.floor(diff / 3600000);
810
+ const mins = Math.floor((diff % 3600000) / 60000);
811
+ if (hours > 0) {
812
+ el.textContent = 'Back in ' + hours + 'h ' + mins + 'm';
813
+ } else {
814
+ el.textContent = 'Back in ' + mins + ' minutes';
815
+ }
816
+ }
817
+ update();
818
+ setInterval(update, 60000);
819
+ })();
820
+ </script>`;
821
+ }
822
+ }
823
+
824
+ const contactHtml = config.contactUrl
825
+ ? `<a href="${config.contactUrl}" class="btn btn-secondary">
826
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
827
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
828
+ <polyline points="22,6 12,13 2,6"/>
829
+ </svg>
830
+ Contact Support
831
+ </a>`
832
+ : '';
833
+
834
+ return `<!DOCTYPE html>
835
+ <html lang="en">
836
+ <head>
837
+ <meta charset="UTF-8">
838
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
839
+ <title>${title} - ${productName}</title>
840
+ <style>
841
+ ${statusPageStyles}
842
+ :root {
843
+ --accent-color: #f59e0b;
844
+ --accent-glow: rgba(245, 158, 11, 0.3);
845
+ }
846
+ .bg-gradient {
847
+ background:
848
+ radial-gradient(ellipse at 30% 30%, rgba(245, 158, 11, 0.12) 0%, transparent 50%),
849
+ radial-gradient(ellipse at 70% 70%, rgba(234, 179, 8, 0.08) 0%, transparent 50%);
850
+ }
851
+ .icon-wrapper {
852
+ background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
853
+ box-shadow: 0 20px 40px rgba(245, 158, 11, 0.3);
854
+ }
855
+ </style>
856
+ </head>
857
+ <body>
858
+ <div class="bg-gradient"></div>
859
+
860
+ <div class="container">
861
+ <div class="icon-wrapper">
862
+ <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
863
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
864
+ </svg>
865
+ </div>
866
+
867
+ <h1>${title}</h1>
868
+ <p class="subtitle">${message}</p>
869
+
870
+ <div class="status-card">
871
+ <div class="status-row">
872
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
873
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
874
+ </svg>
875
+ <span>We're performing upgrades to improve your experience</span>
876
+ </div>
877
+ </div>
878
+
879
+ ${etaHtml}
880
+
881
+ <div class="actions">
882
+ <button onclick="location.reload()" class="btn btn-primary">
883
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
884
+ <path d="M23 4v6h-6"/>
885
+ <path d="M1 20v-6h6"/>
886
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
887
+ </svg>
888
+ Check Again
889
+ </button>
890
+ ${contactHtml}
891
+ </div>
892
+ </div>
893
+
894
+ <div class="footer">
895
+ ${productName}
896
+ </div>
629
897
  </body>
630
898
  </html>`;
631
899
  }
632
900
 
633
901
  /**
634
- * Create a gateway that proxies to an internal service
902
+ * Generate a service unavailable page HTML (when proxy fails)
903
+ */
904
+ function generateServiceUnavailablePageHtml(
905
+ appName: string,
906
+ path: string,
907
+ config: FallbackConfig | undefined,
908
+ productName: string
909
+ ): string {
910
+ const title = config?.title || 'Service Unavailable';
911
+ const message = config?.message || `${appName} is temporarily unavailable. Our team has been notified.`;
912
+ const showRetry = config?.showRetry !== false;
913
+ const autoRefresh = config?.autoRefresh ?? 30;
914
+
915
+ const autoRefreshScript = autoRefresh > 0
916
+ ? `<script>
917
+ let countdown = ${autoRefresh};
918
+ const el = document.getElementById('refresh-countdown');
919
+ setInterval(() => {
920
+ countdown--;
921
+ if (countdown <= 0) location.reload();
922
+ el.textContent = countdown;
923
+ }, 1000);
924
+ </script>`
925
+ : '';
926
+
927
+ const autoRefreshHtml = autoRefresh > 0
928
+ ? `<div class="status-row" style="margin-top: 1rem;">
929
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
930
+ <circle cx="12" cy="12" r="10"/>
931
+ <path d="M12 6v6l4 2"/>
932
+ </svg>
933
+ <span>Auto-refreshing in <strong id="refresh-countdown">${autoRefresh}</strong>s</span>
934
+ </div>`
935
+ : '';
936
+
937
+ const retryButtonHtml = showRetry
938
+ ? `<button onclick="location.reload()" class="btn btn-primary">
939
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
940
+ <path d="M23 4v6h-6"/>
941
+ <path d="M1 20v-6h6"/>
942
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
943
+ </svg>
944
+ Try Again
945
+ </button>`
946
+ : '';
947
+
948
+ return `<!DOCTYPE html>
949
+ <html lang="en">
950
+ <head>
951
+ <meta charset="UTF-8">
952
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
953
+ <title>${title} - ${productName}</title>
954
+ <style>
955
+ ${statusPageStyles}
956
+ :root {
957
+ --accent-color: #ef4444;
958
+ --accent-glow: rgba(239, 68, 68, 0.3);
959
+ }
960
+ .bg-gradient {
961
+ background:
962
+ radial-gradient(ellipse at 30% 30%, rgba(239, 68, 68, 0.1) 0%, transparent 50%),
963
+ radial-gradient(ellipse at 70% 70%, rgba(220, 38, 38, 0.08) 0%, transparent 50%);
964
+ }
965
+ .icon-wrapper {
966
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
967
+ box-shadow: 0 20px 40px rgba(239, 68, 68, 0.3);
968
+ }
969
+ </style>
970
+ </head>
971
+ <body>
972
+ <div class="bg-gradient"></div>
973
+
974
+ <div class="container">
975
+ <div class="icon-wrapper">
976
+ <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
977
+ <circle cx="12" cy="12" r="10"/>
978
+ <line x1="12" y1="8" x2="12" y2="12"/>
979
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
980
+ </svg>
981
+ </div>
982
+
983
+ <h1>${title}</h1>
984
+ <p class="subtitle">${message}</p>
985
+
986
+ <div class="status-card">
987
+ <div class="status-row">
988
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
989
+ <rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
990
+ <rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
991
+ <line x1="6" y1="6" x2="6.01" y2="6"/>
992
+ <line x1="6" y1="18" x2="6.01" y2="18"/>
993
+ </svg>
994
+ <span>The service at <code style="background: rgba(255,255,255,0.1); padding: 0.2rem 0.4rem; border-radius: 4px;">${path}</code> is not responding</span>
995
+ </div>
996
+ ${autoRefreshHtml}
997
+ </div>
998
+
999
+ <div class="actions">
1000
+ ${retryButtonHtml}
1001
+ <a href="/" class="btn btn-secondary">
1002
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
1003
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
1004
+ <polyline points="9,22 9,12 15,12 15,22"/>
1005
+ </svg>
1006
+ Go Home
1007
+ </a>
1008
+ </div>
1009
+ </div>
1010
+
1011
+ <div class="footer">
1012
+ ${productName}
1013
+ </div>
1014
+ ${autoRefreshScript}
1015
+ </body>
1016
+ </html>`;
1017
+ }
1018
+
1019
+ /**
1020
+ * Create a gateway that proxies to multiple internal services
635
1021
  *
636
1022
  * @param config - Gateway configuration
637
- * @param serviceFactory - Factory function to create the internal service
638
1023
  * @returns Gateway instance
639
1024
  *
640
1025
  * @example
641
1026
  * ```typescript
642
1027
  * import { createGateway } from '@qwickapps/server';
643
1028
  *
644
- * const gateway = createGateway(
645
- * {
646
- * productName: 'My Service',
647
- * gatewayPort: 3101,
648
- * servicePort: 3100,
1029
+ * const gateway = createGateway({
1030
+ * productName: 'My Product',
1031
+ * port: 3000,
1032
+ * controlPanel: {
1033
+ * path: '/cpanel',
1034
+ * port: 3001,
1035
+ * plugins: [...],
649
1036
  * },
650
- * async (port) => {
651
- * const app = createMyApp();
652
- * const server = app.listen(port);
653
- * return {
654
- * app,
655
- * server,
656
- * shutdown: async () => { server.close(); },
657
- * };
658
- * }
659
- * );
1037
+ * apps: [
1038
+ * { path: '/api', source: { type: 'proxy', target: 'http://localhost:3002' } },
1039
+ * { path: '/docs', source: { type: 'static', directory: './docs' } },
1040
+ * ],
1041
+ * });
660
1042
  *
661
1043
  * await gateway.start();
662
1044
  * ```
663
1045
  */
664
- export function createGateway(
665
- config: GatewayConfig,
666
- serviceFactory: ServiceFactory
667
- ): GatewayInstance {
668
- // Initialize logging subsystem first
669
- const loggingSubsystem = initializeLogging({
1046
+ export function createGateway(config: GatewayConfig): GatewayInstance {
1047
+ // Initialize logging (side effect - subsystem is initialized globally)
1048
+ initializeLogging({
670
1049
  namespace: config.productName,
671
1050
  ...config.logging,
672
1051
  });
673
1052
 
674
- // Use provided logger or get one from the logging subsystem
675
1053
  const logger = config.logger || getControlPanelLogger('Gateway');
676
1054
 
677
- const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
678
- const servicePort = config.servicePort || parseInt(process.env.SERVICE_PORT || '3100', 10);
1055
+ // Port configuration - new scheme: 3000 gateway, 3001 cpanel, 3002+ apps
1056
+ const gatewayPort = config.port || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3000', 10);
679
1057
  const nodeEnv = process.env.NODE_ENV || 'development';
1058
+ const version = config.version || process.env.npm_package_version || '1.0.0';
680
1059
 
681
- // Control panel mount path (defaults to /cpanel)
682
- const controlPanelPath = config.controlPanelPath || '/cpanel';
1060
+ // Control panel configuration
1061
+ const cpConfig = config.controlPanel ?? { enabled: true };
1062
+ const cpEnabled = cpConfig.enabled !== false;
1063
+ const cpPath = cpConfig.path || '/cpanel';
1064
+ const cpPort = cpConfig.port || 3001;
683
1065
 
684
- // Guard configuration for control panel
685
- const guardConfig = config.controlPanelGuard;
1066
+ // Create gateway Express app
1067
+ const app = express();
1068
+ let server: Server | null = null;
1069
+ let controlPanelInstance: ReturnType<typeof createControlPanel> | null = null;
1070
+ const mountedApps: GatewayInstance['mountedApps'] = [];
686
1071
 
687
- // API paths to proxy
688
- const proxyPaths = config.proxyPaths || ['/api/v1'];
1072
+ /**
1073
+ * Setup proxy middleware for an app
1074
+ */
1075
+ const setupProxyApp = (appConfig: MountedAppConfig, httpServer: Server) => {
1076
+ const { path, source, stripPrefix = true, name, maintenance, fallback } = appConfig;
689
1077
 
690
- // Version for display
691
- const version = config.version || process.env.npm_package_version || '1.0.0';
1078
+ if (source.type !== 'proxy') return;
692
1079
 
693
- let service: GatewayInstance['service'] = null;
694
-
695
- // Create control panel
696
- const controlPanel = createControlPanel({
697
- config: {
698
- productName: config.productName,
699
- port: gatewayPort,
700
- version,
701
- branding: config.branding,
702
- cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
703
- // Skip body parsing for proxied paths
704
- skipBodyParserPaths: [...proxyPaths, '/health'],
705
- // Mount path for control panel
706
- mountPath: controlPanelPath,
707
- // Route guard
708
- guard: guardConfig,
709
- // Custom UI path
710
- customUiPath: config.customUiPath,
711
- links: config.links,
712
- },
713
- plugins: config.plugins || [],
714
- logger,
715
- });
1080
+ const appName = name || path.replace(/^\//, '') || 'Service';
1081
+ logger.debug(`Setting up proxy: ${path} -> ${source.target}`);
716
1082
 
717
- // Setup proxy middleware for API paths
718
- const setupProxyMiddleware = () => {
719
- const target = `http://localhost:${servicePort}`;
720
-
721
- // Proxy each API path
722
- for (const apiPath of proxyPaths) {
723
- const proxyOptions: Options = {
724
- target,
725
- changeOrigin: false,
726
- pathFilter: `${apiPath}/**`,
727
- on: {
728
- error: (err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
729
- logger.error('Proxy error', { error: err.message, path: apiPath });
730
- if (res && 'writeHead' in res && !res.headersSent) {
731
- res.writeHead(503, { 'Content-Type': 'application/json' });
732
- res.end(
733
- JSON.stringify({
734
- error: 'Service Unavailable',
735
- message: 'The service is currently unavailable. Please try again later.',
736
- details: nodeEnv === 'development' ? err.message : undefined,
737
- })
738
- );
739
- }
740
- },
741
- },
742
- };
743
- controlPanel.app.use(createProxyMiddleware(proxyOptions));
1083
+ // Maintenance mode middleware - intercepts all requests when enabled
1084
+ if (maintenance?.enabled) {
1085
+ logger.info(`Maintenance mode enabled for ${path}`);
1086
+ app.use(path, (req, res, next) => {
1087
+ // Check bypass paths
1088
+ if (maintenance.bypassPaths?.some(bp => req.path.startsWith(bp))) {
1089
+ return next();
1090
+ }
1091
+ const html = generateMaintenancePageHtml(appName, maintenance, config.productName);
1092
+ res.status(503).type('html').send(html);
1093
+ });
1094
+ mountedApps.push({ path, type: 'proxy', target: source.target });
1095
+ return; // Don't setup proxy when in maintenance mode
744
1096
  }
745
1097
 
746
- // Proxy /health endpoint to internal service
747
- const healthProxyOptions: Options = {
748
- target,
749
- changeOrigin: false,
750
- pathFilter: '/health',
1098
+ const proxyOptions: Options = {
1099
+ target: source.target,
1100
+ changeOrigin: true,
1101
+ ws: source.ws ?? false,
1102
+ pathRewrite: stripPrefix ? { [`^${path}`]: '' } : undefined,
751
1103
  on: {
752
- error: (_err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
1104
+ proxyReq: (proxyReq) => {
1105
+ // Add X-Forwarded headers so app knows its mounted path
1106
+ proxyReq.setHeader('X-Forwarded-Prefix', path);
1107
+ },
1108
+ error: (err: Error, req: IncomingMessage, res: ServerResponse | Socket) => {
1109
+ logger.error(`Proxy error for ${path}`, { error: err.message });
1110
+
753
1111
  if (res && 'writeHead' in res && !res.headersSent) {
754
- res.writeHead(503, { 'Content-Type': 'application/json' });
755
- res.end(
756
- JSON.stringify({
757
- status: 'unhealthy',
758
- error: 'Service unavailable',
759
- gateway: 'healthy',
760
- })
761
- );
1112
+ // Check if this looks like an API request (Accept: application/json or /api/ path)
1113
+ const acceptHeader = req.headers['accept'] || '';
1114
+ const isApiRequest = acceptHeader.includes('application/json') || req.url?.includes('/api/');
1115
+
1116
+ if (isApiRequest) {
1117
+ // Return JSON error for API requests
1118
+ res.writeHead(503, { 'Content-Type': 'application/json' });
1119
+ res.end(JSON.stringify({
1120
+ error: 'Service Unavailable',
1121
+ message: `The service at ${path} is currently unavailable.`,
1122
+ details: nodeEnv === 'development' ? err.message : undefined,
1123
+ }));
1124
+ } else {
1125
+ // Return beautiful HTML page for browser requests
1126
+ const html = generateServiceUnavailablePageHtml(appName, path, fallback, config.productName);
1127
+ res.writeHead(503, { 'Content-Type': 'text/html' });
1128
+ res.end(html);
1129
+ }
762
1130
  }
763
1131
  },
764
1132
  },
765
1133
  };
766
- controlPanel.app.use(createProxyMiddleware(healthProxyOptions));
1134
+
1135
+ const proxy = createProxyMiddleware(proxyOptions);
1136
+
1137
+ // Mount proxy
1138
+ app.use(path, proxy);
1139
+
1140
+ // WebSocket upgrade handling
1141
+ if (source.ws && httpServer) {
1142
+ httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {
1143
+ if (req.url?.startsWith(path)) {
1144
+ proxy.upgrade?.(req, socket as Socket, head);
1145
+ }
1146
+ });
1147
+ }
1148
+
1149
+ mountedApps.push({ path, type: 'proxy', target: source.target });
767
1150
  };
768
1151
 
769
- // Calculate API base path for landing page
770
- const apiBasePath = controlPanelPath === '/' ? '/api' : `${controlPanelPath}/api`;
1152
+ /**
1153
+ * Setup static file serving for an app
1154
+ */
1155
+ const setupStaticApp = (appConfig: MountedAppConfig) => {
1156
+ const { path, source } = appConfig;
771
1157
 
772
- // Setup frontend app at root path
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);
1158
+ if (source.type !== 'static') return;
1159
+
1160
+ logger.debug(`Setting up static: ${path} -> ${source.directory}`);
1161
+
1162
+ if (!existsSync(source.directory)) {
1163
+ logger.warn(`Static directory not found: ${source.directory}`);
1164
+ return;
1165
+ }
1166
+
1167
+ // Serve static files
1168
+ app.use(path, express.static(source.directory, { index: false }));
1169
+
1170
+ // SPA fallback
1171
+ if (source.spa) {
1172
+ const indexPath = join(source.directory, 'index.html');
1173
+
1174
+ // Read and cache index.html with path rewriting
1175
+ let cachedHtml: string | null = null;
1176
+ const getIndexHtml = (): string => {
1177
+ if (cachedHtml) return cachedHtml;
1178
+ let html = readFileSync(indexPath, 'utf-8');
1179
+ // Rewrite asset paths for non-root mount
1180
+ if (path !== '/') {
1181
+ html = html.replace(/src="\/assets\//g, `src="${path}/assets/`);
1182
+ html = html.replace(/href="\/assets\//g, `href="${path}/assets/`);
1183
+ }
1184
+ cachedHtml = html;
1185
+ return html;
1186
+ };
1187
+
1188
+ app.get(`${path}/*`, (_req, res) => {
1189
+ res.type('html').send(getIndexHtml());
1190
+ });
1191
+
1192
+ if (path !== '/') {
1193
+ app.get(path, (_req, res) => {
1194
+ res.type('html').send(getIndexHtml());
780
1195
  });
781
- logger.debug('Frontend app: Serving logo at /logo.svg');
782
1196
  }
783
1197
  }
784
1198
 
785
- // If no frontend app configured, serve default landing page with status
786
- if (!config.frontendApp) {
787
- logger.debug('Frontend app: Serving default landing page');
788
- controlPanel.app.get('/', (_req, res) => {
1199
+ mountedApps.push({ path, type: 'static' });
1200
+ };
1201
+
1202
+ /**
1203
+ * Setup frontend app at root path
1204
+ */
1205
+ const setupFrontendApp = () => {
1206
+ const { frontendApp, logoIconUrl } = config;
1207
+
1208
+ // Default landing page
1209
+ if (!frontendApp) {
1210
+ logger.debug('Frontend: Serving default landing page');
1211
+ app.get('/', (_req, res) => {
789
1212
  const html = generateDefaultLandingPageHtml(
790
1213
  config.productName,
791
- controlPanelPath,
792
- apiBasePath,
793
- version,
794
- config.logoUrl
1214
+ cpPath,
1215
+ logoIconUrl
795
1216
  );
796
1217
  res.type('html').send(html);
797
1218
  });
798
1219
  return;
799
1220
  }
800
1221
 
801
- const { redirectUrl, staticPath, landingPage } = config.frontendApp;
1222
+ const { redirectUrl, staticPath, landingPage } = frontendApp;
802
1223
 
803
1224
  // Priority 1: Redirect
804
1225
  if (redirectUrl) {
805
- logger.debug(`Frontend app: Redirecting / to ${redirectUrl}`);
806
- controlPanel.app.get('/', (_req, res) => {
807
- res.redirect(redirectUrl);
808
- });
1226
+ logger.debug(`Frontend: Redirecting / to ${redirectUrl}`);
1227
+ app.get('/', (_req, res) => res.redirect(redirectUrl));
809
1228
  return;
810
1229
  }
811
1230
 
812
- // Priority 2: Serve static files
1231
+ // Priority 2: Static files
813
1232
  if (staticPath && existsSync(staticPath)) {
814
- logger.debug(`Frontend app: Serving static files from ${staticPath}`);
815
- controlPanel.app.use('/', express.static(staticPath));
816
-
817
- // SPA fallback for root
818
- controlPanel.app.get('/', (_req, res) => {
1233
+ logger.debug(`Frontend: Serving static from ${staticPath}`);
1234
+ app.use('/', express.static(staticPath));
1235
+ app.get('/', (_req, res) => {
819
1236
  res.sendFile(resolve(staticPath, 'index.html'));
820
1237
  });
821
1238
  return;
@@ -823,73 +1240,116 @@ export function createGateway(
823
1240
 
824
1241
  // Priority 3: Landing page
825
1242
  if (landingPage) {
826
- logger.debug(`Frontend app: Serving landing page`);
827
- controlPanel.app.get('/', (_req, res) => {
828
- const html = generateLandingPageHtml(landingPage, controlPanelPath);
1243
+ logger.debug('Frontend: Serving custom landing page');
1244
+ app.get('/', (_req, res) => {
1245
+ const html = generateLandingPageHtml(landingPage, cpPath);
829
1246
  res.type('html').send(html);
830
1247
  });
831
1248
  }
832
1249
  };
833
1250
 
1251
+ /**
1252
+ * Start the gateway
1253
+ */
834
1254
  const start = async (): Promise<void> => {
835
1255
  logger.debug('Starting gateway...');
836
1256
 
837
- // 1. Start internal service
838
- logger.debug(`Starting internal service on port ${servicePort}...`);
839
- service = await serviceFactory(servicePort);
840
- logger.debug(`Internal service started on port ${servicePort}`);
1257
+ // 1. Start internal control panel if enabled
1258
+ if (cpEnabled) {
1259
+ logger.debug(`Starting control panel on port ${cpPort}...`);
841
1260
 
842
- // 2. Setup proxy middleware (after service is started)
843
- setupProxyMiddleware();
1261
+ controlPanelInstance = createControlPanel({
1262
+ config: {
1263
+ productName: config.productName,
1264
+ port: cpPort,
1265
+ version,
1266
+ logoIconUrl: config.logoIconUrl,
1267
+ branding: config.branding,
1268
+ cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
1269
+ mountPath: '/', // Control panel runs at / internally
1270
+ guard: cpConfig.guard,
1271
+ customUiPath: cpConfig.customUiPath,
1272
+ links: cpConfig.links,
1273
+ },
1274
+ plugins: cpConfig.plugins || [],
1275
+ logger,
1276
+ });
844
1277
 
845
- // 3. Setup frontend app at root path
846
- setupFrontendApp();
1278
+ await controlPanelInstance.start();
1279
+ logger.debug(`Control panel started on port ${cpPort}`);
1280
+ }
1281
+
1282
+ // 2. Create HTTP server
1283
+ server = app.listen(gatewayPort);
847
1284
 
848
- // 4. Start control panel gateway
849
- await controlPanel.start();
1285
+ // 3. Setup mounted apps (proxy and static)
1286
+ const apps = config.apps || [];
850
1287
 
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})`
1288
+ // Add control panel as a proxy app if enabled
1289
+ if (cpEnabled) {
1290
+ setupProxyApp({
1291
+ path: cpPath,
1292
+ source: { type: 'proxy', target: `http://localhost:${cpPort}` },
1293
+ }, server);
1294
+ }
1295
+
1296
+ // Setup additional apps
1297
+ for (const appConfig of apps) {
1298
+ if (appConfig.source.type === 'proxy') {
1299
+ setupProxyApp(appConfig, server);
1300
+ } else {
1301
+ setupStaticApp(appConfig);
1302
+ }
1303
+ }
1304
+
1305
+ // 4. Setup frontend app at root
1306
+ setupFrontendApp();
1307
+
1308
+ // Log startup info
1309
+ const authInfo = cpConfig.guard?.type === 'basic'
1310
+ ? `(auth: ${cpConfig.guard.username})`
1311
+ : cpConfig.guard?.type && cpConfig.guard.type !== 'none'
1312
+ ? `(auth: ${cpConfig.guard.type})`
856
1313
  : '(no auth)';
857
1314
 
858
- logger.info(`${config.productName} started on port ${gatewayPort} ${authInfo}`);
1315
+ logger.info(`${config.productName} gateway started on port ${gatewayPort} ${authInfo}`);
859
1316
 
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`);
867
- for (const apiPath of proxyPaths) {
868
- logger.debug(`Service API: * ${apiPath}/*`);
1317
+ // Log mounted apps
1318
+ for (const mounted of mountedApps) {
1319
+ if (mounted.type === 'proxy') {
1320
+ logger.debug(` ${mounted.path}/* -> ${mounted.target}`);
1321
+ } else {
1322
+ logger.debug(` ${mounted.path}/* -> [static]`);
1323
+ }
869
1324
  }
870
1325
  };
871
1326
 
1327
+ /**
1328
+ * Stop the gateway
1329
+ */
872
1330
  const stop = async (): Promise<void> => {
873
1331
  logger.debug('Shutting down gateway...');
874
1332
 
875
1333
  // Stop control panel
876
- await controlPanel.stop();
1334
+ if (controlPanelInstance) {
1335
+ await controlPanelInstance.stop();
1336
+ }
877
1337
 
878
- // Stop internal service
879
- if (service) {
880
- await service.shutdown();
881
- service.server.close();
1338
+ // Stop gateway server
1339
+ if (server) {
1340
+ await new Promise<void>((resolve) => server!.close(() => resolve()));
882
1341
  }
883
1342
 
884
1343
  logger.debug('Gateway shutdown complete');
885
1344
  };
886
1345
 
887
1346
  return {
888
- controlPanel,
889
- service,
1347
+ app,
1348
+ server,
1349
+ controlPanel: controlPanelInstance,
1350
+ mountedApps,
890
1351
  start,
891
1352
  stop,
892
- gatewayPort,
893
- servicePort,
1353
+ port: gatewayPort,
894
1354
  };
895
1355
  }