--- phase: 04-progetti-multi-project plan: "00" type: execute wave: 0 depends_on: [] files_modified: - src/app/client/[token]/layout.tsx - src/app/client/[token]/page.tsx - src/proxy.ts - src/components/admin/ClientRow.tsx - src/app/admin/clients/[id]/page.tsx - Dockerfile - .env.example autonomous: false must_haves: truths: - "La route /client/[token] esiste e risponde correttamente (cartella rinominata da /c/)" - "src/proxy.ts controlla /client/ invece di /c/ nel guard e nel matcher" - "ClientRow.tsx e admin/clients/[id]/page.tsx usano /client/ nei link generati" - "Il Dockerfile produce un build Next.js funzionante (exit code 0)" - "DATABASE_URL in .env punta al Postgres self-hosted su Coolify (non Neon)" - "drizzle-kit push sul nuovo DB completes con exit code 0" - "L'app risponde su hub.iamcavalli.net dopo il deploy su Coolify" artifacts: - path: "src/app/client/[token]/page.tsx" provides: "Dashboard cliente alla nuova route" contains: "export default" - path: "src/proxy.ts" provides: "Middleware aggiornato per /client/" contains: "pathname.startsWith(\"/client/\")" - path: "Dockerfile" provides: "Container image per Coolify" contains: "FROM node:" key_links: - from: "src/proxy.ts" to: "src/app/client/[token]/" via: "matcher: ['/admin/:path*', '/client/:path*']" pattern: "/client/:path*" - from: "src/components/admin/ClientRow.tsx" to: "src/app/client/[token]/" via: "href={`/client/${client.token}`}" pattern: "/client/" --- Infrastruttura pre-Phase 4: migrazione da Neon a Postgres self-hosted su Coolify (Hetzner), rinomina route cliente da /c/ a /client/, Dockerfile per deploy, dominio hub.iamcavalli.net. Questo piano esegue PRIMA di 04-01 per garantire che tutto il codice Phase 4 venga scritto e testato sull'infrastruttura finale. Nessun refactor post-esecuzione. Output: App deployata su hub.iamcavalli.net con DB Postgres su Coolify, route /client/[token] funzionante. @/Users/simonecavalli/IAMCAVALLI/.planning/ROADMAP.md @/Users/simonecavalli/IAMCAVALLI/CLAUDE.md proxy.ts riga 32: `if (pathname.startsWith("/c/"))` proxy.ts riga 33: `pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/)` proxy.ts riga 63: `matcher: ["/admin/:path*", "/c/:path*"]` ClientRow.tsx riga 59: `href={\`/c/${client.token}\`}` admin/clients/[id]/page.tsx riga 46: `href={\`/c/${client.token}\`}` Cartella da rinominare: src/app/c/ → src/app/client/ T-00-01: Redirect loop dopo rinomina — mitigazione: aggiornare proxy.ts prima di rinominare la cartella, verificare matcher. T-00-02: DB connection string esposta — mitigazione: .env mai committato, .env.example con placeholder. T-00-03: Dati di test persi durante migrazione DB — accettabile: tutti i dati attuali sono test data (CONTEXT.md). T-00-04: Coolify deploy fallisce silenziosamente — mitigazione: verificare health check dopo deploy. Task 1: Postgres su Coolify + migrazione DATABASE_URL - .env (connection string Neon attuale — non committare) - drizzle.config.ts (per confermare che legge DATABASE_URL) **A. Crea Postgres su Coolify (manuale — non automatizzabile)** Nel pannello Coolify su Hetzner: 1. New Resource → Database → PostgreSQL 2. Nome: `clienthub-db` 3. Version: 16 4. Salva le credenziali generate (host, port, user, password, dbname) 5. Abilita "Public port" se necessario per drizzle-kit push da locale **B. Aggiorna .env locale** Sostituisci la riga DATABASE_URL con la connection string Coolify: ``` DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DBNAME?sslmode=require ``` Mantieni la vecchia riga commentata come backup: ``` # DATABASE_URL=postgresql://neon... (backup) ``` **C. Aggiorna .env.example** Sostituisci il placeholder Neon con quello generico: ``` DATABASE_URL=postgresql://user:password@host:5432/dbname?sslmode=require ``` **D. Push schema al nuovo DB** ```bash npx drizzle-kit push ``` Il DB è fresh — nessuna migrazione soft necessaria, tutti i dati attuali sono test data. npx drizzle-kit push 2>&1; echo "Exit: $?" - drizzle-kit push completes con exit code 0 - .env contiene DATABASE_URL che punta a Coolify (non neon.tech) - .env.example non contiene credenziali reali - npx tsc --noEmit non produce errori legati al DB Task 2: Rinomina route /c/ → /client/ + aggiorna reference - src/proxy.ts (middleware completo — LEGGERE PRIMA di modificare) - src/components/admin/ClientRow.tsx (link generato riga 59) - src/app/admin/clients/[id]/page.tsx (link generato riga 46) - src/app/c/[token]/page.tsx (dashboard cliente da spostare) - src/app/c/[token]/layout.tsx (layout da spostare) **A. Rinomina cartella App Router** ```bash mv src/app/c src/app/client ``` Questo sposta automaticamente page.tsx e layout.tsx alla nuova route. **B. Aggiorna src/proxy.ts** Tre sostituzioni esatte: Riga 32 — cambia: ```typescript if (pathname.startsWith("/c/")) { ``` in: ```typescript if (pathname.startsWith("/client/")) { ``` Riga 33 — cambia: ```typescript const tokenMatch = pathname.match(/^\/c\/([a-zA-Z0-9_-]+)/); ``` in: ```typescript const tokenMatch = pathname.match(/^\/client\/([a-zA-Z0-9_-]+)/); ``` Riga 63 — cambia: ```typescript matcher: ["/admin/:path*", "/c/:path*"], ``` in: ```typescript matcher: ["/admin/:path*", "/client/:path*"], ``` **C. Aggiorna ClientRow.tsx** Riga 59 — cambia: ```typescript href={`/c/${client.token}`} ``` in: ```typescript href={`/client/${client.token}`} ``` **D. Aggiorna admin/clients/[id]/page.tsx** Riga 46 — cambia: ```typescript href={`/c/${client.token}`} ``` in: ```typescript href={`/client/${client.token}`} ``` grep -r '"/c/' src/ --include="*.ts" --include="*.tsx" 2>&1; echo "---"; grep -r "'/c/" src/ --include="*.ts" --include="*.tsx" 2>&1; echo "---"; grep -r '/c/:path' src/ --include="*.ts" --include="*.tsx" 2>&1 - `src/app/client/[token]/page.tsx` esiste (ls conferma) - `src/app/c/` NON esiste più (ls conferma) - `grep '"/c/' src/ -r` produce zero risultati - `grep "startsWith(\"/client/\")" src/proxy.ts` produce un match - `grep "/client/:path\*" src/proxy.ts` produce un match - `npx tsc --noEmit` exit code 0 Task 3: Dockerfile per Coolify - package.json (script build, versione Node richiesta) - next.config.ts (per verificare se output: standalone è già presente) - .gitignore (per confermare che .env non è committato) **A. Aggiungi output standalone a next.config.ts** Next.js standalone mode produce un bundle minimale ottimale per Docker. In next.config.ts, aggiungi dentro l'oggetto config: ```typescript output: "standalone", ``` **B. Crea Dockerfile nella root del progetto** ```dockerfile FROM node:20-alpine AS base FROM base AS deps RUN apk add --no-cache libc6-compat WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV=production ENV NEXTAUTH_URL=https://hub.iamcavalli.net RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" CMD ["node", "server.js"] ``` **C. Crea .dockerignore nella root** ``` .env .env.local node_modules .next .git .planning ``` **D. Aggiungi variabili d'ambiente su Coolify** Nel pannello Coolify → app → Environment Variables, aggiungere: ``` DATABASE_URL=postgresql://... (la stessa del .env locale) NEXTAUTH_SECRET=... NEXTAUTH_URL=https://hub.iamcavalli.net ADMIN_EMAIL=... ADMIN_PASSWORD=... ``` **E. Configura dominio su Coolify** Nel pannello Coolify → app → Domains: - Aggiungi: `hub.iamcavalli.net` - Abilita SSL automatico (Let's Encrypt) docker build -t clienthub-test . 2>&1 | tail -5; echo "Exit: $?" - `Dockerfile` esiste nella root del progetto - `.dockerignore` esiste e contiene `.env` - `next.config.ts` contiene `output: "standalone"` - `docker build` completes con exit code 0 (se Docker disponibile localmente) - In alternativa: `npm run build` exit code 0 (verifica il build Next.js) - Il file `.env` NON appare in `git status` (confermato da .gitignore) Checkpoint: verifica deploy su hub.iamcavalli.net - Postgres self-hosted su Coolify con schema applicato - Route /client/[token] funzionante (rinominata da /c/) - Dockerfile per deploy su Coolify - Dominio hub.iamcavalli.net configurato 1. Apri https://hub.iamcavalli.net — deve rispondere (anche con pagina 404 standard Next.js) 2. Apri https://hub.iamcavalli.net/admin/login — deve mostrare la schermata di login 3. Fai login come admin — deve accedere alla lista clienti 4. Copia il link di un cliente — deve essere nella forma `hub.iamcavalli.net/client/[token]` 5. Apri il link cliente — la dashboard deve caricare correttamente 6. Verifica che il vecchio link `/c/[token]` restituisca 404 (non più attivo) Digita "hub ok" quando tutti i controlli passano. 1. `src/app/c/` non esiste — `src/app/client/[token]/` esiste 2. `grep -r '"/c/' src/` produce zero risultati 3. `grep "startsWith(\"/client/\")" src/proxy.ts` produce un match 4. `npx drizzle-kit push` exit code 0 sul nuovo DB 5. `npm run build` exit code 0 6. https://hub.iamcavalli.net risponde dopo deploy Coolify 7. https://hub.iamcavalli.net/client/[token] carica la dashboard cliente ## Plan 04-00 Complete **Infrastruttura migrata:** - DB: Neon → Postgres self-hosted su Coolify (Hetzner) - Route: /c/[token] → /client/[token] - Deploy: Vercel → Coolify via Docker - Dominio: hub.iamcavalli.net attivo **File modificati:** src/proxy.ts, src/components/admin/ClientRow.tsx, src/app/admin/clients/[id]/page.tsx, next.config.ts **File creati:** Dockerfile, .dockerignore **File spostati:** src/app/c/ → src/app/client/