@qwickapps/server 1.7.2 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/src/core/control-panel.js +5 -5
  3. package/dist/src/core/control-panel.js.map +1 -1
  4. package/dist/src/core/gateway.d.ts.map +1 -1
  5. package/dist/src/core/gateway.js +117 -15
  6. package/dist/src/core/gateway.js.map +1 -1
  7. package/dist/src/core/plugin-registry.d.ts +70 -0
  8. package/dist/src/core/plugin-registry.d.ts.map +1 -1
  9. package/dist/src/core/plugin-registry.js +94 -0
  10. package/dist/src/core/plugin-registry.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/plugins/api-keys/api-keys-plugin.d.ts.map +1 -1
  14. package/dist/src/plugins/api-keys/api-keys-plugin.js +53 -1
  15. package/dist/src/plugins/api-keys/api-keys-plugin.js.map +1 -1
  16. package/dist/src/plugins/api-keys/index.d.ts +1 -1
  17. package/dist/src/plugins/api-keys/index.d.ts.map +1 -1
  18. package/dist/src/plugins/api-keys/index.js.map +1 -1
  19. package/dist/src/plugins/api-keys/stores/postgres-store.d.ts.map +1 -1
  20. package/dist/src/plugins/api-keys/stores/postgres-store.js +83 -65
  21. package/dist/src/plugins/api-keys/stores/postgres-store.js.map +1 -1
  22. package/dist/src/plugins/api-keys/types.d.ts +13 -1
  23. package/dist/src/plugins/api-keys/types.d.ts.map +1 -1
  24. package/dist/src/plugins/api-keys/types.js.map +1 -1
  25. package/dist/src/plugins/diagnostics-plugin.d.ts.map +1 -1
  26. package/dist/src/plugins/diagnostics-plugin.js +73 -0
  27. package/dist/src/plugins/diagnostics-plugin.js.map +1 -1
  28. package/dist/src/plugins/index.d.ts +1 -1
  29. package/dist/src/plugins/index.d.ts.map +1 -1
  30. package/dist/src/plugins/maintenance/SeedExecutor.d.ts +2 -0
  31. package/dist/src/plugins/maintenance/SeedExecutor.d.ts.map +1 -1
  32. package/dist/src/plugins/maintenance/SeedExecutor.js +6 -2
  33. package/dist/src/plugins/maintenance/SeedExecutor.js.map +1 -1
  34. package/dist/src/plugins/maintenance/SeedList.d.ts +2 -2
  35. package/dist/src/plugins/maintenance/SeedList.d.ts.map +1 -1
  36. package/dist/src/plugins/maintenance/SeedList.js +39 -14
  37. package/dist/src/plugins/maintenance/SeedList.js.map +1 -1
  38. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts +1 -1
  39. package/dist/src/plugins/maintenance/SeedManagementPage.d.ts.map +1 -1
  40. package/dist/src/plugins/maintenance/SeedManagementPage.js +9 -5
  41. package/dist/src/plugins/maintenance/SeedManagementPage.js.map +1 -1
  42. package/dist/src/plugins/maintenance/seed-executor.d.ts +6 -4
  43. package/dist/src/plugins/maintenance/seed-executor.d.ts.map +1 -1
  44. package/dist/src/plugins/maintenance/seed-executor.js +53 -17
  45. package/dist/src/plugins/maintenance/seed-executor.js.map +1 -1
  46. package/dist/src/plugins/maintenance-plugin.d.ts +24 -0
  47. package/dist/src/plugins/maintenance-plugin.d.ts.map +1 -1
  48. package/dist/src/plugins/maintenance-plugin.js +222 -34
  49. package/dist/src/plugins/maintenance-plugin.js.map +1 -1
  50. package/dist/src/plugins/postgres-plugin.d.ts +12 -0
  51. package/dist/src/plugins/postgres-plugin.d.ts.map +1 -1
  52. package/dist/src/plugins/postgres-plugin.js +319 -5
  53. package/dist/src/plugins/postgres-plugin.js.map +1 -1
  54. package/dist/ui/src/components/ControlPanelApp.d.ts.map +1 -1
  55. package/dist/ui/src/components/ControlPanelApp.js +4 -3
  56. package/dist/ui/src/components/ControlPanelApp.js.map +1 -1
  57. package/dist/ui/src/dashboard/builtInWidgets.d.ts.map +1 -1
  58. package/dist/ui/src/dashboard/builtInWidgets.js +3 -1
  59. package/dist/ui/src/dashboard/builtInWidgets.js.map +1 -1
  60. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.d.ts.map +1 -1
  61. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js +17 -4
  62. package/dist/ui/src/dashboard/widgets/CMSMaintenanceWidget.js.map +1 -1
  63. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.d.ts.map +1 -1
  64. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js +5 -1
  65. package/dist/ui/src/dashboard/widgets/CMSStatusWidget.js.map +1 -1
  66. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.d.ts.map +1 -1
  67. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js +4 -2
  68. package/dist/ui/src/dashboard/widgets/CacheMaintenanceWidget.js.map +1 -1
  69. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +12 -0
  70. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.d.ts.map +1 -0
  71. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js +174 -0
  72. package/dist/ui/src/dashboard/widgets/DatabaseOperationsWidget.js.map +1 -0
  73. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.d.ts.map +1 -1
  74. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js +6 -3
  75. package/dist/ui/src/dashboard/widgets/LogsMaintenanceWidget.js.map +1 -1
  76. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  77. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.d.ts.map +1 -1
  78. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js +256 -16
  79. package/dist/ui/src/dashboard/widgets/SeedManagementWidget.js.map +1 -1
  80. package/dist/ui/src/dashboard/widgets/index.d.ts +1 -0
  81. package/dist/ui/src/dashboard/widgets/index.d.ts.map +1 -1
  82. package/dist/ui/src/dashboard/widgets/index.js +1 -0
  83. package/dist/ui/src/dashboard/widgets/index.js.map +1 -1
  84. package/dist-ui/assets/index-BkGp7ZKd.js +529 -0
  85. package/dist-ui/assets/index-BkGp7ZKd.js.map +1 -0
  86. package/dist-ui/index.html +1 -1
  87. package/dist-ui-lib/index.js +3735 -3187
  88. package/dist-ui-lib/index.js.map +1 -1
  89. package/dist-ui-lib/src/dashboard/widgets/DatabaseOperationsWidget.d.ts +11 -0
  90. package/dist-ui-lib/src/dashboard/widgets/SeedManagementWidget.d.ts +1 -1
  91. package/dist-ui-lib/src/dashboard/widgets/index.d.ts +1 -0
  92. package/package.json +2 -2
  93. package/src/core/control-panel.ts +5 -5
  94. package/src/core/gateway.ts +135 -15
  95. package/src/core/plugin-registry.ts +171 -0
  96. package/src/index.ts +2 -0
  97. package/src/plugins/api-keys/api-keys-plugin.ts +58 -1
  98. package/src/plugins/api-keys/index.ts +1 -0
  99. package/src/plugins/api-keys/stores/postgres-store.ts +90 -67
  100. package/src/plugins/api-keys/types.ts +14 -1
  101. package/src/plugins/diagnostics-plugin.ts +77 -0
  102. package/src/plugins/index.ts +1 -1
  103. package/src/plugins/maintenance/SeedExecutor.tsx +9 -1
  104. package/src/plugins/maintenance/SeedList.tsx +85 -38
  105. package/src/plugins/maintenance/SeedManagementPage.tsx +10 -4
  106. package/src/plugins/maintenance/seed-executor.ts +56 -17
  107. package/src/plugins/maintenance-plugin.ts +267 -36
  108. package/src/plugins/postgres-plugin.ts +410 -5
  109. package/ui/src/App.tsx +3 -3
  110. package/ui/src/components/ControlPanelApp.tsx +4 -3
  111. package/ui/src/dashboard/builtInWidgets.tsx +3 -0
  112. package/ui/src/dashboard/widgets/CMSMaintenanceWidget.tsx +17 -4
  113. package/ui/src/dashboard/widgets/CMSStatusWidget.tsx +5 -1
  114. package/ui/src/dashboard/widgets/CacheMaintenanceWidget.tsx +4 -2
  115. package/ui/src/dashboard/widgets/DatabaseOperationsWidget.tsx +410 -0
  116. package/ui/src/dashboard/widgets/LogsMaintenanceWidget.tsx +6 -3
  117. package/ui/src/dashboard/widgets/SeedManagementWidget.tsx +533 -49
  118. package/ui/src/dashboard/widgets/index.ts +1 -0
  119. package/dist-ui/assets/index-0gzisPdy.js +0 -528
  120. package/dist-ui/assets/index-0gzisPdy.js.map +0 -1
@@ -21,6 +21,7 @@ import type {
21
21
  UpdateApiKeyParams,
22
22
  ApiKey,
23
23
  ApiKeyWithPlaintext,
24
+ StoreInitializationResult,
24
25
  } from '../types.js';
25
26
 
26
27
  // Pool interface (from pg package)
@@ -239,82 +240,104 @@ export function postgresApiKeyStore(config: PostgresApiKeyStoreConfig): ApiKeySt
239
240
  return {
240
241
  name: 'postgres',
241
242
 
242
- async initialize(): Promise<void> {
243
- if (!autoCreateTables) return;
243
+ async initialize(): Promise<StoreInitializationResult> {
244
+ if (!autoCreateTables) {
245
+ return { success: true };
246
+ }
244
247
 
245
- const pool = getPool();
248
+ try {
249
+ const pool = getPool();
246
250
 
247
- // Create table with foreign key to users
248
- await pool.query(`
249
- CREATE TABLE IF NOT EXISTS ${tableFullName} (
250
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
251
- user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE,
252
- name VARCHAR(255) NOT NULL,
253
- key_hash VARCHAR(64) NOT NULL,
254
- key_prefix VARCHAR(12) NOT NULL,
255
- key_type VARCHAR(10) NOT NULL CHECK (key_type IN ('m2m', 'pat')),
256
- scopes TEXT[] NOT NULL DEFAULT '{}',
257
- last_used_at TIMESTAMPTZ,
258
- expires_at TIMESTAMPTZ,
259
- is_active BOOLEAN NOT NULL DEFAULT true,
260
- created_at TIMESTAMPTZ DEFAULT NOW(),
261
- updated_at TIMESTAMPTZ DEFAULT NOW()
262
- );
251
+ // Create table with foreign key to users
252
+ await pool.query(`
253
+ CREATE TABLE IF NOT EXISTS ${tableFullName} (
254
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
255
+ user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE,
256
+ name VARCHAR(255) NOT NULL,
257
+ key_hash VARCHAR(64) NOT NULL,
258
+ key_prefix VARCHAR(12) NOT NULL,
259
+ key_type VARCHAR(10) NOT NULL CHECK (key_type IN ('m2m', 'pat')),
260
+ scopes TEXT[] NOT NULL DEFAULT '{}',
261
+ last_used_at TIMESTAMPTZ,
262
+ expires_at TIMESTAMPTZ,
263
+ is_active BOOLEAN NOT NULL DEFAULT true,
264
+ created_at TIMESTAMPTZ DEFAULT NOW(),
265
+ updated_at TIMESTAMPTZ DEFAULT NOW()
266
+ );
267
+
268
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
269
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_key_prefix ON ${tableFullName}(key_prefix);
270
+ CREATE INDEX IF NOT EXISTS idx_${tableName}_key_hash ON ${tableFullName}(key_hash);
271
+ `);
263
272
 
264
- CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
265
- CREATE INDEX IF NOT EXISTS idx_${tableName}_key_prefix ON ${tableFullName}(key_prefix);
266
- CREATE INDEX IF NOT EXISTS idx_${tableName}_key_hash ON ${tableFullName}(key_hash);
267
- `);
268
-
269
- // Migration: Add user_id column if it doesn't exist (for existing installations)
270
- await pool.query(`
271
- DO $$
272
- DECLARE
273
- row_count INTEGER;
274
- BEGIN
275
- IF NOT EXISTS (
276
- SELECT 1 FROM information_schema.columns
277
- WHERE table_schema = '${schema}'
278
- AND table_name = '${tableName}'
279
- AND column_name = 'user_id'
280
- ) THEN
281
- -- Check if table has data
282
- EXECUTE 'SELECT COUNT(*) FROM ${tableFullName}' INTO row_count;
283
-
284
- IF row_count > 0 THEN
285
- -- If table has data, cannot add NOT NULL column
286
- -- This requires manual migration or data cleanup
287
- RAISE EXCEPTION 'Cannot add user_id column: table ${tableFullName} contains % rows. Please migrate data or clear the table first.', row_count;
288
- ELSE
289
- -- Table is empty, safe to add NOT NULL column
273
+ // Migration: Check if table needs user_id column migration
274
+ const needsMigration = await pool.query(`
275
+ SELECT
276
+ NOT EXISTS (
277
+ SELECT 1 FROM information_schema.columns
278
+ WHERE table_schema = '${schema}'
279
+ AND table_name = '${tableName}'
280
+ AND column_name = 'user_id'
281
+ ) AS missing_column,
282
+ (SELECT COUNT(*) FROM ${tableFullName}) AS row_count
283
+ `);
284
+
285
+ const { missing_column, row_count } = needsMigration.rows[0] as { missing_column: boolean; row_count: number };
286
+
287
+ if (missing_column) {
288
+ if (row_count > 0) {
289
+ // Table has data but is missing user_id column - requires maintenance
290
+ logger.warn(
291
+ `Table ${tableFullName} has ${row_count} rows but is missing user_id column. ` +
292
+ `Requires maintenance action to migrate or clear data.`
293
+ );
294
+ return {
295
+ success: false,
296
+ error: `Table ${tableFullName} contains ${row_count} rows without user_id column. Migration required.`,
297
+ requiresMaintenance: true,
298
+ };
299
+ } else {
300
+ // Table is empty, safe to add NOT NULL column
301
+ await pool.query(`
290
302
  ALTER TABLE ${tableFullName}
291
303
  ADD COLUMN user_id UUID NOT NULL REFERENCES "public"."users"(id) ON DELETE CASCADE;
292
304
 
293
305
  CREATE INDEX IF NOT EXISTS idx_${tableName}_user_id ON ${tableFullName}(user_id);
294
- END IF;
295
- END IF;
296
- END $$;
297
- `);
298
-
299
- // Enable RLS if configured
300
- if (enableRLS) {
301
- await pool.query(`
302
- ALTER TABLE ${tableFullName} ENABLE ROW LEVEL SECURITY;
303
- ALTER TABLE ${tableFullName} FORCE ROW LEVEL SECURITY;
304
- `);
306
+ `);
307
+ logger.info(`Added user_id column to empty table ${tableFullName}`);
308
+ }
309
+ }
305
310
 
306
- // Create or replace the RLS policy
307
- await pool.query(`
308
- DROP POLICY IF EXISTS "${tableName}_owner" ON ${tableFullName};
309
- `);
311
+ // Enable RLS if configured
312
+ if (enableRLS) {
313
+ await pool.query(`
314
+ ALTER TABLE ${tableFullName} ENABLE ROW LEVEL SECURITY;
315
+ ALTER TABLE ${tableFullName} FORCE ROW LEVEL SECURITY;
316
+ `);
317
+
318
+ // Create or replace the RLS policy
319
+ await pool.query(`
320
+ DROP POLICY IF EXISTS "${tableName}_owner" ON ${tableFullName};
321
+ `);
322
+
323
+ // RLS policy: users can only access their own keys
324
+ await pool.query(`
325
+ CREATE POLICY "${tableName}_owner" ON ${tableFullName}
326
+ FOR ALL
327
+ USING (user_id::text = current_setting('app.current_user_id', true))
328
+ WITH CHECK (user_id::text = current_setting('app.current_user_id', true));
329
+ `);
330
+ }
310
331
 
311
- // RLS policy: users can only access their own keys
312
- await pool.query(`
313
- CREATE POLICY "${tableName}_owner" ON ${tableFullName}
314
- FOR ALL
315
- USING (user_id::text = current_setting('app.current_user_id', true))
316
- WITH CHECK (user_id::text = current_setting('app.current_user_id', true));
317
- `);
332
+ logger.info(`API keys store initialized successfully (table: ${tableFullName})`);
333
+ return { success: true };
334
+ } catch (error) {
335
+ const errorMessage = error instanceof Error ? error.message : String(error);
336
+ logger.error(`Failed to initialize API keys store: ${errorMessage}`, { error });
337
+ return {
338
+ success: false,
339
+ error: `Database initialization failed: ${errorMessage}`,
340
+ };
318
341
  }
319
342
  },
320
343
 
@@ -140,6 +140,18 @@ export interface ApiKeyWithPlaintext extends ApiKey {
140
140
  plaintext_key: string;
141
141
  }
142
142
 
143
+ /**
144
+ * Result of store initialization
145
+ */
146
+ export interface StoreInitializationResult {
147
+ /** Whether initialization succeeded */
148
+ success: boolean;
149
+ /** Error message if initialization failed */
150
+ error?: string;
151
+ /** Additional details about what needs to be done */
152
+ requiresMaintenance?: boolean;
153
+ }
154
+
143
155
  /**
144
156
  * API key store interface - all storage backends must implement this
145
157
  */
@@ -149,8 +161,9 @@ export interface ApiKeyStore {
149
161
 
150
162
  /**
151
163
  * Initialize the store (create tables, RLS policies, etc.)
164
+ * Returns initialization status instead of throwing
152
165
  */
153
- initialize(): Promise<void>;
166
+ initialize(): Promise<StoreInitializationResult>;
154
167
 
155
168
  /**
156
169
  * Create a new API key
@@ -148,6 +148,83 @@ export function createDiagnosticsPlugin(config: DiagnosticsPluginConfig = {}): P
148
148
  },
149
149
  });
150
150
 
151
+ // Register maintenance listing endpoint (GET /diagnostics/maintenance)
152
+ registry.addRoute({
153
+ method: 'get',
154
+ path: '/maintenance',
155
+ pluginId: 'diagnostics',
156
+ handler: (_req: Request, res: Response) => {
157
+ try {
158
+ const pluginsNeedingMaintenance = registry.getPluginsNeedingMaintenance();
159
+
160
+ res.json({
161
+ timestamp: new Date().toISOString(),
162
+ count: pluginsNeedingMaintenance.length,
163
+ plugins: pluginsNeedingMaintenance.map(info => ({
164
+ pluginId: info.pluginId,
165
+ error: info.error,
166
+ recommendedAction: info.recommendedAction,
167
+ actions: info.actions.map(action => ({
168
+ id: action.id,
169
+ name: action.name,
170
+ description: action.description,
171
+ destructive: action.destructive,
172
+ })),
173
+ })),
174
+ });
175
+ } catch (error) {
176
+ res.status(500).json({
177
+ error: 'Failed to retrieve maintenance info',
178
+ message: error instanceof Error ? error.message : String(error),
179
+ });
180
+ }
181
+ },
182
+ });
183
+
184
+ // Register maintenance action endpoint (POST /diagnostics/maintenance/:pluginId/:actionId)
185
+ registry.addRoute({
186
+ method: 'post',
187
+ path: '/maintenance/:pluginId/:actionId',
188
+ pluginId: 'diagnostics',
189
+ handler: async (req: Request, res: Response) => {
190
+ try {
191
+ const { pluginId, actionId } = req.params;
192
+
193
+ if (!pluginId || !actionId) {
194
+ return res.status(400).json({
195
+ error: 'Missing required parameters',
196
+ message: 'Both pluginId and actionId are required',
197
+ });
198
+ }
199
+
200
+ logger.info(`Executing maintenance action ${actionId} for plugin ${pluginId}`);
201
+
202
+ const result = await registry.runMaintenanceAction(pluginId, actionId);
203
+
204
+ if (result.success) {
205
+ res.json({
206
+ success: true,
207
+ message: result.message || 'Maintenance action completed successfully',
208
+ pluginId,
209
+ actionId,
210
+ });
211
+ } else {
212
+ res.status(500).json({
213
+ success: false,
214
+ error: result.error || 'Maintenance action failed',
215
+ pluginId,
216
+ actionId,
217
+ });
218
+ }
219
+ } catch (error) {
220
+ res.status(500).json({
221
+ error: 'Failed to execute maintenance action',
222
+ message: error instanceof Error ? error.message : String(error),
223
+ });
224
+ }
225
+ },
226
+ });
227
+
151
228
  logger.debug('Diagnostics plugin initialized');
152
229
  },
153
230
 
@@ -14,7 +14,7 @@ export { createLogsPlugin } from './logs-plugin.js';
14
14
  export type { LogsPluginConfig } from './logs-plugin.js';
15
15
 
16
16
  export { createMaintenancePlugin } from './maintenance-plugin.js';
17
- export type { MaintenancePluginConfig } from './maintenance-plugin.js';
17
+ export type { MaintenancePluginConfig, SeedTask, SeedTaskHandler } from './maintenance-plugin.js';
18
18
  export {
19
19
  MaintenanceManagementPage,
20
20
  MaintenanceStatusWidget,
@@ -11,6 +11,8 @@ import React, { useEffect, useRef, useState } from 'react';
11
11
  export interface SeedExecutorProps {
12
12
  apiPrefix: string;
13
13
  seedName: string;
14
+ seedType?: string;
15
+ seedOptions?: any;
14
16
  onComplete: () => void;
15
17
  onCancel: () => void;
16
18
  }
@@ -26,6 +28,8 @@ interface OutputLine {
26
28
  export const SeedExecutor: React.FC<SeedExecutorProps> = ({
27
29
  apiPrefix,
28
30
  seedName,
31
+ seedType = 'file',
32
+ seedOptions,
29
33
  onComplete,
30
34
  onCancel,
31
35
  }) => {
@@ -63,7 +67,11 @@ export const SeedExecutor: React.FC<SeedExecutorProps> = ({
63
67
  headers: {
64
68
  'Content-Type': 'application/json',
65
69
  },
66
- body: JSON.stringify({ name: seedName }),
70
+ body: JSON.stringify({
71
+ name: seedName,
72
+ type: seedType,
73
+ options: seedOptions,
74
+ }),
67
75
  });
68
76
 
69
77
  if (!response.ok) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Seed List Component
3
3
  *
4
- * Displays available seed scripts with metadata.
4
+ * Displays available seed scripts and custom tasks with metadata.
5
5
  *
6
6
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
7
  */
@@ -10,10 +10,11 @@ import React, { useEffect, useState } from 'react';
10
10
 
11
11
  export interface SeedListProps {
12
12
  apiPrefix: string;
13
- onExecute: (seedName: string) => void;
13
+ onExecute: (seedName: string, type?: string, options?: any) => void;
14
14
  }
15
15
 
16
16
  interface SeedFile {
17
+ type: 'file';
17
18
  name: string;
18
19
  path: string;
19
20
  size: number;
@@ -21,8 +22,18 @@ interface SeedFile {
21
22
  modifiedAt: string;
22
23
  }
23
24
 
25
+ interface CustomTask {
26
+ type: 'task';
27
+ id: string;
28
+ name: string;
29
+ description: string;
30
+ options?: Record<string, any>;
31
+ }
32
+
33
+ type SeedItem = SeedFile | CustomTask;
34
+
24
35
  export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
25
- const [seeds, setSeeds] = useState<SeedFile[]>([]);
36
+ const [seeds, setSeeds] = useState<SeedItem[]>([]);
26
37
  const [loading, setLoading] = useState(true);
27
38
  const [error, setError] = useState<string | null>(null);
28
39
 
@@ -57,6 +68,14 @@ export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
57
68
  return new Date(dateString).toLocaleString();
58
69
  };
59
70
 
71
+ const isFileType = (item: SeedItem): item is SeedFile => {
72
+ return item.type === 'file';
73
+ };
74
+
75
+ const isTaskType = (item: SeedItem): item is CustomTask => {
76
+ return item.type === 'task';
77
+ };
78
+
60
79
  if (loading) {
61
80
  return <div style={{ padding: '20px' }}>Loading seeds...</div>;
62
81
  }
@@ -72,55 +91,83 @@ export const SeedList: React.FC<SeedListProps> = ({ apiPrefix, onExecute }) => {
72
91
  if (seeds.length === 0) {
73
92
  return (
74
93
  <div style={{ padding: '20px', color: '#666' }}>
75
- No seed scripts found in scripts directory.
94
+ No seed scripts or tasks found.
76
95
  </div>
77
96
  );
78
97
  }
79
98
 
80
99
  return (
81
100
  <div style={{ padding: '20px' }}>
82
- <h3>Available Seed Scripts ({seeds.length})</h3>
101
+ <h3>Available Seeds & Tasks ({seeds.length})</h3>
83
102
  <table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '16px' }}>
84
103
  <thead>
85
104
  <tr style={{ borderBottom: '2px solid #ddd', textAlign: 'left' }}>
105
+ <th style={{ padding: '12px' }}>Type</th>
86
106
  <th style={{ padding: '12px' }}>Name</th>
87
- <th style={{ padding: '12px' }}>Size</th>
88
- <th style={{ padding: '12px' }}>Modified</th>
107
+ <th style={{ padding: '12px' }}>Description</th>
108
+ <th style={{ padding: '12px' }}>Details</th>
89
109
  <th style={{ padding: '12px' }}>Action</th>
90
110
  </tr>
91
111
  </thead>
92
112
  <tbody>
93
- {seeds.map((seed) => (
94
- <tr key={seed.name} style={{ borderBottom: '1px solid #eee' }}>
95
- <td style={{ padding: '12px', fontFamily: 'monospace' }}>
96
- {seed.name}
97
- </td>
98
- <td style={{ padding: '12px' }}>{formatFileSize(seed.size)}</td>
99
- <td style={{ padding: '12px', fontSize: '14px', color: '#666' }}>
100
- {formatDate(seed.modifiedAt)}
101
- </td>
102
- <td style={{ padding: '12px' }}>
103
- <button
104
- onClick={() => {
105
- if (confirm(`Execute ${seed.name}?`)) {
106
- onExecute(seed.name);
107
- }
108
- }}
109
- style={{
110
- padding: '6px 12px',
111
- backgroundColor: '#1976d2',
112
- color: 'white',
113
- border: 'none',
114
- borderRadius: '4px',
115
- cursor: 'pointer',
116
- }}
117
- data-testid={`execute-${seed.name}`}
118
- >
119
- Execute
120
- </button>
121
- </td>
122
- </tr>
123
- ))}
113
+ {seeds.map((seed) => {
114
+ const itemKey = isFileType(seed) ? seed.name : seed.id;
115
+ return (
116
+ <tr key={itemKey} style={{ borderBottom: '1px solid #eee' }}>
117
+ <td style={{ padding: '12px' }}>
118
+ <span
119
+ style={{
120
+ display: 'inline-block',
121
+ padding: '4px 8px',
122
+ borderRadius: '4px',
123
+ fontSize: '12px',
124
+ fontWeight: 'bold',
125
+ backgroundColor: isFileType(seed) ? '#e3f2fd' : '#f3e5f5',
126
+ color: isFileType(seed) ? '#1976d2' : '#7b1fa2',
127
+ }}
128
+ >
129
+ {isFileType(seed) ? 'FILE' : 'TASK'}
130
+ </span>
131
+ </td>
132
+ <td style={{ padding: '12px', fontFamily: 'monospace' }}>
133
+ {isFileType(seed) ? seed.name : seed.name}
134
+ </td>
135
+ <td style={{ padding: '12px', fontSize: '14px', color: '#666' }}>
136
+ {isTaskType(seed) ? seed.description : '-'}
137
+ </td>
138
+ <td style={{ padding: '12px', fontSize: '14px', color: '#666' }}>
139
+ {isFileType(seed)
140
+ ? `${formatFileSize(seed.size)} • ${formatDate(seed.modifiedAt)}`
141
+ : '-'}
142
+ </td>
143
+ <td style={{ padding: '12px' }}>
144
+ <button
145
+ onClick={() => {
146
+ const displayName = isFileType(seed) ? seed.name : seed.name;
147
+ if (confirm(`Execute ${displayName}?`)) {
148
+ if (isFileType(seed)) {
149
+ onExecute(seed.name, 'file');
150
+ } else {
151
+ onExecute(seed.id, 'task', seed.options);
152
+ }
153
+ }
154
+ }}
155
+ style={{
156
+ padding: '6px 12px',
157
+ backgroundColor: '#1976d2',
158
+ color: 'white',
159
+ border: 'none',
160
+ borderRadius: '4px',
161
+ cursor: 'pointer',
162
+ }}
163
+ data-testid={`execute-${itemKey}`}
164
+ >
165
+ Execute
166
+ </button>
167
+ </td>
168
+ </tr>
169
+ );
170
+ })}
124
171
  </tbody>
125
172
  </table>
126
173
  </div>
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Seed Management Page
3
3
  *
4
- * Main page for managing and executing seed scripts.
4
+ * Main page for managing and executing seed scripts and custom tasks.
5
5
  *
6
6
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
7
  */
@@ -23,9 +23,13 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
23
23
  }) => {
24
24
  const [activeTab, setActiveTab] = useState<Tab>('list');
25
25
  const [selectedSeed, setSelectedSeed] = useState<string | null>(null);
26
+ const [selectedType, setSelectedType] = useState<string>('file');
27
+ const [selectedOptions, setSelectedOptions] = useState<any>(undefined);
26
28
 
27
- const handleExecute = (seedName: string) => {
29
+ const handleExecute = (seedName: string, type: string = 'file', options?: any) => {
28
30
  setSelectedSeed(seedName);
31
+ setSelectedType(type);
32
+ setSelectedOptions(options);
29
33
  setActiveTab('execute');
30
34
  };
31
35
 
@@ -36,7 +40,7 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
36
40
  return (
37
41
  <PluginManagementPage
38
42
  title="Seed Management"
39
- description="Manage and execute database seed scripts"
43
+ description="Manage and execute database seed scripts and custom tasks"
40
44
  >
41
45
  <div style={{ marginBottom: '20px' }}>
42
46
  <button
@@ -51,7 +55,7 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
51
55
  cursor: 'pointer',
52
56
  }}
53
57
  >
54
- Available Seeds
58
+ Available Seeds & Tasks
55
59
  </button>
56
60
  <button
57
61
  onClick={() => setActiveTab('history')}
@@ -76,6 +80,8 @@ export const SeedManagementPage: React.FC<SeedManagementPageProps> = ({
76
80
  <SeedExecutor
77
81
  apiPrefix={apiPrefix}
78
82
  seedName={selectedSeed}
83
+ seedType={selectedType}
84
+ seedOptions={selectedOptions}
79
85
  onComplete={handleExecutionComplete}
80
86
  onCancel={() => setActiveTab('list')}
81
87
  />