diff --git a/CHANGELOG.d/2.21.2-cancelled-analysis-next-actions.md b/CHANGELOG.d/2.21.2-cancelled-analysis-next-actions.md new file mode 100644 index 000000000..5fb2e6c89 --- /dev/null +++ b/CHANGELOG.d/2.21.2-cancelled-analysis-next-actions.md @@ -0,0 +1,5 @@ +# 2.21.2 Name the next action for every cancelled analysis kind + +Cancelled lineage, TEPP, topic-lineage, and period-report runs now tell the +operator the kind-specific valid next action. A terminal cancellation no +longer leaves the analysis list and detail without guidance. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index a6a525e31..d3cfdf986 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -84,8 +84,8 @@ context only. | PR | Exact observed head | Merge/check state at this snapshot | | ---: | --- | --- | -| #667 | `dbc438e2` | baseline refresh branch exact head at this snapshot; BLOCKED with review required | -| #668 | `facbae58` | evidence-bound project history projection with normalized action keys; BLOCKED with review required | +| #667 | `a9731d52` | baseline refresh branch exact head at this snapshot; BLOCKED with review required | +| #668 | `f272f4b0` | evidence-bound project history projection with request guards and normalized keys; BLOCKED with review required | | #663 | `7ac1483e` | project ontology traversal, cutoff-snapshot project focus, bounded MCP admission, and migration-fixture/worker startup repair; BLOCKED with review required | | #658 | `f497a6e8` | evidence-honest Global Ask cutoff with robust revision timestamps; BLOCKED with review required | | #657 | `a59a2023` | TEPP lifecycle persistence; BLOCKED with review required | @@ -372,6 +372,7 @@ this file per §3.5 of the prior snapshot). | --- | --- | --- | | Protected release | Protected `main@494b54e2` includes the #660/#664 semantic-unit/backend stack and #659 ontology readability/token repair. Twelve PRs remain open; every candidate still requires exact-head checks, unresolved-thread review, and independent approval before merge | Terminal exact-head checks, no unresolved threads, two independent exact-head approvals including last-push approval, protected squash-merge SHA | | Evidence-grounded operations workspace | Protected-main #614 delivers governed semantic Ask, live Similar VOC, disjoint pending/failed analysis metrics, full Storybook state inventory, and current desktop/mobile screenshot evidence. Authorized-corpus backfill acceptance remains unavailable | Perform authenticated authorized-corpus acceptance with aggregate evidence and retain fail-closed no-match behavior | +| Cancelled analysis guidance | PRD-FR-5 requires every lifecycle state to identify a valid next action, while ADR 0013 makes Cancelled terminal. Protected `main@494b54e2` rendered Cancelled without a next action. This stacked candidate adds kind-specific guidance for lineage, TEPP, topic lineage, and period reports; 390×844 and 1440×1000 authenticated synthetic-runtime audits are retained in `docs/screenshots/cancelled-analysis-runs-{mobile,desktop}.png`. The audit also found and repaired attached count/action text and the three-column mobile squeeze | Land through the protected gate, then repeat authenticated keyboard and screen-reader acceptance on the exact release head; no cancelled run may imply that it can resume or that a measurement exists | | Shared frontend gate | The ADR 0109 login repair is on protected `main`; eight older branches carried the defect and received the same verified repair this loop (#521–#560) | Keep every future branch cut from post-repair bases; re-verify with frontend lint/test/build before push | | Identifying baseline regression | `main` gap file listed real post identifiers; separately, closed #506 and pre-existing public history contain a private runtime source-table identifier, while current `main` and #507 trees are clean | Land this non-identifying rewrite, then coordinate ADR 0001 history remediation with security/privacy owners; do not reproduce the value, force-push, or delete evidence ad hoc | | Authorized-corpus runtime | Repository tests use synthetic fixtures; private records remain outside git | Authenticated runtime validation returning only aggregate, non-identifying evidence | @@ -408,10 +409,10 @@ before a commercial release claim. Figma File ID `1Su3lDRmiZdcUs47t1QwIX`. | Touch & Interaction | Click-first popup and lists | 44px targets, swipe/escape to dismiss popup, no hover-only actions | | Performance | Board caps and hint render limits exist | Interaction-to-next-paint on board search, DAG, Ask; no N+1 query regressions | | Style Selection | Korean UI standards merged (#347) | Tokenized light/dark; Anti-Slop-UI density; no decorative noise | -| Layout & Responsive | Desktop popup shell | 402px-class phone layout; stacked GNB; readable DAG | +| Layout & Responsive | Desktop popup shell; this candidate stacks analysis-run caption, document count, and next action at 390px and retains explicit grid columns at 1440px | Complete 402px-class phone acceptance for remaining panels; stacked GNB; readable DAG | | Typography & Color | Badge tokens extracted | Contrast on badges, links, error/status; no raw hex in components | | Animation | Minimal | Reduced-motion; no blocking animation on evidence open | -| Forms & Feedback | Login, Ask, tickets, admin brand | Inline validation, next-action copy, unavailable vs failed distinction | +| Forms & Feedback | Login, Ask, tickets, admin brand; this candidate gives all four Cancelled analysis kinds an evidence-safe next action | Inline validation and unavailable-vs-failed acceptance across remaining workflows | | Navigation Patterns | Board / customers / calendar / Ask / admin | Deep-link post + OIDC return URL (#426); bookmarkable Ask | | Charts & Data | Period reports, leftover pairs, Rankings, DAG | Honest empty/unavailable; no invented theta; Storybook chart states | diff --git a/docs/screenshots/cancelled-analysis-runs-desktop.png b/docs/screenshots/cancelled-analysis-runs-desktop.png new file mode 100644 index 000000000..f1c0f5d56 Binary files /dev/null and b/docs/screenshots/cancelled-analysis-runs-desktop.png differ diff --git a/docs/screenshots/cancelled-analysis-runs-mobile.png b/docs/screenshots/cancelled-analysis-runs-mobile.png new file mode 100644 index 000000000..64ed8bd6d Binary files /dev/null and b/docs/screenshots/cancelled-analysis-runs-mobile.png differ diff --git a/frontend/src/App.css b/frontend/src/App.css index b4b65dfc8..b7482e61f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -936,6 +936,27 @@ gap: 0.5rem; } +.analysis-run-item { + display: grid; + gap: 0.75rem; +} + +.analysis-run-item.has-document-count.has-next-action { + grid-template-columns: minmax(12rem, 1fr) auto minmax(18rem, 2fr); +} + +.analysis-run-item.has-document-count:not(.has-next-action) { + grid-template-columns: minmax(12rem, 1fr) auto; +} + +.analysis-run-item.has-next-action:not(.has-document-count) { + grid-template-columns: minmax(12rem, 1fr) minmax(18rem, 2fr); +} + +.analysis-run-item:not(.has-document-count):not(.has-next-action) { + grid-template-columns: 1fr; +} + .ranking-hit { flex-direction: column; align-items: stretch; @@ -1115,6 +1136,14 @@ align-items: flex-start; gap: 0.5rem; } + + .analysis-run-item, + .analysis-run-item.has-document-count.has-next-action, + .analysis-run-item.has-document-count:not(.has-next-action), + .analysis-run-item.has-next-action:not(.has-document-count), + .analysis-run-item:not(.has-document-count):not(.has-next-action) { + grid-template-columns: 1fr; + } } .ontology-explorer { diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 2dee4513d..1110d8fb0 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor, within } from "@testing-library/rea import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import App from "./App"; +import type { AnalysisRunKindCode } from "./api"; import { setLocale } from "./i18n"; import { OIDC_RETURN_URL_STORAGE_KEY } from "./oidcReturnUrl"; @@ -106,6 +107,7 @@ describe("App, authenticated", () => { succeededReportRun?: boolean; succeededTeppRun?: boolean; pendingTeppRun?: boolean; + cancelledRunKind?: AnalysisRunKindCode; pluralAffiliations?: boolean; deferMe?: boolean; deferPostOne?: boolean; @@ -161,6 +163,27 @@ describe("App, authenticated", () => { releasePostOne = resolve; }) : Promise.resolve(); + const cancelledRun = options?.cancelledRunKind + ? { + analysis_run_id: "run-demo-cancelled", + run_kind_code: options.cancelledRunKind, + run_kind_label: { + analysis_run_lineage: "Lineage reconstruction", + analysis_run_tepp: "TEPP measurement", + analysis_run_topic_lineage: "Topic lineage", + analysis_run_report: "Period report", + }[options.cancelledRunKind], + scope_kind_code: "analysis_scope_corporate_entity", + scope_kind_label: "Corporate entity", + scope_entity_name: "Demo Corp", + status_code: "analysis_status_cancelled" as const, + status_label: "Cancelled", + knowledge_cutoff: "2026-01-12T12:00:00Z", + requested_at: "2026-01-12T12:42:00Z", + source_counts: [], + visible_posts: [], + } + : null; const fetchMock = vi.fn((input: RequestInfo | URL, init?: RequestInit) => { const url = String(input); @@ -269,6 +292,9 @@ describe("App, authenticated", () => { jsonResponse({ post_id: "post-1", has_commitment: true, ticket }), ); } + if (url.endsWith("/api/analysis-runs/run-demo-cancelled") && cancelledRun) { + return Promise.resolve(jsonResponse(cancelledRun)); + } if (url.endsWith("/api/analysis-runs/run-demo-report")) { const reportSucceeded = !options?.failedReportRun; return Promise.resolve( @@ -686,6 +712,7 @@ describe("App, authenticated", () => { return Promise.resolve( jsonResponse({ analysis_runs: [ + ...(cancelledRun ? [cancelledRun] : []), ...(createdPendingLineage ? [createdPendingLineage] : []), ...(createdPendingTepp ? [createdPendingTepp] : []), { @@ -3399,6 +3426,33 @@ describe("App, authenticated", () => { expect(screen.queryByRole("heading", { name: "Body this run knew" })).not.toBeInTheDocument(); }); + it.each([ + ["analysis_run_lineage", "Request a new lineage reconstruction from a current snapshot."], + [ + "analysis_run_tepp", + "Connect the measurement service, then ask an administrator to submit a new TEPP run from a current snapshot.", + ], + [ + "analysis_run_topic_lineage", + "Connect the TEPP transport, then ask an administrator to submit new topic-lineage analysis from a current snapshot.", + ], + ["analysis_run_report", "Rebuild the period report from a current snapshot."], + ] satisfies [AnalysisRunKindCode, string][])( + "gives a cancelled %s run its kind-exact next action", + async (runKindCode, nextAction) => { + stubBackend({ cancelledRunKind: runKindCode }); + render(); + + const cancelledRunButton = await screen.findByRole("button", { + name: /Open analysis run: .* · Cancelled · Demo Corp/, + }); + expect(cancelledRunButton).toHaveTextContent(`This run was cancelled. ${nextAction}`); + await userEvent.click(cancelledRunButton); + expect(screen.getAllByText(`This run was cancelled. ${nextAction}`)).not.toHaveLength(0); + expect(screen.queryByRole("button", { name: /Start/ })).not.toBeInTheDocument(); + }, + ); + it("tells a running lineage run to refresh the durable outbox", async () => { stubBackend({ runningLineageRun: true }); render(); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 76ff51dec..6e1fff1c9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2701,8 +2701,22 @@ function analysisRunNextAction(run: AnalysisRun): string | null { } case "analysis_status_running": return "Refresh this run. Start already queued the work on the durable outbox."; - case "analysis_status_succeeded": case "analysis_status_cancelled": + switch (run.run_kind_code) { + case "analysis_run_lineage": + return "This run was cancelled. Request a new lineage reconstruction from a current snapshot."; + case "analysis_run_tepp": + return "This run was cancelled. Connect the measurement service, then ask an administrator to submit a new TEPP run from a current snapshot."; + case "analysis_run_topic_lineage": + return "This run was cancelled. Connect the TEPP transport, then ask an administrator to submit new topic-lineage analysis from a current snapshot."; + case "analysis_run_report": + return "This run was cancelled. Rebuild the period report from a current snapshot."; + default: { + const unexpected: never = run.run_kind_code; + return unexpected; + } + } + case "analysis_status_succeeded": case null: return null; default: { @@ -3152,7 +3166,7 @@ function AnalysisRunsPanel({ return (