Skip to content

feat(frontend): per-project isolation with sidebar switcher#2

Open
nikzdevz wants to merge 2 commits intodevin/scaffold-obsmcpfrom
devin/frontend-project-isolation
Open

feat(frontend): per-project isolation with sidebar switcher#2
nikzdevz wants to merge 2 commits intodevin/scaffold-obsmcpfrom
devin/frontend-project-isolation

Conversation

@nikzdevz
Copy link
Copy Markdown
Owner

@nikzdevz nikzdevz commented Apr 19, 2026

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

  • ProjectSelector dropdown in the sidebar (fed by GET /api/projects) with a "New project" option
  • CreateProjectModal — quick create flow that auto-selects the new project
  • Selection persisted to localStorage via a small Zustand store (frontend/src/stores/project.ts)
  • Auto-selects the first project on first load so empty states don't look broken

Data plumbing

  • api/client.ts gets a buildQuery(path, params) helper
  • Every list query across all 10 pages now keys on { projectId } and sends ?project_id=… — Dashboard, Tasks, Sessions, Blockers, Decisions, Work Logs, Code Atlas, Knowledge Graph, Performance Logs, Settings
  • Create mutations (tasks, blockers, decisions, code-atlas scan) stamp project_id on the body so new rows land in the right bucket
  • EventBus ignores SSE events whose payload.project_id is set and doesn't match the current selection — scoped dashboards stay scoped and cross-project events no longer thrash the React Query cache

Backend

  • GET /api/stats now accepts ?project_id=… and scopes counts for tasks/sessions/blockers/decisions/work_logs/nodes/edges. Agent count stays global since agent_configs has no project_id.

Verified locally

  • npm run typecheck + npm run build — green
  • ruff check + pytest on server/ — green

Review & Testing Checklist for Human

  • Run the backend + npm run dev, create two projects via the sidebar modal, and confirm tasks/blockers/etc. created in one do not appear under the other
  • Flip the project in the sidebar and watch the Dashboard counts re-fetch
  • Trigger a write via the MCP tool on one project while the dashboard is open on another; the other tab's queries should not invalidate (EventBus SSE filter)
  • Confirm localStorage key obsmcp:project survives a reload
  • Sanity-check Settings page still loads (it doesn't need project scoping)

Notes

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>
devin-ai-integration[bot]

This comment was marked as resolved.

…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>
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment on lines +23 to +25
qc.invalidateQueries({ queryKey: ['projects'] });
setCurrent(project.id);
onClose();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant