From b9795134f586fe733411de9cba69b429f65ac3e0 Mon Sep 17 00:00:00 2001 From: Moltar Date: Mon, 30 Mar 2026 10:45:11 -0400 Subject: [PATCH] feat(projects): add fix missing links action to registry view --- web/src/pages/Projects.jsx | 57 ++++++++++++++++++++++++++------- web/src/pages/Projects.test.jsx | 41 ++++++++++++++++++++++-- 2 files changed, 85 insertions(+), 13 deletions(-) diff --git a/web/src/pages/Projects.jsx b/web/src/pages/Projects.jsx index b8d29cf..e86fb2b 100644 --- a/web/src/pages/Projects.jsx +++ b/web/src/pages/Projects.jsx @@ -6,9 +6,16 @@ import { FolderOpenIcon, ArchiveBoxIcon, TrashIcon, + WrenchScrewdriverIcon, } from '@heroicons/react/24/outline'; import Header from '../components/Header'; -import { createProject, getProjects, updateProject, deleteProject } from '../api/client'; +import { + createProject, + getProjects, + updateProject, + deleteProject, + repairProjectLinkHealth, +} from '../api/client'; import { useAuthStore } from '../stores/authStore'; import { useToastStore } from '../stores/toastStore'; import { normalizeProjectSlug } from '../utils/projectSlug'; @@ -230,6 +237,7 @@ export default function Projects() { const [isSavingProject, setIsSavingProject] = useState(false); const [deletingProjectId, setDeletingProjectId] = useState(null); const [togglingArchiveProjectId, setTogglingArchiveProjectId] = useState(null); + const [isRepairingLinks, setIsRepairingLinks] = useState(false); const loadProjects = useCallback(async () => { setIsLoadingProjects(true); @@ -342,6 +350,22 @@ export default function Projects() { } }; + const handleRepairMissingLinks = async () => { + setIsRepairingLinks(true); + try { + const result = await repairProjectLinkHealth({ limit: 200 }); + showToast( + `Link repair complete · repaired ${result?.repaired ?? 0}, failed ${result?.failed ?? 0}`, + result?.failed ? 'warning' : 'success', + ); + await loadProjects(); + } catch (err) { + showToast(err?.response?.data?.error?.message || err.message || 'Failed to repair project links', 'error'); + } finally { + setIsRepairingLinks(false); + } + }; + return (
@@ -358,16 +382,27 @@ export default function Projects() {
{isAdmin() && ( - +
+ + +
)} diff --git a/web/src/pages/Projects.test.jsx b/web/src/pages/Projects.test.jsx index 6d3a6b8..26e5636 100644 --- a/web/src/pages/Projects.test.jsx +++ b/web/src/pages/Projects.test.jsx @@ -8,21 +8,25 @@ vi.mock('../api/client', () => ({ createProject: vi.fn(), updateProject: vi.fn(), deleteProject: vi.fn(), + repairProjectLinkHealth: vi.fn(), })); vi.mock('../stores/authStore', () => ({ useAuthStore: () => ({ isAdmin: () => true }), })); +const showToast = vi.fn(); + vi.mock('../stores/toastStore', () => ({ - useToastStore: () => ({ showToast: vi.fn() }), + useToastStore: () => ({ showToast }), })); -const { getProjects, updateProject } = await import('../api/client'); +const { getProjects, updateProject, repairProjectLinkHealth } = await import('../api/client'); describe('Projects', () => { beforeEach(() => { vi.clearAllMocks(); + showToast.mockReset(); }); it('renders registry summary and project cards', async () => { @@ -159,4 +163,37 @@ describe('Projects', () => { expect(updateProject).toHaveBeenCalledWith('p1', expect.objectContaining({ status: 'archived' })); }); }); + + it('repairs missing links from the projects registry', async () => { + getProjects.mockResolvedValue([ + { + id: 'p1', + slug: 'project-alpha', + name: 'Project Alpha', + description: 'Sample project description', + root_path: '/projects/project-alpha', + status: 'active', + assigned_agents: 2, + updated_at: '2026-03-12T19:00:00.000Z', + }, + ]); + repairProjectLinkHealth.mockResolvedValue({ repaired: 2, failed: 0 }); + + render( + + + , + ); + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Fix missing links' })).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByRole('button', { name: 'Fix missing links' })); + + await waitFor(() => { + expect(repairProjectLinkHealth).toHaveBeenCalledWith({ limit: 200 }); + expect(showToast).toHaveBeenCalledWith('Link repair complete · repaired 2, failed 0', 'success'); + }); + }); });