hex-audit 0.4.4 → 0.5.0
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/DEMO.md +14 -12
- package/README.md +66 -61
- package/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/app-build-manifest.json +18 -11
- package/dashboard/.next/app-path-routes-manifest.json +1 -1
- package/dashboard/.next/build-manifest.json +4 -4
- package/dashboard/.next/next-server.js.nft.json +1 -1
- package/dashboard/.next/prerender-manifest.js +1 -1
- package/dashboard/.next/prerender-manifest.json +1 -1
- package/dashboard/.next/required-server-files.json +1 -1
- package/dashboard/.next/routes-manifest.json +1 -1
- package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/server/app/_not-found.rsc +4 -4
- package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/api/flows/route.js +1 -1
- package/dashboard/.next/server/app/api/issues/[id]/move/route.js +1 -1
- package/dashboard/.next/server/app/api/issues/[id]/route.js +1 -1
- package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram.html +1 -1
- package/dashboard/.next/server/app/diagram.rsc +4 -4
- package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/flows.html +1 -1
- package/dashboard/.next/server/app/flows.rsc +4 -4
- package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/issues/page.js +12 -1
- package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/overleaf/page.js +1 -0
- package/dashboard/.next/server/app/overleaf/page.js.nft.json +1 -0
- package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -0
- package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app-paths-manifest.json +9 -8
- package/dashboard/.next/server/chunks/431.js +1 -1
- package/dashboard/.next/server/functions-config-manifest.json +1 -1
- package/dashboard/.next/server/middleware-build-manifest.js +1 -1
- package/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/server/pages/500.html +1 -1
- package/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/static/chunks/app/issues/page-2bb52a36f20497fe.js +1 -0
- package/dashboard/.next/static/chunks/app/{layout-527d8aa86e483fbd.js → layout-9c3c84993dd895f6.js} +1 -1
- package/dashboard/.next/static/chunks/app/overleaf/page-bcea12a80c97b92f.js +1 -0
- package/dashboard/.next/static/chunks/{main-70c6d841ad572657.js → main-8f1e837090aef5cf.js} +1 -1
- package/dashboard/.next/static/css/9c84313d0484b74f.css +3 -0
- package/dashboard/app/api/flows/route.ts +4 -0
- package/dashboard/app/api/issues/[id]/move/route.ts +29 -58
- package/dashboard/app/api/issues/[id]/route.ts +11 -102
- package/dashboard/app/issues/IssuesBoardClient.tsx +155 -50
- package/dashboard/app/issues/page.tsx +40 -3
- package/dashboard/app/overleaf/OverleafClient.tsx +97 -0
- package/dashboard/app/overleaf/page.tsx +32 -0
- package/dashboard/components/DashboardLayout.tsx +8 -2
- package/dashboard/lib/finding-markdown.ts +110 -0
- package/dashboard/next.config.mjs +4 -0
- package/dist/analysis/external-calls.d.ts +8 -1
- package/dist/analysis/external-calls.d.ts.map +1 -1
- package/dist/analysis/external-calls.js +57 -13
- package/dist/analysis/external-calls.js.map +1 -1
- package/dist/commands/calls.d.ts.map +1 -1
- package/dist/commands/calls.js +15 -6
- package/dist/commands/calls.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +12 -3
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/init.js +19 -19
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/issue.d.ts +3 -0
- package/dist/commands/issue.d.ts.map +1 -0
- package/dist/commands/issue.js +195 -0
- package/dist/commands/issue.js.map +1 -0
- package/dist/core/config.d.ts +1 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +10 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/issues.d.ts +116 -0
- package/dist/core/issues.d.ts.map +1 -0
- package/dist/core/issues.js +403 -0
- package/dist/core/issues.js.map +1 -0
- package/dist/core/schema.d.ts +24 -8
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +12 -1
- package/dist/core/schema.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/skills/generate-overleaf.md +18 -10
- package/dist/skills/ingest-aa-report.md +15 -19
- package/dist/skills/init-audit.md +13 -18
- package/dist/skills/sync-issues.md +86 -195
- package/dist/skills/validate-all-findings.md +50 -0
- package/dist/skills/validate-issue.md +20 -10
- package/dist/skills/write-finding.md +23 -28
- package/package.json +1 -1
- package/dashboard/.next/server/app/api/flows.body +0 -1
- package/dashboard/.next/server/app/api/flows.meta +0 -1
- package/dashboard/.next/static/chunks/app/issues/page-dc110d4cc0a5c398.js +0 -1
- package/dashboard/.next/static/css/7a38e5062ba6d403.css +0 -3
- /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → NrFPHIFHWQ5jNF62Px_X8}/_buildManifest.js +0 -0
- /package/dashboard/.next/static/{ePObaFrrwft0BlvlllUMo → NrFPHIFHWQ5jNF62Px_X8}/_ssgManifest.js +0 -0
|
@@ -11,12 +11,15 @@ import {
|
|
|
11
11
|
useDroppable,
|
|
12
12
|
} from '@dnd-kit/core';
|
|
13
13
|
import { SeverityBadge } from '@/components/SeverityBadge';
|
|
14
|
+
import { findingToHackmd, type FormatContext } from '@/lib/finding-markdown';
|
|
14
15
|
|
|
15
|
-
export type BoardColumn = 'potential' | 'verified' | 'invalid' | 'duplicate';
|
|
16
|
+
export type BoardColumn = 'potential' | 'verified' | 'synced' | 'invalid' | 'duplicate';
|
|
17
|
+
export type SyncState = 'none' | 'unsynced' | 'synced_open' | 'synced_closed' | 'conflict';
|
|
16
18
|
export type IssueStatus =
|
|
17
19
|
| 'pending_validation'
|
|
18
20
|
| 'unverified'
|
|
19
21
|
| 'verified'
|
|
22
|
+
| 'synced'
|
|
20
23
|
| 'rejected'
|
|
21
24
|
| 'duplicate';
|
|
22
25
|
export type IssueSource = 'manual' | 'auditagent' | 'conformance' | 'github';
|
|
@@ -36,7 +39,10 @@ export interface BoardIssue {
|
|
|
36
39
|
code_locations: Array<{ file: string; snippet?: string }>;
|
|
37
40
|
github_synced: boolean;
|
|
38
41
|
github_issue_url?: string;
|
|
42
|
+
github_issue_number?: number;
|
|
39
43
|
github_state?: string;
|
|
44
|
+
/** Derived in page.tsx: how this verified finding stands vs GitHub. */
|
|
45
|
+
sync_state?: SyncState;
|
|
40
46
|
duplicate_of: string | null;
|
|
41
47
|
match_signals?: {
|
|
42
48
|
contract: boolean;
|
|
@@ -54,7 +60,8 @@ export interface BoardIssue {
|
|
|
54
60
|
|
|
55
61
|
const COLUMN_DEFS: Array<{ id: BoardColumn; label: string; description: string }> = [
|
|
56
62
|
{ id: 'potential', label: 'Potential', description: 'Awaiting validation' },
|
|
57
|
-
{ id: 'verified', label: 'Verified', description: 'Confirmed
|
|
63
|
+
{ id: 'verified', label: 'Verified', description: 'Confirmed, ready to push' },
|
|
64
|
+
{ id: 'synced', label: 'Synced to GitHub', description: 'On GitHub — edit there, not here' },
|
|
58
65
|
{ id: 'invalid', label: 'Invalid', description: 'Rejected after review' },
|
|
59
66
|
{ id: 'duplicate', label: 'Duplicate', description: 'Covered by another entry' },
|
|
60
67
|
];
|
|
@@ -70,7 +77,13 @@ const SEVERITY_ORDER: Record<BoardIssue['severity'], number> = {
|
|
|
70
77
|
const SEVERITIES: BoardIssue['severity'][] = ['Critical', 'High', 'Medium', 'Low', 'Info'];
|
|
71
78
|
const RESOLUTIONS = ['Fixed', 'Mitigated', 'Acknowledged', 'Not Fixed', 'Unresolved'];
|
|
72
79
|
|
|
73
|
-
export function IssuesBoardClient({
|
|
80
|
+
export function IssuesBoardClient({
|
|
81
|
+
issues: initialIssues,
|
|
82
|
+
formatCtx = {},
|
|
83
|
+
}: {
|
|
84
|
+
issues: BoardIssue[];
|
|
85
|
+
formatCtx?: FormatContext;
|
|
86
|
+
}) {
|
|
74
87
|
const [issues, setIssues] = useState<BoardIssue[]>(initialIssues);
|
|
75
88
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
76
89
|
const [sourceFilter, setSourceFilter] = useState<IssueSource | 'all'>('all');
|
|
@@ -92,6 +105,7 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
|
|
|
92
105
|
const map: Record<BoardColumn, BoardIssue[]> = {
|
|
93
106
|
potential: [],
|
|
94
107
|
verified: [],
|
|
108
|
+
synced: [],
|
|
95
109
|
invalid: [],
|
|
96
110
|
duplicate: [],
|
|
97
111
|
};
|
|
@@ -112,6 +126,7 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
|
|
|
112
126
|
const total: Record<BoardColumn, number> = {
|
|
113
127
|
potential: 0,
|
|
114
128
|
verified: 0,
|
|
129
|
+
synced: 0,
|
|
115
130
|
invalid: 0,
|
|
116
131
|
duplicate: 0,
|
|
117
132
|
};
|
|
@@ -181,6 +196,8 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
|
|
|
181
196
|
const id = active.id as string;
|
|
182
197
|
const current = issues.find((i) => i.id === id);
|
|
183
198
|
if (!current || current.column === dest) return;
|
|
199
|
+
// Synced is reached only via /sync-issues, and synced cards are locked.
|
|
200
|
+
if (dest === 'synced' || current.column === 'synced') return;
|
|
184
201
|
void moveIssue(id, dest);
|
|
185
202
|
};
|
|
186
203
|
|
|
@@ -193,15 +210,15 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
|
|
|
193
210
|
<h1 className="text-title font-semibold text-text-primary">Issues</h1>
|
|
194
211
|
<p className="mt-1 text-body text-text-secondary">
|
|
195
212
|
{issues.length} total — {counts.potential} potential, {counts.verified} verified,{' '}
|
|
196
|
-
{counts.invalid} invalid, {counts.duplicate} duplicate. Drag a
|
|
197
|
-
click to edit.
|
|
213
|
+
{counts.synced} synced, {counts.invalid} invalid, {counts.duplicate} duplicate. Drag a
|
|
214
|
+
card between columns; click to edit. Synced cards are read-only (edit on GitHub).
|
|
198
215
|
</p>
|
|
199
216
|
</div>
|
|
200
217
|
<SourceFilter value={sourceFilter} onChange={setSourceFilter} />
|
|
201
218
|
</header>
|
|
202
219
|
|
|
203
220
|
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
|
|
204
|
-
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-
|
|
221
|
+
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
205
222
|
{COLUMN_DEFS.map((col) => (
|
|
206
223
|
<Column key={col.id} column={col} issues={grouped[col.id]} onOpen={setOpenId} />
|
|
207
224
|
))}
|
|
@@ -211,6 +228,7 @@ export function IssuesBoardClient({ issues: initialIssues }: { issues: BoardIssu
|
|
|
211
228
|
{openIssue && (
|
|
212
229
|
<IssueModal
|
|
213
230
|
issue={openIssue}
|
|
231
|
+
formatCtx={formatCtx}
|
|
214
232
|
onClose={() => setOpenId(null)}
|
|
215
233
|
onSave={(updates) => updateIssue(openIssue.id, updates)}
|
|
216
234
|
/>
|
|
@@ -225,6 +243,8 @@ function columnToStatus(column: BoardColumn): IssueStatus {
|
|
|
225
243
|
return 'pending_validation';
|
|
226
244
|
case 'verified':
|
|
227
245
|
return 'verified';
|
|
246
|
+
case 'synced':
|
|
247
|
+
return 'synced';
|
|
228
248
|
case 'invalid':
|
|
229
249
|
return 'rejected';
|
|
230
250
|
case 'duplicate':
|
|
@@ -278,7 +298,8 @@ function Column({
|
|
|
278
298
|
issues: BoardIssue[];
|
|
279
299
|
onOpen: (id: string) => void;
|
|
280
300
|
}) {
|
|
281
|
-
|
|
301
|
+
// The Synced column is reached only via /sync-issues — never a drop target.
|
|
302
|
+
const { isOver, setNodeRef } = useDroppable({ id: column.id, disabled: column.id === 'synced' });
|
|
282
303
|
return (
|
|
283
304
|
<section
|
|
284
305
|
ref={setNodeRef}
|
|
@@ -307,8 +328,11 @@ function Column({
|
|
|
307
328
|
}
|
|
308
329
|
|
|
309
330
|
function Card({ issue, onOpen }: { issue: BoardIssue; onOpen: (id: string) => void }) {
|
|
331
|
+
// Synced cards are locked: not draggable (edits happen on GitHub).
|
|
332
|
+
const locked = issue.column === 'synced';
|
|
310
333
|
const { attributes, listeners, setNodeRef, isDragging, transform } = useDraggable({
|
|
311
334
|
id: issue.id,
|
|
335
|
+
disabled: locked,
|
|
312
336
|
});
|
|
313
337
|
const style: React.CSSProperties = transform
|
|
314
338
|
? {
|
|
@@ -321,8 +345,8 @@ function Card({ issue, onOpen }: { issue: BoardIssue; onOpen: (id: string) => vo
|
|
|
321
345
|
<article
|
|
322
346
|
ref={setNodeRef}
|
|
323
347
|
style={style}
|
|
324
|
-
{...attributes}
|
|
325
|
-
{...listeners}
|
|
348
|
+
{...(locked ? {} : attributes)}
|
|
349
|
+
{...(locked ? {} : listeners)}
|
|
326
350
|
onClick={(e) => {
|
|
327
351
|
// Drag listeners may swallow the click during a drag; treat anything
|
|
328
352
|
// with a transform as a drag, not a click.
|
|
@@ -330,7 +354,9 @@ function Card({ issue, onOpen }: { issue: BoardIssue; onOpen: (id: string) => vo
|
|
|
330
354
|
e.stopPropagation();
|
|
331
355
|
onOpen(issue.id);
|
|
332
356
|
}}
|
|
333
|
-
className={`group
|
|
357
|
+
className={`group rounded-md border bg-surface-0 p-3 text-left ${
|
|
358
|
+
locked ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'
|
|
359
|
+
} ${
|
|
334
360
|
isDragging ? 'border-accent shadow-lg' : 'border-border-subtle hover:border-border-default'
|
|
335
361
|
}`}
|
|
336
362
|
>
|
|
@@ -341,17 +367,7 @@ function Card({ issue, onOpen }: { issue: BoardIssue; onOpen: (id: string) => vo
|
|
|
341
367
|
<h3 className="mt-1 text-body font-medium text-text-primary line-clamp-2">{issue.title}</h3>
|
|
342
368
|
<div className="mt-2 flex flex-wrap items-center gap-1.5 text-caption">
|
|
343
369
|
<SourceChip source={issue.source} />
|
|
344
|
-
{issue
|
|
345
|
-
<a
|
|
346
|
-
href={issue.github_issue_url}
|
|
347
|
-
target="_blank"
|
|
348
|
-
rel="noopener noreferrer"
|
|
349
|
-
onClick={(e) => e.stopPropagation()}
|
|
350
|
-
className="rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary hover:bg-surface-3 hover:text-text-primary"
|
|
351
|
-
>
|
|
352
|
-
GH {issue.github_state === 'closed' ? '✕' : '✓'}
|
|
353
|
-
</a>
|
|
354
|
-
)}
|
|
370
|
+
<SyncChip issue={issue} />
|
|
355
371
|
{issue.duplicate_of && (
|
|
356
372
|
<span className="rounded-md bg-surface-2 px-1.5 py-0.5 text-text-secondary">
|
|
357
373
|
dup of {issue.duplicate_of}
|
|
@@ -409,12 +425,60 @@ function SourceChip({ source }: { source: IssueSource }) {
|
|
|
409
425
|
);
|
|
410
426
|
}
|
|
411
427
|
|
|
428
|
+
/**
|
|
429
|
+
* GitHub-sync state chip. Only verified findings are push candidates, so other
|
|
430
|
+
* statuses render nothing (sync_state === 'none').
|
|
431
|
+
*/
|
|
432
|
+
function SyncChip({ issue }: { issue: BoardIssue }) {
|
|
433
|
+
const state = issue.sync_state ?? 'none';
|
|
434
|
+
if (state === 'none') return null;
|
|
435
|
+
|
|
436
|
+
if (state === 'unsynced') {
|
|
437
|
+
return (
|
|
438
|
+
<span
|
|
439
|
+
title="Run /sync-issues to push verified findings to GitHub."
|
|
440
|
+
className="rounded-md border border-dashed border-border-default px-1.5 py-0.5 text-text-tertiary"
|
|
441
|
+
>
|
|
442
|
+
GH unsynced
|
|
443
|
+
</span>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
if (state === 'conflict') {
|
|
447
|
+
return (
|
|
448
|
+
<span
|
|
449
|
+
title="Local and remote diverged. Re-run /sync-issues to reconcile."
|
|
450
|
+
className="rounded-md bg-[var(--critical)]/15 px-1.5 py-0.5 text-[var(--critical)]"
|
|
451
|
+
>
|
|
452
|
+
GH conflict
|
|
453
|
+
</span>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const closed = state === 'synced_closed';
|
|
457
|
+
const label = `GH #${issue.github_issue_number ?? '?'} ${closed ? 'closed' : 'open'}`;
|
|
458
|
+
const cls = closed ? 'bg-surface-2 text-text-tertiary' : 'bg-[var(--low)]/15 text-[var(--low)]';
|
|
459
|
+
return issue.github_issue_url ? (
|
|
460
|
+
<a
|
|
461
|
+
href={issue.github_issue_url}
|
|
462
|
+
target="_blank"
|
|
463
|
+
rel="noopener noreferrer"
|
|
464
|
+
onClick={(e) => e.stopPropagation()}
|
|
465
|
+
className={`rounded-md px-1.5 py-0.5 hover:underline ${cls}`}
|
|
466
|
+
>
|
|
467
|
+
{label}
|
|
468
|
+
</a>
|
|
469
|
+
) : (
|
|
470
|
+
<span className={`rounded-md px-1.5 py-0.5 ${cls}`}>{label}</span>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
412
474
|
function IssueModal({
|
|
413
475
|
issue,
|
|
476
|
+
formatCtx,
|
|
414
477
|
onClose,
|
|
415
478
|
onSave,
|
|
416
479
|
}: {
|
|
417
480
|
issue: BoardIssue;
|
|
481
|
+
formatCtx: FormatContext;
|
|
418
482
|
onClose: () => void;
|
|
419
483
|
onSave: (updates: Partial<BoardIssue>) => Promise<void> | void;
|
|
420
484
|
}) {
|
|
@@ -425,12 +489,39 @@ function IssueModal({
|
|
|
425
489
|
const [resolution, setResolution] = useState(issue.resolution ?? '');
|
|
426
490
|
const [updateFromClient, setUpdateFromClient] = useState(issue.update_from_client ?? '');
|
|
427
491
|
const [saving, setSaving] = useState(false);
|
|
492
|
+
const [copied, setCopied] = useState(false);
|
|
428
493
|
const overlayRef = useRef<HTMLDivElement>(null);
|
|
429
494
|
|
|
495
|
+
// Synced issues are read-only in Hex; edits happen on GitHub.
|
|
496
|
+
const locked = issue.status === 'synced';
|
|
497
|
+
|
|
430
498
|
useEffect(() => {
|
|
431
499
|
overlayRef.current?.focus();
|
|
432
500
|
}, []);
|
|
433
501
|
|
|
502
|
+
// Copy the current (possibly edited) field values, not just the saved ones.
|
|
503
|
+
const copyAsHackmd = async () => {
|
|
504
|
+
const md = findingToHackmd(
|
|
505
|
+
{
|
|
506
|
+
...issue,
|
|
507
|
+
title,
|
|
508
|
+
severity,
|
|
509
|
+
description,
|
|
510
|
+
recommendation,
|
|
511
|
+
resolution: resolution || undefined,
|
|
512
|
+
update_from_client: updateFromClient || undefined,
|
|
513
|
+
},
|
|
514
|
+
formatCtx,
|
|
515
|
+
);
|
|
516
|
+
try {
|
|
517
|
+
await navigator.clipboard.writeText(md);
|
|
518
|
+
setCopied(true);
|
|
519
|
+
setTimeout(() => setCopied(false), 1500);
|
|
520
|
+
} catch {
|
|
521
|
+
/* clipboard blocked (insecure context) — no-op */
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
|
|
434
525
|
const submit = async () => {
|
|
435
526
|
setSaving(true);
|
|
436
527
|
const updates: Partial<BoardIssue> = {
|
|
@@ -466,19 +557,10 @@ function IssueModal({
|
|
|
466
557
|
onClick={(e) => e.stopPropagation()}
|
|
467
558
|
>
|
|
468
559
|
<div className="flex items-center justify-between border-b border-border-default bg-surface-2 px-4 py-3">
|
|
469
|
-
<div className="flex items-center gap-2">
|
|
560
|
+
<div className="flex items-center gap-2 text-caption">
|
|
470
561
|
<span className="font-mono text-body text-text-tertiary">{issue.id}</span>
|
|
471
562
|
<SourceChip source={issue.source} />
|
|
472
|
-
{issue
|
|
473
|
-
<a
|
|
474
|
-
href={issue.github_issue_url}
|
|
475
|
-
target="_blank"
|
|
476
|
-
rel="noopener noreferrer"
|
|
477
|
-
className="rounded-md bg-surface-3 px-1.5 py-0.5 text-caption text-text-secondary hover:text-text-primary"
|
|
478
|
-
>
|
|
479
|
-
GitHub issue
|
|
480
|
-
</a>
|
|
481
|
-
)}
|
|
563
|
+
<SyncChip issue={issue} />
|
|
482
564
|
</div>
|
|
483
565
|
<button
|
|
484
566
|
type="button"
|
|
@@ -500,12 +582,19 @@ function IssueModal({
|
|
|
500
582
|
</div>
|
|
501
583
|
|
|
502
584
|
<div className="flex-1 overflow-y-auto p-4">
|
|
585
|
+
{locked && (
|
|
586
|
+
<div className="mb-4 rounded-md border border-border-default bg-surface-2 px-3 py-2 text-caption text-text-secondary">
|
|
587
|
+
This issue is synced to GitHub and is read-only here. Edit it on GitHub, then run{' '}
|
|
588
|
+
<span className="font-mono">/sync-issues</span> to pull the changes back.
|
|
589
|
+
</div>
|
|
590
|
+
)}
|
|
503
591
|
<Field label="Title">
|
|
504
592
|
<input
|
|
505
593
|
type="text"
|
|
506
594
|
value={title}
|
|
507
595
|
onChange={(e) => setTitle(e.target.value)}
|
|
508
|
-
|
|
596
|
+
disabled={locked}
|
|
597
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
509
598
|
/>
|
|
510
599
|
</Field>
|
|
511
600
|
|
|
@@ -514,7 +603,8 @@ function IssueModal({
|
|
|
514
603
|
<select
|
|
515
604
|
value={severity}
|
|
516
605
|
onChange={(e) => setSeverity(e.target.value as BoardIssue['severity'])}
|
|
517
|
-
|
|
606
|
+
disabled={locked}
|
|
607
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
518
608
|
>
|
|
519
609
|
{SEVERITIES.map((s) => (
|
|
520
610
|
<option key={s} value={s}>
|
|
@@ -527,7 +617,8 @@ function IssueModal({
|
|
|
527
617
|
<select
|
|
528
618
|
value={resolution}
|
|
529
619
|
onChange={(e) => setResolution(e.target.value)}
|
|
530
|
-
|
|
620
|
+
disabled={locked}
|
|
621
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 text-body text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
531
622
|
>
|
|
532
623
|
<option value="">— none —</option>
|
|
533
624
|
{RESOLUTIONS.map((r) => (
|
|
@@ -544,7 +635,8 @@ function IssueModal({
|
|
|
544
635
|
value={description}
|
|
545
636
|
onChange={(e) => setDescription(e.target.value)}
|
|
546
637
|
rows={8}
|
|
547
|
-
|
|
638
|
+
disabled={locked}
|
|
639
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
548
640
|
/>
|
|
549
641
|
</Field>
|
|
550
642
|
|
|
@@ -553,7 +645,8 @@ function IssueModal({
|
|
|
553
645
|
value={recommendation}
|
|
554
646
|
onChange={(e) => setRecommendation(e.target.value)}
|
|
555
647
|
rows={4}
|
|
556
|
-
|
|
648
|
+
disabled={locked}
|
|
649
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
557
650
|
/>
|
|
558
651
|
</Field>
|
|
559
652
|
|
|
@@ -562,8 +655,9 @@ function IssueModal({
|
|
|
562
655
|
value={updateFromClient}
|
|
563
656
|
onChange={(e) => setUpdateFromClient(e.target.value)}
|
|
564
657
|
rows={4}
|
|
658
|
+
disabled={locked}
|
|
565
659
|
placeholder="How the client responded — used by /generate-overleaf."
|
|
566
|
-
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none"
|
|
660
|
+
className="w-full rounded-md border border-border-default bg-surface-0 px-3 py-2 font-mono text-caption text-text-primary focus:border-accent focus:outline-none disabled:opacity-60"
|
|
567
661
|
/>
|
|
568
662
|
</Field>
|
|
569
663
|
|
|
@@ -596,22 +690,33 @@ function IssueModal({
|
|
|
596
690
|
)}
|
|
597
691
|
</div>
|
|
598
692
|
|
|
599
|
-
<div className="flex items-center justify-
|
|
600
|
-
<button
|
|
601
|
-
type="button"
|
|
602
|
-
onClick={onClose}
|
|
603
|
-
className="rounded-md px-3 py-1.5 text-body text-text-secondary hover:text-text-primary"
|
|
604
|
-
>
|
|
605
|
-
Cancel
|
|
606
|
-
</button>
|
|
693
|
+
<div className="flex items-center justify-between gap-2 border-t border-border-default bg-surface-2 px-4 py-3">
|
|
607
694
|
<button
|
|
608
695
|
type="button"
|
|
609
|
-
onClick={
|
|
610
|
-
|
|
611
|
-
className="rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 hover:bg-accent-secondary disabled:opacity-50"
|
|
696
|
+
onClick={copyAsHackmd}
|
|
697
|
+
className="rounded-md border border-border-default px-3 py-1.5 text-body text-text-secondary hover:bg-surface-3 hover:text-text-primary"
|
|
612
698
|
>
|
|
613
|
-
{
|
|
699
|
+
{copied ? 'Copied!' : 'Copy as HackMD'}
|
|
614
700
|
</button>
|
|
701
|
+
<div className="flex items-center gap-2">
|
|
702
|
+
<button
|
|
703
|
+
type="button"
|
|
704
|
+
onClick={onClose}
|
|
705
|
+
className="rounded-md px-3 py-1.5 text-body text-text-secondary hover:text-text-primary"
|
|
706
|
+
>
|
|
707
|
+
{locked ? 'Close' : 'Cancel'}
|
|
708
|
+
</button>
|
|
709
|
+
{!locked && (
|
|
710
|
+
<button
|
|
711
|
+
type="button"
|
|
712
|
+
onClick={submit}
|
|
713
|
+
disabled={saving}
|
|
714
|
+
className="rounded-md bg-accent px-3 py-1.5 text-body font-medium text-surface-0 hover:bg-accent-secondary disabled:opacity-50"
|
|
715
|
+
>
|
|
716
|
+
{saving ? 'Saving…' : 'Save'}
|
|
717
|
+
</button>
|
|
718
|
+
)}
|
|
719
|
+
</div>
|
|
615
720
|
</div>
|
|
616
721
|
</div>
|
|
617
722
|
</div>
|
|
@@ -8,7 +8,6 @@ import { IssuesBoardClient, type BoardIssue } from './IssuesBoardClient';
|
|
|
8
8
|
// stale (or empty) HTML for every audit project, ignoring the runtime cwd.
|
|
9
9
|
export const dynamic = 'force-dynamic';
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
interface FindingsFile {
|
|
13
12
|
findings: Array<{
|
|
14
13
|
id: string;
|
|
@@ -20,11 +19,17 @@ interface FindingsFile {
|
|
|
20
19
|
update_from_client?: string;
|
|
21
20
|
root_cause?: { locations?: Array<{ file: string; snippet?: string }> };
|
|
22
21
|
poc?: { status?: string; file?: string | null; validation_memo?: string | null };
|
|
23
|
-
github?: { issue_number?: number; issue_url?: string; state?: string };
|
|
22
|
+
github?: { issue_number?: number; issue_url?: string; state?: string; sync_status?: string };
|
|
24
23
|
category?: string;
|
|
25
24
|
}>;
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
interface ConfigFile {
|
|
28
|
+
settings?: {
|
|
29
|
+
report?: { repository_url?: string; initial_commit?: string };
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
interface TrackingFile {
|
|
29
34
|
findings: Array<{
|
|
30
35
|
id: string;
|
|
@@ -74,10 +79,33 @@ interface SpecConformanceFile {
|
|
|
74
79
|
}>;
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Derive the GitHub sync chip state. Verified findings are push candidates
|
|
84
|
+
* (unsynced until pushed); synced findings always carry an issue number.
|
|
85
|
+
* Everything else gets no chip.
|
|
86
|
+
*/
|
|
87
|
+
function computeSyncState(
|
|
88
|
+
status: string | undefined,
|
|
89
|
+
github: { issue_number?: number; state?: string; sync_status?: string } | undefined,
|
|
90
|
+
): BoardIssue['sync_state'] {
|
|
91
|
+
if (status !== 'verified' && status !== 'synced') return 'none';
|
|
92
|
+
if (!github?.issue_number) return 'unsynced';
|
|
93
|
+
if (
|
|
94
|
+
github.sync_status === 'conflict' ||
|
|
95
|
+
github.sync_status === 'local_ahead' ||
|
|
96
|
+
github.sync_status === 'remote_ahead'
|
|
97
|
+
) {
|
|
98
|
+
return 'conflict';
|
|
99
|
+
}
|
|
100
|
+
return github.state === 'closed' ? 'synced_closed' : 'synced_open';
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
function statusToColumn(status: string | undefined): BoardIssue['column'] {
|
|
78
104
|
switch (status) {
|
|
79
105
|
case 'verified':
|
|
80
106
|
return 'verified';
|
|
107
|
+
case 'synced':
|
|
108
|
+
return 'synced';
|
|
81
109
|
case 'rejected':
|
|
82
110
|
return 'invalid';
|
|
83
111
|
case 'duplicate':
|
|
@@ -179,7 +207,9 @@ function buildBoardIssues(): BoardIssue[] {
|
|
|
179
207
|
code_locations: locations,
|
|
180
208
|
github_synced: Boolean(finding?.github?.issue_number),
|
|
181
209
|
github_issue_url: finding?.github?.issue_url,
|
|
210
|
+
github_issue_number: finding?.github?.issue_number,
|
|
182
211
|
github_state: finding?.github?.state,
|
|
212
|
+
sync_state: computeSyncState(t.status, finding?.github),
|
|
183
213
|
duplicate_of: t.duplicate_of ?? dup?.matches ?? null,
|
|
184
214
|
match_signals: matchSignals,
|
|
185
215
|
reasoning: dup?.reasoning,
|
|
@@ -210,7 +240,9 @@ function buildBoardIssues(): BoardIssue[] {
|
|
|
210
240
|
code_locations: f.root_cause?.locations ?? [],
|
|
211
241
|
github_synced: Boolean(f.github?.issue_number),
|
|
212
242
|
github_issue_url: f.github?.issue_url,
|
|
243
|
+
github_issue_number: f.github?.issue_number,
|
|
213
244
|
github_state: f.github?.state,
|
|
245
|
+
sync_state: computeSyncState('verified', f.github),
|
|
214
246
|
duplicate_of: null,
|
|
215
247
|
match_signals: undefined,
|
|
216
248
|
reasoning: undefined,
|
|
@@ -227,6 +259,11 @@ function buildBoardIssues(): BoardIssue[] {
|
|
|
227
259
|
|
|
228
260
|
export default function IssuesPage() {
|
|
229
261
|
const issues = buildBoardIssues();
|
|
262
|
+
const config = readJsonFile<ConfigFile>('config.json');
|
|
263
|
+
const formatCtx = {
|
|
264
|
+
repositoryUrl: config?.settings?.report?.repository_url,
|
|
265
|
+
commit: config?.settings?.report?.initial_commit,
|
|
266
|
+
};
|
|
230
267
|
|
|
231
268
|
if (issues.length === 0) {
|
|
232
269
|
return (
|
|
@@ -242,5 +279,5 @@ export default function IssuesPage() {
|
|
|
242
279
|
);
|
|
243
280
|
}
|
|
244
281
|
|
|
245
|
-
return <IssuesBoardClient issues={issues} />;
|
|
282
|
+
return <IssuesBoardClient issues={issues} formatCtx={formatCtx} />;
|
|
246
283
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface OverleafSections {
|
|
6
|
+
executive_summary: string | null;
|
|
7
|
+
audited_files: string | null;
|
|
8
|
+
summary_of_findings: string | null;
|
|
9
|
+
findings: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type SectionKey = keyof OverleafSections;
|
|
13
|
+
|
|
14
|
+
const TABS: Array<{ key: SectionKey; label: string; file: string }> = [
|
|
15
|
+
{ key: 'executive_summary', label: 'Executive Summary', file: 'executive_summary.txt' },
|
|
16
|
+
{ key: 'audited_files', label: 'Audited Files', file: 'audited_files.txt' },
|
|
17
|
+
{ key: 'summary_of_findings', label: 'Summary of Issues', file: 'summary_of_findings.txt' },
|
|
18
|
+
{ key: 'findings', label: 'Issues', file: 'findings.txt' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function OverleafClient({ sections }: { sections: OverleafSections }) {
|
|
22
|
+
const [active, setActive] = useState<SectionKey>('executive_summary');
|
|
23
|
+
const [copied, setCopied] = useState(false);
|
|
24
|
+
|
|
25
|
+
const content = sections[active];
|
|
26
|
+
|
|
27
|
+
const copy = async () => {
|
|
28
|
+
if (!content) return;
|
|
29
|
+
try {
|
|
30
|
+
await navigator.clipboard.writeText(content);
|
|
31
|
+
setCopied(true);
|
|
32
|
+
setTimeout(() => setCopied(false), 1500);
|
|
33
|
+
} catch {
|
|
34
|
+
/* clipboard blocked (insecure context) — no-op */
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<header className="mb-6">
|
|
41
|
+
<h1 className="text-title font-semibold text-text-primary">Overleaf</h1>
|
|
42
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
43
|
+
LaTeX report sections from <span className="font-mono text-caption">.hex/overleaf/</span>.
|
|
44
|
+
Copy each into the matching slot of the Nethermind Overleaf template.
|
|
45
|
+
</p>
|
|
46
|
+
</header>
|
|
47
|
+
|
|
48
|
+
{/* Segmented control */}
|
|
49
|
+
<div className="mb-4 flex flex-wrap gap-1.5">
|
|
50
|
+
{TABS.map((tab) => {
|
|
51
|
+
const isActive = tab.key === active;
|
|
52
|
+
const present = Boolean(sections[tab.key]);
|
|
53
|
+
return (
|
|
54
|
+
<button
|
|
55
|
+
key={tab.key}
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => setActive(tab.key)}
|
|
58
|
+
className={`rounded-md px-3 py-1.5 text-caption font-medium ${
|
|
59
|
+
isActive
|
|
60
|
+
? 'bg-accent text-surface-0'
|
|
61
|
+
: 'bg-surface-2 text-text-secondary hover:text-text-primary'
|
|
62
|
+
} ${present ? '' : 'opacity-50'}`}
|
|
63
|
+
>
|
|
64
|
+
{tab.label}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1">
|
|
71
|
+
<div className="flex items-center justify-between border-b border-border-subtle px-4 py-2.5">
|
|
72
|
+
<span className="font-mono text-caption text-text-tertiary">
|
|
73
|
+
{TABS.find((t) => t.key === active)?.file}
|
|
74
|
+
</span>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
onClick={copy}
|
|
78
|
+
disabled={!content}
|
|
79
|
+
className="rounded-md border border-border-default px-2.5 py-1 text-caption text-text-secondary hover:bg-surface-2 hover:text-text-primary disabled:opacity-50"
|
|
80
|
+
>
|
|
81
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
{content ? (
|
|
85
|
+
<pre className="max-h-[70vh] overflow-auto p-4 font-mono text-caption leading-relaxed text-text-secondary whitespace-pre-wrap break-words">
|
|
86
|
+
{content}
|
|
87
|
+
</pre>
|
|
88
|
+
) : (
|
|
89
|
+
<p className="p-4 text-body text-text-tertiary">
|
|
90
|
+
This section has not been generated yet. Run{' '}
|
|
91
|
+
<span className="font-mono text-caption">/generate-overleaf</span>.
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readMarkdownFile } from '@/lib/data';
|
|
2
|
+
import { NotYetGenerated } from '@/components/NotYetGenerated';
|
|
3
|
+
import { OverleafClient, type OverleafSections } from './OverleafClient';
|
|
4
|
+
|
|
5
|
+
// Read .hex/overleaf/ at request time, not at build time.
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
7
|
+
|
|
8
|
+
export default function OverleafPage() {
|
|
9
|
+
const sections: OverleafSections = {
|
|
10
|
+
executive_summary: readMarkdownFile('overleaf/executive_summary.txt'),
|
|
11
|
+
audited_files: readMarkdownFile('overleaf/audited_files.txt'),
|
|
12
|
+
summary_of_findings: readMarkdownFile('overleaf/summary_of_findings.txt'),
|
|
13
|
+
findings: readMarkdownFile('overleaf/findings.txt'),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const anyPresent = Object.values(sections).some(Boolean);
|
|
17
|
+
if (!anyPresent) {
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<header className="mb-6">
|
|
21
|
+
<h1 className="text-title font-semibold text-text-primary">Overleaf</h1>
|
|
22
|
+
<p className="mt-1 text-body text-text-secondary">
|
|
23
|
+
LaTeX report sections, ready to paste into the Nethermind Overleaf template.
|
|
24
|
+
</p>
|
|
25
|
+
</header>
|
|
26
|
+
<NotYetGenerated command="/generate-overleaf" />
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return <OverleafClient sections={sections} />;
|
|
32
|
+
}
|
|
@@ -326,7 +326,13 @@ const NAV_GROUPS = [
|
|
|
326
326
|
{ href: '/flows', label: 'Flows', icon: IconFlows },
|
|
327
327
|
],
|
|
328
328
|
},
|
|
329
|
-
{
|
|
329
|
+
{
|
|
330
|
+
label: 'Issues',
|
|
331
|
+
items: [
|
|
332
|
+
{ href: '/issues', label: 'Board', icon: IconFindings },
|
|
333
|
+
{ href: '/overleaf', label: 'Overleaf', icon: IconReport },
|
|
334
|
+
],
|
|
335
|
+
},
|
|
330
336
|
];
|
|
331
337
|
|
|
332
338
|
export { NAV_GROUPS };
|
|
@@ -395,7 +401,7 @@ export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
|
395
401
|
<div className="px-5 pb-4 pt-6">
|
|
396
402
|
<div className="flex items-baseline gap-2">
|
|
397
403
|
<span className="text-title font-semibold text-text-primary">Hex</span>
|
|
398
|
-
<span className="text-caption text-text-tertiary">v0.
|
|
404
|
+
<span className="text-caption text-text-tertiary">v0.5.0</span>
|
|
399
405
|
</div>
|
|
400
406
|
</div>
|
|
401
407
|
|