export const APP_NAME = "Tessera"; 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; } /** * Desktop-Pakete (Phase 18, D-08): Quelle ist ausschliesslich `manifest.json`, * geschrieben nur von `.gitea/scripts/desktop-collect.sh` im CI. `platform` ist * ein geschlossener Wertevorrat -- eine dritte Plattform waere eine bewusste * Erweiterung hier und an der Konstante `PLATFORMS` im API-Dienst. */ export type DesktopPlatform = 'windows' | 'linux'; export interface DesktopManifestFile { name: string; size: number; sha256: string; /** * Base64-Inhalt der `.sig`-Datei des Tauri-Bundlers (minisign), geschrieben * von desktop-collect.sh, gelesen nur von `GET /desktop/update`. Fehlt bei * Bauten mit `--no-sign` -- dann gibt es kein Update in der App. */ signature?: string; } export interface DesktopManifest { version: string; channel: string; commit: string; buildTime: string; /** * SemVer-Form, die der Updater im Client vergleicht: `X.Y.Z` bei live, * `X.Y.Z-beta.g` bei beta (Praefix `g` Pflicht -- ein rein numerischer * SHA mit fuehrender Null waere kein gueltiger SemVer-Identifier). */ updateVersion?: string; files: Partial>; } /** * Antwort von `GET /desktop/update` -- das dynamische Antwortformat von * `tauri-plugin-updater`. Die Feldnamen sind vom Plugin vorgegeben, darum * snake_case `pub_date`. `url` muss absolut sein, `signature` ist Pflicht. */ export interface DesktopUpdateResponse { version: string; pub_date?: string; url: string; signature: string; notes?: string; } export interface DesktopLatestFile extends DesktopManifestFile { url: string; } export interface DesktopLatestResponse { version: string; channel: string; commit: string; buildTime: string; files: Partial>; } /** * Dashboard-Kacheln: EINE Typliste fuer Web und API (quick-260922-m1h). * * Vorher stand dieselbe Liste an sieben Stellen (Union-Typ, Constraints, * Registry, Katalog-Liste, `@IsIn`-Whitelist ...). Vergass man eine, fehlte * die Kachel im Katalog oder die API lehnte sie mit 400 ab. Seit m1h leiten * beide Seiten von hier ab: `apps/web/src/components/dashboard/ * widget-registry.tsx` (Registry + Katalog) und * `apps/api/src/dashboard/dto/create-widget.dto.ts` (`@IsIn`). * * ACHTUNG: Dies ist der erste LAUFZEIT-Import aus `@tessera/shared` (alle * uebrigen sind `import type`). `packages/shared` liefert rohes TypeScript * (`main: src/index.ts`, kein Bauschritt); die API laedt es im Betrieb ueber * das native Type-Stripping von Node 24. Deshalb darf diese Datei nur * loeschbare Syntax enthalten — keine `enum`, kein `namespace`, keine * Parameter-Eigenschaften. * * Der zweite Laufzeit-Import sind `parseReleaseVersion`/`compareReleaseVersions` * weiter unten (quick-260925-bow) — dieselbe Regel gilt dort, und neue * Laufzeit-Funktionen gehoeren direkt in diese Datei (CJS-`require` findet * relative Importe ohne `.ts`-Endung nicht). */ export const WIDGET_TYPES = [ 'clock', 'search', 'calendar', 'note', 'calculator', 'favorites', 'stopwatch', 'picture-frame', 'xframe', 'proxmox', ] as const; export type WidgetType = (typeof WIDGET_TYPES)[number]; /** * Kachel → Modul-Slug. Eine Kachel ohne Eintrag ist immer sichtbar; eine * Kachel MIT Eintrag erscheint nur fuer Benutzer, die das Modul nutzen * duerfen — im Katalog (Komfort, `visibleWidgetTypes`) und verbindlich * serverseitig in `DashboardService.getWidgets` (fail-closed). * * Die neun Kacheln von vor quick-260924-i8v sind Plattform-Kacheln ohne * Modulbezug. Proxmox ist die erste modulgebundene Kachel; ihr Slug ist * derselbe wie `@UseModule('proxmox')` im Controller und `slug: 'proxmox'` * in `proxmox.seed.ts`. */ export const WIDGET_MODULE_SLUGS: Partial> = { proxmox: 'proxmox', }; /** * Freigegebene Versionen (quick-260925-bow, D-02/D-06) — EINE Implementierung * fuer API und Web ("Was ist neu"-Fenster nach einem Versionswechsel). * * Laufzeit-Import aus `@tessera/shared` (siehe Warnkommentar ueber * `WIDGET_TYPES`): nur loeschbare Syntax, keine relativen Importe. * * Angenommen werden genau: optionales `v`, drei Zifferngruppen zu je 1 bis 6 * Ziffern, optional der Describe-Anhang von `git describe` * (`--g<4 bis 40 Hex-Zeichen>`). Alles andere ist KEINE freigegebene * Version: `dev`, ein blosser Commit-Stempel, Vorabversionen wie `-rc.1`, * `-dirty`, Leerzeichen am Rand (kein Trimmen). Eingaben ueber 64 Zeichen * werden vor dem Muster abgewiesen; der Ausdruck ist verankert und hat keine * verschachtelten Wiederholungen. */ const RELEASE_VERSION_PATTERN = /^v?(\d{1,6})\.(\d{1,6})\.(\d{1,6})(?:-\d{1,6}-g[0-9a-f]{4,40})?$/; const RELEASE_VERSION_MAX_LENGTH = 64; /** * Liefert die kanonische Form `X.Y.Z` (ohne `v`, ohne fuehrende Nullen) oder * `null`, wenn die Eingabe keine freigegebene Version ist. * Beispiel: `v10.2.3-5-gabc1234` → `10.2.3`, `dev` → `null`. */ export function parseReleaseVersion(raw: string): string | null { if (typeof raw !== 'string' || raw.length > RELEASE_VERSION_MAX_LENGTH) { return null; } const match = RELEASE_VERSION_PATTERN.exec(raw); if (!match) { return null; } return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`; } /** * Numerischer Vergleich zweier freigegebener Versionen: -1, 0 oder 1. * `1.10.0` liegt ueber `1.9.0`. Wirft, wenn eine Seite nicht parsebar ist — * Aufrufer pruefen vorher mit `parseReleaseVersion`. */ export function compareReleaseVersions(a: string, b: string): number { const left = parseReleaseVersion(a); const right = parseReleaseVersion(b); if (left === null || right === null) { throw new Error(`Keine freigegebene Version: ${left === null ? a : b}`); } const l = left.split('.').map(Number); const r = right.split('.').map(Number); for (let i = 0; i < 3; i++) { if (l[i] > r[i]) return 1; if (l[i] < r[i]) return -1; } return 0; } /** * Antwort von `GET /users/me/release-notice` (quick-260925-bow). * `currentRelease` ist die laufende freigegebene Version der API * (`getRunningRelease()`, `null` auf `dev`-Staenden), `lastSeenReleaseVersion` * der gemerkte Stand des angemeldeten Benutzers (`null` = Bestandsbenutzer). */ export interface ReleaseNoticeResponse { currentRelease: string | null; lastSeenReleaseVersion: string | null; } /** * Dashboard-Hintergrund pro Benutzer (quick-260928-ujj) — EINE Pruefregel * fuer API und Web. * * Gespeichert in `User.dashboardBackground` (JSONB), geschrieben nur ueber * `PATCH /users/me/dashboard-background`, gelesen mit der Sitzungsantwort * (`GET /auth/me`, neben `accentColor`). Das Web rendert die Wahl als * CSS-Hintergrund (`url("...")`) — deshalb ist die Pruefung streng * (T-ujj-01): `kind` und Preset-Kennung aus einer festen Liste, `imageId` * nur als UUID (Kennung eines Bilderrahmen-Bildes, `DashboardImage.id`). * * Laufzeit-Import aus `@tessera/shared` (siehe Warnkommentar ueber * `WIDGET_TYPES`): nur loeschbare Syntax, keine relativen Importe. */ export const DASHBOARD_BACKGROUND_PRESET_IDS = [ 'mist', 'pebble', 'bloom', 'dunes', 'mosaic', ] as const; export type DashboardBackgroundPresetId = (typeof DASHBOARD_BACKGROUND_PRESET_IDS)[number]; export type DashboardBackground = | { kind: 'none' } | { kind: 'preset'; id: DashboardBackgroundPresetId } | { kind: 'image'; imageId: string }; const DASHBOARD_BACKGROUND_IMAGE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; /** * Liefert eine gueltige Wahl als FRISCH aufgebautes Objekt (nur die * erlaubten Felder, Zusatzschluessel fallen weg) oder `null`, wenn der Wert * keine gueltige Wahl ist. `null` bedeutet beim Lesen „nie gewaehlt“ und * beim Schreiben „abweisen“. */ export function parseDashboardBackground(value: unknown): DashboardBackground | null { if (typeof value !== 'object' || value === null || Array.isArray(value)) { return null; } const v = value as Record; if (v.kind === 'none') { return { kind: 'none' }; } if (v.kind === 'preset') { const id = v.id; if (typeof id !== 'string') return null; const known = DASHBOARD_BACKGROUND_PRESET_IDS.find((p) => p === id); return known ? { kind: 'preset', id: known } : null; } if (v.kind === 'image') { const imageId = v.imageId; if (typeof imageId !== 'string' || !DASHBOARD_BACKGROUND_IMAGE_ID_PATTERN.test(imageId)) { return null; } return { kind: 'image', imageId }; } return null; } /** * Die fuenf Seitenleisten-Kategorien der Module (quick-260929-9wc). Die * Liste entspricht den Kategorien in den Seeds der eingebauten Module und * den Schluesseln `moduleCategories` in den Uebersetzungen (de.json/en.json); * `apps/web/src/messages/module-categories.spec.ts` haelt den Gleichlauf. * Die API prueft damit die Kategorie eigener Module, das Formular baut die * Auswahl daraus. */ export const MODULE_CATEGORIES = [ "domain-tools", "security-tools", "fleet", "infrastructure", "procurement", ] as const; export type ModuleCategory = (typeof MODULE_CATEGORIES)[number]; /** * Zusaetzliche Kategorie nur fuer eigene Module (Nutzerwunsch 29.09.2026): * „Eigene Module“ sammelt selbst angelegte Eintraege in einer eigenen Gruppe * der Seitenleiste. Wie jede Kategorie erscheint sie nur, wenn ein Eintrag * darin liegt, und steht immer zuletzt. */ export const CUSTOM_MODULE_CATEGORY = "custom-modules" as const; /** Auswahl im Formular „Eigene Module“ und Pruefung in der API. */ export const CUSTOM_MODULE_CATEGORIES = [...MODULE_CATEGORIES, CUSTOM_MODULE_CATEGORY] as const; export type CustomModuleCategory = (typeof CUSTOM_MODULE_CATEGORIES)[number];