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 |