@qwickapps/server 1.7.2 → 1.8.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 (120) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/src/core/control-panel.js +5 -5
  3. package/dist/src/core/control-panel.js.map +1 -1
  4. package/dist/src/core/gateway.d.ts.map +1 -1
  5. package/dist/src/core/gateway.js +117 -15
  6. package/dist/src/core/gateway.js.map +1 -1
  7. package/dist/src/core/plugin-registry.d.ts +70 -0
  8. package/dist/src/core/plugin-registry.d.ts.map +1 -1
  9. package/dist/src/core/plugin-registry.js +94 -0
  10. package/dist/src/core/plugin-registry.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
  14. package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
  15. package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
  16. package/dist/src/plugins/api-keys/index.d.ts +1 -1
  17. package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
  18. package/dist/src/plugins/api-keys/index.js.map +1 -1
  19. package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
  20. package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
  21. package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
  22. package/dist/src/plugins/api-keys/types.d.ts +13 -1
  23. package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
  24. package/dist/src/plugins/api-keys/types.js.map +1 -1
  25. package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
  26. package/dist/src/plugins/diagnostics-plugin.js +73 -0
  27. package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
  28. package/dist/src/plugins/index.d.ts +1 -1
  29. package/dist/src/plugins/index.d.ts.map +1 -1
  30. package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
  31. package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
  32. package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
  33. package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
  34. package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
  35. package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
  36. package/dist/src/plugins/maintenance/SeedList.js +39 -14
  37. package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
  38. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
  39. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
  40. package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
  41. package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
  42. package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
  43. package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
  44. package/dist/src/plugins/maintenance/seed-executor.js +53 -17
  45. package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
  46. package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
  47. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  48. package/dist/src/plugins/maintenance-plugin.js +222 -34
  49. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  50. package/dist/src/plugins/postgres-plugin.d.ts +12 -0
  51. package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
  52. package/dist/src/plugins/postgres-plugin.js +319 -5
  53. package/dist/src/plugins/postgres-plugin.js.map +1 -1
  54. package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
  55. package/dist/ui/src/components/ControlPanelApp.js +4 -3
  56. package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
  57. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  58. package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
  59. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  60. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
  61. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
  62. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
  63. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
  64. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
  65. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
  66. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
  67. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
  68. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
  69. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
  70. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
  71. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
  72. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
  73. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
  74. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
  75. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
  76. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  77. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
  78. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
  79. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
  80. package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
  81. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  82. package/dist/ui/src/dashboard/widgets/index.js +1 -0
  83. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  84. package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
  85. package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
  86. package/dist-ui/index.html +1 -1
  87. package/dist-ui-lib/index.js +3735 -3187
  88. package/dist-ui-lib/index.js.map +1 -1
  89. package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
  90. package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  91. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
  92. package/package.json +2 -2
  93. package/src/core/control-panel.ts +5 -5
  94. package/src/core/gateway.ts +135 -15
  95. package/src/core/plugin-registry.ts +171 -0
  96. package/src/index.ts +2 -0
  97. package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
  98. package/src/plugins/api-keys/index.ts +1 -0
  99. package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
  100. package/src/plugins/api-keys/types.ts +14 -1
  101. package/src/plugins/diagnostics-plugin.ts +77 -0
  102. package/src/plugins/index.ts +1 -1
  103. package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
  104. package/src/plugins/maintenance/SeedList.tsx +85 -38
  105. package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
  106. package/src/plugins/maintenance/seed-executor.ts +56 -17
  107. package/src/plugins/maintenance-plugin.ts +267 -36
  108. package/src/plugins/postgres-plugin.ts +410 -5
  109. package/ui/src/App.tsx +3 -3
  110. package/ui/src/components/ControlPanelApp.tsx +4 -3
  111. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  112. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
  113. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
  114. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
  115. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
  116. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
  117. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
  118. package/ui/src/dashboard/widgets/index.ts +1 -0
  119. package/dist-ui/assets/index-0gzisPdy.js +0 -528
  120. package/dist-ui/assets/index-0gzisPdy.js.map +0 -1
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Database Operations Widget Component
3
+ * Displays database status and provides manual initialization/recreation controls
4
+ *
5
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
6
+ */
7
+ import React from 'react';
8
+ export interface DatabaseOperationsWidgetProps {
9
+ }
10
+ export declare const DatabaseOperationsWidget: React.FC<DatabaseOperationsWidgetProps>;
11
+ export default DatabaseOperationsWidget;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Seed Management Widget
3
3
  *
4
- * Displays available seed scripts and allows executing them.
4
+ * Displays available seed scripts grouped by folder and allows executing them.
5
5
  * Part of the maintenance plugin.
6
6
  *
7
7
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
@@ -13,5 +13,6 @@ export { SeedManagementWidget } from './SeedManagementWidget';
13
13
  export { ServiceControlWidget } from './ServiceControlWidget';
14
14
  export { EnvironmentConfigWidget } from './EnvironmentConfigWidget';
15
15
  export { DatabaseOpsWidget } from './DatabaseOpsWidget';
16
+ export { DatabaseOperationsWidget } from './DatabaseOperationsWidget';
16
17
  export { LogsMaintenanceWidget } from './LogsMaintenanceWidget';
17
18
  export { CacheMaintenanceWidget } from './CacheMaintenanceWidget';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -68,7 +68,7 @@
68
68
  "@mui/material": "^7.2.0",
69
69
  "@mui/x-data-grid": "^7.29.12",
70
70
  "@playwright/test": "^1.57.0",
71
- "@qwickapps/auth-client": "^1.1.0",
71
+ "@qwickapps/auth-client": "^1.2.0",
72
72
  "@qwickapps/react-framework": "^1.5.5",
73
73
  "@testing-library/jest-dom": "^6.0.0",
74
74
  "@testing-library/react": "^14.0.0",
@@ -39,14 +39,14 @@ import { bearerTokenAuth } from '../plugins/api-keys/index.js';
39
39
  import { createCorePlugin } from '../plugins/core/index.js';
40
40
 
41
41
  // Get the package root directory for serving UI assets
42
- const __filename = fileURLToPath(import.meta.url);
43
- const __dirname = dirname(__filename);
42
+ const _filename = fileURLToPath(import.meta.url);
43
+ const _dirname = dirname(_filename);
44
44
  // Handle both src/core and dist/src/core paths - go up to find package root
45
45
  // - src/core/control-panel.ts → go up 2 levels to package root
46
46
  // - dist/src/core/control-panel.js → go up 3 levels to package root
47
- const packageRoot = __dirname.includes('/dist/src/')
48
- ? join(__dirname, '..', '..', '..') // dist/src/core → package root (3 levels)
49
- : join(__dirname, '..', '..'); // src/core → package root (2 levels)
47
+ const packageRoot = _dirname.includes('/dist/src/')
48
+ ? join(_dirname, '..', '..', '..') // dist/src/core → package root (3 levels)
49
+ : join(_dirname, '..', '..'); // src/core → package root (2 levels)
50
50
  const uiDistPath = join(packageRoot, 'dist-ui');
51
51
 
52
52
  // Read @qwickapps/server package version
@@ -21,6 +21,7 @@
21
21
  */
22
22
 
23
23
  import type { Application } from 'express';
24
+ import { createServer } from 'http';
24
25
  import type { IncomingMessage, ServerResponse, Server } from 'http';
25
26
  import type { Socket } from 'net';
26
27
  import type { Duplex } from 'stream';
@@ -35,13 +36,13 @@ import { resolve, join, dirname } from 'path';
35
36
  import { fileURLToPath } from 'url';
36
37
 
37
38
  // Get QwickApps Server version from package.json
38
- const __filename = fileURLToPath(import.meta.url);
39
- const __dirname = dirname(__filename);
39
+ const _filename = fileURLToPath(import.meta.url);
40
+ const _dirname = dirname(_filename);
40
41
 
41
42
  // Find package.json by walking up directories
42
43
  // Recommended approach for libraries that work in both source and compiled contexts
43
44
  function findPackageJson(): string {
44
- let currentDir = __dirname;
45
+ let currentDir = _dirname;
45
46
  // Walk up max 5 levels looking for the correct package.json
46
47
  for (let i = 0; i < 5; i++) {
47
48
  try {
@@ -1075,7 +1076,7 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1075
1076
 
1076
1077
  const logger = config.logger || getControlPanelLogger('Gateway');
1077
1078
 
1078
- // Port configuration - new scheme: 3000 gateway, 3001 cpanel, 3002+ apps
1079
+ // Port configuration - gateway with integrated control panel, apps on 3001+
1079
1080
  const gatewayPort = config.port || parseInt(process.env.GATEWAY_PORT || process.env.PORT || '3000', 10);
1080
1081
  const nodeEnv = process.env.NODE_ENV || 'development';
1081
1082
  const version = config.version || process.env.npm_package_version || '1.0.0';
@@ -1084,7 +1085,6 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1084
1085
  const cpConfig = config.controlPanel ?? { enabled: true };
1085
1086
  const cpEnabled = cpConfig.enabled !== false;
1086
1087
  const cpPath = cpConfig.path || '/cpanel';
1087
- const cpPort = cpConfig.port || 3001;
1088
1088
 
1089
1089
  // Create gateway Express app
1090
1090
  const app = express();
@@ -1279,6 +1279,7 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1279
1279
 
1280
1280
  // 1. Start internal control panel if enabled
1281
1281
  if (cpEnabled) {
1282
+ const cpPort = parseInt(process.env.CPANEL_PORT || String(gatewayPort + 1), 10);
1282
1283
  logger.debug(`Starting control panel on port ${cpPort}...`);
1283
1284
 
1284
1285
  controlPanelInstance = createControlPanel({
@@ -1289,7 +1290,7 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1289
1290
  logoIconUrl: config.logoIconUrl,
1290
1291
  branding: config.branding,
1291
1292
  cors: config.corsOrigins ? { origins: config.corsOrigins } : undefined,
1292
- mountPath: '/', // Control panel runs at / internally
1293
+ mountPath: cpPath, // Control panel runs at same path as gateway mount (no path translation needed)
1293
1294
  guard: cpConfig.guard,
1294
1295
  customUiPath: cpConfig.customUiPath,
1295
1296
  links: cpConfig.links,
@@ -1300,20 +1301,132 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1300
1301
 
1301
1302
  await controlPanelInstance.start();
1302
1303
  logger.debug(`Control panel started on port ${cpPort}`);
1304
+
1305
+ // Check if any plugins require maintenance
1306
+ const pluginsNeedingMaintenance = controlPanelInstance.getPluginRegistry().getPluginsNeedingMaintenance();
1307
+ if (pluginsNeedingMaintenance.length > 0) {
1308
+ logger.warn(
1309
+ `${pluginsNeedingMaintenance.length} plugin(s) require maintenance`,
1310
+ { plugins: pluginsNeedingMaintenance.map(p => p.pluginId) }
1311
+ );
1312
+ }
1313
+
1314
+ // 2. Setup control panel proxies (BEFORE server.listen)
1315
+ // Control panel runs at / internally with APIs at /api and UI at /
1316
+ // We proxy both /api and /cpanel to the control panel
1317
+
1318
+ // Proxy control panel APIs at {cpPath}/api
1319
+ // IMPORTANT: Express strips the API path prefix before passing to middleware,
1320
+ // so we need pathRewrite to reconstruct the full path for the control panel
1321
+ const cpApiPath = `${cpPath}/api`;
1322
+ const apiProxy = createProxyMiddleware({
1323
+ target: `http://localhost:${cpPort}`,
1324
+ changeOrigin: true,
1325
+ pathRewrite: (path) => `/api${path}`, // Express removed {cpPath}/api, add back just /api (control panel APIs are at /api/*)
1326
+ on: {
1327
+ proxyReq: (proxyReq, req) => {
1328
+ // Set X-Forwarded-Prefix so control panel knows its public mount path
1329
+ proxyReq.setHeader('X-Forwarded-Prefix', cpPath);
1330
+ logger.debug(`[API Proxy] Forwarding ${req.method} ${req.url} to control panel`);
1331
+ },
1332
+ proxyRes: (proxyRes, req) => {
1333
+ logger.debug(`[API Proxy] Response for ${req.url}: ${proxyRes.statusCode} ${proxyRes.headers['content-type']}`);
1334
+ },
1335
+ error: (err: Error, req: IncomingMessage, res: ServerResponse | Socket) => {
1336
+ logger.error(`[API Proxy] Error for ${req.url}`, { error: err.message });
1337
+ if (res && 'writeHead' in res && !res.headersSent) {
1338
+ res.writeHead(503, { 'Content-Type': 'application/json' });
1339
+ res.end(JSON.stringify({
1340
+ error: 'Service Unavailable',
1341
+ message: 'The control panel API is currently unavailable.',
1342
+ details: nodeEnv === 'development' ? err.message : undefined,
1343
+ }));
1344
+ }
1345
+ },
1346
+ },
1347
+ });
1348
+ app.use(cpApiPath, apiProxy);
1349
+ logger.debug(`Setting up proxy: ${cpApiPath} -> http://localhost:${cpPort}/api`);
1350
+ mountedApps.push({ path: cpApiPath, type: 'proxy', target: `http://localhost:${cpPort}` });
1303
1351
  }
1304
1352
 
1305
- // 2. Create HTTP server
1306
- server = app.listen(gatewayPort);
1353
+ // 3. Create HTTP server (but don't listen yet - need server object for WS)
1354
+ server = createServer(app);
1307
1355
 
1308
- // 3. Setup mounted apps (proxy and static)
1356
+ // 4. Setup control panel UI proxy (needs server for WebSocket handling)
1357
+ if (cpEnabled) {
1358
+ const cpPort = parseInt(process.env.CPANEL_PORT || String(gatewayPort + 1), 10);
1359
+
1360
+ // Proxy /cpanel to control panel UI
1361
+ // Express strips cpPath prefix, so we need custom proxy to add it back
1362
+ const cpUiProxy = createProxyMiddleware({
1363
+ target: `http://localhost:${cpPort}`,
1364
+ changeOrigin: true,
1365
+ ws: true,
1366
+ pathRewrite: (path) => `${cpPath}${path}`, // Express removed cpPath, add it back
1367
+ on: {
1368
+ proxyReq: (proxyReq) => {
1369
+ // Set X-Forwarded-Prefix so control panel knows its public mount path
1370
+ proxyReq.setHeader('X-Forwarded-Prefix', cpPath);
1371
+ },
1372
+ error: (err: Error, req: IncomingMessage, res: ServerResponse | Socket) => {
1373
+ logger.error(`[Control Panel UI Proxy] Error for ${req.url}`, { error: err.message });
1374
+ if (res && 'writeHead' in res && !res.headersSent) {
1375
+ res.writeHead(503, { 'Content-Type': 'text/html' });
1376
+ res.end('<h1>Control Panel Unavailable</h1><p>The control panel service is not responding.</p>');
1377
+ }
1378
+ },
1379
+ },
1380
+ });
1381
+ app.use(cpPath, cpUiProxy);
1382
+
1383
+ // WebSocket upgrade handling for control panel
1384
+ server!.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {
1385
+ if (req.url?.startsWith(cpPath)) {
1386
+ cpUiProxy.upgrade?.(req, socket as Socket, head);
1387
+ }
1388
+ });
1389
+
1390
+ mountedApps.push({ path: cpPath, type: 'proxy', target: `http://localhost:${cpPort}` });
1391
+ }
1392
+
1393
+ // 5. Setup mounted apps (proxy and static)
1309
1394
  const apps = config.apps || [];
1310
1395
 
1311
- // Add control panel as a proxy app if enabled
1396
+ // If plugins need maintenance, add root-level maintenance page
1397
+ // but keep /cpanel and /diagnostics accessible
1312
1398
  if (cpEnabled) {
1313
- setupProxyApp({
1314
- path: cpPath,
1315
- source: { type: 'proxy', target: `http://localhost:${cpPort}` },
1316
- }, server);
1399
+ const pluginsNeedingMaintenance = controlPanelInstance!.getPluginRegistry().getPluginsNeedingMaintenance();
1400
+ if (pluginsNeedingMaintenance.length > 0) {
1401
+ logger.info('Adding maintenance page at root path due to plugin failures');
1402
+
1403
+ // Add middleware BEFORE frontend setup to intercept root requests
1404
+ app.use((req, res, next) => {
1405
+ // Allow access to control panel, API, and diagnostics
1406
+ if (req.path.startsWith(cpPath) || req.path.startsWith('/api') || req.path.startsWith('/health') || req.path.startsWith('/ping')) {
1407
+ return next();
1408
+ }
1409
+
1410
+ // Show maintenance page for all other requests to root
1411
+ if (req.path === '/' || (!req.path.startsWith(cpPath) && !req.path.startsWith('/api'))) {
1412
+ const maintenanceConfig: MaintenanceConfig = {
1413
+ enabled: true,
1414
+ title: 'System Maintenance Required',
1415
+ message: `${pluginsNeedingMaintenance.length} system component(s) require attention before the service can start normally.`,
1416
+ contactUrl: `${cpPath}/diagnostics/maintenance`,
1417
+ };
1418
+ const html = generateMaintenancePageHtml(
1419
+ config.productName,
1420
+ maintenanceConfig,
1421
+ config.productName
1422
+ );
1423
+ res.status(503).type('html').send(html);
1424
+ return;
1425
+ }
1426
+
1427
+ next();
1428
+ });
1429
+ }
1317
1430
  }
1318
1431
 
1319
1432
  // Setup additional apps
@@ -1325,9 +1438,16 @@ export function createGateway(config: GatewayConfig): GatewayInstance {
1325
1438
  }
1326
1439
  }
1327
1440
 
1328
- // 4. Setup frontend app at root
1441
+ // 6. Setup frontend app at root
1329
1442
  setupFrontendApp();
1330
1443
 
1444
+ // 7. Start listening (LAST STEP - after all middleware is registered)
1445
+ await new Promise<void>((resolve) => {
1446
+ server!.listen(gatewayPort, () => {
1447
+ resolve();
1448
+ });
1449
+ });
1450
+
1331
1451
  // Log startup info
1332
1452
  const authInfo = cpConfig.guard?.type === 'basic'
1333
1453
  ? `(auth: ${cpConfig.guard.username})`
@@ -76,6 +76,48 @@ export interface PluginScope {
76
76
  category?: 'read' | 'write' | 'admin';
77
77
  }
78
78
 
79
+ /**
80
+ * Result of running a maintenance action
81
+ */
82
+ export interface MaintenanceActionResult {
83
+ /** Whether the action succeeded */
84
+ success: boolean;
85
+ /** Error message if action failed */
86
+ error?: string;
87
+ /** Optional message with additional details */
88
+ message?: string;
89
+ }
90
+
91
+ /**
92
+ * Maintenance action that can be executed to fix a plugin issue
93
+ */
94
+ export interface MaintenanceAction {
95
+ /** Unique action identifier (e.g., 'truncate-table', 'migrate-data') */
96
+ id: string;
97
+ /** Human-readable action name */
98
+ name: string;
99
+ /** Description of what the action does */
100
+ description: string;
101
+ /** Whether this action is destructive (shows warning in UI) */
102
+ destructive?: boolean;
103
+ /** Handler function that performs the action */
104
+ handler: () => Promise<MaintenanceActionResult>;
105
+ }
106
+
107
+ /**
108
+ * Information about a plugin that requires maintenance
109
+ */
110
+ export interface PluginMaintenanceInfo {
111
+ /** Plugin ID that needs maintenance */
112
+ pluginId: string;
113
+ /** Error message describing what's wrong */
114
+ error: string;
115
+ /** Available actions to fix the issue */
116
+ actions: MaintenanceAction[];
117
+ /** Optional suggestion for which action to run */
118
+ recommendedAction?: string;
119
+ }
120
+
79
121
  /**
80
122
  * The Plugin interface - simple lifecycle with event handling
81
123
  */
@@ -364,6 +406,25 @@ export interface PluginRegistry {
364
406
  /** Register a health check */
365
407
  registerHealthCheck(check: HealthCheck): void;
366
408
 
409
+ // ---------------------------------------------------------------------------
410
+ // Maintenance actions
411
+ // ---------------------------------------------------------------------------
412
+
413
+ /** Register maintenance actions for a plugin that failed to start */
414
+ registerMaintenance(info: PluginMaintenanceInfo): void;
415
+
416
+ /** Get maintenance info for a plugin (if it needs maintenance) */
417
+ getMaintenanceInfo(pluginId: string): PluginMaintenanceInfo | undefined;
418
+
419
+ /** Get all plugins that need maintenance */
420
+ getPluginsNeedingMaintenance(): PluginMaintenanceInfo[];
421
+
422
+ /** Run a maintenance action for a plugin */
423
+ runMaintenanceAction(pluginId: string, actionId: string): Promise<MaintenanceActionResult>;
424
+
425
+ /** Clear maintenance state for a plugin (used after successful fix) */
426
+ clearMaintenance(pluginId: string): void;
427
+
367
428
  // ---------------------------------------------------------------------------
368
429
  // Express integration
369
430
  // ---------------------------------------------------------------------------
@@ -406,6 +467,7 @@ export class PluginRegistryImpl implements PluginRegistry {
406
467
  private pluginConfigs = new Map<string, PluginConfig>();
407
468
  private pluginSlugs = new Map<string, string>(); // pluginId -> slug
408
469
  private currentPlugin: string | null = null; // Track plugin during onStart
470
+ private maintenanceActions = new Map<string, PluginMaintenanceInfo>(); // pluginId -> maintenance info
409
471
 
410
472
  private routes: RouteDefinition[] = [];
411
473
  private menuItems: MenuContribution[] = [];
@@ -829,6 +891,115 @@ export class PluginRegistryImpl implements PluginRegistry {
829
891
  await this.stopPlugin(pluginId);
830
892
  }
831
893
  }
894
+
895
+ // ---------------------------------------------------------------------------
896
+ // Maintenance actions
897
+ // ---------------------------------------------------------------------------
898
+
899
+ /**
900
+ * Register maintenance actions for a plugin that failed to start
901
+ */
902
+ registerMaintenance(info: PluginMaintenanceInfo): void {
903
+ this.maintenanceActions.set(info.pluginId, info);
904
+ this.logger.warn(
905
+ `Plugin ${info.pluginId} requires maintenance: ${info.error}`,
906
+ { availableActions: info.actions.map(a => a.id) }
907
+ );
908
+ }
909
+
910
+ /**
911
+ * Get maintenance info for a plugin (if it needs maintenance)
912
+ */
913
+ getMaintenanceInfo(pluginId: string): PluginMaintenanceInfo | undefined {
914
+ return this.maintenanceActions.get(pluginId);
915
+ }
916
+
917
+ /**
918
+ * Get all plugins that need maintenance
919
+ */
920
+ getPluginsNeedingMaintenance(): PluginMaintenanceInfo[] {
921
+ return Array.from(this.maintenanceActions.values());
922
+ }
923
+
924
+ /**
925
+ * Run a maintenance action for a plugin
926
+ */
927
+ async runMaintenanceAction(pluginId: string, actionId: string): Promise<MaintenanceActionResult> {
928
+ const maintenanceInfo = this.maintenanceActions.get(pluginId);
929
+
930
+ if (!maintenanceInfo) {
931
+ return {
932
+ success: false,
933
+ error: `Plugin ${pluginId} does not have any registered maintenance actions`,
934
+ };
935
+ }
936
+
937
+ const action = maintenanceInfo.actions.find(a => a.id === actionId);
938
+
939
+ if (!action) {
940
+ return {
941
+ success: false,
942
+ error: `Action ${actionId} not found for plugin ${pluginId}`,
943
+ };
944
+ }
945
+
946
+ this.logger.info(`Running maintenance action ${actionId} for plugin ${pluginId}`);
947
+
948
+ try {
949
+ const result = await action.handler();
950
+
951
+ if (result.success) {
952
+ this.logger.info(`Maintenance action ${actionId} succeeded for plugin ${pluginId}`);
953
+
954
+ // Try to restart the plugin after successful maintenance action
955
+ const plugin = this.plugins.get(pluginId);
956
+ const config = this.pluginConfigs.get(pluginId);
957
+
958
+ if (plugin && config) {
959
+ this.logger.info(`Attempting to restart plugin ${pluginId} after maintenance`);
960
+ const started = await this.startPlugin(plugin, config);
961
+
962
+ if (started) {
963
+ // Plugin started successfully, clear maintenance state
964
+ this.clearMaintenance(pluginId);
965
+ return {
966
+ success: true,
967
+ message: `${result.message || 'Action completed successfully'}. Plugin restarted successfully.`,
968
+ };
969
+ } else {
970
+ // Plugin still failed to start
971
+ const pluginInfo = this.listPlugins().find(p => p.id === pluginId);
972
+ return {
973
+ success: false,
974
+ error: `Action completed but plugin failed to restart: ${pluginInfo?.error || 'Unknown error'}`,
975
+ };
976
+ }
977
+ }
978
+
979
+ return result;
980
+ } else {
981
+ this.logger.error(`Maintenance action ${actionId} failed for plugin ${pluginId}`, { error: result.error });
982
+ return result;
983
+ }
984
+ } catch (error) {
985
+ const errorMessage = error instanceof Error ? error.message : String(error);
986
+ this.logger.error(`Maintenance action ${actionId} threw exception for plugin ${pluginId}`, { error: errorMessage });
987
+ return {
988
+ success: false,
989
+ error: `Action threw exception: ${errorMessage}`,
990
+ };
991
+ }
992
+ }
993
+
994
+ /**
995
+ * Clear maintenance state for a plugin (used after successful fix)
996
+ */
997
+ clearMaintenance(pluginId: string): void {
998
+ const wasPresent = this.maintenanceActions.delete(pluginId);
999
+ if (wasPresent) {
1000
+ this.logger.info(`Cleared maintenance state for plugin ${pluginId}`);
1001
+ }
1002
+ }
832
1003
  }
833
1004
 
834
1005
  // =============================================================================
package/src/index.ts CHANGED
@@ -310,6 +310,8 @@ export type {
310
310
  HealthPluginConfig,
311
311
  LogsPluginConfig,
312
312
  MaintenancePluginConfig,
313
+ SeedTask,
314
+ SeedTaskHandler,
313
315
  ConfigPluginConfig,
314
316
  DiagnosticsPluginConfig,
315
317
  FrontendAppPluginConfig,
@@ -100,7 +100,64 @@ export function createApiKeysPlugin(config: Partial<ApiKeysPluginConfig> = {}):
100
100
  log('Starting API keys plugin');
101
101
 
102
102
  // Initialize the store (creates tables and RLS policies if needed)
103
- await store.initialize();
103
+ const initResult = await store.initialize();
104
+
105
+ if (!initResult.success) {
106
+ logger.error('API keys store initialization failed', { error: initResult.error });
107
+
108
+ // If migration is required, register maintenance actions
109
+ if (initResult.requiresMaintenance) {
110
+ registry.registerMaintenance({
111
+ pluginId: 'api-keys',
112
+ error: initResult.error || 'Store initialization failed',
113
+ actions: [
114
+ {
115
+ id: 'truncate-api-keys-table',
116
+ name: 'Clear API Keys Table',
117
+ description: 'Delete all API keys from the database. This allows the migration to proceed by removing old data.',
118
+ destructive: true,
119
+ handler: async () => {
120
+ try {
121
+ const pool = getPostgres().getPool();
122
+ await pool.query('TRUNCATE TABLE "public"."api_keys" CASCADE');
123
+ logger.info('API keys table truncated successfully');
124
+ return {
125
+ success: true,
126
+ message: 'API keys table cleared. Plugin will retry initialization.',
127
+ };
128
+ } catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : String(error);
130
+ logger.error('Failed to truncate API keys table', { error: errorMessage });
131
+ return {
132
+ success: false,
133
+ error: `Failed to truncate table: ${errorMessage}`,
134
+ };
135
+ }
136
+ },
137
+ },
138
+ ],
139
+ recommendedAction: 'truncate-api-keys-table',
140
+ });
141
+
142
+ logger.warn('API keys plugin requires maintenance - registered recovery actions');
143
+ }
144
+
145
+ // Register unhealthy health check
146
+ registry.registerHealthCheck({
147
+ name: 'api-keys-store',
148
+ type: 'custom',
149
+ check: async () => ({
150
+ healthy: false,
151
+ details: {
152
+ error: initResult.error || 'Initialization failed',
153
+ requiresMaintenance: initResult.requiresMaintenance,
154
+ },
155
+ }),
156
+ });
157
+
158
+ return; // Don't continue startup
159
+ }
160
+
104
161
  log('API keys store initialized');
105
162
 
106
163
  // Initialize optional Phase 2 stores
@@ -31,6 +31,7 @@ export type {
31
31
  UpdateApiKeyParams,
32
32
  PostgresApiKeyStoreConfig,
33
33
  ApiKeysApiConfig,
34
+ StoreInitializationResult,
34
35
  } from './types.js';
35
36
 
36
37
  // Zod schemas