Files
jbergner 7c69432097
Some checks failed
release-tag / release-image (push) Successful in 2m3s
release-main / release-images (push) Failing after 3m9s
RC-2 0.5.2
2026-08-24 05:46:42 +02:00

140 lines
72 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package master
const masterHTML = `<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark light"><title>SessionGuard Master</title><style>
:root{
color-scheme:dark;
--bg:#07101f;--bg-soft:#0b1527;--surface:#101c30;--surface-2:#15243b;--surface-3:#1a2b45;
--border:#263a58;--border-strong:#365174;--text:#f1f5fb;--muted:#91a3bd;--subtle:#677b99;
--primary:#6d8dff;--primary-strong:#5276ff;--primary-soft:rgba(109,141,255,.14);
--success:#43d17b;--success-soft:rgba(67,209,123,.12);--warning:#f2be55;--warning-soft:rgba(242,190,85,.13);
--danger:#ff6b78;--danger-soft:rgba(255,107,120,.12);--info:#56c7ff;--shadow:0 18px 45px rgba(0,0,0,.24);
--radius:16px;--radius-sm:10px;--sidebar:250px;
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
:root[data-theme="light"]{
color-scheme:light;
--bg:#f3f6fb;--bg-soft:#eaf0f8;--surface:#fff;--surface-2:#f8faff;--surface-3:#eef3fa;
--border:#dce5f0;--border-strong:#c6d4e5;--text:#172235;--muted:#64748b;--subtle:#8795aa;
--primary:#4f6ff2;--primary-strong:#3d5fe8;--primary-soft:rgba(79,111,242,.10);
--success:#169a52;--success-soft:rgba(22,154,82,.09);--warning:#b87807;--warning-soft:rgba(184,120,7,.10);
--danger:#d94758;--danger-soft:rgba(217,71,88,.09);--info:#138fbf;--shadow:0 16px 40px rgba(31,50,81,.10);
}
*{box-sizing:border-box}html{scroll-behavior:smooth;scroll-padding-top:84px}body{margin:0;background:var(--bg);color:var(--text);min-height:100vh;font-size:14px;line-height:1.45}
button,input,textarea,select{font:inherit}button{border:0}.app-shell{min-height:100vh;display:grid;grid-template-columns:var(--sidebar) minmax(0,1fr)}
.sidebar{position:sticky;top:0;height:100vh;padding:20px 14px;display:flex;flex-direction:column;gap:18px;background:linear-gradient(180deg,var(--surface),var(--bg-soft));border-right:1px solid var(--border);z-index:30}
.brand-block{display:flex;align-items:center;gap:11px;padding:3px 8px 12px}.logo{width:42px;height:42px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,var(--primary),#8a67ff);box-shadow:0 8px 24px rgba(83,107,255,.28);font-weight:900;color:white;letter-spacing:-.04em}.brand-name{font-size:16px;font-weight:800;letter-spacing:-.01em}.brand-sub{font-size:11px;color:var(--muted);margin-top:2px}.nav-group{display:grid;gap:4px}.nav-label{padding:8px 11px 5px;color:var(--subtle);font-size:10px;text-transform:uppercase;letter-spacing:.12em;font-weight:800}.nav-link{display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:10px;color:var(--muted);text-decoration:none;font-weight:650;transition:.16s ease}.nav-link:hover,.nav-link.active{background:var(--primary-soft);color:var(--text)}.nav-icon{width:20px;text-align:center;color:var(--primary);font-weight:900}.sidebar-spacer{flex:1}.sidebar-footer{display:grid;gap:8px;border-top:1px solid var(--border);padding-top:14px}.app-main{min-width:0}.topbar{height:68px;position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:0 28px;background:color-mix(in srgb,var(--bg) 84%,transparent);backdrop-filter:blur(18px);border-bottom:1px solid color-mix(in srgb,var(--border) 78%,transparent)}
.topbar-left{display:flex;align-items:center;gap:12px;min-width:0}.menu-toggle{display:none}.topbar-title{font-size:15px;font-weight:780}.topbar-sub{font-size:11px;color:var(--muted)}.topbar-actions{display:flex;align-items:center;gap:8px}.live-pill{display:inline-flex;align-items:center;gap:7px;padding:6px 10px;border:1px solid var(--border);border-radius:999px;color:var(--muted);font-size:11px}.live-dot{width:7px;height:7px;border-radius:50%;background:var(--success);box-shadow:0 0 0 4px var(--success-soft)}
.page{max-width:1740px;margin:0 auto;padding:24px 28px 52px}.page-section{margin-bottom:22px}.section-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin:0 0 12px}.section-heading h1,.section-heading h2{margin:0;font-size:20px;letter-spacing:-.025em}.section-heading h1{font-size:25px}.section-copy{margin-top:4px;color:var(--muted);font-size:12px}.eyebrow{font-size:10px;text-transform:uppercase;letter-spacing:.13em;color:var(--primary);font-weight:850;margin-bottom:5px}
.metrics{display:grid;grid-template-columns:repeat(8,minmax(125px,1fr));gap:10px}.card,.metric-card,.panel{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 1px 0 rgba(255,255,255,.02)}.metric-card,.card{padding:14px 15px;min-height:98px;position:relative;overflow:hidden}.metric-card:after,.card:after{content:"";position:absolute;right:-22px;top:-28px;width:82px;height:82px;border-radius:50%;background:var(--primary-soft)}.metric-head{display:flex;justify-content:space-between;gap:8px;align-items:center}.metric-label,.card .muted:first-child{color:var(--muted);font-size:11px;font-weight:700}.metric-icon{width:28px;height:28px;border-radius:9px;display:grid;place-items:center;background:var(--primary-soft);color:var(--primary);font-size:13px;font-weight:900}.value{font-size:25px;font-weight:850;letter-spacing:-.035em;margin-top:9px;position:relative;z-index:1}.workspace-grid,.split{display:grid;grid-template-columns:minmax(380px,.82fr) minmax(600px,1.45fr);gap:14px}.panel{overflow:hidden;margin-top:0;min-width:0}.panel-head,.panel>h2{min-height:57px;margin:0;padding:13px 16px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:14px;font-weight:800;background:linear-gradient(180deg,color-mix(in srgb,var(--surface-2) 66%,transparent),transparent)}.panel-head h2{font-size:14px;margin:0}.panel-kicker{font-size:10px;color:var(--muted);font-weight:500;margin-top:2px}.panel-body{min-width:0}.content{max-height:400px;overflow:auto}.empty{padding:32px 20px;color:var(--muted);text-align:center}.empty:before{content:"—";display:block;color:var(--border-strong);font-size:24px;margin-bottom:5px}
.table{width:100%;border-collapse:separate;border-spacing:0}.table th,.table td{padding:11px 12px;border-bottom:1px solid var(--border);text-align:left;font-size:12px;vertical-align:middle}.table th{position:sticky;top:0;background:var(--surface-2);color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.055em;font-weight:800;z-index:2}.table tr:last-child td{border-bottom:0}.table tbody tr{transition:background .14s ease}.table tbody tr:hover{background:color-mix(in srgb,var(--primary-soft) 65%,transparent)}.row{cursor:pointer}.row.selected{background:var(--primary-soft)!important}.muted{color:var(--muted)}code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;background:var(--surface-3);border:1px solid var(--border);padding:2px 5px;border-radius:6px;color:var(--text)}
.status-badge,.badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;font-size:10px;font-weight:800;border:1px solid var(--border);white-space:nowrap}.status-online,.badge.good{color:var(--success);background:var(--success-soft);border-color:color-mix(in srgb,var(--success) 30%,var(--border))}.status-offline,.badge.neutral{color:var(--muted);background:var(--surface-3)}.status-warning,.badge.warning{color:var(--warning);background:var(--warning-soft);border-color:color-mix(in srgb,var(--warning) 30%,var(--border))}.status-danger,.badge.bad{color:var(--danger);background:var(--danger-soft);border-color:color-mix(in srgb,var(--danger) 30%,var(--border))}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--success);box-shadow:0 0 0 3px var(--success-soft)}.dot.off{background:var(--subtle);box-shadow:none}.good{color:var(--success)}.bad,.event-error{color:var(--danger)}.warning,.event-warning,.event-dry-run{color:var(--warning)}
.form{padding:16px;display:grid;gap:14px}.cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:11px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:11px}label{display:grid;gap:6px;color:var(--muted);font-size:11px;font-weight:700}input,textarea,select{width:100%;color:var(--text);background:var(--surface-2);border:1px solid var(--border);border-radius:10px;padding:9px 10px;outline:0;transition:border-color .15s,box-shadow .15s,background .15s}input:hover,textarea:hover,select:hover{border-color:var(--border-strong)}input:focus,textarea:focus,select:focus{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft);background:var(--surface)}textarea{min-height:90px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.check{display:flex;align-items:center;gap:9px;min-height:30px}.check input{appearance:none;width:36px;height:20px;min-width:36px;padding:0;border-radius:999px;position:relative;background:var(--surface-3);cursor:pointer}.check input:before{content:"";position:absolute;left:3px;top:3px;width:12px;height:12px;border-radius:50%;background:var(--muted);transition:.16s}.check input:checked{background:var(--primary);border-color:var(--primary)}.check input:checked:before{left:19px;background:white}.check label{display:block;color:var(--text);font-weight:650;font-size:12px}
button{display:inline-flex;align-items:center;justify-content:center;gap:6px;background:var(--primary);color:#fff;border:1px solid transparent;border-radius:10px;padding:8px 11px;font-weight:750;font-size:11px;cursor:pointer;transition:transform .12s,filter .12s,background .12s,border-color .12s}button:hover{filter:brightness(1.08)}button:active{transform:translateY(1px)}button.secondary{background:var(--surface-3);border-color:var(--border);color:var(--text)}button.ghost{background:transparent;border-color:var(--border);color:var(--muted)}button.danger{background:var(--danger-soft);border-color:color-mix(in srgb,var(--danger) 25%,var(--border));color:var(--danger)}button:disabled{opacity:.45;cursor:not-allowed}.actions{display:flex;gap:6px;flex-wrap:wrap}.toolbar{display:flex;justify-content:space-between;align-items:center;gap:10px}.section-title{font-weight:800;padding:10px 0 6px;margin-top:3px;border:0;color:var(--text);font-size:12px}.section{border-top:1px solid var(--border);padding-top:13px;margin-top:4px}.list{display:grid;gap:9px}.item{border:1px solid var(--border);background:var(--surface-2);border-radius:12px;padding:12px;display:grid;gap:10px}.item-head{display:flex;justify-content:space-between;align-items:center;gap:10px}.note{padding:10px 12px;border:1px solid var(--border);border-left:3px solid var(--primary);background:var(--primary-soft);border-radius:9px;color:var(--muted);font-size:11px}.log{max-height:420px;overflow:auto}.toast{position:fixed;right:22px;bottom:22px;max-width:min(420px,calc(100vw - 44px));background:var(--surface);border:1px solid var(--border-strong);box-shadow:var(--shadow);padding:11px 14px;border-radius:11px;display:none;z-index:100;font-size:12px}.theme-toggle{width:100%}.mobile-overlay{display:none}
#agents,#farmData,#resourceData,#sessions,#profiles,#events,#history,#policyHistory,#alerts,#audit,#sessionList,#profileList,#processList,#telemetryList,#eventList{overflow:auto}
@media(max-width:1350px){.metrics{grid-template-columns:repeat(4,1fr)}.workspace-grid,.split{grid-template-columns:1fr}.cols{grid-template-columns:repeat(2,1fr)}}
@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){.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.2 — 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">Control Plane · v0.5.2</div></div></div>
<nav class="nav-group"><div class="nav-label">Übersicht</div><a class="nav-link active" href="#overview"><span class="nav-icon">⌂</span>Dashboard</a><a class="nav-link" href="#servers"><span class="nav-icon">▣</span>Terminalserver</a><a class="nav-link" href="#farms-section"><span class="nav-icon">⌘</span>Farms & Broker</a><a class="nav-link" href="#resources-section"><span class="nav-icon">◆</span>Apps & Desktops</a><a class="nav-link" href="#access-section"><span class="nav-icon">◎</span>Access-Sessions</a></nav>
<nav class="nav-group"><div class="nav-label">Betrieb</div><a class="nav-link" href="#history-section"><span class="nav-icon">↻</span>Session-Historie</a><a class="nav-link" href="#policies-section"><span class="nav-icon">≋</span>Policy-Historie</a><a class="nav-link" href="#alerts-section"><span class="nav-icon">!</span>Alerts</a><a class="nav-link" href="#audit-section"><span class="nav-icon">✓</span>Audit-Log</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">Master Console</div><div class="topbar-sub">Broker, Director, Profile & Policy Management</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" title="Theme wechseln">◐</button></div></header>
<main class="page"><section class="page-section" id="overview"><div class="section-heading"><div><div class="eyebrow">Control Plane</div><h1>Infrastruktur auf einen Blick</h1><div class="section-copy">Live-Status der RDS-Farm, Profile und Broker-Ressourcen.</div></div></div>
<div class="metrics"><div class="metric-card"><div class="metric-head"><span class="metric-label">Server</span><span class="metric-icon">▣</span></div><div class="value" id="mServers"></div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Online</span><span class="metric-icon">●</span></div><div class="value" id="mOnline"></div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Aktiv</span><span class="metric-icon">▶</span></div><div class="value" id="mActive"></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="mDisc"></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="mProfile"></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="mCleanup"></div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Farms</span><span class="metric-icon">⌘</span></div><div class="value" id="mFarms"></div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Access</span><span class="metric-icon">◎</span></div><div class="value" id="mAccess"></div></div><div class="metric-card"><div class="metric-head"><span class="metric-label">Alerts</span><span class="metric-icon">!</span></div><div class="value" id="mAlerts"></div></div></div></section>
<section class="page-section" id="servers"><div class="section-heading"><div><div class="eyebrow">Director</div><h2>Terminalserver</h2><div class="section-copy">Server auswählen, Sitzungen steuern und Health-Details prüfen.</div></div></div><section class="panel"><div class="panel-head"><div><h2>Serverliste</h2><div class="panel-kicker">Server anklicken, um Live-Details und Konfiguration im Arbeitsdialog zu öffnen.</div></div><div class="panel-toolbar"><span class="badge neutral" id="serverHint">Live-Ansicht</span></div></div><div id="agents"></div></section></section>
<section class="page-section" id="farms-section"><div class="section-heading"><div><div class="eyebrow">Broker</div><h2>Farms & Broker</h2><div class="section-copy">Hosts gruppieren, Tags definieren und aktive Leases nachvollziehen.</div></div><div class="section-actions"><button class="primary-soft" type="button" data-global-action="open-farm-modal"> Farm anlegen</button></div></div><section class="panel"><div id="farms"></div></section></section>
<section class="page-section" id="resources-section"><div class="section-heading"><div><div class="eyebrow">Delivery</div><h2>Apps & Desktops</h2><div class="section-copy">Guacamole-Ressourcen zu Farms zuordnen und RemoteApps optional direkt durch die Windows-Agents veröffentlichen.</div></div><div class="section-actions"><button class="primary-soft" type="button" data-global-action="open-resource-modal"> Resource anlegen</button></div></div><section class="panel"><div id="resources"></div></section></section>
<section class="page-section" id="access-section"><div class="section-heading"><div><div class="eyebrow">Access Plane</div><h2>Guacamole Access-Sessions</h2><div class="section-copy">Serverseitige PocketID-Sitzungen, die der Reverse Proxy zur Laufzeit vor Guacamole-Requests prüft. Eine widerrufene oder abgelaufene Session liefert keinen Benutzer-Header mehr.</div></div></div><section class="panel"><div id="accessSessions"><div class="empty">Access-Sessions werden geladen…</div></div></section></section>
<section class="page-section" id="history-section"><div class="section-heading"><div><div class="eyebrow">Historie</div><h2>Session-Historie</h2><div class="section-copy">Logon, Reconnect, Disconnect und Profilereignisse nachvollziehen.</div></div></div><section class="panel"><div id="history" class="content"></div></section></section>
<section class="page-section" id="policies-section"><div class="section-heading"><div><div class="eyebrow">Governance</div><h2>Policy-Historie & Rollback</h2><div class="section-copy">Revisionen vergleichen und frühere Sollstände wieder aktivieren.</div></div></div><section class="panel"><div id="policyHistory" class="content"></div></section></section>
<section class="page-section" id="alerts-section"><div class="section-heading"><div><div class="eyebrow">Monitoring</div><h2>Alerts</h2><div class="section-copy">Aktive und gelöste Betriebswarnungen.</div></div></div><section class="panel"><div id="alerts" class="content"></div></section></section>
<section class="page-section" id="audit-section"><div class="section-heading"><div><div class="eyebrow">Compliance</div><h2>Audit-Log</h2><div class="section-copy">Administrative Änderungen und Aktionen revisionsfreundlich nachvollziehen.</div></div></div><section class="panel"><div id="audit" class="content"></div></section></section>
</main></div></div>
<dialog class="sg-modal wide" id="agentModal"><div class="modal-shell"><div class="modal-head"><div><div class="modal-title" id="detailTitle">Terminalserver</div><div class="modal-sub">Live-Daten, Sitzungen und Richtlinien</div></div><button type="button" class="modal-close icon-btn" data-close-modal="agentModal" aria-label="Schließen">×</button></div><div class="modal-body"><div id="detail" class="empty">Server wird geladen…</div></div></div></dialog>
<dialog class="sg-modal compact" id="farmModal"><div class="modal-shell"><div class="modal-head"><div><div class="modal-title">Neue Farm</div><div class="modal-sub">Broker-Ziel und optionale Tag-Selektion definieren</div></div><button type="button" class="modal-close icon-btn" data-close-modal="farmModal">×</button></div><div class="modal-body pad"><div id="farmEditor" class="form"><div class="form-grid"><label>Name<input id="farmName" placeholder="Office"></label><label>Beschreibung<input id="farmDesc" placeholder="Standard Desktop-Farm"></label></div><label>Required Tags (key=value)<textarea id="farmTags" placeholder="role=office"></textarea></label><div class="actions end"><button type="button" class="secondary" data-close-modal="farmModal">Abbrechen</button><button type="button" data-global-action="create-farm">Farm anlegen</button></div></div></div></div></dialog>
<dialog class="sg-modal" id="resourceModal"><div class="modal-shell"><div class="modal-head"><div><div class="modal-title" id="resourceModalTitle">Resource anlegen</div><div class="modal-sub">Desktop oder RemoteApp für eine Farm veröffentlichen</div></div><button type="button" class="modal-close icon-btn" data-close-modal="resourceModal">×</button></div><div class="modal-body"><div id="resourceEditor" class="form"><div class="form-grid three"><label>Name<input id="resName" placeholder="Sage 100"></label><label>Typ<select id="resKind"><option value="desktop">Desktop</option><option value="remoteapp">RemoteApp</option></select></label><label>Farm<select id="resFarm"></select></label><label>Guacamole Connection ID<input id="resConnID"></label><label>Guacamole Connection Name<input id="resConnName" placeholder="Sage"></label><label>RemoteApp Alias<input id="resRemoteApp" placeholder="||Sage"></label><label>Guacamole Working Dir<input id="resRemoteDir"></label><label>Guacamole Argumente<input id="resRemoteArgs"></label></div><div class="note" id="remoteAppManageNote"><strong>Agent-gesteuerte RemoteApp-Veröffentlichung</strong><br>SessionGuard hält den Alias auf allen geeigneten Farm-Agents im Sollzustand; fehlerhafte Hosts werden für diese Resource nicht gebrokert.</div><div class="check"><input id="resManage" type="checkbox"><label>RemoteApp auf Farm-Agents automatisch veröffentlichen und überwachen</label></div><div class="form-grid three"><label>Executable auf dem Terminalserver<input id="resRemotePath" placeholder="C:\Program Files\Hersteller\app.exe"></label><label>Icon-Pfad (optional)<input id="resRemoteIcon" placeholder="leer = Executable"></label><label>Icon-Index<input id="resIconIndex" type="number" value="0"></label><label>Command-Line-Policy<select id="resCmd"><option value="0">Keine Argumente erlauben</option><option value="1">Argumente aus RDP/Guacamole erlauben</option><option value="2">Vorgegebene Argumente erzwingen</option></select></label><label>Vorgegebene Argumente<input id="resRemoteRequired"></label><div class="check"><input id="resPortal" type="checkbox"><label>Auch in RD Web Access anzeigen</label></div></div><div class="actions end"><button type="button" class="secondary" data-close-modal="resourceModal">Abbrechen</button><button id="resSave" type="button" data-global-action="save-resource">Resource anlegen</button></div></div></div></div></dialog>
<div class="toast" id="toast"></div><script src="/app.js?v=0.5.2"></script></body></html>`
const masterJS = `
let selected=null,current=null,editorAgent=null,resourceEditID=null,policyTemplates=[],profileFolders=[],agentCache=[],farmCache=[],resourceCache=[],brokerLeases=[],policyHistory=[],me=null;const $=id=>document.getElementById(id);const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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&&i<u.length-1){n/=1024;i++}return n.toFixed(i>1?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 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();if(!document.querySelector('dialog[open]'))document.body.classList.remove('modal-open')}function activateDetailTab(name){document.querySelectorAll('#detail .detail-tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===name));document.querySelectorAll('#detail .detail-pane').forEach(p=>p.classList.toggle('active',p.dataset.pane===name))}
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('dialog.sg-modal').forEach(d=>d.addEventListener('close',()=>{if(!document.querySelector('dialog[open]'))document.body.classList.remove('modal-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='<table class="table"><thead><tr><th>Status</th><th>Server</th><th>Mode</th><th>Health</th><th>Sitzungen</th><th>Jobs</th></tr></thead><tbody>'+a.map(x=>'<tr class="row '+(x.id===selected?'selected':'')+'" data-id="'+esc(x.id)+'"><td><span class="status-badge '+(x.online?'status-online':'status-offline')+'"><i class="dot '+(x.online?'':'off')+'"></i>'+(x.online?'Online':'Offline')+'</span></td><td><strong>'+esc(x.name)+'</strong><br><span class="muted">'+esc(x.snapshot.agent_version||'')+'</span></td><td><span class="badge '+((x.maintenance_mode||'online')==='online'?'good':(x.maintenance_mode==='drain'?'warning':'bad'))+'">'+esc(x.maintenance_mode||'online')+'</span></td><td>'+esc((x.snapshot.health||{}).score??'')+'</td><td>'+x.active_sessions+' aktiv / '+x.total_sessions+'</td><td>'+((x.snapshot.profile_jobs||[]).length)+' P / '+((x.snapshot.pending_cleanup||[]).length)+' C</td></tr>').join('')+'</tbody></table>';document.querySelectorAll('#agents .row').forEach(r=>r.onclick=async()=>{await selectAgent(r.dataset.id,false,false);openModal('agentModal')})}
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;$('mAccess').textContent=d.access_sessions||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='<div class="empty">Policy-Historie erfordert Policy-Admin/Admin-Rolle.</div>'}let al=await api('/api/v1/alerts');renderAlerts(al.alerts||[]);if(!me)me=await api('/api/v1/me');if((me.permissions||[]).includes('manage')){try{let as=await api('/api/v1/access/sessions');renderAccessSessions(as.sessions||[])}catch(e){$('accessSessions').innerHTML='<div class="empty">Access-Sessions konnten nicht geladen werden: '+esc(e.message)+'</div>'}}else $('accessSessions').innerHTML='<div class="empty">Access-Sessions erfordern Manage/Admin-Rechte.</div>';try{let au=await api('/api/v1/audit');renderAudit(au.audit||[])}catch(e){$('audit').innerHTML='<div class="empty">Audit-Log erfordert Auditor/Admin-Rolle.</div>'}}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?'<table class="table"><thead><tr><th>Zeit</th><th>Ziel</th><th>Revision</th><th>Akteur</th><th></th></tr></thead><tbody>'+rows.map(x=>'<tr><td>'+when(x.created_at)+'</td><td>'+esc(x.target)+'</td><td><code>'+esc(x.revision)+'</code></td><td>'+esc(x.actor||'')+'</td><td><button class="secondary" data-global-action="rollback-policy" data-target="'+esc(x.target)+'" data-revision="'+esc(x.revision)+'">Rollback</button></td></tr>').join('')+'</tbody></table>':'<div class="empty">Noch keine Policy-Versionen.</div>'}
function renderAudit(rows){rows=(rows||[]).slice().reverse().slice(0,250);$('audit').innerHTML=rows.length?'<table class="table"><thead><tr><th>Zeit</th><th>Akteur</th><th>Aktion</th><th>Ziel</th><th>Ergebnis</th></tr></thead><tbody>'+rows.map(x=>'<tr><td>'+esc(when(x.time))+'</td><td>'+esc(x.actor)+'</td><td>'+esc(x.action)+'</td><td>'+esc(x.target||'')+'</td><td class="'+(x.result==='error'?'bad':x.result==='success'?'good':'')+'">'+esc(x.result)+(x.details?'<br><span class="muted">'+esc(x.details)+'</span>':'')+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Noch keine Audit-Einträge.</div>'}
function renderAccessSessions(rows){let root=$('accessSessions');if(!root)return;rows=rows||[];root.innerHTML=rows.length?'<table class="table"><thead><tr><th>Benutzer</th><th>Gruppen</th><th>Erstellt</th><th>Läuft ab</th><th></th></tr></thead><tbody>'+rows.map(x=>'<tr><td><strong>'+esc(x.username||'')+'</strong>'+(x.name?'<br><span class="muted">'+esc(x.name)+'</span>':'')+(x.email?'<br><span class="muted">'+esc(x.email)+'</span>':'')+'</td><td>'+esc((x.groups||[]).join(', ')||'')+'</td><td>'+esc(when(x.created_at))+'</td><td>'+esc(when(x.expires_at))+'</td><td><button class="danger" data-global-action="revoke-access" data-id="'+esc(x.id)+'" data-user="'+esc(x.username||'')+'">Widerrufen</button></td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine aktiven Guacamole Access-Sessions.</div>'}
function overviewStats(a){let s=a.snapshot||{},v=s.server||{},h=s.health||{},mem=v.memory_total?((v.memory_total-v.memory_available)*100/v.memory_total):0;return'<div class="form"><div class="cols"><div><span class="muted">OS</span><br><strong>'+esc(v.os||'')+'</strong></div><div><span class="muted">Health</span><br><strong class="'+((h.score||0)>=70?'good':'bad')+'">'+esc(h.score??'')+'/100</strong></div><div><span class="muted">CPU</span><br><strong>'+Number(v.cpu_percent||0).toFixed(1)+' %</strong></div><div><span class="muted">RAM</span><br><strong>'+mem.toFixed(1)+' % · '+bytes(v.memory_available)+' frei</strong></div><div><span class="muted">Systemdisk</span><br><strong>'+bytes(v.disk_free)+' frei</strong></div><div><span class="muted">Heartbeat</span><br><strong>'+when(a.last_seen)+'</strong></div></div><div class="note">RDP: '+(h.rdp_listener_ok?'✓':'✗')+' · Profile Store: '+(h.profile_store_ok?'✓':'✗')+' · Broker Score: '+esc(Math.round((h.score||0)*10-(v.cpu_percent||0)*2))+' · Pending Commands: '+(a.pending_commands||[]).length+'</div></div>'}
function controlHTML(a){let tags=Object.entries(a.tags||{}).map(([k,v])=>k+'='+v).join('\n');return'<div class="form"><div class="two"><label>Maintenance Mode<select id="agentMode"><option value="online" '+((a.maintenance_mode||'online')==='online'?'selected':'')+'>Online</option><option value="drain" '+(a.maintenance_mode==='drain'?'selected':'')+'>Drain</option><option value="maintenance" '+(a.maintenance_mode==='maintenance'?'selected':'')+'>Maintenance</option></select></label><label>Farm IDs (eine pro Zeile)<textarea id="agentFarms">'+esc((a.farm_ids||[]).join('\n'))+'</textarea></label><label>Tags key=value<textarea id="agentTags">'+esc(tags)+'</textarea></label><div><div class="check"><input id="restartDrained" type="checkbox" '+(a.restart_when_drained?'checked':'')+'><label>Neu starten sobald leer</label></div><div class="actions"><button type="button" data-action="save-control">Serversteuerung speichern</button><button type="button" class="secondary" data-action="broadcast">Nachricht an alle</button><button type="button" class="danger" data-action="logoff-disconnected">Getrennte abmelden</button><button type="button" class="secondary" data-action="reload-control">Steuerung neu laden</button></div></div></div></div>'}
async function sessionAction(id,action){if(!selected)return;let body={action};if(action==='message'){let m=prompt('Nachricht an Sitzung '+id+':');if(!m)return;body.message=m;body.title='SessionGuard'}if(action==='logoff'&&!confirm('Sitzung '+id+' wirklich abmelden? Die Profilsicherung startet nach dem Sitzungsende.'))return;try{await api('/api/v1/agents/'+encodeURIComponent(selected)+'/sessions/'+id+'/action',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Sitzungsaktion an Agent gesendet');setTimeout(()=>selectAgent(selected,true,true),1000)}catch(e){toast(e.message)}}
function sessionsHTML(a){let s=a.snapshot||{},ss=s.sessions||[],p=(a.desired_policy&&a.desired_policy.revision)?a.desired_policy:s.policy,control=p&&p.sessions&&p.sessions.control_enabled;return ss.length?'<table class="table"><thead><tr><th>ID</th><th>Benutzer</th><th>Status</th><th>Logon / Idle</th><th>Client</th><th>Aktionen</th></tr></thead><tbody>'+ss.map(x=>'<tr><td>'+x.id+'</td><td>'+esc((x.domain?x.domain+'\\':'')+x.user)+'</td><td>'+esc(x.state)+'</td><td>'+esc(when(x.logon_at))+'<br><span class="muted">Idle '+Math.round((x.idle_seconds||0)/60)+' min</span></td><td>'+esc(x.client_name||'')+'<br><span class="muted">'+esc(x.client_address||'')+'</span></td><td>'+(control&&x.user?'<div class="actions"><button class="secondary" data-session="'+x.id+'" data-session-action="message">Nachricht</button><button class="secondary" data-session="'+x.id+'" data-session-action="disconnect">Trennen</button><button class="danger" data-session="'+x.id+'" data-session-action="logoff">Abmelden</button></div>':'')+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine Benutzersitzungen.</div>'}
function profileHTML(a){let s=a.snapshot||{},jobs=s.profile_jobs||[],st=Object.values(s.profile_status||{});let h=jobs.length?'<table class="table"><thead><tr><th>Operation</th><th>Benutzer</th><th>Fällig</th><th>Fehler</th></tr></thead><tbody>'+jobs.map(j=>'<tr><td>'+esc(j.operation)+'</td><td>'+esc(j.user)+'</td><td>'+when(j.due_at)+'</td><td>'+esc(j.last_error||'')+'</td></tr>').join('')+'</tbody></table>':'';if(st.length)h+='<table class="table"><thead><tr><th>Benutzer</th><th>Backup</th><th>Restore</th><th>Fehler</th></tr></thead><tbody>'+st.map(x=>'<tr><td>'+esc(x.user||x.sid)+'</td><td>'+when(x.last_backup_at)+'</td><td>'+when(x.last_restore_at)+'</td><td>'+esc(x.last_backup_error||x.last_restore_error||'')+'</td></tr>').join('')+'</tbody></table>';return h||'<div class="empty">Keine Profil-Jobs/Statusdaten.</div>'}
function remoteAppsHTML(a){let rows=((a.snapshot||{}).remote_apps||[]);return rows.length?'<table class="table"><thead><tr><th>Status</th><th>Alias</th><th>Pfad</th><th>Quelle</th><th>Fehler</th></tr></thead><tbody>'+rows.map(x=>'<tr><td><span class="badge '+(x.published&&x.path_exists&&x.in_sync?'good':'bad')+'">'+(x.published&&x.path_exists&&x.in_sync?'Bereit':'Nicht bereit')+'</span></td><td><strong>||'+esc(x.alias)+'</strong><br><span class="muted">'+esc(x.display_name||x.resource_id||'')+'</span></td><td>'+esc(x.path||'')+'</td><td>'+(x.managed?'<span class="badge good">SessionGuard</span>'+(x.owned?' <span class="muted">(angelegt)</span>':' <span class="muted">(übernommen)</span>'):'<span class="badge neutral">entdeckt</span>')+'</td><td>'+esc(x.error||'')+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine RemoteApps entdeckt oder verwaltet.</div>'}
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?'<table class="table"><thead><tr><th>PID</th><th>Session</th><th>Prozess</th><th>RAM</th><th></th></tr></thead><tbody>'+ps.map(p=>'<tr><td>'+p.pid+'</td><td>'+p.session_id+'</td><td>'+esc(p.name)+'</td><td>'+bytes(p.memory_bytes)+'</td><td><button class="danger" data-process="'+p.pid+'" data-action="kill-process">Beenden</button></td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine Session-Prozesse erfasst.</div>'}
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?'<table class="table"><thead><tr><th>Session</th><th>Benutzer</th><th>Logon erkannt</th><th>Restore</th><th>Bis Ready</th></tr></thead><tbody>'+rows.map(x=>'<tr><td>'+x.session_id+'</td><td>'+esc(x.user||x.sid||'')+'</td><td>'+when(x.logon_at||x.first_seen_at)+'</td><td>'+((x.restore_duration_ms||0)/1000).toFixed(2)+' s</td><td>'+((x.observed_logon_ms||0)/1000).toFixed(2)+' s</td></tr>').join('')+'</tbody></table>':'<div class="empty">Noch keine Logon-Telemetrie.</div>'}
function renderHistory(rows){rows=(rows||[]).slice().reverse();$('history').innerHTML=rows.length?'<table class="table"><thead><tr><th>Zeit</th><th>Server</th><th>Benutzer</th><th>Ereignis</th><th>Client</th><th>Details</th></tr></thead><tbody>'+rows.map(x=>'<tr><td>'+when(x.time)+'</td><td>'+esc(x.hostname)+'</td><td>'+esc(x.user)+'</td><td>'+esc(x.event)+'</td><td>'+esc(x.client_name||'')+'</td><td>'+esc(x.details||'')+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Noch keine Session-Historie.</div>'}
function renderAlerts(rows){$('alerts').innerHTML=rows.length?'<table class="table"><thead><tr><th>Status</th><th>Server</th><th>Typ</th><th>Meldung</th><th>Zuletzt</th></tr></thead><tbody>'+rows.map(x=>'<tr><td><span class="badge '+(x.active?'bad':'good')+'">'+(x.active?'AKTIV':'gelöst')+'</span></td><td>'+esc(x.hostname||'')+'</td><td>'+esc(x.type)+'</td><td>'+esc(x.message)+'</td><td>'+when(x.last_seen_at)+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine Alerts.</div>'}
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;let lease=brokerLeases.length?'<div class="section-title" style="padding:0 15px">Aktive Broker-Leases</div><div class="table-wrap"><table class="table"><thead><tr><th>Benutzer</th><th>Agent</th><th>Farm</th><th>Resource</th><th>Grund</th><th>Bis</th></tr></thead><tbody>'+brokerLeases.map(l=>'<tr><td>'+esc(l.user_key)+'</td><td>'+esc(l.agent_id)+'</td><td>'+esc(l.farm_id||'')+'</td><td>'+esc(l.resource_id||'')+'</td><td>'+esc(l.reason||'')+'</td><td>'+when(l.expires_at)+'</td></tr>').join('')+'</tbody></table></div>':'';root.innerHTML=(farmCache.length?'<div class="entity-grid">'+farmCache.map(f=>{let members=farmMembers(f);return'<article class="entity-card"><div class="entity-card-head"><div><div class="entity-title">'+esc(f.name)+'</div><div class="muted">'+esc(f.description||f.id)+'</div></div><span class="badge '+(f.enabled===false?'bad':'good')+'">'+(f.enabled===false?'Deaktiviert':'Aktiv')+'</span></div><div class="entity-stats"><div class="mini-stat"><b>'+members.length+'</b><span>Agenten</span></div><div class="mini-stat"><b>'+resourceCache.filter(r=>r.farm_id===f.id).length+'</b><span>Resources</span></div><div class="mini-stat"><b>'+brokerLeases.filter(l=>l.farm_id===f.id).length+'</b><span>Leases</span></div></div><div class="entity-meta">'+(Object.entries(f.required_tags||{}).map(([k,v])=>'<span class="badge neutral">'+esc(k)+'='+esc(v)+'</span>').join('')||'<span class="muted">Keine Required Tags</span>')+'</div><div class="actions end"><button class="danger" data-global-action="delete-farm" data-id="'+esc(f.id)+'">Löschen</button></div></article>'}).join('')+'</div>':'<div class="empty">Noch keine Farms. Über „Farm anlegen“ kannst du die erste Broker-Gruppe erstellen.</div>')+lease}
function resourceReadiness(x){if(x.kind!=='remoteapp')return{ready:0,total:0,text:'Desktop',cls:'neutral',detail:''};if(!x.manage_remote_app)return{ready:0,total:0,text:'Extern',cls:'neutral',detail:'RemoteApp wird nicht durch SessionGuard provisioniert'};let f=farmCache.find(z=>z.id===x.farm_id),members=f?farmMembers(f):[],ready=0,detail=[];members.forEach(a=>{let st=((a.snapshot||{}).remote_apps||[]).find(r=>r.resource_id===x.id||(!r.resource_id&&String(r.alias||'').toLowerCase()===String(x.remote_app||'').replace(/^\|\|/,'').toLowerCase()));let ok=!!(st&&st.published&&st.path_exists&&st.in_sync&&!st.error);if(ok)ready++;detail.push((a.name||a.id)+': '+(ok?'bereit':(st&&st.error?st.error:'noch kein Status')))});return{ready,total:members.length,text:ready+'/'+members.length+' bereit',cls:members.length&&ready===members.length?'good':'bad',detail:detail.join('\n')}}
function resourceField(id,show){let e=$(id);if(!e)return;let w=e.closest('label')||e.closest('.check');if(w)w.hidden=!show;e.disabled=!show}function updateResourceEditorState(){if(!$('resKind'))return;let remote=$('resKind').value==='remoteapp';if($('remoteAppManageNote'))$('remoteAppManageNote').hidden=!remote;['resRemoteApp','resRemoteDir','resRemoteArgs'].forEach(id=>resourceField(id,remote));resourceField('resManage',remote);if(!remote&&$('resManage'))$('resManage').checked=false;let managed=remote&&$('resManage')&&$('resManage').checked;['resRemotePath','resRemoteIcon','resIconIndex','resCmd','resRemoteRequired','resPortal'].forEach(id=>resourceField(id,managed))}function resetResourceForm(){resourceEditID=null;['resName','resConnID','resConnName','resRemoteApp','resRemoteDir','resRemoteArgs','resRemotePath','resRemoteIcon','resRemoteRequired'].forEach(id=>{if($(id))$(id).value=''});if($('resKind'))$('resKind').value='desktop';if($('resManage'))$('resManage').checked=false;if($('resIconIndex'))$('resIconIndex').value='0';if($('resCmd'))$('resCmd').value='0';if($('resPortal'))$('resPortal').checked=false;if($('resSave'))$('resSave').textContent='Resource anlegen';if($('resourceModalTitle'))$('resourceModalTitle').textContent='Resource anlegen';updateResourceEditorState()}function populateFarmSelect(){let sel=$('resFarm');if(!sel)return;let cur=sel.value;sel.innerHTML=farmCache.map(f=>'<option value="'+esc(f.id)+'">'+esc(f.name)+'</option>').join('');if(cur&&farmCache.some(f=>f.id===cur))sel.value=cur}function editResource(id){let x=resourceCache.find(r=>r.id===id);if(!x)return;populateFarmSelect();resourceEditID=id;$('resName').value=x.name||'';$('resKind').value=x.kind||'desktop';$('resFarm').value=x.farm_id||'';$('resConnID').value=x.guacamole_connection_id||'';$('resConnName').value=x.guacamole_connection_name||'';$('resRemoteApp').value=x.remote_app||'';$('resRemoteDir').value=x.remote_app_dir||'';$('resRemoteArgs').value=x.remote_app_args||'';$('resManage').checked=!!x.manage_remote_app;$('resRemotePath').value=x.remote_app_path||'';$('resRemoteIcon').value=x.remote_app_icon_path||'';$('resIconIndex').value=x.remote_app_icon_index||0;$('resCmd').value=String(x.remote_app_command_line_setting||0);$('resRemoteRequired').value=x.remote_app_required_command_line||'';$('resPortal').checked=!!x.remote_app_show_in_portal;$('resSave').textContent='Änderungen speichern';$('resourceModalTitle').textContent='Resource bearbeiten';updateResourceEditorState();openModal('resourceModal')}function renderResources(){let root=$('resources');if(!root)return;populateFarmSelect();root.innerHTML=resourceCache.length?'<div class="entity-grid">'+resourceCache.map(x=>{let h=resourceReadiness(x),farm=farmCache.find(f=>f.id===x.farm_id);return'<article class="entity-card"><div class="entity-card-head"><div><div class="entity-title">'+esc(x.name)+'</div><div class="muted">'+esc(farm?farm.name:x.farm_id)+'</div></div><span class="badge '+h.cls+'">'+esc(h.text)+'</span></div><div class="entity-meta"><span class="badge neutral">'+esc(x.kind)+'</span><span class="badge neutral">Guac '+esc(x.guacamole_connection_id||x.guacamole_connection_name||'')+'</span>'+(x.remote_app?'<span class="badge neutral">'+esc(x.remote_app)+'</span>':'')+'</div>'+(x.manage_remote_app?'<div class="muted">'+esc(x.remote_app_path||'Executable fehlt')+'</div>':'')+'<div class="actions end"><button class="secondary" data-global-action="edit-resource" data-id="'+esc(x.id)+'">Bearbeiten</button><button class="danger" data-global-action="delete-resource" data-id="'+esc(x.id)+'">Löschen</button></div></article>'}).join('')+'</div>':'<div class="empty">Noch keine Resources. Lege einen Desktop oder eine RemoteApp an.</div>';updateResourceEditorState()}
function eventsHTML(a){let e=((a.snapshot||{}).events||[]).slice().reverse().slice(0,160);return e.length?'<table class="table"><thead><tr><th>Zeit</th><th>Typ</th><th>Benutzer</th><th>Meldung</th></tr></thead><tbody>'+e.map(x=>'<tr class="event-'+esc(x.level)+'"><td>'+when(x.time)+'</td><td>'+esc(x.level)+'</td><td>'+esc(x.user||'')+'</td><td>'+esc(x.message)+'</td></tr>').join('')+'</tbody></table>':'<div class="empty">Keine Ereignisse.</div>'}
function activePolicy(a){if(a.desired_policy&&a.desired_policy.revision)return a.desired_policy;if(a.snapshot&&a.snapshot.policy)return a.snapshot.policy;return{cleanup:{grace_seconds:600,poll_seconds:10,retry_seconds:60,dry_run:true,allowed_profile_roots:['C:\\Users']},profiles:{retry_seconds:60,keep_versions:2,folders:[]},sessions:{disconnected_timeout_seconds:3600},templates:[]}}
function renderDetail(a,preserve){$('detailTitle').textContent=a.name||'Terminalserver';if(!preserve||editorAgent!==a.id||!$('overviewStats')){let p=activePolicy(a);policyTemplates=JSON.parse(JSON.stringify(p.templates||[]));profileFolders=JSON.parse(JSON.stringify((p.profiles&&p.profiles.folders)||[]));$('detail').className='';$('detail').innerHTML='<div class="detail-tabs"><button class="detail-tab active" data-tab="overview">Übersicht</button><button class="detail-tab" data-tab="sessions">Sitzungen</button><button class="detail-tab" data-tab="apps">Apps & Prozesse</button><button class="detail-tab" data-tab="profile">Profile & Ereignisse</button><button class="detail-tab" data-tab="config">Konfiguration</button></div><div class="detail-pane active" data-pane="overview"><div id="overviewStats"></div><div class="detail-grid"><div class="detail-block full"><div class="section-title">Serversteuerung</div><div id="controlEditor"></div></div><div class="detail-block"><div class="section-title">Logon Performance</div><div id="telemetryList" class="content"></div></div><div class="detail-block"><div class="section-title">RemoteApps</div><div id="remoteAppSummary" class="content"></div></div></div></div><div class="detail-pane" data-pane="sessions"><div class="detail-block full"><div class="section-title">Benutzersitzungen</div><div id="sessionList" class="content"></div></div></div><div class="detail-pane" data-pane="apps"><div class="detail-grid"><div class="detail-block full"><div class="section-title">RemoteApps</div><div id="remoteAppList" class="content"></div></div><div class="detail-block full"><div class="section-title">Prozesse</div><div id="processList" class="content"></div></div></div></div><div class="detail-pane" data-pane="profile"><div class="detail-grid"><div class="detail-block"><div class="section-title">Profil-Pipeline</div><div id="profileList" class="content"></div></div><div class="detail-block"><div class="section-title">Agent-Ereignisse</div><div id="eventList" class="content"></div></div></div></div><div class="detail-pane" data-pane="config"><div class="detail-block full"><div class="section-title">Policy</div><div id="policyEditor">'+policyForm(p,a)+'</div></div></div>';$('controlEditor').innerHTML=controlHTML(a);editorAgent=a.id;renderTemplates();renderProfileFolders()}$('overviewStats').innerHTML=overviewStats(a);$('sessionList').innerHTML=sessionsHTML(a);$('telemetryList').innerHTML=telemetryHTML(a);$('processList').innerHTML=processesHTML(a);$('remoteAppList').innerHTML=remoteAppsHTML(a);if($('remoteAppSummary'))$('remoteAppSummary').innerHTML=remoteAppsHTML(a);$('profileList').innerHTML=profileHTML(a);$('eventList').innerHTML=eventsHTML(a)}
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'<div class="muted">Keine weiteren Angaben.</div>';if(k==='url')return'<label>URL<input data-field="url" value="'+esc(t.url||'')+'"></label>';if(k==='shortcut'){let s=t.shortcut||{};return'<div class="two"><label>Zielprogramm<input data-field="shortcut.target" value="'+esc(s.target||'')+'"></label><label>Argumente<input data-field="shortcut.arguments" value="'+esc(s.arguments||'')+'"></label><label>Arbeitsverzeichnis<input data-field="shortcut.working_directory" value="'+esc(s.working_directory||'')+'"></label><label>Icon<input data-field="shortcut.icon_location" value="'+esc(s.icon_location||'')+'"></label></div><label>Beschreibung<input data-field="shortcut.description" value="'+esc(s.description||'')+'"></label>'}return'<label>Quelldatei / UNC-Pfad<input data-field="source" value="'+esc(t.source||'')+'"></label><div class="two"><label>Inline-Inhalt<textarea data-field="content">'+esc(t.content||'')+'</textarea></label><label>Inline Base64<textarea data-field="content_base64">'+esc(t.content_base64||'')+'</textarea></label></div>'}
function renderTemplates(){let h=$('templateList');if(!h)return;h.innerHTML=policyTemplates.length?policyTemplates.map((t,i)=>'<div class="item template-card"><div class="item-head"><strong>'+esc(t.id||('Template '+(i+1)))+'</strong><button type="button" class="danger" data-action="remove-template" data-index="'+i+'">Löschen</button></div><div class="cols"><label>ID<input data-field="id" value="'+esc(t.id||'')+'"></label><label>Typ<select data-field="kind" data-action="template-kind"><option value="file" '+((t.kind||'file')==='file'?'selected':'')+'>Datei</option><option value="directory" '+(t.kind==='directory'?'selected':'')+'>Ordner</option><option value="url" '+(t.kind==='url'?'selected':'')+'>URL</option><option value="shortcut" '+(t.kind==='shortcut'?'selected':'')+'>.lnk</option></select></label><label>Ziel<input data-field="target" value="'+esc(t.target||'')+'"></label></div><div class="check"><input data-field="overwrite" type="checkbox" '+(t.overwrite?'checked':'')+'><label>Aktualisieren/überschreiben</label></div>'+templateSpecific(t)+'</div>').join(''):'<div class="empty">Keine Templates.</div>'}
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})}
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>Relativer Profilpfad<input data-field="path" value="'+esc(f.path||'')+'"></label><label>Ausschlüsse<textarea data-field="exclude">'+esc((f.exclude_globs||[]).join('\n'))+'</textarea></label></div></div>').join(''):'<div class="empty">Keine Profilordner.</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 policyForm(p,a){let c=p.cleanup||{},pr=p.profiles||{},sp=p.sessions||{},source=a.desired_policy&&a.desired_policy.revision?'Master-Soll-Policy':'Agent-Ist-Policy';return'<div class="form"><div class="note">Editor-Basis: '+esc(source)+'. Live-Refresh verändert offene Eingaben nicht.</div><strong>Profil-Sicherung & Wiederherstellung</strong><div class="check"><input id="profEnabled" type="checkbox" '+(pr.enabled?'checked':'')+'><label>Profil-Synchronisation aktiv</label></div><div class="cols"><label>Store Root<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" 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>Profil-Sync Benutzer ausschließen<textarea id="profUsers">'+esc((pr.exclude_users||[]).join('\n'))+'</textarea></label><label>Profil-Sync SIDs ausschließen<textarea id="profSids">'+esc((pr.exclude_sids||[]).join('\n'))+'</textarea></label><div class="toolbar"><strong>Profilordner</strong><button type="button" class="secondary" data-action="add-profile-folder">Ordner hinzufügen</button></div><div id="profileFolderList" class="list"></div><strong>Sitzungsrichtlinie</strong><div class="check"><input id="controlEnabled" type="checkbox" '+(sp.control_enabled?'checked':'')+'><label>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>Disconnect-Timeout (s)<input id="discTimeout" type="number" min="60" value="'+(sp.disconnected_timeout_seconds||3600)+'"></label><label>Auto-Logoff Benutzer ausschließen<textarea id="sessUsers">'+esc((sp.exclude_users||[]).join('\n'))+'</textarea></label><label>Auto-Logoff SIDs ausschließen<textarea id="sessSids">'+esc((sp.exclude_sids||[]).join('\n'))+'</textarea></label><strong>Profil-Cleanup</strong><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 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 class="actions"><button type="button" data-action="save-one">Für diesen Server speichern</button><button type="button" class="secondary" data-action="save-all">Auf alle anwenden</button><button type="button" class="secondary" data-action="reload-policy">Editor neu laden</button></div></div>'}
async function bulkSessionAction(action,scope){if(!selected)return;let body={action,scope};if(action==='message'){let m=prompt('Nachricht an alle Benutzersitzungen:');if(!m)return;body.message=m;body.title='SessionGuard'}if(action==='logoff'&&!confirm('Die ausgewählten Sitzungen wirklich abmelden? Die Profil-Pipeline läuft anschließend.'))return;try{let r=await api('/api/v1/agents/'+encodeURIComponent(selected)+'/sessions/bulk',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast((r.queued||0)+' Sitzungsaktionen eingeplant')}catch(e){toast(e.message)}}
async function saveAgentControl(){if(!selected)return;let tags={};($('agentTags').value||'').split('\n').map(x=>x.trim()).filter(Boolean).forEach(x=>{let i=x.indexOf('=');if(i>0)tags[x.slice(0,i).trim()]=x.slice(i+1).trim()});let body={mode:$('agentMode').value,restart_when_drained:$('restartDrained').checked,tags,farm_ids:$('agentFarms').value.split('\n').map(x=>x.trim()).filter(Boolean)};try{await api('/api/v1/agents/'+encodeURIComponent(selected)+'/control',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast('Serversteuerung gespeichert');await selectAgent(selected,true,false)}catch(e){toast(e.message)}}
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 tab=e.target.closest('button[data-tab]');if(tab){activateDetailTab(tab.dataset.tab);return}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'&&current&&$('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('change',e=>{if(e.target&&((e.target.id==='resKind')||(e.target.id==='resManage')))updateResourceEditorState()});document.addEventListener('click',async e=>{let close=e.target.closest('[data-close-modal]');if(close){closeModal(close.dataset.closeModal);return}let b=e.target.closest('button[data-global-action]');if(!b)return;let a=b.dataset.globalAction;try{if(a==='open-farm-modal'){$('farmName').value='';$('farmDesc').value='';$('farmTags').value='';openModal('farmModal');return}if(a==='open-resource-modal'){resetResourceForm();populateFarmSelect();openModal('resourceModal');return}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');closeModal('farmModal');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'){resetResourceForm();return}if(a==='edit-resource'){editResource(b.dataset.id);return}if(a==='save-resource'){let name=$('resName').value.trim(),farm_id=$('resFarm').value;if(!name||!farm_id)return;let body={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(),manage_remote_app:$('resManage').checked,remote_app_path:$('resRemotePath').value.trim(),remote_app_icon_path:$('resRemoteIcon').value.trim(),remote_app_icon_index:+$('resIconIndex').value,remote_app_command_line_setting:+$('resCmd').value,remote_app_required_command_line:$('resRemoteRequired').value,remote_app_show_in_portal:$('resPortal').checked,enabled:true};let editing=!!resourceEditID;await api(editing?'/api/v1/resources/'+encodeURIComponent(resourceEditID):'/api/v1/resources',{method:editing?'PUT':'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});toast(editing?'Resource aktualisiert':'Resource angelegt');closeModal('resourceModal');resetResourceForm();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==='revoke-access'){if(!confirm('Access-Session von '+(b.dataset.user||'diesem Benutzer')+' wirklich widerrufen?'))return;await api('/api/v1/access/sessions/'+encodeURIComponent(b.dataset.id),{method:'DELETE'});toast('Access-Session widerrufen');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)}});
initShell();refresh();setInterval(refresh,5000);`