@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
@@ -90,6 +90,26 @@ export interface PostgresPluginConfig {
90
90
 
91
91
  /** Called on pool errors */
92
92
  onError?: (error: Error) => void;
93
+
94
+ // Database initialization options
95
+
96
+ /** Admin user for database operations (e.g., 'postgres') */
97
+ adminUser?: string;
98
+
99
+ /** Admin password for database operations */
100
+ adminPassword?: string;
101
+
102
+ /** Admin database to connect to for operations (default: 'postgres') */
103
+ adminDatabase?: string;
104
+
105
+ /** Database name to create/ensure exists (parsed from url if not provided) */
106
+ databaseName?: string;
107
+
108
+ /** User who should own the database (parsed from url if not provided) */
109
+ databaseOwner?: string;
110
+
111
+ /** Automatically initialize database if connection fails (default: true if admin credentials provided) */
112
+ autoInitialize?: boolean;
93
113
  }
94
114
 
95
115
  /**
@@ -140,6 +160,127 @@ export interface PostgresInstance {
140
160
  // Global registry of PostgreSQL instances by name
141
161
  const instances = new Map<string, PostgresInstance>();
142
162
 
163
+ /**
164
+ * Parse database connection URL to extract components
165
+ */
166
+ function parseConnectionUrl(url: string): {
167
+ user: string;
168
+ password: string;
169
+ host: string;
170
+ port: number;
171
+ database: string;
172
+ } {
173
+ const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
174
+ if (!match) {
175
+ throw new Error('Invalid PostgreSQL connection URL format');
176
+ }
177
+ return {
178
+ user: match[1],
179
+ password: match[2],
180
+ host: match[3],
181
+ port: parseInt(match[4], 10),
182
+ database: match[5],
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Helper to create an admin pool for database operations
188
+ */
189
+ function createAdminPool(config: {
190
+ adminUser: string;
191
+ adminPassword: string;
192
+ host: string;
193
+ port: number;
194
+ adminDatabase?: string;
195
+ }): pg.Pool {
196
+ return new Pool({
197
+ user: config.adminUser,
198
+ password: config.adminPassword,
199
+ host: config.host,
200
+ port: config.port,
201
+ database: config.adminDatabase || 'postgres',
202
+ max: 1,
203
+ idleTimeoutMillis: 5000,
204
+ connectionTimeoutMillis: 5000,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Ensure database user exists with password
210
+ */
211
+ async function ensureUserExists(
212
+ adminPool: pg.Pool,
213
+ user: string,
214
+ password: string
215
+ ): Promise<void> {
216
+ const result = await adminPool.query(
217
+ `SELECT 1 FROM pg_roles WHERE rolname = $1`,
218
+ [user]
219
+ );
220
+
221
+ if (result.rows.length === 0) {
222
+ await adminPool.query(
223
+ `CREATE USER ${user} WITH PASSWORD '${password}'`
224
+ );
225
+ } else {
226
+ await adminPool.query(
227
+ `ALTER USER ${user} WITH PASSWORD '${password}'`
228
+ );
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Ensure database exists with correct owner
234
+ */
235
+ async function ensureDatabaseExists(
236
+ adminPool: pg.Pool,
237
+ database: string,
238
+ owner: string
239
+ ): Promise<void> {
240
+ const result = await adminPool.query(
241
+ `SELECT 1 FROM pg_database WHERE datname = $1`,
242
+ [database]
243
+ );
244
+
245
+ if (result.rows.length === 0) {
246
+ await adminPool.query(
247
+ `CREATE DATABASE ${database} OWNER ${owner}`
248
+ );
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Grant all permissions to user on database
254
+ */
255
+ async function grantPermissions(
256
+ adminPool: pg.Pool,
257
+ database: string,
258
+ user: string
259
+ ): Promise<void> {
260
+ const tempPool = new Pool({
261
+ user: adminPool.options.user as string,
262
+ password: adminPool.options.password as string,
263
+ host: adminPool.options.host as string,
264
+ port: adminPool.options.port as number,
265
+ database,
266
+ max: 1,
267
+ idleTimeoutMillis: 5000,
268
+ connectionTimeoutMillis: 5000,
269
+ });
270
+
271
+ try {
272
+ await tempPool.query(`
273
+ GRANT ALL ON SCHEMA public TO ${user};
274
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${user};
275
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${user};
276
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${user};
277
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${user};
278
+ `);
279
+ } finally {
280
+ await tempPool.end();
281
+ }
282
+ }
283
+
143
284
  /**
144
285
  * Get a PostgreSQL instance by name
145
286
  *
@@ -305,15 +446,279 @@ export function createPostgresPlugin(
305
446
  const instance = createInstance();
306
447
  instances.set(instanceName, instance);
307
448
 
308
- // Test connection
449
+ // Register maintenance widget FIRST (before connection attempt)
450
+ // This ensures the widget is available even if database connection fails
451
+ registry.addWidget({
452
+ id: `postgres-operations-${instanceName}`,
453
+ title: `Database Operations (${instanceName})`,
454
+ component: 'DatabaseOperationsWidget',
455
+ type: 'maintenance',
456
+ priority: 50,
457
+ showByDefault: true,
458
+ pluginId: pluginId,
459
+ });
460
+
461
+ // Three-phase initialization: connect → auto-repair → error state
462
+
463
+ // PHASE 1: Try to connect with DATABASE_URI
309
464
  try {
310
465
  await instance.query('SELECT 1');
311
- logger.debug(`PostgreSQL "${instanceName}" connected`);
312
- } catch (err) {
313
- logger.error(`PostgreSQL "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
314
- throw err;
466
+ logger.info(`PostgreSQL "${instanceName}" connected successfully`);
467
+ } catch (connectionError) {
468
+ const errorMsg = connectionError instanceof Error ? connectionError.message : String(connectionError);
469
+ logger.warn(`PostgreSQL "${instanceName}" connection failed: ${errorMsg}`);
470
+
471
+ // PHASE 2: Auto-repair if admin credentials provided
472
+ const shouldAutoRepair =
473
+ config.adminUser &&
474
+ config.adminPassword &&
475
+ config.autoInitialize !== false &&
476
+ config.url;
477
+
478
+ if (shouldAutoRepair) {
479
+ logger.info(`Attempting auto-repair for "${instanceName}"...`);
480
+
481
+ try {
482
+ const connParams = parseConnectionUrl(config.url!);
483
+ const adminPool = createAdminPool({
484
+ adminUser: config.adminUser!,
485
+ adminPassword: config.adminPassword!,
486
+ host: connParams.host,
487
+ port: connParams.port,
488
+ adminDatabase: config.adminDatabase,
489
+ });
490
+
491
+ try {
492
+ // Ensure user exists
493
+ logger.debug(`Ensuring user "${connParams.user}" exists...`);
494
+ await ensureUserExists(adminPool, connParams.user, connParams.password);
495
+
496
+ // Ensure database exists
497
+ logger.debug(`Ensuring database "${connParams.database}" exists...`);
498
+ await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
499
+
500
+ // Grant permissions
501
+ logger.debug(`Granting permissions to "${connParams.user}" on "${connParams.database}"...`);
502
+ await grantPermissions(adminPool, connParams.database, connParams.user);
503
+
504
+ logger.info(`Auto-repair completed successfully for "${instanceName}"`);
505
+
506
+ // Try connection again after repair
507
+ await instance.query('SELECT 1');
508
+ logger.info(`PostgreSQL "${instanceName}" connected after auto-repair`);
509
+ } finally {
510
+ await adminPool.end();
511
+ }
512
+ } catch (repairError) {
513
+ const repairMsg = repairError instanceof Error ? repairError.message : String(repairError);
514
+ // Log error but don't throw - allow plugin to continue starting
515
+ // This ensures the widget and API routes are available for manual database initialization
516
+ logger.error(
517
+ `PostgreSQL connection failed and auto-repair unsuccessful. ` +
518
+ `Original error: ${errorMsg}. Repair error: ${repairMsg}. ` +
519
+ `Use the maintenance UI to manually initialize the database.`
520
+ );
521
+ }
522
+ } else {
523
+ // PHASE 3: No auto-repair available, remain in error state
524
+ const missingConfig = [];
525
+ if (!config.adminUser) missingConfig.push('adminUser');
526
+ if (!config.adminPassword) missingConfig.push('adminPassword');
527
+
528
+ const hint = missingConfig.length > 0
529
+ ? ` Provide ${missingConfig.join(', ')} in config to enable auto-repair.`
530
+ : ' Set autoInitialize=true to enable auto-repair.';
531
+
532
+ // Log error but don't throw - allow plugin to continue starting
533
+ // This ensures the widget and API routes are available for manual database initialization
534
+ logger.error(
535
+ `PostgreSQL connection failed: ${errorMsg}.${hint} ` +
536
+ `Use the maintenance UI to manually initialize the database.`
537
+ );
538
+ }
315
539
  }
316
540
 
541
+ // Register API routes for database operations
542
+ registry.addRoute({
543
+ method: 'get',
544
+ path: '/status',
545
+ pluginId: pluginId,
546
+ handler: async (req: import('express').Request, res: import('express').Response) => {
547
+ try {
548
+ const requestedInstance = (req.query.instance as string) || 'default';
549
+ const targetInstance = instances.get(requestedInstance);
550
+
551
+ if (!targetInstance) {
552
+ return res.status(404).json({
553
+ status: 'error',
554
+ connected: false,
555
+ errorMessage: `PostgreSQL instance "${requestedInstance}" not found`,
556
+ autoInitializeEnabled: false,
557
+ adminCredentialsProvided: false,
558
+ });
559
+ }
560
+
561
+ let connParams: ReturnType<typeof parseConnectionUrl> | null = null;
562
+ if (config.url) {
563
+ try {
564
+ connParams = parseConnectionUrl(config.url);
565
+ } catch (err) {
566
+ // URL parsing failed, ignore
567
+ }
568
+ }
569
+
570
+ try {
571
+ await targetInstance.query('SELECT 1');
572
+ res.json({
573
+ status: 'healthy',
574
+ connected: true,
575
+ database: connParams?.database,
576
+ user: connParams?.user,
577
+ host: connParams?.host,
578
+ port: connParams?.port,
579
+ autoInitializeEnabled: config.autoInitialize !== false,
580
+ adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
581
+ });
582
+ } catch (err) {
583
+ res.json({
584
+ status: 'error',
585
+ connected: false,
586
+ database: connParams?.database,
587
+ user: connParams?.user,
588
+ host: connParams?.host,
589
+ port: connParams?.port,
590
+ errorMessage: err instanceof Error ? err.message : String(err),
591
+ autoInitializeEnabled: config.autoInitialize !== false,
592
+ adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
593
+ });
594
+ }
595
+ } catch (err) {
596
+ res.status(500).json({
597
+ status: 'error',
598
+ connected: false,
599
+ errorMessage: err instanceof Error ? err.message : 'Unknown error',
600
+ autoInitializeEnabled: false,
601
+ adminCredentialsProvided: false,
602
+ });
603
+ }
604
+ },
605
+ });
606
+
607
+ registry.addRoute({
608
+ method: 'post',
609
+ path: '/initialize',
610
+ pluginId: pluginId,
611
+ handler: async (req: import('express').Request, res: import('express').Response) => {
612
+ try {
613
+ const { instance: requestedInstance, adminUser, adminPassword } = req.body;
614
+ const targetInstance = requestedInstance || 'default';
615
+
616
+ if (!instances.has(targetInstance)) {
617
+ return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
618
+ }
619
+
620
+ if (!config.url) {
621
+ return res.status(400).json({ message: 'No database URL configured' });
622
+ }
623
+
624
+ const connParams = parseConnectionUrl(config.url);
625
+ const effectiveAdminUser = adminUser || config.adminUser;
626
+ const effectiveAdminPassword = adminPassword || config.adminPassword;
627
+
628
+ if (!effectiveAdminUser || !effectiveAdminPassword) {
629
+ return res.status(400).json({
630
+ message: 'Admin credentials required. Provide adminUser and adminPassword.',
631
+ });
632
+ }
633
+
634
+ const adminPool = createAdminPool({
635
+ adminUser: effectiveAdminUser,
636
+ adminPassword: effectiveAdminPassword,
637
+ host: connParams.host,
638
+ port: connParams.port,
639
+ adminDatabase: config.adminDatabase,
640
+ });
641
+
642
+ try {
643
+ await ensureUserExists(adminPool, connParams.user, connParams.password);
644
+ await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
645
+ await grantPermissions(adminPool, connParams.database, connParams.user);
646
+
647
+ logger.info(`Database "${connParams.database}" initialized successfully`);
648
+ res.json({ message: `Database "${connParams.database}" initialized successfully` });
649
+ } finally {
650
+ await adminPool.end();
651
+ }
652
+ } catch (err) {
653
+ logger.error('Database initialization failed', { error: err });
654
+ res.status(500).json({
655
+ message: err instanceof Error ? err.message : 'Unknown error',
656
+ });
657
+ }
658
+ },
659
+ });
660
+
661
+ registry.addRoute({
662
+ method: 'post',
663
+ path: '/recreate',
664
+ pluginId: pluginId,
665
+ handler: async (req: import('express').Request, res: import('express').Response) => {
666
+ try {
667
+ const { instance: requestedInstance, adminUser, adminPassword } = req.body;
668
+ const targetInstance = requestedInstance || 'default';
669
+
670
+ if (!instances.has(targetInstance)) {
671
+ return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
672
+ }
673
+
674
+ if (!config.url) {
675
+ return res.status(400).json({ message: 'No database URL configured' });
676
+ }
677
+
678
+ const connParams = parseConnectionUrl(config.url);
679
+ const effectiveAdminUser = adminUser || config.adminUser;
680
+ const effectiveAdminPassword = adminPassword || config.adminPassword;
681
+
682
+ if (!effectiveAdminUser || !effectiveAdminPassword) {
683
+ return res.status(400).json({
684
+ message: 'Admin credentials required. Provide adminUser and adminPassword.',
685
+ });
686
+ }
687
+
688
+ const adminPool = createAdminPool({
689
+ adminUser: effectiveAdminUser,
690
+ adminPassword: effectiveAdminPassword,
691
+ host: connParams.host,
692
+ port: connParams.port,
693
+ adminDatabase: config.adminDatabase,
694
+ });
695
+
696
+ try {
697
+ // Drop database if exists
698
+ await adminPool.query(`DROP DATABASE IF EXISTS ${connParams.database}`);
699
+
700
+ // Recreate database
701
+ await adminPool.query(
702
+ `CREATE DATABASE ${connParams.database} OWNER ${connParams.user}`
703
+ );
704
+
705
+ // Grant permissions
706
+ await grantPermissions(adminPool, connParams.database, connParams.user);
707
+
708
+ logger.info(`Database "${connParams.database}" recreated successfully`);
709
+ res.json({ message: `Database "${connParams.database}" recreated successfully` });
710
+ } finally {
711
+ await adminPool.end();
712
+ }
713
+ } catch (err) {
714
+ logger.error('Database recreation failed', { error: err });
715
+ res.status(500).json({
716
+ message: err instanceof Error ? err.message : 'Unknown error',
717
+ });
718
+ }
719
+ },
720
+ });
721
+
317
722
  // Register health check if enabled
318
723
  if (config.healthCheck !== false) {
319
724
  registry.registerHealthCheck({
package/ui/src/App.tsx CHANGED
@@ -71,9 +71,9 @@ declare global {
71
71
  */
72
72
  const basePath = window.__APP_BASE_PATH__ ?? '';
73
73
 
74
- // API routes are always at '/api' regardless of control panel mount path
75
- // The control panel might be mounted at /cpanel, but API is always at /api
76
- api.setBaseUrl('');
74
+ // Set API base URL to match the control panel mount path
75
+ // When proxied through a gateway at /cpanel, API calls need to go to /cpanel/api
76
+ api.setBaseUrl(basePath);
77
77
 
78
78
  // Footer content with QwickApps Server branding
79
79
  const footerContent = (
@@ -147,9 +147,10 @@ export function ControlPanelApp({
147
147
  // Combine built-in widget components with custom ones
148
148
  const allWidgetComponents = [...getBuiltInWidgetComponents(), ...widgetComponents];
149
149
 
150
- // Configure API base URL - API routes are always at '/api' regardless of control panel mount path
151
- // The control panel might be mounted at /cpanel, but API is always at /api (not /cpanel/api)
152
- const apiBasePath = '';
150
+ // Configure API base URL - read from injected __APP_BASE_PATH__ if available
151
+ // Server injects window.__APP_BASE_PATH__ when control panel is mounted at a base path
152
+ // Example: mounted at /cpanel → API calls go to /cpanel/api (not /api)
153
+ const apiBasePath = (window as any).__APP_BASE_PATH__ || '';
153
154
  api.setBaseUrl(apiBasePath);
154
155
 
155
156
  // Fetch version from API
@@ -21,6 +21,7 @@ import {
21
21
  ServiceControlWidget,
22
22
  EnvironmentConfigWidget,
23
23
  DatabaseOpsWidget,
24
+ DatabaseOperationsWidget,
24
25
  LogsMaintenanceWidget,
25
26
  CacheMaintenanceWidget,
26
27
  } from './widgets';
@@ -42,6 +43,7 @@ export const builtInWidgetComponents: Record<string, React.ComponentType> = {
42
43
  ServiceControlWidget: ServiceControlWidget,
43
44
  EnvironmentConfigWidget: EnvironmentConfigWidget,
44
45
  DatabaseOpsWidget: DatabaseOpsWidget,
46
+ DatabaseOperationsWidget: DatabaseOperationsWidget,
45
47
  LogsMaintenanceWidget: LogsMaintenanceWidget,
46
48
  CacheMaintenanceWidget: CacheMaintenanceWidget,
47
49
  PreferencesPage: PreferencesPage,
@@ -66,6 +68,7 @@ export function getBuiltInWidgetComponents(): WidgetComponent[] {
66
68
  { name: 'ServiceControlWidget', component: ServiceControlWidget },
67
69
  { name: 'EnvironmentConfigWidget', component: EnvironmentConfigWidget },
68
70
  { name: 'DatabaseOpsWidget', component: DatabaseOpsWidget },
71
+ { name: 'DatabaseOperationsWidget', component: DatabaseOperationsWidget },
69
72
  { name: 'LogsMaintenanceWidget', component: LogsMaintenanceWidget },
70
73
  { name: 'CacheMaintenanceWidget', component: CacheMaintenanceWidget },
71
74
  { name: 'PreferencesPage', component: PreferencesPage },
@@ -49,7 +49,11 @@ export function CMSMaintenanceWidget() {
49
49
 
50
50
  const fetchStatus = async () => {
51
51
  try {
52
- const response = await fetch('/api/cms/status');
52
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
53
+ const response = await fetch(`${basePath}/api/cms/status`);
54
+ if (!response.ok) {
55
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
56
+ }
53
57
  const data = await response.json();
54
58
  setStatus(data);
55
59
  } catch (err) {
@@ -59,7 +63,11 @@ export function CMSMaintenanceWidget() {
59
63
 
60
64
  const fetchSeeds = async () => {
61
65
  try {
62
- const response = await fetch('/api/cms/seeds');
66
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
67
+ const response = await fetch(`${basePath}/api/cms/seeds`);
68
+ if (!response.ok) {
69
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
70
+ }
63
71
  const data = await response.json();
64
72
  setSeeds(data.seeds || []);
65
73
  } catch (err) {
@@ -80,7 +88,8 @@ export function CMSMaintenanceWidget() {
80
88
  setError(null);
81
89
  setSuccess(null);
82
90
  try {
83
- const response = await fetch('/api/cms/restart', { method: 'POST' });
91
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
92
+ const response = await fetch(`${basePath}/api/cms/restart`, { method: 'POST' });
84
93
  const data = await response.json();
85
94
 
86
95
  if (response.ok) {
@@ -100,9 +109,13 @@ export function CMSMaintenanceWidget() {
100
109
  setSuccess(null);
101
110
 
102
111
  try {
103
- const response = await fetch(`/api/cms/seeds/${seedName}/execute`, {
112
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
113
+ const response = await fetch(`${basePath}/api/cms/seeds/${seedName}/execute`, {
104
114
  method: 'POST',
105
115
  });
116
+ if (!response.ok) {
117
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
118
+ }
106
119
  const data = await response.json();
107
120
 
108
121
  if (data.success) {
@@ -35,7 +35,11 @@ export function CMSStatusWidget() {
35
35
 
36
36
  const fetchStatus = async () => {
37
37
  try {
38
- const response = await fetch('/api/cms/status');
38
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
39
+ const response = await fetch(`${basePath}/api/cms/status`);
40
+ if (!response.ok) {
41
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
42
+ }
39
43
  const data = await response.json();
40
44
  setStatus(data);
41
45
  setError(null);
@@ -47,7 +47,8 @@ export function CacheMaintenanceWidget() {
47
47
  setLoading(true);
48
48
  setError(null);
49
49
  try {
50
- const response = await fetch('/api/cache:default/stats');
50
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
51
+ const response = await fetch(`${basePath}/api/cache:default/stats`);
51
52
  if (!response.ok) {
52
53
  if (response.status === 404) {
53
54
  throw new Error('Cache plugin not configured');
@@ -75,7 +76,8 @@ export function CacheMaintenanceWidget() {
75
76
  setSuccess(null);
76
77
 
77
78
  try {
78
- const response = await fetch('/api/cache:default/flush', {
79
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
80
+ const response = await fetch(`${basePath}/api/cache:default/flush`, {
79
81
  method: 'POST',
80
82
  headers: { 'Content-Type': 'application/json' },
81
83
  });