From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally
---
packages/bibliothek-client/src/components/bibliothek-einstellungen.ts | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 224 insertions(+), 0 deletions(-)
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;
+ }
+}
--
Gitblit v1.9.3