diff --git a/backend/main.py b/backend/main.py
index 7fbe195..3ebd1fa 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -250,7 +250,6 @@ from routes.ernaehrung import router as ernaehrung_router
from routes.challenges import router as challenges_router
from routes.gassi_zeiten import router as gassi_zeiten_router
from routes.help import router as help_router
-from routes.feedback import router as feedback_router
app.include_router(auth_router, prefix="/api/auth", tags=["Auth"])
app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"])
@@ -313,7 +312,6 @@ app.include_router(ernaehrung_router, prefix="/api/dogs", tag
app.include_router(challenges_router, prefix="/api/challenges", tags=["Foto-Challenge"])
app.include_router(gassi_zeiten_router, prefix="/api/gassi-zeiten", tags=["Gassi-Zeiten"])
app.include_router(help_router, prefix="/api/help", tags=["Hilfe/FAQ"])
-app.include_router(feedback_router, prefix="/api/feedback", tags=["Feedback"])
# ------------------------------------------------------------------
@@ -343,7 +341,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
os.makedirs(MEDIA_DIR, exist_ok=True)
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
-APP_VER = "834" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "826" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py
index fef8624..6c35334 100644
--- a/backend/routes/dogs.py
+++ b/backend/routes/dogs.py
@@ -6,7 +6,7 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
-from auth import get_current_user, has_pro_access
+from auth import get_current_user
from routes.push import send_push_to_user
from media_utils import safe_media_path, preview_url_from
@@ -131,14 +131,6 @@ def _is_plausible_dog(name: str, rasse: str, geburtstag) -> tuple[bool, str]:
@router.post("")
async def create_dog(data: DogCreate, user=Depends(get_current_user)):
with db() as conn:
- existing = conn.execute(
- "SELECT COUNT(*) FROM dogs WHERE user_id=?", (user["id"],)
- ).fetchone()[0]
- if existing >= 1 and not has_pro_access(user):
- raise HTTPException(
- status_code=403,
- detail="Mehrere Hunde sind ein Pro-Feature. Upgrade auf Ban Yaro Pro, um weitere Hunde anzulegen."
- )
conn.execute(
"""INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht,
gewicht_kg, chip_nr, bio, is_public)
diff --git a/backend/routes/feedback.py b/backend/routes/feedback.py
deleted file mode 100644
index a5c4792..0000000
--- a/backend/routes/feedback.py
+++ /dev/null
@@ -1,56 +0,0 @@
-"""
-BAN YARO — User-Feedback per E-Mail an support@banyaro.app
-"""
-
-from typing import Annotated, Literal
-from fastapi import APIRouter, Depends
-from pydantic import BaseModel, Field
-
-from auth import get_current_user
-from mailer import send_email, email_html
-
-router = APIRouter()
-
-SUPPORT_MAIL = "support@banyaro.app"
-
-KATEGORIEN = {"bug": "🐛 Bug / Fehler", "idee": "💡 Idee / Wunsch", "lob": "🎉 Lob", "sonstiges": "💬 Sonstiges"}
-
-
-class FeedbackIn(BaseModel):
- kategorie: Literal["bug", "idee", "lob", "sonstiges"]
- text: Annotated[str, Field(min_length=5, max_length=2000)]
-
-
-@router.post("")
-async def submit_feedback(
- payload: FeedbackIn,
- user=Depends(get_current_user),
-):
- kat_label = KATEGORIEN.get(payload.kategorie, payload.kategorie)
- username = user.get("name", "?")
- email = user.get("email", "")
- tier = user.get("subscription_tier", "standard")
-
- subject = f"[Feedback] {kat_label} von @{username}"
-
- body = f"""
-
- Neues Feedback aus der App:
-
-
- | Kategorie |
- {kat_label} |
- | User |
- @{username} ({email}) |
- | Tier |
- {tier} |
-
-
-{payload.text}
-
"""
-
- plain = f"Feedback [{kat_label}] von @{username} ({email})\n\n{payload.text}"
-
- await send_email(SUPPORT_MAIL, subject, email_html(body), plain)
- return {"ok": True}
diff --git a/backend/static/index.html b/backend/static/index.html
index b47048a..cf2117f 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 0b78595..893f1b4 100644
--- a/backend/static/js/api.js
+++ b/backend/static/js/api.js
@@ -58,10 +58,7 @@ const API = (() => {
try { data = await response.json(); } catch { data = null; }
if (!response.ok) {
- const _d = data?.detail;
- const message = (typeof _d === 'string' ? _d
- : Array.isArray(_d) ? (_d[0]?.msg || 'Ungültige Eingabe')
- : null) || data?.message || `Fehler ${response.status}`;
+ 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)
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 40d29a9..db6b183 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 = '834'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '826'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.0'; // ← 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/admin.js b/backend/static/js/pages/admin.js
index 8877953..0dfa8c7 100644
--- a/backend/static/js/pages/admin.js
+++ b/backend/static/js/pages/admin.js
@@ -534,22 +534,22 @@ window.Page_admin = (() => {
};
el.innerHTML = `
-
- ${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)', 'nutzer')}
- ${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)', 'nutzer')}
- ${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)', 'nutzer')}
- ${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)', 'nutzer')}
- ${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)','forum')}
- ${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)', 'moderation')}
- ${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)', 'moderation')}
- ${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)', 'nutzer')}
- ${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)', 'system')}
- ${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)','system')}
+
+ ${_statCard('users', 'Nutzer gesamt', s.users_total, 'var(--c-primary)')}
+ ${_statCard('user-plus', 'Neu heute', s.users_today, 'var(--c-success)')}
+ ${_statCard('activity', 'Aktiv (7 Tage)', s.active_users_7d, 'var(--c-primary)')}
+ ${_statCard('paw-print', 'Hunde', s.dogs_total, 'var(--c-primary)')}
+ ${_statCard('chat-circle-dots','Threads', s.threads, 'var(--c-text-secondary)')}
+ ${_statCard('warning', 'Offene Meldungen', s.open_reports, s.open_reports > 0 ? 'var(--c-danger)' : 'var(--c-text-muted)')}
+ ${_statCard('camera', 'Fotos freizugeben', s.pending_fotos ?? 0, (s.pending_fotos ?? 0) > 0 ? 'var(--c-warning)' : 'var(--c-text-muted)')}
+ ${_statCard('skull', 'Gesperrte User', s.banned, s.banned > 0 ? '#f59e0b' : 'var(--c-text-muted)')}
+ ${_statCard('warning-octagon', 'Giftk. aktiv', s.poison_active, 'var(--c-danger)')}
+ ${_statCard('bell', 'Push-Abos', s.push_subscriptions, 'var(--c-text-secondary)')}
${_statCard('image', 'Media-Einträge', s.media_count, 'var(--c-text-secondary)')}
${_statCard('map-pin', 'Routen', s.routes_total, 'var(--c-text-secondary)')}
${_statCard('calendar', 'Events', s.events_total, 'var(--c-text-secondary)')}
- ${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)', 'system')}
- ${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)', 'system')}
+ ${_statCard('map-trifold', 'OSM-Marker', s.osm_total.toLocaleString('de'), 'var(--c-success)')}
+ ${_statCard('squares-four', 'Gecachte Tiles', s.osm_tiles.toLocaleString('de'), 'var(--c-text-secondary)')}
@@ -705,19 +705,11 @@ window.Page_admin = (() => {
`;
-
- el.querySelector('#adm-overview-grid')?.addEventListener('click', e => {
- const card = e.target.closest('[data-adm-tab]');
- if (!card) return;
- const tab = card.dataset.admTab;
- _container.querySelector(`#adm-tabs .by-tab[data-tab="${tab}"]`)?.click();
- });
}
- function _statCard(icon, label, value, color, tab = null) {
- const clickable = tab ? `data-adm-tab="${tab}" style="padding:var(--space-4);text-align:center;cursor:pointer"` : `style="padding:var(--space-4);text-align:center"`;
+ function _statCard(icon, label, value, color) {
return `
-
+
-
${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? `
-
-
-
-
-
-
-
-
-
- `,
- footer: `
-
-
-
-
- `,
- });
-
- sel('feedback-form')?.addEventListener('submit', async e => {
- e.preventDefault();
- const btn = sel('feedback-submit-btn');
- const kat = sel('feedback-kat')?.value;
- const text = sel('feedback-text')?.value?.trim();
- if (!text) { UI.toast.error('Bitte schreib etwas.'); return; }
- await UI.asyncButton(btn, async () => {
- await API.post('/feedback', { kategorie: kat, text });
- UI.modal.close?.();
- UI.toast.success('Vielen Dank für dein Feedback!');
- });
- });
- });
-
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
const ok = await UI.modal.confirm({
title : 'Abmelden?',
diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js
index 5a580f4..7ec576a 100644
--- a/backend/static/js/ui.js
+++ b/backend/static/js/ui.js
@@ -79,34 +79,15 @@ const UI = (() => {
document.getElementById('modal-container').appendChild(overlay);
document.documentElement.classList.add('modal-open');
-
- // Tastatur auf Mobilgeräten: Modal nach oben schieben wenn Keyboard erscheint
- let _vvCleanup = null;
- const vv = window.visualViewport;
- if (vv) {
- const adjust = () => {
- const kb = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
- overlay.style.paddingBottom = (kb + 16) + 'px';
- };
- vv.addEventListener('resize', adjust);
- vv.addEventListener('scroll', adjust);
- _vvCleanup = () => {
- vv.removeEventListener('resize', adjust);
- vv.removeEventListener('scroll', adjust);
- overlay.style.paddingBottom = '';
- };
- }
-
- _current = { overlay, onClose, _vvCleanup };
+ _current = { overlay, onClose };
return overlay.querySelector('.modal');
}
function close() {
if (!_current) return;
- const { onClose, _vvCleanup } = _current;
+ const { onClose } = _current;
onClose?.();
- _vvCleanup?.();
_current.overlay.remove();
document.documentElement.classList.remove('modal-open');
_current = null;
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js
index 850bdbc..f23f983 100644
--- a/backend/static/js/worlds.js
+++ b/backend/static/js/worlds.js
@@ -1097,10 +1097,7 @@ window.Worlds = (() => {
${gassiScore ?? '—'}
${gassiScore ? `
/10` : ''}
- ${w ? `
-
${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen
- ${w.next_rain_time ? `
ab ${w.next_rain_time} Uhr
` : ''}
-
` : ''}
+ ${w ? `
${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen` : ''}
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 2c6afd1..5901c29 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-v834';
+const CACHE_VERSION = 'by-v826';
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/backend/weather.py b/backend/weather.py
index 467f657..5e836c6 100644
--- a/backend/weather.py
+++ b/backend/weather.py
@@ -58,7 +58,6 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
f"?latitude={lat}&longitude={lon}"
"¤t=temperature_2m,apparent_temperature,weathercode,windspeed_10m,is_day"
"&daily=precipitation_probability_max,uv_index_max"
- "&hourly=precipitation_probability"
"&timezone=Europe%2FBerlin&forecast_days=1"
)
async with httpx.AsyncClient(timeout=8.0) as client:
@@ -66,9 +65,8 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
resp.raise_for_status()
raw = resp.json()
- cur = raw.get('current', {})
- daily = raw.get('daily', {})
- hourly = raw.get('hourly', {})
+ cur = raw.get('current', {})
+ daily = raw.get('daily', {})
temp = cur.get('temperature_2m')
feels_like = cur.get('apparent_temperature')
@@ -87,36 +85,17 @@ async def get_weather_for_location(lat: float, lon: float) -> dict:
if temp is not None and temp > 7.0 and 3 <= month <= 10:
zecken = 'hoch' if temp > 20 else ('mittel' if temp > 12 else 'niedrig')
- # Nächste Regenstunde: erstes stündliches Fenster (jetzt+1h bis +12h) mit ≥60% Niederschlag
- next_rain_time = None
- already_raining = wcode >= 51
- if not already_raining:
- now_h = datetime.now().hour
- h_times = hourly.get('time', [])
- h_precip = hourly.get('precipitation_probability', [])
- for t, p in zip(h_times, h_precip):
- try:
- entry_h = int(t[11:13])
- except Exception:
- continue
- if entry_h <= now_h or entry_h > now_h + 12:
- continue
- if p is not None and p >= 20:
- next_rain_time = f"{entry_h:02d}:00"
- break
-
data = {
- 'temp_c': temp,
- 'feels_like_c': feels_like,
- 'weathercode': wcode,
- 'desc': desc,
- 'icon': icon,
- 'wind_kmh': wind,
- 'precip_prob': precip,
- 'uv_index': uv,
- 'is_day': bool(is_day),
- 'zecken_warnung': zecken,
- 'next_rain_time': next_rain_time,
+ 'temp_c': temp,
+ 'feels_like_c': feels_like,
+ 'weathercode': wcode,
+ 'desc': desc,
+ 'icon': icon,
+ 'wind_kmh': wind,
+ 'precip_prob': precip,
+ 'uv_index': uv,
+ 'is_day': bool(is_day),
+ 'zecken_warnung': zecken,
}
_location_cache[key] = (now, data)
return data