cursor_Setup
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

This commit is contained in:
Lars 2026-04-04 14:05:50 +02:00
parent c607cd1833
commit dc87e7f3b8
13 changed files with 1328 additions and 0 deletions

View File

@ -27,3 +27,9 @@ ALLOWED_ORIGINS=https://mitai.jinkendo.de
# ── Pfade ─────────────────────────────────────────────────────── # ── Pfade ───────────────────────────────────────────────────────
PHOTOS_DIR=/app/photos PHOTOS_DIR=/app/photos
ENVIRONMENT=production ENVIRONMENT=production
# ── Gitea API (lokal, für scripts/gitea/gitea_api.py niemals committen) ──
GITEA_BASE_URL=http://192.168.2.144:3000
GITEA_OWNER=Lars
GITEA_REPO=mitai-jinkendo
GITEA_TOKEN=

3
.gitignore vendored
View File

@ -61,4 +61,7 @@ tmp/
#.claude Konfiguration #.claude Konfiguration
.claude/ .claude/
# Cursor MCP mit Secrets (Example: .cursor/mcp.json.example)
.cursor/mcp.json
.claude/settings.local.jsonfrontend/package-lock.json .claude/settings.local.jsonfrontend/package-lock.json

View File

@ -0,0 +1,269 @@
# Gitea offene Issues Review-Entwurf (Stand Code 2026-04-04)
> **Zweck:** Du kannst diese Datei **lesen, anpassen, abstreichen**. Nach Freigabe: Kommentare / Schließungen / Konsolidierungen in Gitea umsetzen (manuell oder per `scripts/gitea/gitea_api.py`).
>
> **Sortierung:** Nach **`created_at` aufsteigend** (älteste Issues zuerst). Duplikat-Issues mit gleichem Inhalt sind am Ende des Abschnitts „Duplikate“ gebündelt.
>
> **Legende `Vorschlag`:**
> - `OFFEN` noch sinnvoll, weiterverfolgen
> - `TEILWEISE` Teil umgesetzt, Beschreibung/AC anpassen
> - `PRÜFEN` kurzer manueller Test nötig
> - `DUPLIKAT` mit anderem Issue zusammenführen
> - `DONE?` wirkt im Repo erledigt, ggf. schließen nach deiner Bestätigung
---
## Kurz-Übersicht (28 eindeutige Issue-Nummern, zeitlich älteste zuerst)
| # | Titel (gekürzt) | Vorschlag | Notiz |
|---|-----------------|-----------|--------|
| 14 | Icon Picker Trainingstypen | OFFEN | Nur Freitext-Feld `icon` |
| 15 | Quality-Filter KI & Charts | TEILWEISE | Registry/Charts Confidence, kein durchgängiges Produkt |
| 21 | Universeller CSV-Parser + Mapping | OFFEN | Modul-spezifische Parser |
| 25 | Ziele-System Goals | DONE? | GoalsPage, Router, Focus Areas |
| 26 | Charts erweitern | TEILWEISE | Phase-0c API + History/NutritionCharts |
| 27 | Korrelationen & Insights | TEILWEISE | C-Charts + offene Data-Layer-TODOs |
| 29 | Abilities-Matrix UI | TEILWEISE | Admin/ProfileBuilder, UX offen |
| 30 | Responsive UI Sidebar | OFFEN | Weiterhin Bottom-Nav-fokussiert |
| 32 | version.py + `/api/version` | OFFEN | `version.py` ja, dedizierter Endpoint nein |
| 33 | main.py Hardcoded Version | OFFEN | FastAPI `3.0.0`, Root `v9c-dev` |
| 34 | External Volumes Doku | PRÜFEN | Gegen Compose abgleichen |
| 35 | `subscriptions` Tabelle | PRÜFEN | Schema prüfen |
| 36 | BUG Trainingstyp ISE | PRÜFEN | Logs nötig |
| 37 | Feature Enforcement Activity CSV | OFFEN | Import ohne vorgeschalteten Check |
| 38 | Feature Enforcement Nutrition CSV UI | DONE? / TEILWEISE | Backend-Check da |
| 39 | Usage-Badges Dashboard/Assistent | TEILWEISE | v. a. Gewicht im Dashboard |
| 40 | Logout Header | DONE? | `App.jsx` LogOut-Button |
| 42 | Enhanced Debug UI | DUPLIKAT | Mit #43 zusammenführen |
| 43 | Enhanced Debug UI | DUPLIKAT | Mit #42 zusammenführen |
| 45 | Prompt-Optimierer | OFFEN | Backlog |
| 46 | Prompt-Ersteller | OFFEN | Backlog |
| 47 | Wertetabelle Optimierung | OFFEN | UX-Feinschliff |
| 49 | Prompt-Zuordnung Verlauf | OFFEN | Kein klarer Treffer |
| 54 | Placeholder Registry … | DUPLIKAT | Wie #55 |
| 55 | Placeholder Registry … | OFFEN / TEILWEISE | `validate_all` existiert |
| 5658 | Body Cluster Restarbeiten | DUPLIKAT | Dreimal gleicher Inhalt → ein Issue |
---
## Details je Issue (älteste zuerst)
### #14 [FEAT-001] Icon Picker für Trainingstypen
**Code-Stand:** `AdminTrainingTypesPage.jsx` nutzt ein **Textfeld** `icon` (frei, z. B. Emoji). Kein dedizierter Icon-Picker (Palette, Vorschau, Kategorien).
**Vorschlag:** `OFFEN` Issue beibehalten; optional präzisieren: „Emoji-Picker oder vordefinierte Icon-Liste statt Freitext“.
**Kommentar-Entwurf für Gitea:**
> Stand Backend/Frontend: `icon` wird als String in `training_types` gespeichert, Eingabe ist Freitext. Icon-Picker-UX steht noch aus.
---
### #15 [FEAT-002] Quality-Filter für KI-Auswertungen & Charts
**Code-Stand:** Charts/Data-Layer nutzen **Confidence** u. a.; Placeholder-Registry hat viele `quality_filter_policy` / Evidence-Felder (teilweise `UNRESOLVED`/`TO_VERIFY`). Ein **einheitlicher** „Quality-Filter“-Mechanismus über alle KI-Auswertungen ist nicht eindeutig als fertiges Feature erkennbar.
**Vorschlag:** `TEILWEISE` Issue-Text auf konkrete Lücken schärfen (welche Prompts/Charts, welche Schwellen, SSoT Registry?).
**Kommentar-Entwurf:**
> Teilaspekte existieren (z. B. Confidence in Chart-Endpoints, Registry-Felder). Offen: durchgängige KI-/Chart-Quality-Pipeline und Abgleich mit Issue-Zielbild.
---
### #21 [FEATURE] Universeller CSV-Parser mit lernbarem Feldmapping
**Code-Stand:** Modulspezifische CSV-Imports (Activity, Nutrition, Vitals, Sleep, …) mit jeweils eigenem Parser; **lernbares Mapping** stark bei **Activity** über `activity_type_mappings`. Kein **ein** generischer CSV-Engine wie im Issue beschrieben.
**Vorschlag:** `OFFEN` oder Scope reduzieren („pro Modul konsolidieren“).
---
### #25 [FEAT] Ziele-System (Goals) v9e Kernfeature
**Code-Stand:** `goals`-Router, `GoalsPage`, Focus Areas, Migrationen laut Projektstand **weitgehend implementiert**.
**Vorschlag:** `DONE?` nach deiner Abnahme Issue-Body auf verbleibende Teilziele kürzen oder schließen.
**Kommentar-Entwurf:**
> Backend/Frontend Goals + Focus Areas sind im Repo vorhanden. Bitte verbleibende Wünsche als neue Sub-Issues oder AC hier abhaken und schließen.
---
### #26 [FEAT] Charts & Visualisierungen erweitern
**Code-Stand:** `backend/routers/charts.py` (Phase 0c), viele `api.get…Chart` in `api.js`; `History.jsx` + `NutritionCharts` / `RecoveryCharts` nutzen Chart-Daten.
**Vorschlag:** `TEILWEISE` Issue auf konkrete fehlende Chart-Typen/UI-Verdrahtung schärfen (falls noch offen).
---
### #27 [FEAT] Korrelationen & Insights erweitern
**Code-Stand:** Chart-Endpunkte C1C4 u. a.; Data-Layer `correlations.py` mit TODO-Stellen in Teilen.
**Vorschlag:** `TEILWEISE` Liste fehlender Korrelationen/Insights vs. Code ergänzen.
---
### #29 [FEAT] Abilities-Matrix UI (v9f)
**Code-Stand:** Training Types mit `abilities` JSONB, `AdminTrainingProfiles`, `ProfileBuilder` vollständige „5D Matrix“-UX unklar ohne Produktvorgabe.
**Vorschlag:** `TEILWEISE` AC mit aktuellen Screenshots/Flows abgleichen.
---
### #30 [FEAT] Responsive UI Desktop Sidebar + 2-spaltig
**Code-Stand:** Weiterhin stark **Mobile-first** (z. B. `bottom-nav` in `App.jsx`); keine ausgebaute Desktop-Sidebar wie im klassischen Admin-Dashboard.
**Vorschlag:** `OFFEN`.
---
### #32 Version-System (`version.py` + `/api/version`)
**Code-Stand:** `backend/version.py` existiert mit `APP_VERSION`. **`GET /api/version`** im Backend **nicht** gefunden (Suche nach Route); Root liefert u. a. `"version": "v9c-dev"`.
**Vorschlag:** `OFFEN` für #32 `/api/version` implementieren oder Issue anpassen („nur version.py ohne Endpoint“).
---
### #33 main.py hardcoded Version entfernen
**Code-Stand:** `main.py`: `FastAPI(..., version="3.0.0")`; Root-JSON noch `v9c-dev`.
**Vorschlag:** `OFFEN` auf `version.py` vereinheitlichen (inkl. FastAPI-`version`-Feld und Health-Payload).
---
### #34 External Volumes dokumentieren (Legacy bodytrack_*)
**Vorschlag:** `PRÜFEN` gegen aktuelle `docker-compose`/Deploy-Doku im Repo halten; dann schließen oder Aktualisierung kommentieren.
---
### #35 Deprecated Tabelle `subscriptions` entfernen
**Code-Stand:** Im Migrations-Ordner **kein** aktueller Treffer auf `subscriptions` (Stichprobe); Membership-System nutzt andere Tabellen.
**Vorschlag:** `PRÜFEN` einmal `schema.sql` / DB prüfen, ob Tabelle noch existiert. Wenn weg: Issue schließen mit Verweis auf Migration.
---
### #36 BUG-009: Trainingstyp-Erstellung → Internal Server Error
**Code-Stand:** `TrainingTypeCreate` enthält `abilities` / `profile` (JSONB). ISE oft durch DB-Constraint, NULL/JSON oder fehlende Spalte **ohne Laufzeit-Log nicht verifiziert**.
**Vorschlag:** `PRÜFEN` in Gitea Notiz: aktueller Request-Body + Stacktrace aus `docker logs`; wenn behoben: schließen.
---
### #37 Feature-Enforcement Activity CSV-Import
**Code-Stand:** `create_activity` nutzt `check_feature_access` für `activity_entries`. **`import_activity_csv`** startet **ohne** vorgeschalteten Limit-Check (im gelesenen Abschnitt nur `get_pid` + Parse) von Issue #37 noch **nicht** erfüllt.
**Vorschlag:** `OFFEN` hoch priorisieren; analog Nutrition: ein Check vor Bulk-Import + Zählung.
---
### #38 Feature-Enforcement Nutrition CSV-Import UI
**Code-Stand:** `import_nutrition_csv` ruft **`check_feature_access`** für `nutrition_entries` auf (inkl. Logging).
**Vorschlag:** `TEILWEISE` / `DONE?` falls UI-Feedback gewünscht, im Issue auf konkrete UI-Lücken eingehen (Banner, Disable Button).
---
### #39 Usage-Badges im Dashboard-Assistenten
**Code-Stand:** `Dashboard.jsx` nutzt `getFeatureUsage()` für **Gewichts-Widget** (Limit/Lock). Unklar ob „Assistent“-Modus = gesamtes Dashboard oder separater Guide.
**Vorschlag:** `TEILWEISE` Issue präzisieren, welche Kacheln/Bereiche Badges brauchen.
---
### #40 Logout-Button im App-Header (neben Avatar)
**Code-Stand:** `App.jsx` Header mit **`LogOut` neben Avatar** (Umsetzung vorhanden).
**Vorschlag:** `DONE?` nach kurzem Klicktest **schließen**.
**Kommentar-Entwurf:**
> In `App.jsx` ist ein Logout-Button im Header umgesetzt. Bitte in target Umgebung verifizieren und schließen.
---
### #42 / #43 Enhanced Debug / Prompt Analysis UI (Issue #28 Phase C)
**Code-Stand:** `Analysis.jsx` mit Expert-Modus, Platzhalter-Gruppierung kann Teile von Phase C abdecken.
**Vorschlag:** `DUPLIKAT` **ein** Issue behalten; anderes schließen mit Verweis. Inhalte zusammenführen.
---
### #45 KI Prompt-Optimierer
**Vorschlag:** `OFFEN` Backlog, nicht im aktuellen Code sichtbar.
---
### #46 KI Prompt-Ersteller
**Vorschlag:** `OFFEN` wie #45.
---
### #47 Wertetabelle Optimierung
**Code-Stand:** Wertetabelle in `Analysis.jsx` / Metadata viele Punkte eher UX/Performance.
**Vorschlag:** `OFFEN` konkrete UI-Schmerzpunkte in Sub-Tasks splitten.
---
### #49 Prompt-Zuordnung zu Verlaufsseiten
**Code-Stand:** Kein eindeutiger Treffer zu „History page prompt assignment“ in kurzer Suche.
**Vorschlag:** `OFFEN` kurz präzisieren (Welche Seite, welches Datenmodell).
---
### #54 / #55 Placeholder Registry UNRESOLVED & TO_VERIFY
**Code-Stand:** `placeholder_registry_export.py` liefert **`validation_report` über `registry.validate_all()`** (nicht mehr leer aus dem frühen Issue-Text für Body-Cluster „{}“-Teil). Evidence `TO_VERIFY`/`UNRESOLVED` existieren weiter in Registrations.
**Vorschlag:** `#54` und `#55` **zusammenlegen** (gleiches Thema, Titel nur Encoding-Unterschied). Ein Issue offen lassen, Metadaten-Audit fortsetzen.
---
### #56 / #57 / #58 Body Cluster Restarbeiten & Metadaten-Verifizierung
**Inhalt:** identische bzw. nahezu identische Langbeschreibung (Metadaten, Layer 2b, Nutrition confidence_logic, Validation Report).
**Vorschlag:** `DUPLIKAT` **eines** behalten (z. B. niedrigste Nummer oder neueste #58 mit aktualisiertem Stand), andere **schließen** mit Verweis „Duplicate of #X“. Validation-Teil: Code hat bereits `validate_all` Issue-Text Abschnitt „leeres validation_report“ **aktualisieren**.
**Kommentar-Entwurf:**
> Dreimal dasselbe Issue. Vorschlag: #56/#57 schließen, Tracking nur in #XX. `validation_report` wird aus `registry.validate_all()` befüllt; verbleibende Arbeit: TO_VERIFY-Felder Layer 2b + Nutrition confidence_logic laut Checkliste.
---
## Nachbearbeitung in Gitea (Checkliste für dich)
- [ ] Duplikate schließen und verlinken (#42/#43, #54/#55, #56#58).
- [ ] „DONE?“-Issues manuell testen (`#25`, `#38`, `#40`).
- [ ] `#37` umsetzen oder Kommentar „noch offen“ bestätigen.
- [ ] `#32``#33` Versionierung planen (ein gemeinsames Mini-Epic).
- [ ] Kommentare aus diesem Dokument kopieren/anpassen.
- [ ] Optional: Labels in Gitea setzen (`duplicate`, `blocked`, `needs-retest`).
---
## Technischer Hinweis (Audit / Security)
Aus dem Code-Audit 2026-04-04: kritische Punkte (`get_pid` / `X-Profile-Id`, `/api/profiles` ohne Admin) sind **nicht** 1:1 als Gitea-Issues in dieser offenen Liste sichtbar ggf. **separate** Security-Issues aus `.claude/docs/audit/20260404_code_audit/gitea/` anlegen, falls noch nicht vorhanden.
---
*Erzeugt aus Gitea API (28 offene Issues, sortiert nach `created_at`) und statischer Code-Analyse im Workspace. Kein Laufzeit-Test auf dem Pi.*

View File

@ -0,0 +1,64 @@
# Gitea MCP-Server für Cursor
Damit der Agent **strukturierte Tools** nutzen kann (`gitea_list_issues`, `gitea_close_issue`, …), registrierst du diesen MCP-S **lokal** in Cursor.
## 1. Abhängigkeit
```powershell
pip install -r scripts/gitea/requirements-mcp.txt
```
Oder: `pip install "mcp>=1.2.0"`
## 2. Secrets
Wie beim CLI: **`GITEA_*` in der Repo-Root `.env`** (wird von `gitea_lib` geladen), **oder** dieselben Variablen in der MCP-`env` (siehe unten).
**Niemals** Tokens in Git committen.
## 3. Cursor MCP konfigurieren
**Variante A UI:** Einstellungen → **MCP** / Tools → Server hinzufügen → Typ „Command“:
- **Command:** `python` (oder voller Pfad zu `python.exe`)
- **Args:** vollständiger Pfad zu `mcp_server_gitea.py`, z. B.
`C:\Dev\mitai-jinkendo\scripts\gitea\mcp_server_gitea.py`
- **Working directory (optional):** `C:\Dev\mitai-jinkendo\scripts\gitea`
- **Env:** nur nötig, wenn du **keine** `.env` im Repo nutzt:
```text
GITEA_BASE_URL=http://192.168.2.144:3000
GITEA_OWNER=Lars
GITEA_REPO=mitai-jinkendo
GITEA_TOKEN=…
```
**Variante B JSON:** Datei `~/.cursor/mcp.json` (Benutzer) oder projektbezogen laut Cursor-Doku. Beispielinhalt siehe **`.cursor/mcp.json.example`** im Repo (Platzhalter, ohne echtes Token).
Cursor nach Änderung **vollständig neu starten**.
## 4. Netzwerk
Die Gitea-URL muss von deinem Rechner erreichbar sein (z. B. `192.168.2.144:3000` im LAN).
## 5. Repo-Zugriff
- **API:** Tool `gitea_get_repo_file` (Dateiinhalt / Metadaten).
- **Git (lokal):** unverändert `git pull` / Agent liest Workspace-Dateien dafür brauchst du kein MCP.
## Bereitgestellte Tools (Kurzüberblick)
| Tool | Zweck |
|------|--------|
| `gitea_list_issues` | Issues listen, optional alle Seiten |
| `gitea_get_issue` | Ein Issue mit Body |
| `gitea_comment_issue` | Kommentar |
| `gitea_create_issue` | Neu anlegen |
| `gitea_close_issue` / `gitea_reopen_issue` | Status |
| `gitea_get_repo_file` | Datei remote via API |
## Issue-Triage durch den Agent
Sinnvoller Ablauf: Issues listen → je Issue **Code/Commits prüfen** → bei eindeutig erledigt: kurzer Kommentar + **close**; bei teilweise: Kommentar mit Checkboxen; bei unklar: nur Kommentar, **nicht** schließen.
Autonom alles schließen ist fehleranfällig; klare Regeln oder manuelle Freigabe für `close` empfohlen.

68
scripts/gitea/README.md Normal file
View File

@ -0,0 +1,68 @@
# Gitea API lokales CLI
Dient dazu, **Issues** auf deiner Gitea-Instanz zu lesen und anzulegen mit den in **`.env`** gesetzten Variablen (nicht committen).
## Umgebungsvariablen (Root `.env`)
| Variable | Beispiel |
|----------|----------|
| `GITEA_BASE_URL` | `http://192.168.2.144:3000` |
| `GITEA_TOKEN` | Personal Access Token (nur Scope **repo** + **issue** nötig) |
| `GITEA_OWNER` | `Lars` |
| `GITEA_REPO` | `mitai-jinkendo` |
## Voraussetzung
Python 3.10+ (nur Standardbibliothek).
## Aufruf (im Repo-Root)
```powershell
# Issues auflisten (offen)
python scripts/gitea/gitea_api.py issues list
# Issues mit State
python scripts/gitea/gitea_api.py issues list --state all
# Ein Issue lesen
python scripts/gitea/gitea_api.py issues get 42
# Issue anlegen (Titel + Body aus Datei oder direkt)
python scripts/gitea/gitea_api.py issues create --title "Fix: …" --body "…"
python scripts/gitea/gitea_api.py issues create --title "Fix: …" --body-file path/to/body.md
# Kommentar
python scripts/gitea/gitea_api.py issues comment 42 --body "…"
# Schließen / wieder öffnen
python scripts/gitea/gitea_api.py issues close 42
python scripts/gitea/gitea_api.py issues reopen 42
# Alle Issues (alle Seiten, Vorsicht bei großen Repos)
python scripts/gitea/gitea_api.py issues list --all-pages --state open
# Markdown-Datei (z. B. Audit-Template) als Issue-Body
python scripts/gitea/gitea_api.py issues create --title "…" --body-file .claude/docs/audit/.../gitea/TEMPLATE_P0-....md
```
## Repository-Inhalt (read-only)
```powershell
# Datei über Gitea-API (bei Dateien: Text-Inhalt; bei Verzeichnissen: JSON-Listing)
python scripts/gitea/gitea_api.py repo file README.md
python scripts/gitea/gitea_api.py repo file backend/main.py --ref develop
# Clone/Push: normales `git remote` Token nicht dauerhaft in der Remote-URL; SSH oder Credential Helper.
```
## Sicherheit
- **Niemals** `GITEA_TOKEN` ins Git oder in Issues/Pastebins.
- Token, das in Chat oder Logs gelandet ist, in Gitea **widerrufen** und **neu erzeugen**.
- Cursor-Agenten können das CLI über das Terminal nutzen, wenn `.env` gesetzt und Netzwerk zu `GITEA_BASE_URL` erreichbar ist.
## MCP (Tools direkt im Agent)
Siehe [`MCP_SETUP.md`](./MCP_SETUP.md) und [`../.cursor/mcp.json.example`](../../.cursor/mcp.json.example).

183
scripts/gitea/gitea_api.py Normal file
View File

@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
Minimal Gitea API client. Reads GITEA_* from environment or .env in repo root.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from gitea_lib import (
issues_comment,
issues_create,
issues_get,
issues_list_all,
issues_list_page,
issues_patch,
load_dotenv,
repo_file_content,
repo_root,
require_config,
)
def cmd_issues_list(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
if args.all_pages:
items = issues_list_all(
base, token, owner, repo, state=args.state, limit=args.limit
)
else:
_, items = issues_list_page(
base,
token,
owner,
repo,
state=args.state,
page=args.page,
limit=args.limit,
)
for it in items:
num = it.get("number")
title = it.get("title")
st = it.get("state")
print(f"#{num} [{st}] {title}")
def cmd_issues_get(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
status, payload = issues_get(base, token, owner, repo, args.number)
print(json.dumps(payload, indent=2, ensure_ascii=False))
if status >= 400:
sys.exit(1)
def cmd_issues_create(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
body = args.body or ""
if args.body_file:
body = Path(args.body_file).read_text(encoding="utf-8")
status, payload = issues_create(
base,
token,
owner,
repo,
title=args.title,
body=body,
labels=args.labels or [],
)
print(json.dumps(payload, indent=2, ensure_ascii=False))
if status >= 400:
sys.exit(1)
def cmd_issues_comment(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
status, payload = issues_comment(
base, token, owner, repo, args.number, args.body
)
print(json.dumps(payload, indent=2, ensure_ascii=False))
if status >= 400:
sys.exit(1)
def cmd_issues_close(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
status, payload = issues_patch(
base, token, owner, repo, args.number, {"state": "closed"}
)
print(json.dumps(payload, indent=2, ensure_ascii=False))
if status >= 400:
sys.exit(1)
def cmd_issues_reopen(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
status, payload = issues_patch(
base, token, owner, repo, args.number, {"state": "open"}
)
print(json.dumps(payload, indent=2, ensure_ascii=False))
if status >= 400:
sys.exit(1)
def cmd_repo_contents(args: argparse.Namespace, base: str, token: str, owner: str, repo: str) -> None:
status, payload = repo_file_content(
base, token, owner, repo, args.path, ref=args.ref or ""
)
if status >= 400:
print(json.dumps(payload, indent=2, ensure_ascii=False))
sys.exit(1)
if isinstance(payload, dict) and payload.get("encoding") == "text":
print(payload.get("content", ""))
else:
print(json.dumps(payload, indent=2, ensure_ascii=False))
def main() -> None:
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
root = repo_root()
load_dotenv(root)
parser = argparse.ArgumentParser(description="Gitea API helper")
sub = parser.add_subparsers(dest="domain", required=True)
p_issues = sub.add_parser("issues", help="Issues")
i_sub = p_issues.add_subparsers(dest="issues_cmd", required=True)
p_il = i_sub.add_parser("list", help="List issues")
p_il.add_argument("--state", default="open", choices=["open", "closed", "all"])
p_il.add_argument("--limit", type=int, default=50)
p_il.add_argument("--page", type=int, default=1)
p_il.add_argument(
"--all-pages",
action="store_true",
help="Alle Seiten abfragen (Vorsicht bei sehr vielen Issues)",
)
p_il.set_defaults(_handler=cmd_issues_list)
p_ig = i_sub.add_parser("get", help="Get one issue")
p_ig.add_argument("number", type=int)
p_ig.set_defaults(_handler=cmd_issues_get)
p_ic = i_sub.add_parser("create", help="Create issue")
p_ic.add_argument("--title", required=True)
p_ic.add_argument("--body", default="")
p_ic.add_argument("--body-file")
p_ic.add_argument("--labels", nargs="*", default=[])
p_ic.set_defaults(_handler=cmd_issues_create)
p_co = i_sub.add_parser("comment", help="Add comment")
p_co.add_argument("number", type=int)
p_co.add_argument("--body", required=True)
p_co.set_defaults(_handler=cmd_issues_comment)
p_cl = i_sub.add_parser("close", help="Close issue")
p_cl.add_argument("number", type=int)
p_cl.set_defaults(_handler=cmd_issues_close)
p_ro = i_sub.add_parser("reopen", help="Reopen issue")
p_ro.add_argument("number", type=int)
p_ro.set_defaults(_handler=cmd_issues_reopen)
p_repo = sub.add_parser("repo", help="Repository (API)")
r_sub = p_repo.add_subparsers(dest="repo_cmd", required=True)
p_rc = r_sub.add_parser("file", help="Get file or directory metadata/content")
p_rc.add_argument("path")
p_rc.add_argument("--ref", default="", help="branch/tag/commit")
p_rc.set_defaults(_handler=cmd_repo_contents)
args = parser.parse_args()
try:
base, token, owner, reponame = require_config()
except RuntimeError as e:
sys.stderr.write(str(e) + "\n")
sys.exit(1)
handler = args._handler
handler(args, base, token, owner, reponame)
if __name__ == "__main__":
main()

226
scripts/gitea/gitea_lib.py Normal file
View File

@ -0,0 +1,226 @@
"""
Shared Gitea REST helpers (stdlib). Used by gitea_api.py CLI and mcp_server_gitea.py.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any
def load_dotenv(repo_root: Path) -> None:
env_path = repo_root / ".env"
if not env_path.is_file():
return
for line in env_path.read_text(encoding="utf-8", errors="replace").splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" not in line:
continue
k, _, v = line.partition("=")
k, v = k.strip(), v.strip().strip('"').strip("'")
if k and k not in os.environ:
os.environ[k] = v
def repo_root() -> Path:
return Path(__file__).resolve().parents[2]
def get_config() -> tuple[str, str, str, str]:
base = os.getenv("GITEA_BASE_URL", "").rstrip("/")
token = os.getenv("GITEA_TOKEN", "")
owner = os.getenv("GITEA_OWNER", "")
reponame = os.getenv("GITEA_REPO", "")
return base, token, owner, reponame
def require_config() -> tuple[str, str, str, str]:
base, token, owner, reponame = get_config()
missing = [n for n, v in (
("GITEA_BASE_URL", base),
("GITEA_TOKEN", token),
("GITEA_OWNER", owner),
("GITEA_REPO", reponame),
) if not v]
if missing:
raise RuntimeError(
"Fehlende Umgebungsvariablen: " + ", ".join(missing)
+ " — setze sie in .env im Repo-Root oder in der MCP-env."
)
return base, token, owner, reponame
def request_json(
method: str,
url: str,
token: str,
data: dict | None = None,
) -> tuple[int, Any]:
body = None if data is None else json.dumps(data).encode("utf-8")
req = urllib.request.Request(url, data=body, method=method)
req.add_header("Authorization", f"token {token}")
req.add_header("Accept", "application/json")
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=120) as resp:
raw = resp.read().decode("utf-8", errors="replace")
status = resp.status
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", errors="replace")
try:
return e.code, json.loads(raw) if raw else {}
except json.JSONDecodeError:
return e.code, {"message": raw or str(e)}
if not raw:
return status, {}
try:
return status, json.loads(raw)
except json.JSONDecodeError:
return status, raw
def issues_list_page(
base: str,
token: str,
owner: str,
repo: str,
*,
state: str = "open",
page: int = 1,
limit: int = 50,
) -> tuple[int, list]:
if state == "all":
open_st, open_i = issues_list_page(
base, token, owner, repo, state="open", page=page, limit=limit
)
closed_st, closed_i = issues_list_page(
base, token, owner, repo, state="closed", page=page, limit=limit
)
merged = (open_i or []) + (closed_i or [])
st = max(open_st, closed_st) if open_st >= 400 or closed_st >= 400 else 200
return st, merged[:limit]
q = f"?state={state}&page={page}&limit={limit}"
url = f"{base}/api/v1/repos/{owner}/{repo}/issues{q}"
status, payload = request_json("GET", url, token)
if status >= 400:
return status, []
if not isinstance(payload, list):
return status, []
return status, payload
def issues_list_all(
base: str,
token: str,
owner: str,
repo: str,
*,
state: str = "open",
limit: int = 50,
) -> list[dict]:
if state == "all":
o = issues_list_all(
base, token, owner, repo, state="open", limit=limit
)
c = issues_list_all(
base, token, owner, repo, state="closed", limit=limit
)
return o + c
out: list[dict] = []
page = 1
while True:
_, batch = issues_list_page(
base, token, owner, repo, state=state, page=page, limit=limit
)
if not batch:
break
out.extend(batch)
if len(batch) < limit:
break
page += 1
return out
def issues_get(
base: str, token: str, owner: str, repo: str, number: int
) -> tuple[int, Any]:
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}"
return request_json("GET", url, token)
def issues_create(
base: str,
token: str,
owner: str,
repo: str,
*,
title: str,
body: str = "",
labels: list[str] | None = None,
) -> tuple[int, Any]:
url = f"{base}/api/v1/repos/{owner}/{repo}/issues"
return request_json(
"POST",
url,
token,
{"title": title, "body": body, "labels": labels or []},
)
def issues_comment(
base: str,
token: str,
owner: str,
repo: str,
number: int,
body: str,
) -> tuple[int, Any]:
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}/comments"
return request_json("POST", url, token, {"body": body})
def issues_patch(
base: str,
token: str,
owner: str,
repo: str,
number: int,
fields: dict,
) -> tuple[int, Any]:
"""Gitea: PATCH issue (state, title, body, …)."""
url = f"{base}/api/v1/repos/{owner}/{repo}/issues/{number}"
return request_json("PATCH", url, token, fields)
def repo_file_content(
base: str,
token: str,
owner: str,
repo: str,
path: str,
ref: str = "",
) -> tuple[int, Any]:
from urllib.parse import quote
from base64 import b64decode
p = quote(path, safe="/")
r = f"?ref={ref}" if ref else ""
url = f"{base}/api/v1/repos/{owner}/{repo}/contents/{p}{r}"
st, payload = request_json("GET", url, token)
if st >= 400:
return st, payload
if isinstance(payload, dict) and payload.get("type") == "file" and payload.get(
"content"
):
try:
text = b64decode(payload["content"]).decode("utf-8", errors="replace")
return st, {"path": path, "encoding": "text", "content": text}
except Exception:
return st, payload
return st, payload

View File

@ -0,0 +1,127 @@
#!/usr/bin/env python3
"""
MCP-Server für Gitea (Issues + Datei-Inhalt via API).
Cursor: in den MCP-Einstellungen dieses Skript starten (siehe MCP_SETUP.md).
Transport: stdio (Standard FastMCP).
Abhängigkeit: pip install "mcp>=1.2.0" (siehe requirements-mcp.txt)
"""
from __future__ import annotations
import json
import sys
from gitea_lib import (
issues_comment,
issues_create,
issues_get,
issues_list_all,
issues_list_page,
issues_patch,
load_dotenv,
repo_file_content,
repo_root,
require_config,
)
from mcp.server.fastmcp import FastMCP # noqa: E402
mcp = FastMCP(
"mitai-gitea",
instructions=(
"Gitea-Tools für das Repo aus GITEA_OWNER/GITEA_REPO. "
"Schließe Issues nur nach klarer Code-Verifikation; sonst Kommentar mit offenen Punkten."
),
)
def _cfg():
load_dotenv(repo_root())
return require_config()
def _json(obj) -> str:
return json.dumps(obj, indent=2, ensure_ascii=False)
@mcp.tool()
def gitea_list_issues(
state: str = "open",
limit_per_page: int = 50,
fetch_all_pages: bool = False,
) -> str:
"""Listet Issues. state: open | closed | all. fetch_all_pages=true holt alle Seiten (kann langsam sein)."""
base, token, owner, repo = _cfg()
if fetch_all_pages:
items = issues_list_all(
base, token, owner, repo, state=state, limit=limit_per_page
)
return _json(
[{"number": i.get("number"), "title": i.get("title"), "state": i.get("state")} for i in items]
)
_, items = issues_list_page(
base, token, owner, repo, state=state, page=1, limit=limit_per_page
)
return _json(
[{"number": i.get("number"), "title": i.get("title"), "state": i.get("state")} for i in items]
)
@mcp.tool()
def gitea_get_issue(issue_number: int) -> str:
"""Holt ein Issue inkl. Body, Labels, State (JSON)."""
base, token, owner, repo = _cfg()
st, payload = issues_get(base, token, owner, repo, issue_number)
return _json({"http_status": st, "issue": payload})
@mcp.tool()
def gitea_create_issue(title: str, body: str = "", labels: str = "") -> str:
"""Legt ein Issue an. labels: kommagetrennte Namen, z.B. \"bug,backend\"."""
base, token, owner, repo = _cfg()
lab = [x.strip() for x in labels.split(",") if x.strip()]
st, payload = issues_create(
base, token, owner, repo, title=title, body=body, labels=lab
)
return _json({"http_status": st, "result": payload})
@mcp.tool()
def gitea_comment_issue(issue_number: int, body: str) -> str:
"""Kommentar an ein Issue anhängen."""
base, token, owner, repo = _cfg()
st, payload = issues_comment(base, token, owner, repo, issue_number, body)
return _json({"http_status": st, "result": payload})
@mcp.tool()
def gitea_close_issue(issue_number: int) -> str:
"""Issue schließen (state=closed)."""
base, token, owner, repo = _cfg()
st, payload = issues_patch(
base, token, owner, repo, issue_number, {"state": "closed"}
)
return _json({"http_status": st, "result": payload})
@mcp.tool()
def gitea_reopen_issue(issue_number: int) -> str:
"""Geschlossenes Issue wieder öffnen."""
base, token, owner, repo = _cfg()
st, payload = issues_patch(
base, token, owner, repo, issue_number, {"state": "open"}
)
return _json({"http_status": st, "result": payload})
@mcp.tool()
def gitea_get_repo_file(path: str, git_ref: str = "") -> str:
"""Liest eine Datei aus dem Repo über die Gitea-API (Standard: Default-Branch)."""
base, token, owner, repo = _cfg()
st, payload = repo_file_content(base, token, owner, repo, path, ref=git_ref)
return _json({"http_status": st, "payload": payload})
if __name__ == "__main__":
mcp.run()

View File

@ -0,0 +1,2 @@
# Nur für MCP-Server (nicht im Backend-Container nötig)
mcp>=1.2.0

94
tests/phase3_e2e_test.sql Normal file
View File

@ -0,0 +1,94 @@
-- Phase 3 E2E Test: Branching Workflow
-- Test workflow: body analysis → logic (if relevanz = "decrease") → then/else paths
-- 1. Cleanup (use slug for lookup)
DELETE FROM workflow_executions WHERE workflow_id IN (
SELECT id FROM workflow_definitions WHERE slug = 'phase3-e2e-branching'
);
DELETE FROM workflow_definitions WHERE slug = 'phase3-e2e-branching';
-- 2. Create test workflow
INSERT INTO workflow_definitions (name, slug, description, graph, active)
VALUES (
'Phase 3 E2E Test - Branching',
'phase3-e2e-branching',
'Test workflow with logic node and conditional branching',
'{
"nodes": [
{
"id": "start",
"type": "start",
"position": {"x": 100, "y": 100}
},
{
"id": "body_analysis",
"type": "analysis",
"prompt_slug": "pipeline_body",
"question_augmentations": [
{
"id": "q1",
"type": "relevanz",
"question": "Hat sich die Fettmasse relevant verändert?",
"answer_spectrum": ["increase", "stable", "decrease"],
"reasoning_required": true
}
],
"position": {"x": 100, "y": 200}
},
{
"id": "logic_check",
"type": "logic",
"condition": {
"expression": {
"operator": "eq",
"ref": "body_analysis.relevanz",
"value": "decrease"
},
"then_path": "e3",
"else_path": "e4"
},
"fallback": {
"strategy": "default_path"
},
"position": {"x": 100, "y": 300}
},
{
"id": "decrease_path",
"type": "analysis",
"prompt_slug": "pipeline_body",
"position": {"x": 50, "y": 400}
},
{
"id": "not_decrease_path",
"type": "analysis",
"prompt_slug": "pipeline_body",
"position": {"x": 150, "y": 400}
},
{
"id": "end",
"type": "end",
"position": {"x": 100, "y": 500}
}
],
"edges": [
{"id": "e1", "from": "start", "to": "body_analysis"},
{"id": "e2", "from": "body_analysis", "to": "logic_check"},
{"id": "e3", "from": "logic_check", "to": "decrease_path", "label": "then"},
{"id": "e4", "from": "logic_check", "to": "not_decrease_path", "label": "else"},
{"id": "e5", "from": "decrease_path", "to": "end"},
{"id": "e6", "from": "not_decrease_path", "to": "end"}
]
}',
true
);
-- 3. Verify workflow was created
SELECT
id,
name,
slug,
active,
jsonb_array_length(graph->'nodes') as node_count,
jsonb_array_length(graph->'edges') as edge_count
FROM workflow_definitions
WHERE slug = 'phase3-e2e-branching';

123
tests/phase4_e2e_test.sql Normal file
View File

@ -0,0 +1,123 @@
-- Phase 4 E2E Test: Branching + Join Workflow
-- Test workflow mit 2 parallelen Pfaden, die wieder zusammengeführt werden
-- 1. Insert workflow definition
INSERT INTO workflow_definitions (
id,
name,
description,
graph,
active,
created_by
) VALUES (
'phase4-join-test',
'Phase 4 Join Test Workflow',
'Test workflow with branching and join node',
'{
"nodes": [
{
"id": "start",
"type": "start",
"position": {"x": 100, "y": 100}
},
{
"id": "body_analysis",
"type": "analysis",
"prompt_slug": "body",
"position": {"x": 100, "y": 200},
"question_augmentations": [
{
"id": "relevanz",
"type": "relevanz",
"question": "Ist eine Gewichtsveränderung relevant?",
"answer_spectrum": ["ja", "nein", "unklar"]
}
]
},
{
"id": "decision_logic",
"type": "logic",
"position": {"x": 100, "y": 300},
"condition": {
"type": "if",
"expression": {
"operator": "eq",
"ref": "body_analysis.relevanz",
"value": "ja"
}
},
"fallback": {
"strategy": "default_path"
}
},
{
"id": "path_a_nutrition",
"type": "analysis",
"prompt_slug": "nutrition",
"position": {"x": 50, "y": 400},
"question_augmentations": [
{
"id": "prioritaet",
"type": "prioritaet",
"question": "Wie wichtig ist Ernährungsoptimierung?",
"answer_spectrum": ["hoch", "mittel", "niedrig", "unklar"]
}
]
},
{
"id": "path_b_activity",
"type": "analysis",
"prompt_slug": "activity",
"position": {"x": 150, "y": 400},
"question_augmentations": [
{
"id": "prioritaet",
"type": "prioritaet",
"question": "Wie wichtig ist Trainingsoptimierung?",
"answer_spectrum": ["hoch", "mittel", "niedrig", "unklar"]
}
]
},
{
"id": "join_consolidation",
"type": "join",
"position": {"x": 100, "y": 500},
"join_strategy": "best_effort",
"skip_handling": "ignore_skipped"
},
{
"id": "end",
"type": "end",
"position": {"x": 100, "y": 600}
}
],
"edges": [
{"id": "e1", "from_node": "start", "to_node": "body_analysis"},
{"id": "e2", "from_node": "body_analysis", "to_node": "decision_logic"},
{"id": "e3", "from_node": "decision_logic", "to_node": "path_a_nutrition", "label": "then"},
{"id": "e4", "from_node": "decision_logic", "to_node": "path_b_activity", "label": "else"},
{"id": "e5", "from_node": "path_a_nutrition", "to_node": "join_consolidation"},
{"id": "e6", "from_node": "path_b_activity", "to_node": "join_consolidation"},
{"id": "e7", "from_node": "join_consolidation", "to_node": "end"}
]
}'::jsonb,
true,
'test-user'
);
-- 2. Verify workflow was created
SELECT
id,
name,
active,
jsonb_array_length(graph->'nodes') as node_count,
jsonb_array_length(graph->'edges') as edge_count
FROM workflow_definitions
WHERE id = 'phase4-join-test';
-- Expected result:
-- id: phase4-join-test
-- name: Phase 4 Join Test Workflow
-- active: true
-- node_count: 7
-- edge_count: 7

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Quick test for deployed join_evaluator.py"""
from join_evaluator import evaluate_join_node
from workflow_models import (
WorkflowNode, WorkflowGraph, WorkflowEdge,
JoinStrategy, NodeStatus, NodeExecutionState
)
# Minimal test: 2 paths → join
join_node = WorkflowNode(
id="test_join",
type="join",
join_strategy=JoinStrategy.BEST_EFFORT
)
graph = WorkflowGraph(
nodes=[
WorkflowNode(id="path_a", type="analysis"),
WorkflowNode(id="path_b", type="analysis"),
join_node
],
edges=[
WorkflowEdge(id="e1", from_node="path_a", to_node="test_join"),
WorkflowEdge(id="e2", from_node="path_b", to_node="test_join")
]
)
context = {
"node_results": {
"path_a": NodeExecutionState(
node_id="path_a",
status=NodeStatus.EXECUTED,
analysis_core="Analysis from path A"
),
"path_b": NodeExecutionState(
node_id="path_b",
status=NodeStatus.EXECUTED,
analysis_core="Analysis from path B"
)
}
}
# Execute
result = evaluate_join_node(join_node, graph, context)
# Verify
print(f"✅ Ready: {result.ready}")
print(f"✅ Consolidated cores: {len(result.consolidated_analysis_core)}")
print(f"✅ Executed paths: {result.metadata.get('executed_paths')}")
print(f"✅ Strategy: {result.metadata.get('join_strategy')}")
assert result.ready is True
assert len(result.consolidated_analysis_core) == 2
assert "path_a" in result.consolidated_analysis_core
assert "path_b" in result.consolidated_analysis_core
print("\n🎉 Phase 4 Join Evaluator: DEPLOYED AND WORKING!")

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""Integration test: Full workflow with join node"""
import asyncio
import sys
from workflow_executor import execute_node
from workflow_models import (
WorkflowNode, WorkflowGraph, WorkflowEdge,
JoinStrategy, NodeStatus, NodeExecutionState
)
async def test_join_node_integration():
"""Test execute_join_node via execute_node dispatcher"""
# Setup: 2 executed paths
path_a_state = NodeExecutionState(
node_id="path_a",
status=NodeStatus.EXECUTED,
analysis_core="Path A completed successfully"
)
path_b_state = NodeExecutionState(
node_id="path_b",
status=NodeStatus.EXECUTED,
analysis_core="Path B completed successfully"
)
# Join node
join_node = WorkflowNode(
id="join",
type="join",
join_strategy=JoinStrategy.WAIT_ALL
)
# Graph
graph = WorkflowGraph(
nodes=[
WorkflowNode(id="path_a", type="analysis"),
WorkflowNode(id="path_b", type="analysis"),
join_node
],
edges=[
WorkflowEdge(id="e1", from_node="path_a", to_node="join"),
WorkflowEdge(id="e2", from_node="path_b", to_node="join")
]
)
# Context with previous node results
context = {
"variables": {},
"profile_id": "test-profile",
"node_results": {
"path_a": path_a_state,
"path_b": path_b_state
},
"active_edges": {}
}
# Execute join node via dispatcher
async def mock_llm(prompt, model):
return "Mock LLM response"
result = await execute_node(
node=join_node,
context=context,
catalog={},
graph=graph,
openrouter_call_func=mock_llm
)
# Verify
print(f"✅ Node executed: {result.node_id}")
print(f"✅ Status: {result.status.value}")
print(f"✅ Analysis core exists: {result.analysis_core is not None}")
assert result.node_id == "join"
assert result.status == NodeStatus.EXECUTED
assert result.analysis_core is not None
# Check consolidated data
import json
consolidated = json.loads(result.analysis_core)
print(f"✅ Consolidated paths: {len(consolidated)}")
assert len(consolidated) == 2
assert "path_a" in consolidated
assert "path_b" in consolidated
print(f"✅ Path A analysis: {consolidated['path_a'][:50]}...")
print(f"✅ Path B analysis: {consolidated['path_b'][:50]}...")
print("\n🎉 Integration Test: JOIN NODE WORKING IN WORKFLOW EXECUTOR!")
print(" - Both paths consolidated successfully")
print(" - Analysis cores merged correctly")
print(" - Join strategy executed properly")
return True
if __name__ == "__main__":
try:
success = asyncio.run(test_join_node_integration())
sys.exit(0 if success else 1)
except Exception as e:
print(f"❌ Test failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)