140 lines
72 KiB
Go
140 lines
72 KiB
Go
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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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'&¤t&&$('controlEditor'))$('controlEditor').innerHTML=controlHTML(current);if(a==='broadcast')bulkSessionAction('message','all');if(a==='logoff-disconnected')bulkSessionAction('logoff','disconnected');if(a==='kill-process')killProcess(+b.dataset.process);if(a==='save-one')savePolicy(false);if(a==='save-all')savePolicy(true);if(a==='reload-policy')selectAgent(selected,false,false);if(a==='add-template'){policyTemplates=collectTemplates();policyTemplates.push(templateDefault());renderTemplates()}if(a==='remove-template'){policyTemplates=collectTemplates();policyTemplates.splice(+b.dataset.index,1);renderTemplates()}if(a==='add-profile-folder'){profileFolders=collectProfileFolders();profileFolders.push({path:'AppData\\Roaming\\Hersteller',exclude_globs:['Cache/**']});renderProfileFolders()}if(a==='remove-profile-folder'){profileFolders=collectProfileFolders();profileFolders.splice(+b.dataset.index,1);renderProfileFolders()}});$('detail').addEventListener('change',e=>{if(e.target.matches('select[data-action="template-kind"]')){policyTemplates=collectTemplates();renderTemplates()}});document.addEventListener('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);`
|