From 84d828885029552393ad3dbd094b60f433f22726 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 13 Sep 2026 21:55:55 +0200 Subject: [PATCH] docs: add implementation plan for Einstellungen (settings) feature Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V57jSQPqwkGG8BuAXg59X5 --- docs/superpowers/plans/2026-09-13-settings.md | 756 ++++++++++++++++++ 1 file changed, 756 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-13-settings.md diff --git a/docs/superpowers/plans/2026-09-13-settings.md b/docs/superpowers/plans/2026-09-13-settings.md new file mode 100644 index 0000000..8a68142 --- /dev/null +++ b/docs/superpowers/plans/2026-09-13-settings.md @@ -0,0 +1,756 @@ +# JARVIS Einstellungen Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ein Einstellungen-Menuepunkt in JARVIS, ueber den Assistenten-Name, Firmenname und Kontakt-E-Mail (reine Branding-Werte) live aenderbar sind - sofort wirksam im Chat-System-Prompt und im Frontend (Seitentitel, Chat-Platzhalter), ohne Deploy/Neustart. + +**Architecture:** Neue Postgres-Tabelle `settings` (Key-Value, fester Katalog aus drei Keys mit Code-Defaults). Zwei neue admin-key-geschuetzte REST-Endpoints (`GET`/`PUT /api/v1/settings`). Die bisher statische Identitaets-Zeile im Chat-System-Prompt (`CLAUDE_SYSTEM_PROMPT`-Env-Var) wird durch eine dynamisch aus den Settings gebaute Zeile ersetzt, analog zum bestehenden `_current_datetime_context()`-Muster. Frontend bekommt eine neue Settings-Seite plus zwei kleine dynamische Stellen (Seitentitel, Chat-Platzhalter). + +**Tech Stack:** Python 3.11, FastAPI, psycopg2 (Postgres), pytest + pytest-asyncio + unittest.mock. React/Vite, TypeScript. Alles in der bestehenden einzigen Backend-Datei `Claude outputs/main.py` (etabliertes Pattern dieses Projekts). + +**Spec:** `docs/superpowers/specs/2026-09-13-settings-design.md` + +## Global Constraints + +- E-Mailadresse ist ein reiner Anzeige-/Branding-Wert - das technische Postfach (`EMAIL_USER`/`EMAIL_PASSWORD`) bleibt unveraendert ein Deployment-Secret in `.env`. +- Einstellungen sind NUR ueber die Settings-Seite aenderbar, kein Chat-Tool. +- Alle drei Settings-Keys sind fest vorgegeben (`assistant_name`, `company_name`, `contact_email`) - `PUT` mit einem unbekannten Key wird komplett abgelehnt (`400`), auch die gueltigen Keys im selben Request werden dann NICHT gespeichert (alles oder nichts). +- `GET`/`PUT /api/v1/settings` sind wie alle bestehenden `/api/v1/*`-Routen (ausser `/health`) hinter `X-Admin-Key` - der Login-Screen (`Login.tsx`) bleibt bewusst statisch "JARVIS", da er vor dem Login keinen Zugriff auf Settings hat. +- Tests laufen nicht lokal (kein psycopg2-Wheel fuer Python 3.14) - Ausfuehrung ueber den Docker-Testcontainer-Weg aus `JARVIS_HANDOFF.md` ("Backend-Tests lokal ausfuehren"). +- Frontend-Komponenten haben in diesem Projekt keine eigenen Tests (nur `web/src/api.test.ts` testet das API-Modul) - Verifikation ueber `npm run build`. + +--- + +## Task 1: Migration `007_settings.sql` + +**Files:** +- Create: `Claude outputs/migrations/007_settings.sql` + +**Interfaces:** +- Produces: Tabelle `settings (key, value, updated_at)`, auf die Task 2 per SQL zugreift. + +- [ ] **Step 1: Migration schreiben** + +```sql +CREATE TABLE settings ( + key VARCHAR(100) PRIMARY KEY, + value TEXT NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +- [ ] **Step 2: Kein automatisierter Test moeglich** + +Reines SQL-DDL ohne lokale Postgres-Instanz - kann nicht per pytest +verifiziert werden. Anwendung und Verifikation (`\d settings`) passiert in +Task 7 gegen die echte VPS-Datenbank. + +- [ ] **Step 3: Commit** + +```bash +git add "Claude outputs/migrations/007_settings.sql" +git commit -m "feat: add settings table migration" +``` + +--- + +## Task 2: Settings DB-Helper + +**Files:** +- Modify: `Claude outputs/main.py` (neue Konstante + Funktionen direkt nach `get_all_memory_facts()`, main.py:296-304) +- Test: `Claude outputs/tests/test_settings.py` (neu) + +**Interfaces:** +- Consumes: `db_query(query, params, fetch)` (bestehend, main.py:143) +- Produces: `SETTINGS_DEFAULTS: dict`, `get_setting(key: str) -> str`, `get_all_settings() -> dict`, `set_setting(key: str, value: str) -> None` - werden von Task 3 (REST-Endpoints) und Task 4 (System-Prompt) konsumiert. + +- [ ] **Step 1: Failing tests schreiben** + +```python +# Claude outputs/tests/test_settings.py +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +sys.modules["psycopg2"] = MagicMock() +sys.modules["psycopg2.pool"] = MagicMock() +sys.modules["psycopg2.extras"] = MagicMock() + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import pytest + +import main + + +@pytest.mark.asyncio +async def test_get_setting_returns_stored_value(): + with patch.object(main, "db_query", new=AsyncMock(return_value={"value": "Custom GmbH"})) as mock_query: + value = await main.get_setting("company_name") + + assert value == "Custom GmbH" + args, kwargs = mock_query.call_args + assert "SELECT value FROM settings" in args[0] + assert args[1] == ("company_name",) + + +@pytest.mark.asyncio +async def test_get_setting_falls_back_to_default_when_missing(): + with patch.object(main, "db_query", new=AsyncMock(return_value=None)): + value = await main.get_setting("assistant_name") + + assert value == "JARVIS" + + +@pytest.mark.asyncio +async def test_get_all_settings_merges_defaults_with_stored_overrides(): + rows = [{"key": "assistant_name", "value": "FRIDAY"}] + with patch.object(main, "db_query", new=AsyncMock(return_value=rows)): + settings = await main.get_all_settings() + + assert settings == { + "assistant_name": "FRIDAY", + "company_name": "MBO-Tech-IT", + "contact_email": "kontakt@mbo-tech-it.de", + } + + +@pytest.mark.asyncio +async def test_get_all_settings_returns_defaults_when_table_empty(): + with patch.object(main, "db_query", new=AsyncMock(return_value=[])): + settings = await main.get_all_settings() + + assert settings == main.SETTINGS_DEFAULTS + + +@pytest.mark.asyncio +async def test_set_setting_upserts_value(): + with patch.object(main, "db_query", new=AsyncMock(return_value=None)) as mock_query: + await main.set_setting("company_name", "Neue Firma GmbH") + + args, kwargs = mock_query.call_args + assert "INSERT INTO settings" in args[0] + assert "ON CONFLICT (key) DO UPDATE" in args[0] + assert args[1] == ("company_name", "Neue Firma GmbH") +``` + +- [ ] **Step 2: Tests laufen lassen, Fehlschlag pruefen** + +Run: `python -m pytest tests/test_settings.py -v` (im Docker-Testcontainer, +siehe `JARVIS_HANDOFF.md`) +Expected: FAIL mit `AttributeError: module 'main' has no attribute +'get_setting'` (und analog fuer `get_all_settings`/`set_setting`) + +- [ ] **Step 3: Implementierung** + +In `main.py`, direkt nach `get_all_memory_facts()`: + +```python +SETTINGS_DEFAULTS = { + "assistant_name": "JARVIS", + "company_name": "MBO-Tech-IT", + "contact_email": "kontakt@mbo-tech-it.de", +} + + +async def get_setting(key: str) -> str: + row = await db_query("SELECT value FROM settings WHERE key = %s", (key,), fetch="one") + return row["value"] if row else SETTINGS_DEFAULTS[key] + + +async def get_all_settings() -> dict: + rows = await db_query("SELECT key, value FROM settings", fetch="all") + merged = dict(SETTINGS_DEFAULTS) + for row in rows: + merged[row["key"]] = row["value"] + return merged + + +async def set_setting(key: str, value: str): + await db_query( + """ + INSERT INTO settings (key, value) VALUES (%s, %s) + ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP + """, + (key, value), + ) +``` + +- [ ] **Step 4: Tests laufen lassen, Erfolg pruefen** + +Run: `python -m pytest tests/test_settings.py -v` +Expected: 5 PASS + +- [ ] **Step 5: Commit** + +```bash +git add "Claude outputs/main.py" "Claude outputs/tests/test_settings.py" +git commit -m "feat: add settings DB helpers with default fallback" +``` + +--- + +## Task 3: REST-Endpoints `GET`/`PUT /api/v1/settings` + +**Files:** +- Modify: `Claude outputs/main.py` (Import von `Body` ergaenzen, main.py:6; neue Routen nach `get_orders`, main.py:1460-1467) +- Test: `Claude outputs/tests/test_settings.py` (erweitern) + +**Interfaces:** +- Consumes: `get_all_settings`, `set_setting`, `SETTINGS_DEFAULTS` (Task 2), `require_admin_key` (bestehend) +- Produces: `GET /api/v1/settings` -> vollstaendiges Settings-Dict, `PUT /api/v1/settings` -> aktualisiertes Dict oder `400` bei unbekanntem Key. + +- [ ] **Step 1: Failing tests schreiben** + +An `Claude outputs/tests/test_settings.py` anhaengen: + +```python +from fastapi.testclient import TestClient + +main.API_KEY_ADMIN = "test-secret" +HEADERS = {"X-Admin-Key": "test-secret"} + + +@pytest.fixture() +def client(): + with TestClient(main.app) as c: + yield c + + +def test_get_settings_endpoint_returns_merged_settings(client): + settings = {"assistant_name": "JARVIS", "company_name": "MBO-Tech-IT", "contact_email": "kontakt@mbo-tech-it.de"} + with patch.object(main, "get_all_settings", new=AsyncMock(return_value=settings)): + response = client.get("/api/v1/settings", headers=HEADERS) + + assert response.status_code == 200 + assert response.json() == settings + + +def test_get_settings_endpoint_requires_admin_key(client): + response = client.get("/api/v1/settings") + assert response.status_code == 401 + + +def test_put_settings_endpoint_updates_and_returns_merged_settings(client): + updated = {"assistant_name": "FRIDAY", "company_name": "MBO-Tech-IT", "contact_email": "kontakt@mbo-tech-it.de"} + with patch.object(main, "set_setting", new=AsyncMock()) as mock_set, patch.object( + main, "get_all_settings", new=AsyncMock(return_value=updated) + ): + response = client.put("/api/v1/settings", headers=HEADERS, json={"assistant_name": "FRIDAY"}) + + assert response.status_code == 200 + assert response.json() == updated + mock_set.assert_called_once_with("assistant_name", "FRIDAY") + + +def test_put_settings_endpoint_rejects_unknown_key(client): + with patch.object(main, "set_setting", new=AsyncMock()) as mock_set: + response = client.put("/api/v1/settings", headers=HEADERS, json={"nonsense_key": "x"}) + + assert response.status_code == 400 + mock_set.assert_not_called() + + +def test_put_settings_endpoint_requires_admin_key(client): + response = client.put("/api/v1/settings", json={"assistant_name": "X"}) + assert response.status_code == 401 +``` + +- [ ] **Step 2: Tests laufen lassen, Fehlschlag pruefen** + +Run: `python -m pytest tests/test_settings.py -v` +Expected: die 5 neuen Tests FAILEN mit `404 Not Found` (Routen existieren +noch nicht) + +- [ ] **Step 3: Implementierung** + +Import-Zeile in `main.py` (Zeile 6) erweitern: + +```python +from fastapi import FastAPI, HTTPException, Depends, Header, Body +``` + +Nach `get_orders` (main.py:1460-1467): + +```python +@app.get("/api/v1/settings", dependencies=[Depends(require_admin_key)]) +async def get_settings_endpoint(): + return await get_all_settings() + + +@app.put("/api/v1/settings", dependencies=[Depends(require_admin_key)]) +async def update_settings_endpoint(updates: dict = Body(...)): + for key in updates: + if key not in SETTINGS_DEFAULTS: + raise HTTPException(status_code=400, detail=f"Unknown setting: {key}") + for key, value in updates.items(): + await set_setting(key, str(value)) + return await get_all_settings() +``` + +Die Validierung laeuft in einer eigenen Schleife VOR dem Speichern, damit +bei einem unbekannten Key nichts geschrieben wird (alles oder nichts). + +- [ ] **Step 4: Tests laufen lassen, Erfolg pruefen** + +Run: `python -m pytest tests/test_settings.py -v` +Expected: 10 PASS insgesamt + +- [ ] **Step 5: Commit** + +```bash +git add "Claude outputs/main.py" "Claude outputs/tests/test_settings.py" +git commit -m "feat: add GET/PUT /api/v1/settings endpoints" +``` + +--- + +## Task 4: Dynamische Identitaets-Zeile im Chat-System-Prompt + +**Files:** +- Modify: `Claude outputs/main.py` (CLAUDE_SYSTEM_PROMPT main.py:59-62 entfernen, neue Funktion vor `run_chat_completion`, Aufrufstelle main.py:1047-1050 anpassen) +- Test: `Claude outputs/tests/test_settings.py` (erweitern), `Claude outputs/tests/test_chat_tools.py`, `Claude outputs/tests/test_memory.py`, `Claude outputs/tests/test_orders.py` (bestehende `run_chat_completion`-Aufrufe anpassen) + +**Interfaces:** +- Consumes: `get_all_settings` (Task 2) +- Produces: `_identity_system_prompt() -> str` (async!) - ersetzt `CLAUDE_SYSTEM_PROMPT` in `run_chat_completion`. + +**Wichtig:** `_identity_system_prompt()` ruft `get_all_settings()` auf, das +intern `db_query()` nutzt. Jeder bestehende Test, der `run_chat_completion` +aufruft, muss deshalb zusaetzlich `main._identity_system_prompt` mocken - +sonst schlaegt der Test fehl, weil `pg_pool` in der Testumgebung nicht +gesetzt ist (`db_query` wirft dann `RuntimeError: Database not +configured`). Betroffen sind alle 7 bestehenden Aufrufstellen (siehe +Step 1). + +- [ ] **Step 1: Failing Test fuer die neue Funktion schreiben + bestehende Tests anpassen** + +An `Claude outputs/tests/test_settings.py` anhaengen: + +```python +@pytest.mark.asyncio +async def test_identity_system_prompt_uses_settings(): + settings = {"assistant_name": "FRIDAY", "company_name": "Stark Industries", "contact_email": "info@stark.example"} + with patch.object(main, "get_all_settings", new=AsyncMock(return_value=settings)): + prompt = await main._identity_system_prompt() + + assert "FRIDAY" in prompt + assert "Stark Industries" in prompt + assert "info@stark.example" in prompt +``` + +In `Claude outputs/tests/test_chat_tools.py` bei allen 5 Aufrufstellen von +`main.run_chat_completion(...)` das bestehende `with patch.object(main, +"build_memory_context", ...)` um `patch.object(main, +"_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS."))` +ergaenzen. Konkret: + +Zeile 46 (`test_run_chat_completion_without_tool_use`): + +```python + with patch.object(main, "build_memory_context", new=AsyncMock(return_value="")), patch.object( + main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.") + ): + text, output_tokens, total_tokens = await main.run_chat_completion( + [{"role": "user", "content": "Hi"}], conversation_id=1 + ) +``` + +Zeile 71-73 (`test_run_chat_completion_executes_tool_and_returns_followup`): + +```python + with patch.object(main, "list_upcoming_events", new=AsyncMock(return_value=[])), patch.object( + main, "build_memory_context", new=AsyncMock(return_value="") + ), patch.object(main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.")): +``` + +Zeile 109-111 (`test_run_chat_completion_handles_two_sequential_tool_calls`): + +```python + with patch.object(main, "list_upcoming_events", new=AsyncMock(return_value=[{"uid": "abc"}])), patch.object( + main, "update_event", new=AsyncMock(return_value={"uid": "abc"}) + ), patch.object(main, "build_memory_context", new=AsyncMock(return_value="")), patch.object( + main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.") + ): +``` + +Zeile 137-139 (`test_run_chat_completion_lists_recent_emails`): + +```python + with patch.object(main, "list_recent_emails", new=AsyncMock(return_value=[])), patch.object( + main, "build_memory_context", new=AsyncMock(return_value="") + ), patch.object(main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.")): +``` + +Zeile 169-171 (`test_run_chat_completion_includes_current_datetime_in_system_prompt`): + +```python + with patch.object(main, "build_memory_context", new=AsyncMock(return_value="")), patch.object( + main, "_current_datetime_context", return_value="Aktuelles Datum und Uhrzeit: TESTMARKER" + ), patch.object(main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.")): +``` + +In `Claude outputs/tests/test_memory.py`, Zeile 207-209 +(`test_run_chat_completion_includes_memory_context_in_system_prompt`): + +```python + with patch.object( + main, "build_memory_context", new=AsyncMock(return_value="Bekannte Fakten ueber den Nutzer:\n- Hund heisst Bruno") + ), patch.object(main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.")): +``` + +In `Claude outputs/tests/test_orders.py`, Zeile 331 +(`test_run_chat_completion_includes_order_tools_and_instructions`): + +```python + with patch.object(main, "build_memory_context", new=AsyncMock(return_value="")), patch.object( + main, "_identity_system_prompt", new=AsyncMock(return_value="Du bist JARVIS.") + ): +``` + +- [ ] **Step 2: Tests laufen lassen, Fehlschlag pruefen** + +Run: `python -m pytest tests/test_settings.py tests/test_chat_tools.py tests/test_memory.py tests/test_orders.py -v` +Expected: alle 8 betroffenen Tests (der neue +`test_identity_system_prompt_uses_settings` und die 7 angepassten +`run_chat_completion`-Tests) FAILEN mit `AttributeError: +does not have the attribute '_identity_system_prompt'` - `patch.object` +verlangt, dass das Attribut bereits existiert, was erst nach Schritt 3 der +Fall ist. + +- [ ] **Step 3: Implementierung** + +In `main.py`, Zeilen 59-62 (`CLAUDE_SYSTEM_PROMPT = os.getenv(...)`) +komplett entfernen. + +Direkt vor `async def run_chat_completion(...)` (main.py:1043) einfuegen: + +```python +async def _identity_system_prompt() -> str: + settings = await get_all_settings() + return ( + f"Du bist {settings['assistant_name']}, der KI-Assistent von " + f"{settings['company_name']}. Bei Fragen zur Erreichbarkeit kannst " + f"du auf {settings['contact_email']} verweisen." + ) +``` + +In `run_chat_completion` die Zeilen 1047-1050 ersetzen: + +```python + system_prompt = ( + f"{await _identity_system_prompt()}\n\n{_current_datetime_context()}\n\n{CALENDAR_ASSISTANT_INSTRUCTIONS}\n\n" + f"{MEMORY_ASSISTANT_INSTRUCTIONS}\n\n{ORDER_ASSISTANT_INSTRUCTIONS}" + ) +``` + +- [ ] **Step 4: Tests laufen lassen, Erfolg pruefen** + +Run: `python -m pytest tests/ -v` (kompletter Testcontainer-Durchlauf aus +`JARVIS_HANDOFF.md`) +Expected: alle Tests aller Dateien PASS, keine Regression + +- [ ] **Step 5: Commit** + +```bash +git add "Claude outputs/main.py" "Claude outputs/tests/test_settings.py" \ + "Claude outputs/tests/test_chat_tools.py" "Claude outputs/tests/test_memory.py" \ + "Claude outputs/tests/test_orders.py" +git commit -m "feat: build chat identity line from settings instead of a static env var" +``` + +--- + +## Task 5: Frontend `Settings.tsx` + +**Files:** +- Create: `web/src/components/Settings.tsx` + +**Interfaces:** +- Consumes: `apiFetch` (`web/src/api.ts`), `GET`/`PUT /api/v1/settings` (Task 3) +- Produces: ``-Komponente, wird in Task 6 in `App.tsx` eingebunden. + +**Hinweis zu Tests:** Wie `CalendarWidget`/`EmailWidget`/`OrdersWidget` +bekommt auch `Settings.tsx` keinen eigenen Komponenten-Test (Projekt- +Konvention) - Verifikation ueber `npm run build` in Task 6. + +- [ ] **Step 1: Komponente erstellen** + +```tsx +// web/src/components/Settings.tsx +import { useEffect, useState } from "react"; +import { apiFetch } from "../api"; + +type SettingsData = { assistant_name: string; company_name: string; contact_email: string }; + +export default function Settings() { + const [settings, setSettings] = useState(null); + const [error, setError] = useState(null); + const [saving, setSaving] = useState(false); + const [saved, setSaved] = useState(false); + + useEffect(() => { + async function load() { + try { + const response = await apiFetch("/api/v1/settings"); + if (!response.ok) { + setError("Einstellungen nicht verfuegbar"); + return; + } + setSettings(await response.json()); + } catch { + setError("Einstellungen nicht verfuegbar"); + } + } + load(); + }, []); + + function updateField(field: keyof SettingsData, value: string) { + setSettings((prev) => (prev ? { ...prev, [field]: value } : prev)); + setSaved(false); + } + + async function save() { + if (!settings) return; + setSaving(true); + setError(null); + try { + const response = await apiFetch("/api/v1/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings), + }); + if (!response.ok) { + setError("Speichern fehlgeschlagen"); + return; + } + setSettings(await response.json()); + setSaved(true); + } catch { + setError("Speichern fehlgeschlagen"); + } finally { + setSaving(false); + } + } + + if (!settings) { + return ( +
+ {error ?

{error}

:

Lade Einstellungen...

} +
+ ); + } + + return ( +
+

Einstellungen

+ {error &&

{error}

} + + + + + {saved &&

Gespeichert.

} +
+ ); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add web/src/components/Settings.tsx +git commit -m "feat: add Settings component" +``` + +--- + +## Task 6: Frontend-Integration (`App.tsx`, `Chat.tsx`) + +**Files:** +- Modify: `web/src/App.tsx`, `web/src/components/Chat.tsx`, `web/src/index.css` + +**Interfaces:** +- Consumes: `` (Task 5), `apiFetch` (bestehend) +- Produces: neuer Nav-Punkt "Einstellungen", dynamischer Seitentitel und Chat-Platzhalter aus `assistant_name`. + +- [ ] **Step 1: `App.tsx` erweitern** + +`View`-Type und Imports anpassen: + +```tsx +import { useState, useEffect } from "react"; +import { apiFetch, getStoredKey, onUnauthorized, logout } from "./api"; +import { getSpeechMuted, setSpeechMuted } from "./speech"; +import Login from "./components/Login"; +import Chat from "./components/Chat"; +import Dashboard from "./components/Dashboard"; +import Settings from "./components/Settings"; +import WeatherWidget from "./components/WeatherWidget"; + +type View = "chat" | "dashboard" | "settings"; +``` + +Im `App`-Component-Body, nach dem bestehenden `muted`-State, den +Assistentennamen laden und `document.title` setzen: + +```tsx + const [assistantName, setAssistantName] = useState("JARVIS"); + + useEffect(() => { + if (!authenticated) return; + async function loadAssistantName() { + try { + const response = await apiFetch("/api/v1/settings"); + if (!response.ok) return; + const data = await response.json(); + setAssistantName(data.assistant_name); + document.title = data.assistant_name; + } catch { + // Default "JARVIS" bleibt bei Fehlern erhalten. + } + } + loadAssistantName(); + }, [authenticated]); +``` + +Nav-Buttons und View-Rendering anpassen (bestehenden Block ersetzen): + +```tsx + + {view === "chat" && } + {view === "dashboard" && } + {view === "settings" && } +``` + +- [ ] **Step 2: `Chat.tsx` um `assistantName`-Prop erweitern** + +Die Funktionssignatur `export default function Chat() {` (main.py-Aequivalent +`web/src/components/Chat.tsx:40`) aendern zu: + +```tsx +type ChatProps = { assistantName: string }; + +export default function Chat({ assistantName }: ChatProps) { +``` + +Den bestehenden Platzhaltertext (`web/src/components/Chat.tsx:173`, +`placeholder="Nachricht an JARVIS..."`) ersetzen durch: + +```tsx + placeholder={`Nachricht an ${assistantName}...`} +``` + +- [ ] **Step 3: CSS ergaenzen** + +An `web/src/index.css` anhaengen: + +```css +.settings { padding: 1rem; max-width: 400px; } +.settings label { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 1rem; } +.settings input { padding: 0.5rem; } +``` + +- [ ] **Step 4: Build/Typecheck lokal pruefen** + +Run: `cd web && npm run build` +Expected: kein TypeScript-Fehler, Build erfolgreich + +- [ ] **Step 5: Commit** + +```bash +git add web/src/App.tsx web/src/components/Chat.tsx web/src/index.css +git commit -m "feat: wire Settings page and dynamic assistant name into the frontend" +``` + +--- + +## Task 7: Deployment + +**Files:** +- Keine Code-Aenderungen - reine Deployment-Aktion gegen die echte VPS-Infrastruktur (Befehle aus `JARVIS_HANDOFF.md`, Abschnitte "Database Management" und "Quick Reference"). + +**Interfaces:** +- Consumes: alle vorherigen Tasks (fertiger, getesteter Code + Migration) +- Produces: laufendes Feature auf `https://jarvis.mbo-tech-it.de` + +- [ ] **Step 1: Migration auf die VPS kopieren und anwenden** + +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key \ + "Claude outputs/migrations/007_settings.sql" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/migrations/ +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "docker exec -i jarvis-postgres psql -U jarvis -d jarvis < /home/jarvis-core/jarvis/migrations/007_settings.sql" +``` + +- [ ] **Step 2: Migration verifizieren** + +```bash +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "docker exec jarvis-postgres psql -U jarvis -d jarvis -c '\d settings'" +``` + +Expected: Tabelle mit den erwarteten Spalten wird angezeigt. + +- [ ] **Step 3: Backend deployen** + +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key \ + "Claude outputs/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api" +``` + +- [ ] **Step 4: Frontend deployen** + +```bash +scp -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key -r \ + web/src jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/web/ +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "cd /home/jarvis-core/jarvis && docker compose build jarvis-web && docker compose up -d jarvis-web" +``` + +- [ ] **Step 5: Manueller End-to-End-Test** + +1. `https://jarvis.mbo-tech-it.de` oeffnen, einloggen, "Einstellungen" oeffnen. +2. Assistentenname auf einen Testwert (z.B. "Onza-Bot") aendern, speichern. +3. Pruefen: Browser-Tab-Titel und Chat-Eingabefeld-Platzhalter aktualisieren sich sofort. +4. Im Chat fragen "Wie heisst du?" - Antwort muss den neuen Namen nutzen. +5. Assistentenname wieder auf "JARVIS" zuruecksetzen und speichern. + +- [ ] **Step 6: Logs pruefen** + +```bash +ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98 \ + "docker compose -f /home/jarvis-core/jarvis/docker-compose.yml logs --tail 100 jarvis-api" +``` + +Expected: keine unerwarteten Exceptions.