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
450 lines
14 KiB
Markdown
450 lines
14 KiB
Markdown
# 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
|