@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.
Files changed (81) hide show
  1. package/LICENSE +45 -0
  2. package/README.md +321 -0
  3. package/dist/core/control-panel.d.ts +21 -0
  4. package/dist/core/control-panel.d.ts.map +1 -0
  5. package/dist/core/control-panel.js +416 -0
  6. package/dist/core/control-panel.js.map +1 -0
  7. package/dist/core/gateway.d.ts +133 -0
  8. package/dist/core/gateway.d.ts.map +1 -0
  9. package/dist/core/gateway.js +270 -0
  10. package/dist/core/gateway.js.map +1 -0
  11. package/dist/core/health-manager.d.ts +52 -0
  12. package/dist/core/health-manager.d.ts.map +1 -0
  13. package/dist/core/health-manager.js +192 -0
  14. package/dist/core/health-manager.js.map +1 -0
  15. package/dist/core/index.d.ts +10 -0
  16. package/dist/core/index.d.ts.map +1 -0
  17. package/dist/core/index.js +8 -0
  18. package/dist/core/index.js.map +1 -0
  19. package/dist/core/logging.d.ts +83 -0
  20. package/dist/core/logging.d.ts.map +1 -0
  21. package/dist/core/logging.js +191 -0
  22. package/dist/core/logging.js.map +1 -0
  23. package/dist/core/types.d.ts +195 -0
  24. package/dist/core/types.d.ts.map +1 -0
  25. package/dist/core/types.js +7 -0
  26. package/dist/core/types.js.map +1 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +17 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/plugins/config-plugin.d.ts +15 -0
  32. package/dist/plugins/config-plugin.d.ts.map +1 -0
  33. package/dist/plugins/config-plugin.js +96 -0
  34. package/dist/plugins/config-plugin.js.map +1 -0
  35. package/dist/plugins/diagnostics-plugin.d.ts +29 -0
  36. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -0
  37. package/dist/plugins/diagnostics-plugin.js +142 -0
  38. package/dist/plugins/diagnostics-plugin.js.map +1 -0
  39. package/dist/plugins/health-plugin.d.ts +17 -0
  40. package/dist/plugins/health-plugin.d.ts.map +1 -0
  41. package/dist/plugins/health-plugin.js +25 -0
  42. package/dist/plugins/health-plugin.js.map +1 -0
  43. package/dist/plugins/index.d.ts +14 -0
  44. package/dist/plugins/index.d.ts.map +1 -0
  45. package/dist/plugins/index.js +10 -0
  46. package/dist/plugins/index.js.map +1 -0
  47. package/dist/plugins/logs-plugin.d.ts +22 -0
  48. package/dist/plugins/logs-plugin.d.ts.map +1 -0
  49. package/dist/plugins/logs-plugin.js +242 -0
  50. package/dist/plugins/logs-plugin.js.map +1 -0
  51. package/dist-ui/assets/index-Bk7ypbI4.js +465 -0
  52. package/dist-ui/assets/index-Bk7ypbI4.js.map +1 -0
  53. package/dist-ui/assets/index-CiizQQnb.css +1 -0
  54. package/dist-ui/index.html +13 -0
  55. package/package.json +98 -0
  56. package/src/core/control-panel.ts +493 -0
  57. package/src/core/gateway.ts +421 -0
  58. package/src/core/health-manager.ts +227 -0
  59. package/src/core/index.ts +25 -0
  60. package/src/core/logging.ts +234 -0
  61. package/src/core/types.ts +218 -0
  62. package/src/index.ts +55 -0
  63. package/src/plugins/config-plugin.ts +117 -0
  64. package/src/plugins/diagnostics-plugin.ts +178 -0
  65. package/src/plugins/health-plugin.ts +35 -0
  66. package/src/plugins/index.ts +17 -0
  67. package/src/plugins/logs-plugin.ts +314 -0
  68. package/ui/index.html +12 -0
  69. package/ui/src/App.tsx +65 -0
  70. package/ui/src/api/controlPanelApi.ts +148 -0
  71. package/ui/src/config/AppConfig.ts +18 -0
  72. package/ui/src/index.css +29 -0
  73. package/ui/src/index.tsx +11 -0
  74. package/ui/src/pages/ConfigPage.tsx +199 -0
  75. package/ui/src/pages/DashboardPage.tsx +264 -0
  76. package/ui/src/pages/DiagnosticsPage.tsx +315 -0
  77. package/ui/src/pages/HealthPage.tsx +204 -0
  78. package/ui/src/pages/LogsPage.tsx +267 -0
  79. package/ui/src/pages/NotFoundPage.tsx +41 -0
  80. package/ui/tsconfig.json +19 -0
  81. package/ui/vite.config.ts +21 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Control Panel API Client
3
+ *
4
+ * Communicates with the backend Express API
5
+ */
6
+
7
+ export interface HealthCheck {
8
+ status: 'healthy' | 'degraded' | 'unhealthy';
9
+ latency?: number;
10
+ lastChecked: string;
11
+ error?: string;
12
+ }
13
+
14
+ export interface HealthResponse {
15
+ status: 'healthy' | 'degraded' | 'unhealthy';
16
+ timestamp: string;
17
+ uptime: number;
18
+ checks: Record<string, HealthCheck>;
19
+ }
20
+
21
+ export interface InfoResponse {
22
+ product: string;
23
+ version: string;
24
+ uptime: number;
25
+ links: Array<{ label: string; url: string; external?: boolean }>;
26
+ branding?: {
27
+ primaryColor?: string;
28
+ logo?: string;
29
+ };
30
+ }
31
+
32
+ export interface DiagnosticsResponse {
33
+ timestamp: string;
34
+ product: string;
35
+ version?: string;
36
+ uptime: number;
37
+ health: Record<string, HealthCheck>;
38
+ system: {
39
+ nodeVersion: string;
40
+ platform: string;
41
+ arch: string;
42
+ memory: {
43
+ total: number;
44
+ used: number;
45
+ free: number;
46
+ };
47
+ cpu: {
48
+ usage: number;
49
+ };
50
+ };
51
+ }
52
+
53
+ export interface ConfigResponse {
54
+ config: Record<string, string | number | boolean>;
55
+ masked: string[];
56
+ }
57
+
58
+ export interface LogEntry {
59
+ timestamp: string;
60
+ level: string;
61
+ message: string;
62
+ source?: string;
63
+ }
64
+
65
+ export interface LogsResponse {
66
+ logs: LogEntry[];
67
+ total: number;
68
+ page: number;
69
+ limit: number;
70
+ }
71
+
72
+ export interface LogSource {
73
+ name: string;
74
+ type: 'file' | 'api';
75
+ available: boolean;
76
+ }
77
+
78
+ class ControlPanelApi {
79
+ private baseUrl: string;
80
+
81
+ constructor(baseUrl = '') {
82
+ this.baseUrl = baseUrl;
83
+ }
84
+
85
+ async getHealth(): Promise<HealthResponse> {
86
+ const response = await fetch(`${this.baseUrl}/api/health`);
87
+ if (!response.ok) {
88
+ throw new Error(`Health check failed: ${response.statusText}`);
89
+ }
90
+ return response.json();
91
+ }
92
+
93
+ async getInfo(): Promise<InfoResponse> {
94
+ const response = await fetch(`${this.baseUrl}/api/info`);
95
+ if (!response.ok) {
96
+ throw new Error(`Info request failed: ${response.statusText}`);
97
+ }
98
+ return response.json();
99
+ }
100
+
101
+ async getDiagnostics(): Promise<DiagnosticsResponse> {
102
+ const response = await fetch(`${this.baseUrl}/api/diagnostics`);
103
+ if (!response.ok) {
104
+ throw new Error(`Diagnostics request failed: ${response.statusText}`);
105
+ }
106
+ return response.json();
107
+ }
108
+
109
+ async getConfig(): Promise<ConfigResponse> {
110
+ const response = await fetch(`${this.baseUrl}/api/config`);
111
+ if (!response.ok) {
112
+ throw new Error(`Config request failed: ${response.statusText}`);
113
+ }
114
+ return response.json();
115
+ }
116
+
117
+ async getLogs(options: {
118
+ source?: string;
119
+ level?: string;
120
+ search?: string;
121
+ limit?: number;
122
+ page?: number;
123
+ } = {}): Promise<LogsResponse> {
124
+ const params = new URLSearchParams();
125
+ if (options.source) params.set('source', options.source);
126
+ if (options.level) params.set('level', options.level);
127
+ if (options.search) params.set('search', options.search);
128
+ if (options.limit) params.set('limit', options.limit.toString());
129
+ if (options.page) params.set('page', options.page.toString());
130
+
131
+ const response = await fetch(`${this.baseUrl}/api/logs?${params}`);
132
+ if (!response.ok) {
133
+ throw new Error(`Logs request failed: ${response.statusText}`);
134
+ }
135
+ return response.json();
136
+ }
137
+
138
+ async getLogSources(): Promise<LogSource[]> {
139
+ const response = await fetch(`${this.baseUrl}/api/logs/sources`);
140
+ if (!response.ok) {
141
+ throw new Error(`Log sources request failed: ${response.statusText}`);
142
+ }
143
+ const data = await response.json();
144
+ return data.sources;
145
+ }
146
+ }
147
+
148
+ export const api = new ControlPanelApi();
@@ -0,0 +1,18 @@
1
+ import { AppConfigBuilder } from '@qwickapps/react-framework';
2
+
3
+ /**
4
+ * Default control panel configuration
5
+ * Consumers can override this by passing their own config
6
+ */
7
+ export const defaultConfig = AppConfigBuilder.create()
8
+ .withName('Control Panel')
9
+ .withId('com.qwickapps.control-panel')
10
+ .withVersion('1.0.0')
11
+ .withDefaultTheme('dark')
12
+ .withDefaultPalette('cosmic')
13
+ .withThemeSwitcher(true)
14
+ .withPaletteSwitcher(true)
15
+ .withDisplay('standalone')
16
+ .build();
17
+
18
+ export type ControlPanelUIConfig = ReturnType<typeof AppConfigBuilder.prototype.build>;
@@ -0,0 +1,29 @@
1
+ /* Custom scrollbar styling */
2
+ ::-webkit-scrollbar {
3
+ width: 8px;
4
+ height: 8px;
5
+ }
6
+
7
+ ::-webkit-scrollbar-track {
8
+ background: var(--theme-background, #0f172a);
9
+ }
10
+
11
+ ::-webkit-scrollbar-thumb {
12
+ background: var(--theme-border, #334155);
13
+ border-radius: 4px;
14
+ }
15
+
16
+ ::-webkit-scrollbar-thumb:hover {
17
+ background: var(--theme-text-secondary, #64748b);
18
+ }
19
+
20
+ /* Smooth scrolling */
21
+ html {
22
+ scroll-behavior: smooth;
23
+ }
24
+
25
+ /* Better focus styles */
26
+ :focus-visible {
27
+ outline: 2px solid var(--theme-primary, #6366f1);
28
+ outline-offset: 2px;
29
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import '@qwickapps/react-framework/index.css';
4
+ import './index.css';
5
+ import { App } from './App';
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>
11
+ );
@@ -0,0 +1,199 @@
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
+ IconButton,
16
+ Tooltip,
17
+ Snackbar,
18
+ Alert,
19
+ } from '@mui/material';
20
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy';
21
+ import VisibilityIcon from '@mui/icons-material/Visibility';
22
+ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
23
+ import LockIcon from '@mui/icons-material/Lock';
24
+ import { api, ConfigResponse } from '../api/controlPanelApi';
25
+
26
+ export function ConfigPage() {
27
+ const [config, setConfig] = useState<ConfigResponse | null>(null);
28
+ const [loading, setLoading] = useState(true);
29
+ const [error, setError] = useState<string | null>(null);
30
+ const [showMasked, setShowMasked] = useState<Record<string, boolean>>({});
31
+ const [snackbar, setSnackbar] = useState<{ open: boolean; message: string }>({
32
+ open: false,
33
+ message: '',
34
+ });
35
+
36
+ useEffect(() => {
37
+ const fetchConfig = async () => {
38
+ try {
39
+ const data = await api.getConfig();
40
+ setConfig(data);
41
+ setError(null);
42
+ } catch (err) {
43
+ setError(err instanceof Error ? err.message : 'Failed to fetch config');
44
+ } finally {
45
+ setLoading(false);
46
+ }
47
+ };
48
+
49
+ fetchConfig();
50
+ }, []);
51
+
52
+ const handleCopy = (value: string) => {
53
+ navigator.clipboard.writeText(value);
54
+ setSnackbar({ open: true, message: 'Copied to clipboard' });
55
+ };
56
+
57
+ const toggleMasked = (key: string) => {
58
+ setShowMasked((prev) => ({ ...prev, [key]: !prev[key] }));
59
+ };
60
+
61
+ const isMasked = (key: string): boolean => {
62
+ if (!config?.masked || !Array.isArray(config.masked)) return false;
63
+ return config.masked.some((mask) =>
64
+ key.toUpperCase().includes(mask.toUpperCase())
65
+ );
66
+ };
67
+
68
+ const formatValue = (key: string, value: unknown): string => {
69
+ const strValue = String(value);
70
+ if (isMasked(key) && !showMasked[key]) {
71
+ return '••••••••';
72
+ }
73
+ return strValue;
74
+ };
75
+
76
+ if (loading) {
77
+ return (
78
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
79
+ <CircularProgress />
80
+ </Box>
81
+ );
82
+ }
83
+
84
+ if (error) {
85
+ return (
86
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
87
+ <CardContent>
88
+ <Typography color="error">{error}</Typography>
89
+ </CardContent>
90
+ </Card>
91
+ );
92
+ }
93
+
94
+ const configEntries = config ? Object.entries(config.config) : [];
95
+
96
+ return (
97
+ <Box>
98
+ <Typography variant="h4" sx={{ mb: 1, color: 'var(--theme-text-primary)' }}>
99
+ Configuration
100
+ </Typography>
101
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
102
+ Current environment configuration (read-only)
103
+ </Typography>
104
+
105
+ {/* Config Table */}
106
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
107
+ <TableContainer>
108
+ <Table>
109
+ <TableHead>
110
+ <TableRow>
111
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
112
+ Variable
113
+ </TableCell>
114
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)' }}>
115
+ Value
116
+ </TableCell>
117
+ <TableCell sx={{ color: 'var(--theme-text-secondary)', borderColor: 'var(--theme-border)', width: 100 }}>
118
+ Actions
119
+ </TableCell>
120
+ </TableRow>
121
+ </TableHead>
122
+ <TableBody>
123
+ {configEntries.map(([key, value]) => (
124
+ <TableRow key={key}>
125
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
126
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
127
+ <Typography sx={{ color: 'var(--theme-text-primary)', fontFamily: 'monospace' }}>
128
+ {key}
129
+ </Typography>
130
+ {isMasked(key) && (
131
+ <Chip
132
+ icon={<LockIcon sx={{ fontSize: 14 }} />}
133
+ label="Sensitive"
134
+ size="small"
135
+ sx={{
136
+ bgcolor: 'var(--theme-warning)20',
137
+ color: 'var(--theme-warning)',
138
+ height: 20,
139
+ '& .MuiChip-icon': { color: 'var(--theme-warning)' },
140
+ }}
141
+ />
142
+ )}
143
+ </Box>
144
+ </TableCell>
145
+ <TableCell sx={{ color: 'var(--theme-text-primary)', borderColor: 'var(--theme-border)', fontFamily: 'monospace' }}>
146
+ {formatValue(key, value)}
147
+ </TableCell>
148
+ <TableCell sx={{ borderColor: 'var(--theme-border)' }}>
149
+ <Box sx={{ display: 'flex', gap: 0.5 }}>
150
+ {isMasked(key) && (
151
+ <Tooltip title={showMasked[key] ? 'Hide value' : 'Show value'}>
152
+ <IconButton
153
+ size="small"
154
+ onClick={() => toggleMasked(key)}
155
+ sx={{ color: 'var(--theme-text-secondary)' }}
156
+ >
157
+ {showMasked[key] ? <VisibilityOffIcon fontSize="small" /> : <VisibilityIcon fontSize="small" />}
158
+ </IconButton>
159
+ </Tooltip>
160
+ )}
161
+ <Tooltip title="Copy value">
162
+ <IconButton
163
+ size="small"
164
+ onClick={() => handleCopy(String(value))}
165
+ sx={{ color: 'var(--theme-text-secondary)' }}
166
+ >
167
+ <ContentCopyIcon fontSize="small" />
168
+ </IconButton>
169
+ </Tooltip>
170
+ </Box>
171
+ </TableCell>
172
+ </TableRow>
173
+ ))}
174
+ </TableBody>
175
+ </Table>
176
+ </TableContainer>
177
+
178
+ {configEntries.length === 0 && (
179
+ <CardContent>
180
+ <Typography sx={{ color: 'var(--theme-text-secondary)', textAlign: 'center' }}>
181
+ No configuration variables available
182
+ </Typography>
183
+ </CardContent>
184
+ )}
185
+ </Card>
186
+
187
+ <Snackbar
188
+ open={snackbar.open}
189
+ autoHideDuration={2000}
190
+ onClose={() => setSnackbar({ ...snackbar, open: false })}
191
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
192
+ >
193
+ <Alert severity="success" variant="filled">
194
+ {snackbar.message}
195
+ </Alert>
196
+ </Snackbar>
197
+ </Box>
198
+ );
199
+ }
@@ -0,0 +1,264 @@
1
+ import { useState, useEffect } from 'react';
2
+ import {
3
+ Box,
4
+ Card,
5
+ CardContent,
6
+ Typography,
7
+ Grid,
8
+ CircularProgress,
9
+ Chip,
10
+ } from '@mui/material';
11
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
12
+ import ErrorIcon from '@mui/icons-material/Error';
13
+ import WarningIcon from '@mui/icons-material/Warning';
14
+ import AccessTimeIcon from '@mui/icons-material/AccessTime';
15
+ import MemoryIcon from '@mui/icons-material/Memory';
16
+ import StorageIcon from '@mui/icons-material/Storage';
17
+ import { api, HealthResponse, InfoResponse } from '../api/controlPanelApi';
18
+
19
+ function formatUptime(ms: number): string {
20
+ const seconds = Math.floor(ms / 1000);
21
+ const minutes = Math.floor(seconds / 60);
22
+ const hours = Math.floor(minutes / 60);
23
+ const days = Math.floor(hours / 24);
24
+
25
+ if (days > 0) {
26
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
27
+ }
28
+ if (hours > 0) {
29
+ return `${hours}h ${minutes % 60}m`;
30
+ }
31
+ if (minutes > 0) {
32
+ return `${minutes}m ${seconds % 60}s`;
33
+ }
34
+ return `${seconds}s`;
35
+ }
36
+
37
+ function getStatusIcon(status: string) {
38
+ switch (status) {
39
+ case 'healthy':
40
+ return <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />;
41
+ case 'degraded':
42
+ return <WarningIcon sx={{ color: 'var(--theme-warning)' }} />;
43
+ case 'unhealthy':
44
+ return <ErrorIcon sx={{ color: 'var(--theme-error)' }} />;
45
+ default:
46
+ return <CircularProgress size={20} />;
47
+ }
48
+ }
49
+
50
+ function getStatusColor(status: string): string {
51
+ switch (status) {
52
+ case 'healthy':
53
+ return 'var(--theme-success)';
54
+ case 'degraded':
55
+ return 'var(--theme-warning)';
56
+ case 'unhealthy':
57
+ return 'var(--theme-error)';
58
+ default:
59
+ return 'var(--theme-text-secondary)';
60
+ }
61
+ }
62
+
63
+ export function DashboardPage() {
64
+ const [health, setHealth] = useState<HealthResponse | null>(null);
65
+ const [info, setInfo] = useState<InfoResponse | null>(null);
66
+ const [loading, setLoading] = useState(true);
67
+ const [error, setError] = useState<string | null>(null);
68
+
69
+ useEffect(() => {
70
+ const fetchData = async () => {
71
+ try {
72
+ const [healthData, infoData] = await Promise.all([
73
+ api.getHealth(),
74
+ api.getInfo(),
75
+ ]);
76
+ setHealth(healthData);
77
+ setInfo(infoData);
78
+ setError(null);
79
+ } catch (err) {
80
+ setError(err instanceof Error ? err.message : 'Failed to fetch data');
81
+ } finally {
82
+ setLoading(false);
83
+ }
84
+ };
85
+
86
+ fetchData();
87
+ const interval = setInterval(fetchData, 10000);
88
+ return () => clearInterval(interval);
89
+ }, []);
90
+
91
+ if (loading) {
92
+ return (
93
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
94
+ <CircularProgress />
95
+ </Box>
96
+ );
97
+ }
98
+
99
+ if (error) {
100
+ return (
101
+ <Card sx={{ bgcolor: 'var(--theme-surface)', border: '1px solid var(--theme-error)' }}>
102
+ <CardContent>
103
+ <Typography color="error">{error}</Typography>
104
+ </CardContent>
105
+ </Card>
106
+ );
107
+ }
108
+
109
+ const healthChecks = health ? Object.entries(health.checks) : [];
110
+ const healthyCount = healthChecks.filter(([, c]) => c.status === 'healthy').length;
111
+ const totalCount = healthChecks.length;
112
+
113
+ return (
114
+ <Box>
115
+ <Typography variant="h4" sx={{ mb: 1, color: 'var(--theme-text-primary)' }}>
116
+ Dashboard
117
+ </Typography>
118
+ <Typography variant="body2" sx={{ mb: 4, color: 'var(--theme-text-secondary)' }}>
119
+ Real-time overview of {info?.product || 'your service'}
120
+ </Typography>
121
+
122
+ {/* Status Banner */}
123
+ <Card
124
+ sx={{
125
+ mb: 4,
126
+ bgcolor: 'var(--theme-surface)',
127
+ border: `2px solid ${getStatusColor(health?.status || 'unknown')}`,
128
+ }}
129
+ >
130
+ <CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
131
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
132
+ {getStatusIcon(health?.status || 'unknown')}
133
+ <Box>
134
+ <Typography variant="h6" sx={{ color: 'var(--theme-text-primary)' }}>
135
+ Service Status: {health?.status?.charAt(0).toUpperCase()}{health?.status?.slice(1)}
136
+ </Typography>
137
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
138
+ Last updated: {health?.timestamp ? new Date(health.timestamp).toLocaleString() : 'N/A'}
139
+ </Typography>
140
+ </Box>
141
+ </Box>
142
+ <Chip
143
+ label={`${healthyCount}/${totalCount} checks passing`}
144
+ sx={{
145
+ bgcolor: getStatusColor(health?.status || 'unknown') + '20',
146
+ color: getStatusColor(health?.status || 'unknown'),
147
+ }}
148
+ />
149
+ </CardContent>
150
+ </Card>
151
+
152
+ {/* Stats Grid */}
153
+ <Grid container spacing={3} sx={{ mb: 4 }}>
154
+ <Grid size={{ xs: 12, sm: 6, md: 3 }}>
155
+ <Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
156
+ <CardContent>
157
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
158
+ <AccessTimeIcon sx={{ color: 'var(--theme-primary)' }} />
159
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
160
+ Uptime
161
+ </Typography>
162
+ </Box>
163
+ <Typography variant="h4" sx={{ color: 'var(--theme-primary)' }}>
164
+ {health ? formatUptime(health.uptime) : 'N/A'}
165
+ </Typography>
166
+ </CardContent>
167
+ </Card>
168
+ </Grid>
169
+
170
+ <Grid size={{ xs: 12, sm: 6, md: 3 }}>
171
+ <Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
172
+ <CardContent>
173
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
174
+ <CheckCircleIcon sx={{ color: 'var(--theme-success)' }} />
175
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
176
+ Health Checks
177
+ </Typography>
178
+ </Box>
179
+ <Typography variant="h4" sx={{ color: 'var(--theme-success)' }}>
180
+ {healthyCount}/{totalCount}
181
+ </Typography>
182
+ </CardContent>
183
+ </Card>
184
+ </Grid>
185
+
186
+ <Grid size={{ xs: 12, sm: 6, md: 3 }}>
187
+ <Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
188
+ <CardContent>
189
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
190
+ <MemoryIcon sx={{ color: 'var(--theme-warning)' }} />
191
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
192
+ Version
193
+ </Typography>
194
+ </Box>
195
+ <Typography variant="h4" sx={{ color: 'var(--theme-text-primary)' }}>
196
+ {info?.version || 'N/A'}
197
+ </Typography>
198
+ </CardContent>
199
+ </Card>
200
+ </Grid>
201
+
202
+ <Grid size={{ xs: 12, sm: 6, md: 3 }}>
203
+ <Card sx={{ bgcolor: 'var(--theme-surface)', height: '100%' }}>
204
+ <CardContent>
205
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
206
+ <StorageIcon sx={{ color: 'var(--theme-info)' }} />
207
+ <Typography variant="body2" sx={{ color: 'var(--theme-text-secondary)' }}>
208
+ Product
209
+ </Typography>
210
+ </Box>
211
+ <Typography variant="h5" sx={{ color: 'var(--theme-text-primary)' }}>
212
+ {info?.product || 'N/A'}
213
+ </Typography>
214
+ </CardContent>
215
+ </Card>
216
+ </Grid>
217
+ </Grid>
218
+
219
+ {/* Health Checks Detail */}
220
+ <Card sx={{ bgcolor: 'var(--theme-surface)' }}>
221
+ <CardContent>
222
+ <Typography variant="h6" sx={{ mb: 3, color: 'var(--theme-text-primary)' }}>
223
+ Health Checks
224
+ </Typography>
225
+ <Grid container spacing={2}>
226
+ {healthChecks.map(([name, check]) => (
227
+ <Grid size={{ xs: 12, sm: 6, md: 4 }} key={name}>
228
+ <Box
229
+ sx={{
230
+ p: 2,
231
+ borderRadius: 1,
232
+ bgcolor: 'var(--theme-background)',
233
+ border: `1px solid ${getStatusColor(check.status)}40`,
234
+ }}
235
+ >
236
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
237
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
238
+ {getStatusIcon(check.status)}
239
+ <Typography sx={{ color: 'var(--theme-text-primary)', fontWeight: 500 }}>
240
+ {name}
241
+ </Typography>
242
+ </Box>
243
+ {check.latency !== undefined && (
244
+ <Chip
245
+ label={`${check.latency}ms`}
246
+ size="small"
247
+ sx={{ bgcolor: 'var(--theme-surface)', color: 'var(--theme-text-secondary)' }}
248
+ />
249
+ )}
250
+ </Box>
251
+ {check.error && (
252
+ <Typography variant="caption" sx={{ color: 'var(--theme-error)' }}>
253
+ {check.error}
254
+ </Typography>
255
+ )}
256
+ </Box>
257
+ </Grid>
258
+ ))}
259
+ </Grid>
260
+ </CardContent>
261
+ </Card>
262
+ </Box>
263
+ );
264
+ }