Compare commits
No commits in common. "52160e4dc02df517e57a11474c9133a2001a6487" and "0a7bb931b3b48ca63115f0cab8c02fbec487cedf" have entirely different histories.
52160e4dc0
...
0a7bb931b3
16 changed files with 76 additions and 695 deletions
|
|
@ -2341,24 +2341,6 @@ def _migrate(conn_factory):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# upgrade_requests: Abo-Upgrade-Anfragen von Nutzern
|
||||
try:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS upgrade_requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
tier TEXT NOT NULL,
|
||||
message TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
||||
fulfilled_at TEXT,
|
||||
fulfilled_by INTEGER REFERENCES users(id)
|
||||
)
|
||||
""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_upgrade_req_pending ON upgrade_requests(fulfilled_at, created_at DESC)")
|
||||
logger.info("Migration: upgrade_requests bereit.")
|
||||
except Exception as e:
|
||||
logger.warning(f"Migration upgrade_requests: {e}")
|
||||
|
||||
# route_dogs: bestehende Routen allen Hunden des Users zuweisen
|
||||
try:
|
||||
existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0]
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request):
|
|||
raise _HE(404, "Nicht gefunden.")
|
||||
return _media_response(filepath)
|
||||
|
||||
APP_VER = "921" # muss mit APP_VER in app.js übereinstimmen
|
||||
APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen
|
||||
|
||||
@app.get("/.well-known/assetlinks.json")
|
||||
async def assetlinks():
|
||||
|
|
@ -465,11 +465,10 @@ async def sitemap():
|
|||
today = date.today().isoformat()
|
||||
urls = [
|
||||
("https://banyaro.app/", "weekly", "1.0"),
|
||||
("https://banyaro.app/zuechter", "weekly", "0.9"),
|
||||
("https://banyaro.app/info", "monthly", "0.8"),
|
||||
("https://banyaro.app/presse", "monthly", "0.7"),
|
||||
("https://banyaro.app/info", "monthly", "0.9"),
|
||||
("https://banyaro.app/presse", "monthly", "0.8"),
|
||||
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
|
||||
("https://banyaro.app/knigge", "monthly", "0.7"),
|
||||
("https://banyaro.app/knigge", "monthly", "0.8"),
|
||||
("https://banyaro.app/wurfboerse", "daily", "0.8"),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -124,9 +124,6 @@ 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]
|
||||
return {
|
||||
"jobs_pending": jobs,
|
||||
"breeder_pending": breeders,
|
||||
|
|
@ -134,7 +131,6 @@ async def action_items(user=Depends(require_mod)):
|
|||
"fotos_pending": fotos,
|
||||
"poi_edits_pending": poi_edits,
|
||||
"users_today": users_today,
|
||||
"upgrades_pending": upgrades_pending,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1095,66 +1091,3 @@ async def generate_media_previews(user=Depends(require_admin)):
|
|||
errors += 1
|
||||
|
||||
return {"generated": generated, "skipped": skipped, "errors": errors}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/admin/upgrade-requests — offene Upgrade-Anfragen
|
||||
# POST /api/admin/upgrade-requests/{id}/fulfill — Tier setzen + Mail
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/upgrade-requests")
|
||||
async def list_upgrade_requests(user=Depends(require_admin)):
|
||||
with db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT r.id, r.user_id, r.tier, r.message, r.created_at, r.fulfilled_at,
|
||||
u.name, u.email
|
||||
FROM upgrade_requests r
|
||||
JOIN users u ON u.id = r.user_id
|
||||
ORDER BY r.fulfilled_at IS NOT NULL, r.created_at DESC
|
||||
LIMIT 100
|
||||
""").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/upgrade-requests/{req_id}/fulfill")
|
||||
async def fulfill_upgrade_request(req_id: int, user=Depends(require_admin)):
|
||||
with db() as conn:
|
||||
req = conn.execute(
|
||||
"SELECT r.*, u.name, u.email FROM upgrade_requests r JOIN users u ON u.id=r.user_id WHERE r.id=?",
|
||||
(req_id,)
|
||||
).fetchone()
|
||||
if not req:
|
||||
raise HTTPException(404, "Anfrage nicht gefunden.")
|
||||
if req["fulfilled_at"]:
|
||||
raise HTTPException(400, "Bereits erledigt.")
|
||||
if req["tier"] not in _VALID_TIERS:
|
||||
raise HTTPException(400, "Ungültiger Tier.")
|
||||
conn.execute(
|
||||
"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=?",
|
||||
(user["id"], req_id)
|
||||
)
|
||||
_audit(conn, user, "fulfill_upgrade", f"user:{req['user_id']}", f"tier={req['tier']}")
|
||||
|
||||
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||
tier_label = tier_labels.get(req["tier"], req["tier"])
|
||||
try:
|
||||
from mailer import send_email, email_html
|
||||
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)
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Bestätigungsmail fehlgeschlagen: {e}")
|
||||
|
||||
return {"ok": True, "tier": req["tier"], "user": req["name"]}
|
||||
|
|
|
|||
|
|
@ -240,7 +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
|
||||
preferred_theme
|
||||
FROM users WHERE id=?""",
|
||||
(user["id"],)
|
||||
).fetchone()
|
||||
|
|
@ -335,46 +335,6 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
|||
return {"ok": True}
|
||||
|
||||
|
||||
class UpgradeRequestBody(BaseModel):
|
||||
tier: str
|
||||
message: Optional[str] = None
|
||||
|
||||
@router.post("/upgrade-request")
|
||||
async def create_upgrade_request(data: UpgradeRequestBody, user=Depends(get_current_user)):
|
||||
_VALID = {"pro", "breeder"}
|
||||
if data.tier not in _VALID:
|
||||
raise HTTPException(400, "Ungültiger Tarif.")
|
||||
with db() as conn:
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM upgrade_requests WHERE user_id=? AND tier=? AND fulfilled_at IS NULL",
|
||||
(user["id"], data.tier)
|
||||
).fetchone()
|
||||
if existing:
|
||||
return {"ok": True, "already": True}
|
||||
conn.execute(
|
||||
"INSERT INTO upgrade_requests (user_id, tier, message) VALUES (?,?,?)",
|
||||
(user["id"], data.tier, data.message or None)
|
||||
)
|
||||
email = conn.execute("SELECT email FROM users WHERE id=?", (user["id"],)).fetchone()["email"]
|
||||
|
||||
tier_labels = {"pro": "Ban Yaro Pro", "breeder": "Züchter"}
|
||||
tier_label = tier_labels[data.tier]
|
||||
admin_email = os.getenv("ADMIN_EMAIL", "")
|
||||
if admin_email:
|
||||
try:
|
||||
from routes.outreach import _send_smtp
|
||||
subject = f"[Ban Yaro] Upgrade-Anfrage: {tier_label} — {user['name']}"
|
||||
body = (f"Neue Upgrade-Anfrage:\n\n"
|
||||
f"Nutzer: {user['name']} ({email})\n"
|
||||
f"Tarif: {tier_label}\n"
|
||||
f"Nachricht: {data.message or '—'}\n\n"
|
||||
f"Admin-Panel: https://banyaro.app/#admin")
|
||||
_send_smtp(admin_email, subject, body, "support")
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||
|
|
|
|||
|
|
@ -183,27 +183,6 @@ async def admin_pending_breeders(admin=Depends(require_admin)):
|
|||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/admin/breeders — alle aktiven Züchter
|
||||
# ------------------------------------------------------------------
|
||||
@router.get("/admin/breeders")
|
||||
async def admin_all_breeders(admin=Depends(require_admin)):
|
||||
with db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT u.id, u.name, u.email, u.created_at, u.subscription_tier,
|
||||
u.breeder_status, u.last_login,
|
||||
bp.zwingername, bp.rasse_text, bp.verein, bp.vdh_mitglied,
|
||||
bp.stadt, bp.website, bp.verified_at,
|
||||
(SELECT COUNT(*) FROM litters WHERE user_id=u.id) AS wuerfe_count,
|
||||
(SELECT COUNT(*) FROM dogs WHERE user_id=u.id AND is_zucht_hund=1) AS zuchthunde_count
|
||||
FROM users u
|
||||
LEFT JOIN breeder_profiles bp ON bp.user_id = u.id
|
||||
WHERE u.rolle = 'breeder' OR u.breeder_status = 'approved'
|
||||
ORDER BY bp.verified_at DESC NULLS LAST, u.created_at DESC
|
||||
""").fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -599,10 +599,10 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=919"></script>
|
||||
<script src="/js/ui.js?v=919"></script>
|
||||
<script src="/js/app.js?v=919"></script>
|
||||
<script src="/js/worlds.js?v=919"></script>
|
||||
<script src="/js/api.js?v=918"></script>
|
||||
<script src="/js/ui.js?v=918"></script>
|
||||
<script src="/js/app.js?v=918"></script>
|
||||
<script src="/js/worlds.js?v=918"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
|
|||
|
|
@ -124,9 +124,6 @@ const API = (() => {
|
|||
return get('/auth/me');
|
||||
},
|
||||
referral: () => get('/auth/referral'),
|
||||
upgradeRequest(tier, message) {
|
||||
return post('/auth/upgrade-request', { tier, message });
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -691,7 +688,6 @@ const API = (() => {
|
|||
updateProfile(data) { return put('/breeder/profile', data); },
|
||||
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
|
||||
pendingList() { return get('/admin/breeders/pending'); },
|
||||
allList() { return get('/admin/breeders'); },
|
||||
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
|
||||
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
|
||||
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '921'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '918'; // ← 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
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ window.Page_admin = (() => {
|
|||
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
||||
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
||||
{ id: 'referrals', label: 'Referrals', icon: 'share-network' },
|
||||
{ id: 'upgrades', label: 'Upgrades', icon: 'crown-simple' },
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -91,7 +90,6 @@ window.Page_admin = (() => {
|
|||
try { d = await API.get('/admin/action-items'); } catch { return; }
|
||||
|
||||
const items = [
|
||||
{ key: 'upgrades_pending', label: 'Upgrade-Anfragen', tab: 'upgrades', icon: 'crown-simple' },
|
||||
{ key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' },
|
||||
{ key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' },
|
||||
{ key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' },
|
||||
|
|
@ -165,7 +163,6 @@ window.Page_admin = (() => {
|
|||
case 'hilfe': await _renderHilfe(el); break;
|
||||
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
||||
case 'referrals': await _renderReferrals(el); break;
|
||||
case 'upgrades': await _renderUpgrades(el); break;
|
||||
}
|
||||
} catch (e) {
|
||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||
|
|
@ -1893,17 +1890,12 @@ window.Page_admin = (() => {
|
|||
${UI.icon('arrows-clockwise')} Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div id="adm-zuchter-antraege">Lade…</div>
|
||||
<div id="adm-zuchter-liste" style="margin-top:var(--space-4)">Lade…</div>
|
||||
<div id="adm-zuchter-list">Lade…</div>
|
||||
`;
|
||||
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => {
|
||||
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege'));
|
||||
_loadZuechterListe(el.querySelector('#adm-zuchter-liste'));
|
||||
});
|
||||
await Promise.all([
|
||||
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')),
|
||||
_loadZuechterListe(el.querySelector('#adm-zuchter-liste')),
|
||||
]);
|
||||
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () =>
|
||||
_loadZuechterAntraege(el.querySelector('#adm-zuchter-list'))
|
||||
);
|
||||
await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list'));
|
||||
}
|
||||
|
||||
async function _loadZuechterAntraege(el) {
|
||||
|
|
@ -1917,20 +1909,12 @@ window.Page_admin = (() => {
|
|||
}
|
||||
|
||||
if (!antraege.length) {
|
||||
el.innerHTML = `<div class="card" style="padding:var(--space-4)">
|
||||
<div class="by-card-section-header">Offene Anträge</div>
|
||||
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-sm);color:var(--c-text-muted)">
|
||||
${UI.icon('check-circle')} Keine offenen Anträge
|
||||
</div>
|
||||
</div>`;
|
||||
el.innerHTML = _emptyState('certificate', 'Keine offenen Anträge', 'Aktuell liegen keine Züchter-Anträge zur Prüfung vor.');
|
||||
return;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card" style="margin-bottom:0">
|
||||
<div class="by-card-section-header">Offene Anträge (${antraege.length})</div>
|
||||
</div>
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3);margin-top:var(--space-3)">
|
||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
||||
${antraege.map(a => `
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
|
||||
|
|
@ -2085,74 +2069,6 @@ window.Page_admin = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
async function _loadZuechterListe(el) {
|
||||
el.innerHTML = `<div style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">Lade…</div>`;
|
||||
let breeders;
|
||||
try {
|
||||
breeders = await API.breeder.allList();
|
||||
} catch (e) {
|
||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const tierBadge = t => {
|
||||
if (t === 'breeder') return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#C4843A;color:#fff">Züchter-Abo</span>`;
|
||||
if (t === 'breeder_test') return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#aaa;color:#fff">Test</span>`;
|
||||
return `<span style="display:inline-block;padding:1px 7px;border-radius:999px;font-size:10px;font-weight:700;background:#eee;color:#666">Standard</span>`;
|
||||
};
|
||||
|
||||
const rows = breeders.map(b => `
|
||||
<tr>
|
||||
<td style="padding:var(--space-2) var(--space-3)">
|
||||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(b.name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">${_esc(b.email)}</div>
|
||||
</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-sm)">${_esc(b.zwingername || '—')}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.rasse_text || '—')}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(b.stadt || '—')}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);text-align:center;font-size:var(--text-xs)">
|
||||
${b.wuerfe_count || 0} Würfe<br>
|
||||
<span style="color:var(--c-text-muted)">${b.zuchthunde_count || 0} Zuchthunde</span>
|
||||
</td>
|
||||
<td style="padding:var(--space-2) var(--space-3)">${tierBadge(b.subscription_tier)}</td>
|
||||
<td style="padding:var(--space-2) var(--space-3);font-size:var(--text-xs);color:var(--c-text-muted)">
|
||||
${b.verified_at ? new Date(b.verified_at).toLocaleDateString('de-DE') : '—'}
|
||||
</td>
|
||||
<td style="padding:var(--space-2) var(--space-3)">
|
||||
<button class="btn btn-sm btn-ghost adm-breeder-tier-btn"
|
||||
data-uid="${b.id}" data-name="${_esc(b.name)}" data-tier="${_esc(b.subscription_tier || 'standard')}"
|
||||
style="font-size:var(--text-xs)">
|
||||
Abo
|
||||
</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card adm-table-card">
|
||||
<div class="by-card-section-header">Alle Züchter (${breeders.length})</div>
|
||||
<div class="adm-table-scroll">
|
||||
<table class="adm-table" style="width:100%;border-collapse:collapse">
|
||||
<thead><tr>
|
||||
${['Nutzer','Zwingername','Rasse','Stadt','Aktivität','Abo','Seit',''].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);white-space:nowrap">${h}</th>`
|
||||
).join('')}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
${rows || `<tr><td colspan="8" style="padding:var(--space-4);text-align:center;color:var(--c-text-muted)">Noch keine Züchter</td></tr>`}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
el.querySelectorAll('.adm-breeder-tier-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () =>
|
||||
_changeTier(btn.dataset.uid, btn.dataset.name, btn.dataset.tier)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderJobs(el) {
|
||||
el.innerHTML = `
|
||||
|
|
@ -3503,104 +3419,6 @@ window.Page_admin = (() => {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB: UPGRADES
|
||||
// ------------------------------------------------------------------
|
||||
async function _renderUpgrades(el) {
|
||||
const rows = await API.get('/admin/upgrade-requests');
|
||||
|
||||
const tierBadge = t => {
|
||||
const cfg = { pro: ['Pro', '#16a34a'], breeder: ['Züchter', '#C4843A'] };
|
||||
const [label, color] = cfg[t] || [t, '#888'];
|
||||
return `<span style="display:inline-block;padding:1px 8px;border-radius:999px;
|
||||
font-size:11px;font-weight:700;background:${color};color:#fff">${label}</span>`;
|
||||
};
|
||||
|
||||
const pending = rows.filter(r => !r.fulfilled_at);
|
||||
const done = rows.filter(r => r.fulfilled_at);
|
||||
|
||||
const _row = (r, showBtn) => `
|
||||
<tr>
|
||||
<td style="padding:var(--space-2) var(--space-3)">${_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>
|
||||
</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>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : ''}`;
|
||||
|
||||
el.querySelectorAll('.adm-fulfill-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const { id, name, tier } = btn.dataset;
|
||||
const tierLabel = { pro: 'Pro', breeder: 'Züchter' }[tier] || tier;
|
||||
const ok = await UI.modal.confirm({
|
||||
title: `${name} auf ${tierLabel} freischalten?`,
|
||||
body: `<p style="font-size:var(--text-sm)">
|
||||
Der Account wird auf <strong>${tierLabel}</strong> gesetzt und
|
||||
eine Bestätigungsmail gesendet.
|
||||
</p>`,
|
||||
confirmLabel: 'Freischalten',
|
||||
danger: false,
|
||||
});
|
||||
if (!ok) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '…';
|
||||
try {
|
||||
const res = await API.post(`/admin/upgrade-requests/${id}/fulfill`);
|
||||
UI.toast.success(`${res.user} wurde auf ${tierLabel} freigeschaltet.`);
|
||||
_renderTab();
|
||||
} catch (e) {
|
||||
UI.toast.error(e.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Freischalten';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -77,137 +77,6 @@ window.Page_settings = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ABO & TARIF
|
||||
// ----------------------------------------------------------
|
||||
function _tierCard(u) {
|
||||
const tier = u.subscription_tier || 'standard';
|
||||
const rolle = u.rolle || 'user';
|
||||
const isAdmin = rolle === 'admin' || rolle === 'moderator';
|
||||
const isPro = ['pro','pro_test'].includes(tier);
|
||||
const isBreeder = ['breeder','breeder_test'].includes(tier) || rolle === 'breeder';
|
||||
const isStandard = !isAdmin && !isPro && !isBreeder;
|
||||
|
||||
const _badge = (label, color) =>
|
||||
`<span style="display:inline-block;padding:2px 10px;border-radius:20px;
|
||||
font-size:var(--text-xs);font-weight:700;letter-spacing:.03em;
|
||||
background:${color};color:#fff">${label}</span>`;
|
||||
|
||||
const _upgradeBtn = (id, label, price, color) =>
|
||||
`<button id="${id}"
|
||||
style="flex:1;min-width:130px;padding:var(--space-3) var(--space-2);
|
||||
border-radius:var(--radius-md);border:none;cursor:pointer;
|
||||
background:${color};color:#fff;
|
||||
font-size:var(--text-sm);font-weight:600;
|
||||
display:flex;flex-direction:column;align-items:center;gap:2px">
|
||||
<span>${label}</span>
|
||||
<span style="font-size:var(--text-xs);font-weight:400;opacity:.9">${price}</span>
|
||||
</button>`;
|
||||
|
||||
let statusHtml = '';
|
||||
let actionsHtml = '';
|
||||
|
||||
if (isAdmin) {
|
||||
statusHtml = _badge('Admin', '#6366f1');
|
||||
} else if (isBreeder) {
|
||||
statusHtml = _badge('Züchter aktiv', '#C4843A');
|
||||
} else if (isPro) {
|
||||
statusHtml = _badge('Pro aktiv', '#16a34a');
|
||||
actionsHtml = `
|
||||
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||
${_upgradeBtn('settings-upgrade-breeder-btn','Züchter werden','49 €/Jahr','#C4843A')}
|
||||
</div>`;
|
||||
} else {
|
||||
statusHtml = _badge('Kostenlos', '#888');
|
||||
actionsHtml = `
|
||||
<div style="margin-top:var(--space-3);display:flex;gap:var(--space-2);flex-wrap:wrap">
|
||||
${_upgradeBtn('settings-upgrade-pro-btn','Ban Yaro Pro','29 €/Jahr','#16a34a')}
|
||||
${_upgradeBtn('settings-upgrade-breeder-btn','Züchter','49 €/Jahr','#C4843A')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div class="by-card-section-header">Abo & Tarif</div>
|
||||
<div style="padding:var(--space-4)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">Aktueller Tarif:</span>
|
||||
${statusHtml}
|
||||
</div>
|
||||
${actionsHtml}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function _showUpgradeModal(tier) {
|
||||
const isPro = tier === 'pro';
|
||||
const label = isPro ? 'Ban Yaro Pro' : 'Züchter';
|
||||
const price = isPro ? '29 €/Jahr' : '49 €/Jahr';
|
||||
const color = isPro ? '#16a34a' : '#C4843A';
|
||||
const features = isPro
|
||||
? ['Mehrere Hunde verwalten', 'Ernährungsbereich mit KI-Berater', 'Erweiterte Karten-Layer', 'Alle künftigen Pro-Features']
|
||||
: ['Vollständige Züchter-Plattform', 'Warteliste, Läufigkeit & Trächtigkeit', 'Wurfverwaltung, Stammbaum, IK-Rechner', 'KI-Züchter-Assistent & Datenexport'];
|
||||
|
||||
const featureList = features.map(f =>
|
||||
`<li style="padding:var(--space-1) 0;font-size:var(--text-sm)">✓ ${f}</li>`
|
||||
).join('');
|
||||
|
||||
UI.modal.open({
|
||||
title: `${label} freischalten`,
|
||||
body: `
|
||||
<div style="padding:var(--space-2) 0">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4)">
|
||||
<div style="font-size:2rem;font-weight:800;color:${color}">${price}</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.5">
|
||||
Einmaliger Jahresbeitrag<br>Kündigung jederzeit möglich
|
||||
</div>
|
||||
</div>
|
||||
<ul style="list-style:none;padding:0;margin:0 0 var(--space-4)">
|
||||
${featureList}
|
||||
</ul>
|
||||
<div style="padding:var(--space-3);border-radius:var(--radius-md);
|
||||
background:var(--c-surface-raised,rgba(0,0,0,.04));
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary);line-height:1.6">
|
||||
Wir schalten deinen Account manuell frei — innerhalb von 24 Stunden.
|
||||
Wir melden uns mit den Zahlungsdetails per E-Mail.
|
||||
</div>
|
||||
</div>`,
|
||||
footer: `
|
||||
<button data-modal-close
|
||||
style="padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);
|
||||
border:1.5px solid var(--c-border);background:transparent;
|
||||
color:var(--c-text);font-size:var(--text-sm);cursor:pointer">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button id="upgrade-request-send-btn"
|
||||
style="padding:var(--space-2) var(--space-4);border-radius:var(--radius-md);
|
||||
border:none;cursor:pointer;background:${color};color:#fff;
|
||||
font-size:var(--text-sm);font-weight:600">
|
||||
Anfrage senden
|
||||
</button>`
|
||||
});
|
||||
|
||||
document.getElementById('upgrade-request-send-btn')?.addEventListener('click', async () => {
|
||||
const btn = document.getElementById('upgrade-request-send-btn');
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Wird gesendet…';
|
||||
try {
|
||||
const res = await API.auth.upgradeRequest(tier);
|
||||
UI.modal.close();
|
||||
if (res.already) {
|
||||
UI.toast.info('Deine Anfrage liegt bereits vor — wir melden uns bald.');
|
||||
} else {
|
||||
UI.toast.success('Anfrage gesendet! Wir melden uns per E-Mail.');
|
||||
}
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Anfrage senden';
|
||||
UI.toast.error(e.message || 'Fehler beim Senden.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// RENDER
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -407,6 +276,13 @@ window.Page_settings = (() => {
|
|||
<span>Feedback geben</span>
|
||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||
</div>
|
||||
${!_appState.user?.subscription_tier || _appState.user.subscription_tier === 'standard' || _appState.user.subscription_tier === 'standard_test' ? `
|
||||
<div style="margin:var(--space-3) 0;padding:var(--space-3) var(--space-4);
|
||||
background:rgba(196,132,58,0.1);border-radius:var(--radius-md);
|
||||
font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
⭐ <strong>Ban Yaro Pro</strong> kommt bald — mehr Features, mehrere Hunde.
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="padding:var(--space-3) var(--space-4);border-top:1px solid var(--c-border)">
|
||||
<button id="settings-logout-btn"
|
||||
style="width:100%;display:flex;align-items:center;justify-content:center;
|
||||
|
|
@ -440,8 +316,6 @@ window.Page_settings = (() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
${_tierCard(u)}
|
||||
|
||||
<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div class="by-card-section-header">
|
||||
App-Einstellungen
|
||||
|
|
@ -955,13 +829,6 @@ window.Page_settings = (() => {
|
|||
}
|
||||
});
|
||||
|
||||
document.getElementById('settings-upgrade-pro-btn')?.addEventListener('click', () => {
|
||||
_showUpgradeModal('pro');
|
||||
});
|
||||
document.getElementById('settings-upgrade-breeder-btn')?.addEventListener('click', () => {
|
||||
_showUpgradeModal('breeder');
|
||||
});
|
||||
|
||||
document.getElementById('settings-worlds-btn')?.addEventListener('click', () => {
|
||||
if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal();
|
||||
else if (window.Worlds) window.Worlds.openConfig?.();
|
||||
|
|
|
|||
|
|
@ -31,38 +31,19 @@
|
|||
"@type": "MobileApplication",
|
||||
"name": "Ban Yaro",
|
||||
"alternateName": "Ban Yaro — Die Hunde-Plattform",
|
||||
"description": "Ban Yaro ist die deutschsprachige All-in-One Hunde-Plattform für Hundebesitzer und Züchter. Kostenlos: Tagebuch, Impfpass, Gassi-Community, Giftköder-Alarm. Pro (29 €/Jahr): mehrere Hunde, Ernährung. Züchter (49 €/Jahr): Warteliste, Läufigkeit, Wurfverwaltung, Stammbaum, Inzucht-Koeffizient, KI-Assistent — DSGVO-konform, ohne App Store.",
|
||||
"description": "Ban Yaro ist die kostenlose, deutschsprachige All-in-One Hunde-App für Hundebesitzer und Züchter. Tagebuch, Impfpass, Wurfbörse, Stammbaum, Inzucht-Koeffizient, Tierschutz-Check, Giftköder-Alarm, Gassi-Community — DSGVO-konform, ohne App Store.",
|
||||
"url": "https://banyaro.app",
|
||||
"applicationCategory": "LifestyleApplication",
|
||||
"applicationSubCategory": "PetApplication",
|
||||
"operatingSystem": "iOS, Android, Web",
|
||||
"inLanguage": "de",
|
||||
"availableOnDevice": "Smartphone, Tablet",
|
||||
"offers": [
|
||||
{
|
||||
"offers": {
|
||||
"@type": "Offer",
|
||||
"name": "Kostenlos",
|
||||
"price": "0",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock"
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"name": "Ban Yaro Pro",
|
||||
"price": "29",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"description": "Mehrere Hunde, Ernährung, erweiterte Karten-Layer"
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"name": "Züchter",
|
||||
"price": "49",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"description": "Vollständige Züchter-Plattform: Warteliste, Läufigkeit, Wurfverwaltung, Stammbaum, IK-Rechner, KI-Assistent"
|
||||
}
|
||||
],
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Ban Yaro",
|
||||
|
|
@ -127,18 +108,12 @@
|
|||
"DSGVO Datenexport (Art. 20): vollständiger JSON-Download aller eigenen Daten",
|
||||
"Hunde-Persönlichkeitstest mit Trainingstipps",
|
||||
"Reise-Checkliste und EU-Länder-Einreiseregeln",
|
||||
"Integrierte Hilfe und FAQ ohne App Store",
|
||||
"Warteliste: Interessenten mit Präferenzen pro Zuchthündin verwalten",
|
||||
"Läufigkeit und Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, Meilensteine",
|
||||
"Wurf-Buchstabe und Wurf-Name für jeden Wurf",
|
||||
"Privater Züchter-Bereich mit Logo und Zwingername im Header",
|
||||
"Züchter-Profilfotos und Galerie auf der öffentlichen Visitenkarte",
|
||||
"Züchter-Kacheln in HUND-Welt mit Z-Badge erkennbar"
|
||||
"Integrierte Hilfe und FAQ ohne App Store"
|
||||
],
|
||||
"screenshot": "https://banyaro.app/icons/icon-512.png",
|
||||
"softwareVersion": "1.5.1",
|
||||
"datePublished": "2026-05-01",
|
||||
"dateModified": "2026-05-14",
|
||||
"dateModified": "2026-05-12",
|
||||
"areaServed": ["DE", "AT", "CH"],
|
||||
"audience": {
|
||||
"@type": "Audience",
|
||||
|
|
@ -580,8 +555,8 @@
|
|||
<!-- Züchter -->
|
||||
<a href="/zuechter" style="text-decoration:none" class="audience-card">
|
||||
<div style="background:linear-gradient(135deg,#1a1208,#2d1f0e);border:2px solid rgba(196,132,58,.4);border-radius:14px;padding:2rem;height:100%;display:flex;flex-direction:column;gap:1rem">
|
||||
<div style="width:52px;height:52px;background:#C4843A;border-radius:14px;display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:28px;height:28px;color:#ffffff" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg>
|
||||
<div style="width:52px;height:52px;background:rgba(196,132,58,.15);border-radius:14px;display:flex;align-items:center;justify-content:center">
|
||||
<svg style="width:28px;height:28px;color:#C4843A" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 style="margin:0 0 .5rem;font-size:1.15rem;color:white">Für Züchter</h3>
|
||||
|
|
@ -976,10 +951,8 @@
|
|||
<section id="preise">
|
||||
<div class="container">
|
||||
<h2>Preise</h2>
|
||||
<p class="section-intro">Ban Yaro startet kostenlos — mit allem was du täglich brauchst.</p>
|
||||
<div class="pricing-grid" style="max-width:900px;margin:0 auto;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))">
|
||||
|
||||
<!-- Kostenlos -->
|
||||
<p class="section-intro">Ban Yaro startet kostenlos — mit allem was du täglich brauchst. Mehr Features kommen, wenn die Community gewachsen ist.</p>
|
||||
<div class="pricing-grid" style="max-width:860px;margin:0 auto;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))">
|
||||
<div class="pricing-card featured">
|
||||
<h3>Kostenlos</h3>
|
||||
<div class="pricing-price">0 € <span>/ für immer</span></div>
|
||||
|
|
@ -989,58 +962,35 @@
|
|||
<li>Karte, Giftköder-Alarm, Gassiwetter</li>
|
||||
<li>Forum, Wiki, Erste Hilfe</li>
|
||||
<li>Routen, Events, Rückrufe, Knigge</li>
|
||||
<li>Sitting — 0 % Provision</li>
|
||||
<li>Sitting — 0% Provision, ihr handelt selbst aus</li>
|
||||
<li>Persönlichkeitstest, Adoption, Ausgaben</li>
|
||||
</ul>
|
||||
<a href="/" class="section-cta-btn" style="display:block;text-align:center;margin-top:1.5rem"
|
||||
onclick="sessionStorage.setItem('by_stay_in_app','1')">Kostenlos starten</a>
|
||||
</div>
|
||||
|
||||
<!-- Pro -->
|
||||
<div class="pricing-card">
|
||||
<h3>Ban Yaro Pro</h3>
|
||||
<div class="pricing-price">29 € <span>/ Jahr</span></div>
|
||||
<div class="pricing-price" style="font-size:1.4rem">Kommt bald</div>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Mehrere Hunde · erweiterte Community</p>
|
||||
<ul>
|
||||
<li>Mehrere Hunde verwalten</li>
|
||||
<li>KI-Trainer für personalisiertes Training</li>
|
||||
<li>Direktnachrichten, Freunde, Playdate</li>
|
||||
<li>Gassi-Treffen, Ernährung, Reise</li>
|
||||
<li>Notizblock mit KI-Analyse</li>
|
||||
<li>Alles aus Kostenlos inklusive</li>
|
||||
<li>Notizblock</li>
|
||||
</ul>
|
||||
<a href="/" class="section-cta-btn" style="display:block;text-align:center;margin-top:1.5rem"
|
||||
onclick="sessionStorage.setItem('by_stay_in_app','1')">Pro starten</a>
|
||||
<p style="margin-top:1rem;font-size:0.82rem;color:var(--text-muted)">Preis wird mit der Community entwickelt. Wer jetzt dabei ist, profitiert.</p>
|
||||
</div>
|
||||
|
||||
<!-- Züchter -->
|
||||
<div class="pricing-card" style="border-color:var(--primary);position:relative;overflow:hidden">
|
||||
<div style="position:absolute;top:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#C4843A,#f5c07a)"></div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.25rem">
|
||||
<h3 style="margin:0">Züchter</h3>
|
||||
<span style="background:#fef3c7;color:#92400e;font-size:0.7rem;font-weight:700;
|
||||
padding:2px 8px;border-radius:999px">
|
||||
🎉 Gründer: 39 €
|
||||
</span>
|
||||
</div>
|
||||
<div class="pricing-price">49 € <span>/ Jahr</span></div>
|
||||
<div class="pricing-card">
|
||||
<h3>Züchter</h3>
|
||||
<div class="pricing-price" style="font-size:1.4rem">Kommt bald</div>
|
||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Professionelle Zucht · alles aus Pro</p>
|
||||
<ul>
|
||||
<li>Stammbaum bis 4 Generationen</li>
|
||||
<li>Inzucht-Koeffizient nach Wright</li>
|
||||
<li>Wurfverwaltung + Warteliste</li>
|
||||
<li>Kaufvertrag automatisch generiert</li>
|
||||
<li>Läufigkeit & Trächtigkeits-Meilensteine</li>
|
||||
<li>KI-Assistent für Züchter</li>
|
||||
<li>Tierschutz-Check automatisch</li>
|
||||
<li>Verifiziertes Züchterprofil</li>
|
||||
<li>Wurfverwaltung + Kaufvertrag</li>
|
||||
<li>Tierschutz-Check (automatisch)</li>
|
||||
<li>Wurfbörse + verifiziertes Profil</li>
|
||||
</ul>
|
||||
<p style="margin-top:0.75rem;font-size:0.8rem;color:var(--primary-dark);font-weight:600">
|
||||
Jetzt: 39 €/Jahr für die ersten 20 Züchter
|
||||
</p>
|
||||
<a href="/zuechter" class="section-cta-btn" style="display:block;text-align:center;margin-top:1rem">Als Züchter registrieren</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Ban Yaro — Die deutschsprachige Hunde-Plattform für Hundebesitzer und Züchter
|
||||
# Ban Yaro — Die deutschsprachige Hunde-Plattform
|
||||
# https://banyaro.app
|
||||
# Letzte Aktualisierung: 2026-05-14
|
||||
# Letzte Aktualisierung: 2026-05-12
|
||||
|
||||
## Was ist Ban Yaro?
|
||||
|
||||
|
|
@ -10,11 +10,7 @@ Kein App Store, kein US-Konzern, DSGVO-konform, selbst gehostet in Deutschland.
|
|||
|
||||
Das Kern-Versprechen: "Alles rund um deinen Hund — von Welpe bis Opa."
|
||||
|
||||
Ban Yaro richtet sich an zwei Zielgruppen, die nahtlos verzahnt sind:
|
||||
- **Hundebesitzer**: vollständige Alltags-App (Tagebuch, Gesundheit, Training, Gassi-Community)
|
||||
- **Züchter**: professionelles Zucht-Management direkt in derselben App (Warteliste, Läufigkeit, Wurf, Stammbaum)
|
||||
|
||||
Ban Yaro ist im Freemium-Modell nutzbar. Die App ist auf allen Smartphones installierbar
|
||||
Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartphones installierbar
|
||||
(iOS und Android) direkt über den Browser — ohne App Store.
|
||||
|
||||
## Der Name „Ban Yaro"
|
||||
|
|
@ -37,12 +33,12 @@ gegründet, mit eigenem Schutzrecht auf den Namen.
|
|||
- Keine Werbung, keine Datenweitergabe an Dritte, kein Tracking (Umami, cookieless)
|
||||
- Kontakt: hallo@banyaro.app
|
||||
- Keine App-Store-Abhängigkeit: Als PWA direkt installierbar, keine Gatekeeper
|
||||
- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v918
|
||||
- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v885
|
||||
|
||||
## Zielgruppe
|
||||
|
||||
- Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz)
|
||||
- Verantwortungsvolle Hundezüchter (VDH und andere Verbände) — dedizierte Landing Page: https://banyaro.app/zuechter
|
||||
- Verantwortungsvolle Hundezüchter (VDH und andere Verbände)
|
||||
- Welpen-Interessenten und Käufer
|
||||
- Hundeschulen und Hundetrainer
|
||||
- Tierärzte und Praxen
|
||||
|
|
@ -186,10 +182,21 @@ Die Startseite für eingeloggte Nutzer zeigt:
|
|||
- KI lokal: LM Studio (Gemma-4-31B)
|
||||
- KI Cloud: Claude API (claude-sonnet-4-6, Anthropic)
|
||||
|
||||
## Monetarisierung
|
||||
|
||||
**Kostenlos:**
|
||||
- Alle Basis-Features inkl. Züchter-Antrag, Wurfverwaltung, Stammbaum, Tierschutz-Check
|
||||
|
||||
**Züchter-Provision** (geplant): Wurfbörse bleibt für Käufer kostenlos
|
||||
|
||||
**Ban Yaro Plus** (ca. 4,99 €/Monat, in Entwicklung):
|
||||
- KI-Trainingsplan, erweiterte Statistiken
|
||||
|
||||
**Hundesitting**: 8% Provision
|
||||
|
||||
## Öffentliche Seiten (ohne Login)
|
||||
|
||||
- https://banyaro.app — Landing Page (Hundebesitzer + Züchter)
|
||||
- https://banyaro.app/zuechter — Dedizierte Landing Page für Züchter
|
||||
- https://banyaro.app — Landing Page
|
||||
- https://banyaro.app/info — Landing Page (Alias)
|
||||
- https://banyaro.app/wiki/rassen — Alle Hunderassen
|
||||
- https://banyaro.app/wiki/rasse/{slug} — Rassen-Detail
|
||||
|
|
@ -243,39 +250,6 @@ Die Startseite für eingeloggte Nutzer zeigt:
|
|||
- **Auth-geschützte Medien**: Tagebuch-, Gesundheits- und Gassi-Fotos sind nur für eingeloggte Nutzer abrufbar — kein öffentlicher Zugriff über URL möglich.
|
||||
- **Datenschutzerklärung v2**: Vollständige Transparenz über KI-Datenübertragungen (Gesundheitsdaten im Cloud-Prompt, Fotos bei Rassenerkennung), OpenWeatherMap und Nominatim ergänzt, Datenexport konkret beschrieben.
|
||||
|
||||
## Features ab v1.5.1 — Züchter-Plattform Vollausbau (Mai 2026)
|
||||
|
||||
- **Warteliste**: Interessenten mit Präferenzen (Geschlecht, Farbe, Verwendungszweck) pro Zuchthündin verwalten — mit Status (Interessent / Reserviert / Abgesagt) und Kontaktdaten.
|
||||
- **Läufigkeit & Trächtigkeit**: Vollständiger Zykluskalender mit Progesterontests (Datum, ng/mL, Labormethode), Deckdaten (Rüde, Methode, Datum) und automatischer Meilensteinberechnung (Geburt, Absetzen, 8-Wochen-Abgabe).
|
||||
- **Wurf-Buchstabe und -Name**: Jeder Wurf hat einen Rang-Buchstaben (A-Wurf, B-Wurf…) und optional einen freien Namen (z.B. "Vatertags-Wurf").
|
||||
- **Privater Züchter-Bereich**: Wurfverwaltung und Zuchtkartei zeigen Züchter-Logo und Zwingername im Header — professionelle, vertrauliche Arbeitsatmosphäre statt generischer App-Ansicht.
|
||||
- **Züchter-Profilfotos**: Galerie direkt im Züchter-Profil — Fotos hochladen und Reihenfolge verwalten, öffentlich sichtbar auf der Profil-Visitenkarte.
|
||||
- **Züchter-Profil als Visitenkarte**: Hero-Bereich mit Hintergrund, Hunde-Cards mit HD/ED-Ergebnis-Badges, Gesundheitsstatistik, Fotogalerie — öffentlich abrufbar unter banyaro.app/breeder/{zwingername}.
|
||||
- **Dedizierte Züchter-Landing-Page**: https://banyaro.app/zuechter mit Erklärung aller Züchter-Features und Pricing.
|
||||
- **Züchter-Kacheln in HUND-Welt**: Läufigkeit, Wurfverwaltung und Zuchtkartei sind als eigene Kacheln in der HUND-Navigation eingebunden — erkennbar am Z-Badge für Züchter-Features.
|
||||
|
||||
## Monetarisierung
|
||||
|
||||
**Kostenlos (dauerhaft):**
|
||||
- Alle Basis-Features für Hundebesitzer: Tagebuch, Gesundheit, Gassi, Community, Forum, Wissen
|
||||
- Züchter-Antrag, Wurfbörse, Stammbaum-Ansicht, Tierschutz-Check, Symptom-Checker
|
||||
- 1 Hund
|
||||
|
||||
**Ban Yaro Pro — 29 €/Jahr:**
|
||||
- Mehrere Hunde
|
||||
- Ernährungsbereich (KI-Berater, BARF-Guide)
|
||||
- Erweiterte Karten-Layer
|
||||
- Alle künftigen Pro-Features
|
||||
|
||||
**Züchter-Abo — 49 €/Jahr** (Gründer-Preis: **39 €/Jahr** für die ersten 20 Züchter):
|
||||
- Gesamte Züchter-Plattform: Wurfverwaltung, Zuchtkartei, Stammbaum, IK-Rechner
|
||||
- Warteliste, Läufigkeit & Trächtigkeit, Kaufvertrag-Generator
|
||||
- KI-Züchter-Assistenz (Wurfankündigungen, Paarungsanalyse, Jahresbericht)
|
||||
- Datenexport (HTML + ODS)
|
||||
- Verifiziertes Züchter-Profil mit öffentlicher Seite
|
||||
|
||||
**Hundesitting**: 8% Provision (im Vergleich: Rover/Pawshake 20%)
|
||||
|
||||
## Domains
|
||||
|
||||
- https://banyaro.app (primäre Domain)
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/</loc>
|
||||
<lastmod>2026-05-14</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/zuechter</loc>
|
||||
<lastmod>2026-05-14</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/wiki/rassen</loc>
|
||||
<lastmod>2026-05-14</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/wurfboerse</loc>
|
||||
<lastmod>2026-05-14</lastmod>
|
||||
<changefreq>daily</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/knigge</loc>
|
||||
<lastmod>2026-05-01</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
<url>
|
||||
<loc>https://banyaro.app/presse</loc>
|
||||
<lastmod>2026-05-14</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
|
||||
</urlset>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
Offline-Cache + Push Notifications + Tile-Cache
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v921';
|
||||
const CACHE_VERSION = 'by-v918';
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,37 +21,14 @@
|
|||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"name": "Ban Yaro — Züchter-Tool",
|
||||
"description": "Professionelles Zucht-Management für seriöse Hundezüchter direkt in der Ban Yaro App: Warteliste, Läufigkeit und Trächtigkeit, Wurfverwaltung, Stammbaum, Inzuchtkoeffizient, Gesundheitstests, Gentests, KI-Assistent. Ab 49 €/Jahr, Gründer-Preis 39 €/Jahr.",
|
||||
"description": "Digitales Zucht-Management für seriöse Hundezüchter: Stammbaum, Inzuchtkoeffizient nach Wright, Gesundheitstests, Gentests, Wurfverwaltung, Kaufvertrag-Generator, KI-Assistent.",
|
||||
"url": "https://banyaro.app/zuechter",
|
||||
"applicationCategory": "BusinessApplication",
|
||||
"operatingSystem": "iOS, Android, Web",
|
||||
"inLanguage": "de",
|
||||
"offers": [
|
||||
{
|
||||
"@type": "Offer",
|
||||
"name": "Züchter-Abo",
|
||||
"price": "49",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/InStock",
|
||||
"description": "Vollständige Züchter-Plattform"
|
||||
},
|
||||
{
|
||||
"@type": "Offer",
|
||||
"name": "Gründer-Preis (erste 20 Züchter)",
|
||||
"price": "39",
|
||||
"priceCurrency": "EUR",
|
||||
"availability": "https://schema.org/LimitedAvailability"
|
||||
}
|
||||
],
|
||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "EUR" },
|
||||
"audience": { "@type": "Audience", "audienceType": "Hundezüchter, VDH-Züchter, Rassehundzüchter" },
|
||||
"softwareVersion": "1.5.1",
|
||||
"dateModified": "2026-05-14",
|
||||
"featureList": [
|
||||
"Warteliste: Interessenten mit Präferenzen pro Zuchthündin verwalten",
|
||||
"Läufigkeit und Trächtigkeit: Zykluskalender, Progesterontests, Deckdaten, automatische Meilensteine",
|
||||
"Wurf-Buchstabe und Wurf-Name für jeden Wurf",
|
||||
"Privater Bereich mit Züchter-Logo und Zwingername im Header",
|
||||
"Züchter-Profilfotos und öffentliche Visitenkarte",
|
||||
"Stammbaum bis 4 Generationen grafisch",
|
||||
"Inzuchtkoeffizient nach Wright mit Ampel-Bewertung",
|
||||
"Probeverpaarung mit IK-Simulation",
|
||||
|
|
@ -571,15 +548,8 @@
|
|||
<!-- CTA -->
|
||||
<section class="cta-section">
|
||||
<div class="container">
|
||||
<div style="display:inline-block;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);
|
||||
border-radius:999px;padding:.35rem 1.25rem;margin-bottom:1.25rem;font-size:.85rem;font-weight:600">
|
||||
🎉 Gründer-Preis: <strong>39 €/Jahr</strong> für die ersten 20 Züchter
|
||||
</div>
|
||||
<h2>Jetzt starten.</h2>
|
||||
<p style="margin-bottom:.75rem">Züchter-Antrag stellen, Zuchthunde anlegen, ersten Wurf veröffentlichen.</p>
|
||||
<p style="font-size:.9rem;opacity:.7;margin-bottom:1.75rem">
|
||||
39 €/Jahr für die ersten 20 Gründer-Züchter · danach 49 €/Jahr · kein App Store · keine Kreditkarte zum Start
|
||||
</p>
|
||||
<h2>Jetzt kostenlos starten.</h2>
|
||||
<p>Züchter-Antrag stellen, Zuchthunde anlegen, ersten Wurf veröffentlichen — alles kostenlos, kein App Store, keine Kreditkarte.</p>
|
||||
<a href="/#register?rolle=breeder" class="cta-btn" onclick="sessionStorage.setItem('by_stay_in_app','1')">Als Züchter registrieren</a>
|
||||
<p style="margin-top:1.5rem;font-size:0.85rem;opacity:0.55">Fragen? <a href="mailto:hallo@banyaro.app" style="color:rgba(255,255,255,.7)">hallo@banyaro.app</a></p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ services:
|
|||
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
||||
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
||||
- VAPID_CONTACT=mailto:admin@banyaro.app
|
||||
- UMAMI_URL=https://umami.motocamp.de
|
||||
healthcheck:
|
||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
|
||||
interval: 30s
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue