@hustle-together/api-dev-tools 3.12.3 → 4.5.1
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.
- package/.claude/adr-requests/.gitkeep +10 -0
- package/.claude/agents/adr-researcher.md +109 -0
- package/.claude/agents/visual-analyzer.md +183 -0
- package/.claude/api-dev-state.json +7 -463
- package/.claude/documentation-audit.json +114 -0
- package/.claude/registry.json +289 -0
- package/.claude/settings.json +45 -1
- package/.claude/workflow-logs/None.json +49 -0
- package/.claude/workflow-logs/session-20251230-143727.json +106 -0
- package/.skills/adr-deep-research/SKILL.md +351 -0
- package/.skills/api-create/SKILL.md +116 -17
- package/.skills/api-research/SKILL.md +130 -0
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/docs-update/SKILL.md +205 -0
- package/.skills/hustle-brand/SKILL.md +368 -0
- package/.skills/hustle-build/SKILL.md +786 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/.skills/parallel-spawn/SKILL.md +212 -0
- package/.skills/ralph-continue/SKILL.md +151 -0
- package/.skills/ralph-loop/SKILL.md +341 -0
- package/.skills/ralph-status/SKILL.md +87 -0
- package/.skills/refactor/SKILL.md +59 -0
- package/.skills/shadcn/SKILL.md +522 -0
- package/.skills/test-all/SKILL.md +210 -0
- package/.skills/test-builds/SKILL.md +208 -0
- package/.skills/test-debug/SKILL.md +212 -0
- package/.skills/test-e2e/SKILL.md +168 -0
- package/.skills/test-review/SKILL.md +707 -0
- package/.skills/test-unit/SKILL.md +143 -0
- package/.skills/test-visual/SKILL.md +301 -0
- package/.skills/token-report/SKILL.md +132 -0
- package/CHANGELOG.md +575 -0
- package/README.md +426 -56
- package/bin/cli.js +1538 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-build.md +259 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
- package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
- package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
- package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
- package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
- package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
- package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
- package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
- package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
- package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
- package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
- package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
- package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
- package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
- package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
- package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
- package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
- package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
- package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
- package/hooks/api-workflow-check.py +34 -0
- package/hooks/auto-answer.py +305 -0
- package/hooks/check-update.py +132 -0
- package/hooks/completion-promise-detector.py +293 -0
- package/hooks/context-capacity-warning.py +171 -0
- package/hooks/docs-update-check.py +120 -0
- package/hooks/enforce-dry-run.py +134 -0
- package/hooks/enforce-external-research.py +25 -0
- package/hooks/enforce-interview.py +20 -0
- package/hooks/generate-adr-options.py +282 -0
- package/hooks/hook_utils.py +609 -0
- package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/hooks/ntfy-on-question.py +240 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +267 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/parallel-orchestrator.py +451 -0
- package/hooks/periodic-reground.py +270 -67
- package/hooks/project-document-prompt.py +302 -0
- package/hooks/remote-question-proxy.py +284 -0
- package/hooks/remote-question-server.py +1224 -0
- package/hooks/run-code-review.py +176 -29
- package/hooks/run-visual-qa.py +338 -0
- package/hooks/session-logger.py +27 -1
- package/hooks/session-startup.py +113 -0
- package/hooks/update-adr-decision.py +236 -0
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-testing-checklist.py +195 -0
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/.skills/hustle-interview/SKILL.md +174 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
- package/templates/api-dev-state.json +33 -1
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/brand-page/page.tsx +645 -0
- package/templates/component/Component.visual.spec.ts +30 -24
- package/templates/docs/page.tsx +230 -0
- package/templates/eslint-plugin-zod-schema/index.js +446 -0
- package/templates/eslint-plugin-zod-schema/package.json +26 -0
- package/templates/github-workflows/security.yml +274 -0
- package/templates/hustle-build-defaults.json +136 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/page/page.e2e.test.ts +30 -26
- package/templates/performance-budgets.json +63 -5
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/registry.json +279 -3
- package/templates/review-dashboard/page.tsx +510 -0
- package/templates/settings.json +155 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
- package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// TYPES
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
interface ReviewFinding {
|
|
10
|
+
id: string;
|
|
11
|
+
pass: number;
|
|
12
|
+
category: string;
|
|
13
|
+
severity: "critical" | "warning" | "suggestion";
|
|
14
|
+
file: string;
|
|
15
|
+
line: number;
|
|
16
|
+
code: string;
|
|
17
|
+
issue: string;
|
|
18
|
+
fix: string;
|
|
19
|
+
aiReasoning: string;
|
|
20
|
+
status: "open" | "fixed" | "wontfix" | "false-positive";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PassResult {
|
|
24
|
+
name: string;
|
|
25
|
+
items: {
|
|
26
|
+
name: string;
|
|
27
|
+
status: "pass" | "warn" | "fail";
|
|
28
|
+
findings: ReviewFinding[];
|
|
29
|
+
}[];
|
|
30
|
+
totalPass: number;
|
|
31
|
+
totalWarn: number;
|
|
32
|
+
totalFail: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ReviewReport {
|
|
36
|
+
id: string;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
filesReviewed: number;
|
|
39
|
+
duration: string;
|
|
40
|
+
passes: PassResult[];
|
|
41
|
+
summary: {
|
|
42
|
+
critical: number;
|
|
43
|
+
warning: number;
|
|
44
|
+
suggestion: number;
|
|
45
|
+
passRate: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// MOCK DATA (Replace with actual data from .claude/review-log.json)
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
const MOCK_REPORT: ReviewReport = {
|
|
54
|
+
id: "review-2025-12-29-001",
|
|
55
|
+
timestamp: "2025-12-29T15:30:00Z",
|
|
56
|
+
filesReviewed: 45,
|
|
57
|
+
duration: "8m 34s",
|
|
58
|
+
passes: [
|
|
59
|
+
{
|
|
60
|
+
name: "Pass 1: Logic & Bugs",
|
|
61
|
+
items: [
|
|
62
|
+
{ name: "Optional properties checked", status: "pass", findings: [] },
|
|
63
|
+
{ name: "Spread on null values", status: "warn", findings: [
|
|
64
|
+
{
|
|
65
|
+
id: "f1",
|
|
66
|
+
pass: 1,
|
|
67
|
+
category: "Null Handling",
|
|
68
|
+
severity: "warning",
|
|
69
|
+
file: "src/lib/merge.ts",
|
|
70
|
+
line: 23,
|
|
71
|
+
code: "const result = { ...maybeNull }",
|
|
72
|
+
issue: "maybeNull could be null/undefined",
|
|
73
|
+
fix: "const result = { ...(maybeNull ?? {}) }",
|
|
74
|
+
aiReasoning: "The variable 'maybeNull' is typed as 'T | null' but spread operator doesn't handle null. This will throw at runtime if null is passed.",
|
|
75
|
+
status: "open"
|
|
76
|
+
}
|
|
77
|
+
]},
|
|
78
|
+
{ name: "Promise rejections caught", status: "pass", findings: [] },
|
|
79
|
+
{ name: "Loop bounds correct", status: "pass", findings: [] },
|
|
80
|
+
{ name: "Pagination calculations", status: "fail", findings: [
|
|
81
|
+
{
|
|
82
|
+
id: "f2",
|
|
83
|
+
pass: 1,
|
|
84
|
+
category: "Off-by-One",
|
|
85
|
+
severity: "critical",
|
|
86
|
+
file: "src/app/api/users/route.ts",
|
|
87
|
+
line: 67,
|
|
88
|
+
code: "const offset = (page - 1) * limit + 1",
|
|
89
|
+
issue: "Off-by-one error - offset should not add 1",
|
|
90
|
+
fix: "const offset = (page - 1) * limit",
|
|
91
|
+
aiReasoning: "Adding 1 to the offset will skip the first item on every page after page 1. For page 2 with limit 10, this returns items 12-21 instead of 11-20.",
|
|
92
|
+
status: "open"
|
|
93
|
+
}
|
|
94
|
+
]},
|
|
95
|
+
],
|
|
96
|
+
totalPass: 3,
|
|
97
|
+
totalWarn: 1,
|
|
98
|
+
totalFail: 1
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "Pass 2: Security",
|
|
102
|
+
items: [
|
|
103
|
+
{ name: "API routes check session", status: "pass", findings: [] },
|
|
104
|
+
{ name: "JWT tokens verified", status: "pass", findings: [] },
|
|
105
|
+
{ name: "Inputs validated with Zod", status: "pass", findings: [] },
|
|
106
|
+
{ name: "SQL parameterized", status: "pass", findings: [] },
|
|
107
|
+
{ name: "CORS specific origins", status: "warn", findings: [
|
|
108
|
+
{
|
|
109
|
+
id: "f3",
|
|
110
|
+
pass: 2,
|
|
111
|
+
category: "CORS",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
file: "src/middleware.ts",
|
|
114
|
+
line: 12,
|
|
115
|
+
code: "origin: process.env.NODE_ENV === 'development' ? '*' : origins",
|
|
116
|
+
issue: "CORS allows any origin in development",
|
|
117
|
+
fix: "Use specific localhost origins even in development",
|
|
118
|
+
aiReasoning: "While this is only in development, it could mask CORS issues that appear in production. Consider using 'http://localhost:3000' explicitly.",
|
|
119
|
+
status: "open"
|
|
120
|
+
}
|
|
121
|
+
]},
|
|
122
|
+
{ name: "Session cookie SameSite", status: "warn", findings: [
|
|
123
|
+
{
|
|
124
|
+
id: "f4",
|
|
125
|
+
pass: 2,
|
|
126
|
+
category: "Session",
|
|
127
|
+
severity: "warning",
|
|
128
|
+
file: "src/lib/auth.ts",
|
|
129
|
+
line: 45,
|
|
130
|
+
code: "cookies.set('session', token, { httpOnly: true })",
|
|
131
|
+
issue: "Session cookie missing SameSite attribute",
|
|
132
|
+
fix: "Add sameSite: 'lax' or 'strict' to cookie options",
|
|
133
|
+
aiReasoning: "Without SameSite, the cookie defaults to 'None' in some browsers, which requires Secure flag. This could cause auth issues in non-HTTPS environments.",
|
|
134
|
+
status: "open"
|
|
135
|
+
}
|
|
136
|
+
]},
|
|
137
|
+
],
|
|
138
|
+
totalPass: 4,
|
|
139
|
+
totalWarn: 2,
|
|
140
|
+
totalFail: 0
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "Pass 3: Performance",
|
|
144
|
+
items: [
|
|
145
|
+
{ name: "No N+1 queries", status: "fail", findings: [
|
|
146
|
+
{
|
|
147
|
+
id: "f5",
|
|
148
|
+
pass: 3,
|
|
149
|
+
category: "N+1 Query",
|
|
150
|
+
severity: "critical",
|
|
151
|
+
file: "src/app/api/orders/route.ts",
|
|
152
|
+
line: 34,
|
|
153
|
+
code: "orders.map(async (order) => await getCustomer(order.customerId))",
|
|
154
|
+
issue: "N+1 query - fetching customer for each order separately",
|
|
155
|
+
fix: "Use eager loading: include: { customer: true }",
|
|
156
|
+
aiReasoning: "For 100 orders, this makes 101 database queries (1 for orders + 100 for customers). With eager loading, it's just 1 query.",
|
|
157
|
+
status: "open"
|
|
158
|
+
}
|
|
159
|
+
]},
|
|
160
|
+
{ name: "Queries have indexes", status: "pass", findings: [] },
|
|
161
|
+
{ name: "Results paginated", status: "pass", findings: [] },
|
|
162
|
+
{ name: "useMemo for expensive calcs", status: "pass", findings: [] },
|
|
163
|
+
{ name: "useCallback for references", status: "fail", findings: [
|
|
164
|
+
{
|
|
165
|
+
id: "f6",
|
|
166
|
+
pass: 3,
|
|
167
|
+
category: "React Performance",
|
|
168
|
+
severity: "critical",
|
|
169
|
+
file: "src/components/Dashboard.tsx",
|
|
170
|
+
line: 89,
|
|
171
|
+
code: "const handleClick = () => updateData(id)",
|
|
172
|
+
issue: "Missing useCallback causes child re-renders",
|
|
173
|
+
fix: "const handleClick = useCallback(() => updateData(id), [id])",
|
|
174
|
+
aiReasoning: "This handler is passed to DataTable which uses React.memo. Without useCallback, a new function reference is created on every render, breaking memoization.",
|
|
175
|
+
status: "open"
|
|
176
|
+
}
|
|
177
|
+
]},
|
|
178
|
+
],
|
|
179
|
+
totalPass: 3,
|
|
180
|
+
totalWarn: 0,
|
|
181
|
+
totalFail: 2
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "Pass 4: Miscellaneous",
|
|
185
|
+
items: [
|
|
186
|
+
{ name: "Code self-documenting", status: "pass", findings: [] },
|
|
187
|
+
{ name: "Variable names descriptive", status: "pass", findings: [] },
|
|
188
|
+
{ name: "Complex algorithms commented", status: "warn", findings: [
|
|
189
|
+
{
|
|
190
|
+
id: "f7",
|
|
191
|
+
pass: 4,
|
|
192
|
+
category: "Documentation",
|
|
193
|
+
severity: "suggestion",
|
|
194
|
+
file: "src/lib/utils.ts",
|
|
195
|
+
line: 123,
|
|
196
|
+
code: "function calculateScore(a, b, c, d) { ... }",
|
|
197
|
+
issue: "Complex scoring function lacks explanation",
|
|
198
|
+
fix: "Add JSDoc explaining the algorithm and parameters",
|
|
199
|
+
aiReasoning: "This 30-line function implements a weighted scoring algorithm but doesn't explain what the weights represent or how the final score is used.",
|
|
200
|
+
status: "open"
|
|
201
|
+
}
|
|
202
|
+
]},
|
|
203
|
+
{ name: "Error handling consistent", status: "warn", findings: [
|
|
204
|
+
{
|
|
205
|
+
id: "f8",
|
|
206
|
+
pass: 4,
|
|
207
|
+
category: "Consistency",
|
|
208
|
+
severity: "suggestion",
|
|
209
|
+
file: "src/app/api/orders/route.ts",
|
|
210
|
+
line: 15,
|
|
211
|
+
code: "try { ... } catch (e) { console.error(e) }",
|
|
212
|
+
issue: "Inconsistent error handling vs other routes",
|
|
213
|
+
fix: "Use shared error handler: handleApiError(e)",
|
|
214
|
+
aiReasoning: "Other API routes use handleApiError() which logs to monitoring and returns proper error responses. This route just console.error which loses the error in production.",
|
|
215
|
+
status: "open"
|
|
216
|
+
}
|
|
217
|
+
]},
|
|
218
|
+
{ name: "JSDoc on public APIs", status: "warn", findings: [
|
|
219
|
+
{
|
|
220
|
+
id: "f9",
|
|
221
|
+
pass: 4,
|
|
222
|
+
category: "Documentation",
|
|
223
|
+
severity: "suggestion",
|
|
224
|
+
file: "src/types/index.ts",
|
|
225
|
+
line: 45,
|
|
226
|
+
code: "export interface OrderStatus { ... }",
|
|
227
|
+
issue: "Public type lacks documentation",
|
|
228
|
+
fix: "Add JSDoc describing each status value",
|
|
229
|
+
aiReasoning: "This type is exported and used across multiple files. Adding JSDoc would help IDE tooltips and TypeDoc generation.",
|
|
230
|
+
status: "open"
|
|
231
|
+
}
|
|
232
|
+
]},
|
|
233
|
+
],
|
|
234
|
+
totalPass: 2,
|
|
235
|
+
totalWarn: 3,
|
|
236
|
+
totalFail: 0
|
|
237
|
+
}
|
|
238
|
+
],
|
|
239
|
+
summary: {
|
|
240
|
+
critical: 3,
|
|
241
|
+
warning: 6,
|
|
242
|
+
suggestion: 0,
|
|
243
|
+
passRate: 87
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// ============================================================================
|
|
248
|
+
// COMPONENTS
|
|
249
|
+
// ============================================================================
|
|
250
|
+
|
|
251
|
+
function StatusBadge({ status }: { status: "pass" | "warn" | "fail" }) {
|
|
252
|
+
const styles = {
|
|
253
|
+
pass: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
254
|
+
warn: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
|
|
255
|
+
fail: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
256
|
+
};
|
|
257
|
+
const labels = { pass: "Pass", warn: "Warn", fail: "Fail" };
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${styles[status]}`}>
|
|
261
|
+
{labels[status]}
|
|
262
|
+
</span>
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function SeverityBadge({ severity }: { severity: "critical" | "warning" | "suggestion" }) {
|
|
267
|
+
const styles = {
|
|
268
|
+
critical: "bg-red-600 text-white",
|
|
269
|
+
warning: "bg-yellow-500 text-black",
|
|
270
|
+
suggestion: "bg-blue-100 text-blue-800"
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<span className={`px-2 py-1 rounded text-xs font-medium uppercase ${styles[severity]}`}>
|
|
275
|
+
{severity}
|
|
276
|
+
</span>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function FindingCard({ finding }: { finding: ReviewFinding }) {
|
|
281
|
+
const [expanded, setExpanded] = useState(false);
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className="border rounded-lg p-4 mb-3 bg-white dark:bg-zinc-900">
|
|
285
|
+
<div className="flex items-start justify-between">
|
|
286
|
+
<div className="flex-1">
|
|
287
|
+
<div className="flex items-center gap-2 mb-2">
|
|
288
|
+
<SeverityBadge severity={finding.severity} />
|
|
289
|
+
<span className="text-sm text-zinc-500">{finding.category}</span>
|
|
290
|
+
</div>
|
|
291
|
+
<p className="font-medium text-zinc-900 dark:text-zinc-100">{finding.issue}</p>
|
|
292
|
+
<p className="text-sm text-zinc-600 dark:text-zinc-400 mt-1">
|
|
293
|
+
<code className="bg-zinc-100 dark:bg-zinc-800 px-1 rounded">{finding.file}:{finding.line}</code>
|
|
294
|
+
</p>
|
|
295
|
+
</div>
|
|
296
|
+
<button
|
|
297
|
+
onClick={() => setExpanded(!expanded)}
|
|
298
|
+
className="text-sm text-blue-600 hover:underline"
|
|
299
|
+
>
|
|
300
|
+
{expanded ? "Hide Details" : "Show Details"}
|
|
301
|
+
</button>
|
|
302
|
+
</div>
|
|
303
|
+
|
|
304
|
+
{expanded && (
|
|
305
|
+
<div className="mt-4 space-y-3">
|
|
306
|
+
<div>
|
|
307
|
+
<h4 className="text-xs font-semibold uppercase text-zinc-500 mb-1">Code</h4>
|
|
308
|
+
<pre className="bg-zinc-100 dark:bg-zinc-800 p-3 rounded text-sm overflow-x-auto">
|
|
309
|
+
<code>{finding.code}</code>
|
|
310
|
+
</pre>
|
|
311
|
+
</div>
|
|
312
|
+
<div>
|
|
313
|
+
<h4 className="text-xs font-semibold uppercase text-zinc-500 mb-1">Suggested Fix</h4>
|
|
314
|
+
<pre className="bg-green-50 dark:bg-green-900/20 p-3 rounded text-sm overflow-x-auto border-l-4 border-green-500">
|
|
315
|
+
<code>{finding.fix}</code>
|
|
316
|
+
</pre>
|
|
317
|
+
</div>
|
|
318
|
+
<div>
|
|
319
|
+
<h4 className="text-xs font-semibold uppercase text-zinc-500 mb-1">AI Reasoning</h4>
|
|
320
|
+
<div className="bg-blue-50 dark:bg-blue-900/20 p-3 rounded text-sm border-l-4 border-blue-500">
|
|
321
|
+
<p className="italic">“{finding.aiReasoning}”</p>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
<div className="flex gap-2 pt-2">
|
|
325
|
+
<button className="px-3 py-1 text-sm bg-green-600 text-white rounded hover:bg-green-700">
|
|
326
|
+
Mark Fixed
|
|
327
|
+
</button>
|
|
328
|
+
<button className="px-3 py-1 text-sm bg-zinc-200 dark:bg-zinc-700 rounded hover:bg-zinc-300 dark:hover:bg-zinc-600">
|
|
329
|
+
Won't Fix
|
|
330
|
+
</button>
|
|
331
|
+
<button className="px-3 py-1 text-sm bg-zinc-200 dark:bg-zinc-700 rounded hover:bg-zinc-300 dark:hover:bg-zinc-600">
|
|
332
|
+
False Positive
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function PassSection({ pass }: { pass: PassResult }) {
|
|
342
|
+
const [expanded, setExpanded] = useState(true);
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<div className="border rounded-lg mb-4 overflow-hidden">
|
|
346
|
+
<button
|
|
347
|
+
onClick={() => setExpanded(!expanded)}
|
|
348
|
+
className="w-full flex items-center justify-between p-4 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700"
|
|
349
|
+
>
|
|
350
|
+
<h3 className="font-semibold text-lg">{pass.name}</h3>
|
|
351
|
+
<div className="flex items-center gap-4">
|
|
352
|
+
<span className="text-green-600">{pass.totalPass} ✓</span>
|
|
353
|
+
<span className="text-yellow-600">{pass.totalWarn} ⚠</span>
|
|
354
|
+
<span className="text-red-600">{pass.totalFail} ✗</span>
|
|
355
|
+
<span className="text-zinc-400">{expanded ? "▼" : "▶"}</span>
|
|
356
|
+
</div>
|
|
357
|
+
</button>
|
|
358
|
+
|
|
359
|
+
{expanded && (
|
|
360
|
+
<div className="p-4">
|
|
361
|
+
<table className="w-full mb-4">
|
|
362
|
+
<thead>
|
|
363
|
+
<tr className="border-b">
|
|
364
|
+
<th className="text-left py-2 text-sm font-medium text-zinc-500">Checklist Item</th>
|
|
365
|
+
<th className="text-right py-2 text-sm font-medium text-zinc-500 w-24">Status</th>
|
|
366
|
+
</tr>
|
|
367
|
+
</thead>
|
|
368
|
+
<tbody>
|
|
369
|
+
{pass.items.map((item, idx) => (
|
|
370
|
+
<tr key={idx} className="border-b last:border-0">
|
|
371
|
+
<td className="py-2">{item.name}</td>
|
|
372
|
+
<td className="py-2 text-right">
|
|
373
|
+
<StatusBadge status={item.status} />
|
|
374
|
+
</td>
|
|
375
|
+
</tr>
|
|
376
|
+
))}
|
|
377
|
+
</tbody>
|
|
378
|
+
</table>
|
|
379
|
+
|
|
380
|
+
{pass.items.some(item => item.findings.length > 0) && (
|
|
381
|
+
<div className="mt-4">
|
|
382
|
+
<h4 className="font-medium mb-3 text-zinc-700 dark:text-zinc-300">Findings</h4>
|
|
383
|
+
{pass.items.flatMap(item => item.findings).map(finding => (
|
|
384
|
+
<FindingCard key={finding.id} finding={finding} />
|
|
385
|
+
))}
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
</div>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function SummaryCard({ report }: { report: ReviewReport }) {
|
|
395
|
+
return (
|
|
396
|
+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
397
|
+
<div className="bg-white dark:bg-zinc-900 border rounded-lg p-4">
|
|
398
|
+
<p className="text-3xl font-bold text-zinc-900 dark:text-zinc-100">{report.summary.passRate}%</p>
|
|
399
|
+
<p className="text-sm text-zinc-500">Pass Rate</p>
|
|
400
|
+
</div>
|
|
401
|
+
<div className="bg-white dark:bg-zinc-900 border rounded-lg p-4">
|
|
402
|
+
<p className="text-3xl font-bold text-red-600">{report.summary.critical}</p>
|
|
403
|
+
<p className="text-sm text-zinc-500">Critical</p>
|
|
404
|
+
</div>
|
|
405
|
+
<div className="bg-white dark:bg-zinc-900 border rounded-lg p-4">
|
|
406
|
+
<p className="text-3xl font-bold text-yellow-600">{report.summary.warning}</p>
|
|
407
|
+
<p className="text-sm text-zinc-500">Warnings</p>
|
|
408
|
+
</div>
|
|
409
|
+
<div className="bg-white dark:bg-zinc-900 border rounded-lg p-4">
|
|
410
|
+
<p className="text-3xl font-bold text-zinc-600">{report.filesReviewed}</p>
|
|
411
|
+
<p className="text-sm text-zinc-500">Files Reviewed</p>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================================================
|
|
418
|
+
// MAIN PAGE
|
|
419
|
+
// ============================================================================
|
|
420
|
+
|
|
421
|
+
export default function ReviewDashboard() {
|
|
422
|
+
const [report, setReport] = useState<ReviewReport | null>(null);
|
|
423
|
+
const [filter, setFilter] = useState<"all" | "critical" | "warning" | "suggestion">("all");
|
|
424
|
+
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
// In production, fetch from .claude/review-log.json
|
|
427
|
+
setReport(MOCK_REPORT);
|
|
428
|
+
}, []);
|
|
429
|
+
|
|
430
|
+
if (!report) {
|
|
431
|
+
return (
|
|
432
|
+
<div className="container mx-auto p-8">
|
|
433
|
+
<div className="animate-pulse">
|
|
434
|
+
<div className="h-8 bg-zinc-200 rounded w-1/3 mb-4"></div>
|
|
435
|
+
<div className="h-4 bg-zinc-200 rounded w-1/2 mb-8"></div>
|
|
436
|
+
<div className="grid grid-cols-4 gap-4 mb-8">
|
|
437
|
+
{[...Array(4)].map((_, i) => (
|
|
438
|
+
<div key={i} className="h-24 bg-zinc-200 rounded"></div>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div className="container mx-auto p-8 max-w-6xl">
|
|
448
|
+
{/* Header */}
|
|
449
|
+
<div className="mb-8">
|
|
450
|
+
<h1 className="text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-2">
|
|
451
|
+
Code Review Dashboard
|
|
452
|
+
</h1>
|
|
453
|
+
<p className="text-zinc-600 dark:text-zinc-400">
|
|
454
|
+
Multi-pass review results from <code className="bg-zinc-100 dark:bg-zinc-800 px-1 rounded">{report.id}</code>
|
|
455
|
+
</p>
|
|
456
|
+
<p className="text-sm text-zinc-500 mt-1">
|
|
457
|
+
{new Date(report.timestamp).toLocaleString()} • {report.duration} • {report.filesReviewed} files
|
|
458
|
+
</p>
|
|
459
|
+
</div>
|
|
460
|
+
|
|
461
|
+
{/* Summary Cards */}
|
|
462
|
+
<SummaryCard report={report} />
|
|
463
|
+
|
|
464
|
+
{/* Filter */}
|
|
465
|
+
<div className="flex gap-2 mb-6">
|
|
466
|
+
{["all", "critical", "warning", "suggestion"].map((f) => (
|
|
467
|
+
<button
|
|
468
|
+
key={f}
|
|
469
|
+
onClick={() => setFilter(f as typeof filter)}
|
|
470
|
+
className={`px-4 py-2 rounded text-sm font-medium transition-colors ${
|
|
471
|
+
filter === f
|
|
472
|
+
? "bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900"
|
|
473
|
+
: "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
|
474
|
+
}`}
|
|
475
|
+
>
|
|
476
|
+
{f.charAt(0).toUpperCase() + f.slice(1)}
|
|
477
|
+
</button>
|
|
478
|
+
))}
|
|
479
|
+
</div>
|
|
480
|
+
|
|
481
|
+
{/* Pass Sections */}
|
|
482
|
+
<div>
|
|
483
|
+
{report.passes.map((pass, idx) => (
|
|
484
|
+
<PassSection key={idx} pass={pass} />
|
|
485
|
+
))}
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
{/* Actions */}
|
|
489
|
+
<div className="mt-8 flex gap-4">
|
|
490
|
+
<button className="px-6 py-3 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700">
|
|
491
|
+
Mark All Fixed
|
|
492
|
+
</button>
|
|
493
|
+
<button className="px-6 py-3 bg-zinc-200 dark:bg-zinc-700 rounded-lg font-medium hover:bg-zinc-300 dark:hover:bg-zinc-600">
|
|
494
|
+
Export Report
|
|
495
|
+
</button>
|
|
496
|
+
<button className="px-6 py-3 bg-zinc-200 dark:bg-zinc-700 rounded-lg font-medium hover:bg-zinc-300 dark:hover:bg-zinc-600">
|
|
497
|
+
Re-run Review
|
|
498
|
+
</button>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
{/* Footer */}
|
|
502
|
+
<div className="mt-12 pt-8 border-t text-center text-sm text-zinc-500">
|
|
503
|
+
<p>Generated by <code>/test-review --all-passes</code></p>
|
|
504
|
+
<p className="mt-1">
|
|
505
|
+
<a href="/hustle-dev-dashboard" className="text-blue-600 hover:underline">← Back to Dashboard</a>
|
|
506
|
+
</p>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
}
|