# 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
