Firefox ESR, Office aus eigenem Pfad, Warnung bei ungespeicherten Blaupausen

- Katalog: Mozilla Firefox ESR (DE)
- Office-Modus "Offline aus eigenem Pfad" (Netzlaufwerk/Ordner) mit
  Pruefung auf Office\Data und SourcePath in der XML-Vorschau
- Seitenleiste: Speichern-Button + Hinweis bei ungespeicherten Aenderungen;
  Rueckfrage vor Laden einer anderen Blaupause, Beenden und Schliessen

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-21 16:48:33 +02:00
co-authored by Claude Opus 5.5
parent 46a969ee90
commit 59da23d986
7 changed files with 56 additions and 11 deletions
+1 -1
View File
@@ -191,7 +191,7 @@ function Invoke-Api {
'^POST /api/office/xml$' { $b = Read-Body $Ctx; return Send-Response $Ctx ([Text.Encoding]::UTF8.GetBytes((New-OfficeXml $b))) 'application/xml; charset=utf-8' }
'^POST /api/office/install$' {
$b = Read-Body $Ctx
$t = @{ online = 'Office installieren'; download = 'Office-Build auf Stick erstellen'; offline = 'Office offline installieren' }[$b.mode]
$t = @{ online = 'Office installieren'; download = 'Office-Build auf Stick erstellen'; offline = 'Office offline installieren'; custom = 'Office aus eigenem Pfad installieren' }[$b.mode]
return Send-Json $Ctx @{ job = (Add-KdsJob $t 'Install-Office' $b) }
}
'^GET /api/office/configs$' {
+2 -1
View File
@@ -20,6 +20,7 @@ Die Web-API ist immer mit einem zufälligen Token geschützt, das bei jedem Star
- *Online*: direkt vom Microsoft-CDN installieren
- *Build*: Installationsdateien nach `office\source\<Produkt>-<Arch>` auf den Stick laden
- *Offline*: vom Stick installieren
- *Offline aus eigenem Pfad*: vorhandene Office-Quelle nutzen (Netzlaufwerk/Ordner mit `Office\Data`), wird als `SourcePath` in die XML übernommen
- `setup.exe` wird bei Bedarf automatisch geladen; generierte XMLs landen in `office\configs\`.
- **Desktop-Icons** für alle Benutzer (öffentlicher Desktop): Web-Links (`.url`), Programmverknüpfungen (`.lnk`) und **RDP-Verbindungen** (`.rdp`: im Formular erzeugt inkl. Gateway, RemoteApp, Umleitungen, oder vorhandene .rdp-Dateien hochladen), eigene Icons hochladbar nach `icons\` (werden nach `C:\ProgramData\KDS-Agent\Icons` kopiert). Vorlagen in `config/shortcuts.json`.
- **WLAN**: SSIDs mit Kennwort (WPA2/WPA3/offen, versteckt, automatisch verbinden) in der Blaupause speichern und für alle Benutzer anlegen. WLAN-Profile eines Referenzgeräts lassen sich inkl. Kennwort übernehmen.
@@ -34,7 +35,7 @@ Die Web-API ist immer mit einem zufälligen Token geschützt, das bei jedem Star
- **Energie**: Energieplan, Timeouts für Bildschirm/Standby/Ruhezustand (Netz & Akku), Deckel- und Netzschalteraktion, Ruhezustand, Schnellstart.
- **BitLocker**: Status, Aktivierung mit TPM oder TPM+PIN, XTS-AES 128/256, nur belegten Speicher, Sicherung in Entra ID / AD. Der Wiederherstellungsschlüssel wird **immer** unter `bitlocker-keys\` auf dem Stick gespeichert (Computername + Seriennummer).
- **Updates & Treiber**: Windows- und Treiberupdates über die Windows-Update-API (inkl. Microsoft Update) suchen, auswählen, installieren. Hersteller-Tools: Dell Command Update, Lenovo System Update, HP Image Assistant.
- **Blaupausen**: Komplette Standard-Einrichtungen je Kunde unter `blueprints\` speichern, nach Kunde gruppiert anzeigen, suchen, laden, duplizieren, exportieren/importieren. Auf der Übersicht per Dropdown auswählen und direkt starten. Mitgeliefert: `Beispiel / Büro-Standard` und `Beispiel / Minimal`.
- **Blaupausen**: Komplette Standard-Einrichtungen je Kunde unter `blueprints\` speichern, nach Kunde gruppiert anzeigen, suchen, laden, duplizieren, exportieren/importieren. Auf der Übersicht per Dropdown auswählen und direkt starten. Mitgeliefert: `Beispiel / Büro-Standard` und `Beispiel / Minimal`. Ungespeicherte Änderungen werden in der Seitenleiste angezeigt (Speichern-Button direkt darunter); vor dem Laden einer anderen Blaupause, dem Beenden oder Schließen wird gewarnt.
- **Einrichtung mit einem Klick**: Auf der Übersicht alle Schritte in sinnvoller Reihenfolge ausführen.
- **Aufgaben**: Alle Aktionen laufen als Hintergrund-Jobs in einer Warteschlange (nacheinander, damit sich Installer nicht blockieren). Live-Protokoll in der Oberfläche; Logs unter `logs\<Computername>\`.
+1
View File
@@ -3,6 +3,7 @@
"apps": [
{ "category": "Browser", "name": "Google Chrome", "id": "Google.Chrome", "source": "winget" },
{ "category": "Browser", "name": "Mozilla Firefox (DE)", "id": "Mozilla.Firefox.de", "source": "winget" },
{ "category": "Browser", "name": "Mozilla Firefox ESR (DE)", "id": "Mozilla.Firefox.ESR.de", "source": "winget" },
{ "category": "Browser", "name": "Brave", "id": "Brave.Brave", "source": "winget" },
{ "category": "Kommunikation", "name": "Microsoft Teams", "id": "Microsoft.Teams", "source": "winget" },
+9 -1
View File
@@ -307,6 +307,14 @@ function Install-Office {
$cfgDir = Join-Path $global:KdsRoot 'office\configs'
New-Item $cfgDir -ItemType Directory -Force | Out-Null
if ($mode -eq 'custom') {
# vorhandene Office-Quelle (z.B. Netzlaufwerk oder anderer Ordner) verwenden
$src = [Environment]::ExpandEnvironmentVariables([string]$Data.sourcePath).TrimEnd('\')
if (-not $src) { throw 'Kein Pfad zur Office-Quelle angegeben' }
if (-not (Test-Path (Join-Path $src 'Office\Data'))) { throw "Unter '$src' wurde kein Office-Build gefunden (erwartet: $src\Office\Data)" }
$cfg | Add-Member -NotePropertyName sourcePath -NotePropertyValue $src -Force
Write-Log "Office-Quelle: $src"
}
if ($mode -in 'download', 'offline') {
$src = Join-Path $global:KdsRoot "office\source\$($cfg.product)-$($cfg.arch)"
New-Item $src -ItemType Directory -Force | Out-Null
@@ -1453,7 +1461,7 @@ function Invoke-FullProfile {
if ('security' -in $steps) { & $run 'Sicherheit' { Set-SecurityConfig $Data } }
if ('power' -in $steps) { & $run 'Energie' { Set-PowerConfig $Data } }
if ('apps' -in $steps) { & $run 'Programme' { Install-Apps $Data } }
if ('office' -in $steps) { & $run 'Office' { Install-Office ([pscustomobject]@{ office = $Data.office; mode = $Data.officeMode; name = $Data.name }) } }
if ('office' -in $steps) { & $run 'Office' { Install-Office ([pscustomobject]@{ office = $Data.office; mode = $Data.officeMode; sourcePath = $Data.officeSource; name = $Data.name }) } }
if ('shortcuts' -in $steps) { & $run 'Desktop-Verknuepfungen' { Set-DesktopShortcuts $Data } }
if ('drivers' -in $steps) { & $run 'Hersteller-Treiber' { Invoke-VendorDriverUpdate $Data } }
if ('updates' -in $steps) { & $run 'Windows Update' { Install-WindowsUpdates ([pscustomobject]@{ all = $true; type = 'all' }) | Out-Null } }
+35 -8
View File
@@ -37,7 +37,7 @@ const DEFAULTS = () => ({
exclude: ['Groove', 'Lync', 'Bing'], extraProducts: [], pidKey: '', company: '',
updates: true, removeMsi: true, migrateArch: true, sharedComputer: false, disableVba: true, ooxmlDefault: true, showUi: false
},
officeMode: 'online',
officeMode: 'online', officeSource: '',
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: '' },
@@ -61,7 +61,22 @@ function deepMerge(base, add) {
}
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(); }
function persist() { try { localStorage.setItem('kdsState', JSON.stringify(S)); } catch { } renderSummary(); updateDirty(); }
// ---- Ungespeicherte Änderungen erkennen (Vergleich mit dem zuletzt gespeicherten/geladenen Stand)
const snap = o => JSON.stringify(o);
let savedSnap = null;
try { savedSnap = localStorage.getItem('kdsSaved'); } catch { }
if (savedSnap == null) savedSnap = snap(S);
const isDirty = () => snap(S) !== savedSnap;
function markSaved() { savedSnap = snap(S); try { localStorage.setItem('kdsSaved', savedSnap); } catch { } updateDirty(); }
function updateDirty() {
const d = isDirty();
$('#dirtyWarn')?.classList.toggle('hidden', !d);
$('#btnSideSave')?.classList.toggle('pulse', d);
}
const confirmDiscard = () => !isDirty() || confirm('Es gibt ungespeicherte Änderungen an der aktuellen Blaupause. Trotzdem fortfahren und Änderungen verwerfen?');
window.addEventListener('beforeunload', e => { if (isDirty()) { e.preventDefault(); e.returnValue = ''; } });
// generische Formularbindung: data-bind="pfad" und data-bind-list="pfad" (Checkbox-Gruppen)
function bindForms() {
@@ -87,12 +102,14 @@ function fillForms() {
$$('input', box).forEach(i => i.checked = v.includes(i.value));
});
$$('#steps input').forEach(i => i.checked = arr(S.steps).includes(i.value));
$('#officeSourceRow').classList.toggle('hidden', S.officeMode !== 'custom');
$('#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 === 'officeMode' || path === 'officeSource') { fillForms(); updateOfficeXml(); }
if (path === 'bloatware.recommended') renderBloat();
}
function loadState(obj) { S = deepMerge(DEFAULTS(), obj); persist(); fillForms(); renderAll(); }
@@ -288,12 +305,13 @@ $('#officeExtras').onchange = () => { S.office.extraProducts = $$('#officeExtras
let xmlTimer;
function updateOfficeXml() {
clearTimeout(xmlTimer);
xmlTimer = setTimeout(() => guard(async () => { $('#officeXml').textContent = await api('POST', '/api/office/xml', S.office, true); }), 250);
xmlTimer = setTimeout(() => guard(async () => { $('#officeXml').textContent = await api('POST', '/api/office/xml', { ...S.office, sourcePath: S.officeMode === 'custom' ? S.officeSource : '' }, 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);
if (S.officeMode === 'custom' && !S.officeSource) return toast('Pfad zur Office-Quelle angeben', true);
jobStarted((await api('POST', '/api/office/install', { office: S.office, mode: S.officeMode, sourcePath: S.officeSource, name: S.name })).job);
});
async function loadOfficeBuilds() {
const b = arr(await api('GET', '/api/office/configs').catch(() => []));
@@ -670,8 +688,9 @@ function renderQuickSelect() {
}
$('#bpFilter').oninput = renderBlueprints;
async function loadBlueprint(id) {
if (!confirmDiscard()) { renderQuickSelect(); return; }
const data = await api('GET', '/api/blueprints/' + encodeURIComponent(id));
loadState({ ...data, id });
loadState({ ...data, id }); markSaved();
renderBlueprints(); renderQuickSelect();
$('#bpCustomer').value = S.customer || ''; $('#profileSaveName').value = S.name || ''; $('#bpDescription').value = S.description || '';
toast(`Blaupause "${[S.customer, S.name].filter(Boolean).join(' / ')}" geladen`);
@@ -700,6 +719,7 @@ async function saveBlueprint(asNew) {
await api('DELETE', '/api/blueprints/' + encodeURIComponent(S.id));
Object.assign(S, { id, name, customer, description: $('#bpDescription').value.trim() }); persist();
await api('POST', '/api/blueprints/' + encodeURIComponent(id), JSON.stringify(S, null, 2));
markSaved();
toast(`Blaupause "${name}" gespeichert`); fillForms(); loadProfiles();
}
$('#btnProfileSave').onclick = () => guard(() => saveBlueprint(false));
@@ -709,7 +729,14 @@ $('#importFile').onchange = async e => {
const f = e.target.files[0]; e.target.value = '';
if (f) guard(async () => { const d = JSON.parse(await f.text()); d.id = ''; loadState(d); loadProfiles(); toast('Importiert – zum Übernehmen „Speichern“ klicken'); });
};
$('#btnReset').onclick = () => { if (confirm('Alle Einstellungen auf Standard zurücksetzen?')) loadState(DEFAULTS()); };
$('#btnReset').onclick = () => { if (confirm('Alle Einstellungen auf Standard zurücksetzen?')) { loadState(DEFAULTS()); markSaved(); } };
// Speichern-Button in der Seitenleiste
$('#btnSideSave').onclick = () => guard(async () => {
if (!S.name) { showTab('profiles'); $('#profileSaveName').focus(); return toast('Bitte Kunde und Namen für die neue Blaupause angeben', true); }
if (!blueprints.length) await loadProfiles();
$('#bpCustomer').value = S.customer || ''; $('#profileSaveName').value = S.name; $('#bpDescription').value = S.description || '';
await saveBlueprint(false);
});
function download(name, text, type) {
const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([text], { type })); a.download = name; a.click();
@@ -759,13 +786,13 @@ async function waitJob(id) {
}
}
$('#btnShutdown').onclick = () => { if (confirm('Agent beenden? Laufende Aufgaben werden abgebrochen.')) api('POST', '/api/shutdown', {}).finally(() => { document.body.innerHTML = '<main><h1>Agent beendet.</h1><p>Das Fenster kann geschlossen werden.</p></main>'; }); };
$('#btnShutdown').onclick = () => { if (confirmDiscard() && confirm('Agent beenden? Laufende Aufgaben werden abgebrochen.')) api('POST', '/api/shutdown', {}).finally(() => { document.body.innerHTML = '<main><h1>Agent beendet.</h1><p>Das Fenster kann geschlossen werden.</p></main>'; }); };
// ---------------------------------------------------------------- Start
function renderAll() { renderOffice(); renderSelectedApps(); renderPlanned(); renderWifi(); renderSummary(); }
(async () => {
if (!TOKEN) { document.body.innerHTML = '<main><h1>Kein Zugriffstoken</h1><p>Bitte die vollständige URL (mit ?token=…) aus dem Agent-Fenster öffnen.</p></main>'; return; }
bindForms(); renderAll(); fillForms(); scTypeUi();
bindForms(); renderAll(); fillForms(); scTypeUi(); updateDirty();
await guard(loadStatus);
await guard(loadCatalog);
await guard(async () => {
+5
View File
@@ -27,6 +27,8 @@
</nav>
<div class="side-foot">
<div id="profileName" class="muted">Blaupause: (ungespeichert)</div>
<div id="dirtyWarn" class="dirty hidden">⚠ Ungespeicherte Änderungen</div>
<button class="primary" id="btnSideSave">💾 Blaupause speichern</button>
<button class="ghost" id="btnShutdown">Agent beenden</button>
</div>
</aside>
@@ -163,7 +165,10 @@
<option value="online">Online installieren (direkt von Microsoft)</option>
<option value="download">Nur Build erstellen (Dateien auf den Stick laden)</option>
<option value="offline">Vom Stick installieren (Build wird bei Bedarf erstellt)</option>
<option value="custom">Offline aus eigenem Pfad (Netzlaufwerk / Ordner)</option>
</select></label>
<label class="f" id="officeSourceRow">Pfad zur Office-Quelle
<input data-bind="officeSource" placeholder="z.B. \\server\software\Office365 oder D:\Office – Ordner, der Office\Data enthält"></label>
<p class="muted small">setup.exe des Office Deployment Tools wird bei Bedarf automatisch von Microsoft geladen und unter <code>office\</code> abgelegt. Builds liegen in <code>office\source\</code>.</p>
<div id="officeBuilds" class="muted small"></div>
<button class="primary" id="btnOffice">Ausführen</button>
+3
View File
@@ -101,3 +101,6 @@ dialog { border: 1px solid var(--line); border-radius: 12px; background: var(--c
dialog::backdrop { background: rgba(0,0,0,.45); }
.okline { color: var(--ok); font-weight: 600; } .errline { color: var(--err); font-weight: 600; }
.checks label input { flex: none; }
.dirty { background: rgba(178,106,0,.18); color: #ffb648; border: 1px solid rgba(255,182,72,.4); border-radius: 6px; padding: 6px 10px; font-size: 12px; font-weight: 600; }
#btnSideSave.pulse { animation: pulse 1.6s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(76,149,255,.6); } 50% { box-shadow: 0 0 0 6px rgba(76,149,255,0); } }