v9.6.0
release-tag / release-image (push) Successful in 2m28s

This commit is contained in:
2026-09-01 22:46:20 +02:00
parent 63d514e50d
commit f3a687543c
18 changed files with 706 additions and 9 deletions
+30 -2
View File
@@ -35,12 +35,12 @@ function fmtAgo(ts){if(!ts)return'nie';const s=Math.max(0,Math.floor(Date.now()/
function setCrumb(t){$('#crumb').textContent=t}
function nodeName(){return state.node?(state.nodes.find(n=>n.id===state.node)?.name||'Remote-Host'):'Lokaler Docker-Host'}
async function init(){applyPreferences();state.me=await api('/api/me');const [sysR,nodesR]=await Promise.allSettled([api('/api/system'),api('/api/nodes')]);state.system=sysR.status==='fulfilled'?sysR.value:null;state.nodes=nodesR.status==='fulfilled'&&Array.isArray(nodesR.value)?nodesR.value:[];renderUser();renderNodePicker();wireShell();await refreshData(true);navigate('dashboard')}
function renderUser(){const name=state.me.name||state.me.email||'Benutzer';$('#userName').textContent=name;$('#userRole').textContent=roleLabel(state.me.role);$('#avatar').textContent=name[0]?.toUpperCase()||'U';const b=state.system?.build;if($('#buildVersion'))$('#buildVersion').textContent=b?`v${b.version} · ${String(b.commit||'dev').slice(0,8)}`:'Dockwatch';const adminOnly=$$('#nav button[data-view="activity"],#nav button[data-view="notifications"],#nav button[data-view="security"]');adminOnly.forEach(e=>e.hidden=state.me.role!=='admin')}
function renderUser(){const name=state.me.name||state.me.email||'Benutzer';$('#userName').textContent=name;$('#userRole').textContent=roleLabel(state.me.role);$('#avatar').textContent=name[0]?.toUpperCase()||'U';const b=state.system?.build;if($('#buildVersion'))$('#buildVersion').textContent=b?`v${b.version} · ${String(b.commit||'dev').slice(0,8)}`:'Dockwatch';const adminOnly=$$('#nav button[data-view="activity"],#nav button[data-view="notifications"],#nav button[data-view="security"],#nav button[data-view="packages"]');adminOnly.forEach(e=>e.hidden=state.me.role!=='admin')}
function renderNodePicker(){const p=$('#globalNode');p.innerHTML=`<option value="0">Lokaler Docker-Host</option>${state.nodes.map(n=>`<option value="${n.id}" ${n.enabled?'':'disabled'}>${esc(n.name)}${n.enabled?'':' · deaktiviert'}</option>`).join('')}`;if(state.node&&state.nodes.some(n=>n.id===state.node&&!n.enabled))state.node=0;p.value=String(state.node);p.onchange=async()=>{if(state.dirty&&!confirm('Ungespeicherte Stack-Änderungen verwerfen?')){p.value=String(state.node);return}state.node=Number(p.value);state.stack=null;setDirty(false);await refreshData(true);render()}}
function wireShell(){$$('#nav button').forEach(b=>b.onclick=()=>navigate(b.dataset.view));$('#logout').onclick=async()=>{await api('/auth/logout',{method:'POST'});location='/auth/login'};$('#refreshNow').onclick=()=>refreshData(true).then(render).catch(e=>toast(e.message));$('#themeToggle').onclick=toggleTheme;$('#sidebarToggle').onclick=()=>{if(innerWidth<=860){document.body.classList.toggle('sidebar-mobile-open');return}const c=!document.body.classList.contains('sidebar-collapsed');localStorage.setItem('dockwatch:sidebar',c?'collapsed':'expanded');applyPreferences()};$('#mobileMenu')?.addEventListener('click',()=>document.body.classList.toggle('sidebar-mobile-open'));document.addEventListener('keydown',e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'&&state.view==='stacks'&&$('#saveStack')){e.preventDefault();saveStack()}if(e.key==='Escape'&&$('#modalRoot')?.innerHTML)closeModal()});window.addEventListener('online',()=>setApiState(true));window.addEventListener('offline',()=>setApiState(false,'Browser ist offline'));setInterval(()=>{if(!document.hidden)refreshData(false).catch(()=>{})},20000)}
function navigate(v){if(state.dirty&&state.view==='stacks'&&v!=='stacks'&&!confirm('Ungespeicherte Stack-Änderungen verlassen?'))return;if(v!==state.view)teardownInteractive();state.view=v;document.body.classList.remove('sidebar-mobile-open');$$('#nav button').forEach(b=>b.classList.toggle('active',b.dataset.view===v));render()}
async function refreshData(force=false){if(state.refreshing)return;state.refreshing=true;try{const [stR,moR,svR]=await Promise.allSettled([api('/api/stacks'+qnode()),api('/api/monitors'),api('/api/services')]);const errs=[];if(stR.status==='fulfilled')state.stacks=Array.isArray(stR.value)?stR.value:[];else errs.push('Stacks: '+stR.reason.message);if(moR.status==='fulfilled')state.monitors=Array.isArray(moR.value)?moR.value:[];else errs.push('Monitore: '+moR.reason.message);if(svR.status==='fulfilled')state.services=Array.isArray(svR.value)?svR.value:[];else errs.push('Dienste: '+svR.reason.message);$('#navStackCount').textContent=state.stacks.length;$('#navMonitorCount').textContent=state.monitors.length;if(force||!state.dirty){if(state.stack?.name){const n=state.stacks.find(x=>x.name===state.stack.name);if(!n&&stR.status==='fulfilled')state.stack=null}if(state.monitor&&moR.status==='fulfilled')state.monitor=state.monitors.find(m=>m.id===state.monitor.id)||state.monitor}if(errs.length&&force)toast(errs.join(' · '))}finally{state.refreshing=false}}
function render(){({dashboard:renderDashboard,stacks:renderStacks,monitors:renderMonitors,services:renderServices,statuspages:renderStatusPages,maintenance:renderMaintenance,nodes:renderNodes,containers:renderDockerResource,images:renderDockerResource,volumes:renderDockerResource,networks:renderDockerResource,git:renderGit,notifications:renderNotifications,security:renderSecurity,activity:renderActivity}[state.view]||renderDashboard)()}
function render(){({dashboard:renderDashboard,stacks:renderStacks,monitors:renderMonitors,services:renderServices,statuspages:renderStatusPages,maintenance:renderMaintenance,nodes:renderNodes,containers:renderDockerResource,images:renderDockerResource,volumes:renderDockerResource,networks:renderDockerResource,git:renderGit,notifications:renderNotifications,security:renderSecurity,packages:renderPackages,activity:renderActivity}[state.view]||renderDashboard)()}
function pageHead(title,sub,actions=''){return `<div class="pagehead"><div><h1>${esc(title)}</h1><p>${esc(sub)}</p></div><div class="toolbar">${actions}</div></div>`}
function renderDashboard(){setCrumb('Übersicht');const running=state.stacks.filter(s=>s.status==='running').length,down=state.monitors.filter(m=>m.status==='down').length,up=state.monitors.filter(m=>m.status==='up').length,svcDown=state.services.filter(s=>s.status==='down').length,svcUp=state.services.filter(s=>s.status==='up').length;$('#content').innerHTML=`${pageHead('Übersicht',nodeName()+' · Docker- und Uptime-Übersicht','<span class="keyboardHint"><kbd>Ctrl</kbd>+<kbd>S</kbd> speichert Stacks</span><button class="btn" id="dashRefresh">↻ Aktualisieren</button>')}<div class="stats"><div class="stat"><small>Compose-Stacks</small><strong>${state.stacks.length}</strong><div class="trend"><span class="green">${running} aktiv</span></div></div><div class="stat"><small>Monitore verfügbar</small><strong>${up}</strong><div class="trend">${state.monitors.length} konfiguriert</div></div><div class="stat"><small>Monitor-Störungen</small><strong class="${down?'red':''}">${down}</strong><div class="trend">aktuelle Prüfungsfehler</div></div><div class="stat"><small>Dienste</small><strong class="${svcDown?'red':''}">${svcUp}/${state.services.length}</strong><div class="trend">${svcDown} gestört</div></div></div><div class="twocol"><div class="panel"><div class="panelhead"><h2>Compose-Stacks</h2><button class="btn tiny" id="goStacks">Alle anzeigen</button></div>${stackTable(state.stacks.slice(0,8))}</div><div class="panel"><div class="panelhead"><h2>Uptime-Monitore</h2><button class="btn tiny" id="goMons">Alle anzeigen</button></div>${monitorTable(state.monitors.slice(0,8))}</div></div>`;$('#dashRefresh').onclick=()=>refreshData(true).then(render);$('#goStacks').onclick=()=>navigate('stacks');$('#goMons').onclick=()=>navigate('monitors');$$('[data-openstack]').forEach(x=>x.onclick=()=>openStack(x.dataset.openstack));$$('[data-openmon]').forEach(x=>x.onclick=()=>openMonitor(Number(x.dataset.openmon)))}
function stackTable(items){if(!items.length)return'<div class="empty">Keine Compose-Stacks gefunden.</div>';return `<table class="table"><thead><tr><th>Name</th><th>Status</th><th>Dienste</th><th>Images</th></tr></thead><tbody>${items.map(s=>`<tr class="${roleOK()?'clickrow':''}" data-openstack="${esc(s.name)}"><td><div class="namecell"><span class="cube">▱</span><b>${esc(s.name)}</b></div></td><td>${badge(s.status)}</td><td>${(s.services||[]).length}</td><td class="muted">${esc((s.services||[]).slice(0,2).map(v=>v.image).filter(Boolean).join(', ')||'—')}</td></tr>`).join('')}</tbody></table>`}
@@ -228,6 +228,34 @@ function notificationModal(x=null){const type=x?.type||'webhook';modal(`<div cla
async function renderPackages(){
setCrumb(`System / System-Updates / ${nodeName()}`);
if(state.me.role!=='admin'){ $('#content').innerHTML=`${pageHead('System-Updates','Administratorrechte erforderlich.')}<div class="empty">Paketverwaltung ist auf Administratoren beschränkt.</div>`; return }
$('#content').innerHTML=`${pageHead('System-Updates',`Verfügbare Linux-Paketupdates prüfen und installieren · ${nodeName()}`,'<button class="btn" id="pkgReload">↻ Prüfen</button>')}<div class="panel"><div class="empty">Paketupdates werden geprüft…</div></div>`;
$('#pkgReload').onclick=renderPackages;
try{
const d=await api('/api/packages/updates'+qnode()),repo=asArray(d.updates),aur=asArray(d.aur_updates),can=!!d.can_manage,pm=d.package_manager||'unbekannt',aurReady=pm==='pacman'&&d.aur_helper&&d.aur_user;
const total=repo.length+aur.length;
const actionDisabled=can?'':'disabled title="Paketinstallation ist für diese Umgebung nicht freigegeben"';
$('#content').innerHTML=`${pageHead('System-Updates',`Verfügbare Linux-Paketupdates prüfen und installieren · ${nodeName()}`,'<button class="btn" id="pkgReload">↻ Prüfen</button>')}
<div class="packageHero"><div><small>PAKETSTATUS</small><div class="packageCount">${total}<span> Updates</span></div><p>${esc(d.os?.pretty_name||d.os?.name||'Host-Betriebssystem unbekannt')} · ${esc(pm)}${d.aur_helper?` · AUR: ${esc(d.aur_helper)}`:''}</p></div><div class="packageActions"><button class="btn" id="pkgRefresh" ${actionDisabled}>Paketlisten aktualisieren</button><button class="btn primary" id="pkgUpgradeSystem" ${repo.length&&can?'':'disabled'}>${pm==='pacman'?'Pacman-System aktualisieren':'Systemupdates installieren'}</button>${d.aur_helper?`<button class="btn" id="pkgUpgradeAur" ${aur.length&&aurReady&&can?'':'disabled'}>AUR aktualisieren</button><button class="btn primary" id="pkgUpgradeAll" ${(repo.length||aur.length)&&aurReady&&can?'':'disabled'}>Alle Updates</button>`:''}</div></div>
${!can?`<div class="notice warnNotice"><b>Nur Prüfung:</b> Für Installationen müssen <code>HOST_SECURITY_ENABLED</code>, <code>ALLOW_HOST_SECURITY_CHANGES</code> und <code>ALLOW_HOST_PACKAGE_MANAGEMENT</code> aktiviert sein.</div>`:''}
${pm==='pacman'&&d.aur_helper&&!d.aur_user?`<div class="notice warnNotice"><b>AUR nicht konfiguriert:</b> Setze <code>HOST_AUR_USER</code> auf einen lokalen Nicht-root-Benutzer, der ${esc(d.aur_helper)} verwendet und Paket-Eskalation non-interaktiv ausführen darf.</div>`:''}
${asArray(d.warnings).map(w=>`<div class="notice warnNotice">${esc(uiMessage(w))}</div>`).join('')}
<div class="panel" style="margin-top:12px"><div class="panelhead"><h2>System-Repositories</h2><span class="tag">${repo.length} verfügbar</span></div>${packageUpdateTable(repo,'Keine Updates aus den System-Repositories verfügbar.')}</div>
${pm==='pacman'||d.aur_helper?`<div class="panel" style="margin-top:12px"><div class="panelhead"><h2>AUR</h2><span class="tag">${aur.length} verfügbar</span></div><div class="notice warnNotice" style="margin:10px"><b>AUR-Vertrauensgrenze:</b> AUR-Pakete stammen aus benutzergepflegten PKGBUILDs. Für den automatisierten Dockwatch-Workflow werden Review-/Auswahlmenüs des Helpers deaktiviert; installiere AUR-Updates nur auf Hosts, deren AUR-Pakete du bewusst vertraust.</div>${d.aur_helper?packageUpdateTable(aur,'Keine AUR-Updates verfügbar.'):'<div class="empty">Kein unterstützter AUR-Helper erkannt. Unterstützt werden <code>paru</code> und <code>yay</code>.</div>'}</div>`:''}
<div class="panel" style="margin-top:12px"><div class="panelhead"><h2>Sicherheitsregeln für Paketupdates</h2></div><div class="securitySafety"><div><b>Keine freien Shell-Kommandos</b><p>Das Interface kann nur vordefinierte Update-Aktionen auslösen.</p></div><div><b>Arch ohne Partial Upgrade</b><p>Offizielle Arch-Pakete werden ausschließlich über vollständiges <code>pacman -Syu</code> aktualisiert.</p></div><div><b>AUR als Benutzer</b><p><code>paru</code>/<code>yay</code> laufen als konfigurierter Nicht-root-Benutzer; sudo wird non-interaktiv angefordert.</p></div><div><b>Master/Agent identisch</b><p>Remote-Aktionen werden an den ausgewählten Agent weitergereicht und dort gegen dessen Berechtigungen geprüft.</p></div></div></div>`;
$('#pkgReload').onclick=renderPackages;
$('#pkgRefresh')?.addEventListener('click',()=>packageRefresh($('#pkgRefresh')));
$('#pkgUpgradeSystem')?.addEventListener('click',()=>packageUpgrade('system',$('#pkgUpgradeSystem'),repo.length));
$('#pkgUpgradeAur')?.addEventListener('click',()=>packageUpgrade('aur',$('#pkgUpgradeAur'),aur.length));
$('#pkgUpgradeAll')?.addEventListener('click',()=>packageUpgrade('all',$('#pkgUpgradeAll'),total));
}catch(e){$('#content').innerHTML=`${pageHead('System-Updates',nodeName(),'<button class="btn" id="pkgReload">↻ Erneut prüfen</button>')}<div class="empty red">${esc(uiMessage(e.message))}</div>`;$('#pkgReload').onclick=renderPackages}
}
function packageUpdateTable(rows,emptyText){if(!rows.length)return `<div class="empty">${emptyText}</div>`;return `<div class="tablewrap"><table class="packageTable"><thead><tr><th>Paket</th><th>Installiert</th><th>Verfügbar</th><th>Quelle</th></tr></thead><tbody>${rows.map(x=>`<tr><td><b>${esc(x.name)}</b>${x.arch?`<small>${esc(x.arch)}</small>`:''}</td><td>${esc(x.current_version||'—')}</td><td><b>${esc(x.new_version||'—')}</b></td><td>${esc(x.repository||x.source||'—')}</td></tr>`).join('')}</tbody></table></div>`}
async function packageRefresh(btn){setBusy(btn,true,'Wird aktualisiert…');try{const out=await api(`/api/packages/refresh${qnode()}`,{method:'POST',body:'{}'});toast(out.message||'Paketlisten aktualisiert.');await renderPackages()}catch(e){toast(uiMessage(e.message));setBusy(btn,false)}}
async function packageUpgrade(scope,btn,count){const label=scope==='aur'?'AUR-Updates':scope==='all'?'alle System- und AUR-Updates':'Systemupdates',aurRisk=scope==='aur'||scope==='all'?' AUR-PKGBUILDs werden dabei ohne interaktives Review gebaut und installiert.':'';if(!confirm(`${count||''} ${label} auf ${nodeName()} jetzt installieren?${aurRisk} Paketdienste können dabei neu gestartet werden und ein Reboot kann anschließend erforderlich sein.`))return;setBusy(btn,true,'Updates werden installiert…');try{const out=await api(`/api/packages/upgrade${qnode()}`,{method:'POST',body:JSON.stringify({scope})});toast(out.message||'Updates installiert.');await renderPackages();if(out.output)showOutput(`System-Updates · ${nodeName()}`,out.output)}catch(e){toast(uiMessage(e.message));setBusy(btn,false)}}
async function renderSecurity(){
setCrumb(`System / Host-Sicherheit / ${nodeName()}`);
if(state.me.role!=='admin'){ $('#content').innerHTML=`${pageHead('Host-Sicherheit','Administratorrechte erforderlich.')}<div class="empty">Die Host-Sicherheitskonfiguration ist auf Administratoren beschränkt.</div>`; return }
+3 -2
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dockwatch</title>
<link rel="stylesheet" href="/styles.css?v=9.5.3">
<link rel="stylesheet" href="/styles.css?v=9.6.0">
</head>
<body>
<div id="shell">
@@ -29,6 +29,7 @@
<div class="navlabel">System</div>
<button data-view="nodes"><span>◎</span>Umgebungen</button>
<button data-view="security"><span>⛨</span>Host-Sicherheit</button>
<button data-view="packages"><span>⇧</span>System-Updates</button>
<button data-view="activity"><span>≋</span>Aktivitäten</button>
</nav>
<div class="sidebarMeta"><span id="buildVersion">v…</span></div><div class="sidebarFooter"><div class="avatar" id="avatar">U</div><div class="usertext"><b id="userName">…</b><small id="userRole">…</small></div><button id="logout" title="Abmelden">↪</button></div>
@@ -39,5 +40,5 @@
</main>
</div>
<div id="modalRoot"></div><div id="toast"></div>
<script src="/app.js?v=9.5.3"></script>
<script src="/app.js?v=9.6.0"></script>
</body></html>
+8
View File
@@ -186,3 +186,11 @@ body.sidebar-collapsed #shell{grid-template-columns:64px 1fr}body.sidebar-collap
:root[data-theme="light"] .placeholder{background:#f8fafc;color:#94a3b8}
:root[data-theme="light"] .environment{background:#fff}
:root[data-theme="light"] .sidebarFooter{background:#fff}
/* v9.6.0 system package updates */
.packageHero{display:flex;justify-content:space-between;align-items:center;gap:20px;border:1px solid var(--line);border-radius:10px;padding:18px 20px;margin-bottom:12px;background:linear-gradient(135deg,var(--panel),var(--panel2))}.packageHero small{font-size:9px;letter-spacing:.12em;color:var(--muted)}.packageHero p{margin:5px 0 0;color:var(--muted);font-size:11px}.packageCount{font-size:38px;font-weight:750;line-height:1;margin-top:4px}.packageCount span{font-size:13px;color:var(--muted);font-weight:500}.packageActions{display:flex;gap:7px;flex-wrap:wrap;justify-content:flex-end}.packageTable{width:100%;border-collapse:collapse;min-width:720px}.packageTable th,.packageTable td{padding:9px 10px;text-align:left;border-bottom:1px solid var(--line);font-size:10px}.packageTable th{background:var(--panel2);color:var(--muted);font-size:9px;text-transform:uppercase;letter-spacing:.05em}.packageTable tbody tr:last-child td{border-bottom:0}.packageTable td:first-child b{display:block;font-size:11px}.packageTable td:first-child small{display:block;color:var(--muted);margin-top:2px}.packageTable tbody tr:hover{background:var(--panel2)}
@media(max-width:860px){.packageHero{align-items:flex-start;flex-direction:column}.packageActions{justify-content:flex-start;width:100%}}
:root[data-theme="light"] .packageHero{background:linear-gradient(135deg,#fff,#f8fafc)}
:root[data-theme="light"] .packageTable th{background:#f8fafc;color:#64748b}
:root[data-theme="light"] .packageTable td{border-bottom-color:#e5eaf1}
:root[data-theme="light"] .packageTable tbody tr:hover{background:#f8fafc}