Calendario: il PDF e' un file vero, generato dal server

"Scarica in PDF" non apre piu' la stampa del browser: e' un link a
/calendario/<id>.pdf che restituisce un documento disegnato dal dato con PDFKit,
con Content-Disposition attachment. Serve perche' il calendario si gira su
WhatsApp, ed e' cosi' che questo centro parla coi clienti: chi lo riceve non ha
il sito aperto davanti.

Niente browser headless: Chromium in un container da 2 vCPU costa centinaia di
megabyte e un processo per richiesta, per una griglia di sette colonne.

Le linee della griglia si tracciano in una passata a parte, dopo i riempimenti:
disegnandole riga per riga, il riempimento della riga seguente copriva quella
appena fatta e due lezioni uguali in fasce consecutive si leggevano come un
blocco unico lungo il doppio.

Quattro test: intestazione %PDF- e coda %%EOF (un documento non chiuso arriva
monco e il browser mostra una pagina bianca invece di un errore), una pagina
sola, nomi di file distinti. Un id sconosciuto e' 404, non un PDF vuoto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 16:14:02 +02:00
parent 01f49ab6d2
commit 5be0fa787c
6 changed files with 416 additions and 11 deletions
+8 -8
View File
@@ -33,7 +33,7 @@ const presenti = cal
)}
</div>
<div class="cal-dlg__azioni">
<button type="button" class="cal-dlg__btn" data-stampa-calendario>{t('calendario.pdf-label')}</button>
<a class="cal-dlg__btn" href={`/calendario/${cal.id}.pdf`} download>{t('calendario.pdf-label')}</a>
<button type="button" class="cal-dlg__chiudi" data-chiudi-calendario aria-label={t('calendario.chiudi-label')}>×</button>
</div>
</header>
@@ -93,10 +93,9 @@ const presenti = cal
dlg.querySelector('[data-chiudi-calendario]')?.addEventListener('click', () => dlg.close());
// Click sul fondale: il target è il dialog stesso solo quando si preme fuori dal riquadro.
dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); });
// "Scarica in PDF" = stampa con la destinazione "Salva come PDF": la pagina di stampa è
// già impaginata per la sola griglia (vedi @media print), quindi non serve generare nulla
// lato server né caricare una libreria.
dlg.querySelector('[data-stampa-calendario]')?.addEventListener('click', () => window.print());
// "Scarica in PDF" è un link a `/calendario/<id>.pdf`, cioè un file vero generato dal
// server: si salva sul telefono e si gira su WhatsApp. Il CSS di stampa più sotto resta
// per chi preme Ctrl+P col popup aperto.
}
</script>
@@ -122,9 +121,10 @@ const presenti = cal
}
.cal-dlg__azioni { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
.cal-dlg__btn {
font-family: var(--font-heading); font-size: .68rem; font-weight: 600; letter-spacing: .2em;
text-transform: uppercase; border: 0; cursor: pointer; padding: 13px 22px;
background: var(--c-accent); color: #fff; transition: background .2s;
display: inline-block; font-family: var(--font-heading); font-size: .68rem; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase; text-decoration: none; border: 0;
cursor: pointer; padding: 13px 22px; background: var(--c-accent); color: #fff;
transition: background .2s;
}
.cal-dlg__btn:hover { background: var(--c-accent-dark); }
.cal-dlg__chiudi {