@qwickapps/server 1.0.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.
- package/LICENSE +45 -0
- package/README.md +321 -0
- package/dist/core/control-panel.d.ts +21 -0
- package/dist/core/control-panel.d.ts.map +1 -0
- package/dist/core/control-panel.js +416 -0
- package/dist/core/control-panel.js.map +1 -0
- package/dist/core/gateway.d.ts +133 -0
- package/dist/core/gateway.d.ts.map +1 -0
- package/dist/core/gateway.js +270 -0
- package/dist/core/gateway.js.map +1 -0
- package/dist/core/health-manager.d.ts +52 -0
- package/dist/core/health-manager.d.ts.map +1 -0
- package/dist/core/health-manager.js +192 -0
- package/dist/core/health-manager.js.map +1 -0
- package/dist/core/index.d.ts +10 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +8 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logging.d.ts +83 -0
- package/dist/core/logging.d.ts.map +1 -0
- package/dist/core/logging.js +191 -0
- package/dist/core/logging.js.map +1 -0
- package/dist/core/types.d.ts +195 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +7 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/config-plugin.d.ts +15 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -0
- package/dist/plugins/config-plugin.js +96 -0
- package/dist/plugins/config-plugin.js.map +1 -0
- package/dist/plugins/diagnostics-plugin.d.ts +29 -0
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
- package/dist/plugins/diagnostics-plugin.js +142 -0
- package/dist/plugins/diagnostics-plugin.js.map +1 -0
- package/dist/plugins/health-plugin.d.ts +17 -0
- package/dist/plugins/health-plugin.d.ts.map +1 -0
- package/dist/plugins/health-plugin.js +25 -0
- package/dist/plugins/health-plugin.js.map +1 -0
- package/dist/plugins/index.d.ts +14 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +10 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/logs-plugin.d.ts +22 -0
- package/dist/plugins/logs-plugin.d.ts.map +1 -0
- package/dist/plugins/logs-plugin.js +242 -0
- package/dist/plugins/logs-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
- package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
- package/dist-ui/assets/index-CiizQQnb.css +1 -0
- package/dist-ui/index.html +13 -0
- package/package.json +98 -0
- package/src/core/control-panel.ts +493 -0
- package/src/core/gateway.ts +421 -0
- package/src/core/health-manager.ts +227 -0
- package/src/core/index.ts +25 -0
- package/src/core/logging.ts +234 -0
- package/src/core/types.ts +218 -0
- package/src/index.ts +55 -0
- package/src/plugins/config-plugin.ts +117 -0
- package/src/plugins/diagnostics-plugin.ts +178 -0
- package/src/plugins/health-plugin.ts +35 -0
- package/src/plugins/index.ts +17 -0
- package/src/plugins/logs-plugin.ts +314 -0
- package/ui/index.html +12 -0
- package/ui/src/App.tsx +65 -0
- package/ui/src/api/controlPanelApi.ts +148 -0
- package/ui/src/config/AppConfig.ts +18 -0
- package/ui/src/index.css +29 -0
- package/ui/src/index.tsx +11 -0
- package/ui/src/pages/ConfigPage.tsx +199 -0
- package/ui/src/pages/DashboardPage.tsx +264 -0
- package/ui/src/pages/DiagnosticsPage.tsx +315 -0
- package/ui/src/pages/HealthPage.tsx +204 -0
- package/ui/src/pages/LogsPage.tsx +267 -0
- package/ui/src/pages/NotFoundPage.tsx +41 -0
- package/ui/tsconfig.json +19 -0
- package/ui/vite.config.ts +21 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
Typography,
|
|
7
|
+
Grid,
|
|
8
|
+
CircularProgress,
|
|
9
|
+
Chip,
|
|
10
|
+
IconButton,
|
|
11
|
+
Tooltip,
|
|
12
|
+
Snackbar,
|
|
13
|
+
Alert,
|
|
14
|
+
LinearProgress,
|
|
15
|
+
} from '@mui/material';
|
|
16
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
17
|
+
import RefreshIcon from '@mui/icons-material/Refresh';
|
|
18
|
+
import MemoryIcon from '@mui/icons-material/Memory';
|
|
19
|
+
import ComputerIcon from '@mui/icons-material/Computer';
|
|
20
|
+
import StorageIcon from '@mui/icons-material/Storage';
|
|
21
|
+
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
|
22
|
+
import { api, DiagnosticsResponse } from '../api/controlPanelApi';
|
|
23
|
+
|
|
24
|
+
function formatBytes(bytes: number): string {
|
|
25
|
+
if (bytes === 0) return '0 B';
|
|
26
|
+
const k = 1024;
|
|
27
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
28
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
29
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatUptime(ms: number): string {
|
|
33
|
+
const seconds = Math.floor(ms / 1000);
|
|
34
|
+
const minutes = Math.floor(seconds / 60);
|
|
35
|
+
const hours = Math.floor(minutes / 60);
|
|
36
|
+
const days = Math.floor(hours / 24);
|
|
37
|
+
|
|
38
|
+
if (days > 0) {
|
|
39
|
+
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
|
40
|
+
}
|
|
41
|
+
if (hours > 0) {
|
|
42
|
+
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
|
43
|
+
}
|
|
44
|
+
if (minutes > 0) {
|
|
45
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
46
|
+
}
|
|
47
|
+
return `${seconds}s`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function DiagnosticsPage() {
|
|
51
|
+
const [diagnostics, setDiagnostics] = useState<DiagnosticsResponse | null>(null);
|
|
52
|
+
const [loading, setLoading] = useState(true);
|
|
53
|
+
const [error, setError] = useState<string | null>(null);
|
|
54
|
+
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string }>({
|
|
55
|
+
open: false,
|
|
56
|
+
message: '',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const fetchDiagnostics = async () => {
|
|
60
|
+
setLoading(true);
|
|
61
|
+
try {
|
|
62
|
+
const data = await api.getDiagnostics();
|
|
63
|
+
setDiagnostics(data);
|
|
64
|
+
setError(null);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch diagnostics');
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
fetchDiagnostics();
|
|
74
|
+
const interval = setInterval(fetchDiagnostics, 30000);
|
|
75
|
+
return () => clearInterval(interval);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const handleCopyAll = () => {
|
|
79
|
+
navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2));
|
|
80
|
+
setSnackbar({ open: true, message: 'Diagnostics copied to clipboard' });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (loading && !diagnostics) {
|
|
84
|
+
return (
|
|
85
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
|
86
|
+
<CircularProgress />
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (error) {
|
|
92
|
+
return (
|
|
93
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
|
|
94
|
+
<CardContent>
|
|
95
|
+
<Typography color="error">{error}</Typography>
|
|
96
|
+
</CardContent>
|
|
97
|
+
</Card>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const memoryUsedPercent = diagnostics
|
|
102
|
+
? (diagnostics.system.memory.used / diagnostics.system.memory.total) * 100
|
|
103
|
+
: 0;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Box>
|
|
107
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
|
|
108
|
+
<Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
109
|
+
Diagnostics
|
|
110
|
+
</Typography>
|
|
111
|
+
<Box sx={{ display: 'flex', gap: 1 }}>
|
|
112
|
+
<Tooltip title="Copy diagnostics JSON">
|
|
113
|
+
<IconButton onClick={handleCopyAll} sx={{ color: 'var(--theme-primary)' }}>
|
|
114
|
+
<ContentCopyIcon />
|
|
115
|
+
</IconButton>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
<Tooltip title="Refresh">
|
|
118
|
+
<IconButton onClick={fetchDiagnostics} sx={{ color: 'var(--theme-primary)' }}>
|
|
119
|
+
<RefreshIcon />
|
|
120
|
+
</IconButton>
|
|
121
|
+
</Tooltip>
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
<Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
|
|
125
|
+
System information and health diagnostics for AI agents and troubleshooting
|
|
126
|
+
</Typography>
|
|
127
|
+
|
|
128
|
+
<Grid container spacing={3}>
|
|
129
|
+
{/* System Info */}
|
|
130
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
131
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
|
|
132
|
+
<CardContent>
|
|
133
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
|
134
|
+
<ComputerIcon sx={{ color: 'var(--theme-primary)' }} />
|
|
135
|
+
<Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
136
|
+
System Information
|
|
137
|
+
</Typography>
|
|
138
|
+
</Box>
|
|
139
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
140
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
141
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Node.js</Typography>
|
|
142
|
+
<Chip
|
|
143
|
+
label={diagnostics?.system.nodeVersion}
|
|
144
|
+
size="small"
|
|
145
|
+
sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
|
|
146
|
+
/>
|
|
147
|
+
</Box>
|
|
148
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
149
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Platform</Typography>
|
|
150
|
+
<Chip
|
|
151
|
+
label={diagnostics?.system.platform}
|
|
152
|
+
size="small"
|
|
153
|
+
sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
|
|
154
|
+
/>
|
|
155
|
+
</Box>
|
|
156
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
157
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Architecture</Typography>
|
|
158
|
+
<Chip
|
|
159
|
+
label={diagnostics?.system.arch}
|
|
160
|
+
size="small"
|
|
161
|
+
sx={{ bgcolor: 'var(--theme-background)', color: 'var(--theme-text-primary)' }}
|
|
162
|
+
/>
|
|
163
|
+
</Box>
|
|
164
|
+
</Box>
|
|
165
|
+
</CardContent>
|
|
166
|
+
</Card>
|
|
167
|
+
</Grid>
|
|
168
|
+
|
|
169
|
+
{/* Memory Usage */}
|
|
170
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
171
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
|
|
172
|
+
<CardContent>
|
|
173
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
|
174
|
+
<MemoryIcon sx={{ color: 'var(--theme-warning)' }} />
|
|
175
|
+
<Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
176
|
+
Memory Usage
|
|
177
|
+
</Typography>
|
|
178
|
+
</Box>
|
|
179
|
+
<Box sx={{ mb: 2 }}>
|
|
180
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
|
|
181
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>
|
|
182
|
+
Heap Used
|
|
183
|
+
</Typography>
|
|
184
|
+
<Typography sx={{ color: 'var(--theme-text-primary)' }}>
|
|
185
|
+
{formatBytes(diagnostics?.system.memory.used || 0)}
|
|
186
|
+
</Typography>
|
|
187
|
+
</Box>
|
|
188
|
+
<LinearProgress
|
|
189
|
+
variant="determinate"
|
|
190
|
+
value={memoryUsedPercent}
|
|
191
|
+
sx={{
|
|
192
|
+
height: 8,
|
|
193
|
+
borderRadius: 4,
|
|
194
|
+
bgcolor: 'var(--theme-background)',
|
|
195
|
+
'& .MuiLinearProgress-bar': {
|
|
196
|
+
bgcolor: memoryUsedPercent > 80 ? 'var(--theme-error)' : 'var(--theme-warning)',
|
|
197
|
+
borderRadius: 4,
|
|
198
|
+
},
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
</Box>
|
|
202
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
203
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
204
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Heap Total</Typography>
|
|
205
|
+
<Typography sx={{ color: 'var(--theme-text-primary)' }}>
|
|
206
|
+
{formatBytes(diagnostics?.system.memory.total || 0)}
|
|
207
|
+
</Typography>
|
|
208
|
+
</Box>
|
|
209
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
210
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Heap Free</Typography>
|
|
211
|
+
<Typography sx={{ color: 'var(--theme-text-primary)' }}>
|
|
212
|
+
{formatBytes(diagnostics?.system.memory.free || 0)}
|
|
213
|
+
</Typography>
|
|
214
|
+
</Box>
|
|
215
|
+
</Box>
|
|
216
|
+
</CardContent>
|
|
217
|
+
</Card>
|
|
218
|
+
</Grid>
|
|
219
|
+
|
|
220
|
+
{/* Service Info */}
|
|
221
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
222
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
|
|
223
|
+
<CardContent>
|
|
224
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
|
225
|
+
<StorageIcon sx={{ color: 'var(--theme-info)' }} />
|
|
226
|
+
<Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
227
|
+
Service Info
|
|
228
|
+
</Typography>
|
|
229
|
+
</Box>
|
|
230
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
231
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
232
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Product</Typography>
|
|
233
|
+
<Typography sx={{ color: 'var(--theme-text-primary)' }}>
|
|
234
|
+
{diagnostics?.product}
|
|
235
|
+
</Typography>
|
|
236
|
+
</Box>
|
|
237
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
238
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Version</Typography>
|
|
239
|
+
<Chip
|
|
240
|
+
label={diagnostics?.version || 'N/A'}
|
|
241
|
+
size="small"
|
|
242
|
+
sx={{ bgcolor: 'var(--theme-primary)20', color: 'var(--theme-primary)' }}
|
|
243
|
+
/>
|
|
244
|
+
</Box>
|
|
245
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
|
246
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>Timestamp</Typography>
|
|
247
|
+
<Typography sx={{ color: 'var(--theme-text-primary)', fontSize: '0.875rem' }}>
|
|
248
|
+
{diagnostics?.timestamp ? new Date(diagnostics.timestamp).toLocaleString() : 'N/A'}
|
|
249
|
+
</Typography>
|
|
250
|
+
</Box>
|
|
251
|
+
</Box>
|
|
252
|
+
</CardContent>
|
|
253
|
+
</Card>
|
|
254
|
+
</Grid>
|
|
255
|
+
|
|
256
|
+
{/* Uptime */}
|
|
257
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
258
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
|
|
259
|
+
<CardContent>
|
|
260
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
|
261
|
+
<AccessTimeIcon sx={{ color: 'var(--theme-success)' }} />
|
|
262
|
+
<Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
263
|
+
Uptime
|
|
264
|
+
</Typography>
|
|
265
|
+
</Box>
|
|
266
|
+
<Typography variant="h3" sx={{ color: 'var(--theme-success)', mb: 1 }}>
|
|
267
|
+
{formatUptime(diagnostics?.uptime || 0)}
|
|
268
|
+
</Typography>
|
|
269
|
+
<Typography sx={{ color: 'var(--theme-text-secondary)' }}>
|
|
270
|
+
Service has been running without interruption
|
|
271
|
+
</Typography>
|
|
272
|
+
</CardContent>
|
|
273
|
+
</Card>
|
|
274
|
+
</Grid>
|
|
275
|
+
|
|
276
|
+
{/* Raw JSON */}
|
|
277
|
+
<Grid size={{ xs: 12 }}>
|
|
278
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
279
|
+
<CardContent>
|
|
280
|
+
<Typography variant="h6" sx={{ color: 'var(--theme-text-primary)', mb: 2 }}>
|
|
281
|
+
Raw Diagnostics JSON (for AI agents)
|
|
282
|
+
</Typography>
|
|
283
|
+
<Box
|
|
284
|
+
component="pre"
|
|
285
|
+
sx={{
|
|
286
|
+
bgcolor: 'var(--theme-background)',
|
|
287
|
+
p: 2,
|
|
288
|
+
borderRadius: 1,
|
|
289
|
+
overflow: 'auto',
|
|
290
|
+
maxHeight: 300,
|
|
291
|
+
color: 'var(--theme-text-primary)',
|
|
292
|
+
fontFamily: 'monospace',
|
|
293
|
+
fontSize: '0.75rem',
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{JSON.stringify(diagnostics, null, 2)}
|
|
297
|
+
</Box>
|
|
298
|
+
</CardContent>
|
|
299
|
+
</Card>
|
|
300
|
+
</Grid>
|
|
301
|
+
</Grid>
|
|
302
|
+
|
|
303
|
+
<Snackbar
|
|
304
|
+
open={snackbar.open}
|
|
305
|
+
autoHideDuration={2000}
|
|
306
|
+
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
|
307
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
308
|
+
>
|
|
309
|
+
<Alert severity="success" variant="filled">
|
|
310
|
+
{snackbar.message}
|
|
311
|
+
</Alert>
|
|
312
|
+
</Snackbar>
|
|
313
|
+
</Box>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
Typography,
|
|
7
|
+
Table,
|
|
8
|
+
TableBody,
|
|
9
|
+
TableCell,
|
|
10
|
+
TableContainer,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableRow,
|
|
13
|
+
Chip,
|
|
14
|
+
CircularProgress,
|
|
15
|
+
LinearProgress,
|
|
16
|
+
} from '@mui/material';
|
|
17
|
+
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
|
18
|
+
import ErrorIcon from '@mui/icons-material/Error';
|
|
19
|
+
import WarningIcon from '@mui/icons-material/Warning';
|
|
20
|
+
import { api, HealthResponse } from '../api/controlPanelApi';
|
|
21
|
+
|
|
22
|
+
function getStatusIcon(status: string, size = 20) {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'healthy':
|
|
25
|
+
return <CheckCircleIcon sx={{ color: 'var(--theme-success)', fontSize: size }} />;
|
|
26
|
+
case 'degraded':
|
|
27
|
+
return <WarningIcon sx={{ color: 'var(--theme-warning)', fontSize: size }} />;
|
|
28
|
+
case 'unhealthy':
|
|
29
|
+
return <ErrorIcon sx={{ color: 'var(--theme-error)', fontSize: size }} />;
|
|
30
|
+
default:
|
|
31
|
+
return <CircularProgress size={size} />;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getStatusColor(status: string): string {
|
|
36
|
+
switch (status) {
|
|
37
|
+
case 'healthy':
|
|
38
|
+
return 'var(--theme-success)';
|
|
39
|
+
case 'degraded':
|
|
40
|
+
return 'var(--theme-warning)';
|
|
41
|
+
case 'unhealthy':
|
|
42
|
+
return 'var(--theme-error)';
|
|
43
|
+
default:
|
|
44
|
+
return 'var(--theme-text-secondary)';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatLatency(latency?: number): string {
|
|
49
|
+
if (latency === undefined) return '-';
|
|
50
|
+
if (latency < 1000) return `${latency}ms`;
|
|
51
|
+
return `${(latency / 1000).toFixed(2)}s`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function HealthPage() {
|
|
55
|
+
const [health, setHealth] = useState<HealthResponse | null>(null);
|
|
56
|
+
const [loading, setLoading] = useState(true);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const fetchHealth = async () => {
|
|
61
|
+
try {
|
|
62
|
+
const data = await api.getHealth();
|
|
63
|
+
setHealth(data);
|
|
64
|
+
setError(null);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch health');
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
fetchHealth();
|
|
73
|
+
const interval = setInterval(fetchHealth, 5000);
|
|
74
|
+
return () => clearInterval(interval);
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
if (loading) {
|
|
78
|
+
return (
|
|
79
|
+
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
|
80
|
+
<CircularProgress />
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (error) {
|
|
86
|
+
return (
|
|
87
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
|
|
88
|
+
<CardContent>
|
|
89
|
+
<Typography color="error">{error}</Typography>
|
|
90
|
+
</CardContent>
|
|
91
|
+
</Card>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const healthChecks = health ? Object.entries(health.checks) : [];
|
|
96
|
+
const healthyCount = healthChecks.filter(([, c]) => c.status === 'healthy').length;
|
|
97
|
+
const totalCount = healthChecks.length;
|
|
98
|
+
const healthPercentage = totalCount > 0 ? (healthyCount / totalCount) * 100 : 0;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Box>
|
|
102
|
+
<Typography variant="h4" sx={{ mb: 1, color: 'var(--theme-text-primary)' }}>
|
|
103
|
+
Health Checks
|
|
104
|
+
</Typography>
|
|
105
|
+
<Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
|
|
106
|
+
Detailed view of all service health checks
|
|
107
|
+
</Typography>
|
|
108
|
+
|
|
109
|
+
{/* Summary Card */}
|
|
110
|
+
<Card sx={{ mb: 4, bgcolor: 'var(--theme-surface)' }}>
|
|
111
|
+
<CardContent>
|
|
112
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
|
113
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
114
|
+
{getStatusIcon(health?.status || 'unknown', 32)}
|
|
115
|
+
<Box>
|
|
116
|
+
<Typography variant="h5" sx={{ color: 'var(--theme-text-primary)' }}>
|
|
117
|
+
Overall Status: {health?.status?.charAt(0).toUpperCase()}{health?.status?.slice(1)}
|
|
118
|
+
</Typography>
|
|
119
|
+
<Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
|
|
120
|
+
{healthyCount} of {totalCount} checks passing
|
|
121
|
+
</Typography>
|
|
122
|
+
</Box>
|
|
123
|
+
</Box>
|
|
124
|
+
<Typography variant="h3" sx={{ color: getStatusColor(health?.status || 'unknown') }}>
|
|
125
|
+
{healthPercentage.toFixed(0)}%
|
|
126
|
+
</Typography>
|
|
127
|
+
</Box>
|
|
128
|
+
<LinearProgress
|
|
129
|
+
variant="determinate"
|
|
130
|
+
value={healthPercentage}
|
|
131
|
+
sx={{
|
|
132
|
+
height: 8,
|
|
133
|
+
borderRadius: 4,
|
|
134
|
+
bgcolor: 'var(--theme-background)',
|
|
135
|
+
'& .MuiLinearProgress-bar': {
|
|
136
|
+
bgcolor: getStatusColor(health?.status || 'unknown'),
|
|
137
|
+
borderRadius: 4,
|
|
138
|
+
},
|
|
139
|
+
}}
|
|
140
|
+
/>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
|
|
144
|
+
{/* Health Checks Table */}
|
|
145
|
+
<Card sx={{ bgcolor: 'var(--theme-surface)' }}>
|
|
146
|
+
<TableContainer>
|
|
147
|
+
<Table>
|
|
148
|
+
<TableHead>
|
|
149
|
+
<TableRow>
|
|
150
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
151
|
+
Check
|
|
152
|
+
</TableCell>
|
|
153
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
154
|
+
Status
|
|
155
|
+
</TableCell>
|
|
156
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
157
|
+
Latency
|
|
158
|
+
</TableCell>
|
|
159
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
160
|
+
Last Checked
|
|
161
|
+
</TableCell>
|
|
162
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
163
|
+
Error
|
|
164
|
+
</TableCell>
|
|
165
|
+
</TableRow>
|
|
166
|
+
</TableHead>
|
|
167
|
+
<TableBody>
|
|
168
|
+
{healthChecks.map(([name, check]) => (
|
|
169
|
+
<TableRow key={name}>
|
|
170
|
+
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
|
|
171
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
172
|
+
{getStatusIcon(check.status)}
|
|
173
|
+
<Typography fontWeight={500}>{name}</Typography>
|
|
174
|
+
</Box>
|
|
175
|
+
</TableCell>
|
|
176
|
+
<TableCell sx={{ borderColor: 'var(--theme-border)' }}>
|
|
177
|
+
<Chip
|
|
178
|
+
label={check.status}
|
|
179
|
+
size="small"
|
|
180
|
+
sx={{
|
|
181
|
+
bgcolor: getStatusColor(check.status) + '20',
|
|
182
|
+
color: getStatusColor(check.status),
|
|
183
|
+
textTransform: 'capitalize',
|
|
184
|
+
}}
|
|
185
|
+
/>
|
|
186
|
+
</TableCell>
|
|
187
|
+
<TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)' }}>
|
|
188
|
+
{formatLatency(check.latency)}
|
|
189
|
+
</TableCell>
|
|
190
|
+
<TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
|
|
191
|
+
{new Date(check.lastChecked).toLocaleTimeString()}
|
|
192
|
+
</TableCell>
|
|
193
|
+
<TableCell sx={{ color: 'var(--theme-error)', borderColor: 'var(--theme-border)' }}>
|
|
194
|
+
{check.error || '-'}
|
|
195
|
+
</TableCell>
|
|
196
|
+
</TableRow>
|
|
197
|
+
))}
|
|
198
|
+
</TableBody>
|
|
199
|
+
</Table>
|
|
200
|
+
</TableContainer>
|
|
201
|
+
</Card>
|
|
202
|
+
</Box>
|
|
203
|
+
);
|
|
204
|
+
}
|