diff --git a/CLAUDE.md b/CLAUDE.md index 25a3f87..7fc4dad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -280,6 +280,23 @@ NIS2 Agile e' una piattaforma SaaS multi-tenant per supportare le aziende nella Target: PMI, Enterprise, Consulenti/CISO. +## PWA conforme AGID — v1.15.0 (2026-06-14) + +NIS2 e' una **PWA installabile** (Android/Chrome, iOS/iPadOS via "Aggiungi a Home"), responsive, conforme AGID/WCAG 2.1 AA. + +**File**: +- `public/manifest.webmanifest` — manifest (theme `#0066CC` blu Italia, `display: standalone`, `orientation: any`, icone any+maskable, shortcuts). +- `public/sw.js` — service worker. Cache `nis2-shell-vX.Y.Z`. **Strategia**: `/api/*` **network-only** (MAI cacheato → no leak cross-utente su device condiviso); navigazioni HTML **network-first** → fallback `/offline.html`; asset statici **stale-while-revalidate**. **Bumpare il nome cache ad ogni release** (e' legato alla versione). +- `public/offline.html` — fallback offline (Bootstrap Italia + CSS inline di riserva). +- `public/js/pwa.js` — registra il SW + banner "Installa app" (Android) + hint "Aggiungi a Home" (iOS), i18n IT/EN (chiave `nis2_lang`). +- `public/assets/icons/*` — set icone (192/512, maskable, apple-touch 180, favicon 16/32, `icon.svg` master). +- `scripts/gen-pwa-icons.mjs` — **genera le icone** senza dipendenze (pure Node + zlib): `node scripts/gen-pwa-icons.mjs`. +- `scripts/inject-pwa-head.mjs` — **inietta i tag PWA** negli `` (idempotente): `node scripts/inject-pwa-head.mjs`. Esclude `offline.html` e i file `* copy`. + +**AGID/WCAG**: design system Bootstrap Italia (gia' live); **zoom e orientamento non bloccati**; **touch target ≥44px** (`@media (pointer: coarse)` in `style.css`); safe-area inset in `@media (display-mode: standalone)`. La "Dichiarazione di accessibilita'" AGID e' obbligatoria solo per servizi PA → qui opzionale (non implementata). + +**Deploy**: tutto **statico** → live via bind-mount nginx, **nessun `kill -USR2`** (nessun `.php` toccato). Dopo edit a `sw.js`/`pwa.js` o ai tag head ricordarsi il **cache-buster** `?v=` (i tag head usano `pwa.js?v=20260614`) e il bump del nome cache SW. + ## Stack Tecnologico - Backend: PHP 8.4 vanilla (no framework, Front Controller pattern) - Database: MySQL 8.x (nis2_agile_db) diff --git a/docs/CONTEXT_LAST_SESSION.md b/docs/CONTEXT_LAST_SESSION.md index 182d42d..87c4001 100644 --- a/docs/CONTEXT_LAST_SESSION.md +++ b/docs/CONTEXT_LAST_SESSION.md @@ -4,6 +4,15 @@ ## 2026-06-14 — ✅ SUPERVISORE AUTONOMO TICKET LIVE (cron) + chiusura demo/avatar/feedback +### PWA conforme AGID — v1.15.0 (sessione serale) +NIS2 reso **PWA installabile** su Android (Chrome) e iOS/iPadOS (Safari "Aggiungi a Home"), responsive, **conforme AGID/WCAG 2.1 AA**. Avviato come flotta di agenti in background, completato inline dopo riavvio del processo (gli agenti background erano andati persi). +- **File nuovi**: `public/manifest.webmanifest` (theme `#0066CC` blu Italia, display standalone, orientation any, shortcuts Dashboard/Incidenti) · `public/sw.js` · `public/offline.html` · `public/js/pwa.js` (register SW + banner installazione Android + hint iOS, i18n IT/EN) · `public/assets/icons/*` (icon-192/512 + maskable + apple-touch 180 + favicon 16/32 + `icon.svg` master) · `scripts/gen-pwa-icons.mjs` (generatore icone zero-deps, pure Node+zlib) · `scripts/inject-pwa-head.mjs` (iniezione idempotente tag head). +- **Service worker**: cache `nis2-shell-v1.15.0`; `/api/*` **network-only** (no leak cross-utente su device condiviso), navigazioni HTML **network-first** → `offline.html`, asset **stale-while-revalidate**. Bumpare il nome cache ad ogni release. +- **Head**: tag PWA iniettati in **77 HTML** (64 + 6 admin + 7 subdir, escluso `offline.html`); viewport normalizzato a `viewport-fit=cover` **mantenendo lo zoom** (WCAG 1.4.4). +- **AGID/WCAG**: Bootstrap Italia (già live), zoom+orientamento liberi, **touch target ≥44px** (`@media (pointer: coarse)` in `style.css`), safe-area in standalone. "Dichiarazione di accessibilità" obbligatoria solo per PA → opzionale, non implementata. +- **Rigenerare**: `node scripts/gen-pwa-icons.mjs` (icone) · `node scripts/inject-pwa-head.mjs` (tag head, idempotente). Tutto **statico** → live via bind-mount, nessun USR2. +- Bump `version.json` 1.14.1 → **1.15.0**. **commit/push in attesa di conferma utente** al momento del salvataggio. + ### Supervisore autonomo ticket (lavoro principale di oggi) NIS2 = **primo adopter LIVE** dello standard AgileHub `autonomous-supervisor-agent` (`hub_standards` id=32). Un agente `claude -p --model opus` headless (chiave SSH **effimera per ciclo**) fa da supervisore **end-to-end** ai ticket NIS2 → **NIS2 esce dal `ticket-agent-cron` generico**. - **Cron LIVE**: `/etc/cron.d/nis2-supervisor` → `17 */4 * * *` (ogni 4h, minuto 17). **Kill switch**: `touch /etc/agilehub/supervisor-nis2.disabled`. **Rollback**: `rm /etc/cron.d/nis2-supervisor`. Audit: `/var/log/nis2-supervisor-audit.jsonl`. diff --git a/public/_app-bi-demo.html b/public/_app-bi-demo.html index 7d35a00..7618725 100644 --- a/public/_app-bi-demo.html +++ b/public/_app-bi-demo.html @@ -2,7 +2,7 @@ - + Demo Sidebar BI - NIS2 Agile @@ -23,6 +23,19 @@ .demo-note h1 { font-size: 1.25rem; margin: 0 0 8px; color: #17324d; } .demo-note p { margin: 0; color: #5b6772; font-size: .9rem; } + + + + + + + + + + + + +
diff --git a/public/_bi-smoke.html b/public/_bi-smoke.html index db9d604..32204f2 100644 --- a/public/_bi-smoke.html +++ b/public/_bi-smoke.html @@ -2,10 +2,23 @@ - + Bootstrap Italia self-host — smoke test + + + + + + + + + + + + +
diff --git a/public/acn-gap-bi.html b/public/acn-gap-bi.html index f6f1a2f..d7264dc 100644 --- a/public/acn-gap-bi.html +++ b/public/acn-gap-bi.html @@ -2,7 +2,7 @@ - + Gap Analysis ACN (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/acn-gap.html b/public/acn-gap.html index cd0986b..7bbda4a 100644 --- a/public/acn-gap.html +++ b/public/acn-gap.html @@ -2,7 +2,7 @@ - + Gap Analysis ACN - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/index-bi.html b/public/admin/index-bi.html index de30f5b..359ea97 100644 --- a/public/admin/index-bi.html +++ b/public/admin/index-bi.html @@ -2,7 +2,7 @@ - + Pannello Amministrazione (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/index.html b/public/admin/index.html index 37e7530..94ef2b9 100644 --- a/public/admin/index.html +++ b/public/admin/index.html @@ -2,7 +2,7 @@ - + Pannello Amministrazione - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/organizations-bi.html b/public/admin/organizations-bi.html index 02cc5d2..c75606b 100644 --- a/public/admin/organizations-bi.html +++ b/public/admin/organizations-bi.html @@ -2,7 +2,7 @@ - + Gestione Organizzazioni (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/organizations.html b/public/admin/organizations.html index 761f4d2..f4debfa 100644 --- a/public/admin/organizations.html +++ b/public/admin/organizations.html @@ -2,7 +2,7 @@ - + Gestione Organizzazioni - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/users-bi.html b/public/admin/users-bi.html index 643b667..efad163 100644 --- a/public/admin/users-bi.html +++ b/public/admin/users-bi.html @@ -2,7 +2,7 @@ - + Gestione Utenti (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/admin/users.html b/public/admin/users.html index 25b595a..05e981f 100644 --- a/public/admin/users.html +++ b/public/admin/users.html @@ -2,7 +2,7 @@ - + Gestione Utenti - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/architecture.html b/public/architecture.html index 060724a..fec0805 100644 --- a/public/architecture.html +++ b/public/architecture.html @@ -2,7 +2,7 @@ - + Architettura - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/assessment-bi.html b/public/assessment-bi.html index 1b5d19f..907ea98 100644 --- a/public/assessment-bi.html +++ b/public/assessment-bi.html @@ -2,13 +2,26 @@ - + Gap Analysis (BI) - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/assessment.html b/public/assessment.html index 23efd41..7feb63f 100644 --- a/public/assessment.html +++ b/public/assessment.html @@ -2,13 +2,26 @@ - + Gap Analysis - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/assets-bi.html b/public/assets-bi.html index 9e45c4a..5a48ba3 100644 --- a/public/assets-bi.html +++ b/public/assets-bi.html @@ -2,7 +2,7 @@ - + Inventario Asset (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/assets.html b/public/assets.html index 81522c1..3cb17fd 100644 --- a/public/assets.html +++ b/public/assets.html @@ -2,7 +2,7 @@ - + Inventario Asset - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/assets/icons/apple-touch-icon.png b/public/assets/icons/apple-touch-icon.png new file mode 100644 index 0000000..1650ea1 Binary files /dev/null and b/public/assets/icons/apple-touch-icon.png differ diff --git a/public/assets/icons/favicon-16.png b/public/assets/icons/favicon-16.png new file mode 100644 index 0000000..433d020 Binary files /dev/null and b/public/assets/icons/favicon-16.png differ diff --git a/public/assets/icons/favicon-32.png b/public/assets/icons/favicon-32.png new file mode 100644 index 0000000..8018849 Binary files /dev/null and b/public/assets/icons/favicon-32.png differ diff --git a/public/assets/icons/icon-192-maskable.png b/public/assets/icons/icon-192-maskable.png new file mode 100644 index 0000000..4dd44cb Binary files /dev/null and b/public/assets/icons/icon-192-maskable.png differ diff --git a/public/assets/icons/icon-192.png b/public/assets/icons/icon-192.png new file mode 100644 index 0000000..c2cdedc Binary files /dev/null and b/public/assets/icons/icon-192.png differ diff --git a/public/assets/icons/icon-512-maskable.png b/public/assets/icons/icon-512-maskable.png new file mode 100644 index 0000000..0271ea1 Binary files /dev/null and b/public/assets/icons/icon-512-maskable.png differ diff --git a/public/assets/icons/icon-512.png b/public/assets/icons/icon-512.png new file mode 100644 index 0000000..3bbbb82 Binary files /dev/null and b/public/assets/icons/icon-512.png differ diff --git a/public/assets/icons/icon.svg b/public/assets/icons/icon.svg new file mode 100644 index 0000000..bba3428 --- /dev/null +++ b/public/assets/icons/icon.svg @@ -0,0 +1,10 @@ + + + NIS2 Agile + + + + + + + diff --git a/public/companies-bi.html b/public/companies-bi.html index 939879e..a8501ed 100644 --- a/public/companies-bi.html +++ b/public/companies-bi.html @@ -2,7 +2,7 @@ - + Le Mie Aziende - NIS2 Agile @@ -268,6 +268,19 @@ gap: 16px; } + + + + + + + + + + + + +
diff --git a/public/companies.html b/public/companies.html index ffecbda..c212a3f 100644 --- a/public/companies.html +++ b/public/companies.html @@ -2,7 +2,7 @@ - + Le Mie Aziende - NIS2 Agile @@ -268,6 +268,19 @@ gap: 16px; } + + + + + + + + + + + + +
diff --git a/public/cross-analysis-bi.html b/public/cross-analysis-bi.html index 1b350db..59935a8 100644 --- a/public/cross-analysis-bi.html +++ b/public/cross-analysis-bi.html @@ -2,7 +2,7 @@ - + AI Cross-Analysis (BI) — NIS2 Agile + + + + + + + + + + + +
diff --git a/public/cross-analysis.html b/public/cross-analysis.html index f3ea826..50b8811 100644 --- a/public/cross-analysis.html +++ b/public/cross-analysis.html @@ -2,7 +2,7 @@ - + AI Cross-Analysis — NIS2 Agile + + + + + + + + + + + +
diff --git a/public/css/style.css b/public/css/style.css index 563a7b8..e0275f6 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -2411,3 +2411,44 @@ tbody tr.clickable:active { .feedback-resolve-actions { flex-direction: column; } .feedback-password-row { flex-direction: column; } } + +/* ── PWA / AGID — Accessibilità mobile ────────────────────────────── + Touch target >= 44x44px (WCAG 2.5.5 / Linee guida di design AGID) e + gestione safe-area in modalità installata (notch). Solo aggiunte. */ +@media (pointer: coarse) { + .btn, + button, + input[type="submit"], + input[type="button"], + .nav-link, + .sidebar a, + a.btn, + .icon-btn, + .sidebar-toggle { + min-height: 44px; + } + .btn, + a.btn, + input[type="submit"], + input[type="button"], + .icon-btn { + display: inline-flex; + align-items: center; + justify-content: center; + } + .icon-btn, + .sidebar-toggle { + min-width: 44px; + } +} + +/* Safe-area inset quando l'app gira installata (standalone) */ +@media all and (display-mode: standalone) { + .content-header { padding-top: max(16px, env(safe-area-inset-top)); } + .main-content { + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); + padding-bottom: env(safe-area-inset-bottom); + } + .sidebar { padding-top: env(safe-area-inset-top); } +} diff --git a/public/dashboard-bi.html b/public/dashboard-bi.html index 042025b..86f3c04 100644 --- a/public/dashboard-bi.html +++ b/public/dashboard-bi.html @@ -2,13 +2,26 @@ - + Dashboard (BI) - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/dashboard.html b/public/dashboard.html index 6e6f164..2f21560 100644 --- a/public/dashboard.html +++ b/public/dashboard.html @@ -2,13 +2,26 @@ - + Dashboard - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/docs/api.html b/public/docs/api.html index ba73458..6388ec5 100644 --- a/public/docs/api.html +++ b/public/docs/api.html @@ -2,7 +2,7 @@ - + NIS2 Agile — API Reference + + + + + + + + + + + + +
diff --git a/public/docs/testing-simulazione.html b/public/docs/testing-simulazione.html index 76fc327..d6c591c 100644 --- a/public/docs/testing-simulazione.html +++ b/public/docs/testing-simulazione.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Sistema di Testing & Simulazione + + + + + + + + + + + + + diff --git a/public/forgot-password-bi.html b/public/forgot-password-bi.html index 24cd74c..b5e5f6e 100644 --- a/public/forgot-password-bi.html +++ b/public/forgot-password-bi.html @@ -2,7 +2,7 @@ - + Password dimenticata - NIS2 Agile @@ -15,6 +15,19 @@ .auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/forgot-password.html b/public/forgot-password.html index 88bbdfc..a8c94d3 100644 --- a/public/forgot-password.html +++ b/public/forgot-password.html @@ -2,7 +2,7 @@ - + Password dimenticata - NIS2 Agile @@ -15,6 +15,19 @@ .auth-brand .brand-sub { color: #5a6772; font-size: .9rem; margin-top: 4px; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/guida-bi.html b/public/guida-bi.html index 921d4bd..45c6c22 100644 --- a/public/guida-bi.html +++ b/public/guida-bi.html @@ -2,7 +2,7 @@ - + Guida all'uso (BI) - NIS2 Agile @@ -88,6 +88,19 @@ .guide-toc-card { position: static; } } + + + + + + + + + + + + +
diff --git a/public/guida.html b/public/guida.html index 668cef1..8b46e9f 100644 --- a/public/guida.html +++ b/public/guida.html @@ -2,7 +2,7 @@ - + Guida all'uso - NIS2 Agile @@ -88,6 +88,19 @@ .guide-toc-card { position: static; } } + + + + + + + + + + + + +
diff --git a/public/incidents-bi.html b/public/incidents-bi.html index b4be6ca..aff8024 100644 --- a/public/incidents-bi.html +++ b/public/incidents-bi.html @@ -2,7 +2,7 @@ - + Gestione Incidenti (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/incidents.html b/public/incidents.html index af57793..743dd57 100644 --- a/public/incidents.html +++ b/public/incidents.html @@ -2,7 +2,7 @@ - + Gestione Incidenti - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/index-bi.html b/public/index-bi.html index fc41117..9a7c169 100644 --- a/public/index-bi.html +++ b/public/index-bi.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Compliance NIS2 semplificata per aziende e consulenti @@ -33,6 +33,19 @@ footer.app-footer a { color: #cbd5e1; } + + + + + + + + + + + + + diff --git a/public/index-en.html b/public/index-en.html index ea7acdb..c67660d 100644 --- a/public/index-en.html +++ b/public/index-en.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Simplified NIS2 compliance for companies and consultants @@ -678,6 +678,19 @@ + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 065ae8e..6511d8e 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Compliance NIS2 semplificata per aziende e consulenti @@ -693,6 +693,19 @@ + + + + + + + + + + + + + diff --git a/public/integrations/allrisk.html b/public/integrations/allrisk.html index b60522b..712a432 100644 --- a/public/integrations/allrisk.html +++ b/public/integrations/allrisk.html @@ -2,7 +2,7 @@ - + AllRisk × NIS2 Agile — Integrazione + + + + + + + + + + + + +
diff --git a/public/integrations/index.html b/public/integrations/index.html index 329d8da..6046072 100644 --- a/public/integrations/index.html +++ b/public/integrations/index.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Integrazioni + + + + + + + + + + + + +
diff --git a/public/integrations/lg231.html b/public/integrations/lg231.html index 863283e..9000182 100644 --- a/public/integrations/lg231.html +++ b/public/integrations/lg231.html @@ -2,7 +2,7 @@ - + 231 Agile × NIS2 Agile — Integrazione + + + + + + + + + + + + +
diff --git a/public/integrations/siem.html b/public/integrations/siem.html index 31d9fee..57411bb 100644 --- a/public/integrations/siem.html +++ b/public/integrations/siem.html @@ -2,7 +2,7 @@ - + SIEM × NIS2 Agile — Integrazione + + + + + + + + + + + + +
diff --git a/public/integrations/sustainai.html b/public/integrations/sustainai.html index 71f6b24..9295cd3 100644 --- a/public/integrations/sustainai.html +++ b/public/integrations/sustainai.html @@ -2,7 +2,7 @@ - + SustainAI × NIS2 Agile — Integrazione + + + + + + + + + + + + +
diff --git a/public/integrazioniext.html b/public/integrazioniext.html index f74cefc..4fe4ce0 100644 --- a/public/integrazioniext.html +++ b/public/integrazioniext.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Integrazioni Esterne + + + + + + + + + + + + + diff --git a/public/isms-bi.html b/public/isms-bi.html index 4954c25..595834f 100644 --- a/public/isms-bi.html +++ b/public/isms-bi.html @@ -2,7 +2,7 @@ - + Modello Organizzativo SGSI (ISO 27001) (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/isms.html b/public/isms.html index 5e0b025..ec1629f 100644 --- a/public/isms.html +++ b/public/isms.html @@ -2,7 +2,7 @@ - + Modello Organizzativo SGSI (ISO 27001) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/js/pwa.js b/public/js/pwa.js new file mode 100644 index 0000000..82bb8f5 --- /dev/null +++ b/public/js/pwa.js @@ -0,0 +1,130 @@ +/* + * NIS2 Agile — PWA bootstrap + * Registra il service worker e gestisce l'UI di installazione (Android + iOS). + * Vanilla JS, nessuna dipendenza. Localizzato IT/EN (chiave i18n: 'nis2_lang'). + */ +(function () { + 'use strict'; + + // --- i18n minimale, allineato a i18n.js (localStorage 'nis2_lang') --- + function lang() { + try { return (localStorage.getItem('nis2_lang') || 'it').slice(0, 2); } + catch (e) { return 'it'; } + } + var STR = { + it: { + install: 'Installa app', + banner: 'Installa NIS2 Agile sul dispositivo per un accesso più rapido.', + ios: 'Per installare: tocca Condividi e poi "Aggiungi a Home".', + close: 'Chiudi' + }, + en: { + install: 'Install app', + banner: 'Install NIS2 Agile on your device for faster access.', + ios: 'To install: tap Share, then "Add to Home Screen".', + close: 'Close' + } + }; + function t(k) { var l = lang(); return (STR[l] && STR[l][k]) || STR.it[k]; } + + function isStandalone() { + return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) + || window.navigator.standalone === true; + } + + // --- 1. Registrazione service worker --- + if ('serviceWorker' in navigator) { + window.addEventListener('load', function () { + navigator.serviceWorker.register('/sw.js', { scope: '/' }) + .catch(function (err) { console.warn('[pwa] SW registration failed', err); }); + }); + } + + // Gia' installata (standalone): niente banner di installazione. + if (isStandalone()) return; + + // --- UI banner sobrio, indipendente dal CSS dell'app --- + function makeBanner(message, withInstallBtn) { + var bar = document.createElement('div'); + bar.setAttribute('role', 'region'); + bar.setAttribute('aria-label', t('install')); + bar.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;z-index:9999;' + + 'background:#0066CC;color:#fff;border-radius:10px;padding:12px 14px;' + + 'box-shadow:0 6px 24px rgba(0,0,0,.25);display:flex;align-items:center;gap:10px;' + + 'font-family:system-ui,Arial,sans-serif;font-size:.95rem;max-width:560px;margin:0 auto;'; + + var txt = document.createElement('span'); + txt.style.cssText = 'flex:1;line-height:1.35;'; + txt.textContent = message; + bar.appendChild(txt); + + var btnInstall = null; + if (withInstallBtn) { + btnInstall = document.createElement('button'); + btnInstall.type = 'button'; + btnInstall.textContent = t('install'); + btnInstall.style.cssText = 'background:#fff;color:#0066CC;border:0;border-radius:8px;' + + 'padding:10px 16px;font-weight:600;min-height:44px;cursor:pointer;white-space:nowrap;'; + bar.appendChild(btnInstall); + } + + var btnClose = document.createElement('button'); + btnClose.type = 'button'; + btnClose.setAttribute('aria-label', t('close')); + btnClose.textContent = '✕'; + btnClose.style.cssText = 'background:transparent;color:#fff;border:0;font-size:1.1rem;' + + 'line-height:1;min-width:44px;min-height:44px;cursor:pointer;'; + bar.appendChild(btnClose); + + document.body.appendChild(bar); + return { bar: bar, btnInstall: btnInstall, btnClose: btnClose }; + } + + // --- 2. Android / Chrome: beforeinstallprompt --- + var deferred = null; + var DISMISS_KEY = 'pwa-install-dismissed'; + function dismissed() { try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch (e) { return false; } } + function setDismissed() { try { localStorage.setItem(DISMISS_KEY, '1'); } catch (e) {} } + + window.addEventListener('beforeinstallprompt', function (e) { + e.preventDefault(); + deferred = e; + if (dismissed()) return; + try { + var ui = makeBanner(t('banner'), true); + ui.btnInstall.addEventListener('click', function () { + ui.bar.remove(); + if (!deferred) return; + deferred.prompt(); + if (deferred.userChoice && deferred.userChoice.finally) { + deferred.userChoice.finally(function () { deferred = null; }); + } else { deferred = null; } + }); + ui.btnClose.addEventListener('click', function () { ui.bar.remove(); setDismissed(); }); + } catch (err) { console.warn('[pwa] banner error', err); } + }); + + window.addEventListener('appinstalled', function () { + deferred = null; + setDismissed(); + }); + + // --- 3. iOS Safari: nessun beforeinstallprompt -> hint manuale (una volta) --- + try { + var ua = navigator.userAgent || ''; + var isIOS = /iphone|ipad|ipod/i.test(ua) + || (/Macintosh/.test(ua) && 'ontouchend' in document); // iPad iPadOS desktop-mode + var isSafari = /safari/i.test(ua) && !/crios|fxios|edgios|android/i.test(ua); + var IOS_KEY = 'pwa-ios-hint-dismissed'; + var iosDismissed = (function () { try { return localStorage.getItem(IOS_KEY) === '1'; } catch (e) { return false; } })(); + if (isIOS && isSafari && !iosDismissed) { + window.addEventListener('load', function () { + var ui = makeBanner(t('ios'), false); + ui.btnClose.addEventListener('click', function () { + ui.bar.remove(); + try { localStorage.setItem(IOS_KEY, '1'); } catch (e) {} + }); + }); + } + } catch (err) { console.warn('[pwa] ios hint error', err); } +})(); diff --git a/public/kb-bi.html b/public/kb-bi.html index cba19c0..7029a84 100644 --- a/public/kb-bi.html +++ b/public/kb-bi.html @@ -2,7 +2,7 @@ - + Knowledge Base (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/kb.html b/public/kb.html index 2922929..eb171f4 100644 --- a/public/kb.html +++ b/public/kb.html @@ -2,7 +2,7 @@ - + Knowledge Base - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/licenseExt.html b/public/licenseExt.html index f12fd71..1911efd 100644 --- a/public/licenseExt.html +++ b/public/licenseExt.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Gestione Licenze + + + + + + + + + + + + + diff --git a/public/login-bi.html b/public/login-bi.html index 26ac11e..d507dae 100644 --- a/public/login-bi.html +++ b/public/login-bi.html @@ -2,7 +2,7 @@ - + Accedi - NIS2 Agile @@ -19,6 +19,19 @@ .pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772; cursor: pointer; padding: 6px; } + + + + + + + + + + + + +
diff --git a/public/login.html b/public/login.html index 6fbc4bc..6cd90f1 100644 --- a/public/login.html +++ b/public/login.html @@ -2,7 +2,7 @@ - + Accedi - NIS2 Agile @@ -19,6 +19,19 @@ .pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772; cursor: pointer; padding: 6px; } + + + + + + + + + + + + +
diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..ecf8fc5 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,59 @@ +{ + "name": "NIS2 Agile — Compliance NIS2 & D.Lgs. 138/2024", + "short_name": "NIS2 Agile", + "description": "Piattaforma per la conformità alla direttiva NIS2 e al D.Lgs. 138/2024.", + "lang": "it", + "dir": "ltr", + "start_url": "/dashboard.html?source=pwa", + "scope": "/", + "display": "standalone", + "display_override": ["standalone", "minimal-ui"], + "orientation": "any", + "theme_color": "#0066CC", + "background_color": "#FFFFFF", + "categories": ["business", "productivity", "security"], + "icons": [ + { + "src": "/assets/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/assets/icons/icon-192-maskable.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/assets/icons/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "Dashboard", + "short_name": "Dashboard", + "url": "/dashboard.html?source=pwa", + "icons": [ + { "src": "/assets/icons/icon-192.png", "sizes": "192x192", "type": "image/png" } + ] + }, + { + "name": "Incidenti Art.23", + "short_name": "Incidenti", + "url": "/incidents.html?source=pwa", + "icons": [ + { "src": "/assets/icons/icon-192.png", "sizes": "192x192", "type": "image/png" } + ] + } + ] +} diff --git a/public/mktg-api-doc.html b/public/mktg-api-doc.html index ca880eb..f20f137 100644 --- a/public/mktg-api-doc.html +++ b/public/mktg-api-doc.html @@ -2,7 +2,7 @@ - + NIS2 Agile — License API · Documentazione Marketing + + + + + + + + + + + + +
diff --git a/public/normative-bi.html b/public/normative-bi.html index d489f9b..47ca1e2 100644 --- a/public/normative-bi.html +++ b/public/normative-bi.html @@ -2,7 +2,7 @@ - + Aggiornamenti Normativi (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/normative.html b/public/normative.html index b622864..4c15194 100644 --- a/public/normative.html +++ b/public/normative.html @@ -2,7 +2,7 @@ - + Aggiornamenti Normativi - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..5fb78c4 --- /dev/null +++ b/public/offline.html @@ -0,0 +1,36 @@ + + + + + + + Offline - NIS2 Agile + + + + + + +
+ NIS2 Agile +

Sei offline

+

Controlla la connessione di rete e riprova. Le pagine già visitate restano disponibili anche senza connessione.

+ +
+ + diff --git a/public/onboarding-bi.html b/public/onboarding-bi.html index aa1caca..942cfcb 100644 --- a/public/onboarding-bi.html +++ b/public/onboarding-bi.html @@ -2,7 +2,7 @@ - + Onboarding - NIS2 Agile @@ -100,6 +100,19 @@ .wizard-step-title { font-size: 1.25rem; } } + + + + + + + + + + + + +
diff --git a/public/onboarding.html b/public/onboarding.html index aa1caca..942cfcb 100644 --- a/public/onboarding.html +++ b/public/onboarding.html @@ -2,7 +2,7 @@ - + Onboarding - NIS2 Agile @@ -100,6 +100,19 @@ .wizard-step-title { font-size: 1.25rem; } } + + + + + + + + + + + + +
diff --git a/public/policies-bi.html b/public/policies-bi.html index 4b32a1c..21354fe 100644 --- a/public/policies-bi.html +++ b/public/policies-bi.html @@ -2,7 +2,7 @@ - + Policy (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/policies.html b/public/policies.html index 4e3534e..81fc960 100644 --- a/public/policies.html +++ b/public/policies.html @@ -2,7 +2,7 @@ - + Policy - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/presentation.html b/public/presentation.html index f80e7b1..b29458c 100644 --- a/public/presentation.html +++ b/public/presentation.html @@ -2,7 +2,7 @@ - + NIS2 Agile - Compliance NIS2 AI-Powered | Agile Technology SRL + + + + + + + + + + + + + diff --git a/public/register-bi.html b/public/register-bi.html index 08bd7c5..3ca8005 100644 --- a/public/register-bi.html +++ b/public/register-bi.html @@ -2,7 +2,7 @@ - + Registrazione - NIS2 Agile @@ -62,6 +62,19 @@ #step-0 { display: block; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/register.html b/public/register.html index 1538164..526b701 100644 --- a/public/register.html +++ b/public/register.html @@ -2,7 +2,7 @@ - + Registrazione - NIS2 Agile @@ -62,6 +62,19 @@ #step-0 { display: block; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/reports-bi.html b/public/reports-bi.html index 5424c51..b94b088 100644 --- a/public/reports-bi.html +++ b/public/reports-bi.html @@ -2,7 +2,7 @@ - + Audit e Report (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/reports.html b/public/reports.html index ce116e3..3eb2921 100644 --- a/public/reports.html +++ b/public/reports.html @@ -2,7 +2,7 @@ - + Audit e Report - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/reset-password-bi.html b/public/reset-password-bi.html index c116a2e..f388c95 100644 --- a/public/reset-password-bi.html +++ b/public/reset-password-bi.html @@ -2,7 +2,7 @@ - + Imposta nuova password - NIS2 Agile @@ -18,6 +18,19 @@ .pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772; cursor: pointer; padding: 6px; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/reset-password.html b/public/reset-password.html index 61fd128..17480b6 100644 --- a/public/reset-password.html +++ b/public/reset-password.html @@ -2,7 +2,7 @@ - + Imposta nuova password - NIS2 Agile @@ -18,6 +18,19 @@ .pw-toggle { position: absolute; right: 8px; top: 8px; background: none; border: none; color: #5a6772; cursor: pointer; padding: 6px; } .ico { width: 1em; height: 1em; fill: currentColor; vertical-align: -.125em; } + + + + + + + + + + + + +
diff --git a/public/risks-bi.html b/public/risks-bi.html index 189ab3c..3446643 100644 --- a/public/risks-bi.html +++ b/public/risks-bi.html @@ -2,7 +2,7 @@ - + Gestione Rischi (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/risks.html b/public/risks.html index 2e14a0f..ebc1f2c 100644 --- a/public/risks.html +++ b/public/risks.html @@ -2,7 +2,7 @@ - + Gestione Rischi - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/service-continuity-bi.html b/public/service-continuity-bi.html index 19ac883..09d0e3d 100644 --- a/public/service-continuity-bi.html +++ b/public/service-continuity-bi.html @@ -2,7 +2,7 @@ - + Standard Continuita di Servizio (BI) — NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/service-continuity.html b/public/service-continuity.html index f89f2c4..f4488e5 100644 --- a/public/service-continuity.html +++ b/public/service-continuity.html @@ -2,7 +2,7 @@ - + Standard Continuita di Servizio — NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/settings-bi.html b/public/settings-bi.html index 4089142..0459d17 100644 --- a/public/settings-bi.html +++ b/public/settings-bi.html @@ -2,7 +2,7 @@ - + Impostazioni (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/settings.html b/public/settings.html index cfc1f9a..c44c50f 100644 --- a/public/settings.html +++ b/public/settings.html @@ -2,7 +2,7 @@ - + Impostazioni - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/setup-org.html b/public/setup-org.html index 3545c9f..69a4d92 100644 --- a/public/setup-org.html +++ b/public/setup-org.html @@ -2,9 +2,22 @@ - + Configura Organizzazione - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/simulate-b2b.html b/public/simulate-b2b.html index 1bd8e3a..2d00c9b 100644 --- a/public/simulate-b2b.html +++ b/public/simulate-b2b.html @@ -2,7 +2,7 @@ - + Simulazione B2B — NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/simulate.html b/public/simulate.html index fc0b90e..a1303eb 100644 --- a/public/simulate.html +++ b/public/simulate.html @@ -2,7 +2,7 @@ - + NIS2 Agile — Simulazione Demo + + + + + + + + + + + + + diff --git a/public/supplier-assessment.html b/public/supplier-assessment.html index a29da9e..4c9dc32 100644 --- a/public/supplier-assessment.html +++ b/public/supplier-assessment.html @@ -2,7 +2,7 @@ - + Questionario di Sicurezza Fornitore — NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/supplier-portal.html b/public/supplier-portal.html index 5140419..2188ddd 100644 --- a/public/supplier-portal.html +++ b/public/supplier-portal.html @@ -2,7 +2,7 @@ - + Portale Fornitori - NIS2 Agile + + + + + + + + + + + + +
diff --git a/public/supply-chain-bi.html b/public/supply-chain-bi.html index 083dcdc..f55bafc 100644 --- a/public/supply-chain-bi.html +++ b/public/supply-chain-bi.html @@ -2,7 +2,7 @@ - + Supply Chain (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/supply-chain.html b/public/supply-chain.html index 6190501..d7cc071 100644 --- a/public/supply-chain.html +++ b/public/supply-chain.html @@ -2,7 +2,7 @@ - + Supply Chain - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..30e7b88 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,96 @@ +/* + * NIS2 Agile — Service Worker (PWA) + * ----------------------------------------------------------------------------- + * Strategia (SaaS autenticato multi-tenant — sicurezza prima di tutto): + * - /api/* -> NETWORK-ONLY, MAI in cache. Nessun dato autenticato + * viene cacheato => zero leak cross-utente su un + * dispositivo condiviso. La sessione resta gestita da + * JWT / auth-gate.js, fuori dal service worker. + * - navigazioni HTML -> NETWORK-FIRST, fallback alla cache e infine /offline.html. + * - asset statici -> STALE-WHILE-REVALIDATE (risposta immediata dalla cache, + * aggiornamento in background). + * + * Il nome cache include la release: va BUMPATO ad ogni rilascio (vedi version.json), + * cosi' activate elimina automaticamente la shell vecchia. + */ +const CACHE = 'nis2-shell-v1.15.0'; + +const PRECACHE = [ + '/offline.html', + '/manifest.webmanifest', + '/css/style.css', + '/js/common.js', + '/js/common-bi.js', + '/js/api.js', + '/js/i18n.js', + '/js/pwa.js', + '/vendor/bootstrap-italia/dist/css/bootstrap-italia.min.css', + '/vendor/bootstrap-italia/dist/js/bootstrap-italia.bundle.min.js', + '/vendor/bootstrap-italia/dist/svg/sprites.svg', + '/assets/icons/icon-192.png', + '/assets/icons/icon-512.png' +]; + +self.addEventListener('install', (event) => { + event.waitUntil((async () => { + const cache = await caches.open(CACHE); + // add uno-a-uno: un singolo asset mancante non deve far fallire l'install + await Promise.all(PRECACHE.map((url) => + cache.add(new Request(url, { cache: 'reload' })).catch((err) => + console.warn('[sw] precache skip', url, err && err.message)) + )); + await self.skipWaiting(); + })()); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + const keys = await caches.keys(); + await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))); + await self.clients.claim(); + })()); +}); + +self.addEventListener('fetch', (event) => { + const req = event.request; + if (req.method !== 'GET') return; // non-GET: rete normale + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; // cross-origin: rete normale + + // /api/* -> network-only, mai in cache + if (url.pathname.startsWith('/api/')) { + event.respondWith( + fetch(req).catch(() => new Response( + JSON.stringify({ error: 'offline', message: 'Connessione di rete assente' }), + { status: 503, headers: { 'Content-Type': 'application/json' } } + )) + ); + return; + } + + // navigazioni HTML -> network-first, fallback cache -> offline + if (req.mode === 'navigate') { + event.respondWith((async () => { + try { + return await fetch(req); + } catch (e) { + const cached = await caches.match(req); + return cached || (await caches.match('/offline.html')); + } + })()); + return; + } + + // asset statici stesso-origin -> stale-while-revalidate + event.respondWith((async () => { + const cache = await caches.open(CACHE); + const cached = await cache.match(req); + const network = fetch(req).then((res) => { + if (res && res.status === 200 && res.type === 'basic') { + cache.put(req, res.clone()); + } + return res; + }).catch(() => null); + return cached || (await network) || (await caches.match('/offline.html')); + })()); +}); diff --git a/public/training-bi.html b/public/training-bi.html index 3421225..c69bd13 100644 --- a/public/training-bi.html +++ b/public/training-bi.html @@ -2,7 +2,7 @@ - + Formazione (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/training.html b/public/training.html index c741106..cdf5c3c 100644 --- a/public/training.html +++ b/public/training.html @@ -2,7 +2,7 @@ - + Formazione - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/version.json b/public/version.json index e26bb4a..45b7213 100644 --- a/public/version.json +++ b/public/version.json @@ -1 +1 @@ -{"version":"1.14.1","build":"2026-06-14-v1.14.1","date":"2026-06-14","changelog":"Help online: rimossa icona FontAwesome residua (fas fa-comment-alt) non piu renderizzata dopo lo swap a Bootstrap Italia V2, nella guida 'Come aprire una segnalazione' (help.js). Cache-buster ?v di help.js -> 20260614 su 35 pagine. Nessuna modifica a dati o schema DB."} +{"version":"1.15.0","build":"2026-06-14-v1.15.0","date":"2026-06-14","changelog":"PWA installabile (Android/iOS): aggiunti manifest.webmanifest, service worker (offline shell, /api network-only, asset stale-while-revalidate), set icone (192/512 + maskable + apple-touch + favicon), tag PWA in tutte le pagine HTML. Conformita AGID/WCAG 2.1 AA: zoom e orientamento non bloccati, touch target >=44px, safe-area in modalita standalone. Nessuna modifica a dati o schema DB."} diff --git a/public/whistleblowing-bi.html b/public/whistleblowing-bi.html index 5e773bf..7dcf193 100644 --- a/public/whistleblowing-bi.html +++ b/public/whistleblowing-bi.html @@ -2,7 +2,7 @@ - + Segnalazioni Sicurezza (BI) - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/whistleblowing.html b/public/whistleblowing.html index e008e25..5a17db3 100644 --- a/public/whistleblowing.html +++ b/public/whistleblowing.html @@ -2,7 +2,7 @@ - + Segnalazioni Sicurezza - NIS2 Agile + + + + + + + + + + + +
diff --git a/public/workflow.html b/public/workflow.html index b781aeb..5be519a 100644 --- a/public/workflow.html +++ b/public/workflow.html @@ -2,7 +2,7 @@ - + Compliance Journey - NIS2 Agile @@ -285,6 +285,19 @@ .journey-header { flex-direction: column; } } + + + + + + + + + + + + +
diff --git a/scripts/gen-pwa-icons.mjs b/scripts/gen-pwa-icons.mjs new file mode 100644 index 0000000..f2202ba --- /dev/null +++ b/scripts/gen-pwa-icons.mjs @@ -0,0 +1,487 @@ +#!/usr/bin/env node +/** + * gen-pwa-icons.mjs — NIS2 Agile PWA icon generator (dependency-free) + * + * Generates the full PWA icon set using ONLY Node v20 built-ins: + * - node:zlib (deflate for PNG IDAT) + * - node:fs (write files) + * - node:path (resolve paths) + * + * Design: Italy-blue (#0066CC) square tile, centered white shield + * (compliance/security silhouette) with a bold "negative" check mark + * cut out of the shield (rendered in #0066CC) so it stays readable + * down to 16px. + * + * Rendering: each icon is drawn at 4x (supersampling) into an RGBA + * buffer using hand-written primitives (rounded rect, scanline polygon + * fill), then box-downsampled to final size for smooth anti-aliased + * edges. PNG is encoded manually (8-bit, color type 6 RGBA). + * + * Run: cd /projects/nis2-agile && node scripts/gen-pwa-icons.mjs + */ + +import zlib from 'node:zlib'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const PROJECT_ROOT = path.resolve(__dirname, '..'); +const ICONS_DIR = path.join(PROJECT_ROOT, 'public', 'assets', 'icons'); + +// --------------------------------------------------------------------------- +// Palette +// --------------------------------------------------------------------------- +const BLUE = [0, 102, 204]; // #0066CC — Italy blue +const WHITE = [255, 255, 255]; + +// --------------------------------------------------------------------------- +// RGBA canvas helpers +// --------------------------------------------------------------------------- +function makeCanvas(w, h, fill) { + // fill = [r,g,b,a] or null => transparent (0,0,0,0) + const buf = new Uint8Array(w * h * 4); + if (fill) { + for (let i = 0; i < w * h; i++) { + const o = i * 4; + buf[o] = fill[0]; + buf[o + 1] = fill[1]; + buf[o + 2] = fill[2]; + buf[o + 3] = fill[3]; + } + } + return { w, h, buf }; +} + +function setPx(cv, x, y, rgba) { + if (x < 0 || y < 0 || x >= cv.w || y >= cv.h) return; + const o = (y * cv.w + x) * 4; + cv.buf[o] = rgba[0]; + cv.buf[o + 1] = rgba[1]; + cv.buf[o + 2] = rgba[2]; + cv.buf[o + 3] = rgba[3]; +} + +// Fill the whole canvas with an OPAQUE color. +function fillBackground(cv, rgb) { + for (let i = 0; i < cv.w * cv.h; i++) { + const o = i * 4; + cv.buf[o] = rgb[0]; + cv.buf[o + 1] = rgb[1]; + cv.buf[o + 2] = rgb[2]; + cv.buf[o + 3] = 255; + } +} + +// Filled rounded rectangle (opaque). r = corner radius in px (canvas space). +function fillRoundedRect(cv, x0, y0, x1, y1, r, rgb, alpha = 255) { + const minX = Math.max(0, Math.floor(x0)); + const minY = Math.max(0, Math.floor(y0)); + const maxX = Math.min(cv.w - 1, Math.ceil(x1)); + const maxY = Math.min(cv.h - 1, Math.ceil(y1)); + const rr = r * r; + for (let y = minY; y <= maxY; y++) { + for (let x = minX; x <= maxX; x++) { + const px = x + 0.5; + const py = y + 0.5; + let inside = true; + // corner clipping + if (px < x0 + r && py < y0 + r) { + const dx = px - (x0 + r), dy = py - (y0 + r); + inside = dx * dx + dy * dy <= rr; + } else if (px > x1 - r && py < y0 + r) { + const dx = px - (x1 - r), dy = py - (y0 + r); + inside = dx * dx + dy * dy <= rr; + } else if (px < x0 + r && py > y1 - r) { + const dx = px - (x0 + r), dy = py - (y1 - r); + inside = dx * dx + dy * dy <= rr; + } else if (px > x1 - r && py > y1 - r) { + const dx = px - (x1 - r), dy = py - (y1 - r); + inside = dx * dx + dy * dy <= rr; + } + if (inside) setPx(cv, x, y, [rgb[0], rgb[1], rgb[2], alpha]); + } + } +} + +// Scanline polygon fill. pts = [[x,y],...] in canvas space. Even-odd rule. +function fillPolygon(cv, pts, rgb, alpha = 255) { + let minY = Infinity, maxY = -Infinity; + for (const p of pts) { + if (p[1] < minY) minY = p[1]; + if (p[1] > maxY) maxY = p[1]; + } + minY = Math.max(0, Math.floor(minY)); + maxY = Math.min(cv.h - 1, Math.ceil(maxY)); + const n = pts.length; + for (let y = minY; y <= maxY; y++) { + const yc = y + 0.5; + const xs = []; + for (let i = 0; i < n; i++) { + const a = pts[i]; + const b = pts[(i + 1) % n]; + const ay = a[1], by = b[1]; + if ((ay <= yc && by > yc) || (by <= yc && ay > yc)) { + const t = (yc - ay) / (by - ay); + xs.push(a[0] + t * (b[0] - a[0])); + } + } + xs.sort((p, q) => p - q); + for (let i = 0; i + 1 < xs.length; i += 2) { + const xStart = Math.max(0, Math.ceil(xs[i] - 0.5)); + const xEnd = Math.min(cv.w - 1, Math.floor(xs[i + 1] - 0.5)); + for (let x = xStart; x <= xEnd; x++) setPx(cv, x, y, [rgb[0], rgb[1], rgb[2], alpha]); + } + } +} + +// --------------------------------------------------------------------------- +// Shield + check geometry +// --------------------------------------------------------------------------- +// All geometry is expressed in a normalized 0..1 box, then scaled into the +// target region (cx,cy = center, half = half-size of the content square). + +// Build shield outline polygon (heraldic shield: flat top, rounded shoulders +// approximated by points, tapering to a point at the bottom). +function shieldPolygon(cx, cy, half) { + // Normalized shield within a square: x in [-0.5,0.5], y in [-0.6,0.6] + // top edge slightly inset, sides curve down, bottom comes to a point. + const W = half * 0.92; // half width + const Htop = -half * 1.02; + const pts = []; + // Top edge + pts.push([cx - W, cy + Htop]); + pts.push([cx + W, cy + Htop]); + // Right side going down with a gentle inward curve, then to the point. + // Approximate the right & bottom with sampled points along a parametric path. + const right = [ + [cx + W, cy + Htop], + [cx + W * 0.99, cy - half * 0.35], + [cx + W * 0.92, cy + half * 0.05], + [cx + W * 0.74, cy + half * 0.45], + [cx + W * 0.42, cy + half * 0.85], + [cx + 0, cy + half * 1.12], // bottom point + ]; + const left = [ + [cx - W * 0.42, cy + half * 0.85], + [cx - W * 0.74, cy + half * 0.45], + [cx - W * 0.92, cy + half * 0.05], + [cx - W * 0.99, cy - half * 0.35], + [cx - W, cy + Htop], + ]; + // Compose: top-left -> top-right -> right path -> bottom -> left path + const poly = []; + poly.push([cx - W, cy + Htop]); + poly.push([cx + W, cy + Htop]); + for (let i = 1; i < right.length; i++) poly.push(right[i]); + for (let i = 0; i < left.length; i++) poly.push(left[i]); + return poly; +} + +// Build a bold check-mark polygon (thick stroke as a filled hexagon-ish shape). +// Centered roughly in the shield. `s` scales overall size, `t` is stroke thickness. +function checkPolygon(cx, cy, s, t) { + // Check has a short arm (down-left) and a long arm (up-right). + // Define center vertex (bottom of the V), short tip, long tip. + const vx = cx - s * 0.10, vy = cy + s * 0.34; // inner corner (bottom) + const s1x = cx - s * 0.46, s1y = cy + s * 0.02; // short arm tip + const l1x = cx + s * 0.50, l1y = cy - s * 0.42; // long arm tip + // Build a thick stroke by offsetting perpendicular to each segment. + // Segment A: short tip -> inner corner + // Segment B: inner corner -> long tip + const half = t / 2; + + function unit(ax, ay, bx, by) { + const dx = bx - ax, dy = by - ay; + const L = Math.hypot(dx, dy) || 1; + return [dx / L, dy / L]; + } + function perp(u) { return [-u[1], u[0]]; } + + const uA = unit(s1x, s1y, vx, vy); + const uB = unit(vx, vy, l1x, l1y); + const pA = perp(uA); + const pB = perp(uB); + + // Outer & inner offsets for each arm; assemble as a single polygon outline. + // Order: short tip outer -> corner outer -> long tip outer -> long tip inner + // -> corner inner -> short tip inner + const poly = [ + [s1x + pA[0] * half, s1y + pA[1] * half], // short tip outer + [vx + pA[0] * half, vy + pA[1] * half], // corner outer (A side) + [vx + pB[0] * half, vy + pB[1] * half], // corner outer (B side) + [l1x + pB[0] * half, l1y + pB[1] * half], // long tip outer + [l1x - pB[0] * half, l1y - pB[1] * half], // long tip inner + [vx - pB[0] * half, vy - pB[1] * half], // corner inner (B side) + [vx - pA[0] * half, vy - pA[1] * half], // corner inner (A side) + [s1x - pA[0] * half, s1y - pA[1] * half], // short tip inner + ]; + return poly; +} + +// --------------------------------------------------------------------------- +// Draw the NIS2 emblem (shield + check) into a canvas region. +// cx,cy: center; half: half content size; simplified: bolder geometry for tiny sizes +// --------------------------------------------------------------------------- +function drawEmblem(cv, cx, cy, half, simplified = false) { + // White shield + const shield = shieldPolygon(cx, cy, half); + fillPolygon(cv, shield, WHITE, 255); + + // Negative blue check inside the shield. + const checkScale = half * (simplified ? 1.18 : 1.05); + const stroke = half * (simplified ? 0.32 : 0.24); + const check = checkPolygon(cx, cy - half * 0.02, checkScale, stroke); + fillPolygon(cv, check, BLUE, 255); +} + +// --------------------------------------------------------------------------- +// Box downsample (supersampled -> final). factor must divide dimensions. +// --------------------------------------------------------------------------- +function downsample(cv, factor) { + const W = cv.w / factor; + const H = cv.h / factor; + const out = makeCanvas(W, H, null); + const f2 = factor * factor; + for (let y = 0; y < H; y++) { + for (let x = 0; x < W; x++) { + let r = 0, g = 0, b = 0, a = 0; + for (let sy = 0; sy < factor; sy++) { + for (let sx = 0; sx < factor; sx++) { + const o = ((y * factor + sy) * cv.w + (x * factor + sx)) * 4; + const sa = cv.buf[o + 3]; + // premultiply for correct edge blending + r += cv.buf[o] * sa; + g += cv.buf[o + 1] * sa; + b += cv.buf[o + 2] * sa; + a += sa; + } + } + const oo = (y * W + x) * 4; + if (a === 0) { + out.buf[oo] = 0; out.buf[oo + 1] = 0; out.buf[oo + 2] = 0; out.buf[oo + 3] = 0; + } else { + out.buf[oo] = Math.round(r / a); + out.buf[oo + 1] = Math.round(g / a); + out.buf[oo + 2] = Math.round(b / a); + out.buf[oo + 3] = Math.round(a / f2); + } + } + } + return out; +} + +// --------------------------------------------------------------------------- +// PNG encoding (manual, 8-bit RGBA, color type 6) +// --------------------------------------------------------------------------- +const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +// CRC32 (standard polynomial 0xEDB88320) +const CRC_TABLE = (() => { + const t = new Uint32Array(256); + for (let n = 0; n < 256; n++) { + let c = n; + for (let k = 0; k < 8; k++) { + c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1); + } + t[n] = c >>> 0; + } + return t; +})(); + +function crc32(buf) { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); + } + return (c ^ 0xffffffff) >>> 0; +} + +function chunk(type, data) { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + const typeBuf = Buffer.from(type, 'ascii'); + const body = Buffer.concat([typeBuf, data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(body), 0); + return Buffer.concat([len, body, crc]); +} + +function encodePNG(cv) { + const { w, h, buf } = cv; + // IHDR + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(w, 0); + ihdr.writeUInt32BE(h, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // color type RGBA + ihdr[10] = 0; // compression + ihdr[11] = 0; // filter + ihdr[12] = 0; // interlace + + // Filtered raw data: each scanline prefixed with filter byte 0 + const stride = w * 4; + const raw = Buffer.alloc((stride + 1) * h); + for (let y = 0; y < h; y++) { + raw[y * (stride + 1)] = 0; // filter: none + buf.subarray(y * stride, y * stride + stride) + .forEach ? null : null; // (Uint8Array subarray copy below) + const srcStart = y * stride; + raw.set(buf.subarray(srcStart, srcStart + stride), y * (stride + 1) + 1); + } + const idat = zlib.deflateSync(raw, { level: 9 }); + + return Buffer.concat([ + PNG_SIG, + chunk('IHDR', ihdr), + chunk('IDAT', idat), + chunk('IEND', Buffer.alloc(0)), + ]); +} + +// --------------------------------------------------------------------------- +// Icon builders +// --------------------------------------------------------------------------- +const SS = 4; // supersampling factor + +// Standard (rounded tile) icon: transparent corners, rounded blue tile, +// margin ~8%, centered emblem. +function buildStandard(size, simplified = false) { + const W = size * SS; + const cv = makeCanvas(W, W, null); // transparent background + const margin = W * 0.08; + const radius = W * 0.18; + fillRoundedRect(cv, margin, margin, W - margin, W - margin, radius, BLUE, 255); + const cx = W / 2, cy = W / 2; + // emblem content sized to ~ inner tile + const half = (W - 2 * margin) * 0.30; + drawEmblem(cv, cx, cy, half, simplified); + return downsample(cv, SS); +} + +// Maskable icon: FULL-BLEED blue, square corners, emblem inside the central +// 80% safe-zone. +function buildMaskable(size) { + const W = size * SS; + const cv = makeCanvas(W, W, null); + fillBackground(cv, BLUE); // full bleed, opaque + const cx = W / 2, cy = W / 2; + // safe zone is central 80% => content sized so it stays inside 0.8*W box + const half = W * 0.5 * 0.80 * 0.62; // ~31% of half-canvas + drawEmblem(cv, cx, cy, half, false); + return downsample(cv, SS); +} + +// Apple touch icon: fully OPAQUE blue background, square corners, emblem. +function buildAppleTouch(size) { + const W = size * SS; + const cv = makeCanvas(W, W, null); + fillBackground(cv, BLUE); // opaque, no transparency + const cx = W / 2, cy = W / 2; + const half = W * 0.30; + drawEmblem(cv, cx, cy, half, false); + return downsample(cv, SS); +} + +// Favicon: rounded blue tile + bolder simplified emblem for legibility. +function buildFavicon(size) { + const W = size * SS; + const cv = makeCanvas(W, W, null); + const margin = W * 0.04; + const radius = W * 0.18; + fillRoundedRect(cv, margin, margin, W - margin, W - margin, radius, BLUE, 255); + const cx = W / 2, cy = W / 2; + const half = W * 0.34; + drawEmblem(cv, cx, cy, half, true); // simplified/bolder + return downsample(cv, SS); +} + +// --------------------------------------------------------------------------- +// SVG master (same design, vector) +// --------------------------------------------------------------------------- +function buildSVG() { + // Express the same geometry in a 512 viewBox. + const cx = 256, cy = 256, half = 512 * 0.30; // 153.6 + const shield = shieldPolygon(cx, cy, half) + .map(p => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); + const check = checkPolygon(cx, cy - half * 0.02, half * 1.05, half * 0.24) + .map(p => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' '); + const margin = 512 * 0.08; + const r = 512 * 0.18; + return ` + + NIS2 Agile + + + + + + + +`; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- +function main() { + fs.mkdirSync(ICONS_DIR, { recursive: true }); + + const outputs = []; + + function write(name, cv) { + const png = encodePNG(cv); + const fp = path.join(ICONS_DIR, name); + fs.writeFileSync(fp, png); + outputs.push({ name, bytes: png.length, w: cv.w, h: cv.h }); + } + + write('icon-192.png', buildStandard(192)); + write('icon-512.png', buildStandard(512)); + write('icon-192-maskable.png', buildMaskable(192)); + write('icon-512-maskable.png', buildMaskable(512)); + write('apple-touch-icon.png', buildAppleTouch(180)); + write('favicon-32.png', buildFavicon(32)); + write('favicon-16.png', buildFavicon(16)); + + // SVG master + const svg = buildSVG(); + fs.writeFileSync(path.join(ICONS_DIR, 'icon.svg'), svg); + + // ---- Validation: re-read each PNG, verify signature + IHDR dims ---- + console.log('\n=== NIS2 PWA Icon Generation ==='); + console.log('Output dir:', ICONS_DIR); + console.log('\nFile | Bytes | Declared | Read(IHDR) | SIG'); + console.log('---------------------------|--------|----------|------------|----'); + + let allOk = true; + for (const o of outputs) { + const data = fs.readFileSync(path.join(ICONS_DIR, o.name)); + const sigOk = + data.length >= 8 && + data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47 && + data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a; + // IHDR starts at byte 16 (8 sig + 4 len + 4 'IHDR'); width@16, height@20 + const rw = data.readUInt32BE(16); + const rh = data.readUInt32BE(20); + const nonEmpty = data.length > 0; + const ok = sigOk && nonEmpty && rw === o.w && rh === o.h; + if (!ok) allOk = false; + console.log( + `${o.name.padEnd(26)} | ${String(o.bytes).padStart(6)} | ${(o.w + 'x' + o.h).padStart(8)} | ${(rw + 'x' + rh).padStart(10)} | ${ok ? 'OK' : 'KO'}` + ); + } + + // SVG report + const svgStat = fs.statSync(path.join(ICONS_DIR, 'icon.svg')); + console.log(`${'icon.svg'.padEnd(26)} | ${String(svgStat.size).padStart(6)} | ${'vector'.padStart(8)} | ${'-'.padStart(10)} | ${svgStat.size > 0 ? 'OK' : 'KO'}`); + + console.log('\nValidation:', allOk ? 'ALL PNG OK' : 'FAILURES PRESENT'); + if (!allOk) process.exitCode = 1; +} + +main(); diff --git a/scripts/inject-pwa-head.mjs b/scripts/inject-pwa-head.mjs new file mode 100644 index 0000000..c342e06 --- /dev/null +++ b/scripts/inject-pwa-head.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/* + * Inietta i tag PWA negli di tutte le pagine HTML sotto public/. + * Idempotente: salta i file che contengono gia' il marker . + * Normalizza il viewport mantenendo lo zoom abilitato (WCAG 1.4.4 / AGID). + * Esegue solo modifiche additive e mirate; tutto e' git-tracked => reversibile. + * + * Uso: node scripts/inject-pwa-head.mjs + */ +import fs from 'node:fs'; +import path from 'node:path'; + +const ROOT = path.resolve(process.cwd(), 'public'); +const EXCLUDE_BASENAMES = new Set(['offline.html']); +const MARKER = ''; +const VIEWPORT = ''; + +const BLOCK = [ + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ' +].join('\n'); + +function walk(dir) { + let out = []; + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, e.name); + if (e.isDirectory()) out = out.concat(walk(full)); + else if (e.isFile() && e.name.toLowerCase().endsWith('.html')) out.push(full); + } + return out; +} + +const files = walk(ROOT).filter((f) => { + const b = path.basename(f); + if (EXCLUDE_BASENAMES.has(b)) return false; + if (/copy/i.test(b)) return false; + return true; +}); + +let modified = 0, already = 0, nohead = 0, viewportFixed = 0; +const anomalies = []; + +for (const f of files) { + let html = fs.readFileSync(f, 'utf8'); + if (html.includes(MARKER)) { already++; continue; } + + if (!/<\/head>/i.test(html)) { + nohead++; + anomalies.push('NO : ' + path.relative(process.cwd(), f)); + continue; + } + + // Normalizza il viewport (zoom SEMPRE consentito: no user-scalable=no / maximum-scale) + const vp = html.match(/]*>/i); + if (vp) { + if (!/viewport-fit=cover/i.test(vp[0])) { + html = html.replace(vp[0], VIEWPORT); + viewportFixed++; + } + } + + const inject = (vp ? '' : (' ' + VIEWPORT + '\n')) + BLOCK + '\n'; + // replace con funzione per evitare interpretazione di pattern '$' nel testo iniettato + html = html.replace(/<\/head>/i, () => inject + ''); + fs.writeFileSync(f, html); + modified++; +} + +console.log('HTML trovati:', files.length); +console.log('Modificati:', modified, '| gia col marker:', already, + '| senza :', nohead, '| viewport normalizzati:', viewportFixed); +if (anomalies.length) { + console.log('Anomalie:'); + anomalies.forEach((a) => console.log(' -', a)); +}