Wave 1: 07-01-PLAN.md — Unified services table (audit trail, backfill, validation) Wave 2: 07-02-PLAN.md — Admin catalog CRUD rewire (preserve historical references) Ready for execution: /gsd-execute-phase 7 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
4.2 KiB
plan, phase, status, completed_at
| plan | phase | status | completed_at |
|---|---|---|---|
| 04-04 | 04-progetti-multi-project | complete | 2026-05-22 |
Summary: 04-04 — Slug Resolution + Multi-Project Dashboard + Slug Edit
What Was Built
/api/internal/validate-slug/route.ts — Internal API route for Edge middleware. Queries clients.slug, returns 200 { valid: true } if found, 404 otherwise. Same pattern as validate-token.
src/proxy.ts — Updated client guard with slug-first resolution (D-06): tries /api/internal/validate-slug first, then falls back to /api/internal/validate-token. Route path is /client/[token] (not /c/ as referenced in the plan — adapted accordingly).
src/lib/client-view.ts — Complete rewrite for multi-project model. Exports:
getClientWithProjectsByToken(tokenOrSlug)— resolves slug → client (slug first, then token fallback), returnsClientProjectSummarywith active (non-archived) projects onlygetProjectView(projectId)— returns fullProjectViewscoped to one project; payments select excludes amount (DASH-07 + CLAUDE.md); no quote_items anywhere in the file
src/app/client/[token]/page.tsx — Rewritten for multi-project logic:
- 0 active projects → placeholder screen
- 1 project →
ClientDashboarddirectly (no selector), usingprojectViewToClientViewadapter - 2+ projects → shared header + shadcn
Tabs(project names as triggers) +ClientDashboardper tab
src/app/admin/clients/[id]/edit/page.tsx — Added slug field with:
- Pattern hint:
[a-z0-9-]{3,50} - Link preview showing
/client/{slug || token} - Error banner when redirected back with
?error=slug_taken
src/app/admin/clients/[id]/actions.ts — Updated clientSchema to include slug (Zod regex + .transform(v => v === "" ? null : v) to convert empty string to null). updateClient now:
- Parses slug from form data
- Persists to
clients.slugvia db.update - Catches unique constraint violation → redirects to edit page with
?error=slug_taken - On success: redirects to
/admin/clients/[id](previously no redirect)
Key Architectural Decisions
Adapter pattern projectViewToClientView: ClientDashboard was written for ClientView shape. Rather than modifying it, an adapter function in the page converts ProjectView + ClientProjectSummary.client → ClientView. Key mappings:
view.project.accepted_total→clientView.client.accepted_total(project-level, not client-level)view.project.client_id→clientView.client.id(used by ChatSection for comment threading)deliverables[].approved_at: Date | null→.toISOString() | null(ClientView expects string)documents[].created_atstripped (ClientView.documents only has id/label/url)
Multi-project tabs with full ClientDashboard: Each tab content renders a complete ClientDashboard (including its header/footer). shadcn Tabs shows only the active one via CSS. This is MVP-acceptable: the outer page provides the tab navigation, and within each tab the full dashboard renders.
Slug empty string → null: Zod schema transforms "" to null so saving with empty slug removes it. This lets admins clear a slug without special UI.
Security invariants verified (grep confirmed):
grep "quote_items" src/lib/client-view.ts→ only in comments, not in queriesgrep "amount" src/lib/client-view.ts→ only in comments documenting the exclusion
Files Changed
| File | Action |
|---|---|
src/app/api/internal/validate-slug/route.ts |
Created |
src/proxy.ts |
Updated — slug-first resolution in client guard |
src/lib/client-view.ts |
Complete rewrite — getClientWithProjectsByToken + getProjectView |
src/app/client/[token]/page.tsx |
Complete rewrite — multi-project dashboard |
src/app/admin/clients/[id]/edit/page.tsx |
Updated — slug field + error banner |
src/app/admin/clients/[id]/actions.ts |
Updated — clientSchema + slug in updateClient |
Notes
- The plan referenced
/c/[token]path but actual route has always been/client/[token]. Proxy and all references use/client/consistently. ClientDashboardcomponent unchanged — the adapter absorbs all type differences.getClientWithProjectsByTokenfilters out archived projects; archived projects are invisible to the client dashboard.