373 lines
23 KiB
Markdown
373 lines
23 KiB
Markdown
---
|
||
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>
|