Skip to content

feat(frontend): /projects and /projects/new pages + first-time flow (#15) - #32

Merged
jpricardo merged 1 commit into
feat/multi-tenancyfrom
feat/projects-pages
Aug 22, 2026
Merged

feat(frontend): /projects and /projects/new pages + first-time flow (#15)#32
jpricardo merged 1 commit into
feat/multi-tenancyfrom
feat/projects-pages

Conversation

@jpricardo

Copy link
Copy Markdown
Owner

Closes #15.

What

Phase 4, part two: a place to create projects, a place to see them all, and a first-run path for users who have neither.

  • /projects/new — name field with a live slug preview. The action derives the slug server-side (the preview is only a preview, the browser doesn't get to pick it), creates the project, writes currentProjectID, and lands on /dashboard.
  • /projects — every project the user belongs to, with slug, role, creation date, and a marker on the current one. Clicking a row posts to /projects/switch with redirectTo=/dashboard, so the session write and its membership re-check stay in one place.
  • First-time flow — the layout loader redirects a user with no projects to /projects/new, which is exempt from the check and is the one protected page that renders without a current project. It sits after the existing stale-session correction so the cookie is cleaned up first.
  • lib/slug.tsslugify(), shared by the preview and the action. Output always satisfies the broker's data.ValidSlug regex or is empty, which the action rejects with a message rather than sending upstream.
  • useProjects() — reads projects off the layout loader instead of re-fetching them per page, mirroring useCsrfToken.
  • Switcher — gains an "All projects" entry so /projects is reachable from the sidebar.

Roles required a backend change

GET /projects returned bare projects, so "lists all user projects with correct roles" was unreachable from the frontend without an N+1 of GET /projects/:id/members. Instead:

  • data.UserProject (project + the caller's role), returned by GetProjectsForUser, which now zips the membership roles it was already fetching into the result.
  • RPCServer.ListUserProjects and the broker's ListProjects carry the new type. Verified the embedded struct survives the gob hop and serializes flat as {id, name, slug, created_at, role}.
  • A membership row can outlive its project, so the projects query decides which entries survive, not the membership list.

Drive-by fix

POST /projects leaked the raw Mongo duplicate-key error on a slug collision. It now maps E11000 to a 409 with "a project with that slug already exists", which is what the form shows. Matching on the string is the existing idiom here — net/rpc flattens errors, so errors.Is can't cross the wire (same as the last-owner guard).

Notes for review

Verification

  • npm run typecheck, oxlint, oxfmt --check, and npm run build clean.
  • Broker and toolbox unit tests pass. Integration tests extended: GetProjectsForUser now asserts owner vs. member roles, and a new TestInsertProject_DuplicateSlug pins the E11000 substring the 409 mapping depends on.
  • TestProjectIsolation_* failed on one full integration run and passed on retry; they fail the same way on an untouched feat/multi-tenancy, so they are flaky here and unrelated.
  • The UI itself was not exercised live — every protected page is behind GitHub OAuth.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
logwolf-docs Ready Ready Preview Aug 22, 2026 12:05am

@jpricardo
jpricardo merged commit 4ec0bba into feat/multi-tenancy Aug 22, 2026
5 checks passed
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.

2 participants