From cdb571c509b816fc86681a4196885ba0e8c75cde Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 14 Sep 2026 15:32:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(quick-260914-ku1):=20Versionsstempel=20?= =?UTF-8?q?=E2=80=94=20GET=20/health/version=20aus=20APP=5F*,=20VersionRes?= =?UTF-8?q?ponse,=20app-version.ts=20und=20Abzeichen=20v=20=C2=B7?= =?UTF-8?q?=20=20in=20der=20Seitenleiste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 () " 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) Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY --- apps/api/src/health/app-version.ts | 32 ++++++ apps/api/src/health/health.controller.spec.ts | 98 +++++++++++++++++++ apps/api/src/health/health.controller.ts | 13 +-- apps/api/src/main.ts | 2 + .../layout/app-version-badge.test.tsx | 98 +++++++++++++++++++ .../components/layout/app-version-badge.tsx | 37 +++++++ .../src/components/layout/sidebar.test.tsx | 17 ++++ apps/web/src/components/layout/sidebar.tsx | 8 ++ apps/web/src/lib/app-version.test.ts | 84 ++++++++++++++++ apps/web/src/lib/app-version.ts | 63 ++++++++++++ apps/web/src/messages/de.json | 7 +- apps/web/src/messages/en.json | 7 +- packages/shared/src/index.ts | 13 +++ 13 files changed, 471 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/health/app-version.ts create mode 100644 apps/api/src/health/health.controller.spec.ts create mode 100644 apps/web/src/components/layout/app-version-badge.test.tsx create mode 100644 apps/web/src/components/layout/app-version-badge.tsx create mode 100644 apps/web/src/lib/app-version.test.ts create mode 100644 apps/web/src/lib/app-version.ts diff --git a/apps/api/src/health/app-version.ts b/apps/api/src/health/app-version.ts new file mode 100644 index 0000000..f49defc --- /dev/null +++ b/apps/api/src/health/app-version.ts @@ -0,0 +1,32 @@ +import type { VersionResponse } from '@tessera/shared'; + +/** + * Versionsstempel der API (quick-260914-ku1). + * + * Woher die Werte kommen: das CI-Skript `.gitea/scripts/publish-images.sh` + * berechnet `APP_VERSION` (`git describe --tags --always`), `APP_CHANNEL` + * (`beta` fuer main, `live` fuer Tags v*), `APP_COMMIT` und `APP_BUILD_TIME` + * und uebergibt sie als `--build-arg`; die Runner-Stufe beider Dockerfiles + * setzt sie als `ENV`, sodass sie hier zur Laufzeit lesbar sind. Lokal ohne + * Build-Args greifen die Vorgaben `dev`/`dev`/``/``. + * + * Warum `||` statt `??`: Docker Compose reicht unbelegte Variablen als + * Leerstring weiter — ein leerer Wert muss wie ein fehlender zaehlen. + * + * `main.ts` protokolliert `formatAppVersionLine()` beim Start direkt nach der + * Port-Zeile, `HealthController.getVersion()` liefert `getAppVersion()`. + */ +export function getAppVersion(): VersionResponse { + return { + name: 'tessera', + version: process.env.APP_VERSION || 'dev', + channel: process.env.APP_CHANNEL || 'dev', + commit: process.env.APP_COMMIT || '', + buildTime: process.env.APP_BUILD_TIME || '', + }; +} + +export function formatAppVersionLine(v: VersionResponse = getAppVersion()): string { + const base = `Tessera API ${v.version} (${v.channel})`; + return v.commit ? `${base} ${v.commit}` : base; +} diff --git a/apps/api/src/health/health.controller.spec.ts b/apps/api/src/health/health.controller.spec.ts new file mode 100644 index 0000000..b02b2a7 --- /dev/null +++ b/apps/api/src/health/health.controller.spec.ts @@ -0,0 +1,98 @@ +import 'reflect-metadata'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { IS_PUBLIC_KEY } from '../auth/decorators/public.decorator'; +import { formatAppVersionLine } from './app-version'; +import { HealthController } from './health.controller'; + +/** + * HealthController.spec — legt die Testlage fuer den Health-Bereich aus dem + * Nichts an (quick-260914-ku1, Aufgabe 1: vorher gab es keinen Spec). + * + * Gegenstand ist der Versionsstempel: `GET /health/version` liest + * `APP_VERSION`, `APP_CHANNEL`, `APP_COMMIT`, `APP_BUILD_TIME` aus der + * Laufzeit-Umgebung (gesetzt als `ENV` in der Runner-Stufe beider + * Dockerfiles, befuellt vom CI-Skript `.gitea/scripts/publish-images.sh`). + * Leere Zeichenketten zaehlen wie ungesetzt, weil Docker Compose unbelegte + * Variablen als Leerstring weiterreicht (siehe `migrate-and-start.sh`). + * + * Der Controller hat keine Abhaengigkeiten und wird direkt instanziiert. + */ + +const ALL_VARS = ['APP_VERSION', 'APP_CHANNEL', 'APP_COMMIT', 'APP_BUILD_TIME'] as const; + +afterEach(() => { + vi.unstubAllEnvs(); +}); + +function stubAll(value: string) { + for (const name of ALL_VARS) { + vi.stubEnv(name, value); + } +} + +function stubSample() { + vi.stubEnv('APP_VERSION', 'v1.2.3'); + vi.stubEnv('APP_CHANNEL', 'live'); + vi.stubEnv('APP_COMMIT', 'abc1234'); + vi.stubEnv('APP_BUILD_TIME', '2026-09-14T12:00:00Z'); +} + +describe('HealthController — Versionsstempel (quick-260914-ku1)', () => { + it('Test 1 (check): liefert status ok und einen Zeitstempel, den Date.parse versteht', () => { + const controller = new HealthController(); + const result = controller.check(); + expect(result.status).toBe('ok'); + expect(Number.isNaN(Date.parse(result.timestamp))).toBe(false); + }); + + it('Test 2 (Vorgaben): ohne die vier Variablen liefert getVersion() dev/dev und leere Kennungen', () => { + for (const name of ALL_VARS) { + vi.stubEnv(name, undefined); + } + const controller = new HealthController(); + expect(controller.getVersion()).toEqual({ + name: 'tessera', + version: 'dev', + channel: 'dev', + commit: '', + buildTime: '', + }); + }); + + it('Test 3 (durchgereicht): die vier Variablen kommen woertlich an, name bleibt tessera', () => { + stubSample(); + const controller = new HealthController(); + expect(controller.getVersion()).toEqual({ + name: 'tessera', + version: 'v1.2.3', + channel: 'live', + commit: 'abc1234', + buildTime: '2026-09-14T12:00:00Z', + }); + }); + + it('Test 4 (Compose-Semantik): leere Zeichenketten zaehlen wie ungesetzt', () => { + stubAll(''); + const controller = new HealthController(); + expect(controller.getVersion()).toEqual({ + name: 'tessera', + version: 'dev', + channel: 'dev', + commit: '', + buildTime: '', + }); + }); + + it('Test 5 (formatAppVersionLine): mit Commit angehaengt, ohne Commit kein Leerzeichen am Ende', () => { + stubSample(); + expect(formatAppVersionLine()).toBe('Tessera API v1.2.3 (live) abc1234'); + + stubAll(''); + expect(formatAppVersionLine()).toBe('Tessera API dev (dev)'); + }); + + it('Test 6 (bewusst oeffentlich, T-KU1-03): getVersion und check tragen @Public()', () => { + expect(Reflect.getMetadata(IS_PUBLIC_KEY, HealthController.prototype.getVersion)).toBe(true); + expect(Reflect.getMetadata(IS_PUBLIC_KEY, HealthController.prototype.check)).toBe(true); + }); +}); diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts index faadb52..99d24c7 100644 --- a/apps/api/src/health/health.controller.ts +++ b/apps/api/src/health/health.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get } from '@nestjs/common'; -import type { HealthResponse } from '@tessera/shared'; +import type { HealthResponse, VersionResponse } from '@tessera/shared'; import { Public } from '../auth/decorators/public.decorator'; +import { getAppVersion } from './app-version'; @Controller('health') export class HealthController { @@ -13,12 +14,12 @@ export class HealthController { }; } + // Bewusst oeffentlich (T-KU1-03): Betreiber-Kontrolle per `curl` auf dem + // Server ohne Anmeldung. Es werden keine Versionen von Systemkomponenten + // preisgegeben; das Repository ist privat. Gepinnt durch Spec-Test 6. @Public() @Get('version') - getVersion() { - return { - version: process.env.npm_package_version ?? '0.0.1', - name: 'tessera', - }; + getVersion(): VersionResponse { + return getAppVersion(); } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 62743b2..018d028 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -3,6 +3,7 @@ import { ConfigService } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; +import { formatAppVersionLine } from './health/app-version'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -31,6 +32,7 @@ async function bootstrap() { await app.listen(3001); console.log('Tessera API running on port 3001'); + console.log(formatAppVersionLine()); } bootstrap(); diff --git a/apps/web/src/components/layout/app-version-badge.test.tsx b/apps/web/src/components/layout/app-version-badge.test.tsx new file mode 100644 index 0000000..69c3419 --- /dev/null +++ b/apps/web/src/components/layout/app-version-badge.test.tsx @@ -0,0 +1,98 @@ +import { cleanup, render, screen, waitFor } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +/** + * AppVersionBadge.test — die Versionszeile unten in der Seitenleiste + * (quick-260914-ku1). Vorlage: sidebar.test.tsx (next-intl-Mock, cleanup, + * dynamischer Import nach dem Setzen der Mocks). + */ + +vi.mock('next-intl', () => ({ + useTranslations: (ns: string) => (key: string) => { + const map: Record> = { + sidebar: { + 'channel.beta': 'Beta', + 'channel.live': 'Live', + 'channel.dev': 'Entwicklung', + }, + }; + return map[ns]?.[key] ?? key; + }, +})); + +let mockAppVersion = { version: 'v1.2.3', channel: 'beta' as 'beta' | 'live' | 'dev', commit: 'abc1234' }; +const mockLoad = vi.fn(); + +vi.mock('@/lib/app-version', () => ({ + get appVersion() { + return mockAppVersion; + }, + loadApiVersion: () => mockLoad(), +})); + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); + mockLoad.mockReset(); + mockAppVersion = { version: 'v1.2.3', channel: 'beta', commit: 'abc1234' }; +}); + +async function importBadge() { + const mod = await import('./app-version-badge'); + return mod.AppVersionBadge; +} + +describe('AppVersionBadge (quick-260914-ku1)', () => { + it('Test 1 (Zeile): zeigt Version und uebersetzten Kanal', async () => { + mockLoad.mockResolvedValue(null); + const AppVersionBadge = await importBadge(); + render(); + + expect(screen.getByTestId('app-version')).toHaveTextContent('v1.2.3 · Beta'); + }); + + it('Test 2 (Tooltip mit API): title nennt Commit und API-Version samt Kanal', async () => { + mockLoad.mockResolvedValue({ + name: 'tessera', + version: 'v1.2.3', + channel: 'beta', + commit: 'abc1234', + buildTime: '', + }); + const AppVersionBadge = await importBadge(); + render(); + + await waitFor(() => { + const title = screen.getByTestId('app-version').getAttribute('title') ?? ''; + expect(title).toContain('Commit abc1234'); + expect(title).toContain('API v1.2.3 (beta)'); + }); + }); + + it('Test 3 (Tooltip ohne API): title nennt den Commit, aber keinen API-Teil', async () => { + mockLoad.mockResolvedValue(null); + const AppVersionBadge = await importBadge(); + render(); + + await waitFor(() => { + expect(mockLoad).toHaveBeenCalled(); + }); + const title = screen.getByTestId('app-version').getAttribute('title') ?? ''; + expect(title).toContain('Commit abc1234'); + expect(title).not.toContain('API'); + }); + + it('Test 4 (Kanal dev, kein Commit): zeigt dev · Entwicklung und hat kein title-Attribut', async () => { + mockAppVersion = { version: 'dev', channel: 'dev', commit: '' }; + mockLoad.mockResolvedValue(null); + const AppVersionBadge = await importBadge(); + render(); + + await waitFor(() => { + expect(mockLoad).toHaveBeenCalled(); + }); + const el = screen.getByTestId('app-version'); + expect(el).toHaveTextContent('dev · Entwicklung'); + expect(el.hasAttribute('title')).toBe(false); + }); +}); diff --git a/apps/web/src/components/layout/app-version-badge.tsx b/apps/web/src/components/layout/app-version-badge.tsx new file mode 100644 index 0000000..a41c2cb --- /dev/null +++ b/apps/web/src/components/layout/app-version-badge.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useTranslations } from 'next-intl'; +import { type ApiVersionInfo, appVersion, loadApiVersion } from '@/lib/app-version'; + +/** + * Versionszeile unten in der Seitenleiste: ` · `, z. B. + * `v1.0.0 · Beta` oder lokal `dev · Entwicklung` (quick-260914-ku1). + * Der Tooltip nennt den Web-Commit und, sobald geladen, die API-Version + * samt Kanal. "Commit" und "API" sind in beiden Sprachen gleich. + */ +export function AppVersionBadge() { + const t = useTranslations('sidebar'); + const [api, setApi] = useState(null); + + useEffect(() => { + let active = true; + loadApiVersion().then((info) => { + if (active) setApi(info); + }); + return () => { + active = false; + }; + }, []); + + const parts: string[] = []; + if (appVersion.commit) parts.push(`Commit ${appVersion.commit}`); + if (api) parts.push(`API ${api.version} (${api.channel})`); + const title = parts.length > 0 ? parts.join(' · ') : undefined; + + return ( + + {appVersion.version} · {t(`channel.${appVersion.channel}`)} + + ); +} diff --git a/apps/web/src/components/layout/sidebar.test.tsx b/apps/web/src/components/layout/sidebar.test.tsx index 01d497c..1b852ef 100644 --- a/apps/web/src/components/layout/sidebar.test.tsx +++ b/apps/web/src/components/layout/sidebar.test.tsx @@ -64,6 +64,12 @@ vi.mock('@/components/layout/sidebar-footer', () => ({ SidebarFooter: () =>
, })); +// Das Abzeichen ruft `loadApiVersion()` und damit `fetch` — ohne Modul-Mock +// bekaeme es das Modul-Array aus `stubFetch()` (quick-260914-ku1). +vi.mock('@/components/layout/app-version-badge', () => ({ + AppVersionBadge: () =>
, +})); + const mockActiveModules = [ { id: 'm1', slug: 'domaincheck', name: 'Domaincheck', category: 'Domain-Tools' }, { id: 'm2', slug: 'converter', name: 'Converter', category: 'Utilities' }, @@ -178,4 +184,15 @@ describe('Sidebar', () => { expect(newCallCount).toBeGreaterThan(initialCallCount); }); }); + + it('renders the version badge below the navigation', async () => { + const Sidebar = await importSidebar(); + render(); + + await waitFor(() => { + expect(screen.getByText('Dashboard')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('app-version-badge')).toBeInTheDocument(); + }); }); diff --git a/apps/web/src/components/layout/sidebar.tsx b/apps/web/src/components/layout/sidebar.tsx index d452a1b..5346d47 100644 --- a/apps/web/src/components/layout/sidebar.tsx +++ b/apps/web/src/components/layout/sidebar.tsx @@ -8,6 +8,7 @@ import { TesseraLogo } from '@/components/brand/tessera-logo'; import { useSidebarStore } from '@/lib/stores/sidebar-store'; import { useMarketplaceStore } from '@/lib/stores/marketplace-store'; import { SidebarSearch } from '@/components/layout/sidebar-search'; +import { AppVersionBadge } from '@/components/layout/app-version-badge'; const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; @@ -198,6 +199,13 @@ export function Sidebar() {
+ {/* Versionszeile (quick-260914-ku1): bewusst ohne `hidden md:block`, damit + auch die mobile Schublade sie zeigt; eingeklappt nichts (Muster oben). */} + {!isCollapsed && ( +
+ +
+ )}
); diff --git a/apps/web/src/lib/app-version.test.ts b/apps/web/src/lib/app-version.test.ts new file mode 100644 index 0000000..44ea8b8 --- /dev/null +++ b/apps/web/src/lib/app-version.test.ts @@ -0,0 +1,84 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +/** + * app-version.test — Versionsstempel der Web-Oberflaeche (quick-260914-ku1). + * + * `appVersion` wird beim Laden des Moduls aus `NEXT_PUBLIC_APP_*` gelesen und + * `loadApiVersion()` memoisiert die Antwort von `GET /health/version` — + * deshalb setzt jeder Test die Module zurueck und importiert dynamisch, + * nachdem Umgebung und `fetch` gestubbt sind (Muster: module-access-gate.test.tsx). + */ + +afterEach(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); +}); + +async function importFresh() { + vi.resetModules(); + return import('./app-version'); +} + +describe('app-version (quick-260914-ku1)', () => { + it('Test 1 (Vorgaben): ohne NEXT_PUBLIC_APP_* ist appVersion dev/dev ohne Commit', async () => { + vi.stubEnv('NEXT_PUBLIC_APP_VERSION', undefined); + vi.stubEnv('NEXT_PUBLIC_APP_CHANNEL', undefined); + vi.stubEnv('NEXT_PUBLIC_APP_COMMIT', undefined); + const mod = await importFresh(); + expect(mod.appVersion).toEqual({ version: 'dev', channel: 'dev', commit: '' }); + }); + + it('Test 2 (Umgebung): NEXT_PUBLIC_APP_VERSION/_CHANNEL/_COMMIT werden woertlich durchgereicht', async () => { + vi.stubEnv('NEXT_PUBLIC_APP_VERSION', 'v1.2.3'); + vi.stubEnv('NEXT_PUBLIC_APP_CHANNEL', 'beta'); + vi.stubEnv('NEXT_PUBLIC_APP_COMMIT', 'abc1234'); + const mod = await importFresh(); + expect(mod.appVersion).toEqual({ version: 'v1.2.3', channel: 'beta', commit: 'abc1234' }); + }); + + it('Test 3 (Normalisierung): ein unbekannter Kanal wird zu dev', async () => { + vi.stubEnv('NEXT_PUBLIC_APP_VERSION', 'v1.2.3'); + vi.stubEnv('NEXT_PUBLIC_APP_CHANNEL', 'gamma'); + vi.stubEnv('NEXT_PUBLIC_APP_COMMIT', 'abc1234'); + const mod = await importFresh(); + expect(mod.appVersion.channel).toBe('dev'); + }); + + it('Test 4 (Laden, memoisiert): zwei Aufrufe liefern das Objekt, fetch laeuft genau einmal mit Cookie', async () => { + const payload = { + name: 'tessera', + version: 'v1.2.3', + channel: 'beta', + commit: 'abc1234', + buildTime: 'x', + }; + const fetchMock = vi.fn(() => + Promise.resolve({ ok: true, json: () => Promise.resolve(payload) }), + ); + vi.stubGlobal('fetch', fetchMock); + const mod = await importFresh(); + + const first = await mod.loadApiVersion(); + const second = await mod.loadApiVersion(); + + expect(first).toEqual(payload); + expect(second).toEqual(payload); + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, options] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]; + expect(url.endsWith('/health/version')).toBe(true); + expect(options).toMatchObject({ credentials: 'include' }); + }); + + it('Test 5 (still bei Fehler): Netzfehler und ok=false liefern null, nichts wird geworfen', async () => { + vi.stubGlobal('fetch', vi.fn(() => Promise.reject(new Error('netz')))); + const rejected = await importFresh(); + await expect(rejected.loadApiVersion()).resolves.toBeNull(); + + vi.stubGlobal( + 'fetch', + vi.fn(() => Promise.resolve({ ok: false, json: () => Promise.resolve({}) })), + ); + const notOk = await importFresh(); + await expect(notOk.loadApiVersion()).resolves.toBeNull(); + }); +}); diff --git a/apps/web/src/lib/app-version.ts b/apps/web/src/lib/app-version.ts new file mode 100644 index 0000000..cf91af6 --- /dev/null +++ b/apps/web/src/lib/app-version.ts @@ -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 | 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 { + if (!apiVersionPromise) { + apiVersionPromise = fetch(`${API_URL}/health/version`, { credentials: 'include' }) + .then((res) => (res.ok ? (res.json() as Promise) : null)) + .catch(() => null); + } + return apiVersionPromise; +} diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 4777773..acb58f1 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -104,7 +104,12 @@ "users": "Benutzer", "tenants": "Mandanten", "ldap": "LDAP", - "modules": "Module" + "modules": "Module", + "channel": { + "beta": "Beta", + "live": "Live", + "dev": "Entwicklung" + } }, "dashboard": { "title": "Dashboard", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 2c49292..8eb7286 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -104,7 +104,12 @@ "users": "Users", "tenants": "Tenants", "ldap": "LDAP", - "modules": "Modules" + "modules": "Modules", + "channel": { + "beta": "Beta", + "live": "Live", + "dev": "Development" + } }, "dashboard": { "title": "Dashboard", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9bda3e2..30444cc 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -4,3 +4,16 @@ export interface HealthResponse { status: string; timestamp: string; } + +/** + * Antwort von `GET /health/version` (quick-260914-ku1). + * `channel` ist in der Praxis `beta` | `live` | `dev`; die Wahrheit der + * Version ist der Git-Tag (`git describe`), nicht eine package.json. + */ +export interface VersionResponse { + name: string; + version: string; + channel: string; + commit: string; + buildTime: string; +}