feat(quick-260914-ku1): Versionsstempel — GET /health/version aus APP_*, VersionResponse, app-version.ts und Abzeichen v<Version> · <Kanal> in der Seitenleiste

- packages/shared: VersionResponse { name, version, channel, commit, buildTime }
- apps/api: health/app-version.ts (getAppVersion mit ||-Vorgaben dev/dev, formatAppVersionLine),
  HealthController.getVersion delegiert (bleibt @Public, T-KU1-03), main.ts protokolliert
  "Tessera API <version> (<channel>) <commit>" beim Start; neuer Spec mit 6 Tests
- apps/web: lib/app-version.ts (NEXT_PUBLIC_APP_* mit vollem Literalnamen, loadApiVersion
  memoisiert und still bei Fehler), AppVersionBadge unten in der Seitenleiste (auch mobil,
  nicht eingeklappt), sidebar.channel.{beta,live,dev} in de/en; 5 + 4 + 1 neue Tests
- Suiten: API 65 Dateien / 1060 Tests, Web 40 / 243, tsc in shared/api/web Exit 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 15:32:50 +02:00
parent 1cd4212df0
commit cdb571c509
13 changed files with 471 additions and 8 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* Versionsstempel der Web-Oberflaeche (quick-260914-ku1).
*
* Quelle fuer das Abzeichen unten in der Seitenleiste (`AppVersionBadge`)
* UND fuer den kommenden Fehler-melden-Knopf: beide brauchen die Fassung,
* die der Anwender gerade benutzt.
*
* 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]`. Sonst ist der Wert
* im Browser leer. Die Werte setzt `apps/web/Dockerfile` (builder-Stufe)
* aus den Build-Args des CI-Skripts `.gitea/scripts/publish-images.sh`;
* lokal greifen die Vorgaben `dev`.
*/
export type AppChannel = 'beta' | 'live' | 'dev';
export interface AppVersionInfo {
version: string;
channel: AppChannel;
commit: string;
}
/**
* Spiegel von `VersionResponse` aus `packages/shared`: `apps/web` haengt
* nicht von `@tessera/shared` ab, und ein neuer Import wuerde Lockfile und
* die deps-Stufe des Dockerfiles aendern. Die API-Wahrheit bleibt dort.
*/
export interface ApiVersionInfo {
name: string;
version: string;
channel: string;
commit: string;
buildTime: string;
}
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
function normalizeChannel(raw: string | undefined): AppChannel {
if (raw === 'beta' || raw === 'live') return raw;
return 'dev';
}
export const appVersion: AppVersionInfo = {
version: process.env.NEXT_PUBLIC_APP_VERSION || 'dev',
channel: normalizeChannel(process.env.NEXT_PUBLIC_APP_CHANNEL),
commit: process.env.NEXT_PUBLIC_APP_COMMIT || '',
};
let apiVersionPromise: Promise<ApiVersionInfo | null> | null = null;
/**
* Laedt `GET /health/version` genau einmal je Seitenladung (memoisiert);
* jeder Fehler (Netz, Nicht-2xx) ist still und liefert `null`.
*/
export function loadApiVersion(): Promise<ApiVersionInfo | null> {
if (!apiVersionPromise) {
apiVersionPromise = fetch(`${API_URL}/health/version`, { credentials: 'include' })
.then((res) => (res.ok ? (res.json() as Promise<ApiVersionInfo>) : null))
.catch(() => null);
}
return apiVersionPromise;
}