From 2aaa51e2a8b81b7da8937b7bfbe853965611306d Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sun, 26 Apr 2026 20:32:50 +0000 Subject: [PATCH 1/2] Update-System Frontend: Banner + Was-ist-neu-Modal Beim ersten Login nach einem Update zeigt der Monitor nun ein Modal mit den Release-Notes des Updates (aus RELEASES.json). Wenn waehrend einer laufenden Sitzung ein neues Update live geht, erscheint unten rechts ein Banner mit einem Aktualisieren-Knopf. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/static/dashboard.html | 1 + src/static/js/update-system.js | 263 +++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/static/js/update-system.js diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 6f5defe..70c9cee 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -717,5 +717,6 @@ + diff --git a/src/static/js/update-system.js b/src/static/js/update-system.js new file mode 100644 index 0000000..cb9dd7f --- /dev/null +++ b/src/static/js/update-system.js @@ -0,0 +1,263 @@ +/** + * 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) ---- + function injectStyles() { + if (document.getElementById('aegis-update-styles')) return; + const css = ` + #aegis-update-banner { + position: fixed; bottom: 24px; right: 24px; z-index: 99999; + background: linear-gradient(135deg, #C8A851, #D4B96A); + color: #0A1832; padding: 14px 18px; border-radius: 10px; + box-shadow: 0 8px 32px rgba(10,24,50,0.4); + 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; } + #aegis-update-banner button { + background: #0A1832; color: #C8A851; 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: #132844; } + #aegis-update-banner .close { + background: transparent; color: #0A1832; opacity: 0.6; padding: 0 4px; + font-size: 1.2rem; line-height: 1; + } + #aegis-update-banner .close:hover { opacity: 1; background: transparent; } + + #aegis-update-modal-overlay { + position: fixed; inset: 0; background: rgba(10,24,50,0.75); 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: #132844; color: #E8E8E8; border-radius: 14px; + border: 1px solid rgba(200,168,81,0.25); + box-shadow: 0 24px 80px rgba(0,0,0,0.5); + 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 rgba(200,168,81,0.15); + } + #aegis-update-modal h2 { margin: 0 0 4px; color: #C8A851; font-size: 1.25rem; font-weight: 700; } + #aegis-update-modal header p { margin: 0; color: #A0A8B8; font-size: 0.88rem; } + #aegis-update-modal .body { padding: 8px 28px; overflow-y: auto; } + .aegis-release { padding: 16px 0; border-bottom: 1px solid rgba(255,255,255,0.06); } + .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: #E8E8E8; } + .aegis-release-date { font-size: 0.78rem; color: #5A6478; } + .aegis-release-items { margin: 0; padding-left: 20px; color: #C0C8D8; 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 rgba(200,168,81,0.15); + display: flex; justify-content: flex-end; + } + #aegis-update-modal footer button { + background: #C8A851; color: #0A1832; 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: #D4B96A; } + + @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) { + // Wenn lastSeen leer ist (erster Besuch ueberhaupt), kein Modal, + // sondern nur den aktuellen Stand als "gesehen" markieren. + if (!lastSeen) { + if (notes.entries[0]?.version) { + localStorage.setItem(STORAGE_KEY, notes.entries[0].version); + } + } else { + // mit etwas Verzoegerung, damit das Dashboard erst rendert + 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(); + } +})(); -- 2.49.1 From a9806a586bb1bf9b0772ec7d371cc2defaa673a9 Mon Sep 17 00:00:00 2001 From: IntelSight_Admin Date: Sun, 26 Apr 2026 22:40:34 +0200 Subject: [PATCH 2/2] Release-Notes: Updatenachricht bei Deployment --- RELEASES.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASES.json b/RELEASES.json index a20d7c4..e37cfc1 100644 --- a/RELEASES.json +++ b/RELEASES.json @@ -1,4 +1,13 @@ [ + { + "version": "2026-04-26T20:40Z", + "date": "2026-04-26", + "title": "Updatenachricht bei Deployment", + "items": [ + "Einrichtung Deployment für Updates", + "Message im Monitor bei Update" + ] + }, { "version": "5473ba3", "date": "2026-04-26", -- 2.49.1