Files
tessera-ctl/docs/anleitung-betrieb.md
T
schalli 7704372c3c feat(260923-lrr): API — Favoriten-Symbol hochladen, Vorrang, Versionszaehler, Abrufprobe
- FavoriteLink: neue Spalten uploadedIconMime/iconVersion (Migration 20260923160000)
- favorite-icon-files.ts: Erkennung PNG/JPEG/GIF/WebP/ICO/SVG, Pfadbildung ohne
  Byte aus der Anfrage im Pfad (T-LRR-01), best-effort Dateientfernung
- FavoritesService: uploadIcon/removeUploadedIcon, Vorrang der hochgeladenen
  Datei in getIconBytes, Abrufprobe fuer eine neue iconUrl (422 statt stiller
  Speicherung), iconVersion-Erhoehung bei jeder Aenderung der Symbolquelle
- FavoritesController: POST/DELETE /favorites/:id/icon, Cache-Control private
- T-LRR-07 (Restrisiko aus dem Plan-Threat-Model geschlossen, ueber den Plan
  hinaus): DashboardService.removeWidget/deleteDashboard raeumen jetzt die
  Symboldateien der per Datenbank-Kaskade mitgeloeschten Favoriten auf
  (best effort, nie blockierend)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 16:04:29 +02:00

721 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 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.
## 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).
## 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` ist der alte Variablenname und wird weiterhin akzeptiert (mit Warnung im Log). |
| `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` | SMTP-Relay für ausgehende Mails (Passwort-Reset, Benachrichtigungen). |
| `TESSERA_BUGREPORT_TO` | nein | leer | Rückfall-Postfach für den Knopf „Fehler melden“ in der Kopfleiste, falls unter Administrator → 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. |
| `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 und wie CORS_ORIGIN, NODE_ENV oder weitere in main.ts gelesene Variablen auf dem tatsächlichen Server zusätzlich 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.
## 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.
## 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 (`apps/api/Dockerfile`, `CMD`):
```
prisma migrate deploy --schema apps/api/prisma/schema.prisma && node apps/api/dist/main.js
```
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 `node apps/api/dist/main.js` 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` 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 Administrator → SMTP noch `TESSERA_BUGREPORT_TO`), oder der SMTP-Versand des Mandanten scheitert | Feld „Fehlermeldungen an“ (Administrator → 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.
## 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:** In `CHANGELOG.md` wird 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.
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).
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&current=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. |