@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
package/ui/src/App.tsx CHANGED
@@ -1,21 +1,64 @@
1
+ import { useState, useEffect } from 'react';
1
2
  import { BrowserRouter, Routes, Route } from 'react-router-dom';
2
3
  import { QwickApp, ProductLogo, Text } from '@qwickapps/react-framework';
3
4
  import { Link, Box } from '@mui/material';
4
5
  import { defaultConfig } from './config/AppConfig';
6
+ import { DashboardWidgetProvider } from './dashboard';
5
7
  import { DashboardPage } from './pages/DashboardPage';
6
- import { HealthPage } from './pages/HealthPage';
7
8
  import { LogsPage } from './pages/LogsPage';
8
- import { ConfigPage } from './pages/ConfigPage';
9
- import { DiagnosticsPage } from './pages/DiagnosticsPage';
9
+ import { SystemPage } from './pages/SystemPage';
10
+ import { UsersPage } from './pages/UsersPage';
11
+ import { EntitlementsPage } from './pages/EntitlementsPage';
12
+ import { PluginPage } from './pages/PluginPage';
10
13
  import { NotFoundPage } from './pages/NotFoundPage';
14
+ import { api, type MenuContribution } from './api/controlPanelApi';
15
+
16
+ // Navigation item type
17
+ interface NavigationItem {
18
+ id: string;
19
+ label: string;
20
+ route: string;
21
+ icon: string;
22
+ }
23
+
24
+ // Core navigation items always shown
25
+ const coreNavigationItems: NavigationItem[] = [
26
+ { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
27
+ { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
28
+ { id: 'system', label: 'System', route: '/system', icon: 'settings' },
29
+ ];
30
+
31
+ // Built-in optional navigation items - shown if corresponding plugin is registered
32
+ const builtInPluginNavItems: Record<string, NavigationItem> = {
33
+ users: { id: 'users', label: 'Users', route: '/users', icon: 'people' },
34
+ };
35
+
36
+ // Routes that have dedicated page components
37
+ const dedicatedRoutes = new Set(['/', '/logs', '/system', '/users', '/entitlements']);
11
38
 
12
39
  // Package version - injected at build time or fallback
13
40
  const SERVER_VERSION = '1.0.0';
14
41
 
15
- // Default logo - consumers can customize
16
- const logo = <ProductLogo name="Control Panel" />;
42
+ // Declare global type for injected base path
43
+ declare global {
44
+ interface Window {
45
+ __APP_BASE_PATH__?: string;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get the base path for the application.
51
+ *
52
+ * The server injects window.__APP_BASE_PATH__ at runtime based on
53
+ * either the configured mountPath or X-Forwarded-Prefix header.
54
+ * This is a simple, robust approach - no complex detection needed.
55
+ */
56
+ const basePath = window.__APP_BASE_PATH__ ?? '';
57
+
58
+ // Configure API with the detected base path
59
+ api.setBaseUrl(basePath);
17
60
 
18
- // Default footer content with QwickApps Server branding
61
+ // Footer content with QwickApps Server branding
19
62
  const footerContent = (
20
63
  <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, py: 2 }}>
21
64
  <Text variant="caption" customColor="var(--theme-text-secondary)">
@@ -34,32 +77,143 @@ const footerContent = (
34
77
  );
35
78
 
36
79
  export function App() {
80
+ const [navigationItems, setNavigationItems] = useState<NavigationItem[]>(coreNavigationItems);
81
+ const [registeredPlugins, setRegisteredPlugins] = useState<Set<string>>(new Set());
82
+ const [pluginMenuItems, setPluginMenuItems] = useState<MenuContribution[]>([]);
83
+ const [logoName, setLogoName] = useState<string>('Control Panel');
84
+ const [logoIconUrl, setLogoIconUrl] = useState<string | undefined>();
85
+ const [isLoading, setIsLoading] = useState(true);
86
+
87
+ // Fetch product info and UI contributions on mount
88
+ useEffect(() => {
89
+ const loadData = async () => {
90
+ try {
91
+ // Fetch both in parallel
92
+ const [infoResult, contributionsResult] = await Promise.allSettled([
93
+ api.getInfo(),
94
+ api.getUiContributions(),
95
+ ]);
96
+
97
+ // Update logo name and icon URL if info fetch succeeded
98
+ if (infoResult.status === 'fulfilled') {
99
+ setLogoName(infoResult.value.logoName);
100
+ setLogoIconUrl(infoResult.value.logoIconUrl);
101
+ } else {
102
+ console.warn('Failed to fetch product info:', infoResult.reason);
103
+ }
104
+
105
+ // Update navigation from UI contributions
106
+ if (contributionsResult.status === 'fulfilled') {
107
+ const { plugins, menuItems } = contributionsResult.value;
108
+ const pluginIds = new Set(plugins.map((p) => p.id));
109
+ setRegisteredPlugins(pluginIds);
110
+ setPluginMenuItems(menuItems);
111
+
112
+ // Build navigation: core items + built-in plugin items + dynamic menu items
113
+ const dynamicNav = [...coreNavigationItems];
114
+
115
+ // Add built-in plugin nav items (like Users)
116
+ for (const [pluginId, navItem] of Object.entries(builtInPluginNavItems)) {
117
+ if (pluginIds.has(pluginId)) {
118
+ dynamicNav.push(navItem);
119
+ }
120
+ }
121
+
122
+ // Add plugin-contributed menu items (sorted by order)
123
+ const sortedMenuItems = [...menuItems].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
124
+ for (const menuItem of sortedMenuItems) {
125
+ // Skip if we already have a nav item for this route
126
+ if (dynamicNav.some(nav => nav.route === menuItem.route)) {
127
+ continue;
128
+ }
129
+ dynamicNav.push({
130
+ id: menuItem.id,
131
+ label: menuItem.label,
132
+ route: menuItem.route,
133
+ icon: menuItem.icon || 'extension',
134
+ });
135
+ }
136
+
137
+ setNavigationItems(dynamicNav);
138
+ } else {
139
+ console.warn('Failed to fetch UI contributions:', contributionsResult.reason);
140
+ }
141
+ } finally {
142
+ setIsLoading(false);
143
+ }
144
+ };
145
+
146
+ loadData();
147
+ }, []);
148
+
149
+ // Dynamic logo based on logoName and logoIconUrl from API
150
+ // When logoIconUrl is provided, use it as a custom icon instead of the default QwickIcon
151
+ const logoIcon = logoIconUrl ? (
152
+ <img
153
+ src={logoIconUrl}
154
+ alt={logoName}
155
+ style={{ width: 32, height: 32, objectFit: 'contain' }}
156
+ />
157
+ ) : undefined;
158
+ const logo = <ProductLogo icon={logoIcon} name={logoName} />;
159
+
160
+ // Show loading state until plugins are loaded
161
+ // This ensures QwickApp receives the correct navigation on first render
162
+ if (isLoading) {
163
+ return (
164
+ <Box
165
+ sx={{
166
+ display: 'flex',
167
+ justifyContent: 'center',
168
+ alignItems: 'center',
169
+ minHeight: '100vh',
170
+ bgcolor: 'var(--theme-background, #1a1a2e)',
171
+ }}
172
+ />
173
+ );
174
+ }
175
+
37
176
  return (
38
- <BrowserRouter>
39
- <QwickApp
40
- config={defaultConfig}
41
- logo={logo}
42
- footerContent={footerContent}
43
- enableScaffolding={true}
44
- navigationItems={[
45
- { id: 'dashboard', label: 'Dashboard', route: '/', icon: 'dashboard' },
46
- { id: 'health', label: 'Health', route: '/health', icon: 'favorite' },
47
- { id: 'logs', label: 'Logs', route: '/logs', icon: 'article' },
48
- { id: 'config', label: 'Config', route: '/config', icon: 'settings' },
49
- { id: 'diagnostics', label: 'Diagnostics', route: '/diagnostics', icon: 'bug_report' },
50
- ]}
51
- showThemeSwitcher={true}
52
- showPaletteSwitcher={true}
53
- >
54
- <Routes>
55
- <Route path="/" element={<DashboardPage />} />
56
- <Route path="/health" element={<HealthPage />} />
57
- <Route path="/logs" element={<LogsPage />} />
58
- <Route path="/config" element={<ConfigPage />} />
59
- <Route path="/diagnostics" element={<DiagnosticsPage />} />
60
- <Route path="*" element={<NotFoundPage />} />
61
- </Routes>
62
- </QwickApp>
177
+ <BrowserRouter basename={basePath || undefined}>
178
+ <DashboardWidgetProvider>
179
+ <QwickApp
180
+ config={defaultConfig}
181
+ logo={logo}
182
+ footerContent={footerContent}
183
+ enableScaffolding={true}
184
+ navigationItems={navigationItems}
185
+ showThemeSwitcher={true}
186
+ showPaletteSwitcher={true}
187
+ >
188
+ <Routes>
189
+ {/* Core routes */}
190
+ <Route path="/" element={<DashboardPage />} />
191
+ <Route path="/logs" element={<LogsPage />} />
192
+ <Route path="/system" element={<SystemPage />} />
193
+
194
+ {/* Built-in plugin routes */}
195
+ {registeredPlugins.has('users') && (
196
+ <Route path="/users" element={<UsersPage />} />
197
+ )}
198
+ {registeredPlugins.has('entitlements') && (
199
+ <Route path="/entitlements" element={<EntitlementsPage />} />
200
+ )}
201
+
202
+ {/* Dynamic plugin routes - render generic PluginPage for non-dedicated routes */}
203
+ {pluginMenuItems
204
+ .filter(item => !dedicatedRoutes.has(item.route))
205
+ .map(item => (
206
+ <Route
207
+ key={item.id}
208
+ path={item.route}
209
+ element={<PluginPage pluginId={item.pluginId} title={item.label} route={item.route} />}
210
+ />
211
+ ))}
212
+
213
+ <Route path="*" element={<NotFoundPage />} />
214
+ </Routes>
215
+ </QwickApp>
216
+ </DashboardWidgetProvider>
63
217
  </BrowserRouter>
64
218
  );
65
219
  }
@@ -20,12 +20,13 @@ export interface HealthResponse {
20
20
 
21
21
  export interface InfoResponse {
22
22
  product: string;
23
+ logoName: string;
24
+ logoIconUrl?: string;
23
25
  version: string;
24
26
  uptime: number;
25
27
  links: Array<{ label: string; url: string; external?: boolean }>;
26
28
  branding?: {
27
29
  primaryColor?: string;
28
- logo?: string;
29
30
  };
30
31
  }
31
32
 
@@ -33,6 +34,8 @@ export interface DiagnosticsResponse {
33
34
  timestamp: string;
34
35
  product: string;
35
36
  version?: string;
37
+ /** @qwickapps/server framework version */
38
+ frameworkVersion?: string;
36
39
  uptime: number;
37
40
  health: Record<string, HealthCheck>;
38
41
  system: {
@@ -75,6 +78,131 @@ export interface LogSource {
75
78
  available: boolean;
76
79
  }
77
80
 
81
+ // ==================
82
+ // Users API Types
83
+ // ==================
84
+ export interface User {
85
+ id: string;
86
+ email: string;
87
+ name?: string;
88
+ created_at?: string;
89
+ updated_at?: string;
90
+ last_login?: string;
91
+ metadata?: Record<string, unknown>;
92
+ }
93
+
94
+ export interface UsersResponse {
95
+ users: User[];
96
+ total: number;
97
+ page: number;
98
+ limit: number;
99
+ }
100
+
101
+ // ==================
102
+ // Bans API Types
103
+ // ==================
104
+ export interface Ban {
105
+ id: string;
106
+ user_id?: string;
107
+ email: string;
108
+ reason: string;
109
+ banned_at: string;
110
+ banned_by: string;
111
+ expires_at?: string;
112
+ }
113
+
114
+ export interface BansResponse {
115
+ bans: Ban[];
116
+ total: number;
117
+ }
118
+
119
+ // ==================
120
+ // Entitlements API Types
121
+ // ==================
122
+ export interface EntitlementDefinition {
123
+ id: string;
124
+ name: string;
125
+ category?: string;
126
+ description?: string;
127
+ }
128
+
129
+ export interface EntitlementResult {
130
+ identifier: string;
131
+ entitlements: string[];
132
+ source: string;
133
+ cached?: boolean;
134
+ cachedAt?: string;
135
+ expiresAt?: string;
136
+ }
137
+
138
+ // ==================
139
+ // Entitlements Status
140
+ // ==================
141
+ export interface EntitlementSourceInfo {
142
+ name: string;
143
+ description?: string;
144
+ readonly: boolean;
145
+ primary: boolean;
146
+ }
147
+
148
+ export interface EntitlementsStatus {
149
+ readonly: boolean;
150
+ writeEnabled: boolean;
151
+ cacheEnabled: boolean;
152
+ cacheTtl: number;
153
+ sources: EntitlementSourceInfo[];
154
+ }
155
+
156
+ // ==================
157
+ // Plugin Feature Detection
158
+ // ==================
159
+ export interface PluginFeatures {
160
+ users: boolean;
161
+ bans: boolean;
162
+ entitlements: boolean;
163
+ entitlementsReadonly?: boolean;
164
+ }
165
+
166
+ // ==================
167
+ // UI Contributions Types
168
+ // ==================
169
+
170
+ export interface MenuContribution {
171
+ id: string;
172
+ label: string;
173
+ icon?: string;
174
+ route: string;
175
+ order?: number;
176
+ pluginId: string;
177
+ parent?: string;
178
+ }
179
+
180
+ export interface PageContribution {
181
+ id: string;
182
+ route: string;
183
+ title: string;
184
+ pluginId: string;
185
+ }
186
+
187
+ export interface WidgetContribution {
188
+ id: string;
189
+ title: string;
190
+ /** Component name to render (matched by frontend widget registry) */
191
+ component: string;
192
+ /** Priority for ordering (lower = first, default: 100) */
193
+ priority?: number;
194
+ /** Whether this widget is shown by default */
195
+ showByDefault?: boolean;
196
+ pluginId: string;
197
+ }
198
+
199
+ export interface UiContributionsResponse {
200
+ menuItems: MenuContribution[];
201
+ pages: PageContribution[];
202
+ widgets: WidgetContribution[];
203
+ plugins: Array<{ id: string; name: string; version?: string; status: string }>;
204
+ }
205
+
78
206
  class ControlPanelApi {
79
207
  private baseUrl: string;
80
208
 
@@ -82,6 +210,207 @@ class ControlPanelApi {
82
210
  this.baseUrl = baseUrl;
83
211
  }
84
212
 
213
+ /**
214
+ * Set the base URL for API requests.
215
+ * Call this when the control panel is mounted at a custom path.
216
+ */
217
+ setBaseUrl(baseUrl: string): void {
218
+ this.baseUrl = baseUrl;
219
+ }
220
+
221
+ // ==================
222
+ // Plugin Feature Detection
223
+ // ==================
224
+
225
+ /**
226
+ * Detect which user management plugins are available by probing their endpoints
227
+ */
228
+ async detectFeatures(): Promise<PluginFeatures> {
229
+ const [users, bans, entitlements] = await Promise.all([
230
+ this.checkEndpoint('/api/users'),
231
+ this.checkEndpoint('/api/bans'),
232
+ this.checkEndpoint('/api/entitlements/available'),
233
+ ]);
234
+
235
+ // If entitlements is available, get readonly status
236
+ let entitlementsReadonly = true;
237
+ if (entitlements) {
238
+ try {
239
+ const status = await this.getEntitlementsStatus();
240
+ entitlementsReadonly = status.readonly;
241
+ } catch {
242
+ // Default to readonly if we can't get status
243
+ }
244
+ }
245
+
246
+ return { users, bans, entitlements, entitlementsReadonly };
247
+ }
248
+
249
+ private async checkEndpoint(path: string): Promise<boolean> {
250
+ try {
251
+ const response = await fetch(`${this.baseUrl}${path}`, { method: 'HEAD' });
252
+ // 200, 401, 403 mean the endpoint exists (might need auth)
253
+ // 404 means it doesn't exist
254
+ return response.status !== 404;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+
260
+ // ==================
261
+ // Users API
262
+ // ==================
263
+
264
+ async getUsers(options: {
265
+ limit?: number;
266
+ page?: number;
267
+ search?: string;
268
+ } = {}): Promise<UsersResponse> {
269
+ const params = new URLSearchParams();
270
+ if (options.limit) params.set('limit', options.limit.toString());
271
+ if (options.page) params.set('page', options.page.toString());
272
+ if (options.search) params.set('search', options.search);
273
+
274
+ const response = await fetch(`${this.baseUrl}/api/users?${params}`);
275
+ if (!response.ok) {
276
+ throw new Error(`Users request failed: ${response.statusText}`);
277
+ }
278
+ return response.json();
279
+ }
280
+
281
+ async getUserById(id: string): Promise<User> {
282
+ const response = await fetch(`${this.baseUrl}/api/users/${id}`);
283
+ if (!response.ok) {
284
+ throw new Error(`User request failed: ${response.statusText}`);
285
+ }
286
+ return response.json();
287
+ }
288
+
289
+ // ==================
290
+ // Bans API
291
+ // ==================
292
+
293
+ async getBans(): Promise<BansResponse> {
294
+ const response = await fetch(`${this.baseUrl}/api/bans`);
295
+ if (!response.ok) {
296
+ throw new Error(`Bans request failed: ${response.statusText}`);
297
+ }
298
+ return response.json();
299
+ }
300
+
301
+ async banUser(email: string, reason: string, expiresAt?: string): Promise<void> {
302
+ const response = await fetch(`${this.baseUrl}/api/bans`, {
303
+ method: 'POST',
304
+ headers: { 'Content-Type': 'application/json' },
305
+ body: JSON.stringify({ email, reason, expiresAt }),
306
+ });
307
+ if (!response.ok) {
308
+ const error = await response.json().catch(() => ({}));
309
+ throw new Error(error.error || `Ban request failed: ${response.statusText}`);
310
+ }
311
+ }
312
+
313
+ async unbanUser(email: string): Promise<void> {
314
+ const response = await fetch(`${this.baseUrl}/api/bans/${encodeURIComponent(email)}`, {
315
+ method: 'DELETE',
316
+ });
317
+ if (!response.ok) {
318
+ throw new Error(`Unban request failed: ${response.statusText}`);
319
+ }
320
+ }
321
+
322
+ async checkBan(email: string): Promise<{ banned: boolean; ban?: Ban }> {
323
+ const response = await fetch(`${this.baseUrl}/api/bans/check/${encodeURIComponent(email)}`);
324
+ if (!response.ok) {
325
+ throw new Error(`Ban check failed: ${response.statusText}`);
326
+ }
327
+ return response.json();
328
+ }
329
+
330
+ // ==================
331
+ // Entitlements API
332
+ // ==================
333
+
334
+ async getEntitlements(email: string): Promise<EntitlementResult> {
335
+ const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}`);
336
+ if (!response.ok) {
337
+ throw new Error(`Entitlements request failed: ${response.statusText}`);
338
+ }
339
+ return response.json();
340
+ }
341
+
342
+ async refreshEntitlements(email: string): Promise<EntitlementResult> {
343
+ const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/refresh`, {
344
+ method: 'POST',
345
+ });
346
+ if (!response.ok) {
347
+ throw new Error(`Entitlements refresh failed: ${response.statusText}`);
348
+ }
349
+ return response.json();
350
+ }
351
+
352
+ async checkEntitlement(email: string, entitlement: string): Promise<{ has: boolean }> {
353
+ const response = await fetch(
354
+ `${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/check/${encodeURIComponent(entitlement)}`
355
+ );
356
+ if (!response.ok) {
357
+ throw new Error(`Entitlement check failed: ${response.statusText}`);
358
+ }
359
+ return response.json();
360
+ }
361
+
362
+ async getAvailableEntitlements(): Promise<EntitlementDefinition[]> {
363
+ const response = await fetch(`${this.baseUrl}/api/entitlements/available`);
364
+ if (!response.ok) {
365
+ throw new Error(`Available entitlements request failed: ${response.statusText}`);
366
+ }
367
+ const data = await response.json();
368
+ return data.entitlements;
369
+ }
370
+
371
+ async grantEntitlement(email: string, entitlement: string): Promise<void> {
372
+ const response = await fetch(`${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}`, {
373
+ method: 'POST',
374
+ headers: { 'Content-Type': 'application/json' },
375
+ body: JSON.stringify({ entitlement }),
376
+ });
377
+ if (!response.ok) {
378
+ const error = await response.json().catch(() => ({}));
379
+ throw new Error(error.error || `Grant entitlement failed: ${response.statusText}`);
380
+ }
381
+ }
382
+
383
+ async revokeEntitlement(email: string, entitlement: string): Promise<void> {
384
+ const response = await fetch(
385
+ `${this.baseUrl}/api/entitlements/${encodeURIComponent(email)}/${encodeURIComponent(entitlement)}`,
386
+ { method: 'DELETE' }
387
+ );
388
+ if (!response.ok) {
389
+ throw new Error(`Revoke entitlement failed: ${response.statusText}`);
390
+ }
391
+ }
392
+
393
+ async invalidateEntitlementCache(email: string): Promise<void> {
394
+ const response = await fetch(`${this.baseUrl}/api/entitlements/cache/${encodeURIComponent(email)}`, {
395
+ method: 'DELETE',
396
+ });
397
+ if (!response.ok) {
398
+ throw new Error(`Cache invalidation failed: ${response.statusText}`);
399
+ }
400
+ }
401
+
402
+ async getEntitlementsStatus(): Promise<EntitlementsStatus> {
403
+ const response = await fetch(`${this.baseUrl}/api/entitlements/status`);
404
+ if (!response.ok) {
405
+ throw new Error(`Entitlements status request failed: ${response.statusText}`);
406
+ }
407
+ return response.json();
408
+ }
409
+
410
+ // ==================
411
+ // Health API
412
+ // ==================
413
+
85
414
  async getHealth(): Promise<HealthResponse> {
86
415
  const response = await fetch(`${this.baseUrl}/api/health`);
87
416
  if (!response.ok) {
@@ -143,6 +472,30 @@ class ControlPanelApi {
143
472
  const data = await response.json();
144
473
  return data.sources;
145
474
  }
475
+
476
+ // ==================
477
+ // Plugins API
478
+ // ==================
479
+
480
+ async getPlugins(): Promise<{ plugins: Array<{ id: string; name: string; version?: string }> }> {
481
+ const response = await fetch(`${this.baseUrl}/api/plugins`);
482
+ if (!response.ok) {
483
+ throw new Error(`Plugins request failed: ${response.statusText}`);
484
+ }
485
+ return response.json();
486
+ }
487
+
488
+ // ==================
489
+ // UI Contributions API
490
+ // ==================
491
+
492
+ async getUiContributions(): Promise<UiContributionsResponse> {
493
+ const response = await fetch(`${this.baseUrl}/api/ui-contributions`);
494
+ if (!response.ok) {
495
+ throw new Error(`UI contributions request failed: ${response.statusText}`);
496
+ }
497
+ return response.json();
498
+ }
146
499
  }
147
500
 
148
501
  export const api = new ControlPanelApi();