@odvi/create-dtt-framework 0.1.2 → 0.1.5

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