2836 lines
140 KiB
Python
2836 lines
140 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 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);
|
|
|
|
-- OSM-Account-Verknüpfung (OAuth2) je Nutzer — Basis für OSM-Beiträge
|
|
-- ("Hund war willkommen" → dog=yes) + spätere Gamification/Pro-Freischaltung.
|
|
-- access_token verschlüsselt at rest (token_enc).
|
|
CREATE TABLE IF NOT EXISTS user_osm (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
osm_uid INTEGER NOT NULL,
|
|
osm_name TEXT NOT NULL,
|
|
token_enc TEXT NOT NULL,
|
|
scopes TEXT,
|
|
linked_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- OSM-Beiträge ("Hund war willkommen" → dog=yes). Anti-Fraud: GPS-Beleg
|
|
-- über eine kürzliche eigene Tour (route_id) + Zeit/Rate-Limits.
|
|
-- status: pending → submitted (an OSM) → confirmed (Revert-überlebt) | rejected.
|
|
CREATE TABLE IF NOT EXISTS osm_contributions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
osm_id INTEGER NOT NULL,
|
|
osm_type TEXT NOT NULL DEFAULT 'node', -- node | way
|
|
poi_type TEXT,
|
|
tag_key TEXT NOT NULL DEFAULT 'dog',
|
|
tag_value TEXT NOT NULL DEFAULT 'yes',
|
|
lat REAL,
|
|
lon REAL,
|
|
route_id INTEGER REFERENCES routes(id) ON DELETE SET NULL,
|
|
gps_distance_m REAL,
|
|
gps_points_near INTEGER,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
submitted_at TEXT,
|
|
changeset_id INTEGER,
|
|
UNIQUE(user_id, osm_id, tag_key)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_osm_contrib_user ON osm_contributions(user_id, status);
|
|
|
|
-- Pro-Freischaltungen aus OSM-Beiträgen (1 Zeile = 1 freigeschaltetes Jahr).
|
|
-- Idempotenz: earned = confirmed//100; nur (earned - vorhandene Zeilen) neu gewähren.
|
|
CREATE TABLE IF NOT EXISTS osm_pro_grants (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
-- 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}")
|
|
|
|
# Partner-Profile (öffentlicher Showcase auf /#partner)
|
|
# approved: 0=Entwurf/in Prüfung, 1=freigegeben, -1=abgelehnt
|
|
try:
|
|
# Alt-Schema aus der verlorenen v1102-Session (photos statt photos_json,
|
|
# id-Autoincrement-PK) kann auf Staging/Prod noch existieren → umbauen.
|
|
existing_cols = [r[1] for r in conn.execute(
|
|
"PRAGMA table_info(partner_profiles)"
|
|
).fetchall()]
|
|
if existing_cols and "photos_json" not in existing_cols:
|
|
conn.executescript("""
|
|
ALTER TABLE partner_profiles RENAME TO partner_profiles_old;
|
|
CREATE TABLE partner_profiles (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
display_name TEXT,
|
|
tagline TEXT,
|
|
bio TEXT,
|
|
website TEXT,
|
|
instagram TEXT,
|
|
logo_url TEXT,
|
|
photos_json TEXT NOT NULL DEFAULT '[]',
|
|
approved INTEGER NOT NULL DEFAULT 0,
|
|
submitted_at TEXT,
|
|
updated_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
INSERT INTO partner_profiles
|
|
(user_id, display_name, tagline, bio, website, instagram,
|
|
logo_url, photos_json, approved, submitted_at, updated_at, created_at)
|
|
SELECT user_id, display_name, tagline, bio, website, instagram,
|
|
logo_url, COALESCE(photos, '[]'), COALESCE(approved, 0),
|
|
submitted_at, NULL, datetime('now')
|
|
FROM partner_profiles_old;
|
|
DROP TABLE partner_profiles_old;
|
|
""")
|
|
logger.info("Migration: partner_profiles Alt-Schema → neues Schema umgebaut.")
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS partner_profiles (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
display_name TEXT,
|
|
tagline TEXT,
|
|
bio TEXT,
|
|
website TEXT,
|
|
instagram TEXT,
|
|
logo_url TEXT,
|
|
photos_json TEXT NOT NULL DEFAULT '[]',
|
|
approved INTEGER NOT NULL DEFAULT 0,
|
|
submitted_at TEXT,
|
|
updated_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
""")
|
|
logger.info("Migration: partner_profiles Tabelle bereit.")
|
|
except Exception as e:
|
|
logger.warning(f"Migration partner_profiles: {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),
|
|
)
|