diff --git a/Makefile b/Makefile index 692e7bb..910c66d 100644 --- a/Makefile +++ b/Makefile @@ -128,17 +128,6 @@ staging: check-ssh @echo " ✓ Staging fertig — https://staging.banyaro.app" @ssh $(DS_HOST) "$(DOCKER) logs $(CONTAINER_STAGING) --tail=10" -# ---------------------------------------------------------- -# STAGING-DB — Produktions-DB in Staging kopieren (interaktiv, braucht sudo) -# Aufruf: make staging-db -# ---------------------------------------------------------- -staging-db: check-ssh - @echo "→ Produktions-DB nach Staging kopieren..." - @ssh -t $(DS_HOST) " \ - sudo cp $(DS_PATH)/data/banyaro.db $(DS_PATH_STAGING)/data/banyaro.db && \ - sudo chmod 666 $(DS_PATH_STAGING)/data/banyaro.db && \ - echo '✓ DB kopiert'" - # ---------------------------------------------------------- # RELEASE — develop → main → Production (VERSION= pflichtangabe) # Beispiel: make release VERSION=1.1.0 diff --git a/backend/database.py b/backend/database.py index 1a70aa5..e373f02 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1657,221 +1657,3 @@ def _migrate(conn_factory): ); 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 - ) - """) - - # ---- 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) - ) - """) - - # 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); - """) diff --git a/backend/main.py b/backend/main.py index 8b259f7..6eb99a2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -189,13 +189,6 @@ from routes.zucht_ki import router as zucht_ki_router from routes.partner import router as partner_router from routes.outreach import router as outreach_router from routes.jobs import router as jobs_router -from routes.streak import router as streak_router -from routes.expenses import router as expenses_router -from routes.recalls import router as recalls_router -from routes.adoption import router as adoption_router -from routes.health_docs import router as health_docs_router -from routes.passport import router as passport_router -from routes.playdate import router as playdate_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -247,13 +240,6 @@ app.include_router(training_router, prefix="/api/training", tags= app.include_router(praise_router, prefix="/api/praise", tags=["Praise"]) app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"]) app.include_router(notes_router, prefix="/api/notes", tags=["Notes"]) -app.include_router(streak_router, prefix="/api", tags=["Streak"]) -app.include_router(expenses_router, prefix="/api/expenses", tags=["Ausgaben"]) -app.include_router(recalls_router, prefix="/api/recalls", tags=["Rückrufe"]) -app.include_router(adoption_router, prefix="/api/adoption", tags=["Adoption"]) -app.include_router(health_docs_router, prefix="/api/health-docs", tags=["Gesundheitsdokumente"]) -app.include_router(passport_router, prefix="/api/passport", tags=["Hundepass"]) -app.include_router(playdate_router, prefix="/api/playdate", tags=["Playdate"]) # ------------------------------------------------------------------ @@ -1688,152 +1674,6 @@ for _hp in _HONEYPOT_PATHS: app.add_api_route(_hp, _honeypot_handler, methods=["GET", "POST", "PUT", "DELETE"], include_in_schema=False) -# ------------------------------------------------------------------ -# Digitaler Hundepass — öffentlicher Share-Link (kein Login nötig) -# ------------------------------------------------------------------ -@app.get("/pass/{token}") -async def passport_share_page(token: str): - from fastapi.responses import HTMLResponse - from database import db as _db - from datetime import date as _date - - with _db() as conn: - share = conn.execute( - "SELECT * FROM passport_shares WHERE token=?", (token,) - ).fetchone() - if not share: - return HTMLResponse( - '' - '
Dieser Hundepass-Link ist ungültig.
', - status_code=404 - ) - if share["valid_until"] < _date.today().isoformat(): - return HTMLResponse( - '' - 'Dieser Hundepass-Link ist nicht mehr gültig.
', - status_code=410 - ) - dog_id = share["dog_id"] - dog = conn.execute("SELECT * FROM dogs WHERE id=?", (dog_id,)).fetchone() - meta = conn.execute("SELECT * FROM dog_passport_meta WHERE dog_id=?", (dog_id,)).fetchone() - vaccs = conn.execute( - "SELECT * FROM vaccinations WHERE dog_id=? ORDER BY datum DESC", (dog_id,) - ).fetchall() - meds = conn.execute( - "SELECT * FROM medications WHERE dog_id=? ORDER BY von DESC, id DESC", (dog_id,) - ).fetchall() - def _fmt(d): - if not d: - return "–" - try: - from datetime import datetime as _dt - return _dt.strptime(d[:10], "%Y-%m-%d").strftime("%d.%m.%Y") - except Exception: - return d - - dog = dict(dog) - meta = dict(meta) if meta else {} - vaccs = [dict(v) for v in vaccs] - meds = [dict(m) for m in meds] - - _g_map = {"m": "Rüde", "w": "Hündin"} - - vacc_rows = "".join(f""" -Digitaler Hundepass — {dog['name']}
-| Krankheit | Datum | Nächste | Tierarzt | Charge | -
|---|
| Medikament | Dosierung | Von | Bis | Notiz | -
|---|
- Noch keine Befunde hochgeladen. -
`; - - return ` -- Beschreibe die Symptome deines Hundes. Die KI gibt eine erste Orientierung — - kein Ersatz für einen echten Tierarzt. -
-')
- .replace(/\n/g, '
');
- const restHtml = result.limit - result.anfragen_heute > 0
- ? `
- Noch ${result.limit - result.anfragen_heute} von ${result.limit} Anfragen heute verfügbar. -
` - : `- Tageslimit erreicht. Morgen wieder verfügbar. -
`; - - resultEl.innerHTML = ` -${antwortHtml}
- ${restHtml} -- Standort wird ermittelt… -
-
- Standort konnte nicht automatisch ermittelt werden.
- Klicke auf "Standort aktualisieren".
-
${UI.icon('spinner')} Suche…
`; - - try { - const data = await API.get(`/playdate/nearby?lat=${_userPos.lat}&lon=${_userPos.lon}&radius=${_radius}`); - - if (!data || data.length === 0) { - resultsEl.innerHTML = UI.emptyState({ - icon: UI.icon('paw-print'), - title: 'Niemand in der Nähe', - text: `Aktuell hat sich noch niemand in ${_radius} km Umkreis eingetragen. Trage deinen Hund unter "Meine Inserate" ein!`, - }); - return; - } - - resultsEl.innerHTML = ` -${err.message}
`; - } - } - - function _nearbyCard(d) { - return ` -- ${_esc(d.beschreibung)} -
` : ''} - -${UI.icon('spinner')} Lädt…
`; - await _loadListings(el); - } - - async function _loadListings(el) { - const target = el || document.getElementById('playdate-content'); - if (!target) return; - - if (_dogs.length === 0) { - target.innerHTML = UI.emptyState({ - icon: UI.icon('paw-print'), - title: 'Noch kein Hund', - text: 'Lege zuerst einen Hund in deinem Profil an.', - action: `${_esc(listing.beschreibung)}
` : ''} - ` : ` -- Noch kein Inserat — trage dich ein, damit andere dich finden können. -
- `} - -${UI.icon('spinner')} Lädt…
`; - try { - const data = await API.get('/playdate/requests'); - const incoming = data.incoming || []; - const outgoing = data.outgoing || []; - - // Badge aktualisieren - const pendingCount = incoming.filter(r => r.status === 'pending').length; - const badge = document.getElementById('playdate-req-badge'); - if (badge) { - badge.textContent = pendingCount; - badge.style.display = pendingCount > 0 ? '' : 'none'; - } - - if (incoming.length === 0 && outgoing.length === 0) { - el.innerHTML = UI.emptyState({ - icon: UI.icon('paw-print'), - title: 'Noch keine Anfragen', - text: 'Wenn du oder jemand anderes eine Playdate-Anfrage schickt, erscheint sie hier.', - }); - return; - } - - el.innerHTML = ` -${err.message}
`; - } - } - - function _incomingCard(r) { - const isPending = r.status === 'pending'; - return ` -- "${_esc(r.nachricht)}" -
` : ''} - - ${r.status === 'accepted' ? ` -- ${UI.escape(r.gefahr)} -
` : ''} - - -Wird geladen…
', - }); - let board; - try { board = await API.get('/streak/leaderboard'); } catch { board = []; } - const bodyEl = modalEl?.querySelector('.modal-body'); - if (bodyEl) bodyEl.innerHTML = _leaderboardHTML(board); - }); - } - - function _streakWidgetHTML(s) { - const cur = s.current_streak || 0; - const best = s.longest_streak || 0; - return ` -Noch keine Einträge.
'; - } - const medals = ['🥇', '🥈', '🥉']; - return ` -