From 1edb5785e34dca41d1d58b864d82b7ef013f61ff Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 11 Apr 2026 22:36:36 +0200 Subject: [PATCH] Add Health and Spend for Admins --- deploy/README.md | 73 ++---- deploy/README_sql.md | 58 +++++ deploy/patch-i18n-admin-health.mjs | 86 +++++++ src/app/api/admin/health/route.ts | 92 +++++++ src/components/admin/admin-panel.tsx | 360 +++++++++++++++++++++------ src/lib/litellm.ts | 56 ++++- src/messages/de.json | 17 +- src/messages/en.json | 17 +- src/messages/fr.json | 17 +- src/messages/it.json | 17 +- 10 files changed, 667 insertions(+), 126 deletions(-) create mode 100644 deploy/README_sql.md create mode 100644 deploy/patch-i18n-admin-health.mjs create mode 100644 src/app/api/admin/health/route.ts diff --git a/deploy/README.md b/deploy/README.md index 99ebb79..e178a7c 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -1,58 +1,31 @@ -# Session 6.6 — Items 3 & 4: AGENTS.md / TOOLS.md in Wizard + Default Templates +# Session 6.6 — Items 5 & 6: System Health + Spend Column -## Manual Steps (in order) +## Files -### 1. Deploy the updated portal code -Copy the files from this ZIP into your `pieced-portal` repo, overwriting existing files. -All paths match the project structure — drop-in replacements. +| File | Action | What | +|------|--------|------| +| `src/lib/litellm.ts` | REPLACE | Added `listTeams()`, `getLitellmHealth()`, `getGlobalSpend()`, `getPerTeamSpend()` | +| `src/app/api/admin/health/route.ts` | **NEW** | Returns tenant phase counts, aggregate + per-tenant spend, vLLM & LiteLLM health | +| `src/components/admin/admin-panel.tsx` | REPLACE | Added "Health" tab (service indicators, tenant overview, spend cards) + "Spend (CHF)" column in tenants table | +| `patch-i18n-admin-health.mjs` | **RUN ONCE** | Patches all 4 i18n files with new admin keys | -### 2. The DB migration is automatic -The updated `db.ts` adds these idempotently on first query: -- Column `agents_md TEXT` on `tenant_requests` -- Table `workspace_templates` (file_key TEXT PK, content TEXT, updated_at TIMESTAMPTZ) +## Steps -No manual `ALTER TABLE` needed. +1. Drop in the 3 source files (overwrite existing) +2. Run the i18n patcher from the portal root: + ```bash + node patch-i18n-admin-health.mjs + ``` +3. Build and deploy -### 3. Seed the workspace templates -After the portal has started (so the table exists): +## Environment Variables (optional) -```bash -kubectl exec -i portal-db-1 -n portal -- psql -U portal -d portal < seed-workspace-templates.sql -``` +- `VLLM_HEALTH_URL` — defaults to `http://vllm.inference.svc:8000`. Set if your vLLM is elsewhere. +- `LITELLM_INTERNAL_URL` / `LITELLM_MASTER_KEY` — already configured. -Or connect interactively and paste the SQL. +## Notes -### 4. Edit templates as needed -To update a template later, just UPDATE the row: - -```sql -UPDATE workspace_templates -SET content = '# Your new SOUL.md content here...', updated_at = now() -WHERE file_key = 'SOUL.md'; -``` - -The portal reads templates on every wizard load / approval — no restart needed. - ---- - -## File Manifest - -| File | Action | What changed | -|------|--------|-------------| -| `src/lib/workspace-defaults.ts` | **NEW** | Default content fetching from DB + TOOLS.md generation | -| `src/lib/db.ts` | REPLACE | Added `agents_md` column, `workspace_templates` table + CRUD | -| `src/types/index.ts` | REPLACE | Added `agentsMd` to `TenantRequest` and `OnboardingInput` | -| `src/app/api/onboarding/route.ts` | REPLACE | Accepts `agentsMd` field | -| `src/app/api/admin/requests/[id]/approve/route.ts` | REPLACE | Builds all 3 workspace files (SOUL/AGENTS/TOOLS) | -| `src/app/api/workspace-defaults/route.ts` | **NEW** | API to fetch defaults for wizard pre-fill | -| `src/components/onboarding/wizard.tsx` | REPLACE | "Advanced Configuration" accordion with AGENTS.md textarea + readonly TOOLS.md preview | -| `src/messages/{de,en,fr,it}.json` | REPLACE | Added `agentsMd`, `agentsMdHint`, `toolsMd`, `toolsMdHint`, `advancedConfig`, `readonlyNote` | -| `seed-workspace-templates.sql` | **NEW** | SQL to seed default templates | - -## Design Decisions - -- **TOOLS.md is readonly** in both the wizard and the tenant detail page. It's auto-generated from the base template + per-package sections. Users see it but can't edit it. -- **AGENTS.md is editable** in the wizard (under "Advanced Configuration" accordion) and on the tenant detail workspace editor. -- **Templates live in the DB** (`workspace_templates` table) so you can edit them without redeploying. Hardcoded fallbacks exist in `workspace-defaults.ts` in case the DB rows are missing. -- **TOOLS.md is regenerated on approval** based on the packages selected, so it's always consistent with what's actually enabled. -- The workspace editor on the tenant detail page already supports arbitrary `workspaceFiles` keys from the CR spec, so AGENTS.md and TOOLS.md will appear there automatically. TOOLS.md should be made readonly there too — that's a separate small change to the workspace editor component (mark `TOOLS.md` as readonly based on the filename). +- The health API uses `Promise.allSettled` so a single service being down won't break the whole page. +- Per-tenant spend is fetched from LiteLLM's `/team/list` which returns the cumulative `spend` per team. This is mapped to tenant names via `status.litellmTeamId` on the PiecedTenant CR. +- The spend column in the tenants table piggybacks on the same health data — fetched once when switching to the tenants tab. +- If LiteLLM's `/team/list` or `/global/spend` response format differs from what I assumed, you may need to adjust the parsing in `litellm.ts`. The functions have fallbacks for common response shapes. diff --git a/deploy/README_sql.md b/deploy/README_sql.md new file mode 100644 index 0000000..99ebb79 --- /dev/null +++ b/deploy/README_sql.md @@ -0,0 +1,58 @@ +# Session 6.6 — Items 3 & 4: AGENTS.md / TOOLS.md in Wizard + Default Templates + +## Manual Steps (in order) + +### 1. Deploy the updated portal code +Copy the files from this ZIP into your `pieced-portal` repo, overwriting existing files. +All paths match the project structure — drop-in replacements. + +### 2. The DB migration is automatic +The updated `db.ts` adds these idempotently on first query: +- Column `agents_md TEXT` on `tenant_requests` +- Table `workspace_templates` (file_key TEXT PK, content TEXT, updated_at TIMESTAMPTZ) + +No manual `ALTER TABLE` needed. + +### 3. Seed the workspace templates +After the portal has started (so the table exists): + +```bash +kubectl exec -i portal-db-1 -n portal -- psql -U portal -d portal < seed-workspace-templates.sql +``` + +Or connect interactively and paste the SQL. + +### 4. Edit templates as needed +To update a template later, just UPDATE the row: + +```sql +UPDATE workspace_templates +SET content = '# Your new SOUL.md content here...', updated_at = now() +WHERE file_key = 'SOUL.md'; +``` + +The portal reads templates on every wizard load / approval — no restart needed. + +--- + +## File Manifest + +| File | Action | What changed | +|------|--------|-------------| +| `src/lib/workspace-defaults.ts` | **NEW** | Default content fetching from DB + TOOLS.md generation | +| `src/lib/db.ts` | REPLACE | Added `agents_md` column, `workspace_templates` table + CRUD | +| `src/types/index.ts` | REPLACE | Added `agentsMd` to `TenantRequest` and `OnboardingInput` | +| `src/app/api/onboarding/route.ts` | REPLACE | Accepts `agentsMd` field | +| `src/app/api/admin/requests/[id]/approve/route.ts` | REPLACE | Builds all 3 workspace files (SOUL/AGENTS/TOOLS) | +| `src/app/api/workspace-defaults/route.ts` | **NEW** | API to fetch defaults for wizard pre-fill | +| `src/components/onboarding/wizard.tsx` | REPLACE | "Advanced Configuration" accordion with AGENTS.md textarea + readonly TOOLS.md preview | +| `src/messages/{de,en,fr,it}.json` | REPLACE | Added `agentsMd`, `agentsMdHint`, `toolsMd`, `toolsMdHint`, `advancedConfig`, `readonlyNote` | +| `seed-workspace-templates.sql` | **NEW** | SQL to seed default templates | + +## Design Decisions + +- **TOOLS.md is readonly** in both the wizard and the tenant detail page. It's auto-generated from the base template + per-package sections. Users see it but can't edit it. +- **AGENTS.md is editable** in the wizard (under "Advanced Configuration" accordion) and on the tenant detail workspace editor. +- **Templates live in the DB** (`workspace_templates` table) so you can edit them without redeploying. Hardcoded fallbacks exist in `workspace-defaults.ts` in case the DB rows are missing. +- **TOOLS.md is regenerated on approval** based on the packages selected, so it's always consistent with what's actually enabled. +- The workspace editor on the tenant detail page already supports arbitrary `workspaceFiles` keys from the CR spec, so AGENTS.md and TOOLS.md will appear there automatically. TOOLS.md should be made readonly there too — that's a separate small change to the workspace editor component (mark `TOOLS.md` as readonly based on the filename). diff --git a/deploy/patch-i18n-admin-health.mjs b/deploy/patch-i18n-admin-health.mjs new file mode 100644 index 0000000..3005004 --- /dev/null +++ b/deploy/patch-i18n-admin-health.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +/** + * Run: node patch-i18n-admin-health.mjs + * Adds health/spend keys to all 4 message files. + * Run from the pieced-portal root. + */ +import { readFileSync, writeFileSync } from "fs"; + +const newKeys = { + en: { + health: "Health", + serviceHealth: "Service Health", + vllmDescription: "GPU inference engine", + litellmDescription: "LLM proxy & spend tracking", + tenantOverview: "Tenant Overview", + spendOverview: "Spend Overview", + globalSpend: "Global Spend (CHF)", + activeTenants: "Active Tenants", + tenantsWithSpend: "tenants with recorded spend", + refresh: "Refresh", + healthUnavailable: "Health data unavailable.", + loadingHealth: "Loading health data…", + statusHealthy: "Healthy", + statusDown: "Down", + spendChf: "Spend (CHF)", + }, + de: { + health: "Status", + serviceHealth: "Dienststatus", + vllmDescription: "GPU-Inferenz-Engine", + litellmDescription: "LLM-Proxy & Kostenerfassung", + tenantOverview: "Mandanten-Übersicht", + spendOverview: "Kostenübersicht", + globalSpend: "Gesamtkosten (CHF)", + activeTenants: "Aktive Mandanten", + tenantsWithSpend: "Mandanten mit erfassten Kosten", + refresh: "Aktualisieren", + healthUnavailable: "Statusdaten nicht verfügbar.", + loadingHealth: "Statusdaten werden geladen…", + statusHealthy: "OK", + statusDown: "Ausgefallen", + spendChf: "Kosten (CHF)", + }, + fr: { + health: "Santé", + serviceHealth: "Santé des services", + vllmDescription: "Moteur d'inférence GPU", + litellmDescription: "Proxy LLM & suivi des coûts", + tenantOverview: "Aperçu des locataires", + spendOverview: "Aperçu des coûts", + globalSpend: "Coûts globaux (CHF)", + activeTenants: "Locataires actifs", + tenantsWithSpend: "locataires avec dépenses enregistrées", + refresh: "Actualiser", + healthUnavailable: "Données de santé indisponibles.", + loadingHealth: "Chargement des données de santé…", + statusHealthy: "OK", + statusDown: "Hors service", + spendChf: "Coûts (CHF)", + }, + it: { + health: "Stato", + serviceHealth: "Stato dei servizi", + vllmDescription: "Motore di inferenza GPU", + litellmDescription: "Proxy LLM & monitoraggio costi", + tenantOverview: "Panoramica tenant", + spendOverview: "Panoramica costi", + globalSpend: "Costi globali (CHF)", + activeTenants: "Tenant attivi", + tenantsWithSpend: "tenant con spese registrate", + refresh: "Aggiorna", + healthUnavailable: "Dati di stato non disponibili.", + loadingHealth: "Caricamento dati di stato…", + statusHealthy: "OK", + statusDown: "Non disponibile", + spendChf: "Costi (CHF)", + }, +}; + +for (const [lang, keys] of Object.entries(newKeys)) { + const path = `src/messages/${lang}.json`; + const json = JSON.parse(readFileSync(path, "utf8")); + Object.assign(json.admin, keys); + writeFileSync(path, JSON.stringify(json, null, 2) + "\n"); + console.log(`Patched ${path} — added ${Object.keys(keys).length} keys`); +} diff --git a/src/app/api/admin/health/route.ts b/src/app/api/admin/health/route.ts new file mode 100644 index 0000000..313cd38 --- /dev/null +++ b/src/app/api/admin/health/route.ts @@ -0,0 +1,92 @@ +import { NextResponse } from "next/server"; +import { requirePlatformRole } from "@/lib/session"; +import { listTenants } from "@/lib/k8s"; +import { + getLitellmHealth, + getGlobalSpend, + getPerTeamSpend, +} from "@/lib/litellm"; + +const VLLM_URL = + process.env.VLLM_HEALTH_URL ?? "http://vllm-inference.inference.svc:8000"; + +async function checkVllmHealth(): Promise<{ + healthy: boolean; + details?: any; +}> { + try { + const res = await fetch(`${VLLM_URL}/health`, { + signal: AbortSignal.timeout(5000), + }); + if (res.ok) return { healthy: true }; + return { healthy: false, details: `HTTP ${res.status}` }; + } catch (e: any) { + return { healthy: false, details: e.message }; + } +} + +/** + * GET /api/admin/health + * Returns system health overview for the admin panel. + */ +export async function GET() { + try { + await requirePlatformRole(); + } catch { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const [tenants, litellm, vllm, globalSpend, perTeamSpend] = + await Promise.allSettled([ + listTenants(), + getLitellmHealth(), + checkVllmHealth(), + getGlobalSpend(), + getPerTeamSpend(), + ]); + + const allTenants = + tenants.status === "fulfilled" ? tenants.value : []; + + // Count tenants by phase + const phaseCounts: Record = {}; + for (const t of allTenants) { + const phase = t.spec.suspend + ? "Suspended" + : t.status?.phase ?? "Pending"; + phaseCounts[phase] = (phaseCounts[phase] || 0) + 1; + } + + // Build per-tenant spend map (tenantName → spend) + const spendMap: Record = {}; + const teamSpend = + perTeamSpend.status === "fulfilled" ? perTeamSpend.value : new Map(); + for (const t of allTenants) { + const teamId = t.status?.litellmTeamId; + if (teamId && teamSpend.has(teamId)) { + spendMap[t.metadata.name] = teamSpend.get(teamId)!; + } + } + + return NextResponse.json({ + tenants: { + total: allTenants.length, + phases: phaseCounts, + }, + spend: { + global: + globalSpend.status === "fulfilled" ? globalSpend.value : 0, + perTenant: spendMap, + }, + services: { + litellm: + litellm.status === "fulfilled" + ? litellm.value + : { healthy: false, details: "fetch failed" }, + vllm: + vllm.status === "fulfilled" + ? vllm.value + : { healthy: false, details: "fetch failed" }, + }, + }); +} diff --git a/src/components/admin/admin-panel.tsx b/src/components/admin/admin-panel.tsx index 029588f..a1c0eb7 100644 --- a/src/components/admin/admin-panel.tsx +++ b/src/components/admin/admin-panel.tsx @@ -6,9 +6,18 @@ import type { PiecedTenant, TenantRequest } from "@/types"; import { StatusBadge } from "@/components/ui/status-badge"; import Link from "next/link"; -type Tab = "requests" | "tenants"; +type Tab = "requests" | "tenants" | "health"; type RequestFilter = "all" | "pending" | "provisioning" | "approved" | "rejected"; +interface HealthData { + tenants: { total: number; phases: Record }; + spend: { global: number; perTenant: Record }; + services: { + litellm: { healthy: boolean; details?: any }; + vllm: { healthy: boolean; details?: any }; + }; +} + interface AdminPanelProps { initialTenants: PiecedTenant[]; } @@ -30,6 +39,10 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { const [loadingTenants, setLoadingTenants] = useState(false); const [deleteModal, setDeleteModal] = useState(null); + // Health state + const [health, setHealth] = useState(null); + const [loadingHealth, setLoadingHealth] = useState(false); + // Shared const [error, setError] = useState(""); @@ -79,6 +92,34 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { } }, [tab, fetchTenants]); + // ─── Health fetching ─── + const fetchHealth = useCallback(async () => { + setLoadingHealth(true); + try { + const res = await fetch("/api/admin/health"); + if (!res.ok) throw new Error("Failed to fetch health"); + const data = await res.json(); + setHealth(data); + } catch (e: any) { + setError(e.message); + } finally { + setLoadingHealth(false); + } + }, []); + + useEffect(() => { + if (tab === "health") { + fetchHealth(); + } + }, [tab, fetchHealth]); + + // Also fetch health for spend data when on tenants tab + useEffect(() => { + if (tab === "tenants" && !health) { + fetchHealth(); + } + }, [tab, health, fetchHealth]); + // ─── Request actions ─── const handleApprove = async (id: string) => { setActionLoading(id); @@ -212,6 +253,19 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
)} +
{/* Error banner */} @@ -435,6 +489,9 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { {t("packages")} + + {t("spendChf")} + {t("created")} @@ -444,76 +501,85 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { - {tenants.map((tenant) => ( - - - {tenant.metadata.name} - - - {tenant.spec.displayName} - {tenant.spec.suspend && ( - - {t("suspendedBadge")} - - )} - - - - - - {tenant.spec.packages?.join(", ") || "—"} - - - {tenant.metadata.creationTimestamp - ? new Date( - tenant.metadata.creationTimestamp - ).toLocaleDateString() - : "—"} - - -
- - {t("manage")} - - - -
- - - ))} + {tenants.map((tenant) => { + const tenantSpend = + health?.spend?.perTenant?.[tenant.metadata.name]; + return ( + + + {tenant.metadata.name} + + + {tenant.spec.displayName} + {tenant.spec.suspend && ( + + {t("suspendedBadge")} + + )} + + + + + + {tenant.spec.packages?.join(", ") || "—"} + + + {tenantSpend !== undefined + ? `CHF ${tenantSpend.toFixed(2)}` + : "—"} + + + {tenant.metadata.creationTimestamp + ? new Date( + tenant.metadata.creationTimestamp + ).toLocaleDateString() + : "—"} + + +
+ + {t("manage")} + + + +
+ + + ); + })} @@ -522,6 +588,115 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { )} + {/* ───── HEALTH TAB ───── */} + {tab === "health" && ( + <> + {loadingHealth ? ( +
+
+

{t("loadingHealth")}

+
+ ) : health ? ( +
+ {/* Service health indicators */} +
+

+ {t("serviceHealth")} +

+
+ + +
+
+ + {/* Tenant overview */} +
+

+ {t("tenantOverview")} +

+
+ + + + +
+
+ + {/* Spend overview */} +
+

+ {t("spendOverview")} +

+
+
+

+ {t("globalSpend")} +

+

+ CHF {health.spend.global.toFixed(2)} +

+
+
+

+ {t("activeTenants")} +

+

+ {Object.keys(health.spend.perTenant).length} +

+

+ {t("tenantsWithSpend")} +

+
+
+
+ + {/* Refresh button */} +
+ +
+
+ ) : ( +
+

{t("healthUnavailable")}

+
+ )} + + )} + {/* ───── REJECT MODAL ───── */} {rejectModal && (
@@ -596,6 +771,49 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) { ); } +function ServiceCard({ + name, + subtitle, + healthy, + t, +}: { + name: string; + subtitle: string; + healthy: boolean; + t: any; +}) { + return ( +
+
+
+
+
+

{name}

+

{subtitle}

+
+
+ + {healthy ? t("statusHealthy") : t("statusDown")} + +
+
+ ); +} + function RequestStatusBadge({ status }: { status: string }) { const colors: Record = { pending: "bg-blue-400/15 text-blue-400", diff --git a/src/lib/litellm.ts b/src/lib/litellm.ts index 7742617..45c2523 100644 --- a/src/lib/litellm.ts +++ b/src/lib/litellm.ts @@ -47,4 +47,58 @@ export async function getTeamSpendLogsV2( page_size: String(pageSize), }); return litellmFetch(`/spend/logs/v2?${params}`); -} \ No newline at end of file +} + +/** + * Get all teams registered in LiteLLM. + * Returns team_id, spend, max_budget, etc. + */ +export async function listTeams(): Promise { + const data = await litellmFetch("/team/list"); + // LiteLLM returns either an array or { data: [...] } + return Array.isArray(data) ? data : data?.data ?? data?.teams ?? []; +} + +/** + * Get LiteLLM health status. + */ +export async function getLitellmHealth(): Promise<{ + healthy: boolean; + details?: any; +}> { + try { + const data = await litellmFetch("/health"); + return { healthy: true, details: data }; + } catch (e: any) { + return { healthy: false, details: e.message }; + } +} + +/** + * Get global spend across all teams for the current month. + */ +export async function getGlobalSpend(): Promise { + try { + const data = await litellmFetch("/global/spend"); + // LiteLLM returns { spend: number } or similar + if (typeof data === "number") return data; + return data?.spend ?? data?.total_spend ?? 0; + } catch { + return 0; + } +} + +/** + * Fetch per-team spend as a map: teamId → spend (CHF). + * Uses /team/list which includes current spend per team. + */ +export async function getPerTeamSpend(): Promise> { + const teams = await listTeams(); + const map = new Map(); + for (const team of teams) { + const id = team.team_id ?? team.id; + const spend = team.spend ?? 0; + if (id) map.set(id, spend); + } + return map; +} diff --git a/src/messages/de.json b/src/messages/de.json index 12056aa..05f93d0 100644 --- a/src/messages/de.json +++ b/src/messages/de.json @@ -221,6 +221,21 @@ "confirmDelete": "Endgültig löschen", "loadingTenants": "Mandanten werden geladen…", "filter_deleted": "Gelöscht", - "filter_active": "Aktiv" + "filter_active": "Aktiv", + "health": "Status", + "serviceHealth": "Dienststatus", + "vllmDescription": "GPU-Inferenz-Engine", + "litellmDescription": "LLM-Proxy & Kostenerfassung", + "tenantOverview": "Mandanten-Übersicht", + "spendOverview": "Kostenübersicht", + "globalSpend": "Gesamtkosten (CHF)", + "activeTenants": "Aktive Mandanten", + "tenantsWithSpend": "Mandanten mit erfassten Kosten", + "refresh": "Aktualisieren", + "healthUnavailable": "Statusdaten nicht verfügbar.", + "loadingHealth": "Statusdaten werden geladen…", + "statusHealthy": "OK", + "statusDown": "Ausgefallen", + "spendChf": "Kosten (CHF)" } } diff --git a/src/messages/en.json b/src/messages/en.json index 612f6f2..313649a 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -221,6 +221,21 @@ "confirmDelete": "Delete permanently", "loadingTenants": "Loading tenants…", "filter_deleted": "Deleted", - "filter_active": "Active" + "filter_active": "Active", + "health": "Health", + "serviceHealth": "Service Health", + "vllmDescription": "GPU inference engine", + "litellmDescription": "LLM proxy & spend tracking", + "tenantOverview": "Tenant Overview", + "spendOverview": "Spend Overview", + "globalSpend": "Global Spend (CHF)", + "activeTenants": "Active Tenants", + "tenantsWithSpend": "tenants with recorded spend", + "refresh": "Refresh", + "healthUnavailable": "Health data unavailable.", + "loadingHealth": "Loading health data…", + "statusHealthy": "Healthy", + "statusDown": "Down", + "spendChf": "Spend (CHF)" } } diff --git a/src/messages/fr.json b/src/messages/fr.json index d257f36..a1b607e 100644 --- a/src/messages/fr.json +++ b/src/messages/fr.json @@ -221,6 +221,21 @@ "confirmDelete": "Supprimer définitivement", "loadingTenants": "Chargement des locataires…", "filter_deleted": "Supprimé", - "filter_active": "Actif" + "filter_active": "Actif", + "health": "Santé", + "serviceHealth": "Santé des services", + "vllmDescription": "Moteur d'inférence GPU", + "litellmDescription": "Proxy LLM & suivi des coûts", + "tenantOverview": "Aperçu des locataires", + "spendOverview": "Aperçu des coûts", + "globalSpend": "Coûts globaux (CHF)", + "activeTenants": "Locataires actifs", + "tenantsWithSpend": "locataires avec dépenses enregistrées", + "refresh": "Actualiser", + "healthUnavailable": "Données de santé indisponibles.", + "loadingHealth": "Chargement des données de santé…", + "statusHealthy": "OK", + "statusDown": "Hors service", + "spendChf": "Coûts (CHF)" } } diff --git a/src/messages/it.json b/src/messages/it.json index ccfd223..d7c7d13 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -221,6 +221,21 @@ "confirmDelete": "Elimina definitivamente", "loadingTenants": "Caricamento tenant…", "filter_deleted": "Eliminato", - "filter_active": "Attivo" + "filter_active": "Attivo", + "health": "Stato", + "serviceHealth": "Stato dei servizi", + "vllmDescription": "Motore di inferenza GPU", + "litellmDescription": "Proxy LLM & monitoraggio costi", + "tenantOverview": "Panoramica tenant", + "spendOverview": "Panoramica costi", + "globalSpend": "Costi globali (CHF)", + "activeTenants": "Tenant attivi", + "tenantsWithSpend": "tenant con spese registrate", + "refresh": "Aggiorna", + "healthUnavailable": "Dati di stato non disponibili.", + "loadingHealth": "Caricamento dati di stato…", + "statusHealthy": "OK", + "statusDown": "Non disponibile", + "spendChf": "Costi (CHF)" } }