Server-Bibliothek
vor 18 Std. 98bce1b0da675715b2e5482c0ad61ba21135f62f
ARCHITEKTUR.md
@@ -1,216 +1,207 @@
# Architektur-Dokumentation – Server-Bibliothek
# Architektur
Diese Datei ist für die technische Übergabe gedacht: sie erklärt, wie das
Projekt aufgebaut ist, welche Entscheidungen warum getroffen wurden, und was
bei einer Anpassung an eine andere Infrastruktur zu beachten ist.
Für die Bedienungsanleitung (Endnutzer-Sicht) siehe `README.md`.
Für die Entwicklungshistorie (was wurde wann gebaut) siehe `CHANGELOG.md`.
Die technische Innensicht: wie das Projekt aufgebaut ist, welche Entscheidungen warum so
getroffen wurden und wo die Grenzen liegen. Für Betrieb und Bedienung siehe
[README.md](README.md).
---
## 1. Tech-Stack
## 1. Grundriss
- **Python 3.8+**, **Flask** (Web-Framework) – einzige externe Abhängigkeit
  laut `requirements.txt`
- **SQLite** (`data/bibliothek.db`) – eine einzelne Datei, kein Datenbankserver
- **Server-seitig gerenderte HTML-Templates** (Jinja2), kein JavaScript-Framework,
  kein Build-Prozess – nur Vanilla-JS für Interaktivität (Checklisten,
  Formular-Verhalten)
- Läuft als einfacher Flask-Entwicklungsserver (`app.run(host="0.0.0.0", ...)`)
  – **nicht produktionsgehärtet**, siehe Abschnitt 7 "Bekannte Grenzen"
npm-Workspaces mit drei Paketen. Die Typen liegen bewusst dazwischen: Server und
Oberfläche teilen sich einen Vertrag, den der Übersetzer prüft — ändert sich ein Feld,
bricht die Übersetzung auf beiden Seiten.
Bewusste Design-Entscheidung: keine Frameworks/Build-Tools, damit eine Person
ohne Programmiererfahrung (Zielgruppe dieses Projekts) das System mit einem
Doppelklick starten kann, ohne `npm install` o. Ä.
| Paket | Rolle |
|---|---|
| `bibliothek-types` | Gemeinsame Typen. Kennt weder Server noch Browser |
| `bibliothek-server` | Express, SQLite, Bezugsquellen, Zugriffsgrenze |
| `bibliothek-client` | Oberfläche aus `@dh-software/dh-components` (Lit) |
## 2. Projektstruktur
TypeScript durchgehend im `strict`-Modus.
```
server-bibliothek/
├── app.py                          Haupt-Flask-Anwendung, alle Routen
├── updater.py                      Update-Check-Logik (winget/GitHub/Direkt-Link)
├── automatische_pruefung.py        Eigenständiges Skript für geplante Update-Checks
├── requirements.txt                Python-Abhängigkeiten (nur Flask)
├── data/
│   ├── bibliothek.db               SQLite-Datenbank (wird beim ersten Start angelegt)
│   └── doku_bilder/<software_id>/  Hochgeladene Doku-Screenshots
├── installers/                     Standard-Ablageort für Installer-Dateien
│                                   (Nutzer koennen aber JEDEN Pfad eintragen,
│                                   dieser Ordner ist nur ein Vorschlag)
├── static/style.css                Gesamtes Styling (ein File, keine Build-Pipeline)
├── templates/                      Jinja2-HTML-Templates
├── install.bat                     Einmalige Einrichtung (Python-Abhaengigkeiten)
├── start.bat                       Startet den Webserver
├── pruefe_updates.bat              Manueller Update-Check-Lauf
├── automatisierung_einrichten.bat  Richtet taeglichen Update-Check per Windows-Aufgabenplanung ein
├── firewall-freigabe-einrichten.bat Oeffnet Port 5000 in der Windows-Firewall
├── Update-Quellen-Uebersicht.md    Nachschlagehilfe fuer winget-IDs/GitHub-Pfade
├── README.md                       Bedienungsanleitung
├── ARCHITEKTUR.md                  Diese Datei
└── CHANGELOG.md                    Entwicklungshistorie
### Warum Node und nicht Flask
Der übernommene Prototyp war Flask. Der Umbau auf Node hatte zwei Gründe: Die Oberfläche
sollte auf dem hauseigenen Komponenten-Satz aufsetzen, und der Auslieferungsstand sollte
eine einzelne Programmdatei ohne installierte Laufzeitumgebung sein. Beides zieht Node
nach sich.
### Warum keine Datenbankbibliothek
Ab Node 22 bringt die Laufzeitumgebung SQLite mit (`node:sqlite`). Damit entfällt eine
native Abhängigkeit, die für jede Node-Version neu übersetzt werden müsste — genau daran
war ein erster Versuch mit `better-sqlite3` gescheitert. Journalmodus WAL, damit Lesen
während eines laufenden Abgleichs nicht blockiert.
---
## 2. Bezugsquellen
Das Herzstück. Jede Art, an eine Datei zu kommen, ist eine Klasse hinter derselben
Schnittstelle:
```ts
interface Bezugsquelle {
  gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich>;
}
```
## 3. Datenbankschema
Alle Tabellen werden in `init_db()` (app.py) angelegt. Migrationen (neue
Spalten) laufen automatisch beim Start via `ALTER TABLE ... ADD COLUMN`,
bestehende Daten bleiben dabei erhalten.
### `software` – der Tool-Katalog
| Spalte | Typ | Bedeutung |
|---|---|---|
| id | INTEGER PK | |
| name | TEXT | Anzeigename, wird auch für Dateinamen-Ableitung sanitisiert |
| kategorie | TEXT | Freitext, für Gruppierung in Checklisten |
| version | TEXT | Bei "direkt"-Update-Quelle: technische Kennung (ETag), keine echte Versionsnummer |
| installer_pfad | TEXT | Ordner **oder** Datei-Pfad, siehe Abschnitt 4 "Ordner-Konvention" |
| install_parameter | TEXT | Silent-Install-Flags, z. B. `/S` oder `/quiet /norestart` |
| dokumentation | TEXT | Freitext-Anleitung |
| letzte_pruefung | TEXT (Datum) | Für die Status-Anzeige (Aktuell/Bitte prüfen/Nie geprüft) |
| erstellt_am | TEXT (Zeitstempel) | |
| auto_update | INTEGER (0/1) | Automatische Aktualisierung aktiv? |
| update_quelle | TEXT | `winget` \| `github` \| `direkt` \| leer |
| update_wert | TEXT | Paket-ID / `besitzer/projekt` / URL, je nach Quelle |
### `zuweisungen` – einzelne Tool-Zuweisungen (Zeilen eines Protokolls)
| Spalte | Bedeutung |
| Typ | Vorgehen |
|---|---|
| id, software_id | |
| protokoll_id | FK auf `protokolle` (siehe unten) |
| mitarbeiter, rechnername | Denormalisiert hier UND im Protokoll gespeichert (Absicht: die Installationsverlauf-Ansicht pro Software bleibt dadurch einfach) |
| installierte_version, installiert_von, installiert_am, notiz | |
| `direkt` | HTTP-Abruf eines festen Links |
| `github` | Neuestes Release über die GitHub-Schnittstelle, passendes Asset wählen |
| `winget` | `winget download`, danach `winget show` für das Veröffentlichungsdatum |
| `svn` | `svn export` einer Datei |
| `git` | Repository klonen bzw. aktualisieren, bauen, Ergebnis übernehmen |
| `manuell` | Kein Abruf — die Datei wird einmalig hinterlegt und bleibt liegen |
### `protokolle` – ein Eintrag pro Rechner-Einrichtung
| Spalte | Bedeutung |
|---|---|
| id, mitarbeiter, rechnername, datum, erstellt_von, notiz, erstellt_zeitpunkt | |
Eine neue Quelle anzubinden heißt: eine Klasse schreiben und in der Übersicht eintragen.
Der Rest — Abgleich, Prüfung, Speicherung, Anzeige — bleibt unberührt.
Ein Protokoll fasst mehrere `zuweisungen`-Zeilen zusammen (1 Protokoll : n
Zuweisungen), damit bei z. B. 40 Mitarbeitern × 40 Tools keine unübersichtliche
flache Liste entsteht, sondern eine Übersicht pro Person/Rechner.
### Nichts wird ersetzt, bevor es geprüft ist
### `update_log` – Protokoll jedes automatischen Update-Checks
| Spalte | Bedeutung |
|---|---|
| id, software_id, zeitpunkt, alte_version, neue_version, erfolg (0/1), meldung | |
Der teuerste Fehler wäre, eine funktionierende Datei gegen Müll zu tauschen, weil eine
Quelle stillschweigend etwas anderes liefert als erwartet. Deshalb schreibt keine Quelle
direkt in den Zielordner:
### `doku_bilder` – hochgeladene Screenshots zur Dokumentation
| Spalte | Bedeutung |
|---|---|
| id, software_id, dateiname, hochgeladen_am | Datei liegt unter `data/doku_bilder/<software_id>/<dateiname>` |
1. Herunterladen bzw. bauen in `<zielordner>.neu`
2. `inhaltBeanstanden()` prüft: leer? HTML statt Programmdatei (Signatur und
   Inhaltstyp)? plötzlich unter der Hälfte der bisherigen Größe und kleiner als 100 KB?
3. Erst wenn nichts zu beanstanden ist, wird getauscht — sonst verworfen
## 4. Kernkonzept: Ordner-Konvention (`updater.py`)
Der Eintrag meldet dann das Problem, behält aber seine bisherige Datei. Eine tote Quelle
kostet Aktualität, nie den Download.
Das Feld `installer_pfad` kann zwei Formen haben:
- **Datei-Pfad** (endet auf `.exe`/`.msi`/etc.): ältere/explizite Schreibweise
- **Ordner-Pfad** (kein Dateiname): empfohlene, aktuelle Schreibweise
### Selbstgebaute Werkzeuge
Die Funktion `updater.effektiver_ablageordner(installer_pfad, name)` ist die
zentrale Logik: sie sorgt **immer** dafür, dass jedes Tool in einem eigenen
Unterordner landet (benannt nach dem sanitisierten Tool-Namen), selbst wenn
im Feld nur ein geteilter Basisordner steht oder ein alter kompletter
Dateipfad. Das verhindert, dass mehrere Tools sich einen Ordner teilen und
Dateien "nackt" nebeneinander landen (siehe CHANGELOG, Runden r6–r8, dort
wurde das mehrfach nachgeschärft, nachdem reale Testfälle Lücken zeigten).
Bei `git` beschreibt das Repository seinen eigenen Bau in `software-bibliothek.json`:
`updater.verwaltete_datei_pfad(installer_pfad, name)` findet die aktuell
abgelegte Datei (falls vorhanden), inkl. Rückwärtskompatibilität zu Dateien,
die noch am alten, nicht verschachtelten Ort liegen (Übergangslogik).
```json
{
  "buildCommand": "…",
  "artifact": "dist/MeinWerkzeug.exe",
  "secrets": [{ "name": "…", "description": "…" }]
}
```
**Wichtig für Weiterentwicklung:** Jede neue Stelle im Code, die mit
Installer-Dateien arbeitet, MUSS über diese beiden Funktionen gehen, statt
`installer_pfad` direkt zu verwenden – sonst wird die
Unterordner-Garantie umgangen.
Damit muss die Bibliothek nichts über einzelne Werkzeuge wissen — ein neues anzubinden
erfordert keine Änderung an ihr. Gebaut wird nur, wenn sich der Commit geändert hat.
Zeitgrenze 30 Minuten, weil Bauvorgänge mit vielen Abhängigkeiten lange dauern.
## 5. Automatische Aktualisierung (drei Quellen)
Die Kennung für den Zugriff kommt aus den Einstellungen (`GIT_SSH_COMMAND`), nicht aus der
URL. So bleibt der Katalogeintrag frei von Kontoangaben, und ein Kontowechsel ist eine
einzige Änderung statt einer pro Eintrag.
`updater.pruefe_software(name, quelle, update_wert, aktuelle_version, installer_pfad)`
ist der Einstiegspunkt, delegiert an:
### Bau-Geheimnisse
- **`pruefe_winget`** – ruft `winget show`/`winget download` auf (nur unter
  Windows verfügbar; auf Nicht-Windows-Systemen liefert es einen sauberen
  Fehler statt eines Absturzes)
- **`pruefe_github`** – nutzt die öffentliche GitHub-API
  (`api.github.com/repos/.../releases/latest`), sucht ein `.exe`/`.msi`-Asset
- **`pruefe_direkt`** – fester Download-Link, Versionsvergleich über
  HTTP-`ETag`/`Last-Modified`-Header statt echter Versionsnummer
Werte liegen getrennt vom Katalog in `secrets/<id>.json` und werden beim Bau als
Umgebungsvariablen gesetzt — sie berühren die Platte der Arbeitskopie nie. Das Verfahren
im Ganzen beschreibt der Skill `build-secrets` im Repository `dh-claude-rules`.
Alle drei prüfen zusätzlich, ob die Datei **physisch noch existiert** – auch
bei gleicher Versionsnummer wird neu heruntergeladen, falls die Datei manuell
gelöscht wurde (Bug wurde in Runde r5 gefunden und behoben).
Geschützt sind sie durch Dateiberechtigungen (`icacls`, beschränkt auf SYSTEM,
Administratoren und das Dienstkonto, bei jedem Start neu gesetzt), durch die
Zugriffsgrenze und dadurch, dass sie in keiner aus dem Netzwerk lesbaren Schnittstelle
auftauchen. Eine Verschlüsselung wäre unter Windows nur über die Datenschutz-Schnittstelle
des Betriebssystems sinnvoll; die ist aus der Laufzeitumgebung nicht ohne Zusatzpaket
erreichbar.
Vor jedem Ersetzen einer Datei wird über `sichern_und_ersetzen()` eine
Sicherheitskopie in einen `_sicherungen`-Unterordner gelegt.
---
## 6. "Rechner einrichten" – Verteilungs-Mechanismus
## 3. Zugriffsgrenze
Route `/einrichten` (app.py) erzeugt bei Bedarf ein individuelles
Installations-Paket (ZIP mit `einrichten.ps1` + `Rechner-einrichten-starten.bat`),
statt vorab für jede Mitarbeiter/Tool-Kombination ein festes Skript zu pflegen.
Kein Anmeldeverfahren, sondern eine Grenze an der Netzwerkschnittstelle: Eine Middleware
prüft, ob die Anfrage von `localhost` kommt. Verwaltende Routen tragen sie, lesende nicht.
Ablauf beim Ausführen auf dem Zielrechner:
1. `.bat` kopiert sich selbst + das `.ps1` nach `C:\ClientBibliothek`
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
   entscheiden)
5. Lokales Protokoll unter `C:\ClientBibliothek\protokoll.log`
6. Automatischer POST an `/zuweisungen/neu` auf dem Server, um das zentrale
   Protokoll zu befüllen (Rechnername wird dabei über `$env:COMPUTERNAME`
   automatisch erkannt)
- Frei erreichbar: Katalog lesen, einzeln oder als ZIP herunterladen, Umgebung abfragen
- Nur lokal: Anlegen, Ändern, Löschen, Abgleich anstoßen, Datei hinterlegen, Geheimnisse,
  Export/Import, Einstellungen, Zugriffsprotokoll
Die Ordner-Struktur unter `C:\ClientBibliothek`:
- `\downloads` – transiente Installer-Dateien, werden nach Gebrauch gelöscht
- `\pakete` – ZIPs von portablen Tools, bleiben bewusst liegen
- `\protokoll.log` – lokale Mitschrift
Die Oberfläche fragt beim Start `/api/umgebung` und blendet aus, was ohnehin abgewiesen
würde. Das ist Bequemlichkeit, keine Absicherung — die liegt beim Server.
**Der Client-Ordner-Pfad ist eine Konstante** (`CLIENT_ORDNER` in `app.py`,
aktuell `C:\ClientBibliothek`) – zentral anpassbar, falls die Infrastruktur
einen anderen Standardpfad vorgibt.
Das passt zum Auftrag („Vollzugriff nur am Server") und kommt ohne Benutzerverwaltung aus.
Der Preis: Wer am Server sitzt, darf alles. Rollen und Rechte sind im Ticket bewusst
zurückgestellt.
## 7. Bekannte Grenzen / nicht vollständig getestet
---
- **Keine Authentifizierung.** Es gibt kein Login-System. Jeder, der den
  Server im Netzwerk erreichen kann, kann Software verwalten, Installer
  herunterladen und Installations-Pakete erzeugen. Das war für den internen,
  vertrauenswürdigen Einsatz im Firmennetz eine bewusste Vereinfachung –
  **bei einer Anpassung an eine andere/größere Infrastruktur unbedingt
  prüfen, ob das so bleiben darf**, insbesondere wenn der Server auch von
  außerhalb des Firmennetzes erreichbar sein könnte.
- **Kein HTTPS/TLS.** Die Kommunikation läuft unverschlüsselt über HTTP. Für
  ein internes, abgeschottetes Netzwerk meist unkritisch, sollte aber vor
  einem größeren Rollout bewertet werden.
- **Flask-Entwicklungsserver**, kein WSGI-Produktivserver (z. B. Waitress,
  gunicorn). Für kleine interne Nutzung ausreichend, bei mehr gleichzeitigen
  Nutzern ggf. wechseln.
- **PowerShell-Teile des Verteilungs-Skripts** (Self-Elevation, `msiexec`,
  `Add-MpPreference`) konnten in der Entwicklungsumgebung (Linux-Sandbox)
  nicht Ende-zu-Ende ausgeführt werden, nur mit dem echten PowerShell-Parser
  auf Syntaxfehler geprüft, plus die reinen Netzwerk-Teile
  (Invoke-WebRequest/Invoke-RestMethod, Expand-Archive) wurden live gegen den
  echten Server getestet. Vor größerem Rollout: an 1–2 unkritischen Rechnern
  gegentesten.
- **winget-Pfad der automatischen Aktualisierung** konnte aus demselben
  Grund nicht direkt ausgeführt werden (winget existiert nur unter Windows).
  Der GitHub- und Direkt-Link-Pfad wurden dagegen live gegen echte
  Schnittstellen getestet.
- **Keine mehrsprachige Oberfläche** – komplett auf Deutsch hartkodiert.
## 4. Oberfläche
## 8. Konfigurationspunkte für die Anpassung
Aufgebaut aus `@dh-software/dh-components`, damit das Werkzeug aussieht wie der Rest des
Hauses. Zwei Besonderheiten:
| Was | Wo |
|---|---|
| Client-Ordner auf Zielrechnern | `CLIENT_ORDNER` in `app.py` |
| Port des Webservers | `app.run(..., port=5000)` am Ende von `app.py`, sowie in den `.bat`-Dateien/Firewall-Skript |
| Schwelle "Bitte prüfen"-Warnung | `TAGE_BIS_WARNUNG` in `app.py` (aktuell 30 Tage) |
| Erlaubte Bildformate für Doku-Upload | `ERLAUBTE_BILD_ENDUNGEN` in `app.py` |
| Max. Uploadgröße | `app.config["MAX_CONTENT_LENGTH"]` in `app.py` |
| Versionsmarker (Diagnose, ob neue Version läuft) | `APP_VERSION` in `app.py` |
**Light DOM statt Shadow DOM.** Die Komponenten werden über `createRenderRoot()` ins Light
DOM gerendert, sonst greifen die globalen `::part()`-Regeln des Themes nicht.
**Themewechsel zur Laufzeit.** Zwei `<style>`-Elemente mit den Farbdefinitionen für hell
und dunkel, umgeschaltet über `.disabled`. Drei Modi: System, hell, dunkel. Nach dem
Vorbild des Meeting-Transkriptors, damit sich die Werkzeuge gleich anfühlen.
Der Katalog ist eine kompakte Liste statt Karten — er soll dreistellige Zahlen an
Einträgen aushalten. Reiter trennen lizenzfrei, lizenzpflichtig und intern; die
Mehrfachauswahl wirkt über Reitergrenzen hinweg und lädt als ZIP.
Zusatzinhalte (Sprachpakete, Erweiterungen) hängen über `elternId` an ihrem Werkzeug und
stehen eingerückt darunter, ohne eigene Trennlinie — Werkzeug und Zubehör lesen sich als
ein Block.
---
## 5. Auslieferung als eine Datei
`npm run build:exe` in vier Schritten:
1. TypeScript übersetzen
2. Server mit esbuild zu einer CommonJS-Datei bündeln
3. Die Oberfläche als Einzelressourcen in die SEA-Konfiguration eintragen
4. Node-Programmdatei kopieren und den Blob per `postject` hineinschreiben
**Warum Einzelressourcen und nicht ein eingebettetes Bündel:** Ein früher Versuch hat das
JavaScript-Bündel als Zeichenkette in die Server-Datei eingebettet. Die eingebettete
Schriftart enthält Bytefolgen, die als UTF-8 ungültig sind — sie wurden zu U+FFFD und das
Bündel damit unbrauchbar. Die Seite blieb weiß, ohne Fehlermeldung. Aufgefallen ist es
erst beim Vergleich der ausgelieferten Bytes mit der Datei auf der Platte. Über
`sea.getRawAsset()` kommen die Ressourcen unverändert an.
**Warum esbuild und nicht webpack:** webpack scheitert an einer Abhängigkeit in der
Server-Kette, deren `tsconfig.json` per `extends` auf ein nicht aufgelöstes Paket zeigt.
esbuild kümmert das nicht.
Der ZIP-Erzeuger für Mehrfach-Downloads ist von Hand geschrieben (nur speichernd, ohne
Kompression — Installer sind ohnehin komprimiert). Das spart eine Abhängigkeit in einem
Bündel, das ohne Fremdcode auskommen soll.
---
## 6. Datenmodell
Eine Tabelle `software` und eine Tabelle für das Zugriffsprotokoll. Neue Spalten kommen
über eine Liste hinzu, die beim Start mit dem tatsächlichen Schema abgeglichen wird —
fehlende werden ergänzt. Damit übersteht eine bestehende Installation ein Update ohne
Handarbeit.
Ordnernamen im Datenverzeichnis haben denselben Weg: Eine Umbenennungsliste beim Start
zieht ältere Installationen nach.
Zur Version: Angezeigt wird das **Veröffentlichungsdatum des Anbieters**, nicht der
Abrufzeitpunkt. Nur wenn eine Quelle keins liefert, wird ersatzweise der Abrufzeitpunkt
genannt und als solcher benannt — die Frage „ist das aktuell?" beantwortet das Datum des
Anbieters, unser Abrufzeitpunkt sagt darüber nichts.
---
## 7. Grenzen
- **Kein HTTPS.** Damit fehlt dem Browser der sichere Kontext, und die Schnittstellen für
  Dateizugriff (`showDirectoryPicker`, `crypto.subtle`) stehen an der Netzwerkadresse nicht
  zur Verfügung. Nachgemessen: an `localhost` vorhanden, an der IP nicht.
- **Kein Dienstbetrieb.** Die Programmdatei muss von Hand gestartet werden.
- **Keine automatisierten Tests.** Geprüft wurde bisher von Hand gegen die echten Quellen.
- **Keine Rollen.** Wer am Server sitzt, darf alles.
- **Ein hinterlegtes Geheimnis lässt sich über die Oberfläche nicht einzeln löschen** —
  ein leeres Feld bedeutet „unverändert lassen". Nur über die Schnittstelle mit einer
  Liste, die den Namen weglässt.
- **Bauvorgänge laufen mit den Rechten des Dienstkontos.** Wer einen Bau-Befehl hinterlegen
  kann, kann Code auf dem Server ausführen. Das setzt Vertrauen in die angebundenen
  Repositories voraus.