From 1f44739ba685cd77dc804adcc58fbf584b21e52b Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 06:24:39 +0200
Subject: [PATCH] Replace the Flask prototype with a Node/TypeScript monorepo

---
 .gitignore                                                            |   27 
 package-lock.json                                                     | 6527 ++++++++++++++++++++++++++++++++++
 packages/bibliothek-client/src/globals.d.ts                           |    8 
 packages/bibliothek-server/src/abgleich.ts                            |  108 
 packages/bibliothek-server/src/index.ts                               |  393 ++
 packages/bibliothek-server/src/warnungen.ts                           |   14 
 packages/bibliothek-server/src/bezugsquellen/basis.ts                 |  159 
 packages/bibliothek-types/package.json                                |   10 
 packages/bibliothek-server/src/geheimnisse.ts                         |  127 
 packages/bibliothek-server/src/datenbank.ts                           |  313 +
 packages/bibliothek-server/src/signatur.ts                            |   66 
 packages/bibliothek-client/src/global.css                             |  336 +
 packages/bibliothek-server/src/bezugsquellen/direkt-link.ts           |   40 
 packages/bibliothek-client/src/components/bibliothek-zeile.ts         |  235 +
 packages/bibliothek-server/src/bezugsquellen/winget.ts                |   81 
 packages/bibliothek-client/src/components/bibliothek-einstellungen.ts |  224 +
 packages/bibliothek-client/webpack.config.js                          |   61 
 packages/bibliothek-server/src/zip.ts                                 |  107 
 packages/bibliothek-client/src/components/bibliothek-app.ts           |  345 +
 packages/bibliothek-client/src/index.ts                               |   19 
 packages/bibliothek-types/src/index.ts                                |  160 
 packages/bibliothek-server/src/bezugsquellen/git-build.ts             |  244 +
 packages/bibliothek-server/src/bezugsquellen/index.ts                 |   25 
 packages/bibliothek-server/package.json                               |   21 
 packages/bibliothek-server/src/umzug.ts                               |  131 
 packages/bibliothek-server/src/pfade.ts                               |   45 
 packages/bibliothek-client/src/theme.ts                               |   58 
 packages/bibliothek-server/src/bezugsquellen/svn.ts                   |   53 
 .npmrc                                                                |    1 
 /dev/null                                                             |  371 -
 packages/bibliothek-client/package.json                               |   28 
 packages/bibliothek-types/tsconfig.json                               |   16 
 packages/bibliothek-server/src/bezugsquellen/github.ts                |   95 
 bauen-exe.cjs                                                         |   73 
 packages/bibliothek-client/src/components/bibliothek-formular.ts      |  441 ++
 packages/bibliothek-client/src/index.html                             |   11 
 package.json                                                          |   20 
 packages/bibliothek-server/src/konfiguration.ts                       |  107 
 .prettierrc.json                                                      |    4 
 packages/bibliothek-client/tsconfig.json                              |   15 
 packages/bibliothek-client/src/api-client.ts                          |  130 
 packages/bibliothek-server/tsconfig.json                              |   14 
 42 files changed, 10.882 insertions(+), 381 deletions(-)

diff --git a/.gitignore b/.gitignore
index 9d13436..9869cab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,22 @@
-# Echte Datenbank und Nutzdaten - nicht versionieren, ist Bestand des jeweiligen Servers
-data/bibliothek.db
-data/doku_bilder/
+# Abhaengigkeiten
+node_modules
 
-# Von Nutzern abgelegte Installer-Dateien (Pfad ist frei waehlbar, dieser Ordner ist nur ein Vorschlag)
-installers/*
-!installers/LIESMICH.txt
+# Build-Ausgaben
+dist
+build
+build-exe
+exe/
 
-# Python
-__pycache__/
-*.pyc
+# Logs
+*.log
 
-# Betriebssystem
+# Laufzeit-Daten: Datenbank und geholte Programm-Dateien
+# (gehoeren dem jeweiligen Server, nicht ins Repo)
+data/
+
+# Editor / Betriebssystem / Tooling
+.idea
+.vscode
+.claude
 Thumbs.db
 .DS_Store
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..043daa6
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+@dh-software:registry=https://npm.furnco.de
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..d8ec7c6
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,4 @@
+{
+  "printWidth": 200,
+  "singleQuote": true
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
index 97cb563..0000000
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,148 +0,0 @@
-# Changelog – Server-Bibliothek
-
-Entwicklungshistorie in der Reihenfolge, wie das Projekt entstanden ist.
-Dient als Kontext für die Übergabe: welche Entscheidung wurde warum
-getroffen, welches Problem hat welche Änderung ausgelöst.
-
-## v1 – Erster Prototyp
-Ausgangslage: Software-Installer und Dokumentation lagen verstreut, keine
-Übersicht, wer welche Software auf welchem Rechner hat. Ziel: ein Ort für
-alles.
-- Flask-Webanwendung + SQLite-Datenbank (eine Datei, kein DB-Server)
-- Software-Katalog: Name, Kategorie, Version, Installer-Pfad, Silent-Install-
-  Parameter, Freitext-Dokumentation
-- Status-Anzeige pro Tool: Aktuell / Bitte prüfen / Nie geprüft (Schwelle:
-  30 Tage seit letzter Prüfung) – löst das Kernproblem "Installer-Ordner
-  4 Wochen nicht gepflegt, muss alles neu heruntergeladen werden"
-- Einfache Zuweisungs-Tabelle (ein Eintrag pro Mitarbeiter+Tool)
-- Design bewusst zurückhaltend gehalten (kein Frontend-Framework), damit die
-  Zielperson (keine Programmiererfahrung) es per Doppelklick starten kann
-
-## v2 – Bedienbarkeit für Nicht-Entwickler
-Auslöser: "Ich dachte, ich kann das Tool direkt installieren – ist Python
-nicht nur zum Programmieren da?"
-- `install.bat` / `start.bat`: kein Terminal/Befehle mehr nötig, nur
-  Doppelklick
-- Python-Versionsprüfung mit klarer Fehlermeldung statt kryptischem Absturz
-
-## v3 – Automatische Aktualisierung
-Auslöser: Wunsch, dass sich Standard-Software selbst aktuell hält, ohne
-Verteilung an andere Rechner (das war zu diesem Zeitpunkt explizit
-NICHT gewünscht).
-- Drei Update-Quellen: winget (Windows-Paketverwaltung), GitHub-Releases,
-  fester Download-Link
-- Sicherheitskopie der alten Datei vor jedem Ersetzen
-- Update-Protokoll pro Tool
-- Mini-Doku-Box im Formular, die je nach gewählter Quelle live erklärt, wo
-  man den passenden Wert findet (ersetzt einen langen Erklärtext)
-
-## v4 – Mehrfach-Zuweisung statt Einzel-Protokollierung
-Auslöser: "Wenn ich mehrere Tools gleichzeitig zuweise, will ich nicht für
-jede Software einzeln ein Formular ausfüllen."
-- Checkliste mit "Alle auswählen", ein Formular erzeugt mehrere
-  Zuweisungen auf einmal
-
-## v5 – Protokolle statt flacher Zuweisungsliste
-Auslöser: "Bei 40 Mitarbeitern × 40 Tools wird die Liste unübersichtlich."
-- Neues Datenmodell: `protokolle` (1 Eintrag pro Rechner-Einrichtung) +
-  `zuweisungen` (n Tools pro Protokoll)
-- Download-Button pro Tool (lokale Datei oder externer Link)
-
-## v6 – Ordner-Organisation und Bugfixes (mehrere Anläufe)
-Auslöser: reale Nutzung zeigte, dass Installer-Dateien "nackt" im
-gemeinsamen Ablageordner landeten, weil mehrere Tools denselben Basis-Ordner
-eingetragen hatten.
-- Mehrere Iterationen, bis die Lösung wirklich robust war:
-  1. Automatische Ordner-Erstellung beim Speichern
-  2. Automatische Dateinamens-Vergabe (aus Tool-Namen abgeleitet)
-  3. **Endgültige Lösung:** `effektiver_ablageordner()` erzwingt IMMER einen
-     Tool-eigenen Unterordner, unabhängig davon, was im Feld eingetragen ist
-     – auch rückwirkend, ohne bestehende Einträge ändern zu müssen
-- Bugfix: Wurde eine Installer-Datei manuell gelöscht, meldete das System
-  fälschlich "bereits aktuell" (nur Versionsnummer wurde verglichen, nicht
-  ob die Datei noch existiert) – behoben durch zusätzliche Existenzprüfung
-- Download-Button erkennt jetzt automatisch Ordner vs. Einzeldatei und
-  bietet bei Ordnern den Inhalt als ZIP an (wichtig für portable Tools wie
-  Blender)
-- Versions-Anzeige in der Seitenleiste ergänzt (Diagnose-Hilfe, ob wirklich
-  die neueste Version läuft – nötig geworden, weil mehrfach der Verdacht
-  aufkam, ein Fix "greife nicht", obwohl der laufende Prozess nur nicht neu
-  gestartet worden war)
-
-## v7 – Dokumentations-Bilder
-- Upload/Anzeige/Löschen von Screenshots direkt auf der Tool-Detailseite
-
-## v8 – Verteilung ("Rechner einrichten")
-Auslöser: Rückbesinnung, dass eine Verteilungsfunktion doch gebraucht wird –
-allerdings ohne Remote-Zugriff auf fremde Rechner (das wurde bewusst
-verworfen: zu hoher Aufwand/Risiko ohne vorhandene Domänen-Infrastruktur).
-Stattdessen: Skript, das man AUF dem neuen Zielrechner selbst ausführt.
-- Route `/einrichten`: Checkliste aller Tools, erzeugt bei Bedarf ein
-  individuelles Installations-Paket (kein Pflegeaufwand für 100+ feste
-  Skripte)
-- Generiertes PowerShell-Skript: lädt jedes ausgewählte Tool herunter,
-  installiert es still, meldet danach automatisch ein Protokoll an den
-  Server zurück (Rechnername wird automatisch erkannt)
-- Tools ohne Installer-Datei (portable/entpackte Tools wie Blender) werden
-  als ZIP bereitgestellt – zunächst automatisch entpackt, nach Rückmeldung
-  ("das will ich selbst entscheiden können") bewusst NICHT mehr automatisch
-  entpackt, sondern nur bereitgestellt
-- Fester Client-Ordner `C:\ClientBibliothek` auf dem Zielrechner statt
-  verstreuter Temp-Pfade – ermöglicht eine einzelne Antivirus-/Defender-
-  Ausnahme statt vieler Einzelpfade; wird vom Skript beim ersten Lauf
-  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).
-
-## v9 – Zugriffsbeschränkung lokal (Server) vs. Netzwerk
-Auslöser: Wird der Server über das Netzwerk erreicht (nicht direkt am
-Server), soll nur die Software-Anforderung sichtbar sein, um "Blödsinn" zu
-verhindern (Katalog verändern, Protokolle einsehen usw. nur direkt am
-Server). Wer aus dem Netzwerk Software herunterlädt, muss sich mit Namen
-eintragen, und alle Zugriffe werden protokolliert.
-- Unterscheidung über `request.remote_addr` (127.0.0.1/::1 = lokal, alles
-  andere = Netzwerk)
-- Für Netzwerk-Zugriffe: alle Routen außer der Software-Anforderung
-  (`/einrichten`) geben 403 mit Hinweisseite zurück
-- Software-Anforderung heißt bei Netzwerkzugriff bewusst anders
-  ("Software anfordern" statt "Rechner einrichten") und weist auf die
-  Protokollierung hin
-- Neues Zugriffsprotokoll (`/zugriffslog`, ebenfalls nur lokal einsehbar):
-  Zeitpunkt, IP-Adresse, per Reverse-DNS ermittelter Rechnername (falls
-  auflösbar), selbst angegebener Name, Aktion
-- **Bewusst nicht umgesetzt:** echte Windows-Benutzername-Erkennung. Das
-  bräuchte NTLM/Kerberos-Integration (z. B. IIS-Reverse-Proxy vor Flask),
-  was über reines Flask/HTTP nicht möglich ist – im Architektur-Dokument als
-  offener Punkt festgehalten, nicht stillschweigend als "erledigt" behandelt
-
-## 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
-stattfand, wurde wo immer möglich zusätzlich abgesichert:
-- PowerShell 7 wurde in der Entwicklungsumgebung nachinstalliert, um
-  generierte Skripte mit dem echten Parser auf Syntaxfehler zu prüfen
-- Netzwerk-Operationen (Download, Formular-Rückmeldung, ZIP-Erstellung/
-  -Entpacken) wurden, wo plattformunabhängig möglich, live gegen den
-  laufenden Server ausgeführt statt nur angenommen
-- Windows-exklusive Teile (Admin-Rechte-Anfrage, `msiexec`, `winget`,
-  Defender-Cmdlets) blieben auf Syntaxprüfung + Dokumentations-Abgleich
-  beschränkt – siehe `ARCHITEKTUR.md`, Abschnitt 7, für Details
diff --git a/Update-Quellen-Uebersicht.md b/Update-Quellen-Uebersicht.md
deleted file mode 100644
index 5a1d10c..0000000
--- a/Update-Quellen-Uebersicht.md
+++ /dev/null
@@ -1,89 +0,0 @@
-# Update-Quellen-Übersicht
-
-## So findet ihr die winget-ID für JEDES Programm (egal welches)
-
-Das funktioniert für jedes Tool nach demselben Muster – auch für Kollegen,
-die diese Datei zum ersten Mal sehen:
-
-1. Auf **winstall.app** gehen
-2. Oben den Namen des Programms eintippen (z. B. "chrome", "zoom", "7zip")
-   und das passende Ergebnis anklicken
-3. Auf der Tool-Seite steht ganz oben ein grauer Kasten mit einem Befehl,
-   der immer so aussieht:
-   ```
-   winget install -e --id HIER-STEHT-DIE-ID -v "..."
-   ```
-4. Nur das Wort direkt nach `--id` (bis zum nächsten Leerzeichen) wird
-   gebraucht – der Rest des Befehls (`winget install -e`, `-v "..."`)
-   ist für uns unwichtig
-5. Diesen Wert **exakt** kopieren (markieren + kopieren), **nicht abtippen** –
-   Groß-/Kleinschreibung ist wichtig und nicht einheitlich (mal `7zip.7zip`
-   komplett klein, mal `Google.Chrome` oder `TeamViewer.TeamViewer` gemischt)
-
-**Beispiel:** Steht im Kasten `--id Google.Chrome`, dann ist
-`Google.Chrome` genau der Wert für das Feld "Update-Wert" bei uns im Tool.
-
-Alles andere auf der winstall-Seite (Beschreibung, "Last updated",
-Lizenz, Tags usw.) ist nur Zusatzinfo und kann ignoriert werden.
-
----
-
-Diese Liste hier unten ist zusätzlich eine Starthilfe für besonders
-häufige Tools, damit ihr nicht für jedes Standard-Programm erst selbst
-nachschauen müsst. Sie ist keine Garantie – Paket-IDs können sich ändern,
-im Zweifel immer wie oben beschrieben selbst auf winstall.app nachschauen.
-
-## Über winget (empfohlen für die meisten Standard-Programme)
-
-| Software              | Update-Wert (winget-Paket-ID) |
-|-----------------------|-------------------------------|
-| 7-Zip                 | `7zip.7zip`                    |
-| Adobe Acrobat Reader  | `Adobe.Acrobat.Reader.64-bit`   |
-| Mozilla Firefox       | `Mozilla.Firefox`               |
-| Google Chrome         | `Google.Chrome`                 |
-| Notepad++             | `Notepad++.Notepad++`           |
-| VLC media player      | `VideoLAN.VLC`                  |
-| Zoom                  | `Zoom.Zoom`                      |
-| TeamViewer            | `TeamViewer.TeamViewer`         |
-| PuTTY                 | `PuTTY.PuTTY`                    |
-| Microsoft Teams       | `Microsoft.Teams`                |
-
-So findet ihr selbst eine Paket-ID: in der Eingabeaufforderung
-`winget search notepad` (Name des gesuchten Programms statt "notepad")
-eingeben – in der Spalte "Id" steht der Wert für "Update-Wert".
-
-## Über GitHub-Projekt (für Open-Source-Tools ohne winget-Paket)
-
-Format: `besitzer/projekt` (steht in der Adresse der GitHub-Seite,
-z. B. bei `github.com/videolan/vlc` ist der Wert `videolan/vlc`).
-
-| Software                | Update-Wert (GitHub) |
-|-------------------------|----------------------|
-| 7-Zip                   | `ip7z/7zip` (das ist die offizielle Quelle, die auch winget selbst nutzt) |
-| Notepad++ (alternativ)  | `notepad-plus-plus/notepad-plus-plus` |
-| Everything (Suchtool)   | Hersteller bietet kein GitHub an – winget oder Direkt-Link nutzen |
-| PowerToys               | `microsoft/PowerToys` |
-| WinSCP                  | `winscp/winscp`       |
-
-Wichtig: Es funktioniert nur, wenn das Projekt seine fertigen `.exe`-
-oder `.msi`-Dateien direkt als "Release" auf GitHub anbietet (nicht nur
-den Quellcode).
-
-**Beim Eintragen nur `besitzer/projekt` verwenden, nicht die ganze Adresse**
-(also `ip7z/7zip`, nicht `github.com/ip7z/7zip`). Das Tool erkennt inzwischen
-zwar auch versehentlich eingefügte volle Adressen automatisch, aber sauberer
-ist es trotzdem, gleich nur den kurzen Wert einzutragen.
-
-## Direkter Download-Link
-
-Nur sinnvoll, wenn ein Hersteller eine feste Adresse anbietet, die immer
-zur neuesten Version führt (z. B. manche Portable-Tools oder interne
-Firmen-Tools mit eigenem Ablageort). Hier wird keine echte Versionsnummer
-erkannt, sondern eine technische Kennung des Servers (Änderungsdatum o. Ä.).
-
-## Tools, die absichtlich NICHT automatisch aktualisiert werden sollen
-
-Für interne Tools oder Programme, die bewusst auf einer bestimmten Version
-bleiben sollen: Häkchen bei "Automatische Aktualisierung" einfach weglassen.
-Version und Installer-Datei bleiben dann unangetastet, bis ihr sie manuell
-ändert.
diff --git a/app.py b/app.py
deleted file mode 100644
index 9dff962..0000000
--- a/app.py
+++ /dev/null
@@ -1,1031 +0,0 @@
-import os
-import sqlite3
-import io
-import zipfile
-from datetime import datetime, date
-
-from flask import Flask, render_template, request, redirect, url_for, g, flash, send_file
-from werkzeug.utils import secure_filename
-
-import updater
-
-BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-DB_PATH = os.path.join(BASE_DIR, "data", "bibliothek.db")
-BILDER_DIR = os.path.join(BASE_DIR, "data", "doku_bilder")
-ERLAUBTE_BILD_ENDUNGEN = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
-
-app = Flask(__name__)
-app.secret_key = "server-bibliothek-lokal"  # nur lokal genutzt, kein Login-System
-app.config["MAX_CONTENT_LENGTH"] = 15 * 1024 * 1024  # 15 MB pro Anfrage (Bild-Upload)
-
-# Wird bei jeder Aktualisierung von Claude hochgezaehlt/geaendert - damit
-# 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-23 · r17 (Zugriffsbeschränkung lokal/Netzwerk + Zugriffsprotokoll)"
-
-
-@app.context_processor
-def _version_injizieren():
-    return {"app_version": APP_VERSION, "ist_lokal": ist_lokaler_zugriff()}
-
-# Nach wie vielen Tagen ohne Pruefung ein Tool als "bitte pruefen" markiert wird
-TAGE_BIS_WARNUNG = 30
-
-# ---------------------------------------------------------------------------
-# Zugriffsbeschraenkung: voller Zugriff nur direkt am Server (z. B. per RDP,
-# im Browser "localhost" statt der Netzwerk-IP aufgerufen). Von jedem anderen
-# Rechner im Netzwerk aus ist NUR die eingeschraenkte Software-Anforderung
-# erreichbar - alles andere (Katalog verwalten, Protokolle einsehen usw.)
-# bleibt dem Server selbst vorbehalten.
-# ---------------------------------------------------------------------------
-
-OEFFENTLICHE_ENDPUNKTE = {"einrichten", "static"}
-
-
-def ist_lokaler_zugriff():
-    return request.remote_addr in ("127.0.0.1", "::1")
-
-
-@app.before_request
-def _zugriff_beschraenken():
-    if ist_lokaler_zugriff():
-        return  # voller Zugriff, direkt am Server
-    if request.endpoint in OEFFENTLICHE_ENDPUNKTE:
-        return  # eingeschraenkter Bereich bleibt aus dem Netzwerk erreichbar
-    return render_template("zugriff_eingeschraenkt.html"), 403
-
-
-def protokolliere_zugriff(aktion, angegebener_name=""):
-    """Schreibt einen Eintrag ins Zugriffsprotokoll - IP-Adresse, per
-    Reverse-DNS ermittelter Rechnername (falls aufloesbar) und der selbst
-    angegebene Name. Hinweis: ein echter Windows-Benutzername laesst sich
-    ueber reines HTTP ohne Domänen-/NTLM-Integration nicht zuverlaessig
-    ermitteln, siehe ARCHITEKTUR.md."""
-    import socket
-    ip = request.remote_addr
-    try:
-        rechnername = socket.gethostbyaddr(ip)[0]
-    except Exception:
-        rechnername = None
-    db = get_db()
-    db.execute(
-        """INSERT INTO zugriffslog (zeitpunkt, ip_adresse, rechnername, angegebener_name, aktion, lokal)
-           VALUES (?, ?, ?, ?, ?, ?)""",
-        (
-            datetime.now().isoformat(timespec="seconds"), ip, rechnername, angegebener_name,
-            aktion, 1 if ist_lokaler_zugriff() else 0,
-        ),
-    )
-    db.commit()
-
-
-# ---------------------------------------------------------------------------
-# Datenbank
-# ---------------------------------------------------------------------------
-
-def get_db():
-    if "db" not in g:
-        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
-        g.db = sqlite3.connect(DB_PATH)
-        g.db.row_factory = sqlite3.Row
-        g.db.execute("PRAGMA foreign_keys = ON")
-    return g.db
-
-
-@app.teardown_appcontext
-def close_db(exception=None):
-    db = g.pop("db", None)
-    if db is not None:
-        db.close()
-
-
-def init_db():
-    os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
-    conn = sqlite3.connect(DB_PATH)
-    conn.executescript(
-        """
-        CREATE TABLE IF NOT EXISTS software (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            name TEXT NOT NULL,
-            kategorie TEXT,
-            version TEXT,
-            installer_pfad TEXT,
-            install_parameter TEXT,
-            dokumentation TEXT,
-            letzte_pruefung TEXT,
-            erstellt_am TEXT NOT NULL
-        );
-
-        CREATE TABLE IF NOT EXISTS zuweisungen (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            mitarbeiter TEXT NOT NULL,
-            rechnername TEXT,
-            software_id INTEGER NOT NULL,
-            installierte_version TEXT,
-            installiert_von TEXT,
-            installiert_am TEXT NOT NULL,
-            notiz TEXT,
-            FOREIGN KEY (software_id) REFERENCES software (id) ON DELETE CASCADE
-        );
-
-        CREATE TABLE IF NOT EXISTS update_log (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            software_id INTEGER NOT NULL,
-            zeitpunkt TEXT NOT NULL,
-            alte_version TEXT,
-            neue_version TEXT,
-            erfolg INTEGER NOT NULL,
-            meldung TEXT,
-            FOREIGN KEY (software_id) REFERENCES software (id) ON DELETE CASCADE
-        );
-
-        CREATE TABLE IF NOT EXISTS protokolle (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            mitarbeiter TEXT NOT NULL,
-            rechnername TEXT,
-            datum TEXT NOT NULL,
-            erstellt_von TEXT,
-            notiz TEXT,
-            erstellt_zeitpunkt TEXT NOT NULL
-        );
-
-        CREATE TABLE IF NOT EXISTS doku_bilder (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            software_id INTEGER NOT NULL,
-            dateiname TEXT NOT NULL,
-            hochgeladen_am TEXT NOT NULL,
-            FOREIGN KEY (software_id) REFERENCES software (id) ON DELETE CASCADE
-        );
-
-        CREATE TABLE IF NOT EXISTS zugriffslog (
-            id INTEGER PRIMARY KEY AUTOINCREMENT,
-            zeitpunkt TEXT NOT NULL,
-            ip_adresse TEXT,
-            rechnername TEXT,
-            angegebener_name TEXT,
-            aktion TEXT,
-            lokal INTEGER NOT NULL
-        );
-        """
-    )
-
-    # Migration: Spalten ergaenzen, falls die Datenbank aus einer aelteren
-    # Version stammt (bestehende Daten bleiben dabei unangetastet).
-    vorhandene_spalten = {row[1] for row in conn.execute("PRAGMA table_info(software)")}
-    neue_spalten = {
-        "auto_update": "INTEGER NOT NULL DEFAULT 0",
-        "update_quelle": "TEXT",
-        "update_wert": "TEXT",
-    }
-    for spalte, definition in neue_spalten.items():
-        if spalte not in vorhandene_spalten:
-            conn.execute(f"ALTER TABLE software ADD COLUMN {spalte} {definition}")
-
-    zuweisungen_spalten = {row[1] for row in conn.execute("PRAGMA table_info(zuweisungen)")}
-    if "protokoll_id" not in zuweisungen_spalten:
-        conn.execute("ALTER TABLE zuweisungen ADD COLUMN protokoll_id INTEGER")
-
-    # Rueckwirkend: Eintraege aus der Zeit vor den Protokollen (protokoll_id
-    # noch leer) zu je einem Protokoll pro Mitarbeiter/Rechner/Datum buendeln,
-    # damit auch alte Testdaten in der neuen Ansicht auftauchen.
-    verwaiste = conn.execute(
-        "SELECT DISTINCT mitarbeiter, rechnername, installiert_am FROM zuweisungen WHERE protokoll_id IS NULL"
-    ).fetchall()
-    for mitarbeiter, rechnername, installiert_am in verwaiste:
-        cur = conn.execute(
-            "INSERT INTO protokolle (mitarbeiter, rechnername, datum, erstellt_zeitpunkt) VALUES (?, ?, ?, ?)",
-            (mitarbeiter, rechnername, installiert_am, datetime.now().isoformat(timespec="seconds")),
-        )
-        conn.execute(
-            """UPDATE zuweisungen SET protokoll_id = ?
-               WHERE protokoll_id IS NULL AND mitarbeiter = ? AND rechnername IS ? AND installiert_am = ?""",
-            (cur.lastrowid, mitarbeiter, rechnername, installiert_am),
-        )
-
-    conn.commit()
-    conn.close()
-
-
-# ---------------------------------------------------------------------------
-# Hilfsfunktionen
-# ---------------------------------------------------------------------------
-
-def status_fuer(letzte_pruefung):
-    """Ermittelt Status-Badge (text, css-klasse) anhand des letzten Pruefdatums."""
-    if not letzte_pruefung:
-        return ("Nie geprueft", "status-unbekannt")
-    try:
-        d = datetime.strptime(letzte_pruefung, "%Y-%m-%d").date()
-    except (ValueError, TypeError):
-        return ("Nie geprueft", "status-unbekannt")
-    tage = (date.today() - d).days
-    if tage > TAGE_BIS_WARNUNG:
-        return (f"Bitte pruefen ({tage} Tage)", "status-warnung")
-    return (f"Aktuell (geprueft vor {tage} Tg.)", "status-ok")
-
-
-def heute_str():
-    return date.today().isoformat()
-
-
-def _ps_escape(text):
-    """Escaped einen Text fuer die Verwendung in einem einfach gequoteten
-    PowerShell-String (einzelne Hochkommas werden verdoppelt)."""
-    return (text or "").replace("'", "''")
-
-
-# Fester Ordner auf dem Zielrechner: Downloads, entpackte portable Tools und
-# das lokale Mini-Protokoll landen IMMER hier - ein einziger, vorhersehbarer
-# Pfad, den man z. B. in Windows Defender / der Antivirensoftware als
-# Ausnahme eintragen kann, statt viele verstreute Einzelpfade.
-CLIENT_ORDNER = r"C:\ClientBibliothek"
-
-
-_BAT_LAUNCHER = (
-    "@echo off\r\n"
-    "set ZIEL=C:\\ClientBibliothek\r\n"
-    'if not exist "%ZIEL%" mkdir "%ZIEL%"\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"
-    "echo  Alle Dateien landen unter %ZIEL%\r\n"
-    "echo ============================================================\r\n"
-    "echo.\r\n"
-    'powershell -NoProfile -ExecutionPolicy Bypass -File "%ZIEL%\\einrichten.ps1"\r\n'
-    "pause\r\n"
-)
-
-
-def _erstelle_einrichtungs_skript(server_url, mitarbeiter, tools):
-    """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)}'",
-        f"$mitarbeiter = '{_ps_escape(mitarbeiter)}'",
-        f"$clientOrdner = '{_ps_escape(CLIENT_ORDNER)}'",
-        "",
-        "if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {",
-        '    Start-Process powershell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""',
-        "    exit",
-        "}",
-        "",
-        "$toolsOrdner = Join-Path $clientOrdner \"tools\"",
-        "$paketeOrdner = Join-Path $clientOrdner \"pakete\"",
-        "$protokollDatei = Join-Path $clientOrdner \"protokoll.log\"",
-        "New-Item -ItemType Directory -Force -Path $paketeOrdner | Out-Null",
-        "",
-        "try {",
-        "    Add-MpPreference -ExclusionPath $clientOrdner -ErrorAction Stop",
-        '    Write-Host "Defender-Ausnahme fuer $clientOrdner eingetragen." -ForegroundColor Green',
-        "} catch {",
-        '    Write-Host "Hinweis: Defender-Ausnahme konnte nicht automatisch eingetragen werden (evtl. andere Antivirensoftware oder per Richtlinie gesperrt)." -ForegroundColor Yellow',
-        "}",
-        "",
-        "function Protokolliere($zeile) {",
-        '    $eintrag = (Get-Date -Format "yyyy-MM-dd HH:mm") + " - $mitarbeiter - " + $zeile',
-        "    Add-Content -Path $protokollDatei -Value $eintrag",
-        "}",
-        "",
-        'Write-Host "============================================================"',
-        'Write-Host " Server-Bibliothek - Rechner-Einrichtung fuer $mitarbeiter"',
-        f'Write-Host " Alle Dateien unter {_ps_escape(CLIENT_ORDNER)}"',
-        'Write-Host "============================================================"',
-        'Write-Host ""',
-        "",
-        "$erfolgreicheIds = @()",
-        "",
-        "function InstalliereTool($id, $name, $endung, $parameter) {",
-        '    Write-Host "Installiere: $name ..." -ForegroundColor Cyan',
-        '    $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 \"' + $quelle + '\" ' + $parameter) -Wait",
-        "        } else {",
-        "            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",
-        "    }",
-        '    Protokolliere ($name + ": installiert")',
-        '    Write-Host "  fertig." -ForegroundColor Green',
-        "    return $true",
-        "}",
-        "",
-        "function BereitstellenPortablesTool($id, $name) {",
-        '    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',
-        "    return $true",
-        "}",
-        "",
-    ]
-
-    for t in tools:
-        if t["typ"] == "ordner":
-            zeilen.append(
-                f"if (BereitstellenPortablesTool {t['id']} '{_ps_escape(t['name'])}') "
-                f"{{ $erfolgreicheIds += {t['id']} }}"
-            )
-        else:
-            zeilen.append(
-                f"if (InstalliereTool {t['id']} '{_ps_escape(t['name'])}' '{t['endung']}' "
-                f"'{_ps_escape(t['parameter'])}') {{ $erfolgreicheIds += {t['id']} }}"
-            )
-
-    zeilen += [
-        "",
-        'Write-Host ""',
-        '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")',
-        '$koerper += "&installiert_am=" + (Get-Date -Format "yyyy-MM-dd")',
-        "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 "Zentrales Protokoll auf dem Server aktualisiert." -ForegroundColor Green',
-        "} catch {",
-        '    Write-Host "Kein Netzwerkzugriff auf den Server - zentrales Protokoll nicht aktualisiert (lokales Protokoll unter protokoll.log ist trotzdem vollstaendig)." -ForegroundColor Yellow',
-        "}",
-        "",
-        'Write-Host ""',
-        'Write-Host "============================================================"',
-        f'Write-Host (" Fertig! " + $erfolgreicheIds.Count + " von {len(tools)} Tools installiert.")',
-        'Write-Host "============================================================"',
-        'Read-Host "Enter druecken zum Beenden"',
-    ]
-    return "\r\n".join(zeilen) + "\r\n"
-
-
-@app.route("/zugriffslog")
-def zugriffslog():
-    db = get_db()
-    eintraege = db.execute(
-        "SELECT * FROM zugriffslog ORDER BY zeitpunkt DESC LIMIT 300"
-    ).fetchall()
-    return render_template("zugriffslog.html", eintraege=eintraege)
-
-
-@app.route("/einrichten", methods=["GET", "POST"])
-def einrichten():
-    db = get_db()
-    if request.method == "GET":
-        protokolliere_zugriff("Seite aufgerufen")
-    alle = db.execute(
-        "SELECT id, name, kategorie, version, installer_pfad, install_parameter, auto_update FROM software "
-        "ORDER BY kategorie COLLATE NOCASE, name COLLATE NOCASE"
-    ).fetchall()
-
-    installierbar = []
-    nicht_installierbar = []
-    for sw in alle:
-        pfad = sw["installer_pfad"]
-        if not pfad or pfad.lower().startswith(("http://", "https://")):
-            if pfad:
-                nicht_installierbar.append(sw["name"])
-            continue
-
-        gefunden = updater.verwaltete_datei_pfad(pfad, sw["name"])
-        if gefunden:
-            installierbar.append({
-                "id": sw["id"],
-                "name": sw["name"],
-                "kategorie": sw["kategorie"] or "Ohne Kategorie",
-                "version": sw["version"],
-                "typ": "installer",
-                "endung": os.path.splitext(gefunden)[1].lower(),
-                "parameter": sw["install_parameter"] or "",
-                "installer_pfad": pfad,
-            })
-            continue
-
-        if not sw["auto_update"]:
-            ordner = updater.effektiver_ablageordner(pfad, sw["name"])
-            if os.path.isdir(ordner) and os.listdir(ordner):
-                installierbar.append({
-                    "id": sw["id"],
-                    "name": sw["name"],
-                    "kategorie": sw["kategorie"] or "Ohne Kategorie",
-                    "version": sw["version"],
-                    "typ": "ordner",
-                    "endung": "",
-                    "parameter": "",
-                    "installer_pfad": pfad,
-                })
-                continue
-
-        nicht_installierbar.append(sw["name"])
-
-    if request.method == "POST":
-        mitarbeiter = request.form.get("mitarbeiter", "").strip()
-        ausgewaehlt_ids = set(request.form.getlist("software_ids"))
-        ausgewaehlt = [t for t in installierbar if str(t["id"]) in ausgewaehlt_ids]
-
-        if not mitarbeiter or not ausgewaehlt:
-            flash("Bitte Mitarbeiter angeben und mindestens ein Tool auswaehlen.", "fehler")
-            return redirect(url_for("einrichten"))
-
-        server_url = request.host_url.rstrip("/")
-        ps1_inhalt = _erstelle_einrichtungs_skript(server_url, mitarbeiter, ausgewaehlt)
-
-        protokolliere_zugriff(
-            "Paket heruntergeladen: " + ", ".join(t["name"] for t in ausgewaehlt),
-            angegebener_name=mitarbeiter,
-        )
-
-        speicher = io.BytesIO()
-        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,
-            download_name="rechner-einrichten.zip", mimetype="application/zip",
-        )
-
-    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):
-    """Legt den Tool-eigenen Ordner fuer den angegebenen Installer-Pfad
-    automatisch an, falls er noch nicht existiert. Damit muss nie manuell
-    im Windows-Explorer ein Ordner erstellt werden."""
-    if not installer_pfad or installer_pfad.lower().startswith(("http://", "https://")):
-        return
-    ordner = updater.effektiver_ablageordner(installer_pfad, name)
-    if ordner:
-        try:
-            os.makedirs(ordner, exist_ok=True)
-        except OSError:
-            pass  # z. B. ungueltiges Laufwerk - beim Speichern nicht kritisch
-
-
-app.jinja_env.filters["status"] = status_fuer
-
-
-# ---------------------------------------------------------------------------
-# Routen: Dashboard / Software
-# ---------------------------------------------------------------------------
-
-@app.route("/")
-def dashboard():
-    suche = request.args.get("q", "").strip()
-    db = get_db()
-    if suche:
-        rows = db.execute(
-            """SELECT * FROM software
-               WHERE name LIKE ? OR kategorie LIKE ?
-               ORDER BY name COLLATE NOCASE""",
-            (f"%{suche}%", f"%{suche}%"),
-        ).fetchall()
-    else:
-        rows = db.execute("SELECT * FROM software ORDER BY name COLLATE NOCASE").fetchall()
-
-    gesamt = len(rows)
-    warnungen = sum(1 for r in rows if status_fuer(r["letzte_pruefung"])[1] == "status-warnung")
-    unbekannt = sum(1 for r in rows if status_fuer(r["letzte_pruefung"])[1] == "status-unbekannt")
-    auto_anzahl = sum(1 for r in rows if r["auto_update"])
-
-    return render_template(
-        "dashboard.html",
-        software_liste=rows,
-        suche=suche,
-        gesamt=gesamt,
-        warnungen=warnungen,
-        unbekannt=unbekannt,
-        auto_anzahl=auto_anzahl,
-    )
-
-
-@app.route("/software/neu", methods=["GET", "POST"])
-def software_neu():
-    if request.method == "POST":
-        db = get_db()
-        installer_pfad = request.form.get("installer_pfad", "").strip()
-        auto_update = 1 if request.form.get("auto_update") else 0
-        db.execute(
-            """INSERT INTO software
-               (name, kategorie, version, installer_pfad, install_parameter, dokumentation,
-                letzte_pruefung, erstellt_am, auto_update, update_quelle, update_wert)
-               VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
-            (
-                request.form["name"].strip(),
-                request.form.get("kategorie", "").strip(),
-                request.form.get("version", "").strip(),
-                installer_pfad,
-                request.form.get("install_parameter", "").strip(),
-                request.form.get("dokumentation", "").strip(),
-                heute_str(),
-                datetime.now().isoformat(timespec="seconds"),
-                auto_update,
-                request.form.get("update_quelle", "").strip(),
-                request.form.get("update_wert", "").strip(),
-            ),
-        )
-        db.commit()
-        _ordner_fuer_installer_sicherstellen(installer_pfad, request.form["name"].strip())
-        flash(f"„{request.form['name']}“ wurde hinzugefuegt.", "erfolg")
-        return redirect(url_for("dashboard"))
-    return render_template("software_form.html", software=None)
-
-
-@app.route("/software/<int:software_id>")
-def software_detail(software_id):
-    db = get_db()
-    sw = db.execute("SELECT * FROM software WHERE id = ?", (software_id,)).fetchone()
-    if sw is None:
-        flash("Dieses Tool wurde nicht gefunden.", "fehler")
-        return redirect(url_for("dashboard"))
-    verlauf = db.execute(
-        """SELECT * FROM zuweisungen WHERE software_id = ?
-           ORDER BY installiert_am DESC""",
-        (software_id,),
-    ).fetchall()
-    update_verlauf = db.execute(
-        """SELECT * FROM update_log WHERE software_id = ?
-           ORDER BY zeitpunkt DESC LIMIT 15""",
-        (software_id,),
-    ).fetchall()
-    bilder = db.execute(
-        "SELECT * FROM doku_bilder WHERE software_id = ? ORDER BY hochgeladen_am",
-        (software_id,),
-    ).fetchall()
-    return render_template(
-        "software_detail.html", sw=sw, verlauf=verlauf, update_verlauf=update_verlauf, bilder=bilder
-    )
-
-
-@app.route("/software/<int:software_id>/bearbeiten", methods=["GET", "POST"])
-def software_bearbeiten(software_id):
-    db = get_db()
-    sw = db.execute("SELECT * FROM software WHERE id = ?", (software_id,)).fetchone()
-    if sw is None:
-        flash("Dieses Tool wurde nicht gefunden.", "fehler")
-        return redirect(url_for("dashboard"))
-
-    if request.method == "POST":
-        installer_pfad = request.form.get("installer_pfad", "").strip()
-        auto_update = 1 if request.form.get("auto_update") else 0
-        db.execute(
-            """UPDATE software SET
-               name = ?, kategorie = ?, version = ?, installer_pfad = ?,
-               install_parameter = ?, dokumentation = ?,
-               auto_update = ?, update_quelle = ?, update_wert = ?
-               WHERE id = ?""",
-            (
-                request.form["name"].strip(),
-                request.form.get("kategorie", "").strip(),
-                request.form.get("version", "").strip(),
-                installer_pfad,
-                request.form.get("install_parameter", "").strip(),
-                request.form.get("dokumentation", "").strip(),
-                auto_update,
-                request.form.get("update_quelle", "").strip(),
-                request.form.get("update_wert", "").strip(),
-                software_id,
-            ),
-        )
-        db.commit()
-        _ordner_fuer_installer_sicherstellen(installer_pfad, request.form["name"].strip())
-        flash("Aenderungen gespeichert.", "erfolg")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    return render_template("software_form.html", software=sw)
-
-
-@app.route("/software/<int:software_id>/pruefen", methods=["POST"])
-def software_pruefen(software_id):
-    db = get_db()
-    db.execute(
-        "UPDATE software SET letzte_pruefung = ? WHERE id = ?",
-        (heute_str(), software_id),
-    )
-    db.commit()
-    flash("Als heute geprueft markiert.", "erfolg")
-    return redirect(request.referrer or url_for("dashboard"))
-
-
-def _fuehre_update_check_aus(db, sw_row):
-    """Fuehrt den Update-Check fuer eine Software aus, schreibt das Ergebnis
-    ins Log und aktualisiert bei Erfolg Version/Pruefdatum. Gibt das
-    UpdateResult zurueck."""
-    ergebnis = updater.pruefe_software(
-        sw_row["name"], sw_row["update_quelle"], sw_row["update_wert"], sw_row["version"], sw_row["installer_pfad"]
-    )
-
-    db.execute(
-        """INSERT INTO update_log (software_id, zeitpunkt, alte_version, neue_version, erfolg, meldung)
-           VALUES (?, ?, ?, ?, ?, ?)""",
-        (
-            sw_row["id"],
-            datetime.now().isoformat(timespec="seconds"),
-            ergebnis.alte_version,
-            ergebnis.neue_version,
-            1 if ergebnis.erfolg else 0,
-            ergebnis.meldung,
-        ),
-    )
-
-    if ergebnis.erfolg:
-        # Erfolgreich (egal ob neue Version geholt oder bereits aktuell) ->
-        # Pruefdatum aktualisieren, damit der Status-Badge das widerspiegelt.
-        neue_version = ergebnis.neue_version or sw_row["version"]
-        db.execute(
-            "UPDATE software SET version = ?, letzte_pruefung = ? WHERE id = ?",
-            (neue_version, heute_str(), sw_row["id"]),
-        )
-    # Bei Fehlschlag bewusst NICHT letzte_pruefung aktualisieren, damit das
-    # Tool ueber den Status "Bitte pruefen" sichtbar bleibt.
-
-    db.commit()
-    return ergebnis
-
-
-@app.route("/software/<int:software_id>/update-jetzt", methods=["POST"])
-def software_update_jetzt(software_id):
-    db = get_db()
-    sw = db.execute("SELECT * FROM software WHERE id = ?", (software_id,)).fetchone()
-    if sw is None:
-        flash("Dieses Tool wurde nicht gefunden.", "fehler")
-        return redirect(url_for("dashboard"))
-
-    if not sw["auto_update"] or not sw["update_quelle"]:
-        flash("Fuer dieses Tool ist keine automatische Aktualisierung eingerichtet.", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    ergebnis = _fuehre_update_check_aus(db, sw)
-    kategorie = "erfolg" if ergebnis.erfolg else "fehler"
-    flash(f"{sw['name']}: {ergebnis.meldung}", kategorie)
-    return redirect(url_for("software_detail", software_id=software_id))
-
-
-@app.route("/updates/alle-jetzt", methods=["POST"])
-def updates_alle_jetzt():
-    db = get_db()
-    tools = db.execute("SELECT * FROM software WHERE auto_update = 1 AND update_quelle != ''").fetchall()
-
-    if not tools:
-        flash("Kein Tool hat aktuell die automatische Aktualisierung aktiviert.", "fehler")
-        return redirect(url_for("dashboard"))
-
-    erfolgreich, fehlgeschlagen, aktualisiert = 0, 0, 0
-    for sw in tools:
-        ergebnis = _fuehre_update_check_aus(db, sw)
-        if ergebnis.erfolg:
-            erfolgreich += 1
-            if not ergebnis.unveraendert():
-                aktualisiert += 1
-        else:
-            fehlgeschlagen += 1
-
-    flash(
-        f"Update-Lauf fertig: {len(tools)} geprueft, {aktualisiert} aktualisiert, "
-        f"{erfolgreich - aktualisiert} bereits aktuell, {fehlgeschlagen} fehlgeschlagen.",
-        "erfolg" if fehlgeschlagen == 0 else "fehler",
-    )
-    return redirect(url_for("dashboard"))
-
-
-@app.route("/software/<int:software_id>/loeschen", methods=["POST"])
-def software_loeschen(software_id):
-    db = get_db()
-    db.execute("DELETE FROM software WHERE id = ?", (software_id,))
-    db.commit()
-    flash("Tool wurde geloescht.", "erfolg")
-    return redirect(url_for("dashboard"))
-
-
-# ---------------------------------------------------------------------------
-# Routen: Doku-Bilder
-# ---------------------------------------------------------------------------
-
-@app.route("/software/<int:software_id>/bild-hochladen", methods=["POST"])
-def bild_hochladen(software_id):
-    db = get_db()
-    sw = db.execute("SELECT id FROM software WHERE id = ?", (software_id,)).fetchone()
-    if sw is None:
-        flash("Dieses Tool wurde nicht gefunden.", "fehler")
-        return redirect(url_for("dashboard"))
-
-    datei = request.files.get("bild")
-    if not datei or not datei.filename:
-        flash("Keine Datei ausgewaehlt.", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    endung = os.path.splitext(datei.filename)[1].lower()
-    if endung not in ERLAUBTE_BILD_ENDUNGEN:
-        flash("Nur Bilder erlaubt (PNG, JPG, GIF, WEBP).", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    ordner = os.path.join(BILDER_DIR, str(software_id))
-    os.makedirs(ordner, exist_ok=True)
-
-    sicherer_name = secure_filename(datei.filename) or ("bild" + endung)
-    dateiname = f"{datetime.now().strftime('%Y%m%d%H%M%S%f')}_{sicherer_name}"
-    datei.save(os.path.join(ordner, dateiname))
-
-    db.execute(
-        "INSERT INTO doku_bilder (software_id, dateiname, hochgeladen_am) VALUES (?, ?, ?)",
-        (software_id, dateiname, datetime.now().isoformat(timespec="seconds")),
-    )
-    db.commit()
-    flash("Bild hochgeladen.", "erfolg")
-    return redirect(url_for("software_detail", software_id=software_id))
-
-
-@app.route("/software/<int:software_id>/bild/<int:bild_id>")
-def bild_anzeigen(software_id, bild_id):
-    db = get_db()
-    bild = db.execute(
-        "SELECT * FROM doku_bilder WHERE id = ? AND software_id = ?", (bild_id, software_id)
-    ).fetchone()
-    if bild is None:
-        return "Bild nicht gefunden.", 404
-    pfad = os.path.join(BILDER_DIR, str(software_id), bild["dateiname"])
-    if not os.path.isfile(pfad):
-        return "Bild nicht gefunden.", 404
-    return send_file(pfad)
-
-
-@app.route("/software/<int:software_id>/bild/<int:bild_id>/loeschen", methods=["POST"])
-def bild_loeschen(software_id, bild_id):
-    db = get_db()
-    bild = db.execute(
-        "SELECT * FROM doku_bilder WHERE id = ? AND software_id = ?", (bild_id, software_id)
-    ).fetchone()
-    if bild is not None:
-        pfad = os.path.join(BILDER_DIR, str(software_id), bild["dateiname"])
-        if os.path.isfile(pfad):
-            os.remove(pfad)
-        db.execute("DELETE FROM doku_bilder WHERE id = ?", (bild_id,))
-        db.commit()
-        flash("Bild geloescht.", "erfolg")
-    return redirect(url_for("software_detail", software_id=software_id))
-
-
-@app.errorhandler(413)
-def datei_zu_gross(e):
-    flash("Die Datei ist zu gross (maximal 15 MB pro Bild).", "fehler")
-    return redirect(request.referrer or url_for("dashboard"))
-
-
-# ---------------------------------------------------------------------------
-# Routen: Protokolle (ein Protokoll je Mitarbeiter/Rechner-Einrichtung)
-# ---------------------------------------------------------------------------
-
-@app.route("/protokolle")
-def protokolle():
-    db = get_db()
-    mitarbeiter_filter = request.args.get("mitarbeiter", "").strip()
-    rechner_filter = request.args.get("rechner", "").strip()
-
-    query = """SELECT p.*, COUNT(z.id) AS anzahl_tools
-               FROM protokolle p
-               LEFT JOIN zuweisungen z ON z.protokoll_id = p.id
-               WHERE 1=1"""
-    params = []
-    if mitarbeiter_filter:
-        query += " AND p.mitarbeiter LIKE ?"
-        params.append(f"%{mitarbeiter_filter}%")
-    if rechner_filter:
-        query += " AND p.rechnername LIKE ?"
-        params.append(f"%{rechner_filter}%")
-    query += " GROUP BY p.id ORDER BY p.datum DESC, p.erstellt_zeitpunkt DESC"
-
-    rows = db.execute(query, params).fetchall()
-    return render_template(
-        "protokolle.html",
-        protokolle=rows,
-        mitarbeiter_filter=mitarbeiter_filter,
-        rechner_filter=rechner_filter,
-    )
-
-
-@app.route("/protokolle/<int:protokoll_id>")
-def protokoll_detail(protokoll_id):
-    db = get_db()
-    protokoll = db.execute("SELECT * FROM protokolle WHERE id = ?", (protokoll_id,)).fetchone()
-    if protokoll is None:
-        flash("Dieses Protokoll wurde nicht gefunden.", "fehler")
-        return redirect(url_for("protokolle"))
-
-    eintraege = db.execute(
-        """SELECT z.*, s.name AS software_name FROM zuweisungen z
-           JOIN software s ON s.id = z.software_id
-           WHERE z.protokoll_id = ?
-           ORDER BY s.name COLLATE NOCASE""",
-        (protokoll_id,),
-    ).fetchall()
-    return render_template("protokoll_detail.html", protokoll=protokoll, eintraege=eintraege)
-
-
-@app.route("/protokolle/<int:protokoll_id>/loeschen", methods=["POST"])
-def protokoll_loeschen(protokoll_id):
-    db = get_db()
-    db.execute("DELETE FROM zuweisungen WHERE protokoll_id = ?", (protokoll_id,))
-    db.execute("DELETE FROM protokolle WHERE id = ?", (protokoll_id,))
-    db.commit()
-    flash("Protokoll geloescht.", "erfolg")
-    return redirect(url_for("protokolle"))
-
-
-@app.route("/zuweisungen/neu", methods=["GET", "POST"])
-def zuweisung_neu():
-    db = get_db()
-    protokoll_id = request.values.get("protokoll_id", type=int)
-    bestehendes_protokoll = None
-    if protokoll_id:
-        bestehendes_protokoll = db.execute("SELECT * FROM protokolle WHERE id = ?", (protokoll_id,)).fetchone()
-        if bestehendes_protokoll is None:
-            flash("Dieses Protokoll wurde nicht gefunden.", "fehler")
-            return redirect(url_for("protokolle"))
-
-    if request.method == "POST":
-        software_ids = request.form.getlist("software_ids")
-        if not software_ids:
-            flash("Bitte mindestens ein Tool auswaehlen.", "fehler")
-            return redirect(request.url)
-
-        installiert_von = request.form.get("installiert_von", "").strip()
-        installiert_am = request.form.get("installiert_am") or heute_str()
-        notiz = request.form.get("notiz", "").strip()
-
-        if bestehendes_protokoll:
-            ziel_protokoll_id = bestehendes_protokoll["id"]
-            mitarbeiter = bestehendes_protokoll["mitarbeiter"]
-            rechnername = bestehendes_protokoll["rechnername"]
-        else:
-            mitarbeiter = request.form["mitarbeiter"].strip()
-            rechnername = request.form.get("rechnername", "").strip()
-            cur = db.execute(
-                """INSERT INTO protokolle (mitarbeiter, rechnername, datum, erstellt_von, notiz, erstellt_zeitpunkt)
-                   VALUES (?, ?, ?, ?, ?, ?)""",
-                (mitarbeiter, rechnername, installiert_am, installiert_von, notiz,
-                 datetime.now().isoformat(timespec="seconds")),
-            )
-            ziel_protokoll_id = cur.lastrowid
-
-        anzahl = 0
-        for sw_id in software_ids:
-            sw = db.execute("SELECT version FROM software WHERE id = ?", (sw_id,)).fetchone()
-            if sw is None:
-                continue
-            db.execute(
-                """INSERT INTO zuweisungen
-                   (mitarbeiter, rechnername, software_id, installierte_version, installiert_von,
-                    installiert_am, notiz, protokoll_id)
-                   VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
-                (mitarbeiter, rechnername, sw_id, sw["version"], installiert_von, installiert_am, notiz,
-                 ziel_protokoll_id),
-            )
-            anzahl += 1
-
-        db.commit()
-        flash(f"{anzahl} Tool(s) im Protokoll fuer {mitarbeiter} gespeichert.", "erfolg")
-        return redirect(url_for("protokoll_detail", protokoll_id=ziel_protokoll_id))
-
-    bereits_zugewiesen = set()
-    if bestehendes_protokoll:
-        rows = db.execute(
-            "SELECT software_id FROM zuweisungen WHERE protokoll_id = ?", (protokoll_id,)
-        ).fetchall()
-        bereits_zugewiesen = {r["software_id"] for r in rows}
-
-    software_roh = db.execute(
-        "SELECT id, name, version, kategorie FROM software ORDER BY kategorie COLLATE NOCASE, name COLLATE NOCASE"
-    ).fetchall()
-    software_liste = [
-        {"id": r["id"], "name": r["name"], "version": r["version"], "kategorie": r["kategorie"] or "Ohne Kategorie"}
-        for r in software_roh
-        if r["id"] not in bereits_zugewiesen
-    ]
-    vorausgewaehlt = request.args.get("software_id", type=int)
-    return render_template(
-        "assignment_form.html",
-        software_liste=software_liste,
-        heute=heute_str(),
-        vorausgewaehlt=vorausgewaehlt,
-        bestehendes_protokoll=bestehendes_protokoll,
-    )
-
-
-@app.route("/zuweisungen/<int:zuweisung_id>/loeschen", methods=["POST"])
-def zuweisung_loeschen(zuweisung_id):
-    db = get_db()
-    eintrag = db.execute("SELECT protokoll_id FROM zuweisungen WHERE id = ?", (zuweisung_id,)).fetchone()
-    db.execute("DELETE FROM zuweisungen WHERE id = ?", (zuweisung_id,))
-    db.commit()
-    flash("Eintrag geloescht.", "erfolg")
-    if eintrag and eintrag["protokoll_id"]:
-        return redirect(url_for("protokoll_detail", protokoll_id=eintrag["protokoll_id"]))
-    return redirect(url_for("protokolle"))
-
-
-# ---------------------------------------------------------------------------
-# Route: Installer-Download
-# ---------------------------------------------------------------------------
-
-@app.route("/software/<int:software_id>/download")
-def software_download(software_id):
-    db = get_db()
-    sw = db.execute("SELECT * FROM software WHERE id = ?", (software_id,)).fetchone()
-    if sw is None:
-        flash("Dieses Tool wurde nicht gefunden.", "fehler")
-        return redirect(url_for("dashboard"))
-
-    pfad = sw["installer_pfad"]
-    if not pfad:
-        flash("Fuer dieses Tool ist kein Installer-Pfad hinterlegt.", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    if pfad.lower().startswith(("http://", "https://")):
-        return redirect(pfad)
-
-    # Immer zuerst im Tool-eigenen Unterordner nach der verwalteten Datei
-    # suchen - das gilt jetzt auch fuer aeltere Eintraege mit komplettem
-    # Dateipfad, die frueher noch nicht automatisch verschachtelt wurden.
-    gefunden = updater.verwaltete_datei_pfad(pfad, sw["name"])
-    if gefunden:
-        return send_file(gefunden, as_attachment=True, download_name=os.path.basename(gefunden))
-
-    if sw["auto_update"]:
-        flash("Noch keine Datei vorhanden - zuerst „Jetzt automatisch pruefen“ ausfuehren.", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    # Kein Auto-Update und keine einzelne verwaltete Datei gefunden -> ganzen
-    # Tool-Ordner als ZIP anbieten (Sicherungs-Kopien ausgeschlossen).
-    # Praktisch fuer bereits entpackte/portable Tools (z. B. Blender).
-    ordner = updater.effektiver_ablageordner(pfad, sw["name"])
-    if not os.path.isdir(ordner):
-        flash(f"Ordner nicht gefunden unter: {ordner}", "fehler")
-        return redirect(url_for("software_detail", software_id=software_id))
-
-    zip_bytes = _zippe_ordner_in_bytes(ordner)
-    zip_name = os.path.basename(ordner.rstrip("\\/")) + ".zip"
-    return send_file(
-        io.BytesIO(zip_bytes), as_attachment=True, download_name=zip_name, mimetype="application/zip"
-    )
-
-
-if __name__ == "__main__":
-    init_db()
-    print("=" * 60)
-    print(" Server-Bibliothek laeuft.")
-    print(" Im Browser oeffnen: http://localhost:5000")
-    print(" Beenden mit STRG+C in diesem Fenster.")
-    print("=" * 60)
-    app.run(host="0.0.0.0", port=5000, debug=False)
diff --git a/automatische_pruefung.py b/automatische_pruefung.py
deleted file mode 100644
index a7a9e23..0000000
--- a/automatische_pruefung.py
+++ /dev/null
@@ -1,98 +0,0 @@
-"""
-Fuehrt den automatischen Update-Check fuer alle dafuer markierten Tools aus -
-unabhaengig davon, ob die Weboberffaeche (app.py) gerade laeuft.
-
-Wird genutzt von:
-- pruefe_updates.bat (manueller Lauf per Doppelklick)
-- der Windows-Aufgabenplanung (taeglicher automatischer Lauf, siehe
-  automatisierung_einrichten.bat)
-
-Nutzt dieselbe Datenbank und dieselbe Update-Logik (updater.py) wie die
-Weboberflaeche, damit beide Wege konsistent bleiben.
-"""
-
-import os
-import sqlite3
-from datetime import datetime, date
-
-import updater
-
-BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-DB_PATH = os.path.join(BASE_DIR, "data", "bibliothek.db")
-
-
-def _verbindung():
-    conn = sqlite3.connect(DB_PATH)
-    conn.row_factory = sqlite3.Row
-    conn.execute("PRAGMA foreign_keys = ON")
-    return conn
-
-
-def _heute_str():
-    return date.today().isoformat()
-
-
-def _protokolliere_und_speichere(conn, sw_row, ergebnis):
-    conn.execute(
-        """INSERT INTO update_log (software_id, zeitpunkt, alte_version, neue_version, erfolg, meldung)
-           VALUES (?, ?, ?, ?, ?, ?)""",
-        (
-            sw_row["id"],
-            datetime.now().isoformat(timespec="seconds"),
-            ergebnis.alte_version,
-            ergebnis.neue_version,
-            1 if ergebnis.erfolg else 0,
-            ergebnis.meldung,
-        ),
-    )
-    if ergebnis.erfolg:
-        neue_version = ergebnis.neue_version or sw_row["version"]
-        conn.execute(
-            "UPDATE software SET version = ?, letzte_pruefung = ? WHERE id = ?",
-            (neue_version, _heute_str(), sw_row["id"]),
-        )
-    conn.commit()
-
-
-def pruefe_alle_automatischen():
-    """Prueft alle Tools mit aktivierter automatischer Aktualisierung.
-    Gibt eine Liste von (name, UpdateResult) zurueck."""
-    if not os.path.isfile(DB_PATH):
-        raise SystemExit(
-            f"Datenbank nicht gefunden unter {DB_PATH}. "
-            "Bitte zuerst einmal start.bat ausfuehren, damit sie angelegt wird."
-        )
-
-    conn = _verbindung()
-    tools = conn.execute(
-        "SELECT * FROM software WHERE auto_update = 1 AND update_quelle IS NOT NULL AND update_quelle != ''"
-    ).fetchall()
-
-    ergebnisse = []
-    for sw in tools:
-        ergebnis = updater.pruefe_software(
-            sw["name"], sw["update_quelle"], sw["update_wert"], sw["version"], sw["installer_pfad"]
-        )
-        _protokolliere_und_speichere(conn, sw, ergebnis)
-        ergebnisse.append((sw["name"], ergebnis))
-
-    conn.close()
-    return ergebnisse
-
-
-if __name__ == "__main__":
-    print("=" * 60)
-    print(f" Automatische Update-Pruefung - {datetime.now().strftime('%Y-%m-%d %H:%M')}")
-    print("=" * 60)
-
-    ergebnisse = pruefe_alle_automatischen()
-
-    if not ergebnisse:
-        print("Kein Tool hat die automatische Aktualisierung aktiviert.")
-    else:
-        for name, ergebnis in ergebnisse:
-            status = "OK    " if ergebnis.erfolg else "FEHLER"
-            print(f" [{status}] {name}: {ergebnis.meldung}")
-
-    print("=" * 60)
-    print(" Fertig. Details stehen auch im Update-Protokoll jedes Tools in der Weboberflaeche.")
diff --git a/automatisierung_einrichten.bat b/automatisierung_einrichten.bat
deleted file mode 100644
index d8da997..0000000
--- a/automatisierung_einrichten.bat
+++ /dev/null
@@ -1,40 +0,0 @@
-@echo off
-echo ============================================================
-echo  Automatisierung einrichten
-echo ============================================================
-echo.
-echo  Richtet eine taegliche automatische Update-Pruefung ein
-echo  (standardmaessig um 03:00 Uhr nachts), die auch laeuft, wenn
-echo  gerade niemand die Weboberflaeche geoeffnet hat.
-echo.
-
-where python >nul 2>nul
-if %errorlevel% neq 0 (
-    echo Python wurde nicht gefunden. Bitte zuerst install.bat ausfuehren.
-    pause
-    exit /b
-)
-
-for /f "delims=" %%P in ('where python') do set PYTHONPFAD=%%P
-set SKRIPTPFAD=%~dp0automatische_pruefung.py
-set AUFGABENNAME=ServerBibliothek-AutoUpdate
-
-schtasks /create /tn "%AUFGABENNAME%" /tr "\"%PYTHONPFAD%\" \"%SKRIPTPFAD%\"" /sc daily /st 03:00 /f
-
-if %errorlevel% equ 0 (
-    echo.
-    echo ============================================================
-    echo  Fertig! Ab jetzt laeuft die Pruefung jede Nacht um 03:00 Uhr.
-    echo ============================================================
-    echo.
-    echo  Aendern oder deaktivieren: Windows-Suche - "Aufgabenplanung"
-    echo  oeffnen, dort nach "%AUFGABENNAME%" suchen.
-) else (
-    echo.
-    echo  Es gab ein Problem beim Einrichten.
-    echo  Bitte dieses Fenster als Administrator ausfuehren:
-    echo  Rechtsklick auf automatisierung_einrichten.bat
-    echo  -^> "Als Administrator ausfuehren", dann erneut versuchen.
-)
-
-pause
diff --git a/bauen-exe.cjs b/bauen-exe.cjs
new file mode 100644
index 0000000..3bcfa6d
--- /dev/null
+++ b/bauen-exe.cjs
@@ -0,0 +1,73 @@
+/**
+ * Baut aus dem gebündelten Server und der gebauten Oberfläche eine einzelne
+ * ausführbare Datei (Node SEA). Die Dateien der Oberfläche werden als Assets
+ * eingebettet und zur Laufzeit binärsicher aus der exe ausgeliefert.
+ *
+ * Voraussetzung: `npm run build` und `npm run bundle -w packages/bibliothek-server`.
+ */
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const EXE_NAME = 'Software-Bibliothek.exe';
+const SENTINEL = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
+
+const wurzel = __dirname;
+const oberflaechenOrdner = path.join(wurzel, 'packages', 'bibliothek-client', 'dist');
+const bundleOrdner = path.join(wurzel, 'packages', 'bibliothek-server', 'build-exe');
+const konfigPfad = path.join(bundleOrdner, 'sea-config.json');
+const blobPfad = path.join(bundleOrdner, 'sea-prep.blob');
+const ausgabeOrdner = path.join(wurzel, 'exe');
+const exePfad = path.join(ausgabeOrdner, EXE_NAME);
+
+function schritt(beschreibung, aktion) {
+  process.stdout.write(`${beschreibung} … `);
+  const ergebnis = aktion();
+  console.log(ergebnis ?? 'ok');
+}
+
+schritt('Oberflaechen-Dateien einsammeln', () => {
+  const dateien = fs.readdirSync(oberflaechenOrdner).filter((name) => fs.statSync(path.join(oberflaechenOrdner, name)).isFile());
+  if (!dateien.includes('index.html')) {
+    throw new Error(`In ${oberflaechenOrdner} fehlt index.html — wurde der Client gebaut?`);
+  }
+  const assets = {};
+  for (const name of dateien) {
+    assets[name] = path.relative(wurzel, path.join(oberflaechenOrdner, name)).replace(/\\/g, '/');
+  }
+  fs.writeFileSync(
+    konfigPfad,
+    JSON.stringify(
+      {
+        main: path.relative(wurzel, path.join(bundleOrdner, 'server.cjs')).replace(/\\/g, '/'),
+        output: path.relative(wurzel, blobPfad).replace(/\\/g, '/'),
+        disableExperimentalSEAWarning: true,
+        assets,
+      },
+      null,
+      2
+    )
+  );
+  return `${dateien.length} Datei(en)`;
+});
+
+schritt('Blob erzeugen (Server + eingebettete Oberflaeche)', () => {
+  execFileSync(process.execPath, ['--experimental-sea-config', konfigPfad], { stdio: 'pipe', cwd: wurzel });
+});
+
+schritt('Node-Binary kopieren', () => {
+  fs.mkdirSync(ausgabeOrdner, { recursive: true });
+  fs.copyFileSync(process.execPath, exePfad);
+});
+
+schritt('Code in die exe injizieren', () => {
+  execFileSync('npx', ['--yes', 'postject', exePfad, 'NODE_SEA_BLOB', blobPfad, '--sentinel-fuse', SENTINEL], {
+    stdio: 'pipe',
+    shell: process.platform === 'win32',
+  });
+});
+
+const groesseMb = (fs.statSync(exePfad).size / 1024 / 1024).toFixed(1);
+console.log(`\nFertig: ${exePfad} (${groesseMb} MB)`);
+console.log('Zum Betrieb: exe starten, dann http://localhost:3000 aufrufen.');
+console.log('Die Nutzdaten (Datenbank + geholte Installer) landen im Ordner "data" neben der exe.');
diff --git a/firewall-freigabe-einrichten.bat b/firewall-freigabe-einrichten.bat
deleted file mode 100644
index 866983d..0000000
--- a/firewall-freigabe-einrichten.bat
+++ /dev/null
@@ -1,30 +0,0 @@
-@echo off
-net session >nul 2>&1
-if %errorlevel% neq 0 (
-    echo Administratorrechte werden benoetigt - Fenster wird neu gestartet...
-    powershell -Command "Start-Process '%~f0' -Verb RunAs"
-    exit /b
-)
-
-echo ============================================================
-echo  Firewall-Freigabe fuer die Server-Bibliothek (Port 5000)
-echo ============================================================
-echo.
-
-powershell -Command "New-NetFirewallRule -DisplayName 'Server-Bibliothek' -Direction Inbound -LocalPort 5000 -Protocol TCP -Action Allow -ErrorAction Stop" >nul 2>&1
-
-if %errorlevel% equ 0 (
-    echo Fertig! Port 5000 ist jetzt fuer eingehende Verbindungen freigegeben.
-    echo Andere Rechner im Netzwerk koennen die Bibliothek jetzt erreichen.
-) else (
-    echo Es gab ein Problem beim automatischen Einrichten.
-    echo Bitte manuell in der Windows-Firewall eine eingehende Regel fuer
-    echo TCP-Port 5000 anlegen ^(Windows-Suche -^> "Firewall" -^> "Erweiterte
-    echo Einstellungen" -^> "Eingehende Regeln" -^> "Neue Regel..."^).
-)
-
-echo.
-echo Eure IP-Adresse fuer den Zugriff von anderen Rechnern:
-ipconfig | findstr /C:"IPv4"
-
-pause
diff --git a/install.bat b/install.bat
deleted file mode 100644
index ebe8530..0000000
--- a/install.bat
+++ /dev/null
@@ -1,40 +0,0 @@
-@echo off
-echo ============================================================
-echo  Server-Bibliothek - Einrichtung
-echo ============================================================
-echo.
-
-where python >nul 2>nul
-if %errorlevel% neq 0 (
-    echo Python wurde auf diesem Rechner nicht gefunden.
-    echo.
-    echo Bitte zuerst Python installieren:
-    echo https://www.python.org/downloads/
-    echo.
-    echo WICHTIG: Bei der Installation unten das Kaestchen
-    echo "Add python.exe to PATH" anhaken, bevor auf "Install Now"
-    echo geklickt wird. Danach dieses install.bat erneut starten.
-    echo.
-    pause
-    exit /b
-)
-
-python -c "import sys; exit(0 if sys.version_info >= (3,8) else 1)"
-if %errorlevel% neq 0 (
-    echo Es wurde eine zu alte Python-Version gefunden.
-    echo Bitte eine aktuelle Version installieren:
-    echo https://www.python.org/downloads/
-    echo.
-    pause
-    exit /b
-)
-
-echo Python gefunden ^(passende Version^). Installiere benoetigte Bibliothek...
-echo.
-pip install -r requirements.txt
-
-echo.
-echo ============================================================
-echo  Fertig! Ab jetzt reicht ein Doppelklick auf start.bat
-echo ============================================================
-pause
diff --git a/installers/LIESMICH.txt b/installers/LIESMICH.txt
deleted file mode 100644
index c839478..0000000
--- a/installers/LIESMICH.txt
+++ /dev/null
@@ -1 +0,0 @@
-Hier können die Installer-Dateien (Setup-Dateien) der einzelnen Tools abgelegt werden, z. B. 7zip.exe, adobereader.msi usw. Der Pfad zur jeweiligen Datei wird dann beim Anlegen des Tools im Feld 'Installer-Pfad' eingetragen.
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..154e9b4
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6527 @@
+{
+  "name": "software-bibliothek",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "software-bibliothek",
+      "version": "0.1.0",
+      "workspaces": [
+        "./packages/*"
+      ],
+      "devDependencies": {
+        "postject": "^1.0.0-alpha.6",
+        "prettier": "^3.4.2",
+        "typescript": "^5.7.2"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@dh-software/dh-components": {
+      "version": "3.0.23",
+      "resolved": "https://npm.furnco.de/@dh-software/dh-components/-/dh-components-3.0.23.tgz",
+      "integrity": "sha512-CF5Sj36MliLI0/B/fWbo8+q/an7QPbLwokImoifHS0Y6V6Ovfi88K8It0dHAKbiSwuvJXvO1I8hlfYUnqL+fUw==",
+      "license": "ISC",
+      "peerDependencies": {
+        "@dh-software/furnview-icons": "^1.0.13",
+        "lit": "^3.3.2"
+      }
+    },
+    "node_modules/@dh-software/dh-components-themes": {
+      "version": "1.0.21",
+      "resolved": "https://npm.furnco.de/@dh-software/dh-components-themes/-/dh-components-themes-1.0.21.tgz",
+      "integrity": "sha512-uFAms6McaxKHnbryo9RL84Qtmet882uvXLx7xOfLqy5jTo1kpw8jlTqQD4JsbZpBSrHwXglHNUFWhST+NYUoqA==",
+      "license": "ISC"
+    },
+    "node_modules/@dh-software/furnview-icons": {
+      "version": "1.0.21",
+      "resolved": "https://npm.furnco.de/@dh-software/furnview-icons/-/furnview-icons-1.0.21.tgz",
+      "integrity": "sha512-xKxdBpIcQReJgKpLEOOPXYACejqBrww3XYUoYalWVdo/Sz5Qa/WZXeEN6fvSgIIfMh0bVoEhX82svJKPLhlimA==",
+      "license": "ISC",
+      "peer": true
+    },
+    "node_modules/@dh-software/lit-extensions": {
+      "version": "0.0.14",
+      "resolved": "https://npm.furnco.de/@dh-software/lit-extensions/-/lit-extensions-0.0.14.tgz",
+      "integrity": "sha512-Ik6eZLGn5OqS468i6EirpRHeVpAF1ailTSfpki7qlgXfyS+D414p8i1/cO7xYj9kqQW1cw8hG6u/cQW6q6iEsg==",
+      "license": "ISC",
+      "peerDependencies": {
+        "lit": "^2 || ^3"
+      }
+    },
+    "node_modules/@discoveryjs/json-ext": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz",
+      "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.17.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+      "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+      "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+      "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+      "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+      "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+      "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+      "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+      "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+      "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+      "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+      "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+      "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+      "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+      "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+      "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+      "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+      "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+      "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+      "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+      "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+      "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+      "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+      "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/source-map": {
+      "version": "0.3.11",
+      "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+      "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.25"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@jsonjoy.com/base64": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.67.0.tgz",
+      "integrity": "sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/buffers": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz",
+      "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/codegen": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz",
+      "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-core": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.64.0.tgz",
+      "integrity": "sha512-zs2TAq7Six5jgMuoMNjpspAvOP3mhtgq/k1UyQodEzCtQi/N83y2/y+zcvnZSGp/Rxq96DBN+bValOBQAyn/ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "thingies": "^2.5.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-fsa": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.64.0.tgz",
+      "integrity": "sha512-nMWOVbkLFyEgmXZih3wyvxA9XpgyyqyfrINMHvEFqhi7uqfRl7c9ERJt6yX7vgMPrB9Uo+OJO+Spa0cFzPD01w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-core": "4.64.0",
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "thingies": "^2.5.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.64.0.tgz",
+      "integrity": "sha512-dO+NNkODbUli4uV42bcNrrLvq5rE7SNpdZ5TNd0dtbLsAaNK3MDiIC9lUi+brboGoIjW6vd2fB1qao60nrk5xA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-core": "4.64.0",
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "@jsonjoy.com/fs-print": "4.64.0",
+        "@jsonjoy.com/fs-snapshot": "4.64.0",
+        "glob-to-regex.js": "^1.0.0",
+        "thingies": "^2.5.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node-builtins": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.64.0.tgz",
+      "integrity": "sha512-/o7WRFhUWaM/fOrslwLZGnzn4RmRILykn+lAL+mNObqqRNw+CQSiij6hpCeZ+C7buhdoVo7go/OYqzaSUfDYmA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node-to-fsa": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.64.0.tgz",
+      "integrity": "sha512-WDD9WVs0hb7UAEKTgZW2f66WDrbj7gIIWwpP3spbLyXa0rghtUaFTB8L4gdR3ZCWwiKIsj38/CNijpVmpnuPUw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-fsa": "4.64.0",
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node-utils": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.64.0.tgz",
+      "integrity": "sha512-k5Indsx9hWW9xSF7Y6oSKKwtCUNhzZxadub3owhIlitc+iMRVlPPdX2duTKQWBL3qNWpXya8jykgaaWpheeS4w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "glob-to-regex.js": "^1.0.1"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node/node_modules/@jsonjoy.com/buffers": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
+      "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node/node_modules/@jsonjoy.com/codegen": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
+      "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node/node_modules/@jsonjoy.com/fs-snapshot": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz",
+      "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "^17.65.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "@jsonjoy.com/json-pack": "^17.65.0",
+        "@jsonjoy.com/util": "^17.65.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node/node_modules/@jsonjoy.com/json-pack": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz",
+      "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/base64": "17.67.0",
+        "@jsonjoy.com/buffers": "17.67.0",
+        "@jsonjoy.com/codegen": "17.67.0",
+        "@jsonjoy.com/json-pointer": "17.67.0",
+        "@jsonjoy.com/util": "17.67.0",
+        "hyperdyperid": "^1.2.0",
+        "thingies": "^2.5.0",
+        "tree-dump": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-node/node_modules/@jsonjoy.com/util": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
+      "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "17.67.0",
+        "@jsonjoy.com/codegen": "17.67.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/fs-print": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.64.0.tgz",
+      "integrity": "sha512-PHZFccchvkhWrwPWHjmVAhbC3vSHCtyZvlZfJJ3ho2bnzl450hXri6/8e6pbkWdH+SkmLXNml0sV8e5HDAfxKw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "tree-dump": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/json-pointer": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz",
+      "integrity": "sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/util": "17.67.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/json-pointer/node_modules/@jsonjoy.com/buffers": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
+      "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/json-pointer/node_modules/@jsonjoy.com/codegen": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
+      "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/json-pointer/node_modules/@jsonjoy.com/util": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
+      "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "17.67.0",
+        "@jsonjoy.com/codegen": "17.67.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@jsonjoy.com/util": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz",
+      "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "^1.0.0",
+        "@jsonjoy.com/codegen": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/@leichtgewicht/ip-codec": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
+      "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@lit-labs/ssr-dom-shim": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz",
+      "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@lit/reactive-element": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
+      "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.5.0"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
+      "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@parcel/watcher": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz",
+      "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "detect-libc": "^2.0.3",
+        "is-glob": "^4.0.3",
+        "node-addon-api": "^7.0.0",
+        "picomatch": "^4.0.4"
+      },
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher-android-arm64": "2.6.0",
+        "@parcel/watcher-darwin-arm64": "2.6.0",
+        "@parcel/watcher-darwin-x64": "2.6.0",
+        "@parcel/watcher-freebsd-x64": "2.6.0",
+        "@parcel/watcher-linux-arm-glibc": "2.6.0",
+        "@parcel/watcher-linux-arm-musl": "2.6.0",
+        "@parcel/watcher-linux-arm64-glibc": "2.6.0",
+        "@parcel/watcher-linux-arm64-musl": "2.6.0",
+        "@parcel/watcher-linux-x64-glibc": "2.6.0",
+        "@parcel/watcher-linux-x64-musl": "2.6.0",
+        "@parcel/watcher-win32-arm64": "2.6.0",
+        "@parcel/watcher-win32-x64": "2.6.0"
+      }
+    },
+    "node_modules/@parcel/watcher-android-arm64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz",
+      "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-arm64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz",
+      "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-darwin-x64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz",
+      "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-freebsd-x64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz",
+      "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-glibc": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz",
+      "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm-musl": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz",
+      "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-glibc": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz",
+      "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-arm64-musl": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz",
+      "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-glibc": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz",
+      "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-linux-x64-musl": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz",
+      "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-arm64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz",
+      "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@parcel/watcher-win32-x64": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz",
+      "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/parcel"
+      }
+    },
+    "node_modules/@peculiar/asn1-cms": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz",
+      "integrity": "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "@peculiar/asn1-x509-attr": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-csr": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz",
+      "integrity": "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-ecc": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz",
+      "integrity": "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-pfx": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz",
+      "integrity": "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-cms": "^2.8.0",
+        "@peculiar/asn1-pkcs8": "^2.8.0",
+        "@peculiar/asn1-rsa": "^2.8.0",
+        "@peculiar/asn1-schema": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-pkcs8": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz",
+      "integrity": "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-pkcs9": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz",
+      "integrity": "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-cms": "^2.8.0",
+        "@peculiar/asn1-pfx": "^2.8.0",
+        "@peculiar/asn1-pkcs8": "^2.8.0",
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "@peculiar/asn1-x509-attr": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-rsa": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz",
+      "integrity": "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-schema": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz",
+      "integrity": "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/utils": "^2.0.2",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-x509": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz",
+      "integrity": "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/utils": "^2.0.2",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/asn1-x509-attr": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz",
+      "integrity": "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-schema": "^2.8.0",
+        "@peculiar/asn1-x509": "^2.8.0",
+        "asn1js": "^3.0.10",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/utils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
+      "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@peculiar/x509": {
+      "version": "1.14.3",
+      "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz",
+      "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/asn1-cms": "^2.6.0",
+        "@peculiar/asn1-csr": "^2.6.0",
+        "@peculiar/asn1-ecc": "^2.6.0",
+        "@peculiar/asn1-pkcs9": "^2.6.0",
+        "@peculiar/asn1-rsa": "^2.6.0",
+        "@peculiar/asn1-schema": "^2.6.0",
+        "@peculiar/asn1-x509": "^2.6.0",
+        "pvtsutils": "^1.3.6",
+        "reflect-metadata": "^0.2.2",
+        "tslib": "^2.8.1",
+        "tsyringe": "^4.10.0"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.12",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
+      "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.6",
+      "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+      "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/bonjour": {
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz",
+      "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect-history-api-fallback": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz",
+      "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/express-serve-static-core": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.9",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+      "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/express": {
+      "version": "4.17.25",
+      "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz",
+      "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^4.17.33",
+        "@types/qs": "*",
+        "@types/serve-static": "^1"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "4.19.9",
+      "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz",
+      "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/html-minifier-terser": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+      "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+      "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/http-proxy": {
+      "version": "1.17.17",
+      "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz",
+      "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/mime": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
+      "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.13.3",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+      "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
+    "node_modules/@types/qs": {
+      "version": "6.15.1",
+      "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz",
+      "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/retry": {
+      "version": "0.12.2",
+      "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz",
+      "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-index": {
+      "version": "1.9.4",
+      "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz",
+      "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/express": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "1.15.10",
+      "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz",
+      "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*",
+        "@types/send": "<1"
+      }
+    },
+    "node_modules/@types/serve-static/node_modules/@types/send": {
+      "version": "0.17.6",
+      "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz",
+      "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/mime": "^1",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/sockjs": {
+      "version": "0.3.36",
+      "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
+      "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/trusted-types": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@webassemblyjs/ast": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
+      "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/helper-numbers": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/floating-point-hex-parser": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
+      "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-api-error": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
+      "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-buffer": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
+      "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-numbers": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
+      "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/floating-point-hex-parser": "1.13.2",
+        "@webassemblyjs/helper-api-error": "1.13.2",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/helper-wasm-bytecode": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
+      "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/helper-wasm-section": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
+      "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/wasm-gen": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/ieee754": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
+      "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@xtuc/ieee754": "^1.2.0"
+      }
+    },
+    "node_modules/@webassemblyjs/leb128": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
+      "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webassemblyjs/utf8": {
+      "version": "1.13.2",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
+      "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@webassemblyjs/wasm-edit": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
+      "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/helper-wasm-section": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-opt": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1",
+        "@webassemblyjs/wast-printer": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-gen": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
+      "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-opt": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
+      "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-buffer": "1.14.1",
+        "@webassemblyjs/wasm-gen": "1.14.1",
+        "@webassemblyjs/wasm-parser": "1.14.1"
+      }
+    },
+    "node_modules/@webassemblyjs/wasm-parser": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
+      "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@webassemblyjs/helper-api-error": "1.13.2",
+        "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
+        "@webassemblyjs/ieee754": "1.13.2",
+        "@webassemblyjs/leb128": "1.13.2",
+        "@webassemblyjs/utf8": "1.13.2"
+      }
+    },
+    "node_modules/@webassemblyjs/wast-printer": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
+      "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@webassemblyjs/ast": "1.14.1",
+        "@xtuc/long": "4.2.2"
+      }
+    },
+    "node_modules/@webpack-cli/configtest": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz",
+      "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/info": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz",
+      "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
+      }
+    },
+    "node_modules/@webpack-cli/serve": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz",
+      "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "peerDependencies": {
+        "webpack": "^5.82.0",
+        "webpack-cli": "6.x.x"
+      },
+      "peerDependenciesMeta": {
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@xtuc/ieee754": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
+      "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@xtuc/long": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
+      "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.17.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+      "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.5",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
+      "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "8.20.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+      "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
+      "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ajv-keywords": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
+      "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3"
+      },
+      "peerDependencies": {
+        "ajv": "^8.8.2"
+      }
+    },
+    "node_modules/ansi-html-community": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz",
+      "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==",
+      "dev": true,
+      "engines": [
+        "node >= 0.8.0"
+      ],
+      "license": "Apache-2.0",
+      "bin": {
+        "ansi-html": "bin/ansi-html"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+      "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/anymatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/asn1js": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz",
+      "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "pvtsutils": "^1.3.6",
+        "pvutils": "^1.1.5",
+        "tslib": "^2.8.1"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/baseline-browser-mapping": {
+      "version": "2.11.1",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
+      "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/batch": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz",
+      "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/bibliothek-client": {
+      "resolved": "packages/bibliothek-client",
+      "link": true
+    },
+    "node_modules/bibliothek-server": {
+      "resolved": "packages/bibliothek-server",
+      "link": true
+    },
+    "node_modules/bibliothek-types": {
+      "resolved": "packages/bibliothek-types",
+      "link": true
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+      "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.6",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz",
+      "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.15.1",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/bonjour-service": {
+      "version": "1.4.3",
+      "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.4.3.tgz",
+      "integrity": "sha512-2Kd5UYlFUVgAKMTyuBLl6w49wqfOnbxHqmuH0oCl/n7TfAikR0zoowNOP5BU4dfXmm+Vr9JyEN370auSMx+CNg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "multicast-dns": "^7.2.5"
+      }
+    },
+    "node_modules/boolbase": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+      "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.7",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
+      "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "baseline-browser-mapping": "^2.10.44",
+        "caniuse-lite": "^1.0.30001806",
+        "electron-to-chromium": "^1.5.393",
+        "node-releases": "^2.0.51",
+        "update-browserslist-db": "^1.2.3"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-from": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+      "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/bundle-name": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+      "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "run-applescript": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/bytestreamjs": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/bytestreamjs/-/bytestreamjs-2.0.1.tgz",
+      "integrity": "sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/camel-case": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
+      "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pascal-case": "^3.1.2",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001806",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
+      "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "CC-BY-4.0"
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/chokidar": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+      "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readdirp": "^5.0.0"
+      },
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/chrome-trace-event": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
+      "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0"
+      }
+    },
+    "node_modules/clean-css": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
+      "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "source-map": "~0.6.0"
+      },
+      "engines": {
+        "node": ">= 10.0"
+      }
+    },
+    "node_modules/clone-deep": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
+      "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-plain-object": "^2.0.4",
+        "kind-of": "^6.0.2",
+        "shallow-clone": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
+      "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/compressible": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+      "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": ">= 1.43.0 < 2"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/compression": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
+      "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "compressible": "~2.0.18",
+        "debug": "2.6.9",
+        "negotiator": "~0.6.4",
+        "on-headers": "~1.1.0",
+        "safe-buffer": "5.2.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/compression/node_modules/negotiator": {
+      "version": "0.6.4",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
+      "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/connect-history-api-fallback": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
+      "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/construct-style-sheets-polyfill": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/construct-style-sheets-polyfill/-/construct-style-sheets-polyfill-3.1.0.tgz",
+      "integrity": "sha512-HBLKP0chz8BAY6rBdzda11c3wAZeCZ+kIG4weVC2NM3AXzxx09nhe8t0SQNdloAvg5GLuHwq/0SPOOSPvtCcKw==",
+      "license": "MIT"
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/core-util-is": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+      "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/css-loader": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz",
+      "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "icss-utils": "^5.1.0",
+        "postcss": "^8.4.40",
+        "postcss-modules-extract-imports": "^3.1.0",
+        "postcss-modules-local-by-default": "^4.0.5",
+        "postcss-modules-scope": "^3.2.0",
+        "postcss-modules-values": "^4.0.0",
+        "postcss-value-parser": "^4.2.0",
+        "semver": "^7.6.3"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+        "webpack": "^5.27.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/css-select": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz",
+      "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0",
+        "css-what": "^6.0.1",
+        "domhandler": "^4.3.1",
+        "domutils": "^2.8.0",
+        "nth-check": "^2.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/css-what": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+      "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">= 6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/fb55"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+      "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/default-browser": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz",
+      "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "bundle-name": "^4.1.0",
+        "default-browser-id": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/default-browser-id": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
+      "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+      "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+      "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/detect-node": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz",
+      "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/diff": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
+      "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/dns-packet": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
+      "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@leichtgewicht/ip-codec": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/dom-converter": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
+      "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "utila": "~0.4"
+      }
+    },
+    "node_modules/dom-serializer": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
+      "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.2.0",
+        "entities": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
+      "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "^2.2.0"
+      },
+      "engines": {
+        "node": ">= 4"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domhandler?sponsor=1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
+      "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "^1.0.1",
+        "domelementtype": "^2.2.0",
+        "domhandler": "^4.2.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/domutils?sponsor=1"
+      }
+    },
+    "node_modules/dot-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
+      "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.396",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.396.tgz",
+      "integrity": "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/enhanced-resolve": {
+      "version": "5.24.3",
+      "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz",
+      "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.4",
+        "tapable": "^2.3.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/entities": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/envinfo": {
+      "version": "7.21.0",
+      "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz",
+      "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "envinfo": "dist/cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
+      "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.12",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+      "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.12",
+        "@esbuild/android-arm": "0.25.12",
+        "@esbuild/android-arm64": "0.25.12",
+        "@esbuild/android-x64": "0.25.12",
+        "@esbuild/darwin-arm64": "0.25.12",
+        "@esbuild/darwin-x64": "0.25.12",
+        "@esbuild/freebsd-arm64": "0.25.12",
+        "@esbuild/freebsd-x64": "0.25.12",
+        "@esbuild/linux-arm": "0.25.12",
+        "@esbuild/linux-arm64": "0.25.12",
+        "@esbuild/linux-ia32": "0.25.12",
+        "@esbuild/linux-loong64": "0.25.12",
+        "@esbuild/linux-mips64el": "0.25.12",
+        "@esbuild/linux-ppc64": "0.25.12",
+        "@esbuild/linux-riscv64": "0.25.12",
+        "@esbuild/linux-s390x": "0.25.12",
+        "@esbuild/linux-x64": "0.25.12",
+        "@esbuild/netbsd-arm64": "0.25.12",
+        "@esbuild/netbsd-x64": "0.25.12",
+        "@esbuild/openbsd-arm64": "0.25.12",
+        "@esbuild/openbsd-x64": "0.25.12",
+        "@esbuild/openharmony-arm64": "0.25.12",
+        "@esbuild/sunos-x64": "0.25.12",
+        "@esbuild/win32-arm64": "0.25.12",
+        "@esbuild/win32-ia32": "0.25.12",
+        "@esbuild/win32-x64": "0.25.12"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/eslint-scope": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+      "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/esrecurse/node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/events": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+      "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.x"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.2",
+      "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
+      "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.5",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.15.1",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
+      "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastest-levenshtein": {
+      "version": "1.0.16",
+      "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+      "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4.9.1"
+      }
+    },
+    "node_modules/faye-websocket": {
+      "version": "0.11.4",
+      "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+      "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "websocket-driver": ">=0.5.1"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/flat": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+      "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "bin": {
+        "flat": "cli.js"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.16.0",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+      "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/glob-to-regex.js": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz",
+      "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/handle-thing": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
+      "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/he": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+      "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "he": "bin/he"
+      }
+    },
+    "node_modules/hpack.js": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz",
+      "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.1",
+        "obuf": "^1.0.0",
+        "readable-stream": "^2.0.1",
+        "wbuf": "^1.1.0"
+      }
+    },
+    "node_modules/hpack.js/node_modules/readable-stream": {
+      "version": "2.3.8",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+      "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "core-util-is": "~1.0.0",
+        "inherits": "~2.0.3",
+        "isarray": "~1.0.0",
+        "process-nextick-args": "~2.0.0",
+        "safe-buffer": "~5.1.1",
+        "string_decoder": "~1.1.1",
+        "util-deprecate": "~1.0.1"
+      }
+    },
+    "node_modules/hpack.js/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/hpack.js/node_modules/string_decoder": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+      "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.1.0"
+      }
+    },
+    "node_modules/html-minifier-terser": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
+      "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "camel-case": "^4.1.2",
+        "clean-css": "^5.2.2",
+        "commander": "^8.3.0",
+        "he": "^1.2.0",
+        "param-case": "^3.0.4",
+        "relateurl": "^0.2.7",
+        "terser": "^5.10.0"
+      },
+      "bin": {
+        "html-minifier-terser": "cli.js"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/html-webpack-plugin": {
+      "version": "5.6.7",
+      "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz",
+      "integrity": "sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/html-minifier-terser": "^6.0.0",
+        "html-minifier-terser": "^6.0.2",
+        "lodash": "^4.17.21",
+        "pretty-error": "^4.0.0",
+        "tapable": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/html-webpack-plugin"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || 1.x",
+        "webpack": "^5.20.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/htmlparser2": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
+      "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
+      "dev": true,
+      "funding": [
+        "https://github.com/fb55/htmlparser2?sponsor=1",
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "domhandler": "^4.0.0",
+        "domutils": "^2.5.2",
+        "entities": "^2.0.0"
+      }
+    },
+    "node_modules/http-deceiver": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz",
+      "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/http-parser-js": {
+      "version": "0.5.10",
+      "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
+      "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/http-proxy": {
+      "version": "1.18.1",
+      "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+      "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eventemitter3": "^4.0.0",
+        "follow-redirects": "^1.0.0",
+        "requires-port": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/http-proxy-middleware": {
+      "version": "2.0.10",
+      "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz",
+      "integrity": "sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-proxy": "^1.17.8",
+        "http-proxy": "^1.18.1",
+        "is-glob": "^4.0.1",
+        "is-plain-obj": "^3.0.0",
+        "micromatch": "^4.0.2"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "@types/express": "^4.17.13"
+      },
+      "peerDependenciesMeta": {
+        "@types/express": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/hyperdyperid": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
+      "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.18"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/icss-utils": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
+      "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/immutable": {
+      "version": "5.1.9",
+      "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz",
+      "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/import-local": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
+      "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pkg-dir": "^4.2.0",
+        "resolve-cwd": "^3.0.0"
+      },
+      "bin": {
+        "import-local-fixture": "fixtures/cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/interpret": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
+      "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+      "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.2",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+      "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/is-docker": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+      "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-inside-container": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+      "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^3.0.0"
+      },
+      "bin": {
+        "is-inside-container": "cli.js"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-network-error": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.2.tgz",
+      "integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/is-plain-obj": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
+      "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-plain-object": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
+      "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "isobject": "^3.0.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz",
+      "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-inside-container": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/isarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+      "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/isobject": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+      "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/jest-worker": {
+      "version": "27.5.1",
+      "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
+      "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "merge-stream": "^2.0.0",
+        "supports-color": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/jest-worker/node_modules/supports-color": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+      "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/supports-color?sponsor=1"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/kind-of": {
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+      "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/launch-editor": {
+      "version": "2.14.1",
+      "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz",
+      "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picocolors": "^1.1.1",
+        "shell-quote": "^1.8.4"
+      }
+    },
+    "node_modules/lit": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz",
+      "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==",
+      "license": "BSD-3-Clause",
+      "peer": true,
+      "dependencies": {
+        "@lit/reactive-element": "^2.1.0",
+        "lit-element": "^4.2.0",
+        "lit-html": "^3.3.0"
+      }
+    },
+    "node_modules/lit-element": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
+      "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@lit-labs/ssr-dom-shim": "^1.5.0",
+        "@lit/reactive-element": "^2.1.0",
+        "lit-html": "^3.3.0"
+      }
+    },
+    "node_modules/lit-html": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz",
+      "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@types/trusted-types": "^2.0.2"
+      }
+    },
+    "node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.18.1",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+      "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lower-case": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
+      "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/memfs": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.64.0.tgz",
+      "integrity": "sha512-Kw72fgY7Wn+sD8KmtNWSafl1dz0UvAsE/PHs3YVfLiaZuA3HxNm9sRLqAu0ATiBGJvME1PxZXbBZPv5GycDeAw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/fs-core": "4.64.0",
+        "@jsonjoy.com/fs-fsa": "4.64.0",
+        "@jsonjoy.com/fs-node": "4.64.0",
+        "@jsonjoy.com/fs-node-builtins": "4.64.0",
+        "@jsonjoy.com/fs-node-to-fsa": "4.64.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "@jsonjoy.com/fs-print": "4.64.0",
+        "@jsonjoy.com/fs-snapshot": "4.64.0",
+        "@jsonjoy.com/json-pack": "^1.11.0",
+        "@jsonjoy.com/util": "^1.9.0",
+        "glob-to-regex.js": "^1.0.1",
+        "thingies": "^2.5.0",
+        "tree-dump": "^1.0.3",
+        "tslib": "^2.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/fs-snapshot": {
+      "version": "4.64.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.64.0.tgz",
+      "integrity": "sha512-oM7UDeL83q6NBzzsfKAsYKXKVXlykKFqqOLh4xZZKAzzROTlInkPbc6LTDGThEOnPiFiUzA7tYziHG9xavd76Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "^17.65.0",
+        "@jsonjoy.com/fs-node-utils": "4.64.0",
+        "@jsonjoy.com/json-pack": "^17.65.0",
+        "@jsonjoy.com/util": "^17.65.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/buffers": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz",
+      "integrity": "sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz",
+      "integrity": "sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz",
+      "integrity": "sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/base64": "17.67.0",
+        "@jsonjoy.com/buffers": "17.67.0",
+        "@jsonjoy.com/codegen": "17.67.0",
+        "@jsonjoy.com/json-pointer": "17.67.0",
+        "@jsonjoy.com/util": "17.67.0",
+        "hyperdyperid": "^1.2.0",
+        "thingies": "^2.5.0",
+        "tree-dump": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": {
+      "version": "17.67.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.67.0.tgz",
+      "integrity": "sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/buffers": "17.67.0",
+        "@jsonjoy.com/codegen": "17.67.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/json-pack": {
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz",
+      "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/base64": "^1.1.2",
+        "@jsonjoy.com/buffers": "^1.2.0",
+        "@jsonjoy.com/codegen": "^1.0.0",
+        "@jsonjoy.com/json-pointer": "^1.0.2",
+        "@jsonjoy.com/util": "^1.9.0",
+        "hyperdyperid": "^1.2.0",
+        "thingies": "^2.5.0",
+        "tree-dump": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/base64": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
+      "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/memfs/node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/json-pointer": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz",
+      "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@jsonjoy.com/codegen": "^1.0.0",
+        "@jsonjoy.com/util": "^1.9.0"
+      },
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/merge-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+      "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/micromatch/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/minimizer-webpack-plugin": {
+      "version": "5.6.1",
+      "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz",
+      "integrity": "sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "^0.3.25",
+        "jest-worker": "^27.4.5",
+        "schema-utils": "^4.3.0",
+        "terser": "^5.31.1"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.1.0"
+      },
+      "peerDependenciesMeta": {
+        "@minify-html/node": {
+          "optional": true
+        },
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/css": {
+          "optional": true
+        },
+        "@swc/html": {
+          "optional": true
+        },
+        "clean-css": {
+          "optional": true
+        },
+        "cssnano": {
+          "optional": true
+        },
+        "csso": {
+          "optional": true
+        },
+        "esbuild": {
+          "optional": true
+        },
+        "html-minifier-terser": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "postcss": {
+          "optional": true
+        },
+        "uglify-js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/multicast-dns": {
+      "version": "7.2.5",
+      "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz",
+      "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dns-packet": "^5.2.2",
+        "thunky": "^1.0.2"
+      },
+      "bin": {
+        "multicast-dns": "cli.js"
+      }
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.16",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+      "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/neo-async": {
+      "version": "2.6.2",
+      "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+      "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/no-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
+      "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lower-case": "^2.0.2",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+      "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.51",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
+      "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+      "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/nth-check": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+      "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "boolbase": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/nth-check?sponsor=1"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/obuf": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
+      "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+      "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/open": {
+      "version": "10.2.0",
+      "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+      "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "default-browser": "^5.2.1",
+        "define-lazy-prop": "^3.0.0",
+        "is-inside-container": "^1.0.0",
+        "wsl-utils": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/p-retry": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz",
+      "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/retry": "0.12.2",
+        "is-network-error": "^1.0.0",
+        "retry": "^0.13.1"
+      },
+      "engines": {
+        "node": ">=16.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/param-case": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
+      "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dot-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/pascal-case": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
+      "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "no-case": "^3.0.4",
+        "tslib": "^2.0.3"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+      "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+      "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+      "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkijs": {
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/pkijs/-/pkijs-3.4.0.tgz",
+      "integrity": "sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@noble/hashes": "1.4.0",
+        "asn1js": "^3.0.6",
+        "bytestreamjs": "^2.0.1",
+        "pvtsutils": "^1.3.6",
+        "pvutils": "^1.1.3",
+        "tslib": "^2.8.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.22",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
+      "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "nanoid": "^3.3.16",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-modules-extract-imports": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz",
+      "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-local-by-default": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz",
+      "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "icss-utils": "^5.0.0",
+        "postcss-selector-parser": "^7.0.0",
+        "postcss-value-parser": "^4.1.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-scope": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz",
+      "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "postcss-selector-parser": "^7.0.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-modules-values": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz",
+      "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "icss-utils": "^5.0.0"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >= 14"
+      },
+      "peerDependencies": {
+        "postcss": "^8.1.0"
+      }
+    },
+    "node_modules/postcss-selector-parser": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
+      "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cssesc": "^3.0.0",
+        "util-deprecate": "^1.0.2"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/postject": {
+      "version": "1.0.0-alpha.6",
+      "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
+      "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commander": "^9.4.0"
+      },
+      "bin": {
+        "postject": "dist/cli.js"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/postject/node_modules/commander": {
+      "version": "9.5.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
+      "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.20.0 || >=14"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.9.6",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+      "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/pretty-error": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
+      "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lodash": "^4.17.20",
+        "renderkid": "^3.0.0"
+      }
+    },
+    "node_modules/process-nextick-args": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/pvtsutils": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
+      "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/pvutils": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz",
+      "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.3",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
+      "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "es-define-property": "^1.0.1",
+        "side-channel": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
+      "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 20.19.0"
+      },
+      "funding": {
+        "type": "individual",
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/rechoir": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
+      "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve": "^1.20.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      }
+    },
+    "node_modules/reflect-metadata": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
+      "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/relateurl": {
+      "version": "0.2.7",
+      "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
+      "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/renderkid": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
+      "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "css-select": "^4.1.3",
+        "dom-converter": "^0.2.0",
+        "htmlparser2": "^6.1.0",
+        "lodash": "^4.17.21",
+        "strip-ansi": "^6.0.1"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/requires-port": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+      "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/resolve": {
+      "version": "1.22.12",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+      "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "is-core-module": "^2.16.1",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/resolve-cwd": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+      "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-from": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+      "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/retry": {
+      "version": "0.13.1",
+      "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
+      "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/run-applescript": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+      "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/sass": {
+      "version": "1.101.7",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.7.tgz",
+      "integrity": "sha512-cDeUYU0dhwKVbpYg/ppsjyuoddxYhWlJOkRoI7+/iZsaSp7iowWDfm+tL2HcUafedWBDvbf/+hx0QRgKG4JSHA==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "chokidar": "^5.0.0",
+        "immutable": "^5.1.5",
+        "source-map-js": ">=0.6.2 <2.0.0"
+      },
+      "bin": {
+        "sass": "sass.js"
+      },
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "optionalDependencies": {
+        "@parcel/watcher": "^2.4.1"
+      }
+    },
+    "node_modules/sass-loader": {
+      "version": "16.0.8",
+      "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.8.tgz",
+      "integrity": "sha512-hcov4ZwZJIGbEuyNr9EmiTmZueyrxSToE6GOzoZnq5JM7ecRO7ttyvilPn+VmRsqiP16+VYZzVnGZj/hzZgKBA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "neo-async": "^2.6.2"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0",
+        "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+        "sass": "^1.3.0",
+        "sass-embedded": "*",
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@rspack/core": {
+          "optional": true
+        },
+        "node-sass": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/schema-utils": {
+      "version": "4.3.3",
+      "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+      "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json-schema": "^7.0.9",
+        "ajv": "^8.9.0",
+        "ajv-formats": "^2.1.1",
+        "ajv-keywords": "^5.1.0"
+      },
+      "engines": {
+        "node": ">= 10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/select-hose": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
+      "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/selfsigned": {
+      "version": "5.5.0",
+      "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-5.5.0.tgz",
+      "integrity": "sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@peculiar/x509": "^1.14.2",
+        "pkijs": "^3.3.3"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.8.5",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+      "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-index": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz",
+      "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "batch": "0.6.1",
+        "debug": "2.6.9",
+        "escape-html": "~1.0.3",
+        "http-errors": "~1.8.0",
+        "mime-types": "~2.1.35",
+        "parseurl": "~1.3.3"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/serve-index/node_modules/depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-index/node_modules/http-errors": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz",
+      "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~1.1.2",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": ">= 1.5.0 < 2",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-index/node_modules/statuses": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+      "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/shallow-clone": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
+      "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kind-of": "^6.0.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shell-quote": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
+      "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4",
+        "side-channel-list": "^1.0.1",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/sockjs": {
+      "version": "0.3.24",
+      "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
+      "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "faye-websocket": "^0.11.3",
+        "uuid": "^8.3.2",
+        "websocket-driver": "^0.7.4"
+      }
+    },
+    "node_modules/source-map": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/source-map-support": {
+      "version": "0.5.21",
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+      "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-from": "^1.0.0",
+        "source-map": "^0.6.0"
+      }
+    },
+    "node_modules/spdy": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
+      "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.1.0",
+        "handle-thing": "^2.0.0",
+        "http-deceiver": "^1.2.7",
+        "select-hose": "^2.0.0",
+        "spdy-transport": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/spdy-transport": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz",
+      "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.1.0",
+        "detect-node": "^2.0.4",
+        "hpack.js": "^2.1.6",
+        "obuf": "^1.1.2",
+        "readable-stream": "^3.0.6",
+        "wbuf": "^1.7.3"
+      }
+    },
+    "node_modules/spdy-transport/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/spdy-transport/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/spdy/node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/spdy/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/style-loader": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz",
+      "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.27.0"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tapable": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+      "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      }
+    },
+    "node_modules/terser": {
+      "version": "5.49.0",
+      "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz",
+      "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@jridgewell/source-map": "^0.3.3",
+        "acorn": "^8.15.0",
+        "commander": "^2.20.0",
+        "source-map-support": "~0.5.20"
+      },
+      "bin": {
+        "terser": "bin/terser"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/terser/node_modules/commander": {
+      "version": "2.20.3",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+      "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/thingies": {
+      "version": "2.6.0",
+      "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.6.0.tgz",
+      "integrity": "sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "^2"
+      }
+    },
+    "node_modules/thunky": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
+      "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tree-dump": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz",
+      "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=10.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/streamich"
+      },
+      "peerDependencies": {
+        "tslib": "2"
+      }
+    },
+    "node_modules/ts-loader": {
+      "version": "9.6.2",
+      "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.6.2.tgz",
+      "integrity": "sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "chalk": "^4.1.0",
+        "picomatch": "^4.0.0",
+        "source-map": "^0.7.4"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "loader-utils": "*",
+        "typescript": "*",
+        "webpack": "^4.0.0 || ^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "loader-utils": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ts-loader/node_modules/source-map": {
+      "version": "0.7.6",
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+      "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/ts-node": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cspotcode/source-map-support": "^0.8.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.1",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-esm": "dist/bin-esm.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "dev": true,
+      "license": "0BSD",
+      "peer": true
+    },
+    "node_modules/tsyringe": {
+      "version": "4.10.0",
+      "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz",
+      "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^1.9.3"
+      },
+      "engines": {
+        "node": ">= 6.0.0"
+      }
+    },
+    "node_modules/tsyringe/node_modules/tslib": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+      "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+      "dev": true,
+      "license": "0BSD"
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "peer": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+      "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/browserslist"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/browserslist"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "escalade": "^3.2.0",
+        "picocolors": "^1.1.1"
+      },
+      "bin": {
+        "update-browserslist-db": "cli.js"
+      },
+      "peerDependencies": {
+        "browserslist": ">= 4.21.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/utila": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz",
+      "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "deprecated": "uuid@10 and below is no longer supported.  For ESM codebases, update to uuid@latest.  For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/watchpack": {
+      "version": "2.5.2",
+      "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz",
+      "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.1.2"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/wbuf": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
+      "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimalistic-assert": "^1.0.0"
+      }
+    },
+    "node_modules/webpack": {
+      "version": "5.109.0",
+      "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.109.0.tgz",
+      "integrity": "sha512-vomrngskVVXEZF9sMZfYAd4pXZUnfaWdJGlF+BTNF+gJBCKYCQBnOeVPlrh39Ewl7nlCsirDplMy6o5g9xJHBg==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@types/estree": "^1.0.8",
+        "@types/json-schema": "^7.0.15",
+        "@webassemblyjs/ast": "^1.14.1",
+        "@webassemblyjs/wasm-edit": "^1.14.1",
+        "@webassemblyjs/wasm-parser": "^1.14.1",
+        "acorn": "^8.16.0",
+        "browserslist": "^4.28.1",
+        "chrome-trace-event": "^1.0.2",
+        "enhanced-resolve": "^5.24.2",
+        "es-module-lexer": "^2.1.0",
+        "eslint-scope": "5.1.1",
+        "events": "^3.2.0",
+        "graceful-fs": "^4.2.11",
+        "mime-db": "^1.54.0",
+        "minimizer-webpack-plugin": "^5.6.1",
+        "neo-async": "^2.6.2",
+        "schema-utils": "^4.3.3",
+        "tapable": "^2.3.0",
+        "watchpack": "^2.5.2",
+        "webpack-sources": "^3.5.1"
+      },
+      "bin": {
+        "webpack": "bin/webpack.js"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependenciesMeta": {
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz",
+      "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==",
+      "dev": true,
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@discoveryjs/json-ext": "^0.6.1",
+        "@webpack-cli/configtest": "^3.0.1",
+        "@webpack-cli/info": "^3.0.1",
+        "@webpack-cli/serve": "^3.0.1",
+        "colorette": "^2.0.14",
+        "commander": "^12.1.0",
+        "cross-spawn": "^7.0.3",
+        "envinfo": "^7.14.0",
+        "fastest-levenshtein": "^1.0.12",
+        "import-local": "^3.0.2",
+        "interpret": "^3.1.1",
+        "rechoir": "^0.8.0",
+        "webpack-merge": "^6.0.1"
+      },
+      "bin": {
+        "webpack-cli": "bin/cli.js"
+      },
+      "engines": {
+        "node": ">=18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.82.0"
+      },
+      "peerDependenciesMeta": {
+        "webpack-bundle-analyzer": {
+          "optional": true
+        },
+        "webpack-dev-server": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-cli/node_modules/commander": {
+      "version": "12.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+      "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/webpack-dev-middleware": {
+      "version": "7.4.5",
+      "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz",
+      "integrity": "sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "colorette": "^2.0.10",
+        "memfs": "^4.43.1",
+        "mime-types": "^3.0.1",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "schema-utils": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "webpack": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-dev-middleware/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/webpack-dev-middleware/node_modules/mime-types": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+      "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/webpack-dev-server": {
+      "version": "5.2.6",
+      "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz",
+      "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/bonjour": "^3.5.13",
+        "@types/connect-history-api-fallback": "^1.5.4",
+        "@types/express": "^4.17.25",
+        "@types/express-serve-static-core": "^4.17.21",
+        "@types/serve-index": "^1.9.4",
+        "@types/serve-static": "^1.15.5",
+        "@types/sockjs": "^0.3.36",
+        "@types/ws": "^8.5.10",
+        "ansi-html-community": "^0.0.8",
+        "bonjour-service": "^1.2.1",
+        "chokidar": "^3.6.0",
+        "colorette": "^2.0.10",
+        "compression": "^1.8.1",
+        "connect-history-api-fallback": "^2.0.0",
+        "express": "^4.22.1",
+        "graceful-fs": "^4.2.6",
+        "http-proxy-middleware": "^2.0.9",
+        "ipaddr.js": "^2.1.0",
+        "launch-editor": "^2.14.1",
+        "open": "^10.0.3",
+        "p-retry": "^6.2.0",
+        "schema-utils": "^4.2.0",
+        "selfsigned": "^5.5.0",
+        "serve-index": "^1.9.1",
+        "sockjs": "^0.3.24",
+        "spdy": "^4.0.2",
+        "webpack-dev-middleware": "^7.4.2",
+        "ws": "^8.18.0"
+      },
+      "bin": {
+        "webpack-dev-server": "bin/webpack-dev-server.js"
+      },
+      "engines": {
+        "node": ">= 18.12.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/webpack"
+      },
+      "peerDependencies": {
+        "webpack": "^5.0.0"
+      },
+      "peerDependenciesMeta": {
+        "webpack": {
+          "optional": true
+        },
+        "webpack-cli": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/chokidar": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+      "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "anymatch": "~3.1.2",
+        "braces": "~3.0.2",
+        "glob-parent": "~5.1.2",
+        "is-binary-path": "~2.1.0",
+        "is-glob": "~4.0.1",
+        "normalize-path": "~3.0.0",
+        "readdirp": "~3.6.0"
+      },
+      "engines": {
+        "node": ">= 8.10.0"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/ipaddr.js": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
+      "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/picomatch": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+      "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/webpack-dev-server/node_modules/readdirp": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/webpack-merge": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz",
+      "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "clone-deep": "^4.0.1",
+        "flat": "^5.0.2",
+        "wildcard": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/webpack-sources": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz",
+      "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/webpack/node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/websocket-driver": {
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz",
+      "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "http-parser-js": ">=0.5.1",
+        "safe-buffer": ">=5.1.0",
+        "websocket-extensions": ">=0.1.1"
+      },
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/websocket-extensions": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+      "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=0.8.0"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wildcard": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz",
+      "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/ws": {
+      "version": "8.21.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz",
+      "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/wsl-utils": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+      "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-wsl": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "packages/bibliothek-client": {
+      "version": "0.1.0",
+      "dependencies": {
+        "@dh-software/dh-components": "^3.0.23",
+        "@dh-software/dh-components-themes": "^1.0.21",
+        "@dh-software/furnview-icons": "^1.0.21",
+        "@dh-software/lit-extensions": "^0.0.14",
+        "construct-style-sheets-polyfill": "^3.1.0",
+        "lit": "^3.3.2"
+      },
+      "devDependencies": {
+        "css-loader": "^7.1.2",
+        "html-webpack-plugin": "^5.6.3",
+        "sass": "^1.83.0",
+        "sass-loader": "^16.0.4",
+        "style-loader": "^4.0.0",
+        "ts-loader": "^9.5.2",
+        "webpack": "^5.97.1",
+        "webpack-cli": "^6.0.1",
+        "webpack-dev-server": "^5.2.0"
+      }
+    },
+    "packages/bibliothek-server": {
+      "version": "0.1.0",
+      "dependencies": {
+        "bibliothek-types": "*",
+        "express": "^4.21.2"
+      },
+      "devDependencies": {
+        "@types/express": "^4.17.21",
+        "@types/node": "^24.0.0",
+        "esbuild": "^0.25.0",
+        "ts-node": "^10.9.2"
+      }
+    },
+    "packages/bibliothek-types": {
+      "version": "0.1.0"
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a0491dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "software-bibliothek",
+  "version": "0.1.0",
+  "description": "Interne Software-Bibliothek — Katalog, Datei-Download, Zugriffsprotokoll (D+H)",
+  "private": true,
+  "workspaces": [
+    "./packages/*"
+  ],
+  "scripts": {
+    "build": "npm run build -w packages/bibliothek-types && npm run build -w packages/bibliothek-server && npm run build -w packages/bibliothek-client",
+    "build:exe": "npm run build && npm run bundle -w packages/bibliothek-server && node bauen-exe.cjs",
+    "dev:server": "npm run dev -w packages/bibliothek-server",
+    "dev:client": "npm run dev -w packages/bibliothek-client"
+  },
+  "devDependencies": {
+    "postject": "^1.0.0-alpha.6",
+    "prettier": "^3.4.2",
+    "typescript": "^5.7.2"
+  }
+}
diff --git a/packages/bibliothek-client/package.json b/packages/bibliothek-client/package.json
new file mode 100644
index 0000000..98d0fdf
--- /dev/null
+++ b/packages/bibliothek-client/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "bibliothek-client",
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "build": "webpack --node-env=production",
+    "dev": "webpack serve --node-env=development --host 0.0.0.0"
+  },
+  "dependencies": {
+    "@dh-software/dh-components": "^3.0.23",
+    "@dh-software/dh-components-themes": "^1.0.21",
+    "@dh-software/furnview-icons": "^1.0.21",
+    "@dh-software/lit-extensions": "^0.0.14",
+    "construct-style-sheets-polyfill": "^3.1.0",
+    "lit": "^3.3.2"
+  },
+  "devDependencies": {
+    "css-loader": "^7.1.2",
+    "html-webpack-plugin": "^5.6.3",
+    "sass": "^1.83.0",
+    "sass-loader": "^16.0.4",
+    "style-loader": "^4.0.0",
+    "ts-loader": "^9.5.2",
+    "webpack": "^5.97.1",
+    "webpack-cli": "^6.0.1",
+    "webpack-dev-server": "^5.2.0"
+  }
+}
diff --git a/packages/bibliothek-client/src/api-client.ts b/packages/bibliothek-client/src/api-client.ts
new file mode 100644
index 0000000..11f7bec
--- /dev/null
+++ b/packages/bibliothek-client/src/api-client.ts
@@ -0,0 +1,130 @@
+import type { Einstellungen, EinstellungenAnsicht, GeheimnisUebersicht, Software, SoftwareEingabe } from 'bibliothek-types';
+
+/** Wertet eine Antwort aus, wirft bei Fehler mit der Server-Meldung. */
+async function jsonOderFehler<T>(antwort: Response): Promise<T> {
+  if (!antwort.ok) {
+    const daten = (await antwort.json().catch(() => ({}))) as { fehler?: string };
+    throw new Error(daten.fehler ?? `Fehler ${antwort.status}`);
+  }
+  return antwort.json() as Promise<T>;
+}
+
+/** Lädt den kompletten Software-Katalog vom Server. */
+export async function ladeKatalog(): Promise<Software[]> {
+  return jsonOderFehler(await fetch('/api/software'));
+}
+
+/** Fragt, ob der Zugriff direkt am Server (lokal) erfolgt — steuert die Sichtbarkeit der Verwaltung. */
+export async function ladeUmgebung(): Promise<{ lokal: boolean }> {
+  return jsonOderFehler(await fetch('/api/umgebung'));
+}
+
+export async function softwareAnlegen(eingabe: SoftwareEingabe): Promise<Software> {
+  return jsonOderFehler(
+    await fetch('/api/software', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(eingabe),
+    })
+  );
+}
+
+export async function softwareAktualisieren(id: number, eingabe: SoftwareEingabe): Promise<Software> {
+  return jsonOderFehler(
+    await fetch(`/api/software/${id}`, {
+      method: 'PUT',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(eingabe),
+    })
+  );
+}
+
+export async function softwareLoeschen(id: number): Promise<void> {
+  const antwort = await fetch(`/api/software/${id}`, { method: 'DELETE' });
+  if (!antwort.ok) {
+    throw new Error(`Löschen fehlgeschlagen (Status ${antwort.status})`);
+  }
+}
+
+/** Hinterlegt eine Datei von Hand (Quellentyp „manuell"). */
+export async function dateiHochladen(id: number, datei: File): Promise<Software> {
+  return jsonOderFehler(
+    await fetch(`/api/software/${id}/datei?dateiname=${encodeURIComponent(datei.name)}`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/octet-stream' },
+      body: datei,
+    })
+  );
+}
+
+/** Löst einen sofortigen Abgleich der Bezugsquelle einer Software aus. */
+export async function abgleichen(id: number): Promise<Software> {
+  return jsonOderFehler(await fetch(`/api/software/${id}/abgleichen`, { method: 'POST' }));
+}
+
+/** Gleicht alle Einträge mit gesetzter Quelle ab. */
+export async function alleAbgleichen(): Promise<Software[]> {
+  return jsonOderFehler(await fetch('/api/abgleich/alle', { method: 'POST' }));
+}
+
+/** Stand der Bau-Geheimnisse einer Software (nur Namen, nie die Werte). */
+export async function ladeGeheimnisse(id: number): Promise<GeheimnisUebersicht[]> {
+  return jsonOderFehler(await fetch(`/api/software/${id}/geheimnisse`));
+}
+
+/** Speichert Bau-Geheimnisse. Ein leerer Wert lässt den bisherigen unverändert. */
+export async function speichereGeheimnisse(id: number, werte: Array<{ name: string; wert: string }>): Promise<GeheimnisUebersicht[]> {
+  return jsonOderFehler(
+    await fetch(`/api/software/${id}/geheimnisse`, {
+      method: 'PUT',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(werte),
+    })
+  );
+}
+
+/** URL für den Umzugs-Export (Katalog + Einstellungen, optional mit Geheimnissen). */
+export function exportUrl(mitGeheimnissen: boolean): string {
+  return `/api/export${mitGeheimnissen ? '?geheimnisse=ja' : ''}`;
+}
+
+/** Liest ein Umzugs-Paket ein und ersetzt damit den bisherigen Katalog. */
+export async function importiereUmzugsPaket(paket: unknown): Promise<{ uebernommen: number; mitGeheimnissen: number; hinweis: string }> {
+  return jsonOderFehler(
+    await fetch('/api/import', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(paket),
+    })
+  );
+}
+
+/** Lädt die Einstellungen samt der daraus abgeleiteten Pfade (nur lokal erlaubt). */
+export async function ladeEinstellungen(): Promise<EinstellungenAnsicht> {
+  return jsonOderFehler(await fetch('/api/einstellungen'));
+}
+
+export async function speichereEinstellungen(einstellungen: Einstellungen): Promise<EinstellungenAnsicht> {
+  return jsonOderFehler(
+    await fetch('/api/einstellungen', {
+      method: 'PUT',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(einstellungen),
+    })
+  );
+}
+
+/** URL zum Herunterladen mehrerer Programme als ZIP. */
+export function downloadMehrereUrl(ids: number[], angegebenerName?: string): string {
+  const parameter = new URLSearchParams({ ids: ids.join(',') });
+  if (angegebenerName) {
+    parameter.set('name', angegebenerName);
+  }
+  return `/api/download-mehrere?${parameter.toString()}`;
+}
+
+/** URL zum Herunterladen der Datei einer Software (optional mit angegebenem Namen fürs Protokoll). */
+export function downloadUrl(id: number, angegebenerName?: string): string {
+  const basis = `/api/software/${id}/download`;
+  return angegebenerName ? `${basis}?name=${encodeURIComponent(angegebenerName)}` : basis;
+}
diff --git a/packages/bibliothek-client/src/components/bibliothek-app.ts b/packages/bibliothek-client/src/components/bibliothek-app.ts
new file mode 100644
index 0000000..684b966
--- /dev/null
+++ b/packages/bibliothek-client/src/components/bibliothek-app.ts
@@ -0,0 +1,345 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, query, state } from 'lit/decorators.js';
+import type { DhInput, DhSelect } from '@dh-software/dh-components';
+import { EINSTUFUNGEN, type Einstufung, type Software } from 'bibliothek-types';
+import { ladeKatalog, ladeUmgebung, softwareLoeschen, abgleichen, alleAbgleichen, downloadUrl, downloadMehrereUrl } from '../api-client';
+import { gemerkterModus, setzeTheme, type ThemeModus } from '../theme';
+import './bibliothek-zeile';
+import './bibliothek-formular';
+import './bibliothek-einstellungen';
+
+function themeOptionen(aktuell: ThemeModus) {
+  // Frische Kopie je dh-select (die Komponente mutiert die Options-Objekte).
+  return [
+    { dataId: 'system', showContent: 'System', selected: aktuell === 'system' },
+    { dataId: 'hell', showContent: 'Hell', selected: aktuell === 'hell' },
+    { dataId: 'dunkel', showContent: 'Dunkel', selected: aktuell === 'dunkel' },
+  ];
+}
+
+/** Wurzel-Komponente: Katalog als kompakte, durchsuchbare Liste; Verwaltung + Abgleich nur lokal. */
+@customElement('bibliothek-app')
+export class BibliothekApp extends LitElement {
+  @query('#theme-select') private themeSelect?: DhSelect;
+
+  @state() private katalog: Software[] = [];
+  @state() private lokal = false;
+  @state() private ladefehler = '';
+  @state() private formularOffen = false;
+  @state() private einstellungenOffen = false;
+  @state() private zuBearbeiten?: Software;
+  @state() private suchbegriff = '';
+  @state() private beschaeftigt = false;
+  @state() private themeModus: ThemeModus = gemerkterModus();
+  @state() private aktiveEinstufung: Einstufung = 'lizenzfrei';
+  /** Ids der für den Sammel-Download angehakten Einträge — bleibt über Reiter hinweg bestehen. */
+  @state() private auswahl: ReadonlySet<number> = new Set();
+
+  // Light DOM, damit die globalen dh-components-Theme-Regeln greifen.
+  createRenderRoot() {
+    return this;
+  }
+
+  connectedCallback(): void {
+    super.connectedCallback();
+    this.datenLaden();
+  }
+
+  protected firstUpdated(): void {
+    if (this.themeSelect) {
+      (this.themeSelect as unknown as { selectOptions: ReturnType<typeof themeOptionen> }).selectOptions = themeOptionen(this.themeModus);
+    }
+  }
+
+  render() {
+    return html`
+      <div class="bibliothek-app-rahmen">
+        <header class="kopf">
+          <h1>Software-Bibliothek</h1>
+          <div class="kopf-aktionen">
+            <dh-select
+              id="theme-select"
+              class="theme-select"
+              withoutEmptyDefault
+              notSearchable
+              @changeSelectOption=${this.beiThemeWechsel}
+            ></dh-select>
+            ${this.lokal && !this.formularOffen && !this.einstellungenOffen
+              ? html`
+                  <dh-button buttonText="Alle prüfen" variant="standard" ?disabled=${this.beschaeftigt} @click=${this.allePruefen}></dh-button>
+                  <dh-button buttonText="+ Neue Software" variant="filled" @click=${this.neuAnlegen}></dh-button>
+                  <dh-button icon-button title="Einstellungen" variant="standard" @click=${this.einstellungenOeffnen}>
+                    <span slot="img" class="fv-icon-settings"></span>
+                  </dh-button>
+                `
+              : nothing}
+          </div>
+        </header>
+        ${this.beschaeftigt ? html`<p class="hinweis">Bezugsquellen werden geprüft …</p>` : nothing}
+        ${this.rendereProblemleiste()}
+        ${this.auswahl.size > 0 && !this.formularOffen
+          ? html`<div class="auswahl-leiste">
+              <dh-button icon-button variant="standard" title="Auswahl aufheben" @click=${this.auswahlAufheben}>
+                <span slot="img" class="fv-icon-close"></span>
+              </dh-button>
+              <span class="auswahl-anzahl">${this.auswahl.size} ausgewählt</span>
+              <dh-button buttonText="Herunterladen" variant="filled" @click=${this.auswahlHerunterladen}></dh-button>
+            </div>`
+          : nothing}
+        ${this.ladefehler ? html`<p class="fehler">${this.ladefehler}</p>` : nothing}
+        ${this.einstellungenOffen
+          ? html`<bibliothek-einstellungen
+              @geschlossen=${this.einstellungenSchliessen}
+              @umzugAbgeschlossen=${this.datenLaden}
+            ></bibliothek-einstellungen>`
+          : this.formularOffen
+          ? html`<bibliothek-formular
+              .software=${this.zuBearbeiten}
+              .moeglicheEltern=${this.hauptEintraege()}
+              @gespeichert=${this.beiGespeichert}
+              @abgebrochen=${this.formularSchliessen}
+            ></bibliothek-formular>`
+          : this.rendereKatalog()}
+      </div>
+    `;
+  }
+
+  /**
+   * Weist auf Einträge hin, deren letzter Abgleich fehlgeschlagen ist — sonst
+   * fällt ein toter Bezugslink nur auf, wenn jemand gezielt in die Liste schaut.
+   */
+  private rendereProblemleiste() {
+    const betroffene = this.katalog.filter((software) => software.letzterAbgleich && !software.abgleichErfolg);
+    if (betroffene.length === 0) {
+      return nothing;
+    }
+    const namen = betroffene.slice(0, 3).map((software) => software.name).join(', ');
+    const weitere = betroffene.length > 3 ? ` und ${betroffene.length - 3} weitere` : '';
+    // Der Hinweis auf den erhaltenen Stand ergibt nur Sinn, wenn es überhaupt einen gibt.
+    const mitBisherigerDatei = betroffene.some((software) => software.dateiname);
+    return html`<p class="warnung">
+      <strong>${betroffene.length} Eintrag${betroffene.length === 1 ? '' : 'e'} mit Problemen beim Abgleich:</strong>
+      ${namen}${weitere}.${mitBisherigerDatei ? ' Die zuletzt bereitgestellte Datei bleibt dabei erhalten.' : ''}
+    </p>`;
+  }
+
+  private rendereKatalog() {
+    if (this.katalog.length === 0) {
+      return html`<p>Noch keine Software im Katalog.</p>`;
+    }
+    return html`
+      <dh-input
+        class="suche"
+        type="text"
+        placeholder="Suchen"
+        .value=${this.suchbegriff}
+        @input=${this.beiSuche}
+      ></dh-input>
+      <dh-tab stretch @clickTab=${this.beiReiterWechsel}>
+        ${EINSTUFUNGEN.map(
+          (eintrag) => html`<div
+            slot="tab-head"
+            content=${eintrag.id}
+            class=${eintrag.id === this.aktiveEinstufung ? 'selected' : ''}
+          >
+            ${eintrag.text} (${this.eintraegeMitEinstufung(eintrag.id).length})
+          </div>`
+        )}
+        ${EINSTUFUNGEN.map(
+          (eintrag) => html`<div slot="tab-content" tab=${eintrag.id} class=${eintrag.id === this.aktiveEinstufung ? '' : 'hidden'}>
+            ${eintrag.id === this.aktiveEinstufung ? this.rendereListe() : nothing}
+          </div>`
+        )}
+      </dh-tab>
+    `;
+  }
+
+  private rendereListe() {
+    const gefiltert = this.gefilterterKatalog();
+    if (gefiltert.length === 0) {
+      return html`<p class="leer">
+        ${this.suchbegriff ? html`Keine Treffer für „${this.suchbegriff}".` : 'In dieser Einstufung ist noch nichts hinterlegt.'}
+      </p>`;
+    }
+    return html`
+      ${this.aktiveEinstufung === 'lizenzpflichtig'
+        ? html`<p class="hinweis">Für diese Programme kommt der Lizenzschlüssel über den Support.</p>`
+        : nothing}
+      <div class="liste ${this.lokal ? 'verwaltung' : ''}">
+      ${gefiltert.map(
+        (software) => html`<bibliothek-zeile
+          .software=${software}
+          .zusatzinhalte=${this.zusatzinhalteVon(software.id)}
+          .auswahl=${this.auswahl}
+          ?gewaehlt=${this.auswahl.has(software.id)}
+          ?lokal=${this.lokal}
+          @auswahlGeaendert=${this.beiAuswahlGeaendert}
+          @herunterladen=${this.beiHerunterladen}
+          @pruefen=${this.beiPruefen}
+          @bearbeiten=${this.beiBearbeiten}
+          @loeschen=${this.beiLoeschen}
+        ></bibliothek-zeile>`
+      )}
+    </div>`;
+  }
+
+  private beiAuswahlGeaendert(ereignis: CustomEvent<{ software: Software; gewaehlt: boolean }>): void {
+    const { software, gewaehlt } = ereignis.detail;
+    const neueAuswahl = new Set(this.auswahl);
+    if (gewaehlt) {
+      neueAuswahl.add(software.id);
+    } else {
+      neueAuswahl.delete(software.id);
+    }
+    this.auswahl = neueAuswahl;
+  }
+
+  private auswahlAufheben(): void {
+    this.auswahl = new Set();
+  }
+
+  private auswahlHerunterladen(): void {
+    const angegebenerName = this.namenErfragen();
+    if (angegebenerName === null) {
+      return;
+    }
+    window.location.href = downloadMehrereUrl([...this.auswahl], angegebenerName);
+  }
+
+  /** Fragt im Netzwerk-Betrieb nach dem Namen fürs Protokoll; lokal nicht nötig. */
+  private namenErfragen(): string | undefined | null {
+    if (this.lokal) {
+      return undefined;
+    }
+    return prompt('Bitte deinen Namen angeben (wird protokolliert):');
+  }
+
+  private beiReiterWechsel(ereignis: CustomEvent<string>): void {
+    this.aktiveEinstufung = ereignis.detail as Einstufung;
+  }
+
+  /** Eigenständige Tools einer Einstufung (Grundlage für die Zähler an den Reitern). */
+  private eintraegeMitEinstufung(einstufung: Einstufung): Software[] {
+    return this.hauptEintraege().filter((software) => software.einstufung === einstufung);
+  }
+
+  /** Eigenständige Tools (ohne die Zusatzinhalte, die unter ihrem Tool erscheinen). */
+  private hauptEintraege(): Software[] {
+    return this.katalog.filter((software) => !software.elternId);
+  }
+
+  private zusatzinhalteVon(elternId: number): Software[] {
+    return this.katalog.filter((software) => software.elternId === elternId);
+  }
+
+  /** Einträge des aktiven Reiters, zusätzlich nach dem Suchbegriff gefiltert. */
+  private gefilterterKatalog(): Software[] {
+    const eintraege = this.eintraegeMitEinstufung(this.aktiveEinstufung);
+    const begriff = this.suchbegriff.trim().toLowerCase();
+    if (!begriff) {
+      return eintraege;
+    }
+    return eintraege.filter(
+      (software) => software.name.toLowerCase().includes(begriff) || software.beschreibung.toLowerCase().includes(begriff)
+    );
+  }
+
+  private beiSuche(ereignis: Event): void {
+    this.suchbegriff = (ereignis.target as DhInput).value;
+  }
+
+  private beiThemeWechsel(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
+    this.themeModus = ereignis.detail.selectedOption.dataId as ThemeModus;
+    setzeTheme(this.themeModus);
+  }
+
+  public async datenLaden(): Promise<void> {
+    try {
+      const [umgebung, katalog] = await Promise.all([ladeUmgebung(), ladeKatalog()]);
+      this.lokal = umgebung.lokal;
+      this.katalog = katalog;
+      this.ladefehler = '';
+    } catch (fehler) {
+      this.ladefehler = fehler instanceof Error ? fehler.message : 'Daten konnten nicht geladen werden.';
+    }
+  }
+
+  private neuAnlegen(): void {
+    this.zuBearbeiten = undefined;
+    this.formularOffen = true;
+  }
+
+  private einstellungenOeffnen(): void {
+    this.einstellungenOffen = true;
+  }
+
+  private einstellungenSchliessen(): void {
+    this.einstellungenOffen = false;
+  }
+
+  private beiBearbeiten(ereignis: CustomEvent<Software>): void {
+    this.zuBearbeiten = ereignis.detail;
+    this.formularOffen = true;
+  }
+
+  private formularSchliessen(): void {
+    this.formularOffen = false;
+    this.zuBearbeiten = undefined;
+  }
+
+  private beiGespeichert(): void {
+    this.formularSchliessen();
+    this.datenLaden();
+  }
+
+  private beiHerunterladen(ereignis: CustomEvent<Software>): void {
+    const angegebenerName = this.namenErfragen();
+    if (angegebenerName === null) {
+      return;
+    }
+    window.location.href = downloadUrl(ereignis.detail.id, angegebenerName);
+  }
+
+  private async beiPruefen(ereignis: CustomEvent<Software>): Promise<void> {
+    await this.mitBeschaeftigt(() => abgleichen(ereignis.detail.id));
+    this.datenLaden();
+  }
+
+  private async allePruefen(): Promise<void> {
+    await this.mitBeschaeftigt(() => alleAbgleichen());
+    this.datenLaden();
+  }
+
+  private async beiLoeschen(ereignis: CustomEvent<Software>): Promise<void> {
+    const software = ereignis.detail;
+    const anzahlZusatzinhalte = this.zusatzinhalteVon(software.id).length;
+    const zusatz = anzahlZusatzinhalte > 0 ? ` Die ${anzahlZusatzinhalte} zugehörigen Zusatzinhalte werden mitgelöscht.` : '';
+    if (!confirm(`„${software.name}" wirklich löschen?${zusatz}`)) {
+      return;
+    }
+    try {
+      await softwareLoeschen(software.id);
+      this.datenLaden();
+    } catch (fehler) {
+      this.ladefehler = fehler instanceof Error ? fehler.message : 'Löschen fehlgeschlagen.';
+    }
+  }
+
+  private async mitBeschaeftigt(aktion: () => Promise<unknown>): Promise<void> {
+    this.beschaeftigt = true;
+    this.ladefehler = '';
+    try {
+      await aktion();
+    } catch (fehler) {
+      this.ladefehler = fehler instanceof Error ? fehler.message : 'Abgleich fehlgeschlagen.';
+    } finally {
+      this.beschaeftigt = false;
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'bibliothek-app': BibliothekApp;
+  }
+}
diff --git a/packages/bibliothek-client/src/components/bibliothek-einstellungen.ts b/packages/bibliothek-client/src/components/bibliothek-einstellungen.ts
new file mode 100644
index 0000000..667a173
--- /dev/null
+++ b/packages/bibliothek-client/src/components/bibliothek-einstellungen.ts
@@ -0,0 +1,224 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, state } from 'lit/decorators.js';
+import { event, type EventRef } from '@dh-software/lit-extensions';
+import type { DhInput } from '@dh-software/dh-components';
+import type { EinstellungenAnsicht } from 'bibliothek-types';
+import { ladeEinstellungen, speichereEinstellungen, exportUrl, importiereUmzugsPaket } from '../api-client';
+
+/** Einstellungen der Anwendung — nur direkt am Server erreichbar. */
+@customElement('bibliothek-einstellungen')
+export class BibliothekEinstellungen extends LitElement {
+  @state() private ansicht?: EinstellungenAnsicht;
+  @state() private datenOrdner = '';
+  @state() private port = 3000;
+  @state() private gitBenutzer = '';
+  @state() private gitSchluessel = '';
+  @state() private meldung = '';
+  @state() private fehler = '';
+  @state() private speichert = false;
+
+  @event({ bubbles: true, composed: true }, 'geschlossen')
+  private geschlossen!: EventRef<void>;
+
+  @event({ bubbles: true, composed: true }, 'umzugAbgeschlossen')
+  private umzugAbgeschlossen!: EventRef<void>;
+
+  // Light DOM, damit das globale dh-components-Theme greift.
+  createRenderRoot() {
+    return this;
+  }
+
+  connectedCallback(): void {
+    super.connectedCallback();
+    this.einstellungenLaden();
+  }
+
+  render() {
+    if (!this.ansicht) {
+      return html`<dh-card variant="outlined"><span slot="content">Einstellungen werden geladen …</span></dh-card>`;
+    }
+    const neustartNoetig =
+      this.datenOrdner.trim() !== this.ansicht.einstellungen.datenOrdner || this.port !== this.ansicht.laufenderPort;
+    return html`
+      <dh-card variant="outlined" no-hover-effect no-click-effect>
+        <span slot="header">Einstellungen</span>
+        <div slot="content">
+          <dh-form-grid>
+            <dh-form-field label="Datenordner" span="2">
+              <dh-input
+                type="text"
+                placeholder=${'leer = Standardordner unter ProgramData'}
+                .value=${this.datenOrdner}
+                @input=${this.beiDatenOrdner}
+              ></dh-input>
+              <span class="feldhinweis">Hier liegen die Datenbank und alle bereitgestellten Programm-Dateien.</span>
+            </dh-form-field>
+            <dh-form-field label="Port">
+              <dh-input type="number" min="1" max="65535" .value=${String(this.port)} @input=${this.beiPort}></dh-input>
+            </dh-form-field>
+            <dh-form-field label="Git-Benutzer (für interne Tools)">
+              <dh-input
+                type="text"
+                placeholder="leer = SSH-Konfiguration des Dienstbenutzers"
+                .value=${this.gitBenutzer}
+                @input=${this.beiGitBenutzer}
+              ></dh-input>
+              <span class="feldhinweis">
+                Gilt für alle Git-Quellen. Dadurch bleiben die Katalog-URLs frei von Konto-Angaben — ein
+                Kontowechsel ist eine Änderung hier statt an jedem Eintrag.
+              </span>
+            </dh-form-field>
+            <dh-form-field label="SSH-Schlüssel (optional)">
+              <dh-input
+                type="text"
+                placeholder="C:\\ProgramData\\Software-Bibliothek\\id_bibliothek"
+                .value=${this.gitSchluessel}
+                @input=${this.beiGitSchluessel}
+              ></dh-input>
+              <span class="feldhinweis">
+                Nur nötig, wenn das Konto, unter dem die Bibliothek läuft, keinen eigenen SSH-Schlüssel hat —
+                sonst leer lassen. Wird relevant beim Betrieb als Dienst oder nach einem Serverumzug.
+              </span>
+            </dh-form-field>
+          </dh-form-grid>
+
+          <dl class="einstellungen-infos">
+            <dt>Datenordner (aktuell genutzt)</dt>
+            <dd>${this.ansicht.effektiverDatenOrdner}</dd>
+            <dt>Konfigurationsdatei</dt>
+            <dd>${this.ansicht.konfigurationsDatei}</dd>
+            <dt>Laufender Port</dt>
+            <dd>${this.ansicht.laufenderPort}</dd>
+          </dl>
+
+          <div class="umzug">
+            <h3>Umzug auf einen anderen Rechner</h3>
+            <p class="feldhinweis">
+              Gesichert werden Katalog, Bezugsquellen und Einstellungen — wenige Kilobyte. Die geholten
+              Programm-Dateien kommen nicht mit; die holt der Abgleich auf dem neuen Rechner selbst wieder.
+            </p>
+            <div class="umzug-aktionen">
+              <dh-button buttonText="Exportieren" variant="standard" @click=${this.beiExport}></dh-button>
+              <dh-button buttonText="Mit Schlüsseln exportieren" variant="standard" @click=${this.beiExportMitGeheimnissen}></dh-button>
+              <dh-button buttonText="Paket einlesen" variant="standard" @click=${this.beiImportWaehlen}></dh-button>
+              <input id="import-datei" type="file" accept="application/json,.json" hidden @change=${this.beiImportDatei} />
+            </div>
+            <p class="feldhinweis warnhinweis">
+              „Mit Schlüsseln" schreibt die hinterlegten Werte im Klartext in die Datei — nur bewusst nutzen
+              und die Datei danach sicher aufbewahren bzw. löschen.
+            </p>
+          </div>
+
+          ${neustartNoetig ? html`<p class="hinweis">Änderungen an Datenordner oder Port wirken erst nach einem Neustart der Anwendung.</p>` : nothing}
+          ${this.meldung ? html`<p class="hinweis">${this.meldung}</p>` : nothing}
+        </div>
+        <div slot="actions">
+          ${this.fehler ? html`<span class="fehler">${this.fehler}</span>` : nothing}
+          <dh-button buttonText="Schließen" variant="standard" @click=${this.beiSchliessen}></dh-button>
+          <dh-button
+            buttonText=${this.speichert ? 'Speichert…' : 'Speichern'}
+            variant="filled"
+            ?disabled=${this.speichert}
+            @click=${this.beiSpeichern}
+          ></dh-button>
+        </div>
+      </dh-card>
+    `;
+  }
+
+  private async einstellungenLaden(): Promise<void> {
+    try {
+      this.ansicht = await ladeEinstellungen();
+      this.datenOrdner = this.ansicht.einstellungen.datenOrdner;
+      this.port = this.ansicht.einstellungen.port;
+      this.gitBenutzer = this.ansicht.einstellungen.gitBenutzer;
+      this.gitSchluessel = this.ansicht.einstellungen.gitSchluessel;
+    } catch (fehler) {
+      this.fehler = fehler instanceof Error ? fehler.message : 'Einstellungen konnten nicht geladen werden.';
+    }
+  }
+
+  private beiDatenOrdner(ereignis: Event): void {
+    this.datenOrdner = (ereignis.target as DhInput).value;
+  }
+
+  private beiPort(ereignis: Event): void {
+    this.port = Number((ereignis.target as DhInput).value) || 0;
+  }
+
+  private beiGitBenutzer(ereignis: Event): void {
+    this.gitBenutzer = (ereignis.target as DhInput).value;
+  }
+
+  private beiGitSchluessel(ereignis: Event): void {
+    this.gitSchluessel = (ereignis.target as DhInput).value;
+  }
+
+  private beiSchliessen(): void {
+    this.geschlossen.dispatch();
+  }
+
+  private beiExport(): void {
+    window.location.href = exportUrl(false);
+  }
+
+  private beiExportMitGeheimnissen(): void {
+    if (confirm('Die Datei enthält dann die hinterlegten Schlüssel im Klartext. Wirklich so exportieren?')) {
+      window.location.href = exportUrl(true);
+    }
+  }
+
+  private beiImportWaehlen(): void {
+    this.querySelector<HTMLInputElement>('#import-datei')?.click();
+  }
+
+  private async beiImportDatei(ereignis: Event): Promise<void> {
+    const feld = ereignis.target as HTMLInputElement;
+    const datei = feld.files?.[0];
+    feld.value = ''; // erlaubt, dieselbe Datei erneut zu wählen
+    if (!datei) {
+      return;
+    }
+    if (!confirm('Der bisherige Katalog wird dabei vollständig ersetzt. Fortfahren?')) {
+      return;
+    }
+    this.fehler = '';
+    this.meldung = '';
+    try {
+      const ergebnis = await importiereUmzugsPaket(JSON.parse(await datei.text()));
+      this.meldung = `${ergebnis.uebernommen} Einträge übernommen${ergebnis.mitGeheimnissen > 0 ? `, davon ${ergebnis.mitGeheimnissen} mit Schlüsseln` : ''}. ${ergebnis.hinweis}`;
+      this.umzugAbgeschlossen.dispatch();
+    } catch (fehler) {
+      this.fehler = fehler instanceof Error ? fehler.message : 'Paket konnte nicht eingelesen werden.';
+    }
+  }
+
+  private async beiSpeichern(): Promise<void> {
+    this.speichert = true;
+    this.fehler = '';
+    this.meldung = '';
+    try {
+      this.ansicht = await speichereEinstellungen({
+        datenOrdner: this.datenOrdner.trim(),
+        port: this.port,
+        gitBenutzer: this.gitBenutzer.trim(),
+        gitSchluessel: this.gitSchluessel.trim(),
+      });
+      this.datenOrdner = this.ansicht.einstellungen.datenOrdner;
+      this.port = this.ansicht.einstellungen.port;
+      this.gitBenutzer = this.ansicht.einstellungen.gitBenutzer;
+      this.gitSchluessel = this.ansicht.einstellungen.gitSchluessel;
+      this.meldung = 'Gespeichert. Datenordner und Port gelten ab dem nächsten Start.';
+    } catch (fehler) {
+      this.fehler = fehler instanceof Error ? fehler.message : 'Speichern fehlgeschlagen.';
+    } finally {
+      this.speichert = false;
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'bibliothek-einstellungen': BibliothekEinstellungen;
+  }
+}
diff --git a/packages/bibliothek-client/src/components/bibliothek-formular.ts b/packages/bibliothek-client/src/components/bibliothek-formular.ts
new file mode 100644
index 0000000..e07ce28
--- /dev/null
+++ b/packages/bibliothek-client/src/components/bibliothek-formular.ts
@@ -0,0 +1,441 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+import { event, type EventRef } from '@dh-software/lit-extensions';
+import type { DhInput } from '@dh-software/dh-components';
+import {
+  EINSTUFUNGEN,
+  PROGRAMMARTEN,
+  type Einstufung,
+  type GeheimnisUebersicht,
+  type Programmart,
+  type QuelleTyp,
+  type Software,
+  type SoftwareEingabe,
+} from 'bibliothek-types';
+import { softwareAnlegen, softwareAktualisieren, dateiHochladen, ladeGeheimnisse, speichereGeheimnisse } from '../api-client';
+
+interface QuellenOption {
+  dataId: string;
+  showContent: string;
+  selected?: boolean;
+}
+
+/**
+ * dh-select verwirft Klicks auf Einträge ohne dataId (`if (!dataId) return;`) und lässt
+ * die Liste dann offen stehen. „Keine Quelle" braucht deshalb eine eigene Kennung, die
+ * beim Lesen und Schreiben auf den Leerwert übersetzt wird.
+ */
+const OHNE_QUELLE = 'keine';
+
+const QUELLEN_OPTIONEN: QuellenOption[] = [
+  { dataId: OHNE_QUELLE, showContent: 'Keine (nur Katalog-Eintrag)' },
+  { dataId: 'manuell', showContent: 'Manuell — Datei hochladen (kein Auto-Update)' },
+  { dataId: 'direkt', showContent: 'Direkt-Download-Link' },
+  { dataId: 'github', showContent: 'GitHub-Release' },
+  { dataId: 'winget', showContent: 'winget-Paket' },
+  { dataId: 'svn', showContent: 'SVN-Export' },
+  { dataId: 'git', showContent: 'Git-Repository — wird selbst gebaut' },
+];
+
+const WERT_PLATZHALTER: Record<string, string> = {
+  '': 'zuerst eine Quelle wählen',
+  direkt: 'https://…/setup.exe',
+  github: 'besitzer/projekt  (z.B. ip7z/7zip)',
+  winget: 'Paket-ID  (z.B. Google.Chrome)',
+  svn: 'https://svn…/pfad/datei.exe',
+  git: 'ssh://git.server.de:29418/gruppe/tool.git   (optional mit #zweig)',
+};
+
+/** Was die gewählte Programmart für den Nutzer bedeutet — steht als Hilfstext unter dem Feld. */
+const PROGRAMMART_ERLAEUTERUNG: Record<Programmart, string> = {
+  unbestimmt: 'Noch nicht eingeordnet — am Eintrag erscheint kein Hinweis',
+  installer: 'Am Eintrag erscheint: Datei kann nach der Installation gelöscht werden',
+  eigenstaendig: 'Am Eintrag erscheint: läuft direkt, an einen festen Ort legen',
+};
+
+/** Formular zum Anlegen/Bearbeiten einer Software inkl. Bezugsquelle. */
+@customElement('bibliothek-formular')
+export class BibliothekFormular extends LitElement {
+  @property({ attribute: false }) software?: Software;
+  /** Eigenständige Tools, denen dieser Eintrag als Plug-in zugeordnet werden kann. */
+  @property({ attribute: false }) moeglicheEltern: Software[] = [];
+
+  @state() private name = '';
+  @state() private beschreibung = '';
+  @state() private elternId = 0;
+  @state() private elternOptionen: QuellenOption[] = [];
+  @state() private quelleTyp: QuelleTyp | '' = '';
+  @state() private quelleWert = '';
+  @state() private bauBefehl = '';
+  @state() private bauErgebnis = '';
+  @state() private einstufung: Einstufung = 'lizenzfrei';
+  @state() private einstufungsOptionen: QuellenOption[] = [];
+  @state() private programmart: Programmart = 'unbestimmt';
+  @state() private programmartOptionen: QuellenOption[] = [];
+  @state() private hinweis = '';
+  @state() private anleitungUrl = '';
+  @state() private erwarteterHerausgeber = '';
+  @state() private geheimnisStand: GeheimnisUebersicht[] = [];
+  @state() private geheimwerte: Record<string, string> = {};
+  @state() private datei?: File;
+  @state() private quellenOptionen: QuellenOption[] = QUELLEN_OPTIONEN.map((option) => ({ ...option }));
+  @state() private fehler = '';
+  @state() private speichert = false;
+
+  @event({ bubbles: true, composed: true }, 'gespeichert')
+  private gespeichert!: EventRef<void>;
+
+  @event({ bubbles: true, composed: true }, 'abgebrochen')
+  private abgebrochen!: EventRef<void>;
+
+  // Light DOM, damit das globale dh-components-Theme greift.
+  createRenderRoot() {
+    return this;
+  }
+
+  connectedCallback(): void {
+    super.connectedCallback();
+    if (this.software) {
+      this.name = this.software.name;
+      this.beschreibung = this.software.beschreibung;
+      this.elternId = this.software.elternId;
+      this.quelleTyp = this.software.quelleTyp;
+      this.quelleWert = this.software.quelleWert;
+      this.bauBefehl = this.software.bauBefehl;
+      this.bauErgebnis = this.software.bauErgebnis;
+      this.einstufung = this.software.einstufung;
+      this.programmart = this.software.programmart;
+      this.hinweis = this.software.hinweis;
+      this.anleitungUrl = this.software.anleitungUrl;
+      this.erwarteterHerausgeber = this.software.erwarteterHerausgeber;
+      this.quellenOptionen = QUELLEN_OPTIONEN.map((option) => ({
+        ...option,
+        selected: option.dataId === (this.quelleTyp || OHNE_QUELLE),
+      }));
+    }
+    this.einstufungsOptionen = EINSTUFUNGEN.map((eintrag) => ({
+      dataId: eintrag.id,
+      showContent: eintrag.text,
+      selected: eintrag.id === this.einstufung,
+    }));
+    this.programmartOptionen = PROGRAMMARTEN.map((eintrag) => ({
+      dataId: eintrag.id,
+      showContent: eintrag.text,
+      selected: eintrag.id === this.programmart,
+    }));
+    if (this.software) {
+      void this.geheimnisStandLaden(this.software.id);
+    }
+    // Sich selbst nicht als eigenes Eltern-Tool anbieten.
+    this.elternOptionen = [
+      { dataId: '0', showContent: 'Eigenständiges Tool', selected: !this.elternId },
+      ...this.moeglicheEltern
+        .filter((tool) => tool.id !== this.software?.id)
+        .map((tool) => ({ dataId: String(tool.id), showContent: tool.name, selected: tool.id === this.elternId })),
+    ];
+  }
+
+  render() {
+    return html`
+      <dh-card variant="outlined" no-hover-effect no-click-effect>
+        <span slot="header">${this.software ? 'Software bearbeiten' : 'Neue Software'}</span>
+        <dh-form-grid slot="content">
+          <dh-form-field label="Name">
+            <dh-input type="text" .value=${this.name} @input=${this.beiName}></dh-input>
+          </dh-form-field>
+          <dh-form-field label="Einstufung">
+            <dh-select
+              withoutEmptyDefault
+              notSearchable
+              .selectOptions=${this.einstufungsOptionen}
+              @changeSelectOption=${this.beiEinstufung}
+            ></dh-select>
+          </dh-form-field>
+          <dh-form-field label="Programmart">
+            <dh-select
+              withoutEmptyDefault
+              notSearchable
+              .selectOptions=${this.programmartOptionen}
+              @changeSelectOption=${this.beiProgrammart}
+            ></dh-select>
+            <span class="feldhinweis">${PROGRAMMART_ERLAEUTERUNG[this.programmart]}</span>
+          </dh-form-field>
+          <dh-form-field label="Gehört zu">
+            <dh-select
+              withoutEmptyDefault
+              notSearchable
+              .selectOptions=${this.elternOptionen}
+              @changeSelectOption=${this.beiEltern}
+            ></dh-select>
+            <span class="feldhinweis">Zusatzinhalte (Plug-ins, Sprachpakete, Vorlagen …) erscheinen unter dem gewählten Tool</span>
+          </dh-form-field>
+          <dh-form-field label="Bezugsquelle">
+            <dh-select
+              withoutEmptyDefault
+              notSearchable
+              .selectOptions=${this.quellenOptionen}
+              @changeSelectOption=${this.beiQuelleTyp}
+            ></dh-select>
+          </dh-form-field>
+          ${this.rendereQuellenAngabe()} ${this.rendereBauFelder()} ${this.rendereGeheimnisse()}
+          <dh-form-field label="Anleitung (Link, optional)">
+            <dh-input type="text" placeholder="https://wiki…" .value=${this.anleitungUrl} @input=${this.beiAnleitung}></dh-input>
+          </dh-form-field>
+          <dh-form-field label="Hinweis (optional)">
+            <dh-input
+              type="text"
+              placeholder="z. B. braucht Adminrechte"
+              .value=${this.hinweis}
+              @input=${this.beiHinweis}
+            ></dh-input>
+            <span class="feldhinweis">Erscheint am Eintrag im Katalog</span>
+          </dh-form-field>
+          <dh-form-field label="Erwarteter Signaturgeber (optional)">
+            <dh-input
+              type="text"
+              placeholder="z. B. NOTEPAD++"
+              .value=${this.erwarteterHerausgeber}
+              @input=${this.beiErwarteterHerausgeber}
+            ></dh-input>
+            <span class="feldhinweis">Weicht die Signatur der geholten Datei davon ab, wird sie nicht zum Download angeboten.</span>
+          </dh-form-field>
+          <dh-form-field label="Beschreibung" span="2">
+            <dh-textarea .value=${this.beschreibung} @isChange=${this.beiBeschreibung}></dh-textarea>
+          </dh-form-field>
+        </dh-form-grid>
+        <div slot="actions">
+          ${this.fehler ? html`<span class="fehler">${this.fehler}</span>` : ''}
+          <dh-button buttonText="Abbrechen" variant="standard" @click=${this.beiAbbrechen}></dh-button>
+          <dh-button
+            buttonText=${this.speichert ? 'Speichert…' : 'Speichern'}
+            variant="filled"
+            ?disabled=${this.speichert}
+            @click=${this.beiSpeichern}
+          ></dh-button>
+        </div>
+      </dh-card>
+    `;
+  }
+
+  /** Je nach Quelle: Datei-Auswahl (manuell) oder Textfeld für die Quell-Angabe. */
+  private rendereQuellenAngabe() {
+    if (this.quelleTyp === 'manuell') {
+      return html`
+        <dh-form-field label="Datei">
+          <input type="file" @change=${this.beiDatei} />
+          ${this.software?.dateiname ? html`<span class="feldhinweis">Aktuell hinterlegt: ${this.software.dateiname}</span>` : ''}
+        </dh-form-field>
+      `;
+    }
+    return html`
+      <dh-form-field label="Quell-Angabe">
+        <dh-input
+          type="text"
+          placeholder=${WERT_PLATZHALTER[this.quelleTyp]}
+          ?disabled=${this.quelleTyp === ''}
+          .value=${this.quelleWert}
+          @input=${this.beiQuelleWert}
+        ></dh-input>
+        ${this.quelleTyp === 'git' && this.quelleWert.includes('@')
+          ? html`<span class="feldhinweis warnhinweis">
+              Die URL enthält einen Benutzernamen. Besser ohne — dann liefert die SSH-Konfiguration des
+              Servers die Kennung, und ein Kontowechsel erfordert keine Änderung an den Einträgen.
+            </span>`
+          : nothing}
+      </dh-form-field>
+    `;
+  }
+
+  /**
+   * Bau-Geheimnisse: Das Repository deklariert in `software-bibliothek.json`, was es
+   * braucht; hier wird nur der Wert hinterlegt. Bereits gespeicherte Werte werden nie
+   * zurückgeliefert — ein leer gelassenes Feld lässt den bisherigen Wert unverändert.
+   */
+  private rendereGeheimnisse() {
+    if (this.quelleTyp !== 'git' || !this.software) {
+      return nothing;
+    }
+    const bedarf = this.software.benoetigteGeheimnisse ?? [];
+    if (bedarf.length === 0) {
+      return html`<dh-form-field label="Schlüssel für den Bau" span="2">
+        <span class="feldhinweis">
+          Dieses Repository fordert keine an. Bedarf wird in <code>software-bibliothek.json</code> unter
+          <code>secrets</code> deklariert und beim Bau als Umgebungsvariable bereitgestellt.
+        </span>
+      </dh-form-field>`;
+    }
+    return bedarf.map(
+      (eintrag) => html`<dh-form-field label=${eintrag.name} span="2">
+        <dh-input
+          type="password"
+          toggle-password
+          placeholder=${this.istHinterlegt(eintrag.name) ? 'hinterlegt — leer lassen, um ihn zu behalten' : 'noch nicht hinterlegt'}
+          .value=${this.geheimwerte[eintrag.name] ?? ''}
+          @input=${(ereignis: Event) => this.beiGeheimwert(eintrag.name, ereignis)}
+        ></dh-input>
+        <span class="feldhinweis">${eintrag.beschreibung || 'Wird dem Bau als Umgebungsvariable übergeben.'}</span>
+      </dh-form-field>`
+    );
+  }
+
+  private istHinterlegt(name: string): boolean {
+    return this.geheimnisStand.some((eintrag) => eintrag.name === name && eintrag.hinterlegt);
+  }
+
+  private beiGeheimwert(name: string, ereignis: Event): void {
+    this.geheimwerte = { ...this.geheimwerte, [name]: (ereignis.target as DhInput).value };
+  }
+
+  /** Nur für die Git-Quelle: Bau-Befehl und Pfad des Ergebnisses. */
+  private rendereBauFelder() {
+    if (this.quelleTyp !== 'git') {
+      return nothing;
+    }
+    return html`
+      <dh-form-field label="Bau-Befehl (optional)" span="2">
+        <dh-input
+          type="text"
+          placeholder="kommt normalerweise aus software-bibliothek.json im Repository"
+          .value=${this.bauBefehl}
+          @input=${this.beiBauBefehl}
+        ></dh-input>
+        <span class="feldhinweis">
+          Das Repository beschreibt seinen Bau selbst in <code>software-bibliothek.json</code>
+          (<code>bauBefehl</code> und <code>ergebnis</code>). Nur ausfüllen, um das zu übersteuern.
+        </span>
+      </dh-form-field>
+      <dh-form-field label="Ergebnis-Datei (optional)" span="2">
+        <dh-input type="text" placeholder="z. B. dist/MeinTool.exe" .value=${this.bauErgebnis} @input=${this.beiBauErgebnis}></dh-input>
+        <span class="feldhinweis">Pfad relativ zur Arbeitskopie; im Dateinamen ist ein * erlaubt.</span>
+      </dh-form-field>
+    `;
+  }
+
+  private beiBauBefehl(ereignis: Event): void {
+    this.bauBefehl = (ereignis.target as DhInput).value;
+  }
+
+  private beiBauErgebnis(ereignis: Event): void {
+    this.bauErgebnis = (ereignis.target as DhInput).value;
+  }
+
+  private beiName(ereignis: Event): void {
+    this.name = (ereignis.target as DhInput).value;
+  }
+
+  private beiQuelleWert(ereignis: Event): void {
+    this.quelleWert = (ereignis.target as DhInput).value;
+  }
+
+  private beiAnleitung(ereignis: Event): void {
+    this.anleitungUrl = (ereignis.target as DhInput).value;
+  }
+
+  private beiErwarteterHerausgeber(ereignis: Event): void {
+    this.erwarteterHerausgeber = (ereignis.target as DhInput).value;
+  }
+
+  private async geheimnisStandLaden(id: number): Promise<void> {
+    try {
+      this.geheimnisStand = await ladeGeheimnisse(id);
+    } catch {
+      this.geheimnisStand = []; // ohne lokalen Zugriff nicht abrufbar — dann eben leer
+    }
+  }
+
+  /** Speichert nur die tatsächlich eingegebenen Werte; leere bleiben unangetastet. */
+  private async geheimnisseSpeichern(id: number): Promise<void> {
+    const bedarf = this.software?.benoetigteGeheimnisse ?? [];
+    if (bedarf.length === 0) {
+      return;
+    }
+    await speichereGeheimnisse(
+      id,
+      bedarf.map((eintrag) => ({ name: eintrag.name, wert: this.geheimwerte[eintrag.name] ?? '' }))
+    );
+  }
+
+  private beiEinstufung(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
+    this.einstufung = ereignis.detail.selectedOption.dataId as Einstufung;
+  }
+
+  private beiProgrammart(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
+    this.programmart = ereignis.detail.selectedOption.dataId as Programmart;
+  }
+
+  private beiHinweis(ereignis: CustomEvent<string>): void {
+    this.hinweis = ereignis.detail;
+  }
+
+  private beiBeschreibung(ereignis: CustomEvent<string>): void {
+    this.beschreibung = ereignis.detail;
+  }
+
+  private beiDatei(ereignis: Event): void {
+    this.datei = (ereignis.target as HTMLInputElement).files?.[0];
+  }
+
+  private beiQuelleTyp(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
+    const gewaehlt = ereignis.detail.selectedOption.dataId;
+    this.quelleTyp = gewaehlt === OHNE_QUELLE ? '' : (gewaehlt as QuelleTyp);
+  }
+
+  private beiEltern(ereignis: CustomEvent<{ selectedOption: { dataId: string } }>): void {
+    this.elternId = Number(ereignis.detail.selectedOption.dataId) || 0;
+  }
+
+  private beiAbbrechen(): void {
+    this.abgebrochen.dispatch();
+  }
+
+  private async beiSpeichern(): Promise<void> {
+    if (!this.name.trim()) {
+      this.fehler = 'Name ist erforderlich.';
+      return;
+    }
+    if (this.quelleTyp && this.quelleTyp !== 'manuell' && !this.quelleWert.trim()) {
+      this.fehler = 'Für die gewählte Quelle fehlt die Angabe.';
+      return;
+    }
+    if (this.quelleTyp === 'manuell' && !this.datei && !this.software?.dateiname) {
+      this.fehler = 'Bitte eine Datei auswählen.';
+      return;
+    }
+    this.speichert = true;
+    this.fehler = '';
+    const eingabe: SoftwareEingabe = {
+      name: this.name.trim(),
+      beschreibung: this.beschreibung.trim(),
+      elternId: this.elternId,
+      quelleTyp: this.quelleTyp,
+      quelleWert: this.quelleTyp === 'manuell' ? '' : this.quelleWert.trim(),
+      bauBefehl: this.quelleTyp === 'git' ? this.bauBefehl.trim() : '',
+      bauErgebnis: this.quelleTyp === 'git' ? this.bauErgebnis.trim() : '',
+      einstufung: this.einstufung,
+      programmart: this.programmart,
+      hinweis: this.hinweis.trim(),
+      anleitungUrl: this.anleitungUrl.trim(),
+      erwarteterHerausgeber: this.erwarteterHerausgeber.trim(),
+    };
+    try {
+      const gespeicherteSoftware = this.software
+        ? await softwareAktualisieren(this.software.id, eingabe)
+        : await softwareAnlegen(eingabe);
+      if (this.datei) {
+        await dateiHochladen(gespeicherteSoftware.id, this.datei);
+      }
+      await this.geheimnisseSpeichern(gespeicherteSoftware.id);
+      this.gespeichert.dispatch();
+    } catch (fehler) {
+      this.fehler = fehler instanceof Error ? fehler.message : 'Speichern fehlgeschlagen.';
+    } finally {
+      this.speichert = false;
+    }
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'bibliothek-formular': BibliothekFormular;
+  }
+}
diff --git a/packages/bibliothek-client/src/components/bibliothek-zeile.ts b/packages/bibliothek-client/src/components/bibliothek-zeile.ts
new file mode 100644
index 0000000..5368285
--- /dev/null
+++ b/packages/bibliothek-client/src/components/bibliothek-zeile.ts
@@ -0,0 +1,235 @@
+import { LitElement, html, nothing } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { event, type EventRef } from '@dh-software/lit-extensions';
+import type { Software } from 'bibliothek-types';
+
+/**
+ * Eine kompakte Katalog-Zeile für einen Software-Eintrag. Feuert Aktions-Events an die App.
+ * Die Aktionen liegen in festen Spalten, damit sie über alle Zeilen hinweg untereinander stehen;
+ * fehlt eine Aktion, bleibt ihre Spalte leer.
+ *
+ * Zusatzinhalte (Plug-ins, Sprachpakete, Vorlagen …) stehen eingerückt direkt unter ihrem Tool
+ * und bilden mit ihm eine Gruppe.
+ */
+@customElement('bibliothek-zeile')
+export class BibliothekZeile extends LitElement {
+  @property({ attribute: false }) software!: Software;
+  /** Weitere Einträge, die zu dieser Software gehören. */
+  @property({ attribute: false }) zusatzinhalte: Software[] = [];
+  @property({ type: Boolean }) lokal = false;
+  /** Zeile eines Zusatzinhalts (eingerückt unter seinem Tool). */
+  @property({ type: Boolean }) eingerueckt = false;
+  /** Ist dieser Eintrag für den Sammel-Download angehakt? */
+  @property({ type: Boolean }) gewaehlt = false;
+  /** Ids aller angehakten Einträge — nötig, damit auch eingerückte Zeilen ihren Zustand kennen. */
+  @property({ attribute: false }) auswahl: ReadonlySet<number> = new Set();
+
+  @event({ bubbles: true, composed: true }, 'auswahlGeaendert')
+  private auswahlGeaendert!: EventRef<{ software: Software; gewaehlt: boolean }>;
+
+  @event({ bubbles: true, composed: true }, 'herunterladen')
+  private herunterladen!: EventRef<Software>;
+
+  @event({ bubbles: true, composed: true }, 'pruefen')
+  private pruefen!: EventRef<Software>;
+
+  @event({ bubbles: true, composed: true }, 'bearbeiten')
+  private bearbeiten!: EventRef<Software>;
+
+  @event({ bubbles: true, composed: true }, 'loeschen')
+  private loeschen!: EventRef<Software>;
+
+  // Light DOM, damit das globale dh-components-Theme greift.
+  createRenderRoot() {
+    return this;
+  }
+
+  render() {
+    const software = this.software;
+    return html`
+      <div class="zeile ${this.eingerueckt ? 'zeile-eingerueckt' : ''}">
+        <span class="zeile-auswahl">
+          <dh-checkbox
+            ?checked=${this.gewaehlt}
+            ?disabled=${!software.dateiname}
+            title=${software.dateiname ? 'Für Sammel-Download auswählen' : 'Keine Datei vorhanden'}
+            @isChange=${this.beiAuswahl}
+          ></dh-checkbox>
+        </span>
+        <div class="zeile-info">
+          <span class="zeile-name">
+            ${software.name}
+            ${software.einstufung === 'lizenzpflichtig'
+              ? html`<span class="lizenz-marke" title="Lizenzschlüssel über den Support beziehen">lizenzpflichtig</span>`
+              : nothing}
+            ${software.einstufung === 'intern' ? html`<span class="intern-marke">intern</span>` : nothing}
+          </span>
+          <span class="zeile-meta">
+            ${this.rendereStand()}
+            <span class="${this.statusKlasse()}">${this.statusText()}</span>
+            ${this.rendereSignatur()}
+          </span>
+          ${this.rendereHinweise()}
+        </div>
+        <div class="zeile-aktionen">
+          <span class="aktion">
+            ${software.anleitungUrl
+              ? html`<a class="anleitung" href=${software.anleitungUrl} target="_blank" rel="noopener noreferrer">Anleitung</a>`
+              : nothing}
+          </span>
+          <span class="aktion">
+            ${software.dateiname
+              ? html`<dh-button
+                  icon-button
+                  variant="filled"
+                  title=${`${software.name} herunterladen`}
+                  @click=${this.beiHerunterladen}
+                >
+                  <span slot="img" class="fv-icon-download"></span>
+                </dh-button>`
+              : nothing}
+          </span>
+          ${this.lokal
+            ? html`
+                <span class="aktion">
+                  ${software.quelleTyp && software.quelleTyp !== 'manuell'
+                    ? html`<dh-button buttonText="Prüfen" variant="standard" @click=${this.beiPruefen}></dh-button>`
+                    : nothing}
+                </span>
+                <span class="aktion">
+                  <dh-button buttonText="Bearbeiten" variant="standard" @click=${this.beiBearbeiten}></dh-button>
+                </span>
+                <span class="aktion">
+                  <dh-button buttonText="Löschen" variant="standard" @click=${this.beiLoeschen}></dh-button>
+                </span>
+              `
+            : nothing}
+        </div>
+      </div>
+      ${this.zusatzinhalte.map(
+        (zusatzinhalt) => html`<bibliothek-zeile
+          .software=${zusatzinhalt}
+          .auswahl=${this.auswahl}
+          ?gewaehlt=${this.auswahl.has(zusatzinhalt.id)}
+          ?lokal=${this.lokal}
+          eingerueckt
+        ></bibliothek-zeile>`
+      )}
+    `;
+  }
+
+  private beiAuswahl(ereignis: CustomEvent<boolean>): void {
+    this.auswahlGeaendert.dispatch({ software: this.software, gewaehlt: ereignis.detail });
+  }
+
+  /**
+   * Hinweise zum Umgang mit der Datei: was aus der Programmart folgt, plus der
+   * freie Hinweis am Eintrag. Die eigene Zeile kostet nur dort Höhe, wo wirklich
+   * etwas zu sagen ist — die meisten Einträge bleiben einzeilig.
+   */
+  private rendereHinweise() {
+    const software = this.software;
+    const teile: string[] = [];
+    if (software.dateiname && software.programmart === 'installer') {
+      teile.push('Datei kann nach der Installation gelöscht werden');
+    }
+    if (software.dateiname && software.programmart === 'eigenstaendig') {
+      teile.push('Läuft direkt — an einen festen Ort legen');
+    }
+    if (software.hinweis) {
+      teile.push(software.hinweis);
+    }
+    return teile.length > 0 ? html`<span class="zeile-hinweis">${teile.join(' · ')}</span>` : nothing;
+  }
+
+  /**
+   * Signaturbefund der bereitgestellten Datei — nur in der Verwaltung.
+   * Für die Netzwerk-Ansicht ist das Betriebswissen ohne Nutzen: Wer etwas
+   * herunterlädt, kann daraus keine Entscheidung ableiten.
+   */
+  private rendereSignatur() {
+    const software = this.software;
+    if (!this.lokal || !software.dateiname || !software.signaturStatus) {
+      return nothing;
+    }
+    if (software.signaturStatus === 'Valid') {
+      return html` &nbsp;·&nbsp;
+        <span class="signatur-gut" title="Gültig signiert von ${software.signaturHerausgeber}">
+          signiert: ${software.signaturHerausgeber}
+        </span>`;
+    }
+    const text = software.signaturStatus === 'NotSigned' ? 'unsigniert' : `Signatur unklar (${software.signaturStatus})`;
+    return html` &nbsp;·&nbsp; <span class="signatur-unklar" title="Windows-Signaturstatus: ${software.signaturStatus}">${text}</span>`;
+  }
+
+  /**
+   * Zeigt bevorzugt das Veröffentlichungsdatum des Anbieters — das ist die Angabe,
+   * die den Nutzer interessiert. Nur wenn die Quelle keins liefert, wird ersatzweise
+   * unser Abrufzeitpunkt genannt (und als solcher benannt).
+   */
+  private rendereStand() {
+    const software = this.software;
+    if (software.veroeffentlichtAm) {
+      return html`Stand ${this.alsDatum(software.veroeffentlichtAm)} &nbsp;·&nbsp; `;
+    }
+    if (software.dateiStandVom) {
+      return html`geholt ${this.alsDatum(software.dateiStandVom)} &nbsp;·&nbsp; `;
+    }
+    return nothing;
+  }
+
+  private alsDatum(zeitstempel: string): string {
+    const zeitpunkt = new Date(zeitstempel);
+    return Number.isNaN(zeitpunkt.getTime()) ? '—' : zeitpunkt.toLocaleDateString('de-DE');
+  }
+
+  private statusText(): string {
+    const software = this.software;
+    if (software.dateiname) {
+      return software.dateiname;
+    }
+    if (!software.quelleTyp) {
+      return 'keine Quelle';
+    }
+    if (software.quelleTyp === 'manuell') {
+      return 'keine Datei hinterlegt';
+    }
+    if (software.letzterAbgleich && !software.abgleichErfolg) {
+      return software.abgleichMeldung || 'Abgleich fehlgeschlagen';
+    }
+    return 'Quelle hinterlegt, noch nicht geladen';
+  }
+
+  private statusKlasse(): string {
+    const software = this.software;
+    if (software.dateiname) {
+      return 'status-ok';
+    }
+    if (!software.quelleTyp || software.quelleTyp === 'manuell' || (software.letzterAbgleich && !software.abgleichErfolg)) {
+      return 'status-fehler';
+    }
+    return 'status-ausstehend';
+  }
+
+  private beiHerunterladen(): void {
+    this.herunterladen.dispatch(this.software);
+  }
+
+  private beiPruefen(): void {
+    this.pruefen.dispatch(this.software);
+  }
+
+  private beiBearbeiten(): void {
+    this.bearbeiten.dispatch(this.software);
+  }
+
+  private beiLoeschen(): void {
+    this.loeschen.dispatch(this.software);
+  }
+}
+
+declare global {
+  interface HTMLElementTagNameMap {
+    'bibliothek-zeile': BibliothekZeile;
+  }
+}
diff --git a/packages/bibliothek-client/src/global.css b/packages/bibliothek-client/src/global.css
new file mode 100644
index 0000000..644d3a7
--- /dev/null
+++ b/packages/bibliothek-client/src/global.css
@@ -0,0 +1,336 @@
+html,
+body {
+  margin: 0;
+  min-height: 100%;
+  background: var(--md-sys-color-surface, #ffffff);
+  color: var(--md-sys-color-on-surface, #1a1a1a);
+  font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
+}
+
+.bibliothek-app-rahmen {
+  padding: 24px;
+  /* Dauerhaft Platz für die schwebende Auswahl-Leiste — so verschiebt sich
+     auch am Listenende nichts, wenn sie erscheint. */
+  padding-bottom: 96px;
+  max-width: 1100px;
+  margin: 0 auto;
+}
+
+.kopf {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.kopf-aktionen {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.theme-select {
+  min-width: 150px;
+}
+
+.hinweis {
+  background: var(--md-sys-color-secondary-container, #e6e0ec);
+  color: var(--md-sys-color-on-secondary-container, #1d1b20);
+  padding: 12px 16px;
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+/* --- Kompakte Katalog-Liste --- */
+
+.suche {
+  display: block;
+  max-width: 420px;
+  margin-bottom: 12px;
+}
+
+dh-tab {
+  display: block;
+}
+
+/* Inhalt eines nicht gewählten Reiters (die Komponente setzt die Klasse ebenfalls). */
+[slot='tab-content'].hidden {
+  display: none;
+}
+
+.leer {
+  padding: 16px 4px;
+  opacity: 0.7;
+}
+
+.liste {
+  border: 1px solid var(--md-sys-color-outline-variant, #cac4d0);
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+/*
+ * Die Trennlinie sitzt auf der Gruppe (Tool + seine Zusatzinhalte), nicht auf der
+ * einzelnen Zeile — so wirken zusammengehörige Einträge als ein Block.
+ */
+.liste > bibliothek-zeile {
+  display: block;
+  border-bottom: 1px solid var(--md-sys-color-outline-variant, #e7e0ec);
+}
+
+.liste > bibliothek-zeile:last-child {
+  border-bottom: none;
+}
+
+.zeile {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  padding: 8px 16px;
+}
+
+.zeile-auswahl {
+  display: flex;
+  align-items: center;
+  flex: 0 0 28px;
+}
+
+/* Bei knapper Breite lieber abschneiden als umbrechen — sonst franst die Liste aus. */
+.zeile-info {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  flex: 1 1 auto;
+  overflow: hidden;
+}
+
+.zeile-info > * {
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.warnung {
+  padding: 12px 16px;
+  margin-bottom: 16px;
+  border-radius: 8px;
+  border: 1px solid var(--md-sys-color-error, #b00020);
+  background: var(--md-sys-color-error-container, rgba(176, 0, 32, 0.12));
+  color: var(--md-sys-color-on-error-container, #410002);
+}
+
+/*
+ * Schwebt über dem Inhalt statt ihn zu verschieben: Beim Anhaken soll die Liste
+ * an Ort und Stelle bleiben. Bleibt beim Blättern sichtbar — bei langen Listen
+ * ist die Auswahl sonst aus dem Blick.
+ *
+ * Bewusst eine gewöhnliche erhobene Fläche statt der umgekehrten Farbe: Auf
+ * einer umgekehrten Fläche greift keine der Button-Gestaltungen des Themes,
+ * die Schaltflächen wären dann nicht mehr als solche zu erkennen.
+ */
+.auswahl-leiste {
+  position: fixed;
+  bottom: 24px;
+  left: 50%;
+  transform: translateX(-50%);
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 8px;
+  border-radius: var(--md-sys-shape-corner-large, 16px);
+  border: 1px solid var(--md-sys-color-outline-variant, #cac4d0);
+  background: var(--md-sys-color-surface-container-high);
+  color: var(--md-sys-color-on-surface);
+  box-shadow:
+    0 3px 5px -1px rgba(0, 0, 0, 0.2),
+    0 6px 10px 0 rgba(0, 0, 0, 0.14),
+    0 1px 18px 0 rgba(0, 0, 0, 0.12);
+  max-width: calc(100vw - 32px);
+}
+
+.auswahl-anzahl {
+  font-weight: 500;
+}
+
+.zeile-name {
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.lizenz-marke,
+.intern-marke {
+  flex-shrink: 0;
+}
+
+.zeile-meta {
+  font-size: 0.85em;
+  opacity: 0.7;
+}
+
+/* Handhabungs-Hinweise stehen unter den Metadaten, etwas zurückgenommen. */
+.zeile-hinweis {
+  font-size: 0.85em;
+  color: var(--md-sys-color-on-surface-variant, #49454f);
+  font-style: italic;
+}
+
+/*
+ * Aktionen in festen Spalten, damit gleichartige Schaltflächen über alle Zeilen
+ * hinweg exakt untereinander stehen. Fehlt eine Aktion, bleibt ihre Spalte leer.
+ * Reihenfolge: Plug-ins | Anleitung | Herunterladen [| Prüfen | Bearbeiten | Löschen]
+ */
+.zeile-aktionen {
+  display: grid;
+  grid-template-columns: 84px 48px;
+  gap: 8px;
+  align-items: center;
+  flex-shrink: 0;
+}
+
+.liste.verwaltung .zeile-aktionen {
+  grid-template-columns: 84px 48px 84px 108px 88px;
+}
+
+.aktion {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  min-width: 0;
+}
+
+.aktion dh-button {
+  max-width: 100%;
+}
+
+/* --- Zusatzinhalte: eingerückt, ohne eigene Trennlinie --- */
+
+.zeile-eingerueckt {
+  padding-left: 44px;
+  padding-top: 4px;
+  padding-bottom: 4px;
+}
+
+.intern-marke {
+  font-size: 0.72em;
+  font-weight: 500;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  padding: 2px 8px;
+  border-radius: 999px;
+  background: var(--md-sys-color-primary-container, #ffe4cc);
+  color: var(--md-sys-color-on-primary-container, #4a1e00);
+}
+
+.lizenz-marke {
+  font-size: 0.72em;
+  font-weight: 500;
+  text-transform: uppercase;
+  letter-spacing: 0.04em;
+  padding: 2px 8px;
+  border-radius: 999px;
+  background: var(--md-sys-color-tertiary-container, #ffd9a0);
+  color: var(--md-sys-color-on-tertiary-container, #2b1700);
+}
+
+.status-ok {
+  color: var(--md-sys-color-primary, #ff8014);
+}
+
+.status-fehler {
+  color: var(--md-sys-color-error, #b00020);
+}
+
+.status-ausstehend {
+  font-style: italic;
+}
+
+.signatur-gut {
+  color: var(--md-sys-color-primary, #ff8014);
+}
+
+.signatur-unklar {
+  color: var(--md-sys-color-error, #b00020);
+}
+
+.warnhinweis {
+  color: var(--md-sys-color-error, #b00020);
+  opacity: 1;
+}
+
+.anleitung {
+  color: var(--md-sys-color-primary, #ff8014);
+  font-size: 0.9em;
+  text-decoration: none;
+  white-space: nowrap;
+}
+
+.anleitung:hover {
+  text-decoration: underline;
+}
+
+/* --- Formular --- */
+
+dh-card {
+  display: block;
+  margin-bottom: 16px;
+}
+
+[slot='actions'] {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+}
+
+.umzug {
+  margin-top: 24px;
+  padding-top: 16px;
+  border-top: 1px solid var(--md-sys-color-outline-variant, #cac4d0);
+}
+
+.umzug h3 {
+  margin: 0 0 4px;
+  font-size: 1em;
+}
+
+.umzug-aktionen {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin: 12px 0 8px;
+}
+
+.einstellungen-infos {
+  margin: 20px 0 0;
+  display: grid;
+  grid-template-columns: max-content 1fr;
+  gap: 4px 16px;
+  font-size: 0.9em;
+}
+
+.einstellungen-infos dt {
+  opacity: 0.7;
+}
+
+.einstellungen-infos dd {
+  margin: 0;
+  font-family: ui-monospace, 'Cascadia Code', Consolas, monospace;
+  word-break: break-all;
+}
+
+.feldhinweis {
+  display: block;
+  font-size: 0.8em;
+  opacity: 0.7;
+  margin-top: 4px;
+}
+
+.fehler {
+  color: var(--md-sys-color-error, #b00020);
+  margin-right: auto;
+}
diff --git a/packages/bibliothek-client/src/globals.d.ts b/packages/bibliothek-client/src/globals.d.ts
new file mode 100644
index 0000000..c97c29a
--- /dev/null
+++ b/packages/bibliothek-client/src/globals.d.ts
@@ -0,0 +1,8 @@
+// CSS-Imports haben keine TypeScript-Typen.
+// Mit `?raw` liefert webpack den Quelltext als String (für die Theme-Umschaltung).
+declare module '*.css?raw' {
+  const quelltext: string;
+  export default quelltext;
+}
+
+declare module '*.css';
diff --git a/packages/bibliothek-client/src/index.html b/packages/bibliothek-client/src/index.html
new file mode 100644
index 0000000..77f8820
--- /dev/null
+++ b/packages/bibliothek-client/src/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="de">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Software-Bibliothek</title>
+  </head>
+  <body>
+    <bibliothek-app></bibliothek-app>
+  </body>
+</html>
diff --git a/packages/bibliothek-client/src/index.ts b/packages/bibliothek-client/src/index.ts
new file mode 100644
index 0000000..b0a4d6c
--- /dev/null
+++ b/packages/bibliothek-client/src/index.ts
@@ -0,0 +1,19 @@
+import 'construct-style-sheets-polyfill';
+
+// Theme in fester Reihenfolge: Tokens -> Styles -> Komponenten.
+// Die Farb-Tokens hängt initTheme() als umschaltbare <style>-Elemente ein.
+import { initTheme } from './theme';
+import '@dh-software/dh-components-themes/styles.css';
+import '@dh-software/dh-components';
+
+// eigenes globales Grundlayout (nutzt die Theme-Tokens)
+import './global.css';
+
+// dh-Icon-Font (furnview-icons) beim Start in den <head> injizieren
+import { injectStylesheets } from '@dh-software/furnview-icons';
+injectStylesheets(true);
+
+initTheme();
+
+// eigene Wurzel-Komponente
+import './components/bibliothek-app';
diff --git a/packages/bibliothek-client/src/theme.ts b/packages/bibliothek-client/src/theme.ts
new file mode 100644
index 0000000..d93921f
--- /dev/null
+++ b/packages/bibliothek-client/src/theme.ts
@@ -0,0 +1,58 @@
+/**
+ * Theme-Umschaltung (System/hell/dunkel) auf Basis der Design-System-Defines.
+ * Beide Defines werden als <style> eingehängt; je nach Modus ist genau eines
+ * aktiv (das andere per `disabled` abgeschaltet). „System" folgt der
+ * Windows-Einstellung (prefers-color-scheme).
+ *
+ * Gleiches Vorgehen wie im Projekt meeting-transkriptor, damit die Bedienung
+ * über unsere Anwendungen hinweg einheitlich bleibt.
+ */
+// Relativer Pfad (umgeht das exports-Feld des Pakets, das ?raw sonst blockt).
+import hellCss from '../../../node_modules/@dh-software/dh-components-themes/build/defines.orange-light.css?raw';
+import dunkelCss from '../../../node_modules/@dh-software/dh-components-themes/build/defines.orange-dark.css?raw';
+
+export type ThemeModus = 'system' | 'hell' | 'dunkel';
+
+const SPEICHER_SCHLUESSEL = 'software-bibliothek-theme';
+
+let hellStil: HTMLStyleElement;
+let dunkelStil: HTMLStyleElement;
+let aktuellerModus: ThemeModus = 'system';
+const dunkelMedia = window.matchMedia('(prefers-color-scheme: dark)');
+
+function anwenden(): void {
+  const dunkel = aktuellerModus === 'dunkel' || (aktuellerModus === 'system' && dunkelMedia.matches);
+  hellStil.disabled = dunkel;
+  dunkelStil.disabled = !dunkel;
+  document.documentElement.style.colorScheme = dunkel ? 'dark' : 'light';
+  document.documentElement.dataset.theme = dunkel ? 'dunkel' : 'hell';
+}
+
+/** Liest den zuletzt gewählten Modus (Standard: System). */
+export function gemerkterModus(): ThemeModus {
+  const gemerkt = localStorage.getItem(SPEICHER_SCHLUESSEL);
+  return gemerkt === 'hell' || gemerkt === 'dunkel' || gemerkt === 'system' ? gemerkt : 'system';
+}
+
+/** Hängt beide Theme-Defines ein und aktiviert den gemerkten Modus. */
+export function initTheme(): void {
+  hellStil = document.createElement('style');
+  hellStil.textContent = hellCss;
+  dunkelStil = document.createElement('style');
+  dunkelStil.textContent = dunkelCss;
+  document.head.append(hellStil, dunkelStil);
+  dunkelMedia.addEventListener('change', () => {
+    if (aktuellerModus === 'system') {
+      anwenden();
+    }
+  });
+  aktuellerModus = gemerkterModus();
+  anwenden();
+}
+
+/** Setzt den Modus und merkt ihn für den nächsten Aufruf. */
+export function setzeTheme(modus: ThemeModus): void {
+  aktuellerModus = modus;
+  localStorage.setItem(SPEICHER_SCHLUESSEL, modus);
+  anwenden();
+}
diff --git a/packages/bibliothek-client/tsconfig.json b/packages/bibliothek-client/tsconfig.json
new file mode 100644
index 0000000..995f101
--- /dev/null
+++ b/packages/bibliothek-client/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "lib": ["DOM", "DOM.Iterable", "ES2021"],
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "experimentalDecorators": true,
+    "useDefineForClassFields": false,
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "exclude": ["node_modules", "dist"]
+}
diff --git a/packages/bibliothek-client/webpack.config.js b/packages/bibliothek-client/webpack.config.js
new file mode 100644
index 0000000..0665be4
--- /dev/null
+++ b/packages/bibliothek-client/webpack.config.js
@@ -0,0 +1,61 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+import HtmlWebpackPlugin from 'html-webpack-plugin';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const isProduction = process.env.NODE_ENV === 'production';
+
+export default {
+  mode: isProduction ? 'production' : 'development',
+  entry: './src/index.ts',
+  devtool: isProduction ? false : 'inline-source-map',
+  experiments: {
+    topLevelAwait: true,
+  },
+  module: {
+    rules: [
+      {
+        test: /\.ts$/,
+        use: 'ts-loader',
+        exclude: /node_modules/,
+      },
+      {
+        test: /\.css$/i,
+        resourceQuery: { not: [/raw/] },
+        use: ['style-loader', 'css-loader'],
+      },
+      {
+        // Import mit ?raw liefert den CSS-Quelltext als String (zum Umschalten
+        // der Theme-Defines zur Laufzeit).
+        resourceQuery: /raw/,
+        type: 'asset/source',
+      },
+      {
+        test: /\.(woff|woff2|ttf|eot|svg|png|jpg|gif|ico)$/i,
+        type: 'asset',
+      },
+    ],
+  },
+  resolve: {
+    extensions: ['.ts', '.js'],
+  },
+  plugins: [
+    new HtmlWebpackPlugin({
+      template: 'src/index.html',
+    }),
+  ],
+  devServer: {
+    host: '0.0.0.0',
+    port: 5000,
+    open: false,
+    proxy: [{ context: ['/api'], target: 'http://localhost:3000' }],
+  },
+  output: {
+    path: path.resolve(__dirname, 'dist'),
+    filename: 'index.[contenthash].js',
+    clean: true,
+  },
+  stats: {
+    errorDetails: true,
+  },
+};
diff --git a/packages/bibliothek-server/package.json b/packages/bibliothek-server/package.json
new file mode 100644
index 0000000..9b9fa29
--- /dev/null
+++ b/packages/bibliothek-server/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "bibliothek-server",
+  "version": "0.1.0",
+  "main": "build/index.js",
+  "scripts": {
+    "build": "tsc",
+    "bundle": "esbuild build/index.js --bundle --platform=node --target=node22 --format=cjs --outfile=build-exe/server.cjs --log-level=warning",
+    "start": "node --disable-warning=ExperimentalWarning build/index.js",
+    "dev": "ts-node src/index.ts"
+  },
+  "dependencies": {
+    "bibliothek-types": "*",
+    "express": "^4.21.2"
+  },
+  "devDependencies": {
+    "@types/express": "^4.17.21",
+    "@types/node": "^24.0.0",
+    "esbuild": "^0.25.0",
+    "ts-node": "^10.9.2"
+  }
+}
diff --git a/packages/bibliothek-server/src/abgleich.ts b/packages/bibliothek-server/src/abgleich.ts
new file mode 100644
index 0000000..36f8c36
--- /dev/null
+++ b/packages/bibliothek-server/src/abgleich.ts
@@ -0,0 +1,108 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import type { AbgleichErgebnis, Software } from 'bibliothek-types';
+import { DATEIEN_ORDNER, QUELLEN_ORDNER, abgleichSpeichern } from './datenbank';
+import { bezugsquelleFuer, type QuellenAbgleich } from './bezugsquellen';
+import { pruefeSignatur, weichtHerausgeberAb } from './signatur';
+import { alsUmgebung, ladeGeheimnisse } from './geheimnisse';
+import { gitSshBefehl } from './konfiguration';
+
+/** Größe der aktuell bereitgestellten Datei — Grundlage der Plausibilitätsprüfung. */
+function bisherigeGroesse(software: Software): number {
+  if (!software.dateiname) {
+    return 0;
+  }
+  const pfad = path.join(DATEIEN_ORDNER, String(software.id), software.dateiname);
+  return fs.existsSync(pfad) ? fs.statSync(pfad).size : 0;
+}
+
+/** Unveränderten Stand zurückschreiben (bei Misserfolg oder unbekannter Quelle). */
+function unveraendert(software: Software, meldung: string, version = software.version): AbgleichErgebnis {
+  return {
+    version,
+    dateiname: software.dateiname,
+    erfolg: false,
+    meldung,
+    geaendert: false,
+    signaturStatus: software.signaturStatus,
+    signaturHerausgeber: software.signaturHerausgeber,
+    veroeffentlichtAm: software.veroeffentlichtAm,
+    benoetigteGeheimnisse: software.benoetigteGeheimnisse,
+  };
+}
+
+/** Gleicht eine Software mit ihrer Bezugsquelle ab und speichert das Ergebnis zurück. */
+export async function gleicheSoftwareAb(software: Software): Promise<void> {
+  if (software.quelleTyp === 'manuell') {
+    return; // Datei wird von Hand hinterlegt – es gibt nichts abzugleichen
+  }
+  if (!software.quelleTyp || !software.quelleWert) {
+    return; // keine Quelle gesetzt – nichts zu holen
+  }
+  const quelle = bezugsquelleFuer(software.quelleTyp);
+  if (!quelle) {
+    abgleichSpeichern(software.id, unveraendert(software, `Unbekannte Bezugsquelle: ${software.quelleTyp}`));
+    return;
+  }
+
+  const zielordner = path.join(DATEIEN_ORDNER, String(software.id));
+  const ergebnis = await quelle.gleicheAb({
+    wert: software.quelleWert,
+    aktuelleVersion: software.version,
+    zielordner,
+    arbeitsordner: path.join(QUELLEN_ORDNER, String(software.id)),
+    bauBefehl: software.bauBefehl,
+    bauErgebnis: software.bauErgebnis,
+    vorherigeGroesse: bisherigeGroesse(software),
+    geheimnisse: alsUmgebung(ladeGeheimnisse(software.id)),
+    gitSshBefehl: gitSshBefehl(),
+  });
+
+  if (!ergebnis.erfolg) {
+    abgleichSpeichern(software.id, unveraendert(software, ergebnis.meldung, ergebnis.version));
+    return;
+  }
+
+  abgleichSpeichern(software.id, await mitSignaturbefund(software, ergebnis, zielordner));
+}
+
+/**
+ * Ergänzt das Abgleich-Ergebnis um die Windows-Signatur der bereitgestellten Datei.
+ * Ist ein Signaturgeber erwartet und weicht der gefundene ab, wird die Datei nicht
+ * zum Download freigegeben (`dateiname` bleibt leer) — sie bleibt aber zur Ansicht liegen.
+ */
+async function mitSignaturbefund(software: Software, ergebnis: QuellenAbgleich, zielordner: string): Promise<AbgleichErgebnis> {
+  const befund = await pruefeSignatur(path.join(zielordner, ergebnis.dateiname));
+  const gemeinsam = {
+    version: ergebnis.version,
+    signaturStatus: befund.status,
+    signaturHerausgeber: befund.herausgeber,
+    veroeffentlichtAm: ergebnis.veroeffentlichtAm ?? software.veroeffentlichtAm,
+    benoetigteGeheimnisse: ergebnis.benoetigteGeheimnisse ?? software.benoetigteGeheimnisse,
+  };
+  if (weichtHerausgeberAb(software.erwarteterHerausgeber, befund.herausgeber)) {
+    return {
+      ...gemeinsam,
+      dateiname: '', // nicht ausliefern, bis das geklärt ist
+      erfolg: false,
+      meldung: `Signatur weicht ab: erwartet „${software.erwarteterHerausgeber}", gefunden „${befund.herausgeber || 'unsigniert'}" (${befund.status}). Die Datei wird nicht zum Download angeboten.`,
+      geaendert: false,
+    };
+  }
+  return {
+    ...gemeinsam,
+    dateiname: ergebnis.dateiname,
+    erfolg: true,
+    meldung: ergebnis.meldung,
+    geaendert: ergebnis.geaendert,
+  };
+}
+
+/** Gleicht nacheinander alle Einträge mit gesetzter Quelle ab (täglicher Auto-Check). */
+export async function gleicheAlleAb(alle: Software[]): Promise<void> {
+  for (const software of alle) {
+    if (software.quelleTyp && software.quelleWert) {
+      await gleicheSoftwareAb(software);
+    }
+  }
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/basis.ts b/packages/bibliothek-server/src/bezugsquellen/basis.ts
new file mode 100644
index 0000000..3f81807
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/basis.ts
@@ -0,0 +1,159 @@
+import * as fs from 'fs';
+import * as path from 'path';
+
+/** Alles, was eine Bezugsquelle für einen Abgleich braucht. */
+export interface Abgleichauftrag {
+  /** Quell-spezifische Angabe: URL, `besitzer/repo`, Paket-ID, Repo-URL … */
+  wert: string;
+  /** Bisher bekannte Versions-/Stand-Kennung. */
+  aktuelleVersion: string;
+  /** Ordner, in dem die bereitgestellte Datei liegen soll. */
+  zielordner: string;
+  /** Arbeitskopie für Quellen, die etwas auschecken/bauen müssen. */
+  arbeitsordner: string;
+  /** Befehl zum Bauen (nur für die Git-Quelle). */
+  bauBefehl: string;
+  /** Pfad des Bau-Ergebnisses relativ zur Arbeitskopie (nur für die Git-Quelle). */
+  bauErgebnis: string;
+  /** Größe der bisher bereitgestellten Datei — für die Plausibilitätsprüfung. */
+  vorherigeGroesse: number;
+  /** Hinterlegte Bau-Geheimnisse, die dem Bau als Umgebungsvariablen mitgegeben werden. */
+  geheimnisse: Record<string, string>;
+  /** SSH-Aufruf für Git-Zugriffe aus den Einstellungen (leer = SSH-Konfiguration entscheidet). */
+  gitSshBefehl: string;
+}
+
+/** Ein Geheimnis, das ein Repository für seinen Bau anfordert. */
+export interface GeheimnisBedarf {
+  name: string;
+  beschreibung: string;
+}
+
+/** Ergebnis eines Quellen-Abgleichs. */
+export interface QuellenAbgleich {
+  erfolg: boolean;
+  /** Wurde eine neue Datei geholt (true) oder war schon alles aktuell (false)? */
+  geaendert: boolean;
+  /** Neue Versions-/Stand-Kennung. */
+  version: string;
+  /** Name der aktuell vorliegenden Datei. */
+  dateiname: string;
+  meldung: string;
+  /** Veröffentlichungsdatum beim Anbieter (ISO), falls die Quelle es kennt. */
+  veroeffentlichtAm?: string;
+  /** Vom Repository angeforderte Bau-Geheimnisse (nur die Git-Quelle liefert das). */
+  benoetigteGeheimnisse?: GeheimnisBedarf[];
+}
+
+/**
+ * Eine Bezugsquelle prüft ihre Quelle und stellt bei Bedarf die aktuelle Datei bereit.
+ * Jede neue Quellenart implementiert nur dieses Interface.
+ */
+export interface Bezugsquelle {
+  gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich>;
+}
+
+export function abgleichMisserfolg(version: string, meldung: string): QuellenAbgleich {
+  return { erfolg: false, geaendert: false, version, dateiname: '', meldung };
+}
+
+/** Wandelt eine Datumsangabe der Quelle in ISO um (leer, wenn unbrauchbar). */
+export function alsIsoDatum(rohwert: string | null | undefined): string {
+  if (!rohwert) {
+    return '';
+  }
+  const zeitpunkt = new Date(rohwert);
+  return Number.isNaN(zeitpunkt.getTime()) ? '' : zeitpunkt.toISOString();
+}
+
+// --- Plausibilitätsprüfung ---
+
+/** Unterhalb dieser Größe gilt eine Datei als verdächtig, wenn sie zudem stark geschrumpft ist. */
+const VERDAECHTIG_KLEIN_BYTES = 100 * 1024;
+
+/**
+ * Prüft eine frisch geholte Datei auf typische Anzeichen eines toten oder
+ * übernommenen Links (Fehler-/Anmeldeseite statt Datei, leere oder plötzlich
+ * winzige Datei). Liefert einen Beanstandungstext oder `undefined`, wenn alles passt.
+ */
+export function inhaltBeanstanden(inhalt: Buffer, dateiname: string, inhaltstyp: string | null, vorherigeGroesse: number): string | undefined {
+  if (inhalt.length === 0) {
+    return 'Die Quelle hat eine leere Datei geliefert.';
+  }
+
+  const istWebseiteErwartet = /\.(html?|htm)$/i.test(dateiname);
+  if (!istWebseiteErwartet) {
+    const anfang = inhalt.subarray(0, 200).toString('latin1').trimStart().toLowerCase();
+    if (anfang.startsWith('<!doctype html') || anfang.startsWith('<html')) {
+      return 'Die Quelle hat eine Webseite statt der Datei geliefert — der Link ist vermutlich veraltet oder verlangt eine Anmeldung.';
+    }
+    if (inhaltstyp && inhaltstyp.toLowerCase().includes('text/html')) {
+      return `Die Quelle hat eine Webseite geliefert (${inhaltstyp}) — der Link ist vermutlich veraltet.`;
+    }
+  }
+
+  // Ein plötzlicher Einbruch auf eine winzige Datei deutet auf eine Fehlerseite hin.
+  if (vorherigeGroesse > 0 && inhalt.length < vorherigeGroesse / 2 && inhalt.length < VERDAECHTIG_KLEIN_BYTES) {
+    return `Die geholte Datei ist auffällig klein (${inhalt.length} statt bisher ${vorherigeGroesse} Bytes) — bitte die Quelle prüfen.`;
+  }
+
+  return undefined;
+}
+
+// --- Dateiablage: erst vollständig vorbereiten, dann ersetzen ---
+
+function temporaerenOrdnerAnlegen(zielordner: string): string {
+  const temporaer = `${zielordner}.neu`;
+  fs.rmSync(temporaer, { recursive: true, force: true });
+  fs.mkdirSync(temporaer, { recursive: true });
+  return temporaer;
+}
+
+/**
+ * Ersetzt den Inhalt des Zielordners durch den des temporären Ordners.
+ * Der alte Stand wird erst angefasst, wenn der neue vollständig und geprüft ist —
+ * ein fehlgeschlagener Abgleich kann die funktionierende Datei damit nicht zerstören.
+ */
+function uebernehmeOrdner(temporaer: string, zielordner: string): void {
+  fs.rmSync(zielordner, { recursive: true, force: true });
+  fs.renameSync(temporaer, zielordner);
+}
+
+export function verwerfeOrdner(temporaer: string): void {
+  fs.rmSync(temporaer, { recursive: true, force: true });
+}
+
+/** Legt eine Datei als neuen, alleinigen Inhalt des Zielordners ab (alter Stand bleibt bis zuletzt erhalten). */
+export function ersetzeDateiSicher(zielordner: string, dateiname: string, inhalt: Buffer): void {
+  const temporaer = temporaerenOrdnerAnlegen(zielordner);
+  fs.writeFileSync(path.join(temporaer, dateiname), inhalt);
+  uebernehmeOrdner(temporaer, zielordner);
+}
+
+/**
+ * Für Quellen, die über ein Kommandozeilen-Werkzeug in einen Ordner schreiben:
+ * liefert einen leeren temporären Ordner und übernimmt ihn erst nach erfolgreicher Prüfung.
+ */
+export function mitTemporaeremOrdner(zielordner: string): { ordner: string; uebernehmen: () => void; verwerfen: () => void } {
+  const temporaer = temporaerenOrdnerAnlegen(zielordner);
+  return {
+    ordner: temporaer,
+    uebernehmen: () => uebernehmeOrdner(temporaer, zielordner),
+    verwerfen: () => verwerfeOrdner(temporaer),
+  };
+}
+
+/** Prüft, ob im Zielordner bereits eine bestimmte Datei liegt. */
+export function dateiVorhanden(zielordner: string, dateiname: string): boolean {
+  return !!dateiname && fs.existsSync(path.join(zielordner, dateiname));
+}
+
+/** Leitet einen Dateinamen aus einer URL ab (letztes Pfadsegment ohne Query). */
+export function dateinameAusUrl(url: string): string {
+  try {
+    const segment = decodeURIComponent(new URL(url).pathname.split('/').pop() ?? '');
+    return segment || 'download.bin';
+  } catch {
+    return 'download.bin';
+  }
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/direkt-link.ts b/packages/bibliothek-server/src/bezugsquellen/direkt-link.ts
new file mode 100644
index 0000000..e5d35b2
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/direkt-link.ts
@@ -0,0 +1,40 @@
+import {
+  abgleichMisserfolg,
+  alsIsoDatum,
+  dateinameAusUrl,
+  dateiVorhanden,
+  ersetzeDateiSicher,
+  inhaltBeanstanden,
+  type Abgleichauftrag,
+  type Bezugsquelle,
+  type QuellenAbgleich,
+} from './basis';
+
+/** Direkter Download-Link: lädt die Datei, erkennt Änderungen über ETag/Last-Modified. */
+export class DirektLinkQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    try {
+      const antwort = await fetch(auftrag.wert, { redirect: 'follow' });
+      if (!antwort.ok) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, `Server nicht erreichbar (HTTP ${antwort.status}).`);
+      }
+      const kennung = antwort.headers.get('etag') ?? antwort.headers.get('last-modified') ?? '';
+      const veroeffentlichtAm = alsIsoDatum(antwort.headers.get('last-modified'));
+      const dateiname = dateinameAusUrl(auftrag.wert);
+      if (kennung && kennung === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, dateiname)) {
+        return { erfolg: true, geaendert: false, version: kennung, dateiname, meldung: 'Bereits aktuell.', veroeffentlichtAm };
+      }
+
+      const inhalt = Buffer.from(await antwort.arrayBuffer());
+      const beanstandung = inhaltBeanstanden(inhalt, dateiname, antwort.headers.get('content-type'), auftrag.vorherigeGroesse);
+      if (beanstandung) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, beanstandung);
+      }
+
+      ersetzeDateiSicher(auftrag.zielordner, dateiname, inhalt);
+      return { erfolg: true, geaendert: true, version: kennung || new Date().toISOString(), dateiname, meldung: 'Aktualisiert.', veroeffentlichtAm };
+    } catch (fehler) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, `Download fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
+    }
+  }
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/git-build.ts b/packages/bibliothek-server/src/bezugsquellen/git-build.ts
new file mode 100644
index 0000000..f5ce3c2
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/git-build.ts
@@ -0,0 +1,244 @@
+import { exec, execFile } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs';
+import * as path from 'path';
+import {
+  abgleichMisserfolg,
+  alsIsoDatum,
+  dateiVorhanden,
+  ersetzeDateiSicher,
+  inhaltBeanstanden,
+  type Abgleichauftrag,
+  type Bezugsquelle,
+  type GeheimnisBedarf,
+  type QuellenAbgleich,
+} from './basis';
+
+const execAsync = promisify(exec);
+const execFileAsync = promisify(execFile);
+
+/** Ein Bau darf lange dauern (z. B. PyInstaller mit vielen Abhängigkeiten). */
+const BAU_ZEITGRENZE_MS = 30 * 60 * 1000;
+const GIT_ZEITGRENZE_MS = 10 * 60 * 1000;
+
+/** Name der Bauanleitung, die ein Repository selbst mitbringt. */
+export const BAUANLEITUNG_DATEI = 'software-bibliothek.json';
+
+interface Bauanleitung {
+  bauBefehl: string;
+  ergebnis: string;
+  geheimnisse: GeheimnisBedarf[];
+}
+
+/**
+ * Aufbau der Anleitung, wie sie im Repository steht. Die Feldnamen sind englisch,
+ * weil sie ein Vertrag nach außen sind — jedes anzubindende Tool schreibt sie.
+ * Beschrieben im Skill „build-secrets" des Repos dh-claude-rules.
+ */
+interface AnleitungImRepo {
+  buildCommand?: string;
+  artifact?: string;
+  secrets?: Array<{ name?: string; description?: string }>;
+}
+
+/**
+ * Interne Tools aus einem Git-Repository: Arbeitskopie holen bzw. aktualisieren,
+ * bauen und das erzeugte Programm bereitstellen.
+ *
+ * Angabe der Quelle: nur die Repo-URL, optional mit `#zweig` (z. B. `ssh://…/tool.git#main`).
+ * Wie gebaut wird, beschreibt das Repository selbst in `software-bibliothek.json`:
+ *
+ *   { "buildCommand": "powershell -ExecutionPolicy Bypass -File build_exe.ps1",
+ *     "artifact":     "dist/MeinTool.exe" }
+ *
+ * So bleibt die Bauanleitung dort, wo sie hingehört — beim Tool. In der Bibliothek
+ * lassen sich beide Angaben bei Bedarf überschreiben (für fremde Repos, die man
+ * nicht ändern kann).
+ *
+ * Die Arbeitskopie legt die Anwendung selbst an (`<Datenordner>\quellen\<id>`) — es
+ * wird nichts vorausgesetzt, was auf dem jeweiligen Rechner schon vorhanden sein müsste.
+ *
+ * Hinweis zur Sicherheit: Der Bau-Befehl wird auf dem Server mit den Rechten des
+ * Dienstbenutzers ausgeführt. Pflegen kann ihn nur, wer lokal am Server arbeitet
+ * bzw. wer ins Repository schreiben darf.
+ */
+export class GitBuildQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    const [repoUrl, gewuenschterZweig] = auftrag.wert.split('#');
+    let commit = '';
+    try {
+      commit = await arbeitskopieAktualisieren(repoUrl.trim(), gewuenschterZweig?.trim() ?? '', auftrag.arbeitsordner, auftrag.gitSshBefehl);
+    } catch (fehler) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, gitFehlertext(fehler));
+    }
+
+    const veroeffentlichtAm = await commitZeitpunkt(auftrag.arbeitsordner);
+    const anleitung = leseBauanleitung(auftrag);
+    if (!anleitung.bauBefehl || !anleitung.ergebnis) {
+      return abgleichMisserfolg(
+        auftrag.aktuelleVersion,
+        `Im Repository fehlt die Datei ${BAUANLEITUNG_DATEI} mit „buildCommand" und „artifact" — alternativ beides in der Bibliothek hinterlegen.`
+      );
+    }
+
+    // Nur bauen, wenn sich seit dem letzten Mal etwas geändert hat.
+    const bekannterDateiname = ergebnisDateiname(anleitung.ergebnis);
+    if (commit === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, bekannterDateiname)) {
+      return { erfolg: true, geaendert: false, version: commit, dateiname: bekannterDateiname, meldung: 'Bereits aktuell (unveränderter Stand).', veroeffentlichtAm, benoetigteGeheimnisse: anleitung.geheimnisse };
+    }
+
+    const fehlende = anleitung.geheimnisse.filter((bedarf) => !auftrag.geheimnisse[bedarf.name]).map((bedarf) => bedarf.name);
+    try {
+      // Geheimnisse gehen als Umgebungsvariablen in den Bau — das Tool entscheidet selbst,
+      // was es daraus macht. Sie landen dadurch nirgends auf der Platte.
+      await execAsync(anleitung.bauBefehl, {
+        cwd: auftrag.arbeitsordner,
+        timeout: BAU_ZEITGRENZE_MS,
+        windowsHide: true,
+        maxBuffer: 32 * 1024 * 1024,
+        env: { ...process.env, ...auftrag.geheimnisse },
+      });
+    } catch (fehler) {
+      const hinweis = fehlende.length > 0 ? ` Nicht hinterlegte Schlüssel: ${fehlende.join(', ')}.` : '';
+      return abgleichMisserfolg(auftrag.aktuelleVersion, `Bau fehlgeschlagen: ${kurzeFehlermeldung(fehler)}${hinweis}`);
+    }
+
+    const ergebnisPfad = findeErgebnis(auftrag.arbeitsordner, anleitung.ergebnis);
+    if (!ergebnisPfad) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, `Der Bau lief durch, aber unter „${anleitung.ergebnis}" liegt keine Datei.`);
+    }
+
+    const dateiname = path.basename(ergebnisPfad);
+    const inhalt = fs.readFileSync(ergebnisPfad);
+    const beanstandung = inhaltBeanstanden(inhalt, dateiname, null, auftrag.vorherigeGroesse);
+    if (beanstandung) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, beanstandung);
+    }
+
+    ersetzeDateiSicher(auftrag.zielordner, dateiname, inhalt);
+    return {
+      erfolg: true,
+      geaendert: true,
+      version: commit,
+      dateiname,
+      meldung: `Aus Git gebaut (Stand ${commit.slice(0, 8)}).`,
+      veroeffentlichtAm,
+      benoetigteGeheimnisse: anleitung.geheimnisse,
+    };
+  }
+}
+
+/**
+ * Bauanleitung ermitteln: bevorzugt aus dem Repository selbst, ersatzweise aus den
+ * in der Bibliothek hinterlegten Angaben (die dort gepflegten haben Vorrang, damit
+ * sich fremde Repos übersteuern lassen).
+ */
+function leseBauanleitung(auftrag: Abgleichauftrag): Bauanleitung {
+  let ausRepo: AnleitungImRepo = {};
+  const anleitungsPfad = path.join(auftrag.arbeitsordner, BAUANLEITUNG_DATEI);
+  if (fs.existsSync(anleitungsPfad)) {
+    try {
+      ausRepo = JSON.parse(fs.readFileSync(anleitungsPfad, 'utf8')) as AnleitungImRepo;
+    } catch {
+      // Unlesbare Anleitung wie „nicht vorhanden" behandeln — die Angaben aus der
+      // Bibliothek können den Bau trotzdem ermöglichen.
+    }
+  }
+  const angeforderte = Array.isArray(ausRepo.secrets) ? ausRepo.secrets : [];
+  return {
+    bauBefehl: auftrag.bauBefehl.trim() || (ausRepo.buildCommand ?? '').trim(),
+    ergebnis: auftrag.bauErgebnis.trim() || (ausRepo.artifact ?? '').trim(),
+    geheimnisse: angeforderte
+      .filter((bedarf): bedarf is { name: string; description?: string } => typeof bedarf?.name === 'string' && !!bedarf.name.trim())
+      .map((bedarf) => ({ name: bedarf.name.trim(), beschreibung: String(bedarf.description ?? '') })),
+  };
+}
+
+/** Klont die Arbeitskopie oder bringt sie auf den neuesten Stand. Liefert den Commit. */
+async function arbeitskopieAktualisieren(repoUrl: string, gewuenschterZweig: string, arbeitsordner: string, sshBefehl: string): Promise<string> {
+  // Die Kennung kommt aus den Einstellungen, nicht aus der URL — so bleibt der
+  // Katalogeintrag frei von Konto-Angaben und ein Kontowechsel ist eine Änderung.
+  const umgebung = sshBefehl ? { ...process.env, GIT_SSH_COMMAND: sshBefehl } : process.env;
+  const git = (argumente: string[], cwd?: string) =>
+    execFileAsync('git', argumente, { cwd, timeout: GIT_ZEITGRENZE_MS, windowsHide: true, maxBuffer: 16 * 1024 * 1024, env: umgebung });
+
+  if (!fs.existsSync(path.join(arbeitsordner, '.git'))) {
+    fs.rmSync(arbeitsordner, { recursive: true, force: true });
+    fs.mkdirSync(path.dirname(arbeitsordner), { recursive: true });
+    await git(['clone', repoUrl, arbeitsordner]);
+  } else {
+    await git(['remote', 'set-url', 'origin', repoUrl], arbeitsordner);
+    await git(['fetch', '--prune', 'origin'], arbeitsordner);
+  }
+
+  const zweig = gewuenschterZweig || (await ermittleStandardZweig(arbeitsordner));
+  // Harter Abgleich auf den Fernstand — lokale Reste aus einem früheren Bau stören sonst.
+  await git(['checkout', '-B', zweig, `origin/${zweig}`], arbeitsordner);
+  await git(['reset', '--hard', `origin/${zweig}`], arbeitsordner);
+  await git(['clean', '-ffdx', '-e', 'node_modules', '-e', '.venv'], arbeitsordner);
+
+  const { stdout } = await git(['rev-parse', 'HEAD'], arbeitsordner);
+  return stdout.trim();
+}
+
+/** Zeitpunkt des ausgecheckten Commits — das ist der „Stand" des internen Tools. */
+async function commitZeitpunkt(arbeitsordner: string): Promise<string> {
+  try {
+    const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%cI'], {
+      cwd: arbeitsordner,
+      timeout: 30000,
+      windowsHide: true,
+    });
+    return alsIsoDatum(stdout.trim());
+  } catch {
+    return '';
+  }
+}
+
+async function ermittleStandardZweig(arbeitsordner: string): Promise<string> {
+  try {
+    const { stdout } = await execFileAsync('git', ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], {
+      cwd: arbeitsordner,
+      timeout: 30000,
+      windowsHide: true,
+    });
+    return stdout.trim().replace(/^origin\//, '') || 'main';
+  } catch {
+    return 'main'; // Repos ohne gesetzten HEAD-Zeiger
+  }
+}
+
+/** Löst den Ergebnis-Pfad auf; im Dateinamen ist ein `*` erlaubt. */
+function findeErgebnis(arbeitsordner: string, muster: string): string | undefined {
+  const vollstaendig = path.resolve(arbeitsordner, muster);
+  if (!muster.includes('*')) {
+    return fs.existsSync(vollstaendig) ? vollstaendig : undefined;
+  }
+  const ordner = path.dirname(vollstaendig);
+  if (!fs.existsSync(ordner)) {
+    return undefined;
+  }
+  const regel = new RegExp(`^${path.basename(vollstaendig).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*')}$`, 'i');
+  const treffer = fs.readdirSync(ordner).filter((name) => regel.test(name));
+  return treffer.length > 0 ? path.join(ordner, treffer[0]) : undefined;
+}
+
+function ergebnisDateiname(muster: string): string {
+  const name = path.basename(muster);
+  return name.includes('*') ? '' : name;
+}
+
+function gitFehlertext(fehler: unknown): string {
+  const text = kurzeFehlermeldung(fehler);
+  if (/authenticat|permission denied|could not read|host key|publickey/i.test(text)) {
+    return `Git-Anmeldung fehlgeschlagen. Der Dienstbenutzer braucht Zugriff auf das Repository (SSH-Schlüssel bzw. einmalige Anmeldung im Windows-Anmeldespeicher). Ursprüngliche Meldung: ${text}`;
+  }
+  return `Git fehlgeschlagen: ${text}`;
+}
+
+/** Kommandozeilen-Fehler enthalten oft seitenlange Ausgaben — auf das Wesentliche kürzen. */
+function kurzeFehlermeldung(fehler: unknown): string {
+  const roh = fehler instanceof Error ? ((fehler as Error & { stderr?: string }).stderr || fehler.message) : String(fehler);
+  const zeilen = roh.split('\n').map((zeile) => zeile.trim()).filter(Boolean);
+  return zeilen.slice(-4).join(' | ').slice(0, 500);
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/github.ts b/packages/bibliothek-server/src/bezugsquellen/github.ts
new file mode 100644
index 0000000..de969d7
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/github.ts
@@ -0,0 +1,95 @@
+import {
+  abgleichMisserfolg,
+  alsIsoDatum,
+  dateiVorhanden,
+  ersetzeDateiSicher,
+  inhaltBeanstanden,
+  type Abgleichauftrag,
+  type Bezugsquelle,
+  type QuellenAbgleich,
+} from './basis';
+
+interface GithubAsset {
+  name: string;
+  browser_download_url: string;
+}
+
+interface GithubRelease {
+  tag_name: string;
+  /** Veröffentlichungszeitpunkt des Releases beim Anbieter. */
+  published_at: string;
+  assets: GithubAsset[];
+}
+
+/** GitHub-Releases: holt das neueste Release und lädt ein passendes Installer-Asset. */
+export class GithubQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    try {
+      const antwort = await fetch(`https://api.github.com/repos/${auftrag.wert}/releases/latest`, {
+        headers: { 'User-Agent': 'software-bibliothek', Accept: 'application/vnd.github+json' },
+      });
+      if (!antwort.ok) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, `GitHub nicht erreichbar (HTTP ${antwort.status}).`);
+      }
+      const release = (await antwort.json()) as GithubRelease;
+      const asset = waehlePassendesAsset(release.assets);
+      if (!asset) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, `Im Release ${release.tag_name} liegt keine herunterladbare Datei.`);
+      }
+      const veroeffentlichtAm = alsIsoDatum(release.published_at);
+      if (release.tag_name === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, asset.name)) {
+        return { erfolg: true, geaendert: false, version: release.tag_name, dateiname: asset.name, meldung: 'Bereits aktuell.', veroeffentlichtAm };
+      }
+
+      const dateiAntwort = await fetch(asset.browser_download_url, { headers: { 'User-Agent': 'software-bibliothek' }, redirect: 'follow' });
+      if (!dateiAntwort.ok) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, `Asset-Download fehlgeschlagen (HTTP ${dateiAntwort.status}).`);
+      }
+      const inhalt = Buffer.from(await dateiAntwort.arrayBuffer());
+      const beanstandung = inhaltBeanstanden(inhalt, asset.name, dateiAntwort.headers.get('content-type'), auftrag.vorherigeGroesse);
+      if (beanstandung) {
+        return abgleichMisserfolg(auftrag.aktuelleVersion, beanstandung);
+      }
+
+      ersetzeDateiSicher(auftrag.zielordner, asset.name, inhalt);
+      return { erfolg: true, geaendert: true, version: release.tag_name, dateiname: asset.name, meldung: `Auf ${release.tag_name} aktualisiert.`, veroeffentlichtAm };
+    } catch (fehler) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, `GitHub-Abgleich fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
+    }
+  }
+}
+
+/**
+ * Wählt aus den Release-Dateien den passenden Windows-Installer aus.
+ *
+ * Bewertet statt „erster Treffer gewinnt", damit die Reihenfolge im Release
+ * das Ergebnis nicht beeinflusst: 64-Bit vor allem anderen, ARM und 32-Bit
+ * ausgeschlossen, `.exe` vor `.msi`, portable Varianten nur als Notnagel.
+ */
+export function waehlePassendesAsset(assets: GithubAsset[]): GithubAsset | undefined {
+  const kandidaten = assets
+    .filter((eintrag) => /\.(exe|msi|msix|zip)$/i.test(eintrag.name))
+    .filter((eintrag) => !/arm|aarch|x86|ia32|32.?bit/i.test(eintrag.name));
+  if (kandidaten.length === 0) {
+    return undefined;
+  }
+  const bewertet = kandidaten.map((eintrag) => ({ eintrag, punkte: bewerteAsset(eintrag.name) }));
+  bewertet.sort((links, rechts) => rechts.punkte - links.punkte);
+  return bewertet[0].eintrag;
+}
+
+function bewerteAsset(name: string): number {
+  let punkte = 0;
+  if (/x64|amd64|win64|64.?bit/i.test(name)) {
+    punkte += 100; // ausdrücklich 64-Bit
+  }
+  if (/\.exe$/i.test(name)) {
+    punkte += 20; // Installer bevorzugt gegenüber .msi/.zip
+  } else if (/\.msi$/i.test(name)) {
+    punkte += 10;
+  }
+  if (/portable/i.test(name)) {
+    punkte -= 50; // nur nehmen, wenn es nichts anderes gibt
+  }
+  return punkte;
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/index.ts b/packages/bibliothek-server/src/bezugsquellen/index.ts
new file mode 100644
index 0000000..39a2459
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/index.ts
@@ -0,0 +1,25 @@
+import type { QuelleTyp } from 'bibliothek-types';
+import type { Bezugsquelle } from './basis';
+import { DirektLinkQuelle } from './direkt-link';
+import { GithubQuelle } from './github';
+import { WingetQuelle } from './winget';
+import { SvnQuelle } from './svn';
+import { GitBuildQuelle } from './git-build';
+
+export type { Abgleichauftrag, Bezugsquelle, QuellenAbgleich } from './basis';
+
+// Registry der automatisch abgleichbaren Bezugsquellen. Eine neue Quellenart
+// hinzufügen = hier einen Eintrag ergänzen. `manuell` fehlt bewusst: dort wird
+// die Datei von Hand hinterlegt, es gibt nichts abzugleichen.
+const registry: Partial<Record<QuelleTyp, Bezugsquelle>> = {
+  direkt: new DirektLinkQuelle(),
+  github: new GithubQuelle(),
+  winget: new WingetQuelle(),
+  svn: new SvnQuelle(),
+  git: new GitBuildQuelle(),
+};
+
+/** Liefert den Handler für einen Quell-Typ (oder undefined, falls unbekannt). */
+export function bezugsquelleFuer(typ: QuelleTyp): Bezugsquelle | undefined {
+  return registry[typ];
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/svn.ts b/packages/bibliothek-server/src/bezugsquellen/svn.ts
new file mode 100644
index 0000000..0be2f6f
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/svn.ts
@@ -0,0 +1,53 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import * as path from 'path';
+import { abgleichMisserfolg, alsIsoDatum, dateiVorhanden, mitTemporaeremOrdner, type Abgleichauftrag, type Bezugsquelle, type QuellenAbgleich } from './basis';
+
+const execFileAsync = promisify(execFile);
+
+/**
+ * SVN: exportiert eine Datei aus einem Repository, erkennt Änderungen über die Revision.
+ * Anmeldedaten kommen aus dem Windows-Anmeldespeicher des Dienstbenutzers —
+ * die Erstanmeldung muss einmal interaktiv als dieser Benutzer erfolgt sein.
+ */
+export class SvnQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    const dateiname = decodeURIComponent(auftrag.wert.split('/').pop() ?? 'datei');
+    let revision = '';
+    let veroeffentlichtAm = '';
+    try {
+      const svnInfo = (element: string) =>
+        execFileAsync('svn', ['info', '--show-item', element, '--non-interactive', auftrag.wert], { timeout: 60000, windowsHide: true });
+      revision = (await svnInfo('last-changed-revision')).stdout.trim();
+      veroeffentlichtAm = alsIsoDatum((await svnInfo('last-changed-date')).stdout.trim());
+    } catch (fehler) {
+      return abgleichMisserfolg(auftrag.aktuelleVersion, svnFehlertext(fehler));
+    }
+
+    if (revision && revision === auftrag.aktuelleVersion && dateiVorhanden(auftrag.zielordner, dateiname)) {
+      return { erfolg: true, geaendert: false, version: revision, dateiname, meldung: 'Bereits aktuell.', veroeffentlichtAm };
+    }
+
+    const ablage = mitTemporaeremOrdner(auftrag.zielordner);
+    try {
+      await execFileAsync('svn', ['export', '--force', '--non-interactive', auftrag.wert, path.join(ablage.ordner, dateiname)], {
+        timeout: 300000,
+        windowsHide: true,
+      });
+      ablage.uebernehmen();
+      return { erfolg: true, geaendert: true, version: revision || new Date().toISOString(), dateiname, meldung: `SVN-Revision ${revision} exportiert.`, veroeffentlichtAm };
+    } catch (fehler) {
+      ablage.verwerfen();
+      return abgleichMisserfolg(auftrag.aktuelleVersion, svnFehlertext(fehler));
+    }
+  }
+}
+
+/** Macht fehlende Anmeldedaten als häufigste Ursache kenntlich. */
+function svnFehlertext(fehler: unknown): string {
+  const text = fehler instanceof Error ? fehler.message : String(fehler);
+  if (/authoriz|authentic|E170001|credentials/i.test(text)) {
+    return `SVN-Anmeldung fehlt oder ist ungültig. Einmalig als Dienstbenutzer anmelden (die Anmeldung wird dann im Windows-Speicher hinterlegt). Ursprüngliche Meldung: ${text}`;
+  }
+  return `SVN fehlgeschlagen: ${text}`;
+}
diff --git a/packages/bibliothek-server/src/bezugsquellen/winget.ts b/packages/bibliothek-server/src/bezugsquellen/winget.ts
new file mode 100644
index 0000000..0c9264a
--- /dev/null
+++ b/packages/bibliothek-server/src/bezugsquellen/winget.ts
@@ -0,0 +1,81 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+import * as fs from 'fs';
+import * as path from 'path';
+import { abgleichMisserfolg, alsIsoDatum, mitTemporaeremOrdner, type Abgleichauftrag, type Bezugsquelle, type QuellenAbgleich } from './basis';
+
+const execFileAsync = promisify(execFile);
+
+/** winget: lädt das Paket per `winget download` (nur unter Windows verfügbar). */
+export class WingetQuelle implements Bezugsquelle {
+  async gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich> {
+    // In einen temporären Ordner laden — der bisherige Stand bleibt erhalten, falls etwas schiefgeht.
+    const ablage = mitTemporaeremOrdner(auftrag.zielordner);
+    try {
+      const { stdout } = await execFileAsync(
+        'winget',
+        [
+          'download',
+          '--id',
+          auftrag.wert,
+          '--exact',
+          '--download-directory',
+          ablage.ordner,
+          '--accept-package-agreements',
+          '--accept-source-agreements',
+          '--disable-interactivity',
+        ],
+        { timeout: 300000, windowsHide: true }
+      );
+      const geladeneDateien = fs.readdirSync(ablage.ordner);
+      const installer = geladeneDateien.find((name) => /\.(exe|msi|msix|appx|appxbundle|zip)$/i.test(name));
+      if (!installer) {
+        ablage.verwerfen();
+        return abgleichMisserfolg(auftrag.aktuelleVersion, 'winget hat keine Installer-Datei geliefert.');
+      }
+      // winget legt zusätzlich sein Manifest (.yaml) ab — im Ablageordner soll nur der Installer liegen.
+      for (const name of geladeneDateien) {
+        if (name !== installer) {
+          fs.rmSync(path.join(ablage.ordner, name), { recursive: true, force: true });
+        }
+      }
+      const version = versionAusAusgabe(stdout) || new Date().toISOString();
+      ablage.uebernehmen();
+      return {
+        erfolg: true,
+        geaendert: true,
+        version,
+        dateiname: installer,
+        meldung: `Über winget geladen (${version}).`,
+        // Das Veröffentlichungsdatum steht nur in der Paket-Auskunft, nicht in der Download-Ausgabe.
+        veroeffentlichtAm: alsIsoDatum(await veroeffentlichungsdatum(auftrag.wert)),
+      };
+    } catch (fehler) {
+      ablage.verwerfen();
+      return abgleichMisserfolg(auftrag.aktuelleVersion, `winget fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}`);
+    }
+  }
+}
+
+function versionAusAusgabe(ausgabe: string): string {
+  const treffer = ausgabe.match(/[Vv]ersion[:\s]+([0-9][0-9.]*)/);
+  return treffer ? treffer[1] : '';
+}
+
+/**
+ * Fragt das Veröffentlichungsdatum beim Paket-Verzeichnis ab. `winget download`
+ * gibt es nicht aus, `winget show` schon — dafür ein zweiter, kurzer Aufruf.
+ */
+async function veroeffentlichungsdatum(paketId: string): Promise<string> {
+  try {
+    const { stdout } = await execFileAsync(
+      'winget',
+      ['show', '--id', paketId, '--exact', '--accept-source-agreements', '--disable-interactivity'],
+      { timeout: 120000, windowsHide: true }
+    );
+    const treffer = stdout.match(/(?:Release\s*Date|Ver[öo]ffentlich\w*)[:\s]+([0-9]{4}-[0-9]{2}-[0-9]{2})/i);
+    return treffer ? treffer[1] : '';
+  } catch {
+    return ''; // ohne Datum ist die Zeile nur weniger aussagekräftig, kein Grund zu scheitern
+  }
+}
diff --git a/packages/bibliothek-server/src/datenbank.ts b/packages/bibliothek-server/src/datenbank.ts
new file mode 100644
index 0000000..c3f188e
--- /dev/null
+++ b/packages/bibliothek-server/src/datenbank.ts
@@ -0,0 +1,313 @@
+import { DatabaseSync } from 'node:sqlite';
+import * as fs from 'fs';
+import * as path from 'path';
+import type { AbgleichErgebnis, GeheimnisBedarf, Software, SoftwareEingabe, ZugriffslogEintrag } from 'bibliothek-types';
+import { effektiverDatenOrdner } from './konfiguration';
+
+// Ablageort aus den Einstellungen (Standard: Ordner „data" neben der Anwendung).
+const datenOrdner = effektiverDatenOrdner();
+
+/** Ablageordner für die bereitgestellten Programm-Dateien (Downloads), je Software ein Unterordner. */
+export const DATEIEN_ORDNER = path.join(datenOrdner, 'files');
+
+/** Arbeitskopien der Quellen, die etwas auschecken/bauen müssen (Git), je Software ein Unterordner. */
+export const QUELLEN_ORDNER = path.join(datenOrdner, 'sources');
+
+/**
+ * Bau-Geheimnisse (z. B. API-Schlüssel), die vor dem Bau in die Arbeitskopie gelegt werden.
+ * Wird nirgends ausgeliefert — die Download-Route bedient nur `files/`.
+ */
+export const GEHEIMNIS_ORDNER = path.join(datenOrdner, 'secrets');
+
+/**
+ * Frühere Fassungen legten die Unterordner unter deutschen Namen an. Beim Start
+ * einmalig umbenennen — sonst stünde eine bestehende Installation ohne ihre
+ * Dateien da. Existieren beide Namen, bleibt der alte unangetastet; das von Hand
+ * zu klären ist sicherer, als stillschweigend zusammenzuführen.
+ */
+function benenneAlteOrdnerUm(): void {
+  for (const [alterName, neuerName] of [
+    ['dateien', 'files'],
+    ['quellen', 'sources'],
+    ['geheimnisse', 'secrets'],
+  ]) {
+    const alterPfad = path.join(datenOrdner, alterName);
+    const neuerPfad = path.join(datenOrdner, neuerName);
+    if (!fs.existsSync(alterPfad)) {
+      continue;
+    }
+    if (fs.existsSync(neuerPfad)) {
+      console.warn(`Ordner „${alterName}" und „${neuerName}" liegen beide vor — bitte von Hand zusammenführen.`);
+      continue;
+    }
+    fs.renameSync(alterPfad, neuerPfad);
+    console.log(`Datenordner umbenannt: ${alterName} → ${neuerName}`);
+  }
+}
+
+benenneAlteOrdnerUm();
+
+fs.mkdirSync(DATEIEN_ORDNER, { recursive: true });
+fs.mkdirSync(QUELLEN_ORDNER, { recursive: true });
+fs.mkdirSync(GEHEIMNIS_ORDNER, { recursive: true });
+
+export const datenbank = new DatabaseSync(path.join(datenOrdner, 'bibliothek.db'));
+datenbank.exec('PRAGMA journal_mode = WAL');
+
+/** Legt die Tabellen an und migriert fehlende Spalten. Bestehende Daten bleiben erhalten. */
+export function initialisiereDatenbank(): void {
+  datenbank.exec(`
+    CREATE TABLE IF NOT EXISTS software (
+      id              INTEGER PRIMARY KEY AUTOINCREMENT,
+      name            TEXT NOT NULL,
+      beschreibung    TEXT NOT NULL DEFAULT '',
+      elternId        INTEGER NOT NULL DEFAULT 0,
+      quelleTyp       TEXT NOT NULL DEFAULT '',
+      quelleWert      TEXT NOT NULL DEFAULT '',
+      bauBefehl       TEXT NOT NULL DEFAULT '',
+      bauErgebnis     TEXT NOT NULL DEFAULT '',
+      einstufung      TEXT NOT NULL DEFAULT 'lizenzfrei',
+      programmart     TEXT NOT NULL DEFAULT 'unbestimmt',
+      hinweis         TEXT NOT NULL DEFAULT '',
+      anleitungUrl    TEXT NOT NULL DEFAULT '',
+      version         TEXT NOT NULL DEFAULT '',
+      dateiname       TEXT NOT NULL DEFAULT '',
+      dateiStandVom   TEXT NOT NULL DEFAULT '',
+      veroeffentlichtAm     TEXT NOT NULL DEFAULT '',
+      signaturStatus  TEXT NOT NULL DEFAULT '',
+      signaturHerausgeber   TEXT NOT NULL DEFAULT '',
+      erwarteterHerausgeber TEXT NOT NULL DEFAULT '',
+      benoetigteGeheimnisse TEXT NOT NULL DEFAULT '[]',
+      letzterAbgleich TEXT NOT NULL DEFAULT '',
+      abgleichErfolg  INTEGER NOT NULL DEFAULT 0,
+      abgleichMeldung TEXT NOT NULL DEFAULT '',
+      erstelltAm      TEXT NOT NULL,
+      aktualisiertAm  TEXT NOT NULL
+    );
+
+    CREATE TABLE IF NOT EXISTS zugriffslog (
+      id               INTEGER PRIMARY KEY AUTOINCREMENT,
+      zeitpunkt        TEXT NOT NULL,
+      ipAdresse        TEXT NOT NULL DEFAULT '',
+      rechnername      TEXT NOT NULL DEFAULT '',
+      angegebenerName  TEXT NOT NULL DEFAULT '',
+      aktion           TEXT NOT NULL DEFAULT ''
+    );
+  `);
+
+  // Migration bestehender Datenbanken: neue Spalten ergänzen, falls sie fehlen.
+  const neueSpalten: Array<[string, string]> = [
+    ['quelleTyp', "TEXT NOT NULL DEFAULT ''"],
+    ['quelleWert', "TEXT NOT NULL DEFAULT ''"],
+    ['elternId', 'INTEGER NOT NULL DEFAULT 0'],
+    ['dateiStandVom', "TEXT NOT NULL DEFAULT ''"],
+    ['veroeffentlichtAm', "TEXT NOT NULL DEFAULT ''"],
+    ['signaturStatus', "TEXT NOT NULL DEFAULT ''"],
+    ['signaturHerausgeber', "TEXT NOT NULL DEFAULT ''"],
+    ['erwarteterHerausgeber', "TEXT NOT NULL DEFAULT ''"],
+    ['benoetigteGeheimnisse', "TEXT NOT NULL DEFAULT '[]'"],
+    ['bauBefehl', "TEXT NOT NULL DEFAULT ''"],
+    ['bauErgebnis', "TEXT NOT NULL DEFAULT ''"],
+    ['einstufung', "TEXT NOT NULL DEFAULT 'lizenzfrei'"],
+    ['programmart', "TEXT NOT NULL DEFAULT 'unbestimmt'"],
+    ['hinweis', "TEXT NOT NULL DEFAULT ''"],
+    ['anleitungUrl', "TEXT NOT NULL DEFAULT ''"],
+    ['letzterAbgleich', "TEXT NOT NULL DEFAULT ''"],
+    ['abgleichErfolg', 'INTEGER NOT NULL DEFAULT 0'],
+    ['abgleichMeldung', "TEXT NOT NULL DEFAULT ''"],
+  ];
+  for (const [spalte, definition] of neueSpalten) {
+    spalteErgaenzenFallsFehlt('software', spalte, definition);
+  }
+
+  // Ablösung des früheren Ja/Nein-Feldes durch die dreiteilige Einstufung.
+  if (spalteVorhanden('software', 'lizenzpflichtig')) {
+    datenbank.exec("UPDATE software SET einstufung = 'lizenzpflichtig' WHERE lizenzpflichtig = 1");
+    datenbank.exec('ALTER TABLE software DROP COLUMN lizenzpflichtig');
+  }
+
+  // Kategorien haben sich als überflüssig erwiesen — die Einstufung gliedert den Katalog.
+  if (spalteVorhanden('software', 'kategorie')) {
+    datenbank.exec('ALTER TABLE software DROP COLUMN kategorie');
+  }
+}
+
+function spalteVorhanden(tabelle: string, spalte: string): boolean {
+  const vorhandene = datenbank.prepare(`PRAGMA table_info(${tabelle})`).all() as Array<{ name: string }>;
+  return vorhandene.some((eintrag) => eintrag.name === spalte);
+}
+
+function spalteErgaenzenFallsFehlt(tabelle: string, spalte: string, definition: string): void {
+  if (!spalteVorhanden(tabelle, spalte)) {
+    datenbank.exec(`ALTER TABLE ${tabelle} ADD COLUMN ${spalte} ${definition}`);
+  }
+}
+
+// --- Software-Katalog ---
+
+/** Wandelt eine DB-Zeile in ein Software-Objekt (SQLite kennt kein boolean → INTEGER 0/1). */
+function zeileZuSoftware(zeile: Record<string, unknown>): Software {
+  return {
+    ...zeile,
+    abgleichErfolg: Boolean(zeile.abgleichErfolg),
+    benoetigteGeheimnisse: alsGeheimnisBedarf(zeile.benoetigteGeheimnisse),
+  } as Software;
+}
+
+/** Die angeforderten Geheimnisse liegen als JSON-Text in der Spalte. */
+function alsGeheimnisBedarf(rohwert: unknown): GeheimnisBedarf[] {
+  if (typeof rohwert !== 'string' || !rohwert.trim()) {
+    return [];
+  }
+  try {
+    const gelesen = JSON.parse(rohwert) as GeheimnisBedarf[];
+    return Array.isArray(gelesen) ? gelesen : [];
+  } catch {
+    return [];
+  }
+}
+
+export function alleSoftware(): Software[] {
+  const zeilen = datenbank.prepare('SELECT * FROM software ORDER BY name COLLATE NOCASE').all() as Array<Record<string, unknown>>;
+  return zeilen.map(zeileZuSoftware);
+}
+
+export function softwareNachId(id: number): Software | undefined {
+  const zeile = datenbank.prepare('SELECT * FROM software WHERE id = ?').get(id) as Record<string, unknown> | undefined;
+  return zeile ? zeileZuSoftware(zeile) : undefined;
+}
+
+export function softwareAnlegen(eingabe: SoftwareEingabe): Software {
+  const jetzt = new Date().toISOString();
+  const ergebnis = datenbank
+    .prepare(
+      `INSERT INTO software (name, beschreibung, elternId, quelleTyp, quelleWert, bauBefehl, bauErgebnis, einstufung, programmart, hinweis, anleitungUrl, erwarteterHerausgeber, erstelltAm, aktualisiertAm)
+       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
+    )
+    .run(
+      eingabe.name,
+      eingabe.beschreibung,
+      eingabe.elternId,
+      eingabe.quelleTyp,
+      eingabe.quelleWert,
+      eingabe.bauBefehl,
+      eingabe.bauErgebnis,
+      eingabe.einstufung,
+      eingabe.programmart,
+      eingabe.hinweis,
+      eingabe.anleitungUrl,
+      eingabe.erwarteterHerausgeber,
+      jetzt,
+      jetzt
+    );
+  return softwareNachId(Number(ergebnis.lastInsertRowid)) as Software;
+}
+
+export function softwareAktualisieren(id: number, eingabe: SoftwareEingabe): Software | undefined {
+  if (!softwareNachId(id)) {
+    return undefined;
+  }
+  const jetzt = new Date().toISOString();
+  datenbank
+    .prepare(
+      `UPDATE software SET name = ?, beschreibung = ?, elternId = ?, quelleTyp = ?, quelleWert = ?,
+       bauBefehl = ?, bauErgebnis = ?, einstufung = ?, programmart = ?, hinweis = ?, anleitungUrl = ?,
+       erwarteterHerausgeber = ?, aktualisiertAm = ?
+       WHERE id = ?`
+    )
+    .run(
+      eingabe.name,
+      eingabe.beschreibung,
+      eingabe.elternId,
+      eingabe.quelleTyp,
+      eingabe.quelleWert,
+      eingabe.bauBefehl,
+      eingabe.bauErgebnis,
+      eingabe.einstufung,
+      eingabe.programmart,
+      eingabe.hinweis,
+      eingabe.anleitungUrl,
+      eingabe.erwarteterHerausgeber,
+      jetzt,
+      id
+    );
+  return softwareNachId(id);
+}
+
+/**
+ * Löscht eine Software samt ihrer Zusatzinhalte (ein Zusatzinhalt ohne sein Tool
+ * wäre sinnlos) und räumt die zugehörigen Ordner mit ab — sonst bleiben die
+ * bereitgestellten Dateien und Arbeitskopien für immer liegen.
+ */
+export function softwareLoeschen(id: number): boolean {
+  for (const zusatzinhalt of datenbank.prepare('SELECT id FROM software WHERE elternId = ?').all(id) as Array<{ id: number }>) {
+    entferneOrdnerVon(Number(zusatzinhalt.id));
+  }
+  datenbank.prepare('DELETE FROM software WHERE elternId = ?').run(id);
+  const ergebnis = datenbank.prepare('DELETE FROM software WHERE id = ?').run(id);
+  if (Number(ergebnis.changes) > 0) {
+    entferneOrdnerVon(id);
+    return true;
+  }
+  return false;
+}
+
+/** Entfernt bereitgestellte Datei und Arbeitskopie einer Software. */
+export function entferneOrdnerVon(id: number): void {
+  fs.rmSync(path.join(DATEIEN_ORDNER, String(id)), { recursive: true, force: true });
+  fs.rmSync(path.join(QUELLEN_ORDNER, String(id)), { recursive: true, force: true });
+}
+
+/** Schreibt das Ergebnis eines Bezugsquellen-Abgleichs zurück (Version, Datei, Status). */
+export function abgleichSpeichern(id: number, ergebnis: AbgleichErgebnis): void {
+  const jetzt = new Date().toISOString();
+  // Der Dateistand wird nur fortgeschrieben, wenn wirklich eine neue Datei kam —
+  // eine reine Prüfung ohne Änderung ist kein Update.
+  const bisheriger = softwareNachId(id)?.dateiStandVom ?? '';
+  datenbank
+    .prepare(
+      `UPDATE software SET version = ?, dateiname = ?, dateiStandVom = ?, veroeffentlichtAm = ?, signaturStatus = ?, signaturHerausgeber = ?,
+       benoetigteGeheimnisse = ?, letzterAbgleich = ?, abgleichErfolg = ?, abgleichMeldung = ?, aktualisiertAm = ?
+       WHERE id = ?`
+    )
+    .run(
+      ergebnis.version,
+      ergebnis.dateiname,
+      ergebnis.geaendert ? jetzt : bisheriger,
+      ergebnis.veroeffentlichtAm,
+      ergebnis.signaturStatus,
+      ergebnis.signaturHerausgeber,
+      JSON.stringify(ergebnis.benoetigteGeheimnisse),
+      jetzt,
+      ergebnis.erfolg ? 1 : 0,
+      ergebnis.meldung,
+      jetzt,
+      id
+    );
+}
+
+/**
+ * Übernimmt beim Umzug bekannte Zusatzangaben, ohne einen Abgleich vorzutäuschen:
+ * `letzterAbgleich` bleibt leer, damit der Eintrag als „noch nicht geholt" gilt
+ * und nicht als Problemfall erscheint.
+ */
+export function umzugsAngabenSetzen(id: number, veroeffentlichtAm: string, benoetigteGeheimnisse: GeheimnisBedarf[]): void {
+  datenbank
+    .prepare('UPDATE software SET veroeffentlichtAm = ?, benoetigteGeheimnisse = ? WHERE id = ?')
+    .run(veroeffentlichtAm, JSON.stringify(benoetigteGeheimnisse), id);
+}
+
+// --- Zugriffsprotokoll ---
+
+export function zugriffProtokollieren(eintrag: Omit<ZugriffslogEintrag, 'id'>): void {
+  datenbank
+    .prepare(
+      `INSERT INTO zugriffslog (zeitpunkt, ipAdresse, rechnername, angegebenerName, aktion)
+       VALUES (?, ?, ?, ?, ?)`
+    )
+    .run(eintrag.zeitpunkt, eintrag.ipAdresse, eintrag.rechnername, eintrag.angegebenerName, eintrag.aktion);
+}
+
+export function alleZugriffe(): ZugriffslogEintrag[] {
+  return datenbank.prepare('SELECT * FROM zugriffslog ORDER BY id DESC').all() as unknown as ZugriffslogEintrag[];
+}
diff --git a/packages/bibliothek-server/src/geheimnisse.ts b/packages/bibliothek-server/src/geheimnisse.ts
new file mode 100644
index 0000000..05ccd8b
--- /dev/null
+++ b/packages/bibliothek-server/src/geheimnisse.ts
@@ -0,0 +1,127 @@
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import { GEHEIMNIS_ORDNER } from './datenbank';
+
+/**
+ * Bau-Geheimnisse (API-Schlüssel, Tokens …) nach einheitlichem Muster.
+ *
+ * Standard — bewusst wie bei gängigen Bau-Systemen, damit die Bibliothek nichts
+ * über einzelne Tools wissen muss:
+ *
+ *  1. Das Repository deklariert in `software-bibliothek.json`, welche Geheimnisse
+ *     es braucht:  "secrets": [{ "name": "ANTHROPIC_API_KEY", "description": "…" }]
+ *  2. Der Wert wird in der Bibliothek hinterlegt (nur lokal am Server pflegbar).
+ *  3. Beim Bau wird er als **Umgebungsvariable** gesetzt. Was das Tool daraus macht
+ *     — Datei erzeugen, direkt einkompilieren — entscheidet sein eigenes Bau-Skript.
+ *
+ * Ablage getrennt vom Katalog und NIE im Software-Datensatz: `/api/software` ist aus
+ * dem Netzwerk lesbar. Auch der Ordner `secrets/` wird nirgends ausgeliefert —
+ * die Download-Route bedient ausschließlich `files/<id>/<datei>`.
+ */
+export interface Geheimniswert {
+  /** Name der Umgebungsvariablen, z. B. `ANTHROPIC_API_KEY`. */
+  name: string;
+  wert: string;
+}
+
+/** Was die Oberfläche zu sehen bekommt — nie der Wert selbst. */
+export interface GeheimnisUebersicht {
+  name: string;
+  hinterlegt: boolean;
+  zeichen: number;
+}
+
+function ablagePfad(softwareId: number): string {
+  return path.join(GEHEIMNIS_ORDNER, `${softwareId}.json`);
+}
+
+/**
+ * Schränkt die Zugriffsrechte auf den Geheimnis-Ordner ein.
+ *
+ * Nötig, weil Unterordner von %ProgramData% die Leseberechtigung für die
+ * Gruppe „Benutzer" erben — sonst könnte jeder lokal angemeldete Benutzer die
+ * hinterlegten Schlüssel lesen. Node's `mode` greift unter Windows nicht auf
+ * ACLs, deshalb über `icacls`. Bezeichnung über die bekannten SIDs, damit es
+ * unabhängig von der Systemsprache funktioniert.
+ */
+export function sichereGeheimnisOrdner(): void {
+  if (process.platform !== 'win32') {
+    return;
+  }
+  const dienstbenutzer = `${process.env.USERDOMAIN ?? ''}\\${process.env.USERNAME ?? ''}`;
+  const rechte = [
+    '*S-1-5-18:(OI)(CI)F', // SYSTEM
+    '*S-1-5-32-544:(OI)(CI)F', // Administratoren
+  ];
+  if (process.env.USERNAME) {
+    rechte.push(`${dienstbenutzer}:(OI)(CI)F`); // der Benutzer, unter dem der Dienst läuft
+  }
+  try {
+    execFileSync('icacls', [GEHEIMNIS_ORDNER, '/inheritance:r', '/grant:r', ...rechte], { stdio: 'pipe', windowsHide: true });
+  } catch (fehler) {
+    console.warn(`Zugriffsrechte des Schlüssel-Ordners konnten nicht eingeschränkt werden: ${String(fehler).slice(0, 200)}`);
+  }
+}
+
+export function ladeGeheimnisse(softwareId: number): Geheimniswert[] {
+  const pfad = ablagePfad(softwareId);
+  if (!fs.existsSync(pfad)) {
+    return [];
+  }
+  try {
+    const gelesen = JSON.parse(fs.readFileSync(pfad, 'utf8')) as Geheimniswert[];
+    return Array.isArray(gelesen) ? gelesen.filter((eintrag) => istGueltigerName(eintrag?.name)) : [];
+  } catch {
+    return [];
+  }
+}
+
+/** Nur Namen und Länge — der Wert verlässt den Server nicht. */
+export function uebersichtGeheimnisse(softwareId: number): GeheimnisUebersicht[] {
+  return ladeGeheimnisse(softwareId).map((eintrag) => ({
+    name: eintrag.name,
+    hinterlegt: eintrag.wert.length > 0,
+    zeichen: eintrag.wert.length,
+  }));
+}
+
+/**
+ * Speichert die Werte. Ein leerer Wert bedeutet „unverändert lassen" — so kann die
+ * Oberfläche die Liste zurückschicken, ohne die Geheimnisse je gesehen zu haben.
+ */
+export function speichereGeheimnisse(softwareId: number, eingehende: Geheimniswert[]): GeheimnisUebersicht[] {
+  const bisherige = new Map(ladeGeheimnisse(softwareId).map((eintrag) => [eintrag.name, eintrag.wert]));
+  const zusammengefuehrt: Geheimniswert[] = [];
+  for (const eintrag of eingehende) {
+    const name = (eintrag?.name ?? '').trim();
+    if (!istGueltigerName(name)) {
+      continue;
+    }
+    const neuerWert = String(eintrag.wert ?? '');
+    zusammengefuehrt.push({ name, wert: neuerWert || bisherige.get(name) || '' });
+  }
+  fs.mkdirSync(GEHEIMNIS_ORDNER, { recursive: true });
+  fs.writeFileSync(ablagePfad(softwareId), JSON.stringify(zusammengefuehrt, null, 2), { mode: 0o600 });
+  return zusammengefuehrt.map((eintrag) => ({ name: eintrag.name, hinterlegt: eintrag.wert.length > 0, zeichen: eintrag.wert.length }));
+}
+
+export function loescheGeheimnisse(softwareId: number): void {
+  fs.rmSync(ablagePfad(softwareId), { force: true });
+}
+
+/** Umgebungsvariablen für den Bau — nur Einträge mit hinterlegtem Wert. */
+export function alsUmgebung(geheimnisse: Geheimniswert[]): Record<string, string> {
+  const umgebung: Record<string, string> = {};
+  for (const eintrag of geheimnisse) {
+    if (eintrag.wert) {
+      umgebung[eintrag.name] = eintrag.wert;
+    }
+  }
+  return umgebung;
+}
+
+/** Nur was als Umgebungsvariable taugt — schützt zugleich vor Einschleusen. */
+function istGueltigerName(name: string | undefined): boolean {
+  return !!name && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
+}
diff --git a/packages/bibliothek-server/src/index.ts b/packages/bibliothek-server/src/index.ts
new file mode 100644
index 0000000..a24f6ad
--- /dev/null
+++ b/packages/bibliothek-server/src/index.ts
@@ -0,0 +1,393 @@
+import './warnungen'; // muss vor dem Laden von node:sqlite stehen
+import express from 'express';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as dns from 'node:dns';
+import { EINSTUFUNGEN, PROGRAMMARTEN, type Einstufung, type Programmart, type QuelleTyp, type SoftwareEingabe } from 'bibliothek-types';
+import {
+  initialisiereDatenbank,
+  alleSoftware,
+  softwareNachId,
+  softwareAnlegen,
+  softwareAktualisieren,
+  softwareLoeschen,
+  abgleichSpeichern,
+  zugriffProtokollieren,
+  alleZugriffe,
+  DATEIEN_ORDNER,
+} from './datenbank';
+import { gleicheSoftwareAb, gleicheAlleAb } from './abgleich';
+import { erzeugeZip } from './zip';
+import { pruefeSignatur } from './signatur';
+import { loescheGeheimnisse, sichereGeheimnisOrdner, speichereGeheimnisse, uebersichtGeheimnisse } from './geheimnisse';
+import { erstelleUmzugsPaket, uebernehmeUmzugsPaket } from './umzug';
+import { istEinzeldatei, oberflaechenOrdner } from './pfade';
+import { aktuelleEinstellungen, speichereEinstellungen, effektiverDatenOrdner, effektiverPort, konfigurationsDatei } from './konfiguration';
+
+const PORT = effektiverPort();
+const ABGLEICH_INTERVALL_MS = 24 * 60 * 60 * 1000; // täglicher Auto-Check
+
+initialisiereDatenbank();
+sichereGeheimnisOrdner();
+
+const anwendung = express();
+// Umzugs-Pakete können je nach Kataloggröße etwas umfangreicher sein.
+anwendung.use(express.json({ limit: '25mb' }));
+
+// --- Zugriffs-Herkunft ---
+
+function anfrageIp(anfrage: express.Request): string {
+  const roh = anfrage.ip ?? anfrage.socket.remoteAddress ?? '';
+  return roh.replace(/^::ffff:/, ''); // IPv4-mapped IPv6 auf reine IPv4 normalisieren
+}
+
+function istLokalerZugriff(anfrage: express.Request): boolean {
+  const ip = anfrageIp(anfrage);
+  return ip === '127.0.0.1' || ip === '::1';
+}
+
+/** Middleware: erlaubt die Route nur bei direktem Zugriff am Server (localhost). */
+function nurLokal(anfrage: express.Request, antwort: express.Response, next: express.NextFunction): void {
+  if (!istLokalerZugriff(anfrage)) {
+    antwort.status(403).json({ fehler: 'Diese Aktion ist nur direkt am Server möglich.' });
+    return;
+  }
+  next();
+}
+
+// --- Umgebung & Status ---
+
+anwendung.get('/api/umgebung', (anfrage, antwort) => {
+  antwort.json({ lokal: istLokalerZugriff(anfrage) });
+});
+
+anwendung.get('/api/status', (_anfrage, antwort) => {
+  antwort.json({ status: 'ok', dienst: 'software-bibliothek', softwareAnzahl: alleSoftware().length });
+});
+
+// --- Katalog lesen (auch aus dem Netzwerk erreichbar) ---
+
+anwendung.get('/api/software', (_anfrage, antwort) => {
+  antwort.json(alleSoftware());
+});
+
+anwendung.get('/api/software/:id', (anfrage, antwort) => {
+  const eintrag = softwareNachId(Number(anfrage.params.id));
+  if (!eintrag) {
+    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
+    return;
+  }
+  antwort.json(eintrag);
+});
+
+// --- Datei-Download (auch aus dem Netzwerk, wird protokolliert) ---
+
+anwendung.get('/api/software/:id/download', async (anfrage, antwort) => {
+  const eintrag = softwareNachId(Number(anfrage.params.id));
+  if (!eintrag || !eintrag.dateiname) {
+    antwort.status(404).json({ fehler: 'Für diese Software liegt (noch) keine Datei vor.' });
+    return;
+  }
+  const dateipfad = path.join(DATEIEN_ORDNER, String(eintrag.id), eintrag.dateiname);
+  if (!fs.existsSync(dateipfad)) {
+    antwort.status(404).json({ fehler: 'Die Datei fehlt auf dem Server.' });
+    return;
+  }
+  await protokolliereDownload(anfrage, eintrag.name);
+  antwort.download(dateipfad, eintrag.dateiname);
+});
+
+// --- Sammel-Download mehrerer Programme als ZIP (auch aus dem Netzwerk) ---
+
+anwendung.get('/api/download-mehrere', async (anfrage, antwort) => {
+  const ids = String(anfrage.query.ids ?? '')
+    .split(',')
+    .map((eintrag) => Number(eintrag.trim()))
+    .filter((id) => Number.isInteger(id) && id > 0);
+  if (ids.length === 0) {
+    antwort.status(400).json({ fehler: 'Keine Auswahl übergeben' });
+    return;
+  }
+
+  const eintraege = [];
+  const namen: string[] = [];
+  for (const id of ids) {
+    const software = softwareNachId(id);
+    if (!software?.dateiname) {
+      continue;
+    }
+    const dateipfad = path.join(DATEIEN_ORDNER, String(id), software.dateiname);
+    if (!fs.existsSync(dateipfad)) {
+      continue;
+    }
+    // Je Programm ein eigener Ordner im Archiv — verhindert Namenskollisionen.
+    eintraege.push({ dateiname: `${dateinameSaeubern(software.name)}/${software.dateiname}`, inhalt: fs.readFileSync(dateipfad) });
+    namen.push(software.name);
+  }
+  if (eintraege.length === 0) {
+    antwort.status(404).json({ fehler: 'Zu der Auswahl liegen keine Dateien vor.' });
+    return;
+  }
+
+  await protokolliereDownload(anfrage, `${eintraege.length} Programme: ${namen.join(', ')}`);
+  antwort.type('application/zip');
+  antwort.setHeader('Content-Disposition', 'attachment; filename="software-auswahl.zip"');
+  antwort.send(erzeugeZip(eintraege));
+});
+
+// --- Katalog verwalten (nur lokal am Server) ---
+
+anwendung.post('/api/software', nurLokal, (anfrage, antwort) => {
+  const eingabe = normalisiereEingabe(anfrage.body);
+  if (!eingabe.name) {
+    antwort.status(400).json({ fehler: 'Name ist erforderlich' });
+    return;
+  }
+  const angelegt = softwareAnlegen(eingabe);
+  void gleicheSoftwareAb(angelegt); // Datei im Hintergrund holen, Antwort nicht blockieren
+  antwort.status(201).json(angelegt);
+});
+
+anwendung.put('/api/software/:id', nurLokal, (anfrage, antwort) => {
+  const eingabe = normalisiereEingabe(anfrage.body);
+  if (!eingabe.name) {
+    antwort.status(400).json({ fehler: 'Name ist erforderlich' });
+    return;
+  }
+  const aktualisiert = softwareAktualisieren(Number(anfrage.params.id), eingabe);
+  if (!aktualisiert) {
+    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
+    return;
+  }
+  void gleicheSoftwareAb(aktualisiert); // Quelle könnte geändert sein → neu holen
+  antwort.json(aktualisiert);
+});
+
+anwendung.delete('/api/software/:id', nurLokal, (anfrage, antwort) => {
+  const id = Number(anfrage.params.id);
+  const geloescht = softwareLoeschen(id);
+  if (geloescht) {
+    loescheGeheimnisse(id);
+  }
+  antwort.status(geloescht ? 204 : 404).end();
+});
+
+// --- Bezugsquellen-Abgleich (nur lokal am Server) ---
+
+anwendung.post('/api/software/:id/abgleichen', nurLokal, async (anfrage, antwort) => {
+  const software = softwareNachId(Number(anfrage.params.id));
+  if (!software) {
+    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
+    return;
+  }
+  await gleicheSoftwareAb(software);
+  antwort.json(softwareNachId(software.id));
+});
+
+anwendung.post('/api/abgleich/alle', nurLokal, async (_anfrage, antwort) => {
+  await gleicheAlleAb(alleSoftware());
+  antwort.json(alleSoftware());
+});
+
+// --- Datei von Hand hinterlegen (Quellentyp „manuell", nur lokal am Server) ---
+
+anwendung.post('/api/software/:id/datei', nurLokal, (anfrage, antwort) => {
+  const software = softwareNachId(Number(anfrage.params.id));
+  if (!software) {
+    antwort.status(404).json({ fehler: 'Software nicht gefunden' });
+    return;
+  }
+  // basename schützt davor, über den Dateinamen aus dem Ablageordner auszubrechen.
+  const dateiname = path.basename(String(anfrage.query.dateiname ?? '')).trim();
+  if (!dateiname) {
+    antwort.status(400).json({ fehler: 'Dateiname fehlt' });
+    return;
+  }
+  const zielordner = path.join(DATEIEN_ORDNER, String(software.id));
+  fs.rmSync(zielordner, { recursive: true, force: true }); // nur eine aktuelle Datei je Software
+  fs.mkdirSync(zielordner, { recursive: true });
+
+  const zielpfad = path.join(zielordner, dateiname);
+  const zielDatei = fs.createWriteStream(zielpfad);
+  anfrage.pipe(zielDatei);
+  zielDatei.on('finish', () => {
+    void (async () => {
+      const befund = await pruefeSignatur(zielpfad);
+      abgleichSpeichern(software.id, {
+        version: software.version,
+        dateiname,
+        erfolg: true,
+        meldung: 'Manuell hinterlegt.',
+        geaendert: true,
+        signaturStatus: befund.status,
+        signaturHerausgeber: befund.herausgeber,
+        veroeffentlichtAm: software.veroeffentlichtAm,
+        benoetigteGeheimnisse: software.benoetigteGeheimnisse,
+      });
+      antwort.json(softwareNachId(software.id));
+    })();
+  });
+  zielDatei.on('error', (fehler) => {
+    antwort.status(500).json({ fehler: `Datei konnte nicht gespeichert werden: ${fehler.message}` });
+  });
+});
+
+// --- Bau-Geheimnisse (nur lokal am Server; Werte verlassen den Server nie) ---
+
+anwendung.get('/api/software/:id/geheimnisse', nurLokal, (anfrage, antwort) => {
+  antwort.json(uebersichtGeheimnisse(Number(anfrage.params.id)));
+});
+
+anwendung.put('/api/software/:id/geheimnisse', nurLokal, (anfrage, antwort) => {
+  const eingehende = Array.isArray(anfrage.body) ? anfrage.body : [];
+  antwort.json(speichereGeheimnisse(Number(anfrage.params.id), eingehende));
+});
+
+// --- Umzug auf einen anderen Rechner (nur lokal am Server) ---
+
+anwendung.get('/api/export', nurLokal, (anfrage, antwort) => {
+  const mitGeheimnissen = anfrage.query.geheimnisse === 'ja';
+  const paket = erstelleUmzugsPaket(mitGeheimnissen);
+  const namenszusatz = mitGeheimnissen ? '-mit-schluesseln' : '';
+  antwort.setHeader('Content-Disposition', `attachment; filename="software-bibliothek${namenszusatz}.json"`);
+  antwort.type('application/json').send(JSON.stringify(paket, null, 2));
+});
+
+anwendung.post('/api/import', nurLokal, (anfrage, antwort) => {
+  try {
+    antwort.json(uebernehmeUmzugsPaket(anfrage.body));
+  } catch (fehler) {
+    antwort.status(400).json({ fehler: fehler instanceof Error ? fehler.message : 'Paket konnte nicht eingelesen werden.' });
+  }
+});
+
+// --- Einstellungen (nur lokal am Server) ---
+
+anwendung.get('/api/einstellungen', nurLokal, (_anfrage, antwort) => {
+  antwort.json({
+    einstellungen: aktuelleEinstellungen(),
+    konfigurationsDatei: konfigurationsDatei(),
+    effektiverDatenOrdner: effektiverDatenOrdner(),
+    laufenderPort: PORT,
+  });
+});
+
+anwendung.put('/api/einstellungen', nurLokal, (anfrage, antwort) => {
+  const gespeicherte = speichereEinstellungen(anfrage.body ?? {});
+  antwort.json({
+    einstellungen: gespeicherte,
+    konfigurationsDatei: konfigurationsDatei(),
+    // Nach dem Speichern gilt der neue Ordner erst beim nächsten Start —
+    // die Datenbank ist bereits geöffnet.
+    effektiverDatenOrdner: effektiverDatenOrdner(),
+    laufenderPort: PORT,
+  });
+});
+
+// --- Zugriffsprotokoll (nur lokal am Server) ---
+
+anwendung.get('/api/zugriffslog', nurLokal, (_anfrage, antwort) => {
+  antwort.json(alleZugriffe());
+});
+
+// --- Hilfsfunktionen ---
+
+const ERLAUBTE_QUELLEN: Array<QuelleTyp | ''> = ['', 'direkt', 'github', 'winget', 'svn', 'git', 'manuell'];
+
+/** Säubert die Eingabe und stellt einen gültigen Quell-Typ sicher. */
+function normalisiereEingabe(koerper: Partial<SoftwareEingabe>): SoftwareEingabe {
+  const typ = ERLAUBTE_QUELLEN.includes((koerper.quelleTyp ?? '') as QuelleTyp) ? (koerper.quelleTyp ?? '') : '';
+  return {
+    name: (koerper.name ?? '').trim(),
+    beschreibung: (koerper.beschreibung ?? '').trim(),
+    elternId: Number(koerper.elternId ?? 0) || 0,
+    quelleTyp: typ as QuelleTyp | '',
+    quelleWert: (koerper.quelleWert ?? '').trim(),
+    bauBefehl: (koerper.bauBefehl ?? '').trim(),
+    bauErgebnis: (koerper.bauErgebnis ?? '').trim(),
+    einstufung: EINSTUFUNGEN.some((eintrag) => eintrag.id === koerper.einstufung) ? (koerper.einstufung as Einstufung) : 'lizenzfrei',
+    programmart: PROGRAMMARTEN.some((eintrag) => eintrag.id === koerper.programmart) ? (koerper.programmart as Programmart) : 'unbestimmt',
+    hinweis: (koerper.hinweis ?? '').trim(),
+    erwarteterHerausgeber: (koerper.erwarteterHerausgeber ?? '').trim(),
+    anleitungUrl: (koerper.anleitungUrl ?? '').trim(),
+  };
+}
+
+/** Ersetzt Zeichen, die in Datei-/Ordnernamen nicht zulässig sind. */
+function dateinameSaeubern(name: string): string {
+  return name.replace(/[\\/:*?"<>|]/g, '_').trim() || 'Programm';
+}
+
+/** Schreibt einen Download in das Zugriffsprotokoll (IP, Reverse-DNS-Rechnername, angegebener Name). */
+async function protokolliereDownload(anfrage: express.Request, softwareName: string): Promise<void> {
+  const ip = anfrageIp(anfrage);
+  let rechnername = '';
+  try {
+    const namen = await dns.promises.reverse(ip);
+    rechnername = namen[0] ?? '';
+  } catch {
+    // Kein Reverse-DNS-Eintrag verfügbar – Rechnername bleibt leer.
+  }
+  const angegebenerName = typeof anfrage.query.name === 'string' ? anfrage.query.name : '';
+  zugriffProtokollieren({
+    zeitpunkt: new Date().toISOString(),
+    ipAdresse: ip,
+    rechnername,
+    angegebenerName,
+    aktion: `Heruntergeladen: ${softwareName}`,
+  });
+}
+
+/** Liest ein in die exe eingebettetes Asset als Puffer (undefined, falls nicht vorhanden). */
+function eingebettetesAsset(name: string): Buffer | undefined {
+  try {
+    const sea = require('node:sea') as { getRawAsset(schluessel: string): ArrayBuffer };
+    return Buffer.from(sea.getRawAsset(name));
+  } catch {
+    return undefined; // Asset nicht eingebettet
+  }
+}
+
+/**
+ * Liefert die gebaute Oberfläche aus: als exe aus den eingebetteten Assets,
+ * sonst aus dem dist-Ordner des Client-Pakets. Muss nach den API-Routen stehen.
+ */
+function richteOberflaecheEin(): void {
+  if (istEinzeldatei()) {
+    anwendung.use((anfrage, antwort, next) => {
+      if (anfrage.path.startsWith('/api/')) {
+        next();
+        return;
+      }
+      const gewuenscht = anfrage.path === '/' ? 'index.html' : anfrage.path.replace(/^\//, '');
+      const inhalt = eingebettetesAsset(gewuenscht);
+      if (inhalt) {
+        antwort.type(path.extname(gewuenscht) || '.html').send(inhalt);
+        return;
+      }
+      const startseite = eingebettetesAsset('index.html');
+      if (!startseite) {
+        next();
+        return;
+      }
+      antwort.type('.html').send(startseite);
+    });
+    return;
+  }
+  if (fs.existsSync(oberflaechenOrdner())) {
+    anwendung.use(express.static(oberflaechenOrdner()));
+  }
+}
+
+richteOberflaecheEin();
+
+// Täglicher Hintergrund-Abgleich aller Quellen.
+setInterval(() => {
+  void gleicheAlleAb(alleSoftware());
+}, ABGLEICH_INTERVALL_MS);
+
+anwendung.listen(PORT, () => {
+  console.log(`Software-Bibliothek läuft auf http://localhost:${PORT}`);
+  console.log(`Daten:         ${effektiverDatenOrdner()}`);
+  console.log(`Einstellungen: ${konfigurationsDatei()}`);
+});
diff --git a/packages/bibliothek-server/src/konfiguration.ts b/packages/bibliothek-server/src/konfiguration.ts
new file mode 100644
index 0000000..a13c673
--- /dev/null
+++ b/packages/bibliothek-server/src/konfiguration.ts
@@ -0,0 +1,107 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import type { Einstellungen } from 'bibliothek-types';
+import { anwendungsDatenOrdner } from './pfade';
+
+/**
+ * Einstellungen liegen in `konfiguration.json` neben der Anwendung. Die Datei wird
+ * beim ersten Start mit Standardwerten angelegt und kann über die Oberfläche
+ * (nur lokal am Server) oder von Hand gepflegt werden.
+ *
+ * Umgebungsvariablen haben Vorrang — praktisch für abweichende Testläufe.
+ */
+
+const STANDARD_EINSTELLUNGEN: Einstellungen = {
+  datenOrdner: '', // leer = Unterordner „data" im Anwendungsdaten-Ordner
+  port: 3000,
+  gitBenutzer: '', // leer = SSH-Konfiguration des Dienstbenutzers entscheidet
+  gitSchluessel: '',
+};
+
+const konfigurationsPfad = path.join(anwendungsDatenOrdner(), 'konfiguration.json');
+
+/** Liest die Konfigurationsdatei (und legt sie beim ersten Start an). */
+function leseOderErzeuge(): Einstellungen {
+  try {
+    if (fs.existsSync(konfigurationsPfad)) {
+      return normalisiere(JSON.parse(fs.readFileSync(konfigurationsPfad, 'utf8')) as Partial<Einstellungen>);
+    }
+    const standard = { ...STANDARD_EINSTELLUNGEN };
+    schreibeDatei(standard);
+    return standard;
+  } catch (fehler) {
+    console.warn(`konfiguration.json konnte nicht gelesen werden, Standardwerte greifen: ${String(fehler)}`);
+    return { ...STANDARD_EINSTELLUNGEN };
+  }
+}
+
+// Beim Laden des Moduls einlesen, damit auch die Datenbank-Schicht schon den
+// konfigurierten Ordner sieht (sie wird beim Start direkt initialisiert).
+let gelesene: Einstellungen = leseOderErzeuge();
+
+/** Speichert geänderte Einstellungen. Der Datenordner wirkt erst nach einem Neustart. */
+export function speichereEinstellungen(neue: Partial<Einstellungen>): Einstellungen {
+  gelesene = normalisiere({ ...gelesene, ...neue });
+  schreibeDatei(gelesene);
+  return gelesene;
+}
+
+export function aktuelleEinstellungen(): Einstellungen {
+  return gelesene;
+}
+
+export function konfigurationsDatei(): string {
+  return konfigurationsPfad;
+}
+
+/** Tatsächlich genutzter Datenordner — Umgebungsvariable vor Einstellung vor Standard. */
+export function effektiverDatenOrdner(): string {
+  const ausUmgebung = process.env.DATEN_ORDNER?.trim();
+  if (ausUmgebung) {
+    return path.resolve(ausUmgebung);
+  }
+  if (gelesene.datenOrdner.trim()) {
+    return path.resolve(gelesene.datenOrdner.trim());
+  }
+  return path.join(anwendungsDatenOrdner(), 'data');
+}
+
+/** Tatsächlich genutzter Port — Umgebungsvariable vor Einstellung vor Standard. */
+export function effektiverPort(): number {
+  const ausUmgebung = Number(process.env.PORT);
+  if (Number.isInteger(ausUmgebung) && ausUmgebung > 0) {
+    return ausUmgebung;
+  }
+  return gelesene.port;
+}
+
+function normalisiere(eingabe: Partial<Einstellungen>): Einstellungen {
+  const port = Number(eingabe.port);
+  return {
+    datenOrdner: typeof eingabe.datenOrdner === 'string' ? eingabe.datenOrdner.trim() : '',
+    port: Number.isInteger(port) && port > 0 && port < 65536 ? port : STANDARD_EINSTELLUNGEN.port,
+    // Nur was als SSH-Benutzername taugt — schützt zugleich vor Einschleusen in die Befehlszeile.
+    gitBenutzer: /^[A-Za-z0-9._-]*$/.test(String(eingabe.gitBenutzer ?? '').trim()) ? String(eingabe.gitBenutzer ?? '').trim() : '',
+    gitSchluessel: String(eingabe.gitSchluessel ?? '').trim(),
+  };
+}
+
+/**
+ * Baut den SSH-Aufruf für Git aus den Einstellungen. Leer, wenn nichts gepflegt ist —
+ * dann greift die SSH-Konfiguration des Dienstbenutzers wie bisher.
+ */
+export function gitSshBefehl(): string {
+  const teile = ['ssh'];
+  if (gelesene.gitBenutzer) {
+    teile.push('-o', `User=${gelesene.gitBenutzer}`);
+  }
+  if (gelesene.gitSchluessel) {
+    teile.push('-i', `"${gelesene.gitSchluessel.replace(/\\/g, '/')}"`);
+  }
+  return teile.length > 1 ? teile.join(' ') : '';
+}
+
+function schreibeDatei(einstellungen: Einstellungen): void {
+  fs.mkdirSync(path.dirname(konfigurationsPfad), { recursive: true });
+  fs.writeFileSync(konfigurationsPfad, `${JSON.stringify(einstellungen, null, 2)}\n`);
+}
diff --git a/packages/bibliothek-server/src/pfade.ts b/packages/bibliothek-server/src/pfade.ts
new file mode 100644
index 0000000..0ebad90
--- /dev/null
+++ b/packages/bibliothek-server/src/pfade.ts
@@ -0,0 +1,45 @@
+import * as path from 'path';
+
+/** Läuft die Anwendung als gebündelte Einzeldatei (Node SEA / exe)? */
+export function istEinzeldatei(): boolean {
+  try {
+    return (require('node:sea') as { isSea(): boolean }).isSea();
+  } catch {
+    return false; // node:sea nicht verfügbar -> normaler Node-Start
+  }
+}
+
+const ANWENDUNGSNAME = 'Software-Bibliothek';
+
+/**
+ * Ordner für Konfiguration und Daten der Anwendung.
+ *
+ * Bewusst neutral unter `%ProgramData%` (üblicher Ort für maschinenweite
+ * Anwendungsdaten unter Windows) statt neben der exe oder gar im Quellcode-
+ * Verzeichnis: So liegen die Daten immer am selben Ort, egal wo die exe
+ * abgelegt oder wohin sie verschoben wird — und ein Entwicklungslauf
+ * verschmutzt das Projektverzeichnis nicht.
+ *
+ * Der Datenordner selbst lässt sich in den Einstellungen frei umlegen.
+ */
+export function anwendungsDatenOrdner(): string {
+  const programmDaten = process.env.ProgramData?.trim();
+  if (programmDaten) {
+    return path.join(programmDaten, ANWENDUNGSNAME);
+  }
+  // Kein Windows bzw. Variable nicht gesetzt: neben der Anwendung ablegen.
+  return path.join(anwendungsOrdner(), `.${ANWENDUNGSNAME.toLowerCase()}`);
+}
+
+/** Ordner der Anwendung selbst: als exe der Ordner neben der exe, sonst die Projekt-Wurzel. */
+export function anwendungsOrdner(): string {
+  if (istEinzeldatei()) {
+    return path.dirname(process.execPath);
+  }
+  return path.join(__dirname, '..', '..', '..');
+}
+
+/** Ordner mit der gebauten Oberfläche (nur im Nicht-exe-Betrieb genutzt). */
+export function oberflaechenOrdner(): string {
+  return path.join(__dirname, '..', '..', 'bibliothek-client', 'dist');
+}
diff --git a/packages/bibliothek-server/src/signatur.ts b/packages/bibliothek-server/src/signatur.ts
new file mode 100644
index 0000000..566db02
--- /dev/null
+++ b/packages/bibliothek-server/src/signatur.ts
@@ -0,0 +1,66 @@
+import { execFile } from 'child_process';
+import { promisify } from 'util';
+
+const execFileAsync = promisify(execFile);
+
+export interface Signaturbefund {
+  /** Windows-Status: Valid, NotSigned, UnknownError, HashMismatch … bzw. „nicht geprüft". */
+  status: string;
+  /** Anzeigename des Signaturgebers (leer, wenn unsigniert). */
+  herausgeber: string;
+}
+
+const NICHT_GEPRUEFT: Signaturbefund = { status: 'nicht geprüft', herausgeber: '' };
+
+/**
+ * Prüft die Windows-Signatur (Authenticode) einer Datei.
+ *
+ * Bewusst nur erfassend, nicht blockierend: Manche Programme werden legitim
+ * unsigniert ausgeliefert (z. B. 7-Zip). Erst ein hinterlegter Erwartungswert
+ * macht daraus eine Zusicherung — siehe `weichtHerausgeberAb`.
+ *
+ * Der Dateipfad geht über eine Umgebungsvariable an PowerShell, damit
+ * Anführungszeichen im Pfad nichts zerbrechen können.
+ */
+export async function pruefeSignatur(dateipfad: string): Promise<Signaturbefund> {
+  if (process.platform !== 'win32') {
+    return NICHT_GEPRUEFT;
+  }
+  const skript = [
+    '$befund = Get-AuthenticodeSignature -LiteralPath $env:ZU_PRUEFENDE_DATEI;',
+    '$name = if ($befund.SignerCertificate) { $befund.SignerCertificate.Subject } else { "" };',
+    'ConvertTo-Json -Compress @{ status = $befund.Status.ToString(); herausgeber = $name }',
+  ].join(' ');
+  try {
+    const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-NonInteractive', '-Command', skript], {
+      timeout: 60000,
+      windowsHide: true,
+      env: { ...process.env, ZU_PRUEFENDE_DATEI: dateipfad },
+    });
+    const roh = JSON.parse(stdout) as { status?: string; herausgeber?: string };
+    return {
+      status: roh.status ?? 'nicht geprüft',
+      herausgeber: lesbarerHerausgeber(roh.herausgeber ?? ''),
+    };
+  } catch {
+    return NICHT_GEPRUEFT; // Prüfung selbst fehlgeschlagen — kein Grund, den Abgleich zu verwerfen
+  }
+}
+
+/** Zieht aus dem Zertifikats-Betreff den sprechenden Teil (CN bzw. erstes Feld). */
+function lesbarerHerausgeber(betreff: string): string {
+  if (!betreff) {
+    return '';
+  }
+  const felder = betreff.split(',').map((feld) => feld.trim());
+  const gemeinsamerName = felder.find((feld) => feld.startsWith('CN='));
+  return (gemeinsamerName ?? felder[0]).replace(/^CN=/, '').replace(/^"|"$/g, '');
+}
+
+/** Weicht der gefundene Herausgeber von der Erwartung ab? Ohne Erwartung nie. */
+export function weichtHerausgeberAb(erwartet: string, gefunden: string): boolean {
+  if (!erwartet.trim()) {
+    return false;
+  }
+  return !gefunden.toLowerCase().includes(erwartet.trim().toLowerCase());
+}
diff --git a/packages/bibliothek-server/src/umzug.ts b/packages/bibliothek-server/src/umzug.ts
new file mode 100644
index 0000000..dc7accb
--- /dev/null
+++ b/packages/bibliothek-server/src/umzug.ts
@@ -0,0 +1,131 @@
+import type { Einstellungen, Software } from 'bibliothek-types';
+import { alleSoftware, softwareAnlegen, softwareLoeschen, umzugsAngabenSetzen } from './datenbank';
+import { aktuelleEinstellungen, speichereEinstellungen } from './konfiguration';
+import { ladeGeheimnisse, loescheGeheimnisse, speichereGeheimnisse, type Geheimniswert } from './geheimnisse';
+
+/**
+ * Umzug auf einen anderen Rechner.
+ *
+ * Bewusst als lesbares JSON und nicht als Ordner-Kopie:
+ *  - Die geholten Dateien (mehrere hundert MB) und die Git-Arbeitskopien (GB)
+ *    sind jederzeit reproduzierbar — ein Abgleich holt sie neu. Nur Katalog,
+ *    Einstellungen und Geheimnisse sind unersetzlich, und das sind wenige KB.
+ *  - Die Datenbank im laufenden Betrieb zu kopieren wäre unsicher: die jüngsten
+ *    Änderungen stehen im WAL und fehlten in der Kopie.
+ *  - JSON übersteht Schema-Änderungen: beim Einlesen werden Felder zugeordnet,
+ *    statt eine Binärdatei einer bestimmten Version vorauszusetzen.
+ */
+
+/** Aktuelle Fassung des Austauschformats — erlaubt später sauberes Nachziehen. */
+const FORMAT_FASSUNG = 1;
+
+export interface UmzugsPaket {
+  fassung: number;
+  erstelltAm: string;
+  einstellungen: Einstellungen;
+  software: Software[];
+  /** Nur enthalten, wenn ausdrücklich mit exportiert (enthält Klartext-Schlüssel!). */
+  geheimnisse?: Record<string, Geheimniswert[]>;
+}
+
+export function erstelleUmzugsPaket(mitGeheimnissen: boolean): UmzugsPaket {
+  const eintraege = alleSoftware();
+  const paket: UmzugsPaket = {
+    fassung: FORMAT_FASSUNG,
+    erstelltAm: new Date().toISOString(),
+    einstellungen: aktuelleEinstellungen(),
+    software: eintraege,
+  };
+  if (mitGeheimnissen) {
+    paket.geheimnisse = {};
+    for (const eintrag of eintraege) {
+      const werte = ladeGeheimnisse(eintrag.id);
+      if (werte.length > 0) {
+        paket.geheimnisse[String(eintrag.id)] = werte;
+      }
+    }
+  }
+  return paket;
+}
+
+export interface UmzugsErgebnis {
+  uebernommen: number;
+  mitGeheimnissen: number;
+  hinweis: string;
+}
+
+/**
+ * Liest ein Paket ein und ersetzt den bisherigen Katalog vollständig.
+ * Die eigentlichen Programm-Dateien kommen nicht mit — sie werden beim
+ * nächsten Abgleich neu geholt bzw. gebaut.
+ */
+export function uebernehmeUmzugsPaket(paket: UmzugsPaket): UmzugsErgebnis {
+  if (!paket || typeof paket !== 'object' || !Array.isArray(paket.software)) {
+    throw new Error('Die Datei ist kein gültiges Umzugs-Paket.');
+  }
+  if (paket.fassung > FORMAT_FASSUNG) {
+    throw new Error(`Das Paket stammt aus einer neueren Fassung (${paket.fassung}) — bitte die Anwendung aktualisieren.`);
+  }
+
+  // Bisherigen Bestand räumen, damit keine Mischung aus altem und neuem Stand entsteht.
+  for (const vorhandene of alleSoftware()) {
+    loescheGeheimnisse(vorhandene.id);
+    softwareLoeschen(vorhandene.id);
+  }
+
+  // Die Ids ändern sich beim Neuanlegen — für die Eltern-Zuordnung mitführen.
+  const idZuordnung = new Map<number, number>();
+  for (const eintrag of paket.software.filter((eintrag) => !eintrag.elternId)) {
+    idZuordnung.set(eintrag.id, uebernehmeEintrag(eintrag, 0).id);
+  }
+  for (const eintrag of paket.software.filter((eintrag) => eintrag.elternId)) {
+    idZuordnung.set(eintrag.id, uebernehmeEintrag(eintrag, idZuordnung.get(eintrag.elternId) ?? 0).id);
+  }
+
+  let mitGeheimnissen = 0;
+  for (const [alteId, werte] of Object.entries(paket.geheimnisse ?? {})) {
+    const neueId = idZuordnung.get(Number(alteId));
+    if (neueId) {
+      speichereGeheimnisse(neueId, werte);
+      mitGeheimnissen++;
+    }
+  }
+
+  if (paket.einstellungen) {
+    // Datenordner und Port gehören zum Zielrechner und werden bewusst nicht überschrieben.
+    speichereEinstellungen({
+      gitBenutzer: paket.einstellungen.gitBenutzer ?? '',
+      gitSchluessel: paket.einstellungen.gitSchluessel ?? '',
+    });
+  }
+
+  return {
+    uebernommen: idZuordnung.size,
+    mitGeheimnissen,
+    hinweis: 'Die Programm-Dateien wurden nicht mit übertragen — sie werden beim nächsten Abgleich neu geholt bzw. gebaut.',
+  };
+}
+
+/** Legt einen Eintrag neu an und stellt den bekannten Abgleich-Stand wieder her. */
+function uebernehmeEintrag(eintrag: Software, elternId: number): Software {
+  const angelegt = softwareAnlegen({
+    name: eintrag.name ?? '',
+    beschreibung: eintrag.beschreibung ?? '',
+    elternId,
+    quelleTyp: eintrag.quelleTyp ?? '',
+    quelleWert: eintrag.quelleWert ?? '',
+    bauBefehl: eintrag.bauBefehl ?? '',
+    bauErgebnis: eintrag.bauErgebnis ?? '',
+    einstufung: eintrag.einstufung ?? 'lizenzfrei',
+    programmart: eintrag.programmart ?? 'unbestimmt',
+    hinweis: eintrag.hinweis ?? '',
+    anleitungUrl: eintrag.anleitungUrl ?? '',
+    erwarteterHerausgeber: eintrag.erwarteterHerausgeber ?? '',
+  });
+  // Version und Datei bleiben leer (Standard beim Anlegen): Die Datei fehlt auf dem
+  // neuen Rechner noch, der nächste Abgleich soll sie wirklich holen. Bewusst KEIN
+  // fehlgeschlagener Abgleich — „noch nicht geholt" ist kein Problemfall und soll
+  // nicht in der Warnleiste auftauchen.
+  umzugsAngabenSetzen(angelegt.id, eintrag.veroeffentlichtAm ?? '', eintrag.benoetigteGeheimnisse ?? []);
+  return angelegt;
+}
diff --git a/packages/bibliothek-server/src/warnungen.ts b/packages/bibliothek-server/src/warnungen.ts
new file mode 100644
index 0000000..d303ed6
--- /dev/null
+++ b/packages/bibliothek-server/src/warnungen.ts
@@ -0,0 +1,14 @@
+/**
+ * Node meldet `node:sqlite` beim Start als experimentelles Feature. Für den Betrieb
+ * ist das ohne Belang, im Konsolenfenster der Anwendung verunsichert die Meldung aber.
+ * Alle anderen Warnungen werden weiterhin ausgegeben.
+ *
+ * Muss als erstes Modul importiert werden, damit der Filter vor dem Laden von
+ * `node:sqlite` steht.
+ */
+process.removeAllListeners('warning');
+process.on('warning', (warnung: Error) => {
+  if (warnung.name !== 'ExperimentalWarning') {
+    console.warn(warnung);
+  }
+});
diff --git a/packages/bibliothek-server/src/zip.ts b/packages/bibliothek-server/src/zip.ts
new file mode 100644
index 0000000..0ed22aa
--- /dev/null
+++ b/packages/bibliothek-server/src/zip.ts
@@ -0,0 +1,107 @@
+/**
+ * Minimaler ZIP-Schreiber (Speicher-Methode, ohne Komprimierung).
+ *
+ * Bewusst ohne Fremdbibliothek: Installer und Archive sind bereits komprimiert,
+ * erneutes Packen brächte nichts und kostet nur Zeit. Damit bleibt der Server
+ * ohne Zusatz-Abhängigkeit.
+ *
+ * Grenze: Das klassische ZIP-Format adressiert mit 32 Bit, ein Archiv (bzw. eine
+ * Einzeldatei darin) darf also 4 GB nicht überschreiten — ZIP64 ist nicht umgesetzt.
+ */
+
+const CRC_TABELLE = erzeugeCrcTabelle();
+
+function erzeugeCrcTabelle(): Uint32Array {
+  const tabelle = new Uint32Array(256);
+  for (let index = 0; index < 256; index++) {
+    let wert = index;
+    for (let bit = 0; bit < 8; bit++) {
+      wert = wert & 1 ? 0xedb88320 ^ (wert >>> 1) : wert >>> 1;
+    }
+    tabelle[index] = wert >>> 0;
+  }
+  return tabelle;
+}
+
+function crc32(daten: Buffer): number {
+  let pruefsumme = 0xffffffff;
+  for (const byte of daten) {
+    pruefsumme = CRC_TABELLE[(pruefsumme ^ byte) & 0xff] ^ (pruefsumme >>> 8);
+  }
+  return (pruefsumme ^ 0xffffffff) >>> 0;
+}
+
+/** Wandelt einen Zeitpunkt in das von ZIP verwendete DOS-Format (Datum + Uhrzeit). */
+function dosZeitstempel(zeitpunkt: Date): { zeit: number; datum: number } {
+  const zeit = (zeitpunkt.getHours() << 11) | (zeitpunkt.getMinutes() << 5) | (Math.floor(zeitpunkt.getSeconds() / 2) & 0x1f);
+  const datum = ((zeitpunkt.getFullYear() - 1980) << 9) | ((zeitpunkt.getMonth() + 1) << 5) | zeitpunkt.getDate();
+  return { zeit, datum };
+}
+
+export interface ZipEintrag {
+  dateiname: string;
+  inhalt: Buffer;
+}
+
+/** Packt die Einträge zu einem ZIP-Archiv (ohne Komprimierung). */
+export function erzeugeZip(eintraege: ZipEintrag[], zeitpunkt: Date = new Date()): Buffer {
+  const { zeit, datum } = dosZeitstempel(zeitpunkt);
+  const lokaleBloecke: Buffer[] = [];
+  const zentralBloecke: Buffer[] = [];
+  let versatz = 0;
+
+  for (const eintrag of eintraege) {
+    const nameBytes = Buffer.from(eintrag.dateiname, 'utf8');
+    const pruefsumme = crc32(eintrag.inhalt);
+    const groesse = eintrag.inhalt.length;
+
+    const lokalerKopf = Buffer.alloc(30);
+    lokalerKopf.writeUInt32LE(0x04034b50, 0); // Signatur
+    lokalerKopf.writeUInt16LE(20, 4); // benötigte Version
+    lokalerKopf.writeUInt16LE(0x0800, 6); // Flags: Dateiname ist UTF-8
+    lokalerKopf.writeUInt16LE(0, 8); // Methode 0 = gespeichert
+    lokalerKopf.writeUInt16LE(zeit, 10);
+    lokalerKopf.writeUInt16LE(datum, 12);
+    lokalerKopf.writeUInt32LE(pruefsumme, 14);
+    lokalerKopf.writeUInt32LE(groesse, 18); // komprimierte Größe
+    lokalerKopf.writeUInt32LE(groesse, 22); // Originalgröße
+    lokalerKopf.writeUInt16LE(nameBytes.length, 26);
+    lokalerKopf.writeUInt16LE(0, 28); // keine Extra-Felder
+    lokaleBloecke.push(lokalerKopf, nameBytes, eintrag.inhalt);
+
+    const zentralKopf = Buffer.alloc(46);
+    zentralKopf.writeUInt32LE(0x02014b50, 0); // Signatur
+    zentralKopf.writeUInt16LE(20, 4); // erstellt mit Version
+    zentralKopf.writeUInt16LE(20, 6); // benötigte Version
+    zentralKopf.writeUInt16LE(0x0800, 8);
+    zentralKopf.writeUInt16LE(0, 10);
+    zentralKopf.writeUInt16LE(zeit, 12);
+    zentralKopf.writeUInt16LE(datum, 14);
+    zentralKopf.writeUInt32LE(pruefsumme, 16);
+    zentralKopf.writeUInt32LE(groesse, 20);
+    zentralKopf.writeUInt32LE(groesse, 24);
+    zentralKopf.writeUInt16LE(nameBytes.length, 28);
+    zentralKopf.writeUInt16LE(0, 30); // Extra
+    zentralKopf.writeUInt16LE(0, 32); // Kommentar
+    zentralKopf.writeUInt16LE(0, 34); // Datenträger
+    zentralKopf.writeUInt16LE(0, 36); // interne Attribute
+    zentralKopf.writeUInt32LE(0, 38); // externe Attribute
+    zentralKopf.writeUInt32LE(versatz, 42); // Position des lokalen Kopfes
+    zentralBloecke.push(zentralKopf, nameBytes);
+
+    versatz += lokalerKopf.length + nameBytes.length + groesse;
+  }
+
+  const zentralVerzeichnis = Buffer.concat(zentralBloecke);
+  const abschluss = Buffer.alloc(22);
+  abschluss.writeUInt32LE(0x06054b50, 0); // Signatur
+  abschluss.writeUInt16LE(0, 4); // Datenträger
+  abschluss.writeUInt16LE(0, 6); // Datenträger mit Verzeichnis
+  abschluss.writeUInt16LE(eintraege.length, 8);
+  abschluss.writeUInt16LE(eintraege.length, 10);
+  abschluss.writeUInt32LE(zentralVerzeichnis.length, 12);
+  abschluss.writeUInt32LE(versatz, 16); // Beginn des Zentralverzeichnisses
+  abschluss.writeUInt16LE(0, 20); // kein Kommentar
+
+  return Buffer.concat([...lokaleBloecke, zentralVerzeichnis, abschluss]);
+}
diff --git a/packages/bibliothek-server/tsconfig.json b/packages/bibliothek-server/tsconfig.json
new file mode 100644
index 0000000..fe78878
--- /dev/null
+++ b/packages/bibliothek-server/tsconfig.json
@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "target": "es2021",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "rootDir": "./src",
+    "outDir": "./build",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "exclude": ["node_modules"]
+}
diff --git a/packages/bibliothek-types/package.json b/packages/bibliothek-types/package.json
new file mode 100644
index 0000000..18f9405
--- /dev/null
+++ b/packages/bibliothek-types/package.json
@@ -0,0 +1,10 @@
+{
+  "name": "bibliothek-types",
+  "version": "0.1.0",
+  "main": "build/index.js",
+  "types": "build/index.d.ts",
+  "files": ["build"],
+  "scripts": {
+    "build": "tsc"
+  }
+}
diff --git a/packages/bibliothek-types/src/index.ts b/packages/bibliothek-types/src/index.ts
new file mode 100644
index 0000000..a5250b8
--- /dev/null
+++ b/packages/bibliothek-types/src/index.ts
@@ -0,0 +1,160 @@
+/** Geteilte Typen zwischen Server und Client. */
+
+/**
+ * Bezugsquelle, aus der die Datei einer Software geholt und aktuell gehalten wird.
+ * `manuell` bedeutet: die Datei wird von Hand hochgeladen und nicht automatisch aktualisiert.
+ */
+export type QuelleTyp = 'direkt' | 'github' | 'winget' | 'svn' | 'git' | 'manuell';
+
+/** Einstufung einer Software — gliedert den Katalog und steuert die Hinweise. */
+export type Einstufung = 'lizenzfrei' | 'lizenzpflichtig' | 'intern';
+
+/** Alle Einstufungen mit Anzeigetext, in der Reihenfolge der Katalog-Reiter. */
+export const EINSTUFUNGEN: Array<{ id: Einstufung; text: string }> = [
+  { id: 'lizenzfrei', text: 'Lizenzfrei' },
+  { id: 'lizenzpflichtig', text: 'Lizenzpflichtig' },
+  { id: 'intern', text: 'Intern' },
+];
+
+/**
+ * Wie das Programm auf den Rechner kommt. `unbestimmt` ist der Ausgangswert —
+ * ein Eintrag muss nicht sofort eingeordnet werden.
+ */
+export type Programmart = 'unbestimmt' | 'installer' | 'eigenstaendig';
+
+/** Alle Programmarten mit Anzeigetext, in der Reihenfolge des Auswahlfelds. */
+export const PROGRAMMARTEN: Array<{ id: Programmart; text: string }> = [
+  { id: 'unbestimmt', text: 'Unbestimmt' },
+  { id: 'installer', text: 'Installationsprogramm' },
+  { id: 'eigenstaendig', text: 'Eigenständiges Programm' },
+];
+
+/** Ein Bau-Geheimnis, das ein Repository für seinen Bau anfordert. */
+export interface GeheimnisBedarf {
+  /** Name der Umgebungsvariablen, z. B. `ANTHROPIC_API_KEY`. */
+  name: string;
+  beschreibung: string;
+}
+
+/** Stand eines Bau-Geheimnisses in der Oberfläche — nie der Wert selbst. */
+export interface GeheimnisUebersicht {
+  name: string;
+  hinterlegt: boolean;
+  zeichen: number;
+}
+
+/** Ein Software-Eintrag im Katalog. */
+export interface Software {
+  id: number;
+  name: string;
+  beschreibung: string;
+  /** Id der übergeordneten Software, falls dieser Eintrag ein Zusatzinhalt dazu ist (0 = eigenständiges Tool). */
+  elternId: number;
+  /** Typ der Bezugsquelle (leer, falls noch keine gesetzt). */
+  quelleTyp: QuelleTyp | '';
+  /** Quell-spezifische Angabe: URL (direkt), `besitzer/repo` (github), Paket-ID (winget), Repo-URL (svn/git). Bei `manuell` leer. */
+  quelleWert: string;
+  /** Nur bei Quelle `git`: Befehl, der das Tool baut (im Wurzelverzeichnis der Arbeitskopie ausgeführt). */
+  bauBefehl: string;
+  /** Nur bei Quelle `git`: Pfad des Bau-Ergebnisses relativ zur Arbeitskopie, `*` im Dateinamen erlaubt. */
+  bauErgebnis: string;
+  /** Einstufung: lizenzfrei, lizenzpflichtig (Schlüssel über den Support) oder intern (Eigenentwicklung). */
+  einstufung: Einstufung;
+  /** Installationsprogramm oder eigenständige Datei — bestimmt den Hinweis am Eintrag. */
+  programmart: Programmart;
+  /** Freier Hinweis am Eintrag, z. B. „braucht Adminrechte". Leer = keiner. */
+  hinweis: string;
+  /** Link zur Anleitung (WikiJS intern oder extern), leer falls keine hinterlegt. */
+  anleitungUrl: string;
+  /** Aktuelle Versions-/Stand-Kennung, vom Abgleich gesetzt (Release-Tag, ETag, winget-Version, SVN-Revision). */
+  version: string;
+  /** Aktuell bereitgestellte Datei im Ablageordner (leer, solange noch nichts vorliegt). */
+  dateiname: string;
+  /** Zeitpunkt, zu dem die bereitgestellte Datei zuletzt tatsächlich erneuert wurde (unser Abruf). */
+  dateiStandVom: string;
+  /** Veröffentlichungsdatum beim Anbieter, sofern die Quelle es mitliefert. */
+  veroeffentlichtAm: string;
+  /** Windows-Signaturstatus der bereitgestellten Datei (Valid, NotSigned, …). */
+  signaturStatus: string;
+  /** Signaturgeber der bereitgestellten Datei (leer, wenn unsigniert). */
+  signaturHerausgeber: string;
+  /** Optional: erwarteter Signaturgeber. Weicht er ab, wird die Datei nicht bereitgestellt. */
+  erwarteterHerausgeber: string;
+  /** Vom Repository angeforderte Bau-Geheimnisse (aus `software-bibliothek.json`). */
+  benoetigteGeheimnisse: GeheimnisBedarf[];
+  /** Zeitpunkt des letzten Abgleichs (leer, falls nie geprüft). */
+  letzterAbgleich: string;
+  /** War der letzte Abgleich erfolgreich? */
+  abgleichErfolg: boolean;
+  /** Meldung des letzten Abgleichs (z.B. „Aktualisiert." oder ein Fehlertext). */
+  abgleichMeldung: string;
+  erstelltAm: string;
+  aktualisiertAm: string;
+}
+
+/** Nutzlast beim Anlegen/Bearbeiten (server-verwaltete Felder wie Version/Datei/Abgleich ausgenommen). */
+export interface SoftwareEingabe {
+  name: string;
+  beschreibung: string;
+  elternId: number;
+  quelleTyp: QuelleTyp | '';
+  quelleWert: string;
+  bauBefehl: string;
+  bauErgebnis: string;
+  einstufung: Einstufung;
+  programmart: Programmart;
+  hinweis: string;
+  anleitungUrl: string;
+  erwarteterHerausgeber: string;
+}
+
+/** Ergebnis eines Bezugsquellen-Abgleichs, das in die DB zurückgeschrieben wird. */
+export interface AbgleichErgebnis {
+  version: string;
+  dateiname: string;
+  erfolg: boolean;
+  meldung: string;
+  /** Wurde tatsächlich eine neue Datei bereitgestellt? Nur dann zählt es als Update. */
+  geaendert: boolean;
+  signaturStatus: string;
+  signaturHerausgeber: string;
+  veroeffentlichtAm: string;
+  benoetigteGeheimnisse: GeheimnisBedarf[];
+}
+
+/** Einstellungen der Anwendung (Datei `konfiguration.json` neben der Anwendung). */
+export interface Einstellungen {
+  /** Ablageort für Datenbank und Programm-Dateien. Leer = Ordner „data" neben der Anwendung. */
+  datenOrdner: string;
+  /** Port, auf dem die Oberfläche erreichbar ist. */
+  port: number;
+  /**
+   * Kennung für Git-Zugriffe über SSH. Leer = die SSH-Konfiguration des
+   * Dienstbenutzers entscheidet. Hier zentral gepflegt, damit ein Kontowechsel
+   * keine Anpassung an den Katalogeinträgen erfordert.
+   */
+  gitBenutzer: string;
+  /** Optionaler Pfad zum privaten SSH-Schlüssel für Git-Zugriffe. */
+  gitSchluessel: string;
+}
+
+/** Einstellungen samt der daraus abgeleiteten, tatsächlich genutzten Pfade. */
+export interface EinstellungenAnsicht {
+  einstellungen: Einstellungen;
+  /** Vollständiger Pfad der Konfigurationsdatei. */
+  konfigurationsDatei: string;
+  /** Tatsächlich verwendeter Datenordner (aufgelöst aus der Einstellung). */
+  effektiverDatenOrdner: string;
+  /** Läuft gerade auf diesem Port (Änderungen wirken erst nach Neustart). */
+  laufenderPort: number;
+}
+
+/** Ein protokollierter Zugriff/Download aus dem Netzwerk. */
+export interface ZugriffslogEintrag {
+  id: number;
+  zeitpunkt: string;
+  ipAdresse: string;
+  rechnername: string;
+  angegebenerName: string;
+  aktion: string;
+}
diff --git a/packages/bibliothek-types/tsconfig.json b/packages/bibliothek-types/tsconfig.json
new file mode 100644
index 0000000..932ee3e
--- /dev/null
+++ b/packages/bibliothek-types/tsconfig.json
@@ -0,0 +1,16 @@
+{
+  "compilerOptions": {
+    "target": "es2021",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "rootDir": "./src",
+    "outDir": "./build",
+    "declaration": true,
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "build"]
+}
diff --git a/pruefe_updates.bat b/pruefe_updates.bat
deleted file mode 100644
index 7cf9dc2..0000000
--- a/pruefe_updates.bat
+++ /dev/null
@@ -1,10 +0,0 @@
-@echo off
-echo ============================================================
-echo  Update-Pruefung wird ausgefuehrt...
-echo ============================================================
-echo.
-
-python automatische_pruefung.py
-
-echo.
-pause
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index 95fef4e..0000000
--- a/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-Flask==3.0.3
diff --git a/start.bat b/start.bat
deleted file mode 100644
index 40daee1..0000000
--- a/start.bat
+++ /dev/null
@@ -1,17 +0,0 @@
-@echo off
-echo ============================================================
-echo  Server-Bibliothek wird gestartet...
-echo ============================================================
-echo.
-echo  Gleich im Browser oeffnen:  http://localhost:5000
-echo.
-echo  Dieses Fenster bitte OFFEN LASSEN, solange das Tool
-echo  genutzt werden soll. Schliessen = Tool wird beendet.
-echo.
-echo  Beenden: einfach dieses Fenster schliessen oder STRG+C
-echo ============================================================
-echo.
-
-python app.py
-
-pause
diff --git a/static/style.css b/static/style.css
deleted file mode 100644
index 0d0dd71..0000000
--- a/static/style.css
+++ /dev/null
@@ -1,578 +0,0 @@
-:root {
-  --bg: #f2f4f1;
-  --surface: #ffffff;
-  --ink: #1e2a32;
-  --ink-soft: #5b6b74;
-  --line: #dfe3e0;
-  --sidebar: #16232b;
-  --sidebar-ink: #cfd9dc;
-  --sidebar-ink-dim: #7c8b93;
-  --accent: #2f8f86;
-  --accent-ink: #0c3a35;
-  --status-ok-bg: #e5f3ec;
-  --status-ok-ink: #1f6b45;
-  --status-warn-bg: #fbf0dc;
-  --status-warn-ink: #8a5a12;
-  --status-unbekannt-bg: #eceef0;
-  --status-unbekannt-ink: #5b6b74;
-
-  --font-ui: "Segoe UI", "Segoe UI Variable", system-ui, -apple-system, sans-serif;
-  --font-mono: Consolas, "Cascadia Mono", "SFMono-Regular", Menlo, monospace;
-}
-
-* { box-sizing: border-box; }
-
-html, body {
-  margin: 0;
-  padding: 0;
-  background: var(--bg);
-  color: var(--ink);
-  font-family: var(--font-ui);
-  font-size: 15px;
-  line-height: 1.5;
-}
-
-a { color: var(--accent-ink); text-decoration: none; }
-a:hover { text-decoration: underline; }
-
-.layout {
-  display: flex;
-  min-height: 100vh;
-}
-
-/* --- Sidebar --- */
-.sidebar {
-  width: 230px;
-  flex-shrink: 0;
-  background: var(--sidebar);
-  color: var(--sidebar-ink);
-  padding: 24px 18px;
-  display: flex;
-  flex-direction: column;
-  gap: 28px;
-  position: sticky;
-  top: 0;
-  height: 100vh;
-  overflow-y: auto;
-}
-
-.brand {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-}
-
-.brand-mark {
-  width: 30px;
-  height: 30px;
-  border-radius: 7px;
-  background: var(--accent);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-weight: 700;
-  color: #08211d;
-  font-size: 14px;
-  flex-shrink: 0;
-}
-
-.brand-text {
-  font-weight: 700;
-  letter-spacing: 0.02em;
-  font-size: 15px;
-  color: #fff;
-  line-height: 1.2;
-}
-.brand-text small {
-  display: block;
-  font-weight: 400;
-  color: var(--sidebar-ink-dim);
-  font-size: 11.5px;
-  letter-spacing: 0.03em;
-  text-transform: uppercase;
-}
-
-.nav {
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-}
-
-.nav a {
-  color: var(--sidebar-ink);
-  padding: 9px 12px;
-  border-radius: 6px;
-  font-size: 14px;
-  display: flex;
-  align-items: center;
-  gap: 9px;
-}
-.nav a:hover { background: #1f303a; text-decoration: none; }
-.nav a.active { background: var(--accent); color: #05201c; font-weight: 600; }
-
-.nav-dot {
-  width: 6px;
-  height: 6px;
-  border-radius: 50%;
-  background: currentColor;
-  opacity: 0.55;
-  flex-shrink: 0;
-}
-
-.sidebar-foot {
-  margin-top: auto;
-  font-size: 11.5px;
-  color: var(--sidebar-ink-dim);
-  letter-spacing: 0.02em;
-  border-top: 1px solid #223038;
-  padding-top: 14px;
-}
-
-/* --- Main --- */
-.main {
-  flex: 1;
-  padding: 32px 40px;
-  max-width: 1100px;
-}
-
-.page-head {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-end;
-  margin-bottom: 22px;
-  gap: 16px;
-  flex-wrap: wrap;
-}
-
-.eyebrow {
-  text-transform: uppercase;
-  letter-spacing: 0.09em;
-  font-size: 11.5px;
-  color: var(--ink-soft);
-  font-weight: 600;
-  margin: 0 0 4px;
-}
-
-h1 {
-  font-size: 24px;
-  margin: 0;
-  letter-spacing: -0.01em;
-}
-
-h2 { font-size: 17px; margin: 0 0 12px; }
-
-.btn {
-  display: inline-flex;
-  align-items: center;
-  gap: 6px;
-  background: var(--accent);
-  color: #06251f;
-  border: none;
-  padding: 9px 16px;
-  border-radius: 7px;
-  font-weight: 600;
-  font-size: 13.5px;
-  cursor: pointer;
-  transition: transform 0.08s ease, background 0.15s ease;
-}
-.btn:hover { background: #278178; text-decoration: none; }
-.btn:active { transform: translateY(1px); }
-
-.btn-secondary {
-  background: var(--surface);
-  color: var(--ink);
-  border: 1px solid var(--line);
-}
-.btn-secondary:hover { background: #ebedea; }
-
-.btn-danger {
-  background: #fdeceb;
-  color: #9c3a2f;
-  border: 1px solid #f3d3ce;
-}
-.btn-danger:hover { background: #fadad6; }
-
-.btn-small { padding: 6px 11px; font-size: 12.5px; border-radius: 6px; }
-
-/* --- Stat strip --- */
-.stat-strip {
-  display: flex;
-  gap: 14px;
-  margin-bottom: 24px;
-  flex-wrap: wrap;
-}
-.stat-card {
-  background: var(--surface);
-  border: 1px solid var(--line);
-  border-radius: 10px;
-  padding: 14px 18px;
-  min-width: 150px;
-}
-.stat-num { font-size: 22px; font-weight: 700; line-height: 1.1; }
-.stat-label { font-size: 12px; color: var(--ink-soft); margin-top: 2px; }
-
-/* --- Search --- */
-.search-row {
-  margin-bottom: 18px;
-}
-.search-row input[type="text"] {
-  width: 100%;
-  max-width: 340px;
-  padding: 9px 13px;
-  border-radius: 7px;
-  border: 1px solid var(--line);
-  background: var(--surface);
-  font-family: var(--font-ui);
-  font-size: 14px;
-}
-
-/* --- Catalog cards (Signature element) --- */
-.catalog {
-  display: flex;
-  flex-direction: column;
-  gap: 10px;
-}
-
-.catalog-card {
-  background: var(--surface);
-  border: 1px solid var(--line);
-  border-radius: 10px;
-  padding: 14px 18px;
-  display: flex;
-  align-items: center;
-  gap: 16px;
-}
-
-.tag-punch {
-  width: 34px;
-  height: 34px;
-  border-radius: 8px;
-  background: var(--bg);
-  border: 1px solid var(--line);
-  position: relative;
-  flex-shrink: 0;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-.tag-punch::after {
-  content: "";
-  width: 8px;
-  height: 8px;
-  border-radius: 50%;
-  background: var(--status-dot, var(--ink-soft));
-}
-
-.catalog-main { flex: 1; min-width: 0; }
-.catalog-name {
-  font-weight: 600;
-  font-size: 15px;
-}
-.catalog-meta {
-  font-size: 12.5px;
-  color: var(--ink-soft);
-  margin-top: 2px;
-  display: flex;
-  gap: 10px;
-  flex-wrap: wrap;
-}
-.catalog-meta .v { font-family: var(--font-mono); }
-
-.badge {
-  display: inline-flex;
-  align-items: center;
-  gap: 6px;
-  padding: 5px 11px;
-  border-radius: 100px;
-  font-size: 12px;
-  font-weight: 600;
-  white-space: nowrap;
-}
-.status-ok { background: var(--status-ok-bg); color: var(--status-ok-ink); }
-.status-warnung { background: var(--status-warn-bg); color: var(--status-warn-ink); }
-.status-unbekannt { background: var(--status-unbekannt-bg); color: var(--status-unbekannt-ink); }
-
-.catalog-card:has(.status-ok) .tag-punch { --status-dot: var(--status-ok-ink); }
-.catalog-card:has(.status-warnung) .tag-punch { --status-dot: var(--status-warn-ink); }
-.catalog-card:has(.status-unbekannt) .tag-punch { --status-dot: var(--status-unbekannt-ink); }
-
-.catalog-actions {
-  display: flex;
-  gap: 8px;
-  flex-shrink: 0;
-}
-
-.empty-state {
-  background: var(--surface);
-  border: 1px dashed var(--line);
-  border-radius: 10px;
-  padding: 40px 20px;
-  text-align: center;
-  color: var(--ink-soft);
-}
-
-/* --- Detail page --- */
-.detail-grid {
-  display: grid;
-  grid-template-columns: 2fr 1fr;
-  gap: 20px;
-  align-items: start;
-}
-@media (max-width: 800px) {
-  .detail-grid { grid-template-columns: 1fr; }
-}
-
-.panel {
-  background: var(--surface);
-  border: 1px solid var(--line);
-  border-radius: 10px;
-  padding: 20px 22px;
-  margin-bottom: 18px;
-}
-
-.kv-row {
-  display: flex;
-  justify-content: space-between;
-  padding: 9px 0;
-  border-bottom: 1px solid var(--line);
-  font-size: 13.5px;
-  gap: 12px;
-}
-.kv-row:last-child { border-bottom: none; }
-.kv-label { color: var(--ink-soft); }
-.kv-value { font-family: var(--font-mono); text-align: right; word-break: break-all; }
-
-.docs-block {
-  white-space: pre-wrap;
-  font-size: 13.5px;
-  line-height: 1.6;
-  color: var(--ink);
-}
-
-table.data-table {
-  width: 100%;
-  border-collapse: collapse;
-  font-size: 13.5px;
-}
-table.data-table th {
-  text-align: left;
-  color: var(--ink-soft);
-  font-weight: 600;
-  font-size: 11.5px;
-  text-transform: uppercase;
-  letter-spacing: 0.04em;
-  padding: 8px 10px;
-  border-bottom: 1px solid var(--line);
-}
-table.data-table td {
-  padding: 10px 10px;
-  border-bottom: 1px solid var(--line);
-  vertical-align: top;
-}
-table.data-table tr:last-child td { border-bottom: none; }
-
-/* --- Forms --- */
-.form-grid {
-  display: grid;
-  gap: 14px;
-  max-width: 640px;
-}
-.field label {
-  display: block;
-  font-size: 13px;
-  font-weight: 600;
-  margin-bottom: 5px;
-}
-.field .hint {
-  font-size: 12px;
-  color: var(--ink-soft);
-  font-weight: 400;
-  margin-top: 3px;
-}
-.field .hint code {
-  font-family: var(--font-mono);
-  background: var(--bg);
-  border: 1px solid var(--line);
-  border-radius: 4px;
-  padding: 1px 5px;
-  font-size: 11.5px;
-  color: var(--ink);
-}
-.field input[type="text"],
-.field input[type="date"],
-.field select,
-.field textarea {
-  width: 100%;
-  padding: 9px 12px;
-  border-radius: 7px;
-  border: 1px solid var(--line);
-  font-family: var(--font-ui);
-  font-size: 14px;
-  background: var(--surface);
-}
-.field textarea { font-family: var(--font-mono); resize: vertical; }
-.field input:focus, .field select:focus, .field textarea:focus {
-  outline: 2px solid var(--accent);
-  outline-offset: 1px;
-}
-
-.form-actions {
-  display: flex;
-  gap: 10px;
-  margin-top: 6px;
-}
-
-/* --- Auto-Update: Felder + Mini-Doku nebeneinander --- */
-.auto-update-layout {
-  display: flex;
-  gap: 18px;
-  align-items: flex-start;
-  flex-wrap: wrap;
-}
-.auto-update-fields {
-  flex: 1 1 240px;
-  min-width: 220px;
-  display: flex;
-  flex-direction: column;
-  gap: 14px;
-}
-
-.mini-doku {
-  flex: 1 1 240px;
-  min-width: 220px;
-  background: var(--status-ok-bg);
-  border: 1px solid var(--accent);
-  border-radius: 8px;
-  padding: 14px 16px;
-  font-size: 13px;
-  line-height: 1.55;
-  color: var(--ink);
-}
-.mini-doku-titel {
-  font-weight: 600;
-  font-size: 12px;
-  text-transform: uppercase;
-  letter-spacing: 0.04em;
-  color: var(--accent-ink);
-  margin-bottom: 8px;
-}
-.mini-doku ol {
-  margin: 0 0 10px;
-  padding-left: 18px;
-}
-.mini-doku li { margin-bottom: 4px; }
-.mini-doku code {
-  font-family: var(--font-mono);
-  background: var(--surface);
-  padding: 1px 5px;
-  border-radius: 4px;
-  font-size: 12px;
-}
-.mini-doku .beispiel {
-  font-family: var(--font-mono);
-  font-size: 12px;
-  background: var(--surface);
-  border: 1px solid var(--line);
-  border-radius: 6px;
-  padding: 6px 10px;
-  display: inline-block;
-}
-
-/* --- Checkliste bei Zuweisungen --- */
-.checklist-kopf {
-  display: flex;
-  align-items: center;
-  gap: 7px;
-  font-size: 13px;
-  font-weight: 600;
-  margin: 4px 0 8px;
-  cursor: pointer;
-}
-.checklist {
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  background: var(--surface);
-  max-height: 340px;
-  overflow-y: auto;
-  padding: 4px 0;
-}
-.checklist-gruppe { padding: 6px 0; }
-.checklist-gruppe + .checklist-gruppe { border-top: 1px solid var(--line); }
-.checklist-gruppen-titel {
-  font-size: 11px;
-  text-transform: uppercase;
-  letter-spacing: 0.04em;
-  color: var(--ink-soft);
-  font-weight: 600;
-  padding: 5px 14px;
-}
-.check-item {
-  display: flex;
-  align-items: center;
-  gap: 9px;
-  padding: 7px 14px;
-  font-size: 13.5px;
-  cursor: pointer;
-}
-.check-item:hover { background: var(--bg); }
-.check-item input[type="checkbox"] { width: auto; }
-.check-item .v { font-family: var(--font-mono); color: var(--ink-soft); font-size: 12px; }
-
-/* --- Flash messages --- */
-.flash {
-  padding: 11px 16px;
-  border-radius: 8px;
-  font-size: 13.5px;
-  margin-bottom: 16px;
-}
-.flash-erfolg { background: var(--status-ok-bg); color: var(--status-ok-ink); }
-.flash-fehler { background: #fdeceb; color: #9c3a2f; }
-
-.filter-row {
-  display: flex;
-  gap: 10px;
-  margin-bottom: 16px;
-  flex-wrap: wrap;
-}
-.filter-row input {
-  padding: 8px 12px;
-  border-radius: 7px;
-  border: 1px solid var(--line);
-  font-family: var(--font-ui);
-  font-size: 13.5px;
-}
-
-/* --- Doku-Bilder --- */
-.doku-bilder-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
-  gap: 12px;
-  margin-top: 16px;
-}
-.doku-bild-kachel {
-  border: 1px solid var(--line);
-  border-radius: 8px;
-  overflow: hidden;
-  background: var(--bg);
-}
-.doku-bild-kachel img {
-  width: 100%;
-  height: 100px;
-  object-fit: cover;
-  display: block;
-}
-.doku-bild-kachel form { padding: 6px; }
-.doku-bild-kachel .btn { width: 100%; }
-
-.bild-upload-form {
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin-top: 16px;
-  padding-top: 14px;
-  border-top: 1px solid var(--line);
-}
-.bild-upload-form input[type="file"] {
-  font-size: 13px;
-  color: var(--ink-soft);
-}
diff --git a/templates/assignment_form.html b/templates/assignment_form.html
deleted file mode 100644
index 5aac45f..0000000
--- a/templates/assignment_form.html
+++ /dev/null
@@ -1,91 +0,0 @@
-{% extends "base.html" %}
-{% block title %}{{ "Tools hinzufügen" if bestehendes_protokoll else "Neues Protokoll" }} – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">Protokoll</p>
-    <h1>{{ "Weitere Tools für " + bestehendes_protokoll.mitarbeiter if bestehendes_protokoll else "Neues Protokoll anlegen" }}</h1>
-  </div>
-</div>
-
-{% if not software_liste %}
-  <div class="empty-state">
-    {% if bestehendes_protokoll %}
-      Diesem Protokoll sind bereits alle vorhandenen Tools zugewiesen.
-      <a href="{{ url_for('protokoll_detail', protokoll_id=bestehendes_protokoll.id) }}">Zurück zum Protokoll</a>.
-    {% else %}
-      Es ist noch keine Software im Katalog. <a href="{{ url_for('software_neu') }}">Erst ein Tool anlegen</a>.
-    {% endif %}
-  </div>
-{% else %}
-<form method="post" class="form-grid">
-  {% if bestehendes_protokoll %}
-    <input type="hidden" name="protokoll_id" value="{{ bestehendes_protokoll.id }}">
-    <div class="panel" style="padding:14px 18px;">
-      <div class="kv-row"><span class="kv-label">Mitarbeiter</span><span class="kv-value">{{ bestehendes_protokoll.mitarbeiter }}</span></div>
-      <div class="kv-row"><span class="kv-label">Rechnername</span><span class="kv-value">{{ bestehendes_protokoll.rechnername or "–" }}</span></div>
-    </div>
-  {% else %}
-    <div class="field">
-      <label for="mitarbeiter">Mitarbeiter *</label>
-      <input type="text" id="mitarbeiter" name="mitarbeiter" required placeholder="z. B. Max Mustermann">
-    </div>
-
-    <div class="field">
-      <label for="rechnername">Rechnername</label>
-      <input type="text" id="rechnername" name="rechnername" placeholder="z. B. PC-047 oder Notebook-Vertrieb-3">
-    </div>
-  {% endif %}
-
-  <div class="field">
-    <label>Software (mehrere möglich) *</label>
-    <label class="checklist-kopf">
-      <input type="checkbox" id="alle_auswaehlen" onchange="alleUmschalten(this)"> Alle auswählen
-    </label>
-    <div class="checklist">
-      {% for kategorie, gruppe in software_liste | groupby("kategorie") %}
-      <div class="checklist-gruppe">
-        <div class="checklist-gruppen-titel">{{ kategorie }}</div>
-        {% for sw in gruppe %}
-        <label class="check-item">
-          <input type="checkbox" name="software_ids" value="{{ sw.id }}" class="sw-checkbox" {{ "checked" if sw.id == vorausgewaehlt }}>
-          {{ sw.name }}{% if sw.version %} <span class="v">v{{ sw.version }}</span>{% endif %}
-        </label>
-        {% endfor %}
-      </div>
-      {% endfor %}
-    </div>
-  </div>
-
-  <div class="field">
-    <label for="installiert_am">Datum</label>
-    <input type="date" id="installiert_am" name="installiert_am" value="{{ heute }}">
-  </div>
-
-  <div class="field">
-    <label for="installiert_von">Eingerichtet von</label>
-    <input type="text" id="installiert_von" name="installiert_von" placeholder="Dein Name">
-  </div>
-
-  <div class="field">
-    <label for="notiz">Notiz</label>
-    <textarea id="notiz" name="notiz" rows="3" placeholder="Optional: gilt fuer alle angehakten Tools in dieser Zuweisung"></textarea>
-  </div>
-
-  <div class="form-actions">
-    <button type="submit" class="btn">{{ "Hinzufügen" if bestehendes_protokoll else "Protokoll anlegen" }}</button>
-    <a href="{{ url_for('protokoll_detail', protokoll_id=bestehendes_protokoll.id) if bestehendes_protokoll else url_for('protokolle') }}" class="btn btn-secondary">Abbrechen</a>
-  </div>
-</form>
-
-<script>
-  function alleUmschalten(quelle) {
-    document.querySelectorAll(".sw-checkbox").forEach(function (cb) {
-      cb.checked = quelle.checked;
-    });
-  }
-</script>
-{% endif %}
-
-{% endblock %}
diff --git a/templates/base.html b/templates/base.html
deleted file mode 100644
index f21dcc9..0000000
--- a/templates/base.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<!DOCTYPE html>
-<html lang="de">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title>{% block title %}Server-Bibliothek{% endblock %}</title>
-  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
-</head>
-<body>
-  <div class="layout">
-    <aside class="sidebar">
-      <div class="brand">
-        <div class="brand-mark">SB</div>
-        <div class="brand-text">Server-Bibliothek<small>Software &amp; Doku-Ablage</small></div>
-      </div>
-      <nav class="nav">
-        {% if ist_lokal %}
-        <a href="{{ url_for('dashboard') }}" class="{{ 'active' if request.endpoint == 'dashboard' }}">
-          <span class="nav-dot"></span> Software-Katalog
-        </a>
-        <a href="{{ url_for('protokolle') }}" class="{{ 'active' if request.endpoint in ['protokolle', 'protokoll_detail', 'zuweisung_neu'] }}">
-          <span class="nav-dot"></span> Protokolle
-        </a>
-        <a href="{{ url_for('zugriffslog') }}" class="{{ 'active' if request.endpoint == 'zugriffslog' }}">
-          <span class="nav-dot"></span> Zugriffsprotokoll
-        </a>
-        <a href="{{ url_for('einrichten') }}" class="{{ 'active' if request.endpoint == 'einrichten' }}">
-          <span class="nav-dot"></span> Rechner einrichten
-        </a>
-        {% else %}
-        <a href="{{ url_for('einrichten') }}" class="active">
-          <span class="nav-dot"></span> Software anfordern
-        </a>
-        {% endif %}
-      </nav>
-      <div class="sidebar-foot">Version: {{ app_version }}{% if not ist_lokal %}<br>Netzwerkzugriff{% endif %}</div>
-    </aside>
-    <main class="main">
-      {% with messages = get_flashed_messages(with_categories=true) %}
-        {% if messages %}
-          {% for category, message in messages %}
-            <div class="flash flash-{{ category }}">{{ message }}</div>
-          {% endfor %}
-        {% endif %}
-      {% endwith %}
-      {% block content %}{% endblock %}
-    </main>
-  </div>
-</body>
-</html>
diff --git a/templates/dashboard.html b/templates/dashboard.html
deleted file mode 100644
index 7246bbd..0000000
--- a/templates/dashboard.html
+++ /dev/null
@@ -1,72 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Software-Katalog – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">Server-Bibliothek</p>
-    <h1>Software-Katalog</h1>
-  </div>
-  <div class="catalog-actions">
-    {% if auto_anzahl > 0 %}
-    <form method="post" action="{{ url_for('updates_alle_jetzt') }}">
-      <button type="submit" class="btn btn-secondary">Alle {{ auto_anzahl }} Auto-Updates jetzt ausfuehren</button>
-    </form>
-    {% endif %}
-    <a href="{{ url_for('software_neu') }}" class="btn">+ Neues Tool anlegen</a>
-  </div>
-</div>
-
-<div class="stat-strip">
-  <div class="stat-card">
-    <div class="stat-num">{{ gesamt }}</div>
-    <div class="stat-label">Tools insgesamt</div>
-  </div>
-  <div class="stat-card">
-    <div class="stat-num">{{ warnungen }}</div>
-    <div class="stat-label">Bitte pruefen (&gt; {{ 30 }} Tage)</div>
-  </div>
-  <div class="stat-card">
-    <div class="stat-num">{{ unbekannt }}</div>
-    <div class="stat-label">Nie geprueft</div>
-  </div>
-</div>
-
-<div class="search-row">
-  <form method="get">
-    <input type="text" name="q" placeholder="Suche nach Name oder Kategorie…" value="{{ suche }}">
-  </form>
-</div>
-
-{% if software_liste %}
-<div class="catalog">
-  {% for sw in software_liste %}
-    {% set badge = sw.letzte_pruefung | status %}
-    <div class="catalog-card">
-      <div class="tag-punch"></div>
-      <div class="catalog-main">
-        <div class="catalog-name"><a href="{{ url_for('software_detail', software_id=sw.id) }}">{{ sw.name }}</a></div>
-        <div class="catalog-meta">
-          {% if sw.kategorie %}<span>{{ sw.kategorie }}</span>{% endif %}
-          {% if sw.version %}<span class="v">v{{ sw.version }}</span>{% endif %}
-          {% if sw.auto_update %}<span>&#9889; Auto</span>{% endif %}
-        </div>
-      </div>
-      <span class="badge {{ badge[1] }}">{{ badge[0] }}</span>
-      <div class="catalog-actions">
-        <a href="{{ url_for('software_detail', software_id=sw.id) }}" class="btn btn-secondary btn-small">Details</a>
-      </div>
-    </div>
-  {% endfor %}
-</div>
-{% else %}
-<div class="empty-state">
-  {% if suche %}
-    Keine Treffer fuer „{{ suche }}“.
-  {% else %}
-    Noch keine Software hinterlegt. Leg das erste Tool mit „+ Neues Tool anlegen“ an.
-  {% endif %}
-</div>
-{% endif %}
-
-{% endblock %}
diff --git a/templates/einrichten.html b/templates/einrichten.html
deleted file mode 100644
index 06e1907..0000000
--- a/templates/einrichten.html
+++ /dev/null
@@ -1,97 +0,0 @@
-{% extends "base.html" %}
-{% block title %}{{ "Rechner einrichten" if ist_lokal else "Software anfordern" }} – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">{{ "Verteilung" if ist_lokal else "Selbstbedienung" }}</p>
-    <h1>{{ "Rechner einrichten" if ist_lokal else "Software anfordern" }}</h1>
-  </div>
-</div>
-
-{% if not ist_lokal %}
-<div class="panel" style="margin-bottom:18px; border-color: var(--accent);">
-  <p style="margin:0; font-size:13.5px;">
-    Dein Name wird zusammen mit deiner Rechner-/IP-Adresse protokolliert.
-    Bitte nur Tools anfordern, die du für deine Arbeit wirklich brauchst.
-  </p>
-</div>
-{% endif %}
-
-<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. 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>
-
-{% if not installierbar %}
-  <div class="empty-state">
-    Kein Tool im Katalog hat aktuell eine verfügbare Installer-Datei.
-    <a href="{{ url_for('software_neu') }}">Erst ein Tool anlegen</a> oder
-    eine automatische Aktualisierung einmal ausführen.
-  </div>
-{% else %}
-<form method="post" class="form-grid">
-  <div class="field">
-    <label for="mitarbeiter">Mitarbeiter *</label>
-    <input type="text" id="mitarbeiter" name="mitarbeiter" required placeholder="z. B. Max Mustermann">
-    <div class="hint">Der Rechnername wird vom Skript automatisch erkannt (nicht nötig, hier einzutragen).</div>
-  </div>
-
-  <div class="field">
-    <label>Software (mehrere möglich) *</label>
-    <label class="checklist-kopf">
-      <input type="checkbox" id="alle_auswaehlen" onchange="alleUmschalten(this)"> Alle auswählen
-    </label>
-    <div class="checklist">
-      {% for kategorie, gruppe in installierbar | groupby("kategorie") %}
-      <div class="checklist-gruppe">
-        <div class="checklist-gruppen-titel">{{ kategorie }}</div>
-        {% for sw in gruppe %}
-        <label class="check-item">
-          <input type="checkbox" name="software_ids" value="{{ sw.id }}" class="sw-checkbox">
-          {{ sw.name }}{% if sw.version %} <span class="v">v{{ sw.version }}</span>{% endif %}
-          {% if sw.typ == "ordner" %}<span class="v">(als ZIP bereitgestellt, kein Auto-Entpacken)</span>{% endif %}
-        </label>
-        {% endfor %}
-      </div>
-      {% endfor %}
-    </div>
-  </div>
-
-  <div class="form-actions">
-    <button type="submit" class="btn">Installations-Paket herunterladen</button>
-    <a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Abbrechen</a>
-  </div>
-</form>
-
-{% if nicht_installierbar %}
-<div class="hint" style="margin-top:18px;">
-  Nicht wählbar (keine automatisch verwaltete Installer-Datei vorhanden):
-  {{ nicht_installierbar | join(", ") }}
-</div>
-{% endif %}
-
-<script>
-  function alleUmschalten(quelle) {
-    document.querySelectorAll(".sw-checkbox").forEach(function (cb) {
-      cb.checked = quelle.checked;
-    });
-  }
-</script>
-{% endif %}
-
-{% endblock %}
diff --git a/templates/protokoll_detail.html b/templates/protokoll_detail.html
deleted file mode 100644
index de9b834..0000000
--- a/templates/protokoll_detail.html
+++ /dev/null
@@ -1,63 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Protokoll {{ protokoll.mitarbeiter }} – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">Protokoll{% if protokoll.rechnername %} · {{ protokoll.rechnername }}{% endif %}</p>
-    <h1>{{ protokoll.mitarbeiter }}</h1>
-  </div>
-  <div class="catalog-actions">
-    <a href="{{ url_for('zuweisung_neu', protokoll_id=protokoll.id) }}" class="btn">+ Weitere Tools hinzufügen</a>
-    <form method="post" action="{{ url_for('protokoll_loeschen', protokoll_id=protokoll.id) }}"
-          onsubmit="return confirm('Das komplette Protokoll fuer {{ protokoll.mitarbeiter }} wirklich loeschen?');">
-      <button type="submit" class="btn btn-danger">Protokoll löschen</button>
-    </form>
-  </div>
-</div>
-
-<div class="detail-grid">
-  <div>
-    <div class="panel">
-      <h2>Zugewiesene Tools ({{ eintraege | length }})</h2>
-      {% if eintraege %}
-        <table class="data-table">
-          <thead>
-            <tr><th>Tool</th><th>Version</th><th></th></tr>
-          </thead>
-          <tbody>
-            {% for e in eintraege %}
-            <tr>
-              <td><a href="{{ url_for('software_detail', software_id=e.software_id) }}">{{ e.software_name }}</a></td>
-              <td>{{ e.installierte_version or "–" }}</td>
-              <td>
-                <form method="post" action="{{ url_for('zuweisung_loeschen', zuweisung_id=e.id) }}"
-                      onsubmit="return confirm('{{ e.software_name }} aus diesem Protokoll entfernen?');">
-                  <button type="submit" class="btn btn-danger btn-small">Entfernen</button>
-                </form>
-              </td>
-            </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-      {% else %}
-        <p class="hint">Noch keine Tools in diesem Protokoll.</p>
-      {% endif %}
-    </div>
-  </div>
-
-  <div>
-    <div class="panel">
-      <h2>Details</h2>
-      <div class="kv-row"><span class="kv-label">Mitarbeiter</span><span class="kv-value">{{ protokoll.mitarbeiter }}</span></div>
-      <div class="kv-row"><span class="kv-label">Rechnername</span><span class="kv-value">{{ protokoll.rechnername or "–" }}</span></div>
-      <div class="kv-row"><span class="kv-label">Datum</span><span class="kv-value">{{ protokoll.datum }}</span></div>
-      <div class="kv-row"><span class="kv-label">Eingerichtet von</span><span class="kv-value">{{ protokoll.erstellt_von or "–" }}</span></div>
-      {% if protokoll.notiz %}
-      <div class="kv-row"><span class="kv-label">Notiz</span><span class="kv-value">{{ protokoll.notiz }}</span></div>
-      {% endif %}
-    </div>
-  </div>
-</div>
-
-{% endblock %}
diff --git a/templates/protokolle.html b/templates/protokolle.html
deleted file mode 100644
index a01e21b..0000000
--- a/templates/protokolle.html
+++ /dev/null
@@ -1,48 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Protokolle – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">Protokoll</p>
-    <h1>Protokolle</h1>
-  </div>
-  <a href="{{ url_for('zuweisung_neu') }}" class="btn">+ Neues Protokoll</a>
-</div>
-
-<form method="get" class="filter-row">
-  <input type="text" name="mitarbeiter" placeholder="Nach Mitarbeiter filtern…" value="{{ mitarbeiter_filter }}">
-  <input type="text" name="rechner" placeholder="Nach Rechnername filtern…" value="{{ rechner_filter }}">
-  <button type="submit" class="btn btn-secondary btn-small">Filtern</button>
-</form>
-
-{% if protokolle %}
-<div class="catalog">
-  {% for p in protokolle %}
-  <div class="catalog-card">
-    <div class="tag-punch"></div>
-    <div class="catalog-main">
-      <div class="catalog-name"><a href="{{ url_for('protokoll_detail', protokoll_id=p.id) }}">{{ p.mitarbeiter }}</a></div>
-      <div class="catalog-meta">
-        {% if p.rechnername %}<span>{{ p.rechnername }}</span>{% endif %}
-        <span>{{ p.anzahl_tools }} Tool{{ "s" if p.anzahl_tools != 1 }}</span>
-        <span>{{ p.datum }}</span>
-      </div>
-    </div>
-    <div class="catalog-actions">
-      <a href="{{ url_for('protokoll_detail', protokoll_id=p.id) }}" class="btn btn-secondary btn-small">Details</a>
-    </div>
-  </div>
-  {% endfor %}
-</div>
-{% else %}
-<div class="empty-state">
-  {% if mitarbeiter_filter or rechner_filter %}
-    Keine Treffer.
-  {% else %}
-    Noch keine Protokolle vorhanden. Leg das erste mit „+ Neues Protokoll" an.
-  {% endif %}
-</div>
-{% endif %}
-
-{% endblock %}
diff --git a/templates/software_detail.html b/templates/software_detail.html
deleted file mode 100644
index d50bfa1..0000000
--- a/templates/software_detail.html
+++ /dev/null
@@ -1,168 +0,0 @@
-{% extends "base.html" %}
-{% block title %}{{ sw.name }} – Server-Bibliothek{% endblock %}
-{% block content %}
-
-{% set badge = sw.letzte_pruefung | status %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">{{ sw.kategorie or "Ohne Kategorie" }}</p>
-    <h1>{{ sw.name }}</h1>
-  </div>
-  <div class="catalog-actions">
-    {% if sw.installer_pfad %}
-      <a href="{{ url_for('software_download', software_id=sw.id) }}" class="btn">Herunterladen</a>
-    {% endif %}
-    <a href="{{ url_for('zuweisung_neu', software_id=sw.id) }}" class="btn btn-secondary">+ Installation protokollieren</a>
-    <a href="{{ url_for('software_bearbeiten', software_id=sw.id) }}" class="btn btn-secondary">Bearbeiten</a>
-  </div>
-</div>
-
-<div class="detail-grid">
-  <div>
-    <div class="panel">
-      <h2>Dokumentation / Einrichtung</h2>
-      {% if sw.dokumentation %}
-        <div class="docs-block">{{ sw.dokumentation }}</div>
-      {% else %}
-        <p class="hint">Noch keine Dokumentation hinterlegt.</p>
-      {% endif %}
-
-      {% if bilder %}
-      <div class="doku-bilder-grid">
-        {% for b in bilder %}
-        <div class="doku-bild-kachel">
-          <a href="{{ url_for('bild_anzeigen', software_id=sw.id, bild_id=b.id) }}" target="_blank">
-            <img src="{{ url_for('bild_anzeigen', software_id=sw.id, bild_id=b.id) }}" alt="Doku-Bild zu {{ sw.name }}">
-          </a>
-          <form method="post" action="{{ url_for('bild_loeschen', software_id=sw.id, bild_id=b.id) }}"
-                onsubmit="return confirm('Bild loeschen?');">
-            <button type="submit" class="btn btn-danger btn-small">Löschen</button>
-          </form>
-        </div>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <form method="post" action="{{ url_for('bild_hochladen', software_id=sw.id) }}"
-            enctype="multipart/form-data" class="bild-upload-form">
-        <input type="file" name="bild" accept="image/png,image/jpeg,image/gif,image/webp" required>
-        <button type="submit" class="btn btn-secondary btn-small">Bild hochladen</button>
-      </form>
-    </div>
-
-    <div class="panel">
-      <h2>Installationsverlauf ({{ verlauf | length }})</h2>
-      {% if verlauf %}
-        <table class="data-table">
-          <thead>
-            <tr><th>Mitarbeiter</th><th>Rechner</th><th>Version</th><th>Datum</th><th>Von</th></tr>
-          </thead>
-          <tbody>
-            {% for z in verlauf %}
-            <tr>
-              <td>
-                {% if z.protokoll_id %}
-                  <a href="{{ url_for('protokoll_detail', protokoll_id=z.protokoll_id) }}">{{ z.mitarbeiter }}</a>
-                {% else %}
-                  {{ z.mitarbeiter }}
-                {% endif %}
-              </td>
-              <td>{{ z.rechnername or "–" }}</td>
-              <td>{{ z.installierte_version or "–" }}</td>
-              <td>{{ z.installiert_am }}</td>
-              <td>{{ z.installiert_von or "–" }}</td>
-            </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-      {% else %}
-        <p class="hint">Noch keine Installation fuer dieses Tool protokolliert.</p>
-      {% endif %}
-    </div>
-
-    {% if sw.auto_update %}
-    <div class="panel">
-      <h2>Update-Protokoll</h2>
-      {% if update_verlauf %}
-        <table class="data-table">
-          <thead>
-            <tr><th>Zeitpunkt</th><th>Alte Version</th><th>Neue Version</th><th>Ergebnis</th></tr>
-          </thead>
-          <tbody>
-            {% for log in update_verlauf %}
-            <tr>
-              <td>{{ log.zeitpunkt[:16].replace("T", " ") }}</td>
-              <td>{{ log.alte_version or "–" }}</td>
-              <td>{{ log.neue_version or "–" }}</td>
-              <td>
-                {% if log.erfolg %}
-                  <span class="badge status-ok">{{ log.meldung }}</span>
-                {% else %}
-                  <span class="badge status-warnung">{{ log.meldung }}</span>
-                {% endif %}
-              </td>
-            </tr>
-            {% endfor %}
-          </tbody>
-        </table>
-      {% else %}
-        <p class="hint">Noch kein automatischer Check gelaufen.</p>
-      {% endif %}
-    </div>
-    {% endif %}
-  </div>
-
-  <div>
-    <div class="panel">
-      <h2>Status</h2>
-      <p><span class="badge {{ badge[1] }}">{{ badge[0] }}</span></p>
-
-      {% if sw.auto_update and sw.update_quelle %}
-        <p class="hint" style="margin-top:10px;">Automatische Aktualisierung aktiv ueber
-          <strong>{{ {"winget": "winget", "github": "GitHub", "direkt": "Direkter Link"}[sw.update_quelle] or sw.update_quelle }}</strong>.</p>
-        <form method="post" action="{{ url_for('software_update_jetzt', software_id=sw.id) }}" style="margin-top:8px;">
-          <button type="submit" class="btn btn-small" style="width:100%;">Jetzt automatisch pruefen &amp; aktualisieren</button>
-        </form>
-      {% else %}
-        <form method="post" action="{{ url_for('software_pruefen', software_id=sw.id) }}" style="margin-top:12px;">
-          <button type="submit" class="btn btn-secondary btn-small" style="width:100%;">Jetzt als geprueft markieren</button>
-        </form>
-      {% endif %}
-    </div>
-
-    <div class="panel">
-      <h2>Details</h2>
-      <div class="kv-row"><span class="kv-label">Version</span><span class="kv-value">{{ sw.version or "–" }}</span></div>
-      <div class="kv-row">
-        <span class="kv-label">Ablageordner</span>
-        <span class="kv-value" style="display:flex; align-items:center; gap:6px; justify-content:flex-end;">
-          {{ sw.installer_pfad or "–" }}
-          {% if sw.installer_pfad %}
-          <button type="button" class="btn btn-secondary btn-small" style="padding:2px 8px;" onclick="pfadKopieren(this)" data-pfad="{{ sw.installer_pfad }}">Kopieren</button>
-          {% endif %}
-        </span>
-      </div>
-      <div class="kv-row"><span class="kv-label">Silent-Parameter</span><span class="kv-value">{{ sw.install_parameter or "–" }}</span></div>
-      <div class="kv-row"><span class="kv-label">Angelegt am</span><span class="kv-value">{{ sw.erstellt_am[:10] }}</span></div>
-    </div>
-
-    <form method="post" action="{{ url_for('software_loeschen', software_id=sw.id) }}"
-          onsubmit="return confirm('„{{ sw.name }}“ wirklich loeschen? Das kann nicht rueckgaengig gemacht werden.');">
-      <button type="submit" class="btn btn-danger btn-small">Tool loeschen</button>
-    </form>
-  </div>
-</div>
-
-<script>
-  function pfadKopieren(btn) {
-    const pfad = btn.getAttribute("data-pfad");
-    navigator.clipboard.writeText(pfad).then(function () {
-      const alterText = btn.textContent;
-      btn.textContent = "Kopiert!";
-      setTimeout(function () { btn.textContent = alterText; }, 1500);
-    });
-  }
-</script>
-
-{% endblock %}
diff --git a/templates/software_form.html b/templates/software_form.html
deleted file mode 100644
index 927b8bb..0000000
--- a/templates/software_form.html
+++ /dev/null
@@ -1,176 +0,0 @@
-{% extends "base.html" %}
-{% block title %}{{ "Tool bearbeiten" if software else "Neues Tool" }} – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">{{ "Bearbeiten" if software else "Neu anlegen" }}</p>
-    <h1>{{ software.name if software else "Neues Software-Tool" }}</h1>
-  </div>
-</div>
-
-<form method="post" class="form-grid">
-  <div class="field">
-    <label for="name">Name *</label>
-    <input type="text" id="name" name="name" required value="{{ software.name if software else '' }}" placeholder="z. B. 7-Zip, Adobe Reader, Firmen-VPN-Client">
-  </div>
-
-  <div class="field">
-    <label for="kategorie">Kategorie</label>
-    <input type="text" id="kategorie" name="kategorie" value="{{ software.kategorie if software else '' }}" placeholder="z. B. Basis-Software, Sicherheit, Inhouse-Tool">
-  </div>
-
-  <div class="field">
-    <label for="version">Aktuelle Version</label>
-    <input type="text" id="version" name="version" value="{{ software.version if software else '' }}" placeholder="z. B. 24.08">
-  </div>
-
-  <div class="field">
-    <label for="installer_pfad">Ablageordner</label>
-    <input type="text" id="installer_pfad" name="installer_pfad" value="{{ software.installer_pfad if software else '' }}" placeholder="z. B. C:\Bibliothek ablage\7-Zip\">
-    <div class="hint">Nur der Ordner – das Tool legt automatisch einen eigenen Unterordner und Dateinamen an, auch wenn ihr überall denselben Basisordner eintragt.</div>
-  </div>
-
-  <div class="field">
-    <label for="install_parameter">Silent-Install-Parameter</label>
-    <input type="text" id="install_parameter" name="install_parameter" value="{{ software.install_parameter if software else '' }}" placeholder="z. B. /S oder /quiet /norestart">
-  </div>
-
-  <div class="field">
-    <label for="dokumentation">Dokumentation / Einrichtungshinweise</label>
-    <textarea id="dokumentation" name="dokumentation" rows="8" placeholder="Schritt-fuer-Schritt Anleitung, Lizenzhinweise, Besonderheiten…">{{ software.dokumentation if software else '' }}</textarea>
-  </div>
-
-  <div class="field panel" style="padding:16px 18px;">
-    <label style="display:flex; align-items:center; gap:8px; font-size:14px;">
-      <input type="checkbox" id="auto_update" name="auto_update" style="width:auto;"
-             {{ "checked" if software and software.auto_update }}>
-      Automatische Aktualisierung aktivieren
-    </label>
-    <div class="hint">Wenn aktiv, sucht das Tool selbststaendig nach neuen Versionen und ersetzt die
-      Installer-Datei automatisch – ohne Rueckfrage. Die alte Datei wird vorher immer gesichert.</div>
-
-    <div id="update_quelle_felder" style="margin-top:14px; display:{{ 'block' if software and software.auto_update else 'none' }};">
-      <div class="auto-update-layout">
-        <div class="auto-update-fields">
-          <div class="field">
-            <label for="update_quelle">Update-Quelle</label>
-            <select id="update_quelle" name="update_quelle" onchange="updateQuelleHinweis()">
-              <option value="">– auswaehlen –</option>
-              <option value="winget" {{ "selected" if software and software.update_quelle == "winget" }}>winget (Windows-Paketverwaltung)</option>
-              <option value="github" {{ "selected" if software and software.update_quelle == "github" }}>GitHub-Projekt (Releases)</option>
-              <option value="direkt" {{ "selected" if software and software.update_quelle == "direkt" }}>Direkter Download-Link</option>
-            </select>
-          </div>
-          <div class="field">
-            <label for="update_wert" id="update_wert_label">Update-Wert</label>
-            <input type="text" id="update_wert" name="update_wert" value="{{ software.update_wert if software else '' }}" placeholder="– zuerst Update-Quelle auswaehlen –">
-          </div>
-        </div>
-
-        <div class="mini-doku" id="mini_doku">
-          <div class="mini-doku-titel" id="mini_doku_titel">Wo pruefen?</div>
-          <div id="mini_doku_inhalt">Zuerst eine Update-Quelle auswaehlen, dann erscheinen hier die Pruef-Schritte.</div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <div class="form-actions">
-    <button type="submit" class="btn">{{ "Speichern" if software else "Tool anlegen" }}</button>
-    <a href="{{ url_for('software_detail', software_id=software.id) if software else url_for('dashboard') }}" class="btn btn-secondary">Abbrechen</a>
-  </div>
-</form>
-
-<script>
-  document.getElementById("auto_update").addEventListener("change", function () {
-    document.getElementById("update_quelle_felder").style.display = this.checked ? "block" : "none";
-  });
-
-  const MINI_DOKU = {
-    "": {
-      label: "Update-Wert",
-      platzhalter: "– zuerst Update-Quelle auswaehlen –",
-      inhalt: "Zuerst eine Update-Quelle auswaehlen, dann erscheinen hier die Pruef-Schritte."
-    },
-    "winget": {
-      label: "winget-Paket-ID",
-      platzhalter: "z. B. 7zip.7zip",
-      inhalt: `
-        <ol>
-          <li><strong>winstall.app</strong> oeffnen, Tool-Namen eingeben</li>
-          <li>Im grauen Befehlskasten das Wort direkt nach <code>--id</code> kopieren (bis zum naechsten Leerzeichen)</li>
-          <li>Exakt einfuegen wie kopiert – Gross-/Kleinschreibung ist wichtig, nicht abtippen</li>
-        </ol>
-        <div class="beispiel">--id <strong>Google.Chrome</strong> → Google.Chrome</div>`
-    },
-    "github": {
-      label: "GitHub-Projekt",
-      platzhalter: "z. B. videolan/vlc",
-      inhalt: `
-        <ol>
-          <li>Auf <strong>github.com</strong> nach dem Tool suchen</li>
-          <li>Auf der Projektseite rechts auf „Releases" schauen</li>
-          <li>Pruefen: liegt dort eine .exe- oder .msi-Datei?</li>
-        </ol>
-        <div class="beispiel">Beispiel: videolan/vlc<br>(aus github.com/videolan/vlc)</div>
-        <div class="hint" style="margin-top:8px;">Auch okay, falls aus Versehen die ganze Adresse eingefuegt wird (z. B. https://github.com/videolan/vlc) – wird automatisch erkannt.</div>`
-    },
-    "direkt": {
-      label: "Download-Link",
-      platzhalter: "z. B. https://beispiel.de/tool/aktuell.exe",
-      inhalt: `
-        <ol>
-          <li>Download-Seite des Herstellers oeffnen</li>
-          <li>Nach einem Link suchen, der immer zur neuesten Datei fuehrt (oft bei Portable-Tools)</li>
-        </ol>
-        <div class="beispiel">Versionsnummer ist hier technisch, keine echte Nummer.</div>`
-    }
-  };
-
-  function updateQuelleHinweis() {
-    const quelle = document.getElementById("update_quelle").value;
-    const angaben = MINI_DOKU[quelle] || MINI_DOKU[""];
-    document.getElementById("update_wert_label").textContent = angaben.label;
-    document.getElementById("update_wert").placeholder = angaben.platzhalter;
-    document.getElementById("mini_doku_inhalt").innerHTML = angaben.inhalt;
-  }
-  updateQuelleHinweis();
-
-  // Ordner-Vorschlag: merkt sich den zuletzt genutzten Basis-Ordner und
-  // schlaegt ihn bei neuen Tools automatisch vor - so muss der Ablageort
-  // nur beim allerersten Tool einmal komplett eingetippt werden.
-  (function () {
-    var nameFeld = document.getElementById("name");
-    var pfadFeld = document.getElementById("installer_pfad");
-    var SPEICHER_KEY = "sb_basis_ordner";
-
-    function sanitisiere(text) {
-      return (text || "").trim().replace(/[<>:"/\\|?*]/g, "");
-    }
-
-    if (nameFeld && pfadFeld && !pfadFeld.value) {
-      nameFeld.addEventListener("blur", function () {
-        if (pfadFeld.value.trim() !== "" || !nameFeld.value.trim()) return;
-        var basis = localStorage.getItem(SPEICHER_KEY);
-        if (!basis) return;
-        pfadFeld.value = basis.replace(/[\\/]+$/, "") + "\\" + sanitisiere(nameFeld.value) + "\\";
-      });
-    }
-
-    if (pfadFeld) {
-      pfadFeld.addEventListener("change", function () {
-        var wert = pfadFeld.value.trim();
-        if (!wert) return;
-        var teile = wert.replace(/[\\/]+$/, "").split(/[\\/]/);
-        if (teile.length > 1) {
-          teile.pop();
-          var basis = teile.join("\\");
-          if (basis) localStorage.setItem(SPEICHER_KEY, basis);
-        }
-      });
-    }
-  })();
-</script>
-
-{% endblock %}
diff --git a/templates/zugriff_eingeschraenkt.html b/templates/zugriff_eingeschraenkt.html
deleted file mode 100644
index bde86d3..0000000
--- a/templates/zugriff_eingeschraenkt.html
+++ /dev/null
@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html lang="de">
-<head>
-  <meta charset="UTF-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title>Zugriff eingeschränkt – Server-Bibliothek</title>
-  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
-</head>
-<body>
-  <div class="layout">
-    <main class="main" style="max-width:520px; margin:60px auto;">
-      <div class="panel">
-        <p class="eyebrow">Zugriff eingeschränkt</p>
-        <h1 style="margin-bottom:14px;">Nur eingeschränkter Zugriff verfügbar</h1>
-        <p style="font-size:13.5px; line-height:1.6;">
-          Von diesem Rechner aus (über das Netzwerk) ist nur die Software-Anforderung
-          verfügbar. Die vollständige Verwaltung (Software-Katalog, Protokolle usw.)
-          ist ausschließlich direkt am Server selbst zugänglich.
-        </p>
-        <a href="{{ url_for('einrichten') }}" class="btn" style="margin-top:10px; display:inline-block;">
-          Zur Software-Anforderung
-        </a>
-      </div>
-    </main>
-  </div>
-</body>
-</html>
diff --git a/templates/zugriffslog.html b/templates/zugriffslog.html
deleted file mode 100644
index 11edb5e..0000000
--- a/templates/zugriffslog.html
+++ /dev/null
@@ -1,56 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Zugriffsprotokoll – Server-Bibliothek{% endblock %}
-{% block content %}
-
-<div class="page-head">
-  <div>
-    <p class="eyebrow">Sicherheit</p>
-    <h1>Zugriffsprotokoll</h1>
-  </div>
-</div>
-
-<div class="hint" style="margin-bottom:16px;">
-  Zeigt jeden Zugriff auf die Software-Anforderung von außerhalb des Servers.
-  Ein echter Windows-Benutzername lässt sich ohne Domänen-Integration nicht
-  zuverlässig ermitteln – hier stehen IP-Adresse, per Reverse-DNS ermittelter
-  Rechnername (falls auflösbar) und der selbst angegebene Name.
-</div>
-
-{% if eintraege %}
-<div class="panel" style="padding:0; overflow-x:auto;">
-  <table class="data-table" style="margin:0;">
-    <thead>
-      <tr>
-        <th style="padding-left:22px;">Zeitpunkt</th>
-        <th>Herkunft</th>
-        <th>IP-Adresse</th>
-        <th>Rechnername</th>
-        <th>Angegebener Name</th>
-        <th style="padding-right:22px;">Aktion</th>
-      </tr>
-    </thead>
-    <tbody>
-      {% for e in eintraege %}
-      <tr>
-        <td style="padding-left:22px;">{{ e.zeitpunkt.replace("T", " ") }}</td>
-        <td>
-          {% if e.lokal %}
-            <span class="badge status-ok">Lokal (Server)</span>
-          {% else %}
-            <span class="badge status-warnung">Netzwerk</span>
-          {% endif %}
-        </td>
-        <td>{{ e.ip_adresse or "–" }}</td>
-        <td>{{ e.rechnername or "–" }}</td>
-        <td>{{ e.angegebener_name or "–" }}</td>
-        <td style="padding-right:22px;">{{ e.aktion }}</td>
-      </tr>
-      {% endfor %}
-    </tbody>
-  </table>
-</div>
-{% else %}
-<div class="empty-state">Noch keine Zugriffe protokolliert.</div>
-{% endif %}
-
-{% endblock %}
diff --git a/updater.py b/updater.py
deleted file mode 100644
index 95f9988..0000000
--- a/updater.py
+++ /dev/null
@@ -1,371 +0,0 @@
-"""
-Update-Checker fuer die Server-Bibliothek.
-
-Unterstuetzt drei Update-Quellen pro Software:
-- winget   : Windows Paketverwaltung, deckt die meiste bekannte Standard-Software ab
-- github   : GitHub-Releases, fuer Open-Source-Tools ohne winget-Paket
-- direkt   : ein fester Download-Link, der immer zur neuesten Datei fuehrt
-
-WICHTIGES PRINZIP - Ordner-Konvention:
-Das Feld "Installer-Pfad" kann entweder auf eine konkrete Datei zeigen
-(z. B. '...\\7-Zip\\7zip.exe' - alte Schreibweise, funktioniert weiter) oder
-einfach nur auf einen ORDNER (z. B. '...\\7-Zip\\' - empfohlen). Zeigt es auf
-einen Ordner, kuemmert sich dieses Modul selbst um einen sauberen, stabilen
-Dateinamen darin (aus dem Tool-Namen abgeleitet) - niemand muss sich einen
-Dateinamen ausdenken oder eintragen.
-
-Jede Pruef-Funktion gibt ein UpdateResult zurueck und aendert NIE die
-Datenbank direkt - das uebernimmt app.py, nachdem das Ergebnis vorliegt.
-"""
-
-import json
-import os
-import re
-import shutil
-import subprocess
-import urllib.request
-import urllib.error
-from datetime import datetime
-
-USER_AGENT = "ServerBibliothek-UpdateChecker/1.0"
-DATEI_ENDUNGEN = (".exe", ".msi", ".msix", ".appx", ".zip")
-
-
-class UpdateResult:
-    def __init__(self, erfolg, alte_version=None, neue_version=None, meldung=""):
-        self.erfolg = erfolg
-        self.alte_version = alte_version
-        self.neue_version = neue_version
-        self.meldung = meldung
-
-    def unveraendert(self):
-        return self.erfolg and self.alte_version == self.neue_version
-
-
-# ---------------------------------------------------------------------------
-# Ordner-Konvention: Dateiname automatisch verwalten
-# ---------------------------------------------------------------------------
-
-def sanitisiere_dateiname(name):
-    """Macht aus einem Tool-Namen einen sicheren Dateinamen-Stamm."""
-    name = (name or "tool").strip()
-    name = re.sub(r'[<>:"/\\|?*]', "", name)
-    name = re.sub(r"\s+", "-", name)
-    return name or "tool"
-
-
-def ist_datei_ziel(pfad):
-    """True, wenn der Pfad bereits auf eine konkrete Datei zeigt (erkennbar an
-    der Endung)."""
-    return (pfad or "").lower().endswith(DATEI_ENDUNGEN)
-
-
-def _roh_ordner_und_wunschname(installer_pfad):
-    """Zerlegt den eingetragenen Pfad in (Ordneranteil, bevorzugter Dateiname
-    oder None). Egal ob eine Datei-Endung dabei ist oder nicht - der
-    Ordneranteil wird in jedem Fall separat ermittelt, damit anschliessend
-    IMMER in einem Tool-eigenen Unterordner gelandet wird."""
-    pfad = (installer_pfad or "").rstrip("\\/")
-    if ist_datei_ziel(pfad):
-        return os.path.dirname(pfad), os.path.basename(pfad)
-    return pfad, None
-
-
-def effektiver_ablageordner(installer_pfad, name):
-    """Ermittelt den tatsaechlichen Ordner, in dem die Datei(en) fuer DIESES
-    Tool liegen sollen - egal ob im Feld ein reiner Ordner oder ein
-    kompletter Dateipfad eingetragen ist (auch aeltere Eintraege). Haengt
-    automatisch einen Unterordner mit dem Tool-Namen an, falls der Ordner
-    noch nicht selbst so heisst. Dadurch landen mehrere Tools nie gemeinsam
-    'nackt' im selben Ordner, selbst wenn ueberall derselbe Basisordner
-    eingetragen wurde oder frueher schon eine Datei direkt dort abgelegt war."""
-    roh_ordner, _ = _roh_ordner_und_wunschname(installer_pfad)
-    stamm = sanitisiere_dateiname(name)
-    letzter_teil = os.path.basename(roh_ordner)
-    if letzter_teil.lower() == stamm.lower():
-        return roh_ordner
-    return os.path.join(roh_ordner, stamm)
-
-
-def verwaltete_datei_pfad(installer_pfad, name):
-    """Gibt den Pfad zur automatisch verwalteten Installer-Datei zurueck,
-    falls eine existiert (im Tool-eigenen Unterordner). None, wenn (noch)
-    keine Datei da ist."""
-    if not installer_pfad:
-        return None
-    ordner = effektiver_ablageordner(installer_pfad, name)
-    _, wunschname = _roh_ordner_und_wunschname(installer_pfad)
-    kandidaten = ([wunschname] if wunschname else []) + [
-        sanitisiere_dateiname(name) + endung for endung in DATEI_ENDUNGEN
-    ]
-    for kandidat in kandidaten:
-        pfad = os.path.join(ordner, kandidat)
-        if os.path.isfile(pfad):
-            return pfad
-    # Uebergangsweise: eine aeltere, noch nicht verschachtelte Datei direkt
-    # am urspruenglich eingetragenen Pfad findet der Download-Button auch
-    # noch - bis der naechste automatische Check eine frische Kopie im
-    # Tool-eigenen Unterordner anlegt.
-    if ist_datei_ziel(installer_pfad) and os.path.isfile(installer_pfad):
-        return installer_pfad
-    return None
-
-
-def _ziel_datei_ermitteln(installer_pfad, name, quell_dateiname):
-    """Bestimmt den tatsaechlichen Ziel-Dateipfad fuer einen Download: immer
-    im Tool-eigenen Unterordner. Der Dateiname folgt einer schon frueher
-    explizit eingetragenen Datei-Angabe, sonst wird er automatisch aus dem
-    Tool-Namen + Endung der heruntergeladenen Datei gebildet."""
-    ordner = effektiver_ablageordner(installer_pfad, name)
-    _, wunschname = _roh_ordner_und_wunschname(installer_pfad)
-    if wunschname:
-        return os.path.join(ordner, wunschname)
-    endung = os.path.splitext(quell_dateiname)[1].lower()
-    if endung not in DATEI_ENDUNGEN:
-        endung = ".exe"
-    return os.path.join(ordner, sanitisiere_dateiname(name) + endung)
-
-
-def _endung_aus_url(url):
-    pfad_teil = url.split("?")[0].rstrip("/")
-    _, ext = os.path.splitext(pfad_teil)
-    return ext.lower() if ext.lower() in DATEI_ENDUNGEN else ".exe"
-
-
-# ---------------------------------------------------------------------------
-# Hilfsfunktionen
-# ---------------------------------------------------------------------------
-
-def _http_get_json(url):
-    req = urllib.request.Request(
-        url, headers={"User-Agent": USER_AGENT, "Accept": "application/vnd.github+json"}
-    )
-    with urllib.request.urlopen(req, timeout=20) as resp:
-        return json.loads(resp.read().decode("utf-8"))
-
-
-def _http_download(url, timeout=120):
-    """Laedt eine Datei in eine temporaere Datei und gibt den Pfad zurueck."""
-    req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
-    tmp_pfad = os.path.join(
-        os.environ.get("TEMP", "."), f"_sb_download_{datetime.now().strftime('%Y%m%d%H%M%S%f')}"
-    )
-    with urllib.request.urlopen(req, timeout=timeout) as resp, open(tmp_pfad, "wb") as f:
-        shutil.copyfileobj(resp, f)
-    return tmp_pfad
-
-
-def sichern_und_ersetzen(ziel_pfad, neue_datei_pfad):
-    """Legt eine Sicherheitskopie der alten Datei an (Ordner '_sicherungen'
-    neben dem Installer) und verschiebt dann die neue Datei an den Zielort.
-    ziel_pfad muss eine konkrete Datei sein (kein Ordner)."""
-    if os.path.isdir(ziel_pfad):
-        raise ValueError(
-            f"Interner Fehler: Zielpfad ist ein Ordner statt einer Datei: '{ziel_pfad}'."
-        )
-
-    ziel_ordner = os.path.dirname(ziel_pfad) or "."
-    os.makedirs(ziel_ordner, exist_ok=True)
-
-    if os.path.isfile(ziel_pfad):
-        sicherungs_ordner = os.path.join(ziel_ordner, "_sicherungen")
-        os.makedirs(sicherungs_ordner, exist_ok=True)
-        zeitstempel = datetime.now().strftime("%Y%m%d_%H%M%S")
-        sicherungs_ziel = os.path.join(
-            sicherungs_ordner, f"{os.path.basename(ziel_pfad)}.{zeitstempel}.bak"
-        )
-        shutil.copy2(ziel_pfad, sicherungs_ziel)
-
-    shutil.move(neue_datei_pfad, ziel_pfad)
-
-
-# ---------------------------------------------------------------------------
-# Quelle: GitHub Releases
-# ---------------------------------------------------------------------------
-
-def _normalisiere_github_pfad(wert):
-    """Macht das Feld tolerant gegenueber versehentlich mitkopierten
-    URL-Bestandteilen, z. B. 'github.com/ip7z/7zip' -> 'ip7z/7zip'."""
-    wert = (wert or "").strip()
-    for praefix in ("https://github.com/", "http://github.com/", "www.github.com/", "github.com/"):
-        if wert.lower().startswith(praefix):
-            wert = wert[len(praefix):]
-            break
-    wert = wert.strip("/")
-    if wert.lower().endswith(".git"):
-        wert = wert[:-4]
-    return wert
-
-
-def pruefe_github(name, update_wert, aktuelle_version, installer_pfad):
-    """update_wert Format: 'besitzer/projekt', z. B. 'videolan/vlc' """
-    update_wert = _normalisiere_github_pfad(update_wert)
-    if not update_wert or "/" not in update_wert:
-        return UpdateResult(False, aktuelle_version, None,
-                             "Ungueltiges Format. Erwartet: besitzer/projekt")
-
-    try:
-        daten = _http_get_json(f"https://api.github.com/repos/{update_wert}/releases/latest")
-    except (urllib.error.URLError, urllib.error.HTTPError) as e:
-        return UpdateResult(False, aktuelle_version, None, f"GitHub nicht erreichbar: {e}")
-
-    neue_version = str(daten.get("tag_name", "")).lstrip("vV")
-    if not neue_version:
-        return UpdateResult(False, aktuelle_version, None, "Keine Version im GitHub-Release gefunden.")
-
-    assets = daten.get("assets", [])
-    passendes_asset = next(
-        (a for a in assets if a.get("name", "").lower().endswith((".exe", ".msi"))), None
-    )
-    if not passendes_asset:
-        return UpdateResult(False, aktuelle_version, neue_version,
-                             "Neue Version gefunden, aber keine .exe/.msi-Datei im Release vorhanden.")
-
-    if not installer_pfad:
-        return UpdateResult(False, aktuelle_version, neue_version,
-                             "Kein Ablageort hinterlegt, Download uebersprungen.")
-
-    ziel_pfad = _ziel_datei_ermitteln(installer_pfad, name, passendes_asset["name"])
-    datei_fehlt = not os.path.isfile(ziel_pfad)
-
-    if neue_version == (aktuelle_version or "") and not datei_fehlt:
-        return UpdateResult(True, aktuelle_version, aktuelle_version, "Bereits aktuell.")
-
-    try:
-        tmp = _http_download(passendes_asset["browser_download_url"])
-        sichern_und_ersetzen(ziel_pfad, tmp)
-    except Exception as e:
-        return UpdateResult(False, aktuelle_version, neue_version, f"Download fehlgeschlagen: {e}")
-
-    meldung = "Datei fehlte, wurde neu heruntergeladen." if datei_fehlt else "Erfolgreich aktualisiert."
-    return UpdateResult(True, aktuelle_version, neue_version, meldung)
-
-
-# ---------------------------------------------------------------------------
-# Quelle: winget (Windows Paketverwaltung)
-# ---------------------------------------------------------------------------
-
-def _winget_verfuegbar():
-    return shutil.which("winget") is not None
-
-
-def pruefe_winget(name, update_wert, aktuelle_version, installer_pfad):
-    """update_wert = winget-Paket-ID, z. B. '7zip.7zip' """
-    update_wert = (update_wert or "").strip()
-    if not update_wert:
-        return UpdateResult(False, aktuelle_version, None, "Keine winget-Paket-ID hinterlegt.")
-
-    if not _winget_verfuegbar():
-        return UpdateResult(False, aktuelle_version, None, "winget ist auf diesem Server nicht verfuegbar.")
-
-    try:
-        ausgabe = subprocess.run(
-            ["winget", "show", "--id", update_wert, "--exact", "--accept-source-agreements"],
-            capture_output=True, text=True, timeout=60,
-        )
-    except Exception as e:
-        return UpdateResult(False, aktuelle_version, None, f"winget-Aufruf fehlgeschlagen: {e}")
-
-    treffer = re.search(r"^Version:\s*(.+)$", ausgabe.stdout, re.MULTILINE)
-    if not treffer:
-        return UpdateResult(False, aktuelle_version, None,
-                             f"Version ueber winget nicht auslesbar (Paket-ID korrekt? '{update_wert}')")
-    neue_version = treffer.group(1).strip()
-
-    if not installer_pfad:
-        return UpdateResult(False, aktuelle_version, neue_version,
-                             "Kein Ablageort hinterlegt, Download uebersprungen.")
-
-    datei_fehlt = verwaltete_datei_pfad(installer_pfad, name) is None
-    if neue_version == (aktuelle_version or "") and not datei_fehlt:
-        return UpdateResult(True, aktuelle_version, aktuelle_version, "Bereits aktuell.")
-
-    staging_ordner = os.path.join(os.environ.get("TEMP", "."), f"_sb_winget_{update_wert.replace('.', '_')}")
-    if os.path.isdir(staging_ordner):
-        shutil.rmtree(staging_ordner, ignore_errors=True)
-    os.makedirs(staging_ordner, exist_ok=True)
-
-    try:
-        subprocess.run(
-            ["winget", "download", "--id", update_wert, "--exact", "-d", staging_ordner,
-             "--accept-source-agreements", "--accept-package-agreements"],
-            capture_output=True, text=True, timeout=300, check=True,
-        )
-    except Exception as e:
-        return UpdateResult(False, aktuelle_version, neue_version, f"Download ueber winget fehlgeschlagen: {e}")
-
-    gefundene_dateien = []
-    for wurzel, _, dateien in os.walk(staging_ordner):
-        for datei in dateien:
-            if datei.lower().endswith((".exe", ".msi")):
-                gefundene_dateien.append(os.path.join(wurzel, datei))
-
-    if not gefundene_dateien:
-        shutil.rmtree(staging_ordner, ignore_errors=True)
-        return UpdateResult(False, aktuelle_version, neue_version,
-                             "winget-Download hat keine .exe/.msi-Datei geliefert.")
-
-    neueste_datei = max(gefundene_dateien, key=os.path.getmtime)
-    ziel_pfad = _ziel_datei_ermitteln(installer_pfad, name, os.path.basename(neueste_datei))
-    try:
-        sichern_und_ersetzen(ziel_pfad, neueste_datei)
-    finally:
-        shutil.rmtree(staging_ordner, ignore_errors=True)
-
-    meldung = "Datei fehlte, wurde neu heruntergeladen." if datei_fehlt else "Ueber winget aktualisiert."
-    return UpdateResult(True, aktuelle_version, neue_version, meldung)
-
-
-# ---------------------------------------------------------------------------
-# Quelle: direkter Download-Link
-# ---------------------------------------------------------------------------
-
-def pruefe_direkt(name, update_wert, aktuelle_version, installer_pfad):
-    """update_wert = feste URL, die immer zur aktuellen Datei fuehrt.
-    Da kein Versionsstand aus der URL ablesbar ist, wird ETag/Last-Modified
-    des Servers als technische Kennung genutzt, um Aenderungen zu erkennen."""
-    update_wert = (update_wert or "").strip()
-    if not update_wert:
-        return UpdateResult(False, aktuelle_version, None, "Kein Download-Link hinterlegt.")
-
-    try:
-        req = urllib.request.Request(update_wert, method="HEAD", headers={"User-Agent": USER_AGENT})
-        with urllib.request.urlopen(req, timeout=20) as resp:
-            kennung = resp.headers.get("ETag") or resp.headers.get("Last-Modified") or ""
-    except Exception as e:
-        return UpdateResult(False, aktuelle_version, None, f"Link nicht erreichbar: {e}")
-
-    if not kennung:
-        kennung = "unbekannt-" + datetime.now().strftime("%Y%m%d")
-
-    if not installer_pfad:
-        return UpdateResult(False, aktuelle_version, kennung, "Kein Ablageort hinterlegt, Download uebersprungen.")
-
-    datei_fehlt = verwaltete_datei_pfad(installer_pfad, name) is None
-    if kennung == (aktuelle_version or "") and not datei_fehlt:
-        return UpdateResult(True, aktuelle_version, aktuelle_version, "Bereits aktuell (laut Server unveraendert).")
-
-    ziel_pfad = _ziel_datei_ermitteln(installer_pfad, name, "download" + _endung_aus_url(update_wert))
-    try:
-        tmp = _http_download(update_wert)
-        sichern_und_ersetzen(ziel_pfad, tmp)
-    except Exception as e:
-        return UpdateResult(False, aktuelle_version, kennung, f"Download fehlgeschlagen: {e}")
-
-    meldung = ("Datei fehlte, wurde neu heruntergeladen." if datei_fehlt
-               else "Neu heruntergeladen (Aenderung am Link erkannt).")
-    return UpdateResult(True, aktuelle_version, kennung, meldung)
-
-
-# ---------------------------------------------------------------------------
-# Einstiegspunkt
-# ---------------------------------------------------------------------------
-
-def pruefe_software(name, quelle, update_wert, aktuelle_version, installer_pfad):
-    if quelle == "winget":
-        return pruefe_winget(name, update_wert, aktuelle_version, installer_pfad)
-    if quelle == "github":
-        return pruefe_github(name, update_wert, aktuelle_version, installer_pfad)
-    if quelle == "direkt":
-        return pruefe_direkt(name, update_wert, aktuelle_version, installer_pfad)
-    return UpdateResult(False, aktuelle_version, None, "Keine gueltige Update-Quelle hinterlegt.")

--
Gitblit v1.9.3