""" BAN YARO — Datenbank SQLite mit WAL-Modus (bewährt von akku-werkstatt). """ import sqlite3 import os import logging import unicodedata from contextlib import contextmanager logger = logging.getLogger(__name__) DB_PATH = os.getenv("DB_PATH", "/data/banyaro.db") def _norm(s): """Diakritika entfernen + lowercase — für akzentunabhängige Suche.""" if not s: return '' return unicodedata.normalize('NFKD', str(s)).encode('ascii', 'ignore').decode('ascii').lower() def get_connection() -> sqlite3.Connection: conn = sqlite3.connect(DB_PATH, check_same_thread=False) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA foreign_keys=ON") conn.execute("PRAGMA busy_timeout=5000") conn.execute("PRAGMA cache_size = -32000") # 32MB Page-Cache conn.execute("PRAGMA temp_store = MEMORY") # Temp-Tabellen im RAM statt auf Disk conn.execute("PRAGMA mmap_size = 268435456") # 256MB Memory-Mapped I/O conn.create_function('norm', 1, _norm) return conn @contextmanager def db(): conn = get_connection() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def init_db(): """Erstellt alle Tabellen falls nicht vorhanden.""" logger.info(f"Initialisiere Datenbank: {DB_PATH}") os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) with db() as conn: conn.executescript(""" -- USERS CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, pw_hash TEXT NOT NULL, name TEXT NOT NULL, rolle TEXT NOT NULL DEFAULT 'user', is_premium INTEGER NOT NULL DEFAULT 0, push_sub TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), last_login TEXT ); -- HUNDE CREATE TABLE IF NOT EXISTS dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, rasse TEXT, geburtstag TEXT, geschlecht TEXT, gewicht_kg REAL, chip_nr TEXT, foto_url TEXT, bio TEXT, is_public INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- TAGEBUCH CREATE TABLE IF NOT EXISTS diary ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, datum TEXT NOT NULL DEFAULT (date('now')), typ TEXT NOT NULL DEFAULT 'eintrag', titel TEXT, text TEXT, media_url TEXT, tags TEXT, -- JSON Array gps_lat REAL, gps_lon REAL, is_milestone INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_diary_dog ON diary(dog_id, datum DESC); -- TAGEBUCH ↔ HUNDE (n:m — ein Eintrag kann mehrere Hunde betreffen) CREATE TABLE IF NOT EXISTS diary_dogs ( diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, PRIMARY KEY (diary_id, dog_id) ); CREATE INDEX IF NOT EXISTS idx_diary_dogs_dog ON diary_dogs(dog_id); -- GESUNDHEIT CREATE TABLE IF NOT EXISTS health ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, typ TEXT NOT NULL, -- impfung | entwurmung | tierarzt | medikament | gewicht bezeichnung TEXT NOT NULL, datum TEXT NOT NULL, naechstes TEXT, notiz TEXT, wert REAL, -- für Gewicht o.ä. einheit TEXT, erinnerung INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_health_dog ON health(dog_id, datum DESC); -- GIFTKÖDER-ALARM CREATE TABLE IF NOT EXISTS poison ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), lat REAL NOT NULL, lon REAL NOT NULL, beschreibung TEXT, typ TEXT DEFAULT 'unbekannt', foto_url TEXT, bestaetigt INTEGER NOT NULL DEFAULT 0, bestaetigt_von INTEGER, geloest INTEGER NOT NULL DEFAULT 0, expires_at TEXT NOT NULL, -- auto-expire nach 7 Tagen created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_poison_location ON poison(lat, lon); CREATE INDEX IF NOT EXISTS idx_poison_active ON poison(geloest, expires_at); -- ORTE (hundefreundliche Orte, Kotbeutelspender, etc.) CREATE TABLE IF NOT EXISTS places ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), name TEXT NOT NULL, typ TEXT NOT NULL, -- restaurant | shop | freilauf | kotbeutel | tierarzt | hundeschule lat REAL NOT NULL, lon REAL NOT NULL, adresse TEXT, website TEXT, hund_rein INTEGER, leine_pflicht INTEGER, wasser_fuer_hunde INTEGER, foto_url TEXT, bewertung REAL DEFAULT 0, anz_bewertungen INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_places_location ON places(lat, lon); CREATE INDEX IF NOT EXISTS idx_places_typ ON places(typ); -- ROUTEN CREATE TABLE IF NOT EXISTS routes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), name TEXT NOT NULL, beschreibung TEXT, gps_track TEXT NOT NULL, -- JSON Array von {lat, lon} distanz_km REAL, dauer_min INTEGER, schwierigkeit TEXT DEFAULT 'leicht', untergrund TEXT, -- wald | asphalt | wiese | mix schatten INTEGER, leine_empfohlen INTEGER, bewertung REAL DEFAULT 0, anz_bewertungen INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS route_walks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, route_id INTEGER REFERENCES routes(id) ON DELETE SET NULL, walked_km REAL NOT NULL, walked_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_route_walks_user ON route_walks(user_id); CREATE TABLE IF NOT EXISTS exercise_progress ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, exercise_id TEXT NOT NULL, status TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, exercise_id) ); CREATE INDEX IF NOT EXISTS idx_exercise_progress_user ON exercise_progress(user_id); CREATE TABLE IF NOT EXISTS training_plan_progress ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, item_key TEXT NOT NULL, checked INTEGER NOT NULL DEFAULT 1, checked_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (user_id, item_key) ); -- GASSI-TREFFEN CREATE TABLE IF NOT EXISTS walks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), titel TEXT NOT NULL, datum TEXT NOT NULL, uhrzeit TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL, ort_name TEXT, max_teilnehmer INTEGER DEFAULT 10, beschreibung TEXT, status TEXT NOT NULL DEFAULT 'offen', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- GASSI-TREFFEN TEILNEHMER CREATE TABLE IF NOT EXISTS walk_participants ( walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id), joined_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (walk_id, user_id) ); -- GASSI-TREFFEN ↔ HUNDE (n:m — Teilnehmer kann mehrere Hunde mitbringen) CREATE TABLE IF NOT EXISTS walk_participant_dogs ( walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, PRIMARY KEY (walk_id, user_id, dog_id) ); CREATE INDEX IF NOT EXISTS idx_wpd_dog ON walk_participant_dogs(dog_id); -- FORUM CREATE TABLE IF NOT EXISTS forum_threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), dog_id INTEGER REFERENCES dogs(id), titel TEXT NOT NULL, kategorie TEXT NOT NULL DEFAULT 'allgemein', rasse_tag TEXT, plz TEXT, views INTEGER NOT NULL DEFAULT 0, pinned INTEGER NOT NULL DEFAULT 0, locked INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_threads_kat ON forum_threads(kategorie, created_at DESC); CREATE TABLE IF NOT EXISTS forum_posts ( id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER NOT NULL REFERENCES forum_threads(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), edited_at TEXT ); -- PUSH SUBSCRIPTIONS (alternativ zu users.push_sub für mehrere Geräte) CREATE TABLE IF NOT EXISTS push_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, endpoint TEXT NOT NULL UNIQUE, p256dh TEXT NOT NULL, auth TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- PREMIUM-TRANSAKTIONEN (später für Zahlungsabwicklung) CREATE TABLE IF NOT EXISTS premium_orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), status TEXT NOT NULL DEFAULT 'pending', betrag_cent INTEGER NOT NULL, zahlungsart TEXT, valid_until TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- EVENTS (Hundeveranstaltungen) CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), titel TEXT NOT NULL, datum TEXT NOT NULL, -- YYYY-MM-DD uhrzeit TEXT, lat REAL, lon REAL, ort_name TEXT, typ TEXT NOT NULL DEFAULT 'sonstiges', beschreibung TEXT, link TEXT, status TEXT NOT NULL DEFAULT 'aktiv', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC); -- SITTING — Sitter-Profile CREATE TABLE IF NOT EXISTS sitters ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, beschreibung TEXT, preis_pro_tag REAL DEFAULT 0, max_hunde INTEGER DEFAULT 1, lat REAL, lon REAL, radius_km INTEGER DEFAULT 20, services TEXT DEFAULT '[]', -- JSON: ['tagesbetreuung','uebernachtung','gassi','hausbesuch'] aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- SITTING — Anfragen CREATE TABLE IF NOT EXISTS sitting_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id), -- Anfragender sitter_id INTEGER NOT NULL REFERENCES sitters(id), dog_ids TEXT DEFAULT '[]', -- JSON Array von TEXT NOT NULL, -- YYYY-MM-DD bis TEXT NOT NULL, nachricht TEXT, status TEXT NOT NULL DEFAULT 'offen', -- offen|angenommen|abgelehnt|abgebrochen created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- OSM POI-Cache (Mülleimer, Hundewiesen, Wasserstellen aus OpenStreetMap) CREATE TABLE IF NOT EXISTS osm_pois ( osm_id INTEGER NOT NULL, type TEXT NOT NULL, lat REAL NOT NULL, lon REAL NOT NULL, name TEXT, cached_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (osm_id, type) ); CREATE INDEX IF NOT EXISTS idx_osm_pois_loc ON osm_pois(type, lat, lon); -- VERLORENE HUNDE CREATE TABLE IF NOT EXISTS lost_dogs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, name TEXT NOT NULL, rasse TEXT, beschreibung TEXT NOT NULL, foto_url TEXT, lat REAL NOT NULL, lon REAL NOT NULL, is_active INTEGER NOT NULL DEFAULT 1, gefunden_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_lost_active ON lost_dogs(is_active, created_at DESC); -- OSM Tile-Cache: welche Kacheln wurden schon geladen? CREATE TABLE IF NOT EXISTS osm_tiles ( type TEXT NOT NULL, tile_key TEXT NOT NULL, -- "zoom_x_y" cached_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (type, tile_key) ); -- Community-Pins: von Nutzern gesetzte Karten-Marker CREATE TABLE IF NOT EXISTS user_map_pois ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, type TEXT NOT NULL, -- waste_basket | drinking_water | dog_park | sonstiges lat REAL NOT NULL, lon REAL NOT NULL, name TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_user_pois_loc ON user_map_pois(type, lat, lon); -- Meldungen: ungültige OSM- oder Community-Marker CREATE TABLE IF NOT EXISTS osm_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, osm_id INTEGER, -- gesetzt wenn OSM-Marker gemeldet user_poi_id INTEGER, -- gesetzt wenn Community-Marker gemeldet type TEXT, grund TEXT NOT NULL, -- existiert_nicht | falsche_position | spam | sonstiges created_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- TIERÄRZTE (user-level, nie löschen — Historien-Erhalt bei Umzug) CREATE TABLE IF NOT EXISTS tieraerzte ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, name TEXT NOT NULL, strasse TEXT, plz TEXT, ort TEXT, telefon TEXT, notfall_telefon TEXT, email TEXT, website TEXT, notizen TEXT, ist_notfallpraxis INTEGER NOT NULL DEFAULT 0, aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_tieraerzte_user ON tieraerzte(user_id, aktiv); """) # Migrations: neue Spalten zu bestehenden Tabellen hinzufügen (idempotent) _migrate(conn_factory=db) logger.info("Datenbank initialisiert.") def _migrate(conn_factory): """Fügt fehlende Spalten hinzu und führt Datenmigration durch (idempotent).""" migrations = [ # Giftköder: Auflösungs-Details für spätere KI-Analyse ("poison", "geloest_von", "INTEGER"), ("poison", "geloest_at", "TEXT"), ("poison", "geloest_grund", "TEXT"), # Gesundheit: erweiterte Felder je nach Eintragstyp ("health", "charge_nr", "TEXT"), ("health", "tierarzt_name", "TEXT"), ("health", "kosten", "REAL"), ("health", "diagnose", "TEXT"), ("health", "dosierung", "TEXT"), ("health", "haeufigkeit", "TEXT"), ("health", "aktiv", "INTEGER NOT NULL DEFAULT 1"), ("health", "bis_datum", "TEXT"), ("health", "schweregrad", "TEXT"), ("health", "reaktion", "TEXT"), ("health", "datei_url", "TEXT"), ("health", "datei_typ", "TEXT"), ("health", "tierarzt_id", "INTEGER"), # Tierärzte: Adresse aufgeteilt in Strasse/PLZ/Ort ("tieraerzte", "strasse", "TEXT"), ("tieraerzte", "plz", "TEXT"), ("tieraerzte", "ort", "TEXT"), ("tieraerzte", "opening_hours", "TEXT"), ("tieraerzte", "lat", "REAL"), ("tieraerzte", "lon", "REAL"), ("tieraerzte", "osm_id", "TEXT"), # Gesundheit: Erinnerungsintervall für wiederkehrende Einträge ("health", "intervall_tage", "INTEGER"), # Routen: neue Felder ("routes", "is_public", "INTEGER NOT NULL DEFAULT 1"), ("routes", "hunde_tauglichkeit", "TEXT"), ("routes", "foto_urls", "TEXT NOT NULL DEFAULT '[]'"), # OSM POIs: Kontaktdaten ("osm_pois", "opening_hours", "TEXT"), ("osm_pois", "phone", "TEXT"), ("osm_pois", "website", "TEXT"), ("osm_pois", "user_edited", "INTEGER NOT NULL DEFAULT 0"), # Forum: Threads brauchen text + antworten-Zähler ("forum_threads", "text", "TEXT NOT NULL DEFAULT ''"), ("forum_threads", "antworten", "INTEGER NOT NULL DEFAULT 0"), # Forum Sprint 11: erweiterte Thread-Felder ("forum_threads", "foto_urls", "TEXT"), ("forum_threads", "is_pinned", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "is_locked", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "is_deleted", "INTEGER NOT NULL DEFAULT 0"), ("forum_threads", "likes", "INTEGER NOT NULL DEFAULT 0"), # Forum Sprint 11: erweiterte Post-Felder ("forum_posts", "foto_urls", "TEXT"), ("forum_posts", "is_deleted", "INTEGER NOT NULL DEFAULT 0"), ("forum_posts", "likes", "INTEGER NOT NULL DEFAULT 0"), # Users: Moderator-Flag + Forum-Standort ("users", "is_moderator", "INTEGER NOT NULL DEFAULT 0"), ("users", "forum_lat", "REAL"), ("users", "forum_lon", "REAL"), ("users", "forum_show_location", "INTEGER NOT NULL DEFAULT 0"), # Events: Quelle + externe ID für gescrapte Events ("events", "quelle", "TEXT NOT NULL DEFAULT 'nutzer'"), ("events", "external_id", "TEXT"), # Admin: User-Sperre ("users", "is_banned", "INTEGER NOT NULL DEFAULT 0"), ("users", "ban_reason", "TEXT"), # WebCal: Kalender-Abo-Token ("users", "calendar_token", "TEXT"), # User-Profil-Felder ("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"), ("users", "verification_token", "TEXT"), ("users", "bio", "TEXT"), ("users", "wohnort", "TEXT"), ("users", "erfahrung", "TEXT"), ("users", "social_link", "TEXT"), ("users", "profil_sichtbarkeit", "TEXT NOT NULL DEFAULT 'public'"), ("users", "avatar_url", "TEXT"), ("places", "telefon", "TEXT"), # Chat: Foto-Versand + Read Receipts — nur wenn Tabelle existiert (wird später per CREATE IF NOT EXISTS angelegt) # Hinweis: Wird nach dem direct_messages-CREATE unten nochmal als separate Migration behandelt # Chat: Online-Indikator ("users", "last_seen", "TEXT"), # Foto-Editor: Zoom + Position ("dogs", "foto_zoom", "REAL NOT NULL DEFAULT 1.0"), ("dogs", "foto_offset_x", "REAL NOT NULL DEFAULT 0.0"), ("dogs", "foto_offset_y", "REAL NOT NULL DEFAULT 0.0"), # Tagebuch: Ortsname (POI/Adresse) ("diary", "location_name", "TEXT"), # Bewertungen: walks + sitters brauchen bewertung + anz_bewertungen ("walks", "bewertung", "REAL DEFAULT 0"), ("walks", "anz_bewertungen", "INTEGER DEFAULT 0"), ("sitters", "bewertung", "REAL DEFAULT 0"), ("sitters", "anz_bewertungen", "INTEGER DEFAULT 0"), # Tagebuch-Medien: Cover-Bild markieren ("diary_media", "is_cover", "INTEGER NOT NULL DEFAULT 0"), # Forum-Threads: optionaler Standort ("forum_threads", "thread_lat", "REAL"), ("forum_threads", "thread_lon", "REAL"), ("forum_threads", "thread_ort", "TEXT"), # Referral (idempotent, falls try/except-Block oben fehlgeschlagen ist) ("users", "referral_code", "TEXT"), ("users", "referred_by", "INTEGER"), # Routen: Original-Werte für Gamification (bleiben nach Kürzen erhalten) ("routes", "original_km", "REAL"), ("routes", "original_dauer_min","INTEGER"), # Gamification: Streaks ("users", "current_streak", "INTEGER NOT NULL DEFAULT 0"), ("users", "max_streak", "INTEGER NOT NULL DEFAULT 0"), ("users", "last_activity_date","TEXT"), # Social Media Manager ("users", "is_social_media", "INTEGER NOT NULL DEFAULT 0"), ("social_content", "coaching", "TEXT"), ("social_content", "media_url", "TEXT"), ("social_content", "category", "TEXT"), ("social_content", "exercise_id", "TEXT"), ("social_content", "post_url", "TEXT"), ("dogs", "rasse_id", "INTEGER"), # Pflege: Schere vs. Trimmen unterscheiden ("pflege_tipps", "fell_pflege_art", "TEXT"), # Wiki-Foto-Einreichungen: Bildrechte-Bestätigung ("wiki_foto_submissions", "rights_confirmed", "INTEGER NOT NULL DEFAULT 0"), # Tagebuch: Wetter + POI-Metadaten beim Eintrag ("diary", "weather_json", "TEXT"), ("diary", "poi_json", "TEXT"), # Notizen: Ort + Label + KI-Assistent User-Setting ("notes", "location_name", "TEXT"), ("notes", "parent_label", "TEXT"), ("users", "notes_ki_enabled", "INTEGER NOT NULL DEFAULT 1"), # Züchter-Rolle ("users", "breeder_status", "TEXT"), # Würfe: Verknüpfung mit Zuchtkartei-Hunden + Welfare ("litters", "vater_id", "INTEGER"), ("litters", "mutter_id", "INTEGER"), ("litters", "welfare_level", "TEXT"), ("litters", "welfare_acknowledged", "INTEGER NOT NULL DEFAULT 0"), # KI-Züchter-Features (pro User an/abschaltbar, außer Tierschutz) ("users", "ki_zucht_wurfankuendigung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_genetik", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_paarung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"), ("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"), # Partner-Code + Gründer-Lizenz ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"), ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"), ("users", "founder_number", "INTEGER"), ("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"), # Passwort-Zurücksetzen ("users", "password_reset_token", "TEXT"), ("users", "password_reset_expires", "TEXT"), ] with conn_factory() as conn: for table, column, col_type in migrations: existing = [ row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall() ] if column not in existing: conn.execute( f"ALTER TABLE {table} ADD COLUMN {column} {col_type}" ) logger.info(f"Migration: {table}.{column} hinzugefügt.") # Social Media Manager conn.executescript(""" CREATE TABLE IF NOT EXISTS social_content ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL DEFAULT (datetime('now')), created_by INTEGER REFERENCES users(id) ON DELETE SET NULL, platform TEXT NOT NULL DEFAULT 'both', format TEXT NOT NULL DEFAULT 'post', topic TEXT NOT NULL, caption TEXT, hashtags TEXT, visual_brief TEXT, image_prompt TEXT, canva_notes TEXT, script TEXT, hook TEXT, cta TEXT, unsplash_query TEXT, ai_score INTEGER, status TEXT NOT NULL DEFAULT 'idea', scheduled_at TEXT, published_at TEXT, source TEXT NOT NULL DEFAULT 'generated', breed_id INTEGER REFERENCES wiki_rassen(id) ON DELETE SET NULL, coaching TEXT, notes TEXT, media_url TEXT, category TEXT, exercise_id TEXT ); CREATE INDEX IF NOT EXISTS idx_social_content_status ON social_content(status); """) # Training-Übungen (für Social Media + Auswertung) conn.executescript(""" CREATE TABLE IF NOT EXISTS training_exercises ( id INTEGER PRIMARY KEY AUTOINCREMENT, exercise_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, kategorie TEXT NOT NULL, schwierigkeit TEXT, alter_ab TEXT, dauer TEXT, beschreibung TEXT, schritte TEXT, tipp TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_training_exercises_kat ON training_exercises(kategorie); """) # Pflege-Tipps (für Social Media + User-spezifisch) conn.executescript(""" CREATE TABLE IF NOT EXISTS pflege_tipps ( id INTEGER PRIMARY KEY AUTOINCREMENT, tipp_id TEXT NOT NULL UNIQUE, titel TEXT NOT NULL, kategorie TEXT NOT NULL, beschreibung TEXT, schritte TEXT, materialien TEXT, haeufigkeit TEXT, fell_typ TEXT, saison TEXT, rassengruppe TEXT, tipp TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_pflege_tipps_kat ON pflege_tipps(kategorie); """) # Knigge: Community-Votes conn.executescript(""" CREATE TABLE IF NOT EXISTS knigge_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, szenario_id TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, answer TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(szenario_id, user_id) ); """) # Forum Sprint 11: neue Tabellen conn.executescript(""" CREATE TABLE IF NOT EXISTS forum_likes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id INTEGER NOT NULL, UNIQUE(user_id, target_type, target_id) ); CREATE TABLE IF NOT EXISTS forum_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id INTEGER NOT NULL, grund TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), resolved INTEGER NOT NULL DEFAULT 0 ); """) # Wiki: Community-Berichte conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_berichte ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rasse TEXT NOT NULL, titel TEXT NOT NULL, text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_wiki_berichte_rasse ON wiki_berichte(rasse, created_at DESC); """) # Hunde-Filme: Katalog + Bewertungen + Hund des Monats conn.executescript(""" CREATE TABLE IF NOT EXISTS movies ( id TEXT PRIMARY KEY, titel TEXT NOT NULL, originaltitel TEXT, jahr INTEGER, genre TEXT, typ TEXT NOT NULL DEFAULT 'film', hund_rasse TEXT, stirbt_der_hund INTEGER NOT NULL DEFAULT 0, beschreibung TEXT, bild_emoji TEXT DEFAULT '🐾', imdb_rating REAL, streaming TEXT, sort_order INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_movies_typ ON movies(typ); CREATE INDEX IF NOT EXISTS idx_movies_jahr ON movies(jahr DESC); """) conn.executescript(""" CREATE TABLE IF NOT EXISTS movie_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, film_id TEXT NOT NULL, bewertung INTEGER NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, film_id) ); CREATE TABLE IF NOT EXISTS hund_des_monats_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, monat TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, monat) ); """) # Events: Unique-Index für externe IDs (idempotent) conn.executescript(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external ON events(external_id) WHERE external_id IS NOT NULL; """) # Users: Eindeutiger Benutzername (case-insensitive via COLLATE NOCASE) conn.executescript(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_users_name_unique ON users(name COLLATE NOCASE); """) # Wiki: User-Foto-Einreichungen conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_foto_submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rasse_id INTEGER NOT NULL REFERENCES wiki_rassen(id) ON DELETE CASCADE, foto_url TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT (datetime('now')), reviewed_by INTEGER REFERENCES users(id), reviewed_at TEXT, reject_reason TEXT ); CREATE INDEX IF NOT EXISTS idx_wfs_status ON wiki_foto_submissions(status, created_at DESC); """) # Freundschaften + Direktnachrichten conn.executescript(""" CREATE TABLE IF NOT EXISTS friendships ( id INTEGER PRIMARY KEY AUTOINCREMENT, requester_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, addressee_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT, UNIQUE(requester_id, addressee_id) ); CREATE INDEX IF NOT EXISTS idx_friendships_addressee ON friendships(addressee_id, status); CREATE INDEX IF NOT EXISTS idx_friendships_requester ON friendships(requester_id, status); CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_a_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_b_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, last_msg_at TEXT, a_read_at TEXT, b_read_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_a_id, user_b_id) ); CREATE TABLE IF NOT EXISTS direct_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, sender_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, text TEXT NOT NULL, is_deleted INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dm_conv ON direct_messages(conversation_id, created_at ASC); """) # Chat-Spalten sicher nach CREATE hinzufügen _dm_cols = [r[1] for r in conn.execute("PRAGMA table_info(direct_messages)").fetchall()] for _col, _typedef in [("media_url", "TEXT"), ("media_type", "TEXT"), ("read_at", "TEXT")]: if _col not in _dm_cols: conn.execute(f"ALTER TABLE direct_messages ADD COLUMN {_col} {_typedef}") # Wiki: Rassen-Datenbank (TheDogAPI) conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_rassen ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id INTEGER UNIQUE, name TEXT NOT NULL, name_de TEXT, gruppe TEXT, herkunft TEXT, temperament TEXT, gewicht_min_kg REAL, gewicht_max_kg REAL, groesse TEXT, lebensdauer TEXT, foto_url TEXT, bred_for TEXT, aktivitaet TEXT, wohnung_geeignet INTEGER DEFAULT 0, kinder_geeignet INTEGER DEFAULT 1, erfahrung TEXT DEFAULT 'anfaenger', slug TEXT UNIQUE, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_wiki_rassen_slug ON wiki_rassen(slug); CREATE INDEX IF NOT EXISTS idx_wiki_rassen_gruppe ON wiki_rassen(gruppe); """) # Datenmigration: diary_dogs für bestehende Einträge befüllen conn.execute(""" INSERT OR IGNORE INTO diary_dogs (diary_id, dog_id) SELECT id, dog_id FROM diary """) logger.info("Migration: diary_dogs Backfill abgeschlossen.") # Benachrichtigungs-Center conn.executescript(""" CREATE TABLE IF NOT EXISTS notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, type TEXT NOT NULL, title TEXT NOT NULL, body TEXT, data TEXT, read_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications(user_id, created_at DESC); """) # Hund-Teilen: Einladungssystem conn.executescript(""" CREATE TABLE IF NOT EXISTS dog_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, owner_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, shared_with_id INTEGER REFERENCES users(id) ON DELETE SET NULL, invite_token TEXT NOT NULL UNIQUE, role TEXT NOT NULL DEFAULT 'editor', accepted_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dog_shares_dog ON dog_shares(dog_id); CREATE INDEX IF NOT EXISTS idx_dog_shares_token ON dog_shares(invite_token); CREATE INDEX IF NOT EXISTS idx_dog_shares_user ON dog_shares(shared_with_id); """) logger.info("Migration: dog_shares Tabelle bereit.") # Event-RSVP conn.executescript(""" CREATE TABLE IF NOT EXISTS event_rsvp ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, user_id INTEGER NOT NULL, status TEXT DEFAULT 'going', created_at TEXT DEFAULT (datetime('now')), UNIQUE(event_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_event_rsvp_event ON event_rsvp(event_id); """) logger.info("Migration: event_rsvp Tabelle bereit.") # Events: user_id NOT NULL Constraint entfernen (für Scheduler-Imports ohne User) _ev_cols = {r[1]: r[3] for r in conn.execute("PRAGMA table_info(events)").fetchall()} if _ev_cols.get("user_id") == 1: conn.executescript(""" CREATE TABLE IF NOT EXISTS events_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, titel TEXT NOT NULL, datum TEXT NOT NULL, uhrzeit TEXT, lat REAL, lon REAL, ort_name TEXT, typ TEXT NOT NULL DEFAULT 'sonstiges', beschreibung TEXT, link TEXT, status TEXT NOT NULL DEFAULT 'aktiv', created_at TEXT NOT NULL DEFAULT (datetime('now')), quelle TEXT NOT NULL DEFAULT 'nutzer', external_id TEXT ); INSERT OR IGNORE INTO events_new SELECT * FROM events; DROP TABLE events; ALTER TABLE events_new RENAME TO events; CREATE INDEX IF NOT EXISTS idx_events_datum ON events(datum ASC); CREATE UNIQUE INDEX IF NOT EXISTS idx_events_external ON events(external_id) WHERE external_id IS NOT NULL; """) logger.info("Migration: events.user_id NOT NULL Constraint entfernt.") # Service-Angebote: Sitting + Walks Matching conn.executescript(""" CREATE TABLE IF NOT EXISTS service_offers ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, type TEXT NOT NULL, -- 'sitting' oder 'walks' beschreibung TEXT, preis_pro_tag REAL, lat REAL, lon REAL, radius_km INTEGER DEFAULT 10, aktiv INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_service_offers_type ON service_offers(type, aktiv); CREATE INDEX IF NOT EXISTS idx_service_offers_user ON service_offers(user_id, type); """) # Ratings — einheitliches Bewertungssystem conn.executescript(""" CREATE TABLE IF NOT EXISTS ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_type TEXT NOT NULL, target_id INTEGER NOT NULL, stars INTEGER NOT NULL, kommentar TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, target_type, target_id) ); CREATE INDEX IF NOT EXISTS idx_ratings_target ON ratings(target_type, target_id); """) logger.info("Migration: ratings Tabelle bereit.") # Tagebuch: mehrere Mediendateien pro Eintrag conn.executescript(""" CREATE TABLE IF NOT EXISTS diary_media ( id INTEGER PRIMARY KEY AUTOINCREMENT, diary_id INTEGER NOT NULL REFERENCES diary(id) ON DELETE CASCADE, url TEXT NOT NULL, media_type TEXT NOT NULL DEFAULT 'image', sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_diary_media_entry ON diary_media(diary_id, sort_order); """) logger.info("Migration: diary_media Tabelle bereit.") # Gesundheit: mehrere Mediendateien pro Eintrag conn.executescript(""" CREATE TABLE IF NOT EXISTS health_media ( id INTEGER PRIMARY KEY AUTOINCREMENT, health_id INTEGER NOT NULL REFERENCES health(id) ON DELETE CASCADE, url TEXT NOT NULL, media_type TEXT NOT NULL DEFAULT 'image', sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_health_media_entry ON health_media(health_id, sort_order); """) logger.info("Migration: health_media Tabelle bereit.") # Gasthund-Zugang: Sitter darf temporär für Hund schreiben conn.executescript(""" CREATE TABLE IF NOT EXISTS sitting_subscriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, owner_id INTEGER NOT NULL REFERENCES users(id), sitter_id INTEGER NOT NULL REFERENCES users(id), valid_until TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), UNIQUE(dog_id, sitter_id) ); CREATE INDEX IF NOT EXISTS idx_sitting_sub_sitter ON sitting_subscriptions(sitter_id, valid_until); CREATE INDEX IF NOT EXISTS idx_sitting_sub_owner ON sitting_subscriptions(owner_id); """) logger.info("Migration: sitting_subscriptions Tabelle bereit.") # Walk-Einladungen (RSVP) conn.executescript(""" CREATE TABLE IF NOT EXISTS walk_invitations ( id INTEGER PRIMARY KEY AUTOINCREMENT, walk_id INTEGER NOT NULL REFERENCES walks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'invited', invited_at TEXT NOT NULL DEFAULT (datetime('now')), responded_at TEXT, UNIQUE(walk_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_walk_inv_walk ON walk_invitations(walk_id); CREATE INDEX IF NOT EXISTS idx_walk_inv_user ON walk_invitations(user_id, status); """) logger.info("Migration: walk_invitations Tabelle bereit.") # Referral-Code für jeden User (einmalig generiert) try: conn.execute("ALTER TABLE users ADD COLUMN referral_code TEXT UNIQUE") conn.execute("ALTER TABLE users ADD COLUMN referred_by INTEGER REFERENCES users(id)") # Bestehende User bekommen einen Code import secrets, string rows = conn.execute("SELECT id FROM users WHERE referral_code IS NULL").fetchall() for r in rows: code = ''.join(secrets.choice(string.ascii_uppercase + string.digits) for _ in range(8)) conn.execute("UPDATE users SET referral_code=? WHERE id=?", (code, r['id'])) logger.info("Migration: referral_code + referred_by zu users hinzugefügt.") except Exception: pass # Gamification: Badge-Tabelle conn.executescript(""" CREATE TABLE IF NOT EXISTS user_badges ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, badge_id TEXT NOT NULL, earned_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(user_id, badge_id) ); CREATE INDEX IF NOT EXISTS idx_user_badges_user ON user_badges(user_id); """) logger.info("Migration: user_badges Tabelle bereit.") # Username / Real-Name Trennung try: conn.execute("ALTER TABLE users ADD COLUMN real_name TEXT") # Bestehende User: Leerzeichen im name → name enthält echten Namen → in real_name verschieben rows = conn.execute("SELECT id, name FROM users WHERE name LIKE '% %'").fetchall() for r in rows: first_word = r['name'].split()[0] conn.execute("UPDATE users SET real_name=?, name=? WHERE id=?", (r['name'], first_word, r['id'])) logger.info("Migration: real_name Spalte hinzugefügt, %d User migriert.", len(rows)) except Exception: pass # Wiki: Rassen-Anreicherung for col, typedef in [ ("beschreibung", "TEXT"), ("vorkommen_de", "TEXT"), ("wikipedia_url_de","TEXT"), ("ki_enriched", "INTEGER DEFAULT 0"), ("ki_model", "TEXT"), ("ki_source", "TEXT"), ]: try: conn.execute(f"ALTER TABLE wiki_rassen ADD COLUMN {col} {typedef}") except Exception: pass logger.info("Migration: wiki_rassen Anreicherungs-Felder bereit.") # Moderation-Logging: resolved_by/at für forum_reports, verified_by/at/reject für wiki_zuchter for table, col, typedef in [ ("forum_reports", "resolved_by", "INTEGER"), ("forum_reports", "resolved_at", "TEXT"), ("wiki_zuchter", "verified_by", "INTEGER"), ("wiki_zuchter", "verified_at", "TEXT"), ("wiki_zuchter", "reject_reason", "TEXT"), ]: try: conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}") except Exception: pass # Wiki: Züchter-Verzeichnis conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_zuchter ( id INTEGER PRIMARY KEY AUTOINCREMENT, rasse_slug TEXT NOT NULL, name TEXT NOT NULL, zwingername TEXT, ort TEXT, plz TEXT, bundesland TEXT, vdh_mitglied INTEGER DEFAULT 0, website TEXT, telefon TEXT, beschreibung TEXT, verified INTEGER DEFAULT 0, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_wiki_zuchter_rasse ON wiki_zuchter(rasse_slug, verified); """) logger.info("Migration: wiki_zuchter Tabelle bereit.") # Wiki: Rasse-Interesse ("So einen hab ich" / "Interessiert mich") conn.executescript(""" CREATE TABLE IF NOT EXISTS wiki_breed_interest ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, rasse_slug TEXT NOT NULL, typ TEXT NOT NULL DEFAULT 'hat', created_at TEXT DEFAULT (datetime('now')), UNIQUE(user_id, rasse_slug) ); CREATE INDEX IF NOT EXISTS idx_wbi_rasse ON wiki_breed_interest(rasse_slug, typ); CREATE INDEX IF NOT EXISTS idx_wbi_user ON wiki_breed_interest(user_id); """) logger.info("Migration: wiki_breed_interest Tabelle bereit.") # Training: Session-Protokoll conn.executescript(""" CREATE TABLE IF NOT EXISTS training_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, exercise_id TEXT NOT NULL, exercise_name TEXT NOT NULL, datum TEXT NOT NULL DEFAULT (date('now')), wiederholungen INTEGER DEFAULT 1, erfolgsquote INTEGER DEFAULT 50, hund_stimmung TEXT DEFAULT 'aufmerksam', zufriedenheit INTEGER DEFAULT 3, notiz TEXT, ist_top INTEGER DEFAULT 0, diary_entry_id INTEGER REFERENCES diary(id) ON DELETE SET NULL, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_ts_user_dog ON training_sessions(user_id, dog_id, datum DESC); CREATE INDEX IF NOT EXISTS idx_ts_exercise ON training_sessions(exercise_id, user_id); """) logger.info("Migration: training_sessions Tabelle bereit.") # Training: KI-Feedback-Cache conn.executescript(""" CREATE TABLE IF NOT EXISTS training_ki_cache ( dog_id INTEGER PRIMARY KEY, feedback TEXT NOT NULL, generated_at TEXT NOT NULL DEFAULT (datetime('now')) ); """) logger.info("Migration: training_ki_cache Tabelle bereit.") # Fortschritts-Lober: wöchentliche Lob-Karten conn.executescript(""" CREATE TABLE IF NOT EXISTS weekly_praise ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, week_key TEXT NOT NULL, -- ISO-Woche, z.B. "2026-W17" praise_text TEXT NOT NULL, stats_json TEXT, -- JSON der gesammelten Stats (für Debugging) generated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(dog_id, week_key) ); CREATE INDEX IF NOT EXISTS idx_wp_dog_week ON weekly_praise(dog_id, week_key DESC); """) logger.info("Migration: weekly_praise Tabelle bereit.") # Push: Standort-Filter (last_lat/lon für geo-basierte Alerts) for col in ["last_lat REAL", "last_lon REAL"]: try: conn.execute(f"ALTER TABLE push_subscriptions ADD COLUMN {col}") except Exception: pass logger.info("Migration: push_subscriptions last_lat/lon bereit.") # Routen: Geschwindigkeits-Validierung try: conn.execute("ALTER TABLE routes ADD COLUMN is_valid INTEGER NOT NULL DEFAULT 1") except Exception: pass logger.info("Migration: routes.is_valid bereit.") # ki_daily_calls: source-Spalte + PK auf (user_id, date, source) erweitern ki_cols = {r[1] for r in conn.execute("PRAGMA table_info(ki_daily_calls)").fetchall()} if "source" not in ki_cols: conn.executescript(""" ALTER TABLE ki_daily_calls RENAME TO ki_daily_calls_old; CREATE TABLE ki_daily_calls ( user_id INTEGER NOT NULL, date TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 0, source TEXT NOT NULL DEFAULT 'cloud', PRIMARY KEY (user_id, date, source) ); INSERT INTO ki_daily_calls (user_id, date, count, source) SELECT user_id, date, count, 'cloud' FROM ki_daily_calls_old; DROP TABLE ki_daily_calls_old; CREATE INDEX IF NOT EXISTS idx_ki_daily_source ON ki_daily_calls(date, source); """) logger.info("Migration: ki_daily_calls.source bereit.") # Notizen: generische polymorphe Notiz-Tabelle conn.executescript(""" CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, parent_type TEXT NOT NULL, parent_id INTEGER NOT NULL, text TEXT NOT NULL DEFAULT '', meta_json TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_notes_parent ON notes(parent_type, parent_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id, created_at DESC); """) logger.info("Migration: notes Tabelle bereit.") conn.execute(""" CREATE TABLE IF NOT EXISTS ki_health_reports ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, bericht TEXT NOT NULL, erstellt_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_ki_health_reports_dog ON ki_health_reports(dog_id, erstellt_at DESC) """) logger.info("Migration: ki_health_reports Tabelle bereit.") conn.execute(""" CREATE TABLE IF NOT EXISTS osm_poi_edits ( id INTEGER PRIMARY KEY AUTOINCREMENT, osm_id TEXT NOT NULL, poi_name TEXT NOT NULL, field TEXT NOT NULL DEFAULT 'opening_hours', old_value TEXT, new_value TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending', mod_id INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now')), resolved_at TEXT ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_osm_poi_edits_status ON osm_poi_edits(status, created_at DESC) """) logger.info("Migration: osm_poi_edits Tabelle bereit.") # Einmalige Datenmigration: dogs.gewicht_kg mit aktuellem Gesundheits-Gewicht synchronisieren conn.execute(""" UPDATE dogs SET gewicht_kg = ( SELECT wert FROM health WHERE health.dog_id = dogs.id AND health.typ = 'gewicht' AND health.wert IS NOT NULL ORDER BY health.datum DESC, health.id DESC LIMIT 1 ) WHERE EXISTS ( SELECT 1 FROM health WHERE health.dog_id = dogs.id AND health.typ = 'gewicht' AND health.wert IS NOT NULL ) """) logger.info("Migration: dogs.gewicht_kg aus health synchronisiert.") # Performance-Indizes für häufige Queries conn.execute("CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL") conn.execute("CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)") conn.execute("CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)") conn.execute("CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)") conn.execute("CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)") logger.info("Migration: Performance-Indizes bereit.") # Züchter-Tabellen conn.executescript(""" CREATE TABLE IF NOT EXISTS breeder_profiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE, zwingername TEXT NOT NULL, rasse_text TEXT NOT NULL, verein TEXT NOT NULL, vdh_mitglied INTEGER NOT NULL DEFAULT 0, stadt TEXT NOT NULL, website TEXT, beschreibung TEXT, location_lat REAL, location_lng REAL, verified_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS breeder_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dokument_typ TEXT NOT NULL, file_path TEXT NOT NULL, uploaded_at TEXT NOT NULL DEFAULT (datetime('now')) ); """) logger.info("Migration: breeder_profiles + breeder_documents bereit.") # Würfe + Welpen conn.executescript(""" CREATE TABLE IF NOT EXISTS litters ( id INTEGER PRIMARY KEY AUTOINCREMENT, breeder_id INTEGER NOT NULL REFERENCES breeder_profiles(id) ON DELETE CASCADE, vater_name TEXT, mutter_name TEXT, geburt_datum TEXT, erwartetes_datum TEXT, welpen_gesamt INTEGER, welpen_verfuegbar INTEGER, beschreibung TEXT, gesundheitstests TEXT, preis_spanne TEXT, status TEXT NOT NULL DEFAULT 'geplant', sichtbar INTEGER NOT NULL DEFAULT 1, sichtbar_bis TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_litters_breeder ON litters(breeder_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_litters_status ON litters(status, sichtbar); CREATE TABLE IF NOT EXISTS puppies ( id INTEGER PRIMARY KEY AUTOINCREMENT, wurf_id INTEGER NOT NULL REFERENCES litters(id) ON DELETE CASCADE, name TEXT, geschlecht TEXT, farbe TEXT, chip_nr TEXT, geburtsgewicht REAL, status TEXT NOT NULL DEFAULT 'verfuegbar', status_sichtbar INTEGER NOT NULL DEFAULT 1, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_puppies_wurf ON puppies(wurf_id); CREATE TABLE IF NOT EXISTS puppy_weights ( id INTEGER PRIMARY KEY AUTOINCREMENT, welpe_id INTEGER NOT NULL REFERENCES puppies(id) ON DELETE CASCADE, gewicht_g REAL NOT NULL, gemessen_am TEXT NOT NULL ); """) logger.info("Migration: litters + puppies + puppy_weights bereit.") # Züchter-Fotos conn.executescript(""" CREATE TABLE IF NOT EXISTS breeder_photos ( id INTEGER PRIMARY KEY AUTOINCREMENT, breeder_id INTEGER NOT NULL REFERENCES breeder_profiles(id) ON DELETE CASCADE, entity_type TEXT NOT NULL, entity_id INTEGER NOT NULL, file_path TEXT NOT NULL, thumbnail_path TEXT, caption TEXT, is_primary INTEGER NOT NULL DEFAULT 0, visibility TEXT NOT NULL DEFAULT 'public', sort_order INTEGER NOT NULL DEFAULT 0, uploaded_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_breeder_photos_entity ON breeder_photos(entity_type, entity_id); """) logger.info("Migration: breeder_photos bereit.") # Züchter-Hunde-Stammdaten (Stammbaum, Gesundheit, Genetik, Titel) conn.executescript(""" CREATE TABLE IF NOT EXISTS zucht_hunde ( id INTEGER PRIMARY KEY AUTOINCREMENT, breeder_id INTEGER REFERENCES breeder_profiles(id) ON DELETE SET NULL, name TEXT NOT NULL, rufname TEXT, geschlecht TEXT, geburtsdatum TEXT, sterbedatum TEXT, chip_nr TEXT, taetowiernummer TEXT, zuchtbuchnummer TEXT, farbe TEXT, vater_id INTEGER REFERENCES zucht_hunde(id), mutter_id INTEGER REFERENCES zucht_hunde(id), zuechter_name TEXT, eigentuemer_name TEXT, is_public INTEGER NOT NULL DEFAULT 1, notiz TEXT, foto_url TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_zucht_hunde_breeder ON zucht_hunde(breeder_id); CREATE INDEX IF NOT EXISTS idx_zucht_hunde_eltern ON zucht_hunde(vater_id, mutter_id); CREATE TABLE IF NOT EXISTS dog_health_tests ( id INTEGER PRIMARY KEY AUTOINCREMENT, hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, test_typ TEXT NOT NULL, test_name TEXT, ergebnis TEXT NOT NULL, untersuch_am TEXT NOT NULL, gueltig_bis TEXT, untersucher TEXT, labor TEXT, zertifikat_nr TEXT, is_public INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dog_health_tests_hund ON dog_health_tests(hund_id); CREATE TABLE IF NOT EXISTS dog_genetic_tests ( id INTEGER PRIMARY KEY AUTOINCREMENT, hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, marker_name TEXT NOT NULL, marker_kategorie TEXT, genotyp TEXT, ergebnis_klasse TEXT, getestet_am TEXT NOT NULL, labor TEXT, zertifikat_nr TEXT, is_public INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dog_genetic_tests_hund ON dog_genetic_tests(hund_id); CREATE TABLE IF NOT EXISTS dog_titles ( id INTEGER PRIMARY KEY AUTOINCREMENT, hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, titel_typ TEXT NOT NULL, titel_name TEXT NOT NULL, verliehen_am TEXT NOT NULL, ort TEXT, richter TEXT, ausstellung TEXT, formwert TEXT, is_public INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dog_titles_hund ON dog_titles(hund_id); """) logger.info("Migration: zucht_hunde + dog_health_tests + dog_genetic_tests + dog_titles bereit.") # Läufigkeit: Deckdatum + Wurftermin existing_h = [row[1] for row in conn.execute("PRAGMA table_info(health)").fetchall()] for col, typedef in [("deckdatum", "TEXT"), ("wurftermin", "TEXT")]: if col not in existing_h: conn.execute(f"ALTER TABLE health ADD COLUMN {col} {typedef}") logger.info(f"Migration: health.{col} hinzugefügt.") # Route-Suggest Rate-Limiting conn.executescript(""" CREATE TABLE IF NOT EXISTS route_suggest_usage ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, week TEXT NOT NULL, count INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (user_id, week) ); """) logger.info("Migration: route_suggest_usage Tabelle bereit.") # ORS tägliche Gesamtaufrufe (für Admin-Dashboard) existing_ors = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='ors_daily_total'").fetchone() if not existing_ors: conn.executescript(""" CREATE TABLE ors_daily_total ( date TEXT PRIMARY KEY, count INTEGER NOT NULL DEFAULT 0 ); """) logger.info("Migration: ors_daily_total erstellt.") # Fehlende Legacy-JS-Übungen nachträglich einfügen import json as _json _legacy = [ ('trick_platz_decke', 'Platz auf Decke / Matte', 'Trick', 'leicht', '5-10 min', 'Der Hund geht auf Kommando auf seine Decke oder Matte und legt sich hin.', _json.dumps(['Decke auslegen', 'Hund mit Leckerli auf die Matte locken', 'Markerwort + Leckerli wenn er drauf steht', 'Platz-Signal einbauen', 'Distanz schrittweise erhöhen']), 'Die Matte zum positiven Ort machen, nie erzwingen.'), ('trick_suchspiel', 'Suchspiel / Nasenarbeit', 'Trick', 'leicht', '10-15 min', 'Der Hund sucht versteckte Leckerlis per Nase — mentale Auslastung pur.', _json.dumps(['Leckerli offen zeigen', 'Hund kurz festhalten, Leckerli verstecken', 'Freigabe-Wort und suchen lassen', 'Schwierigkeit langsam steigern']), 'Nasenarbeit erschöpft mehr als körperliche Aktivität.'), ('pb_nicht_springen2', 'Nicht springen / Begrüßung', 'Problemverhalten', 'leicht', '5-10 min', 'Der Hund begrüßt Menschen ohne hochzuspringen.', _json.dumps(['Hund ignorieren wenn er springt (Rücken zudrehen)', 'Erst belohnen wenn alle vier Pfoten am Boden', 'Gäste in die Übung einbeziehen', 'Konsequent sein']), 'Alle Familienmitglieder müssen gleich reagieren.'), ('pb_leinenfuehrigkeit', 'Leinenführigkeit — Nicht ziehen', 'Problemverhalten', 'mittel', '10-20 min', 'Der Hund läuft locker an der Leine ohne zu ziehen.', _json.dumps(['Bei Leinenzug stehen bleiben oder Richtung wechseln', 'Lockere Leine immer belohnen', 'Kurzstrecken üben, nicht lange Spaziergänge']), 'Dauert Wochen konsequentes Üben — Geduld.'), ('pb_bellen_klaffen', 'Bellen / Kläffen', 'Problemverhalten', 'mittel', '10 min', 'Übermäßiges Bellen auf Kommando reduzieren.', _json.dumps(['Auslöser identifizieren', '"Ruhig"-Kommando einführen wenn Pause entsteht', 'Ruhige Pausen direkt belohnen']), 'Bellen nie durch Schreien lösen — das wirkt wie Mitmachen.'), ('pb_enttriggern', 'Enttriggern / Desensibilisierung', 'Problemverhalten', 'schwer', '15-30 min', 'Den Hund langsam an angstauslösende Reize gewöhnen — für reaktive Hunde.', _json.dumps(['Angst-Auslöser in großer Distanz zeigen', 'Bei Ruhe belohnen (kein Stress sichtbar)', 'Distanz sehr langsam verringern', 'Niemals erzwingen']), 'Immer unterhalb der Stressschwelle bleiben.'), ] for ex in _legacy: if not conn.execute('SELECT 1 FROM training_exercises WHERE exercise_id=?', (ex[0],)).fetchone(): conn.execute( 'INSERT INTO training_exercises (exercise_id,name,kategorie,schwierigkeit,dauer,beschreibung,schritte,tipp) VALUES (?,?,?,?,?,?,?,?)', ex ) logger.info(f"Migration: Übung '{ex[1]}' eingefügt.") # Gespeicherte KI-Jahresberichte für Züchter conn.executescript(""" CREATE TABLE IF NOT EXISTS breeder_jahresberichte ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, breeder_id INTEGER NOT NULL, jahr INTEGER NOT NULL, text TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_bj_user ON breeder_jahresberichte(user_id, jahr DESC); """) # Partner-Codes (Influencer-Kooperationen + Gründer-Lizenz) try: conn.executescript(""" CREATE TABLE IF NOT EXISTS partner_codes ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL UNIQUE COLLATE NOCASE, label TEXT NOT NULL, grants_founder INTEGER NOT NULL DEFAULT 1, max_uses INTEGER DEFAULT NULL, uses INTEGER NOT NULL DEFAULT 0, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_partner_codes_code ON partner_codes(code); """) logger.info("Migration: partner_codes Tabelle bereit.") except Exception as e: logger.warning(f"Migration partner_codes: {e}") # Outreach-Log (Admin-E-Mail-Versand) try: conn.executescript(""" CREATE TABLE IF NOT EXISTS outreach_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, sent_by INTEGER REFERENCES users(id), recipient TEXT NOT NULL, subject TEXT NOT NULL, body TEXT NOT NULL, sent_at TEXT NOT NULL DEFAULT (datetime('now')) ); """) except Exception as e: logger.warning(f"Migration outreach_log: {e}") # E-Mail-Vorlagen (DB-gespeichert, CRUD über Admin) try: conn.execute(""" CREATE TABLE IF NOT EXISTS email_templates ( id INTEGER PRIMARY KEY AUTOINCREMENT, key TEXT UNIQUE NOT NULL, label TEXT NOT NULL, subject TEXT NOT NULL, body TEXT NOT NULL, from_account TEXT NOT NULL DEFAULT 'partner', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT ) """) # Startwert-Vorlage einspielen wenn Tabelle noch leer count = conn.execute("SELECT COUNT(*) FROM email_templates").fetchone()[0] if count == 0: conn.execute(""" INSERT INTO email_templates (key, label, subject, body, from_account) VALUES ('influencer_de', 'Influencer-Ansprache (DE)', 'Ban Yaro — 100 Gründer-Plätze, einer davon für deine Community', 'Hallo {name},\n\nich bin René und habe Ban Yaro gebaut — eine Hunde-App für Tagebuch, Gesundheit, Giftköder-Alarm und Community. Kostenlos, ohne App Store, direkt als PWA.\n\nIch kontaktiere gerade einige Influencer aus der deutschen Hunde-Community mit einem konkreten Angebot:\n\nWas deine Follower bekommen: Wer sich mit deinem persönlichen Code registriert, sichert sich einen der 100 Gründer-Plätze — eine nummerierte Badge ("Gründer #42") die dauerhaft im Profil sichtbar ist. Diese 100 Plätze gibt es genau einmal.\n\nWas du bekommst: Partner-Badge in der App, eigener Code, öffentliches Ranking wer die meisten Gründer bringt.\n\nKein Geld, kein verpflichtender Post — aber eine echte Exklusivität die du deiner Community geben kannst.\n\nAlle Infos: https://banyaro.app/partner\n\nWenn dich das interessiert, antworte einfach kurz — ich richte deinen Code binnen 24h ein.\n\nViele Grüße,\nRené\nbanyaro.app', 'partner') """) except Exception as e: logger.warning(f"Migration email_templates: {e}") # from_account-Spalte in outreach_log nachträglich hinzufügen existing_ol = [row[1] for row in conn.execute("PRAGMA table_info(outreach_log)").fetchall()] if 'from_account' not in existing_ol: conn.execute("ALTER TABLE outreach_log ADD COLUMN from_account TEXT DEFAULT 'partner'") # Job-Bewerbungen + Luna-Probezugang conn.executescript(""" CREATE TABLE IF NOT EXISTS job_applications ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, name TEXT NOT NULL, email TEXT NOT NULL, dog_name TEXT, dog_rasse TEXT, social_handle TEXT, motivation TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', admin_note TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), reviewed_at TEXT ); CREATE INDEX IF NOT EXISTS idx_job_apps_status ON job_applications(status, created_at DESC); CREATE TABLE IF NOT EXISTS job_application_docs ( id INTEGER PRIMARY KEY AUTOINCREMENT, application_id INTEGER NOT NULL REFERENCES job_applications(id) ON DELETE CASCADE, filename TEXT NOT NULL, file_path TEXT NOT NULL, uploaded_at TEXT NOT NULL DEFAULT (datetime('now')) ); """) existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] if 'luna_trial_until' not in existing_u: conn.execute("ALTER TABLE users ADD COLUMN luna_trial_until TEXT") # js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()] if 'js_exercise_id' not in existing_te: import re as _re _CAT_TO_TAB = { 'Grundkommando': 'grundkommandos', 'Trick': 'tricks', 'Problemverhalten': 'problemverhalten', 'Mentale Auslastung': 'mentale-auslastung', 'Hundesport': 'hundesport', 'Körperpflege': 'koerperpflege', 'Welpe Basics': 'welpe-basics', 'Grundlagen': 'grundlagen', } conn.execute("ALTER TABLE training_exercises ADD COLUMN js_exercise_id TEXT") # "Fuß (Leinenführigkeit)" → "Fuß" (damit es mit alten exercise_progress-Einträgen matcht) conn.execute("UPDATE training_exercises SET name='Fuß' WHERE exercise_id='gk_fuss'") rows = conn.execute("SELECT id, name, kategorie FROM training_exercises").fetchall() for row in rows: tab = _CAT_TO_TAB.get(row['kategorie'], row['kategorie'].lower().replace(' ', '-')) js_id = tab + '_' + _re.sub(r'[\s/]+', '_', row['name']) conn.execute("UPDATE training_exercises SET js_exercise_id=? WHERE id=?", (js_id, row['id'])) conn.execute("CREATE INDEX IF NOT EXISTS idx_te_js_id ON training_exercises(js_exercise_id)") logger.info("Migration: training_exercises.js_exercise_id hinzugefügt, 'Fuß' bereinigt.") # Hund des Monats — dauerhafte Gewinner-Tabelle conn.executescript(""" CREATE TABLE IF NOT EXISTS hund_des_monats_wins ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, monat TEXT NOT NULL, stimmen INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(dog_id, monat) ); CREATE INDEX IF NOT EXISTS idx_hdm_wins_dog ON hund_des_monats_wins(dog_id); """) # Trainings-Streak-Tabelle conn.execute(""" CREATE TABLE IF NOT EXISTS training_streaks ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, current_streak INTEGER NOT NULL DEFAULT 0, longest_streak INTEGER NOT NULL DEFAULT 0, last_training_date TEXT, UNIQUE(user_id, dog_id) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_streaks_user ON training_streaks(user_id)") # Ausgaben-Tracker conn.executescript(""" CREATE TABLE IF NOT EXISTS expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, kategorie TEXT NOT NULL, betrag REAL NOT NULL, datum TEXT NOT NULL, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_expenses_user ON expenses(user_id, datum DESC); """) # KI-Tierarztfragen Rate-Limit-Log conn.execute(""" CREATE TABLE IF NOT EXISTS ki_tierarzt_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) # KI Rassen-Erkennungs-Log (Rate-Limit: 10/Tag pro User) conn.execute(""" CREATE TABLE IF NOT EXISTS ki_rasse_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_ki_rasse_log_user ON ki_rasse_log(user_id, created_at DESC) """) # feed_recalls — Rückruf-Alarm für Tierfutter (RASFF) conn.execute(""" CREATE TABLE IF NOT EXISTS feed_recalls ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT NOT NULL UNIQUE, titel TEXT NOT NULL, produkt TEXT, gefahr TEXT, herkunft TEXT, datum TEXT NOT NULL, quelle TEXT NOT NULL DEFAULT 'rasff', url TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_recalls_datum ON feed_recalls(datum DESC)") # Adoption-Cache conn.execute(""" CREATE TABLE IF NOT EXISTS adoption_cache ( id INTEGER PRIMARY KEY AUTOINCREMENT, external_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, rasse TEXT, alter_jahre REAL, geschlecht TEXT, foto_url TEXT, tierheim TEXT, tierheim_plz TEXT, tierheim_lat REAL, tierheim_lon REAL, adoptions_url TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')), expires_at TEXT NOT NULL ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS community_adoption ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, name TEXT NOT NULL, rasse TEXT, alter_jahre REAL, geschlecht TEXT, foto_url TEXT, beschreibung TEXT NOT NULL, gruende TEXT, ort TEXT, plz TEXT, lat REAL, lon REAL, status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS community_adoption_interest ( id INTEGER PRIMARY KEY AUTOINCREMENT, listing_id INTEGER NOT NULL REFERENCES community_adoption(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, nachricht TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(listing_id, user_id) ) """) # ---- Wetter-Log (historische Vorhersage-Daten) ---- conn.execute(""" CREATE TABLE IF NOT EXISTS weather_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, logged_at TEXT NOT NULL DEFAULT (datetime('now')), date TEXT NOT NULL, lat_r REAL NOT NULL, lon_r REAL NOT NULL, temp_max REAL, temp_min REAL, feels_max REAL, precip_prob INTEGER, precip_sum REAL, wind_kmh REAL, wind_dir TEXT, uv_index REAL, weathercode INTEGER, weatherdesc TEXT, sunrise TEXT, sunset TEXT, asphalt_temp REAL, asphalt_warn TEXT, zecken TEXT, pollen_erle INTEGER, pollen_birke INTEGER, pollen_graeser INTEGER, pollen_beifuss INTEGER, pollen_ambrosia INTEGER, forecast_json TEXT, UNIQUE(date, lat_r, lon_r) ) """) # ---- Favoriten-Tierarzt + Gesundheitsdokumente ---- conn.execute(""" CREATE TABLE IF NOT EXISTS favorite_vets ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, vet_id INTEGER NOT NULL REFERENCES tieraerzte(id) ON DELETE CASCADE, PRIMARY KEY (user_id, vet_id) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS health_documents ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, typ TEXT NOT NULL, titel TEXT NOT NULL, beschreibung TEXT, file_path TEXT NOT NULL, file_type TEXT NOT NULL, datum TEXT, vet_id INTEGER REFERENCES tieraerzte(id), created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_health_docs_dog ON health_documents(dog_id, created_at DESC)") # Digitaler Hundepass — Impfungen, Medikamente, Metadaten, Share-Links try: conn.execute(""" CREATE TABLE IF NOT EXISTS vaccinations ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, krankheit TEXT NOT NULL, datum TEXT NOT NULL, naechste TEXT, tierarzt TEXT, charge_nr TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS medications ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, name TEXT NOT NULL, dosierung TEXT, von TEXT, bis TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS dog_passport_meta ( dog_id INTEGER PRIMARY KEY REFERENCES dogs(id) ON DELETE CASCADE, blutgruppe TEXT, allergien TEXT, besonderheiten TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS passport_shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, valid_until TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_passport_shares_token ON passport_shares(token) """) logger.info("Migration: Hundepass-Tabellen bereit.") except Exception as e: logger.warning(f"Migration Hundepass: {e}") # ---- Playdate ---- conn.execute(""" CREATE TABLE IF NOT EXISTS playdate_listings ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, lat REAL NOT NULL, lon REAL NOT NULL, ort_name TEXT, radius_km INTEGER NOT NULL DEFAULT 10, beschreibung TEXT, aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(dog_id) ) """) conn.execute(""" CREATE INDEX IF NOT EXISTS idx_playdate_listings_geo ON playdate_listings(lat, lon) WHERE aktiv=1 """) conn.execute(""" CREATE TABLE IF NOT EXISTS playdate_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, from_dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, to_dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, from_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, to_user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, status TEXT NOT NULL DEFAULT 'pending', nachricht TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(from_dog_id, to_dog_id) ) """) # Welten-Chip-Konfiguration pro User existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] if 'world_config' not in existing_u: conn.execute("ALTER TABLE users ADD COLUMN world_config TEXT") # Tagessprüche-Pool conn.executescript(""" CREATE TABLE IF NOT EXISTS daily_quotes ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL, autor TEXT, kategorie TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_dq_kategorie ON daily_quotes(kategorie); """) # Wiederkehrende Ausgaben (Daueraufträge) conn.executescript(""" CREATE TABLE IF NOT EXISTS recurring_expenses ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, kategorie TEXT NOT NULL, betrag REAL NOT NULL, haeufigkeit TEXT NOT NULL, -- monatlich|quartalsweise|jaehrlich startdatum TEXT NOT NULL, naechste_faelligkeit TEXT NOT NULL, notiz TEXT, aktiv INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_recurring_user ON recurring_expenses(user_id, aktiv); """)