# 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