cursor_Setup
This commit is contained in:
parent
c607cd1833
commit
dc87e7f3b8
|
|
@ -27,3 +27,9 @@ ALLOWED_ORIGINS=https://mitai.jinkendo.de
|
|||
# ── Pfade ───────────────────────────────────────────────────────
|
||||
PHOTOS_DIR=/app/photos
|
||||
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
3
.gitignore
vendored
|
|
@ -61,4 +61,7 @@ tmp/
|
|||
|
||||
#.claude Konfiguration
|
||||
.claude/
|
||||
|
||||
# Cursor MCP mit Secrets (Example: .cursor/mcp.json.example)
|
||||
.cursor/mcp.json
|
||||
.claude/settings.local.jsonfrontend/package-lock.json
|
||||
|
|
|
|||
269
docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md
Normal file
269
docs/issues/REVIEW_OPEN_ISSUES_2026-04-04.md
Normal 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 |
|
||||
| 56–58 | 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 C1–C4 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.*
|
||||
64
scripts/gitea/MCP_SETUP.md
Normal file
64
scripts/gitea/MCP_SETUP.md
Normal 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
68
scripts/gitea/README.md
Normal 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
183
scripts/gitea/gitea_api.py
Normal 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
226
scripts/gitea/gitea_lib.py
Normal 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
|
||||
127
scripts/gitea/mcp_server_gitea.py
Normal file
127
scripts/gitea/mcp_server_gitea.py
Normal 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()
|
||||
2
scripts/gitea/requirements-mcp.txt
Normal file
2
scripts/gitea/requirements-mcp.txt
Normal 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
94
tests/phase3_e2e_test.sql
Normal 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
123
tests/phase4_e2e_test.sql
Normal 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
|
||||
58
tests/test_join_deployed.py
Normal file
58
tests/test_join_deployed.py
Normal 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!")
|
||||
105
tests/test_join_integration.py
Normal file
105
tests/test_join_integration.py
Normal 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)
|
||||
Loading…
Reference in New Issue
Block a user