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