/** * Update-System fuer den AegisSight Monitor. * * Zeigt zwei Dinge: * 1) Beim ersten Page-Load nach einem Update -> Modal "Was ist neu?" * mit den Eintraegen aus RELEASES.json, die der User noch nicht gesehen hat. * * 2) Wenn der User die Seite offen hat und im Hintergrund ein neues Update * live geht -> kleiner Banner unten rechts: * "Eine neue Version ist verfuegbar. [Jetzt aktualisieren]" * * Datenquellen (Backend): * GET /api/version -> { commit, deployed_at } * GET /api/release-notes -> { entries: [...], current } * * Persistenz im Browser: * localStorage 'aegis_last_seen_release' -> "version"-Feld des zuletzt * gesehenen Eintrags */ (function () { 'use strict'; const POLL_INTERVAL_MS = 60_000; // alle 60 Sekunden const STORAGE_KEY = 'aegis_last_seen_release'; let initialBootCommit = null; // Commit-Hash beim Page-Load let pollTimer = null; let updateBannerShown = false; // ---- Mini-DOM-Helpers ---- function el(tag, attrs, ...children) { const e = document.createElement(tag); for (const k in (attrs || {})) { if (k === 'class') e.className = attrs[k]; else if (k === 'html') e.innerHTML = attrs[k]; else if (k.startsWith('on')) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of children) { if (c == null) continue; e.appendChild(typeof c === 'string' ? document.createTextNode(c) : c); } return e; } // ---- Styles inline injecten (kein zusaetzlicher CSS-File noetig) ---- // Nutzt die globalen Theme-Variablen aus style.css, damit Banner und // Modal automatisch dem Hell-/Dunkelmodus folgen. function injectStyles() { if (document.getElementById('aegis-update-styles')) return; const css = ` #aegis-update-banner { position: fixed; bottom: 24px; right: 24px; z-index: 99999; background: var(--bg-card); color: var(--text-primary); border: 1px solid var(--border); border-left: 4px solid var(--accent); padding: 14px 18px; border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.25); font-family: 'Inter', -apple-system, sans-serif; font-size: 0.92rem; display: flex; align-items: center; gap: 12px; max-width: 380px; animation: aegis-slide-in 0.4s cubic-bezier(0.4,0,0.2,1); } @keyframes aegis-slide-in { from { transform: translateX(420px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } #aegis-update-banner b { font-weight: 700; color: var(--accent); } #aegis-update-banner button { background: var(--accent); color: #fff; border: 0; padding: 7px 14px; border-radius: 6px; font: inherit; font-size: 0.86rem; font-weight: 600; cursor: pointer; flex-shrink: 0; } #aegis-update-banner button:hover { background: var(--accent-hover); } #aegis-update-banner .close { background: transparent; color: var(--text-secondary); padding: 0 4px; font-size: 1.2rem; line-height: 1; } #aegis-update-banner .close:hover { color: var(--text-primary); background: transparent; } #aegis-update-modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 99998; backdrop-filter: blur(3px); display: flex; align-items: center; justify-content: center; padding: 24px; animation: aegis-fade-in 0.25s ease; } @keyframes aegis-fade-in { from { opacity: 0; } to { opacity: 1; } } #aegis-update-modal { background: var(--bg-card); color: var(--text-primary); border-radius: 14px; border: 1px solid var(--border); box-shadow: 0 24px 80px rgba(0,0,0,0.4); font-family: 'Inter', -apple-system, sans-serif; max-width: 540px; width: 100%; max-height: 80vh; overflow: hidden; display: flex; flex-direction: column; } #aegis-update-modal header { padding: 22px 28px 18px; border-bottom: 1px solid var(--border); } #aegis-update-modal h2 { margin: 0 0 4px; color: var(--accent); font-size: 1.25rem; font-weight: 700; } #aegis-update-modal header p { margin: 0; color: var(--text-secondary); font-size: 0.88rem; } #aegis-update-modal .body { padding: 8px 28px; overflow-y: auto; } .aegis-release { padding: 16px 0; border-bottom: 1px solid var(--border); } .aegis-release:last-child { border: 0; } .aegis-release-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 8px; } .aegis-release-title { font-size: 1rem; font-weight: 600; color: var(--text-primary); } .aegis-release-date { font-size: 0.78rem; color: var(--text-tertiary); } .aegis-release-items { margin: 0; padding-left: 20px; color: var(--text-secondary); font-size: 0.92rem; line-height: 1.6; } .aegis-release-items li { margin-bottom: 4px; } #aegis-update-modal footer { padding: 16px 28px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; } #aegis-update-modal footer button { background: var(--accent); color: #fff; border: 0; padding: 10px 22px; border-radius: 6px; font: inherit; font-size: 0.92rem; font-weight: 600; cursor: pointer; } #aegis-update-modal footer button:hover { background: var(--accent-hover); } @media (max-width: 600px) { #aegis-update-banner { left: 12px; right: 12px; bottom: 12px; max-width: none; } }`; document.head.appendChild(el('style', { id: 'aegis-update-styles', html: css })); } // ---- Backend-Kommunikation ---- async function fetchVersion() { try { const r = await fetch('/api/version', { cache: 'no-store' }); if (!r.ok) return null; return await r.json(); } catch (e) { return null; } } async function fetchReleaseNotes(since) { try { const url = '/api/release-notes' + (since ? '?since=' + encodeURIComponent(since) : ''); const r = await fetch(url, { cache: 'no-store' }); if (!r.ok) return null; return await r.json(); } catch (e) { return null; } } // ---- Banner ---- function showUpdateBanner() { if (updateBannerShown) return; if (document.getElementById('aegis-update-banner')) return; updateBannerShown = true; const banner = el('div', { id: 'aegis-update-banner' }, el('div', null, el('b', null, 'Update verfügbar'), document.createElement('br'), el('span', { style: 'font-size:0.85rem;opacity:0.85' }, 'Eine neue Version ist live. Bitte Seite neu laden, um sie zu nutzen.') ), el('button', { onclick: () => location.reload() }, 'Aktualisieren'), el('button', { class: 'close', title: 'Schließen', onclick: () => banner.remove() }, '×') ); document.body.appendChild(banner); } // ---- Modal ---- function showWhatsNewModal(entries, currentVersion) { if (document.getElementById('aegis-update-modal-overlay')) return; if (!entries || !entries.length) return; const releases = entries.map(e => { const items = (e.items || []).map(i => el('li', null, i)); return el('div', { class: 'aegis-release' }, el('div', { class: 'aegis-release-head' }, el('span', { class: 'aegis-release-title' }, e.title || 'Update'), el('span', { class: 'aegis-release-date' }, e.date || '') ), items.length ? el('ul', { class: 'aegis-release-items' }, ...items) : null ); }); const overlay = el('div', { id: 'aegis-update-modal-overlay' }, el('div', { id: 'aegis-update-modal' }, el('header', null, el('h2', null, 'Was ist neu?'), el('p', null, 'Diese Änderungen sind seit deinem letzten Besuch dazugekommen.') ), el('div', { class: 'body' }, ...releases), el('footer', null, el('button', { onclick: () => { // Hoechste (= neueste) Version als gesehen markieren const newest = entries[0]?.version; if (newest) localStorage.setItem(STORAGE_KEY, newest); overlay.remove(); } }, 'Verstanden') ) ) ); // ESC oder Klick auf Hintergrund -> wie "Verstanden" overlay.addEventListener('click', (ev) => { if (ev.target === overlay) { const newest = entries[0]?.version; if (newest) localStorage.setItem(STORAGE_KEY, newest); overlay.remove(); } }); document.addEventListener('keydown', function escHandler(ev) { if (ev.key === 'Escape' && document.getElementById('aegis-update-modal-overlay')) { const newest = entries[0]?.version; if (newest) localStorage.setItem(STORAGE_KEY, newest); overlay.remove(); document.removeEventListener('keydown', escHandler); } }); document.body.appendChild(overlay); } // ---- Polling ---- async function pollVersion() { const v = await fetchVersion(); if (v && v.commit && initialBootCommit && v.commit !== initialBootCommit) { showUpdateBanner(); // Polling beenden, sobald Banner gezeigt if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } } // ---- Initial-Boot ---- async function init() { injectStyles(); const v = await fetchVersion(); if (v && v.commit) initialBootCommit = v.commit; // Was-ist-neu-Modal: nur wenn Eintraege NEUER als 'lastSeen' existieren const lastSeen = localStorage.getItem(STORAGE_KEY); const notes = await fetchReleaseNotes(lastSeen); if (notes && notes.entries && notes.entries.length > 0) { // Modal mit etwas Verzoegerung zeigen, damit das Dashboard erst rendert. // Auch beim allerersten Besuch wird das Modal gezeigt — damit Kunden // beim Onboarding sehen, was das Update-System leistet bzw. welche // Highlights aktuell live sind. setTimeout(() => showWhatsNewModal(notes.entries, v?.commit), 800); } // Polling starten pollTimer = setInterval(pollVersion, POLL_INTERVAL_MS); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();