jarvis-assist/docs/superpowers/plans/2026-09-13-settings.md

27 KiB

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

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
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

# 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():

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
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:

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:

from fastapi import FastAPI, HTTPException, Depends, Header, Body

Nach get_orders (main.py:1460-1467):

@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
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:

@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):

    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):

    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):

    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):

    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):

    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):

    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):

    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: <module 'main'> 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:

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:

    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
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: <Settings />-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
// 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<SettingsData | null>(null);
  const [error, setError] = useState<string | null>(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 (
      <div className="settings">
        {error ? <p className="error">{error}</p> : <p>Lade Einstellungen...</p>}
      </div>
    );
  }

  return (
    <div className="settings">
      <h2>Einstellungen</h2>
      {error && <p className="error">{error}</p>}
      <label>
        Name des Assistenten
        <input
          value={settings.assistant_name}
          onChange={(e) => updateField("assistant_name", e.target.value)}
        />
      </label>
      <label>
        Firmenname
        <input
          value={settings.company_name}
          onChange={(e) => updateField("company_name", e.target.value)}
        />
      </label>
      <label>
        Kontakt-E-Mail
        <input
          value={settings.contact_email}
          onChange={(e) => updateField("contact_email", e.target.value)}
        />
      </label>
      <button onClick={save} disabled={saving}>
        {saving ? "Speichert..." : "Speichern"}
      </button>
      {saved && <p>Gespeichert.</p>}
    </div>
  );
}
  • Step 2: Commit
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: <Settings /> (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:

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:

  const [assistantName, setAssistantName] = useState<string>("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):

      <nav className="nav">
        <button onClick={() => setView("chat")} disabled={view === "chat"}>Chat</button>
        <button onClick={() => setView("dashboard")} disabled={view === "dashboard"}>Dashboard</button>
        <button onClick={() => setView("settings")} disabled={view === "settings"}>Einstellungen</button>
        <button onClick={toggleMuted} title={muted ? "Sprachausgabe einschalten" : "Sprachausgabe stummschalten"}>
          {muted ? "🔇" : "🔊"}
        </button>
        <button onClick={() => logout()}>Logout</button>
        <WeatherWidget />
      </nav>
      {view === "chat" && <Chat assistantName={assistantName} />}
      {view === "dashboard" && <Dashboard />}
      {view === "settings" && <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:

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:

          placeholder={`Nachricht an ${assistantName}...`}
  • Step 3: CSS ergaenzen

An web/src/index.css anhaengen:

.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
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

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
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
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
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
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.