# Fase 2b — Anagrafiche · Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Collegare ogni `Player` al suo `User` alla registrazione e dare al giocatore una pagina `/profilo` dove modificare nick TtA (unico), nick TdG e email (con ri-verifica), con validazione sempre server-side.

**Architecture:** Better Auth `databaseHooks.user.create` crea/valida la riga `player` al signup (nick = `user.name`); un modulo dominio server-only `src/lib/server/players.ts` centralizza tutte le scritture/letture su `player` con check di unicità case-insensitive; `/profilo` usa una **form action** SvelteKit per i campi dominio (nick) e il client Better Auth (`authClient.changeEmail`) per l'email. Guardia auth per-pagina come la dashboard (no refactor `locals`: è debito Fase 3).

**Tech Stack:** SvelteKit (Svelte 5 runes), Drizzle ORM (`neon-http`), Better Auth 1.6.14, Neon Postgres, Vitest (unit offline + integration opt-in).

---

## Decisioni bloccate (impianto)

Queste scelte sono fissate dallo spec (`docs/superpowers/specs/2026-06-08-fase-2-vetrina-anagrafiche-design.md` §4, §7, §9) e dall'analisi del codice attuale. Non rinegoziarle in implementazione:

1. **FK `player.user_id` → `user.id`** con `onDelete: 'set null'`, colonna **nullable** (un player può esistere senza user → necessario per lo storico in Fase 3/migrazione). Spec §9 la lasciava opzionale: **la aggiungiamo** (integrità a costo nullo ora).
2. **Nick canonico = `player.nome_app_tta`**, **mirrorato** su `user.name`. Al signup Better Auth scrive `user.name` (= nick); l'hook copia in `player.nome_app_tta`. In `/profilo` la modifica del nick aggiorna **entrambi** (player + user.name) così la sessione Better Auth resta coerente.
3. **Unicità nick TtA = case-insensitive** a livello applicativo (`lower(nome_app_tta) = lower(:nick)`), escludendo se stessi per `player.id`. Il vincolo `UNIQUE` di colonna (case-sensitive) resta come **backstop** del DB. (Indice funzionale `lower()` = hardening opzionale, fuori scope 2b.)
4. **Creazione player al signup** via `databaseHooks.user.create.before` (valida nick + unicità, `throw APIError` per abortire) e `.after` (insert player). La race `before`→`after` (nick rubato in mezzo) è coperta dal `UNIQUE` del DB ed è trascurabile a questa scala (pre-lancio).
5. **Cambio email** via Better Auth `user.changeEmail` (`sendChangeEmailConfirmation` → link all'email **attuale**) + `emailVerification.sendVerificationEmail` già esistente. Avviato dal client in `/profilo`.
6. **Guardia auth per-pagina** (come `dashboard/+page.server.ts`), non `event.locals`. Refactor `locals` = debito Fase 3.
7. **Utenti di test esistenti**: backfill **non distruttivo** via script standalone `scripts/backfill-players.mjs` (idempotente, `@neondatabase/serverless` + `--env-file`).
8. **Migrazioni**: si usa `drizzle-kit push` (coerente con Fase 1; niente cartella `drizzle/`). Migrazioni versionate = debito Fase 3.

## File Structure

**Create:**
- `src/lib/server/players.ts` — modulo dominio server-only: letture/scritture su `player`, check unicità, mirror `user.name`.
- `src/lib/server/players.integration.test.ts` — test d'integrazione (skip se no `DATABASE_URL`, escluso dal `pnpm test` di default).
- `src/routes/profilo/+page.server.ts` — load (guardia + player) + `actions.updateIdentity`.
- `src/routes/profilo/+page.svelte` — UI profilo (form identità + form email), tema pergamena.
- `scripts/backfill-players.mjs` — one-off non distruttivo per allineare gli user esistenti.

**Modify:**
- `src/lib/validation/nickname.ts` — aggiunge `canonicalNick()` (pura, testabile).
- `src/lib/validation/nickname.test.ts` — test per `canonicalNick`.
- `src/lib/server/db/schema.ts` — FK su `player.userId`.
- `src/lib/server/auth.ts` — `databaseHooks.user.create` + `user.changeEmail`.
- `src/routes/dashboard/+page.svelte` — link a `/profilo`.
- `package.json` — script `db:push`.

---

## Task 1: helper `canonicalNick` (puro, TDD)

**Files:**
- Modify: `src/lib/validation/nickname.ts`
- Test: `src/lib/validation/nickname.test.ts`

- [ ] **Step 1: Write the failing test**

Aggiungi in `src/lib/validation/nickname.test.ts` (dentro o dopo il `describe` esistente, importando anche `canonicalNick`):

```ts
import { describe, it, expect } from 'vitest';
import { normalizeNickname, isValidNickname, canonicalNick } from './nickname';

// ...describe('nickname', ...) esistente invariato...

describe('canonicalNick', () => {
  it('normalizza e abbassa il case per il confronto di unicità', () => {
    expect(canonicalNick('  Rocko  Coccodrillo ')).toBe('rocko coccodrillo');
  });
  it('è stabile per stringhe già canoniche', () => {
    expect(canonicalNick('sellux')).toBe('sellux');
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: FAIL — `canonicalNick is not a function` / export mancante.

- [ ] **Step 3: Write minimal implementation**

In `src/lib/validation/nickname.ts` aggiungi in fondo:

```ts
/** Forma canonica per il confronto di unicità (case-insensitive, spazi normalizzati). */
export function canonicalNick(input: string): string {
	return normalizeNickname(input).toLowerCase();
}
```

- [ ] **Step 4: Run test to verify it passes**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: PASS (tutti i test nickname verdi).

- [ ] **Step 5: Commit**

```bash
git add src/lib/validation/nickname.ts src/lib/validation/nickname.test.ts
git commit -m "feat(fase2b): canonicalNick per confronto unicità nickname (TDD)"
```

---

## Task 2: FK `player.user_id` → `user.id` + script `db:push`

**Files:**
- Modify: `src/lib/server/db/schema.ts`
- Modify: `package.json`

- [ ] **Step 1: Aggiungi la FK nello schema**

Sostituisci il contenuto di `src/lib/server/db/schema.ts` con:

```ts
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core';
import { user } from './auth-schema';

export const player = pgTable('player', {
  id: serial('id').primaryKey(),
  userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
  nickTdg: text('nick_tdg').unique(),
  nomeAppTta: text('nome_app_tta').notNull().unique(),
  attivo: boolean('attivo').notNull().default(true),
  note: text('note'),
  createdAt: timestamp('created_at').notNull().defaultNow()
});

export * from './auth-schema';
```

- [ ] **Step 2: Aggiungi lo script `db:push`**

In `package.json`, dentro `"scripts"`, aggiungi (dopo `"test:integration"`):

```json
		"db:push": "drizzle-kit push"
```

- [ ] **Step 3: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori, 0 warning.

- [ ] **Step 4: Applica lo schema al DB Neon** *(operazione sul DB condiviso — la esegue il controller, non il subagent)*

Prova prima:
```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && pnpm db:push
```
Se segnala `DATABASE_URL` mancante, carica l'env esplicitamente:
```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && node --env-file=.env node_modules/.bin/drizzle-kit push
```
Expected: drizzle-kit applica `ALTER TABLE "player" ADD CONSTRAINT ... FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE set null`. Additivo e reversibile. Conferma "Changes applied".

- [ ] **Step 5: Commit**

```bash
git add src/lib/server/db/schema.ts package.json
git commit -m "feat(fase2b): FK player.user_id -> user.id (set null) + script db:push"
```

---

## Task 3: modulo dominio `players.ts` + test d'integrazione

**Files:**
- Create: `src/lib/server/players.ts`
- Test: `src/lib/server/players.integration.test.ts`

- [ ] **Step 1: Write the failing integration test**

Crea `src/lib/server/players.integration.test.ts`:

```ts
import { describe, it, expect, afterAll } from 'vitest';
import { eq } from 'drizzle-orm';

const hasDb = !!process.env.DATABASE_URL;

describe.skipIf(!hasDb)('players (integration)', () => {
  const suffix = String(Date.now());
  const userId = `test-user-${suffix}`;
  const otherUserId = `test-user-other-${suffix}`;
  const nickA = `TestNick ${suffix}`;
  const nickOther = `OtherNick ${suffix}`;

  afterAll(async () => {
    const { db } = await import('./db');
    const { player, user } = await import('./db/schema');
    await db.delete(player).where(eq(player.userId, userId));
    await db.delete(player).where(eq(player.userId, otherUserId));
    await db.delete(user).where(eq(user.id, userId));
    await db.delete(user).where(eq(user.id, otherUserId));
  });

  async function seedUser(id: string) {
    const { db } = await import('./db');
    const { user } = await import('./db/schema');
    await db.insert(user).values({ id, name: id, email: `${id}@example.test`, emailVerified: true });
  }

  it('createPlayerForUser crea la riga player collegata', async () => {
    const { createPlayerForUser, getPlayerByUserId } = await import('./players');
    await seedUser(userId);
    await createPlayerForUser(userId, nickA);
    const p = await getPlayerByUserId(userId);
    expect(p?.nomeAppTta).toBe(nickA.trim());
    expect(p?.userId).toBe(userId);
  });

  it('isNickTtaTaken è case-insensitive ed esclude il player indicato', async () => {
    const { isNickTtaTaken, getPlayerByUserId } = await import('./players');
    const mine = await getPlayerByUserId(userId);
    // stesso nick, case diverso, escludendo me stesso => libero
    expect(await isNickTtaTaken(nickA.toUpperCase(), mine!.id)).toBe(false);
    // stesso nick senza esclusione => occupato
    expect(await isNickTtaTaken(nickA.toLowerCase())).toBe(true);
  });

  it('updatePlayerIdentity rifiuta un nick già usato da un altro player', async () => {
    const { createPlayerForUser, updatePlayerIdentity } = await import('./players');
    await seedUser(otherUserId);
    await createPlayerForUser(otherUserId, nickOther);
    const res = await updatePlayerIdentity(userId, { nomeAppTta: nickOther.toUpperCase(), nickTdg: null });
    expect(res.ok).toBe(false);
  });

  it('updatePlayerIdentity aggiorna nick + nickTdg e mirrora user.name', async () => {
    const { updatePlayerIdentity, getPlayerByUserId } = await import('./players');
    const { db } = await import('./db');
    const { user } = await import('./db/schema');
    const nuovo = `Rinominato ${suffix}`;
    const res = await updatePlayerIdentity(userId, { nomeAppTta: nuovo, nickTdg: `tdg-${suffix}` });
    expect(res.ok).toBe(true);
    const p = await getPlayerByUserId(userId);
    expect(p?.nomeAppTta).toBe(nuovo);
    expect(p?.nickTdg).toBe(`tdg-${suffix}`);
    const u = await db.select().from(user).where(eq(user.id, userId)).limit(1);
    expect(u[0]?.name).toBe(nuovo);
  });
});
```

- [ ] **Step 2: Run test to verify it fails**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test:integration`
Expected: FAIL — modulo `./players` inesistente (import error).

- [ ] **Step 3: Write the implementation**

Crea `src/lib/server/players.ts`:

```ts
import { and, eq, ne, sql } from 'drizzle-orm';
import { db } from './db';
import { player, user } from './db/schema';
import { canonicalNick, isValidNickname, normalizeNickname } from '$lib/validation/nickname';

export type PlayerRow = typeof player.$inferSelect;

export type IdentityInput = { nomeAppTta: string; nickTdg: string | null };
export type IdentityResult = { ok: true } | { ok: false; error: string };

export async function getPlayerByUserId(userId: string): Promise<PlayerRow | undefined> {
  const rows = await db.select().from(player).where(eq(player.userId, userId)).limit(1);
  return rows[0];
}

/** True se un altro player usa già questo nick TtA (confronto case-insensitive). */
export async function isNickTtaTaken(nick: string, exceptPlayerId?: number): Promise<boolean> {
  const canon = canonicalNick(nick);
  const sameNick = sql`lower(${player.nomeAppTta}) = ${canon}`;
  const where =
    exceptPlayerId !== undefined ? and(sameNick, ne(player.id, exceptPlayerId)) : sameNick;
  const rows = await db.select({ id: player.id }).from(player).where(where).limit(1);
  return rows.length > 0;
}

/** True se un altro player usa già questo nick TdG (case-insensitive). */
export async function isNickTdgTaken(nickTdg: string, exceptPlayerId?: number): Promise<boolean> {
  const canon = canonicalNick(nickTdg);
  const same = sql`lower(${player.nickTdg}) = ${canon}`;
  const where = exceptPlayerId !== undefined ? and(same, ne(player.id, exceptPlayerId)) : same;
  const rows = await db.select({ id: player.id }).from(player).where(where).limit(1);
  return rows.length > 0;
}

export async function createPlayerForUser(userId: string, nick: string): Promise<void> {
  await db.insert(player).values({ userId, nomeAppTta: normalizeNickname(nick) });
}

/**
 * Aggiorna l'identità di gioco del player collegato a `userId`.
 * Valida + controlla l'unicità (escludendo se stesso) prima di scrivere,
 * poi mirrora il nick TtA su `user.name` per coerenza con Better Auth.
 */
export async function updatePlayerIdentity(
  userId: string,
  input: IdentityInput
): Promise<IdentityResult> {
  const existing = await getPlayerByUserId(userId);
  if (!existing) return { ok: false, error: 'Profilo giocatore non trovato.' };

  const nick = normalizeNickname(input.nomeAppTta);
  if (!isValidNickname(nick)) {
    return { ok: false, error: 'Il nick TtA deve avere tra 2 e 40 caratteri.' };
  }
  if (await isNickTtaTaken(nick, existing.id)) {
    return { ok: false, error: 'Questo nick TtA è già in uso da un altro giocatore.' };
  }

  const nickTdg = input.nickTdg ? normalizeNickname(input.nickTdg) : null;
  if (nickTdg) {
    if (!isValidNickname(nickTdg)) {
      return { ok: false, error: 'Il nick TdG deve avere tra 2 e 40 caratteri.' };
    }
    if (await isNickTdgTaken(nickTdg, existing.id)) {
      return { ok: false, error: 'Questo nick TdG è già in uso da un altro giocatore.' };
    }
  }

  await db.update(player).set({ nomeAppTta: nick, nickTdg }).where(eq(player.id, existing.id));
  await db.update(user).set({ name: nick }).where(eq(user.id, userId));
  return { ok: true };
}
```

- [ ] **Step 4: Run integration test to verify it passes**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test:integration`
Expected: PASS (4 test players verdi). Le righe di test vengono ripulite in `afterAll`.

- [ ] **Step 5: Verifica che il `pnpm test` di default resti verde e offline**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" test`
Expected: PASS, **senza** toccare il DB (il file `*.integration.test.ts` è escluso dal config di default).

- [ ] **Step 6: Commit**

```bash
git add src/lib/server/players.ts src/lib/server/players.integration.test.ts
git commit -m "feat(fase2b): modulo dominio players (CRUD + unicità nick) con test integrazione"
```

---

## Task 4: hook di registrazione (signup → player)

**Files:**
- Modify: `src/lib/server/auth.ts`

- [ ] **Step 1: Aggiungi i databaseHooks**

In `src/lib/server/auth.ts`:

1. Aggiungi gli import in testa (dopo gli import esistenti):

```ts
import { APIError } from 'better-auth/api';
import { isValidNickname, normalizeNickname } from '$lib/validation/nickname';
import { createPlayerForUser, isNickTtaTaken } from './players';
```

2. Dentro l'oggetto passato a `betterAuth({ ... })`, aggiungi la proprietà `databaseHooks` (es. subito dopo il blocco `emailVerification`):

```ts
  databaseHooks: {
    user: {
      create: {
        before: async (user) => {
          const nick = normalizeNickname(String(user.name ?? ''));
          if (!isValidNickname(nick)) {
            throw new APIError('BAD_REQUEST', {
              message: 'Il nickname deve avere tra 2 e 40 caratteri.'
            });
          }
          if (await isNickTtaTaken(nick)) {
            throw new APIError('BAD_REQUEST', {
              message: 'Questo nickname è già in uso. Scegline un altro.'
            });
          }
          return { data: { ...user, name: nick } };
        },
        after: async (user) => {
          await createPlayerForUser(user.id, String(user.name));
        }
      }
    }
  }
```

- [ ] **Step 2: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde.

- [ ] **Step 3: Verifica manuale in dev** *(controller)*

Con `pnpm dev` attivo: registra un nuovo utente da `/register` (es. nick `HookTest <timestamp>`). Poi controlla che la riga player sia stata creata:

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && node --env-file=.env -e "import('@neondatabase/serverless').then(async ({neon})=>{const sql=neon(process.env.DATABASE_URL);const r=await sql\`select p.nome_app_tta, p.user_id, u.email from player p join \\\"user\\\" u on u.id=p.user_id order by p.created_at desc limit 3\`;console.log(r);})"
```
Expected: la registrazione recente compare con `nome_app_tta` = nick e `user_id` valorizzato. Verifica anche il caso d'errore: registrarsi con lo **stesso** nick deve fallire con messaggio "già in uso".

- [ ] **Step 4: Commit**

```bash
git add src/lib/server/auth.ts
git commit -m "feat(fase2b): collega Player a User al signup via databaseHooks (valida + unicità nick)"
```

---

## Task 5: configurazione cambio email

**Files:**
- Modify: `src/lib/server/auth.ts`

- [ ] **Step 1: Aggiungi l'opzione `user.changeEmail`**

In `src/lib/server/auth.ts`, dentro `betterAuth({ ... })`, aggiungi la proprietà top-level `user` (separata da `databaseHooks`):

```ts
  user: {
    changeEmail: {
      enabled: true,
      sendChangeEmailConfirmation: async ({ user, newEmail, url }) => {
        await sendEmail(
          user.email,
          'Conferma il cambio email',
          `Hai richiesto di cambiare l'email del tuo account in ${newEmail}.\n` +
            `Conferma cliccando questo link dal tuo indirizzo ATTUALE: ${url}\n` +
            `Se non hai richiesto tu il cambio, ignora questa email.`
        );
      }
    }
  }
```

> Nota: con `requireEmailVerification: true` e senza `updateEmailWithoutVerification`, Better Auth invia la conferma all'email **attuale** (se verificata) e applica il cambio solo dopo il click. Se il nuovo indirizzo appartiene già a un altro account, l'API ritorna comunque `status:true` senza inviare nulla (anti-enumeration): il client mostrerà sempre lo stesso messaggio neutro.

- [ ] **Step 2: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde.

- [ ] **Step 3: Commit**

```bash
git add src/lib/server/auth.ts
git commit -m "feat(fase2b): abilita cambio email con conferma all'indirizzo attuale"
```

---

## Task 6: `/profilo` — load + form action (server)

**Files:**
- Create: `src/routes/profilo/+page.server.ts`

- [ ] **Step 1: Implementa load + action**

Crea `src/routes/profilo/+page.server.ts`:

```ts
import { auth } from '$lib/server/auth';
import { getPlayerByUserId, createPlayerForUser, updatePlayerIdentity } from '$lib/server/players';
import { fail, redirect } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ request }) => {
  const session = await auth.api.getSession({ headers: request.headers });
  if (!session) throw redirect(302, '/login');

  // Self-heal: se manca la riga player (utente creato prima dell'hook), la creiamo.
  let player = await getPlayerByUserId(session.user.id);
  if (!player) {
    await createPlayerForUser(session.user.id, session.user.name);
    player = await getPlayerByUserId(session.user.id);
  }

  return {
    email: session.user.email,
    emailVerified: session.user.emailVerified,
    nomeAppTta: player?.nomeAppTta ?? session.user.name,
    nickTdg: player?.nickTdg ?? null
  };
};

export const actions: Actions = {
  updateIdentity: async ({ request }) => {
    const session = await auth.api.getSession({ headers: request.headers });
    if (!session) throw redirect(302, '/login');

    const form = await request.formData();
    const nomeAppTta = String(form.get('nomeAppTta') ?? '');
    const nickTdgRaw = String(form.get('nickTdg') ?? '').trim();
    const nickTdg = nickTdgRaw === '' ? null : nickTdgRaw;

    const result = await updatePlayerIdentity(session.user.id, { nomeAppTta, nickTdg });
    if (!result.ok) {
      return fail(400, { error: result.error, nomeAppTta, nickTdg });
    }
    return { success: true, nomeAppTta, nickTdg };
  }
};
```

- [ ] **Step 2: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori (i tipi `./$types` esistono dopo `svelte-kit sync`, già incluso in `check`).

- [ ] **Step 3: Commit**

```bash
git add src/routes/profilo/+page.server.ts
git commit -m "feat(fase2b): /profilo load (guardia + self-heal player) + action updateIdentity"
```

---

## Task 7: `/profilo` — UI (form identità + form email)

**Files:**
- Create: `src/routes/profilo/+page.svelte`

- [ ] **Step 1: Implementa la pagina**

Crea `src/routes/profilo/+page.svelte`:

```svelte
<script lang="ts">
  import { enhance } from '$app/forms';
  import { authClient } from '$lib/auth-client';
  let { data, form }: { data: import('./$types').PageData; form: import('./$types').ActionData } =
    $props();

  // Stato form email (gestito da Better Auth lato client)
  let newEmail = $state('');
  let emailMsg = $state('');
  let emailBusy = $state(false);

  async function changeEmail(e: SubmitEvent) {
    e.preventDefault();
    emailBusy = true;
    emailMsg = '';
    const { error } = await authClient.changeEmail({ newEmail, callbackURL: '/profilo' });
    emailBusy = false;
    emailMsg = error
      ? (error.message ?? 'Errore imprevisto.')
      : 'Se l’indirizzo è valido, ti abbiamo inviato un link di conferma all’email attuale. Dopo la conferma, il cambio diventa effettivo.';
    if (!error) newEmail = '';
  }
</script>

<h1 class="text-3xl font-[var(--font-display)] mb-2" style="color:var(--color-inchiostro)">Il tuo profilo</h1>
<p class="mb-8" style="color:var(--color-sepia)">Gestisci la tua identità di gioco e l’email dell’account.</p>

<section class="mb-10 p-6 rounded-[var(--radius-card)]" style="background:var(--color-carta);box-shadow:var(--shadow-soft)">
  <h2 class="text-xl mb-4" style="color:var(--color-bronzo)">Identità di gioco</h2>
  <form method="POST" action="?/updateIdentity" use:enhance class="flex flex-col gap-4 max-w-md">
    <label class="flex flex-col gap-1 text-sm">
      <span style="color:var(--color-inchiostro)">Nick TtA <span style="color:var(--color-oxblood)">*</span></span>
      <input
        name="nomeAppTta"
        class="border p-2 rounded"
        style="border-color:color-mix(in srgb, var(--color-sepia) 40%, transparent)"
        value={form?.nomeAppTta ?? data.nomeAppTta}
        required
        minlength="2"
        maxlength="40"
      />
      <span class="text-xs" style="color:var(--color-sepia)">È il nome con cui giochi nell’app. Deve essere unico.</span>
    </label>

    <label class="flex flex-col gap-1 text-sm">
      <span style="color:var(--color-inchiostro)">Nick Tana dei Goblin <span style="color:var(--color-sepia)">(opzionale)</span></span>
      <input
        name="nickTdg"
        class="border p-2 rounded"
        style="border-color:color-mix(in srgb, var(--color-sepia) 40%, transparent)"
        value={form?.nickTdg ?? data.nickTdg ?? ''}
        maxlength="40"
      />
      <span class="text-xs" style="color:var(--color-sepia)">Il tuo nick su goblins.net, se vuoi collegarlo.</span>
    </label>

    <div>
      <button class="px-4 py-2 rounded text-white" style="background:var(--color-bronzo)">Salva identità</button>
    </div>

    {#if form?.error}
      <p class="text-sm" style="color:var(--color-oxblood)">{form.error}</p>
    {:else if form?.success}
      <p class="text-sm" style="color:var(--color-verderame)">Identità aggiornata.</p>
    {/if}
  </form>
</section>

<section class="p-6 rounded-[var(--radius-card)]" style="background:var(--color-carta);box-shadow:var(--shadow-soft)">
  <h2 class="text-xl mb-4" style="color:var(--color-bronzo)">Email</h2>
  <p class="text-sm mb-4" style="color:var(--color-sepia)">
    Indirizzo attuale: <strong style="color:var(--color-inchiostro)">{data.email}</strong>
    {#if !data.emailVerified}<span style="color:var(--color-oxblood)"> · non verificata</span>{/if}
  </p>
  <form onsubmit={changeEmail} class="flex flex-col gap-4 max-w-md">
    <label class="flex flex-col gap-1 text-sm">
      <span style="color:var(--color-inchiostro)">Nuovo indirizzo email</span>
      <input
        type="email"
        class="border p-2 rounded"
        style="border-color:color-mix(in srgb, var(--color-sepia) 40%, transparent)"
        bind:value={newEmail}
        required
      />
    </label>
    <div>
      <button class="px-4 py-2 rounded text-white" style="background:var(--color-verderame)" disabled={emailBusy}>
        {emailBusy ? 'Invio…' : 'Richiedi cambio email'}
      </button>
    </div>
    {#if emailMsg}<p class="text-sm" style="color:var(--color-inchiostro)">{emailMsg}</p>{/if}
  </form>
</section>

<p class="mt-8 text-sm"><a href="/dashboard" style="color:var(--color-bronzo)">← Torna alla dashboard</a></p>
```

- [ ] **Step 2: Verifica typecheck + build**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check && pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" build`
Expected: 0 errori; build verde.

- [ ] **Step 3: Verifica visiva in dev** *(controller)*

Con `pnpm dev`: login → vai su `/profilo`. Verifica: i campi mostrano nick/nickTdg/email correnti; salvataggio identità con nick valido = "Identità aggiornata"; nick duplicato = messaggio d'errore; richiesta cambio email = messaggio neutro + (in dev) link di conferma in console.

- [ ] **Step 4: Commit**

```bash
git add src/routes/profilo/+page.svelte
git commit -m "feat(fase2b): UI /profilo (identità di gioco + cambio email), tema pergamena"
```

---

## Task 8: link a `/profilo` dalla dashboard

**Files:**
- Modify: `src/routes/dashboard/+page.svelte`

- [ ] **Step 1: Aggiungi il link**

Sostituisci il contenuto di `src/routes/dashboard/+page.svelte` con:

```svelte
<script lang="ts">
  import { authClient } from '$lib/auth-client';
  import { goto } from '$app/navigation';
  let { data }: { data: import('./$types').PageData } = $props();
  async function logout() { await authClient.signOut(); goto('/'); }
</script>
<h1 class="text-2xl mb-4">Ciao, {data.user.name}</h1>
<p class="mb-4">Email: {data.user.email}</p>
<div class="flex items-center gap-3">
  <a class="px-4 py-2 rounded text-white inline-block" style="background:var(--color-bronzo)" href="/profilo">Il tuo profilo</a>
  <button class="px-4 py-2 rounded text-white" style="background:var(--color-verderame)" onclick={logout}>Esci</button>
</div>
```

- [ ] **Step 2: Verifica typecheck**

Run: `pnpm -C "/Users/paolo/Server/Siti Web/Lavoro/TtA" check`
Expected: 0 errori.

- [ ] **Step 3: Commit**

```bash
git add src/routes/dashboard/+page.svelte
git commit -m "feat(fase2b): link al profilo dalla dashboard"
```

---

## Task 9: backfill non distruttivo degli utenti esistenti

**Files:**
- Create: `scripts/backfill-players.mjs`

- [ ] **Step 1: Scrivi lo script**

Crea `scripts/backfill-players.mjs`:

```js
// One-off non distruttivo: crea una riga `player` per ogni `user` che non ne ha una.
// Uso: node --env-file=.env scripts/backfill-players.mjs
import { neon } from '@neondatabase/serverless';

const url = process.env.DATABASE_URL;
if (!url) {
  console.error('DATABASE_URL mancante. Esegui con: node --env-file=.env scripts/backfill-players.mjs');
  process.exit(1);
}
const sql = neon(url);

const orfani = await sql`
  select u.id, u.name
  from "user" u
  left join player p on p.user_id = u.id
  where p.id is null
`;

if (orfani.length === 0) {
  console.log('Nessun utente da allineare: ogni user ha già un player.');
  process.exit(0);
}

console.log(`Trovati ${orfani.length} utenti senza player. Creo le righe…`);
let creati = 0;
for (const u of orfani) {
  try {
    await sql`insert into player (user_id, nome_app_tta) values (${u.id}, ${u.name})`;
    creati++;
  } catch (err) {
    console.error(`  ! impossibile creare player per ${u.id} (nick "${u.name}"): ${err.message}`);
    console.error('    Probabile collisione di nome_app_tta: risolvi a mano e rilancia.');
  }
}
console.log(`Fatto: ${creati}/${orfani.length} player creati.`);
```

- [ ] **Step 2: Esegui il backfill** *(controller, sul DB condiviso)*

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && node --env-file=.env scripts/backfill-players.mjs
```
Expected: o "Nessun utente da allineare", oppure "N/N player creati". In caso di collisione di nick, lo script lo segnala senza interrompersi.

- [ ] **Step 3: Commit**

```bash
git add scripts/backfill-players.mjs
git commit -m "chore(fase2b): script backfill non distruttivo player per user esistenti"
```

---

## Task 10: gate finali + E2E manuale

- [ ] **Step 1: Gate automatici**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && pnpm check && pnpm test && pnpm build
```
Expected: `check` 0/0 · `test` verde (unit, offline) · `build` ok.

- [ ] **Step 2: Test d'integrazione (opt-in)**

```bash
cd "/Users/paolo/Server/Siti Web/Lavoro/TtA" && pnpm test:integration
```
Expected: verde (player + db). Pulisce da sé le righe di test.

- [ ] **Step 3: E2E manuale in dev** *(controller)*

Con `pnpm dev`:
1. Registra un nuovo utente → conferma in console il link di verifica → riga `player` creata con nick.
2. Tenta una seconda registrazione con lo **stesso** nick → errore "già in uso".
3. Login → `/profilo`: modifica nick TtA (valido) → ok; prova un nick duplicato → errore; modifica nick TdG → ok.
4. Richiedi cambio email → messaggio neutro + link di conferma in console (dev).

- [ ] **Step 4: Review finale + chiusura branch**

Dopo il via libera dei gate, lancia la review finale multi-dimensione (sicurezza, correttezza, spec-compliance, type-design) e poi usa **superpowers:finishing-a-development-branch** per merge su `main` + deploy prod su Vercel + aggiornamento memorie.

---

## Self-review del piano

- **Copertura spec §4:** wiring `Player↔User` (Task 4) ✓ · `/profilo` nick TtA unico (Task 3+6+7) ✓ · nick TdG opzionale (Task 3+7) ✓ · email con ri-verifica (Task 5+7) ✓ · scritture server-side validate (Task 3+6) ✓ · backfill utenti test (Task 9) ✓.
- **Spec §7 (test):** validazione nick già testata + nuova `canonicalNick` (Task 1) ✓ · unicità via test d'integrazione (Task 3) ✓ · gate check/build (Task 10) ✓.
- **Spec §9 (debiti):** FK `player.user_id` decisa e aggiunta (Task 2) ✓ · guardia `locals` esplicitamente rimandata a Fase 3 (Decisione 6) ✓.
- **Coerenza tipi:** `IdentityInput/IdentityResult`, `PlayerRow`, `isNickTtaTaken(nick, exceptPlayerId?)`, `updatePlayerIdentity(userId, input)` usati coerentemente fra Task 3/4/6.
- **Niente placeholder:** ogni step ha codice/comando reale.
- **Operazioni sul DB condiviso** (Task 2.4 push, Task 4.3 verifica, Task 9.2 backfill) marcate come *controller* (non subagent), additive/reversibili, pre-lancio.
</content>
</invoke>
