[FEAT] Sessione + release: refresh token silenzioso + avviso "nuova versione"

Evita la "sessione scaduta" e i deploy che restano invisibili all'utente.
- api.js: refresh PROATTIVO dell'access token ~2 min prima della scadenza
  (scheduleTokenRefresh su exp del JWT, riprogrammato ad ogni setTokens) +
  ensureFreshToken() per rinnovo immediato al ritorno sulla scheda. Con refresh
  a 7gg l'utente resta loggato senza mai vedere il login.
- common.js: watch versione (poll version.json ogni 5 min + su visibilitychange)
  → banner "È disponibile una nuova versione — Aggiorna" (reload) quando cambia;
  nudge service worker (registration.update); su visibilitychange chiama anche
  api.ensureFreshToken(). Nessun hard-refresh manuale.
- Cache-buster api/common ?v=20260729b. version 1.25.12. Solo frontend.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DevEnv nis2-agile
2026-07-29 16:07:57 +02:00
co-authored by Claude Opus 4.8
parent faadb62da6
commit cf224480cc
45 changed files with 171 additions and 82 deletions
+37
View File
@@ -10,6 +10,42 @@ class NIS2API {
this.token = localStorage.getItem('nis2_access_token');
this.refreshToken = localStorage.getItem('nis2_refresh_token');
this.orgId = localStorage.getItem('nis2_org_id');
this._refreshTimer = null;
if (this.token) this.scheduleTokenRefresh();
}
// ── Refresh proattivo/silenzioso ────────────────────────────────────
// Rinnova l'access token PRIMA che scada, così l'utente non incontra 401 né deve
// rifare il login (il refresh token dura 7 giorni). Evita la "sessione scaduta".
_tokenExp(token) {
try {
const raw = (token || this.token || '').split('.')[1];
const p = JSON.parse(atob(raw.replace(/-/g, '+').replace(/_/g, '/')));
return (typeof p.exp === 'number') ? p.exp : 0;
} catch (e) { return 0; }
}
scheduleTokenRefresh() {
if (this._refreshTimer) { clearTimeout(this._refreshTimer); this._refreshTimer = null; }
if (!this.token || !this.refreshToken) return;
const exp = this._tokenExp(this.token);
if (!exp) return;
let delay = exp * 1000 - Date.now() - 120000; // rinnova ~2 min prima della scadenza
if (delay < 1000) delay = 1000;
if (delay > 2000000000) delay = 2000000000; // cap (limite setTimeout a 32 bit)
this._refreshTimer = setTimeout(() => {
if (this.isAuthenticated() && this.refreshToken) this.doRefreshToken().catch(() => {});
}, delay);
}
// Rinnova SUBITO se l'access è scaduto o entro 5 min (usato al ritorno sulla scheda
// dopo sospensione/inattività, quando i timer possono non essere scattati).
async ensureFreshToken() {
if (!this.token || !this.refreshToken) return;
const exp = this._tokenExp(this.token);
if (exp && exp * 1000 - Date.now() < 300000) {
await this.doRefreshToken().catch(() => {});
}
}
// ═══════════════════════════════════════════════════════════════════
@@ -133,6 +169,7 @@ class NIS2API {
this.refreshToken = refresh;
localStorage.setItem('nis2_access_token', access);
localStorage.setItem('nis2_refresh_token', refresh);
this.scheduleTokenRefresh(); // riprogramma il refresh silenzioso sul nuovo exp
}
clearTokens() {
+52
View File
@@ -507,11 +507,17 @@ async function _loadFirmBranding() {
// ── Versioning live (Fase 4 / G13) ──────────────────────────────────────
let _versionInfo = null;
let _loadedVersion = null; // versione con cui l'app è stata caricata (per il check aggiornamenti)
let _updateWatchStarted = false;
async function _loadVersionFooter() {
try {
const r = await fetch('/version.json?_=' + Date.now());
if (!r.ok) return;
_versionInfo = await r.json();
if (_loadedVersion === null && _versionInfo && _versionInfo.version) {
_loadedVersion = _versionInfo.version;
_startUpdateWatch();
}
const el = document.getElementById('sidebar-version');
if (el && _versionInfo.version) {
el.textContent = 'v' + _versionInfo.version + (_versionInfo.build ? ' · ' + _versionInfo.build : '');
@@ -524,6 +530,52 @@ function _showVersionChangelog() {
alert('NIS2 Agile v' + v.version + '\nBuild: ' + (v.build || '—') + '\nData: ' + (v.date || '—') + '\n\n' + (v.changelog || 'Nessun changelog disponibile'));
}
// ── Gestione release: avvisa (senza forzare) quando è online una nuova versione ──
// Poll di version.json ogni 5 min + al ritorno sulla scheda; se la versione è cambiata
// mostra un banner "Aggiorna" che ricarica (asset freschi via cache-buster + service worker).
// Al ritorno sulla scheda rinnova anche il token (evita la sessione scaduta dopo sospensione).
function _startUpdateWatch() {
if (_updateWatchStarted) return;
_updateWatchStarted = true;
setInterval(_checkForUpdate, 5 * 60 * 1000);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
_checkForUpdate();
if (typeof api !== 'undefined' && api.ensureFreshToken) api.ensureFreshToken();
}
});
_swUpdate();
}
function _swUpdate() {
try { if (navigator.serviceWorker) navigator.serviceWorker.getRegistration().then(r => { if (r) r.update(); }).catch(() => {}); } catch (e) { /* noop */ }
}
async function _checkForUpdate() {
try {
const r = await fetch('/version.json?_=' + Date.now(), { cache: 'no-store' });
if (!r.ok) return;
const info = await r.json();
if (info && info.version && _loadedVersion && info.version !== _loadedVersion) {
_showUpdateBanner(info.version);
}
_swUpdate();
} catch (e) { /* silenzioso */ }
}
function _showUpdateBanner(newVersion) {
if (document.getElementById('nis2-update-banner')) return;
const b = document.createElement('div');
b.id = 'nis2-update-banner';
b.style.cssText = 'position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:9998;' +
'background:#0A2E54;color:#fff;border-radius:12px;padding:12px 16px;display:flex;align-items:center;gap:14px;' +
'box-shadow:0 10px 30px rgba(11,40,80,.28);font:500 14px/1.4 Inter,system-ui,Arial,sans-serif;max-width:92vw';
b.innerHTML =
'<span>È disponibile una <b>nuova versione</b>' + (newVersion ? ' (v' + escapeHtml(String(newVersion)) + ')' : '') + '. Aggiorna per averla.</span>' +
'<button id="nis2-update-btn" style="border:0;border-radius:8px;background:#F0B429;color:#1a1a1a;font-weight:700;padding:8px 14px;cursor:pointer">Aggiorna</button>' +
'<button id="nis2-update-x" aria-label="Chiudi" style="border:0;background:transparent;color:#cfe0f1;font-size:18px;cursor:pointer;line-height:1">×</button>';
document.body.appendChild(b);
document.getElementById('nis2-update-btn').addEventListener('click', () => location.reload());
document.getElementById('nis2-update-x').addEventListener('click', () => b.remove());
}
function _setupMobileToggle() {
// Crea pulsante toggle se non esiste
if (!document.querySelector('.sidebar-toggle')) {