Compare commits

..

21 commits

Author SHA1 Message Date
18d6bd407a Fix: manifest.json — display_override + launch_handler für PWABuilder-Score (SW by-v760) 2026-05-07 17:32:07 +02:00
8683de4bf2 UX: Android-Installationsanleitung — Hinweis bei Sicherheitswarnung ergänzt (SW by-v759) 2026-05-07 17:26:25 +02:00
5d37c96796 UX: Welcome-Fußzeile → Impressum + Datenschutz statt Werbetext (SW by-v758) 2026-05-07 17:22:01 +02:00
f12e29c83c UX: Welcome-Seite — untere CTA-Buttons entfernt (SW by-v757) 2026-05-07 17:20:16 +02:00
65193c3030 UX: Welcome-Seite — Feature-Sektion und Privacy-Block entfernt, nur Hero+Install+CTA (SW by-v756) 2026-05-07 17:18:26 +02:00
e5e95efaed UX: Welcome-Seite — Anmelde-Einstieg vereinfacht, Install-Block prominent mit PWA-Erklärung (SW by-v755) 2026-05-07 17:15:19 +02:00
b31116abf6 Fix: Landing-Page Footer/Nav bereinigt — Wiki+Social raus, kaputte CTAs entfernt (SW by-v754) 2026-05-07 17:09:02 +02:00
1fe878924a Fix: Landing-Page — Sitting kostenlos, Phosphor-Icons, KI-Datenschutz korrekt, Pro-Features ausgeblendet (SW by-v753) 2026-05-07 17:05:56 +02:00
cf2191efeb Fix: 'Weitere Funktionen' zeigt nur ausgeblendete Chips — bereits konfigurierte werden gefiltert (SW by-v752) 2026-05-07 16:43:30 +02:00
bd7b694874 Feature: FAB → 'Alle Funktionen' — kompaktes Chip-Grid aller verfügbaren Seiten (SW by-v751) 2026-05-07 16:39:22 +02:00
fdbd5448bc Fix: FAB-Overlay z-index 300→460 — war hinter worlds-overlay (z-index 450) versteckt (SW by-v750) 2026-05-07 15:51:03 +02:00
cb46f08f2a Fix: Vergleichstabelle landing.html — nur Hundeo+Dogorama, korrekte Daten aus echtem Research (SW by-v749) 2026-05-07 06:24:00 +02:00
81bc0f50a0 Fix: Nach Login ohne Hund → Onboarding mit Skip-Option statt direkt zu dog-profile (SW by-v748) 2026-05-06 21:02:55 +02:00
fd87c9af7b UX: Pro-Chips komplett ausgeblendet für Standard-User (kein 25% Opacity mehr) (SW by-v747) 2026-05-06 20:47:13 +02:00
7749aa6d2a Fix: verified/token Hash-Parameter setzt by_stay_in_app — kein /info-Redirect nach Verifikationslink (SW by-v746) 2026-05-06 20:34:22 +02:00
5f2c3476f9 Fix: SMTP Port 25 mit STARTTLS (465/587 von Synology geblockt) 2026-05-06 20:27:40 +02:00
a666efd25f Fix: SMTP Port 465 SSL statt 587 STARTTLS (Port 587 von Synology geblockt) 2026-05-06 20:23:45 +02:00
e4b661222c Fix: Züchter-Chips im Safety-Net nicht mehr ausgeschlossen — _chipAllowed() entscheidet für alle (SW by-v745) 2026-05-06 20:02:00 +02:00
b8a240e8e5 Fix: JETZT-Welt locked-Parameter fehlte in features.map → Pro-Chips jetzt auch dort 25% (SW by-v744) 2026-05-06 19:55:58 +02:00
4009a1d2a7 Fix: Breeder-Chips nie aus Sicherheitsnetz erzwingen — nur Admin/Mod/Social (SW by-v743) 2026-05-06 19:39:03 +02:00
4a4e98ea6f Fix: Rolle-gebundene Chips (Admin/Mod/Social) immer sichtbar unabhängig von persönlicher Config (SW by-v742) 2026-05-06 19:36:44 +02:00
11 changed files with 276 additions and 256 deletions

View file

@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
os.makedirs(MEDIA_DIR, exist_ok=True) os.makedirs(MEDIA_DIR, exist_ok=True)
app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media")
APP_VER = "741" # muss mit APP_VER in app.js übereinstimmen APP_VER = "760" # muss mit APP_VER in app.js übereinstimmen
@app.get("/api/version") @app.get("/api/version")
async def app_version(): async def app_version():

View file

@ -116,11 +116,17 @@ def _send_smtp(to: str, subject: str, body: str, account: str = "partner", html:
msg = _build_message(to, subject, body + _LEGAL_FOOTER, account, html=html) msg = _build_message(to, subject, body + _LEGAL_FOOTER, account, html=html)
msg_bytes = msg.as_bytes() msg_bytes = msg.as_bytes()
ctx = ssl.create_default_context() ctx = ssl.create_default_context()
with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s: if _SMTP_PORT == 465:
s.ehlo() with smtplib.SMTP_SSL(_SMTP_HOST, _SMTP_PORT, context=ctx, timeout=15) as s:
s.starttls(context=ctx) s.login(acc["user"], acc["pass"])
s.login(acc["user"], acc["pass"]) s.sendmail(acc["from"], [to], msg_bytes)
s.sendmail(acc["from"], [to], msg_bytes) else: # 587 oder 25 mit STARTTLS
with smtplib.SMTP(_SMTP_HOST, _SMTP_PORT, timeout=15) as s:
s.ehlo()
if s.has_extn("starttls"):
s.starttls(context=ctx)
s.login(acc["user"], acc["pass"])
s.sendmail(acc["from"], [to], msg_bytes)
_imap_save_sent(msg_bytes, account) _imap_save_sent(msg_bytes, account)

View file

@ -578,7 +578,7 @@
<script src="/js/api.js?v=94"></script> <script src="/js/api.js?v=94"></script>
<script src="/js/ui.js?v=94"></script> <script src="/js/ui.js?v=94"></script>
<script src="/js/app.js?v=94"></script> <script src="/js/app.js?v=94"></script>
<script src="/js/worlds.js?v=741"></script> <script src="/js/worlds.js?v=760"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '741'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '760'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app'; const IS_STAGING = location.hostname === 'staging.banyaro.app';
@ -555,20 +555,15 @@ const App = (() => {
} }
async function _checkNearbyAlerts() { async function _checkNearbyAlerts() {
const nav = document.getElementById('bottom-nav');
if (!nav) return;
try { try {
const pos = await new Promise((resolve, reject) => const pos = await new Promise((resolve, reject) =>
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000, maximumAge: 120_000 }) navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 5000, maximumAge: 120_000 })
); );
const { latitude: lat, longitude: lon } = pos.coords; const { latitude: lat, longitude: lon } = pos.coords;
const data = await API.get(`/alerts?lat=${lat}&lon=${lon}`); await API.get(`/alerts?lat=${lat}&lon=${lon}`);
nav.classList.toggle('alert-poison', !!data.poison); // Standort-Update für Push-Subscriptions (serverseitig in /alerts gespeichert)
nav.classList.toggle('alert-lost', !data.poison && !!data.lost);
// Burger-Badge: Giftköder/Verlorener Hund in der Nähe
document.getElementById('notif-nav-badge')?.classList.toggle('hidden', !data.poison && !data.lost);
} catch { } catch {
// Kein Standort verfügbar — kein Alert anzeigen // Kein Standort verfügbar — ignorieren
} }
} }
@ -848,6 +843,14 @@ const App = (() => {
// INITIALISIERUNG // INITIALISIERUNG
// ---------------------------------------------------------- // ----------------------------------------------------------
async function init() { async function init() {
// Spezielle Hash-Parameter → in App bleiben (kein /info-Redirect)
const _rawHash = location.hash.replace('#', '');
const _hashQuery = _rawHash.split('?')[1] || '';
const _hashP = new URLSearchParams(_hashQuery);
if (_hashP.get('verified') || _hashP.get('token') || location.pathname.startsWith('/teilen/')) {
sessionStorage.setItem('by_stay_in_app', '1');
}
// Referral-Code aus URL ?ref=CODE speichern // Referral-Code aus URL ?ref=CODE speichern
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const refCode = urlParams.get('ref'); const refCode = urlParams.get('ref');

View file

@ -440,9 +440,12 @@ window.Page_onboarding = (() => {
function _finish() { function _finish() {
localStorage.setItem('by_onboarding_done', '1'); localStorage.setItem('by_onboarding_done', '1');
if (_appState.dogs.length > 0) { if (_appState.dogs.length > 0) {
App.navigate('diary'); if (window.Worlds) window.Worlds.init(_appState);
else App.navigate('diary');
} else { } else {
App.navigate('map'); // Ohne Hund: Welten zeigen (HUND-Tab mit "Jetzt anlegen"-Karte)
if (window.Worlds) window.Worlds.init(_appState);
else App.navigate('welcome');
} }
} }

View file

@ -50,10 +50,12 @@ window.Page_settings = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// INIT / REFRESH // INIT / REFRESH
// ---------------------------------------------------------- // ----------------------------------------------------------
async function init(container, appState) { async function init(container, appState, params = {}) {
_container = container; _container = container;
_appState = appState; _appState = appState;
_render(); _render();
if (params.tab === 'login') setTimeout(() => _renderAuth('login'), 50);
if (params.tab === 'register') setTimeout(() => _renderAuth('register'), 50);
// Frischen User-State laden damit Badges (is_founder, is_partner) aktuell sind // Frischen User-State laden damit Badges (is_founder, is_partner) aktuell sind
if (_appState.user) { if (_appState.user) {
try { try {
@ -1689,11 +1691,11 @@ window.Page_settings = (() => {
_offerPushNotifications(); _offerPushNotifications();
} }
// Nach Login: Welten initialisieren (mit User-State) oder Profil anlegen // Nach Login: Welten initialisieren oder Onboarding (mit Skip-Option)
if (_appState.activeDog) { if (_appState.activeDog) {
window.Worlds?.init(_appState); window.Worlds?.init(_appState);
} else { } else {
App.navigate('dog-profile'); App.navigate('onboarding');
} }
}); });
}); });

View file

@ -87,7 +87,6 @@ window.Page_welcome = (() => {
// LANDING PAGE — nicht eingeloggte Besucher // LANDING PAGE — nicht eingeloggte Besucher
// ---------------------------------------------------------- // ----------------------------------------------------------
function _renderLanding(isInstalled) { function _renderLanding(isInstalled) {
// Browser-Besucher (kein PWA) ohne Login → auf /info weiterleiten
const isPWA = window.matchMedia('(display-mode: standalone)').matches const isPWA = window.matchMedia('(display-mode: standalone)').matches
|| window.navigator.standalone === true; || window.navigator.standalone === true;
if (!isPWA && !sessionStorage.getItem('by_stay_in_app')) { if (!isPWA && !sessionStorage.getItem('by_stay_in_app')) {
@ -113,17 +112,10 @@ window.Page_welcome = (() => {
</div> </div>
<div class="wc-lhero-cta"> <div class="wc-lhero-cta">
${hasPrompt ? ` <button class="btn wc-btn-hero" id="welcome-register-btn">
<button class="btn wc-btn-hero" id="welcome-install-hero-btn"> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg> Kostenlos registrieren
Zum Home-Bildschirm hinzufügen </button>
</button>
` : `
<button class="btn wc-btn-hero" id="welcome-register-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Kostenlos loslegen
</button>
`}
<button class="btn wc-btn-login" id="welcome-login-btn"> <button class="btn wc-btn-login" id="welcome-login-btn">
Schon dabei? Anmelden Schon dabei? Anmelden
</button> </button>
@ -145,119 +137,37 @@ window.Page_welcome = (() => {
</div> </div>
</div> </div>
<!-- Feature 1: Tagebuch --> <!-- Install-Block -->
<div class="wc-feature wc-feature--a"> ${!isInstalled ? `
<div class="wc-feature-icon"> <div class="wc-install-block">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg> <div class="wc-install-block-header">
</div> <div class="wc-install-block-icon">
<div class="wc-feature-text"> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#device-mobile"></use></svg>
<h2>Tagebuch & Erinnerungen</h2> </div>
<p>Halte jeden gemeinsamen Moment fest Fotos, Einträge, Stimmungen. Nur für dich, privat und sicher.</p> <div>
</div> <div class="wc-install-block-title">Kein App Store nötig</div>
</div> <div class="wc-install-block-sub">Füge Ban Yaro zum Home-Bildschirm hinzu einmal, dann immer griffbereit</div>
<!-- Feature 2: Gesundheit -->
<div class="wc-feature wc-feature--b">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Gesundheit im Blick</h2>
<p>Impfungen, Gewicht, Tierarzttermine alles an einem Ort. Du siehst immer, wann was ansteht.</p>
</div>
</div>
<!-- Feature 3: Community -->
<div class="wc-feature wc-feature--c">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Community vor Ort</h2>
<p>Giftköder-Warnungen, Gassi-Treffen, Events was in deiner Gegend gerade passiert.</p>
</div>
</div>
<!-- Feature 4: Training -->
<div class="wc-feature wc-feature--d">
<div class="wc-feature-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#target"></use></svg>
</div>
<div class="wc-feature-text">
<h2>Training & KI-Trainer</h2>
<p>Über 100 Übungen mit Schritt-für-Schritt-Anleitungen. Mit KI-Unterstützung, die deinen Hund kennt.</p>
</div>
</div>
<!-- Privacy Block -->
<div class="wc-privacy">
<div class="wc-privacy-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shield-check"></use></svg>
</div>
<h2 class="wc-privacy-title">Deine Daten gehören dir.</h2>
<p class="wc-privacy-sub">
Kein Facebook. Kein Google. Keine Werbung.<br>
Ban Yaro läuft auf einem eigenen Server in Deutschland
dein Tagebuch, deine Routen, deine Gesundheitsdaten
bleiben privat.
</p>
</div>
<!-- Und noch mehr (einklappbar) -->
<div class="wc-more">
<button class="wc-more-toggle" id="wc-more-toggle" aria-expanded="false">
<span>Und noch viel mehr</span>
<svg class="ph-icon wc-more-chevron" aria-hidden="true"><use href="/icons/phosphor.svg#caret-down"></use></svg>
</button>
<div class="wc-grid wc-grid--collapsed" id="wc-more-grid">
${FEATURES.map(f => `
<div class="wc-tile wc-tile--static">
<div class="wc-tile-icon">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${f.icon}"></use></svg>
</div>
<span class="wc-tile-label">${f.label}</span>
</div>
`).join('')}
</div>
</div>
<!-- Bottom CTA -->
<div class="wc-bottom-cta">
${hasPrompt ? `
<button class="btn wc-btn-hero" id="welcome-install-hero-btn2">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Zum Home-Bildschirm hinzufügen
</button>
` : `
<button class="btn wc-btn-hero" id="welcome-register-btn2">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paw-print"></use></svg>
Jetzt kostenlos starten
</button>
`}
<p class="wc-bottom-hint">Kein App Store · Direkt auf den Home-Bildschirm</p>
${!isInstalled ? `
<button class="wc-install-link" id="welcome-install-link">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Zum Home-Bildschirm hinzufügen
</button>
` : ''}
</div>
<!-- Install Card (via Einstellungen) -->
${_showInstall ? `
<div class="page-container" style="padding:0 var(--space-4) var(--space-6)">
<div class="card wc-install-card">
<div class="wc-install-header">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Immer griffbereit kein App Store
</div>
<div style="padding:var(--space-4)">${_installHTML()}</div>
</div> </div>
</div> </div>
<p class="wc-install-block-why">
Ban Yaro ist eine Web-App (PWA). Das bedeutet: kein App-Store-Download, automatische Updates ohne dein Zutun, und sie verhält sich genau wie eine native App mit Icon, Vollbild und Offline-Modus.
</p>
${hasPrompt ? `
<button class="btn btn-primary wc-install-block-btn" id="welcome-install-hero-btn">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#download-simple"></use></svg>
Jetzt zum Home-Bildschirm hinzufügen
</button>
` : `
<div class="wc-install-block-steps">${_installHTML()}</div>
`}
</div>
` : ''} ` : ''}
<p class="wc-footer">Ban Yaro · Deine Daten auf eigenem Server in Deutschland</p> <p class="wc-footer">
<a href="/#impressum" style="color:var(--c-text-muted)">Impressum</a>
&nbsp;·&nbsp;
<a href="/#datenschutz" style="color:var(--c-text-muted)">Datenschutz</a>
</p>
</div> </div>
`; `;
} }
@ -811,6 +721,43 @@ window.Page_welcome = (() => {
} }
.wc-grid.wc-grid--collapsed { display: none; } .wc-grid.wc-grid--collapsed { display: none; }
/* Install Block */
.wc-install-block {
background: var(--c-bg-card);
border-top: 3px solid var(--c-primary);
padding: var(--space-5) var(--space-5) var(--space-6);
}
.wc-install-block-header {
display: flex; align-items: flex-start; gap: var(--space-3);
margin-bottom: var(--space-3);
}
.wc-install-block-icon {
width: 44px; height: 44px; border-radius: var(--radius-md);
background: var(--c-primary-subtle); flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
}
.wc-install-block-icon .ph-icon { width: 22px; height: 22px; color: var(--c-primary); }
.wc-install-block-title {
font-size: var(--text-base); font-weight: var(--weight-bold);
color: var(--c-text); margin-bottom: 2px;
}
.wc-install-block-sub {
font-size: var(--text-sm); color: var(--c-text-secondary); line-height: 1.4;
}
.wc-install-block-why {
font-size: var(--text-sm); color: var(--c-text-secondary);
line-height: 1.6; margin: 0 0 var(--space-4);
padding: var(--space-3) var(--space-4);
background: var(--c-surface); border-radius: var(--radius-md);
border-left: 3px solid var(--c-primary);
}
.wc-install-block-btn {
width: 100%; font-size: var(--text-base);
padding: 14px; border-radius: var(--radius-lg);
display: flex; align-items: center; justify-content: center; gap: var(--space-2);
}
.wc-install-block-steps { margin-top: var(--space-2); }
/* Bottom CTA */ /* Bottom CTA */
.wc-bottom-cta { .wc-bottom-cta {
padding: var(--space-8) var(--space-5) var(--space-6); padding: var(--space-8) var(--space-5) var(--space-6);
@ -1127,6 +1074,15 @@ window.Page_welcome = (() => {
['dots-three-circle','Tippe auf das <strong>Menü ⋮</strong> oben rechts'], ['dots-three-circle','Tippe auf das <strong>Menü ⋮</strong> oben rechts'],
['download-simple', 'Wähle <strong>„App installieren"</strong> oder<br>„Zum Startbildschirm hinzufügen"'], ['download-simple', 'Wähle <strong>„App installieren"</strong> oder<br>„Zum Startbildschirm hinzufügen"'],
])} ])}
<div style="display:flex;gap:var(--space-2);align-items:flex-start;
background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3) var(--space-3);margin-top:var(--space-4)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;color:var(--c-text-secondary)"
aria-hidden="true"><use href="/icons/phosphor.svg#shield-warning"></use></svg>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:0;line-height:1.5">
Falls eine Sicherheitswarnung erscheint: Das ist normal für neuere Webseiten unabhängig vom Inhalt. Ban Yaro läuft auf einem deutschen Server, ist DSGVO-konform und enthält keine Schadsoftware. Tippe auf <strong>Trotzdem hinzufügen"</strong>.
</p>
</div>
<button class="btn btn-ghost btn-sm" id="install-copy-btn" <button class="btn btn-ghost btn-sm" id="install-copy-btn"
style="margin-top:var(--space-3);width:100%"> style="margin-top:var(--space-3);width:100%">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#copy"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#copy"></use></svg>
@ -1150,6 +1106,15 @@ window.Page_welcome = (() => {
['monitor', 'Klicke auf das <strong>Installations-Symbol</strong> in der Adressleiste'], ['monitor', 'Klicke auf das <strong>Installations-Symbol</strong> in der Adressleiste'],
['check', 'Bestätigen — fertig!'], ['check', 'Bestätigen — fertig!'],
])} ])}
<div style="display:flex;gap:var(--space-2);align-items:flex-start;
background:var(--c-surface);border-radius:var(--radius-md);
padding:var(--space-3);margin-top:var(--space-4)">
<svg class="ph-icon" style="width:16px;height:16px;flex-shrink:0;margin-top:2px;color:var(--c-text-secondary)"
aria-hidden="true"><use href="/icons/phosphor.svg#shield-warning"></use></svg>
<p style="font-size:var(--text-xs);color:var(--c-text-secondary);margin:0;line-height:1.5">
Falls eine Sicherheitswarnung erscheint: Das ist normal für neuere Webseiten. Tippe auf <strong>Trotzdem hinzufügen"</strong>.
</p>
</div>
</div> </div>
<div id="inst-panel-ios" style="display:none"> <div id="inst-panel-ios" style="display:none">
${_steps([ ${_steps([
@ -1194,15 +1159,10 @@ window.Page_welcome = (() => {
_container.querySelector('#welcome-install-hero-btn2')?.addEventListener('click', installBtn); _container.querySelector('#welcome-install-hero-btn2')?.addEventListener('click', installBtn);
// Register / Login // Register / Login
const toSettings = () => App.navigate('settings'); _container.querySelector('#welcome-register-btn')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'register' }));
_container.querySelector('#welcome-register-btn')?.addEventListener('click', toSettings); _container.querySelector('#welcome-register-btn2')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'register' }));
_container.querySelector('#welcome-register-btn2')?.addEventListener('click', toSettings); _container.querySelector('#welcome-login-btn')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'login' }));
_container.querySelector('#welcome-login-btn')?.addEventListener('click', toSettings); _container.querySelector('#welcome-login-btn2')?.addEventListener('click', () => App.navigate('settings', true, { tab: 'login' }));
// Installationsanleitung Link
_container.querySelector('#welcome-install-link')?.addEventListener('click', () => {
App.navigate('welcome', true, { install: true });
});
// Link kopieren // Link kopieren
_container.querySelector('#install-copy-btn')?.addEventListener('click', async () => { _container.querySelector('#install-copy-btn')?.addEventListener('click', async () => {

View file

@ -267,7 +267,6 @@ window.Worlds = (() => {
function _openFab() { function _openFab() {
const options = _fabOptions(); const options = _fabOptions();
if (!options.length) return;
const meldenPages = new Set(['poison','lost','recalls','map']); const meldenPages = new Set(['poison','lost','recalls','map']);
const meldenCount = options.filter(o => meldenPages.has(o.page)).length; const meldenCount = options.filter(o => meldenPages.has(o.page)).length;
@ -275,14 +274,14 @@ window.Worlds = (() => {
const ov = document.createElement('div'); const ov = document.createElement('div');
ov.id = 'fab-overlay'; ov.id = 'fab-overlay';
ov.style.cssText = 'position:fixed;inset:0;z-index:300;display:flex;flex-direction:column;justify-content:flex-end'; ov.style.cssText = 'position:fixed;inset:0;z-index:460;display:flex;flex-direction:column;justify-content:flex-end';
ov.innerHTML = ` ov.innerHTML = `
<div id="fab-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(2px)"></div> <div id="fab-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(2px)"></div>
<div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0; <div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0;
padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px); padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px);
box-shadow:0 -8px 32px rgba(0,0,0,0.2)"> box-shadow:0 -8px 32px rgba(0,0,0,0.2)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div style="font-size:var(--text-base);font-weight:700">${title}</div> <div style="font-size:var(--text-base);font-weight:700">${options.length ? title : 'Schnellzugriff'}</div>
<button id="fab-close" style="background:var(--c-border);border:none;border-radius:50%; <button id="fab-close" style="background:var(--c-border);border:none;border-radius:50%;
width:28px;height:28px;cursor:pointer;display:flex;align-items:center;justify-content:center"> width:28px;height:28px;cursor:pointer;display:flex;align-items:center;justify-content:center">
<svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg> <svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
@ -308,6 +307,12 @@ window.Worlds = (() => {
</button> </button>
`).join('')} `).join('')}
</div> </div>
<button id="fab-all-btn" style="display:flex;align-items:center;justify-content:center;gap:8px;
width:100%;margin-top:${options.length ? '14px' : '0'};padding:12px;border:none;
background:none;cursor:pointer;color:var(--c-primary);font-size:var(--text-sm);font-weight:600">
<svg class="ph-icon" style="width:16px;height:16px"><use href="/icons/phosphor.svg#squares-four"></use></svg>
Weitere Funktionen
</button>
</div> </div>
`; `;
@ -316,6 +321,7 @@ window.Worlds = (() => {
const _close = () => ov.remove(); const _close = () => ov.remove();
ov.querySelector('#fab-backdrop').addEventListener('click', _close); ov.querySelector('#fab-backdrop').addEventListener('click', _close);
ov.querySelector('#fab-close').addEventListener('click', _close); ov.querySelector('#fab-close').addEventListener('click', _close);
ov.querySelector('#fab-all-btn').addEventListener('click', () => { _close(); _openAllChips(); });
ov.querySelectorAll('.fab-option').forEach(btn => { ov.querySelectorAll('.fab-option').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
_close(); _close();
@ -333,6 +339,75 @@ window.Worlds = (() => {
}); });
} }
function _openAllChips() {
const worldNames = ['jetzt', 'hund', 'welt'];
const worldLabels = { jetzt: 'JETZT', hund: 'HUND', welt: 'WELT' };
// Alle Seiten die aktuell in irgendeiner Welt konfiguriert sind
const cfg = _getConfig();
const configured = new Set(worldNames.flatMap(w => cfg[w] || []));
const sections = worldNames.map(w => {
const chips = (_DEFAULT_CONFIG[w] || []).map(_chipMeta)
.filter(c => c && _chipAllowed(c) && !configured.has(c.page));
if (!chips.length) return '';
return `
<div style="margin-bottom:20px">
<div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary);
letter-spacing:0.08em;margin-bottom:10px">${worldLabels[w]}</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
${chips.map(c => `
<button class="all-chip-btn" data-page="${c.page}"
style="display:flex;flex-direction:column;align-items:center;gap:6px;
background:var(--c-bg-card);border:1px solid var(--c-border);
border-radius:14px;padding:12px 6px;cursor:pointer;
transition:background .12s">
<svg class="ph-icon" style="width:1.4rem;height:1.4rem;color:var(--c-primary)">
<use href="/icons/phosphor.svg#${c.icon}"></use>
</svg>
<span style="font-size:10px;font-weight:600;color:var(--c-text);text-align:center;
line-height:1.2;word-break:break-word">${c.label}</span>
</button>
`).join('')}
</div>
</div>
`;
}).join('');
const ov = document.createElement('div');
ov.id = 'fab-overlay';
ov.style.cssText = 'position:fixed;inset:0;z-index:460;display:flex;flex-direction:column;justify-content:flex-end';
ov.innerHTML = `
<div id="fab-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(2px)"></div>
<div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0;
padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px);
box-shadow:0 -8px 32px rgba(0,0,0,0.2);max-height:82vh;overflow-y:auto">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
<div style="font-size:var(--text-base);font-weight:700">Ausgeblendete Funktionen</div>
<button id="fab-close" style="background:var(--c-border);border:none;border-radius:50%;
width:28px;height:28px;cursor:pointer;display:flex;align-items:center;justify-content:center">
<svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
</button>
</div>
${sections || `<div style="text-align:center;padding:24px 0;color:var(--c-text-secondary);font-size:var(--text-sm)">
Alle Funktionen sind bereits in deinen Welten sichtbar.
</div>`}
</div>
`;
document.body.appendChild(ov);
const _close = () => ov.remove();
ov.querySelector('#fab-backdrop').addEventListener('click', _close);
ov.querySelector('#fab-close').addEventListener('click', _close);
ov.querySelectorAll('.all-chip-btn').forEach(btn => {
btn.addEventListener('click', () => {
_close();
navigateTo(btn.dataset.page);
});
});
}
// ── SCHNELL-GASSI ───────────────────────────────────────────── // ── SCHNELL-GASSI ─────────────────────────────────────────────
async function _openQuickGassi() { async function _openQuickGassi() {
@ -569,6 +644,16 @@ window.Worlds = (() => {
const u = _state?.user; const u = _state?.user;
const tier = u?.subscription_tier || 'standard'; const tier = u?.subscription_tier || 'standard';
const isTest = tier.endsWith('_test'); const isTest = tier.endsWith('_test');
// Pro-Chips: komplett ausblenden wenn kein Zugriff
if (chip.pro) {
if (!u) return false;
if (isTest) return ['pro_test','breeder_test'].includes(tier);
if (u.rolle === 'admin' || u.rolle === 'moderator') return true;
if (u.is_moderator || u.is_social_media) return true;
return ['pro','breeder'].includes(tier);
}
// Role-Checks (hart — komplett ausblenden) // Role-Checks (hart — komplett ausblenden)
if (!chip?.role) return true; if (!chip?.role) return true;
if (chip.role === 'breeder') { if (chip.role === 'breeder') {
@ -594,7 +679,18 @@ window.Worlds = (() => {
function _chipsForWorld(world) { function _chipsForWorld(world) {
const pages = _getConfig()[world] || _DEFAULT_CONFIG[world]; const pages = _getConfig()[world] || _DEFAULT_CONFIG[world];
return pages.map(_chipMeta).filter(c => c && _chipAllowed(c)); // Alle Chips filtern — _chipAllowed entscheidet ob angezeigt
const chips = pages.map(_chipMeta).filter(c => c && _chipAllowed(c));
// Sicherheitsnetz: Rolle-gebundene Chips aus Default einfügen wenn berechtigt
// (aber noch nicht in persönlicher Config) — _chipAllowed() entscheidet für alle
const alreadyIn = new Set(chips.map(c => c.page));
for (const page of (_DEFAULT_CONFIG[world] || [])) {
if (alreadyIn.has(page)) continue;
const meta = _chipMeta(page);
if (!meta?.role) continue; // nur role-gebundene Chips
if (_chipAllowed(meta)) { chips.push(meta); alreadyIn.add(page); }
}
return chips;
} }
// ── KONFIGURATIONS-MODAL ───────────────────────────────────── // ── KONFIGURATIONS-MODAL ─────────────────────────────────────
@ -1259,7 +1355,7 @@ window.Worlds = (() => {
` : ''} ` : ''}
<div class="world-section-label">Alles über ${_esc(dog.name)}</div> <div class="world-section-label">Alles über ${_esc(dog.name)}</div>
<div class="world-chips-grid"> <div class="world-chips-grid">
${chips.map(c => _chip(c.icon, c.label, c.page, !!(c.pro && !_hasProAccess()))).join('')} ${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
</div> </div>
<div class="world-footer-links"> <div class="world-footer-links">
<span data-wnav="gruender">Die 100 Gründer</span> <span data-wnav="gruender">Die 100 Gründer</span>
@ -1353,7 +1449,7 @@ window.Worlds = (() => {
<div class="world-bottom"> <div class="world-bottom">
<div class="world-section-label">Die Welt da draußen</div> <div class="world-section-label">Die Welt da draußen</div>
<div class="world-chips-grid"> <div class="world-chips-grid">
${chips.map(c => _chip(c.icon, c.label, c.page, !!(c.pro && !_hasProAccess()))).join('')} ${chips.map(c => _chip(c.icon, c.label, c.page)).join('')}
</div> </div>
<div class="world-footer-links"> <div class="world-footer-links">
<span data-wnav="datenschutz">Datenschutz</span> <span data-wnav="datenschutz">Datenschutz</span>

View file

@ -373,7 +373,7 @@
margin-top: 2rem; margin-top: 2rem;
} }
.usp-item { display: flex; align-items: flex-start; gap: 0.75rem; } .usp-item { display: flex; align-items: flex-start; gap: 0.75rem; }
.usp-icon { font-size: 1.5rem; flex-shrink: 0; margin-top: 0.1rem; } .usp-icon { width: 1.6rem; height: 1.6rem; flex-shrink: 0; margin-top: 0.15rem; color: var(--primary); }
.usp-item h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.2rem; } .usp-item h3 { font-size: 0.95rem; font-weight: 700; margin-bottom: 0.2rem; }
.usp-item p { font-size: 0.85rem; color: var(--text-secondary); } .usp-item p { font-size: 0.85rem; color: var(--text-secondary); }
@ -503,7 +503,6 @@
<a href="#vergleich">Vergleich</a> <a href="#vergleich">Vergleich</a>
<a href="#preise">Preise</a> <a href="#preise">Preise</a>
<a href="#warum">Warum Ban Yaro?</a> <a href="#warum">Warum Ban Yaro?</a>
<a href="/wiki/rassen">Rassen-Wiki</a>
<a href="/">App öffnen</a> <a href="/">App öffnen</a>
</div> </div>
</nav> </nav>
@ -591,14 +590,14 @@
<svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg> <svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg>
</div> </div>
<h3>Leute unter sich</h3> <h3>Leute unter sich</h3>
<p>Ein Forum nur für Hundemenschen. Fragen stellen, Erfahrungen teilen, lokale Gassi-Treffen organisieren — ohne Algorithmen, ohne Werbung.</p> <p>Ein Forum nur für Hundemenschen. Fragen stellen, Erfahrungen teilen, Tipps weitergeben — ohne Algorithmen, ohne Werbung.</p>
</div> </div>
<div class="outcome-card"> <div class="outcome-card">
<div class="oc-icon"> <div class="oc-icon">
<svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#house-line"></use></svg> <svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#house-line"></use></svg>
</div> </div>
<h3>Jemanden für die Gassi gesucht?</h3> <h3>Jemanden für die Gassi gesucht?</h3>
<p>Du musst da oder dort hin — finde jemanden der auf deinen Hund aufpasst. Hundesitting-Vermittlung mit nur 8% Provision statt 20% bei anderen.</p> <p>Du musst da oder dort hin — finde jemanden der auf deinen Hund aufpasst. Hundesitting-Vermittlung kostenlos, ohne Provision. Ihr einigt euch direkt.</p>
</div> </div>
</div> </div>
@ -636,14 +635,6 @@
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#cloud-sun"></use></svg></span> <span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#cloud-sun"></use></svg></span>
<div><h3>Wetter, Gassi-Score &amp; Zecken-Alarm</h3><p>7-Tage-Wetter, tägliche Bewertung 110 für Gassi-Eignung, automatische Zecken-Warnung.</p></div> <div><h3>Wetter, Gassi-Score &amp; Zecken-Alarm</h3><p>7-Tage-Wetter, tägliche Bewertung 110 für Gassi-Eignung, automatische Zecken-Warnung.</p></div>
</div> </div>
<div class="feature-card">
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#fork-knife"></use></svg></span>
<div><h3>Ernährung &amp; Futter</h3><p>Kalorienbedarf berechnen, BARF-Guide, Giftliste und KI-Futterberater.</p></div>
</div>
<div class="feature-card">
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#airplane"></use></svg></span>
<div><h3>Reise mit Hund</h3><p>Reisecheckliste und EU-Länder-Guide mit länderspezifischen Einreiseregeln und Impfvorschriften.</p></div>
</div>
<div class="feature-card"> <div class="feature-card">
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg></span> <span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#certificate"></use></svg></span>
<div><h3>NFC-Halsband-Tags</h3><p>Öffentliche Profilseite für jeden Hund. Finder kontaktiert dich anonym — ohne deine Nummer preiszugeben.</p></div> <div><h3>NFC-Halsband-Tags</h3><p>Öffentliche Profilseite für jeden Hund. Finder kontaktiert dich anonym — ohne deine Nummer preiszugeben.</p></div>
@ -666,10 +657,6 @@
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#path"></use></svg></span> <span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#path"></use></svg></span>
<div><h3>Tages-Gassirunde</h3><p>Täglich neue Rundroute — 2, 4 oder 6 km ab deinem Standort. Berechnet via OpenRouteService.</p></div> <div><h3>Tages-Gassirunde</h3><p>Täglich neue Rundroute — 2, 4 oder 6 km ab deinem Standort. Berechnet via OpenRouteService.</p></div>
</div> </div>
<div class="feature-card">
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#paw-print"></use></svg></span>
<div><h3>Gassi-Treffen</h3><p>Spontane oder geplante Gassi-Treffen erstellen und finden.</p></div>
</div>
<div class="feature-card"> <div class="feature-card">
<span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg></span> <span class="feature-icon"><svg viewBox="0 0 256 256"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg></span>
<div><h3>Forum</h3><p>Öffentlich lesbar ohne Anmeldung. Kategorien nach Rasse, Region, Gesundheit und Erziehung.</p></div> <div><h3>Forum</h3><p>Öffentlich lesbar ohne Anmeldung. Kategorien nach Rasse, Region, Gesundheit und Erziehung.</p></div>
@ -714,7 +701,6 @@
</div> </div>
</div> </div>
<a href="/" class="section-cta-btn">Züchter-Profil anlegen — kostenlos</a>
</div> </div>
</section> </section>
@ -748,48 +734,39 @@
</div> </div>
</div> </div>
<a href="/wurfboerse" class="section-cta-btn">Wurfbörse durchstöbern — kostenlos</a>
</div> </div>
</section> </section>
<section id="vergleich"> <section id="vergleich">
<div class="container"> <div class="container">
<h2>Ban Yaro vs. Konkurrenz</h2> <h2>Ban Yaro vs. Konkurrenz</h2>
<p class="section-intro">Andere Apps decken einzelne Bereiche ab — Ban Yaro vereint alles in einer Plattform. Kein anderer Anbieter kombiniert Community, Training, Zucht und KI auf Deutsch.</p> <p class="section-intro">Hundeo ist stark im Training — Dogorama stark in der Community. Ban Yaro vereint beides und geht weit darüber hinaus: Zucht-Management, KI-Trainer, Gesundheitsakte und Sitting in einer einzigen App — ohne App Store.</p>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Funktion</th> <th>Funktion</th>
<th>Ban Yaro</th> <th>Ban Yaro</th>
<th>Hundeo (DE)</th> <th>Hundeo</th>
<th>Dogorama</th> <th>Dogorama</th>
<th>Tractive</th>
<th>PetDesk</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>Kostenlos nutzbar</td> <td>Kostenlos nutzbar</td>
<td class="check">✓ Ja</td> <td class="check">✓ Ja</td>
<td>Begrenzt</td> <td>Freemium</td>
<td>Begrenzt</td> <td>Freemium</td>
<td class="cross">✗ Abo</td>
<td class="cross">✗ Nein</td>
</tr> </tr>
<tr> <tr>
<td>DSGVO / EU-Hosting</td> <td>DSGVO / EU-konform</td>
<td class="check">✓ DE</td> <td class="check">✓ DE</td>
<td class="check">✓ EU</td>
<td class="check">✓ DE</td> <td class="check">✓ DE</td>
<td class="cross">✗ Nein</td>
<td>Teilweise</td>
<td class="cross">✗ USA</td>
</tr> </tr>
<tr> <tr>
<td>Kein App Store nötig</td> <td>Kein App Store nötig (PWA)</td>
<td class="check">✓ PWA</td> <td class="check"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
</tr> </tr>
@ -798,62 +775,46 @@
<td class="check"></td> <td class="check"></td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Giftköder-Alarm</td> <td>Giftköder-Alarm</td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="check"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Digitaler Impfpass</td> <td>Gesundheitsakte &amp; Impfpass</td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="check"></td> <td class="check"></td>
</tr> </tr>
<tr> <tr>
<td>Forum & Community</td> <td>Forum &amp; Community</td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="check"></td> <td class="check"></td>
</tr>
<tr>
<td>Gassi-Treffen &amp; Playdates</td>
<td class="check"></td>
<td class="cross"></td>
<td class="check"></td>
</tr>
<tr>
<td>Wurfbörse &amp; Zucht-Management</td>
<td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Gassi-Treffen & Community</td> <td>Stammbaum &amp; Inzucht-Check</td>
<td class="check"></td>
<td class="cross"></td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Wurfbörse & Zucht-Management</td> <td>Hundesitting-Vermittlung</td>
<td class="check"></td> <td class="check">✓ kostenlos</td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr>
<tr>
<td>Stammbaum & Inzucht-Check</td>
<td class="check"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr>
<tr>
<td>Hundesitting</td>
<td class="check">✓ 8%</td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
</tr> </tr>
@ -862,32 +823,24 @@
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="check"></td> <td class="check"></td>
<td class="check">✓ GPS</td>
<td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Rassen-Wiki (1003 Rassen, KI)</td> <td>Rassen-Wiki (KI-angereichert)</td>
<td class="check"></td> <td class="check">✓ 1.003</td>
<td class="cross"></td> <td>200+</td>
<td class="cross"></td> <td>Basis</td>
<td class="cross"></td>
<td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Offline-Modus</td> <td>Offline-Modus</td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td>nur Premium</td>
<td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
</tr> </tr>
<tr> <tr>
<td>Täglicher Routenvorschlag</td> <td>KI-Routenvorschlag</td>
<td class="check"></td> <td class="check"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td> <td class="cross"></td>
<td class="cross"></td>
<td class="cross"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@ -948,59 +901,59 @@
<p class="section-intro">Ban Yaro wurde von Hundebesitzern für Hundebesitzer entwickelt — mit einem klaren Standpunkt zu Datenschutz und Fairness.</p> <p class="section-intro">Ban Yaro wurde von Hundebesitzern für Hundebesitzer entwickelt — mit einem klaren Standpunkt zu Datenschutz und Fairness.</p>
<div class="usp-strip"> <div class="usp-strip">
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">🇩🇪</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#flag"></use></svg>
<div> <div>
<h3>Deutsche Plattform</h3> <h3>Deutsche Plattform</h3>
<p>Hosting in Deutschland, deutschsprachiger Support, auf DACH-Nutzer zugeschnitten.</p> <p>Hosting in Deutschland, deutschsprachiger Support, auf DACH-Nutzer zugeschnitten.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">🔒</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#lock-simple"></use></svg>
<div> <div>
<h3>Deine Daten. Dein Eigentum.</h3> <h3>Deine Daten. Dein Eigentum.</h3>
<p>Keine Datenweitergabe an US-Konzerne. Cookielose Analytics (Umami). Transparente Datennutzung.</p> <p>Keine Datenweitergabe an US-Konzerne. Cookielose Analytics (Umami). Transparente Datennutzung.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">📱</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#device-mobile"></use></svg>
<div> <div>
<h3>Kein App Store</h3> <h3>Kein App Store</h3>
<p>Als Progressive Web App direkt über den Browser installierbar — auf iOS und Android. Sofort updatebar.</p> <p>Als Progressive Web App direkt über den Browser installierbar — auf iOS und Android. Sofort updatebar.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">📡</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#cloud-slash"></use></svg>
<div> <div>
<h3>Offline-fähig</h3> <h3>Offline-fähig</h3>
<p>Service Worker sorgt dafür dass die App auch ohne Internet funktioniert — beim Gassi gehen in der Natur.</p> <p>Service Worker sorgt dafür dass die App auch ohne Internet funktioniert — beim Gassi gehen in der Natur.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">💸</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#handshake"></use></svg>
<div> <div>
<h3>Faire Provision</h3> <h3>Sitting ohne Provision</h3>
<p>Hundesitting nur 8% Provision — Rover und Pawshake nehmen 20%. Mehr Geld bleibt beim Sitter.</p> <p>Hundesitting-Vermittlung kostenlos — keine Plattform-Provision, ihr einigt euch direkt. Rover und Pawshake nehmen 20%.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">🗺️</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#map-trifold"></use></svg>
<div> <div>
<h3>OpenStreetMap</h3> <h3>OpenStreetMap</h3>
<p>Karten von OpenStreetMap statt Google — keine Tracking-Cookies, kein API-Lock-in, günstiger für alle.</p> <p>Karten von OpenStreetMap statt Google — keine Tracking-Cookies, kein API-Lock-in, günstiger für alle.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">🔐</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#shield-check"></use></svg>
<div> <div>
<h3>Aktive Sicherheit</h3> <h3>Aktive Sicherheit</h3>
<p>HSTS, Content-Security-Policy, Rate Limiting auf allen Endpunkten, Account-Lockout nach Fehlversuchen, E-Mail-Verifikation. Sicherheit by Default, nicht als Nachgedanke.</p> <p>HSTS, Content-Security-Policy, Rate Limiting auf allen Endpunkten, Account-Lockout nach Fehlversuchen, E-Mail-Verifikation. Sicherheit by Default, nicht als Nachgedanke.</p>
</div> </div>
</div> </div>
<div class="usp-item"> <div class="usp-item">
<span class="usp-icon">🤖</span> <svg class="usp-icon" viewBox="0 0 256 256"><use href="/icons/phosphor.svg#robot"></use></svg>
<div> <div>
<h3>KI Made in Europe</h3> <h3>KI mit Datenschutz</h3>
<p>Alle KI-Funktionen laufen über Claude (Anthropic) — kein Training mit deinen Daten, kein Opt-out nötig, deine Daten bleiben deine Daten.</p> <p>KI-Funktionen laufen standardmäßig lokal auf unserem Server in Deutschland — deine Anfragen verlassen Europa nicht. Bei Überlast Fallback auf Claude Sonnet (Anthropic, USA). Übertragen wird nur deine Frage und der Kontext (Rasse, Alter) — keine Fotos, keine sensiblen Profildaten. Kein Training mit deinen Daten.</p>
</div> </div>
</div> </div>
</div> </div>
@ -1068,14 +1021,9 @@
<p><strong style="color:white">Ban Yaro</strong> — Die deutschsprachige Hunde-Plattform</p> <p><strong style="color:white">Ban Yaro</strong> — Die deutschsprachige Hunde-Plattform</p>
<p style="margin-top:0.5rem">banyaro.app · DSGVO-konform · Hosting in Deutschland · Made with 🐾</p> <p style="margin-top:0.5rem">banyaro.app · DSGVO-konform · Hosting in Deutschland · Made with 🐾</p>
<div class="footer-links"> <div class="footer-links">
<a href="/">App öffnen</a>
<a href="/info">Über Ban Yaro</a>
<a href="/wiki/rassen">Hunde-Rassen</a>
<a href="/knigge">Hunde-Knigge</a>
<a href="https://instagram.com/banyaro.app" rel="noopener" target="_blank">Instagram</a>
<a href="https://tiktok.com/@banyaro.app" rel="noopener" target="_blank">TikTok</a>
<a href="/#impressum">Impressum</a> <a href="/#impressum">Impressum</a>
<a href="/#datenschutz">Datenschutz</a> <a href="/#datenschutz">Datenschutz</a>
<a href="/presse">Presse</a>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -7,6 +7,8 @@
"start_url": "/", "start_url": "/",
"scope": "/", "scope": "/",
"display": "standalone", "display": "standalone",
"display_override": ["window-controls-overlay", "standalone"],
"launch_handler": { "client_mode": "focus-existing" },
"orientation": "portrait-primary", "orientation": "portrait-primary",
"background_color": "#FAF7F2", "background_color": "#FAF7F2",
"theme_color": "#C4843A", "theme_color": "#C4843A",

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v741'; const CACHE_VERSION = 'by-v760';
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