## Task 4: Script di generazione (Turso + R2 + Puppeteer)

**Files:**
- Create: `app/scripts/pdf/template.ts` (porting di `pdf-prototype/template.js`)
- Create: `app/scripts/pdf/generate.ts`
- Modify: `app/package.json` (devDeps `puppeteer`, `tsx`; script `pdf:gen`)

**Interfaces:**
- Consumes: `loadFingerprintInput`/`computeDvrFingerprint` (Task 2), `dvrPdf` (Task 1).
- Produces: eseguibile `npx tsx scripts/pdf/generate.ts <agenziaId|all>` che genera il/i PDF, li carica su R2 (`pdf/{codice}_DVR.pdf`) e aggiorna `dvr_pdf` (`stato='ready'`, `data_hash`, `generato_il`, `r2_key`) o `stato='error'`.
- **Variabili d'ambiente richieste:** `DATABASE_URL`, `DATABASE_AUTH_TOKEN`, `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`.

- [ ] **Step 1: Installa le dipendenze (solo dev: non finiranno nel bundle Vercel)**

```bash
cd app && npm install -D puppeteer tsx
```
In `app/package.json` aggiungi a `scripts`: `"pdf:gen": "tsx scripts/pdf/generate.ts"`.

- [ ] **Step 2: Evita il download di Chromium sul build Vercel**

Su Vercel (pannello del progetto → Environment Variables) aggiungi `PUPPETEER_SKIP_DOWNLOAD=true` per tutti gli ambienti, così `npm install` in fase di build Vercel non scarica Chromium (non serve: la generazione non gira su Vercel). Annotalo anche in `infrastruttura_alleanza` (memoria). In CI (GitHub Action) NON impostarla, così Chromium viene scaricato.

- [ ] **Step 3: Porta il template**

Copia `pdf-prototype/template.js` in `app/scripts/pdf/template.ts` esportando `renderHtml(data)` come prima. **Unica modifica funzionale**: le immagini non si leggono più dal filesystem locale. Sostituisci `fileToDataUri(path.join(ROOT, allegato.storage_path))` con una funzione che riceve già i byte dell'immagine (base64) precaricati: cambia la firma in modo che `renderHtml(data)` usi `allegato.dataUri` già valorizzato a monte (lo riempie `generate.ts` scaricando da R2). Mantieni identici layout, CSS, sezioni, matrice. Adatta i `require` in `import` TypeScript.

- [ ] **Step 4: Implementa `generate.ts`**

Crea `app/scripts/pdf/generate.ts`:

```ts
import { createClient, type Client } from '@libsql/client';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import puppeteer from 'puppeteer';
import { renderHtml } from './template';
import { loadFingerprintInput, hashFingerprintInput } from '../../src/lib/server/pdf/fingerprint';

const DB_URL = process.env.DATABASE_URL!;
const DB_TOKEN = process.env.DATABASE_AUTH_TOKEN!;
const r2 = new S3Client({
	region: 'auto',
	endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
	credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID!, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY! }
});
const BUCKET = process.env.R2_BUCKET!;

async function r2GetBase64(key: string): Promise<string | null> {
	try {
		const o = await r2.send(new GetObjectCommand({ Bucket: BUCKET, Key: key }));
		const buf = Buffer.from(await o.Body!.transformToByteArray());
		const mime = key.endsWith('.png') ? 'image/png' : 'image/jpeg';
		return `data:${mime};base64,${buf.toString('base64')}`;
	} catch { return null; }
}

// Carica i dati per il render (riusa le stesse query di load.js, ma da Turso e con
// resolveDvrAgenziaId per le gemelle). Per brevità, fattorizza una loadDvrData(client, agenziaId)
// che ritorna { agenzia, dvr, pericoli, valutazioni, valutazioniMisure, incendioDoc, attivita, allegati, settings }
// con la STESSA forma usata da pdf-prototype/load.js (l'origine immagini diventa R2).
import { loadDvrData } from './data'; // vedi Step 5

async function generateOne(client: Client, agenziaId: number): Promise<void> {
	const data = await loadDvrData(client, agenziaId);
	// precarica immagini da R2 → dataUri
	for (const a of data.allegati) {
		const key = a.storage_path.replace(/^storage\//, '');
		a.dataUri = (await r2GetBase64(key)) ?? '';
	}
	const html = renderHtml(data);
	const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox'] });
	try {
		const page = await browser.newPage();
		await page.emulateMediaType('print');
		await page.setContent(html, { waitUntil: 'load' });
		const pdf = await page.pdf({ format: 'A4', printBackground: true, preferCSSPageSize: true });
		const codice = data.agenzia.codice_fisico as string;
		const key = `pdf/${codice}_DVR.pdf`;
		await r2.send(new PutObjectCommand({ Bucket: BUCKET, Key: key, Body: pdf, ContentType: 'application/pdf' }));
		const hash = hashFingerprintInput(await loadFingerprintInput(client, agenziaId));
		await client.execute({
			sql: `INSERT INTO dvr_pdf(agenzia_id, r2_key, data_hash, stato, generato_il, error_msg, updated_at)
			      VALUES (?,?,?,?,?,NULL,?)
			      ON CONFLICT(agenzia_id) DO UPDATE SET r2_key=excluded.r2_key, data_hash=excluded.data_hash,
			        stato='ready', generato_il=excluded.generato_il, error_msg=NULL, updated_at=excluded.updated_at`,
			args: [agenziaId, key, hash, 'ready', new Date().toISOString(), new Date().toISOString()]
		});
		console.log(`OK ${agenziaId} → ${key}`);
	} finally {
		await browser.close();
	}
}

async function main() {
	const arg = process.argv[2];
	const client = createClient({ url: DB_URL, authToken: DB_TOKEN });
	if (arg === 'all') {
		const r = await client.execute(`SELECT id FROM agenzie WHERE deleted_at IS NULL ORDER BY id`);
		let n = 0;
		for (const row of r.rows) {
			const id = row.id as number;
			try { await generateOne(client, id); }
			catch (e) {
				await client.execute({ sql: `INSERT INTO dvr_pdf(agenzia_id,stato,error_msg,updated_at) VALUES (?,?,?,?)
					ON CONFLICT(agenzia_id) DO UPDATE SET stato='error', error_msg=excluded.error_msg, updated_at=excluded.updated_at`,
					args: [id, 'error', String(e), new Date().toISOString()] });
				console.error(`ERR ${id}: ${e}`);
			}
			console.log(`-- ${++n}/${r.rows.length}`);
		}
	} else {
		const id = Number(arg);
		try { await generateOne(client, id); }
		catch (e) {
			await client.execute({ sql: `INSERT INTO dvr_pdf(agenzia_id,stato,error_msg,updated_at) VALUES (?,?,?,?)
				ON CONFLICT(agenzia_id) DO UPDATE SET stato='error', error_msg=excluded.error_msg, updated_at=excluded.updated_at`,
				args: [id, 'error', String(e), new Date().toISOString()] });
			console.error(`ERR ${id}: ${e}`); process.exit(1);
		}
	}
}
main();
```

- [ ] **Step 5: Fattorizza `loadDvrData` da `pdf-prototype/load.js`**

Crea `app/scripts/pdf/data.ts` portando le query di `pdf-prototype/load.js` su `@libsql/client` (async), aggiungendo all'inizio `const dvrAgenziaId = await resolveDvrAgenziaId(client, agenziaId)` e usando `dvrAgenziaId` per le query che partono da `dvr.agenzia_id`, mentre `agenzia` e `allegati` restano sull'`agenziaId` proprio (anagrafica/codice/nome-file della gemella; allegati per-sede). Tipizza il ritorno con un campo `dataUri?: string` su ogni allegato.

- [ ] **Step 6: Genera 1 PDF in locale puntando a prod (verifica end-to-end)**

```bash
cd app
DATABASE_URL='libsql://alleanza-dvr-prod-paoloalby.aws-eu-west-1.turso.io' \
DATABASE_AUTH_TOKEN='<token>' R2_ACCOUNT_ID='6164844dd7d12a5c2b03a6b5d2ba26c3' \
R2_ACCESS_KEY_ID='<id>' R2_SECRET_ACCESS_KEY='<secret>' R2_BUCKET='alleanza-dvr-allegati' \
npx tsx scripts/pdf/generate.ts 467
```
Atteso: `OK 467 → pdf/10899_DVR.pdf`. Verifica su R2 che l'oggetto esista e che `dvr_pdf` per 467 sia `ready` con `data_hash` valorizzato. Scarica il PDF e confrontalo col riferimento Chromium approvato (deve combaciare).

- [ ] **Step 7: Verifica che il bundle Vercel non includa Puppeteer**

Run: `cd app && npm run build`
Expected: build OK; nessun aumento anomalo; Puppeteer non referenziato dalle route (è importato solo da `scripts/`, non bundlato).

- [ ] **Step 8: Commit**

```bash
git add app/scripts/pdf/ app/package.json app/package-lock.json
git commit -m "PDF: script di generazione Chromium (Turso+R2) per Action e seed"
```

---

