@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.
- package/README.md +318 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +99 -60
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +683 -315
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +271 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +326 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +9 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +9 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +12 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +13 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +148 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +96 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +8 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +47 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +189 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +39 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +242 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +6441 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +18 -6
- package/src/core/control-panel.ts +122 -68
- package/src/core/gateway.ts +870 -399
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +653 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +118 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +9 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/index.ts +33 -0
- package/src/plugins/auth/types.ts +165 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +105 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +106 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +49 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/users/index.ts +35 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +209 -0
- package/src/plugins/users/users-plugin.ts +281 -0
- package/ui/src/App.tsx +185 -31
- package/ui/src/api/controlPanelApi.ts +354 -1
- package/ui/src/components/ControlPanelApp.tsx +209 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
- package/ui/src/dashboard/builtInWidgets.tsx +29 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +51 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- package/ui/src/pages/HealthPage.tsx +0 -204
package/dist/core/gateway.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
* Gateway Server for @qwickapps/server
|
|
3
3
|
*
|
|
4
4
|
* Provides a production-ready gateway pattern that:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
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 (
|
|
11
|
+
* Internet → Gateway (:3000) → [Control Panel (:3001), Admin (:3002), API (:3003), ...]
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
*/
|
|
@@ -18,15 +23,21 @@ import { createControlPanel } from './control-panel.js';
|
|
|
18
23
|
import { initializeLogging, getControlPanelLogger } from './logging.js';
|
|
19
24
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
20
25
|
import express from 'express';
|
|
21
|
-
import { existsSync } from 'fs';
|
|
22
|
-
import { resolve } from 'path';
|
|
26
|
+
import { existsSync, readFileSync } from 'fs';
|
|
27
|
+
import { resolve, join, dirname } from 'path';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
// Get QwickApps Server version from package.json
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = dirname(__filename);
|
|
32
|
+
const serverPackageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
|
|
33
|
+
const QWICKAPPS_SERVER_VERSION = serverPackageJson.version || '1.0.0';
|
|
23
34
|
/**
|
|
24
35
|
* Generate landing page HTML for the frontend app
|
|
25
36
|
*/
|
|
26
37
|
function generateLandingPageHtml(config, controlPanelPath) {
|
|
27
38
|
if (!config)
|
|
28
39
|
return '';
|
|
29
|
-
const primaryColor = '#6366f1';
|
|
40
|
+
const primaryColor = config.branding?.primaryColor || '#6366f1';
|
|
30
41
|
const links = config.links || [
|
|
31
42
|
{ label: 'Control Panel', url: controlPanelPath },
|
|
32
43
|
];
|
|
@@ -115,9 +126,8 @@ function generateLandingPageHtml(config, controlPanelPath) {
|
|
|
115
126
|
}
|
|
116
127
|
/**
|
|
117
128
|
* Generate default landing page HTML when no frontend app is configured
|
|
118
|
-
* Shows system status with animated background
|
|
119
129
|
*/
|
|
120
|
-
function generateDefaultLandingPageHtml(productName, controlPanelPath,
|
|
130
|
+
function generateDefaultLandingPageHtml(productName, controlPanelPath, logoIconUrl) {
|
|
121
131
|
return `<!DOCTYPE html>
|
|
122
132
|
<html lang="en">
|
|
123
133
|
<head>
|
|
@@ -150,81 +160,18 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
150
160
|
justify-content: center;
|
|
151
161
|
}
|
|
152
162
|
|
|
153
|
-
/* Animated gradient background */
|
|
154
163
|
.bg-gradient {
|
|
155
164
|
position: fixed;
|
|
156
|
-
top: 0;
|
|
157
|
-
left: 0;
|
|
158
|
-
right: 0;
|
|
159
|
-
bottom: 0;
|
|
165
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
160
166
|
background:
|
|
161
167
|
radial-gradient(ellipse at 20% 20%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
|
|
162
|
-
radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%)
|
|
163
|
-
radial-gradient(ellipse at 50% 50%, rgba(59, 130, 246, 0.05) 0%, transparent 70%);
|
|
168
|
+
radial-gradient(ellipse at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%);
|
|
164
169
|
animation: gradientShift 15s ease-in-out infinite;
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
@keyframes gradientShift {
|
|
168
|
-
0%, 100% {
|
|
169
|
-
|
|
170
|
-
opacity: 1;
|
|
171
|
-
}
|
|
172
|
-
50% {
|
|
173
|
-
background-position: 100% 0%, 0% 100%, 50% 50%;
|
|
174
|
-
opacity: 0.8;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/* Floating particles */
|
|
179
|
-
.particles {
|
|
180
|
-
position: fixed;
|
|
181
|
-
top: 0;
|
|
182
|
-
left: 0;
|
|
183
|
-
right: 0;
|
|
184
|
-
bottom: 0;
|
|
185
|
-
overflow: hidden;
|
|
186
|
-
pointer-events: none;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.particle {
|
|
190
|
-
position: absolute;
|
|
191
|
-
width: 4px;
|
|
192
|
-
height: 4px;
|
|
193
|
-
background: var(--primary);
|
|
194
|
-
border-radius: 50%;
|
|
195
|
-
opacity: 0.3;
|
|
196
|
-
animation: float 20s infinite;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
.particle:nth-child(1) { left: 10%; animation-delay: 0s; animation-duration: 25s; }
|
|
200
|
-
.particle:nth-child(2) { left: 20%; animation-delay: 2s; animation-duration: 20s; }
|
|
201
|
-
.particle:nth-child(3) { left: 30%; animation-delay: 4s; animation-duration: 28s; }
|
|
202
|
-
.particle:nth-child(4) { left: 40%; animation-delay: 1s; animation-duration: 22s; }
|
|
203
|
-
.particle:nth-child(5) { left: 50%; animation-delay: 3s; animation-duration: 24s; }
|
|
204
|
-
.particle:nth-child(6) { left: 60%; animation-delay: 5s; animation-duration: 26s; }
|
|
205
|
-
.particle:nth-child(7) { left: 70%; animation-delay: 2s; animation-duration: 21s; }
|
|
206
|
-
.particle:nth-child(8) { left: 80%; animation-delay: 4s; animation-duration: 23s; }
|
|
207
|
-
.particle:nth-child(9) { left: 90%; animation-delay: 1s; animation-duration: 27s; }
|
|
208
|
-
|
|
209
|
-
@keyframes float {
|
|
210
|
-
0% { transform: translateY(100vh) scale(0); opacity: 0; }
|
|
211
|
-
10% { opacity: 0.3; }
|
|
212
|
-
90% { opacity: 0.3; }
|
|
213
|
-
100% { transform: translateY(-100vh) scale(1); opacity: 0; }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/* Grid pattern overlay */
|
|
217
|
-
.grid-overlay {
|
|
218
|
-
position: fixed;
|
|
219
|
-
top: 0;
|
|
220
|
-
left: 0;
|
|
221
|
-
right: 0;
|
|
222
|
-
bottom: 0;
|
|
223
|
-
background-image:
|
|
224
|
-
linear-gradient(rgba(99, 102, 241, 0.03) 1px, transparent 1px),
|
|
225
|
-
linear-gradient(90deg, rgba(99, 102, 241, 0.03) 1px, transparent 1px);
|
|
226
|
-
background-size: 60px 60px;
|
|
227
|
-
pointer-events: none;
|
|
173
|
+
0%, 100% { opacity: 1; }
|
|
174
|
+
50% { opacity: 0.8; }
|
|
228
175
|
}
|
|
229
176
|
|
|
230
177
|
.container {
|
|
@@ -251,10 +198,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
251
198
|
box-shadow: 0 20px 40px var(--primary-glow);
|
|
252
199
|
}
|
|
253
200
|
|
|
254
|
-
.logo.custom {
|
|
255
|
-
filter: drop-shadow(0 20px 40px var(--primary-glow));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
201
|
@keyframes logoFloat {
|
|
259
202
|
0%, 100% { transform: translateY(0); }
|
|
260
203
|
50% { transform: translateY(-10px); }
|
|
@@ -267,8 +210,8 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
267
210
|
}
|
|
268
211
|
|
|
269
212
|
.logo img {
|
|
270
|
-
width:
|
|
271
|
-
height:
|
|
213
|
+
width: 64px;
|
|
214
|
+
height: 64px;
|
|
272
215
|
object-fit: contain;
|
|
273
216
|
}
|
|
274
217
|
|
|
@@ -303,17 +246,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
303
246
|
animation: pulse 2s ease-in-out infinite;
|
|
304
247
|
}
|
|
305
248
|
|
|
306
|
-
.status-dot.degraded {
|
|
307
|
-
background: var(--warning);
|
|
308
|
-
box-shadow: 0 0 10px var(--warning);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.status-dot.unhealthy {
|
|
312
|
-
background: var(--error);
|
|
313
|
-
box-shadow: 0 0 10px var(--error);
|
|
314
|
-
animation: none;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
249
|
@keyframes pulse {
|
|
318
250
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
319
251
|
50% { opacity: 0.7; transform: scale(1.1); }
|
|
@@ -322,7 +254,6 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
322
254
|
.status-text {
|
|
323
255
|
font-size: 0.95rem;
|
|
324
256
|
font-weight: 500;
|
|
325
|
-
color: var(--text-primary);
|
|
326
257
|
}
|
|
327
258
|
|
|
328
259
|
.description {
|
|
@@ -375,54 +306,27 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
375
306
|
text-decoration: none;
|
|
376
307
|
font-weight: 500;
|
|
377
308
|
}
|
|
378
|
-
|
|
379
|
-
.footer a:hover {
|
|
380
|
-
text-decoration: underline;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/* Loading state */
|
|
384
|
-
.loading .status-dot {
|
|
385
|
-
background: var(--text-secondary);
|
|
386
|
-
box-shadow: none;
|
|
387
|
-
animation: none;
|
|
388
|
-
}
|
|
389
309
|
</style>
|
|
390
310
|
</head>
|
|
391
311
|
<body>
|
|
392
312
|
<div class="bg-gradient"></div>
|
|
393
|
-
<div class="particles">
|
|
394
|
-
<div class="particle"></div>
|
|
395
|
-
<div class="particle"></div>
|
|
396
|
-
<div class="particle"></div>
|
|
397
|
-
<div class="particle"></div>
|
|
398
|
-
<div class="particle"></div>
|
|
399
|
-
<div class="particle"></div>
|
|
400
|
-
<div class="particle"></div>
|
|
401
|
-
<div class="particle"></div>
|
|
402
|
-
<div class="particle"></div>
|
|
403
|
-
</div>
|
|
404
|
-
<div class="grid-overlay"></div>
|
|
405
313
|
|
|
406
314
|
<div class="container">
|
|
407
|
-
${
|
|
408
|
-
? `<div class="logo
|
|
315
|
+
${logoIconUrl
|
|
316
|
+
? `<div class="logo"><img src="${logoIconUrl}" alt="${productName} logo"></div>`
|
|
409
317
|
: `<div class="logo default">
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
318
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
319
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
320
|
+
</svg>
|
|
321
|
+
</div>`}
|
|
414
322
|
|
|
415
323
|
<h1>${productName}</h1>
|
|
416
324
|
|
|
417
|
-
<div class="status-badge
|
|
418
|
-
<div class="status-dot"
|
|
419
|
-
<span class="status-text"
|
|
325
|
+
<div class="status-badge">
|
|
326
|
+
<div class="status-dot"></div>
|
|
327
|
+
<span class="status-text">Gateway Online</span>
|
|
420
328
|
</div>
|
|
421
329
|
|
|
422
|
-
<p class="description" id="description">
|
|
423
|
-
Enterprise-grade service powered by QwickApps
|
|
424
|
-
</p>
|
|
425
|
-
|
|
426
330
|
<div class="links">
|
|
427
331
|
<a href="${controlPanelPath}" class="link">
|
|
428
332
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
@@ -437,259 +341,723 @@ function generateDefaultLandingPageHtml(productName, controlPanelPath, apiBasePa
|
|
|
437
341
|
</div>
|
|
438
342
|
|
|
439
343
|
<div class="footer">
|
|
440
|
-
Powered by <a href="https://qwickapps.com" target="_blank">QwickApps Server</a> - <a href="https://github.com/qwickapps/server" target="_blank">Version ${
|
|
344
|
+
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>
|
|
441
345
|
</div>
|
|
346
|
+
</body>
|
|
347
|
+
</html>`;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Shared CSS styles for status pages (maintenance, service unavailable)
|
|
351
|
+
*/
|
|
352
|
+
const statusPageStyles = `
|
|
353
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
354
|
+
|
|
355
|
+
:root {
|
|
356
|
+
--bg-dark: #0a0a0f;
|
|
357
|
+
--bg-card: rgba(255, 255, 255, 0.03);
|
|
358
|
+
--text-primary: #f1f5f9;
|
|
359
|
+
--text-secondary: #94a3b8;
|
|
360
|
+
--border-color: rgba(255, 255, 255, 0.08);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
body {
|
|
364
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
|
|
365
|
+
background: var(--bg-dark);
|
|
366
|
+
color: var(--text-primary);
|
|
367
|
+
min-height: 100vh;
|
|
368
|
+
display: flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
overflow: hidden;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.bg-gradient {
|
|
375
|
+
position: fixed;
|
|
376
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
377
|
+
animation: gradientShift 15s ease-in-out infinite;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@keyframes gradientShift {
|
|
381
|
+
0%, 100% { opacity: 1; }
|
|
382
|
+
50% { opacity: 0.8; }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.container {
|
|
386
|
+
position: relative;
|
|
387
|
+
z-index: 10;
|
|
388
|
+
text-align: center;
|
|
389
|
+
max-width: 480px;
|
|
390
|
+
padding: 3rem 2rem;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.icon-wrapper {
|
|
394
|
+
width: 100px;
|
|
395
|
+
height: 100px;
|
|
396
|
+
margin: 0 auto 2rem;
|
|
397
|
+
border-radius: 50%;
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
justify-content: center;
|
|
401
|
+
animation: iconPulse 3s ease-in-out infinite;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
@keyframes iconPulse {
|
|
405
|
+
0%, 100% { transform: scale(1); }
|
|
406
|
+
50% { transform: scale(1.05); }
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.icon-wrapper svg {
|
|
410
|
+
width: 48px;
|
|
411
|
+
height: 48px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
h1 {
|
|
415
|
+
font-size: 2rem;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
margin-bottom: 0.75rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.subtitle {
|
|
421
|
+
color: var(--text-secondary);
|
|
422
|
+
font-size: 1.05rem;
|
|
423
|
+
line-height: 1.6;
|
|
424
|
+
margin-bottom: 2rem;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.status-card {
|
|
428
|
+
background: var(--bg-card);
|
|
429
|
+
border: 1px solid var(--border-color);
|
|
430
|
+
border-radius: 16px;
|
|
431
|
+
padding: 1.5rem;
|
|
432
|
+
margin-bottom: 2rem;
|
|
433
|
+
backdrop-filter: blur(10px);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.status-row {
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
justify-content: center;
|
|
440
|
+
gap: 0.75rem;
|
|
441
|
+
color: var(--text-secondary);
|
|
442
|
+
font-size: 0.95rem;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.status-row svg {
|
|
446
|
+
width: 20px;
|
|
447
|
+
height: 20px;
|
|
448
|
+
flex-shrink: 0;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.eta-badge {
|
|
452
|
+
display: inline-flex;
|
|
453
|
+
align-items: center;
|
|
454
|
+
gap: 0.5rem;
|
|
455
|
+
padding: 0.75rem 1.25rem;
|
|
456
|
+
background: var(--bg-card);
|
|
457
|
+
border: 1px solid var(--border-color);
|
|
458
|
+
border-radius: 100px;
|
|
459
|
+
font-size: 0.9rem;
|
|
460
|
+
color: var(--text-secondary);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.actions {
|
|
464
|
+
display: flex;
|
|
465
|
+
flex-wrap: wrap;
|
|
466
|
+
gap: 1rem;
|
|
467
|
+
justify-content: center;
|
|
468
|
+
margin-top: 2rem;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.btn {
|
|
472
|
+
display: inline-flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 0.5rem;
|
|
475
|
+
padding: 0.875rem 1.5rem;
|
|
476
|
+
border-radius: 12px;
|
|
477
|
+
font-weight: 500;
|
|
478
|
+
font-size: 0.95rem;
|
|
479
|
+
text-decoration: none;
|
|
480
|
+
transition: all 0.3s ease;
|
|
481
|
+
cursor: pointer;
|
|
482
|
+
border: none;
|
|
483
|
+
}
|
|
442
484
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
485
|
+
.btn-primary {
|
|
486
|
+
background: var(--accent-color);
|
|
487
|
+
color: white;
|
|
488
|
+
box-shadow: 0 4px 15px var(--accent-glow);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.btn-primary:hover {
|
|
492
|
+
transform: translateY(-2px);
|
|
493
|
+
box-shadow: 0 8px 25px var(--accent-glow);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.btn-secondary {
|
|
497
|
+
background: var(--bg-card);
|
|
498
|
+
border: 1px solid var(--border-color);
|
|
499
|
+
color: var(--text-primary);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.btn-secondary:hover {
|
|
503
|
+
background: rgba(255, 255, 255, 0.08);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.footer {
|
|
507
|
+
position: fixed;
|
|
508
|
+
bottom: 1.5rem;
|
|
509
|
+
left: 0;
|
|
510
|
+
right: 0;
|
|
511
|
+
text-align: center;
|
|
512
|
+
color: var(--text-secondary);
|
|
513
|
+
font-size: 0.85rem;
|
|
514
|
+
z-index: 10;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
@media (max-width: 480px) {
|
|
518
|
+
.container { padding: 2rem 1.5rem; }
|
|
519
|
+
h1 { font-size: 1.5rem; }
|
|
520
|
+
.icon-wrapper { width: 80px; height: 80px; }
|
|
521
|
+
.icon-wrapper svg { width: 40px; height: 40px; }
|
|
522
|
+
}
|
|
523
|
+
`;
|
|
524
|
+
/**
|
|
525
|
+
* Generate a maintenance page HTML
|
|
526
|
+
*/
|
|
527
|
+
function generateMaintenancePageHtml(appName, config, productName) {
|
|
528
|
+
const title = config.title || 'Under Maintenance';
|
|
529
|
+
const message = config.message || `${appName} is currently undergoing scheduled maintenance.`;
|
|
530
|
+
let etaHtml = '';
|
|
531
|
+
if (config.expectedBackAt) {
|
|
532
|
+
const eta = config.expectedBackAt;
|
|
533
|
+
if (eta === 'soon') {
|
|
534
|
+
etaHtml = `
|
|
535
|
+
<div class="eta-badge">
|
|
536
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
537
|
+
<circle cx="12" cy="12" r="10"/>
|
|
538
|
+
<path d="M12 6v6l4 2"/>
|
|
539
|
+
</svg>
|
|
540
|
+
Back online soon
|
|
541
|
+
</div>`;
|
|
468
542
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
543
|
+
else if (eta.includes('hour') || eta.includes('minute')) {
|
|
544
|
+
etaHtml = `
|
|
545
|
+
<div class="eta-badge">
|
|
546
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
547
|
+
<circle cx="12" cy="12" r="10"/>
|
|
548
|
+
<path d="M12 6v6l4 2"/>
|
|
549
|
+
</svg>
|
|
550
|
+
Expected back in ${eta}
|
|
551
|
+
</div>`;
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
// ISO date string
|
|
555
|
+
etaHtml = `
|
|
556
|
+
<div class="eta-badge">
|
|
557
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
558
|
+
<circle cx="12" cy="12" r="10"/>
|
|
559
|
+
<path d="M12 6v6l4 2"/>
|
|
560
|
+
</svg>
|
|
561
|
+
<span id="eta-countdown">Calculating...</span>
|
|
562
|
+
</div>
|
|
563
|
+
<script>
|
|
564
|
+
(function() {
|
|
565
|
+
const target = new Date('${eta}');
|
|
566
|
+
const el = document.getElementById('eta-countdown');
|
|
567
|
+
function update() {
|
|
568
|
+
const now = new Date();
|
|
569
|
+
const diff = target - now;
|
|
570
|
+
if (diff <= 0) {
|
|
571
|
+
el.textContent = 'Should be back now';
|
|
572
|
+
setTimeout(() => location.reload(), 5000);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
const hours = Math.floor(diff / 3600000);
|
|
576
|
+
const mins = Math.floor((diff % 3600000) / 60000);
|
|
577
|
+
if (hours > 0) {
|
|
578
|
+
el.textContent = 'Back in ' + hours + 'h ' + mins + 'm';
|
|
579
|
+
} else {
|
|
580
|
+
el.textContent = 'Back in ' + mins + ' minutes';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
update();
|
|
584
|
+
setInterval(update, 60000);
|
|
585
|
+
})();
|
|
586
|
+
</script>`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
const contactHtml = config.contactUrl
|
|
590
|
+
? `<a href="${config.contactUrl}" class="btn btn-secondary">
|
|
591
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
592
|
+
<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"/>
|
|
593
|
+
<polyline points="22,6 12,13 2,6"/>
|
|
594
|
+
</svg>
|
|
595
|
+
Contact Support
|
|
596
|
+
</a>`
|
|
597
|
+
: '';
|
|
598
|
+
return `<!DOCTYPE html>
|
|
599
|
+
<html lang="en">
|
|
600
|
+
<head>
|
|
601
|
+
<meta charset="UTF-8">
|
|
602
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
603
|
+
<title>${title} - ${productName}</title>
|
|
604
|
+
<style>
|
|
605
|
+
${statusPageStyles}
|
|
606
|
+
:root {
|
|
607
|
+
--accent-color: #f59e0b;
|
|
608
|
+
--accent-glow: rgba(245, 158, 11, 0.3);
|
|
609
|
+
}
|
|
610
|
+
.bg-gradient {
|
|
611
|
+
background:
|
|
612
|
+
radial-gradient(ellipse at 30% 30%, rgba(245, 158, 11, 0.12) 0%, transparent 50%),
|
|
613
|
+
radial-gradient(ellipse at 70% 70%, rgba(234, 179, 8, 0.08) 0%, transparent 50%);
|
|
614
|
+
}
|
|
615
|
+
.icon-wrapper {
|
|
616
|
+
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
|
617
|
+
box-shadow: 0 20px 40px rgba(245, 158, 11, 0.3);
|
|
618
|
+
}
|
|
619
|
+
</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="bg-gradient"></div>
|
|
623
|
+
|
|
624
|
+
<div class="container">
|
|
625
|
+
<div class="icon-wrapper">
|
|
626
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
|
627
|
+
<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"/>
|
|
628
|
+
</svg>
|
|
629
|
+
</div>
|
|
630
|
+
|
|
631
|
+
<h1>${title}</h1>
|
|
632
|
+
<p class="subtitle">${message}</p>
|
|
633
|
+
|
|
634
|
+
<div class="status-card">
|
|
635
|
+
<div class="status-row">
|
|
636
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
637
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
|
638
|
+
</svg>
|
|
639
|
+
<span>We're performing upgrades to improve your experience</span>
|
|
640
|
+
</div>
|
|
641
|
+
</div>
|
|
642
|
+
|
|
643
|
+
${etaHtml}
|
|
644
|
+
|
|
645
|
+
<div class="actions">
|
|
646
|
+
<button onclick="location.reload()" class="btn btn-primary">
|
|
647
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
648
|
+
<path d="M23 4v6h-6"/>
|
|
649
|
+
<path d="M1 20v-6h6"/>
|
|
650
|
+
<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"/>
|
|
651
|
+
</svg>
|
|
652
|
+
Check Again
|
|
653
|
+
</button>
|
|
654
|
+
${contactHtml}
|
|
655
|
+
</div>
|
|
656
|
+
</div>
|
|
657
|
+
|
|
658
|
+
<div class="footer">
|
|
659
|
+
${productName}
|
|
660
|
+
</div>
|
|
481
661
|
</body>
|
|
482
662
|
</html>`;
|
|
483
663
|
}
|
|
484
664
|
/**
|
|
485
|
-
*
|
|
665
|
+
* Generate a service unavailable page HTML (when proxy fails)
|
|
666
|
+
*/
|
|
667
|
+
function generateServiceUnavailablePageHtml(appName, path, config, productName) {
|
|
668
|
+
const title = config?.title || 'Service Unavailable';
|
|
669
|
+
const message = config?.message || `${appName} is temporarily unavailable. Our team has been notified.`;
|
|
670
|
+
const showRetry = config?.showRetry !== false;
|
|
671
|
+
const autoRefresh = config?.autoRefresh ?? 30;
|
|
672
|
+
const autoRefreshScript = autoRefresh > 0
|
|
673
|
+
? `<script>
|
|
674
|
+
let countdown = ${autoRefresh};
|
|
675
|
+
const el = document.getElementById('refresh-countdown');
|
|
676
|
+
setInterval(() => {
|
|
677
|
+
countdown--;
|
|
678
|
+
if (countdown <= 0) location.reload();
|
|
679
|
+
el.textContent = countdown;
|
|
680
|
+
}, 1000);
|
|
681
|
+
</script>`
|
|
682
|
+
: '';
|
|
683
|
+
const autoRefreshHtml = autoRefresh > 0
|
|
684
|
+
? `<div class="status-row" style="margin-top: 1rem;">
|
|
685
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
686
|
+
<circle cx="12" cy="12" r="10"/>
|
|
687
|
+
<path d="M12 6v6l4 2"/>
|
|
688
|
+
</svg>
|
|
689
|
+
<span>Auto-refreshing in <strong id="refresh-countdown">${autoRefresh}</strong>s</span>
|
|
690
|
+
</div>`
|
|
691
|
+
: '';
|
|
692
|
+
const retryButtonHtml = showRetry
|
|
693
|
+
? `<button onclick="location.reload()" class="btn btn-primary">
|
|
694
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
695
|
+
<path d="M23 4v6h-6"/>
|
|
696
|
+
<path d="M1 20v-6h6"/>
|
|
697
|
+
<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"/>
|
|
698
|
+
</svg>
|
|
699
|
+
Try Again
|
|
700
|
+
</button>`
|
|
701
|
+
: '';
|
|
702
|
+
return `<!DOCTYPE html>
|
|
703
|
+
<html lang="en">
|
|
704
|
+
<head>
|
|
705
|
+
<meta charset="UTF-8">
|
|
706
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
707
|
+
<title>${title} - ${productName}</title>
|
|
708
|
+
<style>
|
|
709
|
+
${statusPageStyles}
|
|
710
|
+
:root {
|
|
711
|
+
--accent-color: #ef4444;
|
|
712
|
+
--accent-glow: rgba(239, 68, 68, 0.3);
|
|
713
|
+
}
|
|
714
|
+
.bg-gradient {
|
|
715
|
+
background:
|
|
716
|
+
radial-gradient(ellipse at 30% 30%, rgba(239, 68, 68, 0.1) 0%, transparent 50%),
|
|
717
|
+
radial-gradient(ellipse at 70% 70%, rgba(220, 38, 38, 0.08) 0%, transparent 50%);
|
|
718
|
+
}
|
|
719
|
+
.icon-wrapper {
|
|
720
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
721
|
+
box-shadow: 0 20px 40px rgba(239, 68, 68, 0.3);
|
|
722
|
+
}
|
|
723
|
+
</style>
|
|
724
|
+
</head>
|
|
725
|
+
<body>
|
|
726
|
+
<div class="bg-gradient"></div>
|
|
727
|
+
|
|
728
|
+
<div class="container">
|
|
729
|
+
<div class="icon-wrapper">
|
|
730
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
|
|
731
|
+
<circle cx="12" cy="12" r="10"/>
|
|
732
|
+
<line x1="12" y1="8" x2="12" y2="12"/>
|
|
733
|
+
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
734
|
+
</svg>
|
|
735
|
+
</div>
|
|
736
|
+
|
|
737
|
+
<h1>${title}</h1>
|
|
738
|
+
<p class="subtitle">${message}</p>
|
|
739
|
+
|
|
740
|
+
<div class="status-card">
|
|
741
|
+
<div class="status-row">
|
|
742
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
743
|
+
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
|
744
|
+
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
|
745
|
+
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
|
746
|
+
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
|
747
|
+
</svg>
|
|
748
|
+
<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>
|
|
749
|
+
</div>
|
|
750
|
+
${autoRefreshHtml}
|
|
751
|
+
</div>
|
|
752
|
+
|
|
753
|
+
<div class="actions">
|
|
754
|
+
${retryButtonHtml}
|
|
755
|
+
<a href="/" class="btn btn-secondary">
|
|
756
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
|
757
|
+
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
|
758
|
+
<polyline points="9,22 9,12 15,12 15,22"/>
|
|
759
|
+
</svg>
|
|
760
|
+
Go Home
|
|
761
|
+
</a>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
|
|
765
|
+
<div class="footer">
|
|
766
|
+
${productName}
|
|
767
|
+
</div>
|
|
768
|
+
${autoRefreshScript}
|
|
769
|
+
</body>
|
|
770
|
+
</html>`;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Create a gateway that proxies to multiple internal services
|
|
486
774
|
*
|
|
487
775
|
* @param config - Gateway configuration
|
|
488
|
-
* @param serviceFactory - Factory function to create the internal service
|
|
489
776
|
* @returns Gateway instance
|
|
490
777
|
*
|
|
491
778
|
* @example
|
|
492
779
|
* ```typescript
|
|
493
780
|
* import { createGateway } from '@qwickapps/server';
|
|
494
781
|
*
|
|
495
|
-
* const gateway = createGateway(
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
782
|
+
* const gateway = createGateway({
|
|
783
|
+
* productName: 'My Product',
|
|
784
|
+
* port: 3000,
|
|
785
|
+
* controlPanel: {
|
|
786
|
+
* path: '/cpanel',
|
|
787
|
+
* port: 3001,
|
|
788
|
+
* plugins: [...],
|
|
500
789
|
* },
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
506
|
-
* server,
|
|
507
|
-
* shutdown: async () => { server.close(); },
|
|
508
|
-
* };
|
|
509
|
-
* }
|
|
510
|
-
* );
|
|
790
|
+
* apps: [
|
|
791
|
+
* { path: '/api', source: { type: 'proxy', target: 'http://localhost:3002' } },
|
|
792
|
+
* { path: '/docs', source: { type: 'static', directory: './docs' } },
|
|
793
|
+
* ],
|
|
794
|
+
* });
|
|
511
795
|
*
|
|
512
796
|
* await gateway.start();
|
|
513
797
|
* ```
|
|
514
798
|
*/
|
|
515
|
-
export function createGateway(config
|
|
516
|
-
// Initialize logging subsystem
|
|
517
|
-
|
|
799
|
+
export function createGateway(config) {
|
|
800
|
+
// Initialize logging (side effect - subsystem is initialized globally)
|
|
801
|
+
initializeLogging({
|
|
518
802
|
namespace: config.productName,
|
|
519
803
|
...config.logging,
|
|
520
804
|
});
|
|
521
|
-
// Use provided logger or get one from the logging subsystem
|
|
522
805
|
const logger = config.logger || getControlPanelLogger('Gateway');
|
|
523
|
-
|
|
524
|
-
const
|
|
806
|
+
// Port configuration - new scheme: 3000 gateway, 3001 cpanel, 3002+ apps
|
|
807
|
+
const gatewayPort = config.port || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3000', 10);
|
|
525
808
|
const nodeEnv = process.env.NODE_ENV || 'development';
|
|
526
|
-
// Control panel mount path (defaults to /cpanel)
|
|
527
|
-
const controlPanelPath = config.controlPanelPath || '/cpanel';
|
|
528
|
-
// Guard configuration for control panel
|
|
529
|
-
const guardConfig = config.controlPanelGuard;
|
|
530
|
-
// API paths to proxy
|
|
531
|
-
const proxyPaths = config.proxyPaths || ['/api/v1'];
|
|
532
|
-
// Version for display
|
|
533
809
|
const version = config.version || process.env.npm_package_version || '1.0.0';
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
810
|
+
// Control panel configuration
|
|
811
|
+
const cpConfig = config.controlPanel ?? { enabled: true };
|
|
812
|
+
const cpEnabled = cpConfig.enabled !== false;
|
|
813
|
+
const cpPath = cpConfig.path || '/cpanel';
|
|
814
|
+
const cpPort = cpConfig.port || 3001;
|
|
815
|
+
// Create gateway Express app
|
|
816
|
+
const app = express();
|
|
817
|
+
let server = null;
|
|
818
|
+
let controlPanelInstance = null;
|
|
819
|
+
const mountedApps = [];
|
|
820
|
+
/**
|
|
821
|
+
* Setup proxy middleware for an app
|
|
822
|
+
*/
|
|
823
|
+
const setupProxyApp = (appConfig, httpServer) => {
|
|
824
|
+
const { path, source, stripPrefix = true, name, maintenance, fallback } = appConfig;
|
|
825
|
+
if (source.type !== 'proxy')
|
|
826
|
+
return;
|
|
827
|
+
const appName = name || path.replace(/^\//, '') || 'Service';
|
|
828
|
+
logger.debug(`Setting up proxy: ${path} -> ${source.target}`);
|
|
829
|
+
// Maintenance mode middleware - intercepts all requests when enabled
|
|
830
|
+
if (maintenance?.enabled) {
|
|
831
|
+
logger.info(`Maintenance mode enabled for ${path}`);
|
|
832
|
+
app.use(path, (req, res, next) => {
|
|
833
|
+
// Check bypass paths
|
|
834
|
+
if (maintenance.bypassPaths?.some(bp => req.path.startsWith(bp))) {
|
|
835
|
+
return next();
|
|
836
|
+
}
|
|
837
|
+
const html = generateMaintenancePageHtml(appName, maintenance, config.productName);
|
|
838
|
+
res.status(503).type('html').send(html);
|
|
839
|
+
});
|
|
840
|
+
mountedApps.push({ path, type: 'proxy', target: source.target });
|
|
841
|
+
return; // Don't setup proxy when in maintenance mode
|
|
842
|
+
}
|
|
843
|
+
const proxyOptions = {
|
|
844
|
+
target: source.target,
|
|
845
|
+
changeOrigin: true,
|
|
846
|
+
ws: source.ws ?? false,
|
|
847
|
+
pathRewrite: stripPrefix ? { [`^${path}`]: '' } : undefined,
|
|
848
|
+
on: {
|
|
849
|
+
proxyReq: (proxyReq) => {
|
|
850
|
+
// Add X-Forwarded headers so app knows its mounted path
|
|
851
|
+
proxyReq.setHeader('X-Forwarded-Prefix', path);
|
|
852
|
+
},
|
|
853
|
+
error: (err, req, res) => {
|
|
854
|
+
logger.error(`Proxy error for ${path}`, { error: err.message });
|
|
855
|
+
if (res && 'writeHead' in res && !res.headersSent) {
|
|
856
|
+
// Check if this looks like an API request (Accept: application/json or /api/ path)
|
|
857
|
+
const acceptHeader = req.headers['accept'] || '';
|
|
858
|
+
const isApiRequest = acceptHeader.includes('application/json') || req.url?.includes('/api/');
|
|
859
|
+
if (isApiRequest) {
|
|
860
|
+
// Return JSON error for API requests
|
|
569
861
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
570
862
|
res.end(JSON.stringify({
|
|
571
863
|
error: 'Service Unavailable',
|
|
572
|
-
message:
|
|
864
|
+
message: `The service at ${path} is currently unavailable.`,
|
|
573
865
|
details: nodeEnv === 'development' ? err.message : undefined,
|
|
574
866
|
}));
|
|
575
867
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const healthProxyOptions = {
|
|
583
|
-
target,
|
|
584
|
-
changeOrigin: false,
|
|
585
|
-
pathFilter: '/health',
|
|
586
|
-
on: {
|
|
587
|
-
error: (_err, _req, res) => {
|
|
588
|
-
if (res && 'writeHead' in res && !res.headersSent) {
|
|
589
|
-
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
590
|
-
res.end(JSON.stringify({
|
|
591
|
-
status: 'unhealthy',
|
|
592
|
-
error: 'Service unavailable',
|
|
593
|
-
gateway: 'healthy',
|
|
594
|
-
}));
|
|
868
|
+
else {
|
|
869
|
+
// Return beautiful HTML page for browser requests
|
|
870
|
+
const html = generateServiceUnavailablePageHtml(appName, path, fallback, config.productName);
|
|
871
|
+
res.writeHead(503, { 'Content-Type': 'text/html' });
|
|
872
|
+
res.end(html);
|
|
873
|
+
}
|
|
595
874
|
}
|
|
596
875
|
},
|
|
597
876
|
},
|
|
598
877
|
};
|
|
599
|
-
|
|
878
|
+
const proxy = createProxyMiddleware(proxyOptions);
|
|
879
|
+
// Mount proxy
|
|
880
|
+
app.use(path, proxy);
|
|
881
|
+
// WebSocket upgrade handling
|
|
882
|
+
if (source.ws && httpServer) {
|
|
883
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
884
|
+
if (req.url?.startsWith(path)) {
|
|
885
|
+
proxy.upgrade?.(req, socket, head);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
mountedApps.push({ path, type: 'proxy', target: source.target });
|
|
600
890
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
891
|
+
/**
|
|
892
|
+
* Setup static file serving for an app
|
|
893
|
+
*/
|
|
894
|
+
const setupStaticApp = (appConfig) => {
|
|
895
|
+
const { path, source } = appConfig;
|
|
896
|
+
if (source.type !== 'static')
|
|
897
|
+
return;
|
|
898
|
+
logger.debug(`Setting up static: ${path} -> ${source.directory}`);
|
|
899
|
+
if (!existsSync(source.directory)) {
|
|
900
|
+
logger.warn(`Static directory not found: ${source.directory}`);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
// Serve static files
|
|
904
|
+
app.use(path, express.static(source.directory, { index: false }));
|
|
905
|
+
// SPA fallback
|
|
906
|
+
if (source.spa) {
|
|
907
|
+
const indexPath = join(source.directory, 'index.html');
|
|
908
|
+
// Read and cache index.html with path rewriting
|
|
909
|
+
let cachedHtml = null;
|
|
910
|
+
const getIndexHtml = () => {
|
|
911
|
+
if (cachedHtml)
|
|
912
|
+
return cachedHtml;
|
|
913
|
+
let html = readFileSync(indexPath, 'utf-8');
|
|
914
|
+
// Rewrite asset paths for non-root mount
|
|
915
|
+
if (path !== '/') {
|
|
916
|
+
html = html.replace(/src="\/assets\//g, `src="${path}/assets/`);
|
|
917
|
+
html = html.replace(/href="\/assets\//g, `href="${path}/assets/`);
|
|
918
|
+
}
|
|
919
|
+
cachedHtml = html;
|
|
920
|
+
return html;
|
|
921
|
+
};
|
|
922
|
+
app.get(`${path}/*`, (_req, res) => {
|
|
923
|
+
res.type('html').send(getIndexHtml());
|
|
924
|
+
});
|
|
925
|
+
if (path !== '/') {
|
|
926
|
+
app.get(path, (_req, res) => {
|
|
927
|
+
res.type('html').send(getIndexHtml());
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
mountedApps.push({ path, type: 'static' });
|
|
932
|
+
};
|
|
933
|
+
/**
|
|
934
|
+
* Setup frontend app at root path
|
|
935
|
+
*/
|
|
604
936
|
const setupFrontendApp = () => {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
937
|
+
const { frontendApp, logoIconUrl } = config;
|
|
938
|
+
// Default landing page
|
|
939
|
+
if (!frontendApp) {
|
|
940
|
+
logger.debug('Frontend: Serving default landing page');
|
|
941
|
+
app.get('/', (_req, res) => {
|
|
942
|
+
const html = generateDefaultLandingPageHtml(config.productName, cpPath, logoIconUrl);
|
|
610
943
|
res.type('html').send(html);
|
|
611
944
|
});
|
|
612
945
|
return;
|
|
613
946
|
}
|
|
614
|
-
const { redirectUrl, staticPath, landingPage } =
|
|
947
|
+
const { redirectUrl, staticPath, landingPage } = frontendApp;
|
|
615
948
|
// Priority 1: Redirect
|
|
616
949
|
if (redirectUrl) {
|
|
617
|
-
logger.
|
|
618
|
-
|
|
619
|
-
res.redirect(redirectUrl);
|
|
620
|
-
});
|
|
950
|
+
logger.debug(`Frontend: Redirecting / to ${redirectUrl}`);
|
|
951
|
+
app.get('/', (_req, res) => res.redirect(redirectUrl));
|
|
621
952
|
return;
|
|
622
953
|
}
|
|
623
|
-
// Priority 2:
|
|
954
|
+
// Priority 2: Static files
|
|
624
955
|
if (staticPath && existsSync(staticPath)) {
|
|
625
|
-
logger.
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
controlPanel.app.get('/', (_req, res) => {
|
|
956
|
+
logger.debug(`Frontend: Serving static from ${staticPath}`);
|
|
957
|
+
app.use('/', express.static(staticPath));
|
|
958
|
+
app.get('/', (_req, res) => {
|
|
629
959
|
res.sendFile(resolve(staticPath, 'index.html'));
|
|
630
960
|
});
|
|
631
961
|
return;
|
|
632
962
|
}
|
|
633
963
|
// Priority 3: Landing page
|
|
634
964
|
if (landingPage) {
|
|
635
|
-
logger.
|
|
636
|
-
|
|
637
|
-
const html = generateLandingPageHtml(landingPage,
|
|
965
|
+
logger.debug('Frontend: Serving custom landing page');
|
|
966
|
+
app.get('/', (_req, res) => {
|
|
967
|
+
const html = generateLandingPageHtml(landingPage, cpPath);
|
|
638
968
|
res.type('html').send(html);
|
|
639
969
|
});
|
|
640
970
|
}
|
|
641
971
|
};
|
|
972
|
+
/**
|
|
973
|
+
* Start the gateway
|
|
974
|
+
*/
|
|
642
975
|
const start = async () => {
|
|
643
|
-
logger.
|
|
644
|
-
// 1. Start internal
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
976
|
+
logger.debug('Starting gateway...');
|
|
977
|
+
// 1. Start internal control panel if enabled
|
|
978
|
+
if (cpEnabled) {
|
|
979
|
+
logger.debug(`Starting control panel on port ${cpPort}...`);
|
|
980
|
+
controlPanelInstance = createControlPanel({
|
|
981
|
+
config: {
|
|
982
|
+
productName: config.productName,
|
|
983
|
+
port: cpPort,
|
|
984
|
+
version,
|
|
985
|
+
logoIconUrl: config.logoIconUrl,
|
|
986
|
+
branding: config.branding,
|
|
987
|
+
cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
|
|
988
|
+
mountPath: '/', // Control panel runs at / internally
|
|
989
|
+
guard: cpConfig.guard,
|
|
990
|
+
customUiPath: cpConfig.customUiPath,
|
|
991
|
+
links: cpConfig.links,
|
|
992
|
+
},
|
|
993
|
+
plugins: cpConfig.plugins || [],
|
|
994
|
+
logger,
|
|
995
|
+
});
|
|
996
|
+
await controlPanelInstance.start();
|
|
997
|
+
logger.debug(`Control panel started on port ${cpPort}`);
|
|
660
998
|
}
|
|
661
|
-
|
|
662
|
-
|
|
999
|
+
// 2. Create HTTP server
|
|
1000
|
+
server = app.listen(gatewayPort);
|
|
1001
|
+
// 3. Setup mounted apps (proxy and static)
|
|
1002
|
+
const apps = config.apps || [];
|
|
1003
|
+
// Add control panel as a proxy app if enabled
|
|
1004
|
+
if (cpEnabled) {
|
|
1005
|
+
setupProxyApp({
|
|
1006
|
+
path: cpPath,
|
|
1007
|
+
source: { type: 'proxy', target: `http://localhost:${cpPort}` },
|
|
1008
|
+
}, server);
|
|
663
1009
|
}
|
|
664
|
-
|
|
665
|
-
|
|
1010
|
+
// Setup additional apps
|
|
1011
|
+
for (const appConfig of apps) {
|
|
1012
|
+
if (appConfig.source.type === 'proxy') {
|
|
1013
|
+
setupProxyApp(appConfig, server);
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
setupStaticApp(appConfig);
|
|
1017
|
+
}
|
|
666
1018
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
1019
|
+
// 4. Setup frontend app at root
|
|
1020
|
+
setupFrontendApp();
|
|
1021
|
+
// Log startup info
|
|
1022
|
+
const authInfo = cpConfig.guard?.type === 'basic'
|
|
1023
|
+
? `(auth: ${cpConfig.guard.username})`
|
|
1024
|
+
: cpConfig.guard?.type && cpConfig.guard.type !== 'none'
|
|
1025
|
+
? `(auth: ${cpConfig.guard.type})`
|
|
1026
|
+
: '(no auth)';
|
|
1027
|
+
logger.info(`${config.productName} gateway started on port ${gatewayPort} ${authInfo}`);
|
|
1028
|
+
// Log mounted apps
|
|
1029
|
+
for (const mounted of mountedApps) {
|
|
1030
|
+
if (mounted.type === 'proxy') {
|
|
1031
|
+
logger.debug(` ${mounted.path}/* -> ${mounted.target}`);
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
logger.debug(` ${mounted.path}/* -> [static]`);
|
|
1035
|
+
}
|
|
673
1036
|
}
|
|
674
1037
|
};
|
|
1038
|
+
/**
|
|
1039
|
+
* Stop the gateway
|
|
1040
|
+
*/
|
|
675
1041
|
const stop = async () => {
|
|
676
|
-
logger.
|
|
1042
|
+
logger.debug('Shutting down gateway...');
|
|
677
1043
|
// Stop control panel
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
1044
|
+
if (controlPanelInstance) {
|
|
1045
|
+
await controlPanelInstance.stop();
|
|
1046
|
+
}
|
|
1047
|
+
// Stop gateway server
|
|
1048
|
+
if (server) {
|
|
1049
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
683
1050
|
}
|
|
684
|
-
logger.
|
|
1051
|
+
logger.debug('Gateway shutdown complete');
|
|
685
1052
|
};
|
|
686
1053
|
return {
|
|
687
|
-
|
|
688
|
-
|
|
1054
|
+
app,
|
|
1055
|
+
server,
|
|
1056
|
+
controlPanel: controlPanelInstance,
|
|
1057
|
+
mountedApps,
|
|
689
1058
|
start,
|
|
690
1059
|
stop,
|
|
691
|
-
gatewayPort,
|
|
692
|
-
servicePort,
|
|
1060
|
+
port: gatewayPort,
|
|
693
1061
|
};
|
|
694
1062
|
}
|
|
695
1063
|
//# sourceMappingURL=gateway.js.map
|