Compare commits
4 commits
947832f63d
...
9da8665aca
| Author | SHA1 | Date | |
|---|---|---|---|
| 9da8665aca | |||
| 6762efa04e | |||
| 60e5c9ba35 | |||
| 163a6ff6a5 |
5 changed files with 135 additions and 52 deletions
|
|
@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request):
|
|||
raise _HE(404, "Nicht gefunden.")
|
||||
return _media_response(filepath)
|
||||
|
||||
APP_VER = "939" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "942" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
|
|||
|
|
@ -124,9 +124,12 @@ async def action_items(user=Depends(require_mod)):
|
|||
users_today = conn.execute(
|
||||
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
|
||||
).fetchone()[0]
|
||||
upgrades_pending = conn.execute(
|
||||
"SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL"
|
||||
).fetchone()[0]
|
||||
try:
|
||||
upgrades_pending = conn.execute(
|
||||
"SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL"
|
||||
).fetchone()[0]
|
||||
except Exception:
|
||||
upgrades_pending = 0
|
||||
return {
|
||||
"jobs_pending": jobs,
|
||||
"breeder_pending": breeders,
|
||||
|
|
@ -1147,19 +1150,87 @@ async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)):
|
|||
|
||||
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||
tier_label = tier_labels.get(req["tier"], req["tier"])
|
||||
tier_price = {"pro": "29 €/Jahr", "breeder": "49 €/Jahr"}.get(req["tier"], "")
|
||||
|
||||
_features_pro = [
|
||||
("users", "Mehrere Hunde", "Bis zu 10 Hunde mit getrennten Trainings-, Gesundheits- und Ernährungsdaten"),
|
||||
("fork-knife", "Ernährung", "Kalorienbedarf-Rechner, BARF-Guide, vollständige Giftliste, KI-Ernährungsberater"),
|
||||
("paw-print", "Gassi-Treffen", "Hundefotos & Rasse der Teilnehmer sichtbar, Fotos hochladen und teilen"),
|
||||
("chat-circle-dots", "Direktnachrichten & Chat", "Schreibe direkt mit anderen Hundebesitzern"),
|
||||
("handshake", "Playdate", "Spielkameraden in der Nähe finden und verabreden"),
|
||||
("airplane", "Reise-Checkliste", "Editierbare Checkliste + EU-Länder-Einreiseregeln"),
|
||||
("note-pencil", "Notizblock mit KI", "KI erkennt Muster in deinen Notizen und macht Vorschläge"),
|
||||
("map-trifold", "Erweiterte Karten-Layer", "Regenradar, Temperatur-Layer und weitere Kartenmodi"),
|
||||
]
|
||||
_features_breeder = [
|
||||
("check-circle", "Alle Pro-Features inklusive", "Mehrere Hunde, Ernährung, Gassi-Community, Chat, Playdate, Reise, Karten"),
|
||||
("tree-structure", "Zuchtkartei", "Gesundheitstests (HD, ED, OCD, Augen, Herz, Patella, ZTP), Gentests (MDR1, PRA, DM, vWD), Titel"),
|
||||
("notebook", "Wurfverwaltung", "Würfe und Welpen verwalten, Gewichtsverlauf, Fotos, Kaufvertrag automatisch ausfüllen"),
|
||||
("list-bullets", "Warteliste", "Interessenten mit Präferenzen pro Zuchthündin verwalten"),
|
||||
("thermometer", "Läufigkeit & Trächtigkeit", "Zykluskalender, Progesterontests, Deckdaten, automatische Meilensteinberechnung"),
|
||||
("graph", "Stammbaum & IK-Rechner", "Stammbaum bis 4 Generationen, Inzuchtkoeffizient nach Wright, Probeverpaarung"),
|
||||
("sparkle", "KI-Züchter-Assistent", "Wurfankündigungen schreiben, Genetik-Erklärung, Paarungsanalyse, Jahresbericht"),
|
||||
("globe", "Öffentliches Züchter-Profil", "Visitenkarte unter banyaro.app/breeder/{zwingername} mit Hunden, Fotos und Gesundheitsstatistik"),
|
||||
("download-simple", "Datenexport", "Vollständiger Export als HTML-Dossier und ODS-Tabelle (LibreOffice/Excel)"),
|
||||
]
|
||||
|
||||
features = _features_breeder if req["tier"] == "breeder" else _features_pro
|
||||
|
||||
def _feature_row(icon, title, desc):
|
||||
return f"""
|
||||
<tr>
|
||||
<td style="padding:10px 12px 10px 0;vertical-align:top;width:28px">
|
||||
<div style="width:28px;height:28px;border-radius:8px;background:#fdf0e3;
|
||||
display:flex;align-items:center;justify-content:center;font-size:14px">✓</div>
|
||||
</td>
|
||||
<td style="padding:10px 0;vertical-align:top">
|
||||
<div style="font-weight:700;font-size:14px;color:#1a1a1a">{title}</div>
|
||||
<div style="font-size:13px;color:#666;margin-top:2px;line-height:1.4">{desc}</div>
|
||||
</td>
|
||||
</tr>"""
|
||||
|
||||
feature_rows = "".join(_feature_row(i, t, d) for i, t, d in features)
|
||||
|
||||
try:
|
||||
from mailer import send_email, email_html
|
||||
import html as _html
|
||||
name_esc = _html.escape(req["name"])
|
||||
body_html = f"""
|
||||
<p>Hallo {req['name']},</p>
|
||||
<p>dein Account wurde soeben auf <strong>{tier_label}</strong> freigeschaltet.</p>
|
||||
<p>Du kannst alle {tier_label}-Features ab sofort in der App nutzen.
|
||||
Öffne Ban Yaro und lade die App einmal neu — dann ist dein neuer Tarif aktiv.</p>
|
||||
<p>Vielen Dank für dein Vertrauen!</p>
|
||||
<p>Viele Grüße<br>René & das Ban Yaro Team</p>"""
|
||||
html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro öffnen")
|
||||
plain = (f"Hallo {req['name']},\n\ndein Account wurde auf {tier_label} freigeschaltet.\n"
|
||||
f"Öffne Ban Yaro und lade die App neu.\n\nViele Grüße\nRené")
|
||||
await send_email(req["email"], f"Dein {tier_label}-Zugang ist aktiv", html, plain)
|
||||
<p style="margin:0 0 8px;font-size:22px;font-weight:800;color:#1a1a1a">
|
||||
Herzlichen Glückwunsch, {name_esc}! 🎉
|
||||
</p>
|
||||
<p style="margin:0 0 20px;font-size:15px;color:#555">
|
||||
Dein Account wurde soeben auf <strong style="color:#C4843A">{tier_label}</strong>
|
||||
freigeschaltet. Vielen Dank für dein Vertrauen in Ban Yaro!
|
||||
</p>
|
||||
|
||||
<div style="background:#fdf6ef;border-radius:10px;padding:16px 20px;margin-bottom:24px">
|
||||
<div style="font-size:12px;font-weight:700;color:#C4843A;text-transform:uppercase;
|
||||
letter-spacing:.06em;margin-bottom:4px">Dein Tarif</div>
|
||||
<div style="font-size:18px;font-weight:800;color:#1a1a1a">{tier_label}
|
||||
<span style="font-size:13px;font-weight:400;color:#888;margin-left:8px">{tier_price}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size:13px;font-weight:700;color:#888;text-transform:uppercase;
|
||||
letter-spacing:.06em;margin-bottom:12px">Deine neuen Features</div>
|
||||
<table style="width:100%;border-collapse:collapse;margin-bottom:24px">
|
||||
{feature_rows}
|
||||
</table>
|
||||
|
||||
<div style="background:#f0f7ff;border-left:3px solid #C4843A;border-radius:0 8px 8px 0;
|
||||
padding:12px 16px;margin-bottom:8px;font-size:13px;color:#444;line-height:1.5">
|
||||
<strong>So aktivierst du deine Features:</strong><br>
|
||||
Öffne Ban Yaro und lade die App einmal neu (Startseite antippen → herunterziehen
|
||||
oder App schließen und neu öffnen). Alle Features sind dann sofort verfügbar.
|
||||
</div>"""
|
||||
|
||||
html = email_html(body_html, cta_url="https://banyaro.app", cta_label="Ban Yaro jetzt öffnen")
|
||||
plain = (f"Herzlichen Glückwunsch, {req['name']}!\n\n"
|
||||
f"Dein Account wurde auf {tier_label} ({tier_price}) freigeschaltet.\n\n"
|
||||
f"Öffne Ban Yaro und lade die App neu — alle Features sind dann aktiv.\n\n"
|
||||
f"Viele Grüße\nRené & das Ban Yaro Team")
|
||||
await send_email(req["email"], f"🎉 Dein {tier_label}-Zugang ist aktiv!", html, plain)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '939'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '942'; // ← 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
|
||||
|
|
|
|||
|
|
@ -3519,55 +3519,65 @@ window.Page_admin = (() => {
|
|||
const pending = rows.filter(r => !r.fulfilled_at);
|
||||
const done = rows.filter(r => r.fulfilled_at);
|
||||
|
||||
const _row = (r, showBtn) => `
|
||||
// Offene Anfragen als Cards (mobile-freundlich, Button immer sichtbar)
|
||||
const _pendingCard = r => `
|
||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3)">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:var(--space-3);flex-wrap:wrap">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(r.name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-bottom:var(--space-2)">${_esc(r.email)}</div>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
${tierBadge(r.tier)}
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">${r.created_at?.slice(0,10) || ''}</span>
|
||||
</div>
|
||||
${r.message ? `<div style="margin-top:var(--space-2);font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
background:var(--c-surface-raised,rgba(0,0,0,.04))">
|
||||
${_esc(r.message)}
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
|
||||
style="width:100%;margin-top:var(--space-3);background:#16a34a;color:#fff;border:none;
|
||||
padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);
|
||||
cursor:pointer;font-size:var(--text-sm);font-weight:600">
|
||||
✓ Freischalten
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
// Erledigte als kompakte Tabellenzeilen
|
||||
const _doneRow = r => `
|
||||
<tr>
|
||||
<td style="padding:var(--space-2) var(--space-3)">${_esc(r.name)}<br>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(r.name)}<br>
|
||||
<span style="font-size:var(--text-xs);color:var(--c-text-muted)">${_esc(r.email)}</span></td>
|
||||
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(r.tier)}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${r.message ? _esc(r.message) : '—'}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${r.created_at?.slice(0,10) || ''}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3)">
|
||||
${showBtn
|
||||
? `<button class="btn btn-sm adm-fulfill-btn" data-id="${r.id}" data-name="${_esc(r.name)}" data-tier="${r.tier}"
|
||||
style="background:#16a34a;color:#fff;border:none;padding:4px 12px;
|
||||
border-radius:var(--radius-md);cursor:pointer;font-size:var(--text-xs);font-weight:600">
|
||||
Freischalten
|
||||
</button>`
|
||||
: `<span style="font-size:var(--text-xs);color:var(--c-success)">✓ ${r.fulfilled_at?.slice(0,10)}</span>`}
|
||||
</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-success)">
|
||||
✓ ${r.fulfilled_at?.slice(0,10) || ''}</td>
|
||||
</tr>`;
|
||||
|
||||
const thead = `<thead><tr>
|
||||
${['Nutzer','Tarif','Nachricht','Datum','Aktion'].map(h =>
|
||||
`<th style="padding:var(--space-2) var(--space-3);text-align:left;font-size:var(--text-xs);
|
||||
color:var(--c-text-muted);font-weight:600;border-bottom:1px solid var(--c-border)">${h}</th>`
|
||||
).join('')}</tr></thead>`;
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card adm-table-card" style="margin-bottom:var(--space-4)">
|
||||
<div class="by-card-section-header">Offene Anfragen (${pending.length})</div>
|
||||
<div class="adm-table-scroll">
|
||||
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||
${thead}
|
||||
<tbody>
|
||||
${pending.length
|
||||
? pending.map(r => _row(r, true)).join('')
|
||||
: `<tr><td colspan="5" style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">
|
||||
Keine offenen Anfragen
|
||||
</td></tr>`}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-bottom:var(--space-4)">
|
||||
<div class="by-card-section-header" style="margin-bottom:var(--space-3)">
|
||||
Offene Anfragen (${pending.length})
|
||||
</div>
|
||||
${pending.length
|
||||
? pending.map(_pendingCard).join('')
|
||||
: `<div class="card" style="padding:var(--space-4);text-align:center;color:var(--c-text-muted);font-size:var(--text-sm)">
|
||||
Keine offenen Anfragen
|
||||
</div>`}
|
||||
</div>
|
||||
${done.length ? `
|
||||
<div class="card adm-table-card">
|
||||
<div class="by-card-section-header">Erledigt (${done.length})</div>
|
||||
<div class="adm-table-scroll">
|
||||
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||
${thead}
|
||||
<tbody>${done.map(r => _row(r, false)).join('')}</tbody>
|
||||
<thead><tr>
|
||||
${['Nutzer','Tarif','Freigegeben'].map(h =>
|
||||
`<th style="padding:var(--space-2) var(--space-3);text-align:left;font-size:var(--text-xs);
|
||||
color:var(--c-text-muted);font-weight:600;border-bottom:1px solid var(--c-border)">${h}</th>`
|
||||
).join('')}
|
||||
</tr></thead>
|
||||
<tbody>${done.map(_doneRow).join('')}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : ''}`;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v939';
|
||||
const CACHE_VERSION = 'by-v942';
|
||||
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
|
||||
|
|
@ -238,6 +238,8 @@ self.addEventListener('fetch', event => {
|
|||
// Media-Uploads + KI-Endpoints: direkt ans Netzwerk — kein Clone, kein Timeout, kein Queue
|
||||
// KI-Anfragen ans lokale LLM können mehrere Minuten dauern
|
||||
if (method === 'POST' && (_isMediaUpload(event.request) || url.pathname.startsWith('/api/ki/') || url.pathname.includes('/health/ki-') || url.pathname.includes('/health/symptom'))) return;
|
||||
// Admin-Endpoints: kein SW-Timeout, direkt ans Netzwerk
|
||||
if (url.pathname.startsWith('/api/admin/')) return;
|
||||
|
||||
// Mutationen (POST/PATCH/PUT/DELETE): mit Timeout, bei Offline → Queue
|
||||
if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue