feat(confidence-engine): v0.59b-c — report freshness on Report page + Portfolio

v0.59b — Report page freshness UI:
- Shows Current / Update available beside the generated report
- Manual Update report action with duplicate prevention guard
- Explanation copy about investigation changes since generation
- Persists generatedFromRevision during update flow

v0.59c — Portfolio Report freshness state:
- Surfaces Current / Update available alongside existing View report link
- Derives solely from revision provenance (zero model calls)
- No Update report action on Portfolio (manual update owned by Report page)
- Neither state shown when no Report exists
- Updated makeSnapshot with investigationRevision for realistic test data
This commit is contained in:
2026-09-03 14:41:02 +01:00
parent 99b3d26817
commit f06138de32
6 changed files with 649 additions and 8 deletions
+65
View File
@@ -9,6 +9,7 @@ export default function ReportPage() {
const [hydrated, setHydrated] = useState(false);
const [generationLoading, setGenerationLoading] = useState(false);
const [generationError, setGenerationError] = useState(false);
const [updateLoading, setUpdateLoading] = useState(false);
const generationAttempted = useRef(false);
@@ -66,6 +67,48 @@ export default function ReportPage() {
})();
}, [hydrated, existing]);
// Manual Report update (v0.59b — freshness manual update)
const handleUpdateReport = async () => {
if (updateLoading) return;
setUpdateLoading(true);
const snap = loadInvestigation();
const situationGraph = snap?.situationGraph;
const findings = snap?.findings ?? [];
const rev = snap?.investigationRevision ?? 0;
if (!situationGraph) {
setUpdateLoading(false);
return;
}
try {
const res = await fetch("/api/cases/overview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ situationGraph, findings }),
});
if (!res.ok) {
setUpdateLoading(false);
return;
}
const data = await res.json();
if (data.success) {
const reportData = { understanding: data.understanding, plausibleInterpretations: data.plausibleInterpretations, hasPlausibleInterpretations: true, generatedFromRevision: rev };
setExisting((p) => {
saveInvestigation({ ...p, investigationReport: reportData });
return { ...p, investigationReport: reportData };
});
}
} catch {
/* failure: retain existing Report and updateAvailable state */
} finally {
setUpdateLoading(false);
}
};
const report = existing?.investigationReport || null;
const scenario = hydrated ? (existing?.scenario || "") : null;
@@ -79,6 +122,28 @@ export default function ReportPage() {
Investigation Report
</h1>
{/* Report freshness — only when a Report exists */}
{report ? (
<div className="mt-6 flex items-center gap-3">
{existing?.investigationRevision === report.generatedFromRevision ? (
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">Current</span>
) : (
<div className="flex items-center gap-3">
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">Update available</span>
<span className="text-xs text-gray-400">The investigation has changed since this report was generated.</span>
<button
type="button"
onClick={handleUpdateReport}
disabled={updateLoading}
className="rounded-lg border border-teal-600 bg-white px-3 py-1.5 text-[11px] font-semibold tracking-wider uppercase text-teal-700 hover:bg-teal-50 transition disabled:opacity-40"
>
{updateLoading ? "Updating&#8230;" : "Update report"}
</button>
</div>
)}
</div>
) : null}
{/* Situation */}
{scenario && (
<div className="mt-8 rounded-xl border-[2.5px] border-teal-300/70 bg-gradient-to-b from-teal-50/60 to-white px-8 pt-6 pb-7 shadow-sm">
+23 -6
View File
@@ -18,6 +18,11 @@ function Portfolio() {
existing?.investigationReport && existing.investigationReport.understanding
);
const reportIsCurrent =
hasReport &&
existing.investigationReport.generatedFromRevision ===
(existing.investigationRevision ?? 0);
return (
<main className="mx-auto max-w-[640px] px-6 py-16">
<h1 className="mb-2 text-3xl font-bold tracking-tight">Confidence Engine</h1>
@@ -39,12 +44,24 @@ function Portfolio() {
<div className="mt-4 flex gap-3 text-sm">
{hasReport ? (
<Link
href={`/investigations/${INVESTIGATION_ID}/report`}
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
>
View report
</Link>
<div className="flex items-center gap-2">
<Link
href={`/investigations/${INVESTIGATION_ID}/report`}
className="rounded-lg border border-teal-600 bg-white px-4 py-2 font-medium text-teal-700 hover:bg-teal-50 transition"
>
View report
</Link>
{reportIsCurrent ? (
<span className="text-[11px] font-semibold tracking-wider uppercase text-teal-700/70">
Current
</span>
) : (
<span className="text-[11px] font-semibold tracking-wider uppercase text-gray-500">
Update available
</span>
)}
</div>
) : null}
<Link