feat(FN-3315): add test coverage for archived insights in three test suites

Adds test coverage for archived insights across the core store, `InsightsView` component, and `useInsights` hook — 156 lines of new tests covering FN-3315.

Fusion-Task-Id: FN-3315
This commit is contained in:
Fusion
2026-05-04 23:18:10 -07:00
committed by gsxdsm
parent bcf558741a
commit 20fc4f8420
10 changed files with 475 additions and 53 deletions

View File

@@ -120,7 +120,7 @@ const INSIGHT_CATEGORIES: InsightCategory[] = [
"trends",
];
const INSIGHT_STATUSES: InsightStatus[] = ["generated", "confirmed", "stale", "dismissed"];
const INSIGHT_STATUSES: InsightStatus[] = ["generated", "confirmed", "stale", "dismissed", "archived"];
const INSIGHT_RUN_STATUSES: InsightRunStatus[] = ["pending", "running", "completed", "failed", "cancelled"];
const INSIGHT_RUN_TRIGGERS: InsightRunTrigger[] = ["schedule", "manual", "task_completion", "merge_event", "api"];

View File

@@ -173,10 +173,15 @@ describe("InsightStore", () => {
it("filters by status", () => {
store.createInsight("proj", { title: "A", category: "quality", status: "confirmed", provenance: createProvenance() });
store.createInsight("proj", { title: "B", category: "quality", status: "generated", provenance: createProvenance() });
store.createInsight("proj", { title: "C", category: "quality", status: "archived", provenance: createProvenance() });
const list = store.listInsights({ projectId: "proj", status: "confirmed" });
expect(list).toHaveLength(1);
expect(list[0].title).toBe("A");
const archived = store.listInsights({ projectId: "proj", status: "archived" });
expect(archived).toHaveLength(1);
expect(archived[0].title).toBe("C");
});
it("supports pagination with limit and offset", () => {
@@ -250,6 +255,18 @@ describe("InsightStore", () => {
expect(updated!.updatedAt >= original.createdAt).toBe(true);
});
it("updates status to archived", () => {
const original = store.createInsight("proj", {
title: "Archive me",
category: "quality",
status: "confirmed",
provenance: createProvenance(),
});
const updated = store.updateInsight(original.id, { status: "archived" });
expect(updated?.status).toBe("archived");
});
it("returns undefined for non-existent insight", () => {
const result = store.updateInsight("INS-NOTFOUND", { title: "X" });
expect(result).toBeUndefined();

View File

@@ -60,11 +60,15 @@ export type InsightCategory =
* generated → confirmed → stale
* ↓
* dismissed
* ↕
* archived
*
* A "stale" insight has been superseded or is no longer relevant.
* A "dismissed" insight was manually rejected by a reviewer.
* An "archived" insight was actioned or intentionally hidden, and can be
* unarchived back to "confirmed".
*/
export type InsightStatus = "generated" | "confirmed" | "stale" | "dismissed";
export type InsightStatus = "generated" | "confirmed" | "stale" | "dismissed" | "archived";
// ── Provenance Metadata ───────────────────────────────────────────────

View File

@@ -8257,6 +8257,24 @@ export function dismissInsight(id: string, projectId?: string): Promise<Insight>
});
}
/**
* Archive an insight (set status to archived).
*/
export function archiveInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}/archive`, projectId), {
method: "POST",
});
}
/**
* Unarchive an insight (set status back to confirmed).
*/
export function unarchiveInsight(id: string, projectId?: string): Promise<Insight> {
return api<Insight>(withProjectId(`/insights/${encodeURIComponent(id)}/unarchive`, projectId), {
method: "POST",
});
}
/**
* Trigger a manual insight generation run.
*/

View File

@@ -419,6 +419,25 @@
color: var(--text-dim);
}
.insight-item-status--archived {
background: color-mix(in srgb, var(--text-dim) 15%, transparent);
color: var(--text-dim);
}
.insight-item--archived {
opacity: 0.6;
border-left-color: var(--text-dim);
}
.insight-item--archived .insight-item-title {
text-decoration: line-through;
color: var(--text-muted);
}
.insight-item--archived .insight-item-content {
text-decoration: line-through;
}
.insight-item-date {
display: flex;
align-items: center;
@@ -539,6 +558,13 @@
min-height: 36px;
}
.insights-show-archived-toggle {
max-width: 180px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.insights-view-close {
min-height: 36px;
min-width: 36px;

View File

@@ -20,6 +20,7 @@ import {
TrendingUp,
ExternalLink,
Archive,
ArchiveRestore,
Clock,
} from "lucide-react";
import { useInsights, type InsightSection } from "../hooks/useInsights";
@@ -63,9 +64,16 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
runInsights,
dismiss,
createTask: createTaskFromInsight,
archive = async () => {},
unarchive = async () => {},
toggleShowArchived = () => {},
dismissStates,
createTaskStates,
archiveStates = new Map(),
unarchiveStates = new Map(),
totalCount,
archivedCount = 0,
showArchived = false,
} = useInsights(projectId);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
@@ -137,6 +145,44 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
[dismiss, addToast],
);
const handleArchive = useCallback(
async (id: string, title: string) => {
try {
setStatusMessage(`Archiving "${title}"...`);
setStatusType("info");
await archive(id);
setStatusMessage(`Archived "${title}"`);
setStatusType("success");
addToast(`Insight archived: ${title}`, "success");
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to archive insight";
setStatusMessage(message);
setStatusType("error");
addToast(message, "error");
}
},
[archive, addToast],
);
const handleUnarchive = useCallback(
async (id: string, title: string) => {
try {
setStatusMessage(`Unarchiving "${title}"...`);
setStatusType("info");
await unarchive(id);
setStatusMessage(`Unarchived "${title}"`);
setStatusType("success");
addToast(`Insight unarchived: ${title}`, "success");
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to unarchive insight";
setStatusMessage(message);
setStatusType("error");
addToast(message, "error");
}
},
[unarchive, addToast],
);
const handleCreateTask = useCallback(
async (id: string, title: string) => {
try {
@@ -213,31 +259,61 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
{activeSection.items.map((insight) => {
const dismissState = dismissStates.get(insight.id);
const createState = createTaskStates.get(insight.id);
const archiveState = archiveStates.get(insight.id);
const unarchiveState = unarchiveStates.get(insight.id);
const isDismissInFlight = dismissState?.running ?? false;
const isCreateInFlight = createState?.running ?? false;
const isArchiveInFlight = archiveState?.running ?? false;
const isUnarchiveInFlight = unarchiveState?.running ?? false;
const isArchived = insight.status === "archived";
const isAnyActionInFlight = activeSection.items.some(
(item) => dismissStates.get(item.id)?.running || createTaskStates.get(item.id)?.running,
(item) =>
dismissStates.get(item.id)?.running ||
createTaskStates.get(item.id)?.running ||
archiveStates.get(item.id)?.running ||
unarchiveStates.get(item.id)?.running,
);
return (
<li key={insight.id} className="insight-item" data-insight-id={insight.id}>
<li key={insight.id} className={`insight-item${isArchived ? " insight-item--archived" : ""}`} data-insight-id={insight.id}>
<div className="insight-item-header">
<h4 className="insight-item-title">{insight.title}</h4>
<div className="insight-item-actions">
<button
className="insight-item-action-btn"
onClick={() => void handleCreateTask(insight.id, insight.title)}
disabled={isCreateInFlight || isAnyActionInFlight}
title="Create task from this insight"
aria-label="Create task from this insight"
data-testid={`create-task-${insight.id}`}
>
{isCreateInFlight ? (
<RefreshCw size={20} className="spin" />
) : (
<Plus size={20} />
)}
</button>
{isArchived ? (
<button
className="insight-item-action-btn"
onClick={() => void handleUnarchive(insight.id, insight.title)}
disabled={isUnarchiveInFlight || isAnyActionInFlight}
title="Unarchive this insight"
aria-label="Unarchive this insight"
data-testid={`unarchive-${insight.id}`}
>
{isUnarchiveInFlight ? <RefreshCw size={20} className="spin" /> : <ArchiveRestore size={20} />}
</button>
) : (
<>
<button
className="insight-item-action-btn"
onClick={() => void handleCreateTask(insight.id, insight.title)}
disabled={isCreateInFlight || isAnyActionInFlight}
title="Create task from this insight"
aria-label="Create task from this insight"
data-testid={`create-task-${insight.id}`}
>
{isCreateInFlight ? <RefreshCw size={20} className="spin" /> : <Plus size={20} />}
</button>
<button
className="insight-item-action-btn"
onClick={() => void handleArchive(insight.id, insight.title)}
disabled={isArchiveInFlight || isAnyActionInFlight}
title="Archive this insight"
aria-label="Archive this insight"
data-testid={`archive-${insight.id}`}
>
{isArchiveInFlight ? <RefreshCw size={20} className="spin" /> : <Archive size={20} />}
</button>
</>
)}
<button
className="insight-item-action-btn"
onClick={() => void handleDismiss(insight.id, insight.title)}
@@ -299,6 +375,17 @@ export function InsightsView({ projectId, addToast, onClose, onCreateTask }: Ins
<X size={16} />
</button>
)}
{archivedCount > 0 && (
<button
className="btn btn-sm insights-show-archived-toggle"
onClick={toggleShowArchived}
aria-label={showArchived ? "Hide archived insights" : "Show archived insights"}
data-testid="toggle-archived-insights"
>
<Archive size={14} />
{showArchived ? "Hide Archived" : `Show Archived (${archivedCount})`}
</button>
)}
<button
className="btn btn-sm"
onClick={() => void refresh()}

View File

@@ -63,6 +63,9 @@ vi.mock("lucide-react", () => ({
Archive: ({ size = 24, className = "" }: { size?: number; className?: string }) => (
<span data-testid="archive-icon" className={className}>{`Archive-${size}`}</span>
),
ArchiveRestore: ({ size = 24, className = "" }: { size?: number; className?: string }) => (
<span data-testid="archive-restore-icon" className={className}>{`ArchiveRestore-${size}`}</span>
),
Clock: ({ size = 24, className = "" }: { size?: number; className?: string }) => (
<span data-testid="clock-icon" className={className}>{`Clock-${size}`}</span>
),
@@ -100,10 +103,17 @@ describe("InsightsView", () => {
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn(),
archive: vi.fn(),
unarchive: vi.fn(),
toggleShowArchived: vi.fn(),
dismissStates: new Map(),
createTaskStates: new Map(),
archiveStates: new Map(),
unarchiveStates: new Map(),
totalCount: 0,
dismissedCount: 0,
archivedCount: 0,
showArchived: false,
});
});
@@ -1014,6 +1024,66 @@ describe("InsightsView", () => {
});
});
describe("archived insights", () => {
it("renders archived insights with archived class and unarchive button", () => {
const sectionsWithArchived = [
{
category: "features" as const,
label: "Features",
items: [
{
id: "INS-ARCH",
projectId: "test",
title: "Archived Insight",
content: "Archived content",
category: "features" as const,
status: "archived" as const,
fingerprint: "fp-arch",
provenance: { trigger: "manual" as const },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
},
],
isLoading: false,
error: null,
},
...mockSections.slice(1),
];
mockUseInsights.mockReturnValue({
sections: sectionsWithArchived,
loading: false,
error: null,
latestRun: null,
isRunInFlight: false,
runError: null,
refresh: vi.fn(),
runInsights: vi.fn(),
dismiss: vi.fn(),
createTask: vi.fn(),
archive: vi.fn(),
unarchive: vi.fn(),
toggleShowArchived: vi.fn(),
dismissStates: new Map(),
createTaskStates: new Map(),
archiveStates: new Map(),
unarchiveStates: new Map(),
totalCount: 1,
dismissedCount: 0,
archivedCount: 1,
showArchived: true,
});
render(<InsightsView {...defaultProps} />);
const item = screen.getByText("Archived Insight").closest("li");
expect(item?.className).toContain("insight-item--archived");
expect(screen.getByTestId("unarchive-INS-ARCH")).toBeTruthy();
expect(screen.getByTestId("toggle-archived-insights")).toHaveTextContent("Hide Archived");
});
});
describe("in-flight disable behavior", () => {
it("should disable dismiss button only for insight being dismissed", async () => {
const sectionsWithInsight = [

View File

@@ -11,6 +11,8 @@ import type { InsightCategory, InsightStatus } from "@fusion/core";
vi.mock("../../api", () => ({
fetchInsights: vi.fn(),
dismissInsight: vi.fn(),
archiveInsight: vi.fn(),
unarchiveInsight: vi.fn(),
triggerInsightRun: vi.fn(),
fetchInsightRuns: vi.fn(),
getInsightCreateTaskData: vi.fn(),
@@ -40,6 +42,8 @@ vi.mock("lucide-react", () => ({
import {
fetchInsights,
dismissInsight,
archiveInsight,
unarchiveInsight,
triggerInsightRun,
fetchInsightRuns,
getInsightCreateTaskData,
@@ -47,6 +51,8 @@ import {
const mockFetchInsights = vi.mocked(fetchInsights);
const mockDismissInsight = vi.mocked(dismissInsight);
const mockArchiveInsight = vi.mocked(archiveInsight);
const mockUnarchiveInsight = vi.mocked(unarchiveInsight);
const mockTriggerInsightRun = vi.mocked(triggerInsightRun);
const mockFetchInsightRuns = vi.mocked(fetchInsightRuns);
const mockGetInsightCreateTaskData = vi.mocked(getInsightCreateTaskData);
@@ -453,6 +459,7 @@ describe("useInsights", () => {
});
expect(mockGetInsightCreateTaskData).toHaveBeenCalledWith("INS-1", "project-1");
expect(mockArchiveInsight).toHaveBeenCalledWith("INS-1", "project-1");
expect(taskData).toEqual({
title: "Implement Feature X",
description: "Detailed description of the feature",
@@ -492,6 +499,74 @@ describe("useInsights", () => {
});
});
describe("archive lifecycle", () => {
it("archives and unarchives an insight", async () => {
const insight = {
id: "INS-1",
projectId: "project-1",
title: "Keep me",
content: "Content",
category: "features" as InsightCategory,
status: "confirmed" as InsightStatus,
fingerprint: "fp1",
provenance: { trigger: "manual" },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
};
mockFetchInsights.mockResolvedValue({ insights: [insight], count: 1 });
mockFetchInsightRuns.mockResolvedValue({ runs: [] });
mockArchiveInsight.mockResolvedValue({ ...insight, status: "archived" as InsightStatus });
mockUnarchiveInsight.mockResolvedValue({ ...insight, status: "confirmed" as InsightStatus });
const { result } = renderHook(() => useInsights("project-1"));
await waitFor(() => expect(result.current.loading).toBe(false));
await act(async () => {
await result.current.archive("INS-1");
});
expect(mockArchiveInsight).toHaveBeenCalledWith("INS-1", "project-1");
expect(result.current.archivedCount).toBe(1);
await act(async () => {
await result.current.unarchive("INS-1");
});
expect(mockUnarchiveInsight).toHaveBeenCalledWith("INS-1", "project-1");
expect(result.current.archivedCount).toBe(0);
});
it("toggleShowArchived hides and shows archived insights", async () => {
const archivedInsight = {
id: "INS-A",
projectId: "project-1",
title: "Archived insight",
content: "Hidden by default",
category: "features" as InsightCategory,
status: "archived" as InsightStatus,
fingerprint: "fpA",
provenance: { trigger: "manual" },
lastRunId: null,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
};
mockFetchInsights.mockResolvedValue({ insights: [archivedInsight], count: 1 });
mockFetchInsightRuns.mockResolvedValue({ runs: [] });
const { result } = renderHook(() => useInsights("project-1"));
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.archivedCount).toBe(1);
expect(result.current.totalCount).toBe(0);
act(() => {
result.current.toggleShowArchived();
});
expect(result.current.showArchived).toBe(true);
expect(result.current.totalCount).toBe(1);
});
});
describe("refresh", () => {
it("should reload insights data", async () => {
mockFetchInsights

View File

@@ -13,6 +13,8 @@ import type { Insight, InsightCategory, InsightStatus, InsightRun } from "@fusio
import {
fetchInsights,
dismissInsight,
archiveInsight,
unarchiveInsight,
triggerInsightRun,
fetchInsightRuns,
getInsightCreateTaskData,
@@ -63,6 +65,7 @@ export const STATUS_LABELS: Record<InsightStatus, string> = {
confirmed: "Confirmed",
stale: "Stale",
dismissed: "Dismissed",
archived: "Archived",
};
// Section data structure
@@ -99,14 +102,21 @@ export interface UseInsightsResult {
runInsights: () => Promise<void>;
dismiss: (id: string) => Promise<void>;
createTask: (id: string) => Promise<{ title: string; description: string } | null>;
archive: (id: string) => Promise<void>;
unarchive: (id: string) => Promise<void>;
toggleShowArchived: () => void;
// Per-insight action states
dismissStates: Map<string, InsightActionState>;
createTaskStates: Map<string, InsightActionState>;
archiveStates: Map<string, InsightActionState>;
unarchiveStates: Map<string, InsightActionState>;
// Dismissed/filtered counts
totalCount: number;
dismissedCount: number;
archivedCount: number;
showArchived: boolean;
}
/**
@@ -116,6 +126,8 @@ export interface UseInsightsResult {
* @returns Insights data and action handlers
*/
export function useInsights(projectId?: string): UseInsightsResult {
const [allInsights, setAllInsights] = useState<Insight[]>([]);
// Section items (keyed by category)
const [sections, setSections] = useState<InsightSection[]>(() =>
INSIGHT_CATEGORIES.map((category) => ({
@@ -141,6 +153,9 @@ export function useInsights(projectId?: string): UseInsightsResult {
// Per-insight action states
const [dismissStates, setDismissStates] = useState<Map<string, InsightActionState>>(new Map());
const [createTaskStates, setCreateTaskStates] = useState<Map<string, InsightActionState>>(new Map());
const [archiveStates, setArchiveStates] = useState<Map<string, InsightActionState>>(new Map());
const [unarchiveStates, setUnarchiveStates] = useState<Map<string, InsightActionState>>(new Map());
const [showArchived, setShowArchived] = useState(false);
// Refresh function
const refresh = useCallback(async () => {
@@ -154,32 +169,7 @@ export function useInsights(projectId?: string): UseInsightsResult {
projectId,
);
// Group by category
const grouped = new Map<InsightCategory, Insight[]>();
// Initialize all categories
for (const category of INSIGHT_CATEGORIES) {
grouped.set(category, []);
}
// Group non-dismissed insights
for (const insight of response.insights) {
if (insight.status !== "dismissed") {
const existing = grouped.get(insight.category) ?? [];
grouped.set(insight.category, [...existing, insight]);
}
}
// Update sections
setSections(
INSIGHT_CATEGORIES.map((category) => ({
category,
label: CATEGORY_LABELS[category] ?? category,
items: grouped.get(category) ?? [],
isLoading: false,
error: null,
})),
);
setAllInsights(response.insights.filter((insight) => insight.status !== "dismissed"));
// Fetch latest run
const runsResponse = await fetchInsightRuns(projectId);
@@ -194,6 +184,10 @@ export function useInsights(projectId?: string): UseInsightsResult {
}
}, [projectId]);
const toggleShowArchived = useCallback(() => {
setShowArchived((prev) => !prev);
}, []);
// Run insights generation
const runInsights = useCallback(async () => {
setIsRunInFlight(true);
@@ -230,12 +224,7 @@ export function useInsights(projectId?: string): UseInsightsResult {
await dismissInsight(id, projectId);
// Update local state
setSections((prev) =>
prev.map((section) => ({
...section,
items: section.items.filter((item) => item.id !== id),
})),
);
setAllInsights((prev) => prev.filter((item) => item.id !== id));
setDismissStates((prev) => {
const next = new Map(prev);
@@ -266,6 +255,16 @@ export function useInsights(projectId?: string): UseInsightsResult {
try {
const data = await getInsightCreateTaskData(id, projectId);
await archiveInsight(id, projectId);
setAllInsights((prev) => prev.map((insight) =>
insight.id === id
? {
...insight,
status: "archived",
updatedAt: new Date().toISOString(),
}
: insight,
));
setCreateTaskStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: null });
@@ -288,15 +287,106 @@ export function useInsights(projectId?: string): UseInsightsResult {
[projectId],
);
useEffect(() => {
const grouped = new Map<InsightCategory, Insight[]>();
for (const category of INSIGHT_CATEGORIES) {
grouped.set(category, []);
}
for (const insight of allInsights) {
if (!showArchived && insight.status === "archived") {
continue;
}
const existing = grouped.get(insight.category) ?? [];
grouped.set(insight.category, [...existing, insight]);
}
setSections(
INSIGHT_CATEGORIES.map((category) => ({
category,
label: CATEGORY_LABELS[category] ?? category,
items: grouped.get(category) ?? [],
isLoading: false,
error: null,
})),
);
}, [allInsights, showArchived]);
const archive = useCallback(
async (id: string) => {
setArchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: true, error: null });
return next;
});
try {
await archiveInsight(id, projectId);
setAllInsights((prev) => prev.map((insight) =>
insight.id === id ? { ...insight, status: "archived", updatedAt: new Date().toISOString() } : insight,
));
setArchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: null });
return next;
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to archive insight";
setArchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: message });
return next;
});
throw err;
}
},
[projectId],
);
const unarchive = useCallback(
async (id: string) => {
setUnarchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: true, error: null });
return next;
});
try {
await unarchiveInsight(id, projectId);
setAllInsights((prev) => prev.map((insight) =>
insight.id === id ? { ...insight, status: "confirmed", updatedAt: new Date().toISOString() } : insight,
));
setUnarchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: null });
return next;
});
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to unarchive insight";
setUnarchiveStates((prev) => {
const next = new Map(prev);
next.set(id, { running: false, error: message });
return next;
});
throw err;
}
},
[projectId],
);
// Computed counts
const totalCount = useMemo(() => {
return sections.reduce((sum, section) => sum + section.items.length, 0);
}, [sections]);
const dismissedCount = useMemo(() => {
// This would need to be tracked separately if needed
return 0;
}, [sections]);
}, []);
const archivedCount = useMemo(() => {
return allInsights.filter((insight) => insight.status === "archived").length;
}, [allInsights]);
// Initial load - intentionally runs once on mount
useEffect(() => {
@@ -314,9 +404,16 @@ export function useInsights(projectId?: string): UseInsightsResult {
runInsights,
dismiss,
createTask,
archive,
unarchive,
toggleShowArchived,
dismissStates,
createTaskStates,
archiveStates,
unarchiveStates,
totalCount,
dismissedCount,
archivedCount,
showArchived,
};
}

View File

@@ -72,7 +72,7 @@ const VALID_CATEGORIES: InsightCategory[] = [
];
// Valid insight statuses
const VALID_STATUSES: InsightStatus[] = ["generated", "confirmed", "stale", "dismissed"];
const VALID_STATUSES: InsightStatus[] = ["generated", "confirmed", "stale", "dismissed", "archived"];
// Valid run triggers
const VALID_TRIGGERS: InsightRunTrigger[] = ["schedule", "manual", "task_completion", "merge_event", "api"];
@@ -593,6 +593,34 @@ export function createInsightsRouter(store: TaskStore): Router {
}
});
router.post("/:id/archive", (req: Request, res: Response) => {
try {
const id = String(req.params.id);
const store = getInsightStore();
const insight = store.updateInsight(id, { status: "archived" });
if (!insight) {
throw notFound(`Insight not found: ${id}`);
}
res.json(insight);
} catch (error) {
rethrowAsApiError(error, "Failed to archive insight");
}
});
router.post("/:id/unarchive", (req: Request, res: Response) => {
try {
const id = String(req.params.id);
const store = getInsightStore();
const insight = store.updateInsight(id, { status: "confirmed" });
if (!insight) {
throw notFound(`Insight not found: ${id}`);
}
res.json(insight);
} catch (error) {
rethrowAsApiError(error, "Failed to unarchive insight");
}
});
// ── Create Task from Insight ────────────────────────────────────────────
router.post("/:id/create-task", (req: Request, res: Response) => {