Compare commits
8 commits
0a7bb931b3
...
52160e4dc0
| Author | SHA1 | Date | |
|---|---|---|---|
| 52160e4dc0 | |||
| f6b37717b4 | |||
| d61fd155c5 | |||
| eaa2e02e88 | |||
| c5d4e730d9 | |||
| 509d4eda2b | |||
| 3da4a1b6d7 | |||
| 5f5f3e9271 |
16 changed files with 695 additions and 76 deletions
|
|
@ -2341,6 +2341,24 @@ def _migrate(conn_factory):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
# route_dogs: bestehende Routen allen Hunden des Users zuweisen
|
||||||
try:
|
try:
|
||||||
existing = conn.execute("SELECT COUNT(*) FROM route_dogs").fetchone()[0]
|
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.")
|
raise _HE(404, "Nicht gefunden.")
|
||||||
return _media_response(filepath)
|
return _media_response(filepath)
|
||||||
|
|
||||||
APP_VER = "918" # muss mit APP_VER in app.js übereinstimmen
|
APP_VER = "921" # muss mit APP_VER in app.js übereinstimmen
|
||||||
|
|
||||||
@app.get("/.well-known/assetlinks.json")
|
@app.get("/.well-known/assetlinks.json")
|
||||||
async def assetlinks():
|
async def assetlinks():
|
||||||
|
|
@ -465,10 +465,11 @@ async def sitemap():
|
||||||
today = date.today().isoformat()
|
today = date.today().isoformat()
|
||||||
urls = [
|
urls = [
|
||||||
("https://banyaro.app/", "weekly", "1.0"),
|
("https://banyaro.app/", "weekly", "1.0"),
|
||||||
("https://banyaro.app/info", "monthly", "0.9"),
|
("https://banyaro.app/zuechter", "weekly", "0.9"),
|
||||||
("https://banyaro.app/presse", "monthly", "0.8"),
|
("https://banyaro.app/info", "monthly", "0.8"),
|
||||||
|
("https://banyaro.app/presse", "monthly", "0.7"),
|
||||||
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
|
("https://banyaro.app/wiki/rassen", "weekly", "0.8"),
|
||||||
("https://banyaro.app/knigge", "monthly", "0.8"),
|
("https://banyaro.app/knigge", "monthly", "0.7"),
|
||||||
("https://banyaro.app/wurfboerse", "daily", "0.8"),
|
("https://banyaro.app/wurfboerse", "daily", "0.8"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,13 +124,17 @@ async def action_items(user=Depends(require_mod)):
|
||||||
users_today = conn.execute(
|
users_today = conn.execute(
|
||||||
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
|
"SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')"
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
|
upgrades_pending = conn.execute(
|
||||||
|
"SELECT COUNT(*) FROM upgrade_requests WHERE fulfilled_at IS NULL"
|
||||||
|
).fetchone()[0]
|
||||||
return {
|
return {
|
||||||
"jobs_pending": jobs,
|
"jobs_pending": jobs,
|
||||||
"breeder_pending": breeders,
|
"breeder_pending": breeders,
|
||||||
"reports_open": reports,
|
"reports_open": reports,
|
||||||
"fotos_pending": fotos,
|
"fotos_pending": fotos,
|
||||||
"poi_edits_pending": poi_edits,
|
"poi_edits_pending": poi_edits,
|
||||||
"users_today": users_today,
|
"users_today": users_today,
|
||||||
|
"upgrades_pending": upgrades_pending,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1091,3 +1095,66 @@ async def generate_media_previews(user=Depends(require_admin)):
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
return {"generated": generated, "skipped": skipped, "errors": errors}
|
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,
|
profil_sichtbarkeit, avatar_url, created_at,
|
||||||
is_founder, is_partner, founder_number, is_founder_pending,
|
is_founder, is_partner, founder_number, is_founder_pending,
|
||||||
notes_ki_enabled, gassi_stunde_push,
|
notes_ki_enabled, gassi_stunde_push,
|
||||||
preferred_theme
|
preferred_theme, subscription_tier
|
||||||
FROM users WHERE id=?""",
|
FROM users WHERE id=?""",
|
||||||
(user["id"],)
|
(user["id"],)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
@ -335,6 +335,46 @@ async def forgot_password(data: ForgotPasswordRequest, request: Request):
|
||||||
return {"ok": True}
|
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")
|
@router.post("/reset-password")
|
||||||
async def reset_password(data: ResetPasswordRequest, request: Request):
|
async def reset_password(data: ResetPasswordRequest, request: Request):
|
||||||
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
rl_check(request, max_requests=5, window_seconds=3600, key="reset_pw")
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,27 @@ async def admin_pending_breeders(admin=Depends(require_admin)):
|
||||||
return [dict(r) for r in rows]
|
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
|
# GET /api/admin/breeder/{user_id}/documents — Dokumente eines Antrags
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -599,10 +599,10 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=918"></script>
|
<script src="/js/api.js?v=919"></script>
|
||||||
<script src="/js/ui.js?v=918"></script>
|
<script src="/js/ui.js?v=919"></script>
|
||||||
<script src="/js/app.js?v=918"></script>
|
<script src="/js/app.js?v=919"></script>
|
||||||
<script src="/js/worlds.js?v=918"></script>
|
<script src="/js/worlds.js?v=919"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,9 @@ const API = (() => {
|
||||||
return get('/auth/me');
|
return get('/auth/me');
|
||||||
},
|
},
|
||||||
referral: () => get('/auth/referral'),
|
referral: () => get('/auth/referral'),
|
||||||
|
upgradeRequest(tier, message) {
|
||||||
|
return post('/auth/upgrade-request', { tier, message });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -688,6 +691,7 @@ const API = (() => {
|
||||||
updateProfile(data) { return put('/breeder/profile', data); },
|
updateProfile(data) { return put('/breeder/profile', data); },
|
||||||
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
|
adminCreateProfile() { return post('/admin/breeder/create-profile', {}); },
|
||||||
pendingList() { return get('/admin/breeders/pending'); },
|
pendingList() { return get('/admin/breeders/pending'); },
|
||||||
|
allList() { return get('/admin/breeders'); },
|
||||||
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
|
documents(userId) { return get(`/admin/breeder/${userId}/documents`); },
|
||||||
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
|
documentUrl(userId, docId) { return `/api/admin/breeder/${userId}/document/${docId}`; },
|
||||||
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },
|
approve(userId) { return post(`/admin/breeder/${userId}/approve`, {}); },
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '918'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '921'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ window.Page_admin = (() => {
|
||||||
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
{ id: 'hilfe', label: 'Hilfe/FAQ', icon: 'question' },
|
||||||
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
{ id: 'uebungen_admin', label: 'Übungen', icon: 'barbell' },
|
||||||
{ id: 'referrals', label: 'Referrals', icon: 'share-network' },
|
{ id: 'referrals', label: 'Referrals', icon: 'share-network' },
|
||||||
|
{ id: 'upgrades', label: 'Upgrades', icon: 'crown-simple' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
@ -90,6 +91,7 @@ window.Page_admin = (() => {
|
||||||
try { d = await API.get('/admin/action-items'); } catch { return; }
|
try { d = await API.get('/admin/action-items'); } catch { return; }
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
{ key: 'upgrades_pending', label: 'Upgrade-Anfragen', tab: 'upgrades', icon: 'crown-simple' },
|
||||||
{ key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' },
|
{ key: 'jobs_pending', label: 'Bewerbungen', tab: 'bewerbungen', icon: 'user-plus' },
|
||||||
{ key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' },
|
{ key: 'breeder_pending', label: 'Züchter-Anträge', tab: 'zuchter', icon: 'certificate' },
|
||||||
{ key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' },
|
{ key: 'reports_open', label: 'Meldungen', tab: 'moderation', icon: 'warning' },
|
||||||
|
|
@ -163,6 +165,7 @@ window.Page_admin = (() => {
|
||||||
case 'hilfe': await _renderHilfe(el); break;
|
case 'hilfe': await _renderHilfe(el); break;
|
||||||
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
case 'uebungen_admin': await _renderUebungenAdmin(el); break;
|
||||||
case 'referrals': await _renderReferrals(el); break;
|
case 'referrals': await _renderReferrals(el); break;
|
||||||
|
case 'upgrades': await _renderUpgrades(el); break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
el.innerHTML = _emptyState('warning', 'Fehler', e.message || 'Unbekannter Fehler.');
|
||||||
|
|
@ -1890,12 +1893,17 @@ window.Page_admin = (() => {
|
||||||
${UI.icon('arrows-clockwise')} Aktualisieren
|
${UI.icon('arrows-clockwise')} Aktualisieren
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="adm-zuchter-list">Lade…</div>
|
<div id="adm-zuchter-antraege">Lade…</div>
|
||||||
|
<div id="adm-zuchter-liste" style="margin-top:var(--space-4)">Lade…</div>
|
||||||
`;
|
`;
|
||||||
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () =>
|
el.querySelector('#adm-zuchter-refresh').addEventListener('click', () => {
|
||||||
_loadZuechterAntraege(el.querySelector('#adm-zuchter-list'))
|
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege'));
|
||||||
);
|
_loadZuechterListe(el.querySelector('#adm-zuchter-liste'));
|
||||||
await _loadZuechterAntraege(el.querySelector('#adm-zuchter-list'));
|
});
|
||||||
|
await Promise.all([
|
||||||
|
_loadZuechterAntraege(el.querySelector('#adm-zuchter-antraege')),
|
||||||
|
_loadZuechterListe(el.querySelector('#adm-zuchter-liste')),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadZuechterAntraege(el) {
|
async function _loadZuechterAntraege(el) {
|
||||||
|
|
@ -1909,12 +1917,20 @@ window.Page_admin = (() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!antraege.length) {
|
if (!antraege.length) {
|
||||||
el.innerHTML = _emptyState('certificate', 'Keine offenen Anträge', 'Aktuell liegen keine Züchter-Anträge zur Prüfung vor.');
|
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>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
<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)">
|
||||||
${antraege.map(a => `
|
${antraege.map(a => `
|
||||||
<div class="card" style="padding:var(--space-4)">
|
<div class="card" style="padding:var(--space-4)">
|
||||||
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
|
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
|
||||||
|
|
@ -2069,6 +2085,74 @@ 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) {
|
async function _renderJobs(el) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
|
|
@ -3419,6 +3503,104 @@ window.Page_admin = (() => {
|
||||||
</div>`;
|
</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 };
|
return { init, refresh, onDogChange };
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,137 @@ 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
|
// RENDER
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -276,13 +407,6 @@ window.Page_settings = (() => {
|
||||||
<span>Feedback geben</span>
|
<span>Feedback geben</span>
|
||||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
||||||
</div>
|
</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)">
|
<div style="padding:var(--space-3) var(--space-4);border-top:1px solid var(--c-border)">
|
||||||
<button id="settings-logout-btn"
|
<button id="settings-logout-btn"
|
||||||
style="width:100%;display:flex;align-items:center;justify-content:center;
|
style="width:100%;display:flex;align-items:center;justify-content:center;
|
||||||
|
|
@ -316,6 +440,8 @@ window.Page_settings = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${_tierCard(u)}
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:var(--space-4)">
|
<div class="card" style="margin-bottom:var(--space-4)">
|
||||||
<div class="by-card-section-header">
|
<div class="by-card-section-header">
|
||||||
App-Einstellungen
|
App-Einstellungen
|
||||||
|
|
@ -829,6 +955,13 @@ 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', () => {
|
document.getElementById('settings-worlds-btn')?.addEventListener('click', () => {
|
||||||
if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal();
|
if (window.Worlds?._openConfigModal) window.Worlds._openConfigModal();
|
||||||
else if (window.Worlds) window.Worlds.openConfig?.();
|
else if (window.Worlds) window.Worlds.openConfig?.();
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,38 @@
|
||||||
"@type": "MobileApplication",
|
"@type": "MobileApplication",
|
||||||
"name": "Ban Yaro",
|
"name": "Ban Yaro",
|
||||||
"alternateName": "Ban Yaro — Die Hunde-Plattform",
|
"alternateName": "Ban Yaro — Die Hunde-Plattform",
|
||||||
"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.",
|
"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.",
|
||||||
"url": "https://banyaro.app",
|
"url": "https://banyaro.app",
|
||||||
"applicationCategory": "LifestyleApplication",
|
"applicationCategory": "LifestyleApplication",
|
||||||
"applicationSubCategory": "PetApplication",
|
"applicationSubCategory": "PetApplication",
|
||||||
"operatingSystem": "iOS, Android, Web",
|
"operatingSystem": "iOS, Android, Web",
|
||||||
"inLanguage": "de",
|
"inLanguage": "de",
|
||||||
"availableOnDevice": "Smartphone, Tablet",
|
"availableOnDevice": "Smartphone, Tablet",
|
||||||
"offers": {
|
"offers": [
|
||||||
"@type": "Offer",
|
{
|
||||||
"price": "0",
|
"@type": "Offer",
|
||||||
"priceCurrency": "EUR",
|
"name": "Kostenlos",
|
||||||
"availability": "https://schema.org/InStock"
|
"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": {
|
"publisher": {
|
||||||
"@type": "Organization",
|
"@type": "Organization",
|
||||||
"name": "Ban Yaro",
|
"name": "Ban Yaro",
|
||||||
|
|
@ -108,12 +127,18 @@
|
||||||
"DSGVO Datenexport (Art. 20): vollständiger JSON-Download aller eigenen Daten",
|
"DSGVO Datenexport (Art. 20): vollständiger JSON-Download aller eigenen Daten",
|
||||||
"Hunde-Persönlichkeitstest mit Trainingstipps",
|
"Hunde-Persönlichkeitstest mit Trainingstipps",
|
||||||
"Reise-Checkliste und EU-Länder-Einreiseregeln",
|
"Reise-Checkliste und EU-Länder-Einreiseregeln",
|
||||||
"Integrierte Hilfe und FAQ ohne App Store"
|
"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"
|
||||||
],
|
],
|
||||||
"screenshot": "https://banyaro.app/icons/icon-512.png",
|
"screenshot": "https://banyaro.app/icons/icon-512.png",
|
||||||
"softwareVersion": "1.5.1",
|
"softwareVersion": "1.5.1",
|
||||||
"datePublished": "2026-05-01",
|
"datePublished": "2026-05-01",
|
||||||
"dateModified": "2026-05-12",
|
"dateModified": "2026-05-14",
|
||||||
"areaServed": ["DE", "AT", "CH"],
|
"areaServed": ["DE", "AT", "CH"],
|
||||||
"audience": {
|
"audience": {
|
||||||
"@type": "Audience",
|
"@type": "Audience",
|
||||||
|
|
@ -555,8 +580,8 @@
|
||||||
<!-- Züchter -->
|
<!-- Züchter -->
|
||||||
<a href="/zuechter" style="text-decoration:none" class="audience-card">
|
<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="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:rgba(196,132,58,.15);border-radius:14px;display:flex;align-items:center;justify-content:center">
|
<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:#C4843A" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg>
|
<svg style="width:28px;height:28px;color:#ffffff" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 style="margin:0 0 .5rem;font-size:1.15rem;color:white">Für Züchter</h3>
|
<h3 style="margin:0 0 .5rem;font-size:1.15rem;color:white">Für Züchter</h3>
|
||||||
|
|
@ -951,8 +976,10 @@
|
||||||
<section id="preise">
|
<section id="preise">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Preise</h2>
|
<h2>Preise</h2>
|
||||||
<p class="section-intro">Ban Yaro startet kostenlos — mit allem was du täglich brauchst. Mehr Features kommen, wenn die Community gewachsen ist.</p>
|
<p class="section-intro">Ban Yaro startet kostenlos — mit allem was du täglich brauchst.</p>
|
||||||
<div class="pricing-grid" style="max-width:860px;margin:0 auto;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))">
|
<div class="pricing-grid" style="max-width:900px;margin:0 auto;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))">
|
||||||
|
|
||||||
|
<!-- Kostenlos -->
|
||||||
<div class="pricing-card featured">
|
<div class="pricing-card featured">
|
||||||
<h3>Kostenlos</h3>
|
<h3>Kostenlos</h3>
|
||||||
<div class="pricing-price">0 € <span>/ für immer</span></div>
|
<div class="pricing-price">0 € <span>/ für immer</span></div>
|
||||||
|
|
@ -962,35 +989,58 @@
|
||||||
<li>Karte, Giftköder-Alarm, Gassiwetter</li>
|
<li>Karte, Giftköder-Alarm, Gassiwetter</li>
|
||||||
<li>Forum, Wiki, Erste Hilfe</li>
|
<li>Forum, Wiki, Erste Hilfe</li>
|
||||||
<li>Routen, Events, Rückrufe, Knigge</li>
|
<li>Routen, Events, Rückrufe, Knigge</li>
|
||||||
<li>Sitting — 0% Provision, ihr handelt selbst aus</li>
|
<li>Sitting — 0 % Provision</li>
|
||||||
<li>Persönlichkeitstest, Adoption, Ausgaben</li>
|
<li>Persönlichkeitstest, Adoption, Ausgaben</li>
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
<!-- Pro -->
|
||||||
<div class="pricing-card">
|
<div class="pricing-card">
|
||||||
<h3>Ban Yaro Pro</h3>
|
<h3>Ban Yaro Pro</h3>
|
||||||
<div class="pricing-price" style="font-size:1.4rem">Kommt bald</div>
|
<div class="pricing-price">29 € <span>/ Jahr</span></div>
|
||||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Mehrere Hunde · erweiterte Community</p>
|
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Mehrere Hunde · erweiterte Community</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Mehrere Hunde verwalten</li>
|
<li>Mehrere Hunde verwalten</li>
|
||||||
<li>KI-Trainer für personalisiertes Training</li>
|
<li>KI-Trainer für personalisiertes Training</li>
|
||||||
<li>Direktnachrichten, Freunde, Playdate</li>
|
<li>Direktnachrichten, Freunde, Playdate</li>
|
||||||
<li>Gassi-Treffen, Ernährung, Reise</li>
|
<li>Gassi-Treffen, Ernährung, Reise</li>
|
||||||
<li>Notizblock</li>
|
<li>Notizblock mit KI-Analyse</li>
|
||||||
|
<li>Alles aus Kostenlos inklusive</li>
|
||||||
</ul>
|
</ul>
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="pricing-card">
|
|
||||||
<h3>Züchter</h3>
|
<!-- Züchter -->
|
||||||
<div class="pricing-price" style="font-size:1.4rem">Kommt bald</div>
|
<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>
|
||||||
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Professionelle Zucht · alles aus Pro</p>
|
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1rem">Professionelle Zucht · alles aus Pro</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Stammbaum bis 4 Generationen</li>
|
<li>Stammbaum bis 4 Generationen</li>
|
||||||
<li>Inzucht-Koeffizient nach Wright</li>
|
<li>Inzucht-Koeffizient nach Wright</li>
|
||||||
<li>Wurfverwaltung + Kaufvertrag</li>
|
<li>Wurfverwaltung + Warteliste</li>
|
||||||
<li>Tierschutz-Check (automatisch)</li>
|
<li>Kaufvertrag automatisch generiert</li>
|
||||||
<li>Wurfbörse + verifiziertes Profil</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>
|
||||||
</ul>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Ban Yaro — Die deutschsprachige Hunde-Plattform
|
# Ban Yaro — Die deutschsprachige Hunde-Plattform für Hundebesitzer und Züchter
|
||||||
# https://banyaro.app
|
# https://banyaro.app
|
||||||
# Letzte Aktualisierung: 2026-05-12
|
# Letzte Aktualisierung: 2026-05-14
|
||||||
|
|
||||||
## Was ist Ban Yaro?
|
## Was ist Ban Yaro?
|
||||||
|
|
||||||
|
|
@ -10,7 +10,11 @@ Kein App Store, kein US-Konzern, DSGVO-konform, selbst gehostet in Deutschland.
|
||||||
|
|
||||||
Das Kern-Versprechen: "Alles rund um deinen Hund — von Welpe bis Opa."
|
Das Kern-Versprechen: "Alles rund um deinen Hund — von Welpe bis Opa."
|
||||||
|
|
||||||
Ban Yaro ist kostenlos nutzbar (Freemium-Modell). Die App ist auf allen Smartphones installierbar
|
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
|
||||||
(iOS und Android) direkt über den Browser — ohne App Store.
|
(iOS und Android) direkt über den Browser — ohne App Store.
|
||||||
|
|
||||||
## Der Name „Ban Yaro"
|
## Der Name „Ban Yaro"
|
||||||
|
|
@ -33,12 +37,12 @@ gegründet, mit eigenem Schutzrecht auf den Namen.
|
||||||
- Keine Werbung, keine Datenweitergabe an Dritte, kein Tracking (Umami, cookieless)
|
- Keine Werbung, keine Datenweitergabe an Dritte, kein Tracking (Umami, cookieless)
|
||||||
- Kontakt: hallo@banyaro.app
|
- Kontakt: hallo@banyaro.app
|
||||||
- Keine App-Store-Abhängigkeit: Als PWA direkt installierbar, keine Gatekeeper
|
- Keine App-Store-Abhängigkeit: Als PWA direkt installierbar, keine Gatekeeper
|
||||||
- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v885
|
- Aktuelle Version: v1.5.1 (Mai 2026), SW by-v918
|
||||||
|
|
||||||
## Zielgruppe
|
## Zielgruppe
|
||||||
|
|
||||||
- Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz)
|
- Deutschsprachige Hundebesitzer (Deutschland, Österreich, Schweiz)
|
||||||
- Verantwortungsvolle Hundezüchter (VDH und andere Verbände)
|
- Verantwortungsvolle Hundezüchter (VDH und andere Verbände) — dedizierte Landing Page: https://banyaro.app/zuechter
|
||||||
- Welpen-Interessenten und Käufer
|
- Welpen-Interessenten und Käufer
|
||||||
- Hundeschulen und Hundetrainer
|
- Hundeschulen und Hundetrainer
|
||||||
- Tierärzte und Praxen
|
- Tierärzte und Praxen
|
||||||
|
|
@ -182,21 +186,10 @@ Die Startseite für eingeloggte Nutzer zeigt:
|
||||||
- KI lokal: LM Studio (Gemma-4-31B)
|
- KI lokal: LM Studio (Gemma-4-31B)
|
||||||
- KI Cloud: Claude API (claude-sonnet-4-6, Anthropic)
|
- 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)
|
## Öffentliche Seiten (ohne Login)
|
||||||
|
|
||||||
- https://banyaro.app — Landing Page
|
- https://banyaro.app — Landing Page (Hundebesitzer + Züchter)
|
||||||
|
- https://banyaro.app/zuechter — Dedizierte Landing Page für Züchter
|
||||||
- https://banyaro.app/info — Landing Page (Alias)
|
- https://banyaro.app/info — Landing Page (Alias)
|
||||||
- https://banyaro.app/wiki/rassen — Alle Hunderassen
|
- https://banyaro.app/wiki/rassen — Alle Hunderassen
|
||||||
- https://banyaro.app/wiki/rasse/{slug} — Rassen-Detail
|
- https://banyaro.app/wiki/rasse/{slug} — Rassen-Detail
|
||||||
|
|
@ -250,6 +243,39 @@ 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.
|
- **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.
|
- **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
|
## Domains
|
||||||
|
|
||||||
- https://banyaro.app (primäre Domain)
|
- https://banyaro.app (primäre Domain)
|
||||||
|
|
|
||||||
46
backend/static/sitemap.xml
Normal file
46
backend/static/sitemap.xml
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?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
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v918';
|
const CACHE_VERSION = 'by-v921';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,37 @@
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
"@type": "SoftwareApplication",
|
"@type": "SoftwareApplication",
|
||||||
"name": "Ban Yaro — Züchter-Tool",
|
"name": "Ban Yaro — Züchter-Tool",
|
||||||
"description": "Digitales Zucht-Management für seriöse Hundezüchter: Stammbaum, Inzuchtkoeffizient nach Wright, Gesundheitstests, Gentests, Wurfverwaltung, Kaufvertrag-Generator, KI-Assistent.",
|
"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.",
|
||||||
"url": "https://banyaro.app/zuechter",
|
"url": "https://banyaro.app/zuechter",
|
||||||
"applicationCategory": "BusinessApplication",
|
"applicationCategory": "BusinessApplication",
|
||||||
"operatingSystem": "iOS, Android, Web",
|
"operatingSystem": "iOS, Android, Web",
|
||||||
"inLanguage": "de",
|
"inLanguage": "de",
|
||||||
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "EUR" },
|
"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"
|
||||||
|
}
|
||||||
|
],
|
||||||
"audience": { "@type": "Audience", "audienceType": "Hundezüchter, VDH-Züchter, Rassehundzüchter" },
|
"audience": { "@type": "Audience", "audienceType": "Hundezüchter, VDH-Züchter, Rassehundzüchter" },
|
||||||
|
"softwareVersion": "1.5.1",
|
||||||
|
"dateModified": "2026-05-14",
|
||||||
"featureList": [
|
"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",
|
"Stammbaum bis 4 Generationen grafisch",
|
||||||
"Inzuchtkoeffizient nach Wright mit Ampel-Bewertung",
|
"Inzuchtkoeffizient nach Wright mit Ampel-Bewertung",
|
||||||
"Probeverpaarung mit IK-Simulation",
|
"Probeverpaarung mit IK-Simulation",
|
||||||
|
|
@ -548,8 +571,15 @@
|
||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<section class="cta-section">
|
<section class="cta-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Jetzt kostenlos starten.</h2>
|
<div style="display:inline-block;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.25);
|
||||||
<p>Züchter-Antrag stellen, Zuchthunde anlegen, ersten Wurf veröffentlichen — alles kostenlos, kein App Store, keine Kreditkarte.</p>
|
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>
|
||||||
<a href="/#register?rolle=breeder" class="cta-btn" onclick="sessionStorage.setItem('by_stay_in_app','1')">Als Züchter registrieren</a>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ services:
|
||||||
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
- VAPID_PUBLIC_KEY=BMKbFAmpsqJ-eFef_4XJcYpuxPWqBNAoy9buMNnMSa6ijcPzltboHi_YccPKJrUD0isBez-vJIzAgjnLTWkzcC0
|
||||||
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
- VAPID_PRIVATE_KEY=8PWa9vvwMqtqsJEJGcwmiLhR0_Yl7duVX3wmWiKS878
|
||||||
- VAPID_CONTACT=mailto:admin@banyaro.app
|
- VAPID_CONTACT=mailto:admin@banyaro.app
|
||||||
|
- UMAMI_URL=https://umami.motocamp.de
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/')"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue