| ARCHITEKTUR.md | ●●●●● Patch | Ansicht | Raw | Blame | Historie | |
| CHANGELOG.md | ●●●●● Patch | Ansicht | Raw | Blame | Historie | |
| README.md | ●●●●● Patch | Ansicht | Raw | Blame | Historie | |
| app.py | ●●●●● Patch | Ansicht | Raw | Blame | Historie | |
| templates/einrichten.html | ●●●●● Patch | Ansicht | Raw | Blame | Historie |
ARCHITEKTUR.md
@@ -146,32 +146,53 @@ ## 6. "Rechner einrichten" – Verteilungs-Mechanismus Route `/einrichten` (app.py) erzeugt bei Bedarf ein individuelles Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat`), Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat` **+ den echten Installer-Dateien/ZIPs selbst** unter `tools/<id>.<endung>`), statt vorab für jede Mitarbeiter/Tool-Kombination ein festes Skript zu pflegen. **Wichtige Design-Entscheidung (Runde r16):** Die Installer-Dateien werden bereits beim Erzeugen des Pakets (auf dem PC, der die Checkliste ausfüllt) eingebettet, nicht erst beim Ausführen auf dem Zielrechner nachgeladen. Ein früherer Ansatz lud die Dateien per `Invoke-WebRequest` zur Laufzeit vom Server nach – das erforderte Netzwerkzugriff des Zielrechners auf den Server in genau dem Moment der Einrichtung, was in der Praxis nicht immer gegeben ist (z. B. Rechner noch nicht ans Firmennetz angebunden). Jetzt braucht der Zielrechner für die eigentliche Installation **kein Netzwerk mehr** – nur die abschließende, optionale Meldung ans zentrale Protokoll (`/zuweisungen/neu`) versucht noch einen Netzwerkzugriff, scheitert dieser, bleibt trotzdem das lokale Protokoll (`protokoll.log`) vollständig. Ablauf beim Ausführen auf dem Zielrechner: 1. `.bat` kopiert sich selbst + das `.ps1` nach `C:\ClientBibliothek` 1. `.bat` kopiert den **kompletten entpackten Ordner** (inkl. `tools/`- Unterordner mit den echten Installer-Dateien) nach `C:\ClientBibliothek` (per `xcopy /E /I /Y`) 2. Self-Elevation (Admin-Rechte-Anfrage) über `Start-Process -Verb RunAs` 3. Automatische Windows-Defender-Ausnahme für `C:\ClientBibliothek` (`Add-MpPreference -ExclusionPath`) – ein einziger Pfad statt vieler Einzelausnahmen 4. Pro ausgewähltem Tool: entweder stille Installation (Download + Ausführung mit hinterlegtem Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei Tools ohne einzelne Installer-Datei ("Ordner-Typ", z. B. portable Programme wie Blender) – Download als ZIP nach `C:\ClientBibliothek\pakete\`, **bewusst ohne automatisches Entpacken** (Entscheidung in Runde r14: der Zielort für portable Tools variiert, das soll eine Person bewusst 4. Pro ausgewähltem Tool: entweder stille Installation aus der **lokal mitgelieferten** Datei unter `tools/<id>.<endung>` (mit hinterlegtem Silent-Parameter, `.msi` läuft über `msiexec`) oder – bei Tools ohne einzelne Installer-Datei ("Ordner-Typ", z. B. portable Programme wie Blender) – die mitgelieferte ZIP-Datei wird nach `C:\ClientBibliothek\pakete\` kopiert, **bewusst ohne automatisches Entpacken** (Entscheidung in Runde r14: der Zielort für portable Tools variiert, das soll eine Person bewusst entscheiden) 5. Lokales Protokoll unter `C:\ClientBibliothek\protokoll.log` 6. Automatischer POST an `/zuweisungen/neu` auf dem Server, um das zentrale 6. Optionaler POST an `/zuweisungen/neu` auf dem Server, um das zentrale Protokoll zu befüllen (Rechnername wird dabei über `$env:COMPUTERNAME` automatisch erkannt) automatisch erkannt) – best effort, kein Abbruch bei Fehlschlag Die Ordner-Struktur unter `C:\ClientBibliothek`: - `\downloads` – transiente Installer-Dateien, werden nach Gebrauch gelöscht - `\tools` – die mitgelieferten Installer-Dateien/ZIPs (Quelle für die Installation, kommen direkt aus dem heruntergeladenen Paket) - `\pakete` – ZIPs von portablen Tools, bleiben bewusst liegen - `\protokoll.log` – lokale Mitschrift Serverseitig teilen sich `/einrichten` (POST) und `/software/<id>/download` die Hilfsfunktion `_zippe_ordner_in_bytes()` für das ZIP-Packen eines Ordners, um Code-Duplikation zu vermeiden. **Der Client-Ordner-Pfad ist eine Konstante** (`CLIENT_ORDNER` in `app.py`, aktuell `C:\ClientBibliothek`) – zentral anpassbar, falls die Infrastruktur einen anderen Standardpfad vorgibt. CHANGELOG.md
@@ -93,6 +93,26 @@ automatisch selbst eingetragen (`Add-MpPreference`) - Firewall-Freigabe-Skript ergänzt, nachdem der Test im echten Firmennetzwerk anstand (Port 5000 war durch die Windows-Firewall blockiert) - **Installer-Dateien werden direkt ins Paket gebündelt statt zur Laufzeit nachzuladen.** Auslöser: beim ersten Test im Firmennetz enthielt der Download nur die Skript-Dateien, die eigentlichen Installer wurden erst beim Ausführen auf dem Zielrechner per Netzwerk nachgeladen – das setzt Netzwerkzugriff des Zielrechners auf den Server in genau dem Moment voraus. Jetzt werden die Dateien beim Erzeugen des Pakets direkt mit eingepackt, der Zielrechner braucht für die Installation selbst kein Netzwerk mehr (nur die abschließende, optionale Protokoll-Meldung). ## Offene Design-Frage: Skript-Ansatz vs. automatische Verteilung Nach dem ersten Test im Firmennetz wurde nochmal grundsätzlich hinterfragt, ob der Skript-Ansatz (manueller Download + Ausführen auf dem Zielrechner) der richtige Weg ist, oder ob Daten "automatisch" auf dem Zielrechner landen sollten. Eine echte automatische Verteilung ohne manuellen Schritt am Zielrechner würde entweder eine dauerhaft laufende Helfer-Software auf jedem Zielrechner oder Windows-Remoting (PowerShell Remoting) voraussetzen – beide Wege brauchen im Kern eine Domänen-Infrastruktur bzw. zentrale Zugangsdaten-Verwaltung, die aktuell nicht vorhanden ist. Diese Frage wurde bewusst nicht in diesem Prototyp entschieden, sondern als offener Punkt für die Übernahme festgehalten (siehe README.md). ## Testmethodik (durchgehend) Da die Zielumgebung Windows ist, die Entwicklung aber in einer Linux-Umgebung README.md
@@ -75,6 +75,35 @@ --- ## 3a. Rechner einrichten (Verteilung an neue Rechner) Menüpunkt "Rechner einrichten": Häkchen bei den benötigten Tools setzen, Mitarbeiter eintragen, "Installations-Paket herunterladen" klicken. Das erzeugte ZIP enthält die ausgewählten Installer-Dateien **direkt mit** (kein Nachladen vom Server nötig). Auf dem **neuen Rechner**: ZIP entpacken, `Rechner-einrichten-starten.bat` doppelklicken. Das Skript: - fragt bei Bedarf automatisch nach Admin-Rechten - installiert jedes Tool still mit dem hinterlegten Silent-Parameter - trägt automatisch eine Windows-Defender-Ausnahme für `C:\ClientBibliothek` ein (dort landen alle Dateien gesammelt) - meldet das Ergebnis am Ende automatisch ans zentrale Protokoll zurück (Rechnername wird dabei selbst erkannt) Tools ohne einzelne Installer-Datei (z. B. bereits entpackte/portable Tools wie Blender) werden als ZIP nach `C:\ClientBibliothek\pakete\` gelegt – bewusst ohne automatisches Entpacken, das entscheidet ihr von Fall zu Fall selbst. ⚠️ Die Windows-spezifischen Teile (Admin-Rechte-Anfrage, stille Installation) wurden nicht auf einem echten Windows-Rechner entwickelt und getestet, nur mit dem echten PowerShell-Parser auf Syntaxfehler geprüft sowie die Netzwerk-/Datei-Logik separat verifiziert. Bei größerem Rollout zuerst an 1–2 unkritischen Rechnern gegentesten. --- ## 4. Automatische Aktualisierung Bei jedem Tool kann beim Anlegen/Bearbeiten "Automatische Aktualisierung app.py
@@ -22,7 +22,7 @@ # jederzeit im Fuss der Seitenleiste sichtbar ist, ob wirklich die neueste # Version laeuft (nicht nur auf der Festplatte liegt, sondern auch vom # gestarteten Programm geladen wurde). APP_VERSION = "2026-07-22 · r15 (Firewall-Freigabe-Skript ergänzt)" APP_VERSION = "2026-07-23 · r16 (Installer-Dateien direkt im Paket, kein Netzwerk zur Laufzeit nötig)" @app.context_processor @@ -189,8 +189,7 @@ "@echo off\r\n" "set ZIEL=C:\\ClientBibliothek\r\n" 'if not exist "%ZIEL%" mkdir "%ZIEL%"\r\n' 'copy /Y "%~dp0einrichten.ps1" "%ZIEL%\\einrichten.ps1" >nul\r\n' 'copy /Y "%~f0" "%ZIEL%\\Rechner-einrichten-starten.bat" >nul\r\n' 'xcopy "%~dp0*" "%ZIEL%\\" /E /I /Y >nul\r\n' "echo ============================================================\r\n" "echo Rechner-Einrichtung wird gestartet...\r\n" "echo Admin-Rechte werden bei Bedarf per Windows-Abfrage angefragt.\r\n" @@ -203,10 +202,12 @@ def _erstelle_einrichtungs_skript(server_url, mitarbeiter, tools): """Baut den Inhalt der einrichten.ps1 zusammen: laedt jedes ausgewaehlte Tool ueber die bestehende Download-Route herunter, installiert es mit dem hinterlegten Silent-Parameter und meldet das Ergebnis am Ende als Protokoll an die Server-Bibliothek zurueck.""" """Baut den Inhalt der einrichten.ps1 zusammen: installiert jedes ausgewaehlte Tool aus der MITGELIEFERTEN Datei (liegt im 'tools'-Unterordner desselben Pakets, kein Netzwerkzugriff auf den Server noetig) und meldet das Ergebnis am Ende BEST-EFFORT als Protokoll an die Server-Bibliothek zurueck (falls Netzwerkzugriff vorhanden ist - schlaegt das fehl, bleibt trotzdem das lokale Protokoll erhalten).""" zeilen = [ '$ErrorActionPreference = "Stop"', f"$server = '{_ps_escape(server_url)}'", @@ -218,10 +219,9 @@ " exit", "}", "", "$tempOrdner = Join-Path $clientOrdner \"downloads\"", "$toolsOrdner = Join-Path $clientOrdner \"tools\"", "$paketeOrdner = Join-Path $clientOrdner \"pakete\"", "$protokollDatei = Join-Path $clientOrdner \"protokoll.log\"", "New-Item -ItemType Directory -Force -Path $tempOrdner | Out-Null", "New-Item -ItemType Directory -Force -Path $paketeOrdner | Out-Null", "", "try {", @@ -246,41 +246,38 @@ "", "function InstalliereTool($id, $name, $endung, $parameter) {", ' Write-Host "Installiere: $name ..." -ForegroundColor Cyan', ' $zielDatei = Join-Path $tempOrdner ("sb_" + $id + $endung)', " try {", ' Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielDatei -UseBasicParsing', " } catch {", ' Write-Host (" FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red', ' Protokolliere ($name + ": FEHLER beim Herunterladen")', ' $quelle = Join-Path $toolsOrdner ($id.ToString() + $endung)', " if (-not (Test-Path $quelle)) {", ' Write-Host (" FEHLER: Datei fehlt im Paket unter " + $quelle) -ForegroundColor Red', ' Protokolliere ($name + ": FEHLER, Datei fehlt im Paket")', " return $false", " }", " try {", ' if ($endung -eq ".msi") {', " Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $zielDatei + '\" ' + $parameter) -Wait", " Start-Process \"msiexec.exe\" -ArgumentList ('/i \"' + $quelle + '\" ' + $parameter) -Wait", " } else {", " Start-Process $zielDatei -ArgumentList $parameter -Wait", " Start-Process $quelle -ArgumentList $parameter -Wait", " }", " } catch {", ' Write-Host (" FEHLER bei der Installation von " + $name + ": " + $_) -ForegroundColor Red', ' Protokolliere ($name + ": FEHLER bei der Installation")', " return $false", " }", " Remove-Item -Path $zielDatei -Force -ErrorAction SilentlyContinue", ' Protokolliere ($name + ": installiert")', ' Write-Host " fertig." -ForegroundColor Green', " return $true", "}", "", "function BereitstellenPortablesTool($id, $name) {", ' Write-Host "Lade herunter (portables Tool): $name ..." -ForegroundColor Cyan', ' $zielZip = Join-Path $paketeOrdner ($name + ".zip")', " try {", ' Invoke-WebRequest -Uri "$server/software/$id/download" -OutFile $zielZip -UseBasicParsing', " } catch {", ' Write-Host (" FEHLER beim Herunterladen von " + $name + ": " + $_) -ForegroundColor Red', ' Protokolliere ($name + ": FEHLER beim Herunterladen")', ' Write-Host "Portables Tool: $name ..." -ForegroundColor Cyan', ' $quelle = Join-Path $toolsOrdner ($id.ToString() + ".zip")', " if (-not (Test-Path $quelle)) {", ' Write-Host (" FEHLER: ZIP fehlt im Paket unter " + $quelle) -ForegroundColor Red', ' Protokolliere ($name + ": FEHLER, ZIP fehlt im Paket")', " return $false", " }", ' $zielZip = Join-Path $paketeOrdner ($name + ".zip")', " Copy-Item -Path $quelle -Destination $zielZip -Force", ' Protokolliere ($name + ": als ZIP bereitgestellt unter " + $zielZip)', ' Write-Host (" fertig, liegt als ZIP unter " + $zielZip) -ForegroundColor Green', ' Write-Host " (bewusst NICHT automatisch entpackt - bitte manuell an den Zielort entpacken)" -ForegroundColor DarkGray', @@ -304,7 +301,7 @@ zeilen += [ "", 'Write-Host ""', 'Write-Host "Protokoll wird auf dem Server gespeichert..."', 'Write-Host "Protokoll wird an den Server gemeldet (optional, braucht Netzwerkzugriff)..."', '$koerper = "mitarbeiter=" + [uri]::EscapeDataString($mitarbeiter)', '$koerper += "&rechnername=" + [uri]::EscapeDataString($env:COMPUTERNAME)', '$koerper += "&installiert_von=" + [uri]::EscapeDataString("Automatisches Einrichtungs-Skript")', @@ -312,9 +309,9 @@ "foreach ($id in $erfolgreicheIds) { $koerper += \"&software_ids=$id\" }", "try {", ' Invoke-RestMethod -Uri "$server/zuweisungen/neu" -Method POST -Body $koerper -ContentType "application/x-www-form-urlencoded" | Out-Null', ' Write-Host "Protokoll gespeichert." -ForegroundColor Green', ' Write-Host "Zentrales Protokoll auf dem Server aktualisiert." -ForegroundColor Green', "} catch {", ' Write-Host ("Konnte Protokoll nicht speichern: " + $_) -ForegroundColor Yellow', ' Write-Host "Kein Netzwerkzugriff auf den Server - zentrales Protokoll nicht aktualisiert (lokales Protokoll unter protokoll.log ist trotzdem vollstaendig)." -ForegroundColor Yellow', "}", "", 'Write-Host ""', @@ -353,6 +350,7 @@ "typ": "installer", "endung": os.path.splitext(gefunden)[1].lower(), "parameter": sw["install_parameter"] or "", "installer_pfad": pfad, }) continue @@ -367,6 +365,7 @@ "typ": "ordner", "endung": "", "parameter": "", "installer_pfad": pfad, }) continue @@ -388,6 +387,20 @@ with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr("einrichten.ps1", ps1_inhalt) zf.writestr("Rechner-einrichten-starten.bat", _BAT_LAUNCHER) # Die echten Installer-Dateien/ZIPs direkt mit ins Paket packen, # damit auf dem Zielrechner beim Ausfuehren KEIN Netzwerkzugriff # auf den Server mehr noetig ist (nur die abschliessende # Protokoll-Rueckmeldung ist optional und best-effort). for t in ausgewaehlt: if t["typ"] == "installer": quelldatei = updater.verwaltete_datei_pfad(t["installer_pfad"], t["name"]) if quelldatei and os.path.isfile(quelldatei): zf.write(quelldatei, f"tools/{t['id']}{t['endung']}") else: ordner = updater.effektiver_ablageordner(t["installer_pfad"], t["name"]) zf.writestr(f"tools/{t['id']}.zip", _zippe_ordner_in_bytes(ordner)) speicher.seek(0) return send_file( speicher, as_attachment=True, @@ -397,6 +410,21 @@ return render_template( "einrichten.html", installierbar=installierbar, nicht_installierbar=nicht_installierbar ) def _zippe_ordner_in_bytes(ordner): """Packt einen Ordner (ohne '_sicherungen'-Unterordner) in ein ZIP im Arbeitsspeicher und gibt die rohen Bytes zurueck. Wird sowohl fuer den Download-Button als auch fuer das Einrichtungs-Paket verwendet.""" speicher = io.BytesIO() with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf: for wurzel, unterordner, dateien in os.walk(ordner): unterordner[:] = [o for o in unterordner if o != "_sicherungen"] for datei in dateien: voller_pfad = os.path.join(wurzel, datei) rel_pfad = os.path.relpath(voller_pfad, ordner) zf.write(voller_pfad, rel_pfad) return speicher.getvalue() def _ordner_fuer_installer_sicherstellen(installer_pfad, name): @@ -913,17 +941,11 @@ flash(f"Ordner nicht gefunden unter: {ordner}", "fehler") return redirect(url_for("software_detail", software_id=software_id)) speicher = io.BytesIO() with zipfile.ZipFile(speicher, "w", zipfile.ZIP_DEFLATED) as zf: for wurzel, unterordner, dateien in os.walk(ordner): unterordner[:] = [o for o in unterordner if o != "_sicherungen"] for datei in dateien: voller_pfad = os.path.join(wurzel, datei) rel_pfad = os.path.relpath(voller_pfad, ordner) zf.write(voller_pfad, rel_pfad) speicher.seek(0) zip_bytes = _zippe_ordner_in_bytes(ordner) zip_name = os.path.basename(ordner.rstrip("\\/")) + ".zip" return send_file(speicher, as_attachment=True, download_name=zip_name, mimetype="application/zip") return send_file( io.BytesIO(zip_bytes), as_attachment=True, download_name=zip_name, mimetype="application/zip" ) if __name__ == "__main__": templates/einrichten.html
@@ -12,17 +12,19 @@ <div class="panel" style="margin-bottom:18px;"> <p style="margin:0 0 8px; font-size:13.5px;"> Häkchen bei den benötigten Tools setzen, Mitarbeiter eintragen und herunterladen. Auf dem <strong>neuen Rechner</strong> das ZIP entpacken und <code>Rechner-einrichten-starten.bat</code> doppelklicken. Alles landet dabei gesammelt unter <code>C:\ClientBibliothek</code> – dafür wird automatisch eine Windows-Defender-Ausnahme für genau diesen einen Ordner eingetragen. Tools mit Installer-Datei werden still installiert (<code>\downloads</code>, danach automatisch aufgeräumt). Tools ohne Installer ("als ZIP bereitgestellt", z. B. portable Tools) landen unangetastet als ZIP in <code>\pakete</code> – die entpackt ihr bewusst von Hand an den gewünschten Zielort. Anschließend trägt das Skript selbst ein Protokoll ein (zentral auf dem Server und lokal unter <code>\protokoll.log</code>). herunterladen. Die ausgewählten Installer-Dateien werden dabei direkt mit ins Paket gepackt – auf dem <strong>neuen Rechner</strong> braucht ihr nur das ZIP zu entpacken und <code>Rechner-einrichten-starten.bat</code> doppelzuklicken, ganz ohne dass der Zielrechner Netzwerkzugriff auf diesen Server haben muss. Alles landet dabei gesammelt unter <code>C:\ClientBibliothek</code> – dafür wird automatisch eine Windows-Defender-Ausnahme für genau diesen einen Ordner eingetragen. Tools mit Installer-Datei werden still installiert. Tools ohne Installer ("als ZIP bereitgestellt", z. B. portable Tools) landen unangetastet als ZIP in <code>\pakete</code> – die entpackt ihr bewusst von Hand an den gewünschten Zielort. Nur die abschließende Meldung ans zentrale Protokoll braucht (optional) Netzwerkzugriff – schlägt das fehl, bleibt trotzdem das lokale Protokoll unter <code>\protokoll.log</code> vollständig. </p> </div>