shinkan-jinkendo/.claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md
Lars 5626be792f
Some checks failed
Deploy Development / deploy (push) Successful in 34s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 5s
Test Suite / playwright-tests (push) Failing after 1m54s
docs: Dokumentation aktualisiert für Skills-Import Complete
Updates:
- DATABASE_SCHEMA.md v0.4.0 (Migration 022+023, Skills-Hierarchie)
- DOMAIN_MODEL.md v0.4.0 (Fähigkeiten-Domäne, Fokusbereich-Zuordnung)
- MEDIAWIKI_IMPORT_SPEC.md v1.1 (Skills via Migration statt API)
- PROJECT_STATUS.md (NEU - Projekt-Übersicht & Deployment-Status)

Änderungen:
- 69 Skills mit Kategorisierung dokumentiert
- Haupt-/Unterkategorien (KARATE/ALLGEMEINE)
- Fokusbereich-Zuordnung (karate/universal)
- Level-Definitionen-Schema
- Migration 021 als DEPRECATED markiert
- Deployment-Status aktualisiert
- Lessons Learned dokumentiert
2026-04-27 11:18:49 +02:00

450 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MediaWiki Import Specification
**Version:** 1.1
**Datum:** 2026-04-27
**Status:** DEPLOYED - Skills via Migration, Exercises via API
**Autor:** Claude Code
---
## 1. Übersicht
**Ziel:** Übungen aus dem Jinkendo MediaWiki-System direkt via API importieren
(NICHT über Export/XML-Dump siehe Architektur-Entscheidung in EXERCISES_ARCHITECTURE.md §8).
**Importrichtung:** Einseitig (MediaWiki → Shinkan). Kein bidirektionaler Sync.
**Import-Reihenfolge:** Skills → Methods → Exercises (Abhängigkeiten zuerst)
**Import-Tracking:** Jeder Import wird geloggt, Duplikate werden via `wiki_page_title`
erkannt und nicht doppelt angelegt.
**⚠️ WICHTIG - Skills-Import (ab 2026-04-27):**
Skills werden NICHT mehr über die API importiert, sondern über **Migration 023**
(vollständiger Import aus Fähigkeitsmatrix mit Kategorisierung).
Siehe `.claude/handover/session-2026-04-27-skills-complete.md` für Details.
---
## 2. MediaWiki API-Zugriff
### 2.1 API-Endpoint
```
GET https://karatetrainer.net/api.php
```
**Benötigte Parameter:**
- `action=query` Seiteninhalte abfragen
- `action=parse` Einzelseite parsen (mit Semantic MediaWiki Properties)
- `prop=text|properties|categories` Relevante Felder
- `format=json`
### 2.2 Authentifizierung
Aktuell: Kein Auth (wenn Wiki öffentlich zugänglich).
Bei privatem Wiki: `lgtoken` via `/api.php?action=login`.
### 2.3 Relevante API-Calls
**Alle Übungen einer Kategorie auflisten:**
```
GET /api.php?action=query&list=categorymembers&cmtitle=Kategorie:Übungen&cmlimit=500&format=json
```
**Einzelne Übung abrufen (mit SMW Properties):**
```
GET /api.php?action=parse&page=Übungsname&prop=text|properties&format=json
```
**SMW-Properties einer Seite:**
```
GET /api.php?action=browsebysubject&subject=Übungsname&format=json
```
---
## 3. Field-Mapping (MediaWiki → Exercises)
**Echte Property-Namen von karatetrainer.net** (via `discover_properties()` ermittelt, 2026-04-24)
### 3.1 Kern-Felder
| Wiki-Property (SMW) | Exercises-Feld | Transformation |
|---------------------|---------------|----------------|
| `Übungsbezeichnung` | `title` | Bevorzugt über Seitentitel |
| `Summary` | `summary` | Direkt |
| `Ziel` | `goal` | Direkt |
| `Durchführung` | `execution` | Wikitext → Plaintext |
| `Hinweise` | `trainer_notes` | Wikitext → Plaintext |
| `Plandauer` | `duration_min` = `duration_max` | Zahl in Minuten z.B. `"10"` |
| `Gruppengröße` | `group_size_min` | Zahl z.B. `"2"` |
| `Hilfsmittel` | `equipment` (JSONB) | Array von Strings |
### 3.2 Katalog-Mapping
| Wiki-Property | Exercises-Tabelle | Transformation |
|---------------|------------------|----------------|
| `Übungstyp` | `exercise_focus_areas` | z.B. `"Karate"` → focus_area Name → ID |
| `Zielgruppe` | `exercise_target_groups` | Name → ID Lookup |
| `Altersgruppe` | `exercise_age_groups` | Name → ID Lookup |
| `Trainingsmethode` | exercise method ref | Wiki-Seitenname → Label (Unterstriche → Leerzeichen) |
| `PrimaryCapability` | `exercise_skills` (Name) | Skill-Name → ID Lookup |
| `CapabilityLevel` | `exercise_skills` (target_level) | Integer → Named Level (1=einsteiger…5=experte) |
| `Schlüsselworte` | — | Keywords (für zukünftige Tag-Funktion) |
### 3.3 Skill-Level-Mapping (CapabilityLevel → INTEGER)
**🆕 ÄNDERUNG (2026-04-27):** `target_level` ist jetzt INTEGER (1-5), nicht mehr String.
| Wiki-Wert | Shinkan DB-Wert | Bedeutung |
|-----------|-----------------|-----------|
| "1" | 1 (INTEGER) | Einsteiger |
| "2" | 2 (INTEGER) | Grundlagen |
| "3" | 3 (INTEGER) | Aufbau |
| "4" | 4 (INTEGER) | Fortgeschritten |
| "5" | 5 (INTEGER) | Experte |
**Implementierung:** `smw_mapper.py` Zeile 271-299 `build_skill_assignments()`
konvertiert String → INTEGER via `int(level_str.strip())`.
### 3.4 Kategorien (karatetrainer.net)
| Import-Typ | Wiki-Kategorie | Status | Import-Methode |
|------------|---------------|--------|----------------|
| exercises | `Übungen` | ✅ Produktiv | API (`/api/import-wiki/execute`) |
| skills | `Fähigkeitsbeschreibung` | ⚠️ DEPRECATED | ~~API~~**Migration 023** |
| methods | `Methodenbeschreibung` | 🔲 Geplant | API (noch nicht implementiert) |
**⚠️ Skills-Import-Änderung (2026-04-27):**
Skills werden NICHT mehr über `import_type: skill` importiert, sondern über:
- **Migration 022:** Schema-Erweiterung (skill_main_categories, focus_areas, level_definitions)
- **Migration 023:** Vollständiger Import (69 Skills mit Kategorisierung aus Fähigkeitsmatrix)
**Grund:** API-Import lieferte nur skill_name, aber KEINE Kategorisierung
(Haupt-/Unterkategorie, Fokusbereich). Migration 023 importiert alle Skills
mit vollständiger Metadaten-Struktur.
**Quelle:** https://karatetrainer.net/index.php?title=Fähigkeitsmatrix
**Details:** `.claude/handover/session-2026-04-27-skills-complete.md`
**Unbekannte Katalog-Werte:** Warnmeldung im Import-Log, Übung wird ohne
diese Zuordnung importiert (kein Abbruch).
### 3.3 Import-Tracking-Felder
| Feld | Wert |
|------|------|
| `import_source` | `'mediawiki'` |
| `import_id` | Wiki-Seitenname (URL-kodiert) |
| `visibility` | `'private'` (Trainer prüft vor Veröffentlichung) |
| `status` | `'draft'` |
### 3.4 Wikitext-Bereinigung
Vor dem Speichern werden folgende Wikitext-Elemente in Plaintext umgewandelt:
- `[[Link|Text]]``Text`
- `[[Link]]``Link`
- `{{Template}}` → (entfernt)
- `'''Bold'''``Bold`
- `''Italic''``Italic`
- `==Überschrift==``\nÜberschrift\n`
- Wikitables → (entfernt, zu komplex für automatischen Import)
---
## 4. Tracking-Tabellen
```sql
-- Migration: wiki_import_tracking
-- (In eigene Migration auslagern: 018_wiki_import_tracking.sql)
CREATE TABLE IF NOT EXISTS wiki_import_log (
id SERIAL PRIMARY KEY,
import_type VARCHAR(50) NOT NULL, -- 'exercise', 'skill', 'method'
import_status VARCHAR(20) CHECK (import_status IN ('running', 'completed', 'failed')),
items_total INT DEFAULT 0,
items_imported INT DEFAULT 0,
items_skipped INT DEFAULT 0,
items_failed INT DEFAULT 0,
error_log JSONB DEFAULT '[]'::jsonb, -- Array von {item, error} Objekten
imported_by INT REFERENCES profiles(id),
started_at TIMESTAMP DEFAULT NOW(),
finished_at TIMESTAMP
);
CREATE TABLE IF NOT EXISTS wiki_import_references (
id SERIAL PRIMARY KEY,
wiki_page_title VARCHAR(500) NOT NULL,
wiki_page_id INT, -- MediaWiki interne ID
content_type VARCHAR(50) NOT NULL, -- 'exercise', 'skill', 'method'
local_id INT NOT NULL, -- ID in der lokalen DB
last_imported TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(wiki_page_title, content_type)
);
CREATE INDEX IF NOT EXISTS idx_wiki_refs_title ON wiki_import_references(wiki_page_title);
CREATE INDEX IF NOT EXISTS idx_wiki_refs_type ON wiki_import_references(content_type);
```
---
## 5. API-Endpoints
### 5.1 Endpoints Overview
| Method | Endpoint | Beschreibung |
|--------|----------|--------------|
| GET | `/import/mediawiki/preview` | Vorschau: Was wird importiert? |
| POST | `/import/mediawiki/execute` | Import ausführen |
| GET | `/import/mediawiki/status/{log_id}` | Import-Status abrufen |
| GET | `/import/mediawiki/logs` | Alle Import-Logs |
| DELETE | `/import/mediawiki/references/{id}` | Referenz löschen (für Re-Import) |
### 5.2 `GET /import/mediawiki/preview`
**Query Parameters:**
- `category` (string, required) Wiki-Kategorie z.B. `Übungen`
- `limit` (int, optional, default: 10) Maximale Anzahl zum Voranschauen
**Response:** `200 OK`
```json
{
"category": "Übungen",
"total_found": 47,
"preview": [
{
"wiki_page_title": "Maai - Distanzübung",
"wiki_page_id": 1234,
"already_imported": false,
"last_imported_at": null,
"mapped_fields": {
"title": "Maai - Distanzübung",
"summary": "Distanzgefühl entwickeln...",
"goal": "Ziel...",
"focus_area": "Karate",
"focus_area_found": true
},
"warnings": [],
"errors": []
},
{
"wiki_page_title": "Kumite Grundtechniken",
"already_imported": true,
"last_imported_at": "2026-04-10T10:00:00Z",
"mapped_fields": {...},
"warnings": ["Feld 'Zielgruppe' hat unbekannten Wert 'Fortgeschrittene' - wird ignoriert"],
"errors": []
}
]
}
```
---
### 5.3 `POST /import/mediawiki/execute`
**Request Body:**
```json
{
"category": "Übungen",
"reimport_existing": false, // true: Bereits importierte Übungen überschreiben
"dry_run": false, // true: Nur simulieren, nichts speichern
"limit": null // null: alle importieren
}
```
**Response:** `202 Accepted` (Import läuft asynchron)
```json
{
"log_id": 5,
"status": "running",
"message": "Import gestartet. Verwende GET /import/mediawiki/status/5 für Status."
}
```
---
### 5.4 `GET /import/mediawiki/status/{log_id}`
**Response:** `200 OK`
```json
{
"id": 5,
"import_type": "exercise",
"import_status": "completed",
"items_total": 47,
"items_imported": 44,
"items_skipped": 2,
"items_failed": 1,
"error_log": [
{
"item": "Unvollständige Übung",
"error": "Pflichtfeld 'Durchführung' fehlt in Wiki-Seite"
}
],
"imported_by": 1,
"started_at": "2026-04-24T10:00:00Z",
"finished_at": "2026-04-24T10:02:30Z"
}
```
---
### 5.5 `GET /import/mediawiki/logs`
**Response:** `200 OK` Array von Import-Log Objekten (ohne error_log Details)
---
## 6. Backend-Implementation
### 6.1 Router-Datei
`backend/routers/import_wiki.py`
### 6.2 Import-Prozess (Pseudocode)
```python
async def execute_wiki_import(category: str, reimport: bool, dry_run: bool):
# 1. Alle Seiten der Kategorie via MediaWiki API abrufen
pages = wiki_api.get_category_members(category)
for page_title in pages:
# 2. Duplikat-Check
existing_ref = db.get_wiki_ref(page_title, 'exercise')
if existing_ref and not reimport:
log.skip(page_title, "bereits importiert")
continue
# 3. Seite parsen
wiki_data = wiki_api.parse_page(page_title)
# 4. Field-Mapping
exercise_data = map_wiki_to_exercise(wiki_data)
# 5. Validierung
errors = validate_exercise(exercise_data)
if errors:
log.fail(page_title, errors)
continue
# 6. Katalog-IDs auflösen
exercise_data = resolve_catalog_ids(exercise_data)
if not dry_run:
# 7. Speichern
exercise_id = db.upsert_exercise(exercise_data)
# 8. Import-Referenz aktualisieren
db.upsert_wiki_ref(page_title, 'exercise', exercise_id)
log.success(page_title)
```
### 6.3 Wikitext Parser
```python
import re
def wikitext_to_plaintext(wikitext: str) -> str:
"""Konvertiert Wikitext in lesbares Plaintext."""
text = wikitext
# Links: [[Link|Text]] → Text, [[Link]] → Link
text = re.sub(r'\[\[([^|]+)\|([^\]]+)\]\]', r'\2', text)
text = re.sub(r'\[\[([^\]]+)\]\]', r'\1', text)
# Templates entfernen
text = re.sub(r'\{\{[^}]+\}\}', '', text)
# Formatierung
text = re.sub(r"'''(.+?)'''", r'\1', text)
text = re.sub(r"''(.+?)''", r'\1', text)
# Überschriften
text = re.sub(r'={2,6}\s*(.+?)\s*={2,6}', r'\n\1\n', text)
# Mehrfache Leerzeilen normalisieren
text = re.sub(r'\n{3,}', '\n\n', text)
return text.strip()
```
---
## 7. Frontend-Import-UI
### 7.1 Route
```
/admin/import/mediawiki → AdminWikiImportPage
```
### 7.2 Layout
```
┌─────────────────────────────────────────┐
│ MediaWiki Import │
│ ─────────────────────────────────────── │
│ Wiki-Kategorie: [Übungen ] │
│ [+ Vorschau anzeigen] │
│ ─────────────────────────────────────── │
│ Vorschau (10 von 47): │
│ │
│ ✅ Maai - Distanzübung (neu) │
│ ✅ Kizami-Zuki (neu) │
│ ⚠️ Kumite XY (bereits importiert) │
│ ❌ Defekte Seite (Pflichtfeld fehlt) │
│ ... │
│ ─────────────────────────────────────── │
│ ☐ Bereits importierte überschreiben │
│ ☐ Nur simulieren (dry run) │
│ ─────────────────────────────────────── │
│ [47 Übungen importieren] │
│ ─────────────────────────────────────── │
│ Letzter Import: 44/47 · 2026-04-10 │
│ [Import-Logs anzeigen] │
└─────────────────────────────────────────┘
```
---
## 8. Fehlerbehandlung
| Fehler | Verhalten |
|--------|-----------|
| Wiki-Seite nicht erreichbar | Import-Prozess abbricht, Log-Eintrag |
| Pflichtfeld (`goal`, `execution`) fehlt | Item wird übersprungen, Warnung im Log |
| Unbekannter Katalog-Wert (Fokusbereich etc.) | Item wird ohne diese Zuordnung importiert |
| Duplikat-Titel im selben Club | Item wird übersprungen (außer reimport=true) |
| Wiki-API Timeout | Retry 3x, dann fail mit Log-Eintrag |
---
## 9. Sicherheit
- Nur Super-Admin darf Import ausführen (`require_role('superadmin')`)
- MediaWiki-URL ist konfigurierbar via `.env` (kein Hardcoding)
- Rate-Limiting: max. 1 Import parallel, max. 10/Tag
- Importierte Übungen starten immer als `visibility='private'`, `status='draft'`
---
## 10. Konfiguration (`.env`)
```
MEDIAWIKI_API_URL=https://karatetrainer.net/api.php
MEDIAWIKI_USER=import-bot # optional, für privates Wiki
MEDIAWIKI_PASSWORD=... # optional
```
---
**Version:** 1.0
**Datum:** 2026-04-24
**Status:** DRAFT - Ready for Review