@qwickapps/server 1.8.0 → 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 (229) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +8 -0
  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 +411 -7
  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-Cez_jyhl.js +532 -0
  156. package/dist-ui/assets/{index-BkGp7ZKd.js.map → index-Cez_jyhl.js.map} +1 -1
  157. package/dist-ui/assets/index-De-dCl_t.css +1 -0
  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 +27 -26
  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/auth-plugin.ts +9 -0
  172. package/src/plugins/bans/BansManagementPage.tsx +1 -1
  173. package/src/plugins/bans/BansStatusWidget.tsx +1 -1
  174. package/src/plugins/cache/CacheManagementPage.tsx +1 -1
  175. package/src/plugins/cache/CacheStatusWidget.tsx +1 -1
  176. package/src/plugins/devices/DevicesManagementPage.tsx +1 -1
  177. package/src/plugins/devices/DevicesStatusWidget.tsx +1 -1
  178. package/src/plugins/diagnostics/DiagnosticsManagementPage.tsx +1 -1
  179. package/src/plugins/diagnostics/DiagnosticsStatusWidget.tsx +1 -1
  180. package/src/plugins/entitlements/EntitlementsManagementPage.tsx +1 -1
  181. package/src/plugins/entitlements/EntitlementsStatusWidget.tsx +1 -1
  182. package/src/plugins/health/HealthManagementPage.tsx +1 -1
  183. package/src/plugins/health/HealthStatusWidget.tsx +1 -1
  184. package/src/plugins/logs/LogsManagementPage.tsx +1 -1
  185. package/src/plugins/logs/LogsStatusWidget.tsx +1 -1
  186. package/src/plugins/maintenance/MaintenanceManagementPage.tsx +1 -1
  187. package/src/plugins/maintenance/MaintenanceStatusWidget.tsx +1 -1
  188. package/src/plugins/maintenance/SeedManagementPage.tsx +1 -1
  189. package/src/plugins/maintenance/seed-executor.ts +2 -2
  190. package/src/plugins/maintenance-plugin.ts +485 -7
  191. package/src/plugins/notifications/NotificationsManagementPage.tsx +1 -1
  192. package/src/plugins/notifications/NotificationsStatusWidget.tsx +1 -1
  193. package/src/plugins/parental/ParentalManagementPage.tsx +1 -1
  194. package/src/plugins/parental/ParentalStatusWidget.tsx +1 -1
  195. package/src/plugins/postgres/PostgresManagementPage.tsx +1 -1
  196. package/src/plugins/postgres/PostgresStatusWidget.tsx +1 -1
  197. package/src/plugins/preferences/PreferencesManagementPage.tsx +1 -1
  198. package/src/plugins/preferences/PreferencesStatusWidget.tsx +1 -1
  199. package/src/plugins/profiles/ProfilesManagementPage.tsx +1 -1
  200. package/src/plugins/profiles/ProfilesStatusWidget.tsx +1 -1
  201. package/src/plugins/qwickbrain/QwickbrainManagementPage.tsx +1 -1
  202. package/src/plugins/qwickbrain/QwickbrainStatusWidget.tsx +1 -1
  203. package/src/plugins/rate-limit/RateLimitManagementPage.tsx +1 -1
  204. package/src/plugins/rate-limit/RateLimitStatusWidget.tsx +1 -1
  205. package/src/plugins/subscriptions/SubscriptionsManagementPage.tsx +1 -1
  206. package/src/plugins/subscriptions/SubscriptionsStatusWidget.tsx +1 -1
  207. package/src/plugins/usage/UsageManagementPage.tsx +1 -1
  208. package/src/plugins/usage/UsageStatusWidget.tsx +1 -1
  209. package/src/plugins/users/UsersManagementPage.tsx +1 -1
  210. package/src/plugins/users/UsersStatusWidget.tsx +1 -1
  211. package/ui/src/App.tsx +4 -3
  212. package/ui/src/api/clientBuilder.ts +5 -5
  213. package/ui/src/api/controlPanelApi.ts +19 -19
  214. package/ui/src/components/ControlPanelApp.tsx +5 -4
  215. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  216. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +8 -8
  217. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +2 -2
  218. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -4
  219. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +2 -1
  220. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -6
  221. package/ui/src/dashboard/widgets/MigrationManagementWidget.tsx +319 -0
  222. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +6 -6
  223. package/ui/src/dashboard/widgets/index.ts +1 -0
  224. package/ui/src/hooks/useJobStream.ts +2 -2
  225. package/ui/src/pages/ContentOpsJobsPage.tsx +3 -3
  226. package/ui/src/pages/PluginPage.tsx +1 -1
  227. package/ui/src/pages/TenantsManagementPage.tsx +1 -1
  228. package/dist-ui/assets/index-BkGp7ZKd.js +0 -529
  229. package/dist-ui/assets/index-DQED0KPI.css +0 -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
  }
@@ -450,12 +453,18 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
450
453
  pluginId: 'maintenance',
451
454
  handler: async (req: Request, res: Response) => {
452
455
  try {
453
- // Security: Only allow in local or development environments
454
- const nodeEnv = process.env.NODE_ENV?.toLowerCase();
455
- if (nodeEnv !== 'local' && nodeEnv !== 'development') {
456
+ // Security: Prevent running on production domain
457
+ const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || process.env.APP_URL || '';
458
+
459
+ // Block if domain is exactly faabzi.com (production)
460
+ // Allow dev.faabzi.com, staging.faabzi.com, localhost, etc.
461
+ const isProductionDomain = /^https?:\/\/faabzi\.com(\/|$)/.test(serverUrl);
462
+
463
+ if (isProductionDomain) {
456
464
  return res.status(403).json({
457
- error: 'Database reset is only available in local or development environments',
458
- currentEnv: nodeEnv || 'production',
465
+ error: 'Database reset is not allowed on production domain (faabzi.com)',
466
+ currentUrl: serverUrl,
467
+ allowedDomains: 'dev.faabzi.com, staging.faabzi.com, localhost, or local',
459
468
  });
460
469
  }
461
470
 
@@ -593,6 +602,462 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
593
602
  });
594
603
  }
595
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
+
596
1061
  // Register maintenance widgets
597
1062
  if (config.enableSeeds !== false) {
598
1063
  registry.addWidget({
@@ -606,6 +1071,19 @@ export function createMaintenancePlugin(config: MaintenancePluginConfig = {}): P
606
1071
  });
607
1072
  }
608
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
+
609
1087
  // TODO: Register service control routes
610
1088
  if (config.enableServiceControl !== false) {
611
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;