Fix: Datenexport — fehlende Spalten + robuste Query-Helper (SW by-v882)
This commit is contained in:
parent
79cfd63ea6
commit
5cbe96ebc4
5 changed files with 77 additions and 120 deletions
|
|
@ -447,7 +447,7 @@ async def serve_media(path: str, request: _Request):
|
||||||
raise _HE(404, "Nicht gefunden.")
|
raise _HE(404, "Nicht gefunden.")
|
||||||
return _media_response(filepath)
|
return _media_response(filepath)
|
||||||
|
|
||||||
APP_VER = "881" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "882" # muss mit APP_VER in app.js übereinstimmen
|
||||||
|
|
||||||
@app.get("/.well-known/assetlinks.json")
|
@app.get("/.well-known/assetlinks.json")
|
||||||
async def assetlinks():
|
async def assetlinks():
|
||||||
|
|
|
||||||
|
|
@ -174,153 +174,110 @@ async def delete_account(user=Depends(get_current_user)):
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
@router.get('/export')
|
@router.get('/export')
|
||||||
async def export_user_data(user=Depends(get_current_user)):
|
async def export_user_data(user=Depends(get_current_user)):
|
||||||
"""Gibt alle personenbezogenen Daten des Users als JSON zurück."""
|
"""Gibt alle personenbezogenen Daten des Users als JSON zurück (Art. 20 DSGVO)."""
|
||||||
import json as _json
|
import json as _json
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
from fastapi.responses import Response as _Response
|
from fastapi.responses import Response as _Response
|
||||||
|
|
||||||
|
def _q(conn, sql, params=()):
|
||||||
|
"""Sicheres Query — gibt leere Liste zurück wenn Tabelle/Spalte fehlt."""
|
||||||
|
try:
|
||||||
|
return [dict(r) for r in conn.execute(sql, params).fetchall()]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _q1(conn, sql, params=()):
|
||||||
|
"""Single-Row-Query — gibt None zurück bei Fehler."""
|
||||||
|
try:
|
||||||
|
r = conn.execute(sql, params).fetchone()
|
||||||
|
return dict(r) if r else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
uid = user['id']
|
uid = user['id']
|
||||||
|
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
# --- Nutzerprofil ---
|
# Nutzerprofil
|
||||||
u = dict(conn.execute(
|
u = _q1(conn,
|
||||||
"SELECT id, name, email, bio, wohnort, erfahrung, social_link, "
|
"SELECT id, name, email, bio, wohnort, erfahrung, social_link, "
|
||||||
"email_verified, is_premium, subscription_tier, created_at "
|
"email_verified, is_premium, subscription_tier, created_at "
|
||||||
"FROM users WHERE id=?", (uid,)
|
"FROM users WHERE id=?", (uid,)) or {}
|
||||||
).fetchone() or {})
|
|
||||||
|
|
||||||
# --- Hunde ---
|
# Hunde
|
||||||
dogs_raw = conn.execute(
|
dogs_raw = _q(conn, "SELECT * FROM dogs WHERE user_id=?", (uid,))
|
||||||
"SELECT * FROM dogs WHERE user_id=?", (uid,)
|
|
||||||
).fetchall()
|
|
||||||
dogs_out = []
|
dogs_out = []
|
||||||
|
|
||||||
for dog in dogs_raw:
|
for dog in dogs_raw:
|
||||||
did = dog['id']
|
did = dog['id']
|
||||||
d = dict(dog)
|
|
||||||
|
|
||||||
# Tagebuch
|
# Tagebuch (nur vorhandene Spalten)
|
||||||
diary_rows = conn.execute(
|
diary_rows = _q(conn,
|
||||||
"SELECT id, datum, typ, titel, text, gps_lat, gps_lon, "
|
"SELECT id, datum, typ, titel, text, gps_lat, gps_lon, "
|
||||||
"location_name, is_milestone, created_at FROM diary WHERE dog_id=?",
|
"is_milestone, created_at FROM diary WHERE dog_id=?", (did,))
|
||||||
(did,)
|
|
||||||
).fetchall()
|
|
||||||
diary_out = []
|
|
||||||
for de in diary_rows:
|
for de in diary_rows:
|
||||||
de_dict = dict(de)
|
# diary_media: preview_url existiert nicht → url + media_type
|
||||||
media = conn.execute(
|
de['media'] = _q(conn,
|
||||||
"SELECT url, preview_url, media_type FROM diary_media WHERE diary_id=?",
|
"SELECT url, media_type FROM diary_media WHERE diary_id=?",
|
||||||
(de['id'],)
|
(de['id'],))
|
||||||
).fetchall()
|
|
||||||
de_dict['media'] = [dict(m) for m in media]
|
|
||||||
diary_out.append(de_dict)
|
|
||||||
|
|
||||||
# Gesundheit
|
# Gesundheit (alle via Migration ergänzten Spalten schützen)
|
||||||
health_rows = conn.execute(
|
health_rows = _q(conn,
|
||||||
"SELECT id, typ, bezeichnung, datum, naechstes, notiz, "
|
"SELECT id, typ, bezeichnung, datum, naechstes, notiz FROM health "
|
||||||
"schweregrad, reaktion, dosierung, haeufigkeit, "
|
"WHERE dog_id=?", (did,))
|
||||||
"tierarzt_name, charge_nr FROM health WHERE dog_id=?",
|
|
||||||
(did,)
|
|
||||||
).fetchall()
|
|
||||||
health_out = []
|
|
||||||
for he in health_rows:
|
for he in health_rows:
|
||||||
he_dict = dict(he)
|
he['media'] = _q(conn,
|
||||||
media = conn.execute(
|
|
||||||
"SELECT url, media_type FROM health_media WHERE health_id=?",
|
"SELECT url, media_type FROM health_media WHERE health_id=?",
|
||||||
(he['id'],)
|
(he['id'],))
|
||||||
).fetchall()
|
|
||||||
he_dict['media'] = [dict(m) for m in media]
|
|
||||||
health_out.append(he_dict)
|
|
||||||
|
|
||||||
# Trainingsfortschritt
|
dog['tagebuch'] = diary_rows
|
||||||
progress = conn.execute(
|
dog['gesundheit'] = health_rows
|
||||||
|
dog['trainingsfortschritt'] = _q(conn,
|
||||||
"SELECT exercise_id, status, updated_at FROM exercise_progress "
|
"SELECT exercise_id, status, updated_at FROM exercise_progress "
|
||||||
"WHERE dog_id=?", (did,)
|
"WHERE dog_id=?", (did,))
|
||||||
).fetchall()
|
dog['ausgaben'] = _q(conn,
|
||||||
|
"SELECT datum, betrag, kategorie, notiz FROM expenses "
|
||||||
# Ausgaben
|
"WHERE dog_id=?", (did,))
|
||||||
expenses = conn.execute(
|
dog['verhaltensprotokoll'] = _q(conn,
|
||||||
"SELECT datum, betrag, kategorie, notiz, is_recurring "
|
|
||||||
"FROM expenses WHERE dog_id=?", (did,)
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
# Verhalten
|
|
||||||
behavior = conn.execute(
|
|
||||||
"SELECT datum, uhrzeit, kategorie, intensitaet, trigger, notiz "
|
"SELECT datum, uhrzeit, kategorie, intensitaet, trigger, notiz "
|
||||||
"FROM behavior_log WHERE dog_id=?", (did,)
|
"FROM behavior_log WHERE dog_id=?", (did,))
|
||||||
).fetchall()
|
dog['versicherung'] = _q(conn,
|
||||||
|
|
||||||
# Versicherung
|
|
||||||
insurance = conn.execute(
|
|
||||||
"SELECT anbieter, police_nr, jahresbeitrag, kontakt, ablaufdatum, notizen "
|
"SELECT anbieter, police_nr, jahresbeitrag, kontakt, ablaufdatum, notizen "
|
||||||
"FROM dog_insurance WHERE dog_id=?", (did,)
|
"FROM dog_insurance WHERE dog_id=?", (did,))
|
||||||
).fetchall()
|
dog['ernaehrungsprofil'] = _q1(conn,
|
||||||
|
|
||||||
# Ernährungs-Profil
|
|
||||||
ern = conn.execute(
|
|
||||||
"SELECT futter_typ, marke, kcal_tag, portionen, notizen "
|
"SELECT futter_typ, marke, kcal_tag, portionen, notizen "
|
||||||
"FROM futter_profil WHERE dog_id=?", (did,)
|
"FROM futter_profil WHERE dog_id=?", (did,))
|
||||||
).fetchone()
|
dog['futter_eintraege'] = _q(conn,
|
||||||
|
|
||||||
# Futter-Einträge
|
|
||||||
futter = conn.execute(
|
|
||||||
"SELECT datum, uhrzeit, futter_name, futter_typ, menge_g, notiz "
|
"SELECT datum, uhrzeit, futter_name, futter_typ, menge_g, notiz "
|
||||||
"FROM futter_eintraege WHERE dog_id=?", (did,)
|
"FROM futter_eintraege WHERE dog_id=?", (did,))
|
||||||
).fetchall()
|
dog['futter_reaktionen'] = _q(conn,
|
||||||
|
|
||||||
# Futter-Reaktionen
|
|
||||||
reaktionen = conn.execute(
|
|
||||||
"SELECT datum, uhrzeit, reaktion_typ, intensitaet, notiz "
|
"SELECT datum, uhrzeit, reaktion_typ, intensitaet, notiz "
|
||||||
"FROM futter_reaktionen WHERE dog_id=?", (did,)
|
"FROM futter_reaktionen WHERE dog_id=?", (did,))
|
||||||
).fetchall()
|
dog['routen'] = _q(conn,
|
||||||
|
"SELECT r.name, r.distanz_km, date(r.created_at) AS datum "
|
||||||
# Routen (via route_dogs)
|
|
||||||
routes = conn.execute(
|
|
||||||
"SELECT r.name, r.distanz_km, r.gps_track IS NOT NULL AS hat_track, "
|
|
||||||
"date(r.created_at) AS datum "
|
|
||||||
"FROM routes r JOIN route_dogs rd ON rd.route_id=r.id "
|
"FROM routes r JOIN route_dogs rd ON rd.route_id=r.id "
|
||||||
"WHERE rd.dog_id=?", (did,)
|
"WHERE rd.dog_id=?", (did,))
|
||||||
).fetchall()
|
dogs_out.append(dog)
|
||||||
|
|
||||||
d['tagebuch'] = diary_out
|
forum = _q(conn,
|
||||||
d['gesundheit'] = [dict(h) for h in health_out]
|
|
||||||
d['trainingsfortschritt'] = [dict(p) for p in progress]
|
|
||||||
d['ausgaben'] = [dict(e) for e in expenses]
|
|
||||||
d['verhaltensprotokoll'] = [dict(b) for b in behavior]
|
|
||||||
d['versicherung'] = [dict(i) for i in insurance]
|
|
||||||
d['ernaehrungsprofil'] = dict(ern) if ern else None
|
|
||||||
d['futter_eintraege'] = [dict(f) for f in futter]
|
|
||||||
d['futter_reaktionen'] = [dict(r) for r in reaktionen]
|
|
||||||
d['routen'] = [dict(r) for r in routes]
|
|
||||||
dogs_out.append(d)
|
|
||||||
|
|
||||||
# --- Forum-Beiträge ---
|
|
||||||
forum = conn.execute(
|
|
||||||
"SELECT ft.title, fp.content, fp.created_at, "
|
"SELECT ft.title, fp.content, fp.created_at, "
|
||||||
"CASE WHEN fp.parent_id IS NULL THEN 'Thread' ELSE 'Antwort' END AS art "
|
"CASE WHEN fp.parent_id IS NULL THEN 'Thread' ELSE 'Antwort' END AS art "
|
||||||
"FROM forum_posts fp "
|
"FROM forum_posts fp LEFT JOIN forum_threads ft ON ft.id=fp.thread_id "
|
||||||
"LEFT JOIN forum_threads ft ON ft.id = fp.thread_id "
|
"WHERE fp.user_id=? ORDER BY fp.created_at DESC", (uid,))
|
||||||
"WHERE fp.user_id=? ORDER BY fp.created_at DESC",
|
|
||||||
(uid,)
|
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
# --- Gassi-Teilnahmen ---
|
walk_participations = _q(conn,
|
||||||
walk_participations = conn.execute(
|
|
||||||
"SELECT w.titel, w.datum, w.uhrzeit, w.ort_name "
|
"SELECT w.titel, w.datum, w.uhrzeit, w.ort_name "
|
||||||
"FROM walk_participants wp JOIN walks w ON w.id=wp.walk_id "
|
"FROM walk_participants wp JOIN walks w ON w.id=wp.walk_id "
|
||||||
"WHERE wp.user_id=?", (uid,)
|
"WHERE wp.user_id=?", (uid,))
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
# --- Gassi-Fotos ---
|
walk_photos = _q(conn,
|
||||||
walk_photos = conn.execute(
|
|
||||||
"SELECT wp.url, w.datum AS walk_datum, w.titel AS walk_titel, wp.created_at "
|
"SELECT wp.url, w.datum AS walk_datum, w.titel AS walk_titel, wp.created_at "
|
||||||
"FROM walk_photos wp JOIN walks w ON w.id=wp.walk_id "
|
"FROM walk_photos wp JOIN walks w ON w.id=wp.walk_id "
|
||||||
"WHERE wp.user_id=?", (uid,)
|
"WHERE wp.user_id=?", (uid,))
|
||||||
).fetchall()
|
|
||||||
|
|
||||||
# --- Push-Subscriptions (Anzahl, kein raw endpoint) ---
|
push_count = _q1(conn,
|
||||||
push_count = conn.execute(
|
"SELECT COUNT(*) AS n FROM push_subscriptions WHERE user_id=?",
|
||||||
"SELECT COUNT(*) FROM push_subscriptions WHERE user_id=?", (uid,)
|
(uid,))
|
||||||
).fetchone()[0]
|
push_count = (push_count or {}).get('n', 0)
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
"export_erstellt": _dt.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
"export_erstellt": _dt.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
|
|
||||||
|
|
@ -101,9 +101,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css?v=881">
|
<link rel="stylesheet" href="/css/design-system.css?v=882">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=881">
|
<link rel="stylesheet" href="/css/layout.css?v=882">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=881">
|
<link rel="stylesheet" href="/css/components.css?v=882">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -583,10 +583,10 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=881"></script>
|
<script src="/js/api.js?v=882"></script>
|
||||||
<script src="/js/ui.js?v=881"></script>
|
<script src="/js/ui.js?v=882"></script>
|
||||||
<script src="/js/app.js?v=881"></script>
|
<script src="/js/app.js?v=882"></script>
|
||||||
<script src="/js/worlds.js?v=881"></script>
|
<script src="/js/worlds.js?v=882"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '881'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '882'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v881';
|
const CACHE_VERSION = 'by-v882';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue