From 36513fa722bf5b67c6529b55841784185c37ea97 Mon Sep 17 00:00:00 2001 From: DevEnv nis2-agile Date: Sun, 14 Jun 2026 21:09:54 +0200 Subject: [PATCH] [FEAT] PWA installabile Android/iOS conforme AGID (v1.15.0) - manifest.webmanifest + service worker (offline shell, /api network-only, asset stale-while-revalidate) - set icone (192/512 + maskable + apple-touch + favicon), generate zero-deps (pure Node+zlib) - tag PWA + viewport viewport-fit=cover (zoom mantenuto, WCAG 1.4.4) in 77 pagine HTML - AGID/WCAG 2.1 AA: touch target >=44px, orientamento/zoom liberi, safe-area standalone - generatori riproducibili: scripts/gen-pwa-icons.mjs + scripts/inject-pwa-head.mjs - bump version 1.14.1 -> 1.15.0 Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 17 + docs/CONTEXT_LAST_SESSION.md | 9 + public/_app-bi-demo.html | 15 +- public/_bi-smoke.html | 15 +- public/acn-gap-bi.html | 15 +- public/acn-gap.html | 15 +- public/admin/index-bi.html | 15 +- public/admin/index.html | 15 +- public/admin/organizations-bi.html | 15 +- public/admin/organizations.html | 15 +- public/admin/users-bi.html | 15 +- public/admin/users.html | 15 +- public/architecture.html | 15 +- public/assessment-bi.html | 15 +- public/assessment.html | 15 +- public/assets-bi.html | 15 +- public/assets.html | 15 +- public/assets/icons/apple-touch-icon.png | Bin 0 -> 1646 bytes public/assets/icons/favicon-16.png | Bin 0 -> 303 bytes public/assets/icons/favicon-32.png | Bin 0 -> 447 bytes public/assets/icons/icon-192-maskable.png | Bin 0 -> 1576 bytes public/assets/icons/icon-192.png | Bin 0 -> 1988 bytes public/assets/icons/icon-512-maskable.png | Bin 0 -> 5023 bytes public/assets/icons/icon-512.png | Bin 0 -> 6569 bytes public/assets/icons/icon.svg | 10 + public/companies-bi.html | 15 +- public/companies.html | 15 +- public/cross-analysis-bi.html | 15 +- public/cross-analysis.html | 15 +- public/css/style.css | 41 ++ public/dashboard-bi.html | 15 +- public/dashboard.html | 15 +- public/docs/api.html | 15 +- public/docs/testing-simulazione.html | 15 +- public/forgot-password-bi.html | 15 +- public/forgot-password.html | 15 +- public/guida-bi.html | 15 +- public/guida.html | 15 +- public/incidents-bi.html | 15 +- public/incidents.html | 15 +- public/index-bi.html | 15 +- public/index-en.html | 15 +- public/index.html | 15 +- public/integrations/allrisk.html | 15 +- public/integrations/index.html | 15 +- public/integrations/lg231.html | 15 +- public/integrations/siem.html | 15 +- public/integrations/sustainai.html | 15 +- public/integrazioniext.html | 15 +- public/isms-bi.html | 15 +- public/isms.html | 15 +- public/js/pwa.js | 130 ++++++ public/kb-bi.html | 15 +- public/kb.html | 15 +- public/licenseExt.html | 15 +- public/login-bi.html | 15 +- public/login.html | 15 +- public/manifest.webmanifest | 59 +++ public/mktg-api-doc.html | 15 +- public/normative-bi.html | 15 +- public/normative.html | 15 +- public/offline.html | 36 ++ public/onboarding-bi.html | 15 +- public/onboarding.html | 15 +- public/policies-bi.html | 15 +- public/policies.html | 15 +- public/presentation.html | 15 +- public/register-bi.html | 15 +- public/register.html | 15 +- public/reports-bi.html | 15 +- public/reports.html | 15 +- public/reset-password-bi.html | 15 +- public/reset-password.html | 15 +- public/risks-bi.html | 15 +- public/risks.html | 15 +- public/service-continuity-bi.html | 15 +- public/service-continuity.html | 15 +- public/settings-bi.html | 15 +- public/settings.html | 15 +- public/setup-org.html | 15 +- public/simulate-b2b.html | 15 +- public/simulate.html | 15 +- public/supplier-assessment.html | 15 +- public/supplier-portal.html | 15 +- public/supply-chain-bi.html | 15 +- public/supply-chain.html | 15 +- public/sw.js | 96 +++++ public/training-bi.html | 15 +- public/training.html | 15 +- public/version.json | 2 +- public/whistleblowing-bi.html | 15 +- public/whistleblowing.html | 15 +- public/workflow.html | 15 +- scripts/gen-pwa-icons.mjs | 487 ++++++++++++++++++++++ scripts/inject-pwa-head.mjs | 86 ++++ 95 files changed, 2050 insertions(+), 78 deletions(-) create mode 100644 public/assets/icons/apple-touch-icon.png create mode 100644 public/assets/icons/favicon-16.png create mode 100644 public/assets/icons/favicon-32.png create mode 100644 public/assets/icons/icon-192-maskable.png create mode 100644 public/assets/icons/icon-192.png create mode 100644 public/assets/icons/icon-512-maskable.png create mode 100644 public/assets/icons/icon-512.png create mode 100644 public/assets/icons/icon.svg create mode 100644 public/js/pwa.js create mode 100644 public/manifest.webmanifest create mode 100644 public/offline.html create mode 100644 public/sw.js create mode 100644 scripts/gen-pwa-icons.mjs create mode 100644 scripts/inject-pwa-head.mjs 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 0000000000000000000000000000000000000000..1650ea13796a699213ce7de67a37bf422c95fbbf GIT binary patch literal 1646 zcmZ{lX;hMF6vx%dz$VwCv^0}kJ7$|{I@tJzqUMt1ieWWbkz-nk3Yg0=TGXKIWR4s5 z3b>JIt`mvoRHHQ-uaQz1nvDgPXedEx<)GD+Fm63?Yf-l3O+SP{YiTS(r!G<(J5hnvkD_bMFkR zZwxq*|EDGxZ;F6vufGDo7V7|d>rcjHuhJi3#FRA-h*OUaxV%q+s~ZFv>cT=)qe>zP z?H z`Rf_W8!d^pWf|%cU(fP1#Pcq04d!@acuajVmN6=`EEE! z?KEs$1d*ANLP1@>ZO}weP)3?&uQYcuBRxq`g!-hg2H~Z)xY%2-d9=qIc!12FoVdra zW&a)3r1zVCs*UnJ$xf&4P)M|9D#%f%4*YTeFmdy5x)UczTAP=yIsRVG-CFW=Pt#dw z5y9Z6FejZsXW8xZOfiYyu@s!^`i#Dm;F{1mh0Ea*QU-?F$RSQ{58l}|z);ht;-8X| z=PG7-v~f>0f=!JkE#K&8M6$3SsB3`nv6cUv$(rb9BtFu|Ai%+;ZGOrLc`q-FpAkci zt{3*rHV-|yw&VRSWZS#c&5ajCm+3}$YNX_bqcDxyel_evI4wC`?x-_(hB!)J2Cf<- zq8)YiCA)5g<3HRjWWaeBtWK)3;&K)pZGhu%C8(=Ve7aCs1h}MT{=#-PAzIy^DI6y?wW@={ZJHIOV2rGw0b` z%~t;@i^>h`@slwol!4Oet1(PAz23W7z0ML$nIN7SU$n+L_&%I>+MFH-T%I7tohbg` zXMUVkL%e`wLQHICRQuNi{%&s^lczGsgWzuFsQlSxEC$DP7#``iBPJYjwJMOzxTB+( z4hIa6uHf0qS8wTW34MFh>)=}=k^{BU{aOr;kK(1+D5raiCF46yfJxHFp15izHyCeX zub*FSrM%c#l9zI0x+Qwlxu!NR_C`kzgnVP_&2a8RuT@}-NA{M6ObbG(7S3s+#b&{F z9z79C*0yb;sz-sMP-Du|dTavb)fe0BR-ZJY(eKHttFJ-~%_6SvF;xUp zY{nE9QS~7XpDj^=rn!#Tbz3}JF#(=3%S3(AXs1-(teV4l3=w{9s+;($>y4|Gx!_8C z1GgM%v@l4>j-mDK1j zy^Vh(&W4_&Z#{2{dcuvUwIkk-PGGI>&)T4*qs<;(T7g86;Aw_Nv+WqnWTy)?u-!k( z9_!R$?DiOubd8btyp3!PP9jp9r`UF-E_F7_x1Wfg+Q?oYvxI!;<3-3Gpo zVmAFuWqik)LO8x0Q!Db9O^mg{Htv_FM4YD3p~RdA$bu7tg0Rm;O!-q4WABL?BoYgSjt$N~aS0I`T9L)L`Abu6I literal 0 HcmV?d00001 diff --git a/public/assets/icons/favicon-16.png b/public/assets/icons/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..433d0205db87dc4040ad6db5522e6c347343ab63 GIT binary patch literal 303 zcmV+~0nq-5P)L9Gomn(AqosoCxHcs1*on-ay~wA z|IJTq2J9!wfb);O{ud}X4|YCY1CVWjf%jkk{8y;F1eRZa{ykU?P6PJe`~=oC|JWO_ zMi2;?b_X87AOKQ>(*Tenm;!_5YyS&YJ_55rhQI(?ET9?yaw-TU%)bxzGl&Km1al1t zz`TRi01yT_9|ZdMy!`Lba}ykZFaVN6OH7ysfNaJVm>@Bb95w?I(32^;vq5Z-8203h zl$4P%$giLP1z|LQqa|gEGZSidZUFlWKW;$I&Hy2Zgn30Wc0K?A002ovPDHLkV1hIv BaSQ+e literal 0 HcmV?d00001 diff --git a/public/assets/icons/favicon-32.png b/public/assets/icons/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..8018849c33121b69b9ecfeef4167d2be472c1063 GIT binary patch literal 447 zcmV;w0YLtVP)gA}K%kHz5K#Hs z7|w^zmQ=?wInWTrclWywpCzRxCvg_3I7?KVr30^|vfbkx59vFvB&2fzqE5RWc}1Pk z4=vqUsGDSLfHX9KeoTOTf6?o&Qy75r<))wCek%RN$9{Z(*=pCaK_Q$0%G1@!`SSDF z(vM!htBgQk%NZaoeyyG3S+0*>==#4GAlV*_HRgioo)&pamp377IRnhsWy=VR!V!&( zHl3sg!Jab!3{&ULJ|LR%aKluG3Wu};pmltCFSMyd^f$mvC4vp{gVYDmq!WheE7n9x z_oSv3({BU7qmqo_6_f@OZ%`)ylX0GUQ`1Sa!3zKp@S(KH&sAe0?WVd*ucsFP7Mjpi zOpWUeAPpu-#KH3)0Ne2_j@=;u=n#^e_tsrFk~tm8`L`Z|J^!P*$FUs42vZJA9JSWR p^57SfNnFj$m(!t~T3${A$RFP#fs-x^3Z?)6002ovPDHLkV1j+^$({fJ literal 0 HcmV?d00001 diff --git a/public/assets/icons/icon-192-maskable.png b/public/assets/icons/icon-192-maskable.png new file mode 100644 index 0000000000000000000000000000000000000000..4dd44cbaeec4b6f71165e40ecea984dc78f48864 GIT binary patch literal 1576 zcmb`I|3A|S9LGQVn9R2`Ho3+zp)WMbBImd^G2b>V4QaVnmwa#T%00?(YvoX(@^$ik z(xag=-%7F71Mw)Vky)5UG+*Kf+sT*P{RP+K9{0oh{eC{)kJsz*{_XWnBl~&kYN50M z0MPaJa`#tF!9NAtqU!#`(Ha1tKJV?0r(AsZX@t)W@jx;cTv(Yh3nlDFRwga9$gzlK zGbpKS|zCc!cW=rRiruXBTJdT+HuRCm&n zyy~PK?zjsS4xj-XzytsY00K||2K>JZCJ{{dYpH!=rewoiV@zT|`!(s(hPYjhRWTEQ zq5%jD2Y~ReK+lEo5o2saUuu{yvn!43!Rb}eS6Fpyf>c-h#nTB3>t=Ze=$qyMWbua4 z!_C%&MdjScdF4_UJ@08AT@%M#_I&o~;Fn2-Ua<#kVM!7N!fBrtYHhU5Hvha0K#!eI zP<*Z;^-g@Imju03hoS`(ixnlKm=v8BIR>h<+{Dc|A;Vp40`bFDtvgnmS~DC{W1mfSoIm(c>hiT72kV|vsQwoh5M z-Irg((+lTvlN`D2-dM=6?*8cPIX*Q;Gw&H9JOC2to1A<0c#_1ii50!=G@0stUI#mUP#x{8QQlfL&T?Sj-6&1bpn`frYLJBnfjDPoFuPABx4< zXvaTIs9T0IY_Q3yQK3%Uko&d%0YiJ@L(q?jXKiLW|>T3R8l+Xh3MngLgP zyy9|1Cc18iJpJOSjzdnMj-|B1RjqH}fL->agyT~Fk&EP92-q&5#UB<1Zp#z7{`Rm` zv7Qq#@jb5|sIkX4@$Aj<-Il{c1B1;kuyI?$;~>Y0H!;hKyY#Vw#tx42XymFMEOxQ{ zv1^>uzFUe)bI*DQYnHDijA;=7)KfG`SmTv~g^=-5_>kMH)#Yr@8A7mFcO4$cx$ zY8f_qTJpR6W5&5C3RpyBiZF++k8dh^wFpfPriP^m+O25vb)C-T;OFv2f~2GZ>?f9X zUJ0@nTuld*u)3)Z1L&p^DZFWPId@ zBSTm7+xD?fmupaiGAyiJ=;@;U7>J+StH$T1HLR9*D605JeOX2QTyZ4Z&Mc7=GDSi{ zy^=zGuGg`oG3nDuFkwS(>twYowXvf=eI6J?fC_f|+{^o^hUL@3GAkQh;@rMvwq5diUjVtW82bzYMb|_8=ym* zDe+mr)M~m#=lY;$9iDkNNKNJaZ%GOH_qgq8=T8(56vjB#eyjQbfVYRA`vW%`>u=;V BhD87X literal 0 HcmV?d00001 diff --git a/public/assets/icons/icon-192.png b/public/assets/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..c2cdedc1e3240699424e52965f75c43d16e06dbb GIT binary patch literal 1988 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 literal 0 HcmV?d00001 diff --git a/public/assets/icons/icon-512.png b/public/assets/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..3bbbb82d305dd670109f33192cbf526a57df7840 GIT binary patch literal 6569 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%0DU-T|_WnRgz2PBuLb_vBAkSJjqar5kjQKb+lRC zBqJY%@{F=b2DdyyI6jBzq@!~JXG#Qd7^KU)zqBh}IifmS$b?jW+B7}L6&nGlzt4py zMN}6*!XQZt=)Jabn7_TE&;2W)er3}S$BqGEu82TPVne5#mJlKyGBuYkF!BBYOB=PfXScSsY+1YJT{U@9%A;H3XBXpiz6mG_M@)HUtO%#GPDpXz z)$F3g$wbt!!%rC6Q+%tVIWz3;ClgT;t)fD%O}6-?&a-kJ8^c*e^`TXvdKyexTg7j} lLps|us5_Lw|Ibggkq5x%D;V4SwGj{jb&sW4zKK)Be*q|bBAoyL literal 0 HcmV?d00001 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)); +}