docs(18): Phase 18 geplant — sechs Plaene (Tracer Linux-Strecke, Pipeline, Web, Client, Windows-Cross-Bau mit Iterationsschleife, Abschluss), Validierungsplan, API-Coverage, Fahrplan

This commit is contained in:
2026-09-16 15:53:45 +02:00
parent e307a8e689
commit 2eb3be8b74
11 changed files with 3117 additions and 21 deletions
@@ -0,0 +1,372 @@
---
phase: 18-desktop-client-fertigstellen
plan: 03
type: execute
wave: 2
depends_on: ["18-01"]
files_modified:
- apps/web/src/lib/desktop.ts
- apps/web/src/lib/desktop.test.ts
- apps/web/src/components/desktop/desktop-download-links.tsx
- apps/web/src/components/desktop/desktop-download-links.test.tsx
- apps/web/src/app/(auth)/login/page.tsx
- apps/web/src/app/(portal)/settings/general/desktop/page.tsx
- apps/web/src/components/settings/desktop-app-settings.tsx
- apps/web/src/components/settings/desktop-app-settings.test.tsx
- apps/web/src/components/settings/settings-sidebar.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
autonomous: true
requirements: [DESK-03]
user_setup: []
estimate:
tokens: 80000
raw_tokens: 80000
tasks: 2
confidence: low
must_haves:
truths:
- "Auf der Anmeldeseite steht unterhalb des Formulars ein unauffaelliger Link 'Desktop-App herunterladen (Windows)' mit kleinem Linux-Link und Versionsangabe — nur wenn /desktop/latest antwortet (D-12)."
- "Unter Einstellungen -> Allgemein -> Desktop-App gibt es eine Seite mit Version, zwei Download-Knoepfen in Primaerfarbe mit Plattform-Symbol, Dateiname und Dateigroesse sowie vier Saetzen in Sie-Form; antwortet die API mit 404, erscheint statt der Knoepfe ein Hinweis (D-12)."
- "Jeder Download laeuft ueber die Tessera-API (API_URL + url aus /desktop/latest); Anwender brauchen keinen Gitea-Zugang (D-01, D-10)."
- "Alle neuen Texte liegen 1:1 in de.json und en.json vor, deutsche Texte mit echten Umlauten (Projektkonvention)."
artifacts:
- path: "apps/web/src/lib/desktop.ts"
provides: "loadDesktopLatest (memoisiert, still bei Fehler), desktopDownloadUrl, formatFileSize"
exports: ["loadDesktopLatest", "desktopDownloadUrl", "formatFileSize"]
- path: "apps/web/src/components/desktop/desktop-download-links.tsx"
provides: "Link-Block der Anmeldeseite, rendert nichts ohne Daten"
exports: ["DesktopDownloadLinks"]
- path: "apps/web/src/components/settings/desktop-app-settings.tsx"
provides: "Inhalt der Einstellungsseite: Version, Knoepfe, Groesse, Saetze, Hinweis"
exports: ["DesktopAppSettings"]
- path: "apps/web/src/app/(portal)/settings/general/desktop/page.tsx"
provides: "Route /settings/general/desktop"
contains: "DesktopAppSettings"
- path: "apps/web/src/messages/de.json"
provides: "auth.desktopDownload.*, settings.categoryDesktopApp, settings.desktop.*"
contains: "desktopDownload"
key_links:
- from: "apps/web/src/lib/desktop.ts"
to: "apps/api/src/desktop/desktop.controller.ts"
via: "fetch(`${API_URL}/desktop/latest`) — im Betrieb ueber den Rewrite /api-proxy"
pattern: "desktop/latest"
- from: "apps/web/src/components/desktop/desktop-download-links.tsx"
to: "apps/web/src/lib/desktop.ts"
via: "loadDesktopLatest() in useEffect; null blendet den Block aus"
pattern: "loadDesktopLatest"
- from: "apps/web/src/components/settings/settings-sidebar.tsx"
to: "apps/web/src/app/(portal)/settings/general/desktop/page.tsx"
via: "Link href=/settings/general/desktop unter 'Allgemein'"
pattern: "settings/general/desktop"
---
<objective>
Anwender sehen die Desktop-App in Tessera selbst: ein Link auf der
Anmeldeseite und eine eigene Einstellungsseite "Desktop-App" mit Version,
Download-Knoepfen fuer Windows und Linux, Dateigroesse und einer kurzen
Erklaerung. Beides liest `GET /desktop/latest` aus 18-01 und blendet sich aus,
wenn der Server keine Pakete traegt.
Purpose: D-12 aus 18-CONTEXT.md (Web-Oberflaeche) und Erfolgskriterium 2.
Output: Fetch-Helfer, zwei Komponenten mit Tests, neue Einstellungsroute,
Seitenleisteneintrag, Uebersetzungen de/en.
Alle Adressen werden aus `API_URL` gebildet (`NEXT_PUBLIC_API_URL`, im
Betrieb `/api-proxy`); es wird nirgends eine feste Server- oder
Firmenadresse eingetragen.
</objective>
## Artifacts this phase produces
Dieser Plan: `apps/web/src/lib/desktop.ts` (`DesktopPlatform`,
`DesktopFileInfo`, `DesktopLatestInfo`, `loadDesktopLatest`,
`desktopDownloadUrl`, `formatFileSize`), `desktop.test.ts`,
`components/desktop/desktop-download-links.tsx` (`DesktopDownloadLinks`),
`desktop-download-links.test.tsx`, `app/(auth)/login/page.tsx` (Einbau),
`app/(portal)/settings/general/desktop/page.tsx` (`DesktopSettingsPage`),
`components/settings/desktop-app-settings.tsx` (`DesktopAppSettings`),
`desktop-app-settings.test.tsx`, `components/settings/settings-sidebar.tsx`
(Eintrag), `messages/de.json` und `messages/en.json` (`auth.desktopDownload.*`,
`settings.categoryDesktopApp`, `settings.desktop.*`). Gesamtliste der Phase:
siehe 18-01-PLAN.md.
<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/18-desktop-client-fertigstellen/18-CONTEXT.md
@.planning/phases/18-desktop-client-fertigstellen/18-PATTERNS.md
@.planning/phases/18-desktop-client-fertigstellen/18-01-SUMMARY.md
@apps/web/src/lib/app-version.ts
@apps/web/src/lib/app-version.test.ts
@apps/web/src/components/layout/app-version-badge.tsx
@apps/web/src/app/(auth)/login/page.tsx
@apps/web/src/app/(portal)/settings/general/account/page.tsx
@apps/web/src/components/settings/settings-sidebar.tsx
@apps/web/src/components/settings/widget-settings-panel.test.tsx
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Fetch-Helfer und der Download-Link auf der Anmeldeseite</name>
<files>
apps/web/src/lib/desktop.ts,
apps/web/src/lib/desktop.test.ts,
apps/web/src/components/desktop/desktop-download-links.tsx,
apps/web/src/components/desktop/desktop-download-links.test.tsx,
apps/web/src/app/(auth)/login/page.tsx,
apps/web/src/messages/de.json,
apps/web/src/messages/en.json
</files>
<read_first>
apps/web/src/lib/app-version.ts (gesamt — Muster fuer API_URL und memoisiertes Laden),
apps/web/src/lib/app-version.test.ts (gesamt — vi.resetModules + dynamischer Import),
apps/web/src/components/layout/app-version-badge.tsx (useEffect/useState-Konsum),
apps/web/src/app/(auth)/login/page.tsx (Einbaustelle nach dem Formular),
apps/web/src/components/settings/widget-settings-panel.test.tsx (Zeilen 1-30, next-intl-Mock mit de.json),
apps/web/src/messages/de.json (Namensraum `auth`),
.planning/phases/18-desktop-client-fertigstellen/18-RESEARCH.md (Code Example 8)
</read_first>
<behavior>
- `loadDesktopLatest()` ruft `${API_URL}/desktop/latest` genau einmal je Modulinstanz auf (zweiter Aufruf liefert dasselbe Promise); `ok=false` und Netzfehler liefern `null`, nichts wird geworfen.
- `desktopDownloadUrl(file)` ergibt `${API_URL}${file.url}` (z. B. `http://localhost:3001/desktop/download/windows` in Tests).
- `formatFileSize(6123456, 'de')` ergibt `5,8 MB`; `formatFileSize(6123456, 'en')` ergibt `5.8 MB`; `formatFileSize(106461688, 'de')` ergibt `101,5 MB`.
- `DesktopDownloadLinks` rendert nichts, solange nichts geladen ist oder `null` kam; mit Daten fuer beide Plattformen erscheinen ein Link "Desktop-App herunterladen (Windows)" (href = Windows-URL, Attribut `download`) und ein Link "Linux-Version" sowie der Text "Version 1.1.0".
- Fehlt `files.windows` (Stand nach 18-01, nur Linux gebaut), erscheint genau ein Link mit dem Text "Desktop-App herunterladen (Linux)" und der Versionstext.
</behavior>
<action>
**`apps/web/src/lib/desktop.ts`** nach dem Vorbild `app-version.ts` (gleicher
`API_URL`-Ausdruck mit woertlichem `process.env.NEXT_PUBLIC_API_URL`,
deutscher Kopfkommentar mit Verweis auf D-10/D-12 und auf den Rewrite
`/api-proxy`). Typen als Spiegel der API (kein Import aus `@tessera/shared`,
gleiche Begruendung wie im Kommentar von `app-version.ts`):
`DesktopPlatform = 'windows' | 'linux'`,
`DesktopFileInfo { name; size; sha256; url }`,
`DesktopLatestInfo { version; channel; commit; buildTime; files: Partial<Record<DesktopPlatform, DesktopFileInfo>> }`.
`loadDesktopLatest()` memoisiert wie `loadApiVersion()`, aber **ohne**
`credentials: 'include'` (oeffentlicher Endpunkt, Anmeldeseite hat noch kein
Cookie). `desktopDownloadUrl(file)` und `formatFileSize(bytes, locale)`
(`Intl.NumberFormat(locale, { maximumFractionDigits: 1 })` auf `bytes / 1048576`,
Suffix ` MB`).
**`desktop.test.ts`** im Stil von `app-version.test.ts` (`importFresh` mit
`vi.resetModules`, `vi.stubGlobal('fetch', …)`): Test 1 memoisiert (ein
Fetch, zwei gleiche Ergebnisse, Aufruf-URL endet auf `/desktop/latest`,
kein `credentials`-Feld in den Optionen); Test 2 still bei `ok=false`; Test 3
still bei Netzfehler; Test 4 `desktopDownloadUrl`; Test 5 die drei
`formatFileSize`-Faelle aus `<behavior>` — Erwartungen von Hand.
**`components/desktop/desktop-download-links.tsx`** (`'use client'`,
`useTranslations('auth')`, `useLocale()` aus `next-intl`): `useEffect` laedt
`loadDesktopLatest()` mit `active`-Schutz wie `AppVersionBadge`; State
`DesktopLatestInfo | null`. Rendert `null`, wenn keine Daten oder keine
Plattform in `files`. Sonst ein `<div className="text-center text-sm text-muted-foreground">`
mit: Hauptlink (Windows, falls vorhanden, sonst Linux) als `<a href={desktopDownloadUrl(file)} download className="hover:text-foreground underline-offset-4 hover:underline">`
mit Text `t('desktopDownload.windows')` bzw. `t('desktopDownload.linux')`;
ist Windows vorhanden **und** Linux vorhanden, dahinter ` · ` und ein
zweiter Link `t('desktopDownload.linuxShort')`; darunter in `text-xs`
`t('desktopDownload.version', { version })`. Keine Fehlermeldung, kein
Spinner — der Block ist unauffaellig (D-12).
**`desktop-download-links.test.tsx`**: next-intl-Mock nach dem Muster in
`widget-settings-panel.test.tsx` (de.json-gestuetzt, zusaetzlich
`useLocale: () => 'de'`), `vi.mock('@/lib/desktop', …)` mit steuerbarem
`loadDesktopLatest` (echte `desktopDownloadUrl`/`formatFileSize` per
`importOriginal` durchreichen). Faelle: (1) `null` -> Container leer
(`container.firstChild` ist `null`); (2) beide Plattformen -> zwei Links mit
den deutschen Texten aus de.json und hrefs `…/desktop/download/windows` bzw.
`…/desktop/download/linux`, Text `Version 1.1.0`; (3) nur Linux -> genau ein
Link mit dem Linux-Text. `findBy…` fuer die asynchrone Aufloesung.
**Anmeldeseite (`(auth)/login/page.tsx`)**: Import der Komponente; direkt
nach dem schliessenden `</form>` innerhalb des `max-w-sm space-y-8`-Blocks
`<DesktopDownloadLinks />` einfuegen. Sonst nichts aendern.
**Uebersetzungen** in `de.json` unter `auth` neuer Block `desktopDownload`:
`windows` = "Desktop-App herunterladen (Windows)", `linux` = "Desktop-App
herunterladen (Linux)", `linuxShort` = "Linux-Version", `version` =
"Version {version}". In `en.json` 1:1: "Download desktop app (Windows)",
"Download desktop app (Linux)", "Linux version", "Version {version}".
</action>
<acceptance_criteria>
- `pnpm --filter @tessera/web exec vitest run src/lib/desktop.test.ts src/components/desktop` meldet 8 Tests bestanden, 0 fehlgeschlagen.
- `grep -v '^\s*//' apps/web/src/lib/desktop.ts | grep -c 'process.env.NEXT_PUBLIC_API_URL'` ergibt 1.
- `grep -c 'DesktopDownloadLinks' "apps/web/src/app/(auth)/login/page.tsx"` ergibt 2 (Import und Einbau).
- `node -e "const de=require('./apps/web/src/messages/de.json');if(de.auth.desktopDownload.windows!=='Desktop-App herunterladen (Windows)')process.exit(1)"` endet mit 0.
- `pnpm --filter @tessera/web type-check` fehlerfrei.
</acceptance_criteria>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/lib/desktop.test.ts src/components/desktop && pnpm --filter @tessera/web type-check</automated>
<fails_when>vitest meldet "failed" oder Exit-Code ungleich 0, oder tsc gibt Fehlerzeilen aus.</fails_when>
</verify>
<done>
Acht Tests gruen, Typpruefung fehlerfrei, die Anmeldeseite baut den
Link-Block ein, de/en tragen den Block `auth.desktopDownload`.
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Einstellungsseite "Desktop-App" mit Knoepfen, Groesse und Erklaerung</name>
<files>
apps/web/src/app/(portal)/settings/general/desktop/page.tsx,
apps/web/src/components/settings/desktop-app-settings.tsx,
apps/web/src/components/settings/desktop-app-settings.test.tsx,
apps/web/src/components/settings/settings-sidebar.tsx,
apps/web/src/messages/de.json,
apps/web/src/messages/en.json
</files>
<read_first>
apps/web/src/app/(portal)/settings/general/account/page.tsx (Seitenhuelle),
apps/web/src/components/settings/settings-sidebar.tsx (Eintrag "Konto" unter "Allgemein"),
apps/web/src/components/settings/widget-settings-panel.test.tsx (Zeilen 1-30),
apps/web/src/app/(auth)/login/page.tsx (Klassen des Primaerknopfs: `rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90`),
apps/web/src/lib/desktop.ts (aus Task 1),
apps/web/src/messages/de.json (Namensraum `settings`, Block `account`)
</read_first>
<behavior>
- Route `/settings/general/desktop` rendert die Ueberschrift "Desktop-App" und die Komponente `DesktopAppSettings`.
- Mit Daten fuer beide Plattformen zeigt die Seite "Aktuelle Version: 1.1.0", zwei Knoepfe "Für Windows herunterladen" und "Für Linux herunterladen" (Primaerfarbe, jeweils mit Plattform-Symbol als inline-SVG, `href` aus `desktopDownloadUrl`, Attribut `download`) und darunter je Knopf die Zeile "{Dateiname} · {Groesse}", z. B. "Tessera-Setup-1.1.0.exe · 5,8 MB".
- Auf dem Beta-Kanal steht zusaetzlich "Beta-Ausgabe, Stand {commit}".
- Vier Saetze in Sie-Form erklaeren, was die App ist, den Erststart mit Server-Adresse, das Verhalten im Infobereich und den Update-Hinweis.
- Antwortet die API mit null, erscheinen statt der Knoepfe der Satz "Auf diesem Server sind derzeit keine Desktop-Pakete hinterlegt." und die vier Saetze bleiben stehen.
- Die Seitenleiste zeigt unter "Allgemein" den Eintrag "Desktop-App" mit `aria-current="page"` auf der Route.
</behavior>
<action>
**Seite `app/(portal)/settings/general/desktop/page.tsx`**: exakt die
Huelle von `account/page.tsx` (`'use client'`, `useTranslations('settings')`,
`<h1>` mit `t('desktop.title')`), Inhalt `<DesktopAppSettings />`. Kein
Anlegen weiterer Layout-Dateien — die Route liegt unter dem bestehenden
`settings`-Layout mit Seitenleiste.
**Komponente `components/settings/desktop-app-settings.tsx`**
(`'use client'`, `useTranslations('settings')`, `useLocale()`): laedt
`loadDesktopLatest()` wie in Task 1 (State `undefined` = laedt, `null` =
nicht verfuegbar, Objekt = Daten). Aufbau: Absatz mit den vier Saetzen
`t('desktop.intro')`, `t('desktop.firstStart')`, `t('desktop.tray')`,
`t('desktop.update')` (ein `<p>` je Satz, `text-sm text-muted-foreground`);
dann bei Daten: `<p>` mit `t('desktop.versionLabel', { version })` und, wenn
`channel === 'beta'`, `t('desktop.channelBeta', { commit })`; dann ein
`<div className="flex flex-wrap gap-4">` mit je Plattform (nur vorhandene,
Reihenfolge Windows, Linux) einem Block aus `<a href download>` im
Primaerknopf-Stil der Anmeldeseite (`inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2.5 text-sm font-medium text-primary-foreground hover:opacity-90`)
mit inline-SVG-Symbol (Windows: vier abgerundete Felder im 2x2-Raster;
Linux: Terminalfenster mit `>_`-Prompt — beide 16x16, `aria-hidden`) und
Text `t('desktop.downloadWindows')` bzw. `t('desktop.downloadLinux')`,
darunter `<p className="mt-1 text-xs text-muted-foreground">` mit
`t('desktop.fileInfo', { name, size: formatFileSize(size, locale) })`. Bei
`null`: `<p>` mit `t('desktop.unavailable')` statt Knoepfen. Waehrend des
Ladens nichts unterhalb der Saetze. `data-testid="desktop-download-windows"`
und `desktop-download-linux` an den Links.
**Seitenleiste `settings-sidebar.tsx`**: im `<nav>` unter "Allgemein" hinter
dem Konto-Link einen zweiten `<Link href="/settings/general/desktop">` mit
identischem Klassen-/`aria-current`-Muster und `t('categoryDesktopApp')`;
`isActive` bleibt unveraendert (`startsWith` deckt die Route ab).
**Uebersetzungen** `de.json` `settings`: `categoryDesktopApp` = "Desktop-App";
Block `desktop`: `title` = "Desktop-App", `intro` = "Die Desktop-App öffnet
Tessera in einem eigenen Fenster – ohne Browser, mit Symbol im Infobereich der
Taskleiste.", `firstStart` = "Beim ersten Start fragt die App nach der Adresse
Ihres Tessera-Servers; das ist die Adresse, unter der Sie Tessera auch im
Browser öffnen.", `tray` = "Schließen Sie das Fenster, läuft Tessera im
Infobereich weiter; über das Symbol dort öffnen Sie das Fenster wieder,
schalten den automatischen Start ein oder beenden die App.", `update` =
"Erscheint eine neuere Version, weist die App Sie darauf hin und führt Sie auf
diese Seite.", `versionLabel` = "Aktuelle Version: {version}", `channelBeta`
= "Beta-Ausgabe, Stand {commit}", `downloadWindows` = "Für Windows
herunterladen", `downloadLinux` = "Für Linux herunterladen", `fileInfo` =
"{name} · {size}", `unavailable` = "Auf diesem Server sind derzeit keine
Desktop-Pakete hinterlegt.". `en.json` 1:1 sinngemaess ("Desktop app",
"The desktop app opens Tessera in its own window – no browser, with an icon
in the notification area of the taskbar.", "On first start the app asks for
the address of your Tessera server; it is the address you also use to open
Tessera in the browser.", "If you close the window, Tessera keeps running in
the notification area; use the icon there to reopen the window, enable
automatic start, or quit the app.", "When a newer version is available the
app notifies you and brings you to this page.", "Current version: {version}",
"Beta build, commit {commit}", "Download for Windows", "Download for Linux",
"{name} · {size}", "No desktop packages are available on this server yet.").
**Test `desktop-app-settings.test.tsx`**: next-intl-Mock wie in Task 1
(Namensraum `settings`, `useLocale: () => 'de'`), `@/lib/desktop` gemockt.
Faelle: (1) beide Plattformen -> Text "Aktuelle Version: 1.1.0", zwei Links
mit den Testids, hrefs `…/desktop/download/windows` und `…/desktop/download/linux`,
Zeile "Tessera-Setup-1.1.0.exe · 5,8 MB" (Groesse 6123456) und
"Tessera-1.1.0.AppImage · 101,5 MB" (Groesse 106461688); (2) Kanal `beta`,
Commit `abc1234` -> "Beta-Ausgabe, Stand abc1234"; (3) `null` -> Hinweistext
sichtbar, keine Links (`queryByTestId` beide `null`), die vier Saetze
weiterhin da (mindestens `intro` per Text geprueft).
</action>
<acceptance_criteria>
- `pnpm --filter @tessera/web exec vitest run src/components/settings/desktop-app-settings.test.tsx` meldet 3 Tests bestanden, 0 fehlgeschlagen.
- `test -f "apps/web/src/app/(portal)/settings/general/desktop/page.tsx"` endet mit 0; `grep -c 'DesktopAppSettings' "apps/web/src/app/(portal)/settings/general/desktop/page.tsx"` ergibt 2.
- `grep -c 'href="/settings/general/desktop"' apps/web/src/components/settings/settings-sidebar.tsx` ergibt 1.
- Parität und Umlaute: das node-Skript aus `<verify>` gibt `i18n OK` aus.
- `pnpm --filter @tessera/web exec vitest run` — gesamte Web-Suite gruen (Basis am 2026-09-16: 52 Dateien / 354 Tests plus die neuen).
</acceptance_criteria>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/settings/desktop-app-settings.test.tsx src/components/desktop src/lib/desktop.test.ts && pnpm --filter @tessera/web type-check</automated>
<fails_when>vitest meldet "failed" oder Exit-Code ungleich 0, oder tsc gibt Fehlerzeilen aus.</fails_when>
<automated>cd /home/vicolab/projects/tessera-ctl && node -e "const de=require('./apps/web/src/messages/de.json'),en=require('./apps/web/src/messages/en.json');const walk=(o,p='')=>Object.entries(o).flatMap(([k,v])=>typeof v==='object'&&v?walk(v,p+k+'.'):[p+k]);for(const ns of ['auth','settings']){const d=walk(de[ns]),e=walk(en[ns]);const miss=d.filter(k=>!e.includes(k)).concat(e.filter(k=>!d.includes(k)));if(miss.length){console.error('Fehlende Uebersetzungen in '+ns+':',miss);process.exit(1)}}const vals=o=>Object.values(o).flatMap(v=>typeof v==='object'&&v?vals(v):[String(v)]);const bad=vals({a:de.auth.desktopDownload,b:de.settings.desktop,c:{k:de.settings.categoryDesktopApp}}).filter(s=>/\b(fuer|ueber|koennen|Groesse|verfuegbar|oeffnen|schliessen|Oeffnen|Schliessen|laeuft|fuehrt)\b/i.test(s));if(bad.length){console.error('ASCII-Umschrift statt Umlaut:',bad);process.exit(1)}console.log('i18n OK')"</automated>
<fails_when>Ausgabe `Fehlende Uebersetzungen` (Schluessel nur in einer Sprache) oder `ASCII-Umschrift statt Umlaut` (deutscher Text mit ae/oe/ue-Umschrift) und Exit 1; `i18n OK` fehlt.</fails_when>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run</automated>
<fails_when>Irgendeine Datei der Web-Suite meldet "failed" — dann hat die Aenderung an de.json/en.json oder an der Seitenleiste bestehende Tests gebrochen.</fails_when>
</verify>
<done>
Die Einstellungsseite existiert mit Knoepfen, Groesse, Saetzen und
Hinweisfall, der Seitenleisteneintrag zeigt darauf, drei neue Tests gruen,
die gesamte Web-Suite gruen, de/en vollstaendig und mit Umlauten.
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser -> API (`/desktop/latest`, `/desktop/download/:platform`) | Oeffentliche Endpunkte; die Web-Oberflaeche rendert nur, was die API liefert. |
| API-Antwort -> DOM (`href`, Dateiname, Groesse) | Werte aus dem Manifest landen als Linkziel und Text in der Seite. |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-18-07 | Tampering | `desktopDownloadUrl` (Linkziel aus API-Daten) | low | mitigate | Das Linkziel wird aus `API_URL` plus dem relativen `url`-Feld gebaut; die Komponenten uebernehmen nie eine absolute Adresse aus der Antwort, ein manipuliertes Manifest kann den Download also nicht auf einen fremden Host lenken. |
| T-18-08 | Spoofing | Dateiname/Version als Text | low | accept | React rendert Text escaped; die Werte stammen aus dem vom CI geschriebenen Manifest (T-18-03 in 18-01). |
| T-18-09 | Information Disclosure | Anmeldeseite zeigt Version vor der Anmeldung | low | accept | Beabsichtigt (D-12); gleiche Abwaegung wie T-18-05. |
| T-18-SC | Tampering | Paketinstallationen | low | accept | Dieser Plan installiert kein neues Paket. |
</threat_model>
<verification>
1. `pnpm --filter @tessera/web exec vitest run` — gesamte Web-Suite gruen.
2. `pnpm --filter @tessera/web type-check` — fehlerfrei.
3. i18n-Paritaets- und Umlautpruefung gibt `i18n OK` aus.
4. Browser-Gegenprobe am Phasenende (18-06): Link auf der Anmeldeseite, Seite
unter Einstellungen -> Allgemein -> Desktop-App, Download startet.
</verification>
<success_criteria>
- Anmeldeseite: Link "Desktop-App herunterladen (Windows)" plus Linux-Link
und Version, nur wenn die API antwortet.
- Einstellungen -> Allgemein -> Desktop-App: Version, zwei Primaerknoepfe mit
Symbol, Dateiname und Groesse, vier erklaerende Saetze, Hinweis bei fehlenden
Paketen.
- Alle Downloads laufen ueber die Tessera-API.
- de/en vollstaendig, deutsche Texte mit Umlauten und in Sie-Form.
</success_criteria>
<output>
Create `.planning/phases/18-desktop-client-fertigstellen/18-03-SUMMARY.md` when done.
</output>