Agent beendet.
Das Fenster kann geschlossen werden.
'use strict';
// ---------------------------------------------------------------- Basis
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const urlToken = new URLSearchParams(location.search).get('token');
if (urlToken) { sessionStorage.setItem('kdsToken', urlToken); history.replaceState(null, '', '/' + location.hash); }
const TOKEN = sessionStorage.getItem('kdsToken') || '';
async function api(method, path, body, raw) {
const r = await fetch(path, {
method, headers: { 'X-Token': TOKEN, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : (typeof body === 'string' ? body : JSON.stringify(body))
});
if (raw) return r.text();
const data = await r.json().catch(() => null);
if (!r.ok) throw new Error(data?.error || `HTTP ${r.status}`);
return data;
}
const arr = v => v == null ? [] : Array.isArray(v) ? v : [v];
function toast(msg, isErr, onClick) {
const d = document.createElement('div');
d.textContent = msg; if (isErr) d.className = 'err';
d.onclick = () => { onClick?.(); d.remove(); };
$('#toast').append(d); setTimeout(() => d.remove(), isErr ? 8000 : 4000);
}
async function guard(fn) { try { return await fn(); } catch (e) { toast(e.message, true); } }
// ---------------------------------------------------------------- Zustand / Profil
const DEFAULTS = () => ({
id: '', name: '', customer: '', description: '',
apps: [],
office: {
product: 'O365ProPlusRetail', channel: 'Current', arch: '64', languages: ['de-de'],
exclude: ['Groove', 'Lync', 'Bing'], extraProducts: [], pidKey: '', company: '',
updates: true, removeMsi: true, migrateArch: true, sharedComputer: false, disableVba: true, ooxmlDefault: true, showUi: false
},
officeMode: 'online',
shortcuts: [],
system: { computerName: '', timeZone: '', showExtensions: true, disableConsumer: true },
security: { defender: true, firewall: true, smartScreen: true, uac: false, disableSmb1: true, disableAutorun: true, lsaProtection: false, telemetryMin: false, rdp: 'keep', localAdminName: '', localAdminPassword: '' },
power: { plan: 'balanced', monitorAc: 15, monitorDc: 5, sleepAc: 0, sleepDc: 15, hibernateAc: 0, hibernateDc: 60, lidAc: 'nothing', lidDc: 'sleep', buttonAc: 'shutdown', buttonDc: 'shutdown', hibernate: '', fastStartup: 'off' },
bloatware: { recommended: true, ids: [], keep: [] },
wifi: [],
vpn: { config: '', user: '', password: '', dns: '' },
domain: { name: '', ou: '', user: '', password: '', admins: '', useVpn: false, vpnService: false, reboot: true },
bitlocker: { method: 'XtsAes256', usedSpaceOnly: true, skipHardwareTest: true, usePin: false, pin: '', backupAad: false, backupAd: false },
steps: ['system', 'security', 'power', 'apps', 'office', 'shortcuts', 'updates']
});
let S = DEFAULTS();
try { const saved = JSON.parse(localStorage.getItem('kdsState') || 'null'); if (saved) S = deepMerge(DEFAULTS(), saved); } catch { }
function deepMerge(base, add) {
for (const k of Object.keys(add || {})) {
if (add[k] && typeof add[k] === 'object' && !Array.isArray(add[k]) && base[k] && typeof base[k] === 'object') deepMerge(base[k], add[k]);
else base[k] = add[k];
}
return base;
}
const getPath = p => p.split('.').reduce((o, k) => o?.[k], S);
function setPath(p, v) { const ks = p.split('.'); const last = ks.pop(); ks.reduce((o, k) => o[k] ??= {}, S)[last] = v; }
function persist() { try { localStorage.setItem('kdsState', JSON.stringify(S)); } catch { } renderSummary(); }
// generische Formularbindung: data-bind="pfad" und data-bind-list="pfad" (Checkbox-Gruppen)
function bindForms() {
$$('[data-bind]').forEach(el => {
el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', () => {
let v = el.type === 'checkbox' ? el.checked : el.value;
if (el.type === 'number') v = el.value === '' ? '' : Number(el.value);
setPath(el.dataset.bind, v); persist(); onChange(el.dataset.bind);
});
if (el.tagName === 'SELECT') el.addEventListener('change', () => el.dispatchEvent(new Event('input')));
});
$$('[data-bind-list]').forEach(box => box.addEventListener('change', () => {
setPath(box.dataset.bindList, $$('input:checked', box).map(i => i.value)); persist(); onChange(box.dataset.bindList);
}));
}
function fillForms() {
$$('[data-bind]').forEach(el => {
const v = getPath(el.dataset.bind);
if (el.type === 'checkbox') el.checked = !!v; else el.value = v ?? '';
});
$$('[data-bind-list]').forEach(box => {
const v = arr(getPath(box.dataset.bindList));
$$('input', box).forEach(i => i.checked = v.includes(i.value));
});
$$('#steps input').forEach(i => i.checked = arr(S.steps).includes(i.value));
$('#profileName').textContent = 'Blaupause: ' + ([S.customer, S.name].filter(Boolean).join(' / ') || '(ungespeichert)');
$$('.pinonly').forEach(e => e.classList.toggle('hidden', !S.bitlocker.usePin));
}
function onChange(path) {
if (path.startsWith('office')) { if (path === 'office.product') { renderOfficeChannels(); renderOfficeExtras(); } updateOfficeXml(); }
if (path === 'bitlocker.usePin') fillForms();
if (path === 'bloatware.recommended') renderBloat();
}
function loadState(obj) { S = deepMerge(DEFAULTS(), obj); persist(); fillForms(); renderAll(); }
// ---------------------------------------------------------------- Navigation
function showTab(t) {
$$('#nav a').forEach(a => a.classList.toggle('active', a.dataset.tab === t));
$$('.tab').forEach(s => s.classList.toggle('active', s.id === 'tab-' + t));
({ shortcuts: loadShortcuts, bitlocker: loadBitlocker, bloatware: () => { if (!bloat) scanBloat(); }, wifi: loadWifi, vpn: loadVpn, profiles: loadProfiles, jobs: pollJobs, power: loadPower, office: loadOfficeBuilds })[t]?.();
}
$('#nav').onclick = e => { const a = e.target.closest('a'); if (a) showTab(a.dataset.tab); };
// ---------------------------------------------------------------- Übersicht
async function loadStatus() {
const s = await api('GET', '/api/status');
const tpm = s.tpm ? (s.tpm.present ? (s.tpm.ready ? 'vorhanden, bereit' : 'vorhanden, nicht bereit') : 'nicht vorhanden') : 'unbekannt';
const items = [['Computer', s.computerName], ['Hersteller / Modell', `${s.manufacturer} ${s.model}`], ['Seriennummer', s.serial],
['Windows', `${s.os}
${esc(s.build)}`], ['Prozessor', s.cpu], ['RAM', s.ramGB + ' GB'],
['Systemlaufwerk', `${s.diskFreeGB} GB frei von ${s.diskSizeGB} GB`], ['TPM', tpm], ['Domäne', s.partOfDomain ? s.domain : 'Arbeitsgruppe'],
['winget', s.winget ? 'verfügbar' : 'fehlt – wird automatisch installiert'], ['Akku', s.battery || '–'], ['Agent-Pfad', s.root]];
$('#sysinfo').innerHTML = items.map(([k, v]) => `
Noch keine eigenen Installer hochgeladen.
'; } const localApp = f => ({ name: f.name, id: 'local:' + f.file, source: 'local', path: f.file, args: f.args }); $('#localList').onchange = e => guard(async () => { const { li, la } = e.target.dataset; if (li != null) { const f = localInstallers[li]; e.target.checked ? addApp(localApp(f)) : removeApp('local:' + f.file); } if (la != null) { const f = localInstallers[la]; f.args = e.target.value; await api('POST', '/api/local-installers', { file: f.file, name: f.name, args: f.args }); const sel = S.apps.find(a => a.id === 'local:' + f.file); if (sel) { sel.args = f.args; persist(); } toast('Parameter gespeichert'); } }); $('#localList').onclick = e => { const i = e.target.dataset.ldel; if (i == null) return; const f = localInstallers[i]; if (!confirm(`"${f.file}" vom Stick löschen?`)) return; guard(async () => { await api('DELETE', `/api/files?kind=installers&name=${encodeURIComponent(f.file.split('\\').pop())}`); removeApp('local:' + f.file); loadLocalInstallers(); }); }; let lastArgsInput = null; $('#localList').addEventListener('focusin', e => { if (e.target.dataset.la != null) lastArgsInput = e.target; }); $$('.hint').forEach(h => h.onclick = () => { if (!lastArgsInput) return toast('Erst in ein Parameter-Feld klicken', true); lastArgsInput.value = h.dataset.hint; lastArgsInput.dispatchEvent(new Event('change', { bubbles: true })); }); async function handleInstallerFiles(files) { const up = await uploadMany('installers', files); await loadLocalInstallers(); // frisch hochgeladene direkt auswählen up.forEach(u => { const f = localInstallers.find(x => x.file === u.path); if (f) addApp(localApp(f)); }); loadLocalInstallers(); } $('#instFile').onchange = e => { handleInstallerFiles([...e.target.files]); e.target.value = ''; }; const drop = $('#instDrop'); drop.ondragover = e => { e.preventDefault(); drop.classList.add('over'); }; drop.ondragleave = () => drop.classList.remove('over'); drop.ondrop = e => { e.preventDefault(); drop.classList.remove('over'); handleInstallerFiles([...e.dataTransfer.files]); }; $('#catalog').onchange = e => { const id = e.target.dataset.app; if (!id) return; const a = catalog.find(x => x.id === id); if (e.target.checked) addApp({ name: a.name, id: a.id, source: a.source, override: a.override }); else removeApp(id); }; function syncCatalogChecks() { $$('#catalog input').forEach(i => i.checked = S.apps.some(a => a.id === i.dataset.app)); } function addApp(a) { if (S.apps.some(x => x.id === a.id)) return; S.apps.push(a); persist(); renderSelectedApps(); } function removeApp(id) { S.apps = S.apps.filter(a => a.id !== id); persist(); renderSelectedApps(); } function renderSelectedApps() { $('#appCount').textContent = S.apps.length; $('#selectedApps').innerHTML = S.apps.length ? S.apps.map(a => `Nichts ausgewählt.
'; syncCatalogChecks(); $$('#localList [data-li]').forEach(i => i.checked = S.apps.some(a => a.id === 'local:' + localInstallers[i.dataset.li]?.file)); } $('#selectedApps').onclick = e => { const id = e.target.dataset.rm; if (id) removeApp(id); }; async function doSearch() { const q = $('#appSearch').value.trim(); if (!q) return; $('#searchResults').innerHTML = 'Suche …
'; await guard(async () => { const res = arr(await api('GET', `/api/winget/search?q=${encodeURIComponent(q)}&source=${$('#appSource').value}`)); $('#searchResults').innerHTML = res.length ? res.map((r, i) => `Keine Treffer.
'; $('#searchResults').onclick = e => { const i = e.target.dataset.add; if (i != null) { addApp(res[i]); e.target.textContent = '✓'; } }; }); } $('#btnSearch').onclick = doSearch; $('#appSearch').onkeydown = e => { if (e.key === 'Enter') doSearch(); }; $('#btnInstallApps').onclick = () => guard(async () => { if (!S.apps.length) return toast('Keine Programme ausgewählt', true); jobStarted((await api('POST', '/api/apps/install', { apps: S.apps })).job); }); $('#btnUpgradeAll').onclick = $('#btnWingetUpg').onclick = () => guard(async () => jobStarted((await api('POST', '/api/apps/upgrade-all', {})).job)); // ---------------------------------------------------------------- Office const PRODUCTS = [ { id: 'O365ProPlusRetail', name: 'Microsoft 365 Apps for enterprise', channels: ['Current', 'MonthlyEnterprise', 'SemiAnnual', 'SemiAnnualPreview', 'BetaChannel'], extras: ['VisioProRetail', 'ProjectProRetail'] }, { id: 'O365BusinessRetail', name: 'Microsoft 365 Apps for business', channels: ['Current', 'MonthlyEnterprise', 'SemiAnnual'], extras: ['VisioProRetail', 'ProjectProRetail'] }, { id: 'ProPlus2024Volume', name: 'Office LTSC Professional Plus 2024 (Volumenlizenz)', channels: ['PerpetualVL2024'], extras: ['VisioPro2024Volume', 'ProjectPro2024Volume'] }, { id: 'Standard2024Volume', name: 'Office LTSC Standard 2024 (Volumenlizenz)', channels: ['PerpetualVL2024'], extras: ['VisioStd2024Volume', 'ProjectStd2024Volume'] }, { id: 'ProPlus2021Volume', name: 'Office LTSC Professional Plus 2021 (Volumenlizenz)', channels: ['PerpetualVL2021'], extras: ['VisioPro2021Volume', 'ProjectPro2021Volume'] }, { id: 'HomeBusiness2024Retail', name: 'Office Home & Business 2024', channels: ['Current'], extras: [] }, { id: 'Home2024Retail', name: 'Office Home 2024', channels: ['Current'], extras: [] } ]; const CHANNEL_NAMES = { Current: 'Aktueller Kanal', MonthlyEnterprise: 'Monatlicher Enterprise-Kanal', SemiAnnual: 'Halbjährlicher Enterprise-Kanal', SemiAnnualPreview: 'Halbjährlich (Vorschau)', BetaChannel: 'Beta', PerpetualVL2024: 'LTSC 2024', PerpetualVL2021: 'LTSC 2021' }; const OFFICE_APPS = [['Word', 'Word'], ['Excel', 'Excel'], ['PowerPoint', 'PowerPoint'], ['Outlook', 'Outlook (klassisch)'], ['OutlookForWindows', 'Outlook (neu)'], ['OneNote', 'OneNote'], ['Access', 'Access'], ['Publisher', 'Publisher'], ['Teams', 'Teams'], ['OneDrive', 'OneDrive'], ['Lync', 'Skype for Business'], ['Groove', 'OneDrive for Business (alt)'], ['Bing', 'Bing-Suche in Chrome']]; function renderOffice() { $('#officeProduct').innerHTML = PRODUCTS.map(p => ``).join(''); $('#officeApps').innerHTML = OFFICE_APPS.map(([id, n]) => ``).join(''); $('#officeProduct').value = S.office.product; renderOfficeChannels(); renderOfficeExtras(); renderOfficeApps(); updateOfficeXml(); } function renderOfficeChannels() { const p = PRODUCTS.find(x => x.id === S.office.product) || PRODUCTS[0]; $('#officeChannel').innerHTML = p.channels.map(c => ``).join(''); if (!p.channels.includes(S.office.channel)) S.office.channel = p.channels[0]; $('#officeChannel').value = S.office.channel; S.office.extraProducts = arr(S.office.extraProducts).filter(e => p.extras.includes(e)); } function renderOfficeExtras() { const p = PRODUCTS.find(x => x.id === S.office.product) || PRODUCTS[0]; const label = e => `${e.startsWith('Visio') ? 'Visio' : 'Project'} ${/Std/.test(e) ? 'Standard' : 'Professional'} ${(e.match(/\d{4}/) || [''])[0]}`; $('#officeExtras').innerHTML = p.extras.length ? p.extras.map(e => ``).join('') : 'keine für dieses Produkt'; } function renderOfficeApps() { $$('#officeApps input').forEach(i => i.checked = !arr(S.office.exclude).includes(i.value)); } $('#officeApps').onchange = () => { S.office.exclude = $$('#officeApps input:not(:checked)').map(i => i.value); persist(); updateOfficeXml(); }; $('#officeExtras').onchange = () => { S.office.extraProducts = $$('#officeExtras input:checked').map(i => i.value); persist(); updateOfficeXml(); }; let xmlTimer; function updateOfficeXml() { clearTimeout(xmlTimer); xmlTimer = setTimeout(() => guard(async () => { $('#officeXml').textContent = await api('POST', '/api/office/xml', S.office, true); }), 250); } $('#btnXmlDownload').onclick = () => download('configuration.xml', $('#officeXml').textContent, 'application/xml'); $('#btnXmlCopy').onclick = () => navigator.clipboard.writeText($('#officeXml').textContent).then(() => toast('XML kopiert')); $('#btnOffice').onclick = () => guard(async () => { jobStarted((await api('POST', '/api/office/install', { office: S.office, mode: S.officeMode, name: S.name })).job); }); async function loadOfficeBuilds() { const b = arr(await api('GET', '/api/office/configs').catch(() => [])); $('#officeBuilds').innerHTML = b.length ? 'Vorhandene Builds auf dem Stick: ' + b.map(esc).join(', ') : 'Noch kein Offline-Build auf dem Stick.'; } // ---------------------------------------------------------------- Desktop-Icons let presets = []; const RDP_FIELDS = { host: 'rdpHost', port: 'rdpPort', user: 'rdpUser', gateway: 'rdpGateway', remoteApp: 'rdpRemoteApp', audio: 'rdpAudio', extra: 'rdpExtra' }; const RDP_CHECKS = { fullscreen: 'rdpFullscreen', multimon: 'rdpMultimon', clipboard: 'rdpClipboard', printers: 'rdpPrinters', drives: 'rdpDrives', smartcards: 'rdpSmartcards', microphone: 'rdpMicrophone', camera: 'rdpCamera', admin: 'rdpAdmin' }; function rdpFromForm() { const r = {}; for (const [k, id] of Object.entries(RDP_FIELDS)) r[k] = $('#' + id).value.trim(); for (const [k, id] of Object.entries(RDP_CHECKS)) r[k] = $('#' + id).checked; return r; } function scFromForm() { const type = $('#scType').value; const s = { type, name: $('#scName').value.trim() }; if (type === 'rdp') { const file = $('#rdpSource').value; if (file) { s.rdpFile = file; s.target = 'rdp\\' + file; } else { s.rdp = rdpFromForm(); s.target = s.rdp.host + (s.rdp.remoteApp ? ` (RemoteApp ${s.rdp.remoteApp})` : ''); if (!s.rdp.host) s.target = ''; } } else { Object.assign(s, { target: $('#scTarget').value.trim(), args: type === 'lnk' ? $('#scArgs').value : '', icon: $('#scIcon').value.trim() }); } return s; } function scValid(s) { if (!s.name || !s.target) { toast(s.type === 'rdp' ? 'Name und Server angeben' : 'Name und Ziel angeben', true); return false; } return true; } function scTypeUi() { const t = $('#scType').value; $$('.t-url, .t-lnk, .t-rdp').forEach(e => e.classList.toggle('hidden', !e.classList.contains('t-' + t))); $('#rdpForm').classList.toggle('hidden', !!$('#rdpSource').value); if (t === 'rdp') updateRdpPreview(); } $('#scType').onchange = scTypeUi; $('#rdpSource').onchange = scTypeUi; let rdpTimer; function updateRdpPreview() { clearTimeout(rdpTimer); rdpTimer = setTimeout(async () => { const r = rdpFromForm(); $('#rdpPreview').textContent = r.host ? await api('POST', '/api/rdp/preview', r, true) : '(Server angeben)'; }, 300); } $('#rdpForm').addEventListener('input', updateRdpPreview); $('#rdpForm').addEventListener('change', updateRdpPreview); $('#rdpUpload').onchange = async e => { const up = await uploadMany('rdp', [...e.target.files]); e.target.value = ''; await loadRdpFiles(); if (up.length) { $('#rdpSource').value = up[0].name; if (!$('#scName').value) $('#scName').value = up[0].name.replace(/\.rdp$/i, ''); scTypeUi(); } }; $('#iconUpload').onchange = async e => { const up = await uploadMany('icons', [...e.target.files]); e.target.value = ''; if (up.length) $('#scIcon').value = up[0].name; loadShortcuts(); }; async function loadRdpFiles() { const files = arr(await api('GET', '/api/rdpfiles')); const cur = $('#rdpSource').value; $('#rdpSource').innerHTML = '' + files.map(f => ``).join(''); $('#rdpSource').value = files.includes(cur) ? cur : ''; } $('#btnScCreate').onclick = () => guard(async () => { const s = scFromForm(); if (!scValid(s)) return; await api('POST', '/api/shortcuts', s); toast('Verknüpfung angelegt'); loadShortcuts(); }); $('#btnScPlan').onclick = () => { const s = scFromForm(); if (!scValid(s)) return; S.shortcuts = arr(S.shortcuts).filter(x => x.name !== s.name).concat(s); persist(); renderPlanned(); }; const SC_TYPES = { url: '🌐', lnk: '🔗', rdp: '🖥️' }; function renderPlanned() { const l = arr(S.shortcuts); $('#scPlanned').innerHTML = l.length ? l.map((s, i) => `Keine geplant.
'; } $('#scPlanned').onclick = e => { const { i, edit } = e.target.dataset; if (i != null) { S.shortcuts.splice(i, 1); persist(); renderPlanned(); } if (edit != null) { const s = S.shortcuts[edit]; $('#scType').value = s.type || 'lnk'; $('#scName').value = s.name; $('#scTarget').value = s.type === 'rdp' ? '' : s.target || ''; $('#scArgs').value = s.args || ''; $('#scIcon').value = s.icon || ''; $('#rdpSource').value = s.rdpFile || ''; const r = s.rdp || {}; for (const [k, id] of Object.entries(RDP_FIELDS)) $('#' + id).value = r[k] ?? (k === 'audio' ? 'local' : ''); for (const [k, id] of Object.entries(RDP_CHECKS)) if (k in r) $('#' + id).checked = !!r[k]; scTypeUi(); } }; $('#btnScApplyAll').onclick = () => guard(async () => { for (const s of arr(S.shortcuts)) { try { await api('POST', '/api/shortcuts', s); } catch (e) { toast(`${s.name}: ${e.message}`, true); } } toast('Verknüpfungen angelegt'); loadShortcuts(); }); async function loadShortcuts() { const [cur, icons] = await Promise.all([api('GET', '/api/shortcuts'), api('GET', '/api/icons')]); $('#scCurrent').innerHTML = arr(cur).length ? arr(cur).map(s => `Keine Verknüpfungen.
'; $('#iconList').innerHTML = arr(icons).map(i => ``).join('')); async function loadPower() { const p = await api('GET', '/api/power').catch(() => null); if (p) $('#powerState').textContent = 'Aktuell: ' + p.active; } // ---------------------------------------------------------------- BitLocker async function loadBitlocker() { $('#blStatus').textContent = 'Lade …'; await guard(async () => { const b = await api('GET', '/api/bitlocker'); let h = `${esc(b.edition)} · TPM: ${b.tpmPresent ? (b.tpmReady ? 'bereit' : 'nicht bereit') : 'nicht vorhanden'}
`; if (!b.available) h += `${esc(b.message)}
`; h += arr(b.volumes).map(v => `Suche läuft … (kann einige Minuten dauern)
'; const j = await waitJob(r.job); updates = arr(j.result); $('#updCount').textContent = updates.length; $('#updList').innerHTML = updates.length ? updates.map((u, i) => ``).join('') : `${j.state.startsWith('Fertig') ? 'Keine Updates gefunden – System ist aktuell.' : 'Suche fehlgeschlagen, siehe Aufgaben-Protokoll.'}
`; })); $('#btnUpdSel').onclick = () => guard(async () => { const ids = $$('#updList input:checked').map(i => updates[i.dataset.u].id); if (!ids.length) return toast('Nichts ausgewählt', true); jobStarted((await api('POST', '/api/updates/install', { ids })).job); }); $('#btnUpdAll').onclick = () => guard(async () => jobStarted((await api('POST', '/api/updates/install', { all: true, type: 'all' })).job)); $('#btnVendor').onclick = () => guard(async () => jobStarted((await api('POST', '/api/updates/vendor', {})).job)); $('#btnWinget').onclick = () => guard(async () => jobStarted((await api('POST', '/api/winget/install', {})).job)); $('#btnReboot').onclick = () => { if (confirm('Computer jetzt neu starten?')) guard(() => api('POST', '/api/reboot', {})); }; // ---------------------------------------------------------------- Kennwortabfrage function askSecret(title, text) { return new Promise(resolve => { const d = $('#secretDlg'); $('#secretTitle').textContent = title; $('#secretText').textContent = text || ''; $('#secretInput').value = ''; d.onclose = () => resolve(d.returnValue === 'ok' ? $('#secretInput').value : null); d.showModal(); $('#secretInput').focus(); }); } // Fehlende Kennwörter (nicht in der Blaupause gespeichert) nur für diesen Lauf abfragen async function withSecrets(state, needDomain, needVpn) { const d = JSON.parse(JSON.stringify(state)); if (needDomain) { if (!d.domain.name || !d.domain.user) { toast('Domäne und Benutzer angeben (Tab „VPN & Domäne“)', true); return null; } if (!d.domain.password) { const p = await askSecret('Domänen-Kennwort', `Kennwort für ${d.domain.user} (${d.domain.name})`); if (p == null) return null; d.domain.password = p; } } if (needVpn) { if (!d.vpn.config) { toast('Keine VPN-Konfiguration ausgewählt', true); return null; } if (d.vpn.user && !d.vpn.password) { const p = await askSecret('VPN-Kennwort', `Kennwort für VPN-Benutzer ${d.vpn.user}`); if (p == null) return null; d.vpn.password = p; } } return d; } // ---------------------------------------------------------------- WLAN $('#wfAuth').onchange = () => $('#wfKeyRow').classList.toggle('hidden', $('#wfAuth').value === 'open'); $('#wfShow').onclick = () => { const i = $('#wfKey'); i.type = i.type === 'password' ? 'text' : 'password'; }; $('#btnWfAdd').onclick = () => { const w = { ssid: $('#wfSsid').value.trim(), auth: $('#wfAuth').value, key: $('#wfKey').value, auto: $('#wfAuto').checked, hidden: $('#wfHidden').checked, connect: $('#wfConnect').checked }; if (!w.ssid) return toast('SSID angeben', true); if (w.auth !== 'open' && w.key.length < 8) return toast('Kennwort muss mind. 8 Zeichen haben', true); S.wifi = arr(S.wifi).filter(x => x.ssid !== w.ssid).concat(w); persist(); renderWifi(); $('#wfSsid').value = ''; $('#wfKey').value = ''; }; const AUTH_NAMES = { WPA2PSK: 'WPA2', WPA3SAE: 'WPA3', open: 'offen' }; function renderWifi() { const l = arr(S.wifi); $('#wfCount').textContent = l.length; $('#wfList').innerHTML = l.length ? l.map((w, i) => `Keine WLANs in der Blaupause.
'; } $('#wfList').onclick = e => { const { wdel, wedit } = e.target.dataset; if (wdel != null) { S.wifi.splice(wdel, 1); persist(); renderWifi(); } if (wedit != null) { const w = S.wifi[wedit]; $('#wfSsid').value = w.ssid; $('#wfAuth').value = w.auth; $('#wfKey').value = w.key || ''; $('#wfAuto').checked = w.auto !== false; $('#wfHidden').checked = !!w.hidden; $('#wfConnect').checked = !!w.connect; $('#wfAuth').onchange(); } }; $('#btnWfApply').onclick = () => guard(async () => { if (!arr(S.wifi).length) return toast('Keine WLANs in der Blaupause', true); jobStarted((await api('POST', '/api/wifi/apply', { wifi: S.wifi })).job); }); async function loadWifi() { await guard(async () => { const s = await api('GET', '/api/wifi'); $('#wfState').innerHTML = s.hasWifi ? (s.connected ? `Verbunden mit ${esc(s.connected)}` : 'Nicht mit einem WLAN verbunden') : 'Kein WLAN-Adapter gefunden.'; $('#wfNetworks').innerHTML = arr(s.networks).map(n => `' + files.map(f => ``).join(''); $('#vpnConfig').value = files.includes(S.vpn.config) ? S.vpn.config : ''; const s = v.status; $('#vpnStatus').innerHTML = `${esc(s.log)}Noch keine Aufgaben.
'; if (selJob && $('#tab-jobs').classList.contains('active')) { const j = await api('GET', '/api/jobs/' + selJob); $('#jobTitle').textContent = `#${j.id} ${j.title} – ${stateName(j.state)}`; const pre = $('#jobLog'), atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 30; pre.innerHTML = esc(j.log || '').replace(/^.*\[(ERROR|WARN|OK)\].*$/gm, (l, lv) => `${l}`); if (atBottom) pre.scrollTop = pre.scrollHeight; $('#btnJobStop').classList.toggle('hidden', !/Laeuft|Wartet/.test(j.state)); } jobTimer = setTimeout(pollJobs, active ? 1500 : 5000); } catch { jobTimer = setTimeout(pollJobs, 5000); } } $('#jobList').onclick = e => { const it = e.target.closest('[data-j]'); if (it) { selJob = it.dataset.j; pollJobs(); } }; $('#btnJobStop').onclick = () => { if (selJob && confirm('Aufgabe abbrechen?')) guard(async () => { await api('DELETE', '/api/jobs/' + selJob); pollJobs(); }); }; async function waitJob(id) { for (; ;) { await new Promise(r => setTimeout(r, 2000)); const j = await api('GET', '/api/jobs/' + id); if (!/Laeuft|Wartet/.test(j.state)) return j; } } $('#btnShutdown').onclick = () => { if (confirm('Agent beenden? Laufende Aufgaben werden abgebrochen.')) api('POST', '/api/shutdown', {}).finally(() => { document.body.innerHTML = 'Das Fenster kann geschlossen werden.
Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.