diff --git a/backend/database.py b/backend/database.py index 3d9757f..50f50ee 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2210,22 +2210,6 @@ def _migrate(conn_factory): 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(""" diff --git a/backend/main.py b/backend/main.py index 45826e0..581b849 100644 --- a/backend/main.py +++ b/backend/main.py @@ -354,59 +354,29 @@ _MIME_MAP = { ".webm": "video/webm", ".pdf": "application/pdf", } -from fastapi import Request as _Request -from fastapi.responses import FileResponse as _FileResponse -from auth import decode_token as _decode_token +if STAGING and os.path.isdir(PROD_MEDIA_DIR): + # Staging: eigene Uploads in MEDIA_DIR, Fallback auf Prod-Medien (read-only) + from fastapi.responses import FileResponse as _FileResponse -# Pfade die Login erfordern (kein DB-Lookup — UUID in Dateiname schützt ausreichend) -_AUTH_REQUIRED = ("diary/", "health/", "walks/") + def _media_response(filepath: str): + ext = os.path.splitext(filepath)[1].lower() + mt = _MIME_MAP.get(ext, "application/octet-stream") + return _FileResponse(filepath, media_type=mt) + @app.api_route("/media/{path:path}", methods=["GET", "HEAD"]) + async def serve_media_staging(path: str): + staging_file = os.path.join(MEDIA_DIR, path) + if os.path.isfile(staging_file): + return _media_response(staging_file) + prod_file = os.path.join(PROD_MEDIA_DIR, path) + if os.path.isfile(prod_file): + return _media_response(prod_file) + from fastapi import HTTPException as _HE + raise _HE(404, "Media not found") +else: + app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -def _is_logged_in(request: _Request) -> bool: - token = request.cookies.get("by_token") - if not token: - return False - try: - _decode_token(token) - return True - except Exception: - return False - - -def _media_response(filepath: str): - ext = os.path.splitext(filepath)[1].lower() - mt = _MIME_MAP.get(ext, "application/octet-stream") - return _FileResponse(filepath, media_type=mt, - headers={"Cache-Control": "private, max-age=3600"}) - - -def _resolve_media_path(path: str) -> str | None: - primary = os.path.join(MEDIA_DIR, path) - if os.path.isfile(primary): - return primary - if STAGING and os.path.isdir(PROD_MEDIA_DIR): - fallback = os.path.join(PROD_MEDIA_DIR, path) - if os.path.isfile(fallback): - return fallback - return None - - -@app.api_route("/media/{path:path}", methods=["GET", "HEAD"]) -async def serve_media(path: str, request: _Request): - from fastapi import HTTPException as _HE - - prefix = path.split("/")[0] + "/" - - # Sensible Pfade: Login erforderlich — UUID-basierte Dateinamen verhindern Raten - if prefix in _AUTH_REQUIRED and not _is_logged_in(request): - raise _HE(401, "Anmeldung erforderlich.") - - filepath = _resolve_media_path(path) - if not filepath: - raise _HE(404, "Nicht gefunden.") - return _media_response(filepath) - -APP_VER = "883" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "877" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/profile.py b/backend/routes/profile.py index a6f3a5b..783951f 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -167,134 +167,3 @@ async def delete_account(user=Depends(get_current_user)): conn.execute("DELETE FROM forum_posts WHERE user_id=?", (uid,)) conn.execute("DELETE FROM users WHERE id=?", (uid,)) return {"status": "deleted"} - - -# ---------------------------------------------------------- -# GET /profile/export — DSGVO Datenexport (Art. 20) -# ---------------------------------------------------------- -@router.get('/export') -async def export_user_data(user=Depends(get_current_user)): - """Gibt alle personenbezogenen Daten des Users als JSON zurück (Art. 20 DSGVO).""" - import json as _json - from datetime import datetime as _dt - 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'] - - with db() as conn: - # Nutzerprofil - u = _q1(conn, - "SELECT id, name, email, bio, wohnort, erfahrung, social_link, " - "email_verified, is_premium, subscription_tier, created_at " - "FROM users WHERE id=?", (uid,)) or {} - - # Hunde - dogs_raw = _q(conn, "SELECT * FROM dogs WHERE user_id=?", (uid,)) - dogs_out = [] - - for dog in dogs_raw: - did = dog['id'] - - # Tagebuch (nur vorhandene Spalten) - diary_rows = _q(conn, - "SELECT id, datum, typ, titel, text, gps_lat, gps_lon, " - "is_milestone, created_at FROM diary WHERE dog_id=?", (did,)) - for de in diary_rows: - # diary_media: preview_url existiert nicht → url + media_type - de['media'] = _q(conn, - "SELECT url, media_type FROM diary_media WHERE diary_id=?", - (de['id'],)) - - # Gesundheit (alle via Migration ergänzten Spalten schützen) - health_rows = _q(conn, - "SELECT id, typ, bezeichnung, datum, naechstes, notiz FROM health " - "WHERE dog_id=?", (did,)) - for he in health_rows: - he['media'] = _q(conn, - "SELECT url, media_type FROM health_media WHERE health_id=?", - (he['id'],)) - - dog['tagebuch'] = diary_rows - dog['gesundheit'] = health_rows - dog['trainingsfortschritt'] = _q(conn, - "SELECT exercise_id, status, updated_at FROM exercise_progress " - "WHERE dog_id=?", (did,)) - dog['ausgaben'] = _q(conn, - "SELECT datum, betrag, kategorie, notiz FROM expenses " - "WHERE dog_id=?", (did,)) - dog['verhaltensprotokoll'] = _q(conn, - "SELECT datum, uhrzeit, kategorie, intensitaet, trigger, notiz " - "FROM behavior_log WHERE dog_id=?", (did,)) - dog['versicherung'] = _q(conn, - "SELECT anbieter, police_nr, jahresbeitrag, kontakt, ablaufdatum, notizen " - "FROM dog_insurance WHERE dog_id=?", (did,)) - dog['ernaehrungsprofil'] = _q1(conn, - "SELECT futter_typ, marke, kcal_tag, portionen, notizen " - "FROM futter_profil WHERE dog_id=?", (did,)) - dog['futter_eintraege'] = _q(conn, - "SELECT datum, uhrzeit, futter_name, futter_typ, menge_g, notiz " - "FROM futter_eintraege WHERE dog_id=?", (did,)) - dog['futter_reaktionen'] = _q(conn, - "SELECT datum, uhrzeit, reaktion_typ, intensitaet, notiz " - "FROM futter_reaktionen WHERE dog_id=?", (did,)) - dog['routen'] = _q(conn, - "SELECT r.name, r.distanz_km, date(r.created_at) AS datum " - "FROM routes r JOIN route_dogs rd ON rd.route_id=r.id " - "WHERE rd.dog_id=?", (did,)) - dogs_out.append(dog) - - forum = _q(conn, - "SELECT ft.title, fp.content, fp.created_at, " - "CASE WHEN fp.parent_id IS NULL THEN 'Thread' ELSE 'Antwort' END AS art " - "FROM forum_posts fp LEFT JOIN forum_threads ft ON ft.id=fp.thread_id " - "WHERE fp.user_id=? ORDER BY fp.created_at DESC", (uid,)) - - walk_participations = _q(conn, - "SELECT w.titel, w.datum, w.uhrzeit, w.ort_name " - "FROM walk_participants wp JOIN walks w ON w.id=wp.walk_id " - "WHERE wp.user_id=?", (uid,)) - - walk_photos = _q(conn, - "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 " - "WHERE wp.user_id=?", (uid,)) - - push_count = _q1(conn, - "SELECT COUNT(*) AS n FROM push_subscriptions WHERE user_id=?", - (uid,)) - push_count = (push_count or {}).get('n', 0) - - export = { - "export_erstellt": _dt.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - "hinweis": "Dieser Export enthält alle personenbezogenen Daten deines Ban-Yaro-Kontos gemäß Art. 20 DSGVO.", - "profil": u, - "hunde": dogs_out, - "forum_beitraege": [dict(f) for f in forum], - "gassi_teilnahmen": [dict(w) for w in walk_participations], - "gassi_fotos": [dict(p) for p in walk_photos], - "push_subscriptions": push_count, - } - - content = _json.dumps(export, ensure_ascii=False, indent=2, default=str) - today = _dt.utcnow().strftime("%Y-%m-%d") - filename = f"banyaro-export-{today}.json" - return _Response( - content = content, - media_type = "application/json", - headers = {"Content-Disposition": f'attachment; filename="{filename}"'}, - ) diff --git a/backend/routes/walks.py b/backend/routes/walks.py index 3a0c48b..85bea3d 100644 --- a/backend/routes/walks.py +++ b/backend/routes/walks.py @@ -1,17 +1,15 @@ """BAN YARO — Gassi-Treffen""" -import math, os, uuid +import math import httpx from datetime import date -from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional, List from database import db from auth import get_current_user from routes.push import send_push_to_user -MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") - router = APIRouter() @@ -373,34 +371,9 @@ async def get_walk(walk_id: int): GROUP BY wp.user_id """, (walk_id,)).fetchall() - # Hunde-Details (Foto + Rasse) pro Teilnehmer - dog_rows = conn.execute(""" - SELECT wpd.user_id, d.name AS dog_name, d.foto_url, d.rasse - FROM walk_participant_dogs wpd - JOIN dogs d ON d.id = wpd.dog_id - WHERE wpd.walk_id = ? - """, (walk_id,)).fetchall() - - # Walk-Fotos - photos = conn.execute( - "SELECT id, user_id, url, created_at FROM walk_photos WHERE walk_id=? ORDER BY created_at", - (walk_id,) - ).fetchall() - - from collections import defaultdict - dogs_by_user = defaultdict(list) - for r in dog_rows: - dogs_by_user[r['user_id']].append({ - 'name': r['dog_name'], 'foto_url': r['foto_url'], 'rasse': r['rasse'] - }) - result = dict(walk) - result['teilnehmer'] = [ - {**dict(p), 'hunde_liste': dogs_by_user.get(p['user_id'], [])} - for p in participants - ] + result['teilnehmer'] = [dict(p) for p in participants] result['teilnehmer_count'] = len(result['teilnehmer']) - result['photos'] = [dict(p) for p in photos] return result @@ -535,91 +508,3 @@ async def leave_walk(walk_id: int, user=Depends(get_current_user)): conn.execute("UPDATE walks SET status = 'offen' WHERE id = ?", (walk_id,)) return {"status": "left", "teilnehmer_count": count} - - -# ------------------------------------------------------------------ -# POST /api/walks/{id}/photos — Foto nach dem Treffen hochladen -# GET /api/walks/{id}/photos — Fotos eines Treffens abrufen -# ------------------------------------------------------------------ -@router.post("/{walk_id}/photos", status_code=201) -async def upload_walk_photo( - walk_id: int, - file: UploadFile = File(...), - user=Depends(get_current_user) -): - with db() as conn: - walk = conn.execute("SELECT * FROM walks WHERE id=?", (walk_id,)).fetchone() - if not walk: - raise HTTPException(404, "Treffen nicht gefunden.") - # Nur Teilnehmer oder Veranstalter dürfen Fotos hochladen - is_participant = conn.execute( - "SELECT 1 FROM walk_participants WHERE walk_id=? AND user_id=?", - (walk_id, user['id']) - ).fetchone() - if not is_participant and walk['user_id'] != user['id']: - raise HTTPException(403, "Nur Teilnehmer können Fotos hochladen.") - - import io - from PIL import Image, ImageOps - try: - import pillow_heif; pillow_heif.register_heif_opener() - except ImportError: - pass - - raw = await file.read() - try: - img = Image.open(io.BytesIO(raw)) - img = ImageOps.exif_transpose(img).convert("RGB") - buf = io.BytesIO() - img.save(buf, format="JPEG", quality=88) - raw = buf.getvalue() - except Exception: - pass - - import re as _re - walk_datum = walk['datum'] or "0000-00-00" # YYYY-MM-DD - uname_raw = (user.get('name') or 'user').lower() - uname_safe = _re.sub(r'[^a-z0-9]', '-', uname_raw)[:20].strip('-') - with db() as conn: - count = conn.execute( - "SELECT COUNT(*) FROM walk_photos WHERE walk_id=? AND user_id=?", - (walk_id, user['id']) - ).fetchone()[0] - filename = f"{walk_datum}-{uname_safe}-{count + 1:03d}.jpg" - path = os.path.join(MEDIA_DIR, "walks", filename) - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "wb") as f: - f.write(raw) - - url = f"/media/walks/{filename}" - with db() as conn: - cur = conn.execute( - "INSERT INTO walk_photos (walk_id, user_id, url) VALUES (?,?,?)", - (walk_id, user['id'], url) - ) - row = conn.execute("SELECT * FROM walk_photos WHERE id=?", (cur.lastrowid,)).fetchone() - return dict(row) - - -@router.get("/{walk_id}/photos") -async def get_walk_photos(walk_id: int): - with db() as conn: - rows = conn.execute( - "SELECT * FROM walk_photos WHERE walk_id=? ORDER BY created_at", - (walk_id,) - ).fetchall() - return [dict(r) for r in rows] - - -@router.delete("/{walk_id}/photos/{photo_id}", status_code=204) -async def delete_walk_photo(walk_id: int, photo_id: int, user=Depends(get_current_user)): - with db() as conn: - photo = conn.execute( - "SELECT * FROM walk_photos WHERE id=? AND walk_id=?", (photo_id, walk_id) - ).fetchone() - if not photo: - raise HTTPException(404) - walk = conn.execute("SELECT user_id FROM walks WHERE id=?", (walk_id,)).fetchone() - if photo['user_id'] != user['id'] and walk['user_id'] != user['id']: - raise HTTPException(403) - conn.execute("DELETE FROM walk_photos WHERE id=?", (photo_id,)) diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 325fc8b..0509d07 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -3539,50 +3539,6 @@ html.modal-open { #walks-map-view { overflow: hidden; } - -/* ── Desktop ≥1024px: Liste links, Karte rechts nebeneinander ── */ -@media (min-width: 1024px) { - /* Tab-Treffen: horizontal splitten */ - #walks-tab-treffen { - flex-direction: row; - overflow: hidden; - } - /* Toolbar über beiden Spalten → eigene Zeile */ - #walks-tab-treffen .by-toolbar { - position: absolute; - top: 0; - left: 0; - right: 0; - z-index: 10; - background: var(--c-bg); - border-bottom: 1px solid var(--c-border); - padding: var(--space-2) var(--space-4); - } - /* Platz für die fixe Toolbar oben */ - #walks-tab-treffen { - padding-top: 52px; - position: relative; - } - /* Liste: feste Breite links, scrollbar */ - #walks-list-view { - display: block !important; - width: 380px; - min-width: 320px; - flex-shrink: 0; - border-right: 1px solid var(--c-border); - overflow-y: auto; - } - /* Karte: Rest des Platzes, sticky */ - #walks-map-view { - display: block !important; - flex: 1; - min-width: 0; - } - /* Liste/Karte-Toggle auf Desktop ausblenden */ - .walks-view-toggle { - display: none; - } -} .walks-participant { display: flex; align-items: center; diff --git a/backend/static/index.html b/backend/static/index.html index 6413439..eca24fc 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -583,10 +583,10 @@ - - - - + + + + diff --git a/backend/static/js/api.js b/backend/static/js/api.js index 7a341a1..b383d66 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -346,9 +346,6 @@ const API = (() => { invite(id, friendId) { return post(`/walks/${id}/invite`, { friend_id: friendId }); }, rsvp(id, status) { return post(`/walks/${id}/rsvp`, { status }); }, participants(id) { return get(`/walks/${id}/participants`); }, - photos(id) { return get(`/walks/${id}/photos`); }, - uploadPhoto(id, formData) { return upload(`/walks/${id}/photos`, formData); }, - deletePhoto(id, photoId) { return del(`/walks/${id}/photos/${photoId}`); }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 95691e8..76478d3 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '883'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '877'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/datenschutz.js b/backend/static/js/pages/datenschutz.js index 425536f..d2a8af8 100644 --- a/backend/static/js/pages/datenschutz.js +++ b/backend/static/js/pages/datenschutz.js @@ -102,20 +102,13 @@ window.Page_datenschutz = (() => {

Als Ausweichlösung bei Nichtverfügbarkeit des lokalen Modells wird Claude Sonnet 4.6 von Anthropic, PBC (San Francisco, USA) genutzt. - In diesem Fall wird der Inhalt deiner Anfrage übermittelt. Bei Gesundheits- und - Ernährungsberichten kann dies Hundedaten (Name, Rasse, Gewicht, Impfhistorie, - Medikamente, Allergien) als Teil des Anfragetextes umfassen. Die Übermittlung - in die USA erfolgt auf Basis der EU-Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO). + In diesem Fall wird ausschließlich der Inhalt deiner Anfrage (Prompt-Text) übermittelt — + keine Account- oder Profildaten. Die Übermittlung in die USA erfolgt auf Basis der + EU-Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO). Datenschutzerklärung von Anthropic: anthropic.com/privacy.

-

- Die Rassenerkennung per Foto sendet das hochgeladene Bild direkt an - Claude von Anthropic (USA) zur Analyse — es gibt hierfür keinen lokalen Fallback. - Das Foto wird nicht dauerhaft bei Anthropic gespeichert. Rechtsgrundlage: Einwilligung - gem. Art. 6 Abs. 1 lit. a DSGVO durch aktive Nutzung der Funktion. -

Der KI-Trainer analysiert deinen bisherigen Trainingsfortschritt (Übungshistorie, Erfolgsquoten, Streaks) und gibt personalisierte Empfehlungen. @@ -129,31 +122,19 @@ window.Page_datenschutz = (() => { findet nicht statt.

`)} - ${sec('Wetterdaten & Kartendienste', ` + ${sec('Wetterdaten (Open-Meteo)', `

- Die Wetter-Funktion übermittelt auf Wunsch deine GPS-Koordinaten serverseitig an - Open-Meteo (Österreich, DSGVO-konform) für die Wettervorhersage. - Für Wetter-Kartenlayer (Regenradar, Temperaturen) werden Kacheln von - OpenWeatherMap (OpenWeather Ltd., UK/USA) geladen — dabei wird - dein Browser direkt kontaktiert. Es werden keine Account-Daten übermittelt. + Die Wetter-Funktion übermittelt auf Wunsch deine GPS-Koordinaten einmalig an + Open-Meteo (Österreich, DSGVO-konform), um die lokale + Wettervorhersage abzurufen. Es werden ausschließlich anonyme Koordinaten übertragen — + keine Account- oder Profildaten. Open-Meteo protokolliert keine personenbezogenen + Daten. Die Funktion wird nur aktiv, wenn du deinen Standort im Browser freigibst. Rechtsgrundlage: Einwilligung gem. Art. 6 Abs. 1 lit. a DSGVO.

- Für die automatische Ortsnamens-Ermittlung (z. B. im Wetter-Detail) werden deine - GPS-Koordinaten serverseitig an Nominatim der OpenStreetMap Foundation - (UK) übermittelt. Es werden ausschließlich Koordinaten weitergegeben — keine - personenbezogenen Daten. -

-

- Datenschutzerklärung Open-Meteo: + Datenschutzerklärung von Open-Meteo: open-meteo.com/en/terms · - OpenWeatherMap: - openweathermap.org/privacy-policy · - OpenStreetMap/Nominatim: - osmfoundation.org + style="${S.a}">open-meteo.com/en/terms

`)} ${sec('Routenvorschläge (OpenRouteService)', ` @@ -219,16 +200,7 @@ window.Page_datenschutz = (() => { (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18) sowie Datenportabilität (Art. 20). Erteilte Einwilligungen kannst du jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3 DSGVO). -

-

- Datenexport (Art. 20 DSGVO): Du kannst jederzeit unter - Einstellungen → „Meine Daten exportieren" eine vollständige Kopie deiner - gespeicherten Daten als JSON-Datei herunterladen. Der Export enthält Profildaten, - Hundedaten, Tagebuch, Gesundheitseinträge, Trainingsfortschritt, Ausgaben, - Verhaltensprotokoll, Forum-Beiträge und Gassi-Teilnahmen. -

-

- Zur Ausübung weiterer Rechte wende dich per E-Mail an + Zur Ausübung deiner Rechte wende dich per E-Mail an hallo@banyaro.app.

Du hast außerdem das Recht, bei der zuständigen Datenschutz-Aufsichtsbehörde Beschwerde einzulegen:
@@ -240,14 +212,14 @@ window.Page_datenschutz = (() => { ${sec('Speicherdauer', `

- Deine Daten werden vollständig gelöscht, sobald du deinen Account löschst — - einschließlich Tagebuch, Gesundheitseinträge, Fotos, Forenbeiträge und Hundeprofil. - Es gibt keine anonymisierte Weiterverarbeitung deiner Inhalte nach Account-Löschung. - Server-Logs werden nach 30 Tagen rotiert. + Deine Daten werden gelöscht, sobald du deinen Account löschst. Server-Logs + werden nach 30 Tagen automatisch gelöscht. Öffentlich gepostete Inhalte + (Forenbeiträge, Giftköder-Meldungen) bleiben nach Account-Löschung anonymisiert + erhalten, sofern sie für die Community relevant sind.

`)}

- Stand: Mai 2026 · Version 2 + Stand: Mai 2026

diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 8767cc6..73e8ee5 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -294,15 +294,6 @@ window.Page_settings = (() => { Abmelden - - ` : ''} - `).join('')} - - -

Veranstaltet von ${UI.escape(walk.veranstalter_name || 'Unbekannt')}

@@ -570,49 +525,6 @@ window.Page_walks = (() => { document.getElementById('wd-close')?.addEventListener('click', UI.modal.close); - // Foto-Upload - document.getElementById('wd-photo-input')?.addEventListener('change', async function() { - if (!this.files.length) return; - const file = this.files[0]; - const formData = new FormData(); - formData.append('file', file); - try { - const photo = await API.walks.uploadPhoto(walk.id, formData); - const grid = document.getElementById('wd-photos-grid'); - if (grid) { - grid.querySelector('p')?.remove(); - const div = document.createElement('div'); - div.style.cssText = 'position:relative;aspect-ratio:1'; - div.innerHTML = ` - - `; - grid.appendChild(div); - _bindPhotoDel(walk.id, div); - UI.toast.success('Foto hochgeladen.'); - } - } catch (err) { - UI.toast.error(err.message || 'Fehler beim Hochladen.'); - } - this.value = ''; - }); - - // Foto löschen — alle bestehenden Buttons - function _bindPhotoDel(walkId, container) { - container.querySelectorAll('.wd-photo-del').forEach(btn => { - btn.addEventListener('click', async () => { - if (!window.confirm('Foto löschen?')) return; - try { - await API.walks.deletePhoto(walkId, parseInt(btn.dataset.photoId)); - btn.closest('[style*="aspect-ratio"]')?.remove(); - UI.toast.success('Foto gelöscht.'); - } catch (err) { UI.toast.error(err.message || 'Fehler.'); } - }); - }); - } - _bindPhotoDel(walk.id, document); - document.getElementById('wd-login')?.addEventListener('click', () => { UI.modal.close(); App.navigate('settings'); diff --git a/backend/static/sw.js b/backend/static/sw.js index c6d2171..d6ce60f 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v883'; +const CACHE_VERSION = 'by-v877'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache