@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
@@ -0,0 +1,410 @@
1
+ /**
2
+ * Database Operations Widget Component
3
+ * Displays database status and provides manual initialization/recreation controls
4
+ *
5
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
6
+ */
7
+
8
+ import React, { useEffect, useState } from 'react';
9
+ import {
10
+ Card,
11
+ CardContent,
12
+ Typography,
13
+ Button,
14
+ Alert,
15
+ Box,
16
+ Chip,
17
+ Dialog,
18
+ DialogTitle,
19
+ DialogContent,
20
+ DialogActions,
21
+ TextField,
22
+ CircularProgress,
23
+ } from '@mui/material';
24
+
25
+ export interface DatabaseOperationsWidgetProps {
26
+ // No props needed - will use default values
27
+ }
28
+
29
+ interface DatabaseStatus {
30
+ status: 'healthy' | 'error' | 'unknown';
31
+ connected: boolean;
32
+ database?: string;
33
+ user?: string;
34
+ host?: string;
35
+ port?: number;
36
+ errorMessage?: string;
37
+ autoInitializeEnabled: boolean;
38
+ adminCredentialsProvided: boolean;
39
+ }
40
+
41
+ interface ConfirmationDialogProps {
42
+ open: boolean;
43
+ title: string;
44
+ message: string;
45
+ confirmText: string;
46
+ requiredInput: string;
47
+ onConfirm: () => void;
48
+ onCancel: () => void;
49
+ }
50
+
51
+ const ConfirmationDialog: React.FC<ConfirmationDialogProps> = ({
52
+ open,
53
+ title,
54
+ message,
55
+ confirmText,
56
+ requiredInput,
57
+ onConfirm,
58
+ onCancel,
59
+ }) => {
60
+ const [inputValue, setInputValue] = useState('');
61
+ const isValid = inputValue === requiredInput;
62
+
63
+ return (
64
+ <Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth>
65
+ <DialogTitle>{title}</DialogTitle>
66
+ <DialogContent>
67
+ <Typography variant="body2" sx={{ mb: 2 }}>
68
+ {message}
69
+ </Typography>
70
+ <Typography variant="body2" sx={{ mb: 1, fontWeight: 'bold' }}>
71
+ Type "{requiredInput}" to confirm:
72
+ </Typography>
73
+ <TextField
74
+ autoFocus
75
+ fullWidth
76
+ value={inputValue}
77
+ onChange={(e) => setInputValue(e.target.value)}
78
+ placeholder={requiredInput}
79
+ sx={{ fontFamily: 'monospace' }}
80
+ />
81
+ </DialogContent>
82
+ <DialogActions>
83
+ <Button onClick={onCancel}>Cancel</Button>
84
+ <Button
85
+ onClick={() => {
86
+ if (isValid) {
87
+ onConfirm();
88
+ setInputValue('');
89
+ }
90
+ }}
91
+ disabled={!isValid}
92
+ variant="contained"
93
+ color="error"
94
+ >
95
+ {confirmText}
96
+ </Button>
97
+ </DialogActions>
98
+ </Dialog>
99
+ );
100
+ };
101
+
102
+ interface AdminCredentialsDialogProps {
103
+ open: boolean;
104
+ onSubmit: (credentials: { adminUser: string; adminPassword: string }) => void;
105
+ onCancel: () => void;
106
+ }
107
+
108
+ const AdminCredentialsDialog: React.FC<AdminCredentialsDialogProps> = ({
109
+ open,
110
+ onSubmit,
111
+ onCancel,
112
+ }) => {
113
+ const [adminUser, setAdminUser] = useState('postgres');
114
+ const [adminPassword, setAdminPassword] = useState('');
115
+
116
+ return (
117
+ <Dialog open={open} onClose={onCancel} maxWidth="sm" fullWidth>
118
+ <DialogTitle>Admin Credentials Required</DialogTitle>
119
+ <DialogContent>
120
+ <Typography variant="body2" sx={{ mb: 2 }}>
121
+ Provide PostgreSQL admin credentials to perform this operation:
122
+ </Typography>
123
+ <TextField
124
+ fullWidth
125
+ label="Admin User"
126
+ value={adminUser}
127
+ onChange={(e) => setAdminUser(e.target.value)}
128
+ sx={{ mb: 2 }}
129
+ placeholder="postgres"
130
+ />
131
+ <TextField
132
+ fullWidth
133
+ type="password"
134
+ label="Admin Password"
135
+ value={adminPassword}
136
+ onChange={(e) => setAdminPassword(e.target.value)}
137
+ placeholder="Enter admin password"
138
+ />
139
+ </DialogContent>
140
+ <DialogActions>
141
+ <Button onClick={onCancel}>Cancel</Button>
142
+ <Button
143
+ onClick={() => {
144
+ if (adminUser && adminPassword) {
145
+ onSubmit({ adminUser, adminPassword });
146
+ setAdminUser('postgres');
147
+ setAdminPassword('');
148
+ }
149
+ }}
150
+ disabled={!adminUser || !adminPassword}
151
+ variant="contained"
152
+ >
153
+ Continue
154
+ </Button>
155
+ </DialogActions>
156
+ </Dialog>
157
+ );
158
+ };
159
+
160
+ export const DatabaseOperationsWidget: React.FC<DatabaseOperationsWidgetProps> = () => {
161
+ // Use default values since props cannot be passed through WidgetContribution
162
+ const apiPrefix = '/api/postgres:default';
163
+ const instanceName = 'default';
164
+ const [status, setStatus] = useState<DatabaseStatus | null>(null);
165
+ const [loading, setLoading] = useState(true);
166
+ const [error, setError] = useState<string | null>(null);
167
+ const [operating, setOperating] = useState(false);
168
+ const [showConfirmDialog, setShowConfirmDialog] = useState(false);
169
+ const [confirmOperation, setConfirmOperation] = useState<'initialize' | 'recreate'>('initialize');
170
+ const [showAdminDialog, setShowAdminDialog] = useState(false);
171
+ const [adminCredentials, setAdminCredentials] = useState<{
172
+ adminUser: string;
173
+ adminPassword: string;
174
+ } | null>(null);
175
+
176
+ const fetchStatus = async () => {
177
+ try {
178
+ const response = await fetch(`${apiPrefix}/status?instance=${instanceName}`);
179
+ if (!response.ok) throw new Error('Failed to fetch database status');
180
+ const data = await response.json();
181
+ setStatus(data);
182
+ setError(null);
183
+ } catch (err) {
184
+ setError(err instanceof Error ? err.message : 'Unknown error');
185
+ } finally {
186
+ setLoading(false);
187
+ }
188
+ };
189
+
190
+ useEffect(() => {
191
+ fetchStatus();
192
+ const interval = setInterval(fetchStatus, 30000);
193
+ return () => clearInterval(interval);
194
+ }, [apiPrefix, instanceName]);
195
+
196
+ const handleInitialize = async (creds?: { adminUser: string; adminPassword: string }) => {
197
+ setOperating(true);
198
+ try {
199
+ const response = await fetch(`${apiPrefix}/initialize`, {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({
203
+ instance: instanceName,
204
+ ...creds,
205
+ }),
206
+ });
207
+
208
+ if (!response.ok) {
209
+ const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
210
+ throw new Error(errorData.message || 'Failed to initialize database');
211
+ }
212
+
213
+ await fetchStatus();
214
+ alert('Database initialized successfully');
215
+ } catch (err) {
216
+ alert(`Initialization failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
217
+ } finally {
218
+ setOperating(false);
219
+ setShowConfirmDialog(false);
220
+ setShowAdminDialog(false);
221
+ setAdminCredentials(null);
222
+ }
223
+ };
224
+
225
+ const handleRecreate = async (creds?: { adminUser: string; adminPassword: string }) => {
226
+ setOperating(true);
227
+ try {
228
+ const response = await fetch(`${apiPrefix}/recreate`, {
229
+ method: 'POST',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({
232
+ instance: instanceName,
233
+ ...creds,
234
+ }),
235
+ });
236
+
237
+ if (!response.ok) {
238
+ const errorData = await response.json().catch(() => ({ message: 'Unknown error' }));
239
+ throw new Error(errorData.message || 'Failed to recreate database');
240
+ }
241
+
242
+ await fetchStatus();
243
+ alert('Database recreated successfully');
244
+ } catch (err) {
245
+ alert(`Recreation failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
246
+ } finally {
247
+ setOperating(false);
248
+ setShowConfirmDialog(false);
249
+ setShowAdminDialog(false);
250
+ setAdminCredentials(null);
251
+ }
252
+ };
253
+
254
+ const startOperation = (operation: 'initialize' | 'recreate') => {
255
+ setConfirmOperation(operation);
256
+
257
+ if (!status?.adminCredentialsProvided) {
258
+ setShowAdminDialog(true);
259
+ } else if (operation === 'recreate') {
260
+ setShowConfirmDialog(true);
261
+ } else {
262
+ handleInitialize();
263
+ }
264
+ };
265
+
266
+ const handleAdminCredentialsSubmit = (creds: { adminUser: string; adminPassword: string }) => {
267
+ setAdminCredentials(creds);
268
+ setShowAdminDialog(false);
269
+
270
+ if (confirmOperation === 'recreate') {
271
+ setShowConfirmDialog(true);
272
+ } else {
273
+ handleInitialize(creds);
274
+ }
275
+ };
276
+
277
+ const handleConfirmDialogConfirm = () => {
278
+ if (confirmOperation === 'recreate') {
279
+ handleRecreate(adminCredentials || undefined);
280
+ } else {
281
+ handleInitialize(adminCredentials || undefined);
282
+ }
283
+ };
284
+
285
+ if (loading) {
286
+ return (
287
+ <Card>
288
+ <CardContent>
289
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
290
+ <CircularProgress size={20} />
291
+ <Typography variant="body2">Loading database status...</Typography>
292
+ </Box>
293
+ </CardContent>
294
+ </Card>
295
+ );
296
+ }
297
+
298
+ if (error || !status) {
299
+ return (
300
+ <Card>
301
+ <CardContent>
302
+ <Typography variant="h6" gutterBottom>
303
+ Database ({instanceName})
304
+ </Typography>
305
+ <Alert severity="error">{error || 'Failed to load database status'}</Alert>
306
+ </CardContent>
307
+ </Card>
308
+ );
309
+ }
310
+
311
+ const requiredInput = status.database
312
+ ? `RECREATE ${status.database.toUpperCase()} DATABASE`
313
+ : 'RECREATE DATABASE';
314
+
315
+ const statusColor = status.connected ? 'success' : 'error';
316
+ const statusLabel = status.connected ? 'CONNECTED' : 'ERROR';
317
+
318
+ return (
319
+ <>
320
+ <Card>
321
+ <CardContent>
322
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
323
+ <Typography variant="h6">Database ({instanceName})</Typography>
324
+ <Chip label={statusLabel} color={statusColor} size="small" />
325
+ </Box>
326
+
327
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
328
+ {operating
329
+ ? 'Processing database operation...'
330
+ : status.connected
331
+ ? `Connected to ${status.database}`
332
+ : status.errorMessage || 'Database connection error'}
333
+ </Typography>
334
+
335
+ <Box sx={{ mb: 2 }}>
336
+ <Typography variant="caption" color="text.secondary">
337
+ Connection
338
+ </Typography>
339
+ <Typography variant="body2" fontWeight="bold">
340
+ {statusLabel}
341
+ </Typography>
342
+ </Box>
343
+
344
+ <Box sx={{ mb: 2 }}>
345
+ <Typography variant="caption" color="text.secondary">
346
+ Database
347
+ </Typography>
348
+ <Typography variant="body2" fontWeight="bold">
349
+ {status.database || 'N/A'}
350
+ </Typography>
351
+ </Box>
352
+
353
+ <Box sx={{ mb: 2 }}>
354
+ <Typography variant="caption" color="text.secondary">
355
+ Host
356
+ </Typography>
357
+ <Typography variant="body2" fontWeight="bold">
358
+ {status.host || 'N/A'}:{status.port || 'N/A'}
359
+ </Typography>
360
+ </Box>
361
+
362
+ {!status.connected && !operating && (
363
+ <Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
364
+ <Button
365
+ variant="contained"
366
+ color="primary"
367
+ onClick={() => startOperation('initialize')}
368
+ size="small"
369
+ >
370
+ Initialize Database
371
+ </Button>
372
+ <Button
373
+ variant="contained"
374
+ color="error"
375
+ onClick={() => startOperation('recreate')}
376
+ size="small"
377
+ >
378
+ Recreate Database
379
+ </Button>
380
+ </Box>
381
+ )}
382
+ </CardContent>
383
+ </Card>
384
+
385
+ <AdminCredentialsDialog
386
+ open={showAdminDialog}
387
+ onSubmit={handleAdminCredentialsSubmit}
388
+ onCancel={() => {
389
+ setShowAdminDialog(false);
390
+ setAdminCredentials(null);
391
+ }}
392
+ />
393
+
394
+ <ConfirmationDialog
395
+ open={showConfirmDialog}
396
+ title="Confirm Database Recreation"
397
+ message={`This will drop and recreate the database "${status.database}". All data will be lost. This action cannot be undone.`}
398
+ confirmText="Recreate"
399
+ requiredInput={requiredInput}
400
+ onConfirm={handleConfirmDialogConfirm}
401
+ onCancel={() => {
402
+ setShowConfirmDialog(false);
403
+ setAdminCredentials(null);
404
+ }}
405
+ />
406
+ </>
407
+ );
408
+ };
409
+
410
+ export default DatabaseOperationsWidget;
@@ -61,7 +61,8 @@ export function LogsMaintenanceWidget() {
61
61
 
62
62
  const fetchSources = async () => {
63
63
  try {
64
- const response = await fetch('/api/logs/sources');
64
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
65
+ const response = await fetch(`${basePath}/api/logs/sources`);
65
66
  if (!response.ok) throw new Error('Failed to fetch log sources');
66
67
  const data = await response.json();
67
68
  setSources(data.sources || []);
@@ -79,7 +80,8 @@ export function LogsMaintenanceWidget() {
79
80
  setLoading(true);
80
81
  setError(null);
81
82
  try {
82
- const response = await fetch(`/api/logs/stats?source=${selectedSource}`);
83
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
84
+ const response = await fetch(`${basePath}/api/logs/stats?source=${selectedSource}`);
83
85
  if (!response.ok) throw new Error('Failed to fetch log stats');
84
86
  const data = await response.json();
85
87
  setStats(data);
@@ -108,7 +110,8 @@ export function LogsMaintenanceWidget() {
108
110
  setSuccess(null);
109
111
 
110
112
  try {
111
- const response = await fetch('/api/logs/clear', {
113
+ const basePath = (window as any).__APP_BASE_PATH__ || '';
114
+ const response = await fetch(`${basePath}/api/logs/clear`, {
112
115
  method: 'POST',
113
116
  headers: { 'Content-Type': 'application/json' },
114
117
  body: JSON.stringify({ source: selectedSource }),