@odvi/create-dtt-framework 0.1.3 → 0.1.6

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 (111) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +16 -13
  3. package/dist/commands/create.js.map +1 -1
  4. package/package.json +3 -2
  5. package/template/.env.example +106 -0
  6. package/template/components.json +22 -0
  7. package/template/docs/framework/01-overview.md +289 -0
  8. package/template/docs/framework/02-techstack.md +503 -0
  9. package/template/docs/framework/api-layer.md +681 -0
  10. package/template/docs/framework/clerk-authentication.md +649 -0
  11. package/template/docs/framework/cli-installation.md +564 -0
  12. package/template/docs/framework/deployment/ci-cd.md +907 -0
  13. package/template/docs/framework/deployment/digitalocean.md +991 -0
  14. package/template/docs/framework/deployment/domain-setup.md +972 -0
  15. package/template/docs/framework/deployment/environment-variables.md +862 -0
  16. package/template/docs/framework/deployment/monitoring.md +927 -0
  17. package/template/docs/framework/deployment/production-checklist.md +649 -0
  18. package/template/docs/framework/deployment/vercel.md +791 -0
  19. package/template/docs/framework/environment-variables.md +646 -0
  20. package/template/docs/framework/health-check-system.md +583 -0
  21. package/template/docs/framework/implementation.md +559 -0
  22. package/template/docs/framework/snowflake-integration.md +594 -0
  23. package/template/docs/framework/state-management.md +615 -0
  24. package/template/docs/framework/supabase-integration.md +582 -0
  25. package/template/docs/framework/testing-guide.md +544 -0
  26. package/template/docs/framework/what-did-i-miss.md +526 -0
  27. package/template/drizzle.config.ts +11 -0
  28. package/template/next.config.js +21 -0
  29. package/template/postcss.config.js +5 -0
  30. package/template/prettier.config.js +4 -0
  31. package/template/public/favicon.ico +0 -0
  32. package/template/src/app/(auth)/layout.tsx +4 -0
  33. package/template/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +10 -0
  34. package/template/src/app/(auth)/sign-up/[[...sign-up]]/page.tsx +10 -0
  35. package/template/src/app/(dashboard)/dashboard/page.tsx +8 -0
  36. package/template/src/app/(dashboard)/health/page.tsx +16 -0
  37. package/template/src/app/(dashboard)/layout.tsx +17 -0
  38. package/template/src/app/api/[[...route]]/route.ts +11 -0
  39. package/template/src/app/api/debug-files/route.ts +33 -0
  40. package/template/src/app/api/webhooks/clerk/route.ts +112 -0
  41. package/template/src/app/layout.tsx +28 -0
  42. package/template/src/app/page.tsx +12 -0
  43. package/template/src/app/providers.tsx +20 -0
  44. package/template/src/components/layouts/navbar.tsx +14 -0
  45. package/template/src/components/shared/loading-spinner.tsx +6 -0
  46. package/template/src/components/ui/badge.tsx +46 -0
  47. package/template/src/components/ui/button.tsx +62 -0
  48. package/template/src/components/ui/card.tsx +92 -0
  49. package/template/src/components/ui/collapsible.tsx +33 -0
  50. package/template/src/components/ui/scroll-area.tsx +58 -0
  51. package/template/src/components/ui/sheet.tsx +139 -0
  52. package/template/src/config/__tests__/env.test.ts +164 -0
  53. package/template/src/config/__tests__/site.test.ts +46 -0
  54. package/template/src/config/env.ts +36 -0
  55. package/template/src/config/site.ts +10 -0
  56. package/template/src/env.js +44 -0
  57. package/template/src/features/__tests__/health-check-config.test.ts +142 -0
  58. package/template/src/features/__tests__/health-check-types.test.ts +201 -0
  59. package/template/src/features/documentation/components/doc-sidebar.tsx +109 -0
  60. package/template/src/features/documentation/components/doc-viewer.tsx +70 -0
  61. package/template/src/features/documentation/index.tsx +92 -0
  62. package/template/src/features/documentation/utils/doc-loader.ts +177 -0
  63. package/template/src/features/health-check/components/health-dashboard.tsx +374 -0
  64. package/template/src/features/health-check/config.ts +71 -0
  65. package/template/src/features/health-check/index.ts +4 -0
  66. package/template/src/features/health-check/stores/health-store.ts +14 -0
  67. package/template/src/features/health-check/types.ts +18 -0
  68. package/template/src/hooks/__tests__/use-debounce.test.tsx +28 -0
  69. package/template/src/hooks/queries/use-health-checks.ts +16 -0
  70. package/template/src/hooks/utils/use-debounce.ts +20 -0
  71. package/template/src/lib/__tests__/utils.test.ts +52 -0
  72. package/template/src/lib/__tests__/validators.test.ts +114 -0
  73. package/template/src/lib/nextbank/client.ts +67 -0
  74. package/template/src/lib/snowflake/client.ts +102 -0
  75. package/template/src/lib/supabase/admin.ts +7 -0
  76. package/template/src/lib/supabase/client.ts +7 -0
  77. package/template/src/lib/supabase/server.ts +23 -0
  78. package/template/src/lib/utils.ts +6 -0
  79. package/template/src/lib/validators.ts +9 -0
  80. package/template/src/middleware.ts +22 -0
  81. package/template/src/server/api/index.ts +22 -0
  82. package/template/src/server/api/middleware/auth.ts +19 -0
  83. package/template/src/server/api/middleware/logger.ts +4 -0
  84. package/template/src/server/api/routes/health/clerk.ts +214 -0
  85. package/template/src/server/api/routes/health/database.ts +141 -0
  86. package/template/src/server/api/routes/health/edge-functions.ts +107 -0
  87. package/template/src/server/api/routes/health/framework.ts +48 -0
  88. package/template/src/server/api/routes/health/index.ts +102 -0
  89. package/template/src/server/api/routes/health/nextbank.ts +46 -0
  90. package/template/src/server/api/routes/health/snowflake.ts +83 -0
  91. package/template/src/server/api/routes/health/storage.ts +177 -0
  92. package/template/src/server/api/routes/users.ts +79 -0
  93. package/template/src/server/db/index.ts +17 -0
  94. package/template/src/server/db/queries/users.ts +8 -0
  95. package/template/src/server/db/schema/__tests__/health-checks.test.ts +31 -0
  96. package/template/src/server/db/schema/__tests__/users.test.ts +46 -0
  97. package/template/src/server/db/schema/health-checks.ts +11 -0
  98. package/template/src/server/db/schema/index.ts +2 -0
  99. package/template/src/server/db/schema/users.ts +16 -0
  100. package/template/src/server/db/schema.ts +1 -0
  101. package/template/src/stores/__tests__/ui-store.test.ts +87 -0
  102. package/template/src/stores/ui-store.ts +14 -0
  103. package/template/src/styles/globals.css +129 -0
  104. package/template/src/test/mocks/clerk.ts +35 -0
  105. package/template/src/test/mocks/snowflake.ts +28 -0
  106. package/template/src/test/mocks/supabase.ts +37 -0
  107. package/template/src/test/setup.ts +69 -0
  108. package/template/src/test/utils/test-helpers.ts +158 -0
  109. package/template/src/types/index.ts +14 -0
  110. package/template/tsconfig.json +43 -0
  111. package/template/vitest.config.ts +44 -0
@@ -0,0 +1,374 @@
1
+ 'use client'
2
+
3
+ import { SERVICES } from '@/features/health-check/config'
4
+ import { LoadingSpinner } from '@/components/shared/loading-spinner'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Card } from '@/components/ui/card'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
9
+ import { CheckCircle, XCircle, AlertCircle, Clock, Shield, Play, RefreshCw, ChevronDown } from 'lucide-react'
10
+ import { useState } from 'react'
11
+ import type { HealthStatus } from '@/features/health-check/types'
12
+ import { useHealthStore } from '../stores/health-store'
13
+ import { useQueryClient } from '@tanstack/react-query'
14
+ import { useAuth } from '@clerk/nextjs'
15
+
16
+ const statusIcons: Record<HealthStatus, React.ReactNode> = {
17
+ healthy: <CheckCircle className="h-5 w-5 text-green-500" />,
18
+ unhealthy: <XCircle className="h-5 w-5 text-red-500" />,
19
+ error: <XCircle className="h-5 w-5 text-red-500" />,
20
+ pending: <Clock className="h-5 w-5 text-yellow-500" />,
21
+ unconfigured: <Shield className="h-5 w-5 text-gray-400" />,
22
+ }
23
+
24
+ const statusColors: Record<HealthStatus, string> = {
25
+ healthy: 'bg-green-100 text-green-800 border-green-200',
26
+ unhealthy: 'bg-red-100 text-red-800 border-red-200',
27
+ error: 'bg-red-100 text-red-800 border-red-200',
28
+ pending: 'bg-yellow-100 text-yellow-800 border-yellow-200',
29
+ unconfigured: 'bg-gray-100 text-gray-800 border-gray-200',
30
+ }
31
+
32
+ interface IndividualCheckResult {
33
+ name: string
34
+ status: HealthStatus
35
+ responseTimeMs?: number
36
+ error?: string
37
+ httpStatus?: number
38
+ timestamp?: string
39
+ data?: any
40
+ }
41
+
42
+ export function HealthDashboard() {
43
+ const [individualChecks, setIndividualChecks] = useState<Record<string, IndividualCheckResult>>({})
44
+ const [loadingChecks, setLoadingChecks] = useState<Set<string>>(new Set())
45
+ const healthStore = useHealthStore()
46
+ const queryClient = useQueryClient()
47
+ const { getToken } = useAuth()
48
+
49
+ const totalHealthChecks = SERVICES.reduce((total, service) => total + service.checks.length, 0)
50
+
51
+ const runIndividualCheck = async (serviceName: string, checkName: string, endpoint: string) => {
52
+ const checkKey = `${serviceName}-${checkName}`
53
+ setLoadingChecks((prev) => new Set(prev).add(checkKey))
54
+
55
+ try {
56
+ let result: { status?: string; responseTimeMs?: number; error?: string; data?: any }
57
+ const start = performance.now()
58
+
59
+ if (endpoint.startsWith('client:')) {
60
+ // Handle client-side checks
61
+ await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate async work
62
+
63
+ if (endpoint === 'client:zustand') {
64
+ healthStore.increment()
65
+ const currentCount = useHealthStore.getState().counter
66
+ result = {
67
+ status: 'healthy',
68
+ responseTimeMs: Math.round(performance.now() - start),
69
+ data: {
70
+ message: 'Zustand store updated successfully',
71
+ counter: currentCount
72
+ }
73
+ }
74
+ } else if (endpoint === 'client:query') {
75
+ // Simulate a query check
76
+ const queryState = queryClient.getQueryCache().getAll()
77
+ result = {
78
+ status: 'healthy',
79
+ responseTimeMs: Math.round(performance.now() - start),
80
+ data: {
81
+ message: 'Query Client is active',
82
+ queryCount: queryState.length,
83
+ isFetching: queryClient.isFetching()
84
+ }
85
+ }
86
+ } else {
87
+ throw new Error(`Unknown client endpoint: ${endpoint}`)
88
+ }
89
+ } else {
90
+ // Get auth token if available
91
+ const token = await getToken()
92
+
93
+ // Handle server-side checks
94
+ const response = await fetch(`/api/health${endpoint}`, {
95
+ method: ['/database/write', '/database/delete', '/storage/upload', '/storage/delete'].includes(endpoint) ? 'POST' : 'GET',
96
+ headers: {
97
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
98
+ },
99
+ ...(endpoint === '/database/delete' || endpoint === '/storage/delete' ? { method: 'DELETE' } : {})
100
+ })
101
+ // Check content type to ensure JSON
102
+ const contentType = response.headers.get('content-type')
103
+ if (!contentType || !contentType.includes('application/json')) {
104
+ const text = await response.text()
105
+ throw new Error(`Invalid response from server: ${text.substring(0, 100)}...`)
106
+ }
107
+ result = await response.json()
108
+ }
109
+
110
+ setIndividualChecks((prev) => ({
111
+ ...prev,
112
+ [checkKey]: {
113
+ name: checkName,
114
+ status: (result.status ?? ('healthy')) as HealthStatus,
115
+ responseTimeMs: result.responseTimeMs,
116
+ error: result.error,
117
+ httpStatus: endpoint.startsWith('client:') ? 200 : undefined,
118
+ timestamp: new Date().toISOString(),
119
+ data: result.data,
120
+ },
121
+ }))
122
+ } catch (err) {
123
+ setIndividualChecks((prev) => ({
124
+ ...prev,
125
+ [checkKey]: {
126
+ name: checkName,
127
+ status: 'error',
128
+ error: err instanceof Error ? err.message : 'Check failed',
129
+ timestamp: new Date().toISOString(),
130
+ // Include responseTimeMs even on error for better UX
131
+ responseTimeMs: 0,
132
+ },
133
+ }))
134
+ } finally {
135
+ setLoadingChecks((prev) => {
136
+ const next = new Set(prev)
137
+ next.delete(checkKey)
138
+ return next
139
+ })
140
+ }
141
+ }
142
+
143
+ const runServiceChecks = async (serviceName: string) => {
144
+ const service = SERVICES.find((s) => s.name === serviceName)
145
+ if (!service) return
146
+
147
+ for (const check of service.checks) {
148
+ await runIndividualCheck(serviceName, check.name, check.endpoint)
149
+ }
150
+ }
151
+
152
+ const runAllChecks = async () => {
153
+ // Run all checks in parallel
154
+ const promises = SERVICES.flatMap((service) =>
155
+ service.checks.map((check) => runIndividualCheck(service.name, check.name, check.endpoint))
156
+ )
157
+ await Promise.all(promises)
158
+ }
159
+
160
+ // Calculate derived status from individual checks
161
+ const getServiceStatus = (serviceName: string): HealthStatus => {
162
+ const service = SERVICES.find((s) => s.name === serviceName)
163
+ if (!service) return 'pending'
164
+
165
+ const checks = service.checks
166
+ .map((check) => {
167
+ const checkKey = `${serviceName}-${check.name}`
168
+ return individualChecks[checkKey]
169
+ })
170
+ .filter((c): c is IndividualCheckResult => !!c)
171
+
172
+ if (checks.length === 0) return 'pending'
173
+
174
+ if (checks.some((c) => c.status === 'error' || (c.httpStatus && c.httpStatus >= 400))) return 'error'
175
+ if (checks.some((c) => c.status === 'unconfigured')) return 'unconfigured'
176
+ if (checks.length < service.checks.length) return 'pending'
177
+
178
+ return 'healthy'
179
+ }
180
+
181
+ const overallStatus = (() => {
182
+ const statuses = SERVICES.map((s) => getServiceStatus(s.name))
183
+ if (statuses.some((s) => s === 'error')) return 'error'
184
+ if (statuses.some((s) => s === 'pending')) return 'pending'
185
+ if (statuses.some((s) => s === 'unconfigured')) return 'unconfigured'
186
+ return 'healthy'
187
+ })()
188
+
189
+ const lastUpdated = Object.values(individualChecks)
190
+ .map((c) => c.timestamp)
191
+ .filter(Boolean)
192
+ .sort()
193
+ .pop()
194
+
195
+ return (
196
+ <div className="space-y-6">
197
+ {/* Overall Status */}
198
+ <Card className="p-6">
199
+ <Collapsible defaultOpen>
200
+ <CollapsibleTrigger className="w-full group">
201
+ <div className="flex items-center justify-between">
202
+ <div className="flex items-center gap-4">
203
+ <div className="text-3xl">{statusIcons[overallStatus]}</div>
204
+ <div className="text-left">
205
+ <h2 className="text-xl font-semibold">Overall System Status</h2>
206
+ </div>
207
+ </div>
208
+ <div className="flex items-center gap-4">
209
+ <Badge className={statusColors[overallStatus]} variant="outline">
210
+ {overallStatus.toUpperCase()}
211
+ </Badge>
212
+ <ChevronDown className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180" />
213
+ </div>
214
+ </div>
215
+ </CollapsibleTrigger>
216
+ <CollapsibleContent>
217
+ <div className="mt-4 flex gap-2">
218
+ <Button onClick={runAllChecks} size="sm" className="gap-2">
219
+ <RefreshCw className="h-4 w-4" />
220
+ Run All Checks ({totalHealthChecks})
221
+ </Button>
222
+ {lastUpdated && (
223
+ <p className="text-xs text-muted-foreground self-center ml-auto">
224
+ Last updated: {new Date(lastUpdated).toLocaleString()}
225
+ </p>
226
+ )}
227
+ </div>
228
+ </CollapsibleContent>
229
+ </Collapsible>
230
+ </Card>
231
+
232
+ {/* Service Cards */}
233
+ <div className="space-y-4">
234
+ {SERVICES.map((service) => {
235
+ const serviceStatus = getServiceStatus(service.name)
236
+ const isServiceLoading = Array.from(loadingChecks).some((key) => key.startsWith(`${service.name}-`))
237
+
238
+ return (
239
+ <Card key={service.name} className="p-4">
240
+ <Collapsible defaultOpen>
241
+ <CollapsibleTrigger className="w-full group">
242
+ <div className="flex items-center justify-between mb-4">
243
+ <div className="flex items-center gap-3">
244
+ <div className="text-xl">{statusIcons[serviceStatus]}</div>
245
+ <div className="text-left">
246
+ <h3 className="font-semibold">{service.name}</h3>
247
+ <p className="text-sm text-muted-foreground">
248
+ {service.checks.length} check{(service.checks.length as number) !== 1 ? 's' : ''}
249
+ </p>
250
+ </div>
251
+ <Button
252
+ size="sm"
253
+ variant="outline"
254
+ className="gap-2 ml-2"
255
+ disabled={isServiceLoading}
256
+ onClick={(e) => {
257
+ e.stopPropagation()
258
+ runServiceChecks(service.name)
259
+ }}
260
+ >
261
+ {isServiceLoading ? (
262
+ <LoadingSpinner className="h-3 w-3" />
263
+ ) : (
264
+ <Play className="h-3 w-3" />
265
+ )}
266
+ Run Checks
267
+ </Button>
268
+ </div>
269
+ <div className="flex items-center gap-4">
270
+ <Badge className={statusColors[serviceStatus]} variant="outline">
271
+ {serviceStatus.toUpperCase()}
272
+ </Badge>
273
+ <ChevronDown className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180" />
274
+ </div>
275
+ </div>
276
+ </CollapsibleTrigger>
277
+
278
+ <CollapsibleContent>
279
+ <div className="space-y-3">
280
+ {service.checks.map((check) => {
281
+ const checkKey = `${service.name}-${check.name}`
282
+ const individualResult = individualChecks[checkKey]
283
+ const isLoadingCheck = loadingChecks.has(checkKey)
284
+ const checkStatus: HealthStatus = individualResult?.status ?? 'pending'
285
+
286
+ return (
287
+ <div key={check.name} className="border rounded-lg p-3 bg-muted/30">
288
+ <Collapsible defaultOpen={!!individualResult}>
289
+ <div className="flex items-center justify-between mb-2">
290
+ <CollapsibleTrigger className="group flex items-center gap-2 hover:opacity-80 transition-opacity">
291
+ <ChevronDown className="h-4 w-4 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180" />
292
+ <div className="flex items-center gap-2 text-left">
293
+ <div className="text-lg">{statusIcons[checkStatus]}</div>
294
+ <div>
295
+ <p className="font-medium text-sm">{check.name}</p>
296
+ <p className="text-xs text-muted-foreground">{check.endpoint}</p>
297
+ </div>
298
+ </div>
299
+ </CollapsibleTrigger>
300
+ <Button
301
+ size="sm"
302
+ variant="outline"
303
+ onClick={() => runIndividualCheck(service.name, check.name, check.endpoint)}
304
+ disabled={isLoadingCheck}
305
+ className="gap-1"
306
+ >
307
+ {isLoadingCheck ? (
308
+ <>
309
+ <LoadingSpinner className="h-3 w-3" />
310
+ Running...
311
+ </>
312
+ ) : (
313
+ <>
314
+ <Play className="h-3 w-3" />
315
+ Run Check
316
+ </>
317
+ )}
318
+ </Button>
319
+ </div>
320
+
321
+ {/* Individual Check Result */}
322
+ <CollapsibleContent>
323
+ {individualResult && (
324
+ <div className="mt-2 p-3 bg-background rounded border">
325
+ <div className="flex items-center justify-between mb-2">
326
+ <div className="flex items-center gap-2">
327
+ {statusIcons[individualResult.status]}
328
+ <Badge className={statusColors[individualResult.status]} variant="outline">
329
+ {individualResult.status.toUpperCase()}
330
+ </Badge>
331
+ </div>
332
+ {individualResult.responseTimeMs && (
333
+ <span className="text-xs text-muted-foreground">
334
+ {individualResult.responseTimeMs}ms
335
+ </span>
336
+ )}
337
+ </div>
338
+ {individualResult.error && (
339
+ <p className="text-sm text-red-600 mt-1">{individualResult.error}</p>
340
+ )}
341
+ {individualResult.httpStatus && (
342
+ <p className="text-xs text-muted-foreground mt-1">
343
+ HTTP Status: {individualResult.httpStatus}
344
+ </p>
345
+ )}
346
+ {individualResult.timestamp && (
347
+ <p className="text-xs text-muted-foreground mt-1">
348
+ Checked at: {new Date(individualResult.timestamp).toLocaleString()}
349
+ </p>
350
+ )}
351
+ {individualResult.data && (
352
+ <div className="mt-2">
353
+ <pre className="p-2 rounded bg-muted text-xs overflow-auto max-h-[200px] whitespace-pre-wrap">
354
+ {JSON.stringify(individualResult.data, null, 2)}
355
+ </pre>
356
+ </div>
357
+ )}
358
+ </div>
359
+ )}
360
+ </CollapsibleContent>
361
+ </Collapsible>
362
+ </div>
363
+ )
364
+ })}
365
+ </div>
366
+ </CollapsibleContent>
367
+ </Collapsible>
368
+ </Card>
369
+ )
370
+ })}
371
+ </div>
372
+ </div>
373
+ )
374
+ }
@@ -0,0 +1,71 @@
1
+ // Health check service configuration placeholder
2
+ export const SERVICES = [
3
+ {
4
+ name: 'Clerk Authentication',
5
+ icon: 'key',
6
+ checks: [
7
+ { name: 'Environment Config', endpoint: '/clerk/env' },
8
+ { name: 'API Connectivity', endpoint: '/clerk/api-status' },
9
+ { name: 'Get Current User', endpoint: '/clerk/user' },
10
+ { name: 'Get Org Membership', endpoint: '/clerk/org' },
11
+ { name: 'List Org Members', endpoint: '/clerk/members' },
12
+ ],
13
+ },
14
+ {
15
+ name: 'Supabase Database',
16
+ icon: 'database',
17
+ checks: [
18
+ { name: 'Write Test Row', endpoint: '/database/write' },
19
+ { name: 'Read Test Row', endpoint: '/database/read' },
20
+ { name: 'Delete Test Row', endpoint: '/database/delete' },
21
+ ],
22
+ },
23
+ {
24
+ name: 'Supabase Storage',
25
+ icon: 'folder',
26
+ checks: [
27
+ { name: 'Upload Test File', endpoint: '/storage/upload' },
28
+ { name: 'Download Test File', endpoint: '/storage/download' },
29
+ { name: 'Delete Test File', endpoint: '/storage/delete' },
30
+ ],
31
+ },
32
+ {
33
+ name: 'Supabase Edge Functions',
34
+ icon: 'zap',
35
+ checks: [
36
+ { name: 'Ping Edge Function', endpoint: '/edge/ping' },
37
+ { name: 'Test Auth Header', endpoint: '/edge/auth' },
38
+ ],
39
+ },
40
+ {
41
+ name: 'Snowflake',
42
+ icon: 'snowflake',
43
+ checks: [
44
+ { name: 'Test Connection', endpoint: '/snowflake/connect' },
45
+ { name: 'Execute Query', endpoint: '/snowflake/query' },
46
+ ],
47
+ },
48
+ {
49
+ name: 'NextBank',
50
+ icon: 'building',
51
+ checks: [
52
+ { name: 'Ping API', endpoint: '/nextbank/ping' },
53
+ ],
54
+ },
55
+ {
56
+ name: 'Framework',
57
+ icon: 'code',
58
+ checks: [
59
+ { name: 'Hono API', endpoint: '/framework/hono' },
60
+ { name: 'Drizzle ORM', endpoint: '/framework/drizzle' },
61
+ ],
62
+ },
63
+ {
64
+ name: 'Client State',
65
+ icon: 'cpu',
66
+ checks: [
67
+ { name: 'Zustand Store', endpoint: 'client:zustand' },
68
+ { name: 'TanStack Query', endpoint: 'client:query' },
69
+ ],
70
+ },
71
+ ] as const
@@ -0,0 +1,4 @@
1
+ // Health check feature index placeholder
2
+ export { HealthDashboard } from './components/health-dashboard'
3
+ export { SERVICES } from './config'
4
+ export type { HealthStatus, ServiceCheck, ServiceHealth } from './types'
@@ -0,0 +1,14 @@
1
+ import { create } from 'zustand'
2
+
3
+ interface HealthStore {
4
+ counter: number
5
+ increment: () => void
6
+ reset: () => void
7
+ }
8
+
9
+ export const useHealthStore = create<HealthStore>((set) => ({
10
+ counter: 0,
11
+ increment: () => set((state) => ({ counter: state.counter + 1 })),
12
+ reset: () => set({ counter: 0 }),
13
+ }))
14
+
@@ -0,0 +1,18 @@
1
+ // Health check types placeholder
2
+ export type HealthStatus = 'healthy' | 'unhealthy' | 'error' | 'pending' | 'unconfigured'
3
+
4
+ export interface ServiceCheck {
5
+ name: string
6
+ endpoint: string
7
+ status: HealthStatus
8
+ responseTimeMs?: number
9
+ error?: string
10
+ }
11
+
12
+ export interface ServiceHealth {
13
+ name: string
14
+ icon: string
15
+ status: HealthStatus
16
+ responseTimeMs: number
17
+ checks: ServiceCheck[]
18
+ }
@@ -0,0 +1,28 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { renderHook } from '@testing-library/react';
3
+ import { useDebounce } from '../utils/use-debounce';
4
+
5
+ describe('useDebounce hook', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks();
12
+ });
13
+
14
+ it('should return initial value immediately', () => {
15
+ const { result } = renderHook(() => useDebounce('test', 500));
16
+ expect(result.current).toBe('test');
17
+ });
18
+
19
+ it('should cleanup timer on unmount', () => {
20
+ const clearTimeoutSpy = vi.spyOn(global, 'clearTimeout');
21
+
22
+ const { unmount } = renderHook(() => useDebounce('test', 500));
23
+
24
+ unmount();
25
+
26
+ expect(clearTimeoutSpy).toHaveBeenCalled();
27
+ });
28
+ });
@@ -0,0 +1,16 @@
1
+ // Health checks query hook placeholder
2
+ 'use client'
3
+
4
+ import { useQuery } from '@tanstack/react-query'
5
+
6
+ export function useHealthChecks() {
7
+ return useQuery({
8
+ queryKey: ['health-checks'],
9
+ queryFn: async () => {
10
+ const response = await fetch('/api/health/all')
11
+ if (!response.ok) throw new Error('Failed to fetch health checks')
12
+ return response.json()
13
+ },
14
+ enabled: false,
15
+ })
16
+ }
@@ -0,0 +1,20 @@
1
+ // Debounce hook placeholder
2
+ 'use client'
3
+
4
+ import { useEffect, useState } from 'react'
5
+
6
+ export function useDebounce<T>(value: T, delay: number = 500): T {
7
+ const [debouncedValue, setDebouncedValue] = useState<T>(value)
8
+
9
+ useEffect(() => {
10
+ const handler = setTimeout(() => {
11
+ setDebouncedValue(value)
12
+ }, delay)
13
+
14
+ return () => {
15
+ clearTimeout(handler)
16
+ }
17
+ }, [value, delay])
18
+
19
+ return debouncedValue
20
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { cn } from '../utils';
3
+
4
+ describe('cn utility function', () => {
5
+ it('should merge class names correctly', () => {
6
+ expect(cn('foo', 'bar')).toBe('foo bar');
7
+ });
8
+
9
+ it('should handle conditional classes', () => {
10
+ expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz');
11
+ });
12
+
13
+ it('should handle undefined and null values', () => {
14
+ expect(cn('foo', undefined, null, 'bar')).toBe('foo bar');
15
+ });
16
+
17
+ it('should handle empty strings', () => {
18
+ expect(cn('foo', '', 'bar')).toBe('foo bar');
19
+ });
20
+
21
+ it('should merge Tailwind classes correctly - later classes should override earlier ones', () => {
22
+ expect(cn('px-2 py-1', 'px-4')).toBe('py-1 px-4');
23
+ });
24
+
25
+ it('should handle arrays of classes', () => {
26
+ expect(cn(['foo', 'bar'], 'baz')).toBe('foo bar baz');
27
+ });
28
+
29
+ it('should handle objects with conditional classes', () => {
30
+ expect(cn({ foo: true, bar: false, baz: true })).toBe('foo baz');
31
+ });
32
+
33
+ it('should handle mixed input types', () => {
34
+ expect(cn('foo', { bar: true }, ['baz'], undefined)).toBe('foo bar baz');
35
+ });
36
+
37
+ it('should handle conflicting Tailwind utility classes', () => {
38
+ expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500');
39
+ });
40
+
41
+ it('should handle responsive classes', () => {
42
+ expect(cn('px-2', 'md:px-4', 'lg:px-6')).toBe('px-2 md:px-4 lg:px-6');
43
+ });
44
+
45
+ it('should return empty string when no valid classes provided', () => {
46
+ expect(cn('', false, null, undefined)).toBe('');
47
+ });
48
+
49
+ it('should handle numbers converted to strings', () => {
50
+ expect(cn('foo', 123 as any)).toBe('foo 123');
51
+ });
52
+ });