diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock
new file mode 100644
index 0000000..f6fdb0d
--- /dev/null
+++ b/.claude/scheduled_tasks.lock
@@ -0,0 +1 @@
+{"sessionId":"39ad9ffb-6cac-40b2-8c2d-b3974db3a4b8","pid":1946,"procStart":"Sat Apr 25 14:22:02 2026","acquiredAt":1777133270339}
\ No newline at end of file
diff --git a/backend/database.py b/backend/database.py
index 76c1c2c..e428a56 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -488,8 +488,7 @@ def _migrate(conn_factory):
# WebCal: Kalender-Abo-Token
("users", "calendar_token", "TEXT"),
# User-Profil-Felder
- ("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"),
- ("users", "verification_token", "TEXT"),
+ ("users", "email_verified", "INTEGER NOT NULL DEFAULT 0"),
("users", "bio", "TEXT"),
("users", "wohnort", "TEXT"),
("users", "erfahrung", "TEXT"),
@@ -561,10 +560,9 @@ def _migrate(conn_factory):
("users", "ki_zucht_beschreibung", "INTEGER NOT NULL DEFAULT 1"),
("users", "ki_zucht_jahresbericht", "INTEGER NOT NULL DEFAULT 1"),
# Partner-Code + Gründer-Lizenz
- ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"),
- ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"),
- ("users", "founder_number", "INTEGER"),
- ("users", "is_founder_pending", "INTEGER NOT NULL DEFAULT 0"),
+ ("users", "is_founder", "INTEGER NOT NULL DEFAULT 0"),
+ ("users", "is_partner", "INTEGER NOT NULL DEFAULT 0"),
+ ("users", "founder_number", "INTEGER"),
]
with conn_factory() as conn:
for table, column, col_type in migrations:
@@ -1510,54 +1508,6 @@ def _migrate(conn_factory):
except Exception as e:
logger.warning(f"Migration partner_codes: {e}")
- # Outreach-Log (Admin-E-Mail-Versand)
- try:
- conn.executescript("""
- CREATE TABLE IF NOT EXISTS outreach_log (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- sent_by INTEGER REFERENCES users(id),
- recipient TEXT NOT NULL,
- subject TEXT NOT NULL,
- body TEXT NOT NULL,
- sent_at TEXT NOT NULL DEFAULT (datetime('now'))
- );
- """)
- except Exception as e:
- logger.warning(f"Migration outreach_log: {e}")
-
- # E-Mail-Vorlagen (DB-gespeichert, CRUD über Admin)
- try:
- conn.execute("""
- CREATE TABLE IF NOT EXISTS email_templates (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- key TEXT UNIQUE NOT NULL,
- label TEXT NOT NULL,
- subject TEXT NOT NULL,
- body TEXT NOT NULL,
- from_account TEXT NOT NULL DEFAULT 'partner',
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
- updated_at TEXT
- )
- """)
- # Startwert-Vorlage einspielen wenn Tabelle noch leer
- count = conn.execute("SELECT COUNT(*) FROM email_templates").fetchone()[0]
- if count == 0:
- conn.execute("""
- INSERT INTO email_templates (key, label, subject, body, from_account) VALUES
- ('influencer_de',
- 'Influencer-Ansprache (DE)',
- 'Ban Yaro — 100 Gründer-Plätze, einer davon für deine Community',
- 'Hallo {name},\n\nich bin René und habe Ban Yaro gebaut — eine Hunde-App für Tagebuch, Gesundheit, Giftköder-Alarm und Community. Kostenlos, ohne App Store, direkt als PWA.\n\nIch kontaktiere gerade einige Influencer aus der deutschen Hunde-Community mit einem konkreten Angebot:\n\nWas deine Follower bekommen: Wer sich mit deinem persönlichen Code registriert, sichert sich einen der 100 Gründer-Plätze — eine nummerierte Badge ("Gründer #42") die dauerhaft im Profil sichtbar ist. Diese 100 Plätze gibt es genau einmal.\n\nWas du bekommst: Partner-Badge in der App, eigener Code, öffentliches Ranking wer die meisten Gründer bringt.\n\nKein Geld, kein verpflichtender Post — aber eine echte Exklusivität die du deiner Community geben kannst.\n\nAlle Infos: https://banyaro.app/partner\n\nWenn dich das interessiert, antworte einfach kurz — ich richte deinen Code binnen 24h ein.\n\nViele Grüße,\nRené\nbanyaro.app',
- 'partner')
- """)
- except Exception as e:
- logger.warning(f"Migration email_templates: {e}")
-
- # from_account-Spalte in outreach_log nachträglich hinzufügen
- existing_ol = [row[1] for row in conn.execute("PRAGMA table_info(outreach_log)").fetchall()]
- if 'from_account' not in existing_ol:
- conn.execute("ALTER TABLE outreach_log ADD COLUMN from_account TEXT DEFAULT 'partner'")
-
# js_exercise_id zu training_exercises — verbindet training_exercises mit exercise_progress
existing_te = [row[1] for row in conn.execute("PRAGMA table_info(training_exercises)").fetchall()]
if 'js_exercise_id' not in existing_te:
diff --git a/backend/main.py b/backend/main.py
index ddb4acc..db883dc 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -163,7 +163,6 @@ from routes.zucht_hunde import router as zucht_hunde_router
from routes.breeder_export import router as breeder_export_router
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
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@@ -196,7 +195,6 @@ app.include_router(zucht_hunde_router, prefix="/api", tags=["Zuchtkart
app.include_router(breeder_export_router, prefix="/api", tags=["Export"])
app.include_router(zucht_ki_router, prefix="/api", tags=["Züchter-KI"])
app.include_router(partner_router, prefix="/api", tags=["Partner"])
-app.include_router(outreach_router, prefix="/api/outreach", tags=["Outreach"])
app.include_router(webcal_router, prefix="/api/webcal", tags=["WebCal"])
app.include_router(profile_router, prefix="/api/profile", tags=["Profil"])
app.include_router(import_router, prefix="/api/import", tags=["Import"])
@@ -1402,208 +1400,6 @@ async def knigge_page():
return HTMLResponse(content=html, headers={"Cache-Control": "max-age=7200"})
-# ------------------------------------------------------------------
-# /partner — Influencer-Landingpage
-# ------------------------------------------------------------------
-@app.get("/partner")
-async def partner_landing():
- from fastapi.responses import HTMLResponse
- from database import db as _db
- with _db() as conn:
- total_founders = conn.execute("SELECT COUNT(*) FROM users WHERE is_founder=1").fetchone()[0]
- partners = conn.execute(
- """SELECT label, uses FROM partner_codes WHERE grants_founder=1 ORDER BY uses DESC LIMIT 5"""
- ).fetchall()
- open_slots = max(0, 100 - total_founders)
-
- partner_rows = ''.join([
- f'
{p["label"]} '
- f'{p["uses"]} Gründer
'
- for p in partners
- ]) or 'Noch keine Partner aktiv — sei der Erste.
'
-
- html = f"""
-
-
-
-
- Ban Yaro Partner — Werde Teil der ersten 100
-
-
-
-
-
-
-
-
-
-
-
-
-
Ban Yaro · Influencer-Programm
-
Gib deiner Community etwas für immer.
-
100 Gründer-Plätze. Weltweit. Nie wieder erhältlich. Als Partner bringst du deine Follower nach vorne — und steigst im Ranking auf.
-
Jetzt Partner werden
-
-
-
-
-
{open_slots}
-
Gründer-Plätze noch frei (von 100)
-
-
0 {total_founders} vergeben 100
-
-
-
-
-
Was du und deine Community bekommen
-
-
-
🏆
-
-
Gründer-Lizenz für deine Follower
-
Jeder der sich mit deinem Code registriert bekommt einen der 100 Gründer-Plätze — mit einer nummerierten Badge „Gründer #N" die dauerhaft im Profil und im Forum sichtbar ist. Nie wieder erhältlich.
-
-
-
-
-
🤝
-
-
Dein persönlicher Partner-Code
-
Du bekommst einen eigenen Code (z.B. HUNDEBLOG ). Follower die sich damit registrieren werden automatisch Gründer — du siehst in Echtzeit wie viele du gebracht hast.
-
-
-
-
-
📊
-
-
Öffentliches Partner-Ranking
-
Auf der
Gründer-Seite siehen alle wer die meisten Gründer gebracht hat. Das Ranking motiviert deine Follower mitzumachen — und stärkt deine Position gegenüber anderen Influencern.
-
-
-
-
-
💜
-
-
Partner-Badge für dich
-
Du selbst bekommst ein „Partner" -Badge in deinem Profil — sichtbar für alle Nutzer der App.
-
-
-
-
-
🎁
-
-
Lebenslang kostenlos — für immer
-
Gründer zahlen nie für Premium-Features — egal was wir in Zukunft einführen. Das ist ein echtes Dankeschön für die Pioniere.
-
-
-
-
-
-
-
Wie es funktioniert
-
-
1
-
Kontakt aufnehmen Schreib uns kurz an partner@banyaro.app — wir richten deinen persönlichen Code ein.
-
-
-
2
-
Code teilen Du postest deinen Code in Story, Reel oder Post — deine Follower registrieren sich auf banyaro.app.
-
-
-
3
-
Gründer werden Jede Registrierung mit deinem Code sichert automatisch einen der 100 Gründer-Plätze. Du siehst deinen Fortschritt in Echtzeit.
-
-
-
4
-
Im Ranking aufsteigen Je mehr Gründer du bringst, desto höher dein Platz auf der öffentlichen Gründer-Seite.
-
-
-
-
- {'
🏅 Aktuelles Partner-Ranking
' + partner_rows + '
' if partners else ''}
-
-
-
-
Was ist Ban Yaro?
-
Ban Yaro ist die Hunde-App für alles was Halter brauchen — Tagebuch, Gesundheit, Routen, Giftköder-Alarm, Community. Kostenlos, ohne App Store, direkt im Browser oder als PWA.
-
banyaro.app entdecken →
-
-
-
-
-
-
-
-
-
-
-"""
- return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"})
-
-
# SPA Fallback — ALLE nicht-API-Routen gehen zur index.html
@app.get("/{full_path:path}")
async def spa_fallback(full_path: str):
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index 7661c80..e46cda0 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -6,7 +6,6 @@ import string
from typing import Optional
from fastapi import APIRouter, HTTPException, Request, Response, Depends
-from fastapi.responses import RedirectResponse
from pydantic import BaseModel, EmailStr
from database import db
from auth import (
@@ -17,28 +16,7 @@ from username_blocklist import is_username_blocked
from ratelimit import check as rl_check
router = APIRouter()
-COOKIE_NAME = "by_token"
-_APP_URL = os.getenv("APP_URL", "https://banyaro.app")
-_SMTP_READY = bool(os.getenv("SMTP_SUPPORT_USER") and os.getenv("SMTP_SUPPORT_PASS"))
-
-
-def _send_verification_email(email: str, name: str, token: str):
- if not _SMTP_READY:
- return
- from routes.outreach import _send_smtp
- subject = "Ban Yaro — bitte bestätige deine E-Mail-Adresse"
- body = (
- f"Hallo {name},\n\n"
- "willkommen bei Ban Yaro! Bitte bestätige deine E-Mail-Adresse mit diesem Link:\n\n"
- f"{_APP_URL}/api/auth/verify-email/{token}\n\n"
- "Der Link ist 7 Tage gültig.\n\n"
- "Falls du dich nicht bei Ban Yaro registriert hast, kannst du diese Mail ignorieren.\n\n"
- "Viele Grüße,\nDas Ban Yaro Team\nbanyaro.app"
- )
- try:
- _send_smtp(email, subject, body, "support")
- except Exception:
- pass # Nicht blockieren wenn SMTP fehlschlägt
+COOKIE_NAME = "by_token"
class LoginRequest(BaseModel):
@@ -86,13 +64,13 @@ async def register(data: RegisterRequest, response: Response, request: Request):
).fetchone():
raise HTTPException(400, "Dieser Name ist bereits vergeben. Bitte wähle einen anderen.")
code = _gen_referral_code()
- verify_token = secrets.token_urlsafe(32)
try:
conn.execute(
- "INSERT INTO users (email, pw_hash, name, referral_code, verification_token) VALUES (?,?,?,?,?)",
- (data.email, hash_password(data.password), name, code, verify_token)
+ "INSERT INTO users (email, pw_hash, name, referral_code) VALUES (?,?,?,?)",
+ (data.email, hash_password(data.password), name, code)
)
except Exception:
+ # Fallback falls UNIQUE-Index greift (Race Condition)
raise HTTPException(400, "Dieser Name ist bereits vergeben. Bitte wähle einen anderen.")
user = conn.execute(
"SELECT id, rolle FROM users WHERE email=?", (data.email,)
@@ -119,8 +97,9 @@ async def register(data: RegisterRequest, response: Response, request: Request):
"SELECT COUNT(*) FROM users WHERE is_founder=1"
).fetchone()[0]
if total_founders < 100:
- # Pending — wird nach erstem Hunde-Profil mit Plausibilitätsprüfung aktiviert
- updates["is_founder_pending"] = 1
+ founder_num = total_founders + 1
+ updates["is_founder"] = 1
+ updates["founder_number"] = founder_num
set_clause = ", ".join(f"{k}=?" for k in updates)
conn.execute(
f"UPDATE users SET {set_clause} WHERE id=?",
@@ -138,8 +117,7 @@ async def register(data: RegisterRequest, response: Response, request: Request):
token = create_token(user["id"], user["rolle"])
_set_cookie(response, token)
- _send_verification_email(data.email, name, verify_token)
- return {"token": token, "name": name, "email_verified": 0}
+ return {"token": token, "name": name}
@router.post("/login")
@@ -220,7 +198,7 @@ async def me(user=Depends(get_current_user)):
"""SELECT id, name, real_name, email, rolle, is_premium, email_verified,
bio, wohnort, erfahrung, social_link,
profil_sichtbarkeit, avatar_url, created_at,
- is_founder, is_partner, founder_number, is_founder_pending
+ is_founder, is_partner, founder_number
FROM users WHERE id=?""",
(user["id"],)
).fetchone()
@@ -229,37 +207,3 @@ async def me(user=Depends(get_current_user)):
data = dict(row)
data["is_premium"] = bool(data["is_premium"])
return data
-
-
-@router.get("/verify-email/{token}")
-async def verify_email(token: str):
- with db() as conn:
- row = conn.execute(
- "SELECT id, email_verified FROM users WHERE verification_token=?", (token,)
- ).fetchone()
- if not row:
- return RedirectResponse(f"{_APP_URL}/#settings?verified=error", status_code=302)
- conn.execute(
- "UPDATE users SET email_verified=1, verification_token=NULL WHERE id=?",
- (row["id"],)
- )
- return RedirectResponse(f"{_APP_URL}/#settings?verified=1", status_code=302)
-
-
-@router.post("/resend-verification")
-async def resend_verification(user=Depends(get_current_user)):
- with db() as conn:
- row = conn.execute(
- "SELECT email, name, email_verified FROM users WHERE id=?", (user["id"],)
- ).fetchone()
- if not row:
- raise HTTPException(404)
- if row["email_verified"]:
- return {"ok": True, "already_verified": True}
- token = secrets.token_urlsafe(32)
- with db() as conn:
- conn.execute(
- "UPDATE users SET verification_token=? WHERE id=?", (token, user["id"])
- )
- _send_verification_email(row["email"], row["name"], token)
- return {"ok": True}
diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py
index bb5efc8..061c9a7 100644
--- a/backend/routes/breeder.py
+++ b/backend/routes/breeder.py
@@ -21,7 +21,7 @@ _TZ = ZoneInfo("Europe/Berlin")
BREEDER_DOCS_DIR = os.getenv("BREEDER_DOCS_DIR", "/data/breeder_docs")
os.makedirs(BREEDER_DOCS_DIR, exist_ok=True)
-ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "admin@banyaro.app")
+ADMIN_EMAIL = os.getenv("ADMIN_EMAIL", "mail@motocamp.de")
APP_URL = os.getenv("APP_URL", "https://banyaro.app")
diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py
index 74f1c95..8e176f8 100644
--- a/backend/routes/dogs.py
+++ b/backend/routes/dogs.py
@@ -78,41 +78,6 @@ async def list_dogs(user=Depends(get_current_user)):
return result
-def _is_plausible_dog(name: str, rasse: str, geburtstag) -> tuple[bool, str]:
- """Einfache Plausibilitätsprüfung für Hunde-Profile."""
- import re, datetime
- name = (name or "").strip()
- rasse = (rasse or "").strip()
-
- if len(name) < 2:
- return False, "Der Name muss mindestens 2 Zeichen haben."
- if not re.search(r'[a-zA-ZäöüÄÖÜß]', name):
- return False, "Der Name muss mindestens einen Buchstaben enthalten."
- if len(set(name.lower())) < 2:
- return False, "Bitte einen echten Namen eingeben."
-
- if rasse and len(rasse) < 2:
- return False, "Bitte eine gültige Rasse eingeben."
- if rasse and not re.search(r'[a-zA-ZäöüÄÖÜß]', rasse):
- return False, "Die Rasse muss Buchstaben enthalten."
-
- if geburtstag:
- try:
- if isinstance(geburtstag, str):
- year = int(geburtstag[:4])
- else:
- year = geburtstag.year
- now = datetime.date.today().year
- if year > now:
- return False, "Das Geburtsdatum liegt in der Zukunft."
- if year < now - 30:
- return False, "Das Geburtsdatum ist unrealistisch."
- except Exception:
- pass
-
- return True, ""
-
-
@router.post("")
async def create_dog(data: DogCreate, user=Depends(get_current_user)):
with db() as conn:
@@ -128,28 +93,6 @@ async def create_dog(data: DogCreate, user=Depends(get_current_user)):
"SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1",
(user["id"],)
).fetchone()
-
- # Gründer-Aktivierung: erstes Hunde-Profil + is_founder_pending
- user_row = conn.execute(
- "SELECT is_founder_pending, is_founder FROM users WHERE id=?",
- (user["id"],)
- ).fetchone()
- if user_row and user_row["is_founder_pending"] and not user_row["is_founder"]:
- dog_count = conn.execute(
- "SELECT COUNT(*) FROM dogs WHERE user_id=?", (user["id"],)
- ).fetchone()[0]
- if dog_count == 1: # genau dieser erste Hund
- plausible, reason = _is_plausible_dog(data.name, data.rasse, data.geburtstag)
- if plausible:
- total = conn.execute(
- "SELECT COUNT(*) FROM users WHERE is_founder=1"
- ).fetchone()[0]
- if total < 100:
- conn.execute(
- "UPDATE users SET is_founder=1, founder_number=?, is_founder_pending=0 WHERE id=?",
- (total + 1, user["id"])
- )
-
return dict(dog)
diff --git a/backend/routes/litters.py b/backend/routes/litters.py
index 2bcf629..f47c809 100644
--- a/backend/routes/litters.py
+++ b/backend/routes/litters.py
@@ -258,7 +258,7 @@ async def welfare_confirm(litter_id: int, user=Depends(_require_breeder)):
"FROM breeder_profiles bp JOIN users u ON u.id=bp.user_id "
"WHERE bp.user_id=?", (user["id"],)
).fetchone()
- admin_email = os.getenv("ADMIN_EMAIL", "admin@banyaro.app")
+ admin_email = os.getenv("ADMIN_EMAIL", "mail@motocamp.de")
app_url = os.getenv("APP_URL", "https://banyaro.app")
zuechter = profile["name"] if profile else user.get("name", "Unbekannt")
zwinger = profile["zwingername"] if profile else "—"
diff --git a/backend/routes/osm.py b/backend/routes/osm.py
index e08742b..d75631b 100644
--- a/backend/routes/osm.py
+++ b/backend/routes/osm.py
@@ -28,7 +28,7 @@ OVERPASS_URLS = [
_overpass_sem = asyncio.Semaphore(1)
_overpass_last_req = 0.0
_OVERPASS_MIN_DELAY = 2.0 # Sekunden zwischen Anfragen
-_OVERPASS_UA = 'BanYaro/1.0 (https://banyaro.app; dog-walking PWA; contact: admin@banyaro.app)'
+_OVERPASS_UA = 'BanYaro/1.0 (https://banyaro.app; dog-walking PWA; contact: mail@motocamp.de)'
_OVERPASS_HEADERS = {
'User-Agent': _OVERPASS_UA,
'Referer': 'https://banyaro.app/', # von overpass-api.de verlangt gegen 406
diff --git a/backend/routes/outreach.py b/backend/routes/outreach.py
deleted file mode 100644
index 11f4152..0000000
--- a/backend/routes/outreach.py
+++ /dev/null
@@ -1,188 +0,0 @@
-"""BAN YARO — Mailing (Admin)"""
-
-import os
-import smtplib
-import ssl
-from email.mime.text import MIMEText
-from email.mime.multipart import MIMEMultipart
-from email.utils import formataddr
-from datetime import datetime
-from typing import List, Optional
-
-from fastapi import APIRouter, Depends, HTTPException
-from pydantic import BaseModel
-
-from auth import require_admin
-from database import db
-
-router = APIRouter()
-
-_SMTP_HOST = os.getenv("SMTP_HOST", "mail.your-server.de")
-_SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
-
-_ACCOUNTS = {
- "partner": {
- "user": os.getenv("SMTP_USER", ""),
- "pass": os.getenv("SMTP_PASS", ""),
- "from": "partner@banyaro.app",
- "name": "Ban Yaro Partner",
- },
- "support": {
- "user": os.getenv("SMTP_SUPPORT_USER", "support@banyaro.de"),
- "pass": os.getenv("SMTP_SUPPORT_PASS", ""),
- "from": "support@banyaro.app",
- "name": "Ban Yaro Support",
- },
-}
-
-
-def _send_smtp(to: str, subject: str, body: str, account: str = "partner"):
- acc = _ACCOUNTS.get(account) or _ACCOUNTS["partner"]
- if not acc["user"] or not acc["pass"]:
- raise RuntimeError(f"SMTP-Account '{account}' nicht konfiguriert.")
- msg = MIMEMultipart("alternative")
- msg["Subject"] = subject
- msg["From"] = formataddr((acc["name"], acc["from"]))
- msg["To"] = to
- msg["Reply-To"] = acc["from"]
- msg.attach(MIMEText(body, "plain", "utf-8"))
- ctx = ssl.create_default_context()
- with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s:
- s.ehlo()
- s.starttls(context=ctx)
- s.login(acc["user"], acc["pass"])
- s.sendmail(acc["from"], [to], msg.as_bytes())
-
-
-# ------------------------------------------------------------------
-# Schemas
-# ------------------------------------------------------------------
-
-class TemplateIn(BaseModel):
- key: str
- label: str
- subject: str
- body: str
- from_account: str = "partner"
-
-
-class TemplateUpdate(BaseModel):
- label: str
- subject: str
- body: str
- from_account: str = "partner"
-
-
-class SendRequest(BaseModel):
- to: List[str]
- subject: str
- body: str
- from_account: str = "partner"
- template_id: Optional[int] = None
-
-
-# ------------------------------------------------------------------
-# Templates CRUD
-# ------------------------------------------------------------------
-
-@router.get("/templates")
-def list_templates(user=Depends(require_admin)):
- with db() as conn:
- rows = conn.execute(
- "SELECT id, key, label, subject, body, from_account FROM email_templates ORDER BY id"
- ).fetchall()
- return [dict(r) for r in rows]
-
-
-@router.post("/templates", status_code=201)
-def create_template(data: TemplateIn, user=Depends(require_admin)):
- try:
- with db() as conn:
- row = conn.execute(
- """INSERT INTO email_templates (key, label, subject, body, from_account, created_at)
- VALUES (?, ?, ?, ?, ?, ?) RETURNING id""",
- (data.key, data.label, data.subject, data.body, data.from_account,
- datetime.utcnow().isoformat())
- ).fetchone()
- return {"id": row["id"]}
- except Exception as e:
- raise HTTPException(400, f"Vorlage konnte nicht angelegt werden: {e}")
-
-
-@router.put("/templates/{tpl_id}")
-def update_template(tpl_id: int, data: TemplateUpdate, user=Depends(require_admin)):
- with db() as conn:
- conn.execute(
- """UPDATE email_templates
- SET label=?, subject=?, body=?, from_account=?, updated_at=?
- WHERE id=?""",
- (data.label, data.subject, data.body, data.from_account,
- datetime.utcnow().isoformat(), tpl_id)
- )
- return {"ok": True}
-
-
-@router.delete("/templates/{tpl_id}")
-def delete_template(tpl_id: int, user=Depends(require_admin)):
- with db() as conn:
- conn.execute("DELETE FROM email_templates WHERE id=?", (tpl_id,))
- return {"ok": True}
-
-
-# ------------------------------------------------------------------
-# Senden
-# ------------------------------------------------------------------
-
-@router.post("/send")
-def send_mail(data: SendRequest, user=Depends(require_admin)):
- if not data.to:
- raise HTTPException(400, "Mindestens eine Empfänger-Adresse angeben.")
- if not data.subject.strip() or not data.body.strip():
- raise HTTPException(400, "Betreff und Text dürfen nicht leer sein.")
-
- sent, failed = [], []
- for addr in data.to:
- addr = addr.strip()
- if not addr:
- continue
- try:
- _send_smtp(addr, data.subject, data.body, data.from_account)
- sent.append(addr)
- with db() as conn:
- conn.execute(
- """INSERT INTO outreach_log
- (sent_by, recipient, subject, body, from_account, sent_at)
- VALUES (?, ?, ?, ?, ?, ?)""",
- (user["id"], addr, data.subject, data.body, data.from_account,
- datetime.utcnow().isoformat())
- )
- except Exception as e:
- failed.append({"addr": addr, "error": str(e)})
-
- return {"sent": sent, "failed": failed}
-
-
-# ------------------------------------------------------------------
-# Support-Versand (intern, ohne Admin-Auth — für Moderatoren-Trigger)
-# ------------------------------------------------------------------
-
-def send_support_mail(to: str, subject: str, body: str):
- """Intern aufrufbar ohne HTTP-Request — z.B. aus Moderations-Logik."""
- _send_smtp(to, subject, body, "support")
-
-
-# ------------------------------------------------------------------
-# Log
-# ------------------------------------------------------------------
-
-@router.get("/log")
-def outreach_log_endpoint(user=Depends(require_admin)):
- with db() as conn:
- rows = conn.execute(
- """SELECT ol.id, ol.recipient, ol.subject, ol.sent_at,
- ol.from_account, u.name AS sent_by_name
- FROM outreach_log ol
- JOIN users u ON u.id = ol.sent_by
- ORDER BY ol.sent_at DESC LIMIT 200"""
- ).fetchall()
- return [dict(r) for r in rows]
diff --git a/backend/static/icons/phosphor.svg b/backend/static/icons/phosphor.svg
index 2fcc061..9b4843c 100644
--- a/backend/static/icons/phosphor.svg
+++ b/backend/static/icons/phosphor.svg
@@ -189,4 +189,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/backend/static/index.html b/backend/static/index.html
index 21afd73..1cd9cfa 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -108,26 +108,6 @@
border-radius:999px;padding:1px 7px;font-size:11px;font-weight:700">
-
-
-
-
-
-
Bitte bestätige deine E-Mail-Adresse — wir haben dir eine Mail geschickt.
-
- Erneut senden
-
-
✕
-
-
@@ -250,7 +230,7 @@
border-top:1px solid var(--c-border,#e5e7eb);
font-size:var(--text-xs);color:var(--c-text-muted);
display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-2)">
-
+
Impressum
Datenschutz
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 6937188..cb4b4a9 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 = '552'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '539'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.1.4'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
@@ -76,15 +76,13 @@ const App = (() => {
// AUTH GUARD — Login-Gate Texte pro Seite
// ----------------------------------------------------------
const AUTH_GATE = {
- diary: { icon: 'book-open', text: 'Dein persönliches Hunde-Tagebuch — Fotos, Notizen, Stimmungen. Nur für dich, privat und sicher.', preview: '/img/screenshots/screen-1.jpg' },
- health: { icon: 'syringe', text: 'Impfungen, Tierarztbesuche, Gewicht und Medikamente — alles an einem Ort, immer abrufbar.', preview: '/img/screenshots/screen-3.jpg' },
- 'dog-profile':{ icon: 'paw-print', text: 'Erstelle ein Profil für deinen Hund mit Foto, Bio, Chip-Nr. und NFC-Tag.', preview: null },
- friends: { icon: 'users', text: 'Finde Hundebesitzer in deiner Nähe und tausche dich aus.', preview: null },
- chat: { icon: 'chat-circle-dots', text: 'Schreibe direkt mit anderen Hundebesitzern.', preview: null },
- walks: { icon: 'paw-print', text: 'Organisiere Gassi-Treffen und triff andere Hunde.', preview: '/img/screenshots/screen-5.jpg' },
- sitting: { icon: 'house-line', text: 'Finde einen Dogsitter oder biete selbst Sitting an.', preview: null },
- uebungen: { icon: 'target', text: 'Über 100 Übungen mit Anleitungen — tracke deinen Trainingsfortschritt.', preview: '/img/screenshots/screen-4.jpg' },
- notes: { icon: 'note-pencil', text: 'Dein persönlicher Notizblock — für alles was du nicht vergessen willst.', preview: null },
+ diary: { icon: 'book-open', text: 'Halte besondere Momente mit deinem Hund fest — Spaziergänge, Meilensteine, Fotos.' },
+ health: { icon: 'syringe', text: 'Impfungen, Tierarztbesuche, Medikamente und Allergien deines Hundes immer im Blick.' },
+ 'dog-profile':{ icon: 'paw-print', text: 'Erstelle ein Profil für deinen Hund — mit Foto, Bio und NFC-Tag.' },
+ friends: { icon: 'users', text: 'Finde Hundebesitzer in deiner Nähe und baue ein Netzwerk auf.' },
+ chat: { icon: 'chat-circle-dots', text: 'Schreibe direkt mit anderen Hundebesitzern.' },
+ walks: { icon: 'paw-print', text: 'Organisiere Gassi-Treffen und triff andere Hunde in deiner Gegend.' },
+ sitting: { icon: 'house-line', text: 'Finde einen Dogsitter oder biete selbst Sitting an.' },
};
// ----------------------------------------------------------
@@ -124,9 +122,10 @@ const App = (() => {
async function _loadPage(pageId, params = {}) {
const page = pages[pageId];
- // AUTH GUARD — geschützte Seiten für nicht-eingeloggte User → Welcome
+ // AUTH GUARD — geschützte Seiten für nicht-eingeloggte User
if (page.requiresAuth && !state.user) {
- navigate('welcome', false);
+ const container = document.querySelector(`#page-${pageId} .page-body`);
+ if (container) _renderLoginGate(container, pageId);
return;
}
@@ -189,34 +188,16 @@ const App = (() => {
container.innerHTML = `
+ min-height:60vh;padding:var(--space-8) var(--space-5);text-align:center;gap:var(--space-5)">
-
- ${gate.preview ? `
-
-
-
-
-
-
-
-
- Nur für Mitglieder
-
-
-
-
` : `
-
-
+
-
`}
+
@@ -232,13 +213,14 @@ const App = (() => {
-
+
+
+ Anmelden
+
+
Kostenlos registrieren
-
- Schon dabei? Anmelden
-
@@ -473,7 +455,6 @@ const App = (() => {
navigate('onboarding');
}
- _showVerifyBanner();
_updateNotifBadge();
_updateChatBadge();
_checkNearbyAlerts();
@@ -548,30 +529,13 @@ const App = (() => {
_updateHeaderUserBtn(false);
- // Nicht eingeloggte User immer zur Welcome-Seite
- navigate('welcome', false);
- }
-
- function _showVerifyBanner() {
- const banner = document.getElementById('verify-banner');
- if (!banner) return;
- if (!state.user || state.user.email_verified) {
- banner.style.display = 'none';
- return;
+ // Wenn aktuelle Seite geschützt ist → zu freier Seite wechseln
+ if (pages[state.page]?.requiresAuth) {
+ navigate('map', false);
+ } else {
+ // Bleib auf der Seite, zeige aber den Gate-Screen
+ _loadPage(state.page);
}
- const dismissed = sessionStorage.getItem('by_verify_dismissed');
- if (dismissed) return;
- banner.style.display = 'flex';
-
- document.getElementById('verify-resend-btn')?.addEventListener('click', async () => {
- await API.post('/auth/resend-verification', {});
- UI.toast.success('Bestätigungs-Mail erneut gesendet.');
- }, { once: true });
-
- document.getElementById('verify-banner-close')?.addEventListener('click', () => {
- banner.style.display = 'none';
- sessionStorage.setItem('by_verify_dismissed', '1');
- }, { once: true });
}
function _updateHeaderUserBtn(loggedIn) {
@@ -823,21 +787,8 @@ const App = (() => {
hashParams[k] = isNaN(v) ? v : Number(v);
});
}
-
- // E-Mail-Verifikation: Redirect von /api/auth/verify-email/{token}
- if (hashParams.verified === '1' || hashParams.verified === 1) {
- if (state.user) state.user.email_verified = 1;
- document.getElementById('verify-banner')?.style?.setProperty('display', 'none');
- UI.toast.success('E-Mail-Adresse erfolgreich bestätigt!');
- history.replaceState(null, '', '/');
- } else if (hashParams.verified === 'error') {
- UI.toast.error('Ungültiger oder abgelaufener Bestätigungs-Link.');
- history.replaceState(null, '', '/');
- }
-
const startPage = (hashPage && pages[hashPage]) ? hashPage : 'welcome';
- // Nicht eingeloggte User immer zur Welcome-Seite — auch bei direktem Link auf Forum, Map etc.
- navigate(state.user ? startPage : 'welcome', false, hashParams);
+ navigate(startPage, false, hashParams);
}
async function _handleInvite(token) {
diff --git a/backend/static/js/pages/admin.js b/backend/static/js/pages/admin.js
index cd34154..69ed773 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -20,7 +20,6 @@ window.Page_admin = (() => {
{ id: 'system', label: 'System', icon: 'gear' },
{ id: 'jobs', label: 'Jobs', icon: 'clock' },
{ id: 'partner', label: 'Partner', icon: 'handshake' },
- { id: 'outreach', label: 'Outreach', icon: 'envelope-simple' },
{ id: 'audit', label: 'Audit-Log', icon: 'clipboard-text' },
];
@@ -91,7 +90,6 @@ window.Page_admin = (() => {
case 'system': await _renderSystem(el); break;
case 'jobs': await _renderJobs(el); break;
case 'partner': await _renderPartner(el); break;
- case 'outreach': await _renderOutreach(el); break;
case 'audit': await _renderAudit(el); break;
}
} catch (e) {
@@ -2018,256 +2016,6 @@ window.Page_admin = (() => {
});
}
- async function _renderOutreach(el) {
- const [templates, log] = await Promise.all([
- API.get('/outreach/templates').catch(() => []),
- API.get('/outreach/log').catch(() => []),
- ]);
-
- const accountBadge = a => a === 'support'
- ? `
support@ `
- : `
partner@ `;
-
- el.innerHTML = `
-
-
-
-
-
-
Vorlagen
-
- ${UI.icon('plus')} Neue Vorlage
-
-
- ${templates.length === 0
- ? `
Noch keine Vorlagen.
`
- : `
- ${templates.map(t => `
-
-
-
- ${_esc(t.label)}
- ${accountBadge(t.from_account)}
-
-
- ${_esc(t.subject)}
-
-
-
-
- ${UI.icon('arrow-bend-up-left')}
-
-
- ${UI.icon('pencil-simple')}
-
-
- ${UI.icon('trash')}
-
-
-
`).join('')}
-
`}
-
-
-
-
-
-
-
-
Versand-Log
- ${log.length === 0
- ? `
Noch keine E-Mails gesendet.
`
- : `
-
-
- Von
- Empfänger
- Betreff
- Wer
- Wann
-
-
-
- ${log.map(l => `
-
- ${accountBadge(l.from_account)}
- ${_esc(l.recipient)}
- ${_esc(l.subject)}
- ${_esc(l.sent_by_name || '')}
- ${(l.sent_at||'').slice(0,16).replace('T',' ')}
- `).join('')}
-
-
`}
-
-
-
- `;
-
- // Vorlage in Compose laden
- function _loadTplIntoCompose(id) {
- const tpl = templates.find(t => t.id === id);
- if (!tpl) return;
- el.querySelector('#adm-outreach-from').value = tpl.from_account || 'partner';
- el.querySelector('#adm-outreach-subject').value = tpl.subject;
- el.querySelector('#adm-outreach-body').value = tpl.body;
- }
-
- el.querySelectorAll('.adm-tpl-load').forEach(btn => {
- btn.addEventListener('click', () => _loadTplIntoCompose(Number(btn.dataset.id)));
- });
-
- // Vorlage löschen
- el.querySelectorAll('.adm-tpl-del').forEach(btn => {
- btn.addEventListener('click', async () => {
- if (!window.confirm('Vorlage löschen?')) return;
- await API.del(`/outreach/templates/${btn.dataset.id}`);
- await _renderOutreach(el);
- });
- });
-
- // Vorlage bearbeiten
- el.querySelectorAll('.adm-tpl-edit').forEach(btn => {
- btn.addEventListener('click', () => {
- const tpl = templates.find(t => t.id === Number(btn.dataset.id));
- if (tpl) _openTplModal(el, tpl);
- });
- });
-
- // Neue Vorlage
- el.querySelector('#adm-tpl-new')?.addEventListener('click', () => _openTplModal(el, null));
-
- // Senden
- el.querySelector('#adm-outreach-form')?.addEventListener('submit', async e => {
- e.preventDefault();
- const btn = e.target.querySelector('[type="submit"]');
- const from_account = el.querySelector('#adm-outreach-from').value;
- const to = (el.querySelector('#adm-outreach-to').value || '')
- .split(',').map(s => s.trim()).filter(Boolean);
- const subject = el.querySelector('#adm-outreach-subject').value.trim();
- const body = el.querySelector('#adm-outreach-body').value.trim();
-
- if (!to.length) { UI.toast.warning('Bitte mindestens einen Empfänger eingeben.'); return; }
- if (!subject) { UI.toast.warning('Betreff fehlt.'); return; }
- if (!body) { UI.toast.warning('Text fehlt.'); return; }
-
- await UI.asyncButton(btn, async () => {
- const res = await API.post('/outreach/send', { to, subject, body, from_account });
- if (res.sent?.length) UI.toast.success(`${res.sent.length} E-Mail(s) gesendet.`);
- if (res.failed?.length) UI.toast.error(`${res.failed.length} Fehler: ${res.failed.map(f => f.error).join(', ')}`);
- await _renderOutreach(el);
- });
- });
- }
-
- function _openTplModal(el, tpl) {
- const isNew = !tpl;
- const id = `adm-tpl-modal-${Date.now()}`;
- UI.modal.open({
- title: isNew ? 'Neue Vorlage' : 'Vorlage bearbeiten',
- body: `
-
-
-
- Name (intern)
-
-
-
- Absender
-
- partner@banyaro.app
- support@banyaro.app
-
-
-
-
- Bezeichnung (sichtbar)
-
-
-
- Betreff
-
-
-
- Text
- ${_esc(tpl?.body || '')}
-
- `,
- footer: `
-
Abbrechen
-
Speichern `,
- });
-
- document.getElementById(id)?.addEventListener('submit', async e => {
- e.preventDefault();
- const payload = {
- label: document.getElementById(`${id}-label`).value.trim(),
- subject: document.getElementById(`${id}-subject`).value.trim(),
- body: document.getElementById(`${id}-body`).value.trim(),
- from_account: document.getElementById(`${id}-from`).value,
- };
- if (!payload.label || !payload.subject || !payload.body) {
- UI.toast.warning('Alle Felder ausfüllen.'); return;
- }
- if (isNew) {
- const key = document.getElementById(`${id}-key`).value.trim();
- if (!key) { UI.toast.warning('Interner Name fehlt.'); return; }
- await API.post('/outreach/templates', { ...payload, key });
- } else {
- await API.put(`/outreach/templates/${tpl.id}`, payload);
- }
- UI.modal.close();
- await _renderOutreach(el);
- });
- }
-
async function _renderAudit(el) {
el.innerHTML = `
diff --git a/backend/static/js/pages/datenschutz.js b/backend/static/js/pages/datenschutz.js
index 73c2ba8..838aaf4 100644
--- a/backend/static/js/pages/datenschutz.js
+++ b/backend/static/js/pages/datenschutz.js
@@ -29,7 +29,7 @@ window.Page_datenschutz = (() => {
${sec('Verantwortlicher', `
René Degelmann, Ringstr. 26, 85560 Ebersberg
- E-Mail: hallo@banyaro.app
+ E-Mail: mail@motocamp.de
`)}
${sec('Deine Daten gehören dir', `
@@ -169,7 +169,7 @@ window.Page_datenschutz = (() => {
(Art. 18) sowie
Datenportabilität (Art. 20). Erteilte Einwilligungen
kannst du jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3 DSGVO).
Zur Ausübung deiner Rechte wende dich per E-Mail an
-
hallo@banyaro.app .
+
mail@motocamp.de .
Du hast außerdem das Recht, bei der zuständigen Datenschutz-Aufsichtsbehörde
Beschwerde einzulegen:
Bayerisches Landesamt für Datenschutzaufsicht (BayLDA)
diff --git a/backend/static/js/pages/impressum.js b/backend/static/js/pages/impressum.js
index ffccb44..ac8b3bf 100644
--- a/backend/static/js/pages/impressum.js
+++ b/backend/static/js/pages/impressum.js
@@ -25,9 +25,9 @@ window.Page_impressum = (() => {
Kontakt
- E-Mail: hallo@banyaro.app
- Kontaktformular: mail@motocamp.de
+ Kontaktformular: Nachricht senden
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js
index ded9a7d..9ea9e0a 100644
--- a/backend/static/js/pages/map.js
+++ b/backend/static/js/pages/map.js
@@ -807,7 +807,6 @@ window.Page_map = (() => {
// Marker setzen (Placement-Mode)
// ----------------------------------------------------------
function _togglePlacementMode() {
- if (!_appState?.user) { App.navigate('welcome'); return; }
_placingMarker = !_placingMarker;
const btn = document.getElementById('map-pin-btn');
if (_placingMarker) {
diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js
index a0f3a9a..9a6b596 100644
--- a/backend/static/js/pages/settings.js
+++ b/backend/static/js/pages/settings.js
@@ -138,15 +138,7 @@ window.Page_settings = (() => {
style="display:none">
${_esc(u.name)}
-
- ${_esc(u.email)}
- ${u.email_verified
- ? ` `
- : `Nicht bestätigt `}
-
+
${_esc(u.email)}
${u.is_premium
? `
@@ -157,12 +149,6 @@ window.Page_settings = (() => {
? `
${u.founder_number ? `Gründer #${u.founder_number}` : 'Gründer'}
- `
- : u.is_founder_pending
- ? `
-
- Gründer-Platz reserviert
` : ''}
${u.is_partner
? `
@@ -488,12 +474,6 @@ window.Page_settings = (() => {
});
// Avatar-Hover-Overlay
- // E-Mail-Verifikation: Chip → erneut senden
- document.getElementById('settings-verify-chip')?.addEventListener('click', async () => {
- await API.post('/auth/resend-verification', {});
- UI.toast.success('Bestätigungs-Mail gesendet — bitte prüfe dein Postfach.');
- });
-
const avatarBtn = document.getElementById('settings-avatar-btn');
const avatarOverlay = avatarBtn?.querySelector('.avatar-overlay');
if (avatarBtn && avatarOverlay) {
@@ -1545,9 +1525,7 @@ window.Page_settings = (() => {
_appState.activeDog = null;
document.getElementById('header-login-btn')?.remove();
- const greeting = _appState.user.is_founder_pending
- ? `Willkommen, ${_appState.user.name}! 🎉 Dein Gründer-Platz ist reserviert — leg jetzt dein Hunde-Profil an um ihn zu sichern!`
- : _appState.user.is_founder
+ const greeting = _appState.user.is_founder
? `Willkommen, Gründer ${_appState.user.name}! 🎉`
: `Willkommen bei Ban Yaro, ${_appState.user.name}!`;
UI.toast.success(greeting);
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 9b0bb55..b443693 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-v575';
+const CACHE_VERSION = 'by-v562';
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
diff --git a/docker-compose.yml b/docker-compose.yml
index d1f4a45..a3d6772 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,8 +15,6 @@ services:
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
- VAPID_CONTACT=mailto:admin@banyaro.app
- - SMTP_SUPPORT_USER=support@banyaro.de
- - SMTP_SUPPORT_PASS=Marbled-Drool8-Whacky
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
interval: 30s