nitrostack 1.0.65 → 1.0.67

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 (61) hide show
  1. package/package.json +3 -2
  2. package/src/studio/README.md +140 -0
  3. package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
  4. package/src/studio/app/api/auth/register-client/route.ts +67 -0
  5. package/src/studio/app/api/chat/route.ts +250 -0
  6. package/src/studio/app/api/health/checks/route.ts +42 -0
  7. package/src/studio/app/api/health/route.ts +13 -0
  8. package/src/studio/app/api/init/route.ts +109 -0
  9. package/src/studio/app/api/ping/route.ts +13 -0
  10. package/src/studio/app/api/prompts/[name]/route.ts +21 -0
  11. package/src/studio/app/api/prompts/route.ts +13 -0
  12. package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
  13. package/src/studio/app/api/resources/route.ts +13 -0
  14. package/src/studio/app/api/roots/route.ts +13 -0
  15. package/src/studio/app/api/sampling/route.ts +14 -0
  16. package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
  17. package/src/studio/app/api/tools/route.ts +23 -0
  18. package/src/studio/app/api/widget-examples/route.ts +44 -0
  19. package/src/studio/app/auth/callback/page.tsx +175 -0
  20. package/src/studio/app/auth/page.tsx +560 -0
  21. package/src/studio/app/chat/page.tsx +1133 -0
  22. package/src/studio/app/chat/page.tsx.backup +390 -0
  23. package/src/studio/app/globals.css +486 -0
  24. package/src/studio/app/health/page.tsx +179 -0
  25. package/src/studio/app/layout.tsx +68 -0
  26. package/src/studio/app/logs/page.tsx +279 -0
  27. package/src/studio/app/page.tsx +351 -0
  28. package/src/studio/app/page.tsx.backup +346 -0
  29. package/src/studio/app/ping/page.tsx +209 -0
  30. package/src/studio/app/prompts/page.tsx +230 -0
  31. package/src/studio/app/resources/page.tsx +315 -0
  32. package/src/studio/app/settings/page.tsx +199 -0
  33. package/src/studio/branding.md +807 -0
  34. package/src/studio/components/EnlargeModal.tsx +138 -0
  35. package/src/studio/components/LogMessage.tsx +153 -0
  36. package/src/studio/components/MarkdownRenderer.tsx +410 -0
  37. package/src/studio/components/Sidebar.tsx +295 -0
  38. package/src/studio/components/ToolCard.tsx +139 -0
  39. package/src/studio/components/WidgetRenderer.tsx +346 -0
  40. package/src/studio/lib/api.ts +207 -0
  41. package/src/studio/lib/http-client-transport.ts +222 -0
  42. package/src/studio/lib/llm-service.ts +480 -0
  43. package/src/studio/lib/log-manager.ts +76 -0
  44. package/src/studio/lib/mcp-client.ts +258 -0
  45. package/src/studio/lib/store.ts +192 -0
  46. package/src/studio/lib/theme-provider.tsx +50 -0
  47. package/src/studio/lib/types.ts +107 -0
  48. package/src/studio/lib/widget-loader.ts +90 -0
  49. package/src/studio/middleware.ts +27 -0
  50. package/src/studio/next.config.js +38 -0
  51. package/src/studio/package.json +35 -0
  52. package/src/studio/postcss.config.mjs +10 -0
  53. package/src/studio/public/nitrocloud.png +0 -0
  54. package/src/studio/tailwind.config.ts +67 -0
  55. package/src/studio/tsconfig.json +42 -0
  56. package/templates/typescript-oauth/AI_AGENT_CLI_REFERENCE.md +0 -701
  57. package/templates/typescript-oauth/AI_AGENT_SDK_REFERENCE.md +0 -1260
  58. package/templates/typescript-oauth/package-lock.json +0 -4253
  59. package/templates/typescript-pizzaz/IMPLEMENTATION.md +0 -98
  60. package/templates/typescript-starter/AI_AGENT_CLI_REFERENCE.md +0 -701
  61. package/templates/typescript-starter/AI_AGENT_SDK_REFERENCE.md +0 -1260
@@ -0,0 +1,486 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ /* Nitrocloud Light Theme - Following Branding Guidelines */
8
+ --background: 0 0% 100%;
9
+ /* #FFFFFF */
10
+ --foreground: 222.2 84% 4.9%;
11
+ /* #020617 - Near black */
12
+ --card: 0 0% 100%;
13
+ /* #FFFFFF */
14
+ --card-foreground: 222.2 84% 4.9%;
15
+ /* #020617 */
16
+ --popover: 0 0% 100%;
17
+ /* #FFFFFF */
18
+ --popover-foreground: 222.2 84% 4.9%;
19
+ /* #020617 */
20
+ --primary: 217 91% 60%;
21
+ /* #3B9FFF - Nitrocloud Blue */
22
+ --primary-foreground: 210 40% 98%;
23
+ /* #F8FAFC */
24
+ --secondary: 210 40% 96.1%;
25
+ /* #F1F5F9 */
26
+ --secondary-foreground: 222.2 84% 4.9%;
27
+ /* #020617 */
28
+ --muted: 210 40% 96.1%;
29
+ /* #F1F5F9 */
30
+ --muted-foreground: 215.4 16.3% 46.9%;
31
+ /* #64748B */
32
+ --accent: 217 91% 60%;
33
+ /* #3B9FFF - Nitrocloud Blue */
34
+ --accent-foreground: 222.2 84% 4.9%;
35
+ /* #020617 */
36
+ --destructive: 0 84.2% 60.2%;
37
+ /* #EF4444 */
38
+ --destructive-foreground: 0 0% 100%;
39
+ /* #FFFFFF */
40
+ --border: 214.3 31.8% 91.4%;
41
+ /* #E2E8F0 */
42
+ --input: 214.3 31.8% 91.4%;
43
+ /* #E2E8F0 */
44
+ --ring: 217 91% 60%;
45
+ /* #3B9FFF - Nitrocloud Blue */
46
+ --radius: 0.75rem;
47
+ /* 12px - Global default */
48
+ --success: 142.1 76.2% 36.3%;
49
+ /* #22C55E */
50
+ --warning: 38 92% 50%;
51
+ /* #F59E0B */
52
+ --info: 217 91% 60%;
53
+ /* #3B9FFF */
54
+
55
+ /* Nitrocloud Gradient */
56
+ --nitrocloud-gradient-start: 217 91% 60%;
57
+ /* Blue */
58
+ --nitrocloud-gradient-end: 221 83% 53%;
59
+ /* Deep Blue-Purple */
60
+ }
61
+
62
+ .dark {
63
+ /* Nitrocloud Dark Theme - Following Branding Guidelines */
64
+ --background: 222.2 84% 4.9%;
65
+ /* #020617 - Deep dark blue */
66
+ --foreground: 210 40% 98%;
67
+ /* #F8FAFC */
68
+ --card: 217.2 32.6% 17.5%;
69
+ /* #1E293B */
70
+ --card-foreground: 210 40% 98%;
71
+ /* #F8FAFC */
72
+ --popover: 217.2 32.6% 17.5%;
73
+ /* #1E293B */
74
+ --popover-foreground: 210 40% 98%;
75
+ /* #F8FAFC */
76
+ --primary: 217 91% 60%;
77
+ /* #3B9FFF - Brighter in dark */
78
+ --primary-foreground: 222.2 47.4% 11.2%;
79
+ /* #0F172A */
80
+ --secondary: 217.2 32.6% 17.5%;
81
+ /* #1E293B */
82
+ --secondary-foreground: 210 40% 98%;
83
+ /* #F8FAFC */
84
+ --muted: 217.2 32.6% 17.5%;
85
+ /* #1E293B */
86
+ --muted-foreground: 215 20.2% 65.1%;
87
+ /* #94A3B8 */
88
+ --accent: 217 91% 60%;
89
+ /* #3B9FFF */
90
+ --accent-foreground: 210 40% 98%;
91
+ /* #F8FAFC */
92
+ --destructive: 0 62.8% 30.6%;
93
+ /* #991B1B */
94
+ --destructive-foreground: 210 40% 98%;
95
+ /* #F8FAFC */
96
+ --border: 217.2 32.6% 17.5%;
97
+ /* #1E293B */
98
+ --input: 217.2 32.6% 17.5%;
99
+ /* #1E293B */
100
+ --ring: 217 91% 60%;
101
+ /* #3B9FFF */
102
+ --success: 142.1 70.6% 45.3%;
103
+ /* #10B981 */
104
+ --warning: 38 92% 50%;
105
+ /* #F59E0B */
106
+ --info: 217 91% 60%;
107
+ /* #3B9FFF */
108
+ }
109
+
110
+ * {
111
+ @apply border-border;
112
+ }
113
+
114
+ body {
115
+ @apply bg-background text-foreground;
116
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
117
+ font-feature-settings: 'rlig' 1, 'calt' 1, 'ss01' 1, 'ss02' 1;
118
+ -webkit-font-smoothing: antialiased;
119
+ -moz-osx-font-smoothing: grayscale;
120
+ }
121
+
122
+ code,
123
+ pre,
124
+ .font-mono {
125
+ font-family: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
126
+ }
127
+
128
+ h1,
129
+ h2,
130
+ h3,
131
+ h4,
132
+ h5,
133
+ h6 {
134
+ @apply font-semibold tracking-tight;
135
+ }
136
+
137
+ h1 {
138
+ @apply text-4xl font-bold;
139
+ }
140
+
141
+ h2 {
142
+ @apply text-3xl font-bold;
143
+ }
144
+
145
+ h3 {
146
+ @apply text-2xl font-semibold;
147
+ }
148
+ }
149
+
150
+ /* Professional Scrollbar */
151
+ ::-webkit-scrollbar {
152
+ width: 10px;
153
+ height: 10px;
154
+ }
155
+
156
+ ::-webkit-scrollbar-track {
157
+ background: hsl(var(--muted) / 0.3);
158
+ border-radius: 10px;
159
+ }
160
+
161
+ ::-webkit-scrollbar-thumb {
162
+ background: hsl(var(--primary) / 0.4);
163
+ border-radius: 10px;
164
+ border: 2px solid transparent;
165
+ background-clip: content-box;
166
+ }
167
+
168
+ ::-webkit-scrollbar-thumb:hover {
169
+ background: hsl(var(--primary) / 0.6);
170
+ background-clip: content-box;
171
+ }
172
+
173
+ /* Smooth Transitions */
174
+ * {
175
+ transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
176
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
177
+ transition-duration: 150ms;
178
+ }
179
+
180
+ /* Professional Card Styles */
181
+ @layer components {
182
+ .card {
183
+ @apply rounded-xl border bg-card text-card-foreground;
184
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
185
+ }
186
+
187
+ .card-hover {
188
+ @apply card hover:shadow-lg hover:border-primary/50;
189
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
190
+ }
191
+
192
+ .card-hover:hover {
193
+ transform: translateY(-2px);
194
+ }
195
+
196
+ .card-interactive {
197
+ @apply card-hover cursor-pointer;
198
+ }
199
+
200
+ /* Professional Button System */
201
+ .btn {
202
+ @apply inline-flex items-center justify-center gap-2 rounded-lg text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
203
+ padding: 0.625rem 1.25rem;
204
+ }
205
+
206
+ .btn-primary {
207
+ background: hsl(var(--primary));
208
+ color: hsl(var(--primary-foreground));
209
+ @apply shadow-sm hover:opacity-90;
210
+ }
211
+
212
+ .btn-secondary {
213
+ background: hsl(var(--secondary));
214
+ color: hsl(var(--secondary-foreground));
215
+ @apply border hover:opacity-80;
216
+ }
217
+
218
+ .btn-ghost {
219
+ background: transparent;
220
+ @apply hover:bg-accent;
221
+ }
222
+
223
+ .btn-ghost:hover {
224
+ color: hsl(var(--accent-foreground));
225
+ }
226
+
227
+ .btn-outline {
228
+ background: hsl(var(--background));
229
+ @apply border border-input hover:bg-accent;
230
+ }
231
+
232
+ .btn-outline:hover {
233
+ color: hsl(var(--accent-foreground));
234
+ }
235
+
236
+ .btn-sm {
237
+ @apply text-xs;
238
+ padding: 0.5rem 0.875rem;
239
+ }
240
+
241
+ .btn-lg {
242
+ @apply text-base;
243
+ padding: 0.75rem 1.5rem;
244
+ }
245
+
246
+ /* Professional Badge System */
247
+ .badge {
248
+ @apply inline-flex items-center rounded-md px-2.5 py-0.5 text-xs font-semibold transition-colors;
249
+ }
250
+
251
+ .badge-primary {
252
+ @apply bg-primary/10 text-primary ring-1 ring-inset ring-primary/20;
253
+ }
254
+
255
+ .badge-secondary {
256
+ background: hsl(var(--secondary));
257
+ color: hsl(var(--secondary-foreground));
258
+ }
259
+
260
+ .badge-success {
261
+ @apply bg-emerald-50 text-emerald-700 ring-1 ring-inset ring-emerald-600/20;
262
+ }
263
+
264
+ .dark .badge-success {
265
+ @apply bg-emerald-500/10 text-emerald-400 ring-emerald-500/20;
266
+ }
267
+
268
+ .badge-warning {
269
+ @apply bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-600/20;
270
+ }
271
+
272
+ .dark .badge-warning {
273
+ @apply bg-amber-500/10 text-amber-400 ring-amber-500/20;
274
+ }
275
+
276
+ .badge-error {
277
+ @apply bg-rose-50 text-rose-700 ring-1 ring-inset ring-rose-600/20;
278
+ }
279
+
280
+ .dark .badge-error {
281
+ @apply bg-rose-500/10 text-rose-400 ring-rose-500/20;
282
+ }
283
+
284
+ /* Professional Input System */
285
+ .input {
286
+ @apply flex h-10 w-full rounded-lg border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50;
287
+ }
288
+
289
+ .textarea {
290
+ @apply input min-h-[80px] resize-y;
291
+ }
292
+
293
+ /* Professional Status Indicators */
294
+ .status-dot {
295
+ @apply relative flex h-3 w-3 rounded-full;
296
+ }
297
+
298
+ .status-dot::before {
299
+ content: '';
300
+ @apply absolute inset-0 rounded-full animate-ping opacity-75;
301
+ }
302
+
303
+ .status-connected {
304
+ @apply bg-emerald-500;
305
+ }
306
+
307
+ .status-connected::before {
308
+ @apply bg-emerald-500;
309
+ }
310
+
311
+ .status-connecting {
312
+ @apply bg-amber-500;
313
+ }
314
+
315
+ .status-connecting::before {
316
+ @apply bg-amber-500;
317
+ }
318
+
319
+ .status-disconnected {
320
+ @apply bg-rose-500;
321
+ }
322
+
323
+ .status-disconnected::before {
324
+ @apply bg-rose-500;
325
+ animation: none;
326
+ }
327
+
328
+ /* Professional Loading States */
329
+ .skeleton {
330
+ @apply animate-pulse rounded-md bg-muted;
331
+ }
332
+
333
+ .shimmer {
334
+ background: linear-gradient(90deg,
335
+ hsl(var(--muted)) 0%,
336
+ hsl(var(--muted) / 0.5) 50%,
337
+ hsl(var(--muted)) 100%);
338
+ background-size: 200% 100%;
339
+ animation: shimmer 1.5s ease-in-out infinite;
340
+ }
341
+
342
+ @keyframes shimmer {
343
+ 0% {
344
+ background-position: -200% 0;
345
+ }
346
+
347
+ 100% {
348
+ background-position: 200% 0;
349
+ }
350
+ }
351
+ }
352
+
353
+ /* Professional Animations */
354
+ @keyframes fadeIn {
355
+ from {
356
+ opacity: 0;
357
+ transform: translateY(8px);
358
+ }
359
+
360
+ to {
361
+ opacity: 1;
362
+ transform: translateY(0);
363
+ }
364
+ }
365
+
366
+ @keyframes slideIn {
367
+ from {
368
+ transform: translateX(-16px);
369
+ opacity: 0;
370
+ }
371
+
372
+ to {
373
+ transform: translateX(0);
374
+ opacity: 1;
375
+ }
376
+ }
377
+
378
+ @keyframes scaleIn {
379
+ from {
380
+ opacity: 0;
381
+ transform: scale(0.95);
382
+ }
383
+
384
+ to {
385
+ opacity: 1;
386
+ transform: scale(1);
387
+ }
388
+ }
389
+
390
+ .animate-fade-in {
391
+ animation: fadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
392
+ }
393
+
394
+ .animate-slide-in {
395
+ animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
396
+ }
397
+
398
+ .animate-scale-in {
399
+ animation: scaleIn 0.2s cubic-bezier(0.4, 0, 0.2, 1);
400
+ }
401
+
402
+ /* Nitrocloud Brand Gradient */
403
+ .nitrocloud-gradient {
404
+ background: linear-gradient(135deg,
405
+ hsl(var(--nitrocloud-gradient-start)),
406
+ hsl(var(--nitrocloud-gradient-end)));
407
+ }
408
+
409
+ .nitrocloud-text-gradient {
410
+ background: linear-gradient(135deg,
411
+ hsl(var(--nitrocloud-gradient-start)),
412
+ hsl(var(--nitrocloud-gradient-end)));
413
+ -webkit-background-clip: text;
414
+ -webkit-text-fill-color: transparent;
415
+ background-clip: text;
416
+ }
417
+
418
+ /* Nitrocloud Blue Glow Effects */
419
+ .nitrocloud-glow {
420
+ box-shadow: 0 0 24px rgba(59, 159, 255, 0.15),
421
+ 0 0 12px rgba(59, 159, 255, 0.1);
422
+ }
423
+
424
+ .dark .nitrocloud-glow {
425
+ box-shadow: 0 0 32px rgba(59, 159, 255, 0.25),
426
+ 0 0 16px rgba(59, 159, 255, 0.15);
427
+ }
428
+
429
+ /* Glassmorphism Effect */
430
+ .glass {
431
+ @apply backdrop-blur-xl bg-background/80 border;
432
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
433
+ }
434
+
435
+ .dark .glass {
436
+ @apply bg-card/60;
437
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
438
+ }
439
+
440
+ /* Professional Hover Effects */
441
+ .hover-lift {
442
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
443
+ box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
444
+ }
445
+
446
+ .hover-lift:hover {
447
+ transform: translateY(-4px);
448
+ box-shadow: 0 12px 24px -4px rgba(0, 0, 0, 0.12),
449
+ 0 8px 16px -4px rgba(0, 0, 0, 0.08);
450
+ }
451
+
452
+ /* Focus Visible Styles */
453
+ .focus-ring {
454
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background;
455
+ }
456
+
457
+ /* Prose/Content Styles */
458
+ .prose {
459
+ @apply text-foreground;
460
+ }
461
+
462
+ .prose :where(code):not(:where([class~="not-prose"] *)) {
463
+ @apply relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm;
464
+ }
465
+
466
+ .prose :where(code):not(:where([class~="not-prose"] *))::before,
467
+ .prose :where(code):not(:where([class~="not-prose"] *))::after {
468
+ content: '';
469
+ }
470
+
471
+ /* Empty State */
472
+ .empty-state {
473
+ @apply flex flex-col items-center justify-center py-12 text-center;
474
+ }
475
+
476
+ .empty-state-icon {
477
+ @apply w-16 h-16 text-muted-foreground/50 mb-4;
478
+ }
479
+
480
+ .empty-state-title {
481
+ @apply text-lg font-semibold text-foreground mb-2;
482
+ }
483
+
484
+ .empty-state-description {
485
+ @apply text-sm text-muted-foreground max-w-md;
486
+ }
@@ -0,0 +1,179 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { api } from '@/lib/api';
5
+ import { Activity, RefreshCw, CheckCircle2, AlertTriangle, XCircle, HelpCircle } from 'lucide-react';
6
+
7
+ interface HealthCheck {
8
+ name: string;
9
+ status: 'up' | 'down' | 'degraded';
10
+ message?: string;
11
+ details?: any;
12
+ timestamp?: number;
13
+ }
14
+
15
+ export default function HealthPage() {
16
+ const [healthChecks, setHealthChecks] = useState<HealthCheck[]>([]);
17
+ const [loading, setLoading] = useState(false);
18
+
19
+ useEffect(() => {
20
+ loadHealth();
21
+ const interval = setInterval(loadHealth, 30000); // Refresh every 30s
22
+ return () => clearInterval(interval);
23
+ }, []);
24
+
25
+ const loadHealth = async () => {
26
+ setLoading(true);
27
+ try {
28
+ const data = await api.getHealth();
29
+ setHealthChecks(data.checks || []);
30
+ } catch (error) {
31
+ console.error('Failed to load health checks:', error);
32
+ } finally {
33
+ setLoading(false);
34
+ }
35
+ };
36
+
37
+ const overallStatus =
38
+ healthChecks.length === 0
39
+ ? 'unknown'
40
+ : healthChecks.every((c) => c.status === 'up')
41
+ ? 'healthy'
42
+ : healthChecks.some((c) => c.status === 'down')
43
+ ? 'unhealthy'
44
+ : 'degraded';
45
+
46
+ const getStatusIcon = (status: string) => {
47
+ switch (status) {
48
+ case 'up':
49
+ case 'healthy':
50
+ return <CheckCircle2 className="w-12 h-12 text-emerald-500" />;
51
+ case 'degraded':
52
+ return <AlertTriangle className="w-12 h-12 text-amber-500" />;
53
+ case 'down':
54
+ case 'unhealthy':
55
+ return <XCircle className="w-12 h-12 text-rose-500" />;
56
+ default:
57
+ return <HelpCircle className="w-12 h-12 text-muted-foreground" />;
58
+ }
59
+ };
60
+
61
+ return (
62
+ <div className="fixed inset-0 flex flex-col bg-background" style={{ left: 'var(--sidebar-width, 15rem)' }}>
63
+ {/* Sticky Header */}
64
+ <div className="sticky top-0 z-10 border-b border-border/50 px-6 py-3 flex items-center justify-between bg-card/80 backdrop-blur-md shadow-sm">
65
+ <div className="flex items-center gap-3">
66
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-500 flex items-center justify-center shadow-md">
67
+ <Activity className="w-5 h-5 text-white" strokeWidth={2.5} />
68
+ </div>
69
+ <div>
70
+ <h1 className="text-lg font-bold text-foreground">Health</h1>
71
+ </div>
72
+ </div>
73
+ <button onClick={loadHealth} className="btn btn-primary text-sm px-4 py-2 gap-2" disabled={loading}>
74
+ <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
75
+ {loading ? 'Checking...' : 'Refresh'}
76
+ </button>
77
+ </div>
78
+
79
+ {/* Content - ONLY this scrolls */}
80
+ <div className="flex-1 overflow-y-auto overflow-x-hidden">
81
+ <div className="max-w-4xl mx-auto px-6 py-6">
82
+ {/* Overall Status */}
83
+ <div className="card p-8 bg-gradient-to-br from-card to-muted/20 mb-6">
84
+ <div className="flex items-center gap-6">
85
+ {getStatusIcon(overallStatus)}
86
+ <div>
87
+ <h2 className="text-3xl font-bold capitalize text-foreground">{overallStatus}</h2>
88
+ <p className="text-muted-foreground mt-1">
89
+ {healthChecks.length} health check{healthChecks.length !== 1 ? 's' : ''} configured
90
+ </p>
91
+ </div>
92
+ </div>
93
+ </div>
94
+
95
+ {/* Health Checks Grid */}
96
+ {loading && healthChecks.length === 0 ? (
97
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
98
+ {[1, 2].map((i) => (
99
+ <div key={i} className="card skeleton h-48"></div>
100
+ ))}
101
+ </div>
102
+ ) : healthChecks.length === 0 ? (
103
+ <div className="empty-state">
104
+ <Activity className="empty-state-icon" />
105
+ <p className="empty-state-title">No health checks configured</p>
106
+ <p className="empty-state-description">
107
+ Add health checks using the @HealthCheck decorator
108
+ </p>
109
+ </div>
110
+ ) : (
111
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
112
+ {healthChecks.map((check) => (
113
+ <div key={check.name} className="card card-hover p-6 animate-fade-in">
114
+ <div className="flex items-start justify-between mb-4">
115
+ <div className="flex items-center gap-3">
116
+ <div className={`w-12 h-12 rounded-lg flex items-center justify-center ${
117
+ check.status === 'up'
118
+ ? 'bg-emerald-500/10'
119
+ : check.status === 'degraded'
120
+ ? 'bg-amber-500/10'
121
+ : 'bg-rose-500/10'
122
+ }`}>
123
+ {check.status === 'up' ? (
124
+ <CheckCircle2 className="w-6 h-6 text-emerald-500" />
125
+ ) : check.status === 'degraded' ? (
126
+ <AlertTriangle className="w-6 h-6 text-amber-500" />
127
+ ) : (
128
+ <XCircle className="w-6 h-6 text-rose-500" />
129
+ )}
130
+ </div>
131
+ <div>
132
+ <h3 className="font-semibold capitalize text-foreground">{check.name}</h3>
133
+ <span
134
+ className={`badge text-xs mt-1 ${
135
+ check.status === 'up'
136
+ ? 'badge-success'
137
+ : check.status === 'degraded'
138
+ ? 'badge-warning'
139
+ : 'badge-error'
140
+ }`}
141
+ >
142
+ {check.status}
143
+ </span>
144
+ </div>
145
+ </div>
146
+ </div>
147
+
148
+ {check.message && (
149
+ <p className="text-sm text-muted-foreground mb-3">{check.message}</p>
150
+ )}
151
+
152
+ {check.details && (
153
+ <div className="mt-3 p-4 bg-muted/30 rounded-lg border border-border">
154
+ <p className="text-xs font-semibold text-muted-foreground mb-3 uppercase tracking-wide">Details</p>
155
+ <div className="space-y-2">
156
+ {Object.entries(check.details).map(([key, value]) => (
157
+ <div key={key} className="flex justify-between text-sm">
158
+ <span className="text-muted-foreground capitalize">{key}:</span>
159
+ <span className="text-foreground font-mono font-medium">{String(value)}</span>
160
+ </div>
161
+ ))}
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ {check.timestamp && (
167
+ <p className="text-xs text-muted-foreground mt-3">
168
+ Last check: {new Date(check.timestamp).toLocaleString()}
169
+ </p>
170
+ )}
171
+ </div>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+ </div>
177
+ </div>
178
+ );
179
+ }