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` ·
+ <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` · <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)} · `;
+ }
+ if (software.dateiStandVom) {
+ return html`geholt ${this.alsDatum(software.dateiStandVom)} · `;
+ }
+ 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 & 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 (> {{ 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>⚡ 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 & 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