From 434ec07885bf2df9a62ae95e0f617908285d038e Mon Sep 17 00:00:00 2001 From: Replit Agent Date: Wed, 10 Jun 2026 18:53:17 +0000 Subject: [PATCH] Add live progress updates and detailed scan checkpoints to scan results Introduce streaming endpoint for NDJSON scan progress, incorporate scan checkpoints into scan details, and update UI components to display this new information. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 0d01f99a-ea6a-447d-82fd-311715434a39 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 2852b526-3bf8-4a93-a62a-a50e26291074 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/e32d2b99-1721-47dd-833c-98b372f48008/0d01f99a-ea6a-447d-82fd-311715434a39/8MCgDZm Replit-Helium-Checkpoint-Created: true --- .agents/memory/MEMORY.md | 1 + .../memory/ndjson-streaming-express-replit.md | 36 ++ .agents/memory/openai-temperature-gpt5.md | 5 +- artifacts/api-server/src/lib/scanEngine.ts | 116 +++++- artifacts/api-server/src/routes/scans.ts | 209 ++++++++--- artifacts/skillguard/public/opengraph.jpg | Bin 18404 -> 82375 bytes .../skillguard/src/components/ui-helpers.tsx | 24 +- artifacts/skillguard/src/lib/streamScan.ts | 119 ++++++ artifacts/skillguard/src/pages/scan-form.tsx | 347 +++++++++++++++--- .../skillguard/src/pages/scan-report.tsx | 74 +++- .../src/generated/api.schemas.ts | 65 ++++ lib/api-spec/openapi.yaml | 36 +- lib/api-zod/src/generated/api.ts | 13 +- lib/api-zod/src/generated/types/index.ts | 5 + .../src/generated/types/scanCheckpoint.ts | 29 ++ .../src/generated/types/scanCheckpointAxis.ts | 18 + .../types/scanCheckpointDetectedBy.ts | 18 + .../generated/types/scanCheckpointSeverity.ts | 21 ++ .../generated/types/scanCheckpointStatus.ts | 17 + lib/api-zod/src/generated/types/scanDetail.ts | 2 + lib/db/src/schema/scans.ts | 19 + 21 files changed, 1064 insertions(+), 110 deletions(-) create mode 100644 .agents/memory/ndjson-streaming-express-replit.md create mode 100644 artifacts/skillguard/src/lib/streamScan.ts create mode 100644 lib/api-zod/src/generated/types/scanCheckpoint.ts create mode 100644 lib/api-zod/src/generated/types/scanCheckpointAxis.ts create mode 100644 lib/api-zod/src/generated/types/scanCheckpointDetectedBy.ts create mode 100644 lib/api-zod/src/generated/types/scanCheckpointSeverity.ts create mode 100644 lib/api-zod/src/generated/types/scanCheckpointStatus.ts diff --git a/.agents/memory/MEMORY.md b/.agents/memory/MEMORY.md index 5793f2b..5968210 100644 --- a/.agents/memory/MEMORY.md +++ b/.agents/memory/MEMORY.md @@ -1,2 +1,3 @@ - [lucide-react icon name collisions](lucide-icon-name-collisions.md) — `Badge`/`Activity` from lucide collide with shadcn/ui Badge and React 19 Activity; import Badge from ui, Activity from lucide. - [OpenAI gpt-5 temperature](openai-temperature-gpt5.md) — gpt-5* reject `temperature != 1`; omit temperature in OpenAI-compatible clients or AI analysis silently fails. +- [NDJSON streaming on Replit](ndjson-streaming-express-replit.md) — use `res.on("close")`+`writableFinished` (NOT `req.on("close")`); persist on disconnect; proxy doesn't buffer; gate fallback to avoid dup rows. diff --git a/.agents/memory/ndjson-streaming-express-replit.md b/.agents/memory/ndjson-streaming-express-replit.md new file mode 100644 index 0000000..aeb823f --- /dev/null +++ b/.agents/memory/ndjson-streaming-express-replit.md @@ -0,0 +1,36 @@ +--- +name: NDJSON streaming under Express behind the Replit proxy +description: Pitfalls for live/streaming HTTP responses (NDJSON/SSE) from an Express API on Replit — client-disconnect detection, persistence, and proxy buffering +--- + +# Streaming responses (NDJSON) from Express on Replit + +For a long-lived POST that streams progress (e.g. `application/x-ndjson` with +`res.flushHeaders()` then line-delimited JSON writes): + +**Detect a real client abort with `res.on("close")` + `res.writableFinished` — never +`req.on("close")`.** +`req.on("close")` fires as soon as the POST *request body* has been fully consumed, +which is normal and happens immediately. Gating writes on that wrongly suppresses the +entire stream. Use the *response* close event, and treat it as an abort only when +`!res.writableFinished`. + +**Persist the result regardless of disconnect.** Run the analysis and the DB write +inside the handler's `try` unconditionally; only guard the `res.write()` calls on +"connection still open". This way a client that disconnects mid-stream still gets its +result persisted (and the client must NOT re-submit on a mid-stream error, or it +creates a duplicate row — see below). + +**The Replit path-proxy does NOT buffer the stream.** Incremental flushes arrive line +by line through `$REPLIT_DEV_DOMAIN`, same as `localhost`. No special proxy header or +chunk-padding needed. + +**Client fetch must be root-relative (`/api/...`), not `import.meta.env.BASE_URL`.** +That matches the generated API client and is what routes correctly through the proxy. + +**Fallback gating to avoid duplicate persists:** a client that streams and also falls +back to a non-streaming POST on error must only fall back when the server never +processed the request (fetch rejected, or `!res.ok` with a 5xx). Once the response +body has started streaming, any read error means the server is already persisting — +falling back then duplicates the record. Distinguish 4xx (show message, no retry) +from transport failures. diff --git a/.agents/memory/openai-temperature-gpt5.md b/.agents/memory/openai-temperature-gpt5.md index 625c1b8..e5da2e5 100644 --- a/.agents/memory/openai-temperature-gpt5.md +++ b/.agents/memory/openai-temperature-gpt5.md @@ -16,6 +16,5 @@ rather than hardcoding a low value for determinism. **Why:** older models accepted `temperature: 0.1` but gpt-5* only accept the default. A hardcoded low temperature breaks every scan against modern models. -**How to apply:** in `artifacts/api-server/src/lib/aiAnalysis.ts` the OpenAI-compatible -path no longer sends `temperature`. If reintroducing sampling controls, gate them per -model or make them optional. +**How to apply:** in the OpenAI-compatible AI-analysis path, do not send `temperature`. +If reintroducing sampling controls, gate them per model or make them optional. diff --git a/artifacts/api-server/src/lib/scanEngine.ts b/artifacts/api-server/src/lib/scanEngine.ts index 377f1e7..92e23a7 100644 --- a/artifacts/api-server/src/lib/scanEngine.ts +++ b/artifacts/api-server/src/lib/scanEngine.ts @@ -15,9 +15,14 @@ import { type Severity, type Axis, } from "./ruleCatalog"; -import type { FindingCounts as DbFindingCounts } from "@workspace/db"; +import type { + FindingCounts as DbFindingCounts, + ScanCheckpoint, +} from "@workspace/db"; import { runAiAnalysis, type AiRuleConfig } from "./aiAnalysis"; +export type { ScanCheckpoint } from "@workspace/db"; + const SEVERITY_WEIGHT: Record = { critical: 50, high: 18, @@ -26,9 +31,18 @@ const SEVERITY_WEIGHT: Record = { info: 0, }; +export type ScanProgressEvent = + | { type: "ai-start" } + | { type: "checkpoint"; checkpoint: ScanCheckpoint }; + +export type ProgressFn = ( + event: ScanProgressEvent, +) => void | Promise; + export type EngineResult = { findings: RawFinding[]; counts: DbFindingCounts; + checkpoints: ScanCheckpoint[]; riskScore: number; verdict: "pass" | "review" | "block"; aiUsed: boolean; @@ -70,29 +84,75 @@ export function computeVerdict( return "pass"; } +function scoreOf(findings: RawFinding[]): number { + return findings.reduce((s, f) => s + SEVERITY_WEIGHT[f.severity], 0); +} + export async function analyzeSkill( files: ParsedFile[], useAi: boolean, + onProgress?: ProgressFn, ): Promise { const dbRules = await db.select().from(rulesTable); const ruleConfig = new Map( - dbRules.map((r) => [r.ruleId, { enabled: r.enabled, severity: r.severity as Severity }]), + dbRules.map((r) => [ + r.ruleId, + { enabled: r.enabled, severity: r.severity as Severity }, + ]), ); const findings: RawFinding[] = []; + const checkpoints: ScanCheckpoint[] = []; + for (const rule of STATIC_RULES) { const cfg = ruleConfig.get(rule.ruleId); - if (cfg && !cfg.enabled) continue; const severity = cfg?.severity ?? rule.defaultSeverity; - for (const file of files) { - findings.push(...runStaticRule(rule, file, severity)); + + if (cfg && !cfg.enabled) { + const checkpoint: ScanCheckpoint = { + id: rule.ruleId, + label: rule.title, + category: rule.category, + axis: rule.axis, + severity, + status: "skipped", + findingCount: 0, + scoreDelta: 0, + detectedBy: "static", + }; + checkpoints.push(checkpoint); + await onProgress?.({ type: "checkpoint", checkpoint }); + continue; } + + const ruleFindings: RawFinding[] = []; + for (const file of files) { + ruleFindings.push(...runStaticRule(rule, file, severity)); + } + findings.push(...ruleFindings); + + const checkpoint: ScanCheckpoint = { + id: rule.ruleId, + label: rule.title, + category: rule.category, + axis: rule.axis, + severity, + status: ruleFindings.length > 0 ? "flagged" : "pass", + findingCount: ruleFindings.length, + scoreDelta: scoreOf(ruleFindings), + detectedBy: "static", + }; + checkpoints.push(checkpoint); + await onProgress?.({ type: "checkpoint", checkpoint }); } let aiUsed = false; let aiError: string | null = null; + let aiFindings: RawFinding[] = []; if (useAi) { + await onProgress?.({ type: "ai-start" }); + const aiRuleIds = new Set(AI_RULES.map((r) => r.ruleId)); const enabledAiRules: AiRuleConfig[] = AI_RULES.filter((rule) => { const cfg = ruleConfig.get(rule.ruleId); @@ -131,14 +191,58 @@ export async function analyzeSkill( aiError = result.error; if (!result.error) { aiUsed = true; + aiFindings = result.findings; findings.push(...result.findings); } } + + for (const rule of AI_RULES) { + const cfg = ruleConfig.get(rule.ruleId); + const severity = cfg?.severity ?? rule.defaultSeverity; + const enabled = cfg ? cfg.enabled : true; + + let status: ScanCheckpoint["status"]; + let findingCount = 0; + let scoreDelta = 0; + + if (!enabled) { + status = "skipped"; + } else if (!aiUsed) { + status = "error"; + } else { + const ruleFindings = aiFindings.filter((f) => f.ruleId === rule.ruleId); + findingCount = ruleFindings.length; + scoreDelta = scoreOf(ruleFindings); + status = findingCount > 0 ? "flagged" : "pass"; + } + + const checkpoint: ScanCheckpoint = { + id: rule.ruleId, + label: rule.title, + category: rule.category, + axis: rule.axis, + severity, + status, + findingCount, + scoreDelta, + detectedBy: "ai", + }; + checkpoints.push(checkpoint); + await onProgress?.({ type: "checkpoint", checkpoint }); + } } const riskScore = computeScore(findings); const counts = computeCounts(findings); const verdict = computeVerdict(findings, riskScore); - return { findings, counts, riskScore, verdict, aiUsed, aiError }; + return { + findings, + counts, + checkpoints, + riskScore, + verdict, + aiUsed, + aiError, + }; } diff --git a/artifacts/api-server/src/routes/scans.ts b/artifacts/api-server/src/routes/scans.ts index 6627569..798a47d 100644 --- a/artifacts/api-server/src/routes/scans.ts +++ b/artifacts/api-server/src/routes/scans.ts @@ -22,12 +22,14 @@ import { parseText, deriveScanName, } from "../lib/skillParser"; -import { analyzeSkill } from "../lib/scanEngine"; -import type { ParsedFile } from "../lib/ruleCatalog"; +import { analyzeSkill, type EngineResult } from "../lib/scanEngine"; +import { STATIC_RULES, AI_RULES, type ParsedFile } from "../lib/ruleCatalog"; import { logger } from "../lib/logger"; const router: IRouter = Router(); +type CreateScanInput = ReturnType; + export function serializeScan(scan: Scan) { return { id: scan.id, @@ -69,32 +71,33 @@ function serializeFinding(f: Finding) { }; } -router.get("/scans", async (_req, res) => { - const rows = await db - .select() - .from(scansTable) - .orderBy(desc(scansTable.createdAt)); - res.json(ListScansResponse.parse(rows.map(serializeScan))); -}); +function serializeScanDetail( + scan: Scan, + files: ScanFile[], + findings: Finding[], +) { + return { + ...serializeScan(scan), + checkpoints: scan.checkpoints ?? [], + files: files.map(serializeFile), + findings: [...findings].sort((a, b) => a.id - b.id).map(serializeFinding), + }; +} -router.post("/scans", async (req, res) => { - const parsed = CreateScanBody.safeParse(req.body); - if (!parsed.success) { - return res - .status(400) - .json({ message: "Ungültige Eingabe", details: parsed.error.issues }); - } - const input = parsed.data; +type ParseResult = + | { ok: true; files: ParsedFile[] } + | { ok: false; status: number; message: string }; - let files: ParsedFile[] = []; +function parseScanInput(input: CreateScanInput): ParseResult { try { + let files: ParsedFile[]; if (input.source === "zip") { if (!input.contentBase64) - return res.status(400).json({ message: "ZIP-Inhalt fehlt." }); + return { ok: false, status: 400, message: "ZIP-Inhalt fehlt." }; files = parseZip(Buffer.from(input.contentBase64, "base64")); } else if (input.source === "file") { if (!input.contentBase64) - return res.status(400).json({ message: "Dateiinhalt fehlt." }); + return { ok: false, status: 400, message: "Dateiinhalt fehlt." }; files = [ parseSingleFile( input.filename ?? "datei", @@ -103,26 +106,33 @@ router.post("/scans", async (req, res) => { ]; } else { if (!input.text || !input.text.trim()) - return res.status(400).json({ message: "Text fehlt." }); + return { ok: false, status: 400, message: "Text fehlt." }; files = [parseText(input.text, input.name ?? "SKILL.md")]; } + if (files.length === 0) + return { + ok: false, + status: 400, + message: "Keine analysierbaren Dateien gefunden.", + }; + return { ok: true, files }; } catch (err) { logger.error({ err }, "Skill-Parsing fehlgeschlagen"); - return res.status(400).json({ + return { + ok: false, + status: 400, message: "Das Skill konnte nicht gelesen werden. Bitte prüfen Sie das Format (gültiges ZIP / Textdatei).", - }); + }; } +} - if (files.length === 0) { - return res - .status(400) - .json({ message: "Keine analysierbaren Dateien gefunden." }); - } - - const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill"); - const result = await analyzeSkill(files, input.useAi); - +async function persistScan( + input: CreateScanInput, + name: string, + files: ParsedFile[], + result: EngineResult, +): Promise<{ scan: Scan; files: ScanFile[]; findings: Finding[] }> { const [scan] = await db .insert(scansTable) .values({ @@ -135,6 +145,7 @@ router.post("/scans", async (req, res) => { aiUsed: result.aiUsed, aiError: result.aiError, findingCounts: result.counts, + checkpoints: result.checkpoints, }) .returning(); @@ -176,14 +187,127 @@ router.post("/scans", async (req, res) => { .returning(); } - const payload = { - ...serializeScan(scan), - files: insertedFiles.map(serializeFile), - findings: insertedFindings - .sort((a, b) => a.id - b.id) - .map(serializeFinding), + return { scan, files: insertedFiles, findings: insertedFindings }; +} + +router.get("/scans", async (_req, res) => { + const rows = await db + .select() + .from(scansTable) + .orderBy(desc(scansTable.createdAt)); + res.json(ListScansResponse.parse(rows.map(serializeScan))); +}); + +router.post("/scans", async (req, res) => { + const parsed = CreateScanBody.safeParse(req.body); + if (!parsed.success) { + return res + .status(400) + .json({ message: "Ungültige Eingabe", details: parsed.error.issues }); + } + const input = parsed.data; + + const parseResult = parseScanInput(input); + if (!parseResult.ok) { + return res.status(parseResult.status).json({ message: parseResult.message }); + } + const files = parseResult.files; + + const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill"); + const result = await analyzeSkill(files, input.useAi); + const { scan, files: insertedFiles, findings } = await persistScan( + input, + name, + files, + result, + ); + + return res + .status(201) + .json(GetScanResponse.parse(serializeScanDetail(scan, insertedFiles, findings))); +}); + +const STREAM_PACING_MS = 80; +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +router.post("/scans/stream", async (req, res) => { + const parsed = CreateScanBody.safeParse(req.body); + if (!parsed.success) { + res + .status(400) + .json({ message: "Ungültige Eingabe", details: parsed.error.issues }); + return; + } + const input = parsed.data; + + const parseResult = parseScanInput(input); + if (!parseResult.ok) { + res.status(parseResult.status).json({ message: parseResult.message }); + return; + } + const files = parseResult.files; + const name = input.name?.trim() || deriveScanName(files, "Unbenanntes Skill"); + + res.status(200); + res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8"); + res.setHeader("Cache-Control", "no-cache, no-transform"); + res.setHeader("X-Accel-Buffering", "no"); + res.setHeader("Connection", "keep-alive"); + res.flushHeaders(); + + // Detect a genuine client disconnect. NOTE: do NOT use req.on("close") here — + // for a POST it fires as soon as the request body is consumed, not on abort. + // res "close" before writableFinished means the client went away. + let aborted = false; + res.on("close", () => { + if (!res.writableFinished) aborted = true; + }); + + const write = (obj: unknown) => { + if (aborted || res.writableEnded) return; + res.write(JSON.stringify(obj) + "\n"); }; - return res.status(201).json(GetScanResponse.parse(payload)); + + write({ + type: "start", + name, + fileCount: files.length, + totalChecks: STATIC_RULES.length + (input.useAi ? AI_RULES.length : 0), + }); + + let cumulative = 0; + try { + const result = await analyzeSkill(files, input.useAi, async (event) => { + if (event.type === "ai-start") { + write({ type: "ai-start" }); + return; + } + cumulative += event.checkpoint.scoreDelta; + write({ + type: "checkpoint", + checkpoint: event.checkpoint, + runningScore: Math.min(100, cumulative), + }); + if (!aborted) await delay(STREAM_PACING_MS); + }); + + const { scan } = await persistScan(input, name, files, result); + + write({ + type: "done", + scanId: scan.id, + riskScore: result.riskScore, + verdict: result.verdict, + findingCounts: result.counts, + aiUsed: result.aiUsed, + aiError: result.aiError, + }); + if (!aborted && !res.writableEnded) res.end(); + } catch (err) { + logger.error({ err }, "Streaming-Scan fehlgeschlagen"); + write({ type: "error", message: "Die Analyse ist fehlgeschlagen." }); + if (!aborted && !res.writableEnded) res.end(); + } }); router.get("/scans/:id", async (req, res) => { @@ -207,12 +331,7 @@ router.get("/scans/:id", async (req, res) => { .where(eq(findingsTable.scanId, scan.id)) .orderBy(findingsTable.id); - const payload = { - ...serializeScan(scan), - files: files.map(serializeFile), - findings: findings.map(serializeFinding), - }; - return res.json(GetScanResponse.parse(payload)); + return res.json(GetScanResponse.parse(serializeScanDetail(scan, files, findings))); }); router.delete("/scans/:id", async (req, res) => { diff --git a/artifacts/skillguard/public/opengraph.jpg b/artifacts/skillguard/public/opengraph.jpg index 01f64318e68c4cd056074cd36b12413a8766b4f3..38feab2cef24bc0fc621492bb2955f9d1814c6cd 100644 GIT binary patch literal 82375 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 Bhb#a9 literal 18404 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 diff --git a/artifacts/skillguard/src/components/ui-helpers.tsx b/artifacts/skillguard/src/components/ui-helpers.tsx index 2e7b773..04b137c 100644 --- a/artifacts/skillguard/src/components/ui-helpers.tsx +++ b/artifacts/skillguard/src/components/ui-helpers.tsx @@ -1,5 +1,27 @@ import { Badge } from "@/components/ui/badge"; -import { ShieldCheck, ShieldAlert, Shield, AlertTriangle, Info, AlertCircle, AlertOctagon } from "lucide-react"; +import { ShieldCheck, ShieldAlert, Shield, AlertTriangle, Info, AlertCircle, AlertOctagon, CheckCircle2, MinusCircle, XCircle } from "lucide-react"; + +export const CHECKPOINT_STATUS_LABELS: Record = { + pass: "Unauffällig", + flagged: "Auffällig", + skipped: "Übersprungen", + error: "Fehler", +}; + +export function CheckpointStatusBadge({ status, className }: { status: string, className?: string }) { + switch (status) { + case "pass": + return Unauffällig; + case "flagged": + return Auffällig; + case "skipped": + return Übersprungen; + case "error": + return Fehler; + default: + return {status}; + } +} export function VerdictBadge({ verdict, className }: { verdict: string, className?: string }) { switch (verdict) { diff --git a/artifacts/skillguard/src/lib/streamScan.ts b/artifacts/skillguard/src/lib/streamScan.ts new file mode 100644 index 0000000..bb40031 --- /dev/null +++ b/artifacts/skillguard/src/lib/streamScan.ts @@ -0,0 +1,119 @@ +import type { + SkillScanInput, + ScanCheckpoint, + FindingCounts, +} from "@workspace/api-client-react"; + +export type ScanStreamEvent = + | { type: "start"; name: string; fileCount: number; totalChecks: number } + | { type: "ai-start" } + | { type: "checkpoint"; checkpoint: ScanCheckpoint; runningScore: number } + | { + type: "done"; + scanId: number; + riskScore: number; + verdict: string; + findingCounts: FindingCounts; + aiUsed: boolean; + aiError: string | null; + } + | { type: "error"; message: string }; + +/** + * Fehler beim Aufruf des Streaming-Endpunkts. `allowFallback` zeigt an, ob ein + * Rückfall auf den nicht-streamenden Endpunkt sinnvoll ist. Bei Client-Fehlern + * (4xx, z. B. ungültige Eingabe) ist das nicht der Fall – die Servermeldung + * soll direkt angezeigt werden, statt dieselbe Anfrage erneut zu senden. + */ +export class ScanStreamError extends Error { + allowFallback: boolean; + constructor(message: string, allowFallback: boolean) { + super(message); + this.name = "ScanStreamError"; + this.allowFallback = allowFallback; + } +} + +/** + * Streamt einen Skill-Scan über NDJSON. Ruft `onEvent` für jedes Ereignis auf, + * sobald es eintrifft (Live-Prüfschritte). Wirft bei Transportfehlern; ein vom + * Server gemeldeter Analysefehler kommt als `{ type: "error" }`-Ereignis an. + * + * Nutzt – wie der generierte API-Client – eine wurzelrelative `/api`-URL, damit + * der Replit-Pfad-Proxy die Anfrage korrekt an den API-Server weiterleitet. + */ +export async function streamScan( + input: SkillScanInput, + onEvent: (event: ScanStreamEvent) => void, + signal?: AbortSignal, +): Promise { + let res: Response; + try { + res = await fetch("/api/scans/stream", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(input), + signal, + }); + } catch (err) { + // Verbindung kam nicht zustande – der Server hat die Anfrage nicht erhalten. + // Rückfall auf den nicht-streamenden Endpunkt ist sicher und sinnvoll. + throw new ScanStreamError( + err instanceof Error ? err.message : "Verbindung zum Server fehlgeschlagen.", + true, + ); + } + + if (!res.ok || !res.body) { + let message = "Live-Analyse nicht verfügbar."; + try { + const body = await res.json(); + if (body && typeof body.message === "string") message = body.message; + } catch { + // ignore – Standardmeldung verwenden + } + // 4xx = Client-/Eingabefehler: Meldung direkt anzeigen, kein erneuter Versuch. + // 5xx oder fehlender Body: Rückfall auf den nicht-streamenden Endpunkt erlauben. + const isClientError = res.status >= 400 && res.status < 500; + throw new ScanStreamError(message, !isClientError); + } + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + + const emit = (line: string) => { + const trimmed = line.trim(); + if (!trimmed) return; + try { + onEvent(JSON.parse(trimmed) as ScanStreamEvent); + } catch { + // Unvollständige / nicht-JSON Zeile ignorieren + } + }; + + // Ab hier verarbeitet der Server die Anfrage und persistiert das Ergebnis auch + // bei Verbindungsabbruch. Ein Fehler beim Lesen darf daher NICHT zum Rückfall + // führen, sonst entsteht ein doppelter Scan-Eintrag. + try { + for (;;) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let newlineIndex: number; + while ((newlineIndex = buffer.indexOf("\n")) >= 0) { + const line = buffer.slice(0, newlineIndex); + buffer = buffer.slice(newlineIndex + 1); + emit(line); + } + } + emit(buffer); + } catch (err) { + throw new ScanStreamError( + err instanceof Error && err.name === "AbortError" + ? "Die Analyse wurde abgebrochen." + : "Die Live-Übertragung wurde unterbrochen. Der Scan läuft möglicherweise im Hintergrund weiter – bitte prüfen Sie den Verlauf.", + false, + ); + } +} diff --git a/artifacts/skillguard/src/pages/scan-form.tsx b/artifacts/skillguard/src/pages/scan-form.tsx index 807bd9b..e08686b 100644 --- a/artifacts/skillguard/src/pages/scan-form.tsx +++ b/artifacts/skillguard/src/pages/scan-form.tsx @@ -1,6 +1,11 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useLocation } from "wouter"; -import { useCreateScan, SkillScanInputSource } from "@workspace/api-client-react"; +import { + useCreateScan, + SkillScanInputSource, + type SkillScanInput, + type ScanCheckpoint, +} from "@workspace/api-client-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -8,26 +13,126 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Switch } from "@/components/ui/switch"; -import { FileUp, FileText, Type, Loader2, ShieldCheck } from "lucide-react"; +import { Progress } from "@/components/ui/progress"; +import { Badge } from "@/components/ui/badge"; +import { CheckpointStatusBadge, AxisBadge } from "@/components/ui-helpers"; +import { + FileUp, + FileText, + Type, + Loader2, + ShieldCheck, + Sparkles, + Activity, + CheckCircle2, +} from "lucide-react"; import { useToast } from "@/hooks/use-toast"; +import { streamScan, ScanStreamError, type ScanStreamEvent } from "@/lib/streamScan"; + +type Phase = "idle" | "scanning" | "done"; + +function scoreColor(score: number): string { + if (score < 30) return "text-emerald-500"; + if (score < 70) return "text-amber-500"; + return "text-rose-500"; +} + +function deltaLabel(checkpoint: ScanCheckpoint): string { + if (checkpoint.status === "skipped") return "übersprungen"; + if (checkpoint.scoreDelta > 0) return `+${checkpoint.scoreDelta} Punkte`; + return "0 Punkte"; +} export default function ScanForm() { const [, setLocation] = useLocation(); const { toast } = useToast(); const createScan = useCreateScan(); - + const [sourceType, setSourceType] = useState("file"); const [name, setName] = useState(""); const [useAi, setUseAi] = useState(false); const [file, setFile] = useState(null); const [text, setText] = useState(""); + const [phase, setPhase] = useState("idle"); + const [steps, setSteps] = useState([]); + const [runningScore, setRunningScore] = useState(0); + const [totalChecks, setTotalChecks] = useState(0); + const [aiActive, setAiActive] = useState(false); + const [finalVerdict, setFinalVerdict] = useState(null); + + const isBusy = phase === "scanning" || phase === "done" || createScan.isPending; + + const groupedSteps = useMemo(() => { + const order: string[] = []; + const groups = new Map(); + for (const step of steps) { + if (!groups.has(step.category)) { + groups.set(step.category, []); + order.push(step.category); + } + groups.get(step.category)!.push(step); + } + return order.map((category) => ({ category, steps: groups.get(category)! })); + }, [steps]); + const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files[0]) { setFile(e.target.files[0]); } }; + const buildInput = async (): Promise => { + let contentBase64: string | undefined = undefined; + let filename: string | undefined = undefined; + + if (file && (sourceType === "file" || sourceType === "zip")) { + const reader = new FileReader(); + const base64Promise = new Promise((resolve, reject) => { + reader.onload = () => { + const result = reader.result as string; + const base64 = result.split(",")[1] || result; + resolve(base64); + }; + reader.onerror = reject; + }); + reader.readAsDataURL(file); + contentBase64 = await base64Promise; + filename = file.name; + } + + return { + name: name || undefined, + source: sourceType, + useAi, + contentBase64, + filename, + text: sourceType === "text" ? text : undefined, + }; + }; + + const finishWithScan = (scanId: number) => { + setPhase("done"); + window.setTimeout(() => { + toast({ title: "Scan abgeschlossen", description: "Der Bericht wird geöffnet." }); + setLocation(`/berichte/${scanId}`); + }, 900); + }; + + const runNonStreaming = async (input: SkillScanInput) => { + try { + const data = await createScan.mutateAsync({ data: input }); + finishWithScan(data.id); + } catch (err) { + setPhase("idle"); + toast({ + title: "Fehler", + description: "Der Scan konnte nicht durchgeführt werden.", + variant: "destructive", + }); + } + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -40,49 +145,193 @@ export default function ScanForm() { return; } + let input: SkillScanInput; try { - let contentBase64: string | undefined = undefined; - let filename: string | undefined = undefined; + input = await buildInput(); + } catch { + toast({ title: "Fehler", description: "Beim Verarbeiten der Datei ist ein Fehler aufgetreten.", variant: "destructive" }); + return; + } - if (file && (sourceType === "file" || sourceType === "zip")) { - const reader = new FileReader(); - const base64Promise = new Promise((resolve, reject) => { - reader.onload = () => { - const result = reader.result as string; - // Strip data URL prefix - const base64 = result.split(',')[1] || result; - resolve(base64); - }; - reader.onerror = reject; - }); - reader.readAsDataURL(file); - contentBase64 = await base64Promise; - filename = file.name; - } + setPhase("scanning"); + setSteps([]); + setRunningScore(0); + setTotalChecks(0); + setAiActive(false); + setFinalVerdict(null); - createScan.mutate({ - data: { - name: name || undefined, - source: sourceType, - useAi, - contentBase64, - filename, - text: sourceType === "text" ? text : undefined - } - }, { - onSuccess: (data) => { - toast({ title: "Scan erfolgreich", description: "Der Skill wurde erfolgreich analysiert." }); - setLocation(`/berichte/${data.id}`); - }, - onError: (err) => { - toast({ title: "Fehler", description: "Der Scan konnte nicht durchgeführt werden.", variant: "destructive" }); + let outcome: "done" | "error" | null = null; + let doneScanId: number | null = null; + let errorMessage = "Die Analyse ist fehlgeschlagen."; + + try { + await streamScan(input, (event: ScanStreamEvent) => { + switch (event.type) { + case "start": + setTotalChecks(event.totalChecks); + break; + case "ai-start": + setAiActive(true); + break; + case "checkpoint": + setSteps((prev) => [...prev, event.checkpoint]); + setRunningScore(event.runningScore); + if (event.checkpoint.detectedBy === "ai") setAiActive(false); + break; + case "done": + outcome = "done"; + doneScanId = event.scanId; + setRunningScore(event.riskScore); + setFinalVerdict(event.verdict); + break; + case "error": + outcome = "error"; + errorMessage = event.message; + break; } }); } catch (err) { - toast({ title: "Fehler", description: "Beim Verarbeiten der Datei ist ein Fehler aufgetreten.", variant: "destructive" }); + // Rückfall nur, wenn der Server die Anfrage nicht verarbeitet hat + // (Verbindung kam nicht zustande oder 5xx). Bei Client-Fehlern (4xx) oder + // einem Abbruch während des Streamings persistiert der Server bereits – + // ein erneuter Versuch würde einen doppelten Eintrag erzeugen. + const canFallback = err instanceof ScanStreamError ? err.allowFallback : true; + if (canFallback) { + await runNonStreaming(input); + return; + } + setPhase("idle"); + toast({ + title: "Fehler", + description: err instanceof Error ? err.message : "Die Analyse ist fehlgeschlagen.", + variant: "destructive", + }); + return; + } + + if (outcome === "done" && doneScanId != null) { + finishWithScan(doneScanId); + } else if (outcome === "error") { + setPhase("idle"); + toast({ title: "Fehler", description: errorMessage, variant: "destructive" }); + } else { + // Stream endete ohne Abschluss-Ereignis: Fallback auf klassischen Scan. + await runNonStreaming(input); } }; + if (phase === "scanning" || phase === "done") { + const completed = steps.length; + const progressPct = totalChecks > 0 ? (completed / totalChecks) * 100 : 0; + return ( +

+
+

+ {phase === "done" ? "Analyse abgeschlossen" : "Analyse läuft"} +

+

+ {phase === "done" + ? "Alle Prüfschritte wurden ausgewertet. Der Bericht wird geöffnet." + : "Verfolgen Sie jeden Prüfschritt und seine Teilbewertung in Echtzeit."} +

+
+ + + +
+ + {phase === "done" ? ( + + ) : ( + + )} + Live-Risiko + +
+ + {runningScore} + + / 100 +
+
+
+ +
+
+ Prüfschritte + + {completed}{totalChecks > 0 ? ` / ${totalChecks}` : ""} + +
+ +
+ + {aiActive && ( +
+ + KI-Analyse läuft – semantische Prüfung der Instruktionen... +
+ )} + {finalVerdict && ( +
+ Vorläufiges Ergebnis:{" "} + + {finalVerdict === "pass" + ? "Freigabe" + : finalVerdict === "review" + ? "Manuelle Prüfung" + : "Blockieren"} + +
+ )} +
+
+ +
+ {groupedSteps.map((group) => ( + + + + {group.category} + + + + {group.steps.map((step) => ( +
+
+ + {step.label} + {step.axis && } + + {step.detectedBy === "ai" ? "KI" : "Statisch"} + +
+ 0 ? "text-rose-600 dark:text-rose-400" : "text-muted-foreground" + }`} + > + {deltaLabel(step)} + +
+ ))} +
+
+ ))} + {groupedSteps.length === 0 && ( +
+ + Initialisiere Prüfung... +
+ )} +
+
+ ); + } + return (
@@ -99,9 +348,9 @@ export default function ScanForm() {
- setName(e.target.value)} /> @@ -113,7 +362,7 @@ export default function ScanForm() { ZIP-Archiv Text - +
@@ -126,9 +375,9 @@ export default function ScanForm() { -