@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.
- package/CHANGELOG.md +33 -0
- package/dist/src/core/control-panel.js +5 -5
- package/dist/src/core/control-panel.js.map +1 -1
- package/dist/src/core/gateway.d.ts.map +1 -1
- package/dist/src/core/gateway.js +117 -15
- package/dist/src/core/gateway.js.map +1 -1
- package/dist/src/core/plugin-registry.d.ts +70 -0
- package/dist/src/core/plugin-registry.d.ts.map +1 -1
- package/dist/src/core/plugin-registry.js +94 -0
- package/dist/src/core/plugin-registry.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
- package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
- package/dist/src/plugins/api-keys/index.d.ts +1 -1
- package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/index.js.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
- package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
- package/dist/src/plugins/api-keys/types.d.ts +13 -1
- package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
- package/dist/src/plugins/api-keys/types.js.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/src/plugins/diagnostics-plugin.js +73 -0
- package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/src/plugins/index.d.ts +1 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
- package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
- package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
- package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedList.js +39 -14
- package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
- package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
- package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
- package/dist/src/plugins/maintenance/seed-executor.js +53 -17
- package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
- package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
- package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
- package/dist/src/plugins/maintenance-plugin.js +222 -34
- package/dist/src/plugins/maintenance-plugin.js.map +1 -1
- package/dist/src/plugins/postgres-plugin.d.ts +12 -0
- package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/src/plugins/postgres-plugin.js +319 -5
- package/dist/src/plugins/postgres-plugin.js.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
- package/dist/ui/src/components/ControlPanelApp.js +4 -3
- package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
- package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
- package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
- package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
- package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
- package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
- package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
- package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
- package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
- package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
- package/dist/ui/src/dashboard/widgets/index.js +1 -0
- package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
- package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
- package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/index.js +3735 -3187
- package/dist-ui-lib/index.js.map +1 -1
- package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
- package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
- package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
- package/package.json +2 -2
- package/src/core/control-panel.ts +5 -5
- package/src/core/gateway.ts +135 -15
- package/src/core/plugin-registry.ts +171 -0
- package/src/index.ts +2 -0
- package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
- package/src/plugins/api-keys/index.ts +1 -0
- package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
- package/src/plugins/api-keys/types.ts +14 -1
- package/src/plugins/diagnostics-plugin.ts +77 -0
- package/src/plugins/index.ts +1 -1
- package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
- package/src/plugins/maintenance/SeedList.tsx +85 -38
- package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
- package/src/plugins/maintenance/seed-executor.ts +56 -17
- package/src/plugins/maintenance-plugin.ts +267 -36
- package/src/plugins/postgres-plugin.ts +410 -5
- package/ui/src/App.tsx +3 -3
- package/ui/src/components/ControlPanelApp.tsx +4 -3
- package/ui/src/dashboard/builtInWidgets.tsx +3 -0
- package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
- package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
- package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
- package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
- package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
- package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
- package/ui/src/dashboard/widgets/index.ts +1 -0
- package/dist-ui/assets/index-0gzisPdy.js +0 -528
- 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.
|
|
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.
|
|
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
|
|
43
|
-
const
|
|
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 =
|
|
48
|
-
? join(
|
|
49
|
-
: join(
|
|
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
|
package/src/core/gateway.ts
CHANGED
|
@@ -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
|
|
39
|
-
const
|
|
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 =
|
|
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 -
|
|
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:
|
|
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
|
-
//
|
|
1306
|
-
server = app
|
|
1353
|
+
// 3. Create HTTP server (but don't listen yet - need server object for WS)
|
|
1354
|
+
server = createServer(app);
|
|
1307
1355
|
|
|
1308
|
-
//
|
|
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
|
-
//
|
|
1396
|
+
// If plugins need maintenance, add root-level maintenance page
|
|
1397
|
+
// but keep /cpanel and /diagnostics accessible
|
|
1312
1398
|
if (cpEnabled) {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
//
|
|
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
|
@@ -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
|