feat(frontend): per-project isolation with sidebar switcher#2
feat(frontend): per-project isolation with sidebar switcher#2nikzdevz wants to merge 2 commits intodevin/scaffold-obsmcpfrom
Conversation
Adds a project picker to the Layout sidebar (with a create-project modal) and threads the selected project_id into every React Query key, API call, and mutation body across all ten pages. The SSE EventBus ignores events whose project_id doesn't match the currently selected project, so dashboards stay scoped. Current selection is persisted to localStorage via a small Zustand store. Backend: /api/stats now accepts ?project_id= and filters counts accordingly. No behavior change for installs with a single project (selection auto-populates to the first project). Co-Authored-By: thesyperdev@gmail.com <thesyperdev@gmail.com>
…ak in /api/stats Addresses Devin Review comments on PR #2: - /api/knowledge-graph get_graph() now accepts project_id query param and filters both knowledge_nodes and knowledge_edges (previously silently ignored project_id, so frontend scoping was ineffective). - /api/stats: remove dead 'scope' variable and '_scope' debug field that leaked the raw SQL fragment ' AND project_id=?' to API consumers. Co-Authored-By: thesyperdev@gmail.com <thesyperdev@gmail.com>
| qc.invalidateQueries({ queryKey: ['projects'] }); | ||
| setCurrent(project.id); | ||
| onClose(); |
There was a problem hiding this comment.
🟡 Race condition: useProjects auto-select overrides newly created project selection
After creating a project in CreateProjectModal, the onSuccess handler calls invalidateQueries (async refetch), then setCurrent(project.id). The useProjects hook at frontend/src/hooks/useProjects.ts:16-21 has a useEffect that depends on [query.data, currentId, setCurrent]. When currentId changes to the new project's ID, the effect fires — but query.data is still the stale list (the refetch hasn't resolved yet). Since the new project isn't in the stale list, !list.some((p) => p.id === currentId) evaluates to true, and setCurrent(list[0].id) immediately reverts the selection to the first project. The user creates a project expecting to switch to it, but ends up on the old first project instead.
Prompt for agents
The race condition occurs because invalidateQueries triggers an async refetch, but setCurrent sets the new project ID synchronously. Before the refetch completes, the useProjects effect (frontend/src/hooks/useProjects.ts:16-21) detects that the new currentId is not in the stale query.data and resets it to list[0].id.
Two possible fixes:
1. In CreateProjectModal.tsx onSuccess, optimistically update the React Query cache before setting the current ID:
qc.setQueryData(['projects'], (old: Project[] | undefined) => [...(old ?? []), project]);
setCurrent(project.id);
qc.invalidateQueries({ queryKey: ['projects'] });
This ensures the project list cache includes the new project before the effect can fire.
2. In useProjects.ts, change the auto-select effect to only activate when currentId is null (not when it is set but missing from the list). Then add a separate mechanism (e.g. check query.isFetched or query.isStale) to handle genuinely deleted projects. This avoids the effect fighting with explicit user selections.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed — caught the same race in the smoke-test recording (sidebar stayed on Alpha after creating Bravo). Fixed in #3 using both suggestions together: qc.setQueryData seeds the cache synchronously, and useProjects only defaults when currentId == null. Belt-and-suspenders.
Summary
Closes the gap identified in review: the backend and MCP tool fully isolate data by
project_id, but the dashboard was showing every project's rows mixed together with no way to pick a project. This PR brings the frontend up to the same isolation model.New UI
ProjectSelectordropdown in the sidebar (fed byGET /api/projects) with a "New project" optionCreateProjectModal— quick create flow that auto-selects the new projectlocalStoragevia a small Zustand store (frontend/src/stores/project.ts)Data plumbing
api/client.tsgets abuildQuery(path, params)helper{ projectId }and sends?project_id=…— Dashboard, Tasks, Sessions, Blockers, Decisions, Work Logs, Code Atlas, Knowledge Graph, Performance Logs, Settingsproject_idon the body so new rows land in the right bucketEventBusignores SSE events whosepayload.project_idis set and doesn't match the current selection — scoped dashboards stay scoped and cross-project events no longer thrash the React Query cacheBackend
GET /api/statsnow accepts?project_id=…and scopes counts for tasks/sessions/blockers/decisions/work_logs/nodes/edges. Agent count stays global sinceagent_configshas noproject_id.Verified locally
npm run typecheck+npm run build— greenruff check+pytestonserver/— greenReview & Testing Checklist for Human
npm run dev, create two projects via the sidebar modal, and confirm tasks/blockers/etc. created in one do not appear under the otherlocalStoragekeyobsmcp:projectsurvives a reloadNotes
devin/scaffold-obsmcp(PR Scaffold OBSMCP: local MCP tool + FastAPI backend + React dashboard #1) so the diff here is only the isolation work. Merge PR Scaffold OBSMCP: local MCP tool + FastAPI backend + React dashboard #1 first; this rebases cleanly ontomainafterwards.agent_configsintentionally remains a global/unscoped table (agents can span projects);/api/stats.agentsreflects that.zustandwas already inpackage.jsonfrom the scaffold.