@qwickapps/server 1.8.1 → 1.8.2

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