@qwickapps/server 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/src/core/control-panel.js +5 -5
  3. package/dist/src/core/control-panel.js.map +1 -1
  4. package/dist/src/core/gateway.d.ts.map +1 -1
  5. package/dist/src/core/gateway.js +117 -15
  6. package/dist/src/core/gateway.js.map +1 -1
  7. package/dist/src/core/plugin-registry.d.ts +70 -0
  8. package/dist/src/core/plugin-registry.d.ts.map +1 -1
  9. package/dist/src/core/plugin-registry.js +94 -0
  10. package/dist/src/core/plugin-registry.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/plugins/api-keys/api-keys-plugin.d.ts +5 -2
  14. package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
  15. package/dist/src/plugins/api-keys/api-keys-plugin.js +113 -19
  16. package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
  17. package/dist/src/plugins/api-keys/index.d.ts +1 -5
  18. package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
  19. package/dist/src/plugins/api-keys/index.js +2 -3
  20. package/dist/src/plugins/api-keys/index.js.map +1 -1
  21. package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
  22. package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
  23. package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
  24. package/dist/src/plugins/api-keys/types.d.ts +22 -4
  25. package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
  26. package/dist/src/plugins/api-keys/types.js.map +1 -1
  27. package/dist/src/plugins/auth/index.d.ts +0 -4
  28. package/dist/src/plugins/auth/index.d.ts.map +1 -1
  29. package/dist/src/plugins/auth/index.js +2 -3
  30. package/dist/src/plugins/auth/index.js.map +1 -1
  31. package/dist/src/plugins/bans/bans-plugin.d.ts +5 -2
  32. package/dist/src/plugins/bans/bans-plugin.d.ts.map +1 -1
  33. package/dist/src/plugins/bans/bans-plugin.js +71 -25
  34. package/dist/src/plugins/bans/bans-plugin.js.map +1 -1
  35. package/dist/src/plugins/bans/index.d.ts +0 -4
  36. package/dist/src/plugins/bans/index.d.ts.map +1 -1
  37. package/dist/src/plugins/bans/index.js +2 -3
  38. package/dist/src/plugins/bans/index.js.map +1 -1
  39. package/dist/src/plugins/bans/types.d.ts +13 -6
  40. package/dist/src/plugins/bans/types.d.ts.map +1 -1
  41. package/dist/src/plugins/devices/devices-plugin.d.ts +5 -2
  42. package/dist/src/plugins/devices/devices-plugin.d.ts.map +1 -1
  43. package/dist/src/plugins/devices/devices-plugin.js +62 -26
  44. package/dist/src/plugins/devices/devices-plugin.js.map +1 -1
  45. package/dist/src/plugins/devices/index.d.ts +0 -4
  46. package/dist/src/plugins/devices/index.d.ts.map +1 -1
  47. package/dist/src/plugins/devices/index.js +2 -3
  48. package/dist/src/plugins/devices/index.js.map +1 -1
  49. package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
  50. package/dist/src/plugins/diagnostics-plugin.js +73 -0
  51. package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
  52. package/dist/src/plugins/entitlements/entitlements-plugin.d.ts +5 -2
  53. package/dist/src/plugins/entitlements/entitlements-plugin.d.ts.map +1 -1
  54. package/dist/src/plugins/entitlements/entitlements-plugin.js +78 -41
  55. package/dist/src/plugins/entitlements/entitlements-plugin.js.map +1 -1
  56. package/dist/src/plugins/entitlements/index.d.ts +0 -4
  57. package/dist/src/plugins/entitlements/index.d.ts.map +1 -1
  58. package/dist/src/plugins/entitlements/index.js +2 -3
  59. package/dist/src/plugins/entitlements/index.js.map +1 -1
  60. package/dist/src/plugins/entitlements/types.d.ts +9 -2
  61. package/dist/src/plugins/entitlements/types.d.ts.map +1 -1
  62. package/dist/src/plugins/index.d.ts +1 -1
  63. package/dist/src/plugins/index.d.ts.map +1 -1
  64. package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
  65. package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
  66. package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
  67. package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
  68. package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
  69. package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
  70. package/dist/src/plugins/maintenance/SeedList.js +39 -14
  71. package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
  72. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
  73. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
  74. package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
  75. package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
  76. package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
  77. package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
  78. package/dist/src/plugins/maintenance/seed-executor.js +53 -17
  79. package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
  80. package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
  81. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  82. package/dist/src/plugins/maintenance-plugin.js +222 -34
  83. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  84. package/dist/src/plugins/notifications/index.d.ts +0 -4
  85. package/dist/src/plugins/notifications/index.d.ts.map +1 -1
  86. package/dist/src/plugins/notifications/index.js +2 -3
  87. package/dist/src/plugins/notifications/index.js.map +1 -1
  88. package/dist/src/plugins/notifications/notifications-plugin.d.ts +5 -2
  89. package/dist/src/plugins/notifications/notifications-plugin.d.ts.map +1 -1
  90. package/dist/src/plugins/notifications/notifications-plugin.js +45 -13
  91. package/dist/src/plugins/notifications/notifications-plugin.js.map +1 -1
  92. package/dist/src/plugins/parental/index.d.ts +0 -4
  93. package/dist/src/plugins/parental/index.d.ts.map +1 -1
  94. package/dist/src/plugins/parental/index.js +2 -3
  95. package/dist/src/plugins/parental/index.js.map +1 -1
  96. package/dist/src/plugins/parental/parental-plugin.d.ts +5 -2
  97. package/dist/src/plugins/parental/parental-plugin.d.ts.map +1 -1
  98. package/dist/src/plugins/parental/parental-plugin.js +60 -24
  99. package/dist/src/plugins/parental/parental-plugin.js.map +1 -1
  100. package/dist/src/plugins/postgres-plugin.d.ts +12 -0
  101. package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
  102. package/dist/src/plugins/postgres-plugin.js +319 -5
  103. package/dist/src/plugins/postgres-plugin.js.map +1 -1
  104. package/dist/src/plugins/preferences/index.d.ts +0 -4
  105. package/dist/src/plugins/preferences/index.d.ts.map +1 -1
  106. package/dist/src/plugins/preferences/index.js +2 -3
  107. package/dist/src/plugins/preferences/index.js.map +1 -1
  108. package/dist/src/plugins/preferences/preferences-plugin.d.ts +5 -2
  109. package/dist/src/plugins/preferences/preferences-plugin.d.ts.map +1 -1
  110. package/dist/src/plugins/preferences/preferences-plugin.js +63 -19
  111. package/dist/src/plugins/preferences/preferences-plugin.js.map +1 -1
  112. package/dist/src/plugins/profiles/index.d.ts +0 -4
  113. package/dist/src/plugins/profiles/index.d.ts.map +1 -1
  114. package/dist/src/plugins/profiles/index.js +2 -3
  115. package/dist/src/plugins/profiles/index.js.map +1 -1
  116. package/dist/src/plugins/profiles/profiles-plugin.d.ts +5 -2
  117. package/dist/src/plugins/profiles/profiles-plugin.d.ts.map +1 -1
  118. package/dist/src/plugins/profiles/profiles-plugin.js +60 -26
  119. package/dist/src/plugins/profiles/profiles-plugin.js.map +1 -1
  120. package/dist/src/plugins/profiles/types.d.ts +9 -2
  121. package/dist/src/plugins/profiles/types.d.ts.map +1 -1
  122. package/dist/src/plugins/qwickbrain/index.d.ts +0 -4
  123. package/dist/src/plugins/qwickbrain/index.d.ts.map +1 -1
  124. package/dist/src/plugins/qwickbrain/index.js +2 -3
  125. package/dist/src/plugins/qwickbrain/index.js.map +1 -1
  126. package/dist/src/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -1
  127. package/dist/src/plugins/qwickbrain/qwickbrain-plugin.js +117 -0
  128. package/dist/src/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -1
  129. package/dist/src/plugins/rate-limit/index.d.ts +0 -4
  130. package/dist/src/plugins/rate-limit/index.d.ts.map +1 -1
  131. package/dist/src/plugins/rate-limit/index.js +2 -3
  132. package/dist/src/plugins/rate-limit/index.js.map +1 -1
  133. package/dist/src/plugins/subscriptions/index.d.ts +0 -4
  134. package/dist/src/plugins/subscriptions/index.d.ts.map +1 -1
  135. package/dist/src/plugins/subscriptions/index.js +2 -3
  136. package/dist/src/plugins/subscriptions/index.js.map +1 -1
  137. package/dist/src/plugins/subscriptions/subscriptions-plugin.d.ts +5 -2
  138. package/dist/src/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -1
  139. package/dist/src/plugins/subscriptions/subscriptions-plugin.js +63 -29
  140. package/dist/src/plugins/subscriptions/subscriptions-plugin.js.map +1 -1
  141. package/dist/src/plugins/subscriptions/types.d.ts +8 -2
  142. package/dist/src/plugins/subscriptions/types.d.ts.map +1 -1
  143. package/dist/src/plugins/tenants/tenants-plugin.d.ts +5 -2
  144. package/dist/src/plugins/tenants/tenants-plugin.d.ts.map +1 -1
  145. package/dist/src/plugins/tenants/tenants-plugin.js +91 -58
  146. package/dist/src/plugins/tenants/tenants-plugin.js.map +1 -1
  147. package/dist/src/plugins/tenants/types.d.ts +8 -2
  148. package/dist/src/plugins/tenants/types.d.ts.map +1 -1
  149. package/dist/src/plugins/usage/index.d.ts +0 -4
  150. package/dist/src/plugins/usage/index.d.ts.map +1 -1
  151. package/dist/src/plugins/usage/index.js +2 -3
  152. package/dist/src/plugins/usage/index.js.map +1 -1
  153. package/dist/src/plugins/usage/usage-plugin.d.ts +5 -2
  154. package/dist/src/plugins/usage/usage-plugin.d.ts.map +1 -1
  155. package/dist/src/plugins/usage/usage-plugin.js +57 -23
  156. package/dist/src/plugins/usage/usage-plugin.js.map +1 -1
  157. package/dist/src/plugins/users/types.d.ts +7 -2
  158. package/dist/src/plugins/users/types.d.ts.map +1 -1
  159. package/dist/src/plugins/users/users-plugin.d.ts +5 -2
  160. package/dist/src/plugins/users/users-plugin.d.ts.map +1 -1
  161. package/dist/src/plugins/users/users-plugin.js +56 -23
  162. package/dist/src/plugins/users/users-plugin.js.map +1 -1
  163. package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
  164. package/dist/ui/src/components/ControlPanelApp.js +4 -3
  165. package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
  166. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  167. package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
  168. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  169. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
  170. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
  171. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
  172. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
  173. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
  174. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
  175. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
  176. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
  177. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
  178. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
  179. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
  180. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
  181. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
  182. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
  183. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
  184. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
  185. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  186. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
  187. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
  188. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
  189. package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
  190. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  191. package/dist/ui/src/dashboard/widgets/index.js +1 -0
  192. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  193. package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
  194. package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
  195. package/dist-ui/index.html +1 -1
  196. package/dist-ui-lib/index.js +3735 -3187
  197. package/dist-ui-lib/index.js.map +1 -1
  198. package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
  199. package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  200. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
  201. package/package.json +8 -5
  202. package/src/core/control-panel.ts +5 -5
  203. package/src/core/gateway.ts +135 -15
  204. package/src/core/plugin-registry.ts +171 -0
  205. package/src/index.ts +2 -0
  206. package/src/plugins/api-keys/api-keys-plugin.ts +121 -20
  207. package/src/plugins/api-keys/index.ts +3 -5
  208. package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
  209. package/src/plugins/api-keys/types.ts +23 -4
  210. package/src/plugins/auth/index.ts +3 -5
  211. package/src/plugins/bans/bans-plugin.ts +71 -26
  212. package/src/plugins/bans/index.ts +3 -5
  213. package/src/plugins/bans/types.ts +13 -6
  214. package/src/plugins/devices/devices-plugin.ts +62 -27
  215. package/src/plugins/devices/index.ts +3 -5
  216. package/src/plugins/diagnostics-plugin.ts +77 -0
  217. package/src/plugins/entitlements/entitlements-plugin.ts +81 -43
  218. package/src/plugins/entitlements/index.ts +3 -5
  219. package/src/plugins/entitlements/types.ts +9 -2
  220. package/src/plugins/index.ts +1 -1
  221. package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
  222. package/src/plugins/maintenance/SeedList.tsx +85 -38
  223. package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
  224. package/src/plugins/maintenance/seed-executor.ts +56 -17
  225. package/src/plugins/maintenance-plugin.ts +267 -36
  226. package/src/plugins/notifications/index.ts +3 -5
  227. package/src/plugins/notifications/notifications-plugin.ts +48 -19
  228. package/src/plugins/parental/index.ts +3 -5
  229. package/src/plugins/parental/parental-plugin.ts +63 -25
  230. package/src/plugins/postgres-plugin.ts +410 -5
  231. package/src/plugins/preferences/index.ts +3 -5
  232. package/src/plugins/preferences/preferences-plugin.ts +66 -20
  233. package/src/plugins/profiles/index.ts +3 -5
  234. package/src/plugins/profiles/profiles-plugin.ts +60 -27
  235. package/src/plugins/profiles/types.ts +9 -2
  236. package/src/plugins/qwickbrain/index.ts +3 -5
  237. package/src/plugins/qwickbrain/qwickbrain-plugin.ts +135 -0
  238. package/src/plugins/rate-limit/index.ts +3 -5
  239. package/src/plugins/subscriptions/index.ts +3 -5
  240. package/src/plugins/subscriptions/subscriptions-plugin.ts +63 -30
  241. package/src/plugins/subscriptions/types.ts +8 -2
  242. package/src/plugins/tenants/tenants-plugin.ts +95 -60
  243. package/src/plugins/tenants/types.ts +8 -2
  244. package/src/plugins/usage/index.ts +3 -5
  245. package/src/plugins/usage/usage-plugin.ts +60 -26
  246. package/src/plugins/users/types.ts +7 -2
  247. package/src/plugins/users/users-plugin.ts +56 -24
  248. package/ui/src/App.tsx +3 -3
  249. package/ui/src/components/ControlPanelApp.tsx +4 -3
  250. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  251. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
  252. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
  253. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
  254. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
  255. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
  256. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
  257. package/ui/src/dashboard/widgets/index.ts +1 -0
  258. package/dist-ui/assets/index-8y0jDGcd.js +0 -528
  259. package/dist-ui/assets/index-8y0jDGcd.js.map +0 -1
@@ -24,6 +24,9 @@ import type {
24
24
  CreateRestrictionInput,
25
25
  LogActivityInput,
26
26
  } from './types.js';
27
+ import { hasPostgres, getPostgres } from '../postgres-plugin.js';
28
+ import { postgresParentalStore } from './stores/index.js';
29
+ import { kidsAdapter } from './adapters/index.js';
27
30
 
28
31
  // Store instances for helper access
29
32
  let currentStore: ParentalStore | null = null;
@@ -54,17 +57,18 @@ function isWithinSchedule(schedule: Record<string, { start: string; end: string
54
57
  }
55
58
 
56
59
  /**
57
- * Create the Parental plugin
60
+ * Create the Parental plugin with smart defaults
61
+ *
62
+ * Config is optional - plugin will use defaults and get dependencies from registry.
63
+ * Gracefully handles missing dependencies with clear log messages.
58
64
  */
59
- export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
60
- const debug = config.debug || false;
61
- const apiPrefix = config.api?.prefix || '/'; // Framework adds /parental prefix automatically
62
- const maxPinAttempts = config.maxPinAttempts || 5;
63
- const pinLockoutMinutes = config.pinLockoutMinutes || 30;
64
-
65
- function log(message: string, data?: Record<string, unknown>) {
66
- if (debug) {
67
- console.log(`[ParentalPlugin] ${message}`, data || '');
65
+ export function createParentalPlugin(config: Partial<ParentalPluginConfig> = {}): Plugin {
66
+ function log(message: string, data?: Record<string, unknown>, isError = false) {
67
+ const prefix = '[ParentalPlugin]';
68
+ if (isError) {
69
+ console.error(`${prefix} ${message}`, data || '');
70
+ } else if (config.debug) {
71
+ console.log(`${prefix} ${message}`, data || '');
68
72
  }
69
73
  }
70
74
 
@@ -74,16 +78,48 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
74
78
  version: '1.0.0',
75
79
 
76
80
  async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
81
+ const logger = registry.getLogger('parental');
82
+
83
+ // Check for postgres in registry
84
+ if (!hasPostgres()) {
85
+ logger.warn('No Database! Parental plugin disabled.');
86
+ registry.registerHealthCheck({
87
+ name: 'parental-store',
88
+ type: 'custom',
89
+ check: async () => ({
90
+ healthy: false,
91
+ details: {
92
+ error: 'PostgreSQL not available',
93
+ state: 'disabled',
94
+ },
95
+ }),
96
+ });
97
+ return;
98
+ }
99
+
100
+ // Smart defaults - get dependencies from registry
101
+ const store = config.store ?? postgresParentalStore({
102
+ pool: () => getPostgres().getPool(),
103
+ autoCreateTables: true,
104
+ });
105
+
106
+ const adapter = config.adapter ?? kidsAdapter();
107
+ const debug = config.debug ?? false;
108
+ const apiPrefix = config.api?.prefix ?? '/'; // Framework adds /parental prefix automatically
109
+ const apiEnabled = config.api?.enabled ?? true;
110
+ const maxPinAttempts = config.maxPinAttempts ?? 5;
111
+ const pinLockoutMinutes = config.pinLockoutMinutes ?? 30;
112
+
77
113
  log('Starting parental plugin');
78
114
 
79
115
  // Initialize the store (creates tables if needed)
80
- await config.store.initialize();
116
+ await store.initialize();
81
117
  log('Parental store initialized');
82
118
 
83
119
  // Store references for helper access
84
- currentStore = config.store;
85
- currentAdapter = config.adapter;
86
- currentConfig = config;
120
+ currentStore = store;
121
+ currentAdapter = adapter;
122
+ currentConfig = { ...config, store, adapter, debug, maxPinAttempts, pinLockoutMinutes };
87
123
 
88
124
  // Register health check
89
125
  registry.registerHealthCheck({
@@ -99,7 +135,7 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
99
135
  });
100
136
 
101
137
  // Add API routes if enabled
102
- if (config.api?.enabled !== false) {
138
+ if (apiEnabled) {
103
139
  // ═══════════════════════════════════════════════════════════════════════
104
140
  // Guardian Settings Routes
105
141
  // ═══════════════════════════════════════════════════════════════════════
@@ -140,7 +176,7 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
140
176
  // Hash PIN if provided
141
177
  const processedInput = {
142
178
  ...input,
143
- adapter_type: config.adapter.name,
179
+ adapter_type: adapter.name,
144
180
  pin: input.pin ? hashPin(input.pin) : undefined,
145
181
  };
146
182
 
@@ -290,8 +326,8 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
290
326
  const input = req.body as CreateRestrictionInput;
291
327
 
292
328
  // Validate with adapter
293
- if (config.adapter.validateRestriction) {
294
- const validation = config.adapter.validateRestriction(input);
329
+ if (adapter.validateRestriction) {
330
+ const validation = adapter.validateRestriction(input);
295
331
  if (!validation.valid) {
296
332
  return res.status(400).json({ error: 'Invalid restriction', errors: validation.errors });
297
333
  }
@@ -416,7 +452,7 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
416
452
  const input = req.body as LogActivityInput;
417
453
  const activity = await logActivity({
418
454
  ...input,
419
- adapter_type: config.adapter.name,
455
+ adapter_type: adapter.name,
420
456
  });
421
457
  res.status(201).json(activity);
422
458
  } catch (error) {
@@ -441,10 +477,10 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
441
477
 
442
478
  // Format details with adapter if available
443
479
  const formattedActivities = activities.map((activity) => {
444
- if (config.adapter.formatActivityDetails) {
480
+ if (adapter.formatActivityDetails) {
445
481
  return {
446
482
  ...activity,
447
- formatted_details: config.adapter.formatActivityDetails(activity),
483
+ formatted_details: adapter.formatActivityDetails(activity),
448
484
  };
449
485
  }
450
486
  return activity;
@@ -466,9 +502,9 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
466
502
  handler: async (_req: Request, res: Response) => {
467
503
  try {
468
504
  res.json({
469
- name: config.adapter.name,
470
- activity_types: config.adapter.getActivityTypes(),
471
- default_daily_limit: config.adapter.getDefaultDailyLimit(),
505
+ name: adapter.name,
506
+ activity_types: adapter.getActivityTypes(),
507
+ default_daily_limit: adapter.getDefaultDailyLimit(),
472
508
  });
473
509
  } catch (error) {
474
510
  console.error('[ParentalPlugin] Get adapter info error:', error);
@@ -483,7 +519,9 @@ export function createParentalPlugin(config: ParentalPluginConfig): Plugin {
483
519
 
484
520
  async onStop(): Promise<void> {
485
521
  log('Stopping parental plugin');
486
- await config.store.shutdown();
522
+ if (currentStore) {
523
+ await currentStore.shutdown();
524
+ }
487
525
  currentStore = null;
488
526
  currentAdapter = null;
489
527
  currentConfig = null;
@@ -90,6 +90,26 @@ export interface PostgresPluginConfig {
90
90
 
91
91
  /** Called on pool errors */
92
92
  onError?: (error: Error) => void;
93
+
94
+ // Database initialization options
95
+
96
+ /** Admin user for database operations (e.g., 'postgres') */
97
+ adminUser?: string;
98
+
99
+ /** Admin password for database operations */
100
+ adminPassword?: string;
101
+
102
+ /** Admin database to connect to for operations (default: 'postgres') */
103
+ adminDatabase?: string;
104
+
105
+ /** Database name to create/ensure exists (parsed from url if not provided) */
106
+ databaseName?: string;
107
+
108
+ /** User who should own the database (parsed from url if not provided) */
109
+ databaseOwner?: string;
110
+
111
+ /** Automatically initialize database if connection fails (default: true if admin credentials provided) */
112
+ autoInitialize?: boolean;
93
113
  }
94
114
 
95
115
  /**
@@ -140,6 +160,127 @@ export interface PostgresInstance {
140
160
  // Global registry of PostgreSQL instances by name
141
161
  const instances = new Map<string, PostgresInstance>();
142
162
 
163
+ /**
164
+ * Parse database connection URL to extract components
165
+ */
166
+ function parseConnectionUrl(url: string): {
167
+ user: string;
168
+ password: string;
169
+ host: string;
170
+ port: number;
171
+ database: string;
172
+ } {
173
+ const match = url.match(/postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)/);
174
+ if (!match) {
175
+ throw new Error('Invalid PostgreSQL connection URL format');
176
+ }
177
+ return {
178
+ user: match[1],
179
+ password: match[2],
180
+ host: match[3],
181
+ port: parseInt(match[4], 10),
182
+ database: match[5],
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Helper to create an admin pool for database operations
188
+ */
189
+ function createAdminPool(config: {
190
+ adminUser: string;
191
+ adminPassword: string;
192
+ host: string;
193
+ port: number;
194
+ adminDatabase?: string;
195
+ }): pg.Pool {
196
+ return new Pool({
197
+ user: config.adminUser,
198
+ password: config.adminPassword,
199
+ host: config.host,
200
+ port: config.port,
201
+ database: config.adminDatabase || 'postgres',
202
+ max: 1,
203
+ idleTimeoutMillis: 5000,
204
+ connectionTimeoutMillis: 5000,
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Ensure database user exists with password
210
+ */
211
+ async function ensureUserExists(
212
+ adminPool: pg.Pool,
213
+ user: string,
214
+ password: string
215
+ ): Promise<void> {
216
+ const result = await adminPool.query(
217
+ `SELECT 1 FROM pg_roles WHERE rolname = $1`,
218
+ [user]
219
+ );
220
+
221
+ if (result.rows.length === 0) {
222
+ await adminPool.query(
223
+ `CREATE USER ${user} WITH PASSWORD '${password}'`
224
+ );
225
+ } else {
226
+ await adminPool.query(
227
+ `ALTER USER ${user} WITH PASSWORD '${password}'`
228
+ );
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Ensure database exists with correct owner
234
+ */
235
+ async function ensureDatabaseExists(
236
+ adminPool: pg.Pool,
237
+ database: string,
238
+ owner: string
239
+ ): Promise<void> {
240
+ const result = await adminPool.query(
241
+ `SELECT 1 FROM pg_database WHERE datname = $1`,
242
+ [database]
243
+ );
244
+
245
+ if (result.rows.length === 0) {
246
+ await adminPool.query(
247
+ `CREATE DATABASE ${database} OWNER ${owner}`
248
+ );
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Grant all permissions to user on database
254
+ */
255
+ async function grantPermissions(
256
+ adminPool: pg.Pool,
257
+ database: string,
258
+ user: string
259
+ ): Promise<void> {
260
+ const tempPool = new Pool({
261
+ user: adminPool.options.user as string,
262
+ password: adminPool.options.password as string,
263
+ host: adminPool.options.host as string,
264
+ port: adminPool.options.port as number,
265
+ database,
266
+ max: 1,
267
+ idleTimeoutMillis: 5000,
268
+ connectionTimeoutMillis: 5000,
269
+ });
270
+
271
+ try {
272
+ await tempPool.query(`
273
+ GRANT ALL ON SCHEMA public TO ${user};
274
+ GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${user};
275
+ GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${user};
276
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${user};
277
+ ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO ${user};
278
+ `);
279
+ } finally {
280
+ await tempPool.end();
281
+ }
282
+ }
283
+
143
284
  /**
144
285
  * Get a PostgreSQL instance by name
145
286
  *
@@ -305,15 +446,279 @@ export function createPostgresPlugin(
305
446
  const instance = createInstance();
306
447
  instances.set(instanceName, instance);
307
448
 
308
- // Test connection
449
+ // Register maintenance widget FIRST (before connection attempt)
450
+ // This ensures the widget is available even if database connection fails
451
+ registry.addWidget({
452
+ id: `postgres-operations-${instanceName}`,
453
+ title: `Database Operations (${instanceName})`,
454
+ component: 'DatabaseOperationsWidget',
455
+ type: 'maintenance',
456
+ priority: 50,
457
+ showByDefault: true,
458
+ pluginId: pluginId,
459
+ });
460
+
461
+ // Three-phase initialization: connect → auto-repair → error state
462
+
463
+ // PHASE 1: Try to connect with DATABASE_URI
309
464
  try {
310
465
  await instance.query('SELECT 1');
311
- logger.debug(`PostgreSQL "${instanceName}" connected`);
312
- } catch (err) {
313
- logger.error(`PostgreSQL "${instanceName}" connection failed: ${err instanceof Error ? err.message : String(err)}`);
314
- throw err;
466
+ logger.info(`PostgreSQL "${instanceName}" connected successfully`);
467
+ } catch (connectionError) {
468
+ const errorMsg = connectionError instanceof Error ? connectionError.message : String(connectionError);
469
+ logger.warn(`PostgreSQL "${instanceName}" connection failed: ${errorMsg}`);
470
+
471
+ // PHASE 2: Auto-repair if admin credentials provided
472
+ const shouldAutoRepair =
473
+ config.adminUser &&
474
+ config.adminPassword &&
475
+ config.autoInitialize !== false &&
476
+ config.url;
477
+
478
+ if (shouldAutoRepair) {
479
+ logger.info(`Attempting auto-repair for "${instanceName}"...`);
480
+
481
+ try {
482
+ const connParams = parseConnectionUrl(config.url!);
483
+ const adminPool = createAdminPool({
484
+ adminUser: config.adminUser!,
485
+ adminPassword: config.adminPassword!,
486
+ host: connParams.host,
487
+ port: connParams.port,
488
+ adminDatabase: config.adminDatabase,
489
+ });
490
+
491
+ try {
492
+ // Ensure user exists
493
+ logger.debug(`Ensuring user "${connParams.user}" exists...`);
494
+ await ensureUserExists(adminPool, connParams.user, connParams.password);
495
+
496
+ // Ensure database exists
497
+ logger.debug(`Ensuring database "${connParams.database}" exists...`);
498
+ await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
499
+
500
+ // Grant permissions
501
+ logger.debug(`Granting permissions to "${connParams.user}" on "${connParams.database}"...`);
502
+ await grantPermissions(adminPool, connParams.database, connParams.user);
503
+
504
+ logger.info(`Auto-repair completed successfully for "${instanceName}"`);
505
+
506
+ // Try connection again after repair
507
+ await instance.query('SELECT 1');
508
+ logger.info(`PostgreSQL "${instanceName}" connected after auto-repair`);
509
+ } finally {
510
+ await adminPool.end();
511
+ }
512
+ } catch (repairError) {
513
+ const repairMsg = repairError instanceof Error ? repairError.message : String(repairError);
514
+ // Log error but don't throw - allow plugin to continue starting
515
+ // This ensures the widget and API routes are available for manual database initialization
516
+ logger.error(
517
+ `PostgreSQL connection failed and auto-repair unsuccessful. ` +
518
+ `Original error: ${errorMsg}. Repair error: ${repairMsg}. ` +
519
+ `Use the maintenance UI to manually initialize the database.`
520
+ );
521
+ }
522
+ } else {
523
+ // PHASE 3: No auto-repair available, remain in error state
524
+ const missingConfig = [];
525
+ if (!config.adminUser) missingConfig.push('adminUser');
526
+ if (!config.adminPassword) missingConfig.push('adminPassword');
527
+
528
+ const hint = missingConfig.length > 0
529
+ ? ` Provide ${missingConfig.join(', ')} in config to enable auto-repair.`
530
+ : ' Set autoInitialize=true to enable auto-repair.';
531
+
532
+ // Log error but don't throw - allow plugin to continue starting
533
+ // This ensures the widget and API routes are available for manual database initialization
534
+ logger.error(
535
+ `PostgreSQL connection failed: ${errorMsg}.${hint} ` +
536
+ `Use the maintenance UI to manually initialize the database.`
537
+ );
538
+ }
315
539
  }
316
540
 
541
+ // Register API routes for database operations
542
+ registry.addRoute({
543
+ method: 'get',
544
+ path: '/status',
545
+ pluginId: pluginId,
546
+ handler: async (req: import('express').Request, res: import('express').Response) => {
547
+ try {
548
+ const requestedInstance = (req.query.instance as string) || 'default';
549
+ const targetInstance = instances.get(requestedInstance);
550
+
551
+ if (!targetInstance) {
552
+ return res.status(404).json({
553
+ status: 'error',
554
+ connected: false,
555
+ errorMessage: `PostgreSQL instance "${requestedInstance}" not found`,
556
+ autoInitializeEnabled: false,
557
+ adminCredentialsProvided: false,
558
+ });
559
+ }
560
+
561
+ let connParams: ReturnType<typeof parseConnectionUrl> | null = null;
562
+ if (config.url) {
563
+ try {
564
+ connParams = parseConnectionUrl(config.url);
565
+ } catch (err) {
566
+ // URL parsing failed, ignore
567
+ }
568
+ }
569
+
570
+ try {
571
+ await targetInstance.query('SELECT 1');
572
+ res.json({
573
+ status: 'healthy',
574
+ connected: true,
575
+ database: connParams?.database,
576
+ user: connParams?.user,
577
+ host: connParams?.host,
578
+ port: connParams?.port,
579
+ autoInitializeEnabled: config.autoInitialize !== false,
580
+ adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
581
+ });
582
+ } catch (err) {
583
+ res.json({
584
+ status: 'error',
585
+ connected: false,
586
+ database: connParams?.database,
587
+ user: connParams?.user,
588
+ host: connParams?.host,
589
+ port: connParams?.port,
590
+ errorMessage: err instanceof Error ? err.message : String(err),
591
+ autoInitializeEnabled: config.autoInitialize !== false,
592
+ adminCredentialsProvided: !!(config.adminUser && config.adminPassword),
593
+ });
594
+ }
595
+ } catch (err) {
596
+ res.status(500).json({
597
+ status: 'error',
598
+ connected: false,
599
+ errorMessage: err instanceof Error ? err.message : 'Unknown error',
600
+ autoInitializeEnabled: false,
601
+ adminCredentialsProvided: false,
602
+ });
603
+ }
604
+ },
605
+ });
606
+
607
+ registry.addRoute({
608
+ method: 'post',
609
+ path: '/initialize',
610
+ pluginId: pluginId,
611
+ handler: async (req: import('express').Request, res: import('express').Response) => {
612
+ try {
613
+ const { instance: requestedInstance, adminUser, adminPassword } = req.body;
614
+ const targetInstance = requestedInstance || 'default';
615
+
616
+ if (!instances.has(targetInstance)) {
617
+ return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
618
+ }
619
+
620
+ if (!config.url) {
621
+ return res.status(400).json({ message: 'No database URL configured' });
622
+ }
623
+
624
+ const connParams = parseConnectionUrl(config.url);
625
+ const effectiveAdminUser = adminUser || config.adminUser;
626
+ const effectiveAdminPassword = adminPassword || config.adminPassword;
627
+
628
+ if (!effectiveAdminUser || !effectiveAdminPassword) {
629
+ return res.status(400).json({
630
+ message: 'Admin credentials required. Provide adminUser and adminPassword.',
631
+ });
632
+ }
633
+
634
+ const adminPool = createAdminPool({
635
+ adminUser: effectiveAdminUser,
636
+ adminPassword: effectiveAdminPassword,
637
+ host: connParams.host,
638
+ port: connParams.port,
639
+ adminDatabase: config.adminDatabase,
640
+ });
641
+
642
+ try {
643
+ await ensureUserExists(adminPool, connParams.user, connParams.password);
644
+ await ensureDatabaseExists(adminPool, connParams.database, connParams.user);
645
+ await grantPermissions(adminPool, connParams.database, connParams.user);
646
+
647
+ logger.info(`Database "${connParams.database}" initialized successfully`);
648
+ res.json({ message: `Database "${connParams.database}" initialized successfully` });
649
+ } finally {
650
+ await adminPool.end();
651
+ }
652
+ } catch (err) {
653
+ logger.error('Database initialization failed', { error: err });
654
+ res.status(500).json({
655
+ message: err instanceof Error ? err.message : 'Unknown error',
656
+ });
657
+ }
658
+ },
659
+ });
660
+
661
+ registry.addRoute({
662
+ method: 'post',
663
+ path: '/recreate',
664
+ pluginId: pluginId,
665
+ handler: async (req: import('express').Request, res: import('express').Response) => {
666
+ try {
667
+ const { instance: requestedInstance, adminUser, adminPassword } = req.body;
668
+ const targetInstance = requestedInstance || 'default';
669
+
670
+ if (!instances.has(targetInstance)) {
671
+ return res.status(404).json({ message: `Instance "${targetInstance}" not found` });
672
+ }
673
+
674
+ if (!config.url) {
675
+ return res.status(400).json({ message: 'No database URL configured' });
676
+ }
677
+
678
+ const connParams = parseConnectionUrl(config.url);
679
+ const effectiveAdminUser = adminUser || config.adminUser;
680
+ const effectiveAdminPassword = adminPassword || config.adminPassword;
681
+
682
+ if (!effectiveAdminUser || !effectiveAdminPassword) {
683
+ return res.status(400).json({
684
+ message: 'Admin credentials required. Provide adminUser and adminPassword.',
685
+ });
686
+ }
687
+
688
+ const adminPool = createAdminPool({
689
+ adminUser: effectiveAdminUser,
690
+ adminPassword: effectiveAdminPassword,
691
+ host: connParams.host,
692
+ port: connParams.port,
693
+ adminDatabase: config.adminDatabase,
694
+ });
695
+
696
+ try {
697
+ // Drop database if exists
698
+ await adminPool.query(`DROP DATABASE IF EXISTS ${connParams.database}`);
699
+
700
+ // Recreate database
701
+ await adminPool.query(
702
+ `CREATE DATABASE ${connParams.database} OWNER ${connParams.user}`
703
+ );
704
+
705
+ // Grant permissions
706
+ await grantPermissions(adminPool, connParams.database, connParams.user);
707
+
708
+ logger.info(`Database "${connParams.database}" recreated successfully`);
709
+ res.json({ message: `Database "${connParams.database}" recreated successfully` });
710
+ } finally {
711
+ await adminPool.end();
712
+ }
713
+ } catch (err) {
714
+ logger.error('Database recreation failed', { error: err });
715
+ res.status(500).json({
716
+ message: err instanceof Error ? err.message : 'Unknown error',
717
+ });
718
+ }
719
+ },
720
+ });
721
+
317
722
  // Register health check if enabled
318
723
  if (config.healthCheck !== false) {
319
724
  registry.registerHealthCheck({
@@ -29,8 +29,6 @@ export type {
29
29
  // Stores
30
30
  export { postgresPreferencesStore, deepMerge } from './stores/index.js';
31
31
 
32
- // UI Components
33
- export { PreferencesStatusWidget } from './PreferencesStatusWidget.js';
34
- export type { PreferencesStatusWidgetProps } from './PreferencesStatusWidget.js';
35
- export { PreferencesManagementPage } from './PreferencesManagementPage.js';
36
- export type { PreferencesManagementPageProps } from './PreferencesManagementPage.js';
32
+ // UI Components are exported from main package index (@qwickapps/server)
33
+ // Do NOT export here to avoid loading UI dependencies when importing plugins
34
+