13571df994
- zap-baseline.sh: passive Aussenpruefung mit festgelegtem ZAP-Abbild, Ziel per ZAP_TARGET (Standard: oeffentliche Adresse von alpha), Anmeldedatei-Weg nur fuer den Ausnahmefall - zap-hooks.py: Spinne fuellt keine Formulare aus und sendet keine (POST=0 lokal bewiesen) - Sicherheitsprotokoll: Abschnitt Pruefung von aussen, erster Lauf direkt gegen die Anwendung (0 hoch, 2 mittel, 6 niedrig, 3 Info), Einordnung und Verlauf - Betriebsanleitung Kapitel 9: Schritt Pruefung von aussen; Entwicklungsanleitung: Abschnitt ZAP; CHANGELOG Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
832 lines
61 KiB
Markdown
832 lines
61 KiB
Markdown
<!-- generated-by: gsd-doc-writer -->
|
||
# Tessera – Betriebshandbuch
|
||
|
||
Anleitung für den laufenden Betrieb von Tessera: Erstinstallation, neue Fassungen
|
||
einspielen, Datenbank-Migrationen, Sicherung/Wiederherstellung und Fehlersuche.
|
||
Richtet sich an Kolleg:innen, die die Docker-Container auf dem Server betreiben.
|
||
|
||
Für den Aufbau der CI/CD-Pipeline (Gitea Actions, act_runner) siehe
|
||
[`docs/ci-cd-setup.md`](./ci-cd-setup.md) – dieses Dokument behandelt nur den
|
||
Betrieb der bereits laufenden Installation, nicht deren automatisierten Build.
|
||
Was an Tessera bisher auf Sicherheit geprüft wurde und mit welchem Ergebnis,
|
||
steht im [Sicherheitsprotokoll](./sicherheitsprotokoll.md).
|
||
|
||
## Inhaltsverzeichnis
|
||
|
||
1. [Überblick der Dienste](#1-überblick-der-dienste)
|
||
2. [Erstinstallation](#2-erstinstallation)
|
||
3. [Konfiguration](#3-konfiguration)
|
||
4. [Neue Fassung einspielen](#4-neue-fassung-einspielen)
|
||
5. [Datenbank-Migrationen](#5-datenbank-migrationen)
|
||
6. [Sicherung und Wiederherstellung](#6-sicherung-und-wiederherstellung)
|
||
7. [Protokolle und Fehlersuche](#7-protokolle-und-fehlersuche)
|
||
8. [Abgrenzung zur CI/CD-Pipeline](#8-abgrenzung-zur-cicd-pipeline)
|
||
9. [Zwei Kanäle: Live und Beta](#9-zwei-kanäle-live-und-beta)
|
||
10. [Desktop-App: Pakete und Release-Dateien](#10-desktop-app-pakete-und-release-dateien)
|
||
|
||
---
|
||
|
||
## 1. Überblick der Dienste
|
||
|
||
Tessera besteht aus drei Containern, definiert in `docker-compose.yml` (Basis) und
|
||
`docker-compose.prod.yml` (Produktionsvariante mit fertigen Images statt lokalem Build):
|
||
|
||
| Dienst | Image / Build | Host-Port | Zweck |
|
||
|--------|---------------|-----------|-------|
|
||
| `web` | `apps/web/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG}` (Prod; `beta` oder `live`, siehe Kapitel 9) | 3000 | Next.js-Frontend (Portal, Dashboard) |
|
||
| `api` | `apps/api/Dockerfile` (Basis) bzw. `git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG}` (Prod; `beta` oder `live`, siehe Kapitel 9) | 3001 | NestJS-Backend (REST-API, Prisma/PostgreSQL-Zugriff) |
|
||
| `db` | `postgres:16-alpine` | kein Host-Port | PostgreSQL-Datenbank |
|
||
|
||
Zusätzlich existiert `docker-compose.dev.yml` (Bind-Mounts für Live-Reload,
|
||
Mailhog, OpenLDAP, phpLDAPadmin) für die lokale Entwicklung sowie
|
||
`docker-compose.ci.yml` für den Gitea `act_runner` – beide sind für den
|
||
Produktivbetrieb ohne Belang und werden hier nicht weiter behandelt.
|
||
|
||
**Netzwerke** (siehe `docker-compose.yml`):
|
||
|
||
- `frontend-net` – nur `web`, darüber ist der Browser-Zugriff erreichbar (Port 3000).
|
||
- `backend-net` – `web` und `api`, Kommunikation zwischen Frontend-Server und Backend.
|
||
- `data-net` – `api` und `db`, als `internal: true` markiert. Die Datenbank ist damit
|
||
aus dem Host-Netzwerk grundsätzlich nicht erreichbar; Zugriff nur über
|
||
`docker compose exec db ...` von innen.
|
||
|
||
**Startreihenfolge:** `db` → `api` → `web`, erzwungen über `depends_on` mit
|
||
`condition: service_healthy`:
|
||
|
||
- `db`: Healthcheck `pg_isready -U tessera` (Intervall 5 s, 5 Versuche).
|
||
- `api`: Healthcheck `wget --spider http://localhost:3001/health` (Intervall 10 s,
|
||
3 Versuche, `start_period` 10 s in der Basis- / 20 s in der Prod-Variante). Der
|
||
`/health`-Endpunkt (`apps/api/src/health/health.controller.ts`) prüft nur, dass
|
||
der NestJS-Prozess antwortet – **keine** Datenbankverbindung. Ein "healthy"
|
||
API-Container sagt also nichts darüber aus, ob Prisma tatsächlich verbunden ist.
|
||
- `web`: hat selbst **keinen** Healthcheck. `docker compose ps` zeigt `web` daher nie
|
||
als "healthy" an, nur als laufend – das ist normal.
|
||
|
||
Der Browser spricht ausschließlich mit `web` (Port 3000). Aufrufe unter
|
||
`/api-proxy/*` werden von Next.js serverseitig auf `API_INTERNAL_URL`
|
||
(`http://api:3001` im Docker-Netz) umgeschrieben (siehe `apps/web/next.config.ts`).
|
||
Der Browser erreicht die API also nie direkt und muss auch keine eigene CORS-Freigabe
|
||
für die API-Origin haben – Port 3001 muss von außen in der Regel nicht erreichbar
|
||
sein, ist es in den mitgelieferten Compose-Dateien aber (Host-Port 3001 ist
|
||
gemappt, sowohl in `docker-compose.yml` als auch in `docker-compose.prod.yml`).
|
||
|
||
**Empfehlung zur Firewall:** Sperren Sie Port 3001 in der Firewall des Servers (bzw.
|
||
lassen Sie ihn nur vom Server selbst zu) und geben Sie nach außen ausschließlich
|
||
Port 3000 frei, im Normalfall über den Nginx Proxy Manager. Der Port 3001 wird für
|
||
den normalen Betrieb nicht gebraucht, weil der Browser die API immer über `web`
|
||
und `/api-proxy` erreicht. Die Abfragen mit `curl` auf `http://localhost:3001/...`
|
||
in dieser Anleitung laufen auf dem Server selbst und bleiben davon unberührt.
|
||
|
||
## 2. Erstinstallation
|
||
|
||
Voraussetzung: Docker und Docker Compose (Plugin, `docker compose ...`) sind auf dem
|
||
Zielsystem installiert.
|
||
|
||
1. Repository auf den Server holen:
|
||
|
||
```bash
|
||
git clone <gitea-url>/schalli/tessera-ctl.git
|
||
cd tessera-ctl
|
||
```
|
||
|
||
2. `.env` aus der Vorlage anlegen:
|
||
|
||
```bash
|
||
cp .env.prod.example .env
|
||
```
|
||
|
||
Die Vorlage `.env.prod.example` enthält die für den Produktivbetrieb relevanten
|
||
Variablennamen; `.env.example` ist die schlankere Entwicklungsvariante. Welche
|
||
Variablen zu setzen sind, steht in Kapitel [3. Konfiguration](#3-konfiguration).
|
||
Mindestens erforderlich, bevor irgendetwas startet:
|
||
|
||
- `DB_PASSWORD` und dazu passend `DATABASE_URL`
|
||
- `JWT_SECRET`
|
||
- `TESSERA_ADMIN_EMAIL`, `TESSERA_ADMIN_PASSWORD`
|
||
- `TESSERA_ENCRYPTION_KEY` (siehe Warnhinweis unten – ohne diesen Wert startet
|
||
nichts)
|
||
|
||
3. Encryption-Key erzeugen und eintragen:
|
||
|
||
```bash
|
||
openssl rand -hex 32
|
||
```
|
||
|
||
Das Ergebnis (64 Hex-Zeichen) als `TESSERA_ENCRYPTION_KEY=` in die `.env`
|
||
eintragen. Dieser Schlüssel verschlüsselt alle bei Tessera hinterlegten
|
||
Zugangsdaten (LDAP-Bind-Passwort, SMTP, Kalender- sowie DKV-/Ausschreibungs-Postfächer).
|
||
**Ohne diesen Wert startet die API nicht** – `docker-compose.prod.yml` verwendet
|
||
die Compose-Syntax `${TESSERA_ENCRYPTION_KEY:?...}`, wodurch bereits `docker
|
||
compose` selbst mit einer Fehlermeldung abbricht, bevor ein Container startet,
|
||
falls die Variable fehlt. Zusätzlich prüft `CryptoService`
|
||
(`apps/api/src/crypto/crypto.service.ts`) beim Hochfahren der API die Länge
|
||
(muss genau 64 Hex-Zeichen sein) und wirft andernfalls einen Fehler.
|
||
|
||
Diesen Schlüssel getrennt von jedem Datenbank-Dump aufbewahren (siehe Kapitel 6)
|
||
– geht er verloren, sind alle gespeicherten Zugangsdaten unwiderruflich
|
||
unbrauchbar und müssen von Hand neu eingegeben werden.
|
||
|
||
4. Container starten:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml pull
|
||
docker compose -f docker-compose.prod.yml up -d
|
||
```
|
||
|
||
Beim ersten Start seedet die API (`AdminSeedService`,
|
||
`apps/api/src/user/admin-seed.service.ts`) automatisch den initialen
|
||
Super-Admin-Account aus `TESSERA_ADMIN_USER` / `_EMAIL` / `_PASSWORD` sowie
|
||
den Mandanten `default`. Das passiert nur, wenn noch **kein** Benutzer mit
|
||
diesem Nutzernamen existiert – bei einer bereits befüllten Datenbank wird der
|
||
Seed-Schritt stillschweigend übersprungen, auch wenn sich die Passwort-Variable
|
||
in der `.env` danach ändert.
|
||
|
||
5. Prüfen, ob alles läuft:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml ps
|
||
curl -s http://localhost:3001/health
|
||
```
|
||
|
||
`api` sollte als `healthy` markiert sein, die `/health`-Abfrage liefert
|
||
`{"status":"ok",...}`. Danach ist die Oberfläche unter Port 3000 erreichbar.
|
||
|
||
## 3. Konfiguration
|
||
|
||
Alle Variablen werden über `.env` im Projektverzeichnis eingelesen (Docker Compose
|
||
liest diese Datei automatisch). Werte unten sind **Platzhalter**, keine echten
|
||
Zugangsdaten.
|
||
|
||
| Variable | Pflicht in Prod? | Default (Prod-Compose) | Zweck |
|
||
|----------|:---:|---|---|
|
||
| `DB_PASSWORD` | ja | – | Passwort des PostgreSQL-Benutzers `tessera`, an den `db`-Container durchgereicht. |
|
||
| `DATABASE_URL` | ja | – | Vollständiger Prisma-Connection-String, z. B. `postgresql://tessera:<passwort>@db:5432/tessera`. Muss zum `DB_PASSWORD` passen. |
|
||
| `JWT_SECRET` | ja | – | Signiert/prüft die JWT-Auth-Cookies. Wird **sowohl** an `api` **als auch** an `web` durchgereicht – beide müssen denselben Wert erhalten, sonst schlägt die Anmeldung fehl. In der Basis-Compose (Dev) gibt es einen unsicheren Default (`tessera-dev-jwt-secret-change-in-production`), in Prod nicht. |
|
||
| `TESSERA_ENCRYPTION_KEY` | ja | – | AES-256-GCM-Schlüssel (64 Hex-Zeichen) für gespeicherte Drittanbieter-Zugangsdaten. Siehe Kapitel 2. |
|
||
| `CALENDAR_ENCRYPTION_KEY` | nein | leer | Alter Name von `TESSERA_ENCRYPTION_KEY` (aus der Zeit, als der Schlüssel nur den Kalender schützte). Wird nur noch aus Kompatibilität gelesen: Ist `TESSERA_ENCRYPTION_KEY` nicht gesetzt, übernimmt die API diesen Wert und schreibt beim Start eine Warnung ins Protokoll. Ist `TESSERA_ENCRYPTION_KEY` gesetzt, hat er Vorrang. Eine bestehende `.env` mit dem alten Namen funktioniert also weiter; für neue Installationen immer `TESSERA_ENCRYPTION_KEY` verwenden. |
|
||
| `TESSERA_MIGRATE_DATABASE_URL` | nein | leer | Eigene Datenbankverbindung, die ausschließlich der Migrationsschritt beim Start des `api`-Containers benutzt (Kapitel 5). Sie ist dafür gedacht, dass die Migration mit den Rechten des Tabelleneigentümers läuft, die laufende Anwendung aber über `DATABASE_URL` mit einer rechteärmeren Rolle arbeitet. Leer (der Normalfall) heißt: Migration und Anwendung nutzen beide `DATABASE_URL`. Erst belegen, wenn die Schritte in `docs/mandantentrennung-datenbankrolle.md` vollständig abgearbeitet sind – eine voreilige Umstellung sperrt die Anwendung von ihren eigenen Daten aus. |
|
||
| `TESSERA_ADMIN_USER` | nein | `admin` | Benutzername des initialen Super-Admin, nur beim allerersten Start relevant. |
|
||
| `TESSERA_ADMIN_EMAIL` | ja (für den Seed) | – | E-Mail des initialen Super-Admin. |
|
||
| `TESSERA_ADMIN_PASSWORD` | ja (für den Seed) | – | Initiales Passwort des Super-Admin. |
|
||
| `TESSERA_FORCE_CHANGE` | nein | `true` | Erzwingt Passwortwechsel beim ersten Login des geseedeten Admin-Accounts. |
|
||
| `TESSERA_SMTP_HOST` / `_PORT` / `_SECURE` / `_USER` / `_PASSWORD` / `_FROM` | nein (aber ohne Host kein Mailversand) | Host leer, Port `587`, `_SECURE=false`, `_FROM` = `Tessera <noreply@tessera.local>` | SMTP-Relay für ausgehende Mails (Passwort-Reset, Benachrichtigungen). Gilt nur als Rückfall: Sobald unter Administration → E-Mail-Versand (SMTP) ein Versandweg eingerichtet ist, verwendet Tessera diesen und ignoriert die Variablen. Der Standard-Absender sollte durch eine echte Adresse der Firma ersetzt werden. |
|
||
| `MAIL_HOST` / `MAIL_PORT` / `MAIL_USER` / `MAIL_PASS` | nein | nicht gesetzt | Alte Rückfallnamen für den SMTP-Versand, die die API im Code weiterhin liest. Sie haben dort Vorrang vor den gleichnamigen `TESSERA_SMTP_*`-Werten (`MAIL_HOST` vor `TESSERA_SMTP_HOST` usw.; ohne beides greift `localhost:1025`, der Mailhog der Entwicklung). Weder die Compose-Dateien noch `.env.prod.example` reichen diese Namen an den `api`-Container durch; sie wirken also nur, wenn jemand sie von Hand in die Compose-Datei einträgt. Nutzen Sie sie nicht für neue Installationen, sondern `TESSERA_SMTP_*` oder besser die Einstellung in der Oberfläche. |
|
||
| `TESSERA_BUGREPORT_TO` | nein | leer | Rückfall-Postfach für den Knopf „Fehler melden“ in der Kopfleiste, falls unter Administration → E-Mail-Versand (SMTP) kein Feld „Fehlermeldungen an“ gesetzt ist. Leer = nur die Einstellung in der Oberfläche gilt. Wie `IMAGE_TAG` (Kapitel 9): die Serverdatei `/opt/tessera/docker-compose.prod.yml` bekommt die Zeile `TESSERA_BUGREPORT_TO: ${TESSERA_BUGREPORT_TO:-}` nur von Hand. |
|
||
| `APP_URL` | empfohlen | `http://localhost:3001` (für `NEXT_PUBLIC_API_URL`) / `http://localhost:3000` (für `TESSERA_APP_URL`) | Öffentliche Basis-URL der Web-Oberfläche. Wird serverseitig u. a. für in E-Mails generierte Links verwendet (`TESSERA_APP_URL`). |
|
||
| `API_INTERNAL_URL` | fest verdrahtet | `http://api:3001` | Adresse, unter der `web` die API **innerhalb** des Docker-Netzes erreicht; dorthin schreibt Next.js die `/api-proxy/*`-Rewrites um. In der Regel nicht ändern. |
|
||
| `CORS_ORIGIN` | nein | `http://localhost:3000` (im Code der API, in keiner Compose-Datei gesetzt) | Erlaubte Herkunftsadresse für Browser-Anfragen direkt an die API (CORS). Im Normalbetrieb ohne Wirkung, weil der Browser die API nie direkt, sondern nur über `web` und `/api-proxy` erreicht (Kapitel 1). Nur nötig, wenn jemand die API bewusst unter einer eigenen Adresse im Browser aufruft; dann muss die Variable zusätzlich in die Compose-Datei beim Dienst `api` eingetragen werden. |
|
||
| `APP_VERSION` / `APP_CHANNEL` / `APP_COMMIT` / `APP_BUILD_TIME` | nein – nicht per `.env` setzen | im Abbild fest eingebaut | Versionsstempel von API (und Web). Sie werden beim Bau des Abbilds durch die Pipeline (`.gitea/scripts/publish-images.sh`) als Build-Argumente übergeben und im Abbild als Umgebungsvariablen abgelegt. Aus ihnen speist sich `/health/version` (Kapitel 9) und die Startzeile im Protokoll. Ein selbst gebautes Abbild ohne diese Angaben meldet als Version und Kanal `dev` und lässt Kennung und Bauzeit leer. |
|
||
| `DASHBOARD_IMAGES_DIR` / `FAVORITE_ICONS_DIR` | nein – im Betrieb nie setzen | nicht gesetzt (Ablage unter `/app/user-files/dashboard-images` bzw. `/app/user-files/favorite-icons`) | Schalter, die die Ablage der Bilderrahmen-Bilder bzw. der Favoriten-Symbole auf ein anderes Verzeichnis umlenken. Sie existieren für Tests. Wer sie dennoch setzt, muss dafür sorgen, dass das neue Verzeichnis ebenfalls in einem Volume liegt und in die Sicherung aufgenommen wird (Kapitel 6), sonst gehen die Dateien beim Neuerstellen des Containers verloren. |
|
||
| `IMAGE_TAG` | empfohlen | `beta` | Welcher Kanal auf diesem Server läuft: `beta` (alle Neuerungen, alpha) oder `live` (nur freigegebene Versionen, tessera.ctl.de). Siehe Kapitel 9. |
|
||
|
||
Hinweis zu `NEXT_PUBLIC_API_URL`: Diese Variable wird beim Image-Build bereits fest
|
||
auf `/api-proxy` gesetzt (`ENV NEXT_PUBLIC_API_URL=/api-proxy` in
|
||
`apps/web/Dockerfile`, vor `pnpm build`). Next.js baut `NEXT_PUBLIC_*`-Variablen zur
|
||
Build-Zeit in das an den Browser ausgelieferte JavaScript ein; der zur Laufzeit über
|
||
Compose gesetzte Wert (`${APP_URL:-http://localhost:3001}`) hat auf das bereits
|
||
gebaute Frontend-Bundle daher keine sichtbare Wirkung mehr. Praktisch reicht es, dass
|
||
`APP_URL` für `TESSERA_APP_URL` (serverseitig, z. B. Mail-Links) korrekt gesetzt ist.
|
||
|
||
<!-- VERIFY: Ob auf dem tatsächlichen Server zusätzliche Variablen (z. B. `CORS_ORIGIN`, `NODE_ENV`) in der Compose-Datei gesetzt sind, lässt sich aus dem Repository allein nicht feststellen. -->
|
||
|
||
**Wichtig – Konfigurationsdrift auf dem Server:** Auf dem Produktivserver wurde die
|
||
laufende Compose-Datei in der Vergangenheit direkt von Hand angepasst und ist damit
|
||
nicht mehr zwingend identisch mit `docker-compose.prod.yml` in diesem Repository.
|
||
Änderungen an `docker-compose*.yml` im Git-Repo wirken sich **nicht** automatisch
|
||
auf die laufende Installation aus – der Deploy-Schritt zieht ausschließlich neue
|
||
Images, die Compose-Datei selbst muss bei Bedarf separat auf dem Server aktualisiert
|
||
werden. Vor grösseren Änderungen an der Compose-Struktur die auf dem Server
|
||
tatsächlich liegende Datei prüfen, nicht blind von der Repo-Version ausgehen.
|
||
|
||
### Dateien (Nextcloud)
|
||
|
||
Das Modul „Dateien“ spricht aus dem `api`-Container mit der Nextcloud der Installation (Adresse im Modul unter „Einstellungen“). Dafür gilt:
|
||
|
||
- **Ausgehender Zugriff:** Der `api`-Container braucht Netzwerkzugriff auf die eingetragene Nextcloud (http oder https, auch interne Adressen). Tessera ruft nur feste Pfade dieser einen Adresse auf, folgt keiner Weiterleitung und schickt nie Cookies.
|
||
- **Zertifikate:** Das Zertifikat der Nextcloud wird immer geprüft, es gibt keinen Schalter, das abzustellen. Nutzt die Nextcloud ein Zertifikat einer eigenen (internen) Zertifizierungsstelle, legen Sie deren Zertifikat in den `api`-Container und setzen Sie `NODE_EXTRA_CA_CERTS=/pfad/zur/ca.pem` in dessen Umgebung (Compose-Datei des Servers, danach `up -d --force-recreate api`). Ohne das meldet „Verbindung prüfen“ im Modul, das Zertifikat ließe sich nicht prüfen.
|
||
- **Nginx Proxy Manager (Tessera-Adresse):** Beim Hochladen schickt der Browser große Dateien in Stücken von 8 MiB durch `/api-proxy`. Der Proxy vor Tessera muss das durchlassen: `client_max_body_size` mindestens `10m` (besser `64m`) und Lese- und Sendezeitlimits (`proxy_read_timeout`, `proxy_send_timeout`) von mindestens 120 Sekunden. Bei der Nginx-Voreinstellung von 1 MiB bricht jeder größere Upload schon beim ersten Stück ab.
|
||
- **Zusammenbau großer Dateien:** Nextcloud setzt die Stücke am Ende zu einer Datei zusammen. Dauert das länger als etwa 20 Sekunden, antwortet Tessera sofort und die Oberfläche fragt den Fortschritt ab; die Verbindung bleibt also nie lange offen. Unvollständige Uploads (Stücke ohne Abschluss, etwa nach einem Seitenwechsel) räumt Nextcloud nach 24 Stunden selbst auf.
|
||
- **Zustand im Arbeitsspeicher:** Offene Browser-Anmeldungen (Zwei-Faktor), laufende Zusammenbauten, die Aufrufsperre gegen die Brute-Force-Sperre der Nextcloud, die zwischengespeicherte Kennung der Nextcloud (10 Minuten) und noch ausstehende Widerrufe von App-Passwörtern liegen im Prozess des `api`-Containers. Ein Widerruf steht aus, wenn Nextcloud ihn gerade nicht annehmen konnte (Sperre nach „zu viele Anfragen“, Netzfehler, Wartung); Tessera versucht ihn nach dem Ende der Sperre bzw. nach einer Minute erneut, höchstens sechsmal. Ebenso beobachtet Tessera eine abgebrochene Browser-Anmeldung bis zu ihrem Ablauf und widerruft ein dort doch noch ausgestelltes App-Passwort sofort. Ein Neustart verwirft all das: Wer gerade eine Browser-Anmeldung offen hatte, startet sie neu; angemeldete Benutzer bleiben angemeldet (ihre Zugänge liegen verschlüsselt in der Datenbank). Ein dabei verlorener Widerruf bleibt als Gerät im Nextcloud-Konto des Benutzers stehen und lässt sich dort entfernen.
|
||
- **Teilen:** Tessera legt je Benutzer höchstens 10 neue Freigaben innerhalb von 10 Minuten an. Das liegt bewusst deutlich unter dem eigenen Limit der Nextcloud von 20 in 10 Minuten: Läge Tessera darüber, meldete die Nextcloud „zu viele Anfragen“, und Tessera hielte dann alle Anfragen an die Nextcloud für 15 Minuten an, für alle Benutzer. Der Zähler liegt im Arbeitsspeicher des `api`-Containers und beginnt nach einem Neustart bei null; die Zusicherung gilt also nur, solange der Container nicht neu startet. Freigaben, die jemand direkt in der Nextcloud anlegt, zählen gegen deren Limit von 20, ohne dass Tessera davon weiß; die Luft zwischen 10 und 20 ist dafür gedacht. Zusätzlich zählt Tessera jeden Versuch, eine Freigabe anzulegen, auch einen abgelehnten (zum Beispiel „gibt es schon“): mehr als 40 Versuche in 10 Minuten je Benutzer weist Tessera ab, ohne die Nextcloud zu fragen, damit wiederholte Fehlversuche sie nicht belasten. Die Regeln für Links (Passwort, Ablaufdatum, öffentliches Hochladen, Gruppen) liest Tessera bei jeder Aktion frisch aus der Nextcloud und speichert nichts davon; eine Änderung in der Nextcloud wirkt nach wenigen Sekunden ohne Neustart (die Nextcloud selbst hält ihre Angaben nach einer Änderung per `occ` kurz in einem Zwischenspeicher). Passwörter von Links speichert und protokolliert Tessera nie, und auch die Adressen der Links stehen in keinem Protokoll. Die Adresse eines Links baut die Nextcloud aus dem Namen, unter dem Tessera sie aufruft (Administrationshandbuch, Abschnitt „Dateien: Nextcloud anbinden“).
|
||
- **Brute-Force-Ausnahme in der Nextcloud:** Tragen Sie die Adresse des Tessera-Servers dort in die Ausnahmeliste ein (Administrationshandbuch, Abschnitt „Dateien: Nextcloud anbinden“). Sonst kann eine Reihe falscher Anmeldungen die Nextcloud für alle Benutzer gleichzeitig sperren.
|
||
- **Verschlüsselung:** Die gespeicherten App-Passwörter sind mit `TESSERA_ENCRYPTION_KEY` verschlüsselt (Kapitel 2). Ein anderer Schlüssel macht sie unlesbar; die Benutzer müssen sich dann neu verbinden.
|
||
|
||
### Zertifikatsmanager
|
||
|
||
Das Modul „Zertifikatsmanager“ braucht keine Einstellungen und keine eigene Konfiguration. Es speichert nichts: Zertifikate, Schlüssel und Passwörter kommen mit jeder Anfrage aus dem Browser, werden im Arbeitsspeicher des `api`-Containers verarbeitet und nicht in der Datenbank oder in Protokollen abgelegt. Für den Betrieb gilt:
|
||
|
||
- **Größe der Anfragen:** Die Dateien gehen über `/api-proxy` an die API. Eine einzelne Analyse schickt höchstens 10 MB (höchstens 30 Dateien, je Datei bis 5 MB). Die Voraussetzung am Proxy ist dieselbe wie im Abschnitt „Dateien (Nextcloud)“: `client_max_body_size` von mindestens `10m`. Beim Herunterladen eines Ergebnisses schickt der Browser nur Zertifikate und höchstens einen Schlüssel als JSON, höchstens 512 KiB je Anfrage (Tessera legt für genau diese Anfrage eine eigene Grenze fest, alle anderen JSON-Anfragen bleiben bei 100 kB).
|
||
- **Ausgehender Zugriff:** Der Zertifikatsmanager ruft von sich aus nichts im Internet ab. Eine Ausnahme gibt es: Klickt ein Benutzer auf „Fehlendes Zertifikat holen“, schickt der `api`-Container eine einzelne Anfrage an die Adresse, die im Zertifikat als Aussteller-Adresse steht (meist `http://`, selten `https://`). Dafür muss der `api`-Container ausgehend per HTTP und HTTPS (Ports 80 und 443) ins Internet kommen; andere Ports ruft Tessera nie ab. Ist das ausgehend gesperrt (Firewall, Proxy-Pflicht), meldet der Knopf „nicht erreichbar“, alles andere im Modul funktioniert weiter, und die Benutzer laden das Zertifikat selbst herunter. Tessera ruft dabei nur öffentliche Adressen ab (keine internen Rechner, keine Adressen des eigenen Netzes), prüft die Adresse im Moment des Verbindens noch einmal, folgt höchstens drei Weiterleitungen, begrenzt Wartezeit (8 Sekunden) und Antwortgröße (256 KiB) und übernimmt nur ein Zertifikat, das das betroffene Zertifikat wirklich ausgestellt hat. Im Protokoll steht bei einem Fehler genau eine Zeile mit dem Servernamen und einem Fehlercode, nie ein Zertifikat.
|
||
- **Rechenaufwand:** Passwortgeschützte PFX-Dateien und verschlüsselte Schlüssel öffnet Tessera mit den eingegebenen Passwörtern (je Datei höchstens zehn verschiedene Versuche); das kostet kurz Rechenzeit, belastet den Server aber nicht dauerhaft. Damit eine präparierte Datei die API nicht ausbremsen kann, gelten Grenzen je Anfrage: eine Passwortableitung höchstens eine Million Runden, alle zusammen höchstens sechs Millionen (mehr überspringt Tessera mit Hinweis), höchstens 200 Zertifikate, 50 Schlüssel und 50 Zertifikatsanfragen sowie 20 MiB für alle Dateien zusammen (Tessera bricht schon beim Empfang ab, mit der Meldung zu große Dateien, HTTP 413). ZIP-Dateien werden mit hartem Deckel entpackt (1 MiB je Eintrag, 20 MiB zusammen, höchstens 100 Einträge je ZIP und ein Kompressionsverhältnis von höchstens 100 : 1 je Eintrag); ein ZIP mit mehr Einträgen wird ganz abgelehnt, ein Eintrag mit auffälligem Verhältnis wird als verdächtig übersprungen und im Ergebnis gemeldet. Ordner, `__MACOSX`, versteckte Dateien (Name mit Punkt am Anfang), `Thumbs.db` und `desktop.ini` zählen nicht mit.
|
||
|
||
## 4. Neue Fassung einspielen
|
||
|
||
Das ist der wichtigste Ablauf im Tagesgeschäft. Zwei Befehle:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml pull
|
||
docker compose -f docker-compose.prod.yml up -d --force-recreate api web
|
||
```
|
||
|
||
**Verifizierte Falle:** `docker compose up -d` **ohne** `--force-recreate` ersetzt
|
||
einen bereits laufenden Container **nicht**, wenn Compose der Meinung ist, an der
|
||
Service-Definition habe sich nichts geändert – auch wenn `pull` gerade ein neues
|
||
Image unter demselben Etikett (`beta` bzw. `live`) heruntergeladen hat. Der alte Container läuft
|
||
dann unverändert mit dem alten Code weiter, ohne Fehlermeldung. Das Deployment
|
||
wirkt erfolgreich, ist es aber nicht. Dieser Fehler ist dem Team schon mehrfach
|
||
passiert. Deshalb: nach jedem `pull` **immer** `--force-recreate` verwenden (nur
|
||
`api` und `web` betrifft das – `db` soll normalerweise nicht neu erstellt werden,
|
||
sonst würde sie kurz neu starten).
|
||
|
||
**Kontrolle, ob es wirklich funktioniert hat:** Startzeitpunkt des Containers mit dem
|
||
Erstellungszeitpunkt des zugehörigen Images vergleichen. Der Container muss
|
||
**nach** dem Image erzeugt worden sein:
|
||
|
||
```bash
|
||
docker inspect -f '{{.State.StartedAt}}' tessera-api-1
|
||
docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/api:beta
|
||
|
||
docker inspect -f '{{.State.StartedAt}}' tessera-web-1
|
||
docker image inspect -f '{{.Created}}' git.vicolab.de/schalli/tessera-ctl/web:beta
|
||
```
|
||
|
||
(Auf dem Live-Server statt `:beta` jeweils `:live` einsetzen – das Etikett, das in
|
||
der `.env` als `IMAGE_TAG` steht, siehe Kapitel 9.)
|
||
|
||
(Container-Namen mit `docker compose -f docker-compose.prod.yml ps` prüfen, falls
|
||
sie auf dem Server abweichen.) Liegt `StartedAt` **vor** `Created` des Images, läuft
|
||
noch die alte Version – dann `--force-recreate` nachholen.
|
||
|
||
**Hinweis für die erste Version nach 1.3.1 – alte Bildspalte fällt weg:** Diese
|
||
Version entfernt die alte Spalte, in der die Bilder des Bilderrahmen-Widgets
|
||
früher in der Datenbank lagen (Migration `20260924120000_dashboard_image_drop_data`).
|
||
Die Bilder selbst liegen seit 1.3.1 im Volume `user-files`; 1.3.1 hat sie beim
|
||
ersten Start von selbst dorthin umgezogen. Hat ein Server 1.3.1 übersprungen, wäre
|
||
der Umzug dort nie gelaufen – dann bricht die Migration ab, **bevor** sie etwas
|
||
ändert, und der `api`-Container startet nicht. In `docker compose logs api` steht
|
||
dann die Meldung „DashboardImage: es gibt noch Zeilen ohne storagePath — Umzug
|
||
(quick-260922-hk4) zuerst mit einer Version >= 1.3.1 laufen lassen, dann erneut
|
||
deployen“. Es gehen dabei keine Bilder verloren. Abhilfe in drei Schritten:
|
||
|
||
1. Den abgebrochenen Versuch als zurückgenommen vermerken – sonst verweigert
|
||
auch 1.3.1 jeden Start, weil Prisma eine fehlgeschlagene Migration in der
|
||
Datenbank sieht (Fehler `P3009`):
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml exec db \
|
||
psql -U tessera -d tessera -c \
|
||
"UPDATE _prisma_migrations SET rolled_back_at = now() WHERE migration_name = '20260924120000_dashboard_image_drop_data' AND finished_at IS NULL;"
|
||
```
|
||
|
||
2. In der `.env` `IMAGE_TAG=v1.3.1` setzen, `pull` und `--force-recreate` wie
|
||
oben, den Start abwarten – der Umzug läuft dabei von selbst.
|
||
3. `IMAGE_TAG` zurück auf den Kanal (`live` bzw. `beta`) und erneut einspielen;
|
||
jetzt läuft die Migration durch.
|
||
|
||
(Dieser Ablauf ist am 24.09.2026 in einer Wegwerf-Datenbank vollständig
|
||
durchgespielt.)
|
||
|
||
## 5. Datenbank-Migrationen
|
||
|
||
Ein separater Migrationsschritt ist **nicht** nötig. Der `api`-Container führt
|
||
Prisma-Migrationen bei jedem Start automatisch aus, bevor der eigentliche Prozess
|
||
hochfährt. Der Startbefehl des Images (`apps/api/Dockerfile`, `CMD`) ruft dafür das
|
||
Skript `apps/api/scripts/migrate-and-start.sh` auf. Es arbeitet in drei Schritten:
|
||
|
||
1. Es prüft, dass `DATABASE_URL` gesetzt ist. Fehlt sie, bricht der Container sofort
|
||
mit der Meldung „FEHLER: DATABASE_URL ist nicht gesetzt.“ ab.
|
||
2. Es führt `prisma migrate deploy --schema apps/api/prisma/schema.prisma` aus. Als
|
||
Verbindung dient `TESSERA_MIGRATE_DATABASE_URL`, falls diese Variable gesetzt ist
|
||
(Kapitel 3); ist sie leer oder fehlt sie, nimmt das Skript `DATABASE_URL`.
|
||
3. Erst wenn die Migration erfolgreich war, startet es die API mit
|
||
`exec node apps/api/dist/main.js`. Die API selbst arbeitet immer mit
|
||
`DATABASE_URL`.
|
||
|
||
Das heisst: Sobald ein neues Image (mit neuen Migrationen im Ordner
|
||
`apps/api/prisma/migrations/`) per `pull` + `--force-recreate` eingespielt wird,
|
||
laufen ausstehende Migrationen beim nächsten Start automatisch. Schlägt eine
|
||
Migration fehl, startet die API (Schritt 3) erst gar nicht – der Container
|
||
bleibt dann im Neustart-Loop bzw. terminiert, sichtbar in `docker compose logs api`.
|
||
|
||
Kontrolle, ob eine Migration tatsächlich angekommen ist:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml exec db \
|
||
psql -U tessera -d tessera -c \
|
||
"SELECT migration_name, finished_at FROM _prisma_migrations ORDER BY finished_at DESC LIMIT 5;"
|
||
```
|
||
|
||
Der Name der zuletzt erwarteten Migration lässt sich mit dem Ordnernamen in
|
||
`apps/api/prisma/migrations/` abgleichen (Namensschema
|
||
`YYYYMMDDHHMMSS_beschreibung`).
|
||
|
||
## 6. Sicherung und Wiederherstellung
|
||
|
||
Es gibt aktuell **kein** eingebautes Backup-Skript und kein Makefile-Target dafür im
|
||
Repository. Sicherung erfolgt manuell über die Postgres-Bordmittel.
|
||
|
||
### Datenbank sichern
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml exec db \
|
||
pg_dump -U tessera -d tessera -F c -f /tmp/tessera-$(date +%Y%m%d).dump
|
||
docker compose -f docker-compose.prod.yml cp db:/tmp/tessera-$(date +%Y%m%d).dump ./
|
||
```
|
||
|
||
### Datenbank wiederherstellen
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml cp ./tessera-YYYYMMDD.dump db:/tmp/restore.dump
|
||
docker compose -f docker-compose.prod.yml exec db \
|
||
pg_restore -U tessera -d tessera --clean --if-exists /tmp/restore.dump
|
||
```
|
||
|
||
Vor einer Wiederherstellung `api` stoppen, damit keine Schreibzugriffe während des
|
||
Restores stattfinden.
|
||
|
||
### Was sonst noch an Zustand existiert
|
||
|
||
- **PostgreSQL-Daten**: liegen im benannten Docker-Volume `pgdata`
|
||
(`docker-compose.prod.yml`, Mount `pgdata:/var/lib/postgresql/data` im
|
||
`db`-Container). `pgdata` ist eines von zwei dauerhaften Docker-Volumes (siehe
|
||
nächster Punkt) und kann zusätzlich über ein `docker volume`-Backup gesichert
|
||
werden.
|
||
- **Verschlüsselungsschlüssel** `TESSERA_ENCRYPTION_KEY`: liegt nur in `.env` auf
|
||
dem Host, **nicht** im Datenbank-Dump. Getrennt sichern (siehe Kapitel 2/3) – ohne
|
||
ihn sind alle per `pg_dump` gesicherten verschlüsselten Zugangsdaten wertlos.
|
||
- **Hochgeladene Dateien** (Avatare unter `user-files/avatars/`, Bilder des
|
||
Bilderrahmen-Widgets unter `user-files/dashboard-images/<Benutzerkennung>/`,
|
||
Symbole des Favoriten-Widgets unter
|
||
`user-files/favorite-icons/<Benutzerkennung>/`, generierte DKV-Exporte unter
|
||
`user-files/`, siehe
|
||
`apps/api/src/user/user.controller.ts`,
|
||
`apps/api/src/dashboard/dashboard-images.service.ts`,
|
||
`apps/api/src/favorites/favorites.service.ts` und
|
||
`apps/api/src/dkv/dkv-export.service.ts`): Diese Dateien liegen im benannten
|
||
Docker-Volume `user-files`, gemountet auf `/app/user-files` im Dienst `api`. Der
|
||
Mount ist in `docker-compose.yml` und `docker-compose.prod.yml` eingetragen:
|
||
|
||
```yaml
|
||
# beim Dienst api:
|
||
- user-files:/app/user-files
|
||
# im Top-Level-Block volumes:
|
||
user-files:
|
||
```
|
||
|
||
Damit überstehen Avatare, Bilderrahmen-Bilder und DKV-Exporte ein
|
||
`--force-recreate` von `api`. Seit den Bilderrahmen-Bildern (Version nach 1.3.0)
|
||
gehört dieses Volume zwingend zur Sicherung: `pg_dump` allein enthält diese
|
||
Bilder nicht mehr — genau das ist der Zweck der Umstellung, der
|
||
Datenbank-Abzug bleibt dadurch klein. Wie bei
|
||
`pgdata` zeigt `docker volume ls` das Volume mit vorangestelltem Projektnamen an
|
||
(`<projekt>_user-files`). Gesichert werden die Dateien weiterhin mit
|
||
`docker compose cp api:/app/user-files ./user-files-backup`, alternativ über eine
|
||
Sicherung des Docker-Volumes selbst (wie bei `pgdata`).
|
||
|
||
**Wichtig für den Betrieb auf einem bestehenden Server:** `/opt/tessera/docker-compose.yml`
|
||
ist keine Arbeitskopie dieses Repositorys – die Datei wurde dort von Hand
|
||
bearbeitet und weicht ab. Ein Deploy holt ausschließlich Images und fasst diese
|
||
Datei nicht an. Diese Compose-Änderung erreicht eine bereits laufende Installation
|
||
deshalb **nicht von selbst**. Wer die Reparatur dort haben will, trägt dieselben
|
||
zwei Zeilen selbst in `/opt/tessera/docker-compose.yml` ein (vorher sichern) und
|
||
erstellt die Container einmal neu. Bis dahin gilt für die laufende Installation
|
||
weiterhin der alte, verlustbehaftete Zustand – prüfbar mit `docker inspect
|
||
tessera-api-1` und einem Blick auf `Mounts`.
|
||
|
||
## 7. Protokolle und Fehlersuche
|
||
|
||
Logs ansehen:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml logs -f api
|
||
docker compose -f docker-compose.prod.yml logs -f web
|
||
docker compose -f docker-compose.prod.yml logs -f db
|
||
```
|
||
|
||
**Gesunder Start sieht so aus:** `db` wird `healthy`, danach startet `api` und
|
||
protokolliert die durchgelaufenen Prisma-Migrationen sowie zuletzt
|
||
`Tessera API running on port 3001` und direkt darunter eine Zeile wie
|
||
`Tessera API v1.0.0 (live) abc1234` (beides aus `apps/api/src/main.ts`) – Version,
|
||
Kanal und Kurzkennung des Standes, der gerade läuft (siehe Kapitel 9). Erst danach
|
||
startet `web`, weil `depends_on: api: condition: service_healthy` das erzwingt.
|
||
|
||
| Symptom | Wahrscheinliche Ursache | Prüfen / Beheben |
|
||
|---|---|---|
|
||
| `docker compose up` bricht sofort ab, ohne dass ein Container startet, mit einer Meldung zu `TESSERA_ENCRYPTION_KEY` | Variable fehlt in `.env` – Compose selbst verweigert die Variablen-Interpolation (`${TESSERA_ENCRYPTION_KEY:?...}`) | `TESSERA_ENCRYPTION_KEY` setzen (siehe Kapitel 2), danach erneut starten. |
|
||
| `api`-Container startet und beendet sich sofort wieder, Log zeigt `TESSERA_ENCRYPTION_KEY is not set` oder `must be a 64-character hex string` | Key ist zwar irgendwo gesetzt, aber leer oder falsch lang | Mit `openssl rand -hex 32` neu erzeugen, Länge prüfen (64 Zeichen). |
|
||
| `api`-Container beendet sich sofort, Log zeigt `FEHLER: DATABASE_URL ist nicht gesetzt.` | `DATABASE_URL` fehlt oder ist leer; das Startskript `apps/api/scripts/migrate-and-start.sh` bricht ab, bevor Prisma sich verbindet (Kapitel 5) | `DATABASE_URL` in der `.env` setzen (Form siehe Kapitel 3) und den `api`-Container neu erstellen (`--force-recreate api`). |
|
||
| `api` wird nie `healthy`, Log zeigt Verbindungsfehler von Prisma/PostgreSQL | `db` ist noch nicht bereit, `DATABASE_URL` falsch, oder `DB_PASSWORD`/`DATABASE_URL` passen nicht zusammen | `docker compose ps` – ist `db` `healthy`? `DATABASE_URL` gegen `DB_PASSWORD` abgleichen. |
|
||
| Login funktioniert nicht, obwohl `api` und `web` laufen | `JWT_SECRET` bei `web` und `api` unterschiedlich, z. B. nach halbherzigem Neustart nur eines Containers | Beide Container mit demselben `JWT_SECRET` neu erstellen (`--force-recreate api web`). |
|
||
| Neue Version scheint nicht anzukommen, obwohl `pull` gelaufen ist | Klassische `up -d`-Falle ohne `--force-recreate` (siehe Kapitel 4) | `StartedAt` des Containers gegen `Created` des Images vergleichen, ggf. `--force-recreate` nachholen. |
|
||
| Initialer Admin-Login funktioniert nicht nach Änderung von `TESSERA_ADMIN_PASSWORD` | Seed läuft nur, wenn der Benutzername noch **nicht** existiert; bestehende Accounts werden nicht überschrieben | Passwort über die Anwendung selbst (bzw. direkt in der Datenbank) ändern, nicht über die `.env`-Variable. |
|
||
| Mails werden nicht versendet | `TESSERA_SMTP_HOST` leer (Prod-Default) | SMTP-Variablen vollständig setzen und Container neu erstellen. |
|
||
| Fehlermeldungen der Anwender kommen nicht an | Kein Postfach gesetzt (weder „Fehlermeldungen an“ unter Administration → E-Mail-Versand (SMTP) noch `TESSERA_BUGREPORT_TO`), oder der SMTP-Versand des Mandanten scheitert | Feld „Fehlermeldungen an“ (Administration → E-Mail-Versand (SMTP)) oder `TESSERA_BUGREPORT_TO` prüfen; API-Log nach `Bug report` durchsuchen (eine Zeile je gesendeter Meldung mit dem Herkunfts-Kürzel `[Browser]`, `[Desktop/Windows]` oder `[Desktop/Linux]`, `Bug report mail failed` bei Versandfehler). |
|
||
| Avatare/DKV-Exporte nach einem Deploy verschwunden | Die verwendete Compose-Datei mountet `user-files/` nicht als Volume – im Repository-Stand seit dieser Version behoben, betrifft nur eine Installation mit abweichender Compose-Datei | Die zwei Zeilen aus Kapitel 6 in die verwendete Compose-Datei eintragen (auf dem Server: `/opt/tessera/docker-compose.yml`, vorher sichern) und `api` neu erstellen. |
|
||
|
||
## 8. Abgrenzung zur CI/CD-Pipeline
|
||
|
||
Dieses Dokument beschreibt den manuellen Betrieb: Erstinstallation, Redeploy,
|
||
Migrations-Kontrolle, Backup und Fehlersuche auf einer bereits eingerichteten
|
||
Installation.
|
||
|
||
Wie die Gitea-Actions-Pipeline (Runner-Registrierung, `.gitea/workflows/ci.yml`,
|
||
Berechtigungen des `act_runner`-Docker-Socket-Mounts) aufgesetzt wird, ist bewusst
|
||
nicht Teil dieses Dokuments – das steht vollständig in
|
||
[`docs/ci-cd-setup.md`](./ci-cd-setup.md). Die Grenze zwischen beiden Dokumenten:
|
||
Sobald ein neues Image lokal vorliegt oder in einer Registry verfügbar ist, beginnt
|
||
dieses Betriebshandbuch (Kapitel 4 und 9); alles davor – wie das Image entsteht –
|
||
gehört in das CI/CD-Runbook.
|
||
|
||
**Automatische Sicherheitsprüfung.** Nach dem Bau der Images läuft in der Pipeline
|
||
ein weiterer Arbeitsschritt namens „Sicherheitsprüfung (nur Bericht)“. Er prüft
|
||
den eingecheckten Quelltext, die gesamte Änderungsgeschichte und die frisch
|
||
gebauten Images auf Zugangsdaten im Quelltext und auf bekannte Schwachstellen in
|
||
den verwendeten Bausteinen. Er meldet nur und hält nie etwas an: Ein Fund macht
|
||
weder den Bau noch eine Veröffentlichung rot, und der Schritt bekommt keine
|
||
Geheimnisse der Pipeline. Die Zahlen stehen im Protokoll des Laufs in den Zeilen,
|
||
die mit `SECURITY-SUMMARY` beginnen; die Rohberichte hängen (sofern der Server das
|
||
zulässt) als Download `sicherheitsberichte` am Lauf. Aufbau und Fehlersuche stehen
|
||
im [CI/CD-Runbook](./ci-cd-setup.md), die Ergebnisse und ihre Einordnung im
|
||
[Sicherheitsprotokoll](./sicherheitsprotokoll.md).
|
||
|
||
## 9. Zwei Kanäle: Live und Beta
|
||
|
||
Seit September 2026 gibt es Tessera in zwei Ausgaben, die getrennt voneinander
|
||
laufen. Dieses Kapitel erklärt, was das bedeutet, welche Zeile auf welchem Server
|
||
stehen muss, wie eine Version freigegeben wird, wie ein dringender Fehler auf Live
|
||
behoben wird, und wie Sie jederzeit sehen, welche Fassung gerade läuft.
|
||
|
||
### Was ein Kanal ist
|
||
|
||
Ein Kanal ist eine Ausgabe von Tessera, die auf einem bestimmten Server läuft und
|
||
nach eigenen Regeln neue Stände bekommt. Es gibt zwei:
|
||
|
||
- **Beta** – alles Neue, sofort nach jeder Änderung. Läuft unter
|
||
`alpha.tessera.ctl.de`. Das Etikett (die Kennzeichnung des Docker-Images in der
|
||
Registry) heißt `beta`. Das ältere Etikett `latest` ist nur ein zweiter Name für
|
||
genau dasselbe Beta-Image; es bleibt vorerst bestehen, damit nichts kaputtgeht,
|
||
und kann später wegfallen.
|
||
- **Live** – nur freigegebene Versionen mit einer Nummer. Läuft unter
|
||
`tessera.ctl.de` auf dem neuen Server. Das Etikett heißt `live`; zusätzlich trägt
|
||
jede freigegebene Version ihre Nummer als eigenes Etikett (`v1.0.0`, `v1.0.1`, …),
|
||
damit man jederzeit auch einen älteren Stand gezielt holen kann.
|
||
|
||
Die Versionsnummer kommt aus der Freigabe (in Git heißt das „Tag“ – eine Markierung
|
||
an einem bestimmten Stand), nicht aus einer Datei im Code. Zwischen zwei Freigaben
|
||
zeigt die Beta eine Kennung wie `v1.0.0-12-gabc1234`: das bedeutet „12 Änderungen
|
||
nach Version 1.0.0, Stand abc1234“. Vor der allerersten Freigabe steht dort nur die
|
||
Kurzkennung des Standes (sieben Zeichen, z. B. `abc1234`).
|
||
|
||
### Die eine Zeile je Server
|
||
|
||
Welchen Kanal ein Server bekommt, entscheidet **eine einzige Zeile** in der Datei
|
||
`/opt/tessera/.env`:
|
||
|
||
- auf **alpha** (Beta): `IMAGE_TAG=beta`
|
||
- auf dem **neuen Live-Server**: `IMAGE_TAG=live`
|
||
|
||
Fehlt die Zeile ganz, nimmt die Compose-Datei von selbst `beta`. Für den Live-Server
|
||
ist die Zeile also Pflicht, sonst zieht er die Beta.
|
||
|
||
Weil `/opt/tessera` keine Arbeitskopie des Repositorys ist (siehe Kapitel 3,
|
||
„Konfigurationsdrift“), muss die Compose-Datei auf dem Server einmal von Hand
|
||
angepasst werden. Auf alpha ist das die Datei `/opt/tessera/docker-compose.prod.yml`
|
||
(die `.env` dort verweist mit `COMPOSE_FILE` auf sie). Zuerst eine Sicherung:
|
||
|
||
```bash
|
||
cd /opt/tessera
|
||
cp docker-compose.prod.yml docker-compose.prod.yml.bak.$(date +%Y%m%d)
|
||
```
|
||
|
||
Dann die zwei `image:`-Zeilen (eine beim Dienst `web`, eine beim Dienst `api`) auf
|
||
diese Form bringen – der einzige Unterschied zu heute ist das Ende der Zeile:
|
||
|
||
```yaml
|
||
image: git.vicolab.de/schalli/tessera-ctl/web:${IMAGE_TAG:-beta}
|
||
```
|
||
|
||
```yaml
|
||
image: git.vicolab.de/schalli/tessera-ctl/api:${IMAGE_TAG:-beta}
|
||
```
|
||
|
||
Danach wie in Kapitel 4:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml pull
|
||
docker compose -f docker-compose.prod.yml up -d --force-recreate api web
|
||
```
|
||
|
||
Hinweis: Die Vorlage `.env.prod.example` im Repository enthält die Zeilen
|
||
`IMAGE_TAG=live` und `COMPOSE_FILE=docker-compose.prod.yml` bereits. Wer eine neue
|
||
`.env` aus der Vorlage anlegt, setzt `IMAGE_TAG` nur noch auf den gewünschten
|
||
Kanal; auf einer älteren, von Hand gepflegten `.env` (wie auf alpha) werden die
|
||
Zeilen einmal ergänzt.
|
||
|
||
### Eine Version freigeben
|
||
|
||
Das Freigeben erledigt Claude; Sie sagen nur „Version X freigeben“. Zur Einordnung,
|
||
was dabei passiert:
|
||
|
||
1. **Änderungsliste abschließen:** Zuerst wird geprüft, ob jedes seit dem letzten
|
||
Tag geänderte Modul einen unveröffentlichten Eintrag in seinem Modul-Changelog
|
||
hat (Prüfbefehl und Regeln: Entwicklungsanleitung, Abschnitt „Modulversion und
|
||
Modul-Changelog pflegen“); die unveröffentlichten Modul-Einträge bekommen das
|
||
Freigabedatum. Dann wird in `CHANGELOG.md` der Abschnitt „Unveröffentlicht“ in
|
||
„X.Y.Z – JJJJ-MM-TT“ umbenannt, darüber ein neues, leeres „Unveröffentlicht“
|
||
angelegt, und das Ganze auf `main` committet und gepusht.
|
||
2. **Prüfung von außen:** Sobald alpha den Beta-Stand zeigt, der freigegeben
|
||
werden soll (die Versionszeile unter `/health/version`, siehe unten, muss
|
||
stimmen), führt Claude vom Entwicklungsrechner aus die passive Grundprüfung
|
||
aus: `sh .gitea/scripts/zap-baseline.sh`. Sie sieht sich alpha an wie ein
|
||
Besucher ohne Konto, füllt keine Formulare aus und greift nichts an. Als Ziel
|
||
gilt die öffentliche Adresse von alpha. Ist sie vom Entwicklungsrechner nicht
|
||
erreichbar, wird mit `ZAP_TARGET` die direkte Adresse der Anwendung auf dem
|
||
Testserver angegeben; das Ergebnis vermerkt dann, dass der vorgeschaltete Proxy
|
||
nicht mitgeprüft wurde. Der Zugangsschutz (Basic Auth) vor alpha bleibt
|
||
unverändert bestehen. Das Ergebnis trägt Claude im
|
||
[Sicherheitsprotokoll](sicherheitsprotokoll.md) ein (Eintrag unter „Verlauf“,
|
||
neue Befunde in „Einordnung der Befunde“). Ein neuer Befund der Stufe „Hoch“
|
||
wird vor der Freigabe behoben oder im Protokoll begründet.
|
||
3. **Zusammenführen und taggen:** Erst dann wird zusammengeführt und getaggt:
|
||
|
||
```bash
|
||
git checkout live
|
||
git merge --ff-only main
|
||
git tag -a vX.Y.Z -m "Tessera X.Y.Z"
|
||
git push origin live vX.Y.Z
|
||
```
|
||
|
||
Der zweite Befehl übernimmt den Stand der Beta in den Live-Zweig. Wenn er sich
|
||
weigert, ist eine frühere Korrektur (siehe Hotfix, Schritt 5) noch nicht zurück in
|
||
`main` – dann wird erst das nachgeholt. Der Push löst die Pipeline zweimal aus: der
|
||
Zweig `live` wird nur geprüft, der Tag `vX.Y.Z` wird gebaut und als `live` und
|
||
`vX.Y.Z` abgelegt. Das dauert etwa vier bis sechs Minuten.
|
||
|
||
Derselbe Tag baut zusätzlich die Desktop-Pakete (Windows-Installer und
|
||
Linux-AppImage) und hängt beide als Dateien an denselben Release an – Details
|
||
dazu in [Kapitel 10](#10-desktop-app-pakete-und-release-dateien).
|
||
|
||
Beim Tag legt die Pipeline zusätzlich einen **Release in Gitea** an: Name
|
||
„Tessera X.Y.Z”, Text ist der Abschnitt dieser Version aus `CHANGELOG.md`. Sie
|
||
finden ihn im Repository unter „Releases”. Fehlt der Abschnitt in der
|
||
Änderungsliste, schlägt genau dieser letzte Schritt fehl – die Abbilder sind dann
|
||
trotzdem gebaut und abgelegt. Der Release wird nachgeholt, sobald der Abschnitt
|
||
nachgetragen ist: entweder durch erneutes Auslösen des Tag-Laufs oder lokal per
|
||
Skript (`.gitea/scripts/publish-release.sh --tag vX.Y.Z`).
|
||
|
||
Danach spielen Sie die Version auf dem Live-Server ein – Kapitel 4 gilt unverändert:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml pull
|
||
docker compose -f docker-compose.prod.yml up -d --force-recreate api web
|
||
```
|
||
|
||
**Erstfreigabe v1.0.0:** Die erste Freigabe ist erfolgt. Der Zweig `live`
|
||
entstand am 2026-09-14 aus `main` (`git checkout -b live main`), bekam den Tag
|
||
`v1.0.0` und wurde zusammen mit dem Tag gepusht; seit 2026-09-15 läuft diese
|
||
Version auf dem Live-Server. Der Release „Tessera 1.0.0“ in Gitea wurde
|
||
nachträglich mit dem Skript angelegt, weil die Änderungsliste erst danach
|
||
eingeführt wurde.
|
||
|
||
### Einen Fehler auf Live beheben (Hotfix)
|
||
|
||
Ein Hotfix ist eine kleine Korrektur, die auf Live landen muss, **ohne** die
|
||
Neuerungen der Beta mitzunehmen. Ablauf (Claude führt die Git-Schritte aus, Sie
|
||
spielen ein):
|
||
|
||
1. Den Live-Stand holen: `git checkout live && git pull`.
|
||
2. Einen Korrekturzweig `hotfix/<kurzer-name>` von `live` anlegen.
|
||
3. Die Korrektur machen und die Tests laufen lassen.
|
||
4. Nach `live` mergen, die nächste Nummer vergeben (`vX.Y.(Z+1)`, also z. B.
|
||
`v1.0.1` nach `v1.0.0`) und beides pushen: `git push origin live vX.Y.(Z+1)`.
|
||
Danach spielen Sie die Version auf dem Live-Server ein (Befehle wie oben).
|
||
5. Die Korrektur in die Beta übernehmen: `git checkout main && git merge live`.
|
||
Vorher wird geprüft, ob die Korrektur dort noch zusammenpasst (Konflikte, Tests),
|
||
dann `git push` – die Beta bekommt sie mit dem nächsten Pipeline-Lauf.
|
||
|
||
**Keine Datenbankänderung als Hotfix.** Der Grund in Alltagssprache:
|
||
Datenbankänderungen (Migrationen) tragen einen Zeitstempel im Namen und werden in
|
||
dieser Reihenfolge ausgeführt. Die Beta hat womöglich schon neuere Änderungen
|
||
eingespielt. Eine Hotfix-Änderung mit noch späterem Zeitstempel landet beim
|
||
Übernehmen in die Beta hinter Änderungen, die sie eigentlich nicht kennt – das ist
|
||
der eine Fall, der beim Zusammenführen still kaputtgehen kann. Braucht eine Korrektur
|
||
eine Datenbankänderung, wird sie als reguläre Version über `main` freigegeben.
|
||
|
||
### Woran Sie erkennen, welche Version läuft
|
||
|
||
Vier Wege, vom einfachsten zum genauesten:
|
||
|
||
1. **In der Oberfläche:** Unten in der Seitenleiste steht `v1.0.0 · Live` bzw.
|
||
`v1.0.0-12-gabc1234 · Beta`. Wenn Sie die Maus darüber halten, erscheinen die
|
||
Kurzkennung des Standes und die Version, die der Server meldet. Weichen
|
||
Oberfläche und Server voneinander ab, wurde nur einer der beiden Container neu
|
||
erstellt – dann Kapitel 4 anwenden (`--force-recreate api web`).
|
||
2. **Auf dem Server per Abfrage:**
|
||
|
||
```bash
|
||
curl -s http://localhost:3001/health/version
|
||
```
|
||
|
||
Die Antwort enthält die Felder `version`, `channel` (`beta` oder `live`),
|
||
`commit` (Kurzkennung) und `buildTime` (wann das Image gebaut wurde).
|
||
Diese Angaben stammen aus den Umgebungsvariablen `APP_VERSION`, `APP_CHANNEL`,
|
||
`APP_COMMIT` und `APP_BUILD_TIME`, die die Pipeline beim Bau des Images fest
|
||
einträgt (`.gitea/scripts/publish-images.sh`); sie werden nicht über die `.env`
|
||
gesetzt. Ein Image, das jemand von Hand ohne diese Angaben gebaut hat, meldet
|
||
deshalb `dev` als Version und Kanal und leere Felder für Kennung und Bauzeit.
|
||
3. **Im Protokoll:**
|
||
|
||
```bash
|
||
docker compose -f docker-compose.prod.yml logs api | grep "Tessera API"
|
||
```
|
||
|
||
Zeigt die Startzeile `Tessera API v1.0.0 (live) abc1234` (siehe Kapitel 7).
|
||
4. **Was sich geändert hat:** Ein Klick auf die Versionszeile unten in der
|
||
Seitenleiste öffnet die Seite „Was ist neu“ mit der Änderungsliste. Auf Live
|
||
sehen Sie nur freigegebene Versionen; auf der Beta steht zusätzlich der
|
||
Abschnitt „Noch nicht freigegeben (Beta)“ mit dem, was seit der letzten
|
||
Freigabe dazugekommen ist.
|
||
|
||
### Den neuen Live-Server einrichten
|
||
|
||
Kapitel 2 gilt vollständig. Die Abweichungen gegenüber alpha:
|
||
|
||
- In der `.env` steht `IMAGE_TAG=live`.
|
||
- Eigene, neu erzeugte Geheimnisse: `JWT_SECRET`, `TESSERA_ENCRYPTION_KEY`,
|
||
`DB_PASSWORD` und das Admin-Passwort. Nichts davon von alpha übernehmen.
|
||
- Eine eigene, leere Datenbank. Die API legt beim ersten Start den ersten Admin an
|
||
(Kapitel 2, Schritt 4). Die alpha-Datenbank wird **nicht** kopiert – es sei denn,
|
||
das wird ausdrücklich gewünscht. In diesem Fall gilt Kapitel 6 (Wiederherstellung)
|
||
**und** der Live-Server muss denselben `TESSERA_ENCRYPTION_KEY` wie alpha
|
||
bekommen, sonst sind alle gespeicherten Zugangsdaten unbrauchbar.
|
||
- `APP_URL=https://tessera.ctl.de`.
|
||
- Der erste `pull` holt das Etikett `live`. Vor der Erstfreigabe v1.0.0 gibt es
|
||
dieses Etikett noch nicht – deshalb erst freigeben, dann installieren. Für einen
|
||
Probelauf davor kann vorübergehend `IMAGE_TAG=beta` stehen; danach auf `live`
|
||
umstellen und `pull` + `up -d --force-recreate api web` wiederholen.
|
||
|
||
## 10. Desktop-App: Pakete und Release-Dateien
|
||
|
||
Seit September 2026 gibt es Tessera zusätzlich als Desktop-App für Windows und
|
||
Linux. Dieses Kapitel beschreibt, woher die Pakete kommen, wo sie liegen und
|
||
wie Sie Fehlerbilder rund um den Download einordnen. Die Anwendersicht (Download,
|
||
Installation, SmartScreen-Hinweis, Bedienung) steht in
|
||
`docs/anleitung-anwender.md`, Kapitel „Desktop-App".
|
||
|
||
### Woher die Pakete kommen
|
||
|
||
Der CI-Job `desktop` läuft nach `test` und vor `publish` – auf Push nach `main`
|
||
und bei jedem Freigabe-Tag `v*`. In diesem einen Job entstehen auf dem
|
||
Linux-Runner sowohl das Linux-AppImage als auch der Windows-Installer per
|
||
Cross-Bau (`cargo-xwin` + NSIS aus dem Ubuntu-Paket, kein Windows-Rechner in
|
||
der Pipeline). Einzelheiten zur Werkzeugkette stehen in
|
||
[`docs/ci-cd-setup.md`](./ci-cd-setup.md), Abschnitt 4.
|
||
|
||
Der Rust-Bau ist auf vier parallele Prozesse begrenzt (`CARGO_BUILD_JOBS`),
|
||
weil sich der Runner den Rechner mit Gitea und dem Entwicklungs-Stack teilt;
|
||
mit acht Prozessen geriet ein Rechner mit 15 GB Arbeitsspeicher an die Grenze.
|
||
Der Job dauert damit etwa fünf bis sieben Minuten (mit warmem Zwischenspeicher),
|
||
der erste Lauf nach einer Änderung der Abhängigkeiten deutlich länger – sofern überhaupt gebaut wird, siehe nächster Abschnitt.
|
||
|
||
### Wann gebaut wird und wann Pakete übernommen werden
|
||
|
||
Seit September 2026 baut die Pipeline die Desktop-Pakete auf dem Beta-Kanal
|
||
nur noch, wenn sich an der Desktop-App etwas geändert hat. Maßgeblich ist ein
|
||
Stempel aus der Versionsnummer des letzten Freigabe-Tags und dem letzten
|
||
Commit an den Desktop-Pfaden (`apps/desktop/`, die Skripte
|
||
`desktop-version.sh`, `desktop-collect.sh`, `desktop-stamp.sh`, die
|
||
Workflow-Datei `ci.yml`). Liegen zu diesem Stempel fertige Pakete im
|
||
Zwischenspeicher des Runners, übernimmt der Job sie unverändert; die
|
||
Bau-Schritte entfallen, und der Job braucht dann unter einer Minute. Im
|
||
Protokoll steht dann eine Zeile wie „Desktop unveraendert seit
|
||
<Commit>: Pakete … aus dem Zwischenspeicher".
|
||
|
||
Drei Regeln dazu:
|
||
|
||
- Freigabe-Tags bauen immer – die Release-Dateien entstehen frisch mit reiner
|
||
Versionsnummer.
|
||
- Nach einer Freigabe wird einmal neu gebaut, auch ohne Änderung an der
|
||
Desktop-App, weil die Versionsnummer zum Stempel gehört; die Beta-Pakete
|
||
tragen danach die neue Basisversion.
|
||
- Übernommene Pakete tragen den Stand ihres Baus – Dateiname (`-beta.<Commit>`)
|
||
und Manifest nennen den Commit des Baus, nicht den des aktuellen Abbilds.
|
||
Das ist gewollt: ein Client dieses Standes bekommt keinen unnötigen Hinweis
|
||
auf einen neuen Beta-Stand, ein älterer Client weiterhin.
|
||
|
||
Fehlt der Eintrag im Zwischenspeicher (der Runner räumt ungenutzte Einträge
|
||
nach einigen Tagen, alte nach etwa einem Monat weg) oder ist er unvollständig,
|
||
wird ganz normal gebaut – die Pipeline prüft vor der Übernahme Manifest,
|
||
Kanal, Version, Dateinamen, Größen und Prüfsummen.
|
||
|
||
### Wo die Pakete im Abbild liegen
|
||
|
||
`publish` kopiert die fertigen Pakete in das API-Abbild nach
|
||
`/app/desktop-dist/`, zusammen mit einer `manifest.json` (Version, Kanal,
|
||
Dateinamen, Größen, Prüfsummen; seit der Update-Funktion zusätzlich
|
||
`updateVersion` – die Form, die der Client vergleicht, `X.Y.Z` auf Live und
|
||
`X.Y.Z-beta.g{commit}` auf Beta – sowie je Plattform die `signature` des
|
||
Pakets). Auf dem Beta-Kanal tragen die Dateinamen
|
||
zusätzlich den Suffix `-beta.{commit}`, zum Beispiel
|
||
`Tessera-Setup-1.1.0-beta.742fb5c.exe` und
|
||
`Tessera-1.1.0-beta.742fb5c.AppImage`; auf Live steht dort die reine Form
|
||
`Tessera-Setup-X.Y.Z.exe` / `Tessera-X.Y.Z.AppImage`.
|
||
|
||
Kontrolle auf dem Server:
|
||
|
||
```bash
|
||
docker compose exec api ls -l /app/desktop-dist
|
||
curl -s https://{ihre-adresse}/api-proxy/desktop/latest
|
||
curl -si "https://{ihre-adresse}/api-proxy/desktop/update?target=windows&arch=x86_64¤t=0.0.0&base=https://{ihre-adresse}"
|
||
```
|
||
|
||
`ls -l` zeigt die abgelegten Dateien samt `manifest.json`; die erste
|
||
`curl`-Abfrage liefert dieselben Angaben als JSON (Version, Kanal, je
|
||
Plattform Dateiname, Größe, Prüfsumme und relative Download-Adresse) – das
|
||
ist genau die Antwort, die auch die Anmeldeseite und die Einstellungsseite
|
||
auswerten. Antwortet die Abfrage mit `404`, fehlt entweder das Verzeichnis
|
||
oder das Manifest; die Web-Oberfläche blendet den Download-Link dann
|
||
automatisch aus.
|
||
|
||
Die zweite Kontrollzeile stellt die Frage, die der Desktop-Client beim Start
|
||
stellt: `200` mit `version`, `url` und `signature` bedeutet, dass sich
|
||
installierte Clients von diesem Server aktualisieren können; `204` bedeutet,
|
||
dass für diese Plattform kein signiertes Paket vorliegt (zum Beispiel ein Stand
|
||
vor September 2026 oder ein Bau ohne Schlüssel). Für Linux `target=linux`
|
||
einsetzen.
|
||
|
||
### Release-Dateien in Gitea
|
||
|
||
Bei einem Freigabe-Tag hängt die Pipeline zusätzlich beide Dateien aus dem
|
||
Manifest als Anhänge an den Gitea-Release desselben Tags (Kapitel 9, „Eine
|
||
Version freigeben") – idempotent: ein erneuter Lauf ersetzt eine bereits
|
||
vorhandene Datei gleichen Namens, statt einen zweiten Anhang anzulegen. Die am
|
||
Release hinterlegte Datei ist byteidentisch mit der im Abbild ausgelieferten;
|
||
die Prüfsumme (`sha256`) aus `manifest.json` gilt für beide gleichermaßen.
|
||
|
||
### Updates in der App und der Signierschlüssel
|
||
|
||
Seit September 2026 aktualisiert sich die Desktop-App per Klick im Menü des
|
||
Infobereich-Symbols. Beim Start fragt sie `GET /api-proxy/desktop/update`
|
||
und installiert ausschließlich Pakete, deren Signatur zu dem im Client
|
||
hinterlegten öffentlichen Schlüssel passt – ein manipuliertes oder fremdes
|
||
Paket wird abgelehnt, bevor irgendetwas installiert wird. Das ist die
|
||
Vertrauensbasis der Update-Funktion, nicht die Prüfsumme im Manifest.
|
||
|
||
Der **private Schlüssel** liegt nicht im Repository. Er existiert an zwei
|
||
Stellen:
|
||
|
||
- als Gitea-Secrets `TAURI_SIGNING_PRIVATE_KEY` und
|
||
`TAURI_SIGNING_PRIVATE_KEY_PASSWORD` (Repository → Einstellungen → Actions →
|
||
Secrets); nur die beiden `tauri build`-Schritte des Jobs `desktop` sehen
|
||
sie, die Skripte kennen den Schlüssel nicht;
|
||
- als Sicherung auf dem Entwicklungsrechner unter
|
||
`~/.tessera/desktop-updater/` (`tessera-updater.key`, `password.txt` und
|
||
der öffentliche Teil `tessera-updater.key.pub`).
|
||
|
||
**Sicherung:** Legen Sie die beiden Dateien zusätzlich an einem zweiten
|
||
sicheren Ort ab. Geht der private Schlüssel verloren, können bereits
|
||
installierte Clients kein Update mehr annehmen: Es muss ein neues
|
||
Schlüsselpaar erzeugt (`pnpm --filter @tessera/desktop exec tauri signer
|
||
generate -w <pfad>`), der öffentliche Teil in
|
||
`apps/desktop/src-tauri/tauri.conf.json` unter `plugins.updater.pubkey`
|
||
eingetragen und jeder Client einmal von Hand neu installiert werden.
|
||
|
||
Ohne die Secrets bricht der CI-Bau ab („A public key has been found, but no
|
||
private key"). `tauri build --no-sign` ist ausschließlich für lokale Proben
|
||
gedacht und im CI nicht vorgesehen – ein so gebautes Paket trägt keine
|
||
Signatur, und der Update-Endpunkt antwortet dafür mit `204`.
|
||
|
||
Windows legt bei jedem Update einen Ordner `%TEMP%\Tessera-{Version}-updater-…`
|
||
(rund 100 MB) an und räumt ihn nicht auf. Das ist kein Fehler; die Ordner
|
||
können jederzeit gelöscht werden.
|
||
|
||
Der Client erlaubt Updates in der App nur über `https`. Anwender mit einer
|
||
`http`-Adresse sehen im Menü den Hinweis „Update nur über https möglich" und
|
||
nutzen weiterhin den Weg über den Browser.
|
||
|
||
### Umgebungsvariablen
|
||
|
||
Für die Desktop-Auslieferung ist keine neue Pflichtvariable nötig.
|
||
|
||
| Variable | Pflicht? | Default | Zweck |
|
||
|----------|:---:|---|---|
|
||
| `DESKTOP_DIST_DIR` | nein | `/app/desktop-dist` (im Abbild) | Ablageort der Desktop-Pakete und der `manifest.json`, aus dem `GET /desktop/latest` und `GET /desktop/download/:platform` lesen. In der Regel nicht ändern. |
|
||
|
||
### Fehlerbilder
|
||
|
||
| Symptom | Wahrscheinliche Ursache | Prüfen / Beheben |
|
||
|---|---|---|
|
||
| Download-Link fehlt auf der Anmeldeseite bzw. `/api-proxy/desktop/latest` liefert `404` | Das laufende Abbild trägt keine Desktop-Pakete – der Job `publish` hätte ohne Manifest eigentlich abbrechen müssen | Den zugehörigen Pipeline-Lauf prüfen (Job `desktop`/`publish` grün?), danach `docker compose pull` + `up -d --force-recreate api` erneut ausführen. |
|
||
| Download bricht bei großen Dateien ab | Größengrenze oder Zeitlimit des vorgeschalteten Proxys (Nginx Proxy Manager) – `client_max_body_size` bzw. Timeout-Einstellungen | Proxy-Konfiguration für die betroffene Adresse prüfen und die Grenze anheben. |
|
||
| Client meldet „Unter dieser Adresse antwortet kein Tessera-Server" | Anwender hat die interne API-Adresse statt der Web-Adresse eingetragen, oder `/api-proxy` ist vom Client-Rechner aus nicht erreichbar | Die im Anwenderhandbuch beschriebene Adresse verwenden (dieselbe wie im Browser); Netzwerk-/Firewall-Erreichbarkeit der Web-Adresse prüfen. |
|
||
| Windows zeigt die SmartScreen-Warnung | Erwartet – die App ist für den internen Gebrauch nicht signiert (D-09) | Kein Fehler; Anwenderhandbuch, Abschnitt „Installation unter Windows", beschreibt den Ablauf. |
|
||
| Beta-Paket nennt einen älteren Commit als das laufende Abbild (Dateiname `-beta.<Commit>`, Einstellungen → Desktop-App) | Erwartet: Desktop-App seit diesem Commit unverändert, Pakete aus dem Zwischenspeicher übernommen (Abschnitt „Wann gebaut wird …") | Kein Fehler. Soll dennoch neu gebaut werden, genügt eine Änderung unter `apps/desktop/` im nächsten Push. |
|
||
| Client meldet „Update fehlgeschlagen" | Download über den Proxy abgebrochen (Größengrenze/Zeitlimit, siehe zweite Zeile dieser Tabelle), oder die Signatur passt nicht – die Pakete stammen nicht aus dem CI-Bau mit dem aktuellen Schlüssel | Kontrollzeile `/api-proxy/desktop/update` (Abschnitt „Wo die Pakete im Abbild liegen"), den Pipeline-Lauf und die Proxy-Einstellungen prüfen. Der Anwender kommt über den Browser-Weg weiter. |
|
||
| `/api-proxy/desktop/update` antwortet dauerhaft `204`, obwohl Pakete da sind | Manifest ohne `signature`/`updateVersion`: Pakete aus einem Bau vor der Update-Funktion oder mit `--no-sign` | Eine Änderung unter `apps/desktop/` pushen bzw. den Tag neu bauen lassen; im CI prüfen, dass die Secrets `TAURI_SIGNING_PRIVATE_KEY`/`_PASSWORD` gesetzt sind (Abschnitt „Updates in der App und der Signierschlüssel"). |
|
||
| Eine Fehlermeldung aus der Desktop-App nennt als Herkunft „Desktop-App (unbekannt)“ ohne Version, Betreff-Kürzel `[Desktop]` | Der Client ist älter als diese Fassung: er meldet dem Server beim Start nur `desktop=1`, nicht Version, Stand und Betriebssystem (Parameter `dv`, `dc`, `dos`, aus denen `web` das Cookie `tessera_desktop_client` bildet) | Kein Fehler, die Meldung ist trotzdem als Desktop-App erkennbar. Client über „Auf Version … aktualisieren“ im Infobereich oder den Browser-Installer aktualisieren; danach stehen Betriebssystem, Version und Stand in der Meldung. |
|
||
| Beim Hochladen in „Dateien“ bricht jede größere Datei beim ersten 8-MB-Stück ab (Fehler „Die Verbindung wurde unterbrochen“, im Proxy-Protokoll `413`) | Der Proxy vor Tessera (Nginx Proxy Manager) lässt keine Anfragen über seiner Größengrenze durch (`client_max_body_size`), oder sein Zeitlimit ist zu kurz | Für die Tessera-Adresse `client_max_body_size` auf mindestens `10m` und die Lese-/Sendezeitlimits auf mindestens 120 Sekunden stellen (Kapitel 3, Abschnitt „Dateien (Nextcloud)“). |
|
||
| Im Zertifikatsmanager bricht das Hochladen mehrerer Dateien ab (Fehlertext „Die Dateien sind zusammen zu groß“ oder `413` im Proxy-Protokoll) | Der Proxy vor Tessera (Nginx Proxy Manager) lässt Anfragen über seiner Größengrenze nicht durch (`client_max_body_size`); die Analyse schickt bis zu 10 MB | Für die Tessera-Adresse `client_max_body_size` auf mindestens `10m` stellen (Kapitel 3, Abschnitt „Dateien (Nextcloud)“); Tessera selbst erlaubt höchstens 20 MB je Analyse. |
|
||
| Im Zertifikatsmanager meldet „Fehlendes Zertifikat holen“, der Server des Ausstellers sei nicht erreichbar | Der `api`-Container kommt ausgehend nicht per HTTP/HTTPS (Ports 80 und 443) ins Internet, oder der Server des Ausstellers antwortet nicht; steht die Meldung, die Adresse werde nicht abgerufen, zeigt die Adresse im Zertifikat auf einen internen Rechner oder einen besonderen Anschluss, was Tessera absichtlich nie abruft | Ausgehenden Zugriff für den `api`-Container freigeben (`docker compose exec api node -e "fetch('http://ye2.i.lencr.org/').then(r=>console.log(r.status))"` muss `200` ausgeben); sonst das Zertifikat beim Aussteller herunterladen und im Reiter „Dateien“ hinzufügen. |
|
||
| In „Dateien“ tragen öffentliche Links eine interne Adresse (zum Beispiel ein interner Rechnername oder eine IP-Adresse) und lassen sich von außen nicht öffnen | Die Nextcloud baut die Adresse eines Links aus dem Namen, unter dem Tessera sie aufruft; in Tessera ist die interne Adresse der Nextcloud eingetragen | In den Einstellungen des Moduls die von außen erreichbare Adresse der Nextcloud eintragen, oder in der `config.php` der Nextcloud `overwritehost`, `overwriteprotocol` und `overwrite.cli.url` auf die externe Adresse setzen; bereits erstellte Links ändern sich nicht rückwirkend, sie müssen neu erstellt werden. |
|
||
| In „Dateien“ erscheint „Sie haben in kurzer Zeit viele Freigaben angelegt. Bitte warten Sie einige Minuten.“ | Tessera hat für den Benutzer 10 neue Freigaben innerhalb von 10 Minuten angelegt oder mehr als 40 Versuche in 10 Minuten gezählt (auch abgelehnte) | Einige Minuten abwarten (der Zähler läuft nach 10 Minuten ab); ein Neustart des `api`-Containers setzt den Zähler zurück, ist aber nur im Ausnahmefall nötig. |
|