From b466737549816d3a6a993aa1bae625d4dfe8ea05 Mon Sep 17 00:00:00 2001 From: jbergner Date: Sat, 22 Aug 2026 19:32:13 +0200 Subject: [PATCH] WebUI-Upate --- CHANGELOG.md | 10 ++++++ README.md | 11 +++++-- internal/agent/ui.go | 62 ++++++++++++++++++++++++++++++++++--- internal/master/ui.go | 72 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 142 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b4241..b6962f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.3.4 — Modern Web UI + +- Master- und Agent-WebUI vollständig modernisiert, weiterhin ohne Framework oder Build-Schritt. +- Responsive Sidebar-Navigation mit Mobile-Menü und Scroll-Tracking. +- Dark-/Light-Theme mit lokaler Browser-Präferenz. +- Neue Dashboard-Karten, Panel-Hierarchie, Status-Badges, moderne Formulare/Switches und Tabellen. +- Live-Bereiche und Editoren bleiben weiterhin getrennt; automatische Refreshes überschreiben keine Eingaben. +- Verbesserte Darstellung für Farms, Broker, Published Resources, Director, Alerts, Audit und lokale Agent-Ansichten. +- Keine neue Runtime-Abhängigkeit: HTML/CSS/Vanilla-JavaScript bleiben direkt in den Go-Binaries eingebettet. + ## 0.3.3 - Fix: Master-WebUI definiert und aktualisiert `agentCache`, bevor die Farm-Mitglieder berechnet werden. diff --git a/README.md b/README.md index 37be3a2..11e0c8b 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,17 @@ SessionGuard is a Go-based **RDS control plane** for Windows Remote Desktop Session Hosts. It is designed to complement Apache Guacamole: Guacamole remains the HTML5/RDP gateway, while SessionGuard provides Citrix-like broker, Director, policy, profile-lifecycle and operations functions. -**Current development version: 0.3.3 (Broker & Director production candidate)** +**Current development version: 0.3.4 (Broker & Director production candidate)** > SessionGuard is not an ICA/HDX implementation and does not replace the Windows RDS runtime. It deliberately reuses standard RDP/WTS, Guacamole and PocketID/OIDC. +## 0.3.4 Modern Web UI + +- Master and Agent consoles use a responsive enterprise-style sidebar and dashboard layout. +- Dark/light theme switching is implemented with plain CSS and Vanilla JavaScript. +- Modern status badges, cards, forms, switches and tables without Node.js, npm, Vue, React or a frontend build step. +- The existing refresh-safe editor behavior remains intact: live telemetry can update without replacing forms currently being edited. + ## 0.3.3 Farm UI regression fix The master UI now maintains an explicit `agentCache` from the dashboard response before rendering farm membership. This fixes `ReferenceError: agentCache is not defined` introduced by the v0.3.2 farm-count display correction. @@ -288,4 +295,4 @@ The current source tree contains unit tests for configuration, templates, profil ## Production-candidate status -The design intentionally fails closed around destructive profile operations and broker farm boundaries. Nevertheless, v0.3.3 should be introduced as a canary before broad production rollout. In particular, validate native WTS behavior, SMB failure/recovery, PostgreSQL backup/restore, Guacamole extension loading and your exact PocketID group claims in your environment. +The design intentionally fails closed around destructive profile operations and broker farm boundaries. Nevertheless, v0.3.4 should be introduced as a canary before broad production rollout. In particular, validate native WTS behavior, SMB failure/recovery, PostgreSQL backup/restore, Guacamole extension loading and your exact PocketID group claims in your environment. diff --git a/internal/agent/ui.go b/internal/agent/ui.go index 8b80cce..1c677f7 100644 --- a/internal/agent/ui.go +++ b/internal/agent/ui.go @@ -1,18 +1,70 @@ package agent -const agentHTML = `SessionGuard Agent
SessionGuard Agent
Lokaler Terminalserver
Aktiv
Getrennt
Sitzungen
Profil-Jobs
Cleanup
Master

Sitzungen

Profil-Pipeline

Aktivitäts- & Audit-Log

Lokale Policy

` +const agentHTML = `SessionGuard Agent +
+
Lokaler Agent
Lokaler Terminalserver
Live · 5s
+
Local Control

Terminalserver-Status

Sitzungen, Profil-Pipeline und Master-Verbindung lokal überwachen.
Aktiv
Getrennt
Sitzungen
Profil-Jobs
Cleanup
Master
+
RDS

Sitzungen

Aktive und getrennte Benutzer sowie administrative Aktionen.
+
Profile

Profil-Pipeline

Backup, Restore, Retry und Cleanup-Status pro Benutzer.
+
Telemetry

Aktivitäts- & Audit-Log

Dry-Run, Profilaktionen, Fehler und lokale Ereignisse.
+
Configuration

Lokale Policy

Fallback-Konfiguration für Profile, Sessions, Cleanup und Templates.
+
` const agentJS = ` let policyTemplates=[],profileFolders=[],lastSnapshot=null,policyDirty=false,policyLoaded=false; const $=id=>document.getElementById(id);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + +function initShell(){ + let saved='';try{saved=localStorage.getItem('sessionguard-theme')||''}catch(e){} + let theme=saved||((window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches)?'light':'dark'); + function applyTheme(t){document.documentElement.dataset.theme=t;try{localStorage.setItem('sessionguard-theme',t)}catch(e){};let label=t==='light'?'☾ Dunkles Theme':'☀ Helles Theme';let b=$('themeToggle');if(b)b.textContent=label;let bt=$('themeTop');if(bt)bt.textContent=t==='light'?'☾':'☀'} + function toggleTheme(){theme=document.documentElement.dataset.theme==='light'?'dark':'light';applyTheme(theme)} + applyTheme(theme);let b=$('themeToggle'),bt=$('themeTop');if(b)b.onclick=toggleTheme;if(bt)bt.onclick=toggleTheme; + let mt=$('menuToggle'),ov=$('mobileOverlay');if(mt)mt.onclick=()=>document.body.classList.toggle('nav-open');if(ov)ov.onclick=()=>document.body.classList.remove('nav-open'); + document.querySelectorAll('.nav-link').forEach(a=>a.addEventListener('click',()=>{document.body.classList.remove('nav-open');document.querySelectorAll('.nav-link').forEach(x=>x.classList.remove('active'));a.classList.add('active')})); + if('IntersectionObserver' in window){let links=[...document.querySelectorAll('.nav-link[href^="#"]')],map=new Map(links.map(a=>[a.getAttribute('href').slice(1),a]));let io=new IntersectionObserver(entries=>{let best=entries.filter(e=>e.isIntersecting).sort((a,b)=>b.intersectionRatio-a.intersectionRatio)[0];if(!best)return;let a=map.get(best.target.id);if(a){links.forEach(x=>x.classList.remove('active'));a.classList.add('active')}},{rootMargin:'-18% 0px -65% 0px',threshold:[0,.15,.4]});map.forEach((_,id)=>{let el=document.getElementById(id);if(el)io.observe(el)})} +} async function api(u,o){let r=await fetch(u,o);if(r.status===401){location='/login';return}let j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||r.statusText);return j}function lines(id){return $(id).value.split('\n').map(x=>x.trim()).filter(Boolean)}function when(v){if(!v)return '–';let d=new Date(v);return Number.isNaN(d.getTime())||d.getFullYear()<2000?'–':d.toLocaleString('de-DE')}function dur(v){if(!v)return '–';let s=Math.max(0,Math.floor((Date.now()-new Date(v).getTime())/1000));return Math.floor(s/3600)+'h '+Math.floor(s%3600/60)+'m'} function renderEvents(events){let rows=(events||[]).slice().reverse().slice(0,200);$('events').innerHTML=rows.length?'
'+rows.map(x=>'').join('')+'
ZeitTypBenutzerMeldung
'+esc(when(x.time))+''+esc(x.level||'info')+''+esc(x.user||'–')+''+esc(x.message||'')+'
':'
Noch keine Ereignisse.
'} function renderProfiles(s){let jobs=s.profile_jobs||[],status=Object.values(s.profile_status||{});let html='';if(jobs.length)html+=''+jobs.map(j=>'').join('')+'
JobBenutzerFälligVersucheFehler
'+esc(j.operation)+''+esc(j.user)+''+esc(when(j.due_at))+''+j.attempts+''+esc(j.last_error||'–')+'
';if(status.length)html+=''+status.map(x=>'').join('')+'
BenutzerLetztes BackupLetzter RestoreStatus
'+esc(x.user||x.sid)+''+esc(when(x.last_backup_at))+''+esc(when(x.last_restore_at))+''+esc(x.last_backup_error||x.last_restore_error||'OK')+'
';$('profiles').innerHTML=html||'
Keine Profil-Jobs oder -Historie.
'} async function sessionAction(id,action){let body={action};if(action==='message'){let m=prompt('Nachricht an die Sitzung:');if(!m)return;body.message=m;body.title='SessionGuard'}if(action==='logoff'&&!confirm('Sitzung '+id+' wirklich abmelden? Die Profil-Pipeline startet nach dem Sitzungsende.'))return;try{await api('/api/v1/sessions/'+id+'/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});setTimeout(refresh,400)}catch(e){alert(e.message)}} function renderSessions(s){let p=s.policy||{},control=p.sessions&&p.sessions.control_enabled,ss=s.sessions||[];$('sessions').innerHTML=''+ss.map(x=>'').join('')+'
IDBenutzerStatusGetrennt seitClientAktionen
'+x.id+''+esc((x.domain?x.domain+'\\':'')+x.user)+''+esc(x.state)+''+esc(x.disconnected_since?when(x.disconnected_since)+' ('+dur(x.disconnected_since)+')':'–')+''+esc(x.client_name||'–')+''+(control&&x.user?'
':'–')+'
'} -async function refresh(){try{let d=await api('/api/v1/status'),s=d.snapshot||{};lastSnapshot=s;let ss=s.sessions||[];$('host').textContent=(s.server&&s.server.hostname)||'Lokaler Terminalserver';$('active').textContent=ss.filter(x=>x.state==='Active').length;$('disc').textContent=ss.filter(x=>x.state==='Disconnected').length;$('total').textContent=ss.filter(x=>x.user).length;$('pending').textContent=(s.pending_cleanup||[]).length;$('profileJobs').textContent=(s.profile_jobs||[]).length;$('master').innerHTML=d.master_error?'Offline':'Verbunden';renderSessions(s);renderProfiles(s);renderEvents(s.events||[])}catch(e){$('master').innerHTML=''+esc(e.message)+''}} +async function refresh(){try{let d=await api('/api/v1/status'),s=d.snapshot||{};lastSnapshot=s;let ss=s.sessions||[];$('host').textContent=(s.server&&s.server.hostname)||'Lokaler Terminalserver';$('active').textContent=ss.filter(x=>x.state==='Active').length;$('disc').textContent=ss.filter(x=>x.state==='Disconnected').length;$('total').textContent=ss.filter(x=>x.user).length;$('pending').textContent=(s.pending_cleanup||[]).length;$('profileJobs').textContent=(s.profile_jobs||[]).length;$('master').innerHTML=d.master_error?'Offline':'Verbunden';renderSessions(s);renderProfiles(s);renderEvents(s.events||[])}catch(e){$('master').innerHTML=''+esc(e.message)+''}} function templateDefault(){return{id:'neues-template',kind:'file',target:'Desktop\\Beispiel.txt',source:'',content:'',content_base64:'',url:'',shortcut:{target:'',arguments:'',working_directory:'',icon_location:'',description:''},overwrite:true}}function templateSpecific(t){let k=(t.kind||'file').toLowerCase();if(k==='directory')return'
Keine weiteren Angaben.
';if(k==='url')return'';if(k==='shortcut'){let s=t.shortcut||{};return'
'}return'
'} function renderTemplates(){let h=$('templateList');if(!h)return;h.innerHTML=policyTemplates.length?policyTemplates.map((t,i)=>'
'+esc(t.id||('Template '+(i+1)))+'
'+templateSpecific(t)+'
').join(''):'
Keine Templates konfiguriert.
'} function collectTemplates(){return[...document.querySelectorAll('.template-card')].map(c=>{let g=n=>{let e=c.querySelector('[data-field="'+n+'"]');return e?e.value:''},k=g('kind')||'file',t={id:g('id').trim(),kind:k,target:g('target').trim(),overwrite:!!c.querySelector('[data-field="overwrite"]:checked')};if(k==='file'){t.source=g('source').trim();t.content=g('content');t.content_base64=g('content_base64').trim()}else if(k==='url')t.url=g('url').trim();else if(k==='shortcut')t.shortcut={target:g('shortcut.target').trim(),arguments:g('shortcut.arguments'),working_directory:g('shortcut.working_directory').trim(),icon_location:g('shortcut.icon_location').trim(),description:g('shortcut.description')};return t})} @@ -22,4 +74,4 @@ function policyHTML(p){let c=p.cleanup||{},pr=p.profiles||{},sp=p.sessions||{};r function updatePolicyDirty(){let e=$('policyDirtyState');if(e)e.textContent=policyDirty?'Ungespeicherte Änderungen':''}function markPolicyDirty(){policyDirty=true;updatePolicyDirty()}async function reloadPolicy(){if(policyDirty&&!confirm('Ungespeicherte Änderungen verwerfen und Policy neu laden?'))return;policyDirty=false;await loadPolicy(true)} async function loadPolicy(force){if(policyDirty&&!force)return;try{let p=await api('/api/v1/policy');policyTemplates=JSON.parse(JSON.stringify(p.templates||[]));profileFolders=JSON.parse(JSON.stringify((p.profiles&&p.profiles.folders)||[]));$('policy').innerHTML=policyHTML(p);renderTemplates();renderProfileFolders();policyDirty=false;policyLoaded=true;updatePolicyDirty()}catch(e){if(!policyLoaded)$('policy').textContent=e.message}} async function savePolicy(){try{policyTemplates=collectTemplates();profileFolders=collectProfileFolders();let p={profiles:{enabled:$('profEnabled').checked,store_root:$('storeRoot').value.trim(),backup_on_logoff:$('backupOnLogoff').checked,restore_on_logon:$('restoreOnLogon').checked,backup_delay_seconds:+$('backupDelay').value,retry_seconds:+$('profRetry').value,restore_window_seconds:+$('restoreWindow').value,keep_versions:+$('keepVersions').value,exclude_users:lines('profUsers'),exclude_sids:lines('profSids'),folders:profileFolders},sessions:{control_enabled:$('controlEnabled').checked,disconnected_logoff_enabled:$('autoLogoff').checked,disconnected_timeout_seconds:+$('discTimeout').value,exclude_users:lines('sessUsers'),exclude_sids:lines('sessSids')},cleanup:{enabled:$('enabled').checked,grace_seconds:+$('grace').value,poll_seconds:+$('poll').value,retry_seconds:+$('retry').value,dry_run:$('dry').checked,exclude_users:lines('users'),exclude_sids:lines('sids'),allowed_profile_roots:lines('roots')},templates:policyTemplates};await api('/api/v1/policy',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});policyDirty=false;await loadPolicy(true);refresh()}catch(e){alert(e.message)}} -$('sessions').addEventListener('click',e=>{let b=e.target.closest('button[data-session-action]');if(b)sessionAction(+b.dataset.session,b.dataset.sessionAction)});$('policy').addEventListener('click',e=>{let b=e.target.closest('button[data-action]');if(!b)return;let a=b.dataset.action;if(a==='save-policy')savePolicy();if(a==='reload-policy')reloadPolicy();if(a==='add-template'){policyTemplates=collectTemplates();policyTemplates.push(templateDefault());renderTemplates();markPolicyDirty()}if(a==='remove-template'){policyTemplates=collectTemplates();policyTemplates.splice(+b.dataset.index,1);renderTemplates();markPolicyDirty()}if(a==='add-profile-folder'){profileFolders=collectProfileFolders();profileFolders.push({path:'AppData\\Roaming\\Hersteller',exclude_globs:['Cache/**']});renderProfileFolders();markPolicyDirty()}if(a==='remove-profile-folder'){profileFolders=collectProfileFolders();profileFolders.splice(+b.dataset.index,1);renderProfileFolders();markPolicyDirty()}});$('policy').addEventListener('input',e=>{if(e.target.matches('input,textarea,select'))markPolicyDirty()});$('policy').addEventListener('change',e=>{if(e.target.matches('select[data-action="template-kind"]')){policyTemplates=collectTemplates();renderTemplates();markPolicyDirty()}});refresh();loadPolicy(true);setInterval(refresh,5000);` +$('sessions').addEventListener('click',e=>{let b=e.target.closest('button[data-session-action]');if(b)sessionAction(+b.dataset.session,b.dataset.sessionAction)});$('policy').addEventListener('click',e=>{let b=e.target.closest('button[data-action]');if(!b)return;let a=b.dataset.action;if(a==='save-policy')savePolicy();if(a==='reload-policy')reloadPolicy();if(a==='add-template'){policyTemplates=collectTemplates();policyTemplates.push(templateDefault());renderTemplates();markPolicyDirty()}if(a==='remove-template'){policyTemplates=collectTemplates();policyTemplates.splice(+b.dataset.index,1);renderTemplates();markPolicyDirty()}if(a==='add-profile-folder'){profileFolders=collectProfileFolders();profileFolders.push({path:'AppData\\Roaming\\Hersteller',exclude_globs:['Cache/**']});renderProfileFolders();markPolicyDirty()}if(a==='remove-profile-folder'){profileFolders=collectProfileFolders();profileFolders.splice(+b.dataset.index,1);renderProfileFolders();markPolicyDirty()}});$('policy').addEventListener('input',e=>{if(e.target.matches('input,textarea,select'))markPolicyDirty()});$('policy').addEventListener('change',e=>{if(e.target.matches('select[data-action="template-kind"]')){policyTemplates=collectTemplates();renderTemplates();markPolicyDirty()}});initShell();refresh();loadPolicy(true);setInterval(refresh,5000);` diff --git a/internal/master/ui.go b/internal/master/ui.go index ba3422d..e32f24e 100644 --- a/internal/master/ui.go +++ b/internal/master/ui.go @@ -1,12 +1,72 @@ package master -const masterHTML = `SessionGuard Master
SessionGuard
Master Console · v0.3.3 · Broker & Director
Server
Online
Aktiv
Getrennt
Profil-Jobs
Cleanup
Farms
Alerts

Terminalserver

Server auswählen

Links einen Agent auswählen.

Farms & Broker

Published Resources

Session-Historie

Policy-Historie & Rollback

Alerts

Audit-Log

` +const masterHTML = `SessionGuard Master +
+ +
Master Console
Broker, Director, Profile & Policy Management
Live · 5s
+
Control Plane

Infrastruktur auf einen Blick

Live-Status der RDS-Farm, Profile und Broker-Ressourcen.
+
Server
Online
Aktiv
Getrennt
Profil-Jobs
Cleanup
Farms
Alerts!
+
Director

Terminalserver

Server auswählen, Sitzungen steuern und Health-Details prüfen.

Serverliste

Online, Sessions, Health und Jobs

Server auswählen

Live-Details und Konfiguration
Links einen Agent auswählen.
+
Broker

Farms & Broker

Hosts gruppieren, Tags definieren und aktive Leases nachvollziehen.
+
Delivery

Published Resources

Desktops und RemoteApps mit Guacamole-Verbindungen und Farms verknüpfen.
+
Historie

Session-Historie

Logon, Reconnect, Disconnect und Profilereignisse nachvollziehen.
+
Governance

Policy-Historie & Rollback

Revisionen vergleichen und frühere Sollstände wieder aktivieren.
+
Monitoring

Alerts

Aktive und gelöste Betriebswarnungen.
+
Compliance

Audit-Log

Administrative Änderungen und Aktionen revisionsfreundlich nachvollziehen.
+
` const masterJS = ` let selected=null,current=null,editorAgent=null,policyTemplates=[],profileFolders=[],agentCache=[],farmCache=[],resourceCache=[],brokerLeases=[],policyHistory=[],me=null;const $=id=>document.getElementById(id);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));function when(v){if(!v)return'–';let d=new Date(v);return Number.isNaN(d.getTime())||d.getFullYear()<2000?'–':d.toLocaleString('de-DE')}function bytes(n){if(!n)return'–';let u=['B','KB','MB','GB','TB'],i=0;while(n>=1024&&i1?1:0)+' '+u[i]}function toast(t){let e=$('toast');e.textContent=t;e.style.display='block';setTimeout(()=>e.style.display='none',3000)}async function api(u,o){let r=await fetch(u,o);if(r.status===401){location='/login';return}let j=await r.json().catch(()=>({}));if(!r.ok)throw new Error(j.error||r.statusText);return j}function lines(id){return $(id).value.split('\n').map(x=>x.trim()).filter(Boolean)} -function renderAgentTable(a){$('agents').innerHTML=''+a.map(x=>'').join('')+'
StatusServerModeHealthSitzungenJobs
'+(x.online?'Online':'Offline')+''+esc(x.name)+'
'+esc(x.snapshot.agent_version||'–')+'
'+esc(x.maintenance_mode||'online')+''+esc((x.snapshot.health||{}).score??'–')+''+x.active_sessions+' aktiv / '+x.total_sessions+''+((x.snapshot.profile_jobs||[]).length)+' P / '+((x.snapshot.pending_cleanup||[]).length)+' C
';document.querySelectorAll('.row').forEach(r=>r.onclick=()=>selectAgent(r.dataset.id,false,false))} + +function initShell(){ + let saved='';try{saved=localStorage.getItem('sessionguard-theme')||''}catch(e){} + let theme=saved||((window.matchMedia&&window.matchMedia('(prefers-color-scheme: light)').matches)?'light':'dark'); + function applyTheme(t){document.documentElement.dataset.theme=t;try{localStorage.setItem('sessionguard-theme',t)}catch(e){};let label=t==='light'?'☾ Dunkles Theme':'☀ Helles Theme';let b=$('themeToggle');if(b)b.textContent=label;let bt=$('themeTop');if(bt)bt.textContent=t==='light'?'☾':'☀'} + function toggleTheme(){theme=document.documentElement.dataset.theme==='light'?'dark':'light';applyTheme(theme)} + applyTheme(theme);let b=$('themeToggle'),bt=$('themeTop');if(b)b.onclick=toggleTheme;if(bt)bt.onclick=toggleTheme; + let mt=$('menuToggle'),ov=$('mobileOverlay');if(mt)mt.onclick=()=>document.body.classList.toggle('nav-open');if(ov)ov.onclick=()=>document.body.classList.remove('nav-open'); + document.querySelectorAll('.nav-link').forEach(a=>a.addEventListener('click',()=>{document.body.classList.remove('nav-open');document.querySelectorAll('.nav-link').forEach(x=>x.classList.remove('active'));a.classList.add('active')})); + if('IntersectionObserver' in window){let links=[...document.querySelectorAll('.nav-link[href^="#"]')],map=new Map(links.map(a=>[a.getAttribute('href').slice(1),a]));let io=new IntersectionObserver(entries=>{let best=entries.filter(e=>e.isIntersecting).sort((a,b)=>b.intersectionRatio-a.intersectionRatio)[0];if(!best)return;let a=map.get(best.target.id);if(a){links.forEach(x=>x.classList.remove('active'));a.classList.add('active')}},{rootMargin:'-18% 0px -65% 0px',threshold:[0,.15,.4]});map.forEach((_,id)=>{let el=document.getElementById(id);if(el)io.observe(el)})} +} +function renderAgentTable(a){$('agents').innerHTML=''+a.map(x=>'').join('')+'
StatusServerModeHealthSitzungenJobs
'+(x.online?'Online':'Offline')+''+esc(x.name)+'
'+esc(x.snapshot.agent_version||'–')+'
'+esc(x.maintenance_mode||'online')+''+esc((x.snapshot.health||{}).score??'–')+''+x.active_sessions+' aktiv / '+x.total_sessions+''+((x.snapshot.profile_jobs||[]).length)+' P / '+((x.snapshot.pending_cleanup||[]).length)+' C
';document.querySelectorAll('.row').forEach(r=>r.onclick=()=>selectAgent(r.dataset.id,false,false))} async function refresh(){try{let d=await api('/api/v1/dashboard'),a=d.agents||[];agentCache=a;$('mServers').textContent=a.length;$('mOnline').textContent=a.filter(x=>x.online).length;$('mActive').textContent=a.reduce((n,x)=>n+x.active_sessions,0);$('mDisc').textContent=a.reduce((n,x)=>n+(x.disconnected_sessions||0),0);$('mProfile').textContent=a.reduce((n,x)=>n+(x.snapshot.profile_jobs||[]).length,0);$('mCleanup').textContent=a.reduce((n,x)=>n+(x.snapshot.pending_cleanup||[]).length,0);$('mFarms').textContent=d.farms||0;$('mAlerts').textContent=d.active_alerts||0;renderAgentTable(a);if(selected)await selectAgent(selected,true,true);let fr=await api('/api/v1/farms');farmCache=fr.farms||[];let rr=await api('/api/v1/resources');resourceCache=rr.resources||[];let lr=await api('/api/v1/leases');brokerLeases=lr.leases||[];renderFarms();renderResources();let hi=await api('/api/v1/history?limit=500');renderHistory(hi.history||[]);try{let ph=await api('/api/v1/policy/history');policyHistory=ph.history||[];renderPolicyHistory()}catch(e){$('policyHistory').innerHTML='
Policy-Historie erfordert Policy-Admin/Admin-Rolle.
'}let al=await api('/api/v1/alerts');renderAlerts(al.alerts||[]);if(!me)me=await api('/api/v1/me');try{let au=await api('/api/v1/audit');renderAudit(au.audit||[])}catch(e){$('audit').innerHTML='
Audit-Log erfordert Auditor/Admin-Rolle.
'}}catch(e){toast(e.message)}} async function selectAgent(id,quiet,preserve){let same=selected===id;selected=id;try{current=await api('/api/v1/agents/'+encodeURIComponent(id));renderDetail(current,!!preserve&&same)}catch(e){if(!quiet)toast(e.message)}} function renderPolicyHistory(){let rows=(policyHistory||[]).slice().reverse().slice(0,250);$('policyHistory').innerHTML=rows.length?''+rows.map(x=>'').join('')+'
ZeitZielRevisionAkteur
'+when(x.created_at)+''+esc(x.target)+''+esc(x.revision)+''+esc(x.actor||'–')+'
':'
Noch keine Policy-Versionen.
'} @@ -19,7 +79,7 @@ function profileHTML(a){let s=a.snapshot||{},jobs=s.profile_jobs||[],st=Object.v function processesHTML(a){let ps=((a.snapshot||{}).processes||[]).filter(p=>p.session_id!==0).sort((x,y)=>y.memory_bytes-x.memory_bytes).slice(0,300);return ps.length?''+ps.map(p=>'').join('')+'
PIDSessionProzessRAM
'+p.pid+''+p.session_id+''+esc(p.name)+''+bytes(p.memory_bytes)+'
':'
Keine Session-Prozesse erfasst.
'} function telemetryHTML(a){let rows=Object.values(((a.snapshot||{}).telemetry)||{}).sort((x,y)=>new Date(y.first_seen_at)-new Date(x.first_seen_at)).slice(0,100);return rows.length?''+rows.map(x=>'').join('')+'
SessionBenutzerLogon erkanntRestoreBis Ready
'+x.session_id+''+esc(x.user||x.sid||'–')+''+when(x.logon_at||x.first_seen_at)+''+((x.restore_duration_ms||0)/1000).toFixed(2)+' s'+((x.observed_logon_ms||0)/1000).toFixed(2)+' s
':'
Noch keine Logon-Telemetrie.
'} function renderHistory(rows){rows=(rows||[]).slice().reverse();$('history').innerHTML=rows.length?''+rows.map(x=>'').join('')+'
ZeitServerBenutzerEreignisClientDetails
'+when(x.time)+''+esc(x.hostname)+''+esc(x.user)+''+esc(x.event)+''+esc(x.client_name||'–')+''+esc(x.details||'–')+'
':'
Noch keine Session-Historie.
'} -function renderAlerts(rows){$('alerts').innerHTML=rows.length?''+rows.map(x=>'').join('')+'
StatusServerTypMeldungZuletzt
'+(x.active?'AKTIV':'gelöst')+''+esc(x.hostname||'–')+''+esc(x.type)+''+esc(x.message)+''+when(x.last_seen_at)+'
':'
Keine Alerts.
'} +function renderAlerts(rows){$('alerts').innerHTML=rows.length?''+rows.map(x=>'').join('')+'
StatusServerTypMeldungZuletzt
'+(x.active?'AKTIV':'gelöst')+''+esc(x.hostname||'–')+''+esc(x.type)+''+esc(x.message)+''+when(x.last_seen_at)+'
':'
Keine Alerts.
'} function parseTags(v){let o={};String(v||'').split('\n').map(x=>x.trim()).filter(Boolean).forEach(x=>{let i=x.indexOf('=');if(i>0)o[x.slice(0,i).trim()]=x.slice(i+1).trim()});return o}function agentMatchesFarm(a,f){if(!a||!f)return false;if((f.agent_ids||[]).includes(a.id)||(a.farm_ids||[]).includes(f.id))return true;let required=Object.entries(f.required_tags||{});return required.length>0&&required.every(([k,v])=>String((a.tags||{})[k]??'')===String(v))}function farmMembers(f){return agentCache.filter(a=>agentMatchesFarm(a,f))}function renderFarms(){let root=$('farms');if(!root)return;if(!$('farmEditor'))root.innerHTML='
';let lease=brokerLeases.length?'

Aktive Broker-Leases

'+brokerLeases.map(l=>'').join('')+'
BenutzerAgentFarmResourceGrundBis
'+esc(l.user_key)+''+esc(l.agent_id)+''+esc(l.farm_id||'–')+''+esc(l.resource_id||'–')+''+esc(l.reason||'–')+''+when(l.expires_at)+'
':'';$('farmData').innerHTML=(farmCache.length?''+farmCache.map(f=>'').join('')+'
NameIDAgentenRequired Tags
'+esc(f.name)+''+esc(f.id)+''+farmMembers(f).length+''+esc(Object.entries(f.required_tags||{}).map(([k,v])=>k+'='+v).join(', '))+'
':'
Noch keine Farms.
')+lease} function renderResources(){let root=$('resources');if(!root)return;if(!$('resourceEditor'))root.innerHTML='
';let sel=$('resFarm'),cur=sel?sel.value:'';if(sel&&document.activeElement!==sel){sel.innerHTML=farmCache.map(f=>'').join('');if(cur&&farmCache.some(f=>f.id===cur))sel.value=cur}$('resourceData').innerHTML=resourceCache.length?''+resourceCache.map(x=>'').join('')+'
NameTypFarmGuacamoleRemoteApp
'+esc(x.name)+''+esc(x.kind)+''+esc(x.farm_id)+''+esc(x.guacamole_connection_id||x.guacamole_connection_name||'–')+''+esc(x.remote_app||'–')+'
':'
Noch keine Resources.
'} function eventsHTML(a){let e=((a.snapshot||{}).events||[]).slice().reverse().slice(0,160);return e.length?''+e.map(x=>'').join('')+'
ZeitTypBenutzerMeldung
'+when(x.time)+''+esc(x.level)+''+esc(x.user||'–')+''+esc(x.message)+'
':'
Keine Ereignisse.
'} @@ -36,4 +96,4 @@ async function saveAgentControl(){if(!selected)return;let tags={};($('agentTags' async function killProcess(pid){if(!selected||!confirm('Prozess '+pid+' wirklich beenden?'))return;try{await api('/api/v1/agents/'+encodeURIComponent(selected)+'/processes/'+pid+'/kill',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});toast('Prozess-Beenden an Agent gesendet')}catch(e){toast(e.message)}} async function savePolicy(all){if(!selected)return;try{policyTemplates=collectTemplates();profileFolders=collectProfileFolders();let p={profiles:{enabled:$('profEnabled').checked,store_root:$('storeRoot').value.trim(),backup_on_logoff:$('backupOnLogoff').checked,restore_on_logon:$('restoreOnLogon').checked,backup_delay_seconds:+$('backupDelay').value,retry_seconds:+$('profRetry').value,restore_window_seconds:+$('restoreWindow').value,keep_versions:+$('keepVersions').value,exclude_users:lines('profUsers'),exclude_sids:lines('profSids'),folders:profileFolders},sessions:{control_enabled:$('controlEnabled').checked,disconnected_logoff_enabled:$('autoLogoff').checked,disconnected_timeout_seconds:+$('discTimeout').value,exclude_users:lines('sessUsers'),exclude_sids:lines('sessSids')},cleanup:{enabled:$('enabled').checked,grace_seconds:+$('grace').value,poll_seconds:+$('poll').value,retry_seconds:+$('retry').value,dry_run:$('dry').checked,exclude_users:lines('users'),exclude_sids:lines('sids'),allowed_profile_roots:lines('roots')},templates:policyTemplates};await api(all?'/api/v1/policy/all':'/api/v1/agents/'+encodeURIComponent(selected)+'/policy',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});toast(all?'Policy auf alle Server angewendet':'Policy gespeichert');await selectAgent(selected,true,false)}catch(e){toast(e.message)}} $('detail').addEventListener('click',e=>{let sb=e.target.closest('button[data-session-action]');if(sb){sessionAction(+sb.dataset.session,sb.dataset.sessionAction);return}let b=e.target.closest('button[data-action]');if(!b)return;let a=b.dataset.action;if(a==='save-control')saveAgentControl();if(a==='reload-control'&¤t&&$('controlEditor'))$('controlEditor').innerHTML=controlHTML(current);if(a==='broadcast')bulkSessionAction('message','all');if(a==='logoff-disconnected')bulkSessionAction('logoff','disconnected');if(a==='kill-process')killProcess(+b.dataset.process);if(a==='save-one')savePolicy(false);if(a==='save-all')savePolicy(true);if(a==='reload-policy')selectAgent(selected,false,false);if(a==='add-template'){policyTemplates=collectTemplates();policyTemplates.push(templateDefault());renderTemplates()}if(a==='remove-template'){policyTemplates=collectTemplates();policyTemplates.splice(+b.dataset.index,1);renderTemplates()}if(a==='add-profile-folder'){profileFolders=collectProfileFolders();profileFolders.push({path:'AppData\\Roaming\\Hersteller',exclude_globs:['Cache/**']});renderProfileFolders()}if(a==='remove-profile-folder'){profileFolders=collectProfileFolders();profileFolders.splice(+b.dataset.index,1);renderProfileFolders()}});$('detail').addEventListener('change',e=>{if(e.target.matches('select[data-action="template-kind"]')){policyTemplates=collectTemplates();renderTemplates()}});document.addEventListener('click',async e=>{let b=e.target.closest('button[data-global-action]');if(!b)return;let a=b.dataset.globalAction;try{if(a==='clear-farm-form'){$('farmName').value='';$('farmDesc').value='';$('farmTags').value='';return}if(a==='create-farm'){let name=$('farmName').value.trim();if(!name)return;await api('/api/v1/farms',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,description:$('farmDesc').value.trim(),enabled:true,required_tags:parseTags($('farmTags').value)})});$('farmName').value='';$('farmDesc').value='';$('farmTags').value='';toast('Farm angelegt');await refresh()}if(a==='delete-farm'){if(!confirm('Farm löschen?'))return;await api('/api/v1/farms/'+encodeURIComponent(b.dataset.id),{method:'DELETE'});toast('Farm gelöscht');await refresh()}if(a==='clear-resource-form'){$('resName').value='';$('resKind').value='desktop';$('resConnID').value='';$('resConnName').value='';$('resRemoteApp').value='';$('resRemoteDir').value='';$('resRemoteArgs').value='';return}if(a==='create-resource'){let name=$('resName').value.trim(),farm_id=$('resFarm').value;if(!name||!farm_id)return;await api('/api/v1/resources',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,kind:$('resKind').value,farm_id,guacamole_connection_id:$('resConnID').value.trim(),guacamole_connection_name:$('resConnName').value.trim(),remote_app:$('resRemoteApp').value.trim(),remote_app_dir:$('resRemoteDir').value.trim(),remote_app_args:$('resRemoteArgs').value.trim(),enabled:true})});$('resName').value='';$('resConnID').value='';$('resConnName').value='';$('resRemoteApp').value='';$('resRemoteDir').value='';$('resRemoteArgs').value='';toast('Resource angelegt');await refresh()}if(a==='delete-resource'){if(!confirm('Resource löschen?'))return;await api('/api/v1/resources/'+encodeURIComponent(b.dataset.id),{method:'DELETE'});toast('Resource gelöscht');await refresh()}if(a==='rollback-policy'){if(!confirm('Policy auf Revision '+b.dataset.revision+' zurückrollen?'))return;let target=b.dataset.target,rev=encodeURIComponent(b.dataset.revision),url;if(target==='global')url='/api/v1/policy/global/rollback/'+rev;else if(target.startsWith('agent:'))url='/api/v1/agents/'+encodeURIComponent(target.slice(6))+'/policy/rollback/'+rev;else if(target.startsWith('farm:'))url='/api/v1/farms/'+encodeURIComponent(target.slice(5))+'/policy/rollback/'+rev;else throw new Error('Unbekanntes Policy-Ziel');await api(url,{method:'POST'});toast('Rollback eingeplant');editorAgent=null;await refresh()}}catch(err){toast(err.message)}}); -refresh();setInterval(refresh,5000);` +initShell();refresh();setInterval(refresh,5000);`