Promote develop → main (2026-04-26 20:40 UTC) #1

Zusammengeführt
IntelSight_Admin hat 2 Commits von develop nach main 2026-04-26 22:40:36 +02:00 zusammengeführt
2 geänderte Dateien mit 264 neuen und 0 gelöschten Zeilen
Nur Änderungen aus Commit 2aaa51e2a8 werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -717,5 +717,6 @@
</div> </div>
</div> </div>
<script src="/static/js/update-system.js"></script>
</body> </body>
</html> </html>

263
src/static/js/update-system.js Normale Datei
Datei anzeigen

@@ -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();
}
})();