Promote develop → main (2026-04-26 20:40 UTC) #1
@@ -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",
|
"version": "5473ba3",
|
||||||
"date": "2026-04-26",
|
"date": "2026-04-26",
|
||||||
|
|||||||
@@ -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
263
src/static/js/update-system.js
Normale Datei
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren