diff --git a/backend/database.py b/backend/database.py
index eeb1add..1a70aa5 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -1747,71 +1747,6 @@ def _migrate(conn_factory):
)
""")
- 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 (
diff --git a/backend/main.py b/backend/main.py
index 229a856..8b259f7 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -11,7 +11,6 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse, JSONResponse
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.gzip import GZipMiddleware
-from brotli_asgi import BrotliMiddleware
from contextlib import asynccontextmanager
from database import init_db
@@ -135,7 +134,6 @@ class MediaCacheMiddleware(BaseHTTPMiddleware):
return response
app.add_middleware(MediaCacheMiddleware)
-app.add_middleware(BrotliMiddleware, minimum_size=1000, quality=4)
app.add_middleware(GZipMiddleware, minimum_size=1000)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index 414ec32..c4e830c 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -15,4 +15,3 @@ odfpy==1.4.1
polyline==2.0.2
fpdf2==2.8.3
python-dateutil>=2.9
-brotli-asgi==1.4.0
diff --git a/backend/routes/admin.py b/backend/routes/admin.py
index c2ffebb..cd3fee1 100644
--- a/backend/routes/admin.py
+++ b/backend/routes/admin.py
@@ -356,15 +356,11 @@ async def list_users(
# ------------------------------------------------------------------
@router.patch("/users/{uid}")
async def patch_user(uid: int, data: UserPatch, user=Depends(require_mod)):
- # Rollenwechsel + Privileg-Flags nur für Admins
+ # Rollenwechsel nur für Admins
if data.rolle is not None and user["rolle"] != "admin":
raise HTTPException(403, "Rollenwechsel nur für Admins.")
if data.rolle and data.rolle not in ("user", "moderator", "admin"):
raise HTTPException(400, "Ungültige Rolle.")
- if data.is_moderator is not None and user["rolle"] != "admin":
- raise HTTPException(403, "is_moderator darf nur von Admins geändert werden.")
- if data.is_social_media is not None and user["rolle"] != "admin":
- raise HTTPException(403, "is_social_media darf nur von Admins geändert werden.")
with db() as conn:
target = conn.execute("SELECT id, rolle, name FROM users WHERE id=?", (uid,)).fetchone()
diff --git a/backend/routes/adoption.py b/backend/routes/adoption.py
index bde0986..d742ccc 100644
--- a/backend/routes/adoption.py
+++ b/backend/routes/adoption.py
@@ -13,17 +13,10 @@ import os
import math
import logging
import asyncio
-import uuid
import httpx
from datetime import datetime, timedelta
-from fastapi import APIRouter, Query, BackgroundTasks, Depends, Form, UploadFile, File, HTTPException
-from pydantic import BaseModel
-from typing import Optional
+from fastapi import APIRouter, Query, BackgroundTasks
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")
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -297,251 +290,3 @@ async def adoption_geocode(plz: str = Query(..., min_length=4, max_length=10)):
except Exception as e:
logger.warning(f"Geocode PLZ {plz}: {e}")
return {"lat": None, "lon": None, "display": plz}
-
-
-# ==================================================================
-# Community Adoption — Privates Weitervermittlungs-Board
-# ==================================================================
-
-class InterestBody(BaseModel):
- nachricht: Optional[str] = None
-
-
-# ------------------------------------------------------------------
-# GET /api/adoption/community/my — eigene Inserate
-# ------------------------------------------------------------------
-@router.get("/community/my")
-def community_my(user=Depends(get_current_user)):
- with db() as conn:
- rows = conn.execute("""
- SELECT ca.*,
- u.name AS besitzer_name,
- (SELECT COUNT(*) FROM community_adoption_interest i WHERE i.listing_id = ca.id) AS interest_count
- FROM community_adoption ca
- JOIN users u ON u.id = ca.user_id
- WHERE ca.user_id = ? AND ca.status != 'deleted'
- ORDER BY ca.created_at DESC
- """, (user["id"],)).fetchall()
- return [dict(r) for r in rows]
-
-
-# ------------------------------------------------------------------
-# GET /api/adoption/community — alle aktiven Inserate (mit optionaler Nähe)
-# ------------------------------------------------------------------
-@router.get("/community")
-def community_list(
- lat: Optional[float] = Query(None),
- lon: Optional[float] = Query(None),
- radius: float = Query(200.0, description="Radius in km (default 200)"),
- user=Depends(get_current_user),
-):
- with db() as conn:
- rows = conn.execute("""
- SELECT ca.*,
- u.name AS besitzer_name,
- (SELECT COUNT(*) FROM community_adoption_interest i WHERE i.listing_id = ca.id) AS interest_count,
- (SELECT COUNT(*) FROM community_adoption_interest i2
- WHERE i2.listing_id = ca.id AND i2.user_id = ?) AS _user_interested
- FROM community_adoption ca
- JOIN users u ON u.id = ca.user_id
- WHERE ca.status = 'active'
- ORDER BY ca.created_at DESC
- LIMIT 50
- """, (user["id"],)).fetchall()
-
- result = []
- for row in rows:
- d = dict(row)
- d["user_interested"] = bool(d.pop("_user_interested", 0))
- if lat is not None and lon is not None and d.get("lat") and d.get("lon"):
- dist = _haversine(lat, lon, d["lat"], d["lon"])
- d["distanz_km"] = round(dist, 1)
- if dist > radius:
- continue
- else:
- d["distanz_km"] = None
- result.append(d)
-
- if lat is not None and lon is not None:
- result.sort(key=lambda x: x["distanz_km"] if x["distanz_km"] is not None else 9999)
-
- return result
-
-
-# ------------------------------------------------------------------
-# POST /api/adoption/community — Inserat erstellen
-# ------------------------------------------------------------------
-@router.post("/community", status_code=201)
-async def community_create(
- name: str = Form(...),
- beschreibung: str = Form(...),
- rasse: str = Form(""),
- alter_jahre: Optional[float] = Form(None),
- geschlecht: str = Form(""),
- gruende: str = Form(""),
- ort: str = Form(""),
- plz: str = Form(""),
- lat: Optional[float] = Form(None),
- lon: Optional[float] = Form(None),
- dog_id: Optional[int] = Form(None),
- foto: Optional[UploadFile] = File(None),
- user=Depends(get_current_user),
-):
- foto_url = None
-
- if foto and foto.filename:
- MAX_SIZE = 5 * 1024 * 1024
- header = await foto.read(12)
- if len(header) < 3:
- raise HTTPException(400, "Ungültige Datei")
- is_jpeg = header[:3] == b"\xff\xd8\xff"
- is_png = header[:4] == b"\x89PNG"
- is_webp = header[:4] == b"RIFF" and len(header) >= 12 and header[8:12] == b"WEBP"
- if not (is_jpeg or is_png or is_webp):
- raise HTTPException(400, "Nur JPEG, PNG oder WebP erlaubt")
- rest = await foto.read(MAX_SIZE)
- if len(rest) >= MAX_SIZE:
- raise HTTPException(400, "Foto zu groß (max 5 MB)")
- data = header + rest
-
- folder = os.path.join(MEDIA_DIR, "adoption")
- os.makedirs(folder, exist_ok=True)
- filename = f"{uuid.uuid4()}.jpg"
- filepath = os.path.join(folder, filename)
- with open(filepath, "wb") as f:
- f.write(data)
- foto_url = f"/media/adoption/{filename}"
-
- with db() as conn:
- cur = conn.execute("""
- INSERT INTO community_adoption
- (user_id, dog_id, name, rasse, alter_jahre, geschlecht,
- foto_url, beschreibung, gruende, ort, plz, lat, lon)
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
- """, (
- user["id"], dog_id, name, rasse or None, alter_jahre,
- geschlecht or None, foto_url, beschreibung,
- gruende or None, ort or None, plz or None, lat, lon,
- ))
- new_id = cur.lastrowid
- row = conn.execute(
- "SELECT * FROM community_adoption WHERE id = ?", (new_id,)
- ).fetchone()
- return dict(row)
-
-
-# ------------------------------------------------------------------
-# PATCH /api/adoption/community/{id} — Status ändern (nur Besitzer)
-# ------------------------------------------------------------------
-class _StatusBody(BaseModel):
- status: str
-
-@router.patch("/community/{listing_id}")
-def community_update_status(
- listing_id: int,
- body: _StatusBody,
- user=Depends(get_current_user),
-):
- allowed = {"active", "reserved", "vermittelt"}
- if body.status not in allowed:
- raise HTTPException(400, f"Status muss einer von {allowed} sein")
- status = body.status
- with db() as conn:
- cur = conn.execute("""
- UPDATE community_adoption
- SET status = ?, updated_at = datetime('now')
- WHERE id = ? AND user_id = ?
- """, (status, listing_id, user["id"]))
- if cur.rowcount == 0:
- raise HTTPException(404, "Inserat nicht gefunden oder kein Zugriff")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# DELETE /api/adoption/community/{id} — Soft-Delete (nur Besitzer)
-# ------------------------------------------------------------------
-@router.delete("/community/{listing_id}")
-def community_delete(listing_id: int, user=Depends(get_current_user)):
- with db() as conn:
- cur = conn.execute("""
- UPDATE community_adoption
- SET status = 'deleted', updated_at = datetime('now')
- WHERE id = ? AND user_id = ?
- """, (listing_id, user["id"]))
- if cur.rowcount == 0:
- raise HTTPException(404, "Inserat nicht gefunden oder kein Zugriff")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# POST /api/adoption/community/{id}/interest — Interesse bekunden
-# ------------------------------------------------------------------
-@router.post("/community/{listing_id}/interest", status_code=201)
-def community_interest(listing_id: int, body: InterestBody = None, user=Depends(get_current_user)):
- nachricht = (body.nachricht if body else None) or None
- with db() as conn:
- listing = conn.execute(
- "SELECT id, name, user_id FROM community_adoption WHERE id = ? AND status != 'deleted'",
- (listing_id,)
- ).fetchone()
- if not listing:
- raise HTTPException(404, "Inserat nicht gefunden")
- if listing["user_id"] == user["id"]:
- raise HTTPException(400, "Eigenes Inserat")
- try:
- conn.execute("""
- INSERT INTO community_adoption_interest (listing_id, user_id, nachricht)
- VALUES (?, ?, ?)
- """, (listing_id, user["id"], nachricht))
- except Exception:
- raise HTTPException(409, "Interesse bereits bekundet")
-
- try:
- send_push_to_user(listing["user_id"], {
- "title": "Jemand interessiert sich für deinen Hund \U0001f43e",
- "body": f"{user['name']} möchte mehr über {listing['name']} erfahren.",
- "url": "/#adoption",
- })
- except Exception as e:
- logger.warning(f"Push interest: {e}")
-
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# DELETE /api/adoption/community/{id}/interest — Interesse zurückziehen
-# ------------------------------------------------------------------
-@router.delete("/community/{listing_id}/interest")
-def community_interest_withdraw(listing_id: int, user=Depends(get_current_user)):
- with db() as conn:
- cur = conn.execute("""
- DELETE FROM community_adoption_interest
- WHERE listing_id = ? AND user_id = ?
- """, (listing_id, user["id"]))
- if cur.rowcount == 0:
- raise HTTPException(404, "Kein Interesse gefunden")
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# GET /api/adoption/community/{id}/interests — Interessenten (nur Besitzer)
-# ------------------------------------------------------------------
-@router.get("/community/{listing_id}/interests")
-def community_interests(listing_id: int, user=Depends(get_current_user)):
- with db() as conn:
- listing = conn.execute(
- "SELECT user_id FROM community_adoption WHERE id = ? AND status != 'deleted'",
- (listing_id,)
- ).fetchone()
- if not listing:
- raise HTTPException(404, "Inserat nicht gefunden")
- if listing["user_id"] != user["id"]:
- raise HTTPException(403, "Nur der Besitzer kann Interessenten sehen")
- rows = conn.execute("""
- SELECT i.id, i.nachricht, i.created_at, u.id AS user_id, u.name, u.avatar_url
- FROM community_adoption_interest i
- JOIN users u ON u.id = i.user_id
- WHERE i.listing_id = ?
- ORDER BY i.created_at ASC
- """, (listing_id,)).fetchall()
- return [dict(r) for r in rows]
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index 4772ae6..13d857d 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -26,14 +26,12 @@ _SMTP_READY = bool(os.getenv("SMTP_SUPPORT_USER") and os.getenv("SMTP_SUPPORT_P
def _send_verification_email(email: str, name: str, token: str):
if not _SMTP_READY:
return
- import html as _html
from routes.outreach import _send_smtp
from mailer import email_html
url = f"{_APP_URL}/api/auth/verify-email/{token}"
subject = "Ban Yaro — bitte bestätige deine E-Mail-Adresse"
- _ename = _html.escape(name)
body_html = f"""
-
Hallo {_ename} ,
+ Hallo {name} ,
willkommen bei Ban Yaro! Bitte bestätige deine E-Mail-Adresse, damit dein Konto aktiv wird.
@@ -308,15 +306,13 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
"UPDATE users SET password_reset_token=?, password_reset_expires=? WHERE id=?",
(token, expires, user["id"])
)
- import html as _html
app_url = os.getenv("APP_URL", "https://banyaro.app")
url = f"{app_url}/#reset-password?token={token}"
subject = "Ban Yaro — Passwort zurücksetzen"
from routes.outreach import _send_smtp
from mailer import email_html
- _ename = _html.escape(user['name'])
body_html = f"""
- Hallo {_ename} ,
+ Hallo {user['name']} ,
du hast eine Passwort-Zurücksetzen-Anfrage gestellt. Klicke auf den Button, um ein neues Passwort zu setzen.
diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py
index a44faa0..c7a9066 100644
--- a/backend/routes/dogs.py
+++ b/backend/routes/dogs.py
@@ -315,13 +315,11 @@ async def update_dog(dog_id: int, data: DogUpdate, user=Depends(get_current_user
values = list(fields.values()) + [dog_id, user["id"]]
with db() as conn:
- updated = conn.execute(
+ conn.execute(
f"UPDATE dogs SET {set_clause} WHERE id=? AND user_id=?", values
- ).rowcount
- if not updated:
- raise HTTPException(404, "Hund nicht gefunden.")
+ )
dog = conn.execute(
- "SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
+ "SELECT * FROM dogs WHERE id=?", (dog_id,)
).fetchone()
return dict(dog)
@@ -415,8 +413,8 @@ async def delete_photo(dog_id: int, user=Depends(get_current_user)):
os.remove(path)
with db() as conn:
conn.execute(
- "UPDATE dogs SET foto_url=NULL, foto_zoom=1.0, foto_offset_x=0.0, foto_offset_y=0.0 WHERE id=? AND user_id=?",
- (dog_id, user["id"])
+ "UPDATE dogs SET foto_url=NULL, foto_zoom=1.0, foto_offset_x=0.0, foto_offset_y=0.0 WHERE id=?",
+ (dog_id,)
)
diff --git a/backend/routes/forum.py b/backend/routes/forum.py
index 2834ab0..fe730d5 100644
--- a/backend/routes/forum.py
+++ b/backend/routes/forum.py
@@ -641,7 +641,7 @@ async def resolve_report(report_id: int, data: ResolveReport, user=Depends(get_c
# GET /api/forum/members/map
# ------------------------------------------------------------------
@router.get("/members/map")
-async def members_map(user=Depends(get_current_user)):
+async def members_map():
with db() as conn:
rows = conn.execute(
"""SELECT SUBSTR(name, 1, INSTR(name || ' ', ' ') - 1) AS vorname,
diff --git a/backend/routes/jobs.py b/backend/routes/jobs.py
index 59c73c2..8714ae2 100644
--- a/backend/routes/jobs.py
+++ b/backend/routes/jobs.py
@@ -1,6 +1,5 @@
"""BAN YARO — Social-Media-Job Bewerbungs-System"""
-import html as _html
import os
import uuid
from datetime import datetime, timedelta
@@ -99,9 +98,8 @@ async def apply(
# Bestätigungs-Mail an Bewerber
try:
- _name = _html.escape(name)
body = f"""
- Hallo {_name} ,
+ Hallo {name} ,
deine Bewerbung als Social-Media-Manager/in bei Ban Yaro ist bei uns eingegangen.
Wir melden uns bald bei dir!
@@ -112,7 +110,7 @@ async def apply(
email,
"Deine Bewerbung bei Ban Yaro 🐾",
email_html(body, cta_url="https://banyaro.app", cta_label="Zur App"),
- f"Hallo {_name}, deine Bewerbung ist eingegangen!",
+ f"Hallo {name}, deine Bewerbung ist eingegangen!",
)
except Exception:
pass
@@ -121,22 +119,16 @@ async def apply(
try:
admin_email = os.getenv("ADMIN_EMAIL", "")
if admin_email:
- _ename = _html.escape(name)
- _eemail = _html.escape(email)
- _edog_name = _html.escape(dog_name)
- _edog_rasse = _html.escape(dog_rasse)
- _ehandle = _html.escape(social_handle)
- _emotivation = _html.escape(motivation[:300])
admin_body = f"""
Neue Job-Bewerbung eingegangen:
- Name {_ename}
- E-Mail {_eemail}
- Hund {_edog_name} ({_edog_rasse})
- Social {_ehandle}
+ Name {name}
+ E-Mail {email}
+ Hund {dog_name} ({dog_rasse})
+ Social {social_handle}
Anhänge {len([f for f in files if f.filename])} Datei(en)
- {_emotivation}{"…" if len(motivation)>300 else ""}
"""
+ {motivation[:300]}{"…" if len(motivation)>300 else ""}
"""
await send_email(
admin_email,
f"[Banyaro Jobs] Neue Bewerbung — {name}",
@@ -301,17 +293,16 @@ async def download_doc(app_id: int, doc_id: int, admin=Depends(require_admin)):
def _send_status_mail(email: str, name: str, status: str, note: str):
import asyncio
- _ename = _html.escape(name)
texts = {
"reviewing": ("Wir schauen uns deine Bewerbung genauer an 🐾",
- f"Hallo {_ename} ,
wir schauen uns deine Bewerbung gerade genauer an. Wir melden uns bald!
"),
+ f"Hallo {name} ,
wir schauen uns deine Bewerbung gerade genauer an. Wir melden uns bald!
"),
"accepted": ("Herzlichen Glückwunsch — du bist dabei! 🎉",
- f"Hallo {_ename} ,
wir freuen uns, dir mitzuteilen: du bist unser neuer Social-Media-Manager/in für Ban Yaro! Du erhältst außerdem den Gründer-Status in unserer Community. Willkommen im Team!
"),
+ f"Hallo {name} ,
wir freuen uns, dir mitzuteilen: du bist unser neuer Social-Media-Manager/in für Ban Yaro! Du erhältst außerdem den Gründer-Status in unserer Community. Willkommen im Team!
"),
"rejected": ("Deine Bewerbung bei Ban Yaro",
- f"Hallo {_ename} ,
vielen Dank für deine Bewerbung. Leider hat es diesmal nicht geklappt — aber wir wünschen dir alles Gute!
"),
+ f"Hallo {name} ,
vielen Dank für deine Bewerbung. Leider hat es diesmal nicht geklappt — aber wir wünschen dir alles Gute!
"),
}
- subj, body_start = texts.get(status, ("Update zu deiner Bewerbung", f"Hallo {_ename},
"))
- note_html = f'{_html.escape(note)}
' if note else ""
+ subj, body_start = texts.get(status, ("Update zu deiner Bewerbung", f"Hallo {name},
"))
+ note_html = f'{note}
' if note else ""
body = body_start + note_html
async def _send():
diff --git a/backend/routes/litters.py b/backend/routes/litters.py
index ddc810c..82ba96f 100644
--- a/backend/routes/litters.py
+++ b/backend/routes/litters.py
@@ -265,14 +265,13 @@ async def welfare_confirm(litter_id: int, user=Depends(_require_breeder)):
eltern = conn.execute(
"SELECT vater_name, mutter_name FROM litters WHERE id=?", (litter_id,)
).fetchone()
- import html as _html
welfare_body = f"""
Kritischer Tierschutz-Hinweis bestätigt
- Züchter {_html.escape(zuechter)}
- Zwinger {_html.escape(zwinger)}
- Vater {_html.escape(eltern['vater_name'] or '—')}
- Mutter {_html.escape(eltern['mutter_name'] or '—')}
+ Züchter {zuechter}
+ Zwinger {zwinger}
+ Vater {eltern['vater_name'] or '—'}
+ Mutter {eltern['mutter_name'] or '—'}
Wurf-ID #{litter_id}
"""
try:
diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py
index fa74871..1357a85 100644
--- a/backend/routes/moderation.py
+++ b/backend/routes/moderation.py
@@ -268,9 +268,6 @@ async def mod_poi_edit_action(edit_id: int, data: dict,
raise HTTPException(409, "Korrektur wurde bereits bearbeitet.")
if action == "approve":
- _ALLOWED_POI_FIELDS = {"opening_hours", "phone", "website", "name"}
- if edit["field"] not in _ALLOWED_POI_FIELDS:
- raise HTTPException(400, f"Ungültiges Feld: {edit['field']}")
conn.execute(
f"UPDATE osm_pois SET {edit['field']}=?, user_edited=1 WHERE osm_id=?",
(edit["new_value"], edit["osm_id"])
diff --git a/backend/routes/streak.py b/backend/routes/streak.py
index c387a68..ea03522 100644
--- a/backend/routes/streak.py
+++ b/backend/routes/streak.py
@@ -29,7 +29,7 @@ async def get_leaderboard(user=Depends(get_current_user)):
JOIN dogs d ON d.id = ts.dog_id
JOIN users u ON u.id = ts.user_id
WHERE ts.current_streak > 0
- AND d.is_public = 1
+ AND (d.is_public = 1 OR d.user_id = ts.user_id)
ORDER BY ts.current_streak DESC
LIMIT 10
""").fetchall()
diff --git a/backend/routes/weather.py b/backend/routes/weather.py
index fced719..319cfd2 100644
--- a/backend/routes/weather.py
+++ b/backend/routes/weather.py
@@ -3,9 +3,8 @@ BAN YARO — Wetter-API
GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort
"""
-from fastapi import APIRouter, Query, HTTPException, Depends
+from fastapi import APIRouter, Query, HTTPException
import weather as weather_module
-from auth import get_current_user
router = APIRouter()
@@ -19,15 +18,3 @@ async def get_weather(
return await weather_module.get_weather_for_location(lat, lon)
except Exception as exc:
raise HTTPException(503, f'Wetter nicht verfügbar: {exc}')
-
-
-@router.get('/forecast')
-async def get_weather_forecast(
- lat: float = Query(..., ge=-90, le=90),
- lon: float = Query(..., ge=-180, le=180),
- user=Depends(get_current_user),
-):
- try:
- return await weather_module.get_forecast(lat, lon)
- except Exception as exc:
- raise HTTPException(503, f'Wettervorhersage nicht verfügbar: {exc}')
diff --git a/backend/routes/wiki.py b/backend/routes/wiki.py
index 45f5bfb..bf3c19c 100644
--- a/backend/routes/wiki.py
+++ b/backend/routes/wiki.py
@@ -317,24 +317,19 @@ async def submit_foto(
if not rights_confirmed:
raise HTTPException(400, "Bildrechte-Bestätigung fehlt.")
- _IMAGE_MAGIC = [
- b"\xff\xd8\xff", # JPEG
- b"\x89PNG\r\n\x1a\n", # PNG
- b"RIFF", # WebP (RIFF....WEBP)
- b"GIF87a", b"GIF89a", # GIF
- ]
+ # Dateiformat prüfen
+ ct = file.content_type or ""
+ if not ct.startswith("image/"):
+ raise HTTPException(400, "Nur Bilddateien erlaubt.")
os.makedirs(SUBMIT_DIR, exist_ok=True)
- ts = int(time.time())
+ ts = int(time.time())
+ filename = f"{slug}_{user['id']}_{ts}.jpg"
+ path = os.path.join(SUBMIT_DIR, filename)
+
content = await file.read()
if len(content) > 8 * 1024 * 1024:
raise HTTPException(400, "Datei zu groß (max. 8 MB).")
-
- if not any(content.startswith(magic) for magic in _IMAGE_MAGIC):
- raise HTTPException(400, "Nur Bilddateien erlaubt (JPEG, PNG, WebP, GIF).")
-
- filename = f"{slug}_{user['id']}_{ts}.jpg"
- path = os.path.join(SUBMIT_DIR, filename)
with open(path, "wb") as f:
f.write(content)
diff --git a/backend/static/css/components.css b/backend/static/css/components.css
index 27cf0d9..fa7f4e7 100644
--- a/backend/static/css/components.css
+++ b/backend/static/css/components.css
@@ -7592,18 +7592,9 @@ svg.empty-state-icon {
font-size: 9px;
font-weight: 800;
letter-spacing: 0.12em;
- color: white;
+ color: var(--c-text-secondary);
opacity: 0.4;
text-transform: uppercase;
- transition: opacity 0.18s;
-}
-.wlabel.active { opacity: 1; }
-
-@media (min-width: 768px) {
- #world-labels { gap: 48px; font-size: 11px; }
- .wlabel { opacity: 0.5; padding: 4px 10px; border-radius: 8px; }
- .wlabel:hover { opacity: 0.8; background: rgba(255,255,255,0.08); }
- .wlabel.active { opacity: 1; background: rgba(255,255,255,0.12); }
}
/* Settings-Button */
diff --git a/backend/static/icons/phosphor.svg b/backend/static/icons/phosphor.svg
index 2b8028e..a9189b9 100644
--- a/backend/static/icons/phosphor.svg
+++ b/backend/static/icons/phosphor.svg
@@ -270,1361 +270,4 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/backend/static/index.html b/backend/static/index.html
index cb75a8f..5e77ed9 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -93,9 +93,9 @@
-
-
-
+
+
+
@@ -565,7 +565,7 @@
-
+
diff --git a/backend/static/js/api.js b/backend/static/js/api.js
index c6b26da..a40b99d 100644
--- a/backend/static/js/api.js
+++ b/backend/static/js/api.js
@@ -6,84 +6,69 @@
const API = (() => {
- // ----------------------------------------------------------
- // Request-Deduplication: gleiche GET-URL nur einmal in-flight
- // ----------------------------------------------------------
- const _inflight = new Map();
-
// ----------------------------------------------------------
// Interner HTTP-Kern
// ----------------------------------------------------------
- async function _doRequest(method, path, body, attempt) {
+ async function _request(method, path, body = null, options = {}) {
const config = {
method,
headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
+ credentials: 'include', // HttpOnly Cookie wird automatisch mitgesendet
};
if (body && !(body instanceof FormData)) {
config.body = JSON.stringify(body);
} else if (body instanceof FormData) {
- delete config.headers['Content-Type'];
+ delete config.headers['Content-Type']; // Browser setzt multipart/form-data
config.body = body;
}
+ // JWT aus localStorage als Bearer (für API-Calls die das brauchen)
const token = localStorage.getItem('by_token');
- if (token) config.headers['Authorization'] = `Bearer ${token}`;
+ if (token) {
+ config.headers['Authorization'] = `Bearer ${token}`;
+ }
let response;
try {
response = await fetch(`/api${path}`, config);
- } catch {
- // Netzwerkfehler: bei GET bis zu 2 Retry-Versuche
- if (method === 'GET' && attempt < 2) {
- await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt)));
- return _doRequest(method, path, body, attempt + 1);
- }
- const msg = 'Kein Internet — du bist offline.';
- if (window.UI?.toast) UI.toast.warning(msg, 4000);
- throw new APIError(msg, 0, 'network');
+ } catch (err) {
+ const offlineMsg = 'Kein Internet — du bist offline.';
+ if (window.UI && UI.toast) UI.toast.warning(offlineMsg, 4000);
+ throw new APIError(offlineMsg, 0, 'network');
}
+ // 204 No Content
if (response.status === 204) return null;
let data;
- try { data = await response.json(); } catch { data = null; }
+ try {
+ data = await response.json();
+ } catch {
+ data = null;
+ }
if (!response.ok) {
const message = data?.detail || data?.message || `Fehler ${response.status}`;
- const isSwOffline = response.status === 503 && message.startsWith('Offline');
-
- // Retry: GET auf echte 5xx (nicht SW-generierte Offline-503)
- if (method === 'GET' && response.status >= 500 && !isSwOffline && attempt < 2) {
- await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt)));
- return _doRequest(method, path, body, attempt + 1);
+ // SW gibt bei Offline-Anfragen 503 + 'Offline — keine Verbindung.' zurück
+ const isOffline = response.status === 503 && message.startsWith('Offline');
+ if (isOffline && window.UI && UI.toast) {
+ UI.toast.warning('Kein Internet — du bist offline.', 4000);
}
-
- if (isSwOffline && window.UI?.toast) UI.toast.warning('Kein Internet — du bist offline.', 4000);
- throw new APIError(message, response.status, isSwOffline ? 'network' : data?.code);
+ throw new APIError(message, response.status, isOffline ? 'network' : data?.code);
}
+ // SW hat die Anfrage in die Offline-Queue eingereiht
if (data?._queued) {
- if (typeof UI !== 'undefined' && UI.toast)
+ if (typeof UI !== 'undefined' && UI.toast) {
UI.toast.info('Offline gespeichert — wird automatisch synchronisiert');
+ }
return data;
}
return data;
}
- async function _request(method, path, body = null) {
- // GET-Deduplication: laufende identische Anfragen zusammenfassen
- if (method === 'GET') {
- if (_inflight.has(path)) return _inflight.get(path);
- const promise = _doRequest('GET', path, null, 0).finally(() => _inflight.delete(path));
- _inflight.set(path, promise);
- return promise;
- }
- return _doRequest(method, path, body, 0);
- }
-
// ----------------------------------------------------------
// Öffentliche HTTP-Methoden
// ----------------------------------------------------------
@@ -441,9 +426,8 @@ const API = (() => {
// WETTER
// ----------------------------------------------------------
const weather = {
- alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); },
- get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); },
- forecast(lat, lon) { return get(`/weather/forecast?lat=${lat}&lon=${lon}`); },
+ alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); },
+ get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); },
};
// ----------------------------------------------------------
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index a248787..003f1b5 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,8 +3,8 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '651'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
-const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
+const APP_VER = '650'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VERSION = '1.2.1'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
const App = (() => {
diff --git a/backend/static/js/pages/adoption.js b/backend/static/js/pages/adoption.js
index b20682e..8e1bc3a 100644
--- a/backend/static/js/pages/adoption.js
+++ b/backend/static/js/pages/adoption.js
@@ -17,8 +17,6 @@ window.Page_adoption = (() => {
let _activeTab = 'hunde';
let _data = null; // { animals, shelters, has_petfinder }
let _loading = false;
- let _communityData = null; // [] listings from /adoption/community
- let _myListings = null; // [] eigene Inserate
// ----------------------------------------------------------
// INIT
@@ -92,12 +90,6 @@ window.Page_adoption = (() => {
border-bottom:2px solid transparent;font-size:var(--text-sm)">
${UI.icon('house-line')} Tierheime
-
- ${UI.icon('heart')} Weitervermittlung
-
@@ -221,43 +213,12 @@ window.Page_adoption = (() => {
}
}
- async function _loadCommunity() {
- const content = _container.querySelector('#adp-content');
- if (content) content.innerHTML = UI.skeleton(4);
- try {
- const url = _lat && _lon
- ? `/adoption/community?lat=${_lat}&lon=${_lon}`
- : '/adoption/community';
- _communityData = await API.get(url);
- if (_appState?.user) {
- try {
- _myListings = await API.get('/adoption/community/my');
- } catch {
- _myListings = [];
- }
- }
- _renderCommunity(content);
- } catch {
- if (content) content.innerHTML = UI.emptyState({
- icon: 'warning',
- title: 'Weitervermittlungs-Inserate konnten nicht geladen werden',
- text: 'Bitte versuche es erneut.',
- });
- }
- }
-
// ----------------------------------------------------------
// INHALT RENDERN (je nach Tab)
// ----------------------------------------------------------
function _renderContent() {
const content = _container.querySelector('#adp-content');
if (!content) return;
-
- if (_activeTab === 'community') {
- _loadCommunity();
- return;
- }
-
if (!_data) { _showNoLocation(); return; }
if (_activeTab === 'hunde') _renderHunde(content);
@@ -494,442 +455,6 @@ window.Page_adoption = (() => {
`;
}
- // ------------------------------------------------------------------
- // TAB: WEITERVERMITTLUNG (Community)
- // ------------------------------------------------------------------
- function _renderCommunity(content) {
- if (!content) return;
-
- const listings = _communityData || [];
- const isLoggedIn = !!_appState?.user;
-
- const fabHtml = isLoggedIn ? `
-
- ${UI.icon('plus')}
-
- ` : '';
-
- if (!listings.length) {
- content.innerHTML = `
-
-
🐾
-
Noch keine Hunde zur Weitervermittlung
-
- Hier können Halter Hunde privat zur Weitervermittlung anbieten —
- zum Beispiel bei Umzug, Krankheit oder Allergie.
-
- ${isLoggedIn ? `
-
- ${UI.icon('plus')} Hund zur Vermittlung anbieten
-
- ` : `
-
- Bitte anmelden, um ein Inserat zu erstellen.
-
- `}
-
- ${fabHtml}
- `;
- content.querySelector('#adp-empty-create')?.addEventListener('click', _openCreateModal);
- content.querySelector('#adp-fab-create')?.addEventListener('click', _openCreateModal);
- return;
- }
-
- // Eigene Inserate trennen
- const myIds = new Set((_myListings || []).map(l => l.id));
-
- content.innerHTML = `
-
- ${listings.length} Inserat${listings.length !== 1 ? 'e' : ''} zur Weitervermittlung
-
-
- ${listings.map(l => _communityCard(l)).join('')}
-
-
- ${isLoggedIn && _myListings && _myListings.length ? `
-
-
Meine Inserate
-
- ${_myListings.map(l => _myListingRow(l)).join('')}
-
-
- ` : ''}
-
- ${fabHtml}
- `;
-
- // Interest-Button Events
- content.querySelectorAll('[data-adp-interest]').forEach(btn => {
- btn.addEventListener('click', () => {
- const id = btn.dataset.adpInterest;
- const interested = btn.dataset.adpInterested === 'true';
- _handleInterest(id, interested, btn);
- });
- });
-
- // FAB
- content.querySelector('#adp-fab-create')?.addEventListener('click', _openCreateModal);
-
- // Meine Inserate: Status-Dropdown + Löschen
- content.querySelectorAll('[data-adp-status-change]').forEach(sel => {
- sel.addEventListener('change', async () => {
- const id = sel.dataset.adpStatusChange;
- try {
- await API.patch(`/adoption/community/${id}`, { status: sel.value });
- UI.toast.success('Status aktualisiert.');
- _loadCommunity();
- } catch {
- UI.toast.error('Status konnte nicht aktualisiert werden.');
- }
- });
- });
-
- content.querySelectorAll('[data-adp-delete]').forEach(btn => {
- btn.addEventListener('click', async () => {
- if (!window.confirm('Inserat wirklich löschen?')) return;
- try {
- await API.del(`/adoption/community/${btn.dataset.adpDelete}`);
- UI.toast.success('Inserat gelöscht.');
- _communityData = null;
- _myListings = null;
- _loadCommunity();
- } catch {
- UI.toast.error('Löschen fehlgeschlagen.');
- }
- });
- });
- }
-
- function _communityCard(l) {
- const foto = l.foto_url
- ? ` `
- : '🐾
';
-
- const isActive = !l.status || l.status === 'active';
- const statusLabel = l.status === 'reserved' ? 'Reserviert'
- : l.status === 'adopted' ? 'Vermittelt'
- : '';
-
- const alterLabel = l.alter_kategorie === 'welpe' ? 'Welpe <6Mo'
- : l.alter_kategorie === 'jung' ? 'Jung 6Mo–2J'
- : l.alter_kategorie === 'adult' ? 'Adult 2–8J'
- : l.alter_kategorie === 'senior' ? 'Senior >8J'
- : '';
-
- const genderIcon = l.geschlecht === 'maennlich' ? '♂'
- : l.geschlecht === 'weiblich' ? '♀'
- : '';
-
- const distTxt = l.distanz_km != null ? `${l.distanz_km} km` : '';
- const ort = [l.plz, l.ort].filter(Boolean).join(' ');
-
- const interestBtn = l.user_interested
- ? `
- ✓ Bereits gemeldet
- `
- : `
- Interesse bekunden
- `;
-
- return `
-
-
-
- ${foto}
- ${!isActive ? `
-
-
- ${_esc(statusLabel)}
-
-
- ` : ''}
-
-
-
-
- ${_esc(l.name)}
-
- ${l.rasse ? `
- ${_esc(l.rasse)}
-
` : ''}
-
-
- ${alterLabel ? `
- ${_esc(alterLabel)}
- ` : ''}
- ${genderIcon ? `
- ${genderIcon}
- ` : ''}
- ${distTxt ? `
- ${_esc(distTxt)}
- ` : ''}
-
- ${ort ? `
${_esc(ort)}
` : ''}
- ${l.beschreibung ? `
- ${_esc(l.beschreibung)}
-
` : ''}
- ${l.interesse_count ? `
- ❤️ ${l.interesse_count} Interessent${l.interesse_count !== 1 ? 'en' : ''}
-
` : ''}
-
- ${interestBtn}
-
-
-
- `;
- }
-
- function _myListingRow(l) {
- const statusOptions = [
- { value: 'active', label: 'Aktiv' },
- { value: 'reserved', label: 'Reserviert' },
- { value: 'adopted', label: 'Vermittelt' },
- ];
- return `
-
-
-
- ${_esc(l.name)}
-
-
- ${l.interesse_count || 0} Interessent${(l.interesse_count || 0) !== 1 ? 'en' : ''}
-
-
-
- ${statusOptions.map(o => `
- ${o.label}
- `).join('')}
-
-
- ${UI.icon('trash')} Löschen
-
-
- `;
- }
-
- // ------------------------------------------------------------------
- // INTERESSE BEKUNDEN / ZURÜCKZIEHEN
- // ------------------------------------------------------------------
- async function _handleInterest(id, isInterested, btn) {
- if (!_appState?.user) {
- UI.toast.error('Bitte anmelden um Interesse zu bekunden.');
- return;
- }
-
- if (isInterested) {
- // Interesse zurückziehen
- try {
- btn.disabled = true;
- await API.del(`/adoption/community/${id}/interest`);
- UI.toast.success('Interesse zurückgezogen.');
- _communityData = null;
- _myListings = null;
- _loadCommunity();
- } catch {
- UI.toast.error('Fehler beim Zurückziehen des Interesses.');
- btn.disabled = false;
- }
- return;
- }
-
- // Interesse bekunden — Modal mit optionaler Nachricht
- const body = `
-
- `;
- const footer = `
-
-
- ${UI.icon('heart')} Interesse bekunden
-
- Abbrechen
-
- `;
-
- UI.modal.open({ title: 'Interesse bekunden', body, footer });
-
- document.getElementById('adp-interest-form')?.addEventListener('submit', async e => {
- e.preventDefault();
- const submitBtn = document.getElementById('adp-interest-submit');
- const fd = new FormData(e.target);
- const payload = { nachricht: fd.get('nachricht') || null };
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
- try {
- await API.post(`/adoption/community/${id}/interest`, payload);
- UI.modal.close();
- UI.toast.success('Interesse gemeldet!');
- _communityData = null;
- _myListings = null;
- _loadCommunity();
- } catch {
- UI.toast.error('Fehler beim Melden des Interesses.');
- if (submitBtn) { submitBtn.disabled = false; submitBtn.innerHTML = `${UI.icon('heart')} Interesse bekunden`; }
- }
- });
- }
-
- // ------------------------------------------------------------------
- // INSERAT ERSTELLEN — Modal
- // ------------------------------------------------------------------
- function _openCreateModal() {
- if (!_appState?.user) {
- UI.toast.error('Bitte anmelden um ein Inserat zu erstellen.');
- return;
- }
-
- const body = `
-
-
- Name *
-
-
-
- Rasse (optional)
-
-
-
-
- Alter
-
- Unbekannt
- Welpe <6Mo
- Jung 6Mo–2J
- Adult 2–8J
- Senior >8J
-
-
-
- Geschlecht
-
- Unbekannt
- Männlich
- Weiblich
-
-
-
-
-
-
- Hintergrund (optional)
-
-
-
- Foto (optional)
-
-
-
- `;
-
- const footer = `
-
-
- ${UI.icon('plus')} Inserat erstellen
-
- Abbrechen
-
- `;
-
- UI.modal.open({ title: 'Hund zur Vermittlung anbieten', body, footer });
-
- document.getElementById('adp-create-form')?.addEventListener('submit', async e => {
- e.preventDefault();
- const submitBtn = document.getElementById('adp-create-submit');
- const fd = new FormData(e.target);
-
- // Mindestlänge Beschreibung manuell prüfen (minlength gilt nur für text)
- const beschreibung = (fd.get('beschreibung') || '').trim();
- if (beschreibung.length < 80) {
- UI.toast.error('Beschreibung muss mindestens 80 Zeichen lang sein.');
- return;
- }
-
- // FormData für multipart aufbauen
- const postData = new FormData();
- postData.append('name', fd.get('name') || '');
- postData.append('rasse', fd.get('rasse') || '');
- postData.append('alter_kategorie', fd.get('alter_kategorie') || '');
- postData.append('geschlecht', fd.get('geschlecht') || '');
- postData.append('plz', fd.get('plz') || '');
- postData.append('ort', fd.get('ort') || '');
- postData.append('beschreibung', beschreibung);
- postData.append('hintergrund', fd.get('hintergrund') || '');
- if (_lat) postData.append('lat', _lat);
- if (_lon) postData.append('lon', _lon);
- const fotoFile = document.getElementById('adp-create-foto')?.files?.[0];
- if (fotoFile) postData.append('foto', fotoFile);
-
- if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = '…'; }
- try {
- await API.upload('/adoption/community', postData);
- UI.modal.close();
- UI.toast.success('Inserat erstellt!');
- _communityData = null;
- _myListings = null;
- _loadCommunity();
- } catch (err) {
- UI.toast.error(err.message || 'Inserat konnte nicht erstellt werden.');
- if (submitBtn) {
- submitBtn.disabled = false;
- submitBtn.innerHTML = `${UI.icon('plus')} Inserat erstellen`;
- }
- }
- });
- }
-
// ----------------------------------------------------------
// HILFSFUNKTIONEN
// ----------------------------------------------------------
diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js
index 8829bb6..f8488b6 100644
--- a/backend/static/js/pages/settings.js
+++ b/backend/static/js/pages/settings.js
@@ -263,12 +263,6 @@ window.Page_settings = (() => {
Kalender abonnieren
›
-