@alepha/devtools 0.13.6 → 0.13.7

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 (62) hide show
  1. package/dist/index.d.ts +249 -32
  2. package/dist/index.js +253 -22
  3. package/dist/index.js.map +1 -1
  4. package/package.json +12 -6
  5. package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
  6. package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
  7. package/src/api/schemas/DevAtomMetadata.ts +26 -0
  8. package/src/api/schemas/DevCommandMetadata.ts +9 -0
  9. package/src/api/schemas/DevEntityMetadata.ts +57 -0
  10. package/src/api/schemas/DevEnvMetadata.ts +22 -0
  11. package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
  12. package/src/api/schemas/DevRouteMetadata.ts +8 -0
  13. package/src/index.ts +23 -16
  14. package/src/ui/AppRouter.tsx +85 -2
  15. package/src/ui/components/DevAtomsViewer.tsx +636 -0
  16. package/src/ui/components/DevCacheInspector.tsx +423 -0
  17. package/src/ui/components/DevDashboard.tsx +188 -0
  18. package/src/ui/components/DevEnvExplorer.tsx +462 -0
  19. package/src/ui/components/DevLayout.tsx +65 -4
  20. package/src/ui/components/DevLogViewer.tsx +161 -163
  21. package/src/ui/components/DevQueueMonitor.tsx +51 -0
  22. package/src/ui/components/DevTopicsViewer.tsx +690 -0
  23. package/src/ui/components/actions/ActionGroup.tsx +37 -0
  24. package/src/ui/components/actions/ActionItem.tsx +138 -0
  25. package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
  26. package/src/ui/components/actions/MethodBadge.tsx +18 -0
  27. package/src/ui/components/actions/SchemaViewer.tsx +21 -0
  28. package/src/ui/components/actions/TryItPanel.tsx +140 -0
  29. package/src/ui/components/actions/constants.ts +7 -0
  30. package/src/ui/components/actions/helpers.ts +18 -0
  31. package/src/ui/components/actions/index.ts +8 -0
  32. package/src/ui/components/db/ColumnBadge.tsx +55 -0
  33. package/src/ui/components/db/DevDbStudio.tsx +485 -0
  34. package/src/ui/components/db/constants.ts +11 -0
  35. package/src/ui/components/db/index.ts +4 -0
  36. package/src/ui/components/db/types.ts +7 -0
  37. package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
  38. package/src/ui/components/graph/GraphControls.tsx +162 -0
  39. package/src/ui/components/graph/NodeDetails.tsx +181 -0
  40. package/src/ui/components/graph/ProviderNode.tsx +97 -0
  41. package/src/ui/components/graph/constants.ts +35 -0
  42. package/src/ui/components/graph/helpers.ts +443 -0
  43. package/src/ui/components/graph/index.ts +7 -0
  44. package/src/ui/components/graph/types.ts +28 -0
  45. package/src/ui/styles.css +0 -6
  46. package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
  47. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
  48. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
  49. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
  50. /package/src/{entities → api/entities}/logs.ts +0 -0
  51. /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
  52. /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
  53. /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
  54. /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
  55. /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
  56. /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
  57. /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
  58. /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
  59. /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
  60. /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
  61. /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
  62. /package/src/{schemas → api/schemas}/DevTopicMetadata.ts +0 -0
@@ -0,0 +1,423 @@
1
+ import { useInject } from "@alepha/react";
2
+ import { ui } from "@alepha/ui";
3
+ import {
4
+ ActionIcon,
5
+ Badge,
6
+ Box,
7
+ Button,
8
+ Flex,
9
+ ScrollArea,
10
+ Stack,
11
+ Table,
12
+ Tabs,
13
+ Text,
14
+ TextInput,
15
+ Tooltip,
16
+ } from "@mantine/core";
17
+ import {
18
+ IconArchive,
19
+ IconClock,
20
+ IconKey,
21
+ IconSearch,
22
+ IconServer,
23
+ IconTrash,
24
+ } from "@tabler/icons-react";
25
+ import { HttpClient } from "alepha/server";
26
+ import { useEffect, useMemo, useState } from "react";
27
+ import type { DevCacheMetadata } from "../../api/schemas/DevCacheMetadata.ts";
28
+ import { devMetadataSchema } from "../../api/schemas/DevMetadata.ts";
29
+
30
+ const PROVIDER_COLORS: Record<string, string> = {
31
+ memory: "#69db7c",
32
+ redis: "#ff6b6b",
33
+ default: "#495057",
34
+ };
35
+
36
+ const getProviderColor = (provider: string): string => {
37
+ return PROVIDER_COLORS[provider] ?? PROVIDER_COLORS.default;
38
+ };
39
+
40
+ const formatTtl = (ttl: any): string => {
41
+ if (!ttl) return "No TTL";
42
+ if (typeof ttl === "number") {
43
+ if (ttl < 60) return `${ttl}s`;
44
+ if (ttl < 3600) return `${Math.floor(ttl / 60)}m`;
45
+ if (ttl < 86400) return `${Math.floor(ttl / 3600)}h`;
46
+ return `${Math.floor(ttl / 86400)}d`;
47
+ }
48
+ return String(ttl);
49
+ };
50
+
51
+ const CacheSidebar = ({
52
+ caches,
53
+ selectedCache,
54
+ onSelectCache,
55
+ search,
56
+ onSearchChange,
57
+ }: {
58
+ caches: DevCacheMetadata[];
59
+ selectedCache: DevCacheMetadata | null;
60
+ onSelectCache: (cache: DevCacheMetadata) => void;
61
+ search: string;
62
+ onSearchChange: (search: string) => void;
63
+ }) => {
64
+ // Group caches by provider
65
+ const grouped = useMemo(() => {
66
+ const groups: Record<string, DevCacheMetadata[]> = {};
67
+ for (const cache of caches) {
68
+ const provider = cache.provider || "default";
69
+ if (!groups[provider]) groups[provider] = [];
70
+ groups[provider].push(cache);
71
+ }
72
+ return groups;
73
+ }, [caches]);
74
+
75
+ const providers = Object.keys(grouped).sort();
76
+
77
+ return (
78
+ <Stack gap={0} h="100%">
79
+ <Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
80
+ <TextInput
81
+ placeholder="Search caches..."
82
+ leftSection={<IconSearch size={14} />}
83
+ size="xs"
84
+ value={search}
85
+ onChange={(e) => onSearchChange(e.target.value)}
86
+ />
87
+ </Box>
88
+ <ScrollArea style={{ flex: 1 }}>
89
+ {providers.map((provider) => (
90
+ <Box key={provider}>
91
+ <Text
92
+ size="xs"
93
+ fw={600}
94
+ c="dimmed"
95
+ px="sm"
96
+ py="xs"
97
+ style={{
98
+ backgroundColor: ui.colors.background,
99
+ borderBottom: `1px solid ${ui.colors.border}`,
100
+ textTransform: "uppercase",
101
+ letterSpacing: "0.05em",
102
+ }}
103
+ >
104
+ {provider}
105
+ </Text>
106
+ {grouped[provider].map((cache) => {
107
+ const isSelected = selectedCache?.name === cache.name;
108
+ const providerColor = getProviderColor(cache.provider);
109
+ return (
110
+ <Flex
111
+ key={cache.name}
112
+ align="center"
113
+ gap="xs"
114
+ px="sm"
115
+ py={6}
116
+ onClick={() => onSelectCache(cache)}
117
+ style={{
118
+ cursor: "pointer",
119
+ backgroundColor: isSelected
120
+ ? `${providerColor}15`
121
+ : undefined,
122
+ borderLeft: isSelected
123
+ ? `2px solid ${providerColor}`
124
+ : "2px solid transparent",
125
+ borderBottom: `1px solid ${ui.colors.border}`,
126
+ opacity: cache.disabled ? 0.5 : 1,
127
+ }}
128
+ >
129
+ <IconArchive size={14} opacity={0.5} />
130
+ <Text size="sm" style={{ flex: 1 }} truncate>
131
+ {cache.name}
132
+ </Text>
133
+ {cache.disabled && (
134
+ <Badge size="xs" variant="light" color="gray">
135
+ off
136
+ </Badge>
137
+ )}
138
+ </Flex>
139
+ );
140
+ })}
141
+ </Box>
142
+ ))}
143
+ </ScrollArea>
144
+ </Stack>
145
+ );
146
+ };
147
+
148
+ const ConfigTab = ({ cache }: { cache: DevCacheMetadata }) => {
149
+ return (
150
+ <ScrollArea h="100%" p="md">
151
+ <Stack gap="lg">
152
+ <Box>
153
+ <Text size="sm" fw={600} mb="xs">
154
+ Configuration
155
+ </Text>
156
+ <Table striped highlightOnHover withTableBorder>
157
+ <Table.Tbody>
158
+ <Table.Tr>
159
+ <Table.Td w={150}>
160
+ <Text size="sm" c="dimmed">
161
+ Name
162
+ </Text>
163
+ </Table.Td>
164
+ <Table.Td>
165
+ <Text size="sm" ff="monospace">
166
+ {cache.name}
167
+ </Text>
168
+ </Table.Td>
169
+ </Table.Tr>
170
+ <Table.Tr>
171
+ <Table.Td>
172
+ <Text size="sm" c="dimmed">
173
+ Provider
174
+ </Text>
175
+ </Table.Td>
176
+ <Table.Td>
177
+ <Badge
178
+ size="xs"
179
+ variant="light"
180
+ color={cache.provider === "redis" ? "red" : "green"}
181
+ >
182
+ {cache.provider}
183
+ </Badge>
184
+ </Table.Td>
185
+ </Table.Tr>
186
+ <Table.Tr>
187
+ <Table.Td>
188
+ <Text size="sm" c="dimmed">
189
+ TTL
190
+ </Text>
191
+ </Table.Td>
192
+ <Table.Td>
193
+ <Flex align="center" gap="xs">
194
+ <IconClock size={14} opacity={0.5} />
195
+ <Text size="sm" ff="monospace">
196
+ {formatTtl(cache.ttl)}
197
+ </Text>
198
+ </Flex>
199
+ </Table.Td>
200
+ </Table.Tr>
201
+ <Table.Tr>
202
+ <Table.Td>
203
+ <Text size="sm" c="dimmed">
204
+ Status
205
+ </Text>
206
+ </Table.Td>
207
+ <Table.Td>
208
+ <Badge
209
+ size="xs"
210
+ variant="light"
211
+ color={cache.disabled ? "gray" : "green"}
212
+ >
213
+ {cache.disabled ? "Disabled" : "Enabled"}
214
+ </Badge>
215
+ </Table.Td>
216
+ </Table.Tr>
217
+ </Table.Tbody>
218
+ </Table>
219
+ </Box>
220
+
221
+ <Box>
222
+ <Text size="sm" fw={600} mb="xs">
223
+ Actions
224
+ </Text>
225
+ <Flex gap="sm">
226
+ <Tooltip label="Clear all cached entries (coming soon)">
227
+ <Button
228
+ size="xs"
229
+ variant="light"
230
+ color="red"
231
+ leftSection={<IconTrash size={14} />}
232
+ disabled
233
+ >
234
+ Clear All
235
+ </Button>
236
+ </Tooltip>
237
+ </Flex>
238
+ </Box>
239
+ </Stack>
240
+ </ScrollArea>
241
+ );
242
+ };
243
+
244
+ const KeysTab = ({ cache }: { cache: DevCacheMetadata }) => {
245
+ const [pattern, setPattern] = useState("");
246
+
247
+ return (
248
+ <Flex direction="column" h="100%">
249
+ <Box p="sm" style={{ borderBottom: `1px solid ${ui.colors.border}` }}>
250
+ <Flex gap="sm" align="center">
251
+ <TextInput
252
+ placeholder="Filter by pattern..."
253
+ leftSection={<IconSearch size={14} />}
254
+ size="xs"
255
+ value={pattern}
256
+ onChange={(e) => setPattern(e.target.value)}
257
+ style={{ flex: 1 }}
258
+ />
259
+ <Tooltip label="Invalidate matching keys (coming soon)">
260
+ <ActionIcon size="sm" variant="light" color="orange" disabled>
261
+ <IconTrash size={14} />
262
+ </ActionIcon>
263
+ </Tooltip>
264
+ </Flex>
265
+ </Box>
266
+ <Flex align="center" justify="center" style={{ flex: 1 }} c="dimmed">
267
+ <Stack align="center" gap="xs">
268
+ <IconKey size={48} opacity={0.3} />
269
+ <Text size="sm">Key browser coming soon</Text>
270
+ <Text size="xs" c="dimmed">
271
+ Browse and manage cached keys for {cache.name}
272
+ </Text>
273
+ </Stack>
274
+ </Flex>
275
+ </Flex>
276
+ );
277
+ };
278
+
279
+ const CachePanel = ({ cache }: { cache: DevCacheMetadata }) => {
280
+ const providerColor = getProviderColor(cache.provider);
281
+
282
+ return (
283
+ <Flex direction="column" h="100%">
284
+ {/* Header */}
285
+ <Box
286
+ px="md"
287
+ py="sm"
288
+ style={{
289
+ borderBottom: `1px solid ${ui.colors.border}`,
290
+ backgroundColor: `${providerColor}08`,
291
+ }}
292
+ >
293
+ <Flex align="center" gap="sm">
294
+ <IconArchive size={18} opacity={0.7} />
295
+ <Text size="md" fw={600}>
296
+ {cache.name}
297
+ </Text>
298
+ <Badge
299
+ size="xs"
300
+ variant="light"
301
+ color={cache.provider === "redis" ? "red" : "green"}
302
+ >
303
+ {cache.provider}
304
+ </Badge>
305
+ {cache.disabled && (
306
+ <Badge size="xs" variant="light" color="gray">
307
+ disabled
308
+ </Badge>
309
+ )}
310
+ </Flex>
311
+ </Box>
312
+
313
+ {/* Tabs */}
314
+ <Tabs
315
+ defaultValue="config"
316
+ style={{ flex: 1, display: "flex", flexDirection: "column" }}
317
+ >
318
+ <Tabs.List px="md">
319
+ <Tabs.Tab value="config">Config</Tabs.Tab>
320
+ <Tabs.Tab value="keys">Keys</Tabs.Tab>
321
+ </Tabs.List>
322
+
323
+ <Tabs.Panel value="config" style={{ flex: 1, overflow: "hidden" }}>
324
+ <ConfigTab cache={cache} />
325
+ </Tabs.Panel>
326
+
327
+ <Tabs.Panel value="keys" style={{ flex: 1, overflow: "hidden" }}>
328
+ <KeysTab cache={cache} />
329
+ </Tabs.Panel>
330
+ </Tabs>
331
+ </Flex>
332
+ );
333
+ };
334
+
335
+ const EmptyState = () => (
336
+ <Flex align="center" justify="center" h="100%" c="dimmed">
337
+ <Stack align="center" gap="xs">
338
+ <IconArchive size={48} opacity={0.3} />
339
+ <Text size="sm">Select a cache to view its configuration</Text>
340
+ </Stack>
341
+ </Flex>
342
+ );
343
+
344
+ const NoCachesState = () => (
345
+ <Flex align="center" justify="center" h="100%" c="dimmed">
346
+ <Stack align="center" gap="xs">
347
+ <IconServer size={48} opacity={0.3} />
348
+ <Text>No caches found</Text>
349
+ <Text size="sm" c="dimmed">
350
+ Add caches using $cache primitive to see them here
351
+ </Text>
352
+ </Stack>
353
+ </Flex>
354
+ );
355
+
356
+ export const DevCacheInspector = () => {
357
+ const http = useInject(HttpClient);
358
+ const [caches, setCaches] = useState<DevCacheMetadata[]>([]);
359
+ const [loading, setLoading] = useState(true);
360
+ const [selectedCache, setSelectedCache] = useState<DevCacheMetadata | null>(
361
+ null,
362
+ );
363
+ const [search, setSearch] = useState("");
364
+
365
+ // Fetch caches
366
+ useEffect(() => {
367
+ http
368
+ .fetch("/devtools/api/metadata", {
369
+ schema: { response: devMetadataSchema },
370
+ })
371
+ .then((res) => {
372
+ setCaches(res.data.caches);
373
+ setLoading(false);
374
+ });
375
+ }, []);
376
+
377
+ // Filter caches by search
378
+ const filteredCaches = useMemo(() => {
379
+ if (!search) return caches;
380
+ const searchLower = search.toLowerCase();
381
+ return caches.filter((c) => c.name.toLowerCase().includes(searchLower));
382
+ }, [caches, search]);
383
+
384
+ if (loading) {
385
+ return (
386
+ <Flex align="center" justify="center" h="100%">
387
+ <Text c="dimmed">Loading...</Text>
388
+ </Flex>
389
+ );
390
+ }
391
+
392
+ if (caches.length === 0) {
393
+ return <NoCachesState />;
394
+ }
395
+
396
+ return (
397
+ <Flex h="100%" style={{ overflow: "hidden" }}>
398
+ {/* Sidebar */}
399
+ <Box
400
+ w={240}
401
+ style={{
402
+ borderRight: `1px solid ${ui.colors.border}`,
403
+ backgroundColor: ui.colors.surface,
404
+ }}
405
+ >
406
+ <CacheSidebar
407
+ caches={filteredCaches}
408
+ selectedCache={selectedCache}
409
+ onSelectCache={setSelectedCache}
410
+ search={search}
411
+ onSearchChange={setSearch}
412
+ />
413
+ </Box>
414
+
415
+ {/* Main content */}
416
+ <Box style={{ flex: 1, overflow: "hidden" }}>
417
+ {selectedCache ? <CachePanel cache={selectedCache} /> : <EmptyState />}
418
+ </Box>
419
+ </Flex>
420
+ );
421
+ };
422
+
423
+ export default DevCacheInspector;
@@ -0,0 +1,188 @@
1
+ import { useInject } from "@alepha/react";
2
+ import { ui } from "@alepha/ui";
3
+ import { Box, Flex, SimpleGrid, Text } from "@mantine/core";
4
+ import {
5
+ IconApi,
6
+ IconArchive,
7
+ IconAtom,
8
+ IconBucket,
9
+ IconCalendarEvent,
10
+ IconDatabase,
11
+ IconLock,
12
+ IconMessageCircle,
13
+ IconPackages,
14
+ IconServer,
15
+ IconStack2,
16
+ IconTerminal,
17
+ IconTools,
18
+ IconVariable,
19
+ } from "@tabler/icons-react";
20
+ import { HttpClient } from "alepha/server";
21
+ import type { ReactNode } from "react";
22
+ import { useEffect, useState } from "react";
23
+ import {
24
+ type DevMetadata,
25
+ devMetadataSchema,
26
+ } from "../../api/schemas/DevMetadata.ts";
27
+
28
+ interface StatCardProps {
29
+ label: string;
30
+ count: number;
31
+ icon: ReactNode;
32
+ }
33
+
34
+ const StatCard = ({ label, count, icon }: StatCardProps) => (
35
+ <Flex
36
+ direction="column"
37
+ align="center"
38
+ justify="center"
39
+ p="xl"
40
+ style={{
41
+ borderRadius: 12,
42
+ border: `1px solid ${ui.colors.border}`,
43
+ cursor: "pointer",
44
+ transition: "all 0.2s ease",
45
+ }}
46
+ bg={ui.colors.surface}
47
+ className="devtools-card"
48
+ >
49
+ <Box opacity={0.5} mb="md">
50
+ {icon}
51
+ </Box>
52
+ <Text size="sm" c="dimmed">
53
+ <Text span fw={600} size="lg" c="var(--mantine-color-text)">
54
+ {count}
55
+ </Text>{" "}
56
+ {label}
57
+ </Text>
58
+ </Flex>
59
+ );
60
+
61
+ export const DevDashboard = () => {
62
+ const http = useInject(HttpClient);
63
+ const [metadata, setMetadata] = useState<DevMetadata | null>(null);
64
+
65
+ useEffect(() => {
66
+ http
67
+ .fetch("/devtools/api/metadata", {
68
+ schema: { response: devMetadataSchema },
69
+ })
70
+ .then((res) => setMetadata(res.data));
71
+ }, []);
72
+
73
+ if (!metadata) {
74
+ return (
75
+ <Flex align="center" justify="center" h="100%">
76
+ <Text c="dimmed">Loading...</Text>
77
+ </Flex>
78
+ );
79
+ }
80
+
81
+ const stats = [
82
+ {
83
+ label: "actions",
84
+ count: metadata.actions.length,
85
+ icon: <IconApi size={48} stroke={1.2} />,
86
+ },
87
+ {
88
+ label: "entities",
89
+ count: metadata.entities.length,
90
+ icon: <IconDatabase size={48} stroke={1.2} />,
91
+ },
92
+ {
93
+ label: "queues",
94
+ count: metadata.queues.length,
95
+ icon: <IconStack2 size={48} stroke={1.2} />,
96
+ },
97
+ {
98
+ label: "schedulers",
99
+ count: metadata.schedulers.length,
100
+ icon: <IconCalendarEvent size={48} stroke={1.2} />,
101
+ },
102
+ {
103
+ label: "topics",
104
+ count: metadata.topics.length,
105
+ icon: <IconMessageCircle size={48} stroke={1.2} />,
106
+ },
107
+ {
108
+ label: "buckets",
109
+ count: metadata.buckets.length,
110
+ icon: <IconBucket size={48} stroke={1.2} />,
111
+ },
112
+ {
113
+ label: "realms",
114
+ count: metadata.realms.length,
115
+ icon: <IconLock size={48} stroke={1.2} />,
116
+ },
117
+ {
118
+ label: "caches",
119
+ count: metadata.caches.length,
120
+ icon: <IconArchive size={48} stroke={1.2} />,
121
+ },
122
+ {
123
+ label: "commands",
124
+ count: metadata.commands.length,
125
+ icon: <IconTerminal size={48} stroke={1.2} />,
126
+ },
127
+ {
128
+ label: "env schemas",
129
+ count: metadata.envs.length,
130
+ icon: <IconVariable size={48} stroke={1.2} />,
131
+ },
132
+ {
133
+ label: "atoms",
134
+ count: metadata.atoms.length,
135
+ icon: <IconAtom size={48} stroke={1.2} />,
136
+ },
137
+ {
138
+ label: "modules",
139
+ count: metadata.modules.length,
140
+ icon: <IconPackages size={48} stroke={1.2} />,
141
+ },
142
+ {
143
+ label: "services",
144
+ count: metadata.providers.length,
145
+ icon: <IconServer size={48} stroke={1.2} />,
146
+ },
147
+ ].filter((stat) => stat.count > 0);
148
+
149
+ return (
150
+ <Flex
151
+ direction="column"
152
+ align="center"
153
+ justify="center"
154
+ flex={1}
155
+ w="100%"
156
+ gap="xl"
157
+ >
158
+ <style>
159
+ {`
160
+ .devtools-card:hover {
161
+ border-color: var(--mantine-color-dimmed);
162
+ transform: translateY(-2px);
163
+ }
164
+ `}
165
+ </style>
166
+
167
+ <Flex direction="column" align="center" gap={4} mb="md">
168
+ <Flex align="center" gap="sm">
169
+ <IconTools size={40} stroke={1.5} opacity={0.8} />
170
+ <Text size="2rem" fw={300} style={{ letterSpacing: "-0.02em" }}>
171
+ Alepha DevTools
172
+ </Text>
173
+ </Flex>
174
+ <Text size="xs" c="dimmed">
175
+ Explore your Alepha application metadata
176
+ </Text>
177
+ </Flex>
178
+
179
+ <SimpleGrid cols={{ base: 2, sm: 3, md: 4 }} spacing="md">
180
+ {stats.map((stat) => (
181
+ <StatCard key={stat.label} {...stat} />
182
+ ))}
183
+ </SimpleGrid>
184
+ </Flex>
185
+ );
186
+ };
187
+
188
+ export default DevDashboard;