banyaro/backend/database.py
rene 9103c7950f Feature: Generische Seiten-Hilfe (UI.pageInfo), POI Multi-Select, Tagessprüche-DB (SW by-v654)
- UI.pageInfo(): generische Hilfe-Funktion — erstes Öffnen zeigt Info-Banner, danach ? Button oben rechts; CSS-Klassen pinfo-*
- Übungen-Seite nutzt UI.pageInfo() als erstes Beispiel
- Karte POI: Mehrfachauswahl (außer Giftköder), Kombi-Typen entfernt, type als comma-separated im Backend
- daily_quotes Tabelle in DB (346 Einträge via import_quotes.py importiert)
- GET /widget/quote — deterministischer Tagesspruch (wechselt täglich)
2026-05-03 20:10:01 +02:00

1959 lines
93 KiB
Python

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