shinkan-jinkendo/.claude/docs/technical/MEDIAWIKI_IMPORT_SPEC.md

450 lines
14 KiB
Markdown
Raw Normal View History

# 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