@qwickapps/server 1.1.9 → 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 +318 -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 +99 -60
  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 +683 -315
  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 +122 -68
  180. package/src/core/gateway.ts +870 -399
  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="${logoUrl}" 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,226 +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
- 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
- }
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;
622
608
  }
623
609
 
624
- // Check status on load and every 30 seconds
625
- checkStatus();
626
- setInterval(checkStatus, 30000);
627
- </script>
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);
724
+ }
725
+
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>
897
+ </body>
898
+ </html>`;
899
+ }
900
+
901
+ /**
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}
628
1015
  </body>
629
1016
  </html>`;
630
1017
  }
631
1018
 
632
1019
  /**
633
- * Create a gateway that proxies to an internal service
1020
+ * Create a gateway that proxies to multiple internal services
634
1021
  *
635
1022
  * @param config - Gateway configuration
636
- * @param serviceFactory - Factory function to create the internal service
637
1023
  * @returns Gateway instance
638
1024
  *
639
1025
  * @example
640
1026
  * ```typescript
641
1027
  * import { createGateway } from '@qwickapps/server';
642
1028
  *
643
- * const gateway = createGateway(
644
- * {
645
- * productName: 'My Service',
646
- * gatewayPort: 3101,
647
- * servicePort: 3100,
1029
+ * const gateway = createGateway({
1030
+ * productName: 'My Product',
1031
+ * port: 3000,
1032
+ * controlPanel: {
1033
+ * path: '/cpanel',
1034
+ * port: 3001,
1035
+ * plugins: [...],
648
1036
  * },
649
- * async (port) => {
650
- * const app = createMyApp();
651
- * const server = app.listen(port);
652
- * return {
653
- * app,
654
- * server,
655
- * shutdown: async () => { server.close(); },
656
- * };
657
- * }
658
- * );
1037
+ * apps: [
1038
+ * { path: '/api', source: { type: 'proxy', target: 'http://localhost:3002' } },
1039
+ * { path: '/docs', source: { type: 'static', directory: './docs' } },
1040
+ * ],
1041
+ * });
659
1042
  *
660
1043
  * await gateway.start();
661
1044
  * ```
662
1045
  */
663
- export function createGateway(
664
- config: GatewayConfig,
665
- serviceFactory: ServiceFactory
666
- ): GatewayInstance {
667
- // Initialize logging subsystem first
668
- const loggingSubsystem = initializeLogging({
1046
+ export function createGateway(config: GatewayConfig): GatewayInstance {
1047
+ // Initialize logging (side effect - subsystem is initialized globally)
1048
+ initializeLogging({
669
1049
  namespace: config.productName,
670
1050
  ...config.logging,
671
1051
  });
672
1052
 
673
- // Use provided logger or get one from the logging subsystem
674
1053
  const logger = config.logger || getControlPanelLogger('Gateway');
675
1054
 
676
- const gatewayPort = config.gatewayPort || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3101', 10);
677
- 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);
678
1057
  const nodeEnv = process.env.NODE_ENV || 'development';
1058
+ const version = config.version || process.env.npm_package_version || '1.0.0';
679
1059
 
680
- // Control panel mount path (defaults to /cpanel)
681
- 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;
682
1065
 
683
- // Guard configuration for control panel
684
- 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'] = [];
685
1071
 
686
- // API paths to proxy
687
- 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;
688
1077
 
689
- // Version for display
690
- const version = config.version || process.env.npm_package_version || '1.0.0';
1078
+ if (source.type !== 'proxy') return;
691
1079
 
692
- let service: GatewayInstance['service'] = null;
693
-
694
- // Create control panel
695
- const controlPanel = createControlPanel({
696
- config: {
697
- productName: config.productName,
698
- port: gatewayPort,
699
- version,
700
- branding: config.branding,
701
- cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
702
- // Skip body parsing for proxied paths
703
- skipBodyParserPaths: [...proxyPaths, '/health'],
704
- // Mount path for control panel
705
- mountPath: controlPanelPath,
706
- // Route guard
707
- guard: guardConfig,
708
- // Custom UI path
709
- customUiPath: config.customUiPath,
710
- links: config.links,
711
- },
712
- plugins: config.plugins || [],
713
- logger,
714
- });
1080
+ const appName = name || path.replace(/^\//, '') || 'Service';
1081
+ logger.debug(`Setting up proxy: ${path} -> ${source.target}`);
715
1082
 
716
- // Setup proxy middleware for API paths
717
- const setupProxyMiddleware = () => {
718
- const target = `http://localhost:${servicePort}`;
719
-
720
- // Proxy each API path
721
- for (const apiPath of proxyPaths) {
722
- const proxyOptions: Options = {
723
- target,
724
- changeOrigin: false,
725
- pathFilter: `${apiPath}/**`,
726
- on: {
727
- error: (err: Error, _req: IncomingMessage, res: ServerResponse | Socket) => {
728
- logger.error('Proxy error', { error: err.message, path: apiPath });
729
- if (res && 'writeHead' in res && !res.headersSent) {
730
- res.writeHead(503, { 'Content-Type': 'application/json' });
731
- res.end(
732
- JSON.stringify({
733
- error: 'Service Unavailable',
734
- message: 'The service is currently unavailable. Please try again later.',
735
- details: nodeEnv === 'development' ? err.message : undefined,
736
- })
737
- );
738
- }
739
- },
740
- },
741
- };
742
- 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
743
1096
  }
744
1097
 
745
- // Proxy /health endpoint to internal service
746
- const healthProxyOptions: Options = {
747
- target,
748
- changeOrigin: false,
749
- pathFilter: '/health',
1098
+ const proxyOptions: Options = {
1099
+ target: source.target,
1100
+ changeOrigin: true,
1101
+ ws: source.ws ?? false,
1102
+ pathRewrite: stripPrefix ? { [`^${path}`]: '' } : undefined,
750
1103
  on: {
751
- 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
+
752
1111
  if (res && 'writeHead' in res && !res.headersSent) {
753
- res.writeHead(503, { 'Content-Type': 'application/json' });
754
- res.end(
755
- JSON.stringify({
756
- status: 'unhealthy',
757
- error: 'Service unavailable',
758
- gateway: 'healthy',
759
- })
760
- );
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
+ }
761
1130
  }
762
1131
  },
763
1132
  },
764
1133
  };
765
- 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 });
766
1150
  };
767
1151
 
768
- // Calculate API base path for landing page
769
- 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;
1157
+
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());
1195
+ });
1196
+ }
1197
+ }
770
1198
 
771
- // Setup frontend app at root path
1199
+ mountedApps.push({ path, type: 'static' });
1200
+ };
1201
+
1202
+ /**
1203
+ * Setup frontend app at root path
1204
+ */
772
1205
  const setupFrontendApp = () => {
773
- // If no frontend app configured, serve default landing page with status
774
- if (!config.frontendApp) {
775
- logger.info('Frontend app: Serving default landing page');
776
- controlPanel.app.get('/', (_req, res) => {
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) => {
777
1212
  const html = generateDefaultLandingPageHtml(
778
1213
  config.productName,
779
- controlPanelPath,
780
- apiBasePath,
781
- version,
782
- config.logoUrl
1214
+ cpPath,
1215
+ logoIconUrl
783
1216
  );
784
1217
  res.type('html').send(html);
785
1218
  });
786
1219
  return;
787
1220
  }
788
1221
 
789
- const { redirectUrl, staticPath, landingPage } = config.frontendApp;
1222
+ const { redirectUrl, staticPath, landingPage } = frontendApp;
790
1223
 
791
1224
  // Priority 1: Redirect
792
1225
  if (redirectUrl) {
793
- logger.info(`Frontend app: Redirecting / to ${redirectUrl}`);
794
- controlPanel.app.get('/', (_req, res) => {
795
- res.redirect(redirectUrl);
796
- });
1226
+ logger.debug(`Frontend: Redirecting / to ${redirectUrl}`);
1227
+ app.get('/', (_req, res) => res.redirect(redirectUrl));
797
1228
  return;
798
1229
  }
799
1230
 
800
- // Priority 2: Serve static files
1231
+ // Priority 2: Static files
801
1232
  if (staticPath && existsSync(staticPath)) {
802
- logger.info(`Frontend app: Serving static files from ${staticPath}`);
803
- controlPanel.app.use('/', express.static(staticPath));
804
-
805
- // SPA fallback for root
806
- controlPanel.app.get('/', (_req, res) => {
1233
+ logger.debug(`Frontend: Serving static from ${staticPath}`);
1234
+ app.use('/', express.static(staticPath));
1235
+ app.get('/', (_req, res) => {
807
1236
  res.sendFile(resolve(staticPath, 'index.html'));
808
1237
  });
809
1238
  return;
@@ -811,74 +1240,116 @@ export function createGateway(
811
1240
 
812
1241
  // Priority 3: Landing page
813
1242
  if (landingPage) {
814
- logger.info(`Frontend app: Serving landing page`);
815
- controlPanel.app.get('/', (_req, res) => {
816
- const html = generateLandingPageHtml(landingPage, controlPanelPath);
1243
+ logger.debug('Frontend: Serving custom landing page');
1244
+ app.get('/', (_req, res) => {
1245
+ const html = generateLandingPageHtml(landingPage, cpPath);
817
1246
  res.type('html').send(html);
818
1247
  });
819
1248
  }
820
1249
  };
821
1250
 
1251
+ /**
1252
+ * Start the gateway
1253
+ */
822
1254
  const start = async (): Promise<void> => {
823
- logger.info('Starting gateway...');
1255
+ logger.debug('Starting gateway...');
824
1256
 
825
- // 1. Start internal service
826
- logger.info(`Starting internal service on port ${servicePort}...`);
827
- service = await serviceFactory(servicePort);
828
- logger.info(`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}...`);
829
1260
 
830
- // 2. Setup proxy middleware (after service is started)
831
- 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
+ });
832
1277
 
833
- // 3. Setup frontend app at root path
834
- setupFrontendApp();
1278
+ await controlPanelInstance.start();
1279
+ logger.debug(`Control panel started on port ${cpPort}`);
1280
+ }
835
1281
 
836
- // 4. Start control panel gateway
837
- await controlPanel.start();
1282
+ // 2. Create HTTP server
1283
+ server = app.listen(gatewayPort);
838
1284
 
839
- // Log startup info
840
- logger.info(`${config.productName} Gateway`);
841
- logger.info(`Gateway Port: ${gatewayPort} (public)`);
842
- logger.info(`Service Port: ${servicePort} (internal)`);
843
-
844
- if (guardConfig && guardConfig.type === 'basic') {
845
- logger.info(`Control Panel Auth: HTTP Basic Auth - Username: ${guardConfig.username}`);
846
- } else if (guardConfig && guardConfig.type !== 'none') {
847
- logger.info(`Control Panel Auth: ${guardConfig.type}`);
848
- } else {
849
- logger.info('Control Panel Auth: None (not recommended)');
1285
+ // 3. Setup mounted apps (proxy and static)
1286
+ const apps = config.apps || [];
1287
+
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);
850
1294
  }
851
1295
 
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`);
856
- for (const apiPath of proxyPaths) {
857
- logger.info(`Service API: * ${apiPath}/*`);
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})`
1313
+ : '(no auth)';
1314
+
1315
+ logger.info(`${config.productName} gateway started on port ${gatewayPort} ${authInfo}`);
1316
+
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
+ }
858
1324
  }
859
1325
  };
860
1326
 
1327
+ /**
1328
+ * Stop the gateway
1329
+ */
861
1330
  const stop = async (): Promise<void> => {
862
- logger.info('Shutting down gateway...');
1331
+ logger.debug('Shutting down gateway...');
863
1332
 
864
1333
  // Stop control panel
865
- await controlPanel.stop();
1334
+ if (controlPanelInstance) {
1335
+ await controlPanelInstance.stop();
1336
+ }
866
1337
 
867
- // Stop internal service
868
- if (service) {
869
- await service.shutdown();
870
- service.server.close();
1338
+ // Stop gateway server
1339
+ if (server) {
1340
+ await new Promise<void>((resolve) => server!.close(() => resolve()));
871
1341
  }
872
1342
 
873
- logger.info('Gateway shutdown complete');
1343
+ logger.debug('Gateway shutdown complete');
874
1344
  };
875
1345
 
876
1346
  return {
877
- controlPanel,
878
- service,
1347
+ app,
1348
+ server,
1349
+ controlPanel: controlPanelInstance,
1350
+ mountedApps,
879
1351
  start,
880
1352
  stop,
881
- gatewayPort,
882
- servicePort,
1353
+ port: gatewayPort,
883
1354
  };
884
1355
  }