Files
tessera-ctl/apps/web/src/lib/desktop-client.ts
T
schalli f2457113a2 feat(web): Herkunft der Fehlermeldung — Cookie tessera_desktop_client und Client-Felder in der Nutzlast
Die Middleware liest jetzt zusaetzlich dv/dc/dos aus der Anfrage und legt
daraus das Cookie tessera_desktop_client an (bereinigt per Muster, nur
wenn alle drei Werte gueltig sind); desktop-client.ts liest es zurueck.
Der Fehler-melden-Dialog fuellt daraus vier neue Nutzlastfelder
(clientKind/clientOs/clientVersion/clientCommit), damit die API die
Herkunft der Meldung ausweisen kann. Ohne das zweite Cookie (alter
Client) bleibt es bei "Desktop-App (unbekannt)".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016g2npLxzH5gZpg8s2S6vKh
2026-09-18 12:36:19 +02:00

87 lines
2.9 KiB
TypeScript

import { useEffect, useState } from 'react';
/**
* desktop-client — erkennt, ob die Seite im Desktop-Client laeuft
* (260917-h2s). Gegenstueck zur Middleware (apps/web/src/middleware.ts,
* `withDesktopCookie`): der Rust-Client haengt `desktop=1` an seine erste
* Navigation, die Middleware setzt daraufhin das Cookie `tessera_desktop`
* auf jede Antwort. `isDesktopClient()` liest das Cookie synchron.
*/
export const DESKTOP_COOKIE_NAME = 'tessera_desktop';
/**
* Gegenstueck zu `buildDesktopClientCookieValue()` in der Middleware
* (quick-260918-gza): dort werden `dv`/`dc`/`dos` mit `${dv}|${dc}|${dos}`
* zusammengesetzt und von Next per `encodeURIComponent` serialisiert --
* `|` wird `%7C` (gemessen). `document.cookie` liefert deshalb die
* KODIERTE Form; `parseDesktopClientCookie()` dekodiert (in try/catch,
* falls der Wert bereits roh vorliegt, z. B. direkt gesetzt im Test).
*/
export const DESKTOP_CLIENT_COOKIE_NAME = 'tessera_desktop_client';
export interface DesktopClientInfo {
version: string;
commit: string;
os: string;
}
/**
* Parst den Wert des Cookies `tessera_desktop_client` aus einem rohen
* `document.cookie`-String. `null` bei fehlendem Cookie oder einer Form,
* die nicht genau drei durch `|` getrennte Teile mit nicht-leerer Version
* und nicht-leerem Betriebssystem ergibt (Commit darf leer sein).
*/
export function parseDesktopClientCookie(cookieString: string): DesktopClientInfo | null {
const entry = cookieString
.split(';')
.map((c) => c.trim())
.find((c) => c.startsWith(`${DESKTOP_CLIENT_COOKIE_NAME}=`));
if (!entry) return null;
const raw = entry.slice(DESKTOP_CLIENT_COOKIE_NAME.length + 1);
let value = raw;
try {
value = decodeURIComponent(raw);
} catch {
// Rohwert (z. B. bereits dekodiert im Test) -- unveraendert weiterverwenden.
}
const parts = value.split('|');
if (parts.length !== 3) return null;
const [version, commit, os] = parts;
if (!version || !os) return null;
return { version, commit, os };
}
/** `null` ohne `document` (SSR) oder ohne gueltiges Cookie. */
export function getDesktopClientInfo(): DesktopClientInfo | null {
if (typeof document === 'undefined') {
return null;
}
return parseDesktopClientCookie(document.cookie);
}
export function isDesktopClient(): boolean {
if (typeof document === 'undefined') {
return false;
}
return document.cookie
.split(';')
.some((c) => c.trim() === `${DESKTOP_COOKIE_NAME}=1`);
}
/**
* Hook statt direktem Aufruf beim Render: `isDesktopClient()` synchron im
* ersten Render zu nutzen wuerde Server-HTML (kein `document`) und den
* ersten Client-Render auseinanderlaufen lassen -- Hydration-Fehler. Der
* `useEffect` gleicht das an; `useState(false)` haelt Server- und
* Erst-Render-Ergebnis identisch.
*/
export function useIsDesktopClient(): boolean {
const [isDesktop, setIsDesktop] = useState(false);
useEffect(() => {
setIsDesktop(isDesktopClient());
}, []);
return isDesktop;
}