diff --git a/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.test.tsx b/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.test.tsx index 11e3064..977a8fc 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.test.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.test.tsx @@ -118,6 +118,11 @@ describe("ReportAuditView", () => { expect(markup).toContain("Completed"); expect(markup).toContain("Runtime output tail"); expect(markup).toContain("State history"); + expect(markup).toContain("data-producer-context"); + expect(markup).toContain("data-runtime-output-tail"); + expect(markup).toContain("print:break-inside-avoid"); + expect(markup).toContain("print:max-h-none"); + expect(markup).toContain("print:overflow-visible"); expect(markup).toContain("<local-path>"); expect(markup).toContain("token=<redacted>"); expect(markup).toContain("<runtime-url>"); @@ -125,4 +130,25 @@ describe("ReportAuditView", () => { expect(markup).not.toContain("abc123"); expect(markup).not.toContain("localhost:8780"); }); + + it("renders a public-safe disconnected producer state", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("Producer context"); + expect(markup).toContain("Runtime disconnected"); + expect(markup).toContain("Job detail is temporarily unavailable; the report remains based on the current public snapshot."); + expect(markup).toContain("data-producer-context"); + expect(markup).not.toContain("localhost"); + expect(markup).not.toContain("ECONNREFUSED"); + }); }); diff --git a/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.tsx b/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.tsx index 8080af3..63cb0fd 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/[track]/ReportAuditView.tsx @@ -33,6 +33,7 @@ export type ReportProducerContext = { updatedAt?: string | null; stdoutTail?: string | null; stderrTail?: string | null; + unavailable?: boolean; stateHistory?: Array<{ state: string; timestamp?: string | null; @@ -106,6 +107,9 @@ export function ReportAuditView({ producerStatus: "状态", producerUpdated: "更新时间", outputTail: "Runtime 输出尾部", + producerUnavailableTitle: "Runtime 未连接", + producerUnavailableBody: + "任务详情暂时不可用;报告仍基于当前公开快照展示。", stateHistory: "状态历史", noProvenanceData: "暂无溯源数据。", } @@ -132,6 +136,9 @@ export function ReportAuditView({ producerStatus: "Status", producerUpdated: "Updated", outputTail: "Runtime output tail", + producerUnavailableTitle: "Runtime disconnected", + producerUnavailableBody: + "Job detail is temporarily unavailable; the report remains based on the current public snapshot.", stateHistory: "State history", noProvenanceData: "No provenance data available.", }; @@ -141,11 +148,13 @@ export function ReportAuditView({ const stdoutTail = sanitizedTail(producerContext?.stdoutTail); const stderrTail = sanitizedTail(producerContext?.stderrTail); const stateHistory = producerContext?.stateHistory ?? []; + const producerUnavailable = producerContext?.unavailable === true; const hasProducerContext = !!producerContext && ( hasValue(producerStatus) || hasValue(producerContext.updatedAt) || hasValue(stdoutTail) || hasValue(stderrTail) + || producerUnavailable || stateHistory.length > 0 ); @@ -304,8 +313,23 @@ export function ReportAuditView({

{historyPlaceholder}

{hasProducerContext ? ( -
-

{t.producerTitle}

+
+
+

{t.producerTitle}

+ {producerUnavailable ? ( + + {t.producerUnavailableTitle} + + ) : null} +
+ {producerUnavailable ? ( +

+ {t.producerUnavailableBody} +

+ ) : null}
{producerStatus ? (
@@ -345,7 +369,10 @@ export function ReportAuditView({
{t.outputTail}
-
+                  
                     {[stdoutTail, stderrTail].filter(Boolean).join("\n")}
                   
diff --git a/apps/web/src/app/(workspace)/workspace/reports/[track]/page.test.tsx b/apps/web/src/app/(workspace)/workspace/reports/[track]/page.test.tsx index 579ace8..ec46535 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/[track]/page.test.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/[track]/page.test.tsx @@ -113,6 +113,36 @@ describe("TrackReportPage", () => { expect(markup).not.toContain("localhost:8780"); }); + it("shows a disconnected producer state when job detail is unavailable", async () => { + vi.stubGlobal("fetch", vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url.includes("/api/v1/audit/jobs/job_demo_004")) { + return new Response("ECONNREFUSED http://localhost:8765", { status: 503 }); + } + + return new Response(null, { status: 404 }); + })); + + const markup = renderToStaticMarkup( + await renderTrackReportPage({ + locale: "en-US", + params: { track: "black-box" }, + searchParams: { + view: "audit", + job: "job_demo_004", + contract: "recon_artifact_mainline", + model: "stable-diffusion-v1-4", + auc: "0.849", + }, + }), + ); + + expect(markup).toContain("Runtime disconnected"); + expect(markup).toContain("Job detail is temporarily unavailable; the report remains based on the current public snapshot."); + expect(markup).not.toContain("ECONNREFUSED"); + expect(markup).not.toContain("localhost:8765"); + }); + it("keeps completed job context safe when no snapshot row matches", async () => { const markup = renderToStaticMarkup( await renderTrackReportPage({ diff --git a/apps/web/src/app/(workspace)/workspace/reports/[track]/track-report-page.tsx b/apps/web/src/app/(workspace)/workspace/reports/[track]/track-report-page.tsx index 72b83a8..03caee1 100644 --- a/apps/web/src/app/(workspace)/workspace/reports/[track]/track-report-page.tsx +++ b/apps/web/src/app/(workspace)/workspace/reports/[track]/track-report-page.tsx @@ -244,12 +244,12 @@ async function fetchProducerContext(jobContext: ReturnType` tags | | Deployment traceability | Active | Gateway health exposes redacted build revision and snapshot status | | Workspace observability | Active | Shell status drawer and Settings show data mode, snapshot state, and build revision | -| Reports | Active | Evidence stack, provenance, track review links, job-linked producer context, charts, PDF and CSV export | +| Reports | Active | Evidence stack, provenance, track review links, print-safe job-linked producer context, charts, PDF and CSV export | | Demo mode | Active | Snapshot-backed demo data keeps the workspace reviewable offline | -| Runtime response facade | Active | Audit job API responses and report-side producer details are normalized through a public-safe redaction layer before rendering | +| Runtime response facade | Active | Audit job API responses and report-side producer details are normalized through a public-safe redaction layer before rendering, with generic disconnected states when Runtime is unavailable | | Workspace controls | Active | Audit filters and create-audit wizard expose named controls, step state, and reduced-motion behavior | | Public API docs | Active | Docs record snapshot-backed API contracts, optional evidence fields, and request-time fallback boundaries | @@ -29,7 +29,6 @@ This roadmap tracks product-facing Platform work. It avoids private deployment d | Priority | Track | Work | | --- | --- | --- | | P1 | Reports | Improve printable report pagination, table wrapping, and long-evidence layout | -| P1 | Audit loop | Improve report-side producer context layout for print/export and disconnected Runtime states | | P2 | Deployment | Add optional image provenance verification helpers for GHCR and local archive deployments | | P2 | Account | Polish account security state for linked providers, verified email, and password access | | P2 | Accessibility | Add menu roles, chart text summaries, and stronger focus handling in shared primitives |