""" 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 INDEX IF NOT EXISTS idx_routes_user ON routes(user_id, created_at DESC); 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 route_dogs ( route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, PRIMARY KEY (route_id, dog_id) ); CREATE INDEX IF NOT EXISTS idx_route_dogs_dog ON route_dogs(dog_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 ); CREATE INDEX IF NOT EXISTS idx_forum_posts_thread ON forum_posts(thread_id, created_at ASC); CREATE INDEX IF NOT EXISTS idx_forum_posts_user ON forum_posts(user_id, created_at DESC); -- 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-Medien: Bildmaße für Querformat-Filter ("diary_media", "img_width", "INTEGER"), ("diary_media", "img_height", "INTEGER"), # 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"), # Fell-Typ für personalisierte Wetter-Hinweise ("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt # Widerristhöhe in cm (höchster Punkt Schulterblatt → Boden) ("dogs", "widerrist_cm", "REAL"), # Tierarzt-Bewertungen: Durchschnitt + Anzahl am Tierarzt-Datensatz ("tieraerzte", "avg_rating", "REAL DEFAULT 0"), ("tieraerzte", "anz_bewertungen", "INTEGER DEFAULT 0"), ] with conn_factory() as conn: for table, column, col_type in migrations: # PRAGMA table_info() liefert leere Liste fuer nicht-existierende Tabellen. # Wir ueberspringen — die CREATE TABLE-Bloecke darunter legen sie an, # ihre Spalten sind dort dann sowieso schon enthalten. existing = [ row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall() ] if not existing: continue 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 ki_cols and "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 REFERENCES users(id) ON DELETE CASCADE, bericht TEXT NOT NULL, erstellt_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) # user_id nachträglich ergänzen falls Tabelle ohne diese Spalte erstellt wurde try: conn.execute("ALTER TABLE ki_health_reports ADD COLUMN user_id INTEGER REFERENCES users(id) ON DELETE CASCADE") except Exception: pass # Spalte existiert bereits 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 # Defensiv — einige Tabellen werden erst beim ersten Zugriff in # ihren jeweiligen Routes-Modulen angelegt (z.B. ki_daily_calls) for _idx_sql in ( "CREATE INDEX IF NOT EXISTS idx_health_naechstes ON health(naechstes) WHERE naechstes IS NOT NULL", "CREATE INDEX IF NOT EXISTS idx_ki_calls_user_date ON ki_daily_calls(user_id, date)", "CREATE INDEX IF NOT EXISTS idx_events_user_datum ON events(user_id, datum)", "CREATE INDEX IF NOT EXISTS idx_diary_created ON diary(dog_id, created_at DESC)", "CREATE INDEX IF NOT EXISTS idx_notes_created ON notes(user_id, created_at DESC)", ): try: conn.execute(_idx_sql) except Exception as _e: logger.debug("Index-Migration uebersprungen (%s): %s", _idx_sql.split()[5], _e) 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); """) # Goldene Gassi-Stunde: User-Einstellung existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] if 'gassi_stunde_push' not in existing_u: conn.execute("ALTER TABLE users ADD COLUMN gassi_stunde_push INTEGER NOT NULL DEFAULT 0") logger.info("Migration: users.gassi_stunde_push bereit.") # Futter-Profil conn.executescript(""" CREATE TABLE IF NOT EXISTS futter_profil ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER REFERENCES dogs(id) ON DELETE CASCADE UNIQUE, futter_typ TEXT, marke TEXT, kcal_tag INTEGER, portionen INTEGER DEFAULT 2, notizen TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); """) logger.info("Migration: futter_profil bereit.") # Futter-Einträge & Reaktionen (Verträglichkeits-Tracking) try: conn.executescript(""" CREATE TABLE IF NOT EXISTS futter_eintraege ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, datum TEXT NOT NULL, uhrzeit TEXT NOT NULL, futter_name TEXT NOT NULL, futter_typ TEXT NOT NULL DEFAULT 'trockenfutter', menge_g INTEGER, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_futter_eintraege_dog ON futter_eintraege(dog_id, datum DESC); CREATE TABLE IF NOT EXISTS futter_reaktionen ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, datum TEXT NOT NULL, uhrzeit TEXT NOT NULL, reaktion_typ TEXT NOT NULL, intensitaet INTEGER NOT NULL DEFAULT 3, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_futter_reaktionen_dog ON futter_reaktionen(dog_id, datum DESC); """) logger.info("Migration: futter_eintraege + futter_reaktionen bereit.") except Exception as e: logger.warning(f"Migration futter_eintraege/reaktionen: {e}") # 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); """) # ---- Tierarzt-Bewertungen ---- conn.executescript(""" CREATE TABLE IF NOT EXISTS tierarzt_bewertungen ( id INTEGER PRIMARY KEY AUTOINCREMENT, tierarzt_id INTEGER NOT NULL REFERENCES tieraerzte(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, gesamt INTEGER NOT NULL CHECK(gesamt BETWEEN 1 AND 5), wartezeit INTEGER CHECK(wartezeit BETWEEN 1 AND 5), freundlichkeit INTEGER CHECK(freundlichkeit BETWEEN 1 AND 5), kompetenz INTEGER CHECK(kompetenz BETWEEN 1 AND 5), text TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(tierarzt_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_tierarzt_bew_arzt ON tierarzt_bewertungen(tierarzt_id); """) # ---- Feature: Foto-Challenge der Woche ---- conn.executescript(""" CREATE TABLE IF NOT EXISTS foto_challenge ( id INTEGER PRIMARY KEY AUTOINCREMENT, thema TEXT NOT NULL, beschreibung TEXT, start_date TEXT NOT NULL, end_date TEXT NOT NULL, created_by INTEGER REFERENCES users(id), created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS challenge_submissions ( id INTEGER PRIMARY KEY AUTOINCREMENT, challenge_id INTEGER REFERENCES foto_challenge(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, foto_url TEXT NOT NULL, caption TEXT, votes INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), UNIQUE(challenge_id, user_id) ); CREATE TABLE IF NOT EXISTS challenge_votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, submission_id INTEGER REFERENCES challenge_submissions(id) ON DELETE CASCADE, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, UNIQUE(submission_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_challenge_sub_chal ON challenge_submissions(challenge_id, created_at DESC); """) logger.info("Migration: Foto-Challenge-Tabellen bereit.") # ---- Feature: Gassi-Zeiten-Pool ---- conn.executescript(""" CREATE TABLE IF NOT EXISTS gassi_zeiten ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE SET NULL, wochentage TEXT NOT NULL, uhrzeit TEXT NOT NULL, ort_name TEXT, lat REAL, lon REAL, radius_m INTEGER DEFAULT 500, notiz TEXT, aktiv INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_gassi_zeiten_user ON gassi_zeiten(user_id, aktiv); """) logger.info("Migration: Gassi-Zeiten-Tabelle bereit.") # ---- Feature: Hilfe/FAQ ---- conn.executescript(""" CREATE TABLE IF NOT EXISTS help_articles ( id INTEGER PRIMARY KEY AUTOINCREMENT, kategorie TEXT NOT NULL, frage TEXT NOT NULL, antwort TEXT NOT NULL, sort_order INTEGER DEFAULT 0, aktiv INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_help_kat ON help_articles(kategorie, sort_order); """) _seed_help_articles(conn) logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") if 'preferred_theme' not in [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()]: conn.execute("ALTER TABLE users ADD COLUMN preferred_theme TEXT DEFAULT 'system'") logger.info("Migration: preferred_theme Spalte zu users hinzugefügt.") conn.executescript(""" CREATE TABLE IF NOT EXISTS bday_ki_cache ( dog_id INTEGER NOT NULL, year INTEGER NOT NULL, mode TEXT NOT NULL, -- 'tomorrow' | 'today' content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), PRIMARY KEY (dog_id, year, mode) ); """) logger.info("Migration: bday_ki_cache Tabelle bereit.") # ---- Feature: Subscription-Tier ---- try: conn.execute("ALTER TABLE users ADD COLUMN subscription_tier TEXT DEFAULT 'standard'") conn.execute("CREATE INDEX IF NOT EXISTS idx_users_tier ON users(subscription_tier)") logger.info("Migration: subscription_tier Spalte hinzugefügt.") except Exception: pass # Spalte existiert bereits # ---- Feature: is_active für Hunde (nach Abo-Downgrade) ---- try: conn.execute("ALTER TABLE dogs ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1") logger.info("Migration: dogs.is_active hinzugefügt.") except Exception: pass # ---- Feature: Subscription-Laufzeit & Kündigung ---- for col, typedef in [ ("subscription_expires_at", "TEXT DEFAULT NULL"), ("subscription_cancelled_at", "TEXT DEFAULT NULL"), ("needs_dog_selection", "INTEGER DEFAULT 0"), ]: try: conn.execute(f"ALTER TABLE users ADD COLUMN {col} {typedef}") logger.info(f"Migration: {col} hinzugefügt.") except Exception: pass # Bestehende TEXT-Werte für needs_dog_selection auf 0/1 normalisieren try: conn.execute("UPDATE users SET needs_dog_selection=0 WHERE needs_dog_selection='0' OR needs_dog_selection IS NULL") conn.execute("UPDATE users SET needs_dog_selection=1 WHERE needs_dog_selection='1'") except Exception: pass # exercise_progress + training_plan_progress: dog_id ergänzen existing_ep = [r[1] for r in conn.execute("PRAGMA table_info(exercise_progress)").fetchall()] if 'dog_id' not in existing_ep: try: # Neue Tabelle mit dog_id erstellen conn.execute(""" CREATE TABLE exercise_progress_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(id) ON DELETE CASCADE, exercise_id TEXT NOT NULL, status TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')), UNIQUE(dog_id, exercise_id) ) """) # Bestehende Daten migrieren: dog_id = erster Hund des Users conn.execute(""" INSERT INTO exercise_progress_new (user_id, dog_id, exercise_id, status, updated_at) SELECT ep.user_id, (SELECT id FROM dogs WHERE user_id=ep.user_id ORDER BY id LIMIT 1), ep.exercise_id, ep.status, ep.updated_at FROM exercise_progress ep """) conn.execute("DROP TABLE exercise_progress") conn.execute("ALTER TABLE exercise_progress_new RENAME TO exercise_progress") conn.execute("CREATE INDEX IF NOT EXISTS idx_exercise_progress_user ON exercise_progress(user_id)") conn.execute("CREATE INDEX IF NOT EXISTS idx_exercise_progress_dog ON exercise_progress(dog_id)") logger.info("Migration: exercise_progress.dog_id hinzugefügt.") except Exception as e: logger.warning(f"Migration exercise_progress.dog_id fehlgeschlagen: {e}") existing_tp = [r[1] for r in conn.execute("PRAGMA table_info(training_plan_progress)").fetchall()] if 'dog_id' not in existing_tp: try: conn.execute(""" CREATE TABLE training_plan_progress_new ( user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, dog_id INTEGER REFERENCES dogs(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 (dog_id, item_key) ) """) conn.execute(""" INSERT INTO training_plan_progress_new (user_id, dog_id, item_key, checked, checked_at) SELECT tp.user_id, (SELECT id FROM dogs WHERE user_id=tp.user_id ORDER BY id LIMIT 1), tp.item_key, tp.checked, tp.checked_at FROM training_plan_progress tp """) conn.execute("DROP TABLE training_plan_progress") conn.execute("ALTER TABLE training_plan_progress_new RENAME TO training_plan_progress") logger.info("Migration: training_plan_progress.dog_id hinzugefügt.") except Exception as e: logger.warning(f"Migration training_plan_progress.dog_id fehlgeschlagen: {e}") # verstorben_am: Hund als verstorben markierbar try: conn.execute("ALTER TABLE dogs ADD COLUMN verstorben_am TEXT") logger.info("Migration: dogs.verstorben_am hinzugefügt.") except Exception: pass # Gassi-Treffen Fotos try: conn.execute(""" CREATE TABLE IF NOT EXISTS walk_photos ( 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, url TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_walk_photos_walk ON walk_photos(walk_id)") logger.info("Migration: walk_photos bereit.") except Exception as e: logger.warning(f"Migration walk_photos: {e}") # Versicherungs-Verwaltung try: conn.execute(""" CREATE TABLE IF NOT EXISTS dog_insurance ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, anbieter TEXT NOT NULL, police_nr TEXT, jahresbeitrag REAL, kontakt TEXT, ablaufdatum TEXT, notizen TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) logger.info("Migration: dog_insurance bereit.") except Exception as e: logger.warning(f"Migration dog_insurance: {e}") # Verhaltens-Protokoll try: conn.execute(""" CREATE TABLE IF NOT EXISTS behavior_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, dog_id INTEGER NOT NULL REFERENCES dogs(id) ON DELETE CASCADE, datum TEXT NOT NULL, uhrzeit TEXT, kategorie TEXT NOT NULL, intensitaet INTEGER NOT NULL DEFAULT 3, trigger TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_behavior_dog ON behavior_log(dog_id, datum DESC)") logger.info("Migration: behavior_log bereit.") except Exception as e: logger.warning(f"Migration behavior_log: {e}") try: conn.execute(""" CREATE TABLE IF NOT EXISTS laeufi_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, beginn TEXT NOT NULL, ende TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS progesteron_tests ( id INTEGER PRIMARY KEY AUTOINCREMENT, laeufi_id INTEGER NOT NULL REFERENCES laeufi_log(id) ON DELETE CASCADE, datum TEXT NOT NULL, wert REAL, einheit TEXT NOT NULL DEFAULT 'ng/ml', labor TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS deckdaten ( id INTEGER PRIMARY KEY AUTOINCREMENT, hund_id INTEGER NOT NULL REFERENCES zucht_hunde(id) ON DELETE CASCADE, laeufi_id INTEGER REFERENCES laeufi_log(id) ON DELETE SET NULL, deckdatum TEXT NOT NULL, ruede_id INTEGER REFERENCES zucht_hunde(id) ON DELETE SET NULL, ruede_name TEXT, deckart TEXT NOT NULL DEFAULT 'natuerlich', traechtig INTEGER NOT NULL DEFAULT 0, ultraschall_datum TEXT, notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_laeufi_hund ON laeufi_log(hund_id, beginn DESC)") conn.execute("CREATE INDEX IF NOT EXISTS idx_prog_laeufi ON progesteron_tests(laeufi_id, datum)") conn.execute("CREATE INDEX IF NOT EXISTS idx_deck_hund ON deckdaten(hund_id, deckdatum DESC)") logger.info("Migration: laeufi_log, progesteron_tests, deckdaten bereit.") except Exception as e: logger.warning(f"Migration laeufi: {e}") try: conn.execute(""" CREATE TABLE IF NOT EXISTS litter_waitlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, litter_id INTEGER NOT NULL REFERENCES litters(id) ON DELETE CASCADE, name TEXT NOT NULL, email TEXT, telefon TEXT, nachricht TEXT, wunsch_geschlecht TEXT DEFAULT 'egal', wunsch_farbe TEXT, prioritaet INTEGER DEFAULT 0, status TEXT DEFAULT 'anfrage', notiz TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_waitlist_litter ON litter_waitlist(litter_id, prioritaet)") logger.info("Migration: litter_waitlist bereit.") except Exception as e: logger.warning(f"Migration litter_waitlist: {e}") try: conn.execute("ALTER TABLE litters ADD COLUMN wurf_rang TEXT") except Exception: pass try: conn.execute("ALTER TABLE litters ADD COLUMN wurf_name TEXT") except Exception: pass # upgrade_requests: Abo-Upgrade-Anfragen von Nutzern try: conn.execute(""" CREATE TABLE IF NOT EXISTS upgrade_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, tier TEXT NOT NULL, message TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), fulfilled_at TEXT, fulfilled_by INTEGER REFERENCES users(id) ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_upgrade_req_pending ON upgrade_requests(fulfilled_at, created_at DESC)") logger.info("Migration: upgrade_requests bereit.") except Exception as e: logger.warning(f"Migration upgrade_requests: {e}") # route_dogs: bestehende Routen allen Hunden des Users zuweisen try: existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0] if existing == 0: conn.execute(""" INSERT OR IGNORE INTO route_dogs (route_id, dog_id) SELECT r.id, d.id FROM routes r JOIN dogs d ON d.user_id = r.user_id """) logger.info("Migration: route_dogs mit bestehenden Routen befüllt.") except Exception as e: logger.warning(f"Migration route_dogs fehlgeschlagen: {e}") # Rechnungs-System try: conn.execute(""" CREATE TABLE IF NOT EXISTS invoices ( id INTEGER PRIMARY KEY AUTOINCREMENT, invoice_number TEXT NOT NULL UNIQUE, user_id INTEGER REFERENCES users(id), recipient_name TEXT NOT NULL, recipient_email TEXT NOT NULL, recipient_address TEXT, description TEXT NOT NULL, service_period TEXT, amount_net REAL NOT NULL, discount_pct REAL DEFAULT 0, discount_amount REAL DEFAULT 0, amount_after_discount REAL NOT NULL, tax_rate REAL DEFAULT 0, tax_amount REAL DEFAULT 0, amount_gross REAL NOT NULL, status TEXT DEFAULT 'draft', notes TEXT, created_at TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), sent_at TEXT, paid_at TEXT, paid_amount REAL, cancelled_at TEXT, cancellation_reason TEXT, cancellation_number TEXT ) """) conn.execute("CREATE INDEX IF NOT EXISTS idx_invoices_status ON invoices(status)") conn.execute("CREATE INDEX IF NOT EXISTS idx_invoices_user ON invoices(user_id)") conn.execute(""" CREATE TABLE IF NOT EXISTS invoice_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, invoice_id INTEGER NOT NULL REFERENCES invoices(id) ON DELETE CASCADE, description TEXT NOT NULL, quantity REAL NOT NULL DEFAULT 1, unit_price REAL NOT NULL, total REAL NOT NULL ) """) # Atomare Rechnungsnummern-Vergabe pro (prefix, year) # next_num = naechste zu vergebende Nummer (startet bei 1) conn.execute(""" CREATE TABLE IF NOT EXISTS invoice_counters ( prefix TEXT NOT NULL, year INTEGER NOT NULL, next_num INTEGER NOT NULL DEFAULT 1, PRIMARY KEY (prefix, year) ) """) logger.info("Migration: invoices + invoice_items + invoice_counters bereit.") except Exception as e: logger.warning(f"Migration invoices: {e}") try: conn.execute("ALTER TABLE users ADD COLUMN billing_address TEXT") logger.info("Migration: billing_address bereit.") except Exception: pass existing_u_gb = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()] if 'geburtstag' not in existing_u_gb: conn.execute("ALTER TABLE users ADD COLUMN geburtstag TEXT") logger.info("Migration: users.geburtstag hinzugefügt.") else: logger.info("Migration: users.geburtstag bereits vorhanden.") # ---- Security Hardening (Sprint60) ---- # JWT-Blacklist: invalidierte Tokens nach Logout / Sperrungen conn.executescript(""" CREATE TABLE IF NOT EXISTS jwt_blacklist ( jti TEXT PRIMARY KEY, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_jwt_blacklist_expires ON jwt_blacklist(expires_at); """) logger.info("Migration: jwt_blacklist Tabelle bereit.") # Login-Brute-Force-Lockout (persistent statt In-Memory) conn.executescript(""" CREATE TABLE IF NOT EXISTS login_attempts ( email TEXT PRIMARY KEY COLLATE NOCASE, attempts INTEGER NOT NULL DEFAULT 0, last_attempt TEXT NOT NULL, locked_until TEXT ); CREATE INDEX IF NOT EXISTS idx_login_attempts_locked ON login_attempts(locked_until); """) logger.info("Migration: login_attempts Tabelle bereit.") # Fehlgeschlagene E-Mail-Zustellungen — für Admin-Retry / Diagnose conn.executescript(""" CREATE TABLE IF NOT EXISTS failed_emails ( id INTEGER PRIMARY KEY AUTOINCREMENT, to_email TEXT NOT NULL, subject TEXT NOT NULL, body TEXT NOT NULL, error TEXT NOT NULL, context TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_failed_emails_created ON failed_emails(created_at DESC); """) logger.info("Migration: failed_emails Tabelle bereit.") # Second-Pass der ALTER-TABLE-Migrations: # Manche Migrations (z.B. diary_media.img_width) referenzieren Tabellen, # die erst weiter unten in dieser Funktion per CREATE IF NOT EXISTS angelegt # werden. Beim ersten Durchgang wurden sie uebersprungen — jetzt nachziehen. for table, column, col_type in migrations: existing = [ row[1] for row in conn.execute(f"PRAGMA table_info({table})").fetchall() ] if not existing: continue if column not in existing: try: conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {col_type}") logger.info(f"Migration (2nd pass): {table}.{column} hinzugefügt.") except Exception as _e: logger.debug(f"Migration (2nd pass) skipped {table}.{column}: {_e}") def _seed_help_articles(conn): """Befüllt help_articles mit Starter-FAQs — nur wenn die Tabelle noch leer ist.""" count = conn.execute("SELECT COUNT(*) FROM help_articles").fetchone()[0] if count > 0: return _SEED = [ # ── installation ────────────────────────────────────────────── ("installation", "Was ist eine PWA?", "Eine Progressive Web App (PWA) ist eine Website, die sich wie eine richtige App verhält. " "Du kannst Ban Yaro direkt im Browser nutzen oder als App auf deinem Startbildschirm speichern — " "ohne den App Store.\n\n" "Vorteile: immer aktuell, kein Download, funktioniert auch offline.", 1), ("installation", "Warum ist Ban Yaro nicht im App Store?", "Als PWA können wir Ban Yaro viel schneller mit neuen Funktionen ausstatten und Fehler sofort " "beheben. App-Store-Apps müssen oft tagelang auf Freigabe warten.\n\n" "Außerdem sparst du Speicherplatz auf deinem Handy — Ban Yaro braucht nur wenige Megabyte.", 2), ("installation", "iPhone: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", "1. Öffne banyaro.app in Safari (nicht Chrome oder Firefox).\n" "2. Tippe auf das Teilen-Symbol (Quadrat mit Pfeil nach oben) unten in der Mitte.\n" "3. Scrolle im Menü nach unten und wähle 'Zum Home-Bildschirm'.\n" "4. Bestätige mit 'Hinzufügen'.\n\n" "Ban Yaro erscheint jetzt als App-Icon auf deinem Startbildschirm.", 3), ("installation", "Android: Wie füge ich Ban Yaro zum Startbildschirm hinzu?", "1. Öffne banyaro.app in Chrome.\n" "2. Tippe oben rechts auf die drei Punkte (Menü).\n" "3. Wähle 'App installieren' oder 'Zum Startbildschirm hinzufügen'.\n" "4. Bestätige die Installation.\n\n" "Bei manchen Android-Geräten erscheint auch automatisch ein Banner am unteren Bildschirmrand.", 4), ("installation", "Wie aktualisiere ich die App?", "Ban Yaro aktualisiert sich automatisch im Hintergrund. Wenn ein Update bereit ist, " "siehst du einen Hinweis in der App.\n\n" "Falls du Probleme hast und die App sich komisch verhält: Einstellungen öffnen, " "ganz unten 'Auf Update prüfen' tippen. Danach die App schließen und neu öffnen.", 5), # ── erste_schritte ──────────────────────────────────────────── ("erste_schritte", "Wie lege ich meinen Hund an?", "Tippe in der Navigation unten auf den Hund-Tab (HUND) und dann auf 'Hund hinzufügen'. " "Du gibst Name, Rasse und Geburtsdatum ein — ein Foto ist optional, macht aber gleich mehr Spaß.\n\n" "Du kannst mehrere Hunde anlegen und zwischen ihnen wechseln.", 1), ("erste_schritte", "Wie navigiere ich zwischen den Welten?", "Ban Yaro ist in drei Welten aufgeteilt: JETZT, HUND und WELT. " "Du kannst links-rechts wischen oder die drei Buttons unten in der Navigation tippen.\n\n" "JETZT zeigt dir alles, was gerade relevant ist. HUND ist alles über deinen Vierbeiner. " "WELT verbindet dich mit der Hunde-Community.", 2), ("erste_schritte", "Was bedeuten JETZT, HUND und WELT?", "JETZT: Dein persönliches Dashboard — Wetter, Gassi-Planer, aktuelle Hinweise in deiner Nähe.\n\n" "HUND: Alles rund um deinen Hund — Tagebuch, Gesundheit, Training, Ernährung, Tierarzt.\n\n" "WELT: Die Community — Forum, Wiki, Events, andere Hundebesitzer, Gassi-Treffen, Karte.", 3), ("erste_schritte", "Wie passe ich die Chips auf der JETZT-Seite an?", "Tippe oben rechts auf der JETZT-Seite auf das Zahnrad-Symbol. " "Dort kannst du auswählen, welche Funktions-Chips du sehen möchtest " "und in welcher Reihenfolge sie angezeigt werden.", 4), ("erste_schritte", "Wie schreibe ich mein erstes Tagebuch?", "Gehe in die HUND-Welt und tippe auf 'Tagebuch'. " "Mit dem Plus-Button kannst du einen neuen Eintrag anlegen.\n\n" "Du kannst Text schreiben, Fotos hinzufügen, deine Stimmung eintragen und den Ort markieren. " "Alle Einträge sind nur für dich sichtbar.", 5), # ── standort ────────────────────────────────────────────────── ("standort", "Wie gebe ich den Standort auf dem iPhone frei?", "1. Öffne die Einstellungen deines iPhones.\n" "2. Scrolle zu Safari.\n" "3. Tippe auf 'Standort'.\n" "4. Wähle 'Beim Benutzen der App erlauben'.\n\n" "Alternativ: Wenn Ban Yaro fragt, ob es deinen Standort nutzen darf, " "tippe auf 'Erlauben'. Diese Abfrage erscheint beim ersten Öffnen der Karte oder Wetter-Funktion.", 1), ("standort", "Wie gebe ich den Standort auf Android frei?", "1. Öffne die Einstellungen deines Handys.\n" "2. Gehe zu Apps → Chrome → Berechtigungen → Standort.\n" "3. Wähle 'Beim Benutzen der App erlauben'.\n\n" "Beim ersten Öffnen der Karte oder Wetter-Funktion fragt Ban Yaro automatisch nach der Berechtigung.", 2), ("standort", "Der Standort ist blockiert — wie setze ich das zurück?", "Wenn du versehentlich 'Ablehnen' getippt hast, kannst du das zurücksetzen:\n\n" "iPhone: Einstellungen → Safari → Standort → 'Beim Benutzen erlauben'\n\n" "Android: In Chrome tippe auf das Schloss-Symbol links in der Adresszeile → " "Website-Einstellungen → Standort → Erlauben.\n\n" "Tipp: Manchmal hilft es, die Website-Daten zu löschen und Ban Yaro neu zu öffnen.", 3), ("standort", "Das Wetter lädt nicht — was kann ich tun?", "Das Wetter benötigt deinen aktuellen Standort. Prüfe zuerst, ob die Standort-Berechtigung " "erteilt ist (siehe 'Standort freigeben').\n\n" "Falls es trotzdem nicht klappt: Schließe Ban Yaro vollständig und öffne es erneut. " "Bei anhaltenden Problemen tippe in den Einstellungen auf 'Auf Update prüfen'.", 4), # ── account ─────────────────────────────────────────────────── ("account", "Ich habe mein Passwort vergessen — was nun?", "Auf der Anmeldeseite findest du den Link 'Passwort vergessen'. " "Gib dort deine E-Mail-Adresse ein — du erhältst innerhalb weniger Minuten " "einen Link zum Zurücksetzen.\n\n" "Schau auch im Spam-Ordner, falls die E-Mail nicht ankommt.", 1), ("account", "Die E-Mail-Bestätigung ist nicht angekommen.", "Bitte prüfe deinen Spam- oder Junk-Ordner. E-Mails von Ban Yaro kommen von noreply@banyaro.app.\n\n" "Falls du die E-Mail dort nicht findest, kannst du die Bestätigung in den Einstellungen " "unter 'Konto' erneut anfordern.\n\n" "Stelle sicher, dass deine E-Mail-Adresse korrekt geschrieben ist.", 2), ("account", "Wie kann ich mein Konto löschen?", "Gehe in die Einstellungen (Zahnrad-Symbol) → Konto → 'Konto löschen'.\n\n" "Achtung: Die Löschung ist endgültig. Alle deine Daten, Hunde-Profile und " "Tagebuch-Einträge werden dauerhaft entfernt. Diese Aktion kann nicht rückgängig gemacht werden.", 3), ("account", "Wie ändere ich meine E-Mail-Adresse?", "Das Ändern der E-Mail-Adresse ist aus Sicherheitsgründen aktuell nur über den " "Support möglich. Schreibe uns an support@banyaro.app mit deiner aktuellen und " "gewünschten neuen E-Mail-Adresse.", 4), # ── features ────────────────────────────────────────────────── ("features", "Was ist der Gassi-Score?", "Der Gassi-Score zeigt dir auf einen Blick, wie gut das Wetter gerade für einen Spaziergang ist. " "Er berücksichtigt Temperatur, Regen, Wind, UV-Index und — bei heißem Wetter — " "die Asphalt-Temperatur.\n\n" "Grün = super. Gelb = geht noch. Rot = lieber warten oder kürzer machen.", 1), ("features", "Was kann der KI-Tierarzt?", "Der KI-Tierarzt beantwortet allgemeine Fragen rund um die Gesundheit deines Hundes — " "zum Beispiel zu Symptomen, Ernährung oder Verhalten.\n\n" "Wichtig: Er ersetzt keinen echten Tierarzt. Bei ernsten Symptomen oder Notfällen " "wende dich bitte sofort an einen Tierarzt in deiner Nähe.", 2), ("features", "Wie funktioniert der Offline-Modus?", "Ban Yaro speichert die wichtigsten Funktionen lokal auf deinem Gerät. " "Auch ohne Internet kannst du dein Tagebuch lesen, Einträge anlegen und " "gespeicherte Karten nutzen.\n\n" "Neue Daten werden automatisch synchronisiert, sobald du wieder online bist.", 3), ("features", "Wie richte ich Push-Benachrichtigungen ein?", "Gehe in die Einstellungen (Zahnrad-Symbol) und tippe auf 'Push-Benachrichtigungen'. " "Dort kannst du auswählen, für welche Ereignisse du Benachrichtigungen erhalten möchtest — " "z.B. Giftköder-Warnungen, neue Nachrichten oder Gassi-Erinnerungen.\n\n" "Auf dem iPhone muss Ban Yaro als App auf dem Startbildschirm installiert sein, " "damit Push-Nachrichten funktionieren.", 4), ("features", "Kann ich mein Tagebuch-Hintergrundbild ändern?", "Ja! Im Tagebuch tippe oben auf das Bild-Symbol oder auf 'Hintergrund anpassen'. " "Du kannst ein eigenes Foto wählen oder eines der vorhandenen Motive nutzen.\n\n" "Das Hintergrundbild gilt für alle Einträge und macht dein Tagebuch ganz persönlich.", 5), ("features", "Was ist der Giftköder-Alarm?", "Der Giftköder-Alarm zeigt dir gemeldete Giftköder in deiner Nähe auf einer Karte. " "Du kannst selbst Funde melden und andere Hundebesitzer warnen.\n\n" "In den Einstellungen kannst du Push-Benachrichtigungen aktivieren, " "damit du sofort gewarnt wirst, wenn in deiner Nähe ein Giftköder gemeldet wird.", 6), # ── probleme ────────────────────────────────────────────────── ("probleme", "Die App zeigt alte Daten — was tun?", "Tippe in den Einstellungen ganz unten auf 'Auf Update prüfen'. " "Danach schließe Ban Yaro vollständig (App aus dem Multitasking entfernen) " "und öffne sie erneut.\n\n" "Falls das nicht hilft: Lösche die Website-Daten in deinem Browser und öffne " "Ban Yaro erneut. Du bleibst dabei angemeldet.", 1), ("probleme", "Das Wetter lädt nicht.", "Stelle sicher, dass die Standort-Berechtigung erteilt ist. " "Ohne Standort kann Ban Yaro kein lokales Wetter laden.\n\n" "Prüfe außerdem deine Internetverbindung. Bei schlechtem WLAN oder Mobilfunk " "kann es zu Verzögerungen kommen. Eine kurze Wartezeit und erneutes Tippen hilft meist.", 2), ("probleme", "Die App reagiert nicht oder friert ein.", "Schließe Ban Yaro vollständig (aus dem Multitasking entfernen) und öffne sie erneut.\n\n" "Falls das Problem anhält, prüfe ob dein Gerät ausreichend Speicher hat. " "Starte dein Handy neu — das löst in den meisten Fällen temporäre Hänger.", 3), ("probleme", "Wie melde ich einen Fehler?", "Wir freuen uns über Feedback! Schreibe uns an support@banyaro.app mit einer kurzen " "Beschreibung des Problems.\n\n" "Hilfreich sind: Was hast du getan? Was hast du erwartet? Was ist stattdessen passiert? " "Welches Gerät und Browser nutzt du?\n\n" "Wir melden uns so schnell wie möglich.", 4), ] for kat, frage, antwort, sort in _SEED: conn.execute( "INSERT INTO help_articles (kategorie, frage, antwort, sort_order) VALUES (?, ?, ?, ?)", (kat, frage, antwort, sort), )