Projects & Teams — Phase 3 Frontend Concept
Projects & Teams — Phase 3 Frontend Concept
Section titled “Projects & Teams — Phase 3 Frontend Concept”Status: Concept / design draft. Not yet a feature spec.
Date: 2026-04-29
Origin: Phases 0–2 landed the backend (schema, services, APIs, policy merge). This concept covers the dashboard surface — project switcher, project-scoped filtering, project settings, creation flows.
Depends on: concepts/projects-teams-concept.md §6 (UX surfaces) for visual baseline.
1. The thesis
Section titled “1. The thesis”The backend is already a complete projects-and-teams system. The dashboard needs the smallest possible UI that lets a sophisticated enterprise admin (a) see their org's projects, (b) switch between them with shareable URLs, (c) filter existing dashboard data by project, and (d) configure each project's members and policy.
Concept §6 already specified the visual treatment (top-bar breadcrumb + team-grouped dropdown + scoped sidebar). This document fills in the integration shape, state management, MVP cuts, and migration safety for current users.
The product test: A new project admin should be able to (1) find their project in the switcher, (2) see only their project's data in Agents/Sessions/Audit, (3) invite a teammate via Project Settings → Members, and (4) tighten the org policy via Project Settings → Policy — within 60 seconds of landing on the dashboard.
2. Decisions locked in (from brainstorm)
Section titled “2. Decisions locked in (from brainstorm)”| # | Decision | Rationale |
|---|---|---|
| 1 | First PR = switcher only, no creation flow | Smallest shippable unit. Read-existing only; admins create projects via API or future PR. |
| 2 | URL ?project=X drives, localStorage fallback, context mirrors | Shareable links, browser history works, last-used persists across sessions. |
| 3 | Existing screens default to "All projects" | Migration safety — current users see what they always saw until they pick a project. |
| 4 | Both dropdown + command palette | Dropdown for discoverability; Cmd+P for power users (already wired). |
| 5 | WIP branch lands first, then Phase 3 stacks | Preserves commit attribution; Phase 3 PR descriptions stay focused. |
| 6 | "All projects" option not in switcher (v1) | Skip it — switcher only shows accessible projects. Pages handle "no filter = all". |
| 7 | Helpers added to existing api.ts | Symmetry with the current centralized fetch layer. No hooks layer yet. |
| 8 | v1 surfaces: switcher + Project Settings only | Skip standalone Teams/Projects index pages — switcher is the index. |
| 9 | Project Settings = Members + Policy + BYOK + Danger | All four tabs. Project admins need real CRUD authority on day 1. |
| 10 | Keep breadcrumb Acme › Team ⚙ › Project ⚙ ▼ | Concept §6.2 default. Hierarchy is visible in the TopBar, never the sidebar. |
3. Architecture
Section titled “3. Architecture”3.1 State management
Section titled “3.1 State management”Three layers, in order of authority:
URL ?project=<id> ──▶ React Context "currentProjectId" ──▶ components ↑ ↓ on first nav, hydrate from └─ user clicks ────── localStorage["tappass.lastProject"]- URL is the source of truth. Every component reads from context, but context mirrors URL via
useSearchParams. - localStorage is a fallback for first navigation only — when the user lands on
/app/agentswith no?project=, look up their last-used project and redirect to add the param. Avoids "where did my filter go?" UX. - No project chosen =
currentProjectId === null= "All projects" semantics on every list endpoint (don't pass?project_idquery).
3.2 API integration
Section titled “3.2 API integration”Every Phase 3 fetch goes through frontend/src/api.ts. New helpers:
api.projects.list() // GET /api/projects (with current-org scope)api.projects.get(id) // GET /api/projects/{id}api.projects.update(id, payload) // PATCH /api/projects/{id}api.projects.updatePolicy(id, override) // PUT /api/projects/{id}/policyapi.projects.delete(id)api.projects.members.list(id)api.projects.members.addDirect(id, body)api.projects.members.removeDirect(id, email)api.teams.list() // GET /api/teamsapi.teams.get(id)api.teams.create({name, description})api.teams.update(id, {name})api.teams.delete(id)api.teams.members.list(id)api.teams.members.add(id, body)api.teams.members.updateRole(id, email, role)api.teams.members.remove(id, email)When a request needs project context (e.g., the agents list filtered by project), the existing fetch wrapper appends ?project=<currentProjectId> automatically when currentProjectId !== null. One place to wire it, every list endpoint downstream "just works."
3.3 Component layout
Section titled “3.3 Component layout”New files (kept small, single-purpose):
frontend/src/├─ context/│ └─ ProjectContext.tsx (currentProjectId + setProject + localStorage sync)├─ components/│ ├─ ProjectSwitcher.tsx (TopBar dropdown — replaces inline org switcher block)│ ├─ ProjectFilterChip.tsx (used on Agents/Pipelines/Sessions/AuditTrail)│ └─ projects/│ ├─ CreateProjectDialog.tsx (3c)│ ├─ CreateTeamDialog.tsx (3c)│ └─ ProjectSettingsTabs.tsx (3d)└─ pages/ └─ ProjectSettings.tsx (3d — `/app/projects/:projectId/settings`)Layout.tsx (currently 1192 lines) gets one new import (<ProjectSwitcher>) plus a <ProjectProvider> wrap around <Outlet>. No restructure of Layout itself — it's tangled but functional, and untangling is unrelated to this work.
3.4 Routing
Section titled “3.4 Routing”/app/projects/:projectId/settings (3d)/app/projects/:projectId/settings/members (default tab)/app/projects/:projectId/settings/policy/app/projects/:projectId/settings/keys/app/projects/:projectId/settings/dangerThe existing scoped pages (Agents, Pipelines, Sessions, AuditTrail) keep their paths unchanged. Project context comes from ?project=<id>, not the path. Two reasons: (a) the user can switch projects without losing their current page; (b) URL-shareability is universal — every page link is project-scoped automatically when the param is present.
4. PR decomposition (5 sub-PRs)
Section titled “4. PR decomposition (5 sub-PRs)”3a — Switcher (read-only)
Section titled “3a — Switcher (read-only)”Files: ProjectContext.tsx, ProjectSwitcher.tsx, api.ts (add projects + teams helpers), Layout.tsx (mount switcher + provider).
Behavior:
- TopBar dropdown next to the existing org switcher
- Shows projects grouped by team header
- Click a project → updates
?project=<id>in URL (no full nav, just searchParams) - Selected project's name renders in the trigger
- Empty state: "No projects yet — ask an admin to create one"
Out of scope: creation, settings, anything that writes.
3b — Project filter on existing pages
Section titled “3b — Project filter on existing pages”Files: Agents.tsx, Pipelines.tsx, Sessions.tsx, AuditTrail.tsx, plus a ProjectFilterChip.tsx shared component, plus a one-line edit to the api.ts fetch wrapper to auto-append ?project=.
Behavior:
- Each scoped page gains a small "Project: …" chip in its filter bar
- Default state when no project is in URL = "All projects" (no filter applied)
- Selecting a project via the switcher narrows every scoped list automatically (because URL drives, fetch wrapper picks it up)
Backend prerequisite: the list endpoints (/api/agents, /api/pipelines/list, /api/observability/sessions, /api/audit/events) need to accept ?project_id=<id>. The /api/projects routes already exist; the resource-list endpoints are existing TapPass code that will need a small filter pass-through. Likely doable in the same PR.
3c — Create Team / Create Project dialogs
Section titled “3c — Create Team / Create Project dialogs”Files: CreateTeamDialog.tsx, CreateProjectDialog.tsx, switcher gains + New team and + New project affordances.
Behavior:
- Single dialog per concept §6.5 (no multi-step wizard)
- Required fields only: name (description optional)
- Create Team is admin-only (gated by
account_role) - Create Project requires picking a destination team — defaults to current team context if available
- On success, redirects the URL
?project=<new-id>so the new project becomes current
3d — Project Settings page
Section titled “3d — Project Settings page”Files: ProjectSettings.tsx, ProjectSettingsTabs.tsx, plus four tab subcomponents (MembersTab, PolicyTab, KeysTab, DangerTab).
Behavior:
- Header: project name + breadcrumb + edit-rename affordance
- Tab navigation:
- Members — list (team-derived + direct), add direct, remove direct, change-role inline
- Policy — form per dimension (allowed_models, max_tokens, etc.) with org-floor displayed as faded baseline; on save, route returns 422 with violations → render inline per-field error messages
- BYOK Keys — list keys for project, set/remove project-specific override (org default key shown as faded baseline)
- Danger zone — rename, delete (delete blocked by 409 if non-empty per Phase 1c route)
3e — Command palette + breadcrumb polish
Section titled “3e — Command palette + breadcrumb polish”Files: CommandPalette.tsx (existing — extend with project actions), Layout.tsx TopBar (full breadcrumb).
Behavior:
Cmd+P → "switch to project: …"opens fuzzy-filtered project picker- Breadcrumb in TopBar shows
Acme › Platform Eng ⚙ › Inference Gateway ⚙ ▼per concept §6.2 - Gear icons appear conditionally on permissions (org_admin / team_manager / project_admin)
5. Migration safety
Section titled “5. Migration safety”Existing TapPass users open the app to find:
- The org switcher unchanged (still works as before).
- A new "Project: …" trigger in the TopBar showing "All projects" by default.
- Their existing pages look identical — list views show all data because no
?project=filter is applied. - When a curious admin clicks the switcher and picks a project, only then do the list views narrow.
This is the migration-safety design from Q3. No "where did my data go?" surprises.
After the org-admin creates additional projects (out of scope here — they use the API for now), users with project membership will start seeing those projects in their switcher dropdown. The switcher only renders projects the caller has actually been added to.
6. Out of scope for Phase 3
Section titled “6. Out of scope for Phase 3”- Standalone Teams index (
/app/teams) — redundant with the switcher's team-grouped dropdown - Standalone Projects index (
/app/projects) — same reason - Org settings → Teams & Projects master tree — defer until org-admins ask for it
- Auto IdP/SCIM mapping UI — concept §3.4 calls this Phase 4 / v2
- Cost-split per project visualizations — concept Q7 said showback only, filter on existing screens
- Project policy dimension editor for tool-arg constraints — the Policy tab will support the eight typed-field dimensions in v1; the constraint-builder UI is its own design (concept §5.2)
- Mobile-specific layouts — keep the existing responsive sidebar collapse, no mobile project switcher polish
7. Coexistence with existing WIP branch
Section titled “7. Coexistence with existing WIP branch”fix/policytab-active-steps-and-guardrails (5 commits ahead of main) lands first as a separate PR. After it merges, Phase 3 PRs base off the new main. None of the WIP touches projects/teams code, so there are no logical conflicts — just file-level overlap on Layout.tsx, Agents.tsx, Sessions.tsx, AuditTrail.tsx, Pipelines.tsx. Since the WIP merges first, Phase 3's diffs come from the post-WIP baseline.
8. Risks & deliberate trade-offs
Section titled “8. Risks & deliberate trade-offs”| # | Risk | Decision |
|---|---|---|
| 1 | Layout.tsx is already 1192 lines — adding the switcher grows it further | Accepted. Don't restructure Layout in this work; the file is functional, untangling is a separate concern. The new switcher is its own component file that Layout imports. |
| 2 | URL-driven state means deep-linking to a project the user lost access to → 403 | Mitigated: the project-context middleware (Phase 1d) returns 403; the frontend catches 403 on /api/projects/{id} and clears the param + shows toast "lost access — switched to All projects". |
| 3 | Auto-redirect from localStorage on first nav can feel surprising | Mitigated: the redirect happens once per session, only on a project-scoped path, only when no ?project= is already set. Fresh tabs = no redirect. |
| 4 | Project Settings tabs URL state (which tab) | Use path-based: /settings/members vs ?tab=members. Path is cleaner, browser history works per tab. |
| 5 | Policy tab violation rendering depends on the route returning structured {field, message} violations | Already true (Phase 2c implements this). Frontend just needs to deserialize and map field→input. |
9. Open questions
Section titled “9. Open questions”These are deliberately left for the implementation phase to decide based on what looks right:
- Empty-state copy on the switcher when the user has no projects yet
- Exact dropdown positioning on small screens (drop-up if near bottom of viewport)
- Whether the breadcrumb wraps or truncates on long names (likely truncate with tooltip)
These don't block the implementation plan. We'll iterate via PR review.