feat(18-03): Task 1 — Fetch-Helfer und Download-Link auf der Anmeldeseite

- apps/web/src/lib/desktop.ts: loadDesktopLatest (memoisiert, still bei
  Fehler), desktopDownloadUrl, formatFileSize (lokalisierte MB-Werte)
- DesktopDownloadLinks: unauffaelliger Link-Block, rendert nichts ohne
  Daten; Windows fuehrt, Linux als Kurzlink wenn beide Pakete vorliegen
- Anmeldeseite bindet den Block nach dem Formular ein (D-12)
- de/en: auth.desktopDownload.* ergaenzt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-16 16:27:44 +02:00
parent 2164cd537a
commit 026d9c39af
7 changed files with 318 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* Desktop-Paket-Info fuer die Web-Oberflaeche (Phase 18, D-10/D-12).
*
* Quelle fuer den unauffaelligen Download-Link auf der Anmeldeseite
* (`DesktopDownloadLinks`) UND fuer die Einstellungsseite
* "Desktop-App" (`DesktopAppSettings`): beide lesen `GET /desktop/latest`
* (oeffentlich, `@Public()` in `apps/api/src/desktop/desktop.controller.ts`)
* und blenden sich aus, wenn der Server keine Pakete traegt (404).
*
* Wichtig: Next.js ersetzt `process.env.NEXT_PUBLIC_*` nur dann zur Bauzeit
* im Browser-Bundle, wenn der Ausdruck woertlich mit vollem Namen im Code
* steht — kein Destructuring, kein `process.env[name]` (siehe `app-version.ts`).
* Im Betrieb laeuft die Anfrage ueber den Rewrite `/api-proxy` auf die API.
*/
export type DesktopPlatform = 'windows' | 'linux';
/**
* Spiegel von `DesktopLatestFile`/`DesktopLatestResponse` aus
* `packages/shared`: `apps/web` haengt nicht von `@tessera/shared` ab
* (gleiche Begruendung wie in `app-version.ts`). Die API-Wahrheit bleibt in
* `apps/api/src/desktop/desktop.service.ts`.
*/
export interface DesktopFileInfo {
name: string;
size: number;
sha256: string;
url: string;
}
export interface DesktopLatestInfo {
version: string;
channel: string;
commit: string;
buildTime: string;
files: Partial<Record<DesktopPlatform, DesktopFileInfo>>;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
let desktopLatestPromise: Promise<DesktopLatestInfo | null> | null = null;
/**
* Laedt `GET /desktop/latest` genau einmal je Modulinstanz (memoisiert);
* jeder Fehler (Netz, Nicht-2xx — insbesondere 404 ohne Manifest) ist still
* und liefert `null`. Kein `credentials: 'include'`: die Anmeldeseite zeigt
* den Link, bevor ein Cookie existiert, und der Endpunkt ist oeffentlich.
*/
export function loadDesktopLatest(): Promise<DesktopLatestInfo | null> {
if (!desktopLatestPromise) {
desktopLatestPromise = fetch(`${API_URL}/desktop/latest`)
.then((res) => (res.ok ? (res.json() as Promise<DesktopLatestInfo>) : null))
.catch(() => null);
}
return desktopLatestPromise;
}
/** Baut die absolute Download-Adresse aus `API_URL` plus dem relativen `url`-Feld (T-18-07). */
export function desktopDownloadUrl(file: DesktopFileInfo): string {
return `${API_URL}${file.url}`;
}
/** Formatiert Dateigroessen in MB, lokalisiert (z. B. `5,8 MB` / `5.8 MB`). */
export function formatFileSize(bytes: number, locale: string): string {
const mb = bytes / 1048576;
return `${new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(mb)} MB`;
}