Skip to content

Commit 05b3f4c

Browse files
committed
feat(frontend): add report summary view
1 parent 52cf2a8 commit 05b3f4c

2 files changed

Lines changed: 171 additions & 9 deletions

File tree

apps/frontend/src/app/globals.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,66 @@ body {
335335
box-shadow: var(--ios-shadow);
336336
}
337337

338+
.report-stack {
339+
display: grid;
340+
gap: 12px;
341+
}
342+
343+
.report-summary {
344+
display: grid;
345+
grid-template-columns: repeat(2, minmax(0, 1fr));
346+
gap: 12px;
347+
background: rgba(255, 255, 255, 0.6);
348+
border-radius: 16px;
349+
padding: 12px;
350+
border: 1px solid var(--ios-outline);
351+
}
352+
353+
.summary-label {
354+
font-size: 11px;
355+
color: rgba(11, 11, 22, 0.55);
356+
text-transform: uppercase;
357+
letter-spacing: 0.08em;
358+
}
359+
360+
.summary-value {
361+
font-size: 18px;
362+
font-weight: 700;
363+
}
364+
365+
.finding-list {
366+
list-style: none;
367+
padding: 0;
368+
margin: 0;
369+
display: grid;
370+
gap: 10px;
371+
}
372+
373+
.finding-list li {
374+
display: grid;
375+
gap: 4px;
376+
padding: 10px;
377+
border-radius: 12px;
378+
background: rgba(140, 82, 255, 0.08);
379+
}
380+
381+
.finding-list li span {
382+
font-size: 12px;
383+
color: rgba(11, 11, 22, 0.65);
384+
}
385+
386+
.finding-empty {
387+
text-align: center;
388+
font-size: 12px;
389+
color: rgba(11, 11, 22, 0.6);
390+
}
391+
392+
.report-actions {
393+
display: flex;
394+
flex-wrap: wrap;
395+
gap: 10px;
396+
}
397+
338398
.result-card pre {
339399
margin: 0;
340400
max-height: 220px;

apps/frontend/src/app/page.tsx

Lines changed: 111 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

3-
import { useRef, useState } from "react";
3+
import { useMemo, useRef, useState } from "react";
44

55
export default function Home() {
66
const fileRef = useRef<HTMLInputElement>(null);
77
const [selectedFile, setSelectedFile] = useState<File | null>(null);
88
const [status, setStatus] = useState<string | null>(null);
9-
const [result, setResult] = useState<string | null>(null);
9+
const [result, setResult] = useState<Record<string, unknown> | null>(null);
10+
const [rawResult, setRawResult] = useState<string | null>(null);
1011
const [isUploading, setIsUploading] = useState(false);
1112

1213
const handleChooseFile = () => {
@@ -18,6 +19,7 @@ export default function Home() {
1819
setSelectedFile(file);
1920
setStatus(file ? `Selected ${file.name}` : "No file selected");
2021
setResult(null);
22+
setRawResult(null);
2123
};
2224

2325
const handleUpload = async () => {
@@ -55,24 +57,53 @@ export default function Home() {
5557
? String((payload as { error?: string }).error)
5658
: `Scan failed (${response.status})`;
5759
setStatus(message);
58-
setResult(rawText || null);
60+
setResult(null);
61+
setRawResult(rawText || null);
5962
return;
6063
}
6164

6265
setStatus("Scan complete");
6366
if (payload && typeof payload === "object") {
64-
setResult(JSON.stringify(payload, null, 2));
67+
setResult(payload as Record<string, unknown>);
68+
setRawResult(JSON.stringify(payload, null, 2));
6569
} else {
66-
setResult(rawText || null);
70+
setResult(null);
71+
setRawResult(rawText || null);
6772
}
6873
} catch (error) {
6974
setStatus("Failed to reach backend. Is it running on :7070?");
7075
setResult(null);
76+
setRawResult(null);
7177
} finally {
7278
setIsUploading(false);
7379
}
7480
};
7581

82+
const summary = useMemo(() => {
83+
const report = result?.report as
84+
| { results?: Array<Record<string, unknown>>; total_duration_ms?: number }
85+
| undefined;
86+
const results = report?.results ?? [];
87+
const failures = results.filter((item) => {
88+
const status = item.status as string | undefined;
89+
return status === "Fail" || status === "Error";
90+
});
91+
const errorCount = failures.filter((item) => item.severity === "Error").length;
92+
const warningCount = failures.filter((item) => item.severity === "Warning").length;
93+
const duration =
94+
typeof report?.total_duration_ms === "number"
95+
? `${report.total_duration_ms}ms`
96+
: null;
97+
98+
return {
99+
results,
100+
failures,
101+
errorCount,
102+
warningCount,
103+
duration,
104+
};
105+
}, [result]);
106+
76107
return (
77108
<div className="page">
78109
<div className="page-glow page-glow--left" />
@@ -177,10 +208,81 @@ export default function Home() {
177208
</div>
178209
{status ? <div className="status-pill">{status}</div> : null}
179210
{result ? (
180-
<details className="result-card" open>
181-
<summary className="result-header">Latest report</summary>
182-
<pre>{result}</pre>
183-
</details>
211+
<div className="report-stack">
212+
<div className="report-summary">
213+
<div>
214+
<div className="summary-label">Errors</div>
215+
<div className="summary-value">{summary.errorCount}</div>
216+
</div>
217+
<div>
218+
<div className="summary-label">Warnings</div>
219+
<div className="summary-value">{summary.warningCount}</div>
220+
</div>
221+
<div>
222+
<div className="summary-label">Findings</div>
223+
<div className="summary-value">{summary.failures.length}</div>
224+
</div>
225+
<div>
226+
<div className="summary-label">Duration</div>
227+
<div className="summary-value">{summary.duration ?? "—"}</div>
228+
</div>
229+
</div>
230+
231+
<div className="result-card">
232+
<div className="result-header">Top findings</div>
233+
<ul className="finding-list">
234+
{summary.failures.slice(0, 5).map((item, index) => (
235+
<li key={`${item.rule_id ?? "rule"}-${index}`}>
236+
<strong>{String(item.rule_name ?? "Untitled rule")}</strong>
237+
<span>{String(item.recommendation ?? "Review this rule")}</span>
238+
</li>
239+
))}
240+
{summary.failures.length === 0 ? (
241+
<li className="finding-empty">No failing rules detected.</li>
242+
) : null}
243+
</ul>
244+
</div>
245+
246+
<div className="result-card">
247+
<div className="result-header">Report actions</div>
248+
<div className="report-actions">
249+
<button
250+
className="secondary-button"
251+
type="button"
252+
onClick={() => {
253+
if (!rawResult) return;
254+
const blob = new Blob([rawResult], { type: "application/json" });
255+
const url = URL.createObjectURL(blob);
256+
const link = document.createElement("a");
257+
link.href = url;
258+
link.download = "verifyos-report.json";
259+
link.click();
260+
URL.revokeObjectURL(url);
261+
}}
262+
>
263+
Download JSON
264+
</button>
265+
<button
266+
className="ghost-button"
267+
type="button"
268+
onClick={() => {
269+
if (!rawResult) return;
270+
void navigator.clipboard?.writeText(rawResult);
271+
setStatus("Report copied to clipboard");
272+
}}
273+
>
274+
Copy JSON
275+
</button>
276+
</div>
277+
</div>
278+
279+
{rawResult ? (
280+
<details className="result-card">
281+
<summary className="result-header">Raw report</summary>
282+
<pre>{rawResult}</pre>
283+
</details>
284+
) : null}
285+
</div>
184286
) : null}
185287
<div className="card-footer">
186288
<div>

0 commit comments

Comments
 (0)