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
14 KiB
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 abfragenaction=parse– Einzelseite parsen (mit Semantic MediaWiki Properties)prop=text|properties|categories– Relevante Felderformat=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 | |
| 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
-- 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.Übungenlimit(int, optional, default: 10) – Maximale Anzahl zum Voranschauen
Response: 200 OK
{
"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:
{
"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)
{
"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
{
"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)
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
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