Update .gitignore to exclude Python-related files and directories. Enhance AGENTS.md and README.md with clarifications on documentation structure and product identity. Add technical architecture references and local setup instructions. Revise privacy guardrails and product identity documents for consistency. Remove obsolete memory and context documentation. Expand reflection spaces documentation to define context boundaries and lifecycle. Ensure all changes align with the ongoing development of the Kanshō project.

This commit is contained in:
Lars 2026-08-21 15:48:43 +02:00
parent 3a367b7bcc
commit d62673e48b
89 changed files with 13463 additions and 241 deletions

View File

@ -0,0 +1,6 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "Kansho"
[setup]
script = ""

View File

@ -16,6 +16,7 @@ Invariante: Identität bleibt lokal. Externe Intelligenz erhält nur den notwend
- Fail Closed: kein stillschweigender Fallback auf unsichere Provider.
- LLM-Egress und Tool-/Web-Egress sind getrennte Policies.
- Antworten lokal validieren und erst dann demaskieren.
- Guardrails haben Vorrang vor Modellqualität, Kosten, Latenz und Komfort; kein Abschalten des Gateway über Admin-Prompts oder Feature-Flags.
## Datenklassen

View File

@ -17,9 +17,9 @@ Nicht bauen oder vorschlagen als: Tagebuch-App mit KI, Meditations-App, Mood Tra
- Einfache UX, intern differenzierte Modelle. Keine Vereinfachung nur weil Implementierung leichter wird.
- Primäre Startlogik: Kontinuität des Dialogs, kein Funktions-Dashboard.
## Technik (entschieden, Stack offen)
## Technik
PWA, Mobile First, responsive Desktop, Offline und Transkription sind Anforderungen. UI-Framework, Datenbank und KI-Runtime nicht stillschweigend festlegen.
Der Produktrahmen folgt Mitai weitgehend (React/Vite-PWA, FastAPI, PostgreSQL, Server-Sessions, Shell-Mechanik, Registry, Prompt-/Workflow-Engine, Data Layer). Der fachliche Konzeptionsstand ist nicht final; Dialog-IA, Memory-Schema, konkrete Prompts und MVP nicht stillschweigend festlegen.
## Quelle

8
.gitignore vendored
View File

@ -32,7 +32,15 @@ Thumbs.db
.vscode/*
!.vscode/extensions.json
# Python
.venv/
**/__pycache__/
*.py[cod]
.pytest_cache/
# Test / local data
tmp/
temp/
data/local/
backend/data/
*.db

View File

@ -4,9 +4,10 @@ Kanshō ist der dialogische Reflexionsraum der Jinkendo-Familie. Die fachliche D
## Vor jeder größeren Arbeit
1. `documentation_index.md` lesen und nur das passende Context Bundle laden.
2. Root-Dokumente plus 24 betroffene Fachkapitel, nicht den ganzen Ordner.
1. `documentation_index.md` (fachlich und bei Umsetzung `docs/architecture/technical/documentation_index.md`) lesen und nur das passende Context Bundle laden.
2. Root-Dokumente plus 24 betroffene Kapitel, nicht den ganzen Ordner.
3. Statuswörter ernst nehmen: Entschieden, Bevorzugte Richtung, Hypothese, Offen, Verworfen.
4. Technische Kapitel ersetzen keine Fachtexte. Dialog-/MVP-Stand ist nicht final. Datenschutz und Guardrails (`guardrails.md`) sind verbindlich; der Produktrahmen folgt Mitai, der LLM-Pfad nur über das Privacy Gateway.
## Produktgrenzen
@ -27,4 +28,4 @@ Bestehende Kapitel nicht stillschweigend kürzen. Änderungen additiv, explizit
## Privacy
Identität bleibt lokal. Persönlicher Kontext geht nie direkt an externe Modelle. Details: `guardrails.md`.
Identität bleibt lokal. Persönlicher Kontext geht nie direkt an externe Modelle. Guardrails haben Vorrang vor Modellqualität und Komfort. Details: `docs/architecture/functional/guardrails.md` und `docs/architecture/technical/privacy_gateway.md`.

View File

@ -8,14 +8,15 @@ Kanshō ist **kein** Tagebuch mit KI, keine Meditations-App und kein generischer
## Aktueller Stand
Dieses Repository enthält derzeit die **fachliche Konzeption**. Es gibt noch keine Anwendungs-Codebasis.
Fachliche Konzeption und technische Rahmenarchitektur plus ein **lokales Produktrahmen-Gerüst** (`frontend/`, `backend/`). Das Gerüst ist Auth, PWA-Shell und Privacy-Gateway-Stub noch kein Reflexions-MVP.
| Bereich | Stand |
|---|---|
| Produktidentität | entschieden: Personal Reflection Companion |
| Technische Form | entschieden: PWA, Mobile First, Offline, Transkription |
| Fachliche Doku | Baseline in `docs/architecture/functional/` |
| App / MVP | noch nicht begonnen |
| Technische Doku | vorläufiger Rahmen in `docs/architecture/technical/` |
| App | lokaler Produktrahmen, Fachkonzept nicht eingefroren |
Remote: [gitea.stommer.de/Lars/Kansho](https://gitea.stommer.de/Lars/Kansho.git)
@ -24,15 +25,41 @@ Remote: [gitea.stommer.de/Lars/Kansho](https://gitea.stommer.de/Lars/Kansho.git)
Nicht alle Kapitel gleichzeitig in den Kontext ziehen. Der Index beschreibt sinnvolle Bundles:
- `docs/architecture/functional/documentation_index.md`
- `docs/architecture/technical/documentation_index.md`
Führende Root-Dokumente:
1. `docs/architecture/functional/fachliche_zielarchitektur.md`
2. `docs/architecture/functional/produktvision_und_produktidentitaet.md`
3. `docs/architecture/functional/interview_plan.md`
4. `docs/architecture/technical/technische_zielarchitektur.md`
Nächster Konzeptblock: Abschluss und Outputs der **Tagesreflexion** (siehe Interviewplan).
## Lokal starten
Ohne Docker. SQLite lokal, PostgreSQL bleibt das spätere Ziel. Python 3.12 unter `%LOCALAPPDATA%\Programs\Python\Python312\python.exe`, falls der Store-Alias `python` stört.
```powershell
# einmalig
.\scripts\dev-setup.ps1
# Terminal 1
cd backend
.\.venv\Scripts\python -m uvicorn main:app --reload --port 8018
# Terminal 2
cd frontend
npm run dev
```
Kanshō nutzt eigene lokale Ports, nicht die Vite-/FastAPI-Defaults: Frontend **5188**, Backend **8018** (`strictPort`, kein Ausweichen auf 5173/5174). Dann http://localhost:5188 erster Start legt das Admin-Profil an. Frame-Test:
```powershell
cd backend
.\.venv\Scripts\python tests\test_frame.py
```
## Lokal weiterarbeiten
```powershell
@ -51,9 +78,8 @@ git push
Projektregeln liegen in `.cursor/rules/` und in `AGENTS.md`. Sie halten Produktgrenzen, Dokumentationsprinzipien und Privacy-Guardrails im Agenten-Kontext.
Noch **nicht** entschieden und deshalb nicht stillschweigend festlegen:
Der Produktrahmen (React/Vite-PWA, FastAPI, PostgreSQL, Mitai-Auth- und Shell-Muster) ist in der technischen Zielarchitektur festgehalten und darf weitgehend von Mitai kopiert werden. Der fachliche Konzeptionsstand ist nicht final. Weiterhin nicht stillschweigend festlegen:
- UI-Framework / konkreter PWA-Stack
- Datenbank und Sync
- genaue Offline-/KI-Ausprägung
- MVP-Schnitt
- Kanshō-Start-IA, Dialog- und Memory-Schemas
- Offline-Sync und konkrete Ports/Domains
- AI-Agentenzerlegung, Integrationsverträge, MVP-Schnitt

71
backend/auth.py Normal file
View File

@ -0,0 +1,71 @@
"""Session auth. Identity always comes from the session, never from a client profile header."""
from __future__ import annotations
import secrets
from datetime import datetime, timedelta
from typing import Optional
import bcrypt
from fastapi import Header, HTTPException
from db import get_db, row_to_dict
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
def verify_password(password: str, stored_hash: str) -> bool:
if not stored_hash:
return False
try:
return bcrypt.checkpw(password.encode(), stored_hash.encode())
except Exception:
return False
def make_token() -> str:
return secrets.token_urlsafe(32)
def get_session(token: str) -> dict | None:
if not token:
return None
with get_db() as conn:
row = conn.execute(
"""
SELECT s.token, s.profile_id, s.expires_at, p.role, p.name, p.email
FROM sessions s
JOIN profiles p ON p.id = s.profile_id
WHERE s.token = ? AND s.expires_at > datetime('now')
""",
(token,),
).fetchone()
return row_to_dict(row)
def require_auth(x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token")):
session = get_session(x_auth_token)
if not session:
raise HTTPException(401, "Nicht eingeloggt")
return session
def require_admin_dep(x_auth_token: Optional[str] = Header(default=None, alias="X-Auth-Token")):
session = get_session(x_auth_token)
if not session:
raise HTTPException(401, "Nicht eingeloggt")
if session.get("role") != "admin":
raise HTTPException(403, "Keine Berechtigung")
return session
def create_session(profile_id: str, session_days: int = 30) -> tuple[str, datetime]:
token = make_token()
expires = datetime.utcnow() + timedelta(days=session_days)
with get_db() as conn:
conn.execute(
"INSERT INTO sessions (token, profile_id, expires_at) VALUES (?, ?, ?)",
(token, profile_id, expires.isoformat(sep=" ")),
)
return token, expires

View File

@ -0,0 +1,20 @@
{
"kinds": [
{
"id": "working_context_snapshot",
"description": "Kurzlebiger Arbeitsstand einer Conversation. Keine bestätigte Erkenntnis."
},
{
"id": "thread_memory",
"description": "Quellengebundene Verdichtung eines Fadens. Ersetzt das Original nicht."
},
{
"id": "thread_candidate",
"description": "Intern markierter möglicher Faden. Nicht automatisch sichtbar oder bestätigt."
},
{
"id": "hypothesis",
"description": "Vorläufige Deutung. Nicht stillschweigend ins Self Model schreiben."
}
]
}

View File

@ -0,0 +1,24 @@
{
"tiers": [
{
"id": "local",
"name": "Lokal",
"description": "Entwicklungs- und Self-Host-Instanz. Keine Zahlungsanbindung.",
"sort_order": 0
}
],
"features": [
{
"id": "ai_calls",
"name": "KI-Aufrufe",
"description": "Ausführung der Prompt-Engine. Jeder Call läuft über das Privacy Gateway.",
"category": "platform",
"limit_type": "count",
"default_limit": null,
"reset_period": "day"
}
],
"tier_limits": [
{ "tier_id": "local", "feature_id": "ai_calls", "limit_value": null }
]
}

View File

@ -0,0 +1 @@
[]

View File

@ -0,0 +1,14 @@
"""Internal context for later Gateway use. Never sends originals to a provider."""
from __future__ import annotations
from data_layer import read
def build_internal_context(profile_id: str, conversation_id: str) -> dict:
messages = read("conversation_messages", profile_id=profile_id, context={"conversation_id": conversation_id})
return {
"conversation_id": conversation_id,
"messages": messages,
"egress": False,
"note": "Volltext bleibt lokal. Minimierung und Egress nur über das Privacy Gateway.",
}

49
backend/continuity.py Normal file
View File

@ -0,0 +1,49 @@
"""Continuity checkpoint. No LLM. Session end must not mutate threads."""
from __future__ import annotations
from db import get_db, row_to_dict
from dialogue_store import StoreError, end_usage_session, list_messages, thread_snapshot
def checkpoint_usage_session(profile_id: str, usage_session_id: str) -> dict:
"""Ensure originals are stored. Does not insert summaries or change thread status."""
threads_before = thread_snapshot(profile_id)
with get_db() as conn:
session = row_to_dict(
conn.execute(
"SELECT * FROM usage_sessions WHERE id = ? AND profile_id = ?",
(usage_session_id, profile_id),
).fetchone()
)
if not session:
raise StoreError("not_found", "Nutzungssitzung nicht gefunden", 404)
conv_rows = conn.execute(
"SELECT id FROM conversations WHERE profile_id = ? AND usage_session_id = ?",
(profile_id, usage_session_id),
).fetchall()
conversations = []
message_count = 0
for row in conv_rows:
messages = list_messages(profile_id, row["id"])
message_count += len(messages)
conversations.append({"id": row["id"], "message_count": len(messages)})
if thread_snapshot(profile_id) != threads_before:
raise StoreError("thread_mutated", "Checkpoint darf Thread-Felder nicht ändern", 500)
return {
"usage_session_id": usage_session_id,
"ended_at": session.get("ended_at"),
"conversations": conversations,
"message_count": message_count,
"threads_unchanged": True,
"llm": False,
}
def close_usage_session(profile_id: str, usage_session_id: str) -> dict:
threads_before = thread_snapshot(profile_id)
ended = end_usage_session(profile_id, usage_session_id)
checkpoint = checkpoint_usage_session(profile_id, usage_session_id)
if thread_snapshot(profile_id) != threads_before:
raise StoreError("thread_mutated", "Session-Ende darf Thread-Status nicht ändern", 500)
checkpoint["usage_session"] = ended
return checkpoint

20
backend/data_layer.py Normal file
View File

@ -0,0 +1,20 @@
"""Layer 1 surface. Domain derivations register here later. No prompt text, no React payloads."""
from __future__ import annotations
from typing import Any, Callable
_READERS: dict[str, Callable[..., Any]] = {}
def register_reader(key: str, reader: Callable[..., Any]) -> None:
_READERS[key] = reader
def read(key: str, *, profile_id: str, context: dict[str, Any] | None = None) -> Any:
if key not in _READERS:
raise KeyError(key)
return _READERS[key](profile_id=profile_id, context=context or {})
def list_readers() -> list[str]:
return sorted(_READERS)

View File

@ -0,0 +1,25 @@
"""Layer-1 readers over dialogue originals and derived records. No formulas."""
from __future__ import annotations
from data_layer import register_reader
from dialogue_store import latest_derived, list_messages
def _conversation_messages(*, profile_id: str, context: dict):
conversation_id = context.get("conversation_id")
if not conversation_id:
raise KeyError("conversation_id")
return list_messages(profile_id, conversation_id)
def _latest_derived(*, profile_id: str, context: dict):
return latest_derived(
profile_id,
context.get("subject_type") or "",
context.get("subject_id") or "",
context.get("kind") or "",
)
register_reader("conversation_messages", _conversation_messages)
register_reader("latest_derived", _latest_derived)

153
backend/db.py Normal file
View File

@ -0,0 +1,153 @@
"""SQLite persistence for the local frame. PostgreSQL remains the later target."""
from __future__ import annotations
import json
import os
import sqlite3
from contextlib import contextmanager
from pathlib import Path
DATA_DIR = Path(__file__).resolve().parent / "data"
SCHEMA_PATH = Path(__file__).resolve().parent / "schema.sql"
SEED_PATH = Path(__file__).resolve().parent / "config" / "platform_seed.json"
PROMPTS_SEED_PATH = Path(__file__).resolve().parent / "config" / "prompts.seed.json"
_env_db = os.environ.get("KANSHO_DB_PATH")
DB_PATH = Path(_env_db) if _env_db else DATA_DIR / "kansho.sqlite"
_PROFILE_COLUMNS = {
"status": "TEXT NOT NULL DEFAULT 'active'",
"tier_id": "TEXT NOT NULL DEFAULT 'local'",
}
_PROMPT_COLUMNS = {
"description": "TEXT NOT NULL DEFAULT ''",
"category": "TEXT NOT NULL DEFAULT 'uncategorized'",
"stages_json": "TEXT",
"graph_json": "TEXT",
"output_format": "TEXT NOT NULL DEFAULT 'text'",
"output_schema_json": "TEXT",
"required_feature": "TEXT NOT NULL DEFAULT 'ai_calls'",
"default_template": "TEXT NOT NULL DEFAULT ''",
"sort_order": "INTEGER NOT NULL DEFAULT 0",
"updated": "TEXT NOT NULL DEFAULT (datetime('now'))",
}
def _connect() -> sqlite3.Connection:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH, timeout=30)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
@contextmanager
def get_db():
conn = _connect()
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def row_to_dict(row: sqlite3.Row | None) -> dict | None:
if row is None:
return None
return dict(row)
def _column_names(conn: sqlite3.Connection, table: str) -> set[str]:
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
return {row["name"] for row in rows}
def _ensure_columns(conn: sqlite3.Connection, table: str, columns: dict[str, str]) -> None:
existing = _column_names(conn, table)
for name, ddl in columns.items():
if name not in existing:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {ddl}")
def _mark(conn: sqlite3.Connection, migration_id: str) -> None:
conn.execute(
"INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)",
(migration_id,),
)
def _seed_platform(conn: sqlite3.Connection) -> None:
seed = json.loads(SEED_PATH.read_text(encoding="utf-8"))
for tier in seed.get("tiers", []):
conn.execute(
"""
INSERT OR IGNORE INTO tiers (id, name, description, sort_order)
VALUES (?, ?, ?, ?)
""",
(tier["id"], tier["name"], tier.get("description") or "", tier.get("sort_order") or 0),
)
for feature in seed.get("features", []):
conn.execute(
"""
INSERT OR IGNORE INTO features
(id, name, description, category, limit_type, default_limit, reset_period)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(
feature["id"],
feature["name"],
feature.get("description") or "",
feature.get("category") or "platform",
feature["limit_type"],
feature.get("default_limit"),
feature.get("reset_period") or "none",
),
)
for limit in seed.get("tier_limits", []):
conn.execute(
"""
INSERT OR IGNORE INTO tier_limits (tier_id, feature_id, limit_value)
VALUES (?, ?, ?)
""",
(limit["tier_id"], limit["feature_id"], limit.get("limit_value")),
)
def _seed_prompts(conn: sqlite3.Connection) -> None:
"""Prompts come from JSON/DB, never from Python string literals."""
items = json.loads(PROMPTS_SEED_PATH.read_text(encoding="utf-8"))
for item in items:
conn.execute(
"""
INSERT OR IGNORE INTO ai_prompts
(id, slug, name, description, category, prompt_type, template,
required_feature, is_system_default, default_template)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?)
""",
(
item["id"],
item["slug"],
item["name"],
item.get("description") or "",
item.get("category") or "uncategorized",
item.get("prompt_type") or "base",
item.get("template") or "",
item.get("required_feature") or "ai_calls",
item.get("template") or "",
),
)
def init_db() -> None:
schema = SCHEMA_PATH.read_text(encoding="utf-8")
with get_db() as conn:
conn.executescript(schema)
_seed_platform(conn)
_seed_prompts(conn)
_ensure_columns(conn, "profiles", _PROFILE_COLUMNS)
_ensure_columns(conn, "ai_prompts", _PROMPT_COLUMNS)
_mark(conn, "001_frame")
_mark(conn, "002_platform")
_mark(conn, "003_dialogue_memory")

27
backend/derived_kinds.py Normal file
View File

@ -0,0 +1,27 @@
"""Registered derived-record kinds. No prompt templates, no domain heuristics."""
from __future__ import annotations
import json
from pathlib import Path
KINDS_PATH = Path(__file__).resolve().parent / "config" / "derived_kinds.json"
def load_kinds() -> dict[str, dict]:
payload = json.loads(KINDS_PATH.read_text(encoding="utf-8"))
return {item["id"]: item for item in payload.get("kinds", [])}
def known_kind_ids() -> set[str]:
return set(load_kinds())
def require_kind(kind: str) -> dict:
kinds = load_kinds()
if kind not in kinds:
raise ValueError(kind)
return kinds[kind]
def catalog() -> list[dict]:
return sorted(load_kinds().values(), key=lambda item: item["id"])

364
backend/dialogue_store.py Normal file
View File

@ -0,0 +1,364 @@
"""Layer-0 dialogue store. Profile-scoped. Auth sessions table is untouched."""
from __future__ import annotations
import json
import uuid
from datetime import datetime, timezone
from db import get_db, row_to_dict
from derived_kinds import require_kind
SUBJECT_TYPES = {"conversation", "thread", "space", "usage_session"}
VISIBILITIES = {"internal", "user"}
MESSAGE_ROLES = {"user", "assistant", "system"}
HANDOFF_TARGETS = {"journal", "memory", "knowledge", "action", "mindnet", "obsidian"}
class StoreError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
def _now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
def _parse_ids(raw: str | None) -> list[str]:
if not raw:
return []
data = json.loads(raw)
if not isinstance(data, list):
return []
return [str(item) for item in data]
def _owned(conn, table: str, record_id: str, profile_id: str) -> dict | None:
return row_to_dict(
conn.execute(
f"SELECT * FROM {table} WHERE id = ? AND profile_id = ?",
(record_id, profile_id),
).fetchone()
)
def start_usage_session(profile_id: str, intent: str = "") -> dict:
session_id = str(uuid.uuid4())
with get_db() as conn:
conn.execute(
"INSERT INTO usage_sessions (id, profile_id, intent) VALUES (?, ?, ?)",
(session_id, profile_id, intent or ""),
)
return row_to_dict(conn.execute("SELECT * FROM usage_sessions WHERE id = ?", (session_id,)).fetchone())
def end_usage_session(profile_id: str, usage_session_id: str) -> dict:
with get_db() as conn:
current = _owned(conn, "usage_sessions", usage_session_id, profile_id)
if not current:
raise StoreError("not_found", "Nutzungssitzung nicht gefunden", 404)
if not current.get("ended_at"):
conn.execute(
"UPDATE usage_sessions SET ended_at = ? WHERE id = ? AND profile_id = ?",
(_now(), usage_session_id, profile_id),
)
return row_to_dict(
conn.execute(
"SELECT * FROM usage_sessions WHERE id = ? AND profile_id = ?",
(usage_session_id, profile_id),
).fetchone()
)
def thread_snapshot(profile_id: str) -> list[dict]:
with get_db() as conn:
rows = conn.execute(
"SELECT id, status, visibility, resurfacing_at FROM threads WHERE profile_id = ? ORDER BY created",
(profile_id,),
).fetchall()
return [row_to_dict(row) for row in rows]
def create_conversation(profile_id: str, usage_session_id: str | None = None, title: str = "") -> dict:
conversation_id = str(uuid.uuid4())
with get_db() as conn:
if usage_session_id:
session = _owned(conn, "usage_sessions", usage_session_id, profile_id)
if not session:
raise StoreError("not_found", "Nutzungssitzung nicht gefunden", 404)
conn.execute(
"""
INSERT INTO conversations (id, profile_id, usage_session_id, title)
VALUES (?, ?, ?, ?)
""",
(conversation_id, profile_id, usage_session_id, title or ""),
)
return row_to_dict(conn.execute("SELECT * FROM conversations WHERE id = ?", (conversation_id,)).fetchone())
def get_conversation(profile_id: str, conversation_id: str) -> dict:
with get_db() as conn:
conv = _owned(conn, "conversations", conversation_id, profile_id)
if not conv:
raise StoreError("not_found", "Conversation nicht gefunden", 404)
return conv
def list_conversations(profile_id: str) -> list[dict]:
with get_db() as conn:
rows = conn.execute(
"""
SELECT c.*,
(SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) AS message_count,
(SELECT COUNT(*) FROM derived_records d
WHERE d.profile_id = c.profile_id AND d.subject_type = 'conversation' AND d.subject_id = c.id
) AS derived_count
FROM conversations c
WHERE c.profile_id = ?
ORDER BY c.created DESC
""",
(profile_id,),
).fetchall()
return [row_to_dict(row) for row in rows]
def append_message(
profile_id: str,
conversation_id: str,
body: str,
role: str = "user",
message_id: str | None = None,
) -> dict:
if role not in MESSAGE_ROLES:
raise StoreError("invalid_role", "Nachrichtenrolle muss user, assistant oder system sein")
if not (body or "").strip():
raise StoreError("empty_body", "Nachricht darf nicht leer sein")
requested_id = message_id or str(uuid.uuid4())
with get_db() as conn:
conv = _owned(conn, "conversations", conversation_id, profile_id)
if not conv:
raise StoreError("not_found", "Conversation nicht gefunden", 404)
existing = row_to_dict(conn.execute("SELECT * FROM messages WHERE id = ?", (requested_id,)).fetchone())
if existing:
if existing["profile_id"] != profile_id or existing["conversation_id"] != conversation_id:
raise StoreError("id_conflict", "Message-ID gehört zu einer anderen Conversation", 409)
return existing
seq_row = conn.execute(
"SELECT COALESCE(MAX(seq), 0) AS n FROM messages WHERE conversation_id = ?",
(conversation_id,),
).fetchone()
seq = int(seq_row["n"]) + 1
conn.execute(
"""
INSERT INTO messages (id, profile_id, conversation_id, seq, role, body)
VALUES (?, ?, ?, ?, ?, ?)
""",
(requested_id, profile_id, conversation_id, seq, role, body.strip()),
)
conn.execute(
"UPDATE conversations SET updated = datetime('now') WHERE id = ?",
(conversation_id,),
)
return row_to_dict(conn.execute("SELECT * FROM messages WHERE id = ?", (requested_id,)).fetchone())
def list_messages(profile_id: str, conversation_id: str) -> list[dict]:
with get_db() as conn:
conv = _owned(conn, "conversations", conversation_id, profile_id)
if not conv:
raise StoreError("not_found", "Conversation nicht gefunden", 404)
rows = conn.execute(
"""
SELECT * FROM messages
WHERE conversation_id = ? AND profile_id = ?
ORDER BY seq
""",
(conversation_id, profile_id),
).fetchall()
return [row_to_dict(row) for row in rows]
def create_thread(profile_id: str, title: str = "", status: str = "open", visibility: str = "internal") -> dict:
if visibility not in VISIBILITIES:
raise StoreError("invalid_visibility", "visibility muss internal oder user sein")
thread_id = str(uuid.uuid4())
with get_db() as conn:
conn.execute(
"""
INSERT INTO threads (id, profile_id, title, status, visibility)
VALUES (?, ?, ?, ?, ?)
""",
(thread_id, profile_id, title or "", status or "open", visibility),
)
return row_to_dict(conn.execute("SELECT * FROM threads WHERE id = ?", (thread_id,)).fetchone())
def link_conversation_thread(profile_id: str, conversation_id: str, thread_id: str) -> dict:
with get_db() as conn:
if not _owned(conn, "conversations", conversation_id, profile_id):
raise StoreError("not_found", "Conversation nicht gefunden", 404)
if not _owned(conn, "threads", thread_id, profile_id):
raise StoreError("not_found", "Thread nicht gefunden", 404)
conn.execute(
"""
INSERT OR IGNORE INTO conversation_threads (conversation_id, thread_id, profile_id)
VALUES (?, ?, ?)
""",
(conversation_id, thread_id, profile_id),
)
return {"conversation_id": conversation_id, "thread_id": thread_id}
def create_space(profile_id: str, title: str = "", visibility: str = "internal") -> dict:
if visibility not in VISIBILITIES:
raise StoreError("invalid_visibility", "visibility muss internal oder user sein")
space_id = str(uuid.uuid4())
with get_db() as conn:
conn.execute(
"INSERT INTO spaces (id, profile_id, title, visibility) VALUES (?, ?, ?, ?)",
(space_id, profile_id, title or "", visibility),
)
return row_to_dict(conn.execute("SELECT * FROM spaces WHERE id = ?", (space_id,)).fetchone())
def link_thread_space(profile_id: str, thread_id: str, space_id: str, confidence: float | None = None) -> dict:
with get_db() as conn:
if not _owned(conn, "threads", thread_id, profile_id):
raise StoreError("not_found", "Thread nicht gefunden", 404)
if not _owned(conn, "spaces", space_id, profile_id):
raise StoreError("not_found", "Space nicht gefunden", 404)
conn.execute(
"""
INSERT OR IGNORE INTO thread_spaces (thread_id, space_id, profile_id, confidence)
VALUES (?, ?, ?, ?)
""",
(thread_id, space_id, profile_id, confidence),
)
return {"thread_id": thread_id, "space_id": space_id, "confidence": confidence}
def insert_derived(
profile_id: str,
kind: str,
subject_type: str,
subject_id: str,
source_message_ids: list[str],
body: str = "",
visibility: str = "internal",
confidence: float | None = None,
) -> dict:
try:
require_kind(kind)
except ValueError as exc:
raise StoreError("unknown_kind", f"Derived-Kind ist nicht registriert: {kind}") from exc
if subject_type not in SUBJECT_TYPES:
raise StoreError("invalid_subject", "Ungültiger subject_type")
if visibility not in VISIBILITIES:
raise StoreError("invalid_visibility", "visibility muss internal oder user sein")
ids = [item for item in source_message_ids if item]
if not ids:
raise StoreError("provenance_required", "Derived records brauchen source_message_ids")
record_id = str(uuid.uuid4())
as_of = _now()
with get_db() as conn:
for message_id in ids:
msg = _owned(conn, "messages", message_id, profile_id)
if not msg:
raise StoreError("not_found", f"Quellnachricht nicht gefunden: {message_id}", 404)
conn.execute(
"""
INSERT INTO derived_records
(id, profile_id, kind, subject_type, subject_id, source_message_ids,
as_of, confidence, visibility, body)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
record_id,
profile_id,
kind,
subject_type,
subject_id,
json.dumps(ids),
as_of,
confidence,
visibility,
body or "",
),
)
row = row_to_dict(conn.execute("SELECT * FROM derived_records WHERE id = ?", (record_id,)).fetchone())
row["source_message_ids"] = ids
return row
def latest_derived(profile_id: str, subject_type: str, subject_id: str, kind: str) -> dict | None:
with get_db() as conn:
row = row_to_dict(
conn.execute(
"""
SELECT * FROM derived_records
WHERE profile_id = ? AND subject_type = ? AND subject_id = ? AND kind = ?
ORDER BY created DESC
LIMIT 1
""",
(profile_id, subject_type, subject_id, kind),
).fetchone()
)
if not row:
return None
row["source_message_ids"] = _parse_ids(row.get("source_message_ids"))
return row
def list_derived_for_conversation(profile_id: str, conversation_id: str) -> list[dict]:
with get_db() as conn:
rows = conn.execute(
"""
SELECT * FROM derived_records
WHERE profile_id = ? AND subject_type = 'conversation' AND subject_id = ?
ORDER BY created
""",
(profile_id, conversation_id),
).fetchall()
result = []
for row in rows:
item = row_to_dict(row)
item["source_message_ids"] = _parse_ids(item.get("source_message_ids"))
result.append(item)
return result
def create_handoff(profile_id: str, target: str, source_conversation_id: str | None = None, payload: dict | None = None) -> dict:
if target not in HANDOFF_TARGETS:
raise StoreError("invalid_target", "Ungültiges Handoff-Ziel")
handoff_id = str(uuid.uuid4())
with get_db() as conn:
if source_conversation_id and not _owned(conn, "conversations", source_conversation_id, profile_id):
raise StoreError("not_found", "Conversation nicht gefunden", 404)
conn.execute(
"""
INSERT INTO handoffs (id, profile_id, target, source_conversation_id, payload_json)
VALUES (?, ?, ?, ?, ?)
""",
(handoff_id, profile_id, target, source_conversation_id, json.dumps(payload or {})),
)
return row_to_dict(conn.execute("SELECT * FROM handoffs WHERE id = ?", (handoff_id,)).fetchone())
def inventory(profile_id: str | None = None) -> dict:
params = (profile_id,) if profile_id else ()
with get_db() as conn:
def count(table: str) -> int:
if profile_id:
return conn.execute(f"SELECT COUNT(*) AS n FROM {table} WHERE profile_id = ?", params).fetchone()["n"]
return conn.execute(f"SELECT COUNT(*) AS n FROM {table}").fetchone()["n"]
return {
"usage_sessions": count("usage_sessions"),
"conversations": count("conversations"),
"messages": count("messages"),
"threads": count("threads"),
"spaces": count("spaces"),
"derived_records": count("derived_records"),
"handoffs": count("handoffs"),
}

57
backend/engine.py Normal file
View File

@ -0,0 +1,57 @@
"""Single prompt execution entry. Templates live in the DB; LLM egress only via Privacy Gateway."""
from __future__ import annotations
from typing import Any
import placeholder_system # noqa: F401 registers system keys
import privacy_placeholders
from entitlements import EntitlementError, check_feature_access
from placeholders import PlaceholderError, resolve_template
from privacy_gateway import GatewayRequest, PrivacyGatewayError, complete
class EngineError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
def preview_prompt(prompt: dict, context: dict[str, Any] | None = None) -> dict:
template = prompt.get("template") or ""
if prompt.get("prompt_type") != "base":
raise EngineError("prompt_type_not_ready", "Pipeline- und Workflow-Typen sind vorbereitet, aber noch ohne Executor-Stufen.")
try:
rendered = resolve_template(template, context or {})
privacy_tokens = privacy_placeholders.validate_tokens(template)
except PlaceholderError as exc:
raise EngineError(exc.code, exc.message) from exc
return {
"prompt_id": prompt["id"],
"slug": prompt["slug"],
"rendered": rendered,
"privacy_tokens": privacy_tokens,
"llm": False,
}
def execute_prompt(prompt: dict, profile_id: str, purpose: str, data_class: str, context: dict[str, Any] | None = None) -> dict:
preview = preview_prompt(prompt, context)
feature_id = prompt.get("required_feature") or "ai_calls"
try:
check_feature_access(profile_id, feature_id)
except EntitlementError as exc:
raise EngineError(exc.code, exc.message, exc.status_code) from exc
try:
complete(
GatewayRequest(
prompt_id=prompt["id"],
purpose=purpose,
data_class=data_class,
payload={"template_chars": len(preview["rendered"]), "privacy_tokens": preview["privacy_tokens"]},
)
)
except PrivacyGatewayError as exc:
raise EngineError(exc.code, exc.message, exc.status_code) from exc
raise EngineError("provider_missing", "Gateway erlaubte einen Aufruf ohne konfigurierten Provider", 500)

129
backend/entitlements.py Normal file
View File

@ -0,0 +1,129 @@
"""Feature access. Identity is Auth; this only answers 'may this profile use feature X?'."""
from __future__ import annotations
from datetime import datetime, timezone
from db import get_db, row_to_dict
class EntitlementError(Exception):
def __init__(self, code: str, message: str, status_code: int = 403):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
def _period_key(reset_period: str) -> str:
now = datetime.now(timezone.utc)
if reset_period == "day":
return now.strftime("%Y-%m-%d")
if reset_period == "month":
return now.strftime("%Y-%m")
return "all"
def _effective_limit(profile_id: str, feature_id: str) -> tuple[dict, int | None]:
with get_db() as conn:
feature = row_to_dict(
conn.execute("SELECT * FROM features WHERE id = ? AND active = 1", (feature_id,)).fetchone()
)
if not feature:
raise EntitlementError("unknown_feature", f"Feature ist nicht registriert: {feature_id}")
override = row_to_dict(
conn.execute(
"SELECT limit_value FROM user_feature_restrictions WHERE profile_id = ? AND feature_id = ?",
(profile_id, feature_id),
).fetchone()
)
if override is not None:
return feature, override["limit_value"]
profile = row_to_dict(
conn.execute("SELECT tier_id FROM profiles WHERE id = ?", (profile_id,)).fetchone()
)
tier_id = (profile or {}).get("tier_id") or "local"
tier_limit = row_to_dict(
conn.execute(
"SELECT limit_value FROM tier_limits WHERE tier_id = ? AND feature_id = ?",
(tier_id, feature_id),
).fetchone()
)
if tier_limit is not None:
return feature, tier_limit["limit_value"]
return feature, feature["default_limit"]
def check_feature_access(profile_id: str, feature_id: str) -> dict:
feature, limit = _effective_limit(profile_id, feature_id)
period = _period_key(feature["reset_period"])
with get_db() as conn:
usage = row_to_dict(
conn.execute(
"""
SELECT used FROM user_feature_usage
WHERE profile_id = ? AND feature_id = ? AND period_key = ?
""",
(profile_id, feature_id, period),
).fetchone()
)
used = int((usage or {}).get("used") or 0)
if feature["limit_type"] == "boolean":
allowed = limit is None or int(limit) >= 1
else:
allowed = limit is None or used < int(limit)
if not allowed:
raise EntitlementError("feature_limit", f"Kontingent erschöpft: {feature_id}")
return {
"feature_id": feature_id,
"allowed": True,
"limit": limit,
"used": used,
"period_key": period,
}
def increment_feature_usage(profile_id: str, feature_id: str) -> None:
feature, _limit = _effective_limit(profile_id, feature_id)
period = _period_key(feature["reset_period"])
with get_db() as conn:
conn.execute(
"""
INSERT INTO user_feature_usage (profile_id, feature_id, period_key, used)
VALUES (?, ?, ?, 1)
ON CONFLICT(profile_id, feature_id, period_key)
DO UPDATE SET used = used + 1
""",
(profile_id, feature_id, period),
)
def subscription_for(profile_id: str) -> dict:
with get_db() as conn:
profile = row_to_dict(
conn.execute(
"""
SELECT p.id, p.tier_id, t.name AS tier_name, t.description AS tier_description
FROM profiles p
JOIN tiers t ON t.id = p.tier_id
WHERE p.id = ?
""",
(profile_id,),
).fetchone()
)
features = [row_to_dict(row) for row in conn.execute("SELECT * FROM features WHERE active = 1 ORDER BY id")]
entitlements = []
for feature in features:
try:
entitlements.append(check_feature_access(profile_id, feature["id"]))
except EntitlementError as exc:
entitlements.append({"feature_id": feature["id"], "allowed": False, "code": exc.code})
return {
"profile_id": profile_id,
"tier": {
"id": (profile or {}).get("tier_id"),
"name": (profile or {}).get("tier_name"),
"description": (profile or {}).get("tier_description"),
},
"features": entitlements,
"billing": None,
}

35
backend/main.py Normal file
View File

@ -0,0 +1,35 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from db import init_db
import data_layer_dialogue # noqa: F401
from routers import admin, auth, dialogue, placeholders, prompts, subscription, users
from version import APP_VERSION
app = FastAPI(title="Kanshō", version=APP_VERSION)
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5188", "http://127.0.0.1:5188"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth.router)
app.include_router(users.router)
app.include_router(dialogue.router)
app.include_router(prompts.router)
app.include_router(placeholders.router)
app.include_router(subscription.router)
app.include_router(admin.router)
@app.on_event("startup")
def on_startup():
init_db()
@app.get("/api/health")
def health():
return {"status": "ok", "service": "kansho", "version": APP_VERSION}

View File

@ -0,0 +1,15 @@
"""System context keys only. Domain keys are registered later, never hardcoded into prompts."""
from __future__ import annotations
from datetime import datetime, timezone
from placeholders import Placeholder, register
register(
Placeholder(
key="now",
description="UTC-Zeitstempel der Auflösung. Kein Personenbezug.",
data_class="C",
resolver=lambda _ctx: datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
)
)

71
backend/placeholders.py Normal file
View File

@ -0,0 +1,71 @@
"""Context placeholders {{key}}. Implementations register here; templates never define ad-hoc keys."""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any, Callable
CONTEXT_PATTERN = re.compile(r"\{\{([a-z][a-z0-9_]*)\}\}")
class PlaceholderError(Exception):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
self.message = message
@dataclass(frozen=True)
class Placeholder:
key: str
description: str
data_class: str
resolver: Callable[[dict[str, Any]], Any]
_REGISTRY: dict[str, Placeholder] = {}
def register(placeholder: Placeholder) -> None:
if placeholder.data_class == "A":
raise PlaceholderError(
"class_a_context_key_forbidden",
"Klasse-A-Werte gehören nicht in {{key}}-Kontext. Identität läuft über [[…]].",
)
_REGISTRY[placeholder.key] = placeholder
def get(key: str) -> Placeholder | None:
return _REGISTRY.get(key)
def list_placeholders() -> list[dict]:
return [
{
"key": item.key,
"description": item.description,
"data_class": item.data_class,
"syntax": f"{{{{{item.key}}}}}",
}
for item in sorted(_REGISTRY.values(), key=lambda item: item.key)
]
def extract_keys(template: str) -> list[str]:
return CONTEXT_PATTERN.findall(template or "")
def resolve_template(template: str, context: dict[str, Any]) -> str:
keys = extract_keys(template)
unknown = sorted({key for key in keys if key not in _REGISTRY})
if unknown:
raise PlaceholderError(
"unknown_placeholder",
"Unbekannte Kontext-Platzhalter: " + ", ".join(f"{{{{{key}}}}}" for key in unknown),
)
rendered = template
for key in keys:
spec = _REGISTRY[key]
value = spec.resolver(context)
rendered = rendered.replace("{{" + key + "}}", "" if value is None else str(value))
return rendered

View File

@ -0,0 +1,65 @@
"""Local privacy gateway stub. Personal LLM egress is not allowed to skip this layer."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
class PrivacyGatewayError(Exception):
def __init__(self, code: str, message: str, status_code: int = 503):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
@dataclass
class GatewayRequest:
prompt_id: str | None
purpose: str
data_class: str
payload: dict[str, Any] = field(default_factory=dict)
@dataclass
class GatewayResult:
allowed: bool
reason: str
provider: str | None = None
content: str | None = None
checked_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
ALLOWED_CLASSES = {"A", "B", "C"}
LOCAL_ONLY_CLASS = "A"
def inspect(request: GatewayRequest) -> GatewayResult:
"""Fail closed. No provider is configured in the local frame."""
data_class = (request.data_class or "").upper()
if data_class not in ALLOWED_CLASSES:
return GatewayResult(
allowed=False,
reason="unknown_data_class",
)
if data_class == LOCAL_ONLY_CLASS:
return GatewayResult(
allowed=False,
reason="class_a_never_leaves_local_zone",
)
return GatewayResult(
allowed=False,
reason="no_egress_provider_configured",
)
def complete(request: GatewayRequest) -> GatewayResult:
result = inspect(request)
if not result.allowed:
raise PrivacyGatewayError(
result.reason,
"Persönlicher KI-Aufruf wurde vom Privacy Gateway blockiert. "
"Es ist kein Egress-Provider konfiguriert.",
)
return result

View File

@ -0,0 +1,54 @@
"""Privacy tokens [[TOKEN]] in templates. Distinct from context {{key}}."""
from __future__ import annotations
import re
from placeholders import PlaceholderError
PRIVACY_PATTERN = re.compile(r"\[\[([A-Z][A-Z0-9_]*(?::[A-Z0-9_]+)?)\]\]")
KNOWN_EXACT = {"SELF"}
KNOWN_PREFIXES = ("PERSON:", "PLACE:")
def catalog() -> list[dict]:
return [
{
"token": "[[SELF]]",
"kind": "exact",
"data_class": "B",
"description": "Die reflektierende Person. Mapping bleibt lokal.",
},
{
"token": "[[PERSON:ROLE]]",
"kind": "prefix",
"data_class": "B",
"description": "Andere Person als nicht sprechende Rolle, nicht als Klarname.",
},
{
"token": "[[PLACE:KIND]]",
"kind": "prefix",
"data_class": "B",
"description": "Ort als Kategorie, nicht als Adresse oder Stadtname.",
},
]
def extract_tokens(template: str) -> list[str]:
return PRIVACY_PATTERN.findall(template or "")
def validate_tokens(template: str) -> list[str]:
tokens = extract_tokens(template)
invalid = []
for token in tokens:
if token in KNOWN_EXACT:
continue
if any(token.startswith(prefix) for prefix in KNOWN_PREFIXES):
continue
invalid.append(f"[[{token}]]")
if invalid:
raise PlaceholderError(
"unknown_privacy_placeholder",
"Unbekannte Privacy-Platzhalter: " + ", ".join(sorted(set(invalid))),
)
return [f"[[{token}]]" for token in tokens]

4
backend/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
fastapi==0.115.6
uvicorn[standard]==0.34.0
bcrypt==4.2.1
httpx==0.28.1

View File

@ -0,0 +1 @@
# Kanshō API routers

56
backend/routers/admin.py Normal file
View File

@ -0,0 +1,56 @@
from fastapi import APIRouter, Depends, HTTPException
from auth import require_admin_dep
from db import get_db
from dialogue_store import StoreError, get_conversation, inventory, list_conversations, list_derived_for_conversation, list_messages
from version import APP_VERSION, BUILD_DATE, MODULE_VERSIONS
router = APIRouter(prefix="/api/admin", tags=["admin"])
@router.get("/health")
def admin_health(session: dict = Depends(require_admin_dep)):
with get_db() as conn:
users = conn.execute("SELECT COUNT(*) AS n FROM profiles").fetchone()["n"]
prompts = conn.execute("SELECT COUNT(*) AS n FROM ai_prompts").fetchone()["n"]
features = conn.execute("SELECT COUNT(*) AS n FROM features").fetchone()["n"]
dialogue = inventory()
return {
"ok": True,
"service": "kansho",
"version": APP_VERSION,
"build_date": BUILD_DATE,
"modules": MODULE_VERSIONS,
"inventory": {
"users": users,
"prompts": prompts,
"features": features,
"dialogue": dialogue,
},
"session": {
"profile_id": session["profile_id"],
"role": session["role"],
},
}
@router.get("/dialogue")
def admin_dialogue(session: dict = Depends(require_admin_dep)):
return {
"instance": inventory(),
"own_conversations": list_conversations(session["profile_id"]),
"note": "Fremde Lived Experience ist kein Alltagspfad. Detailansicht nur für das eigene Profil.",
}
@router.get("/dialogue/conversations/{conversation_id}")
def admin_conversation(conversation_id: str, session: dict = Depends(require_admin_dep)):
try:
conv = get_conversation(session["profile_id"], conversation_id)
return {
"conversation": conv,
"original_messages": list_messages(session["profile_id"], conversation_id),
"derived_records": list_derived_for_conversation(session["profile_id"], conversation_id),
}
except StoreError as exc:
raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc

103
backend/routers/auth.py Normal file
View File

@ -0,0 +1,103 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from auth import create_session, hash_password, require_auth, verify_password
from db import get_db, row_to_dict
from version import APP_VERSION
router = APIRouter(prefix="/api/auth", tags=["auth"])
class LoginRequest(BaseModel):
email: str
password: str = Field(min_length=4)
class SetupRequest(BaseModel):
email: str
name: str = Field(min_length=1)
password: str = Field(min_length=4)
def _profile_count() -> int:
with get_db() as conn:
row = conn.execute("SELECT COUNT(*) AS n FROM profiles").fetchone()
return int(row["n"])
@router.get("/status")
def auth_status():
return {
"status": "ok",
"service": "kansho",
"version": APP_VERSION,
"needs_setup": _profile_count() == 0,
}
@router.post("/setup")
def setup(req: SetupRequest):
if _profile_count() > 0:
raise HTTPException(409, "Setup ist bereits abgeschlossen")
profile_id = str(uuid.uuid4())
with get_db() as conn:
conn.execute(
"""
INSERT INTO profiles (id, email, name, password_hash, role)
VALUES (?, ?, ?, ?, 'admin')
""",
(profile_id, req.email.lower().strip(), req.name.strip(), hash_password(req.password)),
)
token, expires = create_session(profile_id)
return {
"token": token,
"profile_id": profile_id,
"name": req.name.strip(),
"role": "admin",
"expires_at": expires.isoformat(),
}
@router.post("/login")
def login(req: LoginRequest):
with get_db() as conn:
prof = row_to_dict(
conn.execute(
"SELECT * FROM profiles WHERE email = ?",
(req.email.lower().strip(),),
).fetchone()
)
if not prof or not verify_password(req.password, prof["password_hash"]):
raise HTTPException(401, "Ungültige Zugangsdaten")
if (prof.get("status") or "active") != "active":
raise HTTPException(401, "Ungültige Zugangsdaten")
token, expires = create_session(prof["id"], prof.get("session_days") or 30)
return {
"token": token,
"profile_id": prof["id"],
"name": prof["name"],
"role": prof["role"],
"expires_at": expires.isoformat(),
}
@router.post("/logout")
def logout(session: dict = Depends(require_auth)):
with get_db() as conn:
conn.execute("DELETE FROM sessions WHERE token = ?", (session["token"],))
return {"ok": True}
@router.get("/me")
def me(session: dict = Depends(require_auth)):
with get_db() as conn:
prof = row_to_dict(
conn.execute("SELECT id, email, name, role, status, tier_id FROM profiles WHERE id = ?", (session["profile_id"],)).fetchone()
)
if not prof:
raise HTTPException(401, "Profil nicht gefunden")
return prof

215
backend/routers/dialogue.py Normal file
View File

@ -0,0 +1,215 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from auth import require_auth
from context_builder import build_internal_context
from continuity import checkpoint_usage_session, close_usage_session
from derived_kinds import catalog
from dialogue_store import (
StoreError,
append_message,
create_conversation,
create_handoff,
create_space,
create_thread,
get_conversation,
insert_derived,
link_conversation_thread,
link_thread_space,
list_conversations,
list_derived_for_conversation,
list_messages,
start_usage_session,
)
router = APIRouter(prefix="/api/dialogue", tags=["dialogue"])
def _http(exc: StoreError):
raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc
class SessionWrite(BaseModel):
intent: str = ""
class ConversationWrite(BaseModel):
usage_session_id: str | None = None
title: str = ""
class MessageWrite(BaseModel):
body: str = Field(min_length=1)
role: str = "user"
id: str | None = None
class ThreadWrite(BaseModel):
title: str = ""
status: str = "open"
visibility: str = "internal"
class SpaceWrite(BaseModel):
title: str = ""
visibility: str = "internal"
class DerivedWrite(BaseModel):
kind: str
subject_type: str
subject_id: str
source_message_ids: list[str]
body: str = ""
visibility: str = "internal"
confidence: float | None = None
class HandoffWrite(BaseModel):
target: str
source_conversation_id: str | None = None
payload: dict = Field(default_factory=dict)
@router.get("/kinds")
def list_kinds(session: dict = Depends(require_auth)):
return catalog()
@router.post("/sessions")
def create_session(req: SessionWrite, session: dict = Depends(require_auth)):
return start_usage_session(session["profile_id"], req.intent)
@router.post("/sessions/{usage_session_id}/end")
def end_session(usage_session_id: str, session: dict = Depends(require_auth)):
try:
return close_usage_session(session["profile_id"], usage_session_id)
except StoreError as exc:
_http(exc)
@router.post("/sessions/{usage_session_id}/checkpoint")
def checkpoint_session(usage_session_id: str, session: dict = Depends(require_auth)):
try:
return checkpoint_usage_session(session["profile_id"], usage_session_id)
except StoreError as exc:
_http(exc)
@router.post("/conversations")
def post_conversation(req: ConversationWrite, session: dict = Depends(require_auth)):
try:
return create_conversation(session["profile_id"], req.usage_session_id, req.title)
except StoreError as exc:
_http(exc)
@router.get("/conversations")
def get_conversations(session: dict = Depends(require_auth)):
return list_conversations(session["profile_id"])
@router.get("/conversations/{conversation_id}")
def get_one_conversation(conversation_id: str, session: dict = Depends(require_auth)):
try:
conv = get_conversation(session["profile_id"], conversation_id)
conv["messages"] = list_messages(session["profile_id"], conversation_id)
conv["derived"] = list_derived_for_conversation(session["profile_id"], conversation_id)
return conv
except StoreError as exc:
_http(exc)
@router.get("/conversations/{conversation_id}/context")
def get_internal_context(conversation_id: str, session: dict = Depends(require_auth)):
try:
return build_internal_context(session["profile_id"], conversation_id)
except StoreError as exc:
_http(exc)
@router.post("/conversations/{conversation_id}/messages")
def post_message(conversation_id: str, req: MessageWrite, session: dict = Depends(require_auth)):
try:
return append_message(session["profile_id"], conversation_id, req.body, req.role, req.id)
except StoreError as exc:
_http(exc)
@router.get("/conversations/{conversation_id}/messages")
def get_messages(conversation_id: str, session: dict = Depends(require_auth)):
try:
return list_messages(session["profile_id"], conversation_id)
except StoreError as exc:
_http(exc)
@router.post("/threads")
def post_thread(req: ThreadWrite, session: dict = Depends(require_auth)):
try:
return create_thread(session["profile_id"], req.title, req.status, req.visibility)
except StoreError as exc:
_http(exc)
@router.post("/conversations/{conversation_id}/threads/{thread_id}")
def post_conversation_thread(conversation_id: str, thread_id: str, session: dict = Depends(require_auth)):
try:
return link_conversation_thread(session["profile_id"], conversation_id, thread_id)
except StoreError as exc:
_http(exc)
@router.post("/spaces")
def post_space(req: SpaceWrite, session: dict = Depends(require_auth)):
try:
return create_space(session["profile_id"], req.title, req.visibility)
except StoreError as exc:
_http(exc)
@router.post("/threads/{thread_id}/spaces/{space_id}")
def post_thread_space(thread_id: str, space_id: str, confidence: float | None = None, session: dict = Depends(require_auth)):
try:
return link_thread_space(session["profile_id"], thread_id, space_id, confidence)
except StoreError as exc:
_http(exc)
@router.post("/derived")
def post_derived(req: DerivedWrite, session: dict = Depends(require_auth)):
try:
return insert_derived(
session["profile_id"],
req.kind,
req.subject_type,
req.subject_id,
req.source_message_ids,
req.body,
req.visibility,
req.confidence,
)
except StoreError as exc:
_http(exc)
@router.get("/derived/latest")
def get_latest(subject_type: str, subject_id: str, kind: str, session: dict = Depends(require_auth)):
from data_layer import read
return read(
"latest_derived",
profile_id=session["profile_id"],
context={"subject_type": subject_type, "subject_id": subject_id, "kind": kind},
)
@router.post("/handoffs")
def post_handoff(req: HandoffWrite, session: dict = Depends(require_auth)):
try:
return create_handoff(session["profile_id"], req.target, req.source_conversation_id, req.payload)
except StoreError as exc:
_http(exc)

View File

@ -0,0 +1,19 @@
from __future__ import annotations
from fastapi import APIRouter, Depends
from auth import require_auth
import placeholder_system # noqa: F401
from placeholders import list_placeholders
from privacy_placeholders import catalog as privacy_catalog
router = APIRouter(prefix="/api/placeholders", tags=["placeholders"])
@router.get("")
def list_all_placeholders(session: dict = Depends(require_auth)):
return {
"context": list_placeholders(),
"privacy": privacy_catalog(),
"note": "Templates dürfen nur registrierte {{key}} und [[TOKEN]] enthalten. Fachliche Keys kommen später als Registry-Einträge, nicht als Freitext.",
}

182
backend/routers/prompts.py Normal file
View File

@ -0,0 +1,182 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from auth import require_admin_dep, require_auth
from db import get_db, row_to_dict
from engine import EngineError, execute_prompt, preview_prompt
router = APIRouter(prefix="/api/prompts", tags=["prompts"])
LIST_FIELDS = """
id, slug, name, description, category, prompt_type, template, required_feature,
active, is_system_default, sort_order, created, updated
"""
class PromptWrite(BaseModel):
slug: str = Field(min_length=1, max_length=80)
name: str = Field(min_length=1)
description: str = ""
category: str = "uncategorized"
prompt_type: str = "base"
template: str = ""
required_feature: str = "ai_calls"
active: bool = True
class PreviewRequest(BaseModel):
slug: str | None = None
prompt_id: str | None = None
class ExecuteRequest(BaseModel):
slug: str | None = None
prompt_id: str | None = None
purpose: str = "reflection"
data_class: str = Field(default="B")
def _http(exc: EngineError):
raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc
def _load(conn, prompt_id: str | None, slug: str | None) -> dict:
if prompt_id:
row = conn.execute("SELECT * FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone()
elif slug:
row = conn.execute("SELECT * FROM ai_prompts WHERE slug = ?", (slug,)).fetchone()
else:
raise HTTPException(400, "prompt_id oder slug ist erforderlich")
prompt = row_to_dict(row)
if not prompt:
raise HTTPException(404, "Prompt nicht gefunden")
return prompt
@router.get("")
def list_prompts(session: dict = Depends(require_admin_dep)):
with get_db() as conn:
rows = conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts ORDER BY sort_order, name").fetchall()
return [row_to_dict(row) for row in rows]
@router.post("")
def create_prompt(req: PromptWrite, session: dict = Depends(require_admin_dep)):
if req.prompt_type not in {"base", "pipeline", "workflow"}:
raise HTTPException(400, "Ungültiger Prompt-Typ")
prompt_id = str(uuid.uuid4())
if req.prompt_type == "base" and req.template:
try:
preview_prompt(
{
"id": prompt_id,
"slug": req.slug.strip(),
"prompt_type": req.prompt_type,
"template": req.template,
},
{},
)
except EngineError as exc:
_http(exc)
try:
with get_db() as conn:
conn.execute(
"""
INSERT INTO ai_prompts
(id, slug, name, description, category, prompt_type, template,
required_feature, active, default_template)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
prompt_id,
req.slug.strip(),
req.name.strip(),
req.description,
req.category.strip() or "uncategorized",
req.prompt_type,
req.template,
req.required_feature,
1 if req.active else 0,
req.template,
),
)
return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone())
except Exception as exc:
if "UNIQUE" in str(exc).upper():
raise HTTPException(409, "Slug ist bereits vergeben") from exc
raise
@router.put("/{prompt_id}")
def update_prompt(prompt_id: str, req: PromptWrite, session: dict = Depends(require_admin_dep)):
with get_db() as conn:
current = row_to_dict(conn.execute("SELECT id FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone())
if not current:
raise HTTPException(404, "Prompt nicht gefunden")
conn.execute(
"""
UPDATE ai_prompts
SET slug = ?, name = ?, description = ?, category = ?, prompt_type = ?,
template = ?, required_feature = ?, active = ?, updated = datetime('now')
WHERE id = ?
""",
(
req.slug.strip(),
req.name.strip(),
req.description,
req.category.strip() or "uncategorized",
req.prompt_type,
req.template,
req.required_feature,
1 if req.active else 0,
prompt_id,
),
)
return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone())
@router.post("/{prompt_id}/reset")
def reset_prompt(prompt_id: str, session: dict = Depends(require_admin_dep)):
with get_db() as conn:
prompt = row_to_dict(conn.execute("SELECT * FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone())
if not prompt:
raise HTTPException(404, "Prompt nicht gefunden")
if not prompt["is_system_default"]:
raise HTTPException(400, "Nur System-Defaults können zurückgesetzt werden")
conn.execute(
"UPDATE ai_prompts SET template = default_template, updated = datetime('now') WHERE id = ?",
(prompt_id,),
)
return row_to_dict(conn.execute(f"SELECT {LIST_FIELDS} FROM ai_prompts WHERE id = ?", (prompt_id,)).fetchone())
@router.post("/preview")
def preview(req: PreviewRequest, session: dict = Depends(require_admin_dep)):
with get_db() as conn:
prompt = _load(conn, req.prompt_id, req.slug)
try:
return preview_prompt(prompt, {"profile_id": session["profile_id"]})
except EngineError as exc:
_http(exc)
@router.post("/execute")
def execute(req: ExecuteRequest, session: dict = Depends(require_auth)):
with get_db() as conn:
prompt = _load(conn, req.prompt_id, req.slug)
if not prompt["active"] and session.get("role") != "admin":
raise HTTPException(404, "Prompt nicht gefunden")
try:
return execute_prompt(
prompt,
profile_id=session["profile_id"],
purpose=req.purpose,
data_class=req.data_class,
context={"profile_id": session["profile_id"]},
)
except EngineError as exc:
_http(exc)

View File

@ -0,0 +1,29 @@
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from auth import require_admin_dep, require_auth
from db import get_db, row_to_dict
from entitlements import EntitlementError, check_feature_access, subscription_for
router = APIRouter(prefix="/api", tags=["subscription"])
@router.get("/subscription")
def get_subscription(session: dict = Depends(require_auth)):
return subscription_for(session["profile_id"])
@router.get("/features")
def list_features(session: dict = Depends(require_admin_dep)):
with get_db() as conn:
rows = conn.execute("SELECT * FROM features ORDER BY id").fetchall()
return [row_to_dict(row) for row in rows]
@router.get("/features/{feature_id}/access")
def feature_access(feature_id: str, session: dict = Depends(require_auth)):
try:
return check_feature_access(session["profile_id"], feature_id)
except EntitlementError as exc:
raise HTTPException(status_code=exc.status_code, detail={"code": exc.code, "message": exc.message}) from exc

122
backend/routers/users.py Normal file
View File

@ -0,0 +1,122 @@
from __future__ import annotations
import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from auth import hash_password, require_admin_dep, require_auth
from db import get_db, row_to_dict
router = APIRouter(prefix="/api/users", tags=["users"])
PUBLIC_FIELDS = "id, email, name, role, status, tier_id, created"
class CreateUserRequest(BaseModel):
email: str
name: str = Field(min_length=1)
password: str = Field(min_length=4)
role: str = "user"
class PatchUserRequest(BaseModel):
name: str | None = None
role: str | None = None
status: str | None = None
password: str | None = None
def _active_admin_count(conn, exclude_id: str | None = None) -> int:
if exclude_id:
row = conn.execute(
"SELECT COUNT(*) AS n FROM profiles WHERE role = 'admin' AND status = 'active' AND id != ?",
(exclude_id,),
).fetchone()
else:
row = conn.execute(
"SELECT COUNT(*) AS n FROM profiles WHERE role = 'admin' AND status = 'active'"
).fetchone()
return int(row["n"])
@router.get("")
def list_users(session: dict = Depends(require_admin_dep)):
with get_db() as conn:
rows = conn.execute(f"SELECT {PUBLIC_FIELDS} FROM profiles ORDER BY created").fetchall()
return [row_to_dict(row) for row in rows]
@router.post("")
def create_user(req: CreateUserRequest, session: dict = Depends(require_admin_dep)):
if req.role not in {"user", "admin"}:
raise HTTPException(400, "Rolle muss user oder admin sein")
profile_id = str(uuid.uuid4())
try:
with get_db() as conn:
conn.execute(
"""
INSERT INTO profiles (id, email, name, password_hash, role, status, tier_id)
VALUES (?, ?, ?, ?, ?, 'active', 'local')
""",
(
profile_id,
req.email.lower().strip(),
req.name.strip(),
hash_password(req.password),
req.role,
),
)
prof = row_to_dict(conn.execute(f"SELECT {PUBLIC_FIELDS} FROM profiles WHERE id = ?", (profile_id,)).fetchone())
except Exception as exc:
if "UNIQUE" in str(exc).upper():
raise HTTPException(409, "E-Mail ist bereits vergeben") from exc
raise
return prof
@router.patch("/{profile_id}")
def patch_user(profile_id: str, req: PatchUserRequest, session: dict = Depends(require_admin_dep)):
with get_db() as conn:
current = row_to_dict(conn.execute("SELECT * FROM profiles WHERE id = ?", (profile_id,)).fetchone())
if not current:
raise HTTPException(404, "Nutzer nicht gefunden")
name = req.name.strip() if req.name is not None else current["name"]
role = req.role if req.role is not None else current["role"]
status = req.status if req.status is not None else current["status"]
if role not in {"user", "admin"}:
raise HTTPException(400, "Rolle muss user oder admin sein")
if status not in {"active", "disabled"}:
raise HTTPException(400, "Status muss active oder disabled sein")
becomes_non_admin = current["role"] == "admin" and (role != "admin" or status == "disabled")
if becomes_non_admin and _active_admin_count(conn, exclude_id=profile_id) < 1:
raise HTTPException(400, "Der letzte aktive Admin kann nicht entfernt werden")
password_hash = current["password_hash"]
if req.password:
if len(req.password) < 4:
raise HTTPException(400, "Passwort muss mind. 4 Zeichen haben")
password_hash = hash_password(req.password)
conn.execute(
"""
UPDATE profiles SET name = ?, role = ?, status = ?, password_hash = ?
WHERE id = ?
""",
(name, role, status, password_hash, profile_id),
)
if status == "disabled":
conn.execute("DELETE FROM sessions WHERE profile_id = ?", (profile_id,))
return row_to_dict(conn.execute(f"SELECT {PUBLIC_FIELDS} FROM profiles WHERE id = ?", (profile_id,)).fetchone())
@router.get("/me")
def my_account(session: dict = Depends(require_auth)):
with get_db() as conn:
prof = row_to_dict(
conn.execute(
f"SELECT {PUBLIC_FIELDS} FROM profiles WHERE id = ?",
(session["profile_id"],),
).fetchone()
)
if not prof:
raise HTTPException(401, "Profil nicht gefunden")
return prof

204
backend/schema.sql Normal file
View File

@ -0,0 +1,204 @@
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tiers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
active INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS features (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'platform',
limit_type TEXT NOT NULL CHECK (limit_type IN ('boolean', 'count')),
default_limit INTEGER,
reset_period TEXT NOT NULL DEFAULT 'none' CHECK (reset_period IN ('none', 'day', 'month')),
active INTEGER NOT NULL DEFAULT 1,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS tier_limits (
tier_id TEXT NOT NULL REFERENCES tiers(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER,
PRIMARY KEY (tier_id, feature_id)
);
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'admin')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
tier_id TEXT NOT NULL DEFAULT 'local' REFERENCES tiers(id),
session_days INTEGER NOT NULL DEFAULT 30,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS user_feature_restrictions (
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
limit_value INTEGER,
PRIMARY KEY (profile_id, feature_id)
);
CREATE TABLE IF NOT EXISTS user_feature_usage (
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
feature_id TEXT NOT NULL REFERENCES features(id) ON DELETE CASCADE,
period_key TEXT NOT NULL,
used INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (profile_id, feature_id, period_key)
);
CREATE TABLE IF NOT EXISTS ai_prompts (
id TEXT PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
category TEXT NOT NULL DEFAULT 'uncategorized',
prompt_type TEXT NOT NULL DEFAULT 'base' CHECK (prompt_type IN ('base', 'pipeline', 'workflow')),
template TEXT NOT NULL DEFAULT '',
stages_json TEXT,
graph_json TEXT,
output_format TEXT NOT NULL DEFAULT 'text',
output_schema_json TEXT,
required_feature TEXT NOT NULL DEFAULT 'ai_calls',
active INTEGER NOT NULL DEFAULT 1,
is_system_default INTEGER NOT NULL DEFAULT 0,
default_template TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS usage_sessions (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
intent TEXT NOT NULL DEFAULT '',
started_at TEXT NOT NULL DEFAULT (datetime('now')),
ended_at TEXT,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
usage_session_id TEXT REFERENCES usage_sessions(id),
title TEXT NOT NULL DEFAULT '',
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
seq INTEGER NOT NULL,
role TEXT NOT NULL,
body TEXT NOT NULL,
created TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (conversation_id, seq)
);
CREATE TABLE IF NOT EXISTS threads (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'open',
visibility TEXT NOT NULL DEFAULT 'internal' CHECK (visibility IN ('internal', 'user')),
resurfacing_at TEXT,
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS conversation_threads (
conversation_id TEXT NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
PRIMARY KEY (conversation_id, thread_id)
);
CREATE TABLE IF NOT EXISTS spaces (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
title TEXT NOT NULL DEFAULT '',
visibility TEXT NOT NULL DEFAULT 'internal' CHECK (visibility IN ('internal', 'user')),
created TEXT NOT NULL DEFAULT (datetime('now')),
updated TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS thread_spaces (
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
confidence REAL,
PRIMARY KEY (thread_id, space_id)
);
CREATE TABLE IF NOT EXISTS derived_records (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
subject_type TEXT NOT NULL,
subject_id TEXT NOT NULL,
source_message_ids TEXT NOT NULL,
as_of TEXT NOT NULL,
confidence REAL,
visibility TEXT NOT NULL DEFAULT 'internal' CHECK (visibility IN ('internal', 'user')),
body TEXT NOT NULL DEFAULT '',
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS handoffs (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
target TEXT NOT NULL CHECK (target IN ('journal', 'memory', 'knowledge', 'action', 'mindnet', 'obsidian')),
status TEXT NOT NULL DEFAULT 'draft',
source_conversation_id TEXT REFERENCES conversations(id),
payload_json TEXT NOT NULL DEFAULT '{}',
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS external_refs (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
target TEXT NOT NULL CHECK (target IN ('mindnet', 'obsidian')),
external_id TEXT NOT NULL DEFAULT '',
kansho_type TEXT NOT NULL,
kansho_id TEXT NOT NULL,
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS re_grounding_events (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
subject_type TEXT NOT NULL,
subject_id TEXT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
source_message_ids TEXT NOT NULL DEFAULT '[]',
created TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS identity_mappings (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
token TEXT NOT NULL,
local_label TEXT NOT NULL DEFAULT '',
created TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (profile_id, token)
);

View File

@ -0,0 +1 @@
# Kanshō local frame. Not a domain test package.

View File

@ -0,0 +1,157 @@
"""Dialogue memory contracts. Run from backend/: python tests/test_dialogue_memory.py"""
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ["KANSHO_DB_PATH"] = str(Path(tempfile.gettempdir()) / "kansho-dialogue-test.sqlite")
Path(os.environ["KANSHO_DB_PATH"]).unlink(missing_ok=True)
from fastapi.testclient import TestClient
from main import app
def expect(ok: bool, message: str) -> None:
if not ok:
raise SystemExit(f"FAIL: {message}")
print(f"OK {message}")
def header(token: str) -> dict:
return {"X-Auth-Token": token}
def main() -> None:
with TestClient(app) as client:
setup = client.post(
"/api/auth/setup",
json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"},
)
token = setup.json()["token"]
headers = header(token)
usage = client.post("/api/dialogue/sessions", headers=headers, json={"intent": "festhalten"})
expect(usage.status_code == 200, f"start usage session {usage.text}")
usage_id = usage.json()["id"]
conv = client.post(
"/api/dialogue/conversations",
headers=headers,
json={"usage_session_id": usage_id, "title": "Testgespräch"},
)
expect(conv.status_code == 200, f"create conversation {conv.text}")
cid = conv.json()["id"]
msg = client.post(
"/api/dialogue/conversations/{}/messages".format(cid),
headers=headers,
json={"body": "Heute war der Tag anstrengend.", "id": "msg-fixed-1"},
)
expect(msg.status_code == 200 and msg.json()["seq"] == 1, f"append message {msg.text}")
mid = msg.json()["id"]
again = client.post(
"/api/dialogue/conversations/{}/messages".format(cid),
headers=headers,
json={"body": "Heute war der Tag anstrengend.", "id": "msg-fixed-1"},
)
expect(again.status_code == 200 and again.json()["id"] == mid, "idempotent message id")
thread = client.post("/api/dialogue/threads", headers=headers, json={"title": "Offener Faden", "status": "open"})
expect(thread.status_code == 200, "create thread")
tid = thread.json()["id"]
thread_status = thread.json()["status"]
client.post(f"/api/dialogue/conversations/{cid}/threads/{tid}", headers=headers)
derived_bad = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "thread_memory",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [],
"body": "ohne quelle",
},
)
expect(derived_bad.status_code == 400, "derived without provenance rejected")
expect(derived_bad.json()["detail"]["code"] == "provenance_required", "provenance code")
unknown = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "journal_summary",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [mid],
"body": "nein",
},
)
expect(unknown.status_code == 400, "unregistered kind rejected")
derived = client.post(
"/api/dialogue/derived",
headers=headers,
json={
"kind": "working_context_snapshot",
"subject_type": "conversation",
"subject_id": cid,
"source_message_ids": [mid],
"body": "Arbeitsstand, nicht Erkenntnis",
},
)
expect(derived.status_code == 200, f"derived with provenance {derived.text}")
ended = client.post(f"/api/dialogue/sessions/{usage_id}/end", headers=headers)
expect(ended.status_code == 200, f"end usage session {ended.text}")
expect(ended.json()["threads_unchanged"] is True, "checkpoint reports threads unchanged")
expect(ended.json()["llm"] is False, "no llm on checkpoint")
messages = client.get(f"/api/dialogue/conversations/{cid}/messages", headers=headers)
expect(len(messages.json()) == 1, "original survives session end")
expect(messages.json()[0]["body"].startswith("Heute"), "original body intact")
from db import get_db, row_to_dict
with get_db() as conn:
row = row_to_dict(conn.execute("SELECT status FROM threads WHERE id = ?", (tid,)).fetchone())
expect(row["status"] == thread_status, "session end did not change thread status")
other = client.post(
"/api/users",
headers=headers,
json={"email": "ute@example.test", "name": "Ute", "password": "user-pass", "role": "user"},
)
login = client.post("/api/auth/login", json={"email": "ute@example.test", "password": "user-pass"})
other_headers = header(login.json()["token"])
stolen = client.get(f"/api/dialogue/conversations/{cid}", headers=other_headers)
expect(stolen.status_code == 404, "other profile cannot read conversation")
ctx = client.get(f"/api/dialogue/conversations/{cid}/context", headers=headers)
expect(ctx.status_code == 200 and ctx.json()["egress"] is False, "internal context does not egress")
admin = client.get("/api/admin/dialogue", headers=headers)
expect(admin.status_code == 200, "admin dialogue inventory")
expect(admin.json()["instance"]["messages"] >= 1, "admin sees message count")
detail = client.get(f"/api/admin/dialogue/conversations/{cid}", headers=headers)
expect(len(detail.json()["original_messages"]) == 1, "admin original vs derived split")
expect(len(detail.json()["derived_records"]) == 1, "derived listed separately")
spoof = client.get(
f"/api/dialogue/conversations/{cid}",
headers={**headers, "X-Profile-Id": other.json()["id"]},
)
expect(spoof.status_code == 200, "identity from session not profile header")
print("All dialogue memory tests passed.")
if __name__ == "__main__":
main()

197
backend/tests/test_frame.py Normal file
View File

@ -0,0 +1,197 @@
"""Smoke test for the local product frame. Run from backend/: python tests/test_frame.py"""
from __future__ import annotations
import json
import os
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
os.environ["KANSHO_DB_PATH"] = str(Path(tempfile.gettempdir()) / "kansho-frame-test.sqlite")
Path(os.environ["KANSHO_DB_PATH"]).unlink(missing_ok=True)
from fastapi.testclient import TestClient
from main import app
def expect(ok: bool, message: str) -> None:
if not ok:
raise SystemExit(f"FAIL: {message}")
print(f"OK {message}")
def auth_header(token: str) -> dict:
return {"X-Auth-Token": token}
def main() -> None:
seed_prompts = json.loads((ROOT / "config" / "prompts.seed.json").read_text(encoding="utf-8"))
expect(seed_prompts == [], "prompt seed is empty — no domain prompts in git")
with TestClient(app) as client:
health = client.get("/api/health")
expect(health.status_code == 200 and health.json()["service"] == "kansho", "health")
status = client.get("/api/auth/status")
expect(status.json()["needs_setup"] is True, "needs setup before first profile")
denied = client.get("/api/auth/me")
expect(denied.status_code == 401, "me without session is 401")
setup = client.post(
"/api/auth/setup",
json={"email": "lars@example.test", "name": "Lars", "password": "test-pass"},
)
expect(setup.status_code == 200, f"setup {setup.text}")
token = setup.json()["token"]
headers = auth_header(token)
expect(setup.json()["role"] == "admin", "first profile is admin")
again = client.post(
"/api/auth/setup",
json={"email": "other@example.test", "name": "Other", "password": "test-pass"},
)
expect(again.status_code == 409, "second setup is blocked")
me = client.get("/api/auth/me", headers=headers)
expect(me.status_code == 200 and me.json()["email"] == "lars@example.test", "me with session")
spoof = client.get(
"/api/auth/me",
headers={**headers, "X-Profile-Id": "someone-else"},
)
expect(spoof.json()["id"] == me.json()["id"], "identity comes from session, not profile header")
login = client.post(
"/api/auth/login",
json={"email": "lars@example.test", "password": "test-pass"},
)
expect(login.status_code == 200, "login")
token = login.json()["token"]
headers = auth_header(token)
admin = client.get("/api/admin/health", headers=headers)
expect(admin.status_code == 200, "admin health")
expect(admin.json()["inventory"]["prompts"] == 0, "no seeded prompts")
missing = client.post(
"/api/prompts/execute",
headers=headers,
json={"prompt_id": "demo", "data_class": "B"},
)
expect(missing.status_code == 404, "execute requires a stored prompt")
created = client.post(
"/api/prompts",
headers=headers,
json={
"slug": "frame-echo",
"name": "Frame Echo",
"template": "Zeit {{now}} für [[SELF]] am [[PLACE:HOME]].",
},
)
expect(created.status_code == 200, f"create prompt {created.text}")
prompt_id = created.json()["id"]
bad_key = client.post(
"/api/prompts",
headers=headers,
json={"slug": "bad", "name": "Bad", "template": "Hallo {{journal_summary}}"},
)
expect(bad_key.status_code == 400, "unknown context key rejected")
expect(bad_key.json()["detail"]["code"] == "unknown_placeholder", "unknown key code")
bad_privacy = client.post(
"/api/prompts",
headers=headers,
json={"slug": "bad-privacy", "name": "Bad privacy", "template": "[[INDIAN_WIFE]]"},
)
expect(bad_privacy.status_code == 400, "speaking privacy token rejected")
preview = client.post("/api/prompts/preview", headers=headers, json={"prompt_id": prompt_id})
expect(preview.status_code == 200, f"preview {preview.text}")
expect("{{now}}" not in preview.json()["rendered"], "context key resolved")
expect("[[SELF]]" in preview.json()["rendered"], "privacy token left for gateway")
expect(preview.json()["llm"] is False, "preview does not call LLM")
blocked = client.post(
"/api/prompts/execute",
headers=headers,
json={"prompt_id": prompt_id, "data_class": "B"},
)
expect(blocked.status_code == 503, f"gateway fail-closed {blocked.text}")
expect(blocked.json()["detail"]["code"] == "no_egress_provider_configured", "gateway reason")
class_a = client.post(
"/api/prompts/execute",
headers=headers,
json={"prompt_id": prompt_id, "data_class": "A"},
)
expect(class_a.status_code == 503, "class A never leaves")
expect(class_a.json()["detail"]["code"] == "class_a_never_leaves_local_zone", "class A reason")
created_user = client.post(
"/api/users",
headers=headers,
json={"email": "user@example.test", "name": "Ute", "password": "user-pass", "role": "user"},
)
expect(created_user.status_code == 200, f"admin creates user {created_user.text}")
expect(created_user.json()["role"] == "user", "created role is user")
users = client.get("/api/users", headers=headers)
expect(len(users.json()) == 2, "two profiles")
user_login = client.post(
"/api/auth/login",
json={"email": "user@example.test", "password": "user-pass"},
)
expect(user_login.status_code == 200, "user login")
user_headers = auth_header(user_login.json()["token"])
forbidden_users = client.get("/api/users", headers=user_headers)
expect(forbidden_users.status_code == 403, "user cannot list users")
sub = client.get("/api/subscription", headers=user_headers)
expect(sub.status_code == 200, "subscription payload")
expect(sub.json()["tier"]["id"] == "local", "local tier")
expect(sub.json()["billing"] is None, "no billing provider")
expect(any(item["feature_id"] == "ai_calls" and item["allowed"] for item in sub.json()["features"]), "ai_calls allowed")
placeholders = client.get("/api/placeholders", headers=user_headers)
expect(placeholders.status_code == 200, "placeholder catalog")
keys = {item["key"] for item in placeholders.json()["context"]}
expect(keys == {"now"}, "only system context key now")
last_admin = client.patch(
f"/api/users/{me.json()['id']}",
headers=headers,
json={"role": "user"},
)
expect(last_admin.status_code == 400, "last admin cannot be demoted")
disabled = client.patch(
f"/api/users/{created_user.json()['id']}",
headers=headers,
json={"status": "disabled"},
)
expect(disabled.status_code == 200, "disable user")
disabled_login = client.post(
"/api/auth/login",
json={"email": "user@example.test", "password": "user-pass"},
)
expect(disabled_login.status_code == 401, "disabled user cannot login")
bad_login = client.post(
"/api/auth/login",
json={"email": "lars@example.test", "password": "wrong"},
)
expect(bad_login.status_code == 401, "bad password")
print("All frame tests passed.")
if __name__ == "__main__":
main()

13
backend/version.py Normal file
View File

@ -0,0 +1,13 @@
APP_VERSION = "0.1.0-frame"
BUILD_DATE = "2026-08-19"
MODULE_VERSIONS = {
"auth": "0.1.0",
"users": "0.1.0",
"entitlements": "0.1.0",
"privacy_gateway": "0.1.0",
"placeholders": "0.1.0",
"prompts": "0.1.0",
"data_layer": "0.1.0",
"dialogue": "0.1.0",
"continuity": "0.1.0",
}

View File

@ -76,7 +76,13 @@ Dieses Dokument dient dazu, für weitere Konzeptarbeit nur die tatsächlich ben
4. `memory_and_context.md`
5. das konkret betroffene Technik- oder Integrationskapitel
## 4. Ladeprinzip
## 4. Technische Architektur
Seit 2026-08-19 existiert eine vorläufige technische Rahmenarchitektur unter `../technical/`. Einstieg: `../technical/technische_zielarchitektur.md` und `../technical/documentation_index.md`.
Sie übernimmt den Produktrahmen **weitgehend von Mitai**. Datenschutz, Privacy Gateway und Guardrails sind fachlich bereits Invariante und technisch bindend. Dialog-, Memory- und MVP-Schnitte bleiben Arbeitsstand; Phasen FI führen sie weiter. Encryption, Löschen und DSFA (Phase H1) sind Ausprägung, nicht Ersatz der Invariante.
## 5. Ladeprinzip
> **Nicht alle Kanshō-Dokumente gleichzeitig laden.** Root-Dokumente plus die 24 fachlich betroffenen Dateien bilden den Standardkontext.

View File

@ -266,6 +266,8 @@ migration_mapping.md
Die unveränderten Quelldokumente vor der Aufteilung liegen im Verzeichnis `checkpoint_originals/`.
Die **technische** Zielarchitektur liegt parallel unter `docs/architecture/technical/` und beginnt mit `technische_zielarchitektur.md`. Sie ist der Fachdoku nachgeordnet, ersetzt keine Fachkapitel und darf unfertige Interviewblöcke nicht als entschieden darstellen.
# 4. Interviewsteuerung
Der detaillierte Kapitelplan, die Interviewmethode, Definition of Done, Fortschrittsstatus und der nächste Interviewblock werden ab jetzt in `interview_plan.md` gepflegt.

View File

@ -1297,7 +1297,9 @@ Die Konzeptarbeit setzt als Nächstes fort bei:
> **Nutzungssituation 6 Entscheidung / Orientierung**
Vor einer neuen Fachfrage werden die bereits dokumentierten Entscheidungen zu Reflection before Action, Werteabgleich, Perspektivwechsel, Action Candidates und der Produktgrenze zu Kairo geprüft. Erst danach wird der nächste tatsächlich offene, praxisrelevante Entscheidungspunkt bestimmt.
Der gezielte Dokumentationsaudit ist erfolgt. Bereits entschieden sind Reflection before Action, Unterstützung bei Perspektivwechsel, Werteabgleich, inneren Zielkonflikten und Prioritäten, kein automatischer Aufgabenzwang, Action Candidates bei tatsächlicher Handlungsabsicht sowie Kairo als zuständiges System für Planung und Operationalisierung.
Der nächste tatsächlich offene, praxisrelevante Entscheidungspunkt ist die Stärke der KI-Positionierung im Entscheidungsdialog: Darf Kanshō nach ausreichender Reflexion eine begründete eigene Einschätzung oder Empfehlung aussprechen, oder soll es primär neutral spiegeln, Perspektiven öffnen und die Schlussfolgerung vollständig dem Nutzer überlassen?
Die Rollen von Thread Memory, Reflection Frontier, Reflection Memory, Knowledge Delta und Action Candidate sind für die Tagesreflexion ausreichend voneinander abgegrenzt. Detaillierte Heuristiken werden später in ihren jeweiligen Fachkapiteln ausgearbeitet und nicht als Voraussetzung für den Abschluss dieses Nutzungsszenarios vorgezogen.
@ -1520,7 +1522,7 @@ Wenn dieses Dokument in einer neuen Session geladen wird, soll die Session sinng
>
> Der aktive Interviewblock ist **Nutzungssituation 6 Entscheidung / Orientierung**.
>
> Beginne nicht mit einer allgemeinen Projektzusammenfassung oder einer bereits beantworteten Frage. Prüfe vor der ersten neuen Frage die bereits dokumentierten Entscheidungen zu Reflection before Action, Werteabgleich, Perspektivwechsel, Action Candidates und der Produktgrenze zu Kairo. Tagesreflexion, spontaner Gedanke, Fortsetzen eines früheren Fadens und tiefe Reflexion sind vorläufig ausreichend geklärt.
> Beginne nicht mit einer allgemeinen Projektzusammenfassung oder einer bereits beantworteten Frage. Der Audit zu Reflection before Action, Werteabgleich, Action Candidates und der Produktgrenze zu Kairo ist erfolgt. Setze direkt bei der noch offenen Stärke der KI-Positionierung im Entscheidungsdialog fort. Tagesreflexion, spontaner Gedanke, Fortsetzen eines früheren Fadens und tiefe Reflexion sind vorläufig ausreichend geklärt.
>
> Vor der ersten fachlichen Frage bestätige kurz:
> 1. welche kanonischen Repository-Dateien für diesen Schritt gelesen wurden,
@ -1555,7 +1557,8 @@ Wichtig:
**Status früheren Faden fortsetzen:** vorläufig ausreichend geklärt; Contextual Continuation, kompakte Orientierung, Quellenzugang und Re-Grounding entschieden<br>
**Status tiefe Reflexion:** vorläufig ausreichend geklärt; epistemische Vorsicht, Self-Model-Bestätigung und abgestufte Safety-Baseline entschieden<br>
**Nächster Interviewblock:** Nutzungssituation 6 Entscheidung / Orientierung<br>
**Nächster Schritt:** Audit der bestehenden Entscheidungen zu Reflection before Action, Werteabgleich, Perspektivwechsel, Action Candidates und Kairo-Grenze; danach nur den nächsten tatsächlich offenen praxisrelevanten Punkt fragen<br>
**Audit Entscheidung / Orientierung:** erfolgt; Reflection before Action, Werte-/Perspektivarbeit, Action Candidates und Kairo-Grenze bereits festgelegt<br>
**Nächste Entscheidung:** Stärke der KI-Positionierung begründete eigene Einschätzung/Empfehlung oder primär neutrale Reflexionsbegleitung<br>
**Entschiedene Architekturregel:** `Session Lifecycle ≠ Thread Lifecycle`<br>
**Entschieden:** kein zwingendes Ergebnisartefakt; laufende, quellengebundene Kontinuitätsarbeit mit Re-Grounding<br>
**Entschieden:** interne Originalquellen standardmäßig bewahren; keine automatische Löschung wegen Alter, Menge oder geringer Relevanz; Löschung nur auf Nutzeraufforderung oder berechtigte administrative Handlung<br>

View File

@ -1080,7 +1080,16 @@ Konkrete Erkennungsindikatoren, Eskalationsstufen, Formulierungsleitlinien, regi
### Aktiver Interviewblock: Nutzungssituation 6 Entscheidung / Orientierung
Vor der ersten neuen Fachfrage wird erneut gezielt geprüft, welche Entscheidungen zu Reflection before Action, Werteabgleich, Perspektivwechsel, Action Candidates und der Produktgrenze zu Kairo bereits dokumentiert sind. Erst danach wird der nächste tatsächlich offene, praxisrelevante Entscheidungspunkt bestimmt.
Der gezielte Dokumentationsaudit ist erfolgt. Bereits entschieden sind:
- `Reflection before Action`: Erst verstehen und einordnen, dann gegebenenfalls handeln.
- Kanshō unterstützt insbesondere Perspektivwechsel, Werteabgleich, innere Zielkonflikte, Prioritäten und die persönliche Bedeutung einer Entscheidung.
- Das Ziel einer Entscheidungsreflexion ist nicht automatisch die Erzeugung einer Aufgabe.
- Aus einer tatsächlich entstandenen Handlungsabsicht kann ein Action Candidate hervorgehen; Kanshō bietet die Übergabe an Kairo an.
- Kairo bleibt für Ziele, Planung, Operationalisierung, Rituale, Aufgaben und Next Best Actions zuständig. Kanshō übernimmt keine dauerhafte Aufgaben- oder Zielverwaltung.
- Entscheidungen mit persönlicher Tragweite können zusätzlich quellengebundene langfristige Persistierungen in Obsidian beziehungsweise mindnet begründen.
Der nächste tatsächlich offene, praxisrelevante Entscheidungspunkt ist die **Stärke der KI-Positionierung im Entscheidungsdialog**: Darf Kanshō nach ausreichender Reflexion eine begründete eigene Einschätzung oder Empfehlung aussprechen, oder soll es primär neutral spiegeln, Perspektiven öffnen und die Schlussfolgerung vollständig dem Nutzer überlassen?
Danach werden die verbleibenden Nutzungssituationen schrittweise nach derselben Interviewmethode ausgearbeitet.

View File

@ -1,224 +0,0 @@
---
title: "Kanshō Memory und Context"
status: "Arbeitsstand"
date: "2026-08-18"
product_family: "Jinkendo"
document_role: "Fachkapitel / Memory Architecture / Context"
parent_document: "fachliche_zielarchitektur.md"
---
# Kanshō Memory und Context
Dieses Dokument ist das kanonische Home für Working Context, Thread Memory, Episodic Memory, Knowledge-Graph-Bezug und die Rolle von mindnet im kurzfristigen Dialog.
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 10 Gedächtnismodell Einleitung. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
## 10. Gedächtnismodell
Ein zentrales Ergebnis der bisherigen Konzeption ist die Erkenntnis, dass ein einziges LLM-Kontextfenster nicht ausreicht.
Kanshō benötigt mehrere Gedächtnisebenen.
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 10.1 Working Context. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
### 10.1 Working Context
Enthält beispielsweise:
- aktuelle Nachrichten,
- aktuelle Frage,
- unmittelbar vorausgehende Aussagen,
- temporäre Gesprächsinformationen.
Lebensdauer:
- Minuten bis Stunden,
- primär für den aktuellen Dialog.
---
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 10.2 Thread Memory. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
### 10.2 Thread Memory
Enthält:
- Zusammenfassungen einzelner Dialogfäden,
- offene Fragen,
- Zwischenstände,
- noch nicht abgeschlossene Reflexionen,
- relevante Aussagen aus früheren Sitzungen.
Lebensdauer:
- Tage,
- Wochen,
- Monate,
- ggf. länger.
---
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 10.3 Episodic Memory. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
### 10.3 Episodic Memory
Enthält bedeutsame Ereignisse oder Erfahrungen wie:
- Urlaubserlebnisse,
- Konflikte,
- Entscheidungen,
- besondere Erfahrungen,
- persönliche Wendepunkte,
- wichtige Erkenntnisse.
Diese Informationen sind langfristig relevant und sollen in geeigneter Form in Obsidian beziehungsweise mindnet überführt werden.
---
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 10.6 Knowledge Graph / mindnet. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
### 10.6 Knowledge Graph / mindnet
mindnet übernimmt die langfristige Vernetzung von Informationen.
Dazu gehören:
- Erfahrungen,
- Erkenntnisse,
- Beziehungen,
- Referenzen,
- Werte,
- Entscheidungen,
- wiederkehrende Themen,
- offene Zusammenhänge.
Kanshō soll dieses Wissen gezielt abrufen und neue Erkenntnisse wieder zurückführen.
---
<!-- Migriert aus `produktvision_und_produktidentitaet.md`: Abschnitt 11 Rolle von mindnet im kurzfristigen Dialog. Inhalt fachlich unverkuerzt; nur Heading-Level fuer die neue Dateistruktur angepasst. -->
## 11. Rolle von mindnet im kurzfristigen Dialog
Noch nicht abschließend entschieden ist, ob mindnet als kurzfristiger Dialogspeicher geeignet ist.
Der derzeitige konzeptionelle Stand lautet:
**mindnet sollte wahrscheinlich nicht der primäre Working-Memory-Speicher laufender Gespräche sein.**
Gründe dafür sind insbesondere die unterschiedlichen Anforderungen:
Ein Dialogspeicher benötigt unter anderem:
- exakte Reihenfolge,
- hohe Änderungsfrequenz,
- Thread-Zustände,
- lokale/offline Synchronisation,
- schnelle Wiederaufnahme,
- Branching,
- Editierbarkeit,
- Zustandsmanagement.
mindnet ist dagegen besonders geeignet für:
- langfristiges Wissen,
- semantische Retrieval-Prozesse,
- Beziehungen,
- Episoden,
- Erkenntnisse,
- autobiografischen Kontext.
Die derzeit bevorzugte Verantwortungsverteilung lautet deshalb:
> **Kanshō besitzt den Dialog.**
> **mindnet besitzt das langfristige Gedächtnis.**
> **Obsidian bildet das menschenlesbare autobiografische Archiv.**
Diese Entscheidung ist noch technisch zu validieren.
---
## Kontinuitätsarbeit ohne zwingendes Output-Artefakt
**Status: entschieden**
Der Erfolg einer Reflexion hängt nicht davon ab, dass daraus zwingend ein Journal Entry, eine neue Erkenntnis, ein Knowledge Delta oder eine Handlungsabsicht entsteht.
Davon unabhängig benötigt Kanshō interne Kontinuitätsarbeit, damit lange Dialoge, mehrere Threads und spätere Wiederaufnahmen ohne vermeidbaren Bedeutungsverlust möglich bleiben. Dazu können insbesondere gehören:
- Erhalt des Originaldialogs beziehungsweise einer verlässlichen Originalrepräsentation,
- Working-Context- und Thread-Memory-Summaries,
- offene Fragen und Zwischenstände,
- fachlich relevante Erkenntnisse,
- vorläufig erkannte Nebenfäden oder Thread-Kandidaten,
- Zeitbezug, Provenance, Confidence beziehungsweise Unsicherheit.
Nicht jeder erkannte Nebenfaden muss zu einem sichtbaren oder dauerhaft eigenständigen Thread werden. Nicht jede Summary ist eine bestätigte Erkenntnis, und nicht jede Kontinuitätsstruktur ist ein sichtbarer Output.
Da Summaries und weitere Verdichtungen selektiv sind, gelten für ihre langfristige Nutzung verbindlich die Regeln aus `context_fidelity_and_regrounding.md`: Die Originalquelle bleibt maßgeblich; bei ausreichendem Drift-Risiko, Abweichungen, Widersprüchen oder möglicher Fehlinterpretation muss Kanshō ein Re-Grounding aus den relevanten Ursprungsquellen auslösen können. Dies kann automatisch beziehungsweise systemseitig oder ausdrücklich durch den Nutzer angestoßen werden.
Kontinuitätsarbeit ist nicht an das Ende einer Session gebunden. Sie darf und muss bei fachlichem Bedarf bereits während eines langen Dialogs erfolgen, insbesondere wenn der aktive Kontext sonst relevante Aussagen, Nebenfäden oder Zwischenstände verlieren würde.
Das Session-Ende ist ein zusätzlicher fachlicher Kontinuitäts-Checkpoint. Dabei wird geprüft:
- Ist die Originalquelle verlässlich erhalten?
- Müssen Working Context, Summary oder Thread Memory aktualisiert werden?
- Sind tatsächlich relevante Erkenntnisse, offene Fragen oder mögliche Nebenfäden entstanden?
- Haben sich Thread-Zustand, Wiedervorlage oder andere Kontinuitätsinformationen tatsächlich verändert?
- Besteht ein Drift- beziehungsweise Re-Grounding-Bedarf?
Nur tatsächlich notwendige beziehungsweise entstandene Änderungen werden fortgeschrieben. Der Checkpoint erzeugt weder künstlich eine Erkenntnis noch einen Nebenfaden und verlangt kein sichtbares Output-Artefakt.
Für die Produktqualität besitzt die verlässliche Langzeitkommunikation höhere Priorität als die maximale Extraktion möglichst vieler Memories oder Wissenselemente. Verdichtungen sollen Kontinuität ermöglichen, dürfen Erkenntnisse aber nicht schrittweise verwässern, vorläufige Deutungen stabilisieren oder fehlende Zusammenhänge halluzinieren. Die verbindlichen Gegenmaßnahmen werden in `context_fidelity_and_regrounding.md` geführt.
Die konkrete technische Granularität, Speicherform und Aktualisierungslogik dieser Strukturen bleibt später auszuarbeiten.
---
## Default-Quellbewahrung und skalierbare Informationsorganisation
**Status: fachliche Baseline entschieden; konkrete Organisations- und Speicherlogik offen**
### Root Cause
Kanshō ist auf langfristige Kommunikation, spätere Rekonstruktion und Re-Grounding ausgelegt. Dafür müssen auch kurze, zunächst unscheinbare oder erst später bedeutsame Äußerungen als zeitgebundene Quellen verfügbar bleiben. Gleichzeitig kann über Jahre eine sehr große Menge an Dialog- und Textmaterial entstehen.
### Risiko
Eine relevance-, alters- oder mengengetriebene automatische Löschung könnte Quellen vernichten, deren Bedeutung erst später sichtbar wird, und Re-Grounding sowie Point-in-Time-Rekonstruktion beschädigen. Umgekehrt würde eine undifferenzierte Behandlung jedes Textes als sichtbares Objekt, aktives Memory oder eigener mindnet-Knoten das System fachlich und in der Nutzung zumüllen.
### Fachliche Konsequenz
Quellbewahrung und Informationsorganisation sind getrennte Verantwortungen:
> **Alles als Quelle bewahren bedeutet nicht, alles sichtbar, aktiv oder semantisch gleichrangig zu halten.**
### Abgeleitete Entscheidungen
- Vom Nutzer bewusst in Kanshō eingebrachte Texte und der inhaltliche Dialogverlauf werden standardmäßig unmittelbar als zeitgebundene Originalquelle beziehungsweise verlässliche Originalrepräsentation in der internen vertrauenswürdigen Datenhaltung bewahrt.
- Dies gilt auch für kurze spontane Gedanken. Ihre spätere Einordnung, Verknüpfung oder Verdichtung ist von der unmittelbaren Quellbewahrung getrennt.
- Originalquellen werden nicht allein wegen Alter, Umfang, derzeit geringer Relevanz, Konsolidierung oder vorhandener Summaries automatisch gelöscht oder überschrieben.
- Eine Löschung erfolgt nur auf konkrete Nutzeraufforderung beziehungsweise durch eine dafür berechtigte administrative Handlung. Administrative Löschung ist kein automatisches Mittel zur Relevanz- oder Mengenbereinigung.
- Summaries, Thread Memories, Reflection Memories, Knowledge Deltas, Indizes und andere abgeleitete Strukturen dürfen selektiv erzeugt, aktualisiert, konsolidiert, zurückgestuft oder aus dem aktiven Kontext entfernt werden. Sie ersetzen und löschen dadurch nicht ihre Quellen.
- Nicht jeder gespeicherte Text wird zu einem sichtbaren Eintrag, Thread, Reflection Space, Journal Entry, Reflection Memory oder mindnet-Knoten.
- Nur der für die aktuelle Situation relevante Ausschnitt wird in den aktiven Modellkontext geladen. Langfristig gespeichert bedeutet weder dauerhaft aktiv noch dauerhaft sichtbar.
- Wiederfinden und Re-Grounding müssen bei Bedarf bis auf die bewahrte Quellenebene reichen können.
Diese Baseline bezieht sich auf die interne Kanshō-Quellhaltung. Sie erlaubt weder eine unminimierte Weitergabe an externe Modelle noch externe Provider-Speicherung; hierfür gelten weiterhin die Regeln aus `guardrails.md`.
### Später auszuarbeitende Fragen
Die dauerhafte Organisation sehr großer Textmengen wurde bisher noch nicht im erforderlichen Umfang konzipiert. Später zu klären sind insbesondere:
- fachliche Lebenszyklen zwischen aktiv, ruhend, archiviert und bei Bedarf reaktiviert,
- konsolidierte Sichten und Retrieval über viele Jahre, ohne eine unüberschaubare Zahl sichtbarer Einzelobjekte zu erzeugen,
- Granularität, Versionierung, Indexierung und technische Speicherstufen der Quellen,
- Umgang mit echten Duplikaten, Varianten und Korrekturen, ohne zeitliche Perspektiven oder Provenance zu verlieren,
- Auswirkungen einer ausdrücklichen Löschung auf Ableitungen, Indizes, Backlinks, Exporte sowie bereits nach Obsidian oder mindnet überführte Inhalte,
- Rollen, Berechtigungen, Nachvollziehbarkeit und technische Ausführung administrativer Löschungen,
- Kapazitäts-, Kosten-, Offline- und Synchronisationsgrenzen über sehr lange Nutzungszeiträume.
Diese offenen Punkte dürfen die entschiedene Default-Quellbewahrung nicht stillschweigend in eine automatische inhaltliche Lösch- oder Vergessenslogik umdeuten.
---

File diff suppressed because it is too large Load Diff

View File

@ -222,3 +222,276 @@ eine relevante Aussage oder Zusammenfassung basiert.
Dieses **Aufklappen** ist eine Transparenzfunktion für den Nutzer und ist fachlich von dem internen Re-Grounding der KI zu unterscheiden.
---
## Reflection Space als primäre Kontextgrenze
**Status: fachliche Baseline entschieden**
Reflection Spaces dienen nicht nur der langfristigen Organisation von Reflexionsinhalten. Sie bilden im normalen Dialog auch die bevorzugte fachliche Kontextgrenze.
Für die laufende Nutzung gilt deshalb:
> **Kanshō arbeitet standardmäßig innerhalb des aktuellen Reflection Space und erweitert diesen Kontext nur bei konkreter fachlicher Relevanz um tieferes Langzeitgedächtnis oder mindnet.**
Damit bleibt die primäre Nutzung auf den aktuellen Reflexionszusammenhang fokussiert.
Beispiele:
- Bei einem Urlaubstagebuch kann `Lošinj 2026` der aktuelle Reflection Space sein.
- Bei einer längerfristigen Entwicklung kann `Meine Meditationsreise` ein eigener Reflection Space sein.
- Bei einer beruflichen Reflexion kann ein konkreter Space wie `Institute` oder `Berufliche Neuorientierung` den aktuellen Kontext bilden.
Ein Reflection Space ist damit zugleich:
- ein Bedeutungsraum,
- eine Orientierungshilfe,
- eine Kontinuitätsstruktur,
- eine bevorzugte Kontextgrenze für den laufenden Dialog.
Er ist jedoch kein exklusiver Container und kein technischer Prompt.
---
## Primary und Related Reflection Spaces
**Status: fachliche Baseline entschieden**
Eine Session beziehungsweise ein Dialog kann einen **Primary Reflection Space** besitzen und gleichzeitig mit weiteren **Related Reflection Spaces** verbunden sein.
Der Primary Reflection Space ist der Raum, der den aktuellen Dialog fachlich am stärksten trägt.
Related Reflection Spaces sind weitere längerfristige Kontexte, die durch einzelne Experiences, Threads, offene Fragen oder Insights berührt werden.
Beispiel:
Ein Nutzer schreibt während eines Urlaubs:
> „Gestern habe ich meinen Urlaubstag dazu genutzt, tief zu meditieren. Dabei hatte ich erstmals das Gefühl, meinen Körper zu verlassen. Das war eine tiefgreifende Erfahrung.“
Für den unmittelbaren Journaling-Dialog kann gelten:
- Primary Reflection Space: `Lošinj 2026`
- Related Reflection Space: `Meine Meditationsreise`
Das Erlebnis selbst wird dadurch nicht dupliziert.
Verbindliche Regel:
> **Eine Reflexion kann mehrere Spaces berühren, ohne dass ihre Quellen oder fachlichen Kernobjekte mehrfach angelegt werden.**
---
## Identity ist nicht Assignment
**Status: entschieden**
Die fachliche Identität eines Objekts ist von seiner Zuordnung zu Reflection Spaces zu trennen.
Eine Experience, ein Thread, eine Open Question oder ein Insight kann Beziehungen zu mehreren Reflection Spaces besitzen.
Beispiele:
- eine Experience kann gleichzeitig zu Urlaub und Meditation relevant sein,
- ein Thread kann sowohl persönliche Entwicklung als auch Beruf berühren,
- eine Open Question kann mehrere längerfristige Reflexionsräume verbinden.
Daraus folgt:
> **Mehrfachzuordnung erzeugt keine zweite Identität.**
Reflection Spaces organisieren Bedeutungszusammenhänge; sie besitzen keine exklusive Eigentümerschaft an den darin referenzierten Objekten.
---
## Entstehung und Promotion von Reflection Spaces
**Status: fachliche Baseline entschieden; konkrete Schwellenwerte offen**
Nicht jeder situative Kontext wird unmittelbar zu einem langlebigen Reflection Space.
Für die Entstehung wird derselbe allgemeine Promotion-Gedanke verwendet wie für andere langlebige Reflexionsobjekte:
> **Possible Context → Space Candidate → Existing Space Enrichment oder Established Reflection Space**
Ein neuer Reflection Space ist insbesondere dann sinnvoll, wenn ein Zusammenhang:
- über mehrere Interaktionen hinweg eigenständige Reflexionsrelevanz besitzt,
- mehrere zusammenhängende Threads, Experiences, Open Questions oder Insights verbindet,
- vom Nutzer ausdrücklich als zusammenhängender Bereich erlebt oder benannt wird,
- über einen längeren Zeitraum fortgesetzt werden soll,
- eine eigenständige Entwicklungs- oder Historienperspektive benötigt,
- nicht sinnvoll in einen bereits etablierten Space integriert werden kann.
Verbindliche Regel:
> **Existing before new.**
Wenn ein geeigneter bestehender Reflection Space vorhanden ist, soll dieser bevorzugt angereichert werden.
Semantische Ähnlichkeit allein reicht jedoch nicht aus, um zwei unterschiedliche Bedeutungsräume zusammenzuführen.
---
## Unterschiedliche Typen von Reflection-Space-Entstehung
**Status: Arbeitsmodell**
Reflection Spaces können auf unterschiedliche Weise entstehen.
### Situativ klar abgegrenzte Spaces
Diese ergeben sich häufig aus einem klaren zeitlichen oder räumlichen Zusammenhang.
Beispiele:
- Urlaub,
- Reise,
- Seminar,
- Projektphase,
- klar umrissener Lebensabschnitt.
Ein Space wie `Lošinj 2026` kann bereits früh mit hoher Confidence erkannt werden, weil Ort, Zeitraum und wiederkehrender Kontext eine klare Klammer bilden.
### Longitudinal entstehende Spaces
Andere Reflection Spaces entstehen erst über mehrere Interaktionen hinweg.
Beispiele:
- `Meine Meditationsreise`,
- `Umgang mit Verlust`,
- `Meine Rolle als Vater`,
- `Berufliche Neuorientierung`.
Hier kann zunächst nur ein Thread oder Space Candidate bestehen.
Erst wenn ausreichend Kontinuität und eigenständige Bedeutung sichtbar werden, entsteht ein etablierter Reflection Space.
Diese Unterscheidung beschreibt unterschiedliche Entstehungsmuster und noch keine technischen Space-Typen.
---
## Reflection-Space-Lifecycle
**Status: fachliche Baseline entschieden; technische State Machine offen**
Der Lifecycle eines Reflection Space soll bewusst schlank bleiben.
Mindestens folgende Zustände beziehungsweise fachliche Phasen müssen abbildbar sein:
- **Candidate** möglicher eigenständiger Reflection Space,
- **Active** aktuell relevante und genutzte Reflexionsumgebung,
- **Dormant** derzeit nicht aktiv, aber weiterhin langfristig relevant,
- **Historical / Archived** primär historisch relevant und in der normalen Oberfläche zurückgetreten,
- **Reactivated** ein zuvor ruhender oder historischer Space wird erneut aktuell.
Dabei gilt:
- Dormant bedeutet nicht gelöscht.
- Historical bedeutet nicht bedeutungslos.
- Reaktivierung erzeugt keinen neuen Space, wenn fachlich derselbe Bedeutungsraum fortgesetzt wird.
- Frühere Zustände und Historie bleiben nachvollziehbar.
- Die konkrete technische Zustandsmaschine wird später festgelegt.
Ein Space muss nicht allein aufgrund seines Alters archiviert werden. Maßgeblich ist seine aktuelle Reflexionsrelevanz.
---
## Automatische Reflection-Space-Erkennung
**Status: fachliche Baseline entschieden**
Kanshō soll Reflection Spaces möglichst selbstständig erkennen und den Nutzer nicht zum manuellen Verwalter der Reflexionsstruktur machen.
Dabei gilt jedoch:
> **Automatische Reflection-Space-Erkennung darf nicht bedeuten, dass vor jedem Dialogturn ein eigener LLM-Analyseaufruf notwendig ist.**
Die Erkennung soll möglichst inkrementell und opportunistisch erfolgen.
Bevorzugte Signalreihenfolge:
1. bestehender aktiver Primary Reflection Space,
2. aktueller Session-Kontext,
3. Zeit- und gegebenenfalls Ortsbezug,
4. bereits verbundene Threads, Experiences und Open Questions,
5. bestehende Space-Beziehungen,
6. semantische beziehungsweise Retrieval-Signale,
7. strukturierte Einschätzung des ohnehin für die Antwort verwendeten Hauptmodells,
8. separater Analyse-Call nur bei echter Ambiguität oder höherwertiger struktureller Neubewertung.
Designregel:
> **Space recognition should normally piggyback on existing processing rather than create an additional LLM round trip.**
---
## Confidence und Umgang mit Unsicherheit
**Status: fachliche Baseline entschieden; konkrete Schwellenwerte offen**
Kanshō darf eine Space-Zuordnung mit unterschiedlicher Sicherheit behandeln.
Bei hoher Confidence kann eine offensichtliche Zuordnung automatisch erfolgen.
Bei mittlerer Confidence kann:
- eine bestehende Zuordnung vorläufig verwendet,
- ein Space Candidate geführt,
- oder eine spätere Neubewertung vorgesehen werden.
Bei niedriger Confidence soll Kanshō nicht künstlich eine neue langlebige Struktur erzeugen.
Der Nutzer soll nur dann mit einer Rückfrage belastet werden, wenn die Zuordnung für die Reflexion tatsächlich relevant ist und nicht sinnvoll im Hintergrund korrigierbar bleibt.
Verbindliche Regel:
> **Unsicherheit rechtfertigt Zurückhaltung, nicht automatisch eine Rückfrage.**
---
## Reflection Space und Context Builder
**Status: entschieden als Querschnittsregel**
Die kanonische Context-Builder-Logik wird in `memory_and_context.md` geführt.
Für Reflection Spaces gilt daraus abgeleitet:
- der Primary Reflection Space wird bevorzugt als Reflection Context verwendet,
- Related Spaces werden nur situativ aktiviert,
- tieferes Long-Term Memory wird nicht standardmäßig geladen,
- mindnet erweitert den Space nur bei konkreter Relevanz,
- die Space-Struktur dient der Kontextfokussierung und darf nicht selbst zur Quelle unnötiger Prompt-Vergrößerung werden.
Leitprinzip:
> **Reflection Space first, Long-Term Memory on demand.**
---
## Aktualisierter Stand der offenen Punkte
**Status: Arbeitsstand nach Lifecycle- und Context-Klärung**
Weiter offen beziehungsweise bewusst später zu klären sind insbesondere:
1. konkrete Schwellenwerte für Space Promotion, Merge und Reaktivierung,
2. die genaue technische Repräsentation von Primary- und Related-Space-Beziehungen,
3. konkrete UI-Regeln für automatische versus sichtbare Space-Zuordnung,
4. die spätere Integration mit dem finalen Usage-/Intent-Modell,
5. technische Event-/State-Modelle für Lifecycle-Änderungen,
6. konkrete Governance-Regeln für automatische Konsolidierung oder Merge,
7. die genaue technische Zusammenarbeit zwischen Context Builder und Space-Erkennung.
Nicht mehr grundsätzlich offen sind:
- ob Reflection Spaces nur Ablageorte oder aktive Reflexionskontexte sind,
- ob eine Session mehreren Spaces zugeordnet sein kann,
- ob Mehrfachzuordnung Duplikation erzeugt,
- ob jeder situative Kontext automatisch einen Space erzeugt,
- ob jeder Turn einen separaten Space-Erkennungs-Call benötigt,
- ob ruhende Spaces gelöscht oder neu angelegt werden müssen,
- ob bestehende Spaces gegenüber neuen bevorzugt werden sollen.

View File

@ -247,3 +247,244 @@ Dieses Prinzip muss später insbesondere berücksichtigt werden in:
Bei diesen Kapiteln ist jeweils zu prüfen, ob die Point-in-Time-Rekonstruktion der ursprünglichen Innenperspektive erhalten bleibt.
---
## Personal Model fachliche Differenzierung
**Status: fachliche Baseline entschieden; technische Repräsentation offen**
Das bisherige Self Model bleibt als identitätsnaher Kern bestehen, wird jedoch in ein breiteres fachliches Personal Model eingebettet.
> **Personal Context + Interaction Profile + Self Model + Observed Patterns + optionale Structured Assessments**
Diese Differenzierung verhindert, dass alltägliche Präferenzen, Dialogvorlieben, beobachtete Muster und identitätsnahe Aussagen in einem einzigen unscharfen Modell vermischt werden.
### Personal Context
**Status: fachliche Baseline entschieden**
Personal Context enthält langfristig nützliche, überwiegend beschreibende Informationen, die für spätere Dialoge relevant sein können, ohne automatisch Persönlichkeit oder Identität zu beschreiben.
Beispiele:
- Interessen und Vorlieben,
- bevorzugte Sprache,
- wiederkehrende Lebenskontexte,
- alltägliche Präferenzen,
- persönliche Referenzrahmen.
> **Nützlicher persönlicher Kontext ist nicht automatisch eine Persönlichkeits- oder Identitätsaussage.**
Die genaue Grenze zu allgemeinem persönlichem Wissen in mindnet bleibt später zu konkretisieren.
### Interaction Profile
**Status: fachliche Baseline entschieden**
Das Interaction Profile beschreibt, **wie Kanshō mit dem Nutzer kommunizieren soll**, nicht wie der Nutzer „ist“.
Mögliche Inhalte:
- Sprache,
- gewünschte Tiefe und Detailstufe,
- Struktur,
- gewünschter Grad kritischer Rückfragen,
- Umgang mit Rückfragen und Unsicherheit,
- Wiederholung und Zusammenfassung,
- Balance zwischen Guidance und Offenheit,
- bevorzugte Reflexionsfragen,
- Präferenzen für Beispiele, Tabellen oder narrative Formate,
- kontextabhängige Kommunikationsstile.
Ursprünge:
1. explizite Nutzerpräferenz,
2. beobachtete wiederkehrende Präferenz,
3. gelernte beziehungsweise abgeleitete Präferenz.
> **Explizite Nutzeranweisung > gelernte Präferenz > Modellinferenz**
Das Interaction Profile soll versionierbar, zeitlich und kontextuell differenzierbar, provenance-fähig und korrigierbar sein.
### Self Model als identitätsnaher Kern
**Status: bestehende Baseline bestätigt und präzisiert**
Das Self Model enthält insbesondere:
- Werte,
- Leitbild,
- Prinzipien,
- Rollen,
- langfristige Ziele,
- Selbstbeschreibungen,
- bestätigte längerfristige Muster,
- wiederkehrende Spannungsfelder.
Abgrenzungsbeispiele:
- „Autonomie ist mir ein zentraler Wert.“ → Self Model
- „Ich mag Curry.“ → Personal Context
- „Bitte widersprich mir kritisch.“ → Interaction Profile
> **Je identitätsnäher eine Aussage ist, desto höher müssen fachliche Sorgfalt, Evidenzqualität und gegebenenfalls Nutzerbestätigung gewichtet werden.**
### Observed Patterns
**Status: fachliche Baseline entschieden**
Observed Patterns bilden eine Zwischenschicht zwischen Quellen/Beobachtungen und stabilem Self Model.
Beispiele:
- Selbstbestimmung scheint in mehreren Kontexten wichtig zu sein.
- Vor Entscheidungen werden häufig mehrere Alternativen systematisch verglichen.
- In bisherigen Urlauben spielen aktive Tätigkeiten häufig eine wichtige Rolle.
Ein Pattern soll Quellen, Zeitraum, verbundene Reflection Spaces/Threads/Experiences, Evidenzart, Confidence, Status und gegebenenfalls Beziehung zum Self Model nachvollziehbar machen.
> **Observed Pattern ≠ bestätigte Identitätsaussage.**
Patterns können bestätigt, abgelehnt, differenziert, kontextualisiert, konsolidiert oder zurückgestuft werden. Grundlegende identitätsnahe Muster dürfen nicht allein durch Modellinferenz stillschweigend in das stabile Self Model überführt werden.
### Structured Assessments
**Status: optionales Erweiterungsmodell; kein konkretes Framework ausgewählt**
Perspektivisch können strukturierte Modelle integriert werden, beispielsweise Big Five, HEXACO, VIA Character Strengths oder andere bewusst ausgewählte Frameworks.
Regeln:
1. Assessment = zeitgebundenes Ergebnis, kein objektiver Wesenskern.
2. Framework, Version, Zeitpunkt, Quelle und Methode müssen nachvollziehbar sein.
3. KI-Beobachtungen dürfen nicht stillschweigend in Assessment-Scores umgerechnet werden.
4. Frühere Ergebnisse werden nicht überschrieben.
5. Assessments besitzen keinen automatischen Vorrang vor anderen Quellen.
### Provenance- und Vertrauensklassen
**Status: fachliche Baseline entschieden**
Mindestens unterscheidbar:
- Explicit User Statement
- Explicit User Preference
- Structured Assessment
- Observed Pattern
- AI Hypothesis
- User-confirmed Insight / Pattern
- Imported Trusted Source, sofern später unterstützt
Inhaltlich ähnliche Aussagen dürfen nicht gleich behandelt werden, wenn ihre Herkunft unterschiedlich ist.
### Promotion im Personal Model
**Status: fachliche Baseline entschieden; konkrete Schwellenwerte offen**
> **Transient → Candidate → Established**
Die Promotion ist objektspezifisch. Für identitätsnahe Aussagen gilt besonders:
> **Je identitätsnäher eine Aussage ist, desto höher müssen Evidenz, Provenance-Qualität und gegebenenfalls Nutzerbestätigung gewichtet werden.**
Explizite Nutzerentscheidungen besitzen Vorrang gegenüber algorithmischer Promotion.
### Konfigurierbare Personal-Model-Policies
**Status: fachliche Baseline entschieden; konkrete Parameter offen**
Promotion, Re-Evaluation und Konsolidierung persönlicher Aussagen sollen objektspezifisch, konfigurierbar, testbar und versionierbar sein.
Beispielhafte Dimensionen:
- Interaction Profile: Wiederholung, Confidence, Zeitraum, Kontextabhängigkeit, Bestätigung/Korrektur.
- Observed Patterns: unabhängige Evidenzen, zeitliche Streuung, Quellenqualität, Werte-/Zielbezug, Confidence.
- Self Model: Nutzerbestätigung, Evidenzqualität, zeitliche Stabilität, Konfliktbehandlung, Herkunft.
Konkrete Zahlenwerte werden hier nicht festgelegt.
### Re-Evaluation und Policy-Versionierung
**Status: entschieden**
Das Personal Model ist nicht statisch. Neue Dialoge, Experiences, Insights oder Korrekturen können bestehende Aussagen neu bewerten.
Dabei gilt:
- frühere Zustände bleiben nachvollziehbar,
- neue Evidenz überschreibt historische Einordnungen nicht rückwirkend,
- Widerspruch kann Veränderung oder Kontextabhängigkeit anzeigen,
- Nutzerkorrekturen besitzen Vorrang,
- Re-Evaluation braucht Provenance und Zeitpunkt.
Wenn Policies verändert werden, muss nachvollziehbar bleiben, unter welcher Policy-Version frühere Entscheidungen getroffen wurden.
### Personal Model und Reflection Graph
**Status: entschieden**
Das Personal Model steht nicht isoliert neben der Reflexionsgeschichte.
Persönliche Aussagen und Patterns sollen mit relevanten Sources, Experiences, Threads, Reflection Spaces, Insights, Open Questions und zeitlichen States verbunden werden können.
Kanshō soll damit nachvollziehen können, **warum** eine Aussage im Personal Model steht und wie sie sich entwickelt hat.
### Personal Model und mindnet
**Status: fachliche Baseline entschieden; konkrete Synchronisation offen**
Kanshō kennt dialogische Herkunft, Hypothese/Bestätigung, zeitliche Entwicklung und Lived Experience.
mindnet integriert langfristig relevantes persönliches Wissen systemübergreifend.
Bevorzugt werden bidirektionale, provenance-fähige Referenzen.
### Transparenz und Nutzerkontrolle
**Status: fachliche Anforderung entschieden**
Der Nutzer soll perspektivisch nachvollziehen können:
- welche persönlichen Informationen Kanshō hält,
- was explizit geäußert versus gelernt/abgeleitet wurde,
- Hypothese versus bestätigt,
- Quellen,
- letzte Aktualisierung,
- frühere Zustände,
- mindnet-Verbindungen.
Korrekturen müssen zukünftige Dialoge beeinflussen können, ohne historische Quellen zu verfälschen.
### Admin- und Learning-Governance-Schnittstelle
**Status: Querschnittsanforderung; detailliertes Governance-Modell bleibt eigenes späteres Thema**
Eine spätere Admin-/Developer-Ansicht soll Policies, Gewichte/Schwellen, Policy-Versionen, Anpassungen, Evidenz sowie erzeugte, konsolidierte, zurückgestufte oder reaktivierte Objekte nachvollziehbar machen.
> **Adaptive Learning must be governed and auditable.**
Bevorzugte Richtung:
> **Adaptive within governed bounds.**
### Personal Model als selektiv aktivierter Kontext
**Status: entschieden als Querschnittsregel**
Die langfristige Persistenz persönlicher Informationen bedeutet nicht, dass das gesamte Personal Model in jedem Dialogturn aktiv sein soll.
> **Personal Model is available context, not a permanent full prompt.**
Der Context Builder selektiert situationsabhängig nur die tatsächlich relevanten Elemente.
Damit gilt auch hier:
> **Retention und Verfügbarkeit bedeuten nicht permanente Aktivierung.**
Die kanonische Context-Builder- und Aktivierungslogik wird in `memory_and_context.md` geführt.
## Offene Punkte des Personal Models
**Status: Arbeitsstand**
Weiter offen beziehungsweise bewusst später zu klären sind insbesondere:
1. konkrete technische Repräsentation der Personal-Model-Schichten,
2. genaue Abgrenzung Personal Context ↔ mindnet,
3. mögliche Überschneidungen Interaction Profile ↔ `writing_profile_and_journaling.md`,
4. konkrete Structured Assessments und deren Produktrolle,
5. konkrete Schwellenwerte und Gewichtungen,
6. technische Re-Evaluation und Policy-Versionierung,
7. konkrete UX für Transparenz und Nutzerkorrektur,
8. detaillierte Admin-/Learning-Governance,
9. Privacy- und Sensitive-Data-Regeln für identitätsnahe beziehungsweise psychologische Informationen,
10. konkrete Synchronisationsregeln mit mindnet.

View File

@ -0,0 +1,69 @@
---
title: "Kanshō Admin- und Diagnoseansicht"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Admin / Diagnostics"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Admin- und Diagnoseansicht
Kanonisches Home für die technische Admin-/Developer View. Fachlich entschieden: `../functional/fachliche_zielarchitektur.md` §2.7, `../functional/reflection_spaces.md`. UI, Berechtigungen im Detail und Rollenfeinheit sind offen.
## 1. Zweck
Die normale Oberfläche abstrahiert interne KI-Strukturen.
Für Entwicklung, Test, Qualitätssicherung und Administration existiert eine **getrennte Realm** (Mitai-Muster: `/admin/*`, `AdminShell`, `RequireAdmin`).
Reguläre Nutzer erhalten diese Ansicht nicht. Zugriff: Rolle `admin` (v0.1), API und UI gegated (`auth_identity_and_roles.md`).
## 2. Vorgesehene Diagnoseobjekte
Herkunft: Fachdoku, hier nur als zu inspizierende technische Objekte:
- Threads und Thread-Kandidaten
- Reflection-Space-Zuordnungen
- Konsolidierungsentscheidungen
- Memory-Provenance, Confidence, Unsicherheit
- Hypothesen vs. bestätigte Aussagen
- Context-Builder-Ergebnisse und verwendete Quellen
- Strukturänderungen
- Privacy: Datenklasse, maskierte Entitätstypen, Provider, ZDR, Policy-Allow/Deny, Response-Validation **ohne** vollständige Prompts und ohne Mapping als Default
Echte Mapping-Tabelle nur soweit eine besonders berechtigte Rolle es braucht (`guardrails.md` §17).
## 3. Was die Diagnose nicht ist
- kein zweites Nutzerprodukt
- keine Aufgabenverwaltung
- kein Bypass des Privacy Gateway „zum Debuggen in der Cloud“
- keine Anzeige fremder Nutzerdialoge ohne explizites, später zu definierendes Betriebsmodell (Support). Default: Admin sieht Systemzustand und eigene Tests, nicht fremde Lived Experience.
## 4. Mitai-Übernahme
Übernehmen: getrennte Nav-Config (`adminNav.js`-Muster), Shell, `RequireAdmin`, Backend `require_admin`, Admin für Prompts/Workflows/Preview/Import-Export analog Mitai (`platform_extensibility.md`).
Nicht übernehmen: Mitai-Admin für Körpertarife, Coupons, Training Types als Kanshō-Kern. Feature-Admin nur als Muster, wenn Entitlements genutzt werden.
## 5. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Diagnoseansicht erforderlich | ja | entschieden (fachlich) |
| Getrennte Admin-Realm + API-Guard | ja | entschieden |
| Rolle v0.1 | `admin` | entschieden |
| Feineres Rollenmodell | Hauptadmin vs. Entwickler | offen |
| Konkrete Screens und Felder | | offen |
## 6. Offene Fragen
1. Darf ein Admin auf einer Shared Instance jemals fremde Dialoge sehen (Break-glass)?
2. Welche Diagnose-Events werden persistiert vs. nur live berechnet?
3. Wie werden Thread-Kandidaten dargestellt, ohne ein falsches „fertiges“ State Model zu suggerieren?
## 7. Querverweise
- `auth_identity_and_roles.md`, `privacy_gateway.md`, `frontend_pwa_shell.md`, `platform_extensibility.md`
- Fachlich: `../functional/reflection_spaces.md` Admin-/Developer View

View File

@ -0,0 +1,85 @@
---
title: "Kanshō AI-Architektur"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / AI Architecture"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō AI-Architektur
Kanonisches Home für Inferenzpfad und spätere Agenten. Prompt-Engine und Registries: `platform_extensibility.md`. Egress und Datenklassen: `privacy_gateway.md`.
## 1. Rolle der KI
Kanshō ist bewusst stärker KI-kollaborativ als bisherige Jinkendo-Komponenten. Die KI ist Dialogpartner, nicht nur Feature.
Sie darf Zusammenhänge als **Hypothese** anbieten, keine künstliche Gewissheit und keinen erfundenen Wesenskern. Journalstil folgt dem Writing Profile; Inhalt und Stil sind getrennt zu erzeugen (fachlich bevorzugte Richtung).
## 2. Laufzeit
| Thema | Stand | Status |
|---|---|---|
| Externe Inferenz für leistungsfähige Dialoge | vorgesehen | entschieden |
| Lokale große Modelle als alleinige Basis | derzeit nein | entschieden |
| Privacy Gateway vor persönlichem Egress | zwingend | entschieden |
| OpenRouter | möglicher Router | kein Zwang |
| Lokale kleine Modelle / Fallback | möglich | offen |
Kein Router, kein Frontend und kein Prompt-String umgeht `privacy_gateway.md`.
## 3. Engine und Registries
Verbindlich gemäß `platform_extensibility.md`:
- ein Executor (`base` / `pipeline` / `workflow`)
- Prompts in der DB, Admin-CRUD, Preview, Import/Export
- Kontext-Platzhalter `{{key}}` ≠ Privacy-Platzhalter `[[SELF]]`
- LLM nur als Gateway-Callback
Fachliche Reflexions- oder Journal-Prompts sind **Konfiguration**, die später registriert wird nicht Code und nicht der heutige Fachstand als fest verdrahtete Texte.
## 4. Agentenrollen
Interview F3 listet Reflection Agent, Memory Agent, Context Builder, Journal Writer, Critic/Verifier, Routing, Evaluation.
**Status: offen.** Es wird in v0.1 kein Agentengraph festgeschrieben.
Technische Mindestanforderung unabhängig von der späteren Zerlegung:
- Context Builder (wann immer er existiert) erzeugt Internal Context.
- Gateway erzeugt External Model Context.
- Antworten werden validiert und demaskiert, bevor sie lokal weiterverwendet werden.
- Der aktuelle Fachstand zur Strukturierungsautonomie ändert weder Egress-Klasse noch Nutzerhoheit über Identität.
## 5. Context-Fenster
Der heutige Fachstand geht davon aus, dass ein LLM-Kontextfenster nicht ausreicht (`memory_and_context.md`). Orchestrierung von Ausschnitten und Re-Grounding bleibt an diesen Stand gekoppelt, ist aber **kein** vorgezogenes Speicherschema. Algorithmen und Token-Budgets: offen.
Resurfacing und Saturation (aktueller Fachstand) steuern Relevanz, nicht die Egress-Menge. Guardrails bleiben vorrangig.
## 6. Safety
Krisen- und Hochrisiko-Grenzen sind fachlich Phase H3 und **offen**. Technisch v0.1: keine psychiatrische Diagnose, keine stillschweigenden Persönlichkeitsprofile. Konkrete Blocklisten und Eskalations-UX später.
## 7. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Extern + Gateway | ja | entschieden |
| Ein Executor / Gateway-Callback | ja | entschieden |
| Konkrete Prompt-Bibliothek | später als Config | offen |
| Multi-Agent-Zerlegung | | offen |
| Modellwahl und Routing-Policy-Datei | | offen |
| Prompt-Admin (CRUD/Preview/Export) | Mitai-Engine-Muster | entschieden |
| Evaluation / Qualitätsmetriken | | offen |
## 8. Offene Fragen
Siehe Interview F3. Zusätzlich: Wie wird das Writing Profile in die Journal-Pipeline injiziert, ohne Klasse-A-Identität zu leaken?
## 9. Querverweise
- Fachlich: `../functional/guardrails.md`, `../functional/reflection_intelligence.md`, `../functional/writing_profile_and_journaling.md`
- Technisch: `platform_extensibility.md`, `privacy_gateway.md`, `admin_diagnostics.md`

View File

@ -0,0 +1,118 @@
---
title: "Kanshō Auth, Identität und Rollen"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Auth / Identity / Roles"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Auth, Identität und Rollen
Kanonisches Home für Login, Session, Rollen und API-Gates. Fachliche Admin-/Developer-View: `../functional/fachliche_zielarchitektur.md` §2.7. Privacy-Identität: `../functional/guardrails.md`.
Mitai-Referenz: `C:\dev\mitai\.claude\docs\jinkendo-foundation\design-principles\AUTH_SESSION_DESIGN_PRINCIPLES.md`, `backend/auth.py`, `frontend/src/context/AuthContext.jsx`.
## 1. Fachliche Verantwortung
Das Auth-Modul übernimmt:
1. Identität (Profil + Passwort).
2. Session (opaques Token in der Datenbank, Ablauf, Logout).
3. API-Gates (`require_auth`, `require_admin`).
4. Passwort-Lifecycle (Hash, Verify, Reset, Registrierung, E-Mail-Verifizierung).
5. Grobe Rollen: `user` | `admin`.
Es übernimmt **nicht**:
- Feature-Tiers / Entitlements (eigene Schicht, `platform_extensibility.md` §6).
- Org-/Vereinsmandanten.
- OAuth/SSO/JWT (Familien-Vision, nicht Rahmen).
- Privacy-Pseudonymisierung (→ `privacy_gateway.md`).
- fachliche Autorisierung auf Reflexionsinhalten jenseits von `profile_id`.
Spätere feinere Rollen oder ein Abo ersetzen diese Schicht nicht. Sie hängen **additiv** daran: Session bleibt die Identität, `require_admin` bleibt der grobe Gate, Entitlements bleiben `check_feature_access`. Keine Feature-Limits in `role` und keine Rolle in `tier_id` mischen.
## 2. Account-Modell
**Status: entschieden**
Ein Login (E-Mail) entspricht einem Profil. Kanshō ist keine Multi-Profil-App auf einem Login und keine Vereinsapp.
Mehrere Nutzer pro Instanz: ja. Jede Session ist an genau ein `profile_id` gebunden. Alle personenbezogenen Tabellen tragen diese Bindung.
Klarnamen, E-Mail und Mapping-Tabellen sind Klasse A (Local Only). Sie verlassen die Trusted Zone nicht als Teil eines External Model Context.
## 3. Session-Flow (übernommen)
Herkunft: Mitai-Implementierung, als Muster.
1. Login mit E-Mail + Passwort, Rate Limit am Login-Endpoint.
2. Passwortprüfung via bcrypt.
3. Session-Token (`secrets.token_urlsafe`), Speicherung in `sessions` mit `expires_at`.
4. Frontend speichert Token lokal und sendet ihn bei API-Calls (Header-Muster Mitai: `X-Auth-Token`).
5. `require_auth` lädt Session + Profil oder antwortet 401.
6. Logout invalidiert die Session serverseitig.
Session-Dauer: Mitai-Default 30 Tage, profilbezogen konfigurierbar. Für Kanshō **bevorzugte Richtung**, konkreter Default später festzulegen.
Registrierung und E-Mail-Verifizierung: Mitai-Muster übernehmen, SMTP-Konfiguration über Server-`.env`.
## 4. Rollen
| Rolle | Nutzer-PWA | Admin-/Developer View | Status |
|---|---|---|---|
| `user` | eigene Reflexionsdaten | nein | entschieden |
| `admin` | eigenes Nutzerkonto plus Verwaltungs- und Diagnosezugriff | ja | entschieden |
Konkretes feineres Rollenmodell (Hauptadmin vs. Entwickler) ist fachlich offen (`produktvision_und_produktidentitaet.md` Entscheidungsstand). Technisch v0.1: zwei Rollen wie Mitai. Weitere Rollen nur nach fachlicher Entscheidung.
UI-Guard analog `RequireAdmin.jsx`. Backend-Guard analog `require_admin`. Die UI ist kein Ersatz für den API-Guard.
## 5. Bewusst nicht übernehmen (Mitai-Lücke)
Mitai dokumentiert eine bekannte Schwäche: ein Profile-Header (`X-Profile-Id`) ohne harte Bindung an die Session ermöglicht IDOR.
**Status: verworfen für Kanshō**
Regeln:
- Die aktive Identität kommt ausschließlich aus der Session.
- Kein Client-Header darf ein anderes `profile_id` wählen.
- Jede Query auf personenbezogene Daten filtert mit dem Session-Profil.
- Tests müssen fremden Ressourcenzugriff ablehnen.
Historische Multi-Profil-Endpunkte von Mitai (`/profiles` als Umschalter) werden nicht nachgebaut.
## 6. Token-Speicherung im Client
Mitai nutzt `localStorage`. Das ist für eine First-Party-PWA akzeptabel, aber XSS-sensitiv.
**Status: bevorzugte Richtung** gleiches Muster wie Mitai für den Rahmen, mit der Maßgabe, später HttpOnly-Cookie vs. localStorage zu bewerten (`security.md`).
Keine Secrets, API-Keys oder Identity-Mappings im Frontend-Bundle.
## 7. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Server-Sessions | opaque Token in DB | entschieden |
| Rollen | `user` \| `admin` | entschieden |
| 1 Login = 1 Profil | ja | entschieden |
| IDOR-Header-Profilwahl | nicht übernehmen | verworfen |
| JWT/SSO | nicht Rahmen | verworfen für v0.1 |
| Session-Länge | ~30 Tage | bevorzugte Richtung |
| Feineres Rollenmodell | später | offen |
| Token-Ablage Client | localStorage wie Mitai | bevorzugte Richtung |
## 8. Offene Fragen
1. First-Run-Setup (erster Admin) exakt wie Mitai `SetupScreen`?
2. Selbst-Registrierung offen oder einladungsgesteuert?
3. Gemeinsame Jinkendo-Identität später: Migrationspfad von Sessions zu SSO offenhalten, ohne ihn jetzt zu bauen.
## 9. Querverweise
- Fachlich: `../functional/guardrails.md` §45, `../functional/fachliche_zielarchitektur.md` §2.7
- Technisch: `admin_diagnostics.md`, `security.md`, `privacy_gateway.md`
- Mitai: `AUTH.md`, `AUTH_SESSION_DESIGN_PRINCIPLES.md`

View File

@ -0,0 +1,95 @@
---
title: "Kanshō Backend und API"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Backend / API"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Backend und API
Kanonisches Home für FastAPI-Struktur, Router-Schnitt und API-Vertrag. Mitai-Referenz: `.claude/rules/ARCHITECTURE.md` §1, `backend/main.py`, `backend/routers/`.
## 1. Prinzipien
Herkunft: Mitai-Architekturregeln, als Rahmen übernommen.
### 1.1 Ein Modul = ein Router
Jedes fachliche Modul besitzt genau eine Router-Datei unter `backend/routers/`. Endpoints liegen nicht in `main.py` außer Health/Status.
Neue Module = neue Datei, Registrierung via `app.include_router(..., prefix="/api")`.
Rahmen-Module (erste erwartbare Dateien, keine vollständige Liste):
- `auth`
- `profiles`
- später: Dialog, Memory, Journal-Handoff, Gateway-Audit-Metadaten, Admin-Diagnose
Domänenrouter für Mitai-Tracking werden nicht angelegt.
### 1.2 API-First
Jede vom UI benötigte Funktion ist zuerst ein Endpoint. Das Frontend spricht ausschließlich diese API (Mitai: `utils/api.js`). Keine fachliche Berechnung, kein Context-Building und kein Privacy-Demasking im Browser.
### 1.3 Fehlerformat
```text
HTTPException → {"detail": "menschenlesbare Meldung"}
```
Keine parallelen Formate (`error`, `success: false`). Herkunft: Mitai.
### 1.4 Auth an der Grenze
Geschützte Routen nutzen `Depends(require_auth)` bzw. `require_admin`. Ressourcenfilter immer über Session-`profile_id`. Siehe `auth_identity_and_roles.md`.
## 2. Schichten im Backend
Bevorzugte innere Trennung, analog Mitai plus Kanshō-spezifische Gateway-Schicht:
```text
routers/ HTTP, Validierung, Auth-Depends
services/ fachliche Orchestrierung (später)
privacy_gateway/ Minimierung, Pseudonymisierung, Egress, Validation
data/ PostgreSQL-Zugriff
```
**Status: bevorzugte Richtung** für Ordnernamen. Kein Zwang, Mitai-Dateibaum 1:1 zu klonen, wenn die Trennlinie Router / Gateway / Persistenz klar bleibt.
Privacy Gateway darf nicht umgangen werden, indem ein Router den Provider direkt aufruft. KI-Ausführung nur über die Prompt-Engine (`platform_extensibility.md`), Callback = Gateway.
## 3. Versionierung
Mitai führt `backend/version.py` mit `APP_VERSION` und `MODULE_VERSIONS`.
**Status: bevorzugte Richtung** gleiches Muster für Kanshō, sobald Code existiert. Semantic Versioning.
## 4. Health und Betrieb
Ein ungeschützter oder schwach geschützter Status-Endpoint für Deploy-Healthchecks analog Mitai `/api/auth/status` bzw. Health nach Container-Start.
Konkreter Pfad: offen, Muster übernommen.
## 5. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| FastAPI + ein Router je Modul | ja | entschieden |
| API-First, keine Business-Logik im Frontend | ja | entschieden |
| Fehlerkörper `{detail}` | ja | entschieden |
| Direktaufruf externer LLMs aus Routern | verboten | entschieden |
| OpenAPI als dokumentierter Vertrag | später | offen |
| Interne service-Schicht vs. fette Router | service-Schicht | bevorzugte Richtung |
## 6. Offene Fragen
1. Internes API für mindnet/Kairo: synchron REST vs. Events? → `integrations_technical.md`
2. Idempotenz-Keys für Offline-Sync-Queue? → `memory_storage_and_offline.md`
3. Rate Limits außer Auth: welche Dialog-Endpoints?
## 7. Querverweise
- Technisch: `auth_identity_and_roles.md`, `privacy_gateway.md`, `runtime_and_deploy.md`
- Mitai: `ARCHITECTURE.md` §1, `INTERNAL_API_REFERENCE.md`

View File

@ -0,0 +1,85 @@
---
title: "Kanshō Datenarchitektur"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Data Architecture"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Datenarchitektur
Kanonisches Home für Persistenzgrenzen **ohne** vorgezogenes SQL-Schema. Ableitungen, die UI und KI gemeinsam brauchen, folgen dem Data-Layer-Muster in `platform_extensibility.md` (Layer 0/1/2), sobald der Fachstand konkrete Funktionen hergibt.
## 1. Speicherverantwortung
Herkunft: `../functional/memory_and_context.md` §11, bevorzugte Richtung, technisch zu validieren.
| System | Besitz | Nicht besitzen |
|---|---|---|
| Kanshō / PostgreSQL | laufender Dialog, Session, Thread-Arbeitsstand, Working Context, lokale Kontinuitätsstrukturen | langfristiges Wissensnetz als Primärgraph |
| mindnet | langfristiges Wissen, Retrieval, Edges, Reflection Memories, Knowledge Deltas | exakte Dialogreihenfolge, Offline-Branching |
| Obsidian | menschenlesbares Archiv, Journal Entries | interne Thread-Zustände |
**Status: bevorzugte Richtung** für die Verteilung; Validierung bleibt offen.
Kanshō erzeugt kein proprietäres geschlossenes Langzeitarchiv, das Obsidian ersetzen würde.
## 2. Aktueller Fach-Arbeitsstand (nicht final)
Die folgenden Punkte stehen heute in der Fachdoku. Sie sollen bei einer späteren Persistenz **nicht stillschweigend verloren gehen**. Sie sind **kein** Auftrag, jetzt Tabellen, Enums oder State Machines daraus zu bauen. Ändert die Fachkonzeption sie, gilt der neue Fachstand.
1. **Originaldialog ist Primärquelle.** Summaries, Thread Memories und Space-Zustände sind abgeleitet. Re-Grounding rekonstruiert aus Ursprungsquellen (`context_fidelity_and_regrounding.md`).
2. **Session Lifecycle ≠ Thread Lifecycle.** Das Ende einer Nutzungssitzung schließt Threads nicht automatisch (`reflection_outputs.md`). Tabellen und Statusfelder müssen beide Zyklen unabhängig abbilden können.
3. **Lived Experience / Point-in-Time Self.** Frühere Innenperspektiven werden nicht durch heutige Summaries überschrieben (`self_model_and_lived_experience.md`). Versionierung und Zeitbindung sind erforderlich, sobald Self-Model-Daten persistiert werden.
4. **Hypothese ≠ bestätigte Erkenntnis.** Self-Model-Änderungen nicht stillschweigend schreiben.
5. **Action Candidates** sind Handoffs nach Kairo, keine Kanshō-Aufgabenliste.
6. **Resurfacing ≠ Speicherung.** Langfristig gespeichert heißt nicht aktuell relevant (`resurfacing_and_saturation.md`).
## 3. Fachliche Entitäten (noch kein Schema)
Die folgende Liste spiegelt den **heutigen** Fachstand. Sie ist ein Merkzettel, keine Tabellendefinition und keine Zusage, dass all diese Objekte so bleiben. Welche Objektarten und Beziehungen ein späteres Schema **erfüllen** muss, steht als Vertrag in `memory_storage_and_offline.md` §3a.
| Entität | Fachliches Home | Technische Erwartung v0.1 |
|---|---|---|
| Profile / Session | dieses Kapitel + `auth_identity_and_roles.md` | Kanshō-DB, Rahmen |
| Conversation / Message | `dialogue_model.md` | Kanshō-DB, Originalrepräsentation |
| Thread (kein fertiges State Model) | `dialogue_model.md`, `resurfacing_and_saturation.md` | Kanshō-DB; Zustände nicht als geschlossene Enum vortäuschen |
| Reflection Space | `reflection_spaces.md` | Kanshō-DB, sichtbare vs. interne Struktur trennen |
| Working / Thread Memory | `memory_and_context.md` | Kanshō-DB, quellengebunden |
| Episodic / Journal / Memory / Knowledge Delta | `reflection_outputs.md` | Handoff-Ziele mindnet/Obsidian |
| Action Candidate | `reflection_outputs.md` | Handoff Kairo |
| Self Model Version | `self_model_and_lived_experience.md` | lokal, versioniert, Bestätigungspflicht |
| Writing Profile | `writing_profile_and_journaling.md` | lokal langfristig |
| Provenance / Confidence | `context_fidelity_and_regrounding.md` | an abgeleiteten Strukturen, nicht nur an Originalen |
| Privacy Mapping | `guardrails.md` | Klasse A, nur lokal |
Kein Thread-State-Machine-Kapitel in v0.1. Die fachlich genannten Zustände (`Open`, `Anchored / Resurface`, `Dormant`, `Resolved`) sind **nicht abschließend**.
## 4. Was nicht in der Kanshō-DB landen soll
- Vollständiges mindnet-Graphmodell als Kopie.
- Kairo-Projekte, Tasks, Rituale als führendes System.
- Mitai-Vital- und Trainingsrohdaten als zweites Tracking.
- Identity-Mapping in Backup-Logs oder Prompt-Archiven.
## 5. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Kanshō besitzt den Dialogspeicher | ja | bevorzugte Richtung |
| mindnet besitzt langfristiges Gedächtnis | ja | bevorzugte Richtung |
| Obsidian menschenlesbares Archiv | ja | bevorzugte Richtung |
| Session ≠ Thread / Originaldialog / Handoffs | aktueller Fach-Arbeitsstand | nicht einfrieren |
| SQL-Schema / IDs / Edges | nein | offen |
| Thread-Zustandsmenge | nicht schließen | offen |
| Gemeinsame IDs mit mindnet/Obsidian | nötig, Form unbekannt | offen |
## 6. Offene Fragen
Entsprechen den Interview-Leitfragen Phase F1/F4 und werden hier nicht vorab beantwortet: kanonische IDs, Backlinks, welche Strukturen nur in Obsidian existieren, Schema für Reflection Memory und Knowledge Delta.
## 7. Querverweise
- `memory_storage_and_offline.md`, `integrations_technical.md`, `privacy_gateway.md`, `platform_extensibility.md`
- Fachlich: `memory_and_context.md`, `reflection_outputs.md`, `self_model_and_lived_experience.md`

View File

@ -0,0 +1,117 @@
---
title: "Kanshō Technischer Dokumentationsindex und Context Bundles"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Documentation Index / Technical Context Loading Guide"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Technischer Dokumentationsindex
Für technische Arbeit nur die benötigten Dateien laden. Fachliche Homes nicht durch technische Kurzfassungen ersetzen. Dialog-/MVP-Stand nicht als final behandeln. **Datenschutz und Guardrails** (`../functional/guardrails.md`) sind verbindliche Invariante. Mitai-Produktrahmen weitgehend übernehmen, Gateway nicht umgehen.
## 1. Root-Dokumente
Technisch:
- `technische_zielarchitektur.md` Governance, Stack, Kapitelkarte, Entscheidungsstand.
- `documentation_index.md` dieser Index.
Fachlich zusätzlich immer bei Querschnittsentscheidungen:
- `../functional/fachliche_zielarchitektur.md`
- `../functional/produktvision_und_produktidentitaet.md`
## 2. Technische Kapitel
| Datei | Kanonisches Thema |
|---|---|
| `product_frame_and_stack.md` | 3-Tier, Stack, Mitai-Übernahme |
| `auth_identity_and_roles.md` | Sessions, Rollen, Admin-Gate |
| `frontend_pwa_shell.md` | PWA, Responsive Shell, Navigation |
| `backend_and_api.md` | Router, API-First |
| `runtime_and_deploy.md` | Migrationen, Compose, Gitea |
| `platform_extensibility.md` | Registry, Prompt-Engine, Konfiguration |
| `data_architecture.md` | Persistenzgrenzen, Entitäten |
| `memory_storage_and_offline.md` | Dialogspeicher, Sync, Offline |
| `privacy_gateway.md` | Trust Zones, Egress, Demasking |
| `ai_architecture.md` | Inferenzpfad, Agenten |
| `integrations_technical.md` | Technische Handoffs |
| `admin_diagnostics.md` | Diagnoseansicht |
| `voice_and_media.md` | Sprache und Transkription |
| `security.md` | Security-Baseline |
## 3. Empfohlene Context Bundles
### Produktrahmen / erste Shell
1. `technische_zielarchitektur.md`
2. `product_frame_and_stack.md`
3. `auth_identity_and_roles.md`
4. `frontend_pwa_shell.md`
5. `backend_and_api.md`
6. `runtime_and_deploy.md`
7. `platform_extensibility.md`
8. fachlich: `../functional/produktvision_und_produktidentitaet.md` §19 und §21
### Produktrahmen / Erweiterung
1. `technische_zielarchitektur.md`
2. `platform_extensibility.md`
3. `privacy_gateway.md`
4. `ai_architecture.md`
5. `backend_and_api.md`
6. `admin_diagnostics.md`
### Privacy / externe KI
1. `technische_zielarchitektur.md`
2. `privacy_gateway.md`
3. `platform_extensibility.md`
4. `ai_architecture.md`
5. `security.md`
6. fachlich: `../functional/guardrails.md`
7. bei Bedarf: `../functional/self_model_and_lived_experience.md`, `../functional/memory_and_context.md`
### Dialogspeicher / Memory / Offline
1. `data_architecture.md`
2. `memory_storage_and_offline.md`
3. fachlich: `../functional/memory_and_context.md`
4. `../functional/context_fidelity_and_regrounding.md`
5. `../functional/reflection_outputs.md`
6. `../functional/resurfacing_and_saturation.md`
### Integrationen
1. `integrations_technical.md`
2. fachlich: `../functional/integrations.md`
3. `../functional/reflection_outputs.md`
4. das betroffene Fachkapitel (Kairo, mindnet, Mitai, Shinkan)
### Admin / Diagnose
1. `admin_diagnostics.md`
2. `auth_identity_and_roles.md`
3. fachlich: `../functional/fachliche_zielarchitektur.md` §2.7
4. `../functional/reflection_spaces.md` (Admin-/Developer View)
### Voice / PWA-Offline
1. `frontend_pwa_shell.md`
2. `voice_and_media.md`
3. `memory_storage_and_offline.md`
4. `privacy_gateway.md` (eigener Egress-Typ)
5. fachlich: `../functional/produktvision_und_produktidentitaet.md` §19.419.5
## 4. Ladeprinzip
> Root-Dokumente plus 24 betroffene Kapitel. Nicht den ganzen technischen und fachlichen Ordner gleichzeitig laden.
Vor Vereinfachungen die Invariantenliste in `technische_zielarchitektur.md` §3.6 prüfen.
## 5. Code-Gerüst
Lokaler Produktrahmen in `frontend/` und `backend/`. Auth, Nutzerverwaltung, Prompt-DB, Platzhalter, Feature-Check, Privacy-Gateway-Stub. **Dialog-Layer 0:** Conversations/Messages/`usage_sessions`, Thread-/Space-Identität, Derived-Hülle mit Provenance. Keine Dialog-IA, keine Domain-Prompts, kein Stripe. SQLite nur lokal.

View File

@ -0,0 +1,86 @@
---
title: "Kanshō Frontend-PWA-Shell und Navigation"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / PWA Shell / Navigation / Responsive"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Frontend-PWA-Shell und Navigation
Kanonisches Home für PWA-Hülle, Breakpoint und Navigations**mechanik**. Die Mitai-Shell wird weitgehend übernommen. Welche Kanshō-Orte später in dieser Shell stehen, folgt dem **dann aktuellen** Fachstand der heutige Dialog-/Entry-Stand ist nicht final und wird hier nicht als technische Startroute eingefroren.
Mitai-Referenz: `NAVIGATION_IA_DESIGN_PRINCIPLES.md`, `frontend/src/config/appNav.js`, `frontend/src/app.css`, Shells unter `frontend/src/layouts/`.
## 1. Was die Shell übernimmt
1. Progressive Web App, installierbar, Service Worker über Vite PWA-Plugin.
2. Mobile First, vollwertiges Desktop-Layout.
3. Ein Breakpoint: **1024px** (Mitai). Darunter Bottom-Nav, darüber Desktop-Sidebar.
4. Safe Area / Content-Padding für iOS-Homescreen.
5. Eine Navigations-SSoT analog `appNav.js` (Reihenfolge, Labels, Icons, Admin-Sichtbarkeit).
6. Bereichs-Shells, wenn ein Top-Level-Bereich eigene Sub-Navigation braucht.
7. Getrennte Admin-Realm mit `RequireAdmin`.
Die Shell übernimmt die Mitai-Mechanik. Mitai-Labels (Übersicht, Erfassen, Verlauf, Ziele, Analyse) sind **Domänenplatzhalter**. Sie dürfen in einer ersten Rahmen-Kopie stehen, werden aber nicht mit Mitai-Fachseiten gefüllt. Die spätere Kanshō-IA ersetzt oder benennt sie, sobald das Fachkonzept dafür reif ist.
## 2. Fachliche Startlogik nicht technisch einfrieren
Der aktuelle Fachstand bevorzugt Contextual Continuation statt eines Funktions-Dashboards (`dialogue_model.md`, `produktvision_und_produktidentitaet.md`). Das ist **Arbeitsstand**, kein finaler UX-Schnitt.
Technische Folge für den Rahmen:
- Mitai-Home (`/`) und Nav-Mechanik dürfen zunächst wie in Mitai existieren (leere oder minimale Platzhalter).
- Es wird **keine** verbindliche Kanshō-Startroute und **keine** endgültige Nav-Item-Liste in v0.1 festgeschrieben.
- Interne Fachobjekte (Threads, Spaces, Hypothesen) werden nicht als Nutzer-Verwaltungsnav vorgebaut, nur weil der heutige Fachtext sie nennt.
## 3. Responsive Muster
| Viewport | Muster | Status |
|---|---|---|
| < 1024px | Bottom-Nav, volle Breite, Einhand-Nähe der Primäraktion | entschieden als Rahmen |
| ≥ 1024px | Sidebar + Inhaltsfläche; Desktop darf Mehrspalten später nutzen | entschieden als Rahmen |
| Lange Dialoge | scrollbarer Verlauf, Eingabe unten; Desktop darf Kontextspalte später ergänzen | bevorzugte Richtung |
Kein zweites paralleles Layout-System. Zusätzliche Breakpoints nur mit dokumentiertem Grund.
## 4. PWA
- `vite-plugin-pwa` wie Mitai.
- Manifest, Icons und Offline-Cache gehören zur Shell.
- Welche Daten offline schreibbar sind, steht in `memory_storage_and_offline.md` und ist in der Ausprägung **offen**.
- Die Shell muss offline zumindest die installierte UI laden können (App Shell Cache). Fachliche Dialog-Offline-Fähigkeit ist davon getrennt.
## 5. Komponentenrahmen
Übernehmen als Muster, nicht als Domäne:
- `AuthContext`
- Layout-Shells
- Avatar, einfache Markdown-Darstellung wo nötig
- keine Mitai-Widgets, Charts oder Capture-Hubs als Kern
Business-Logik bleibt im Backend (`backend_and_api.md`).
## 6. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| PWA + Vite PWA Plugin | ja | entschieden |
| Breakpoint 1024px | Bottom-Nav / Sidebar | entschieden |
| Nav-SSoT + Shells + Admin-Realm | Mitai-Mechanik | entschieden |
| Mitai-Nav-Labels als Platzhalter | zulässig im Rahmen | bevorzugte Richtung |
| Kanshō-Startroute / endgültige IA | folgt späterem Fachstand | offen |
| Desktop-Mehrspalten | späterer Fach-/UX-Stand | offen |
## 7. Offene Fragen
1. Welche Platzhalter-Routen bleiben in der ersten Rahmen-Kopie sichtbar, welche werden ausgeblendet, bis Fachseiten existieren?
2. Wo liegt Journal-Lesen relativ zum laufenden Dialog (eigene Route vs. Overlay)?
3. Wie wird Spracheingabe in der mobilen Chrome platziert, ohne die Dialogfläche zu verdrängen? → `voice_and_media.md`
## 8. Querverweise
- Fachlich (aktueller Arbeitsstand, nicht final): `../functional/dialogue_model.md`, `../functional/produktvision_und_produktidentitaet.md` §19
- Technisch: `product_frame_and_stack.md`, `auth_identity_and_roles.md`, `admin_diagnostics.md`

View File

@ -0,0 +1,60 @@
---
title: "Kanshō Technische Integrationen"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Integrations"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Technische Integrationen
Kanonisches Home für technische Handoffs. Fachliche Produktgrenzen: `../functional/integrations.md`. Verträge (APIs, Events, Auth zwischen Apps) sind **offen** (Interview F1/F2).
## 1. Prinzip
Integrationen folgen der Produktverantwortung, nicht der Bequemlichkeit gemeinsamer Tabellen.
Kein stilles Mitlesen fremder Datenbanken als Ersatz für einen Vertrag. Berechtigung und Zweckbindung gelten auch familienintern.
## 2. Handoff-Matrix
| Gegenüber | Kanshō darf | Kanshō darf nicht | Status Vertrag |
|---|---|---|---|
| **Kairo** | Ziele/Entwicklungskontext lesen; Action Candidates vorschlagen und zur Übernahme anbieten | Aufgaben, Projekte, Rituale führen | offen |
| **mindnet** | Wissen abrufen; Memories und Knowledge Deltas vorschlagen/schreiben gemäß späterem Schema | Working Memory laufender Dialoge ersetzen | offen |
| **Obsidian** | Journal und strukturierte Reflexionen ablegen, referenzieren | einziges geschlossenes Archiv sein | offen (Ablage/Sync) |
| **Mitai** | bei Kontext und Berechtigung körperlichen Kontext lesen | Vital-/Ernährungs-Tracking nachbauen | offen |
| **Shinkan** | Trainingskontext für Reflexion lesen | Trainingsplanung nachbauen | offen |
Action Candidate fachlich: `../functional/reflection_outputs.md`. Operationalisierung bleibt Kairo.
## 3. Technische Leitplanken bis zum Vertrag
- Lesen, Schreiben und „nur vorschlagen / Nutzer bestätigt“ sind drei verschiedene Rechte. Default: vorschlagen.
- IDs und Backlinks müssen später kanonisch sein; Format offen.
- Egress zu einer Schwester-App ist nicht dasselbe wie LLM-Egress, braucht aber ebenfalls Minimierung identifizierender Daten gegenüber nicht vertrauenswürdigen Netzen.
- Shinkan-Vereinsmodell wird nicht nach Kanshō gespiegelt, auch wenn Shinkan-Kontext gelesen wird: Mapping auf den Kanshō-Nutzer, nicht auf einen Verein.
## 4. Event vs. API
**Status: offen.** Fachlich sind externe Reflexionstrigger erlaubt (andere Komponenten dürfen Anlässe anbieten; Kairo plant, Kanshō reflektiert). Technische Trigger-Architektur ist bewusst noch nicht abgeleitet (`usage_situations.md`).
## 5. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Keine Funktionsduplikation | ja | entschieden (fachlich) |
| Action Candidates → Kairo | ja | entschieden (fachlich) |
| Journal → Obsidian | primär | entschieden (fachlich) |
| Konkrete APIs/Events/Auth-Zertifikate | | offen |
| Shared Database | als Integrationsmuster | verworfen als Default |
## 6. Offene Fragen
Interview F1/F2 vollständig: welche Daten kanonisch wo liegen, welche Edges mindnet braucht, Obsidian-Pfadkonvention, Bestätigungsdialoge vor Schreib-Handoffs.
## 7. Querverweise
- `data_architecture.md`, `privacy_gateway.md`
- Fachlich: `../functional/integrations.md`, `../functional/reflection_outputs.md`

View File

@ -0,0 +1,148 @@
---
title: "Kanshō Memory-Speicher, Sync und Offline"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Memory Storage / Sync / Offline"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Memory-Speicher, Sync und Offline
Kanonisches Home für Gedächtnisschichten und Offline, sobald der Fachstand sie trägt. Die Schichtenbeschreibungen unten spiegeln den **heutigen** Fach-Arbeitsstand (`memory_and_context.md`) und sind nicht als finales Speichermodell zu implementieren. Offline-Ausprägung bleibt offen.
## 1. Schichten
| Schicht | Lebensdauer (fachlich) | Technische Konsequenz |
|---|---|---|
| Working Context | Minuten bis Stunden | lokal, hohe Schreibfrequenz, an die aktive Session/Conversation gebunden |
| Thread Memory | Tage bis Monate+ | persistiert, quellengebunden, darf nicht die Originalnachrichten ersetzen |
| Episodic / langfristig | Jahre | Handoff nach mindnet/Obsidian; Kanshō behält Provenance-Referenzen |
Ein LLM-Kontextfenster ist kein Speicher. Context Builder wählt Ausschnitte; das Gateway minimiert sie erneut vor Egress.
## 2. Primärquelle und Re-Grounding
**Status: fachliche Invariante** (Re-Grounding / Originalquelle). Kein SQL-Schema, keine Drift-Formel in v0.1.
- Vollständige Dialoge bzw. eine verlässliche Originalrepräsentation bleiben erhalten.
- Summaries dürfen laufend fortgeschrieben werden, auch mitten im Dialog, nicht nur am Session-Ende.
- Drift-Erkennung und Re-Grounding arbeiten **lokal** auf Originalquellen. Für externe Inferenz wird danach ein neuer minimierter External Model Context erzeugt. Privacy Guardrails werden dabei nicht umgangen.
Keine unbegrenzte Kette Summary → Summary ohne Rückgriff auf die Quelle.
Konkrete Drift-Metrik, Granularität und Update-Algorithmen: **offen**.
## 3a. Strukturelle Vorsorge (Verträge, kein SQL)
**Status: entschieden als Vorsorge.** Felder, Enums und Tabellennamen bleiben offen. Festgezogen wird, **welche Objektarten und Beziehungen** das Langzeitdialog- und Erinnerungssystem tragen müssen. Herkunft: `memory_and_context.md`, `dialogue_model.md`, `resurfacing_and_saturation.md`, `context_fidelity_and_regrounding.md`, `self_model_and_lived_experience.md`, `reflection_outputs.md`, `reflection_spaces.md`.
Ein späteres Schema **füllt** diese Verträge. Es darf sie nicht kollabieren (z. B. Thread in der Session aufgehen lassen, Summary als Primärquelle, Handoff als Kanshō-Todo).
### Objektarten
| Art | Fachliche Rolle | Darf nicht |
|---|---|---|
| **Conversation** | Geordnete Originalrepräsentation (Layer 0). Stabile Message-IDs, Reihenfolge, Zeit. | Durch Summary ersetzt oder nur im LLM-Fenster existieren |
| **Session** | Eine Nutzungssitzung / Interaktionszyklus inkl. Checkpoint am Ende | Thread mitabschließen |
| **Thread** | Reflexionsfaden über Sitzungen hinweg; Zustand und Wiedervorlage am Faden, nicht an der Session | Geschlossene State-Machine vortäuschen; jeder Nebenfaden muss sichtbar sein |
| **Thread-Kandidat** | Intern markierter möglicher Faden | Automatisch eigener Chat oder bestätigtes Wissen |
| **Working Context** | Kurzlebiger Arbeitsstand der aktiven Conversation | Langzeitgedächtnis; Egress ohne Gateway |
| **Thread Memory** | Quellengebundene Verdichtung eines Fadens (Layer 1, versioniert) | Original ersetzen; still als Erkenntnis schreiben |
| **Reflection Space** | Lebendiger Kontext über Conversations/Threads | Bloßer Ordner; interne Feinstruktur 1:1 in der UX |
| **Reflection Intent** | Absicht *dieser* Interaktion, orthogonal zum Space | Mit Space identisch sein |
| **Derived record** | Summary, Hypothese, Emotionseinordnung, Pattern, Kandidat | Ohne Provenance, ohne Zeit, ohne „abgeleitet“-Kennzeichnung |
| **Handoff** | Journal / Reflection Memory / Knowledge Delta / Action Candidate | Kanshō-Aufgabenliste; mindnet-Graphkopie |
| **External ref** | Nullable Verweis mindnet / Obsidian | Besitz des Langzeitgraphen oder des Archivs |
| **Re-grounding event** | Wann, warum, aus welchen Quellen neu abgeleitet wurde | Nur Debug-Log ohne Quellenbindung |
| **Identity mapping** | Klasse A, lokal | In Summaries, Prompt-Archiv, mindnet-Handoff |
Point-in-Time Self / Lived Experience ist **keine eigene frühe Tabelle**, sondern die Konsequenz: Original + zeitgebundene Ableitungen, die frühere Versionen nicht überschreiben.
### Beziehungen, die das Modell skalieren
- `profile_id` an jeder personenbezogenen Struktur.
- Session 1:n Conversations (oder 1:1, später festlegbar) **ohne** 1:1 mit Thread.
- Conversation n:m Threads (ein Gespräch, mehrere Fäden; ein Faden über mehrere Gespräche).
- Thread n:1 oder n:m Spaces (Zuordnung darf vorläufig/unsicher sein).
- Jede Ableitung: `source_ids[]` auf Messages/Conversations, `as_of`, `confidence`/`uncertainty`, `kind`.
- Sichtbarkeit getrennt von Existenz: intern vs. nutzersichtbar (Space-Regel: KI strukturiert, Nutzer verwaltet nicht die Feinstruktur).
- Handoffs referenzieren Kanshō-Quellen; sie sind nicht der Dialogspeicher.
- Message-IDs idempotent (späteres Offline, Sync-Hypothese).
### Schichtenzuordnung
| Layer | Hier |
|---|---|
| 0 | Conversation/Messages, Session, Thread-Identität und Anker (Wiedervorlage), Space-Identität, Mapping |
| 1 | Working-Context-Snapshot, aktuelle Thread-Memory-Version, Space-„aktueller Stand“, später Relevanz/Saturation als registrierte Leser |
| 2a | Context Builder wählt Layer-1- und Quell-Ausschnitte; Gateway minimiert erneut |
| 2b | Fortsetzungsvorschlag, Space-Ansicht, Aufklappen zur Quelle keine zweite Verdichtungsformel |
Kontinuitätsarbeit ist ein **laufender Schreibpfad** auf Layer 0/1, auslösbar mitten im Dialog und am Session-Checkpoint. Sie ist unabhängig vom sichtbaren Abschluss und unabhängig davon, ob ein Handoff entsteht.
### Bewusst nicht schließen
- Thread-Zustandsliste (`Open` / `Anchored` / `Dormant` / `Resolved` sind Hinweise, kein Enum-Vertrag).
- Drift-Formel, Saturation-Score, Entry-IA (Contextual Continuation).
- Self-Model-Felder, Episodic-Schema in Kanshō (Handoff-Ziel mindnet/Obsidian).
- SQL, Sync-Engine, Offline-Store.
### Erweiterbarkeit
Neue fachliche Arten (z. B. weiteres Derived-`kind`, weiterer Handoff-Typ, feinere Space-Sicht) kommen als **registrierter Typ** plus Provenance, nicht als Sonderpfad in der Conversation-Tabelle und nicht als Prompt-Text im Code. Retrieval in den Langzeitgraphen bleibt Adapter + External ref, kein zweites mindnet in Kanshō.
Session-Ende ist ein Checkpoint, kein Zwang zu Journal/Memory/Delta/Action. Technisch heißt das: Persistenzpfade für Original und interne Kontinuität existieren unabhängig von Handoff-Tabellen oder sichtbaren Abschluss-Screens.
## 4. Offline
**Anforderung: entschieden. Ausprägung: offen.**
Leitplanken, die jede spätere Lösung erfüllen muss:
- Dialoge müssen lokal zwischenspeicherbar sein (fachliche Anforderung).
- Konfliktbehandlung und Sync sind zu definieren, bevor Multi-Gerät produktiv personenbezogene Dialoge führt.
- KI-Funktionen dürfen online bleiben; Fail Closed gilt für unsichere Provider, nicht als Ausrede, lokale Eingaben zu verwerfen.
- App-Shell-Cache (PWA) ist nicht dasselbe wie Dialog-Offline.
- Verschlüsselung at rest auf dem Gerät: offen, siehe `security.md`.
Lokale kleine Modelle: fachlich offen, nicht als Voraussetzung des Rahmens.
## 5. Sync-Hypothese
**Status: Hypothese**
Ein Gerät schreibt in eine lokale Queue, das Backend ist Source of Truth nach erfolgreichem Sync. Idempotente Message-IDs verhindern Duplikate. Konflikte an Originalnachrichten sind selten (ein Nutzer, ein aktiver Dialog); Konflikte an Summaries werden durch Neuberechnung aus der Quelle gelöst, nicht durch Merge von Verdichtungen.
Diese Hypothese ist zu verwerfen oder zu bestätigen, sobald Geräte- und Offline-Szenarien in Phase G4 entschieden sind.
## 6. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Mehrere Memory-Schichten | heutiger Fachstand | nicht als Schema einfrieren |
| Original vor Summary / Re-Grounding | fachliche Invariante | bindend, sobald Memory gebaut wird |
| Objektarten und Lebenszyklus-Trennung | Verträge in §3a | entschieden als Vorsorge, kein SQL |
| mindnet nicht Working Memory | wahrscheinlich | bevorzugte Richtung |
| Offline-Fähigkeit | erforderlich | entschieden (fachlich), Ausprägung offen |
| Sync/Konflikte/lokale DB | | offen |
| Drift-Berechnung | fachlich nötig | bevorzugte Richtung, Technik offen |
## 6.1 Implementierungsstand (Rahmen-Schnitt)
**Status: Code vorhanden, Heuristiken nicht.**
Layer-0-Tabellen `usage_sessions` (nicht Auth-`sessions`), `conversations`, `messages`, Thread-/Space-Identität, `derived_records` (append-only, Kind-Registry), Handoff-/Ref-/Re-Grounding-/Mapping-Hüllen. Continuity-Checkpoint ohne LLM. Admin trennt Original und Derived. Die Dialogseite speichert nur Originaltext.
Nicht enthalten: Entry-IA, Fadenerkennung, Thread-Enum, Summaries über das Gateway.
## 7. Offene Fragen
1. IndexedDB vs. SQLite-WASM vs. nur Service-Worker-Queue?
2. Wird Audio nach Transkription gelöscht oder als Medium behalten? → `voice_and_media.md`
3. Wie greift Re-Grounding bei teilweise gesyncten Sessions?
## 8. Querverweise
- `data_architecture.md`, `frontend_pwa_shell.md`, `privacy_gateway.md`
- Fachlich: `memory_and_context.md`, `resurfacing_and_saturation.md`

View File

@ -0,0 +1,234 @@
---
title: "Kanshō Plattformprinzipien: Registry, Prompt Engine, Konfiguration"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Extensibility / Registry / Prompt Engine / Configuration"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Plattformprinzipien
Kanonisches Home für die aus Mitai übernommenen **Querschnittsmuster**: einheitliche Registrierung, Prompt- und Workflow-Engine, Konfigurierbarkeit, Modularisierung.
Quelle: `C:\dev\mitai\.claude\docs\jinkendo-foundation\design-principles/` (Dokumente 14, 7). Mitai-Pfade sind Beispiele, keine Kanshō-Dateinamen.
Der fachliche Dialog-/MVP-Stand bleibt **nicht final**. Datenschutz und Guardrails sind bindend: die Engine ruft Modelle nur über das Privacy Gateway auf. Dieses Kapitel legt die **Plattform** fest, auf der spätere Fachmodule registriert werden nicht die Module selbst.
## 1. Leitentscheidung
**Status: entschieden** (Produktrahmen; Herkunft: Nutzeranforderung, Mitai-Foundation)
Kanshō übernimmt Mitais Erweiterungsarchitektur:
1. Neue Fähigkeiten kommen als **registrierte Komponenten** hinzu, nicht als Sonderpfade in Routern oder React.
2. KI läuft über **eine Prompt-/Workflow-Engine**, nicht über verstreute Provider-Calls.
3. Soweit fachlich sinnvoll, liegen Texte, Graphen, Sichtbarkeit und Limits in **administrierbarer Konfiguration**, nicht im Deploy-Code.
4. Berechnungen haben eine **Single Source of Truth** (Data Layer); UI und KI konsumieren dieselbe Schicht.
5. Jeder persönliche LLM-Aufruf der Engine geht durch das Privacy Gateway (`privacy_gateway.md`). Das ist fachliche Invariante, kein optionales Plugin. Konfiguration darf Prompts ändern, nicht den Egress-Pfad umgehen.
Nicht übernommen: Mitai-Domäne, parallele Legacy-Executoren, doppelte Registries, IDOR-Profilheader.
---
## 2. Registry- und Plugin-Muster
Mitai nutzt dasselbe Muster dreimal (Platzhalter, Dashboard-Widgets, CSV-Module). Kanshō übernimmt das **Meta-Muster** als einheitliches System für Modularisierung, Skalierung und Erweiterung.
### 2.1 Gemeinsames Schema
```text
REGISTRY (Kanon: ID + Metadaten + Policies)
├── Implementierung (Resolver, Komponente, Executor)
├── Validierung am Rand (unbekannte IDs ablehnen)
└── Konsumenten (GUI-Picker, API, Engine, Tests)
```
### 2.2 Verbindliche Prinzipien
| Prinzip | Kanshō |
|---|---|
| Eine autoritative ID-Liste pro Erweiterungstyp | Router und UI duplizieren keine Listen |
| IDs sind stabile Verträge | Rename = neuer Key + Deprecation |
| Metadaten getrennt von Implementierung | Admin/Export brauchen den Resolver nicht |
| Zwei Phasen: Kanon, dann Runtime-Binding | Auto-Registration per Package-Import |
| Validierung an der Registry-Grenze | unbekannte Keys scheitern früh |
| Konsumenten-agnostisch | dieselbe Registry für Engine, Admin, Diagnose |
| Erweiterungs-Checkliste | Katalog, Validierung, Test, Versions-Bump |
| Optional `requires_feature` | Entitlements nicht in der Registry auflösen |
| Eine Runtime, kein Spiegel | Mitai-Anti-Pattern `PLACEHOLDER_MAP` parallel zur Registry **nicht** nachbauen |
### 2.3 Erste erwartbare Registries (Rahmen, nicht Fachkatalog)
| Registry | Zweck | Status der *Inhalte* |
|---|---|---|
| Prompt-/Workflow-Definitionen | ausführbare KI-Bausteine | Engine entschieden; Kanshō-Prompts offen |
| Kontext-Platzhalter | `{{key}}` für Engine-Templates (Data Layer) | Muster entschieden; Key-Set offen |
| Privacy-Platzhalter | `[[SELF]]` usw. im External Model Context | getrennt von `{{key}}`; siehe `privacy_gateway.md` |
| Feature-IDs | limitierbare Fähigkeiten | Muster entschieden; Liste offen |
| UI-Katalog (Widgets/Karten/Nav-Beiträge) | konfigurierbare Oberflächenbausteine | Mechanik wie Mitai-Widgets; keine Health-Widgets |
| optionale Ingest-Module | später, falls Import nötig | Muster ja, Bedarf offen |
Neue erweiterbare IDs nur über den jeweiligen Kanon. Keine Ad-hoc-IDs in Prompt-Texten oder Layout-JSON.
---
## 3. Prompt- und Workflow-Engine
Mitai: `prompt_executor.py`, `workflow_executor.py`, Tabelle `ai_prompts`, Admin-CRUD, Preview, Import/Export.
### 3.1 Übernehmen
1. **Ein Executor** als einziger Einstieg für modellgebundene Ausführung (`base` / `pipeline` / `workflow`). Kein zweiter Pfad wie Mitais Legacy `insights.py`.
2. **Prompts und Workflows in der DB** (Name, Slug, Kategorie, aktiv, Template, Stages, Graph, Output-Format/Schema), nicht im Anwendungscode.
3. **Drei Typen:** `base` (Baustein), `pipeline` (Sequenz, Stages per Referenz), `workflow` (Graph, Verzweigung, Join).
4. **Platzhalter als Verträge** der Kontext-Registry; Templates enthalten `{{keys}}`, keine eingebettete Berechnung.
5. **Admin konfiguriert, Nutzer führt aus** (`require_admin` vs. `require_auth`).
6. **Preview ohne LLM**, Debug mit aufgelösten Platzhaltern (Ausgabe erst nach Gateway-Regeln; keine Klasse-A-Werte im Client-Debug).
7. **JSON Import/Export** Dev→Prod; System-Defaults mit Reset (`is_system_default` + `default_template`).
8. **LLM-Call nur als injizierte Callback-Funktion**, in Kanshō: Callback = Privacy Gateway, nicht Roh-OpenRouter.
### 3.2 Schichten in der Engine
| Schicht | Rolle |
|---|---|
| Templates / Graphen | Was konfiguriert ist (Admin) |
| Kontext-Platzhalter | Semantische Keys, Resolver |
| Data Layer | Werte für Keys (Layer 1) |
| Privacy Gateway | Minimierung, Pseudonymisierung, Egress, Demasking |
| Workflow-Executor | Reihenfolge, Verzweigung, Aggregation |
Fachliche Reflexions-Workflows (Tagesreflexion, Journal-Entwurf, …) werden **später als Konfiguration** in dieser Engine registriert, nicht als fest verdrahteter Python. Solange das Fachkonzept unfertig ist, bleibt die Bibliothek leer oder enthält nur technische Test-Prompts.
### 3.3 Nicht übernehmen
- Parallele Template-Engines und direkte LLM-Calls neben dem Executor.
- Doppelte Workflow-Speicher (`graph_data` und `workflow_definitions`).
- Frontend-Workflow-Editor ohne `RequireAdmin` (Mitai-Lücke).
- Unversioniertes Overwrite ohne Reset-Pfad.
- Debug-Responses mit Klarnamen oder vollem Internal Context.
- Vermischung von `{{key}}` (Kontext) und `[[ROLE]]` (Privacy).
Modellname darf vorerst Env-Default sein; Routing-Policy später über Konfiguration, nicht über neue Executoren.
---
## 4. Weitgehende Konfigurierbarkeit
Ziel wie Mitai: **Einfachheit in der Nutzer-UX, Differenzierung in administrierbarer Konfiguration.**
### 4.1 Administrierbar (DB / Admin-UI)
- Prompt-Texte, Pipelines, Workflow-Graphen, Aktiv/Sortierung, Output-Schema
- Feature-Definitionen und Limits (wenn Entitlements genutzt werden)
- UI-Katalog: welche Karten/Nav-Beiträge sichtbar, Layout-Overrides
- System-Prompts vs. Instanz-Anpassungen (Reset auf Default)
- später: Privacy-Profile (Strict/Balanced), sobald fachlich konkret
### 4.2 Code / Env (nicht pro Klick im Admin)
- Registry-Schema und Resolver-Implementierungen
- Auth-Gates, Privacy-Pipeline-Schritte, Fail-Closed
- Breakpoint, Router-Zuschnitt, Migrationsmechanismus
- Provider-Secrets
### 4.3 Durchsetzung
Keine Sonderlogik „nur dieses eine Feature hardcodiert im Router“, wenn es ein Registry- oder Prompt-Fall ist. Lint/Konvention: neue KI-Aufrufe nur über den Executor.
**Nicht administrierbar:** Abschalten oder Umgehen des Privacy Gateway, Speichern voller Prompts beim Provider, Training-Freigabe für persönliche Kontexte, Fail-Open auf unsichere Endpoints.
---
## 5. Data Layer als SSoT
Mitai Layer 0 → 1 → 2a/2b. Kanshō übernimmt die **Trennung**, nicht die Körper-Metriken.
| Schicht | Kanshō-Rahmen |
|---|---|
| 0 Persistenz | PostgreSQL, profilbezogen |
| 1 Ableitung | eine Stelle für berechnete/verdichtete Werte, die mehrere Konsumenten brauchen |
| 2a KI | Platzhalter-Resolver liest Layer 1, formatiert für Templates |
| 2b UI | Chart/Listen-Payloads aus Layer 1, keine zweite Formel in React |
Router und React rechnen keine parallelen Scores. Welche Layer-1-Funktionen existieren, folgt dem späteren Fachstand (Summaries, Saturation, …) und wird dann **registriert**, nicht vorab erfunden.
---
## 6. Feature- und Entitlement-Muster
Mitai trennt Identität (Auth) von „darf Feature X wie oft?“. Kanshō übernimmt die **zentrale** `check_feature_access`-Idee an der API, nicht das Fitness-Abo als Produkt.
- Eine Feature-Registry.
- Enforcement in der API, nicht nur in der UI.
- Kataloge **referenzieren** Feature-IDs, lösen Tiers nicht selbst auf.
- Ob Tiers, Trials oder nur an/aus gebraucht werden, ist **offen** (Fach- und Produktstand). Die Erweiterungspunkte werden trotzdem so gebaut, dass Limits später ohne Router-Umbau möglich sind.
### 6.1 Verlustfreie spätere Ausprägung
Der Code enthält absichtlich **kein** fertiges Abo. Er enthält die Auflösungshierarchie, damit Tarif, Limit und Billing später Zeilen und Policies sind, keine neue Auth.
| Später | Wohin | Nicht wohin |
|---|---|---|
| Weitere Tiers / Limits | `tiers`, `tier_limits`, Seed/Admin | Router, React, `profiles.role` |
| Nutzer einem Tarif zuweisen | `profiles.tier_id` | neues Account-Modell |
| Usage nach erfolgreichem Call | `increment_feature_usage` (bereits vorhanden, noch nicht am Erfolgspfad) | Frontend-Zähler |
| User-Overrides | `user_feature_restrictions` | Hardcode in `check_feature_access` |
| Billing / Stripe / Familienabo | eigene Billing-Schicht oder zentrales Jinkendo-Abo | Kanshō-Auth, Prompt-Engine |
| Feinere Rechte (Prompts vs. Diagnose) | Permission-Gate **über** `require_admin`, Rolle bleibt grob | Feature-IDs als Pseudo-Rollen |
| Neue limitierbare Fähigkeit | Zeile in `features` + Check an der API | neue Spalte `max_*` am Profil |
Bestehende Sessions, Profile und Prompt-Rows bleiben gültig. Ein späteres Abo ist ein Fill-in dieser Tabellen plus Enforcement, kein Schnitt durch Login oder Gateway.
---
## 7. Konfigurierbare UI-Bausteine
Mitai-Dashboard-Widgets: Backend-Katalog, Frontend-Register, Config-Whitelist, `allowed`/Entitlement, graceful unknown ID.
Kanshō übernimmt diese **Mechanik** für spätere Oberflächenbausteine (z. B. Einstiegsimpulse, Diagnosekarten). Es werden keine Mitai-Gewichts-/Schlaf-Widgets kopiert. Solange die IA unfertig ist, kann der Katalog leer oder auf Shell-Platzhalter beschränkt sein.
---
## 8. Erweiterungs-Checkliste (neues Modul)
Analog Mitai-Übergabe, für Kanshō:
1. Registry-Kanon: ID, Metadaten, Tests auf Eindeutigkeit.
2. Implementierung registriert sich (Package-Import).
3. Validierung am Rand.
4. API-Router des Moduls; keine Business-Logik nur im Frontend.
5. Optional Prompt/Workflow in der Engine, Ausführung nur über Executor + Gateway.
6. Optional Feature-ID und Admin-Sichtbarkeit.
7. `MODULE_VERSIONS` / App-Version anpassen.
8. Kein zweiter Ausführungspfad.
---
## 9. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Einheitliches Registry-Muster | ja | entschieden |
| Prompt-/Workflow-Engine mit DB-Konfiguration | ja | entschieden |
| Ein Executor, Callback = Privacy Gateway | ja | entschieden |
| Admin konfiguriert, User führt aus | ja | entschieden |
| Data Layer SSoT | ja | entschieden |
| Feature-Check an der API (Muster) | ja | entschieden |
| Konkrete Prompts, Keys, Features, Widgets | | offen (Fachstand) |
| Mitai-Membership als Produkt | | nicht übernehmen |
| Doppel-Registry / Legacy-Executor | | verworfen |
## 10. Offene Fragen
Rahmen-Slice (User-Admin, Prompt-DB, Platzhalter-Registry, Feature-Check) ist im Code angelegt. Bleibt offen:
1. Liegen später fachliche System-Prompts zusätzlich als JSON im Git (`prompts.seed.json`) oder nur in der DB mit Export-Disziplin? Der Lade-Pfad existiert; die Datei ist bewusst leer.
2. Braucht Kanshō früh Limits ungleich unbegrenzt, oder bleibt `ai_calls` ohne Kontingent bis zur Produktentscheidung?
3. Selbst-Registrierung vs. nur Admin legt Nutzer an.
## 11. Querverweise
- Mitai: Foundation #1 Prompt Engine, #2 Data Layer, #3 Entitlements, #4 Registry, #7 Widgets
- Technisch: `ai_architecture.md`, `backend_and_api.md`, `admin_diagnostics.md`, `privacy_gateway.md`, `data_architecture.md`
- Fachlich: Arbeitsstand, nicht als Prompt-Inhalte vorwegnehmen

View File

@ -0,0 +1,113 @@
---
title: "Kanshō Privacy Gateway und AI-Egress"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Privacy Gateway / Egress"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Privacy Gateway und AI-Egress
Technische Abbildung der fachlichen Guardrails. Kanonisches Fachhome: `../functional/guardrails.md` (inkl. Root Cause §2, Datenklassen §5, Gateway §6, Entscheidungsstand §22). Dieses Kapitel dupliziert die Herleitung nicht.
**Rang:** Datenschutz und Guardrails sind im Fachkonzept **grundlegend und entschieden**. Sie stehen nicht auf derselben Ebene wie unfertige Dialog- oder MVP-Schnitte. Offene Punkte betreffen Verfahren und rechtliche Ausprägung (Entity Detection, Encryption at rest, DSFA), nicht die Invariante selbst.
## 1. Invariante
> Identität bleibt lokal. Externe Intelligenz erhält nur den für die Aufgabe notwendigen, minimierten und soweit sinnvoll pseudonymisierten Kontext.
Herkunft: `guardrails.md` §2.4; querschnittlich in der fachlichen Zielarchitektur. Providerunabhängig: OpenRouter ist Kandidat, kein Ersatz für das lokale Gateway. OpenRouter-eigene Guardrail-Features ergänzen höchstens, sie ersetzen die Kanshō-Schicht nicht.
Pseudonymisierung ist keine Anonymisierung. Kanshō behauptet keine vollständige Anonymität nach außen.
Persönlicher Kontext wird **nie** direkt aus UI, Dialog-Router, Prompt-Engine oder Admin-Preview an OpenRouter, Provider oder Tools gesendet. Die Engine darf den Provider nur über den Gateway-Callback erreichen (`platform_extensibility.md`).
### 1.1 Guardrail-Vorrang
Fachlich §19: Guardrails haben Vorrang vor Modellqualität, Kosten, Latenz, Komfort und automatischem Provider-Fallback. Eine Anfrage wird abgelehnt oder degradiert, wenn die Policy nicht erfüllbar ist. Administrierbare Prompts, Workflows oder Feature-Flags dürfen das Gateway nicht abschalten.
## 2. Trust Zones
| Zone | Inhalt | Egress |
|---|---|---|
| Local Trusted Zone | Identity Mapping, Klarnamen, Secrets, volle Primärquellen, Obsidian-Pfade | kein Roh-Egress |
| External AI Zone | External Model Context (Klasse B) | nur nach Policy-Check, ZDR |
| Tool-/Plugin-Zone | Suche, Plugins, Speech | eigene Policy, nicht implizit mit LLM-Freigabe |
## 3. Datenklassen
- **A Local Only:** Mapping, Secrets, vollständige Identität, interne IDs soweit identifizierend. Verlassen die Trusted Zone nicht.
- **B Pseudonymized AI Context:** Normalfall persönlicher Dialoge. Stabile, nicht sprechende Platzhalter (`[[SELF]]`, `[[PERSON:PARTNER]]`). Minimierung zusätzlich zur Maskierung.
- **C Low-Identity:** generische, nicht-personalisierte Inhalte; schwächere Transformation zulässig, eigene Policy bleibt nötig.
Quasi-Identifikatoren (Beruf+Ort+Familie) sind fachlich erkannt; die Bewertungsmethode ist offen, der Gateway-Auftrag „nicht nur Klarnamen“ gilt trotzdem.
## 4. Pflichtpipeline
Jeder persönliche Modellaufruf durchläuft lokal:
1. Context Minimization
2. Entity Detection
3. Pseudonymization
4. Policy Check
5. Provider/Endpoint Eligibility (ZDR, kein Training, kein Prompt-Logging)
6. Egress Validation
7. Response Validation
8. Local Rehydration / Demasking
9. Audit Metadata (ohne volle Prompts)
10. Fail Closed
Fail Closed: kein stillschweigender Fallback auf einen weniger geschützten Provider.
LLM-Egress und Tool-/Web-/Transkriptions-Egress sind getrennte Policies. Ein für ZDR-LLM freigegebener Kontext darf nicht automatisch an Web Search oder Speech-Cloud.
## 5. Internal vs. External Context
Der Context Builder darf intern mit Klaridentität arbeiten. Vor dem Socket nach außen existiert eine zweite Repräsentation ohne Mapping-Tabelle.
Demasking ausschließlich in der Trusted Zone, nach Validierung (Platzhalter erhalten, keine erratenen Klarnamen, keine Policy-Verstöße).
## 6. Audit
Speichern: Request-ID, Zeit, Policy, Provider-/Modellkennung, ZDR-/Region-Status, Typ maskierter Entitäten, Prüf- und Fallback-Status.
Nicht speichern: volle Prompts, volle Antworten, echte Namen, Mapping.
Admin-Sichtbarkeit der Mapping-Tabelle nur für besonders berechtigte Rollen (`admin_diagnostics.md`).
## 7. Beziehung zu anderen Kapiteln
- Resurfacing darf keinen breiteren Egress rechtfertigen als der aktuelle Task braucht.
- Re-Grounding läuft lokal und erzeugt danach erneut einen minimierten External Context.
- Voice/Transkription: eigener Egress-Typ (`voice_and_media.md`).
- Lived Experience / Re-Grounding: volle Primärquellen bleiben lokal; externes Modell bekommt erneut nur Klasse B.
- Prompt-Engine: Preview ohne LLM ist erlaubt; Debug-Ausgaben an den Admin ohne Klasse-A-Klartext und ohne Mapping.
## 8. Was Phase H noch offen lässt
Interview H1 (Encryption at rest, vollständiges Löschen, Portabilität, DSFA bei späterem Mehrbenutzerbetrieb) ergänzt diese Invariante, ersetzt sie nicht. Privacy Profiles (Strict / Balanced / Low-Identity) sind fachlich perspektivisch; Default muss die Invariante erfüllen.
## 9. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Privacy Gateway vor persönlichem Egress | zwingend, nicht abschaltbar | entschieden |
| Guardrails vor Qualität/Kosten/Latenz | ja | entschieden (fachlich) |
| Providerunabhängige Guardrails | ja | entschieden (fachlich) |
| ZDR / kein Training / kein Prompt-Log | persönliche Kontexte | entschieden bzw. bevorzugte verbindliche Policy |
| Fail Closed | ja | bevorzugte Richtung |
| Keine Anonymitätsbehauptung | Pseudonymisierung | entschieden (fachlich) |
| OpenRouter | Kandidat, nicht Gateway-Ersatz | entschieden |
| EU-Routing | bevorzugt | offen in Ausprägung |
| Encryption at rest, Delete, DSFA | Phase H | offen |
| Implementierung Entity Detection | deterministisch vs. KI-gestützt | offen |
## 10. Offene Fragen
Übernommen aus `guardrails.md` §21, hier nicht vorentschieden: Entitätstypen, Quasi-Identifikatoren, Pseudonym-Stabilität, Mehrnutzer-Trennung der Mappings, Verschlüsselung der Mapping-Tabelle, rechtliche DSFA bei Mehrbenutzer-Produktbetrieb.
## 11. Querverweise
- Fachlich: `../functional/guardrails.md`
- Technisch: `ai_architecture.md`, `security.md`, `voice_and_media.md`, `platform_extensibility.md`

View File

@ -0,0 +1,126 @@
---
title: "Kanshō Produktrahmen und Stack"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Product Frame / Stack"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Produktrahmen und Stack
Kanonisches Home für die übertragbare Laufzeit- und UI-Hülle. Fachliche Identität bleibt in `../functional/produktvision_und_produktidentitaet.md`.
## 1. Herkunft
Explizite Nutzeranforderung: Der **Produktrahmen** (PWA, Responsive, Desktop/Mobile-Shell, Authentifizierung, Userrollen, Multiuser auf Instanz-Ebene, API- und Deploy-Muster) soll **weitgehend** von Mitai übernommen werden, nicht neu entwickelt werden.
Der aktuelle fachliche Konzeptionsstand zu Dialog, Memory und MVP ist **nicht final**. **Datenschutz und Guardrails sind bindend.** Mitai enthält kein Kanshō-Privacy-Gateway; dieser Pfad wird ergänzt, nicht von Mitai übernommen.
Referenz: Mitai Jinkendo, nicht Kairo und nicht Shinkan.
Mitai-Foundation beschreibt übertragbare Muster unabhängig von Körper-Tracking: `C:\dev\mitai\.claude\docs\jinkendo-foundation/`.
## 2. Systemüberblick
Kanshō folgt dem Mitai-3-Tier-Modell.
```text
Nutzergerät (PWA)
↓ HTTPS
Frontend (React + Vite, Nginx in Prod)
↓ /api
Backend (FastAPI)
PostgreSQL
↓ nur über Privacy Gateway
Externe KI / Tools
```
Alle persönlichen Daten und das Identity Mapping liegen in der Local Trusted Zone. Siehe `privacy_gateway.md`.
## 3. Stack
| Schicht | Technologie | Status |
|---|---|---|
| UI | React 18 ohne TypeScript-Pflicht | entschieden |
| Build | Vite, `vite-plugin-pwa` | entschieden |
| Routing | React Router 6 | entschieden |
| Icons | Lucide React | entschieden |
| API | FastAPI, Python 3.12 | entschieden |
| DB | PostgreSQL 16 | entschieden |
| Auth-Hash | bcrypt | entschieden |
| Container | Docker Compose | entschieden |
| Reverse Proxy | Nginx vor dem Frontend-Bundle (Prod-Muster Mitai) | bevorzugte Richtung |
| Hosting | selbst gehostet, Gitea-CI wie die Familie | entschieden |
State Management: Context (Auth, Profil), kein Redux. Herkunft: Mitai-Frontend-Muster.
## 4. Was als Rahmen übernommen wird
Weitgehend das Mitai-Muster, inklusive konkreter Datei- und UX-Mechanik:
- PWA-Installierbarkeit, Vite PWA-Plugin, Manifest.
- Breakpoint 1024px, Bottom-Nav, Desktop-Sidebar, Safe Area.
- Nav-SSoT, Bereichs-Shells, `RequireAdmin`, Admin-Realm.
- AuthContext, Login/Register/Verify/Setup analog Mitai.
- Server-Sessions, Rollen `user` / `admin`, Rate Limits, bcrypt.
- Ein Modul = ein Backend-Router, API-First, Fehlerformat `{detail}`.
- Nummerierte SQL-Migrationen, Compose Dev/Prod, Gitea-Branches.
- Multiuser auf Instanz-Ebene, Isolation über Session-`profile_id`.
- Registry-Muster, Prompt-/Workflow-Engine, Data Layer, Feature-Check an der API: `platform_extensibility.md`.
Platzhalter-Seiten der Mitai-Nav dürfen als **leere Shell-Routen** mitwandern, bis der Fachstand Kanshō-Inhalte vorgibt. Sie werden nicht mit Tracking-Fachlogik gefüllt.
## 5. Was nicht übernommen wird
Nur Domäne, Mandantenmodell und bekannte Sicherheitslücken nicht der Rahmen.
| Nicht übernehmen | Begründung |
|---|---|
| Körper-Tracking, Messwerte, Capture-Fachlogik, CSV-Import, Membership als Kern | Mitai-Domäne |
| Shinkan: Verein → viele Nutzer | anderes Produktmodell |
| `X-Profile-Id` ohne Session-Bindung | IDOR; siehe `auth_identity_and_roles.md` |
| TypeScript-Pflicht, JWT/SSO als Ist | nicht Mitai-Rahmen |
| Direkter LLM-Provider-Call aus der Engine | Kanshō ergänzt Privacy Gateway (`privacy_gateway.md`) |
Kanshō-spezifische Startlogik, Dialog-IA und Memory-Modelle kommen aus der **späteren** Fachkonzeption. Sie sind kein Grund, Login, Shell, Deploy oder Router anders zu bauen.
## 6. Multiuser ohne Mandanten
**Status: entschieden**
Kanshō ist nutzerbezogen. Ein Account entspricht einem Profil. Es gibt keine Vereine, Clubs oder Organisations-Workspaces.
Mehrere Nutzer auf einer Instanz sind zulässig (wie Mitai). Isolation erfolgt über `profile_id` der Session, nicht über eine Tenant-Tabelle.
Admin sieht Diagnose- und Verwaltungsfunktionen, nicht die private Reflexion anderer Nutzer als Alltagspfad. Details: `admin_diagnostics.md`.
## 7. KI im Rahmen
Externe Modelle sind für leistungsfähige Dialoge vorgesehen. Sie sind **kein** Teil des UI-Frameworks. Jeder persönliche Aufruf läuft über das Privacy Gateway. OpenRouter ist Kandidat, kein Zwang.
Mitai-Prompt-Engine-Muster (ein Executor, Registry, Admin-Konfiguration) wird übernommen; der LLM-Callback ist in Kanshō das Privacy Gateway, nicht Roh-OpenRouter. Siehe `platform_extensibility.md`.
## 8. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| 3-Tier PWA | React + FastAPI + PostgreSQL | entschieden |
| Referenz | Mitai-Rahmen weitgehend | entschieden |
| Fachliche IA / Startroute | nicht durch den Rahmen vorwegnehmen | offen (Fach-Arbeitsstand) |
| Mandantenmodell | keines | entschieden |
| Konkrete Ports/Domains | nicht aus Mitai kopieren | offen |
| Gemeinsame UI-Bibliothek der Familie | eigene Kopie des Musters, kein Shared-Package vorausgesetzt | bevorzugte Richtung |
## 9. Offene Fragen
1. Soll der Rahmen später in ein gemeinsames Jinkendo-Paket extrahiert werden oder als kopiertes Muster in Kanshō leben?
2. Welche Domain und welche Host-Ports gelten für Dev/Prod?
3. Wird Nginx als eigener Container beibehalten oder das Vite-Preview nur für lokale Entwicklung genutzt?
## 10. Querverweise
- Fachlich: `../functional/produktvision_und_produktidentitaet.md` §19, §21.11
- Technisch: alle übrigen Kapitel dieses Ordners
- Mitai: `.claude/docs/technical/ARCHITECTURE.md`, `FRONTEND.md`, Foundation-Index

View File

@ -0,0 +1,103 @@
---
title: "Kanshō Runtime, Migrationen und Deploy"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Runtime / Deploy / Migrations"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Runtime, Migrationen und Deploy
Kanonisches Home für Container-Start, Schema-Evolution und Gitea-Pipelines. Mitai-Referenz: `MIGRATION_DEPLOY_DESIGN_PRINCIPLES.md`, `docker-compose.yml`, `docker-compose.dev-env.yml`, `.gitea/workflows/`.
## 1. Umgebungen
Muster der Familie: zwei Umgebungen, zwei Branches.
| Umgebung | Git-Branch | Rolle |
|---|---|---|
| Development | `develop` | Auto-Deploy nach Push |
| Production | `main` | Auto-Deploy nach Merge |
**Status: entschieden** als Betriebsmuster. Konkrete Domains, Host-Pfade und **Produktions-Ports** sind **offen** und werden nicht aus Mitai (`3002`/`8002`, `3099`/`8099`, `bodytrack/`) kopiert.
Lokale Entwicklung (ohne Docker) verwendet eigene Ports, nicht die Vite-/FastAPI-Defaults und nicht die Ports anderer lokaler Repos:
| Dienst | Port | Bindung |
|---|---|---|
| Frontend (Vite) | 5188 | `strictPort`, kein Ausweichen |
| Backend (Uvicorn) | 8018 | explizit |
Nicht verwenden: 5173, 5174, 4000, 4001, 8000.
Hypothese für spätere Benennung: `kansho.jinkendo.de` / `dev.kansho.jinkendo.de`, analog zur Foundation-Tabelle. Nicht festgelegt.
## 2. Container
Docker Compose mit mindestens:
- Frontend (Nginx + gebündelte PWA in Prod; Vite-Dev optional lokal)
- Backend (FastAPI)
- PostgreSQL 16
Secrets und SMTP nur in Server-`.env`, nicht im Repository. Vorlage: `.env.example`.
Watchtower oder gleichwertiges Image-Update: Mitai-Betrieb, für Kanshō **später prüfen**.
## 3. Schema-Migrationen
**Status: entschieden** (Muster)
1. Nummerierte SQL-Dateien `backend/migrations/XXX_*.sql`.
2. Tracking-Tabelle `schema_migrations`.
3. Startup: Postgres ready → Migrationen → App-Prozess.
4. Idempotenz wo möglich (`IF NOT EXISTS`).
5. Kein automatisches Schema-Downgrade.
Greenfield-Basis analog Mitai `schema.sql` ist zulässig. Änderungen danach nur über nummerierte Migrationen.
Fail-fast beim Start, wenn Migrationen scheitern. Kein stilles Weiterlaufen mit Drift.
## 4. Gitea CI/CD
Mitai-Pipeline als Vorlage:
```text
push develop → deploy-dev.yml → compose build/up → Healthcheck
push/PR nach Tests → test.yml (pytest, Frontend-Build)
merge in main → deploy-prod.yml
```
**Status: bevorzugte Richtung.** Workflow-Dateien erst anlegen, wenn Code und Runner-Pfade existieren.
Kanshō-Repo liegt bereits auf Gitea (`Lars/Kansho`). HTTPS-Push ist eingerichtet.
## 5. Was Deploy nicht übernimmt
- Blue-Green oder Multi-Region
- Automatisches Rollback der Datenbank
- Mitai-Verzeichnisnamen auf dem Pi
- Fachliche Datenberechnungen
## 6. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Compose + Postgres + nummerierte SQL-Migrationen | ja | entschieden |
| develop → Dev, main → Prod | ja | entschieden |
| Ports/Domains/Hostpfade | Prod offen; lokal Frontend 5188 / Backend 8018 | lokal festgelegt, Prod offen |
| Gitea Workflows | Mitai-Muster | bevorzugte Richtung |
| Auto-Rollback | nein | verworfen |
## 7. Offene Fragen
1. Läuft Kanshō auf demselben Raspberry-Pi-Host wie Mitai, mit eigenen Compose-Projekten?
2. Gemeinsames oder separates Postgres?
3. Backup-Rhythmus und Restore-Übung vor erstem persönlichen Dialogdatenbestand.
## 8. Querverweise
- Technisch: `product_frame_and_stack.md`, `security.md`
- Fachlich: Offline und Sync bleiben `memory_storage_and_offline.md`; Deploy löst Offline nicht.
- Mitai: `MIGRATIONS.md`, `.gitea/workflows/`

View File

@ -0,0 +1,69 @@
---
title: "Kanshō Security"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Security"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Security
Kanonisches Home für Zugriffssicherheit des Mitai-Rahmens (Auth, TLS, Secrets, IDOR). Der Schutz personenbezogener Daten beim AI-Egress liegt in `privacy_gateway.md`. Encryption at rest, selektives Vergessen und DSFA: Interview H1, Ausprägung **offen**.
## 1. Rahmen, übernommen aus Mitai
- bcrypt für Passwort-Hashes, keine Klartext-PINs.
- Rate Limiting an Login/Register (Mitai: Login 5/min, Register restriktiver) konkrete Zahlen für Kanshō **bevorzugte Richtung**, bei Bedarf anpassen.
- CORS explizit, nicht `*` in Prod.
- HTTPS vor der App (Synology/Reverse Proxy der Familie).
- Secrets nur Server-`.env` (SMTP, Provider-Keys, DB). Nie im Frontend, nie ins Git.
- Session-Invalidierung beim Logout.
- Kein IDOR über Client-gewähltes Profil (`auth_identity_and_roles.md`).
## 2. Kanshō-spezifisch
- Provider-Keys für LLM nur hinter dem Gateway, mit Policy-Check vor Verwendung.
- Audit-Logs ohne Prompt-Volltext.
- Admin-API nicht über Security-by-Obscurity; immer `require_admin`.
- Fremde `profile_id` in Pfaden/Bodies ignorieren oder 403.
## 3. Transport und Ruhe
| Thema | Stand | Status |
|---|---|---|
| TLS nach außen | ja, Betriebsstandard der Familie | entschieden als Anforderung |
| Verschlüsselung at rest (DB, Backups, Gerät) | | offen |
| Client-Token in localStorage | XSS-Risiko bekannt | bevorzugte Richtung, später bewerten |
| Vollständiges Löschen / selektives Vergessen | fachlich offen | offen |
| Export/Portabilität | fachlich gewünscht (menschenlesbar) | offen in Technik |
## 4. Dependency- und Deploy-Hygiene
- Keine Provider-Keys in Compose-Dateien im Repo.
- Fail Closed bei fehlender Privacy-Policy des Endpoints, nicht Fail Open wegen Verfügbarkeit.
- Healthchecks ohne Leak von Nutzerdaten.
## 5. Safety vs. Security
Krisengrenzen und medizinische Hochrisikothemen sind **nicht** dieses Kapitel (`ai_architecture.md`, fachlich H3). Hier: Zugriff, Secrets, Isolation, Logging.
## 6. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Guardrails / Privacy-Invariante | bindend, unabhängig von H1-Ausprägung | entschieden |
| IDOR-Profilheader | verworfen | entschieden |
| Encryption at rest, Delete, Cookie-Sessions | | offen |
| Shared-Postgres mit anderen Apps | Default nein | bevorzugte Richtung |
## 7. Offene Fragen
1. Getrennte Datenbank pro App vs. gemeinsamer Server mit getrennten Schemas?
2. Backup-Verschlüsselung und Schlüsselhaltung.
3. DSFA und Retention, sobald Mehrbenutzer über den privaten Betrieb hinausgeht.
## 8. Querverweise
- `auth_identity_and_roles.md`, `privacy_gateway.md`, `runtime_and_deploy.md`
- Fachlich: `../functional/guardrails.md` §1819

View File

@ -0,0 +1,256 @@
---
title: "Kanshō Technische Zielarchitektur"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technische Zielarchitektur / Master Structure"
parent_document: "../functional/fachliche_zielarchitektur.md"
---
# Kanshō Technische Zielarchitektur
## 1. Zweck dieses Dokuments
Dieses Dokument definiert die **technische Zielarchitektur** von Kanshō: Umsetzung, Speicher, Schnittstellen und Betriebsform.
Es ist das führende Referenzdokument des Ordners `docs/architecture/technical/`. Die übrigen technischen Kapitel werden hier eingeordnet.
Die **fachliche Zielarchitektur bleibt führend**. Dialog-, Memory-, Space-, Output- und MVP-Schnitte sind **Arbeitsstand** und nicht final. **Datenschutz, Privacy Gateway und Guardrails** sind davon ausgenommen: Sie sind bereits eine verbindliche Architektur-Invariante (`../functional/guardrails.md`) und dürfen technisch nicht relativiert werden.
Technische Kapitel beschreiben, *wie* umgesetzt wird. Sie ersetzen, kürzen oder überschreiben keine Fachtexte.
Dieses Dokument ist bewusst **keine Implementierungsanleitung** und enthält keine SQL-Schemas, OpenAPI-Verträge oder Prompt-Texte.
---
## 2. Verhältnis zur fachlichen Konzeption
Herkunft: gemeinsam getroffene Entscheidung in der Fachdoku (`fachliche_zielarchitektur.md` §2.6, `dialogue_model.md` §8.5).
| Ebene | Zuständig für | Nicht zuständig für |
|---|---|---|
| Fachliche Architektur | Nutzererlebnis, Verantwortlichkeiten, fachliche Objekte, Nutzerkontrollen | Speicherform, Algorithmen, Deploy |
| Technische Architektur | Stack, Runtime, API, Persistenzgrenzen, Egress, Betrieb | Produktidentität, neue Fachobjekte |
Technische Vorentscheidungen sind nur dann zulässig, wenn sie:
- eine fachliche Produktgrenze unmittelbar beeinflussen,
- später nur mit sehr hohem Aufwand revidierbar wären,
- oder zwingende Auswirkungen auf Datenschutz, Offline-Fähigkeit, Sicherheit oder Integrationen haben.
Der Interviewplan (`interview_plan.md`) vertieft weiter Dialog, Datenverträge, Voice, Offline-Ausprägung, Encryption/Löschen (Phase H1) und MVP. Die **Guardrail-Invariante** (Identität lokal, Privacy Gateway, getrennte Egress-Policies) ist fachlich bereits entschieden und hier technisch bindend. Unfertige Fachobjekte (Threads, Spaces, Prompts) bleiben Arbeitsstand.
### 2.1 Fachstand: was Arbeitsstand ist, was bindet
**Status: entschieden** (Herkunft: Nutzeranforderung 2026-08-19 plus `guardrails.md`)
Nicht als finales Produktschema behandeln:
- Dialog-IA, Startroute, Memory-/Thread-Modelle, Reflection Outputs, MVP-Schnitt.
Verbindlich für jede technische Umsetzung, auch für den Mitai-Rahmen:
- Identität bleibt lokal.
- Persönlicher Kontext geht nie direkt an externe Modelle oder Tools.
- Lokales Privacy Gateway vor jedem persönlichen AI-Egress.
- Datenklassen A/B/C, Minimierung und Pseudonymisierung, lokales Mapping.
- Guardrails haben Vorrang vor Modellqualität, Kosten, Latenz und Komfort (`guardrails.md` §19).
- Ausprägungen wie Entity-Detection-Verfahren, Encryption at rest, DSFA bleiben offen sie schwächen die Invariante nicht.
### 2.2 Produktrahmen versus MVP
Der aus Mitai übernommene Produktrahmen (PWA, Auth, Rollen, Layout-Shell, Deploy) ist ein **Umsetzungs-Slice „Produktrahmen“**. Er darf **weitgehend** der Mitai-Implementierung folgen.
Ein späterer MVP bleibt eine fachliche Entscheidung (Interview Phase I). Die dort genannte kleine Reflexionsschleife ist **aktueller Arbeitsstand**, kein technisch eingefrorener Lieferumfang. Die leere oder fast leere Mitai-Shell ist Infrastruktur, nicht der MVP.
---
## 3. Dokumentationsprinzipien
Die technischen Kapitel folgen denselben Regeln wie die Fachdoku.
### 3.1 Keine stillschweigende Verdichtung
Änderungen erfolgen durch Ergänzung, explizite Ersetzung, Kennzeichnung als überholt, Decision Record oder Gitea-Versionierung.
### 3.2 Entscheidungen und Ideen trennen
Jedes Kapitel unterscheidet:
- **Entschieden**
- **Bevorzugte Richtung**
- **Hypothese**
- **Offen**
- **Verworfen**
- **Später prüfen**
### 3.3 Herkunft von Aussagen erhalten
Wo sinnvoll kenntlich machen, ob ein Punkt stammt aus:
- expliziter Nutzeranforderung,
- gemeinsam getroffener Entscheidung,
- Mitai-Referenzimplementierung,
- technischem Zwang,
- späterer Validierung.
### 3.4 Keine unnötige Duplizierung
Ein Thema hat ein kanonisches Home. Fachliches bleibt in `docs/architecture/functional/`. Technisches verlinkt dorthin.
### 3.5 Stabile Dateinamen
Dateinamen enthalten keine Versionsnummern. Frontmatter mindestens: Titel, Version, Status, Datum, `document_role`.
### 3.6 Schutz vor Übervereinfachung
Interne technische Modelle dürfen differenziert sein, wenn der **jeweils aktuelle** Fachstand es erfordert. Vereinfachung nur nach Capability-Check (`fachliche_zielarchitektur.md` §2.8).
Die folgenden Punkte sind fachliche **Querschnittsinvarianten** (`fachliche_zielarchitektur.md` §2.9). Privacy/Guardrails gelten für den Produktrahmen **sofort**. Die übrigen vier begrenzen spätere Domänen-Persistenz und KI-Nutzung; sie werden nicht als SQL-Schema vorgezogen und nicht abgeschwächt, sobald die jeweilige Fähigkeit gebaut wird.
| Invariante | Fachliches Home | Technisches Home |
|---|---|---|
| Context Fidelity / Re-Grounding | `../functional/context_fidelity_and_regrounding.md` | `memory_storage_and_offline.md`, `data_architecture.md`, `privacy_gateway.md` |
| Thread Resurfacing / Reflection Saturation | `../functional/resurfacing_and_saturation.md` | `data_architecture.md`, `ai_architecture.md` |
| Session Lifecycle ≠ Thread Lifecycle | `../functional/reflection_outputs.md` | `data_architecture.md` |
| Point-in-Time Self / Lived Experience | `../functional/self_model_and_lived_experience.md` | `data_architecture.md`, `privacy_gateway.md` |
| Privacy Gateway / External-AI-Guardrails | `../functional/guardrails.md` | `privacy_gateway.md`, `ai_architecture.md`, `voice_and_media.md` |
---
## 4. Referenz und bewusste Nicht-Übernahmen
**Referenzimplementierung:** Mitai Jinkendo (`C:\dev\mitai`, Gitea `Lars/mitai-jinkendo`).
**Foundation-Muster:** `C:\dev\mitai\.claude\docs\jinkendo-foundation/`.
**Produktrahmen: weitgehend von Mitai übernehmen** (Shell, Auth, Router, Compose, Migrationen, Admin-Realm, Nav-Mechanik, Breakpoint, **Registry-Muster, Prompt-/Workflow-Engine, Data-Layer-Trennung, Feature-Check an der API**). Abweichungen nur dort, wo sie zwingend sind.
**Nicht als Rahmen übernehmen bzw. zwingend ergänzen:**
- Shinkan-Vereins- und Mandantenmodell. Kanshō bleibt nutzerbezogen: ein Login = ein Profil.
- Kairo als UI-/Verhaltensvorlage.
- Mitai-**Domäne** (Körper-Tracking, Messwerte, CSV-Import, Membership-Tiers als Produktkern).
- Mitai-Auth-Lücke: Profilwahl über `X-Profile-Id` ohne Session-Bindung. Siehe `auth_identity_and_roles.md`.
- TypeScript als Pflicht-Stack.
- JWT/SSO als aktuelle Auth.
- **Direktaufrufe an LLM-Provider** wie in Mitai (`prompt_executor` → OpenRouter). Kanshō ergänzt zwingend das Privacy Gateway; das ist die zentrale Abweichung vom Mitai-KI-Pfad.
Mitai-Hauptnav-**Beschriftungen** (Erfassen, Ziele, …) sind Domäne. Die Nav-**Mechanik** (SSoT, Bottom-Nav, Sidebar, Shells) gehört zum Rahmen und wird übernommen. Welche Kanshō-Einträge später in dieser Mechanik stehen, folgt dem **dann aktuellen** Fachstand.
---
## 5. Technische Grundentscheidungen (Stack)
Herkunft: Nutzeranforderung (nahezu identischer Produktrahmen) plus Mitai-Referenz. Fachliche Vorbedingungen: PWA, Mobile First, responsive Desktop (`produktvision_und_produktidentitaet.md` §19).
| Thema | Entscheidung | Status |
|---|---|---|
| Lieferform | Progressive Web App (Mitai-Rahmen) | entschieden |
| Frontend | React 18, Vite, React Router 6, Lucide | entschieden |
| TypeScript | kein Pflicht-Stack | entschieden |
| Backend | FastAPI, Python 3.12 | entschieden |
| Datenbank | PostgreSQL 16 | entschieden |
| API-Stil | REST, API-First, Business-Logik nur Backend | entschieden |
| Auth | serverseitige Sessions, bcrypt, Rollen `user` \| `admin` | entschieden |
| Mandanten | keine Org-/Vereinsmandanten | entschieden |
| Container | Docker Compose, Dev- und Prod-Umgebung | entschieden |
| CI/CD | Gitea, Branch `develop` → Dev, `main` → Prod | entschieden |
| KI-Laufzeit | externe Inferenz grundsätzlich vorgesehen | entschieden (fachlich) |
| Prompt-/Workflow-Engine | ein Executor, DB-Konfiguration, Typen base/pipeline/workflow | entschieden (Mitai-Rahmen) |
| Komponenten-Registry | einheitliches ID-Kanon-Muster | entschieden (Mitai-Rahmen) |
| Privacy Gateway | zwingende lokale Schicht vor persönlichem AI-Egress | entschieden (fachliche Invariante) |
| OpenRouter | Kandidat, kein Architekturzwang, kein Gateway-Ersatz | entschieden (fachlich) |
| Ports / Domains | Muster wie Mitai, konkrete Werte nicht 1:1 kopieren | offen |
| Offline-Ausprägung | Anforderung entschieden, Mechanismus offen | offen |
| Transkription | Anforderung entschieden, Mechanismus offen | offen |
Details: `product_frame_and_stack.md`, `platform_extensibility.md`.
---
## 6. Kanonische Kapitelstruktur
```text
technische_zielarchitektur.md
documentation_index.md
product_frame_and_stack.md
auth_identity_and_roles.md
frontend_pwa_shell.md
backend_and_api.md
runtime_and_deploy.md
platform_extensibility.md
data_architecture.md
memory_storage_and_offline.md
privacy_gateway.md
ai_architecture.md
integrations_technical.md
admin_diagnostics.md
voice_and_media.md
security.md
```
| Datei | Kanonisches Thema | Reife v0.1 |
|---|---|---|
| `product_frame_and_stack.md` | 3-Tier, Stack, Nicht-Übernahmen | Rahmen entschieden |
| `auth_identity_and_roles.md` | Session, Rollen, Admin-Gate | Rahmen entschieden |
| `frontend_pwa_shell.md` | PWA-Shell, Breakpoint, Nav-SSoT | Mitai-Rahmen; Kanshō-IA folgt späterem Fachstand |
| `backend_and_api.md` | Router, API-First, Fehlerformat | Rahmen entschieden |
| `runtime_and_deploy.md` | Migrationen, Compose, Gitea | Muster entschieden, Ports offen |
| `platform_extensibility.md` | Registry, Prompt-/Workflow-Engine, Konfiguration, Data Layer | Mitai-Prinzipien entschieden; Fachinhalte offen |
| `data_architecture.md` | Persistenzgrenzen, Entitäten ohne Schema | Arbeitsstand, nicht final |
| `memory_storage_and_offline.md` | Dialogspeicher, Sync, Offline | Arbeitsstand, Sync offen |
| `privacy_gateway.md` | Trust Zones, Egress, Demasking | Invariante entschieden; Verfahren offen |
| `ai_architecture.md` | Inferenzpfad, Agenten (später) | Gateway bindend; Agenten offen |
| `integrations_technical.md` | Handoffs zur Produktfamilie | Arbeitsstand, Verträge offen |
| `admin_diagnostics.md` | Diagnoseansicht | Mitai-Admin-Realm; Kanshō-Inhalte später |
| `voice_and_media.md` | Sprache / Transkription | Bedarf im Fachstand, Ausprägung offen |
| `security.md` | Rate Limit, Secrets, Encryption | Teil entschieden, Rest offen |
---
## 7. Capability-Check vor Vereinfachung
Vor einer technischen Modellvereinfachung ist zu prüfen:
1. Welche Fachanforderungen brauchen die Struktur Invariante (Privacy) vs. Arbeitsstand (Dialog/MVP)?
2. Welche späteren Fähigkeiten (Memory, Self Model, Threading, Provenance, Re-Grounding) wären betroffen?
3. Welche Integrationen hängen davon ab?
4. Ist die Vereinfachung reversibel?
5. Kann wahrgenommene Komplexität stattdessen in der UX abstrahiert werden?
---
## 8. Aktueller Entscheidungsstand (übergreifend)
| Thema | Stand | Status |
|---|---|---|
| Fachlich vs. technisch getrennt | getrennte Ordner; Fachdoku führend; Guardrails bindend, Dialog/MVP nicht final | entschieden |
| Produktrahmen | Mitai weitgehend (PWA, Auth, Shell, Deploy, Registry, Prompt-Engine) | entschieden |
| Fachstand Dialog/MVP | Arbeitsstand, nicht final | entschieden |
| Datenschutz / Guardrails | Invariante, bindend | entschieden |
| Kanshō-Start-IA / Dialogmodell | folgt späterem Fachstand, nicht jetzt technisch einfrieren | offen |
| Dialogspeicher vs. mindnet | aktueller Fach-Arbeitsstand | bevorzugte Richtung |
| Originaldialog / Session ≠ Thread / Action Candidates | aktueller Fach-Arbeitsstand | nicht als Schema einfrieren |
| AI-Agentenrollen | Reflection/Memory/Journal-Agenten | offen |
| Thread-State-Machine | fachliche Zustände keine technische Vollständigkeit | offen |
| MVP-Schnitt | nach fachlicher Konzeption, nicht dieser Rahmen | offen |
---
## 9. Noch nicht in v0.1
- SQL-Schemas und Migrationsdateien
- OpenAPI
- Prompt-Texte und Modellwahl
- konkrete Ports, Domains, Serverpfade
- MVP-Feature-Schnitt
- Anwendungscode unter `frontend/` oder `backend/`
---
## 10. Querverweise
- Fachliche Governance: `../functional/fachliche_zielarchitektur.md`
- Context Bundles: `documentation_index.md`
- Interviewfortschritt: `../functional/interview_plan.md`

View File

@ -0,0 +1,61 @@
---
title: "Kanshō Sprache und Medien"
version: "0.1"
status: "Arbeitsstand"
date: "2026-08-19"
product_family: "Jinkendo"
document_role: "Technical Chapter / Voice / Transcription / Media"
parent_document: "technische_zielarchitektur.md"
---
# Kanshō Sprache und Medien
Kanonisches Home für Spracheingabe und Transkription. Fachlich erforderlich, Ausprägung offen (`produktvision_und_produktidentitaet.md` §19.5). Interview G3 ist nicht abgeschlossen.
## 1. Anforderung
Auf dem Smartphone darf Reflexion nicht zwingend Tippen erfordern. Transkription ist eine zentrale technische Funktion.
Sie ist kein Freibrief für Cloud-Speech ohne Policy.
## 2. Egress-Klasse
Transkriptionsdienste sind eine **eigene Trust- und Policy-Zone** (`guardrails.md` §13). Ein ZDR-LLM-Endpoint legalisiert nicht denselben Audiostream oder denselben Klartext an einen Speech-Provider.
Klasse-A-Identität und unnötige Quasi-Identifikatoren gelten auch für Metadaten von Audio (Dateiname, GPS, Geräte-ID).
## 3. Offene Ausprägung
Nicht entschieden, daher kein Implementierungszwang in v0.1:
- Live vs. nachträgliche Transkription
- lokal vs. serverseitig vs. externer Speech-Dienst
- Bearbeitung vor Übernahme in den Dialog
- Sprecher-/Pausenlogik
- lange Aufnahmen
- Audio löschen nach Transkript vs. als Medium behalten
- Push-to-talk vs. Daueraufnahme
- Offline-Transkription
## 4. Leitplanken für jede spätere Wahl
- Das Transkript wird zur Originalrepräsentation des Dialogs oder zu einer klar gekennzeichneten Ableitung; die Regel muss dokumentiert werden, bevor Re-Grounding darauf aufsetzt.
- Nutzerkorrektur vor Persistenz als bevorzugte Richtung (fachlich offen, technisch vorbereiten).
- Fail Closed: kein unsicherer Cloud-Speech-Fallback.
- Journal- und Writing-Profile laufen auf Text nach Korrektur, nicht auf Roh-ASR-Fehlern als „persönlicher Stil“.
## 5. Entscheidungsstand
| Thema | Stand | Status |
|---|---|---|
| Transkription erforderlich | ja | entschieden (fachlich) |
| Eigener Egress-Typ | ja | entschieden (fachlich) |
| Verfahren, Vendor, Speicherdauer Audio | | offen |
## 6. Offene Fragen
Siehe Interview G3. Zusätzlich: Wie verhält sich Voice zum Working Context, während ein Thread-Wechsel erkannt wird?
## 7. Querverweise
- `privacy_gateway.md`, `frontend_pwa_shell.md`, `memory_storage_and_offline.md`
- Fachlich: `../functional/produktvision_und_produktidentitaet.md` §19.5

14
frontend/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#3d5a4c" />
<title>Kanshō</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

6131
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
frontend/package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "kansho",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --port 5188 --strictPort",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^0.383.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.0",
"vite": "^5.2.12",
"vite-plugin-pwa": "^0.20.0"
}
}

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#3d5a4c"/>
<circle cx="32" cy="32" r="14" fill="none" stroke="#f6f4ef" stroke-width="3"/>
<circle cx="32" cy="32" r="4" fill="#f6f4ef"/>
</svg>

After

Width:  |  Height:  |  Size: 254 B

140
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,140 @@
import { NavLink, Navigate, Outlet, Route, Routes, useLocation } from 'react-router-dom'
import { LogOut } from 'lucide-react'
import { useAuth } from './context/AuthContext.jsx'
import { getMainNavItems } from './config/appNav.js'
import LoginScreen from './pages/LoginScreen.jsx'
import SetupScreen from './pages/SetupScreen.jsx'
import HomePage from './pages/HomePage.jsx'
import PlaceholderPage from './pages/PlaceholderPage.jsx'
import DialoguePage from './pages/DialoguePage.jsx'
import SettingsPage from './pages/SettingsPage.jsx'
import AdminHomePage from './pages/AdminHomePage.jsx'
import AdminUsersPage from './pages/AdminUsersPage.jsx'
import AdminPromptsPage from './pages/AdminPromptsPage.jsx'
import AdminPlaceholdersPage from './pages/AdminPlaceholdersPage.jsx'
import AdminFeaturesPage from './pages/AdminFeaturesPage.jsx'
import AdminDialoguePage from './pages/AdminDialoguePage.jsx'
import RequireAdmin from './layouts/RequireAdmin.jsx'
import AdminShell from './layouts/AdminShell.jsx'
function navItemActive(pathname, item, routerIsActive) {
if (item.to.startsWith('/admin')) return pathname.startsWith('/admin')
return routerIsActive
}
function BottomNav({ isAdmin }) {
const items = getMainNavItems(isAdmin)
const loc = useLocation()
return (
<nav className="bottom-nav">
{items.map((item) => {
const Icon = item.icon
return (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
navItemActive(loc.pathname, item, isActive) ? 'nav-item active' : 'nav-item'
}
end={item.to === '/'}
>
<Icon size={20} />
<span>{item.label}</span>
</NavLink>
)
})}
</nav>
)
}
function DesktopSidebar({ isAdmin, onLogout, name }) {
const items = getMainNavItems(isAdmin)
const loc = useLocation()
return (
<aside className="desktop-sidebar">
<div className="brand">Kanshō</div>
<nav>
{items.map((item) => {
const Icon = item.icon
return (
<NavLink
key={item.to}
to={item.to}
className={({ isActive }) =>
navItemActive(loc.pathname, item, isActive) ? 'nav-item active' : 'nav-item'
}
end={item.to === '/'}
>
<Icon size={18} />
<span>{item.label}</span>
</NavLink>
)
})}
</nav>
<div className="sidebar-footer">
<span className="user-name">{name}</span>
<button type="button" className="ghost" onClick={onLogout}>
<LogOut size={16} /> Abmelden
</button>
</div>
</aside>
)
}
function AppShell() {
const { session, isAdmin, logout } = useAuth()
return (
<div className="app-shell">
<DesktopSidebar isAdmin={isAdmin} onLogout={logout} name={session?.profile?.name} />
<div className="app-main">
<header className="mobile-header">
<strong>Kanshō</strong>
<button type="button" className="ghost" onClick={logout}>Abmelden</button>
</header>
<main className="page">
<Outlet />
</main>
<BottomNav isAdmin={isAdmin} />
</div>
</div>
)
}
export default function App() {
const { loading, needsSetup, session } = useAuth()
if (loading) {
return <div className="centered">Lädt </div>
}
if (needsSetup) {
return <SetupScreen />
}
if (!session) {
return <LoginScreen />
}
return (
<Routes>
<Route element={<AppShell />}>
<Route path="/" element={<HomePage />} />
<Route path="/dialog" element={<DialoguePage />} />
<Route
path="/journal"
element={<PlaceholderPage title="Journal" note="Journaling ist Werkzeug, nicht Produktkern. Die Inhalte folgen dem Fachkonzept." />}
/>
<Route path="/settings" element={<SettingsPage />} />
<Route element={<RequireAdmin />}>
<Route path="/admin" element={<AdminShell />}>
<Route index element={<AdminHomePage />} />
<Route path="users" element={<AdminUsersPage />} />
<Route path="prompts" element={<AdminPromptsPage />} />
<Route path="placeholders" element={<AdminPlaceholdersPage />} />
<Route path="features" element={<AdminFeaturesPage />} />
<Route path="dialogue" element={<AdminDialoguePage />} />
</Route>
</Route>
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
)
}

20
frontend/src/api.js Normal file
View File

@ -0,0 +1,20 @@
export async function api(path, { token, method = 'GET', body } = {}) {
const headers = {}
if (token) headers['X-Auth-Token'] = token
if (body !== undefined) headers['Content-Type'] = 'application/json'
const response = await fetch(path, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined
})
const data = await response.json().catch(() => null)
if (!response.ok) {
const detail = data?.detail
const message = typeof detail === 'string' ? detail : (detail?.message || 'Anfrage fehlgeschlagen')
const error = new Error(message)
error.status = response.status
error.payload = data
throw error
}
return data
}

162
frontend/src/app.css Normal file
View File

@ -0,0 +1,162 @@
:root {
--bg: #f6f4ef;
--ink: #1c1917;
--muted: #6b645c;
--accent: #3d5a4c;
--card: #fffcf7;
--line: #e4ddd3;
--danger: #9b3d3d;
--nav-h: 64px;
--sidebar-w: 220px;
}
* { box-sizing: border-box; }
html, body, #root { margin: 0; min-height: 100%; }
body {
font-family: "Segoe UI", system-ui, sans-serif;
background: var(--bg);
color: var(--ink);
}
h1 { font-size: 1.4rem; margin: 0 0 0.6rem; }
p { line-height: 1.5; }
.muted { color: var(--muted); }
.error { color: var(--danger); }
.centered { min-height: 100vh; display: grid; place-items: center; }
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 16px;
padding: 1.25rem;
}
.auth-screen {
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.5rem;
}
.auth-screen .card { width: min(420px, 100%); display: grid; gap: 0.8rem; }
label { display: grid; gap: 0.35rem; font-size: 0.9rem; }
input {
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.7rem 0.8rem;
font: inherit;
}
button {
background: var(--accent);
color: white;
border: 0;
border-radius: 10px;
padding: 0.75rem 1rem;
font: inherit;
cursor: pointer;
}
button.ghost {
background: transparent;
color: var(--ink);
padding: 0.4rem 0.6rem;
}
button:disabled { opacity: 0.6; }
.app-shell { min-height: 100vh; }
.app-main { min-height: 100vh; display: flex; flex-direction: column; }
.mobile-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.8rem 1rem;
border-bottom: 1px solid var(--line);
}
.page {
flex: 1;
padding: 1rem 1rem calc(var(--nav-h) + 1rem);
}
.bottom-nav {
position: fixed;
left: 0; right: 0; bottom: 0;
height: var(--nav-h);
display: flex;
background: var(--card);
border-top: 1px solid var(--line);
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.15rem;
color: var(--muted);
text-decoration: none;
font-size: 0.72rem;
}
.nav-item.active { color: var(--accent); }
.desktop-sidebar { display: none; }
.meta { display: grid; grid-template-columns: 8rem 1fr; gap: 0.4rem 1rem; }
.meta dt { color: var(--muted); }
.meta dd { margin: 0; }
h2 { font-size: 1.1rem; margin: 1.2rem 0 0.5rem; }
textarea, select {
border: 1px solid var(--line);
border-radius: 10px;
padding: 0.7rem 0.8rem;
font: inherit;
width: 100%;
}
.stack { display: grid; gap: 0.7rem; margin-top: 1rem; }
.admin-subnav {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 1rem;
}
.admin-link {
text-decoration: none;
color: var(--muted);
border: 1px solid var(--line);
border-radius: 999px;
padding: 0.3rem 0.75rem;
font-size: 0.9rem;
}
.admin-link.active { color: var(--accent); border-color: var(--accent); }
table.plain { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
table.plain th, table.plain td { text-align: left; padding: 0.45rem 0.3rem; border-bottom: 1px solid var(--line); }
.row-item { display: flex; justify-content: space-between; gap: 0.8rem; align-items: center; list-style: none; }
.row-actions { display: flex; gap: 0.3rem; }
ul.stack { padding: 0; }
.dialogue-layout { display: grid; gap: 1rem; margin-top: 1rem; }
@media (min-width: 720px) {
.dialogue-layout { grid-template-columns: 14rem 1fr; }
}
.message-list { max-height: 40vh; overflow: auto; margin-bottom: 0.8rem; }
button.ghost.active { color: var(--accent); }
@media (min-width: 1024px) {
.app-shell { display: grid; grid-template-columns: var(--sidebar-w) 1fr; }
.desktop-sidebar {
display: flex;
flex-direction: column;
border-right: 1px solid var(--line);
background: var(--card);
padding: 1.2rem 0.8rem;
min-height: 100vh;
}
.desktop-sidebar .brand { font-weight: 700; padding: 0 0.6rem 1rem; }
.desktop-sidebar nav { display: grid; gap: 0.2rem; }
.desktop-sidebar .nav-item {
flex-direction: row;
justify-content: flex-start;
gap: 0.6rem;
padding: 0.55rem 0.7rem;
border-radius: 10px;
font-size: 0.95rem;
}
.desktop-sidebar .nav-item.active { background: #eef3ef; }
.sidebar-footer { margin-top: auto; display: grid; gap: 0.4rem; padding: 0.6rem; }
.mobile-header, .bottom-nav { display: none; }
.page { padding: 1.5rem; }
}

View File

@ -0,0 +1,10 @@
export function getAdminNavItems() {
return [
{ to: '/admin', label: 'Übersicht', end: true },
{ to: '/admin/users', label: 'Nutzer' },
{ to: '/admin/prompts', label: 'Prompts' },
{ to: '/admin/placeholders', label: 'Platzhalter' },
{ to: '/admin/features', label: 'Kontingente' },
{ to: '/admin/dialogue', label: 'Dialog' }
]
}

View File

@ -0,0 +1,14 @@
import { Home, MessageCircle, BookOpen, Settings, Shield } from 'lucide-react'
export function getMainNavItems(isAdmin) {
const items = [
{ to: '/', label: 'Start', icon: Home },
{ to: '/dialog', label: 'Dialog', icon: MessageCircle },
{ to: '/journal', label: 'Journal', icon: BookOpen },
{ to: '/settings', label: 'Einstellungen', icon: Settings }
]
if (isAdmin) {
items.push({ to: '/admin', label: 'Admin', icon: Shield })
}
return items
}

View File

@ -0,0 +1,111 @@
import { createContext, useContext, useState, useEffect } from 'react'
const AuthContext = createContext(null)
const TOKEN_KEY = 'kansho_token'
function errorMessage(payload, fallback) {
if (!payload) return fallback
if (typeof payload.detail === 'string') return payload.detail
if (payload.detail?.message) return payload.detail.message
return fallback
}
export function AuthProvider({ children }) {
const [session, setSession] = useState(null)
const [loading, setLoading] = useState(true)
const [needsSetup, setNeedsSetup] = useState(false)
useEffect(() => {
checkStatus()
}, [])
const checkStatus = async () => {
try {
const r = await fetch('/api/auth/status')
const status = await r.json()
if (status.needs_setup) {
setNeedsSetup(true)
setLoading(false)
return
}
const token = localStorage.getItem(TOKEN_KEY)
if (token) {
const me = await fetch('/api/auth/me', { headers: { 'X-Auth-Token': token } })
if (me.ok) {
const profile = await me.json()
setSession({ token, profile_id: profile.id, role: profile.role, profile })
setLoading(false)
return
}
localStorage.removeItem(TOKEN_KEY)
}
} catch (e) {
console.error('Auth check failed', e)
}
setLoading(false)
}
const login = async ({ email, password }) => {
const r = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
const data = await r.json()
if (!r.ok) throw new Error(errorMessage(data, 'Login fehlgeschlagen'))
localStorage.setItem(TOKEN_KEY, data.token)
const me = await fetch('/api/auth/me', { headers: { 'X-Auth-Token': data.token } })
const profile = await me.json()
setSession({ token: data.token, profile_id: data.profile_id, role: data.role, profile })
setNeedsSetup(false)
return data
}
const setup = async (formData) => {
const r = await fetch('/api/auth/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
const data = await r.json()
if (!r.ok) throw new Error(errorMessage(data, 'Setup fehlgeschlagen'))
localStorage.setItem(TOKEN_KEY, data.token)
const me = await fetch('/api/auth/me', { headers: { 'X-Auth-Token': data.token } })
const profile = await me.json()
setSession({ token: data.token, profile_id: data.profile_id, role: data.role, profile })
setNeedsSetup(false)
return data
}
const logout = async () => {
const token = session?.token || localStorage.getItem(TOKEN_KEY)
if (token) {
await fetch('/api/auth/logout', {
method: 'POST',
headers: { 'X-Auth-Token': token }
}).catch(() => {})
}
localStorage.removeItem(TOKEN_KEY)
setSession(null)
}
return (
<AuthContext.Provider value={{
session,
loading,
needsSetup,
isAdmin: session?.role === 'admin',
login,
setup,
logout
}}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
return ctx
}

View File

@ -0,0 +1,23 @@
import { NavLink, Outlet } from 'react-router-dom'
import { getAdminNavItems } from '../config/adminNav.js'
export default function AdminShell() {
const items = getAdminNavItems()
return (
<div className="admin-realm">
<nav className="admin-subnav">
{items.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
className={({ isActive }) => (isActive ? 'admin-link active' : 'admin-link')}
>
{item.label}
</NavLink>
))}
</nav>
<Outlet />
</div>
)
}

View File

@ -0,0 +1,11 @@
import { Navigate, Outlet, useLocation } from 'react-router-dom'
import { useAuth } from '../context/AuthContext.jsx'
export default function RequireAdmin() {
const { isAdmin } = useAuth()
const loc = useLocation()
if (!isAdmin) {
return <Navigate to="/" replace state={{ from: loc.pathname, adminDenied: true }} />
}
return <Outlet />
}

16
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,16 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.jsx'
import { AuthProvider } from './context/AuthContext.jsx'
import './app.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
)

View File

@ -0,0 +1,64 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
export default function AdminDialoguePage() {
const { session } = useAuth()
const [data, setData] = useState(null)
const [detail, setDetail] = useState(null)
const [error, setError] = useState('')
useEffect(() => {
api('/api/admin/dialogue', { token: session.token })
.then(setData)
.catch((e) => setError(e.message))
}, [session.token])
const open = async (id) => {
setError('')
try {
setDetail(await api(`/api/admin/dialogue/conversations/${id}`, { token: session.token }))
} catch (err) {
setError(err.message)
}
}
return (
<section className="card">
<h1>Dialog-Diagnose</h1>
<p className="muted">{data?.note}</p>
{error && <p className="error">{error}</p>}
{data && (
<dl className="meta">
<dt>Conversations</dt>
<dd>{data.instance?.conversations}</dd>
<dt>Messages (Original)</dt>
<dd>{data.instance?.messages}</dd>
<dt>Derived</dt>
<dd>{data.instance?.derived_records}</dd>
<dt>Threads</dt>
<dd>{data.instance?.threads}</dd>
</dl>
)}
<ul className="stack">
{data?.own_conversations?.map((item) => (
<li key={item.id}>
<button type="button" className="ghost" onClick={() => open(item.id)}>
{item.title || item.id} · {item.message_count} Original · {item.derived_count} Derived
</button>
</li>
))}
</ul>
{detail && (
<>
<h2>Original</h2>
<pre className="code">{JSON.stringify(detail.original_messages, null, 2)}</pre>
<h2>Derived</h2>
<pre className="code">{JSON.stringify(detail.derived_records, null, 2)}</pre>
</>
)}
<p className="muted"><Link to="/dialog">Zur dünnen Persistenz-Ansicht</Link></p>
</section>
)
}

View File

@ -0,0 +1,48 @@
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
export default function AdminFeaturesPage() {
const { session } = useAuth()
const [sub, setSub] = useState(null)
const [features, setFeatures] = useState([])
const [error, setError] = useState('')
useEffect(() => {
Promise.all([
api('/api/subscription', { token: session.token }),
api('/api/features', { token: session.token })
])
.then(([subscription, list]) => {
setSub(subscription)
setFeatures(list)
})
.catch((e) => setError(e.message))
}, [session.token])
return (
<section className="card">
<h1>Kontingente</h1>
<p className="muted">Feature-Registry und Tier-Auflösung sind vorbereitet. Kein Stripe, keine Mitai-Fitness-Tarife, keine Coupons.</p>
{error && <p className="error">{error}</p>}
{sub && (
<p>Aktueller Rahmen-Tier: <strong>{sub.tier?.name}</strong> ({sub.tier?.id}). Billing: {sub.billing ? 'aktiv' : 'keine Zahlungsanbindung'}.</p>
)}
<table className="plain">
<thead>
<tr><th>Feature</th><th>Typ</th><th>Reset</th><th>Kategorie</th></tr>
</thead>
<tbody>
{features.map((feature) => (
<tr key={feature.id}>
<td>{feature.id}</td>
<td>{feature.limit_type}</td>
<td>{feature.reset_period}</td>
<td>{feature.category}</td>
</tr>
))}
</tbody>
</table>
</section>
)
}

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
export default function AdminHomePage() {
const { session } = useAuth()
const [health, setHealth] = useState(null)
const [error, setError] = useState('')
useEffect(() => {
api('/api/admin/health', { token: session.token })
.then(setHealth)
.catch((e) => setError(e.message))
}, [session.token])
return (
<section className="card">
<h1>Admin</h1>
<p>Rahmen für Nutzer, Prompts, Platzhalter und Kontingente. Keine Reflexions-Prompts und kein Zahlungsabo.</p>
{error && <p className="error">{error}</p>}
{health && (
<>
<dl className="meta">
<dt>Nutzer</dt>
<dd>{health.inventory?.users}</dd>
<dt>Prompts in der DB</dt>
<dd>{health.inventory?.prompts}</dd>
<dt>Originalnachrichten</dt>
<dd>{health.inventory?.dialogue?.messages ?? 0}</dd>
<dt>Derived</dt>
<dd>{health.inventory?.dialogue?.derived_records ?? 0}</dd>
</dl>
<p className="muted">Weiter: <Link to="/admin/users">Nutzer</Link> · <Link to="/admin/prompts">Prompts</Link> · <Link to="/admin/placeholders">Platzhalter</Link> · <Link to="/admin/features">Kontingente</Link> · <Link to="/admin/dialogue">Dialog</Link></p>
</>
)}
</section>
)
}

View File

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
export default function AdminPlaceholdersPage() {
const { session } = useAuth()
const [data, setData] = useState(null)
const [error, setError] = useState('')
useEffect(() => {
api('/api/placeholders', { token: session.token })
.then(setData)
.catch((e) => setError(e.message))
}, [session.token])
return (
<section className="card">
<h1>Platzhalter</h1>
<p className="muted">Zwei getrennte Systeme: Kontext <code>{'{{key}}'}</code> aus der Registry, Privacy <code>[[TOKEN]]</code> vor dem Egress. Unbekannte Keys scheitern.</p>
{error && <p className="error">{error}</p>}
{data && (
<>
<h2>Kontext</h2>
<ul>
{data.context.map((item) => (
<li key={item.key}><code>{item.syntax}</code> {item.description}</li>
))}
</ul>
<h2>Privacy</h2>
<ul>
{data.privacy.map((item) => (
<li key={item.token}><code>{item.token}</code> {item.description}</li>
))}
</ul>
</>
)}
</section>
)
}

View File

@ -0,0 +1,100 @@
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
const emptyForm = {
slug: '',
name: '',
description: '',
category: 'uncategorized',
prompt_type: 'base',
template: '',
required_feature: 'ai_calls',
active: true
}
export default function AdminPromptsPage() {
const { session } = useAuth()
const [prompts, setPrompts] = useState([])
const [form, setForm] = useState(emptyForm)
const [preview, setPreview] = useState(null)
const [error, setError] = useState('')
const [notice, setNotice] = useState('')
const load = () => api('/api/prompts', { token: session.token }).then(setPrompts).catch((e) => setError(e.message))
useEffect(() => { load() }, [session.token])
const save = async (e) => {
e.preventDefault()
setError('')
setNotice('')
try {
await api('/api/prompts', { token: session.token, method: 'POST', body: form })
setForm(emptyForm)
setPreview(null)
await load()
} catch (err) {
setError(err.message)
}
}
const runPreview = async (prompt) => {
setError('')
try {
setPreview(await api('/api/prompts/preview', { token: session.token, method: 'POST', body: { prompt_id: prompt.id } }))
} catch (err) {
setError(err.message)
}
}
const runExecute = async (prompt) => {
setError('')
setNotice('')
try {
await api('/api/prompts/execute', { token: session.token, method: 'POST', body: { prompt_id: prompt.id } })
} catch (err) {
setNotice(`Ausführung blockiert (erwartet, solange kein Provider): ${err.message}`)
}
}
return (
<section className="card">
<h1>Prompts</h1>
<p className="muted">Texte liegen in der Datenbank bzw. in <code>backend/config/prompts.seed.json</code>, nicht im Anwendungscode. Domain-Prompts gehören nicht in dieses Gerüst.</p>
{error && <p className="error">{error}</p>}
{notice && <p className="muted">{notice}</p>}
{prompts.length === 0 && <p>Noch keine Prompts. Bibliothek bewusst leer.</p>}
<ul className="stack">
{prompts.map((prompt) => (
<li key={prompt.id} className="row-item">
<div>
<strong>{prompt.name}</strong>
<div className="muted">{prompt.slug} · {prompt.prompt_type}</div>
</div>
<div className="row-actions">
<button type="button" className="ghost" onClick={() => runPreview(prompt)}>Preview</button>
<button type="button" className="ghost" onClick={() => runExecute(prompt)}>Execute</button>
</div>
</li>
))}
</ul>
{preview && <pre className="code">{JSON.stringify(preview, null, 2)}</pre>}
<form className="stack" onSubmit={save}>
<h2>Prompt in der DB anlegen</h2>
<label>Slug <input value={form.slug} onChange={(e) => setForm({ ...form, slug: e.target.value })} required /></label>
<label>Name <input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required /></label>
<label>
Template
<textarea
rows={8}
value={form.template}
onChange={(e) => setForm({ ...form, template: e.target.value })}
placeholder="Nur {{now}} und [[SELF]] / [[PERSON:…]] / [[PLACE:…]] — keine Klarnamen."
/>
</label>
<button type="submit">Speichern</button>
</form>
</section>
)
}

View File

@ -0,0 +1,84 @@
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
const emptyForm = { email: '', name: '', password: '', role: 'user' }
export default function AdminUsersPage() {
const { session } = useAuth()
const [users, setUsers] = useState([])
const [form, setForm] = useState(emptyForm)
const [error, setError] = useState('')
const load = () => api('/api/users', { token: session.token }).then(setUsers).catch((e) => setError(e.message))
useEffect(() => { load() }, [session.token])
const create = async (e) => {
e.preventDefault()
setError('')
try {
await api('/api/users', { token: session.token, method: 'POST', body: form })
setForm(emptyForm)
await load()
} catch (err) {
setError(err.message)
}
}
const toggleStatus = async (user) => {
setError('')
try {
await api(`/api/users/${user.id}`, {
token: session.token,
method: 'PATCH',
body: { status: user.status === 'active' ? 'disabled' : 'active' }
})
await load()
} catch (err) {
setError(err.message)
}
}
return (
<section className="card">
<h1>Nutzer</h1>
<p className="muted">Ein Login ist ein Profil. Keine Profilumschaltung, kein Vereinsmodell.</p>
{error && <p className="error">{error}</p>}
<table className="plain">
<thead>
<tr><th>Name</th><th>E-Mail</th><th>Rolle</th><th>Status</th><th /></tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{user.role}</td>
<td>{user.status}</td>
<td>
<button type="button" className="ghost" onClick={() => toggleStatus(user)}>
{user.status === 'active' ? 'Deaktivieren' : 'Aktivieren'}
</button>
</td>
</tr>
))}
</tbody>
</table>
<form className="stack" onSubmit={create}>
<h2>Nutzer anlegen</h2>
<label>Name <input value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required /></label>
<label>E-Mail <input type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} required /></label>
<label>Passwort <input type="password" value={form.password} onChange={(e) => setForm({ ...form, password: e.target.value })} required minLength={4} /></label>
<label>
Rolle
<select value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })}>
<option value="user">user</option>
<option value="admin">admin</option>
</select>
</label>
<button type="submit">Anlegen</button>
</form>
</section>
)
}

View File

@ -0,0 +1,99 @@
import { useEffect, useState } from 'react'
import { api } from '../api.js'
import { useAuth } from '../context/AuthContext.jsx'
export default function DialoguePage() {
const { session } = useAuth()
const [conversations, setConversations] = useState([])
const [activeId, setActiveId] = useState(null)
const [messages, setMessages] = useState([])
const [draft, setDraft] = useState('')
const [error, setError] = useState('')
const loadList = () =>
api('/api/dialogue/conversations', { token: session.token })
.then(setConversations)
.catch((e) => setError(e.message))
const openConversation = async (id) => {
setError('')
try {
const data = await api(`/api/dialogue/conversations/${id}`, { token: session.token })
setActiveId(id)
setMessages(data.messages || [])
} catch (err) {
setError(err.message)
}
}
useEffect(() => { loadList() }, [session.token])
const start = async () => {
setError('')
try {
const usage = await api('/api/dialogue/sessions', { token: session.token, method: 'POST', body: { intent: '' } })
const conv = await api('/api/dialogue/conversations', {
token: session.token,
method: 'POST',
body: { usage_session_id: usage.id, title: 'Gespräch' }
})
await loadList()
await openConversation(conv.id)
} catch (err) {
setError(err.message)
}
}
const send = async (e) => {
e.preventDefault()
if (!activeId || !draft.trim()) return
setError('')
try {
await api(`/api/dialogue/conversations/${activeId}/messages`, {
token: session.token,
method: 'POST',
body: { body: draft, role: 'user' }
})
setDraft('')
await openConversation(activeId)
await loadList()
} catch (err) {
setError(err.message)
}
}
return (
<section className="card">
<h1>Dialog</h1>
<p className="muted">
Nur Persistenz der Originalnachrichten. Keine intelligente Einstiegs-IA, keine automatischen Fäden, kein LLM-Summary.
</p>
{error && <p className="error">{error}</p>}
<button type="button" onClick={start}>Neues Gespräch</button>
<div className="dialogue-layout">
<ul className="stack">
{conversations.map((item) => (
<li key={item.id}>
<button type="button" className={item.id === activeId ? 'ghost active' : 'ghost'} onClick={() => openConversation(item.id)}>
{item.title || 'Gespräch'} · {item.message_count} Nachrichten
</button>
</li>
))}
</ul>
{activeId && (
<div>
<div className="message-list">
{messages.map((item) => (
<p key={item.id}><strong>{item.role}:</strong> {item.body}</p>
))}
</div>
<form className="stack" onSubmit={send}>
<textarea rows={4} value={draft} onChange={(e) => setDraft(e.target.value)} placeholder="Originaltext speichern …" />
<button type="submit">Speichern</button>
</form>
</div>
)}
</div>
</section>
)
}

View File

@ -0,0 +1,12 @@
export default function HomePage() {
return (
<section className="card">
<h1>Start</h1>
<p>
Der Produktrahmen läuft. Die Startlogik bleibt fachlich offen: Kontinuität des Dialogs
statt Funktions-Dashboard.
</p>
<p className="muted">Keine Tracking-Kacheln, keine Aufgabenliste, keine Diagnose.</p>
</section>
)
}

View File

@ -0,0 +1,42 @@
import { useState } from 'react'
import { useAuth } from '../context/AuthContext.jsx'
export default function LoginScreen() {
const { login } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const onSubmit = async (e) => {
e.preventDefault()
setBusy(true)
setError('')
try {
await login({ email, password })
} catch (err) {
setError(err.message)
} finally {
setBusy(false)
}
}
return (
<div className="auth-screen">
<form className="card" onSubmit={onSubmit}>
<h1>Kanshō</h1>
<p>Anmelden, um den Reflexionsraum fortzusetzen.</p>
<label>
E-Mail
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
Passwort
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={4} />
</label>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={busy}>{busy ? 'Prüfe …' : 'Anmelden'}</button>
</form>
</div>
)
}

View File

@ -0,0 +1,8 @@
export default function PlaceholderPage({ title, note }) {
return (
<section className="card">
<h1>{title}</h1>
<p>{note}</p>
</section>
)
}

View File

@ -0,0 +1,35 @@
import { useEffect, useState } from 'react'
import { useAuth } from '../context/AuthContext.jsx'
import { api } from '../api.js'
export default function SettingsPage() {
const { session, isAdmin } = useAuth()
const [sub, setSub] = useState(null)
useEffect(() => {
api('/api/subscription', { token: session.token }).then(setSub).catch(() => setSub(null))
}, [session.token])
return (
<section className="card">
<h1>Einstellungen</h1>
<dl className="meta">
<dt>Name</dt>
<dd>{session?.profile?.name}</dd>
<dt>E-Mail</dt>
<dd>{session?.profile?.email}</dd>
<dt>Rolle</dt>
<dd>{isAdmin ? 'Admin' : 'Nutzer'}</dd>
<dt>Rahmen-Tier</dt>
<dd>{sub?.tier?.name || '…'}</dd>
</dl>
{sub?.features && (
<ul className="muted">
{sub.features.map((item) => (
<li key={item.feature_id}>{item.feature_id}: {item.allowed ? 'verfügbar' : 'gesperrt'}{item.limit == null ? ' (ohne Limit)' : ` (${item.used}/${item.limit})`}</li>
))}
</ul>
)}
</section>
)
}

View File

@ -0,0 +1,47 @@
import { useState } from 'react'
import { useAuth } from '../context/AuthContext.jsx'
export default function SetupScreen() {
const { setup } = useAuth()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const onSubmit = async (e) => {
e.preventDefault()
setBusy(true)
setError('')
try {
await setup({ name, email, password })
} catch (err) {
setError(err.message)
} finally {
setBusy(false)
}
}
return (
<div className="auth-screen">
<form className="card" onSubmit={onSubmit}>
<h1>Ersten Zugang anlegen</h1>
<p>Das erste Profil wird Admin. Es gibt kein Vereinsmodell und keine Profilumschaltung per Header.</p>
<label>
Name
<input value={name} onChange={(e) => setName(e.target.value)} required />
</label>
<label>
E-Mail
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
</label>
<label>
Passwort
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={4} />
</label>
{error && <p className="error">{error}</p>}
<button type="submit" disabled={busy}>{busy ? 'Legt an …' : 'Zugang anlegen'}</button>
</form>
</div>
)
}

38
frontend/vite.config.js Normal file
View File

@ -0,0 +1,38 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg'],
manifest: {
name: 'Kanshō',
short_name: 'Kanshō',
description: 'Persönlicher Reflexionsbegleiter',
theme_color: '#3d5a4c',
background_color: '#f6f4ef',
display: 'standalone',
orientation: 'portrait',
icons: [
{ src: 'favicon.svg', sizes: 'any', type: 'image/svg+xml', purpose: 'any' }
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg}'],
runtimeCaching: [{
urlPattern: /^\/api\//,
handler: 'NetworkFirst',
options: { cacheName: 'api-cache', expiration: { maxEntries: 50 } }
}]
}
})
],
server: {
port: 5188,
strictPort: true,
proxy: { '/api': 'http://127.0.0.1:8018' }
}
})

19
scripts/dev-setup.ps1 Normal file
View File

@ -0,0 +1,19 @@
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$py = "$env:LOCALAPPDATA\Programs\Python\Python312\python.exe"
if (-not (Test-Path $py)) {
$py = "python"
}
$venv = Join-Path $root "backend\.venv\Scripts\python.exe"
if (-not (Test-Path $venv)) {
& $py -m venv (Join-Path $root "backend\.venv")
}
& (Join-Path $root "backend\.venv\Scripts\python.exe") -m pip install -r (Join-Path $root "backend\requirements.txt")
Push-Location (Join-Path $root "frontend")
npm install
Pop-Location
Write-Host "Backend: cd backend; .\.venv\Scripts\python -m uvicorn main:app --reload --port 8018"
Write-Host "Frontend: cd frontend; npm run dev (Port 5188, strict)"