feat(FN-674): add multi-project dashboard support

- Add Project Overview page with responsive grid and health status cards
- Add Project Selector dropdown in header for quick context switching
- Add project drill-down navigation with task board/list views
- Add Setup Wizard components for first-run project registration
- Add global activity feed with project attribution and filtering
- Add project-aware task fetching to backend API
- Add project health polling and status badges
- Update ActivityLogModal with project filter and useActivityLog hook
This commit is contained in:
gsxdsm
2026-04-01 07:27:29 -07:00
parent d781a4f2f8
commit 31fbe031e7
14 changed files with 1042 additions and 191 deletions

View File

@@ -72,6 +72,8 @@ import {
fetchActivityFeed,
fetchFirstRunStatus,
fetchGlobalConcurrency,
fetchProjectTasks,
fetchTasks,
type ProjectInfo,
type DetectedProject,
} from "../../app/api";
@@ -344,4 +346,90 @@ describe("Project Routes API Functions", () => {
expect(result.currentlyActive).toBe(2);
});
});
describe("fetchProjectTasks", () => {
it("sends projectId as query parameter to /api/tasks", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchProjectTasks("proj_abc");
expect(globalThis.fetch).toHaveBeenCalledWith(
expect.stringContaining("/api/tasks"),
expect.any(Object)
);
const url: string = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(url).toContain("projectId=proj_abc");
});
it("returns tasks from the project's store when projectId is provided", async () => {
const mockTasks = [
{
id: "FN-001",
description: "Fix the bug",
column: "todo",
dependencies: [],
steps: [],
log: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
];
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, mockTasks));
const result = await fetchProjectTasks("proj_abc");
expect(result).toHaveLength(1);
expect(result[0].id).toBe("FN-001");
});
it("returns 404 when project is not found", async () => {
globalThis.fetch = vi.fn().mockReturnValue(
mockFetchResponse(false, { error: "Project not found" }, 404)
);
await expect(fetchProjectTasks("nonexistent_proj")).rejects.toThrow();
});
it("returns empty array on graceful degradation when backend error occurs", async () => {
// Backend returns 200 with [] for CentralCore unavailability
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
const result = await fetchProjectTasks("proj_abc");
expect(result).toEqual([]);
});
it("supports limit and offset parameters alongside projectId", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchProjectTasks("proj_abc", 10, 20);
const url: string = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(url).toContain("projectId=proj_abc");
expect(url).toContain("limit=10");
expect(url).toContain("offset=20");
});
});
describe("fetchTasks (default store - no projectId)", () => {
it("fetches from /api/tasks without projectId when no project is specified", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchTasks();
expect(globalThis.fetch).toHaveBeenCalledWith(
"/api/tasks",
expect.any(Object)
);
});
it("does not include projectId parameter in default fetchTasks call", async () => {
globalThis.fetch = vi.fn().mockReturnValue(mockFetchResponse(true, []));
await fetchTasks();
const url: string = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0][0];
expect(url).not.toContain("projectId");
});
});
});