GeschĂŒtzte Operations- und Diagnosekonsole. Der Token bleibt nur fĂŒr den Login im Arbeitsspeicher und wird danach durch eine HttpOnly-Sitzung ersetzt.
-
-
-
-
-
+
-
Ăbersicht
+
Ăbersicht
Live Zustand
Dein Homelab auf einen Blick.
Deterministisch aus Butler-Collectoren â keine geschĂ€tzten ZustĂ€nde.
Noch nicht geladen
@@ -69,6 +55,21 @@
Gib einen bekannten Host oder Service ein.
+
+
Konservative Anomalieerkennung
Emby Account Sharing
Zeitgleiche öffentliche Netze und geografisch unmögliche Wechsel â ĂŒber beide Emby-Server.
read-only
+
+
+
IPv4 wird exakt verglichen. IPv6 wird als /64 angezeigt und fĂŒr die Haushaltserkennung konservativ auf /48 zusammengefasst.
+
+
Analysierte Benutzer
â
AuffÀllige Benutzer
â
Zeitgleiche Netze
â
Unmögliche Wechsel
â
+
Erkannte Ereignisse
0
Analyse noch nicht gestartet.
Benutzer-Risiko
nur begrĂŒndete Treffer
Noch keine Daten.
+
+
Read-only Safety Gate
Wartung & Drift
Blocker erkennen, bevor eine Ănderung Schaden anrichtet.
Maintenance Preflight
Noch kein Preflight ausgefĂŒhrt.
Collector Drift
Drift wird geladen âŠ
@@ -98,13 +99,9 @@
const esc=v=>String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]));
const cookie=name=>document.cookie.split('; ').find(x=>x.startsWith(name+'='))?.split('=').slice(1).join('=')||'';
const toast=msg=>{const el=$('#toast');el.textContent=msg;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),2600)};
- async function api(path,options={}){const method=(options.method||'GET').toUpperCase();const headers={...(options.headers||{})};if(!['GET','HEAD','OPTIONS'].includes(method))headers['X-CSRF-Token']=decodeURIComponent(cookie('butler_csrf'));if(options.body&&!headers['Content-Type'])headers['Content-Type']='application/json';const res=await fetch(path,{credentials:'same-origin',...options,headers});if(res.status===401){showLogin();throw new Error('Sitzung abgelaufen')}const text=await res.text();let data;try{data=JSON.parse(text)}catch{data=text}if(!res.ok)throw new Error(data?.detail||data?.error||`HTTP ${res.status}`);return data}
- function showLogin(){ $('#login').classList.remove('hidden');$('#app').classList.add('hidden') }
- function showApp(){ $('#login').classList.add('hidden');$('#app').classList.remove('hidden') }
- $('#login-form').addEventListener('submit',async e=>{e.preventDefault();const error=$('#login-error');error.textContent='';const token=$('#token').value;try{const res=await fetch('/ui/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token}),credentials:'same-origin'});if(!res.ok)throw new Error('Token wurde abgelehnt');$('#token').value='';showApp();await refreshAll()}catch(err){error.textContent=err.message}});
- $('#logout').addEventListener('click',async()=>{try{await api('/ui/logout',{method:'POST'})}finally{showLogin()}});
+ async function api(path,options={}){const method=(options.method||'GET').toUpperCase();const headers={...(options.headers||{})};if(!['GET','HEAD','OPTIONS'].includes(method))headers['X-CSRF-Token']=decodeURIComponent(cookie('butler_csrf'));if(options.body&&!headers['Content-Type'])headers['Content-Type']='application/json';const res=await fetch(path,{credentials:'same-origin',...options,headers});if(res.status===401){await fetch('/ui/session',{credentials:'same-origin'});location.reload();throw new Error('Read-only Sitzung wurde erneuert')}const text=await res.text();let data;try{data=JSON.parse(text)}catch{data=text}if(!res.ok)throw new Error(data?.detail||data?.error||`HTTP ${res.status}`);return data}
$$('.nav button[data-view]').forEach(btn=>btn.addEventListener('click',()=>switchView(btn.dataset.view)));
- function switchView(name){$$('.view').forEach(v=>v.classList.toggle('active',v.id===`view-${name}`));$$('.nav button[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===name));$('#view-title').textContent={overview:'Ăbersicht',doctor:'Doctor',safety:'Wartung & Drift',audit:'Audit',functions:'Alle Funktionen'}[name];$('#sidebar').classList.remove('open')}
+ function switchView(name){$$('.view').forEach(v=>v.classList.toggle('active',v.id===`view-${name}`));$$('.nav button[data-view]').forEach(b=>b.classList.toggle('active',b.dataset.view===name));$('#view-title').textContent={overview:'Ăbersicht',doctor:'Doctor',sharing:'Emby Account Sharing',safety:'Wartung & Drift',audit:'Audit',functions:'Alle Funktionen'}[name];$('#sidebar').classList.remove('open')}
$('#menu').addEventListener('click',()=>$('#sidebar').classList.toggle('open'));
$('#refresh').addEventListener('click',refreshAll);$('#drift-refresh').addEventListener('click',loadDrift);$('#audit-refresh').addEventListener('click',loadAudit);
async function refreshAll(){toast('Aktualisierung gestartet');await Promise.allSettled([loadOverview(),loadDrift(),loadAudit(),loadCapabilities()]);toast('Live-Daten aktualisiert')}
@@ -114,10 +111,13 @@
function renderOperations(){const q=$('#operation-search').value.trim().toLowerCase();const all=[...state.operations,...extraOperations()];const filtered=all.filter(o=>(state.filter==='all'||(state.filter==='destructive'?o.destructive:o.mode===state.filter))&&`${o.method} ${o.path} ${o.summary} ${o.description}`.toLowerCase().includes(q));$('#operation-total').textContent=`${filtered.length} von ${all.length} Punkten`;$('#operations-grid').innerHTML=filtered.map(o=>`