WebUI-Update
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.1 — Modal-first responsive Web UI
|
||||
|
||||
- Master-WebUI neu strukturiert: Terminalserver öffnen in einem großen responsiven Arbeitsdialog statt in einer langen Inline-Detailspalte.
|
||||
- Serverdetails sind in Tabs für Übersicht, Sitzungen, Apps/Prozesse, Profile/Ereignisse und Konfiguration gegliedert.
|
||||
- Farms und Published Resources werden als kompakte Karten dargestellt; Anlegen/Bearbeiten erfolgt in eigenen Modals.
|
||||
- Resource-Editor behält RemoteApp-/Agent-Managed-Felder und dynamische Sichtbarkeit vollständig bei.
|
||||
- Agent-WebUI bündelt RemoteApps, Profil-Pipeline und Aktivitätslog in einer tab-basierten Betriebsansicht.
|
||||
- Lokale Agent-Policy wird in einem responsiven Modal bearbeitet und zeigt einen sichtbaren Dirty-State; die Übersichtsseite zeigt nur eine kompakte Policy-Zusammenfassung.
|
||||
- Mobile Modals wechseln auf Vollbild, Tabellen bleiben horizontal scrollbar, Formulare reduzieren sich responsiv auf eine Spalte.
|
||||
- Live-Refresh/Dirty-Guards bleiben erhalten; offene Policy-Eingaben werden weiterhin nicht vom 5-Sekunden-Refresh überschrieben.
|
||||
- Keine neue Runtime oder Build-Chain: weiterhin ausschließlich eingebettetes HTML/CSS/Vanilla-JavaScript.
|
||||
- Agent-Protokoll bleibt Version 4; keine Datenbank- oder API-Migration erforderlich.
|
||||
|
||||
## 0.5.0 — Integrated Guacamole Access Auth
|
||||
|
||||
- SessionGuard Master now provides `/auth/verify` as a Traefik ForwardAuth endpoint for Guacamole.
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
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.5.0 (integrated Guacamole Access Auth / ForwardAuth)**
|
||||
**Current development version: 0.5.1 (modal-first responsive Web UI + integrated Guacamole Access Auth)**
|
||||
|
||||
> 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.5.1 Web UI refresh
|
||||
|
||||
The Master and local Agent consoles now use a modal-first, responsive Vanilla-JavaScript interface. Server drill-down, Farm creation, Resource editing and local Agent policy editing no longer occupy permanent large inline forms. The Master server workspace uses tabs, Farms/Resources use compact cards, and the Agent groups operational data into a tabbed workspace. No Node.js/npm/frontend build step is required.
|
||||
|
||||
## 0.5.0 Integrated Guacamole Access Auth
|
||||
|
||||
- `sessionguard-master` can now act directly as Traefik ForwardAuth for Guacamole; no separate forward-auth container is required.
|
||||
|
||||
@@ -35,22 +35,52 @@ button{display:inline-flex;align-items:center;justify-content:center;gap:6px;bac
|
||||
@media(max-width:900px){:root{--sidebar:220px}.page{padding:18px}.topbar{padding:0 18px}.metrics{grid-template-columns:repeat(3,1fr)}.cols,.two{grid-template-columns:1fr}}
|
||||
@media(max-width:720px){.agent-metrics{grid-template-columns:repeat(2,1fr)}.app-shell{display:block}.sidebar{position:fixed;left:0;top:0;transform:translateX(-102%);width:min(290px,86vw);transition:transform .2s ease;box-shadow:var(--shadow)}body.nav-open .sidebar{transform:translateX(0)}body.nav-open .mobile-overlay{display:block;position:fixed;inset:0;background:rgba(0,0,0,.48);z-index:25}.menu-toggle{display:inline-flex}.topbar{height:60px}.live-pill{display:none}.page{padding:14px}.metrics{grid-template-columns:repeat(2,1fr)}.section-heading{align-items:flex-start;flex-direction:column}.table th,.table td{white-space:nowrap}.form{padding:13px}}
|
||||
@media(max-width:430px){.metrics{grid-template-columns:1fr 1fr}.metric-card,.card{min-height:88px;padding:12px}.value{font-size:22px}.topbar-actions .theme-top{display:none}}
|
||||
|
||||
/* SessionGuard UI v0.5.1 — modal-first responsive console */
|
||||
:root{--radius-lg:22px;--radius-xl:28px;--focus:0 0 0 4px var(--primary-soft)}
|
||||
body{background:radial-gradient(circle at 88% -10%,color-mix(in srgb,var(--primary) 13%,transparent),transparent 34rem),var(--bg)}
|
||||
.page{max-width:1680px}.topbar{box-shadow:0 1px 0 rgba(0,0,0,.03)}
|
||||
.sidebar{background:linear-gradient(180deg,color-mix(in srgb,var(--surface) 92%,var(--primary) 8%),var(--bg-soft))}
|
||||
.logo{border-radius:15px}.nav-link{border:1px solid transparent}.nav-link.active{border-color:color-mix(in srgb,var(--primary) 22%,transparent)}
|
||||
.section-heading{align-items:center}.section-heading .section-actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.metrics{grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:12px}.metric-card{min-height:106px;border-radius:18px}.metric-card:hover{border-color:var(--border-strong);transform:translateY(-1px);transition:.18s}
|
||||
.panel{border-radius:18px;box-shadow:0 8px 34px rgba(0,0,0,.05)}.panel-head{padding:15px 18px}.panel-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||
.table-wrap{width:100%;overflow:auto}.table td{max-width:420px}.table tbody tr{cursor:default}.table tbody tr[data-id]{cursor:pointer}
|
||||
.actions{display:flex;align-items:center;gap:7px;flex-wrap:wrap}.actions.end{justify-content:flex-end}.toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||
button{min-height:36px;padding:8px 13px;border-radius:11px}button.icon-btn{width:38px;min-width:38px;padding:0;font-size:18px}button.primary-soft{background:var(--primary-soft);border-color:color-mix(in srgb,var(--primary) 28%,var(--border));color:var(--primary)}
|
||||
.section-title{font-size:12px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:850;margin:18px 0 9px}.note{border:1px solid var(--border);background:var(--surface-2);padding:11px 13px;border-radius:12px;color:var(--muted);font-size:11px}
|
||||
.list{display:grid;gap:10px}.item{border:1px solid var(--border);background:var(--surface-2);border-radius:14px;padding:13px}.item-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px}
|
||||
/* modal */
|
||||
dialog.sg-modal{width:min(1120px,calc(100vw - 32px));max-height:calc(100dvh - 32px);padding:0;border:1px solid var(--border);border-radius:var(--radius-xl);background:var(--surface);color:var(--text);box-shadow:0 32px 100px rgba(0,0,0,.42);overflow:hidden}
|
||||
dialog.sg-modal::backdrop{background:rgba(4,9,18,.68);backdrop-filter:blur(6px)}dialog.sg-modal.wide{width:min(1480px,calc(100vw - 24px))}dialog.sg-modal.compact{width:min(620px,calc(100vw - 32px))}
|
||||
.modal-shell{display:grid;grid-template-rows:auto minmax(0,1fr);max-height:calc(100dvh - 32px)}.modal-head{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:17px 20px;border-bottom:1px solid var(--border);background:linear-gradient(180deg,var(--surface-2),var(--surface))}.modal-title{font-size:18px;font-weight:850;letter-spacing:-.02em}.modal-sub{font-size:11px;color:var(--muted);margin-top:2px}.modal-body{overflow:auto;min-height:0}.modal-body.pad{padding:18px}.modal-close{background:transparent;color:var(--muted);border:1px solid var(--border)}
|
||||
body.modal-open{overflow:hidden}
|
||||
/* detail tabs */
|
||||
.detail-tabs{position:sticky;top:0;z-index:4;display:flex;gap:5px;overflow:auto;padding:10px 14px;border-bottom:1px solid var(--border);background:color-mix(in srgb,var(--surface) 94%,transparent);backdrop-filter:blur(12px)}.detail-tab{white-space:nowrap;background:transparent;color:var(--muted);border:1px solid transparent}.detail-tab.active{background:var(--primary-soft);border-color:color-mix(in srgb,var(--primary) 25%,var(--border));color:var(--text)}.detail-pane{display:none;padding:16px}.detail-pane.active{display:block}.detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.detail-block{border:1px solid var(--border);border-radius:16px;background:var(--surface-2);overflow:hidden}.detail-block>.section-title{margin:0;padding:13px 15px;border-bottom:1px solid var(--border);background:var(--surface)}.detail-block>.content{max-height:460px}.detail-block.full{grid-column:1/-1}
|
||||
/* cards / compact lists */
|
||||
.entity-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;padding:15px}.entity-card{position:relative;border:1px solid var(--border);border-radius:16px;background:var(--surface-2);padding:14px;display:grid;gap:11px}.entity-card:hover{border-color:var(--border-strong)}.entity-card-head{display:flex;justify-content:space-between;gap:12px;align-items:flex-start}.entity-title{font-weight:850;font-size:14px}.entity-meta{display:flex;gap:6px;flex-wrap:wrap}.entity-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.mini-stat{padding:9px;border-radius:11px;background:var(--surface);border:1px solid var(--border)}.mini-stat b{display:block;font-size:16px}.mini-stat span{font-size:10px;color:var(--muted)}
|
||||
.hero-status{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(260px,.6fr);gap:14px}.hero-card{padding:20px;border-radius:20px;background:linear-gradient(135deg,var(--surface),color-mix(in srgb,var(--surface-2) 72%,var(--primary) 28%));border:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:18px}.hero-card h2{margin:0;font-size:22px}.hero-meta{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
|
||||
.form-section{border:1px solid var(--border);border-radius:15px;background:var(--surface-2);padding:14px}.form-section>h3{font-size:12px;margin:0 0 12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted)}
|
||||
.form{padding:18px}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}.form-grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}
|
||||
.dirty-pill{display:inline-flex;align-items:center;gap:6px;color:var(--warning);background:var(--warning-soft);padding:5px 8px;border-radius:999px;font-size:10px;font-weight:800}
|
||||
.mobile-only{display:none}
|
||||
@media(max-width:1050px){.detail-grid{grid-template-columns:1fr}.hero-status{grid-template-columns:1fr}.form-grid.three{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:720px){dialog.sg-modal,dialog.sg-modal.wide,dialog.sg-modal.compact{width:100vw;max-width:none;height:100dvh;max-height:100dvh;border-radius:0;border:0}.modal-shell{max-height:100dvh;height:100dvh}.modal-head{padding:13px 14px}.modal-title{font-size:16px}.modal-body.pad,.detail-pane{padding:12px}.detail-tabs{padding:8px}.form{padding:12px}.form-grid,.form-grid.three,.entity-stats{grid-template-columns:1fr}.entity-grid{grid-template-columns:1fr;padding:10px}.mobile-only{display:inline-flex}.table th,.table td{white-space:nowrap}.section-heading .section-actions{width:100%}.section-heading .section-actions button{flex:1}}
|
||||
|
||||
</style></head><body>
|
||||
<div class="mobile-overlay" id="mobileOverlay"></div><div class="app-shell"><aside class="sidebar" id="sidebar"><div class="brand-block"><div class="logo">SG</div><div><div class="brand-name">SessionGuard</div><div class="brand-sub">Local Agent · v0.5.0</div></div></div><nav class="nav-group"><div class="nav-label">Server</div><a class="nav-link active" href="#overview"><span class="nav-icon">⌂</span>Übersicht</a><a class="nav-link" href="#sessions-section"><span class="nav-icon">▶</span>Sitzungen</a><a class="nav-link" href="#remoteapps-section"><span class="nav-icon">◇</span>RemoteApps</a><a class="nav-link" href="#profiles-section"><span class="nav-icon">↕</span>Profil-Pipeline</a><a class="nav-link" href="#events-section"><span class="nav-icon">≋</span>Aktivitätslog</a><a class="nav-link" href="#policy-section"><span class="nav-icon">⚙</span>Lokale Policy</a></nav><div class="sidebar-spacer"></div><div class="sidebar-footer"><button class="secondary theme-toggle" id="themeToggle" type="button">◐ Theme wechseln</button><form action="/logout" method="post"><button class="ghost" style="width:100%">Abmelden</button></form></div></aside>
|
||||
<div class="mobile-overlay" id="mobileOverlay"></div><div class="app-shell"><aside class="sidebar" id="sidebar"><div class="brand-block"><div class="logo">SG</div><div><div class="brand-name">SessionGuard</div><div class="brand-sub">Local Agent · v0.5.1</div></div></div><nav class="nav-group"><div class="nav-label">Server</div><a class="nav-link active" href="#overview"><span class="nav-icon">⌂</span>Übersicht</a><a class="nav-link" href="#sessions-section"><span class="nav-icon">▶</span>Sitzungen</a><a class="nav-link" href="#operations-section"><span class="nav-icon">◇</span>Betriebsdaten</a><a class="nav-link" href="#policy-section"><span class="nav-icon">⚙</span>Lokale Policy</a></nav><div class="sidebar-spacer"></div><div class="sidebar-footer"><button class="secondary theme-toggle" id="themeToggle" type="button">◐ Theme wechseln</button><form action="/logout" method="post"><button class="ghost" style="width:100%">Abmelden</button></form></div></aside>
|
||||
<div class="app-main"><header class="topbar"><div class="topbar-left"><button class="secondary menu-toggle" id="menuToggle" type="button">☰</button><div><div class="topbar-title">Lokaler Agent</div><div class="topbar-sub" id="host">Lokaler Terminalserver</div></div></div><div class="topbar-actions"><span class="live-pill"><span class="live-dot"></span>Live · 5s</span><button class="secondary theme-top" id="themeTop" type="button">◐</button></div></header>
|
||||
<main class="page"><section class="page-section" id="overview"><div class="section-heading"><div><div class="eyebrow">Local Control</div><h1>Terminalserver-Status</h1><div class="section-copy">Sitzungen, Profil-Pipeline und Master-Verbindung lokal überwachen.</div></div></div><div class="metrics agent-metrics"><div class="metric-card"><div class="metric-head"><span class="metric-label">Aktiv</span><span class="metric-icon">▶</span></div><div class="value" id="active">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Getrennt</span><span class="metric-icon">Ⅱ</span></div><div class="value" id="disc">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Sitzungen</span><span class="metric-icon">◎</span></div><div class="value" id="total">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Profil-Jobs</span><span class="metric-icon">↕</span></div><div class="value" id="profileJobs">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Cleanup</span><span class="metric-icon">⌫</span></div><div class="value" id="pending">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">RemoteApps</span><span class="metric-icon">◇</span></div><div class="value" id="remoteAppCount">–</div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Master</span><span class="metric-icon">⇄</span></div><div class="value" style="font-size:14px;margin-top:14px" id="master">–</div></div></div></section>
|
||||
<section class="page-section" id="sessions-section"><div class="section-heading"><div><div class="eyebrow">RDS</div><h2>Sitzungen</h2><div class="section-copy">Aktive und getrennte Benutzer sowie administrative Aktionen.</div></div></div><section class="panel"><div id="sessions"></div></section></section>
|
||||
<section class="page-section" id="remoteapps-section"><div class="section-heading"><div><div class="eyebrow">Delivery</div><h2>RemoteApps</h2><div class="section-copy">Vom Windows RemoteApp-Provider entdeckte Anwendungen und der vom Master verwaltete Sollzustand.</div></div></div><section class="panel"><div id="remoteapps"></div></section></section>
|
||||
<section class="page-section" id="profiles-section"><div class="section-heading"><div><div class="eyebrow">Profile</div><h2>Profil-Pipeline</h2><div class="section-copy">Backup, Restore, Retry und Cleanup-Status pro Benutzer.</div></div></div><section class="panel"><div id="profiles"></div></section></section>
|
||||
<section class="page-section" id="events-section"><div class="section-heading"><div><div class="eyebrow">Telemetry</div><h2>Aktivitäts- & Audit-Log</h2><div class="section-copy">Dry-Run, Profilaktionen, Fehler und lokale Ereignisse.</div></div></div><section class="panel"><div id="events"></div></section></section>
|
||||
<section class="page-section" id="policy-section"><div class="section-heading"><div><div class="eyebrow">Configuration</div><h2>Lokale Policy</h2><div class="section-copy">Fallback-Konfiguration für Profile, Sessions, Cleanup und Templates.</div></div></div><section class="panel"><div id="policy"></div></section></section>
|
||||
</main></div></div><script src="/app.js?v=0.5.0"></script></body></html>`
|
||||
<section class="page-section" id="operations-section"><div class="section-heading"><div><div class="eyebrow">Operations</div><h2>Betriebsdaten</h2><div class="section-copy">RemoteApps, Profil-Pipeline und lokale Ereignisse kompakt in einer Arbeitsfläche.</div></div></div><section class="panel"><div class="detail-tabs" style="position:static"><button class="detail-tab active" type="button" data-agent-tab="remoteapps">RemoteApps</button><button class="detail-tab" type="button" data-agent-tab="profiles">Profil-Pipeline</button><button class="detail-tab" type="button" data-agent-tab="events">Aktivitätslog</button></div><div class="detail-pane active" data-agent-pane="remoteapps"><div id="remoteapps"></div></div><div class="detail-pane" data-agent-pane="profiles"><div id="profiles"></div></div><div class="detail-pane" data-agent-pane="events"><div id="events"></div></div></section></section>
|
||||
<section class="page-section" id="policy-section"><div class="section-heading"><div><div class="eyebrow">Configuration</div><h2>Lokale Policy</h2><div class="section-copy">Fallback-Konfiguration für Profile, Sessions, Cleanup und Templates.</div></div><div class="section-actions"><button type="button" class="primary-soft" id="openPolicyModal">⚙ Policy bearbeiten</button></div></div><section class="panel"><div class="form"><div class="note"><strong>Fallback-Verhalten:</strong> Die lokale Policy bleibt als Notfallkonfiguration verfügbar. Eine aktive Master-Soll-Policy hat im zentral verwalteten Betrieb Vorrang.</div><div id="policySummary" class="entity-grid" style="padding:0"></div></div></section></section>
|
||||
</main></div></div><dialog class="sg-modal wide" id="policyModal"><div class="modal-shell"><div class="modal-head"><div><div class="modal-title">Lokale Fallback-Policy</div><div class="modal-sub">Änderungen werden erst nach „Lokal speichern“ übernommen.</div></div><div class="actions"><span id="policyDirtyPill"></span><button type="button" class="modal-close icon-btn" id="closePolicyModal">×</button></div></div><div class="modal-body"><div id="policy"></div></div></div></dialog><script src="/app.js?v=0.5.1"></script></body></html>`
|
||||
|
||||
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(){
|
||||
function openModal(id){let d=$(id);if(!d)return;document.body.classList.add('modal-open');if(!d.open)d.showModal()}function closeModal(id){let d=$(id);if(!d)return;if(d.open)d.close();document.body.classList.remove('modal-open')}function renderPolicySummary(p){let root=$('policySummary');if(!root)return;let pr=p.profiles||{},sp=p.sessions||{},c=p.cleanup||{};root.innerHTML='<article class="entity-card"><div class="entity-title">Profil-Sync</div><div class="entity-meta"><span class="badge '+(pr.enabled?'good':'neutral')+'">'+(pr.enabled?'Aktiv':'Aus')+'</span><span class="badge neutral">'+((pr.folders||[]).length)+' Ordner</span></div><div class="muted">'+esc(pr.store_root||'Kein Store konfiguriert')+'</div></article><article class="entity-card"><div class="entity-title">Sitzungsrichtlinie</div><div class="entity-meta"><span class="badge '+(sp.control_enabled?'good':'neutral')+'">Steuerung '+(sp.control_enabled?'an':'aus')+'</span><span class="badge '+(sp.disconnected_logoff_enabled?'warning':'neutral')+'">Auto-Logoff '+(sp.disconnected_logoff_enabled?'an':'aus')+'</span></div><div class="muted">Timeout: '+Math.round((sp.disconnected_timeout_seconds||0)/60)+' min</div></article><article class="entity-card"><div class="entity-title">Cleanup</div><div class="entity-meta"><span class="badge '+(c.enabled?'good':'neutral')+'">'+(c.enabled?'Aktiv':'Aus')+'</span><span class="badge '+(c.dry_run?'warning':'neutral')+'">'+(c.dry_run?'Dry-Run':'Live')+'</span></div><div class="muted">Grace: '+(c.grace_seconds||0)+' s</div></article>'}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'?'☾':'☀'}
|
||||
@@ -58,7 +88,7 @@ function initShell(){
|
||||
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)})}
|
||||
document.querySelectorAll('[data-agent-tab]').forEach(b=>b.onclick=()=>{let n=b.dataset.agentTab;document.querySelectorAll('[data-agent-tab]').forEach(x=>x.classList.toggle('active',x===b));document.querySelectorAll('[data-agent-pane]').forEach(x=>x.classList.toggle('active',x.dataset.agentPane===n))});let op=$('openPolicyModal'),cp=$('closePolicyModal'),pm=$('policyModal');if(op)op.onclick=()=>openModal('policyModal');if(cp)cp.onclick=()=>{if(policyDirty&&!confirm('Ungespeicherte Änderungen verwerfen und schließen?'))return;closeModal('policyModal')};if(pm)pm.addEventListener('cancel',e=>{if(policyDirty&&!confirm('Ungespeicherte Änderungen verwerfen und schließen?')){e.preventDefault();return}document.body.classList.remove('modal-open')});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?'<div class="log"><table class="table"><thead><tr><th>Zeit</th><th>Typ</th><th>Benutzer</th><th>Meldung</th></tr></thead><tbody>'+rows.map(x=>'<tr class="event-'+esc(x.level||'info')+'"><td>'+esc(when(x.time))+'</td><td>'+esc(x.level||'info')+'</td><td>'+esc(x.user||'–')+'</td><td>'+esc(x.message||'')+'</td></tr>').join('')+'</tbody></table></div>':'<div class="empty">Noch keine Ereignisse.</div>'}
|
||||
@@ -73,7 +103,7 @@ function collectTemplates(){return[...document.querySelectorAll('.template-card'
|
||||
function renderProfileFolders(){let h=$('profileFolderList');if(!h)return;h.innerHTML=profileFolders.length?profileFolders.map((f,i)=>'<div class="item profile-folder"><div class="item-head"><strong>'+esc(f.path||('Ordner '+(i+1)))+'</strong><button type="button" class="danger" data-action="remove-profile-folder" data-index="'+i+'">Löschen</button></div><div class="two"><label>Pfad relativ zum Profil<input data-field="path" value="'+esc(f.path||'')+'" placeholder="AppData\\Roaming\\Hersteller"></label><label>Ausschlüsse (eine Zeile; * oder Pfad/**)<textarea data-field="exclude">'+esc((f.exclude_globs||[]).join('\n'))+'</textarea></label></div></div>').join(''):'<div class="empty">Keine Profilordner konfiguriert.</div>'}
|
||||
function collectProfileFolders(){return[...document.querySelectorAll('.profile-folder')].map(c=>({path:c.querySelector('[data-field="path"]').value.trim(),exclude_globs:c.querySelector('[data-field="exclude"]').value.split('\n').map(x=>x.trim()).filter(Boolean)})).filter(x=>x.path)}
|
||||
function policyHTML(p){let c=p.cleanup||{},pr=p.profiles||{},sp=p.sessions||{};return'<div class="form"><div class="section-title">Profil-Sicherung & Wiederherstellung</div><div class="check"><input id="profEnabled" type="checkbox" '+(pr.enabled?'checked':'')+'><label>Profil-Synchronisation aktiv</label></div><div class="two"><label>Profil-Store (UNC oder lokaler Pfad)<input id="storeRoot" value="'+esc(pr.store_root||'')+'" placeholder="\\\\fileserver\\SessionGuardProfiles"></label><label>Backup-Verzögerung (s)<input id="backupDelay" type="number" min="1" value="'+(pr.backup_delay_seconds||5)+'"></label><label>Retry (s)<input id="profRetry" type="number" min="1" value="'+(pr.retry_seconds||60)+'"></label><label>Restore-Fenster (s)<input id="restoreWindow" type="number" min="10" value="'+(pr.restore_window_seconds||120)+'"></label><label>Historische Versionen<input id="keepVersions" type="number" min="0" max="50" value="'+(pr.keep_versions??2)+'"></label></div><div class="check"><input id="backupOnLogoff" type="checkbox" '+(pr.backup_on_logoff?'checked':'')+'><label>Nach Logoff sichern</label></div><div class="check"><input id="restoreOnLogon" type="checkbox" '+(pr.restore_on_logon?'checked':'')+'><label>Bei Logon wiederherstellen</label></div><label>Von Profil-Sync ausgeschlossene Benutzer<textarea id="profUsers">'+esc((pr.exclude_users||[]).join('\n'))+'</textarea></label><label>Von Profil-Sync ausgeschlossene SIDs<textarea id="profSids">'+esc((pr.exclude_sids||[]).join('\n'))+'</textarea></label><div class="toolbar"><strong>Zu sichernde Profilordner</strong><button type="button" class="secondary" data-action="add-profile-folder">Ordner hinzufügen</button></div><div id="profileFolderList" class="list"></div><div class="note">Gesichert werden nur konfigurierte Teilbäume. Reparse-Points/Symlinks werden nicht verfolgt. Cleanup beginnt nach einem Logoff erst nach erfolgreichem Backup.</div><div class="section"><div class="section-title">Sitzungsrichtlinie</div><div class="check"><input id="controlEnabled" type="checkbox" '+(sp.control_enabled?'checked':'')+'><label>Administrative Sitzungssteuerung erlauben</label></div><div class="check"><input id="autoLogoff" type="checkbox" '+(sp.disconnected_logoff_enabled?'checked':'')+'><label>Getrennte Sitzungen automatisch abmelden</label></div><label>Timeout für getrennte Sitzung (s; min. 60)<input id="discTimeout" type="number" min="60" value="'+(sp.disconnected_timeout_seconds||3600)+'"></label><label>Vom Auto-Logoff ausgeschlossene Benutzer<textarea id="sessUsers">'+esc((sp.exclude_users||[]).join('\n'))+'</textarea></label><label>Vom Auto-Logoff ausgeschlossene SIDs<textarea id="sessSids">'+esc((sp.exclude_sids||[]).join('\n'))+'</textarea></label></div><div class="section"><div class="section-title">Profil-Cleanup</div><div class="cols"><label>Grace (s)<input id="grace" type="number" min="1" value="'+(c.grace_seconds||600)+'"></label><label>Polling (s)<input id="poll" type="number" min="2" value="'+(c.poll_seconds||10)+'"></label><label>Retry (s)<input id="retry" type="number" min="1" value="'+(c.retry_seconds||60)+'"></label></div><div class="check"><input id="enabled" type="checkbox" '+(c.enabled?'checked':'')+'><label>Cleanup aktiv</label></div><div class="check"><input id="dry" type="checkbox" '+(c.dry_run?'checked':'')+'><label>Cleanup Dry-Run</label></div><label>Cleanup: Benutzer ausschließen<textarea id="users">'+esc((c.exclude_users||[]).join('\n'))+'</textarea></label><label>Cleanup: SIDs ausschließen<textarea id="sids">'+esc((c.exclude_sids||[]).join('\n'))+'</textarea></label><label>Erlaubte Profil-Roots<textarea id="roots">'+esc((c.allowed_profile_roots||[]).join('\n'))+'</textarea></label></div><div class="section"><div class="toolbar"><strong>Templates</strong><button type="button" class="secondary" data-action="add-template">Template hinzufügen</button></div><div id="templateList" class="list"></div></div><div class="actions"><button type="button" data-action="save-policy">Lokal speichern</button><button type="button" class="secondary" data-action="reload-policy">Editor neu laden</button><span id="policyDirtyState" class="warning"></span></div></div>'}
|
||||
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)}}
|
||||
function updatePolicyDirty(){let e=$('policyDirtyState');if(e)e.textContent=policyDirty?'Ungespeicherte Änderungen':'';let p=$('policyDirtyPill');if(p)p.innerHTML=policyDirty?'<span class="dirty-pill">● Ungespeichert</span>':''}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();renderPolicySummary(p);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);closeModal('policyModal');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()}});initShell();refresh();loadPolicy(true);setInterval(refresh,5000);`
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user