diff --git a/backend/migrations/022_skills_schema_complete.sql b/backend/migrations/022_skills_schema_complete.sql new file mode 100644 index 0000000..01a59b5 --- /dev/null +++ b/backend/migrations/022_skills_schema_complete.sql @@ -0,0 +1,90 @@ +-- Migration 022: Skills Schema Complete - Hauptkategorien + Fokusbereich + Level-Definitionen +-- Purpose: Erweitert Skills-Schema um vollständige Kategorisierung für Prod-Import +-- Date: 2026-04-27 + +-- ====================================================================== +-- 1. Haupt-Kategorien (KARATE Fähigkeiten / ALLGEMEINE sportliche Fähigkeiten) +-- ====================================================================== + +CREATE TABLE IF NOT EXISTS skill_main_categories ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) UNIQUE NOT NULL, + slug VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + sort_order INT, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- ====================================================================== +-- 2. Unterkategorien erweitern (Beziehung zu Hauptkategorie) +-- ====================================================================== + +-- Slug für URL-friendly Namen +ALTER TABLE skill_categories + ADD COLUMN IF NOT EXISTS slug VARCHAR(50); + +-- Beziehung zu Hauptkategorie +ALTER TABLE skill_categories + ADD COLUMN IF NOT EXISTS main_category_id INT REFERENCES skill_main_categories(id); + +-- Unique constraint für slug +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'skill_categories_slug_unique' + ) THEN + ALTER TABLE skill_categories ADD CONSTRAINT skill_categories_slug_unique UNIQUE (slug); + END IF; +END $$; + +-- ====================================================================== +-- 3. Skills erweitern (Hauptkategorie + Fokusbereich) +-- ====================================================================== + +-- Beziehung zu Hauptkategorie (für direkte Queries) +ALTER TABLE skills + ADD COLUMN IF NOT EXISTS main_category_id INT REFERENCES skill_main_categories(id); + +-- Fokusbereich als JSONB Array (z.B. ['karate'], ['universal'], ['karate', 'selbstverteidigung']) +ALTER TABLE skills + ADD COLUMN IF NOT EXISTS focus_areas JSONB DEFAULT '[]'::jsonb; + +-- ====================================================================== +-- 4. Level-Definitionen (1-5 Beschreibungen aus Matrix) +-- ====================================================================== + +CREATE TABLE IF NOT EXISTS skill_level_definitions ( + id SERIAL PRIMARY KEY, + skill_id INT NOT NULL REFERENCES skills(id) ON DELETE CASCADE, + level INT NOT NULL CHECK (level BETWEEN 1 AND 5), + description TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + UNIQUE (skill_id, level) +); + +-- Index für schnelle Abfragen +CREATE INDEX IF NOT EXISTS idx_skill_level_definitions_skill_id + ON skill_level_definitions(skill_id); + +-- ====================================================================== +-- 5. Indizes für Performance +-- ====================================================================== + +CREATE INDEX IF NOT EXISTS idx_skills_main_category_id + ON skills(main_category_id); + +CREATE INDEX IF NOT EXISTS idx_skills_category_id + ON skills(category_id); + +CREATE INDEX IF NOT EXISTS idx_skill_categories_main_category_id + ON skill_categories(main_category_id); + +-- ====================================================================== +-- 6. Kommentare für Dokumentation +-- ====================================================================== + +COMMENT ON TABLE skill_main_categories IS 'Hauptkategorien: KARATE Fähigkeiten / ALLGEMEINE sportliche Fähigkeiten'; +COMMENT ON COLUMN skills.focus_areas IS 'JSONB Array: [''karate''] = nur Karate, [''universal''] = alle Fokussbereiche'; +COMMENT ON TABLE skill_level_definitions IS 'Level 1-5 Beschreibungen aus Fähigkeitsmatrix für jede Fähigkeit'; diff --git a/backend/migrations/023_skills_complete_import.sql b/backend/migrations/023_skills_complete_import.sql new file mode 100644 index 0000000..c3ce07c --- /dev/null +++ b/backend/migrations/023_skills_complete_import.sql @@ -0,0 +1,147 @@ +-- Total: 69 Skills +-- Migration 023: Vollständiger Skills-Import +-- Purpose: Produktionsreifer Import aller 69 Skills mit vollständiger Kategorisierung +-- Source: Fähigkeitsmatrix https://karatetrainer.net/index.php?title=Fähigkeitsmatrix +-- Date: 2026-04-27 + +-- ====================================================================== +-- CLEANUP: Alte Daten löschen +-- ====================================================================== + +-- Erst M:N-Beziehungen löschen +DELETE FROM exercise_skills; + +-- Skills löschen (Cascades zu skill_level_definitions) +DELETE FROM skills; + +-- Kategorien löschen +DELETE FROM skill_categories; +DELETE FROM skill_main_categories WHERE id > 0; -- Falls Tabelle existiert + +-- ====================================================================== +-- 1. HAUPT-KATEGORIEN +-- ====================================================================== + +INSERT INTO skill_main_categories (name, slug, description, sort_order) VALUES +('KARATE Fähigkeiten', 'karate', 'Karate-spezifische Techniken und Fähigkeiten', 1), +('ALLGEMEINE sportliche Fähigkeiten', 'allgemeine', 'Universelle sportliche und mentale Fähigkeiten', 2); + +-- ====================================================================== +-- 2. UNTERKATEGORIEN +-- ====================================================================== + +INSERT INTO skill_categories (name, slug, main_category_id, description, sort_order) VALUES +('Kata', 'kata', (SELECT id FROM skill_main_categories WHERE slug='karate'), '', 1), +('Kihon', 'kihon', (SELECT id FROM skill_main_categories WHERE slug='karate'), '', 2), +('Kumite', 'kumite', (SELECT id FROM skill_main_categories WHERE slug='karate'), '', 3), +('Selbstverteidigung', 'selbstverteidigung', (SELECT id FROM skill_main_categories WHERE slug='karate'), '', 4), +('Kognition', 'kognition', (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '', 5), +('Kondition', 'kondition', (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '', 6), +('Koordination', 'koordination', (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '', 7), +('Psychische Fähigkeiten', 'psychische_faehigkeiten', (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '', 8), +('Soziale Fähigkeiten', 'soziale_faehigkeiten', (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '', 9); + +-- ====================================================================== +-- 3. SKILLS +-- ====================================================================== + +INSERT INTO skills (name, description, category_id, main_category_id, focus_areas) VALUES +('Technik Kombination', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Kata Ablauf', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Bunkai', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Oyo', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Henka', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Kakushi', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Kata Atmung', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Kata Rhythmus', '', (SELECT id FROM skill_categories WHERE slug='kata'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Dachi Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Uke Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Zuki Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Uchi Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Geri Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Nage Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Nukite Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Ken Waza', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Hüfteinsatz', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Kime', '', (SELECT id FROM skill_categories WHERE slug='kihon'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Beinarbeit', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Distanzkontrolle', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Angriff', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Abwehr Konter', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Präzision', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Antizipation', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Timing', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Taktik', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Fokus', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Mentale Stärke', '', (SELECT id FROM skill_categories WHERE slug='kumite'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Gefahrenbewustsein', '', (SELECT id FROM skill_categories WHERE slug='selbstverteidigung'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Selbstbehauptung', '', (SELECT id FROM skill_categories WHERE slug='selbstverteidigung'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Selbstschutz', '', (SELECT id FROM skill_categories WHERE slug='selbstverteidigung'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Gefahrenabwehr', '', (SELECT id FROM skill_categories WHERE slug='selbstverteidigung'), (SELECT id FROM skill_main_categories WHERE slug='karate'), '["karate"]'::jsonb), +('Aufmerksamkeit', '', (SELECT id FROM skill_categories WHERE slug='kognition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Wahrnehmung', '', (SELECT id FROM skill_categories WHERE slug='kognition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Urteilsvermögen', '', (SELECT id FROM skill_categories WHERE slug='kognition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Merkfähigkeit', '', (SELECT id FROM skill_categories WHERE slug='kognition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Lernfähigkeit', '', (SELECT id FROM skill_categories WHERE slug='kognition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Maximalkraft', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Schnellkraft', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Reaktivkraft', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Kraftausdauer', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Muskelaufbau', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Reaktionsschnelligkeit', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Bewegungsschnelligkeit', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Handlungsschnelligkeit', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Schnelligkeitsausdauer', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Grundlagenausdauer', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Aerobe Ausdauer', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Anaerobe Ausdauer', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Regenerationsfähigkeit', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Ermüdungswiderstandsfähigkeit', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Flexibilität', '', (SELECT id FROM skill_categories WHERE slug='kondition'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Orientierung', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Differenzierung', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Kopplung', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Gleichgewicht', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Rhythmisierung', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Reaktion', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Umstellung', '', (SELECT id FROM skill_categories WHERE slug='koordination'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Selbstvertrauen', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Konzentration', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Emotionale Kontrolle', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Motivation', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Stressresistenz', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Stressregulation', '', (SELECT id FROM skill_categories WHERE slug='psychische_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Deeskalation', '', (SELECT id FROM skill_categories WHERE slug='soziale_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Selbstdisziplin', '', (SELECT id FROM skill_categories WHERE slug='soziale_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Toleranz', '', (SELECT id FROM skill_categories WHERE slug='soziale_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb), +('Fairness', '', (SELECT id FROM skill_categories WHERE slug='soziale_faehigkeiten'), (SELECT id FROM skill_main_categories WHERE slug='allgemeine'), '["universal"]'::jsonb); + +-- ====================================================================== +-- 4. VERIFIKATION +-- ====================================================================== + +-- Sollte 69 Skills ergeben +DO $$ +DECLARE + skill_count INT; +BEGIN + SELECT COUNT(*) INTO skill_count FROM skills; + + IF skill_count != 69 THEN + RAISE WARNING 'FEHLER: % Skills gefunden, erwartet 69', skill_count; + ELSE + RAISE NOTICE 'OK: 69 Skills importiert'; + END IF; +END $$; + +-- Zeige Verteilung +SELECT + mc.name AS hauptkategorie, + sc.name AS unterkategorie, + COUNT(s.id) AS anzahl_skills +FROM skills s +JOIN skill_categories sc ON s.category_id = sc.id +JOIN skill_main_categories mc ON s.main_category_id = mc.id +GROUP BY mc.name, sc.name, mc.sort_order, sc.sort_order +ORDER BY mc.sort_order, sc.sort_order; + diff --git a/backend/scripts/generate_migration_023_direct.py b/backend/scripts/generate_migration_023_direct.py new file mode 100644 index 0000000..aec4e19 --- /dev/null +++ b/backend/scripts/generate_migration_023_direct.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python3 +""" +Generiert Migration 023 direkt aus der Fähigkeitsmatrix (ohne CSV-Zwischenschritt). +""" +import sys +import httpx +from bs4 import BeautifulSoup +from collections import defaultdict + +# Force UTF-8 output +sys.stdout.reconfigure(encoding='utf-8') + +def to_slug(name): + """Wandelt Namen in URL-friendly Slugs um.""" + return name.lower().replace(' ', '_').replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue').replace('ß', 'ss') + +# Wiki-Login und Matrix-Abruf +api_url = 'https://karatetrainer.net/api.php' +username = 'Jinkendo' +password = 'Jinkendo6970' + +with httpx.Client(timeout=30) as client: + # Login + r1 = client.get(api_url, params={'action': 'query', 'meta': 'tokens', 'type': 'login', 'format': 'json'}) + r1.raise_for_status() + token = r1.json().get('query', {}).get('tokens', {}).get('logintoken', '') + + r2 = client.post(api_url, data={'action': 'login', 'lgname': username, 'lgpassword': password, 'lgtoken': token, 'format': 'json'}) + r2.raise_for_status() + + # Hole Matrix + r3 = client.get('https://karatetrainer.net/index.php?title=Fähigkeitsmatrix') + r3.raise_for_status() + html = r3.text + +# Parse HTML +soup = BeautifulSoup(html, 'html.parser') +table = soup.find('table', {'class': 'wikitable'}) + +if not table: + print("ERROR: Tabelle nicht gefunden", file=sys.stderr) + sys.exit(1) + +rows = table.find_all('tr') + +# Extrahiere Skills +current_main_cat = 'karate' +current_sub_cat = None +skills_data = [] + +for idx, row in enumerate(rows): + cells = row.find_all(['td', 'th']) + + if len(cells) == 1: + text = cells[0].get_text(strip=True) + + if 'Inhaltsverzeichnis' in text: + continue + + if 'ALLGEMEINE' in text and 'sportliche' in text: + current_main_cat = 'allgemeine' + current_sub_cat = None + else: + current_sub_cat = text.replace('­', '').strip() + + elif len(cells) == 6: + skill_name = cells[0].get_text(strip=True).replace('­', '') + + if skill_name and current_main_cat and current_sub_cat: + focus = 'karate' if current_main_cat == 'karate' else 'universal' + + skills_data.append({ + 'skill': skill_name, + 'sub_cat': current_sub_cat, + 'main_cat': current_main_cat, + 'focus': focus + }) + +# Duplikat-Handling +duplicates_to_remove = { + ('Anaerobe Ausdauer', 'Kumite'), + ('Bewegungsschnelligkeit', 'Kumite'), + ('Flexibilität', 'Kumite'), + ('Reaktionsschnelligkeit', 'Kumite'), + ('Schnelligkeitsausdauer', 'Kumite'), + ('Antizipation', 'Koordination'), + ('Timing', 'Koordination'), +} + +filtered_skills = [] +for s in skills_data: + if (s['skill'], s['sub_cat']) not in duplicates_to_remove: + filtered_skills.append(s) + +# Gruppiere nach Kategorien +by_main_cat = defaultdict(lambda: defaultdict(list)) +sub_categories = {} + +for s in filtered_skills: + main = s['main_cat'] + sub = s['sub_cat'] + by_main_cat[main][sub].append(s) + + if sub not in sub_categories: + sub_categories[sub] = (to_slug(sub), main) + +# Generiere SQL +print("""-- Migration 023: Vollständiger Skills-Import +-- Purpose: Produktionsreifer Import aller 69 Skills mit vollständiger Kategorisierung +-- Source: Fähigkeitsmatrix https://karatetrainer.net/index.php?title=Fähigkeitsmatrix +-- Date: 2026-04-27 + +-- ====================================================================== +-- CLEANUP: Alte Daten löschen +-- ====================================================================== + +-- Erst M:N-Beziehungen löschen +DELETE FROM exercise_skills; + +-- Skills löschen (Cascades zu skill_level_definitions) +DELETE FROM skills; + +-- Kategorien löschen +DELETE FROM skill_categories; +DELETE FROM skill_main_categories WHERE id > 0; -- Falls Tabelle existiert + +-- ====================================================================== +-- 1. HAUPT-KATEGORIEN +-- ====================================================================== + +INSERT INTO skill_main_categories (name, slug, description, sort_order) VALUES +('KARATE Fähigkeiten', 'karate', 'Karate-spezifische Techniken und Fähigkeiten', 1), +('ALLGEMEINE sportliche Fähigkeiten', 'allgemeine', 'Universelle sportliche und mentale Fähigkeiten', 2); + +-- ====================================================================== +-- 2. UNTERKATEGORIEN +-- ====================================================================== +""") + +# Sortiere Unterkategorien +karate_subs = sorted([(name, slug, main) for name, (slug, main) in sub_categories.items() if main == 'karate']) +allgemeine_subs = sorted([(name, slug, main) for name, (slug, main) in sub_categories.items() if main == 'allgemeine']) + +sort_order = 1 +print("INSERT INTO skill_categories (name, slug, main_category_id, description, sort_order) VALUES") + +for idx, (name, slug, main_cat) in enumerate(karate_subs + allgemeine_subs): + comma = "," if idx < len(karate_subs) + len(allgemeine_subs) - 1 else ";" + print(f"('{name}', '{slug}', (SELECT id FROM skill_main_categories WHERE slug='{main_cat}'), '', {sort_order}){comma}") + sort_order += 1 + +print(""" +-- ====================================================================== +-- 3. SKILLS +-- ====================================================================== + +INSERT INTO skills (name, description, category_id, main_category_id, focus_areas) VALUES""") + +# Sortiere Skills +karate_skills = [] +for sub in [name for name, slug, main in karate_subs]: + karate_skills.extend(by_main_cat['karate'][sub]) + +allgemeine_skills = [] +for sub in [name for name, slug, main in allgemeine_subs]: + allgemeine_skills.extend(by_main_cat['allgemeine'][sub]) + +all_skills = karate_skills + allgemeine_skills + +for idx, s in enumerate(all_skills): + name = s['skill'].replace("'", "''") # SQL-Escape + sub_cat_slug = to_slug(s['sub_cat']) + main_cat_slug = s['main_cat'] + focus = s['focus'] + + comma = "," if idx < len(all_skills) - 1 else ";" + + print(f"('{name}', '', (SELECT id FROM skill_categories WHERE slug='{sub_cat_slug}'), (SELECT id FROM skill_main_categories WHERE slug='{main_cat_slug}'), '[\"{focus}\"]'::jsonb){comma}") + +print(""" +-- ====================================================================== +-- 4. VERIFIKATION +-- ====================================================================== + +-- Sollte 69 Skills ergeben +DO $$ +DECLARE + skill_count INT; +BEGIN + SELECT COUNT(*) INTO skill_count FROM skills; + + IF skill_count != 69 THEN + RAISE WARNING 'FEHLER: % Skills gefunden, erwartet 69', skill_count; + ELSE + RAISE NOTICE 'OK: 69 Skills importiert'; + END IF; +END $$; + +-- Zeige Verteilung +SELECT + mc.name AS hauptkategorie, + sc.name AS unterkategorie, + COUNT(s.id) AS anzahl_skills +FROM skills s +JOIN skill_categories sc ON s.category_id = sc.id +JOIN skill_main_categories mc ON s.main_category_id = mc.id +GROUP BY mc.name, sc.name, mc.sort_order, sc.sort_order +ORDER BY mc.sort_order, sc.sort_order; +""") + +print(f"-- Total: {len(all_skills)} Skills", file=sys.stderr) diff --git a/backend/scripts/parse_matrix.py b/backend/scripts/parse_matrix.py new file mode 100644 index 0000000..b455065 --- /dev/null +++ b/backend/scripts/parse_matrix.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Parse Fähigkeitsmatrix und extrahiere vollständige Kategorisierung. +""" +import sys +import httpx +from bs4 import BeautifulSoup + +def parse_matrix(): + """Parse die Fähigkeitsmatrix und gib CSV-Mapping aus.""" + + # Wiki-Login + api_url = 'https://karatetrainer.net/api.php' + username = 'Jinkendo' + password = 'Jinkendo6970' + + # Synchroner Client + with httpx.Client(timeout=30) as client: + # Schritt 1: Login-Token holen + r1 = client.get(api_url, params={ + 'action': 'query', + 'meta': 'tokens', + 'type': 'login', + 'format': 'json', + }) + r1.raise_for_status() + data1 = r1.json() + token = data1.get('query', {}).get('tokens', {}).get('logintoken', '') + + if not token: + print("ERROR: Kein Login-Token erhalten", file=sys.stderr) + return + + # Schritt 2: Login durchführen + r2 = client.post(api_url, data={ + 'action': 'login', + 'lgname': username, + 'lgpassword': password, + 'lgtoken': token, + 'format': 'json', + }) + r2.raise_for_status() + data2 = r2.json() + + if data2.get('login', {}).get('result') != 'Success': + print(f"ERROR: Login fehlgeschlagen: {data2}", file=sys.stderr) + return + + # Hole Matrix-Seite als HTML + page_url = 'https://karatetrainer.net/index.php?title=Fähigkeitsmatrix' + r3 = client.get(page_url) + r3.raise_for_status() + html = r3.text + + if not html: + print("ERROR: Konnte Matrix-HTML nicht abrufen", file=sys.stderr) + return + + # Parse HTML + soup = BeautifulSoup(html, 'html.parser') + table = soup.find('table', {'class': 'wikitable'}) + + if not table: + print("ERROR: Tabelle nicht gefunden", file=sys.stderr) + return + + rows = table.find_all('tr') + + # Extrahiere Struktur + # Default: KARATE Fähigkeiten (bis Row 49 wo ALLGEMEINE beginnt) + current_main_cat = 'karate' + current_sub_cat = None + skills_data = [] + + for idx, row in enumerate(rows): + cells = row.find_all(['td', 'th']) + + if len(cells) == 1: + # Kategorie-Header (1 Zelle) + text = cells[0].get_text(strip=True) + + # Überspringe Inhaltsverzeichnis (Row 4) + if 'Inhaltsverzeichnis' in text: + continue + + # Haupt-Kategorie Wechsel erkennen (nur bei "ALLGEMEINE sportliche Fähigkeiten") + if 'ALLGEMEINE' in text and 'sportliche' in text: + current_main_cat = 'allgemeine' + current_sub_cat = None + # print(f"DEBUG: Row {idx} - Wechsel zu allgemeine", file=sys.stderr) + else: + # Alle anderen 1-Zellen-Rows sind Unterkategorien + # Bereinige Text (entferne Sonderzeichen, Leerzeichen) + current_sub_cat = text.replace('­', '').strip() # soft hyphen entfernen + # print(f"DEBUG: Row {idx} - Unterkategorie '{current_sub_cat}' (main_cat={current_main_cat})", file=sys.stderr) + + elif len(cells) == 6: + # Skill-Zeile (6 Zellen: Name + 5 Level-Beschreibungen) + skill_name = cells[0].get_text(strip=True).replace('­', '') # soft hyphen entfernen + + # Nur Skills mit Hauptkategorie UND Unterkategorie erfassen + if skill_name and current_main_cat and current_sub_cat: + # Fokusbereich bestimmen + focus = 'karate' if current_main_cat == 'karate' else 'universal' + + skills_data.append({ + 'skill': skill_name, + 'sub_cat': current_sub_cat, + 'main_cat': current_main_cat, + 'focus': focus, + 'row': idx + }) + + # Duplikat-Handling: Bevorzuge spezifischere Kategorien + # Bei Duplikaten: Kondition/Koordination > Kumite (für allgemeine Fähigkeiten) + # Kumite > Rest (für Kampf-spezifische wie "Timing", "Antizipation") + + duplicates_to_remove = { + ('Anaerobe Ausdauer', 'Kumite'), # Behalte Kondition + ('Bewegungsschnelligkeit', 'Kumite'), # Behalte Kondition + ('Flexibilität', 'Kumite'), # Behalte Kondition + ('Reaktionsschnelligkeit', 'Kumite'), # Behalte Kondition + ('Schnelligkeitsausdauer', 'Kumite'), # Behalte Kondition + ('Antizipation', 'Koordination'), # Behalte Kumite (kampfspezifisch) + ('Timing', 'Koordination'), # Behalte Kumite (kampfspezifisch) + } + + # Filtere Duplikate + filtered_skills = [] + for s in skills_data: + if (s['skill'], s['sub_cat']) not in duplicates_to_remove: + filtered_skills.append(s) + + # Ausgabe als CSV (UTF-8) + import sys + # Force UTF-8 output + sys.stdout.reconfigure(encoding='utf-8') + + print('skill_name,sub_category,main_category,focus_areas') + for s in filtered_skills: + # Escape Kommas in Namen + skill = s['skill'].replace(',', ';') + sub_cat = s['sub_cat'].replace(',', ';') + print(f"{skill},{sub_cat},{s['main_cat']},{s['focus']}") + + print(f"\n# Total: {len(filtered_skills)} Skills (von {len(skills_data)} Zeilen, {len(skills_data) - len(filtered_skills)} Duplikate entfernt)", file=sys.stderr) + +if __name__ == '__main__': + parse_matrix()