diff --git a/backend/database.py b/backend/database.py index f5aee7b..2c37d10 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2145,31 +2145,6 @@ def _migrate(conn_factory): except Exception: pass # Spalte existiert bereits - # ---- Feature: is_active für Hunde (nach Abo-Downgrade) ---- - try: - conn.execute("ALTER TABLE dogs ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1") - logger.info("Migration: dogs.is_active hinzugefügt.") - except Exception: - pass - - # ---- Feature: Subscription-Laufzeit & Kündigung ---- - for col, typedef in [ - ("subscription_expires_at", "TEXT DEFAULT NULL"), - ("subscription_cancelled_at", "TEXT DEFAULT NULL"), - ("needs_dog_selection", "INTEGER DEFAULT 0"), - ]: - try: - conn.execute(f"ALTER TABLE users ADD COLUMN {col} {typedef}") - logger.info(f"Migration: {col} hinzugefügt.") - except Exception: - pass - # Bestehende TEXT-Werte für needs_dog_selection auf 0/1 normalisieren - try: - conn.execute("UPDATE users SET needs_dog_selection=0 WHERE needs_dog_selection='0' OR needs_dog_selection IS NULL") - conn.execute("UPDATE users SET needs_dog_selection=1 WHERE needs_dog_selection='1'") - except Exception: - pass - # exercise_progress + training_plan_progress: dog_id ergänzen existing_ep = [r[1] for r in conn.execute("PRAGMA table_info(exercise_progress)").fetchall()] if 'dog_id' not in existing_ep: diff --git a/backend/main.py b/backend/main.py index 6c17b53..44b051d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "946" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "944" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/admin.py b/backend/routes/admin.py index be0c8ed..09ab0b8 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -1138,12 +1138,9 @@ async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)): raise HTTPException(400, "Bereits erledigt.") if req["tier"] not in _VALID_TIERS: raise HTTPException(400, "Ungültiger Tier.") - from datetime import timedelta - expires_at = (datetime.now(_TZ) + timedelta(days=365)).strftime('%Y-%m-%dT%H:%M:%SZ') conn.execute( - """UPDATE users SET subscription_tier=?, subscription_expires_at=?, - subscription_cancelled_at=NULL WHERE id=?""", - (req["tier"], expires_at, req["user_id"]) + "UPDATE users SET subscription_tier=? WHERE id=?", + (req["tier"], req["user_id"]) ) conn.execute( "UPDATE upgrade_requests SET fulfilled_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), fulfilled_by=? WHERE id=?", diff --git a/backend/routes/auth.py b/backend/routes/auth.py index 59ad6e8..863be66 100644 --- a/backend/routes/auth.py +++ b/backend/routes/auth.py @@ -240,8 +240,7 @@ async def me(user=Depends(get_current_user)): profil_sichtbarkeit, avatar_url, created_at, is_founder, is_partner, founder_number, is_founder_pending, notes_ki_enabled, gassi_stunde_push, - preferred_theme, subscription_tier, - subscription_expires_at, subscription_cancelled_at, needs_dog_selection + preferred_theme, subscription_tier FROM users WHERE id=?""", (user["id"],) ).fetchone() @@ -395,70 +394,3 @@ async def reset_password(data: ResetPasswordRequest, request: Request): (hash_password(data.password), user["id"]) ) return {"ok": True} - - -@router.post("/subscription/cancel") -async def cancel_subscription(user=Depends(get_current_user)): - with db() as conn: - row = conn.execute( - "SELECT subscription_tier, subscription_expires_at, subscription_cancelled_at FROM users WHERE id=?", - (user["id"],) - ).fetchone() - if not row or row["subscription_tier"] in ("standard", "standard_test"): - raise HTTPException(400, "Kein aktives Abo vorhanden.") - if row["subscription_cancelled_at"]: - raise HTTPException(400, "Abo ist bereits gekündigt.") - conn.execute( - "UPDATE users SET subscription_cancelled_at=strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE id=?", - (user["id"],) - ) - expires = row["subscription_expires_at"] - - # Bestätigungsmail - try: - from mailer import send_email, email_html - import html as _html - tier_label = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}.get(row["subscription_tier"], row["subscription_tier"]) - expires_fmt = expires[:10] if expires else "—" - body_html = f""" -

Hallo {_html.escape(user['name'])},

-

deine Kündigung für {tier_label} wurde bestätigt.

-

Dein Abo ist weiterhin aktiv bis zum {expires_fmt}. - Ab diesem Datum wirst du automatisch auf den kostenlosen Tarif gesetzt.

-

Deine Daten (Tagebuch, Gesundheit, Notizen) bleiben vollständig erhalten. - Wenn du mehrere Hunde hast, kannst du vor dem Ablauf einen als Haupthund festlegen.

-

Wir hoffen, dich bald wieder begrüßen zu dürfen!

-

Viele Grüße
René & das Ban Yaro Team

""" - html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen") - plain = (f"Hallo {user['name']},\n\nKündigung bestätigt für {tier_label}.\n" - f"Aktiv bis: {expires_fmt}\n\nAlle Daten bleiben erhalten.\n\nViele Grüße\nRené") - await send_email(user["email"], f"Kündigung bestätigt — {tier_label}", html, plain) - except Exception: - pass - - return {"ok": True, "expires_at": expires} - - -@router.post("/subscription/select-dog") -async def select_primary_dog(body: dict, user=Depends(get_current_user)): - """Nach Downgrade: Haupthund auswählen, Rest bleibt erhalten aber inaktiv.""" - dog_id = body.get("dog_id") - if not dog_id: - raise HTTPException(400, "dog_id fehlt.") - with db() as conn: - dog = conn.execute( - "SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"]) - ).fetchone() - if not dog: - raise HTTPException(404, "Hund nicht gefunden.") - # Alle anderen Hunde deaktivieren - conn.execute( - "UPDATE dogs SET is_active=0 WHERE user_id=? AND id!=?", (user["id"], dog_id) - ) - conn.execute( - "UPDATE dogs SET is_active=1 WHERE id=?", (dog_id,) - ) - conn.execute( - "UPDATE users SET needs_dog_selection=0 WHERE id=?", (user["id"],) - ) - return {"ok": True} diff --git a/backend/routes/dogs.py b/backend/routes/dogs.py index 45228ed..832e777 100644 --- a/backend/routes/dogs.py +++ b/backend/routes/dogs.py @@ -43,7 +43,7 @@ class DogUpdate(BaseModel): async def list_dogs(user=Depends(get_current_user)): with db() as conn: own = conn.execute( - "SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) AND (is_active IS NULL OR is_active=1) ORDER BY id", + "SELECT *, NULL AS shared_by, NULL AS share_role FROM dogs WHERE user_id=? AND (verstorben_am IS NULL) ORDER BY id", (user["id"],) ).fetchall() shared = conn.execute( diff --git a/backend/scheduler.py b/backend/scheduler.py index 510b348..548e4c9 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -188,15 +188,8 @@ def start(): replace_existing=True, misfire_grace_time=3600, ) - _scheduler.add_job( - _job_subscription_check, - CronTrigger(hour=3, minute=0), - id="subscription_check", - replace_existing=True, - misfire_grace_time=3600, - ) _scheduler.start() - logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00, Abo-Check 03:00. OSM-Cache: on-demand (kein Prewarm).") + logger.info("Scheduler gestartet — Health-Reminder 08:00, Giftköder-Archiv 03:00, Wetter-Alert 07:30, Meilenstein-Check 00:05, Event-Import So 02:00, Rassen-Seed monatlich 1. des Monats, Status-Report täglich 06:00, Moderation-Overdue 12:00, Quartalsbericht 1. Feb/Mai/Aug/Nov 07:00, Streak-Reminder 19:00, Rückruf-Check 08:00, Goldene-Gassi-Stunde 07:00, Jahrestags-Erinnerungen 09:00, Monatlicher-Rückblick 1. des Monats 10:00, Foto-Challenge Mo 08:00. OSM-Cache: on-demand (kein Prewarm).") def stop(): @@ -204,82 +197,6 @@ def stop(): logger.info("Scheduler gestoppt.") -# ------------------------------------------------------------------ -# JOB: Abo-Ablauf prüfen (täglich 03:00) -# ------------------------------------------------------------------ -async def _job_subscription_check(): - """Abgelaufene Abos auf Standard setzen; Warnmails 30 und 7 Tage vorher.""" - from database import db as _db - from mailer import send_email, email_html - import html as _html - now = datetime.now(_TZ) - today = now.date() - - with _db() as conn: - users = conn.execute( - """SELECT id, name, email, subscription_tier, subscription_expires_at - FROM users - WHERE subscription_tier IN ('pro','breeder') - AND subscription_expires_at IS NOT NULL""" - ).fetchall() - - for u in users: - try: - expires = datetime.fromisoformat(u["subscription_expires_at"].replace('Z', '+00:00')).date() - days_left = (expires - today).days - tier_label = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}.get(u["subscription_tier"], u["subscription_tier"]) - - # Abgelaufen → auf Standard setzen - if days_left < 0: - with _db() as conn: - dog_count = conn.execute( - "SELECT COUNT(*) FROM dogs WHERE user_id=? AND is_active!=0", (u["id"],) - ).fetchone()[0] - needs_sel = 1 if dog_count > 1 else 0 - conn.execute( - """UPDATE users SET subscription_tier='standard', - needs_dog_selection=? WHERE id=?""", - (needs_sel, u["id"]) - ) - logger.info(f"Abo abgelaufen: {u['email']} → standard (needs_dog_selection={needs_sel})") - body = f""" -

Hallo {_html.escape(u['name'])},

-

dein {tier_label}-Abo ist heute abgelaufen. - Dein Account wurde auf den kostenlosen Tarif gesetzt.

-

Deine Daten sind vollständig erhalten. Du kannst jederzeit wieder upgraden.

""" - if needs_sel: - body += "

Wichtig: Du hattest mehrere Hunde. Öffne die App und wähle deinen Haupthund aus — alle anderen Profile bleiben gespeichert.

" - html = email_html(body, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen") - await send_email(u["email"], f"Dein {tier_label}-Abo ist abgelaufen", html, - f"Hallo {u['name']},\ndein {tier_label}-Abo ist abgelaufen. Daten bleiben erhalten.") - - # 30 Tage Warnung - elif days_left == 30: - body = f""" -

Hallo {_html.escape(u['name'])},

-

dein {tier_label}-Abo läuft in 30 Tagen - (am {expires.strftime('%d.%m.%Y')}) ab.

-

Um weiterzumachen, überweise einfach den Jahresbetrag und schreib uns kurz — - wir verlängern deinen Zugang sofort.

""" - html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern") - await send_email(u["email"], f"Dein {tier_label}-Abo läuft in 30 Tagen ab", html, - f"Hallo {u['name']},\ndein {tier_label}-Abo läuft in 30 Tagen ab ({expires}).") - - # 7 Tage Warnung - elif days_left == 7: - body = f""" -

Hallo {_html.escape(u['name'])},

-

dein {tier_label}-Abo läuft in 7 Tagen - (am {expires.strftime('%d.%m.%Y')}) ab.

-

Jetzt verlängern und nahtlos weitermachen!

""" - html = email_html(body, cta_url="https://banyaro.app", cta_label="Abo verlängern") - await send_email(u["email"], f"Nur noch 7 Tage — {tier_label}-Abo läuft ab", html, - f"Hallo {u['name']},\nnur noch 7 Tage für dein {tier_label}-Abo.") - - except Exception as e: - logger.warning(f"subscription_check Fehler für {u['email']}: {e}") - - # ------------------------------------------------------------------ # JOB: Gesundheits-Erinnerungen # ------------------------------------------------------------------ diff --git a/backend/static/js/api.js b/backend/static/js/api.js index c8b9c6c..8621935 100644 --- a/backend/static/js/api.js +++ b/backend/static/js/api.js @@ -127,12 +127,6 @@ const API = (() => { upgradeRequest(tier, message) { return post('/auth/upgrade-request', { tier, message }); }, - cancelSubscription() { - return post('/auth/subscription/cancel', {}); - }, - selectPrimaryDog(dog_id) { - return post('/auth/subscription/select-dog', { dog_id }); - }, }; // ---------------------------------------------------------- diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 20c119a..c6289ea 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 = '946'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '944'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen @@ -567,11 +567,6 @@ const App = (() => { navigate('onboarding'); } - // Abo abgelaufen mit mehreren Hunden → Haupthund auswählen (nur wenn explizit 1, nicht "0" string) - if (state.user.needs_dog_selection === 1 && state.dogs.length > 1) { - _showDogSelectionModal(); - } - // Theme aus DB-Profil übernehmen (überschreibt localStorage-Wert) _applyUserTheme(state.user); @@ -673,57 +668,6 @@ const App = (() => { document.getElementById('meta-theme-color')?.setAttribute('content', isDark ? '#0f1623' : '#C4843A'); } - function _showDogSelectionModal() { - const dogs = state.dogs; - const optionHtml = dogs.map(d => ` - `).join(''); - - UI.modal.open({ - title: 'Haupthund auswählen', - body: ` -

- Dein Abo ist ausgelaufen. Wähle einen Haupthund für deinen kostenlosen Account. - Alle anderen Hunde-Profile bleiben vollständig gespeichert — du kannst sie nach - einem erneuten Upgrade wieder aktivieren. -

-
${optionHtml}
`, - footer: ` - ` - }); - - document.getElementById('dog-select-confirm')?.addEventListener('click', async () => { - const chosen = document.querySelector('[name="select-dog"]:checked')?.value; - if (!chosen) { UI.toast.warning('Bitte einen Hund auswählen.'); return; } - const btn = document.getElementById('dog-select-confirm'); - btn.disabled = true; btn.textContent = '…'; - try { - await API.auth.selectPrimaryDog(parseInt(chosen)); - state.user.needs_dog_selection = 0; - state.activeDog = state.dogs.find(d => String(d.id) === chosen) || state.dogs[0]; - localStorage.setItem('by_active_dog', String(state.activeDog.id)); - UI.modal.close(); - UI.toast.success('Haupthund festgelegt.'); - _renderDogSwitcher(); - } catch (e) { - btn.disabled = false; btn.textContent = 'Auswahl bestätigen'; - UI.toast.error(e.message || 'Fehler.'); - } - }); - } - function _showAndroidBetaBanner() { // Nur auf Android, nur einmalig, nur für eingeloggte Nutzer if (!/android/i.test(navigator.userAgent)) return; diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index 89e527b..1bacd62 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -104,43 +104,18 @@ window.Page_settings = (() => { ${price} `; - const expires = u.subscription_expires_at; - const cancelled = u.subscription_cancelled_at; - const expiresDate = expires ? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) : null; - const isPaid = (isPro || isBreeder) && !tier.endsWith('_test') && !isAdmin; - - const _expiryInfo = () => { - if (!isPaid || !expiresDate) return ''; - const color = cancelled ? '#e65100' : 'var(--c-text-secondary)'; - const text = cancelled - ? `Gekündigt — läuft bis ${expiresDate}` - : `Aktiv bis ${expiresDate}`; - return `
${text}
`; - }; - - const _cancelBtn = () => { - if (!isPaid || cancelled) return ''; - return ``; - }; - let statusHtml = ''; let actionsHtml = ''; if (isAdmin) { statusHtml = _badge('Admin', '#6366f1'); } else if (isBreeder) { - statusHtml = _badge(cancelled ? 'Züchter (gekündigt)' : 'Züchter aktiv', '#C4843A'); + statusHtml = _badge('Züchter aktiv', '#C4843A'); } else if (isPro) { - statusHtml = _badge(cancelled ? 'Pro (gekündigt)' : 'Pro aktiv', '#16a34a'); + statusHtml = _badge('Pro aktiv', '#16a34a'); actionsHtml = `
- ${!cancelled ? _upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A') : ''} + ${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')}
`; } else { statusHtml = _badge('Kostenlos', '#888'); @@ -159,9 +134,7 @@ window.Page_settings = (() => { Aktueller Tarif: ${statusHtml} - ${_expiryInfo()} ${actionsHtml} - ${_cancelBtn()} `; } @@ -367,71 +340,6 @@ window.Page_settings = (() => { }); } - function _showCancelModal() { - const u = _appState.user; - const tier = u?.subscription_tier || 'standard'; - const label = { pro: 'Ban Yaro Pro', breeder: 'Züchter' }[tier] || tier; - const expires = u?.subscription_expires_at; - const expiresDate = expires - ? new Date(expires).toLocaleDateString('de-DE', {day:'numeric',month:'long',year:'numeric'}) - : null; - - UI.modal.open({ - title: `${label} kündigen`, - body: ` -
- ${expiresDate ? ` -
- Dein Abo läuft noch bis ${expiresDate} — du hast bis dahin vollen Zugriff. -
` : ''} -
-
✓ Alle deine Daten (Tagebuch, Gesundheit, Notizen) bleiben vollständig erhalten
-
✓ Deine Hunde-Profile bleiben gespeichert
-
✓ Du kannst jederzeit wieder upgraden
- ${_appState.dogs?.length > 1 - ? `
⚠ Du hast mehrere Hunde — nach dem Ablauf wählst du einen als Haupthund
` - : ''} -
-
`, - footer: ` - - ` - }); - - document.getElementById('cancel-sub-confirm-btn')?.addEventListener('click', async () => { - const btn = document.getElementById('cancel-sub-confirm-btn'); - if (!btn) return; - btn.disabled = true; - btn.textContent = '…'; - try { - await API.auth.cancelSubscription(); - // User-State aktualisieren - const fresh = await API.auth.me(); - Object.assign(_appState.user, fresh); - UI.modal.close(); - UI.toast.success('Kündigung bestätigt. Eine Bestätigungsmail wurde gesendet.'); - _render(); - } catch (e) { - btn.disabled = false; - btn.textContent = 'Jetzt kündigen'; - UI.toast.error(e.message || 'Fehler beim Kündigen.'); - } - }); - } - // ---------------------------------------------------------- // RENDER // ---------------------------------------------------------- @@ -1194,9 +1102,6 @@ window.Page_settings = (() => { document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => { _showUpgradeModal('breeder'); }); - document.getElementById('settings-cancel-sub-btn')?.addEventListener('click', () => { - _showCancelModal(); - }); document.getElementById('settings-worlds-btn')?.addEventListener('click', () => { if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal(); diff --git a/backend/static/sw.js b/backend/static/sw.js index c90276e..9cbed57 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-v946'; +const CACHE_VERSION = 'by-v944'; 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