@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
@@ -0,0 +1 @@
1
+ export declare function ConfigPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function DashboardPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function DiagnosticsPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * EntitlementsPage Component
3
+ *
4
+ * Entitlement catalog management page. Allows viewing and managing available entitlements.
5
+ * Write operations (create, edit, delete) are only available when source is not readonly.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ export interface EntitlementsPageProps {
10
+ /** Page title */
11
+ title?: string;
12
+ /** Page subtitle */
13
+ subtitle?: string;
14
+ /** Custom actions to render in the header */
15
+ headerActions?: React.ReactNode;
16
+ }
17
+ export declare function EntitlementsPage({ title, subtitle, headerActions, }: EntitlementsPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function LogsPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1 @@
1
+ export declare function NotFoundPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * PluginPage Component
3
+ *
4
+ * A generic page component for plugin-contributed routes.
5
+ * Fetches and displays plugin-specific content from the API.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ interface PluginPageProps {
10
+ pluginId: string;
11
+ title: string;
12
+ route: string;
13
+ }
14
+ export declare function PluginPage({ pluginId, title, route }: PluginPageProps): import("react/jsx-runtime").JSX.Element;
15
+ export {};
@@ -0,0 +1 @@
1
+ export declare function SystemPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * UsersPage Component
3
+ *
4
+ * Generic user management page that works with Users, Bans, and Entitlements plugins.
5
+ * All features are optional and auto-detected based on available plugins.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ import { type User, type PluginFeatures } from '../api/controlPanelApi';
10
+ export interface UsersPageProps {
11
+ /** Page title */
12
+ title?: string;
13
+ /** Page subtitle */
14
+ subtitle?: string;
15
+ /** Override automatic feature detection */
16
+ features?: Partial<PluginFeatures>;
17
+ /** Custom actions to render in the header */
18
+ headerActions?: React.ReactNode;
19
+ /** Callback when a user is selected */
20
+ onUserSelect?: (user: User) => void;
21
+ }
22
+ export declare function UsersPage({ title, subtitle, features: featureOverrides, headerActions, onUserSelect, }: UsersPageProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.1.9",
3
+ "version": "1.3.0",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -13,25 +13,35 @@
13
13
  "./plugins": {
14
14
  "types": "./dist/plugins/index.d.ts",
15
15
  "import": "./dist/plugins/index.js"
16
+ },
17
+ "./ui": {
18
+ "types": "./dist-ui-lib/components/index.d.ts",
19
+ "import": "./dist-ui-lib/index.js"
16
20
  }
17
21
  },
18
22
  "files": [
19
23
  "dist",
20
24
  "dist-ui",
25
+ "dist-ui-lib",
21
26
  "src",
22
27
  "ui"
23
28
  ],
24
29
  "scripts": {
25
- "build": "npm run build:server && npm run build:ui",
30
+ "build": "npm run build:server && npm run build:ui && npm run build:ui-lib",
26
31
  "build:server": "tsc",
27
32
  "build:ui": "cd ui && vite build",
28
- "build:clean": "rm -rf dist dist-ui && npm run build",
33
+ "build:ui-lib": "cd ui && vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
34
+ "build:clean": "rm -rf dist dist-ui dist-ui-lib && npm run build",
29
35
  "dev": "tsc --watch",
30
36
  "dev:ui": "cd ui && vite",
31
- "clean": "rm -rf dist dist-ui node_modules",
37
+ "clean": "rm -rf dist dist-ui dist-ui-lib node_modules",
32
38
  "test": "vitest run",
33
39
  "test:watch": "vitest",
34
40
  "test:coverage": "vitest run --coverage",
41
+ "test:e2e": "playwright test",
42
+ "test:e2e:ui": "playwright test --ui",
43
+ "test:e2e:headed": "playwright test --headed",
44
+ "demo": "npx tsx examples/demo-server.ts",
35
45
  "type-check": "tsc --noEmit",
36
46
  "type-check:ui": "cd ui && tsc --noEmit",
37
47
  "validate:clean-install": "./qa/clean-install/validate.sh",
@@ -50,6 +60,7 @@
50
60
  "@emotion/styled": "^11.14.0",
51
61
  "@mui/icons-material": "^7.2.0",
52
62
  "@mui/material": "^7.2.0",
63
+ "@playwright/test": "^1.57.0",
53
64
  "@qwickapps/react-framework": "^1.5.5",
54
65
  "@types/compression": "^1.7.5",
55
66
  "@types/cors": "^2.8.17",
@@ -57,14 +68,15 @@
57
68
  "@types/node": "^20.10.5",
58
69
  "@types/pg": "^8.11.0",
59
70
  "@types/react": "^18.2.0",
60
- "ioredis": "^5.4.0",
61
- "pg": "^8.13.0",
62
71
  "@types/react-dom": "^18.2.0",
63
72
  "@vitejs/plugin-react": "^4.3.4",
64
73
  "express-openid-connect": "^2.19.3",
74
+ "ioredis": "^5.4.0",
75
+ "pg": "^8.13.0",
65
76
  "react": "^18.2.0",
66
77
  "react-dom": "^18.2.0",
67
78
  "react-router-dom": "^6.30.1",
79
+ "tsx": "^4.20.6",
68
80
  "typescript": "^5.3.3",
69
81
  "vite": "^6.0.0",
70
82
  "vitest": "^2.1.0"
@@ -10,7 +10,7 @@ import express, { type Application, type Router, type Request, type Response } f
10
10
  import helmet from 'helmet';
11
11
  import cors from 'cors';
12
12
  import compression from 'compression';
13
- import { existsSync } from 'node:fs';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { HealthManager } from './health-manager.js';
@@ -18,13 +18,17 @@ import { initializeLogging, getControlPanelLogger, type LoggingConfig } from './
18
18
  import { createRouteGuard } from './guards.js';
19
19
  import type {
20
20
  ControlPanelConfig,
21
- ControlPanelPlugin,
22
21
  ControlPanelInstance,
23
- PluginContext,
24
22
  DiagnosticsReport,
25
23
  HealthCheck,
26
24
  Logger,
27
25
  } from './types.js';
26
+ import {
27
+ createPluginRegistry,
28
+ type Plugin,
29
+ type PluginConfig,
30
+ type PluginRegistryImpl,
31
+ } from './plugin-registry.js';
28
32
 
29
33
  // Get the package root directory for serving UI assets
30
34
  const __filename = fileURLToPath(import.meta.url);
@@ -35,9 +39,22 @@ const packageRoot = __dirname.includes('/src/')
35
39
  : join(__dirname, '..', '..');
36
40
  const uiDistPath = join(packageRoot, 'dist-ui');
37
41
 
42
+ // Read @qwickapps/server package version
43
+ let frameworkVersion = '1.0.0';
44
+ try {
45
+ const packageJsonPath = join(packageRoot, 'package.json');
46
+ if (existsSync(packageJsonPath)) {
47
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
48
+ frameworkVersion = packageJson.version || '1.0.0';
49
+ }
50
+ } catch {
51
+ // Keep default version if reading fails
52
+ }
53
+
38
54
  export interface CreateControlPanelOptions {
39
55
  config: ControlPanelConfig;
40
- plugins?: ControlPanelPlugin[];
56
+ /** Plugins to start with the control panel */
57
+ plugins?: Array<{ plugin: Plugin; config?: PluginConfig }>;
41
58
  logger?: Logger;
42
59
  /** Logging configuration */
43
60
  logging?: LoggingConfig;
@@ -61,10 +78,18 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
61
78
  const app: Application = express();
62
79
  const router: Router = express.Router();
63
80
  const healthManager = new HealthManager(logger);
64
- const registeredPlugins: ControlPanelPlugin[] = [];
65
81
  let server: ReturnType<typeof app.listen> | null = null;
66
82
  const startTime = Date.now();
67
83
 
84
+ // Initialize the new plugin registry
85
+ const pluginRegistry = createPluginRegistry(
86
+ app,
87
+ router,
88
+ logger,
89
+ healthManager,
90
+ getControlPanelLogger
91
+ );
92
+
68
93
  // Security middleware
69
94
  app.use(
70
95
  helmet({
@@ -95,14 +120,15 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
95
120
  }
96
121
  app.use(compression());
97
122
 
98
- // Apply route guard if configured
123
+ // Get mount path (defaults to /cpanel)
124
+ const mountPath = config.mountPath || '/cpanel';
125
+
126
+ // Apply route guard if configured - only to the control panel mount path
99
127
  if (config.guard && config.guard.type !== 'none') {
100
128
  const guardMiddleware = createRouteGuard(config.guard);
101
- app.use(guardMiddleware);
129
+ // Only protect the control panel path, not the root or other paths
130
+ app.use(mountPath, guardMiddleware);
102
131
  }
103
-
104
- // Get mount path (defaults to /cpanel)
105
- const mountPath = config.mountPath || '/cpanel';
106
132
  const apiBasePath = mountPath === '/' ? '/api' : `${mountPath}/api`;
107
133
 
108
134
  // Request logging
@@ -138,6 +164,8 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
138
164
  router.get('/info', (_req: Request, res: Response) => {
139
165
  res.json({
140
166
  product: config.productName,
167
+ logoName: config.logoName || config.productName,
168
+ logoIconUrl: config.logoIconUrl,
141
169
  version: config.version || 'unknown',
142
170
  uptime: Date.now() - startTime,
143
171
  links: config.links || [],
@@ -153,6 +181,30 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
153
181
  res.json(report);
154
182
  });
155
183
 
184
+ /**
185
+ * GET /api/ui-contributions - UI contributions from all plugins
186
+ *
187
+ * Returns menu items, pages, and widgets registered by plugins.
188
+ * Used by the React UI to build dynamic navigation and pages.
189
+ */
190
+ router.get('/ui-contributions', (_req: Request, res: Response) => {
191
+ res.json({
192
+ menuItems: pluginRegistry.getMenuItems(),
193
+ pages: pluginRegistry.getPages(),
194
+ widgets: pluginRegistry.getWidgets(),
195
+ plugins: pluginRegistry.listPlugins(),
196
+ });
197
+ });
198
+
199
+ /**
200
+ * GET /api/plugins - List all registered plugins
201
+ */
202
+ router.get('/plugins', (_req: Request, res: Response) => {
203
+ res.json({
204
+ plugins: pluginRegistry.listPlugins(),
205
+ });
206
+ });
207
+
156
208
  /**
157
209
  * Serve dashboard UI at the configured mount path
158
210
  *
@@ -170,22 +222,61 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
170
222
 
171
223
  if (useRichUI) {
172
224
  logger.debug(`Serving React UI from ${effectiveUiPath}`);
225
+
226
+ // Read index.html template
227
+ const indexHtmlPath = join(effectiveUiPath, 'index.html');
228
+ const indexHtmlTemplate = readFileSync(indexHtmlPath, 'utf-8');
229
+
230
+ /**
231
+ * Get index.html with the base path injected.
232
+ *
233
+ * The server injects the base path as window.__APP_BASE_PATH__ so the React app
234
+ * can read it at runtime without complex detection logic. This is the standard
235
+ * pattern used by frameworks like Next.js (__NEXT_DATA__).
236
+ *
237
+ * When served behind a gateway proxy, use X-Forwarded-Prefix to determine
238
+ * the public path for assets and the React Router basename.
239
+ */
240
+ const getIndexHtml = (req: Request): string => {
241
+ // Determine the effective public path:
242
+ // - If X-Forwarded-Prefix header is set (proxied), use that
243
+ // - Otherwise, use the configured mountPath
244
+ const forwardedPrefix = req.get('X-Forwarded-Prefix');
245
+ const effectivePath = forwardedPrefix || mountPath;
246
+ const normalizedPath = effectivePath === '/' ? '' : effectivePath;
247
+
248
+ // Inject base path as global variable before other scripts
249
+ const basePathScript = `<script>window.__APP_BASE_PATH__="${normalizedPath}";</script>`;
250
+ let html = indexHtmlTemplate.replace('<head>', `<head>\n ${basePathScript}`);
251
+
252
+ // Rewrite asset paths if mounted at a subpath
253
+ if (normalizedPath) {
254
+ html = html.replace(/src="\/assets\//g, `src="${normalizedPath}/assets/`);
255
+ html = html.replace(/href="\/assets\//g, `href="${normalizedPath}/assets/`);
256
+ }
257
+
258
+ return html;
259
+ };
260
+
173
261
  // Serve static assets from dist-ui at the mount path
174
- app.use(mountPath, express.static(effectiveUiPath));
262
+ // Disable index: false to prevent serving index.html automatically
263
+ // We handle index.html separately with rewritten asset paths
264
+ app.use(mountPath, express.static(effectiveUiPath, { index: false }));
175
265
 
176
266
  // SPA fallback - serve index.html for all non-API routes under the mount path
177
- app.get(`${mountPath}/*`, (req: Request, res: Response, next) => {
267
+ const spaFallbackPath = mountPath === '/' ? '/*' : `${mountPath}/*`;
268
+ app.get(spaFallbackPath, (req: Request, res: Response, next) => {
178
269
  // Skip API routes
179
270
  if (req.path.startsWith(apiBasePath)) {
180
271
  return next();
181
272
  }
182
- res.sendFile(join(effectiveUiPath, 'index.html'));
273
+ res.type('html').send(getIndexHtml(req));
183
274
  });
184
275
 
185
276
  // Also serve the mount path root
186
277
  if (mountPath !== '/') {
187
- app.get(mountPath, (_req: Request, res: Response) => {
188
- res.sendFile(join(effectiveUiPath, 'index.html'));
278
+ app.get(mountPath, (req: Request, res: Response) => {
279
+ res.type('html').send(getIndexHtml(req));
189
280
  });
190
281
  }
191
282
  } else {
@@ -198,47 +289,9 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
198
289
  }
199
290
  }
200
291
 
201
- // Plugin context factory - creates context with plugin-specific logger
202
- const createPluginContext = (pluginName: string): PluginContext => ({
203
- config,
204
- app,
205
- router,
206
- logger: getControlPanelLogger(pluginName),
207
- registerHealthCheck: (check: HealthCheck) => healthManager.register(check),
208
- });
209
-
210
- // Register plugin
211
- const registerPlugin = async (plugin: ControlPanelPlugin): Promise<void> => {
212
- logger.debug(`Registering plugin: ${plugin.name}`);
213
-
214
- // Register routes
215
- if (plugin.routes) {
216
- for (const route of plugin.routes) {
217
- switch (route.method) {
218
- case 'get':
219
- router.get(route.path, route.handler);
220
- break;
221
- case 'post':
222
- router.post(route.path, route.handler);
223
- break;
224
- case 'put':
225
- router.put(route.path, route.handler);
226
- break;
227
- case 'delete':
228
- router.delete(route.path, route.handler);
229
- break;
230
- }
231
- logger.debug(`Registered route: ${route.method.toUpperCase()} ${route.path}`);
232
- }
233
- }
234
-
235
- // Initialize plugin with plugin-specific logger
236
- if (plugin.onInit) {
237
- await plugin.onInit(createPluginContext(plugin.name));
238
- }
239
-
240
- registeredPlugins.push(plugin);
241
- logger.debug(`Plugin registered: ${plugin.name}`);
292
+ // Start a plugin with the registry
293
+ const startPlugin = async (plugin: Plugin, pluginConfig: PluginConfig = {}): Promise<boolean> => {
294
+ return pluginRegistry.startPlugin(plugin, pluginConfig);
242
295
  };
243
296
 
244
297
  // Get diagnostics report
@@ -249,6 +302,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
249
302
  timestamp: new Date().toISOString(),
250
303
  product: config.productName,
251
304
  version: config.version,
305
+ frameworkVersion,
252
306
  uptime: Date.now() - startTime,
253
307
  health: healthManager.getResults(),
254
308
  system: {
@@ -269,14 +323,17 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
269
323
 
270
324
  // Start server
271
325
  const start = async (): Promise<void> => {
272
- // Register initial plugins
273
- for (const plugin of plugins) {
274
- await registerPlugin(plugin);
326
+ // Start initial plugins via registry
327
+ for (const { plugin, config: pluginConfig } of plugins) {
328
+ const success = await pluginRegistry.startPlugin(plugin, pluginConfig || {});
329
+ if (!success) {
330
+ logger.error(`Failed to start plugin: ${plugin.id}`);
331
+ }
275
332
  }
276
333
 
277
334
  return new Promise((resolve) => {
278
335
  server = app.listen(config.port, () => {
279
- logger.info(`Control panel listening on port ${config.port}`);
336
+ logger.debug(`Control panel listening on port ${config.port}`);
280
337
  resolve();
281
338
  });
282
339
  });
@@ -284,12 +341,8 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
284
341
 
285
342
  // Stop server
286
343
  const stop = async (): Promise<void> => {
287
- // Shutdown plugins
288
- for (const plugin of registeredPlugins) {
289
- if (plugin.onShutdown) {
290
- await plugin.onShutdown();
291
- }
292
- }
344
+ // Stop all plugins via registry
345
+ await pluginRegistry.stopAllPlugins();
293
346
 
294
347
  // Shutdown health manager
295
348
  healthManager.shutdown();
@@ -298,7 +351,7 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
298
351
  if (server) {
299
352
  return new Promise((resolve) => {
300
353
  server!.close(() => {
301
- logger.info('Control panel stopped');
354
+ logger.debug('Control panel stopped');
302
355
  resolve();
303
356
  });
304
357
  });
@@ -309,9 +362,10 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
309
362
  app,
310
363
  start,
311
364
  stop,
312
- registerPlugin,
365
+ startPlugin,
313
366
  getHealthStatus: () => healthManager.getResults(),
314
367
  getDiagnostics,
368
+ getPluginRegistry: () => pluginRegistry,
315
369
  };
316
370
  }
317
371