@qwickapps/server 1.8.1 → 1.9.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 (231) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +4 -4
  3. package/dist/src/core/control-panel.d.ts.map +1 -1
  4. package/dist/src/core/control-panel.js +10 -7
  5. package/dist/src/core/control-panel.js.map +1 -1
  6. package/dist/src/core/gateway.d.ts.map +1 -1
  7. package/dist/src/core/gateway.js +46 -47
  8. package/dist/src/core/gateway.js.map +1 -1
  9. package/dist/src/plugins/api-keys/ApiKeysManagementPage.d.ts.map +1 -1
  10. package/dist/src/plugins/api-keys/ApiKeysManagementPage.js +1 -1
  11. package/dist/src/plugins/api-keys/ApiKeysManagementPage.js.map +1 -1
  12. package/dist/src/plugins/api-keys/ApiKeysStatusWidget.d.ts.map +1 -1
  13. package/dist/src/plugins/api-keys/ApiKeysStatusWidget.js +1 -1
  14. package/dist/src/plugins/api-keys/ApiKeysStatusWidget.js.map +1 -1
  15. package/dist/src/plugins/auth/AuthManagementPage.d.ts.map +1 -1
  16. package/dist/src/plugins/auth/AuthManagementPage.js +1 -1
  17. package/dist/src/plugins/auth/AuthManagementPage.js.map +1 -1
  18. package/dist/src/plugins/auth/AuthStatusWidget.d.ts.map +1 -1
  19. package/dist/src/plugins/auth/AuthStatusWidget.js +1 -1
  20. package/dist/src/plugins/auth/AuthStatusWidget.js.map +1 -1
  21. package/dist/src/plugins/auth/auth-plugin.d.ts.map +1 -1
  22. package/dist/src/plugins/auth/auth-plugin.js +9 -0
  23. package/dist/src/plugins/auth/auth-plugin.js.map +1 -1
  24. package/dist/src/plugins/bans/BansManagementPage.d.ts.map +1 -1
  25. package/dist/src/plugins/bans/BansManagementPage.js +1 -1
  26. package/dist/src/plugins/bans/BansManagementPage.js.map +1 -1
  27. package/dist/src/plugins/bans/BansStatusWidget.d.ts.map +1 -1
  28. package/dist/src/plugins/bans/BansStatusWidget.js +1 -1
  29. package/dist/src/plugins/bans/BansStatusWidget.js.map +1 -1
  30. package/dist/src/plugins/cache/CacheManagementPage.js +1 -1
  31. package/dist/src/plugins/cache/CacheManagementPage.js.map +1 -1
  32. package/dist/src/plugins/cache/CacheStatusWidget.js +1 -1
  33. package/dist/src/plugins/cache/CacheStatusWidget.js.map +1 -1
  34. package/dist/src/plugins/devices/DevicesManagementPage.d.ts.map +1 -1
  35. package/dist/src/plugins/devices/DevicesManagementPage.js +1 -1
  36. package/dist/src/plugins/devices/DevicesManagementPage.js.map +1 -1
  37. package/dist/src/plugins/devices/DevicesStatusWidget.d.ts.map +1 -1
  38. package/dist/src/plugins/devices/DevicesStatusWidget.js +1 -1
  39. package/dist/src/plugins/devices/DevicesStatusWidget.js.map +1 -1
  40. package/dist/src/plugins/diagnostics/DiagnosticsManagementPage.js +1 -1
  41. package/dist/src/plugins/diagnostics/DiagnosticsManagementPage.js.map +1 -1
  42. package/dist/src/plugins/diagnostics/DiagnosticsStatusWidget.js +1 -1
  43. package/dist/src/plugins/diagnostics/DiagnosticsStatusWidget.js.map +1 -1
  44. package/dist/src/plugins/entitlements/EntitlementsManagementPage.d.ts.map +1 -1
  45. package/dist/src/plugins/entitlements/EntitlementsManagementPage.js +1 -1
  46. package/dist/src/plugins/entitlements/EntitlementsManagementPage.js.map +1 -1
  47. package/dist/src/plugins/entitlements/EntitlementsStatusWidget.d.ts.map +1 -1
  48. package/dist/src/plugins/entitlements/EntitlementsStatusWidget.js +1 -1
  49. package/dist/src/plugins/entitlements/EntitlementsStatusWidget.js.map +1 -1
  50. package/dist/src/plugins/health/HealthManagementPage.js +1 -1
  51. package/dist/src/plugins/health/HealthManagementPage.js.map +1 -1
  52. package/dist/src/plugins/health/HealthStatusWidget.js +1 -1
  53. package/dist/src/plugins/health/HealthStatusWidget.js.map +1 -1
  54. package/dist/src/plugins/logs/LogsManagementPage.js +1 -1
  55. package/dist/src/plugins/logs/LogsManagementPage.js.map +1 -1
  56. package/dist/src/plugins/logs/LogsStatusWidget.js +1 -1
  57. package/dist/src/plugins/logs/LogsStatusWidget.js.map +1 -1
  58. package/dist/src/plugins/maintenance/MaintenanceManagementPage.js +1 -1
  59. package/dist/src/plugins/maintenance/MaintenanceManagementPage.js.map +1 -1
  60. package/dist/src/plugins/maintenance/MaintenanceStatusWidget.js +1 -1
  61. package/dist/src/plugins/maintenance/MaintenanceStatusWidget.js.map +1 -1
  62. package/dist/src/plugins/maintenance/SeedManagementPage.js +1 -1
  63. package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
  64. package/dist/src/plugins/maintenance/seed-executor.js +2 -2
  65. package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
  66. package/dist/src/plugins/maintenance-plugin.d.ts +2 -0
  67. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  68. package/dist/src/plugins/maintenance-plugin.js +402 -2
  69. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  70. package/dist/src/plugins/notifications/NotificationsManagementPage.js +1 -1
  71. package/dist/src/plugins/notifications/NotificationsManagementPage.js.map +1 -1
  72. package/dist/src/plugins/notifications/NotificationsStatusWidget.d.ts.map +1 -1
  73. package/dist/src/plugins/notifications/NotificationsStatusWidget.js +1 -1
  74. package/dist/src/plugins/notifications/NotificationsStatusWidget.js.map +1 -1
  75. package/dist/src/plugins/parental/ParentalManagementPage.d.ts.map +1 -1
  76. package/dist/src/plugins/parental/ParentalManagementPage.js +1 -1
  77. package/dist/src/plugins/parental/ParentalManagementPage.js.map +1 -1
  78. package/dist/src/plugins/parental/ParentalStatusWidget.d.ts.map +1 -1
  79. package/dist/src/plugins/parental/ParentalStatusWidget.js +1 -1
  80. package/dist/src/plugins/parental/ParentalStatusWidget.js.map +1 -1
  81. package/dist/src/plugins/postgres/PostgresManagementPage.js +1 -1
  82. package/dist/src/plugins/postgres/PostgresManagementPage.js.map +1 -1
  83. package/dist/src/plugins/postgres/PostgresStatusWidget.js +1 -1
  84. package/dist/src/plugins/postgres/PostgresStatusWidget.js.map +1 -1
  85. package/dist/src/plugins/preferences/PreferencesManagementPage.d.ts.map +1 -1
  86. package/dist/src/plugins/preferences/PreferencesManagementPage.js +1 -1
  87. package/dist/src/plugins/preferences/PreferencesManagementPage.js.map +1 -1
  88. package/dist/src/plugins/preferences/PreferencesStatusWidget.d.ts.map +1 -1
  89. package/dist/src/plugins/preferences/PreferencesStatusWidget.js +1 -1
  90. package/dist/src/plugins/preferences/PreferencesStatusWidget.js.map +1 -1
  91. package/dist/src/plugins/profiles/ProfilesManagementPage.d.ts.map +1 -1
  92. package/dist/src/plugins/profiles/ProfilesManagementPage.js +1 -1
  93. package/dist/src/plugins/profiles/ProfilesManagementPage.js.map +1 -1
  94. package/dist/src/plugins/profiles/ProfilesStatusWidget.d.ts.map +1 -1
  95. package/dist/src/plugins/profiles/ProfilesStatusWidget.js +1 -1
  96. package/dist/src/plugins/profiles/ProfilesStatusWidget.js.map +1 -1
  97. package/dist/src/plugins/qwickbrain/QwickbrainManagementPage.js +1 -1
  98. package/dist/src/plugins/qwickbrain/QwickbrainManagementPage.js.map +1 -1
  99. package/dist/src/plugins/qwickbrain/QwickbrainStatusWidget.d.ts.map +1 -1
  100. package/dist/src/plugins/qwickbrain/QwickbrainStatusWidget.js +1 -1
  101. package/dist/src/plugins/qwickbrain/QwickbrainStatusWidget.js.map +1 -1
  102. package/dist/src/plugins/rate-limit/RateLimitManagementPage.js +1 -1
  103. package/dist/src/plugins/rate-limit/RateLimitManagementPage.js.map +1 -1
  104. package/dist/src/plugins/rate-limit/RateLimitStatusWidget.d.ts.map +1 -1
  105. package/dist/src/plugins/rate-limit/RateLimitStatusWidget.js +1 -1
  106. package/dist/src/plugins/rate-limit/RateLimitStatusWidget.js.map +1 -1
  107. package/dist/src/plugins/subscriptions/SubscriptionsManagementPage.d.ts.map +1 -1
  108. package/dist/src/plugins/subscriptions/SubscriptionsManagementPage.js +1 -1
  109. package/dist/src/plugins/subscriptions/SubscriptionsManagementPage.js.map +1 -1
  110. package/dist/src/plugins/subscriptions/SubscriptionsStatusWidget.d.ts.map +1 -1
  111. package/dist/src/plugins/subscriptions/SubscriptionsStatusWidget.js +1 -1
  112. package/dist/src/plugins/subscriptions/SubscriptionsStatusWidget.js.map +1 -1
  113. package/dist/src/plugins/usage/UsageManagementPage.d.ts.map +1 -1
  114. package/dist/src/plugins/usage/UsageManagementPage.js +1 -1
  115. package/dist/src/plugins/usage/UsageManagementPage.js.map +1 -1
  116. package/dist/src/plugins/usage/UsageStatusWidget.d.ts.map +1 -1
  117. package/dist/src/plugins/usage/UsageStatusWidget.js +1 -1
  118. package/dist/src/plugins/usage/UsageStatusWidget.js.map +1 -1
  119. package/dist/src/plugins/users/UsersManagementPage.js +1 -1
  120. package/dist/src/plugins/users/UsersManagementPage.js.map +1 -1
  121. package/dist/src/plugins/users/UsersStatusWidget.js +1 -1
  122. package/dist/src/plugins/users/UsersStatusWidget.js.map +1 -1
  123. package/dist/ui/src/api/clientBuilder.d.ts +3 -3
  124. package/dist/ui/src/api/clientBuilder.js +5 -5
  125. package/dist/ui/src/api/clientBuilder.js.map +1 -1
  126. package/dist/ui/src/api/controlPanelApi.js +19 -19
  127. package/dist/ui/src/api/controlPanelApi.js.map +1 -1
  128. package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
  129. package/dist/ui/src/components/ControlPanelApp.js +5 -4
  130. package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
  131. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  132. package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
  133. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  134. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +8 -8
  135. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
  136. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +2 -2
  137. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
  138. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -4
  139. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
  140. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -1
  141. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +2 -1
  142. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -1
  143. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -6
  144. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
  145. package/dist/ui/src/dashboard/widgets/MigrationManagementWidget.d.ts +8 -0
  146. package/dist/ui/src/dashboard/widgets/MigrationManagementWidget.d.ts.map +1 -0
  147. package/dist/ui/src/dashboard/widgets/MigrationManagementWidget.js +132 -0
  148. package/dist/ui/src/dashboard/widgets/MigrationManagementWidget.js.map +1 -0
  149. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +6 -6
  150. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
  151. package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
  152. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  153. package/dist/ui/src/dashboard/widgets/index.js +1 -0
  154. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  155. package/dist-ui/assets/{index-DRG9n0cx.css → index-De-dCl_t.css} +1 -1
  156. package/dist-ui/assets/{index-D-4HKPkw.js → index-DnEQCOGR.js} +112 -109
  157. package/dist-ui/assets/{index-D-4HKPkw.js.map → index-DnEQCOGR.js.map} +1 -1
  158. package/dist-ui/index.html +2 -2
  159. package/dist-ui-lib/index.js +2623 -2441
  160. package/dist-ui-lib/index.js.map +1 -1
  161. package/dist-ui-lib/src/api/clientBuilder.d.ts +3 -3
  162. package/dist-ui-lib/src/dashboard/widgets/MigrationManagementWidget.d.ts +7 -0
  163. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
  164. package/package.json +5 -2
  165. package/src/core/control-panel.ts +10 -7
  166. package/src/core/gateway.ts +48 -51
  167. package/src/plugins/api-keys/ApiKeysManagementPage.tsx +1 -1
  168. package/src/plugins/api-keys/ApiKeysStatusWidget.tsx +1 -1
  169. package/src/plugins/auth/AuthManagementPage.tsx +1 -1
  170. package/src/plugins/auth/AuthStatusWidget.tsx +1 -1
  171. package/src/plugins/auth/adapters/supertokens-adapter.ts +9 -13
  172. package/src/plugins/auth/auth-plugin.ts +10 -0
  173. package/src/plugins/auth/env-config.ts +1 -0
  174. package/src/plugins/auth/types.ts +2 -0
  175. package/src/plugins/bans/BansManagementPage.tsx +1 -1
  176. package/src/plugins/bans/BansStatusWidget.tsx +1 -1
  177. package/src/plugins/cache/CacheManagementPage.tsx +1 -1
  178. package/src/plugins/cache/CacheStatusWidget.tsx +1 -1
  179. package/src/plugins/devices/DevicesManagementPage.tsx +1 -1
  180. package/src/plugins/devices/DevicesStatusWidget.tsx +1 -1
  181. package/src/plugins/diagnostics/DiagnosticsManagementPage.tsx +1 -1
  182. package/src/plugins/diagnostics/DiagnosticsStatusWidget.tsx +1 -1
  183. package/src/plugins/entitlements/EntitlementsManagementPage.tsx +1 -1
  184. package/src/plugins/entitlements/EntitlementsStatusWidget.tsx +1 -1
  185. package/src/plugins/health/HealthManagementPage.tsx +1 -1
  186. package/src/plugins/health/HealthStatusWidget.tsx +1 -1
  187. package/src/plugins/logs/LogsManagementPage.tsx +1 -1
  188. package/src/plugins/logs/LogsStatusWidget.tsx +1 -1
  189. package/src/plugins/maintenance/MaintenanceManagementPage.tsx +1 -1
  190. package/src/plugins/maintenance/MaintenanceStatusWidget.tsx +1 -1
  191. package/src/plugins/maintenance/SeedManagementPage.tsx +1 -1
  192. package/src/plugins/maintenance/seed-executor.ts +7 -6
  193. package/src/plugins/maintenance-plugin.ts +501 -5
  194. package/src/plugins/notifications/NotificationsManagementPage.tsx +1 -1
  195. package/src/plugins/notifications/NotificationsStatusWidget.tsx +1 -1
  196. package/src/plugins/parental/ParentalManagementPage.tsx +1 -1
  197. package/src/plugins/parental/ParentalStatusWidget.tsx +1 -1
  198. package/src/plugins/postgres/PostgresManagementPage.tsx +1 -1
  199. package/src/plugins/postgres/PostgresStatusWidget.tsx +1 -1
  200. package/src/plugins/preferences/PreferencesManagementPage.tsx +1 -1
  201. package/src/plugins/preferences/PreferencesStatusWidget.tsx +1 -1
  202. package/src/plugins/profiles/ProfilesManagementPage.tsx +1 -1
  203. package/src/plugins/profiles/ProfilesStatusWidget.tsx +1 -1
  204. package/src/plugins/qwickbrain/QwickbrainManagementPage.tsx +1 -1
  205. package/src/plugins/qwickbrain/QwickbrainStatusWidget.tsx +1 -1
  206. package/src/plugins/rate-limit/RateLimitManagementPage.tsx +1 -1
  207. package/src/plugins/rate-limit/RateLimitStatusWidget.tsx +1 -1
  208. package/src/plugins/subscriptions/SubscriptionsManagementPage.tsx +1 -1
  209. package/src/plugins/subscriptions/SubscriptionsStatusWidget.tsx +1 -1
  210. package/src/plugins/usage/UsageManagementPage.tsx +1 -1
  211. package/src/plugins/usage/UsageStatusWidget.tsx +1 -1
  212. package/src/plugins/users/UsersManagementPage.tsx +1 -1
  213. package/src/plugins/users/UsersStatusWidget.tsx +1 -1
  214. package/ui/src/App.tsx +4 -3
  215. package/ui/src/api/clientBuilder.ts +5 -5
  216. package/ui/src/api/controlPanelApi.ts +19 -19
  217. package/ui/src/components/ControlPanelApp.tsx +5 -4
  218. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  219. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +3 -8
  220. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +8 -8
  221. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +2 -2
  222. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -4
  223. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +2 -1
  224. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -6
  225. package/ui/src/dashboard/widgets/MigrationManagementWidget.tsx +319 -0
  226. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +6 -6
  227. package/ui/src/dashboard/widgets/index.ts +1 -0
  228. package/ui/src/hooks/useJobStream.ts +2 -2
  229. package/ui/src/pages/ContentOpsJobsPage.tsx +3 -3
  230. package/ui/src/pages/PluginPage.tsx +1 -1
  231. package/ui/src/pages/TenantsManagementPage.tsx +1 -1
@@ -59,8 +59,8 @@ function scanSeedScripts(dir: string, basePath: string = dir): any[] {
59
59
  if (entry.isDirectory()) {
60
60
  // Recursively scan subdirectories
61
61
  results.push(...scanSeedScripts(fullPath, basePath));
62
- } else if (entry.isFile() && entry.name.endsWith('.mjs')) {
63
- // Found a .mjs file
62
+ } else if (entry.isFile() && (entry.name.endsWith('.mjs') || entry.name.endsWith('.ts'))) {
63
+ // Found a .mjs or .ts file
64
64
  const stats = statSync(fullPath);
65
65
  const relativePath = relative(basePath, fullPath);
66
66
  const description = extractDescription(fullPath);
@@ -132,6 +132,9 @@ export interface MaintenancePluginConfig {
132
132
  /** Enable database operations (default: true) */
133
133
  enableDatabaseOps?: boolean;
134
134
 
135
+ /** Enable migration management (default: true) */
136
+ enableMigrations?: boolean;
137
+
135
138
  /** Custom seed tasks */
136
139
  customTasks?: SeedTask[];
137
140
  }
@@ -413,6 +416,8 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
413
416
  } catch (error) {
414
417
  logger.error('Seed execution failed', { name, error });
415
418
 
419
+ const duration = Date.now() - startTime;
420
+
416
421
  // Send error event via SSE to notify client
417
422
  res.write(`data: ${JSON.stringify({
418
423
  type: 'error',
@@ -420,6 +425,13 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
420
425
  timestamp: new Date().toISOString()
421
426
  })}\n\n`);
422
427
 
428
+ // Send exit event so the UI can transition out of the "Running..." state
429
+ res.write(`data: ${JSON.stringify({
430
+ type: 'exit',
431
+ data: JSON.stringify({ exitCode: 1, duration }),
432
+ timestamp: new Date().toISOString()
433
+ })}\n\n`);
434
+
423
435
  // Update execution record as failed
424
436
  if (hasPostgres() && executionId) {
425
437
  const db = getPostgres();
@@ -473,16 +485,31 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
473
485
 
474
486
  const db = getPostgres();
475
487
 
488
+ // Derive the DB role from the connection URL so GRANT statements
489
+ // work regardless of which user owns the schema.
490
+ let dbRole = 'qwickapps';
491
+ if (config.databaseUrl) {
492
+ try {
493
+ const parsedUrl = new URL(config.databaseUrl);
494
+ const urlUser = parsedUrl.username;
495
+ if (urlUser && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(urlUser)) {
496
+ dbRole = urlUser;
497
+ }
498
+ } catch {
499
+ // Keep default if URL is unparseable
500
+ }
501
+ }
502
+
476
503
  // Drop and recreate public schema (removes all tables, data, etc.)
477
504
  await db.queryRaw('DROP SCHEMA IF EXISTS public CASCADE');
478
505
  await db.queryRaw('CREATE SCHEMA public');
479
506
  await db.queryRaw('GRANT ALL ON SCHEMA public TO public');
480
507
  await db.queryRaw('GRANT ALL ON SCHEMA public TO postgres');
481
- await db.queryRaw('GRANT ALL ON SCHEMA public TO qwickapps');
508
+ await db.queryRaw(`GRANT ALL ON SCHEMA public TO ${dbRole}`);
482
509
 
483
510
  // Grant default privileges for future tables and sequences
484
- await db.queryRaw('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO qwickapps');
485
- await db.queryRaw('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO qwickapps');
511
+ await db.queryRaw(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${dbRole}`);
512
+ await db.queryRaw(`ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${dbRole}`);
486
513
 
487
514
  res.json({
488
515
  success: true,
@@ -599,6 +626,462 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
599
626
  });
600
627
  }
601
628
 
629
+ // Migration Management Routes
630
+ if (config.enableMigrations !== false) {
631
+ // Ensure migration_executions table exists
632
+ registry.addRoute({
633
+ method: 'post',
634
+ path: '/migrations/_init',
635
+ pluginId: 'maintenance',
636
+ handler: async (req: Request, res: Response) => {
637
+ try {
638
+ if (hasPostgres()) {
639
+ const db = getPostgres();
640
+ await db.queryRaw(`
641
+ CREATE TABLE IF NOT EXISTS migration_executions (
642
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
643
+ status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed')),
644
+ started_at TIMESTAMPTZ NOT NULL,
645
+ completed_at TIMESTAMPTZ,
646
+ exit_code INTEGER,
647
+ output TEXT,
648
+ error TEXT,
649
+ duration_ms INTEGER,
650
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
651
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
652
+ )
653
+ `);
654
+ await db.queryRaw(`
655
+ CREATE INDEX IF NOT EXISTS idx_migration_executions_status
656
+ ON migration_executions(status)
657
+ `);
658
+ await db.queryRaw(`
659
+ CREATE INDEX IF NOT EXISTS idx_migration_executions_started_at
660
+ ON migration_executions(started_at DESC)
661
+ `);
662
+ }
663
+ res.json({ success: true });
664
+ } catch (error) {
665
+ logger.error('Failed to initialize migration_executions table', { error });
666
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
667
+ }
668
+ },
669
+ });
670
+
671
+ // GET /migrations/status - Get migration status
672
+ registry.addRoute({
673
+ method: 'get',
674
+ path: '/migrations/status',
675
+ pluginId: 'maintenance',
676
+ handler: async (req: Request, res: Response) => {
677
+ try {
678
+ // Check if there are any pending migrations by checking Payload
679
+ // This is a simple implementation - just returns basic status
680
+ res.json({
681
+ available: true,
682
+ lastChecked: new Date().toISOString(),
683
+ });
684
+ } catch (error) {
685
+ logger.error('Failed to get migration status', { error });
686
+ res.status(500).json({
687
+ error: error instanceof Error ? error.message : String(error),
688
+ });
689
+ }
690
+ },
691
+ });
692
+
693
+ // GET /migrations/execute - Execute Payload migrations with SSE output
694
+ registry.addRoute({
695
+ method: 'get',
696
+ path: '/migrations/execute',
697
+ pluginId: 'maintenance',
698
+ handler: async (req: Request, res: Response) => {
699
+ const MIGRATION_LOCK_ID = 123456789;
700
+ const MIGRATION_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
701
+ let lockAcquired = false;
702
+
703
+ try {
704
+ // Ensure table exists (lazy initialization)
705
+ if (hasPostgres()) {
706
+ const db = getPostgres();
707
+ try {
708
+ await db.queryRaw(`
709
+ CREATE TABLE IF NOT EXISTS migration_executions (
710
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
711
+ status TEXT NOT NULL CHECK (status IN ('running', 'completed', 'failed')),
712
+ started_at TIMESTAMPTZ NOT NULL,
713
+ completed_at TIMESTAMPTZ,
714
+ exit_code INTEGER,
715
+ output TEXT,
716
+ error TEXT,
717
+ duration_ms INTEGER,
718
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
719
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
720
+ )
721
+ `);
722
+ } catch (err) {
723
+ logger.debug('Table initialization check', { err });
724
+ }
725
+ }
726
+
727
+ // Acquire advisory lock for concurrency control
728
+ if (hasPostgres()) {
729
+ const db = getPostgres();
730
+ const lockResult = await db.queryOne<{ pg_try_advisory_lock: boolean }>(
731
+ 'SELECT pg_try_advisory_lock($1) as pg_try_advisory_lock',
732
+ [MIGRATION_LOCK_ID]
733
+ );
734
+
735
+ lockAcquired = lockResult?.pg_try_advisory_lock || false;
736
+
737
+ if (!lockAcquired) {
738
+ return res.status(409).json({
739
+ error: 'Migrations are already running. Please wait for them to complete.',
740
+ });
741
+ }
742
+ }
743
+
744
+ // Set SSE headers
745
+ res.setHeader('Content-Type', 'text/event-stream');
746
+ res.setHeader('Cache-Control', 'no-cache');
747
+ res.setHeader('Connection', 'keep-alive');
748
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
749
+ res.setHeader('Content-Encoding', 'identity'); // Disable compression
750
+ res.flushHeaders();
751
+
752
+ // Create execution record in database
753
+ let executionId: string | null = null;
754
+ if (hasPostgres()) {
755
+ const db = getPostgres();
756
+ const result = await db.queryOne<{ id: string }>(
757
+ `INSERT INTO migration_executions (status, started_at)
758
+ VALUES ($1, NOW())
759
+ RETURNING id`,
760
+ ['running']
761
+ );
762
+ executionId = result?.id || null;
763
+ }
764
+
765
+ const startTime = Date.now();
766
+ let exitCode: number | undefined = undefined;
767
+ let output = '';
768
+ let error = '';
769
+ let migrationProcess: any = null;
770
+ let timeoutHandle: NodeJS.Timeout | null = null;
771
+
772
+ // Cleanup function to ensure resources are released
773
+ const cleanup = async (reason: string) => {
774
+ logger.debug(`Migration cleanup: ${reason}`, { executionId });
775
+
776
+ // Clear timeout if set
777
+ if (timeoutHandle) {
778
+ clearTimeout(timeoutHandle);
779
+ timeoutHandle = null;
780
+ }
781
+
782
+ // Kill process if still running
783
+ if (migrationProcess && !migrationProcess.killed) {
784
+ migrationProcess.kill('SIGTERM');
785
+ }
786
+
787
+ // Update execution record if not already completed
788
+ if (hasPostgres() && executionId && exitCode === undefined) {
789
+ const db = getPostgres();
790
+ const duration = Date.now() - startTime;
791
+ await db.query(
792
+ `UPDATE migration_executions
793
+ SET status = $1, completed_at = NOW(),
794
+ error = $2, duration_ms = $3, updated_at = NOW()
795
+ WHERE id = $4 AND status = 'running'`,
796
+ ['failed', reason, duration, executionId]
797
+ ).catch(err => logger.error('Failed to update execution on cleanup', { err }));
798
+ }
799
+
800
+ // Release advisory lock
801
+ if (hasPostgres() && lockAcquired) {
802
+ const db = getPostgres();
803
+ await db.query(
804
+ 'SELECT pg_advisory_unlock($1)',
805
+ [MIGRATION_LOCK_ID]
806
+ ).catch(err => logger.error('Failed to release advisory lock', { err }));
807
+ lockAcquired = false;
808
+ }
809
+ };
810
+
811
+ // Handle client disconnect
812
+ res.on('close', () => {
813
+ cleanup('Client disconnected before completion').catch(err =>
814
+ logger.error('Cleanup failed on disconnect', { err })
815
+ );
816
+ });
817
+
818
+ try {
819
+ // Execute Payload migration command
820
+ const { spawn } = await import('child_process');
821
+
822
+ migrationProcess = spawn('npx', ['payload', 'migrate', '--force-accept-warning'], {
823
+ cwd: process.cwd(),
824
+ env: {
825
+ ...process.env,
826
+ CI: 'true', // Force non-interactive mode
827
+ NODE_ENV: 'production', // Disable dev mode prompts
828
+ },
829
+ });
830
+
831
+ // Automatically answer 'y' to any interactive prompts
832
+ if (migrationProcess.stdin) {
833
+ migrationProcess.stdin.write('y\n');
834
+ migrationProcess.stdin.end();
835
+ }
836
+
837
+ // Set timeout to prevent hanging migrations
838
+ timeoutHandle = setTimeout(() => {
839
+ logger.warn('Migration timeout - killing process', { executionId, timeout: MIGRATION_TIMEOUT_MS });
840
+ if (migrationProcess && !migrationProcess.killed) {
841
+ migrationProcess.kill('SIGTERM');
842
+ error += '\n[TIMEOUT] Migration exceeded maximum execution time and was terminated.';
843
+ }
844
+ }, MIGRATION_TIMEOUT_MS);
845
+
846
+ // Stream stdout
847
+ migrationProcess.stdout?.on('data', (data: Buffer) => {
848
+ const chunk = data.toString();
849
+ output += chunk;
850
+ if (!res.writableEnded) {
851
+ res.write(`data: ${JSON.stringify({
852
+ type: 'output',
853
+ data: chunk,
854
+ timestamp: new Date().toISOString()
855
+ })}\n\n`);
856
+ }
857
+ });
858
+
859
+ // Stream stderr
860
+ migrationProcess.stderr?.on('data', (data: Buffer) => {
861
+ const chunk = data.toString();
862
+ error += chunk;
863
+ if (!res.writableEnded) {
864
+ res.write(`data: ${JSON.stringify({
865
+ type: 'error',
866
+ data: chunk,
867
+ timestamp: new Date().toISOString()
868
+ })}\n\n`);
869
+ }
870
+ });
871
+
872
+ // Wait for process to complete
873
+ await new Promise<void>((resolve, reject) => {
874
+ migrationProcess.on('close', (code: number | null) => {
875
+ exitCode = code || 0;
876
+ resolve();
877
+ });
878
+ migrationProcess.on('error', (err: Error) => {
879
+ reject(err);
880
+ });
881
+ });
882
+
883
+ // Clear timeout since process completed
884
+ if (timeoutHandle) {
885
+ clearTimeout(timeoutHandle);
886
+ timeoutHandle = null;
887
+ }
888
+
889
+ const duration = Date.now() - startTime;
890
+
891
+ // Update execution record
892
+ if (hasPostgres() && executionId) {
893
+ const db = getPostgres();
894
+ await db.query(
895
+ `UPDATE migration_executions
896
+ SET status = $1, completed_at = NOW(), exit_code = $2,
897
+ output = $3, error = $4, duration_ms = $5, updated_at = NOW()
898
+ WHERE id = $6`,
899
+ [
900
+ exitCode === 0 ? 'completed' : 'failed',
901
+ exitCode,
902
+ output,
903
+ error,
904
+ duration,
905
+ executionId,
906
+ ]
907
+ );
908
+ }
909
+
910
+ // Send completion event
911
+ if (!res.writableEnded) {
912
+ res.write(`data: ${JSON.stringify({
913
+ type: 'complete',
914
+ exitCode,
915
+ duration,
916
+ timestamp: new Date().toISOString()
917
+ })}\n\n`);
918
+ }
919
+
920
+ res.end();
921
+
922
+ // Release advisory lock after successful completion
923
+ if (hasPostgres() && lockAcquired) {
924
+ const db = getPostgres();
925
+ await db.query('SELECT pg_advisory_unlock($1)', [MIGRATION_LOCK_ID]);
926
+ lockAcquired = false;
927
+ }
928
+ } catch (error) {
929
+ logger.error('Migration execution failed', { error });
930
+
931
+ // Send error event via SSE
932
+ if (!res.writableEnded) {
933
+ res.write(`data: ${JSON.stringify({
934
+ type: 'error',
935
+ data: error instanceof Error ? error.message : String(error),
936
+ timestamp: new Date().toISOString()
937
+ })}\n\n`);
938
+ }
939
+
940
+ // Update execution record as failed
941
+ if (hasPostgres() && executionId) {
942
+ const db = getPostgres();
943
+ const duration = Date.now() - startTime;
944
+ await db.query(
945
+ `UPDATE migration_executions
946
+ SET status = $1, completed_at = NOW(),
947
+ error = $2, duration_ms = $3, updated_at = NOW()
948
+ WHERE id = $4`,
949
+ ['failed', error instanceof Error ? error.message : String(error), duration, executionId]
950
+ );
951
+ }
952
+
953
+ res.end();
954
+
955
+ // Release advisory lock after error
956
+ if (hasPostgres() && lockAcquired) {
957
+ const db = getPostgres();
958
+ await db.query('SELECT pg_advisory_unlock($1)', [MIGRATION_LOCK_ID]);
959
+ lockAcquired = false;
960
+ }
961
+ }
962
+ } catch (error) {
963
+ logger.error('Migration execution setup failed', { error });
964
+
965
+ // Release advisory lock if acquired
966
+ if (hasPostgres() && lockAcquired) {
967
+ const db = getPostgres();
968
+ await db.query('SELECT pg_advisory_unlock($1)', [MIGRATION_LOCK_ID])
969
+ .catch(err => logger.error('Failed to release lock on setup error', { err }));
970
+ }
971
+
972
+ res.status(500).json({
973
+ error: error instanceof Error ? error.message : String(error),
974
+ });
975
+ }
976
+ },
977
+ });
978
+
979
+ // GET /migrations/history - Get migration execution history
980
+ registry.addRoute({
981
+ method: 'get',
982
+ path: '/migrations/history',
983
+ pluginId: 'maintenance',
984
+ handler: async (req: Request, res: Response) => {
985
+ try {
986
+ if (!hasPostgres()) {
987
+ return res.json({ executions: [] });
988
+ }
989
+
990
+ const db = getPostgres();
991
+ const { limit = '10', offset = '0', status, search } = req.query;
992
+
993
+ let query = 'SELECT * FROM migration_executions WHERE 1=1';
994
+ const params: (string | number)[] = [];
995
+ let paramIndex = 1;
996
+
997
+ if (status && typeof status === 'string') {
998
+ query += ` AND status = $${paramIndex}`;
999
+ params.push(status);
1000
+ paramIndex++;
1001
+ }
1002
+
1003
+ if (search && typeof search === 'string') {
1004
+ query += ` AND (output ILIKE $${paramIndex} OR error ILIKE $${paramIndex})`;
1005
+ params.push(`%${search}%`);
1006
+ paramIndex++;
1007
+ }
1008
+
1009
+ query += ` ORDER BY started_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
1010
+ params.push(parseInt(limit as string, 10), parseInt(offset as string, 10));
1011
+
1012
+ const executions = await db.query(query, params);
1013
+
1014
+ // Get total count
1015
+ let countQuery = 'SELECT COUNT(*) as count FROM migration_executions WHERE 1=1';
1016
+ const countParams: string[] = [];
1017
+ let countParamIndex = 1;
1018
+
1019
+ if (status && typeof status === 'string') {
1020
+ countQuery += ` AND status = $${countParamIndex}`;
1021
+ countParams.push(status);
1022
+ countParamIndex++;
1023
+ }
1024
+
1025
+ if (search && typeof search === 'string') {
1026
+ countQuery += ` AND (output ILIKE $${countParamIndex} OR error ILIKE $${countParamIndex})`;
1027
+ countParams.push(`%${search}%`);
1028
+ }
1029
+
1030
+ const countResult = await db.queryOne<{ count: string }>(countQuery, countParams);
1031
+ const total = parseInt(countResult?.count || '0', 10);
1032
+
1033
+ res.json({
1034
+ executions,
1035
+ pagination: {
1036
+ total,
1037
+ limit: parseInt(limit as string, 10),
1038
+ offset: parseInt(offset as string, 10),
1039
+ },
1040
+ });
1041
+ } catch (error) {
1042
+ logger.error('Failed to fetch migration history', { error });
1043
+ res.status(500).json({
1044
+ error: error instanceof Error ? error.message : String(error),
1045
+ });
1046
+ }
1047
+ },
1048
+ });
1049
+
1050
+ // GET /migrations/history/:id - Get detailed migration execution result
1051
+ registry.addRoute({
1052
+ method: 'get',
1053
+ path: '/migrations/history/:id',
1054
+ pluginId: 'maintenance',
1055
+ handler: async (req: Request, res: Response) => {
1056
+ try {
1057
+ if (!hasPostgres()) {
1058
+ return res.status(404).json({ error: 'Execution not found' });
1059
+ }
1060
+
1061
+ const { id } = req.params;
1062
+ const db = getPostgres();
1063
+
1064
+ const execution = await db.queryOne(
1065
+ 'SELECT * FROM migration_executions WHERE id = $1',
1066
+ [id]
1067
+ );
1068
+
1069
+ if (!execution) {
1070
+ return res.status(404).json({ error: 'Execution not found' });
1071
+ }
1072
+
1073
+ res.json({ execution });
1074
+ } catch (error) {
1075
+ logger.error('Failed to fetch migration execution detail', { error });
1076
+ res.status(500).json({
1077
+ error: error instanceof Error ? error.message : String(error),
1078
+ message: error instanceof Error ? error.message : String(error),
1079
+ });
1080
+ }
1081
+ },
1082
+ });
1083
+ }
1084
+
602
1085
  // Register maintenance widgets
603
1086
  if (config.enableSeeds !== false) {
604
1087
  registry.addWidget({
@@ -612,6 +1095,19 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
612
1095
  });
613
1096
  }
614
1097
 
1098
+ // Register migration management widget
1099
+ if (config.enableMigrations !== false) {
1100
+ registry.addWidget({
1101
+ id: 'migration-management',
1102
+ title: 'Database Migrations',
1103
+ component: 'MigrationManagementWidget',
1104
+ type: 'maintenance',
1105
+ priority: 15,
1106
+ showByDefault: true, // Show by default on maintenance page
1107
+ pluginId: 'maintenance',
1108
+ });
1109
+ }
1110
+
615
1111
  // TODO: Register service control routes
616
1112
  if (config.enableServiceControl !== false) {
617
1113
  logger.debug('Service control enabled');
@@ -32,7 +32,7 @@ interface NotificationsStats {
32
32
  }
33
33
 
34
34
  export const NotificationsManagementPage: React.FC<NotificationsManagementPageProps> = ({
35
- apiPrefix = '/api/notifications',
35
+ apiPrefix = '/qapi/notifications',
36
36
  }) => {
37
37
  const [notifications, setNotifications] = useState<Notification[]>([]);
38
38
  const [stats, setStats] = useState<NotificationsStats | null>(null);
@@ -10,7 +10,7 @@ export interface NotificationsStatusWidgetProps {
10
10
  apiPrefix?: string;
11
11
  }
12
12
 
13
- export function NotificationsStatusWidget({ apiPrefix = '/api/notifications' }: NotificationsStatusWidgetProps) {
13
+ export function NotificationsStatusWidget({ apiPrefix = '/qapi/notifications' }: NotificationsStatusWidgetProps) {
14
14
  const [stats, setStats] = useState<{
15
15
  totalNotifications: number;
16
16
  pendingNotifications: number;
@@ -23,7 +23,7 @@ interface ParentalControl {
23
23
  updatedAt: string;
24
24
  }
25
25
 
26
- export function ParentalManagementPage({ apiPrefix = '/api/parental' }: ParentalManagementPageProps) {
26
+ export function ParentalManagementPage({ apiPrefix = '/qapi/parental' }: ParentalManagementPageProps) {
27
27
  const [activeTab, setActiveTab] = useState<'all' | 'active' | 'violations' | 'config'>('all');
28
28
  const [controls, setControls] = useState<ParentalControl[]>([]);
29
29
  const [loading, setLoading] = useState(true);
@@ -10,7 +10,7 @@ export interface ParentalStatusWidgetProps {
10
10
  apiPrefix?: string;
11
11
  }
12
12
 
13
- export function ParentalStatusWidget({ apiPrefix = '/api/parental' }: ParentalStatusWidgetProps) {
13
+ export function ParentalStatusWidget({ apiPrefix = '/qapi/parental' }: ParentalStatusWidgetProps) {
14
14
  const [stats, setStats] = useState<{
15
15
  totalControls: number;
16
16
  activeControls: number;
@@ -42,7 +42,7 @@ interface PoolConfig {
42
42
  }
43
43
 
44
44
  export const PostgresManagementPage: React.FC<PostgresManagementPageProps> = ({
45
- apiPrefix = '/api/plugins/postgres',
45
+ apiPrefix = '/qapi/plugins/postgres',
46
46
  }) => {
47
47
  const [connections, setConnections] = useState<ConnectionInfo[]>([]);
48
48
  const [queryLogs, setQueryLogs] = useState<QueryLog[]>([]);
@@ -28,7 +28,7 @@ interface PostgresStats {
28
28
  }
29
29
 
30
30
  export const PostgresStatusWidget: React.FC<PostgresStatusWidgetProps> = ({
31
- apiPrefix = '/api/plugins/postgres',
31
+ apiPrefix = '/qapi/plugins/postgres',
32
32
  }) => {
33
33
  const [stats, setStats] = useState<PostgresStats | null>(null);
34
34
  const [loading, setLoading] = useState(true);
@@ -29,7 +29,7 @@ interface Preference {
29
29
  updatedAt: string;
30
30
  }
31
31
 
32
- export function PreferencesManagementPage({ apiPrefix = '/api/preferences' }: PreferencesManagementPageProps) {
32
+ export function PreferencesManagementPage({ apiPrefix = '/qapi/preferences' }: PreferencesManagementPageProps) {
33
33
  const [activeTab, setActiveTab] = useState<'overview' | 'user' | 'global' | 'config'>('overview');
34
34
  const [preferenceSets, setPreferenceSets] = useState<PreferenceSet[]>([]);
35
35
  const [preferences, setPreferences] = useState<Preference[]>([]);
@@ -10,7 +10,7 @@ export interface PreferencesStatusWidgetProps {
10
10
  apiPrefix?: string;
11
11
  }
12
12
 
13
- export function PreferencesStatusWidget({ apiPrefix = '/api/preferences' }: PreferencesStatusWidgetProps) {
13
+ export function PreferencesStatusWidget({ apiPrefix = '/qapi/preferences' }: PreferencesStatusWidgetProps) {
14
14
  const [stats, setStats] = useState<{
15
15
  totalPreferences: number;
16
16
  activeUsers: number;
@@ -22,7 +22,7 @@ interface Profile {
22
22
  createdAt: string;
23
23
  }
24
24
 
25
- export function ProfilesManagementPage({ apiPrefix = '/api/profiles' }: ProfilesManagementPageProps) {
25
+ export function ProfilesManagementPage({ apiPrefix = '/qapi/profiles' }: ProfilesManagementPageProps) {
26
26
  const [activeTab, setActiveTab] = useState<'all' | 'complete' | 'incomplete' | 'config'>('all');
27
27
  const [profiles, setProfiles] = useState<Profile[]>([]);
28
28
  const [loading, setLoading] = useState(true);
@@ -10,7 +10,7 @@ export interface ProfilesStatusWidgetProps {
10
10
  apiPrefix?: string;
11
11
  }
12
12
 
13
- export function ProfilesStatusWidget({ apiPrefix = '/api/profiles' }: ProfilesStatusWidgetProps) {
13
+ export function ProfilesStatusWidget({ apiPrefix = '/qapi/profiles' }: ProfilesStatusWidgetProps) {
14
14
  const [stats, setStats] = useState<{
15
15
  totalProfiles: number;
16
16
  completeProfiles: number;
@@ -30,7 +30,7 @@ interface QwickbrainStats {
30
30
  }
31
31
 
32
32
  export const QwickbrainManagementPage: React.FC<QwickbrainManagementPageProps> = ({
33
- apiPrefix = '/api/qwickbrain',
33
+ apiPrefix = '/qapi/qwickbrain',
34
34
  }) => {
35
35
  const [repositories, setRepositories] = useState<Repository[]>([]);
36
36
  const [stats, setStats] = useState<QwickbrainStats | null>(null);