diff --git a/README.md b/README.md index e710e64..fb8b469 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ Tutte le impostazioni globali sono **flag build-time** nel file [`lib/config.ts` | Variabile | Default | Descrizione | |---|---|---| | `EXTERNAL_LINK_ENABLED` | `true` | Mostra il tipo di card "External Link" nel menu dell'admin. Le card di quel tipo già esistenti restano comunque visibili e cliccabili anche se messo a `false`. | -| `FACTORY_PRESET_SAVE_ENABLED` | `false` | Mostra il bottone "💾 Salva come Factory Preset" nell'admin (funzione developer per ricreare/aggiornare il preset). Il bottone "🏭 Factory Reset" e lo stato del preset sono **sempre visibili** indipendentemente da questo flag — vedi [Factory Preset](#factory-preset-developer). L'endpoint API `POST /api/admin/factory-preset` resta attivo a prescindere. | +| `FACTORY_PRESET_SAVE_ENABLED` | `false` | Mostra il bottone "💾 Save as Factory Preset (dev)" nell'admin (funzione developer per ricreare/aggiornare il preset). Il bottone "🏭 Factory Reset" e lo stato del preset sono **sempre visibili** indipendentemente da questo flag — vedi [Factory Preset](#factory-preset-developer). L'endpoint API `POST /api/admin/factory-preset` resta attivo a prescindere. | | `DEFAULT_FONT` | `''` | Font di default se il portale non ne ha impostato uno. Stringa vuota = font di sistema (Arial). Altrimenti il nome esatto di un file in `data/fonts/` (es. `"Geist-Variable.woff2"`). | | `TEXT_LIMITS` | vedi sotto | Limiti caratteri di tutti i campi testuali. | | `UPLOAD_LIMITS` | vedi sotto | Dimensioni massime upload per famiglia di file. | @@ -185,7 +185,7 @@ Copiare via questa cartella = backup completo. Sostituirla = ripristino completo Lo stato applicativo (i dati) è **solo** il contenuto di `data/`: `cards.txt`, `portals.txt`, `uploads/`, `fonts/`. Tutto il resto — codice sorgente, `node_modules`, e gli asset di default in `public/` (es. `hero-bg.jpg`, `logo.png`) — **non** fa parte dello stato dati: arriva con il rilascio del software. "Azzerare" il CPC significa quindi sostituire `data/` con uno stato noto. ### Contenuti vs codice (due archivi distinti) -- **Backup dei contenuti**: archivia solo `data/` (lo fanno il pulsante "Scarica backup" e il comando `zip` documentato sotto). È ciò che si conserva e si ripristina. +- **Backup dei contenuti**: archivia solo `data/` (lo fanno il pulsante "Save backup (ZIP)" e il comando `zip` documentato sotto). È ciò che si conserva e si ripristina. - **Aggiornamento del codice**: si sostituisce tutto **tranne** `data/`. I contenuti restano al loro posto. - Da CLI un reset conservativo è: `mv data data.old && `. È l'equivalente del `tar zxf` citato dal QA — vedi nota su ZIP vs tar nella sezione [Backup](#backup-e-ripristino). @@ -202,8 +202,8 @@ Per avere uno stato di partenza noto su ogni macchina nuova, includere nel pacch Disponibile dall'admin in **Settings → Backup & Restore**. ### Dall'interfaccia -- **⬇ Scarica backup ZIP** — scarica `interceptop-backup-.zip` con card, configurazione, media e font (esclude i file temporanei). -- **⤴ Ripristina da ZIP…** — carica uno zip; dopo conferma sovrascrive lo stato attuale e ricarica la pagina. La cartella `data/` precedente viene conservata come `data.bak-/` come rete di sicurezza. +- **⬇ Save backup (ZIP)** — scarica `interceptop-backup-YYYYMMDD-hhmmss.zip` con card, configurazione, media e font (esclude i file temporanei). +- **⤴ Restore from ZIP** — carica uno zip; dopo conferma sovrascrive lo stato attuale e ricarica la pagina. La cartella `data/` precedente viene conservata come `data.bak-/` come rete di sicurezza. ### Da riga di comando (Linux) @@ -218,11 +218,11 @@ zip -r ~/backup-$(date +%Y%m%d-%H%M%S).zip cards.txt portals.txt uploads fonts - unzip -l ~/backup-*.zip ``` -Lo zip così prodotto è caricabile direttamente dal pulsante "Ripristina da ZIP…". +Lo zip così prodotto è caricabile direttamente dal pulsante "Restore from ZIP". > **Struttura obbligatoria:** i file devono stare alla radice dello zip. Uno zip con tutto dentro una cartella `data/` verrà rifiutato con "cards.txt assente". Deve essere uno **ZIP**, non un `.tar`. -> **ZIP vs tar:** il CPC usa archivi **ZIP** (via `zip`/`unzip`), non `tar`. La finalità è la stessa di un `tar cf`/`tar zxf`: un singolo archivio dei soli contenuti, ripristinabile in un colpo. Il restore accetta lo ZIP prodotto dal pulsante "Scarica backup" o dal comando `zip` qui sopra. +> **ZIP vs tar:** il CPC usa archivi **ZIP** (via `zip`/`unzip`), non `tar`. La finalità è la stessa di un `tar cf`/`tar zxf`: un singolo archivio dei soli contenuti, ripristinabile in un colpo. Il restore accetta lo ZIP prodotto dal pulsante "Save backup (ZIP)" o dal comando `zip` qui sopra. --- @@ -234,9 +234,9 @@ Stato "di fabbrica" ripristinabile con un click. Pensato per preparare preset st Il preset è un file fisso: **`factory/preset.zip`** alla radice del progetto (fuori da `data/`, quindi non viene toccato dai reset; fuori da `public/`, quindi non scaricabile via web). -### Dall'interfaccia (con flag attivo) -- **💾 Salva stato attuale come Factory Preset** — congela lo stato corrente in `factory/preset.zip`. -- **🏭 Factory Reset** — ripristina tutto al preset (disabilitato se il preset non esiste). La `data/` precedente resta come `data.bak-/`. +### Dall'interfaccia +- **🏭 Factory Reset** — sempre visibile. Ripristina tutto al preset (disabilitato se il preset non esiste). La `data/` precedente resta come `data.bak-/`. +- **💾 Save as Factory Preset (dev)** — visibile solo con `FACTORY_PRESET_SAVE_ENABLED = true`. Congela lo stato corrente in `factory/preset.zip`. ### Da riga di comando / API ```bash @@ -486,7 +486,7 @@ Sul server servono alcuni binari di sistema (richiamati direttamente, non via np |---|---|---| | `ffmpeg` | Transcodifica video non compatibili | Upload video che richiedono ricodifica → `503` | | `ffprobe` | Riconoscimento codec video | Come sopra | -| `zip` | Creazione backup / preset | Pulsante "Scarica backup" e "Salva preset" → `503` | +| `zip` | Creazione backup / preset | Pulsanti "Save backup (ZIP)" e "Save as Factory Preset (dev)" → `503` | | `unzip` | Ripristino backup / factory reset | Pulsanti di ripristino → `503` | Verifica su server: @@ -510,4 +510,4 @@ rm -rf .next && npm run build **Recupero dopo un ripristino sbagliato**: lo stato precedente è in `data.bak-/`. Ferma il server, rinomina quella cartella in `data/` e riavvia. -**Il bottone "Salva come Factory Preset" non compare**: è dietro il flag `FACTORY_PRESET_SAVE_ENABLED` in `lib/config.ts`. Mettilo a `true` e ricostruisci (di norma lo si tiene `false` in produzione). Il bottone "🏭 Factory Reset" e lo stato del preset restano comunque sempre visibili. +**Il bottone "Save as Factory Preset (dev)" non compare**: è dietro il flag `FACTORY_PRESET_SAVE_ENABLED` in `lib/config.ts`. Mettilo a `true` e ricostruisci (di norma lo si tiene `false` in produzione). Il bottone "🏭 Factory Reset" e lo stato del preset restano comunque sempre visibili. diff --git a/app/admin/page.tsx b/app/admin/page.tsx index e47699f..d3ddcaa 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -56,13 +56,13 @@ function RichTextMini({ value, onChange, limit, className }: RichTextMiniProps) type="button" onClick={() => exec('bold')} className="font-bold w-8 h-8 border border-gray-300 rounded hover:bg-gray-100" - title="Grassetto" + title="Bold" >B
({ onClick={() => setOpen(o => !o)} className={`${inputBase} text-left flex items-center justify-between cursor-pointer`} > - {displayLabel || 'Seleziona…'} + {displayLabel || 'Select…'} {open && ( @@ -320,7 +320,7 @@ export default function AdminDashboard() { imageUrl: prev.imageUrl === url ? '' : prev.imageUrl, } : prev); const msg = data.status === 'failed' - ? `Trascodifica fallita${data.error ? `: ${String(data.error).split('\n')[0]}` : ''}` + ? `Transcoding failed${data.error ? `: ${String(data.error).split('\n')[0]}` : ''}` : 'Trascodifica annullata'; showToast(msg, 'error'); } else { @@ -379,9 +379,9 @@ export default function AdminDashboard() { if (rejected.length > 0) { const list = rejected.length <= 3 ? rejected.join(', ') - : `${rejected.slice(0, 3).join(', ')} e altri ${rejected.length - 3}`; + : `${rejected.slice(0, 3).join(', ')} and ${rejected.length - 3} more`; showToast( - `Formato non supportato! I formati supportati sono: ${PLAYBACK_SUPPORTED_LABEL}. File ignorati: ${list}`, + `Unsupported format! Supported formats: ${PLAYBACK_SUPPORTED_LABEL}. Skipped files: ${list}`, 'error' ); } @@ -504,7 +504,7 @@ export default function AdminDashboard() { // External Link: URL obbligatorio (feedback immediato, ribadito anche lato server) if (isEditing.cardType === 'EXTERNAL_LINK' && !isEditing.actionUrl?.trim()) { - showToast("L'URL è obbligatorio per le card External Link", 'error'); + showToast('URL is required for External Link cards', 'error'); return; } @@ -516,7 +516,7 @@ export default function AdminDashboard() { }); if (!res.ok) { - let message = 'Errore di salvataggio'; + let message = 'Save error'; try { const body = await res.json(); if (res.status === 400 && Array.isArray(body?.errors) && body.errors.length > 0) { @@ -595,7 +595,7 @@ export default function AdminDashboard() { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; - if (!window.confirm('Il ripristino sovrascriverà tutti i dati attuali (card, portale, media, font). Continuare?')) return; + if (!window.confirm('Restore will overwrite all current data (cards, portal, media, fonts). Continue?')) return; setRestoring(true); try { @@ -604,13 +604,13 @@ export default function AdminDashboard() { const res = await fetch(withBasePath('/api/admin/restore'), { method: 'POST', body: fd }); const data = await res.json().catch(() => ({})); if (!res.ok) { - showToast(data?.error || `Errore ripristino (${res.status})`, 'error'); + showToast(data?.error || `Restore error (${res.status})`, 'error'); return; } - showToast(`Ripristino completato: ${data.restored?.cards ?? 0} card, ${data.restored?.portals ?? 0} portali. Ricarico…`); + showToast(`Restore completed: ${data.restored?.cards ?? 0} cards, ${data.restored?.portals ?? 0} portals. Reloading…`); setTimeout(() => window.location.reload(), 1200); } catch (err) { - showToast(`Errore di rete: ${(err as Error).message}`, 'error'); + showToast(`Network error: ${(err as Error).message}`, 'error'); } finally { setRestoring(false); } @@ -634,40 +634,40 @@ export default function AdminDashboard() { const handleSaveFactoryPreset = async () => { const msg = factoryPreset?.exists - ? 'Sovrascrivere il factory preset esistente con lo stato attuale?' - : 'Salvare lo stato attuale come factory preset?'; + ? 'Overwrite the existing factory preset with the current state?' + : 'Save the current state as factory preset?'; if (!window.confirm(msg)) return; setSavingPreset(true); try { const res = await fetch(withBasePath('/api/admin/factory-preset'), { method: 'POST' }); const data = await res.json().catch(() => ({})); if (!res.ok) { - showToast(data?.error || `Errore (${res.status})`, 'error'); + showToast(data?.error || `Error (${res.status})`, 'error'); return; } - showToast('Factory preset aggiornato.'); + showToast('Factory preset updated.'); await refreshFactoryPreset(); } catch (err) { - showToast(`Errore di rete: ${(err as Error).message}`, 'error'); + showToast(`Network error: ${(err as Error).message}`, 'error'); } finally { setSavingPreset(false); } }; const handleFactoryReset = async () => { - if (!window.confirm('FACTORY RESET — tutti i dati attuali verranno sostituiti col factory preset. Continuare?')) return; + if (!window.confirm('FACTORY RESET — all current data will be replaced with the factory preset. Continue?')) return; setFactoryResetting(true); try { const res = await fetch(withBasePath('/api/admin/factory-reset'), { method: 'POST' }); const data = await res.json().catch(() => ({})); if (!res.ok) { - showToast(data?.error || `Errore (${res.status})`, 'error'); + showToast(data?.error || `Error (${res.status})`, 'error'); return; } - showToast(`Factory reset eseguito: ${data.restored?.cards ?? 0} card, ${data.restored?.portals ?? 0} portali. Ricarico…`); + showToast(`Factory reset completed: ${data.restored?.cards ?? 0} cards, ${data.restored?.portals ?? 0} portals. Reloading…`); setTimeout(() => window.location.reload(), 1200); } catch (err) { - showToast(`Errore di rete: ${(err as Error).message}`, 'error'); + showToast(`Network error: ${(err as Error).message}`, 'error'); } finally { setFactoryResetting(false); } @@ -737,7 +737,7 @@ export default function AdminDashboard() { [{card.extraMedia.length}] )} {card.cardType === 'FULLSCREEN_LOCK' && ( - LOCK ATTIVA + LOCK ACTIVE )}
@@ -806,7 +806,7 @@ export default function AdminDashboard() {
- +