From e54b0528be34da8545a06fd2d775a20e344f1b98 Mon Sep 17 00:00:00 2001 From: amertensreplit <49614208-amertensreplit@users.noreply.replit.com> Date: Thu, 11 Jun 2026 05:07:29 +0000 Subject: [PATCH] Preview a checked file's content directly in the report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task #41: Let users click a non-binary file in the "Geprüfte Dateien" tree to view its stored text content, without re-downloading the archive. Changes: - lib/api-spec/openapi.yaml: added optional nullable `content` field to the ScanFile schema so the scan-detail response can carry stored file text. - Regenerated API codegen (api-client-react + api-zod via orval) and ran the libs typecheck. - artifacts/api-server/src/routes/scans.ts: serializeFile now returns `content` alongside the existing `hasContent`. Only used by the scan detail endpoint, so list responses are unaffected. - artifacts/skillguard/src/pages/scan-report.tsx (FilesTree): non-binary file names are now clickable buttons that open a Dialog showing the stored content in a monospace, scrollable panel (ScrollArea). Binary files (hasContent === false) render as plain text and show a clear "Keine Vorschau verfügbar (Binärdatei)." message in the dialog guard. Verification: - tsc -b passes for both skillguard and api-server. - relation + compare route tests pass (8 tests). - Confirmed via API that the detail endpoint exposes content for a text-source scan (hasContent true) and null for binary .skill archives. No new persistence: content is read from existing scan file data. Replit-Task-Id: 931befbc-6ca3-4d15-b422-ac8e9f061f9f --- artifacts/api-server/src/routes/scans.ts | 1 + .../skillguard/src/pages/scan-report.tsx | 33 +++++++++++++++++-- .../src/generated/api.schemas.ts | 5 +++ lib/api-spec/openapi.yaml | 3 ++ lib/api-zod/src/generated/api.ts | 6 ++-- lib/api-zod/src/generated/types/scanFile.ts | 5 +++ 6 files changed, 49 insertions(+), 4 deletions(-) diff --git a/artifacts/api-server/src/routes/scans.ts b/artifacts/api-server/src/routes/scans.ts index b5d380e..8279d4e 100644 --- a/artifacts/api-server/src/routes/scans.ts +++ b/artifacts/api-server/src/routes/scans.ts @@ -68,6 +68,7 @@ function serializeFile(f: ScanFile) { size: f.size, hash: f.hash, hasContent: f.content !== null, + content: f.content, }; } diff --git a/artifacts/skillguard/src/pages/scan-report.tsx b/artifacts/skillguard/src/pages/scan-report.tsx index 71e2414..41a2e61 100644 --- a/artifacts/skillguard/src/pages/scan-report.tsx +++ b/artifacts/skillguard/src/pages/scan-report.tsx @@ -17,6 +17,8 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; import { VerdictBadge, SeverityBadge, AxisBadge, CheckpointStatusBadge, CHECKPOINT_STATUS_LABELS, RelationBadge } from "@/components/ui-helpers"; import { formatDate } from "@/lib/format"; import { ShieldQuestion, ShieldAlert, AlertTriangle, Download, FileCode, CheckCircle2, Code, Shield, FileDown, ListChecks, Fingerprint, GitCompare, History, GitCommitVertical, Sparkles, Loader2, Folder, File as FileIcon, Copy, Check, ChevronRight, ChevronDown } from "lucide-react"; @@ -94,6 +96,7 @@ function FilesTree({ files }: { files: ScanReportFile[] }) { const [collapsed, setCollapsed] = useState>(() => new Set()); const rows = useMemo(() => flattenFileTree(tree, collapsed), [tree, collapsed]); const [copiedHash, setCopiedHash] = useState(null); + const [previewFile, setPreviewFile] = useState(null); const toggleFolder = (path: string) => { setCollapsed((prev) => { @@ -115,6 +118,7 @@ function FilesTree({ files }: { files: ScanReportFile[] }) { }; return ( + <>
@@ -159,10 +163,16 @@ function FilesTree({ files }: { files: ScanReportFile[] }) { ) : (
- + @@ -197,6 +207,25 @@ function FilesTree({ files }: { files: ScanReportFile[] }) {
+ !open && setPreviewFile(null)}> + + + {previewFile?.path} + + {previewFile?.language ? `${previewFile.language} · ` : ""} + {previewFile?.size} B + + + {previewFile?.hasContent && previewFile.content != null ? ( + +
{previewFile.content}
+
+ ) : ( +

Keine Vorschau verfügbar (Binärdatei).

+ )} +
+
+ ); } diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 8423c20..f400723 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -218,6 +218,11 @@ export interface ScanFile { hash: string; /** Whether the text content was stored (false for binary files) */ hasContent: boolean; + /** + * The stored text content of the file, or null for binary files + * @nullable + */ + content?: string | null; } export type FindingAxis = typeof FindingAxis[keyof typeof FindingAxis]; diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index a2a2513..81d7b95 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -607,6 +607,9 @@ components: hasContent: type: boolean description: Whether the text content was stored (false for binary files) + content: + type: ["string", "null"] + description: The stored text content of the file, or null for binary files Finding: type: object diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 30639d7..f998af5 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -232,7 +232,8 @@ export const GetScanResponse = zod.object({ "language": zod.string().nullish(), "size": zod.number(), "hash": zod.string().describe('SHA-256 hash of the file content'), - "hasContent": zod.boolean().describe('Whether the text content was stored (false for binary files)') + "hasContent": zod.boolean().describe('Whether the text content was stored (false for binary files)'), + "content": zod.string().nullish().describe('The stored text content of the file, or null for binary files') })), "findings": zod.array(zod.object({ "id": zod.number(), @@ -317,7 +318,8 @@ export const GenerateScanDescriptionResponse = zod.object({ "language": zod.string().nullish(), "size": zod.number(), "hash": zod.string().describe('SHA-256 hash of the file content'), - "hasContent": zod.boolean().describe('Whether the text content was stored (false for binary files)') + "hasContent": zod.boolean().describe('Whether the text content was stored (false for binary files)'), + "content": zod.string().nullish().describe('The stored text content of the file, or null for binary files') })), "findings": zod.array(zod.object({ "id": zod.number(), diff --git a/lib/api-zod/src/generated/types/scanFile.ts b/lib/api-zod/src/generated/types/scanFile.ts index feeb72a..379d980 100644 --- a/lib/api-zod/src/generated/types/scanFile.ts +++ b/lib/api-zod/src/generated/types/scanFile.ts @@ -17,4 +17,9 @@ export interface ScanFile { hash: string; /** Whether the text content was stored (false for binary files) */ hasContent: boolean; + /** + * The stored text content of the file, or null for binary files + * @nullable + */ + content?: string | null; }