Compare commits

...

16 commits

Author SHA1 Message Date
8467f01b5b Hotfix: SW null-Response Bug — catch gibt 503 statt undefined zurück (Safari FetchEvent-Fehler) (SW by-v774) 2026-05-08 12:27:50 +02:00
ce14bb1d2c Refactoring: Inline-Styles → CSS-Klassen (worlds.js, dog-profile.js, settings.js) — 20 neue w3-/by-Klassen (SW by-v773) 2026-05-08 12:22:04 +02:00
3b8c9a72d8 UX: Wetter-Chip Subtext 9px/75% — kleiner als wj-chip-val, besser lesbar als 50% (SW by-v772) 2026-05-08 12:02:56 +02:00
f5c82101f3 Feature: /.well-known/assetlinks.json für Google Play TWA-Verifikation (app.banyaro.twa) 2026-05-08 11:41:46 +02:00
08382de8cb UX: Wetter-Chip Temperatur/Regen-Text als wj-chip-val — gleiche Lesbarkeit wie Nachbar-Chips (SW by-v771) 2026-05-08 11:30:50 +02:00
a1e5364f25 Fix: iOS bfcache — ?_t=timestamp bei Update-Reload, wird sofort aus URL entfernt (SW by-v770) 2026-05-08 11:27:06 +02:00
572fbf642f Fix: Wetter-Chip ohne farbigen Rand, Update-Prüfung nutzt APP_VER+API+location.replace('/') (SW by-v769) 2026-05-08 11:22:55 +02:00
4a6c056742 Feature: Schnell-Gassi pingt nur noch Streak, kein Tagebucheintrag mehr (SW by-v768) 2026-05-08 11:17:38 +02:00
68c38a2a0a Fix: Update-Reload navigiert zu '/' statt location.href — verhindert Tagebuch-Endloslade (SW by-v767) 2026-05-08 11:13:41 +02:00
b6eaaad47a Fix: '+ Weiteren Hund anlegen' nur für Pro sichtbar, App.hasPro() public (SW by-v766) 2026-05-08 11:06:23 +02:00
b0ea805434 Fix: Hundebuch + Ausweis — Zurück zur App Button ergänzt (standalone Seiten ohne Navigation) (SW by-v765) 2026-05-08 10:59:26 +02:00
2854c60ba8 Fix: Update-Button hängt nicht mehr (kein await update()), Wrapped-Modal × hinter Notch (safe-area) (SW by-v765) 2026-05-08 10:56:36 +02:00
7d89ed8bd2 Fix: Schnell-Gassi zeigt korrekte Meldung wenn offline gequeuet statt falsches Erfolgs-Toast (SW by-v764) 2026-05-08 10:15:31 +02:00
77795345b8 UX: Abmelden-Button als prominenter Outline-Button mit Trennlinie (SW by-v763) 2026-05-07 19:25:27 +02:00
a3c8d77a14 Fix: iOS SW-Update — SKIP_WAITING Handler, location.replace() statt reload(), no-store Header (SW by-v762) 2026-05-07 19:22:22 +02:00
a8b4fd781f Fix: Wetter — windspeed_max → wind_kmh (Feldname-Mismatch, Wind zeigte immer 0) (SW by-v761) 2026-05-07 19:04:31 +02:00
10 changed files with 471 additions and 169 deletions

View file

@ -327,7 +327,16 @@ 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 = "760" # muss mit APP_VER in app.js übereinstimmen APP_VER = "774" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
"""TWA-Verifikation für Google Play Store (app.banyaro.twa)."""
return Response(
content='[{"relation":["delegate_permission/common.handle_all_urls"],"target":{"namespace":"android_app","package_name":"app.banyaro.twa","sha256_cert_fingerprints":["49:02:DC:5B:63:C0:D7:42:7F:A4:DC:2F:EB:78:73:11:CC:B9:36:22:00:01:A0:03:1C:0A:F9:41:35:9F:D4:B7"]}}]',
media_type="application/json",
headers={"Cache-Control": "no-cache"},
)
@app.get("/api/version") @app.get("/api/version")
async def app_version(): async def app_version():
@ -848,7 +857,7 @@ async def share_target(request: Request):
# Weiterleitung zur App mit den Daten # Weiterleitung zur App mit den Daten
return FileResponse( return FileResponse(
f"{STATIC_DIR}/index.html", f"{STATIC_DIR}/index.html",
headers={"Cache-Control": "no-cache"} headers={"Cache-Control": "no-store, no-cache"}
) )
# Öffentliche Hunde-Profilseite (für NFC-Tags, kein Login nötig) # Öffentliche Hunde-Profilseite (für NFC-Tags, kein Login nötig)
@ -1182,17 +1191,17 @@ async def public_dog_page(dog_id: int):
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@app.get("/teilen/{token}") @app.get("/teilen/{token}")
async def invite_page(token: str): async def invite_page(token: str):
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
@app.get("/breeder/{zwingername}") @app.get("/breeder/{zwingername}")
async def breeder_profile_page(zwingername: str): async def breeder_profile_page(zwingername: str):
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
@app.get("/litters") @app.get("/litters")
async def litters_page(): async def litters_page():
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -1200,7 +1209,7 @@ async def litters_page():
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@app.get("/widget") @app.get("/widget")
async def widget_page(): async def widget_page():
return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-cache"}) return FileResponse(f"{STATIC_DIR}/index.html", headers={"Cache-Control": "no-store, no-cache"})
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -1355,7 +1364,14 @@ async def ausweis_page(dog_id: int, request: Request):
</div> </div>
<div class="body"> <div class="body">
<button class="print-btn no-print" onclick="window.print()">🖨 Drucken / Als PDF speichern</button> <div class="no-print" style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:16px">
<button onclick="window.history.length>1?window.history.back():window.close()"
style="background:#f0e6d3;color:#7a4a1e;border:none;border-radius:100px;
padding:10px 20px;font-size:0.9rem;cursor:pointer;font-weight:600">
Zurück zur App
</button>
<button class="print-btn" onclick="window.print()">🖨 Drucken / Als PDF speichern</button>
</div>
<div class="section"> <div class="section">
<h2>Impfungen</h2> <h2>Impfungen</h2>
@ -1726,7 +1742,7 @@ async def partner_landing():
</div> </div>
</body> </body>
</html>""" </html>"""
return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"}) return HTMLResponse(content=html, headers={"Cache-Control": "no-store, no-cache"})
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@ -1924,8 +1940,8 @@ async def spa_fallback(full_path: str):
'<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180.png">', '<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180.png">',
'<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180-staging.png">', '<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180-staging.png">',
) )
return HTMLResponse(content=html, headers={"Cache-Control": "no-cache"}) return HTMLResponse(content=html, headers={"Cache-Control": "no-store, no-cache"})
return FileResponse( return FileResponse(
f"{STATIC_DIR}/index.html", f"{STATIC_DIR}/index.html",
headers={"Cache-Control": "no-cache"} headers={"Cache-Control": "no-store, no-cache"}
) )

View file

@ -742,9 +742,18 @@ async def get_hunde_buch(
</head> </head>
<body> <body>
<button class="print-btn" onclick="window.print()"> <div class="print-btn" style="display:flex;gap:10px;flex-wrap:wrap">
<button onclick="window.history.length>1?window.history.back():window.close()"
style="background:#f0e6d3;color:#7a4a1e;border:none;border-radius:100px;
padding:10px 20px;font-size:0.9rem;cursor:pointer;font-weight:600">
Zurück zur App
</button>
<button onclick="window.print()"
style="background:#C4843A;color:#fff;border:none;border-radius:100px;
padding:10px 20px;font-size:0.9rem;cursor:pointer;font-weight:600">
&#x1F5A8; Drucken / Als PDF speichern &#x1F5A8; Drucken / Als PDF speichern
</button> </button>
</div>
<div class="cover"> <div class="cover">
{cover_img} {cover_img}

View file

@ -8370,3 +8370,336 @@ svg.empty-state-icon {
.breed-community-chip:hover, .breed-community-chip:active { .breed-community-chip:hover, .breed-community-chip:active {
background: #fff3e0; background: #fff3e0;
} }
/* ============================================================
REFACTORING: Extrahierte Inline-Styles aus worlds.js,
dog-profile.js und settings.js
============================================================ */
/* ----------------------------------------------------------
Bottom-Sheet Overlay (position:fixed, flex-column, flex-end)
Verwendet in: _openFab, _openAllChips, _openQuickGassi,
_openConfigModal (worlds.js)
---------------------------------------------------------- */
.w3-sheet-overlay {
position: fixed;
inset: 0;
z-index: 460;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
/* Backdrop (halbtransparent + blur) */
.w3-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(2px);
}
/* Sheet-Panel (weißer Boden, abgerundete Oberkante) */
.w3-sheet-panel {
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);
}
/* Sheet-Panel mit Scroll (für lange Inhalte) */
.w3-sheet-panel--scroll {
max-height: 82vh;
overflow-y: auto;
}
/* Sheet-Header-Zeile (Titel links, Button rechts) */
.w3-sheet-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.w3-sheet-header--mb20 {
margin-bottom: 20px;
}
/* Sheet-Titel */
.w3-sheet-title {
font-size: var(--text-base);
font-weight: 700;
}
/* Runder Schließen-Button (28px) */
.w3-close-btn {
background: var(--c-border);
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* Runder Schließen-Button (32px — größere Variante) */
.w3-close-btn--lg {
background: var(--c-border);
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* FAB-Options-Button (Zeilen-Stil, flex, card-bg) */
.w3-fab-option {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
background: var(--c-bg-card, var(--c-surface));
border: 1px solid var(--c-border);
border-radius: 14px;
padding: 14px 16px;
cursor: pointer;
text-align: left;
transition: background 0.12s;
}
/* Icon-Dot (runder farbiger Container für ph-icons) */
.w3-icon-dot {
width: 40px;
height: 40px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* Icon-Dot groß (44px) */
.w3-icon-dot--lg {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* Chip-Button (Grid-Spalten, vertikal gestapelt) */
.w3-chip-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
background: var(--c-bg-card, var(--c-surface));
border: 1px solid var(--c-border);
border-radius: 14px;
padding: 12px 6px;
cursor: pointer;
transition: background 0.12s;
}
/* Chip-Button Label */
.w3-chip-label {
font-size: 10px;
font-weight: 600;
color: var(--c-text);
text-align: center;
line-height: 1.2;
word-break: break-word;
}
/* Vertikal gestapelte Button-Gruppe (Modal-Footer) */
.w3-btn-stack {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 100%;
}
/* Sektion-Label (uppercase, klein, gedämpft) */
.w3-section-label {
font-size: var(--text-xs);
font-weight: 700;
color: var(--c-text-secondary);
letter-spacing: 0.08em;
text-transform: uppercase;
margin-bottom: 10px;
}
/* Schnell-Gassi Dauer-Button */
.w3-dur-btn {
padding: 12px 6px;
border-radius: 12px;
border: 2px solid var(--c-border);
background: var(--c-bg-card, var(--c-surface));
cursor: pointer;
font-weight: 700;
font-size: var(--text-sm);
color: var(--c-text);
}
.w3-dur-btn.active {
border-color: var(--c-primary);
background: var(--c-primary-subtle);
color: var(--c-primary);
}
/* Submit-Button (volle Breite, primary, flex-center) */
.w3-submit-btn {
width: 100%;
padding: 16px;
border-radius: 14px;
background: var(--c-primary);
color: white;
border: none;
cursor: pointer;
font-size: var(--text-base);
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
/* "Weitere Funktionen"-Link-Button */
.w3-all-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 12px;
border: none;
background: none;
cursor: pointer;
color: var(--c-primary);
font-size: var(--text-sm);
font-weight: 600;
}
/* ----------------------------------------------------------
Settings / Dog-Profile: Card-Sektion-Header
(uppercase Label mit Border-Bottom)
---------------------------------------------------------- */
.by-card-section-header {
padding: var(--space-3) var(--space-4);
font-size: var(--text-xs);
font-weight: 600;
color: var(--c-text-secondary);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--c-border);
}
/* ----------------------------------------------------------
Dog-Profile: Info-Card-Zeile (xs-Label + Wert)
---------------------------------------------------------- */
.dp-info-label {
font-size: var(--text-xs);
color: var(--c-text-secondary);
margin-bottom: 2px;
}
/* Passport: Datensatz-Zeile */
.pp-data-row {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3) 0;
border-bottom: 1px solid var(--c-border);
}
/* Settings: Sidebar-Item mit Padding */
.settings-sidebar-item {
padding: var(--space-4);
border-radius: 0;
border-bottom: 1px solid var(--c-border);
}
/* Settings: Toggle-Zeile (flex, icon + label + toggle) */
.settings-toggle-row {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4);
border-bottom: 1px solid var(--c-border);
}
/* Settings: Toggle-Label-Block */
.settings-toggle-label {
flex: 1;
}
/* Settings: Inline-Toggle (44×24px) */
.by-toggle-wrap {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
flex-shrink: 0;
}
.by-toggle-wrap input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.by-toggle-track {
position: absolute;
cursor: pointer;
inset: 0;
border-radius: 12px;
background: var(--c-border);
transition: background 0.2s;
}
.by-toggle-thumb {
position: absolute;
top: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: #fff;
transition: left 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
/* Settings: Version-Badge */
.by-version-badge {
background: var(--c-surface-2);
border: 1px solid var(--c-border);
border-radius: 100px;
padding: 2px 10px;
font-family: monospace;
font-size: 10px;
color: var(--c-text-muted);
}
/* Avatar-Kreis (56px, primary bg) */
.by-avatar-circle {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--c-primary);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
flex-shrink: 0;
cursor: pointer;
overflow: hidden;
position: relative;
}

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=760"></script> <script src="/js/worlds.js?v=774"></script>
<!-- Feature-Seiten werden lazy geladen --> <!-- Feature-Seiten werden lazy geladen -->
@ -640,7 +640,7 @@
// Wenn neuer SW die Kontrolle übernimmt → Seite neu laden // Wenn neuer SW die Kontrolle übernimmt → Seite neu laden
navigator.serviceWorker.addEventListener('controllerchange', () => { navigator.serviceWorker.addEventListener('controllerchange', () => {
window.location.reload(); window.location.replace('/?_t=' + Date.now());
}); });
navigator.serviceWorker.addEventListener('message', e => { navigator.serviceWorker.addEventListener('message', e => {

View file

@ -3,9 +3,11 @@
Router, State-Management, Navigation, Initialisierung. Router, State-Management, Navigation, Initialisierung.
============================================================ */ ============================================================ */
const APP_VER = '760'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VER = '774'; // ← 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';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
if (location.search.includes('_t=')) history.replaceState(null, '', '/');
const App = (() => { const App = (() => {
@ -1037,15 +1039,15 @@ const App = (() => {
btn.textContent = 'Lädt…'; btn.textContent = 'Lädt…';
btn.disabled = true; btn.disabled = true;
sessionStorage.setItem('by_update_reload', APP_VER); sessionStorage.setItem('by_update_reload', APP_VER);
// ?_t= Timestamp zwingt iOS bfcache zur Aufgabe — wird beim Start sofort entfernt
setTimeout(() => location.replace('/?_t=' + Date.now()), 800);
try { try {
// SW aktivieren + alle Caches leeren für sauberen Reload
const reg = await navigator.serviceWorker?.getRegistration(); const reg = await navigator.serviceWorker?.getRegistration();
if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' }); if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' });
await reg?.update(); reg?.update().catch(() => {}); // kein await — kann hängen
const keys = await caches.keys(); const keys = await caches.keys();
await Promise.all(keys.map(k => caches.delete(k))); await Promise.all(keys.map(k => caches.delete(k)));
} catch { /* ignorieren */ } } catch { /* ignorieren */ }
setTimeout(() => location.reload(), 600);
}); });
} }
@ -1090,6 +1092,7 @@ const App = (() => {
} }
return { init, navigate, callModule, state, setActiveDog, return { init, navigate, callModule, state, setActiveDog,
hasPro: (user) => _hasPro(user ?? state.user),
renderDogSwitcher: _renderDogSwitcher, renderDogSwitcher: _renderDogSwitcher,
getInstallPrompt: () => _installPrompt, requireAuth, getInstallPrompt: () => _installPrompt, requireAuth,
showOnboarding: _showOnboardingModal, showOnboarding: _showOnboardingModal,

View file

@ -108,8 +108,7 @@ window.Page_dog_profile = (() => {
margin-bottom:var(--space-5);text-align:left"> margin-bottom:var(--space-5);text-align:left">
${geburtstag ? ` ${geburtstag ? `
<div class="card" style="padding:var(--space-3)"> <div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div class="dp-info-label"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg> Geburtstag</div>
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg> Geburtstag</div>
<div style="font-weight:500;font-size:var(--text-sm)">${geburtstag}</div> <div style="font-weight:500;font-size:var(--text-sm)">${geburtstag}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
${_calcAlter(dog.geburtstag)} ${_calcAlter(dog.geburtstag)}
@ -118,8 +117,7 @@ window.Page_dog_profile = (() => {
` : ''} ` : ''}
${dog.geschlecht ? ` ${dog.geschlecht ? `
<div class="card" style="padding:var(--space-3)"> <div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div class="dp-info-label">${dog.geschlecht === 'm' ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-male"></use></svg>' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>'} Geschlecht</div>
margin-bottom:2px">${dog.geschlecht === 'm' ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-male"></use></svg>' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>'} Geschlecht</div>
<div style="font-weight:500;font-size:var(--text-sm)"> <div style="font-weight:500;font-size:var(--text-sm)">
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'} ${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
</div> </div>
@ -127,8 +125,7 @@ window.Page_dog_profile = (() => {
` : ''} ` : ''}
${dog.gewicht_kg ? ` ${dog.gewicht_kg ? `
<div class="card" style="padding:var(--space-3);cursor:pointer" data-action="goto-weight"> <div class="card" style="padding:var(--space-3);cursor:pointer" data-action="goto-weight">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary); <div class="dp-info-label"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div> <div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
</div> </div>
` : ''} ` : ''}
@ -202,7 +199,7 @@ window.Page_dog_profile = (() => {
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#identification-card"></use></svg>
Visitenkarte teilen Visitenkarte teilen
</button>` : ''} </button>` : ''}
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-add-dog-btn"> ${!dog.is_guest && App.hasPro(_appState.user) ? `<button class="btn btn-secondary w-full" id="dp-add-dog-btn">
+ Weiteren Hund anlegen + Weiteren Hund anlegen
</button>` : ''} </button>` : ''}
${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-wrapped-btn" ${!dog.is_guest ? `<button class="btn btn-secondary w-full" id="dp-wrapped-btn"
@ -521,9 +518,7 @@ window.Page_dog_profile = (() => {
let activeHtml = ''; let activeHtml = '';
if (active.length) { if (active.length) {
activeHtml = active.map(s => ` activeHtml = active.map(s => `
<div style="display:flex;align-items:center;gap:var(--space-2); <div style="display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--c-surface-2);border-radius:var(--radius-md);margin-bottom:var(--space-2)">
padding:var(--space-2) var(--space-3);background:var(--c-surface-2);
border-radius:var(--radius-md);margin-bottom:var(--space-2)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user"></use></svg>
<div style="flex:1;font-size:var(--text-sm)"> <div style="flex:1;font-size:var(--text-sm)">
<strong>${_esc(s.sitter_name)}</strong> <strong>${_esc(s.sitter_name)}</strong>
@ -619,7 +614,7 @@ window.Page_dog_profile = (() => {
value="${_esc(dog.chip_nr || '')}" placeholder="z.B. 276009200123456" maxlength="20"> value="${_esc(dog.chip_nr || '')}" placeholder="z.B. 276009200123456" maxlength="20">
</div>`, </div>`,
footer: ` footer: `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
<button class="btn btn-primary" id="chip-edit-save-btn" style="width:100%">Speichern</button> <button class="btn btn-primary" id="chip-edit-save-btn" style="width:100%">Speichern</button>
<button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button> <button class="btn btn-secondary" onclick="UI.modal.close()">Abbrechen</button>
</div>`, </div>`,
@ -676,7 +671,7 @@ window.Page_dog_profile = (() => {
`; `;
const footer = ` const footer = `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn" style="width:100%">Speichern</button>` : ''} ${hasPhoto ? `<button class="btn btn-primary" id="pe-save-btn" style="width:100%">Speichern</button>` : ''}
<div style="display:flex;gap:var(--space-2)"> <div style="display:flex;gap:var(--space-2)">
${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''} ${hasPhoto ? `<button class="btn btn-danger" id="pe-delete-btn">${UI.icon('trash')} Löschen</button>` : ''}
@ -1044,7 +1039,7 @@ window.Page_dog_profile = (() => {
title: 'Weiteren Hund anlegen', title: 'Weiteren Hund anlegen',
body: _formHTML(null, true), body: _formHTML(null, true),
footer: ` footer: `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
<button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">${UI.icon('dog')} Hund anlegen</button> <button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">${UI.icon('dog')} Hund anlegen</button>
<button type="button" class="btn btn-secondary" id="dp-form-cancel">Abbrechen</button> <button type="button" class="btn btn-secondary" id="dp-form-cancel">Abbrechen</button>
</div> </div>
@ -1061,7 +1056,7 @@ window.Page_dog_profile = (() => {
title: `${dog.name} bearbeiten`, title: `${dog.name} bearbeiten`,
body: _formHTML(dog, true), body: _formHTML(dog, true),
footer: ` footer: `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
<button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">Speichern</button> <button type="submit" form="dp-form" class="btn btn-primary" style="width:100%">Speichern</button>
<div style="display:flex;gap:var(--space-2)"> <div style="display:flex;gap:var(--space-2)">
<button type="button" class="btn btn-danger" id="dp-delete-btn">Löschen</button> <button type="button" class="btn btn-danger" id="dp-delete-btn">Löschen</button>
@ -1657,8 +1652,7 @@ window.Page_dog_profile = (() => {
</div>` </div>`
: vaccs.map(v => ` : vaccs.map(v => `
<div class="pp-vacc-row" data-id="${v.id}" <div class="pp-vacc-row" data-id="${v.id}"
style="display:flex;align-items:flex-start;gap:var(--space-3); class="pp-data-row">
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border)">
<div style="flex:1"> <div style="flex:1">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(v.krankheit)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${_esc(v.krankheit)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
@ -1695,8 +1689,7 @@ window.Page_dog_profile = (() => {
</div>` </div>`
: meds.map(m => ` : meds.map(m => `
<div class="pp-med-row" data-id="${m.id}" <div class="pp-med-row" data-id="${m.id}"
style="display:flex;align-items:flex-start;gap:var(--space-3); class="pp-data-row">
padding:var(--space-3) 0;border-bottom:1px solid var(--c-border)">
<div style="flex:1"> <div style="flex:1">
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(m.name)}</div> <div style="font-weight:600;font-size:var(--text-sm)">${_esc(m.name)}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
@ -2073,8 +2066,8 @@ window.Page_dog_profile = (() => {
const modalEl = document.createElement('div'); const modalEl = document.createElement('div');
modalEl.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0d0d1a;display:flex;flex-direction:column;overflow:hidden;'; modalEl.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0d0d1a;display:flex;flex-direction:column;overflow:hidden;';
modalEl.innerHTML = ` modalEl.innerHTML = `
<div style="display:flex;justify-content:flex-end;padding:16px 20px 0"> <div style="display:flex;justify-content:flex-end;padding:max(16px,env(safe-area-inset-top)) 20px 0">
<button id="dp-wrapped-close" style="background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:36px;height:36px;font-size:1.2rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button> <button id="dp-wrapped-close" style="background:rgba(255,255,255,0.1);border:none;border-radius:50%;width:44px;height:44px;font-size:1.4rem;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center">×</button>
</div> </div>
<div style="flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative"> <div style="flex:1;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative">
<div id="dp-wrapped-card-container" style="width:100%;max-width:400px;color:#fff;">${cards[0]}</div> <div id="dp-wrapped-card-container" style="width:100%;max-width:400px;color:#fff;">${cards[0]}</div>

View file

@ -120,12 +120,7 @@ window.Page_settings = (() => {
<div class="card" style="padding:var(--space-5);margin-bottom:var(--space-4)"> <div class="card" style="padding:var(--space-5);margin-bottom:var(--space-4)">
<div style="display:flex;align-items:center;gap:var(--space-4)"> <div style="display:flex;align-items:center;gap:var(--space-4)">
<div id="settings-avatar-btn" <div id="settings-avatar-btn" class="by-avatar-circle">
style="width:56px;height:56px;border-radius:50%;
background:var(--c-primary);color:#fff;
display:flex;align-items:center;justify-content:center;
font-size:1.5rem;font-weight:700;flex-shrink:0;
cursor:pointer;overflow:hidden;position:relative">
${avatarInner} ${avatarInner}
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.25); <div style="position:absolute;inset:0;background:rgba(0,0,0,0.25);
display:flex;align-items:center;justify-content:center; display:flex;align-items:center;justify-content:center;
@ -223,9 +218,7 @@ window.Page_settings = (() => {
</div> </div>
<div class="card" id="settings-stats-card" style="margin-bottom:var(--space-4)"> <div class="card" id="settings-stats-card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:600; <div class="by-card-section-header">Aktivität</div>
color:var(--c-text-secondary);text-transform:uppercase;letter-spacing:0.05em;
border-bottom:1px solid var(--c-border)">Aktivität</div>
<div id="settings-stats-body" style="padding:var(--space-4);display:flex;justify-content:space-around"> <div id="settings-stats-body" style="padding:var(--space-4);display:flex;justify-content:space-around">
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</div> <div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</div>
</div> </div>
@ -238,9 +231,7 @@ window.Page_settings = (() => {
<div id="breeder-card-slot"></div> <div id="breeder-card-slot"></div>
<div class="card" style="margin-bottom:var(--space-4)"> <div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4);font-size:var(--text-xs);font-weight:600; <div class="by-card-section-header">Trophäen</div>
color:var(--c-text-secondary);text-transform:uppercase;letter-spacing:0.05em;
border-bottom:1px solid var(--c-border)">Trophäen</div>
<div id="settings-badges-body" style="padding:var(--space-4)"> <div id="settings-badges-body" style="padding:var(--space-4)">
<div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</div> <div style="color:var(--c-text-muted);font-size:var(--text-sm)">Lädt</div>
</div> </div>
@ -285,29 +276,31 @@ window.Page_settings = (() => {
<strong>Ban Yaro Pro</strong> kommt bald mehr Features, mehrere Hunde. <strong>Ban Yaro Pro</strong> kommt bald mehr Features, mehrere Hunde.
</div> </div>
` : ''} ` : ''}
<div class="sidebar-item" id="settings-logout-btn" <div style="padding:var(--space-3) var(--space-4);border-top:1px solid var(--c-border)">
style="padding:var(--space-4);border-radius:0;cursor:pointer; <button id="settings-logout-btn"
color:var(--c-danger)"> style="width:100%;display:flex;align-items:center;justify-content:center;
gap:var(--space-2);padding:var(--space-3) var(--space-4);
border-radius:var(--radius-md);border:1.5px solid var(--c-danger);
background:transparent;color:var(--c-danger);
font-size:var(--text-sm);font-weight:600;cursor:pointer;
transition:background 0.15s">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sign-out"></use></svg> <svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#sign-out"></use></svg>
<span>Abmelden</span> Abmelden
</button>
</div> </div>
</div> </div>
</div> </div>
<div class="card" style="margin-bottom:var(--space-4)"> <div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4); <div class="by-card-section-header">
font-size:var(--text-xs);font-weight:600;
color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:0.05em;border-bottom:1px solid var(--c-border)">
App-Einstellungen App-Einstellungen
</div> </div>
<div class="card-body" style="padding:0"> <div class="card-body" style="padding:0">
<!-- Dark-Mode-Auswahl --> <!-- Dark-Mode-Auswahl -->
<div style="display:flex;align-items:center;gap:var(--space-3); <div class="settings-toggle-row">
padding:var(--space-4);border-bottom:1px solid var(--c-border)">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#moon"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#moon"></use></svg>
<div style="flex:1"> <div class="settings-toggle-label">
<div style="font-weight:500">Dark Mode</div> <div style="font-weight:500">Dark Mode</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Erscheinungsbild der App Erscheinungsbild der App
@ -329,9 +322,9 @@ window.Page_settings = (() => {
</div> </div>
<!-- KI-Notiz-Assistent --> <!-- KI-Notiz-Assistent -->
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4);border-bottom:1px solid var(--c-border)"> <div class="settings-toggle-row">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#brain"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#brain"></use></svg>
<div style="flex:1"> <div class="settings-toggle-label">
<div style="font-weight:500">KI-Notiz-Assistent</div> <div style="font-weight:500">KI-Notiz-Assistent</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Erkennt Muster in deinen Notizen und macht Vorschläge Erkennt Muster in deinen Notizen und macht Vorschläge
@ -353,9 +346,9 @@ window.Page_settings = (() => {
</div> </div>
<!-- Goldene Gassi-Stunde --> <!-- Goldene Gassi-Stunde -->
<div style="display:flex;align-items:center;gap:var(--space-3);padding:var(--space-4)"> <div class="settings-toggle-row" style="border-bottom:none">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#paw-print"></use></svg> <svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#paw-print"></use></svg>
<div style="flex:1"> <div class="settings-toggle-label">
<div style="font-weight:500">Goldene Gassi-Stunde täglich</div> <div style="font-weight:500">Goldene Gassi-Stunde täglich</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
Täglich um 07:00 Uhr: bestes Wetterfenster für den Gassi-Gang Täglich um 07:00 Uhr: bestes Wetterfenster für den Gassi-Gang
@ -392,10 +385,7 @@ window.Page_settings = (() => {
<!-- App installieren --> <!-- App installieren -->
<div class="card" style="margin-bottom:var(--space-4)"> <div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4); <div class="by-card-section-header">
font-size:var(--text-xs);font-weight:600;
color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:0.05em;border-bottom:1px solid var(--c-border)">
App installieren App installieren
</div> </div>
<div class="card-body" style="padding:0"> <div class="card-body" style="padding:0">
@ -711,7 +701,7 @@ window.Page_settings = (() => {
</form> </form>
`, `,
footer: ` footer: `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
<button type="submit" form="profile-form" class="btn btn-primary" style="width:100%">Speichern</button> <button type="submit" form="profile-form" class="btn btn-primary" style="width:100%">Speichern</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button> <button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>
</div> </div>
@ -747,27 +737,24 @@ window.Page_settings = (() => {
} }
if (btn) btn.textContent = 'Prüfe…'; if (btn) btn.textContent = 'Prüfe…';
try { try {
// Aktuelle Version vom Server holen (no-cache) // Versionsnummer direkt vom API-Endpunkt holen
const serverResp = await fetch('/js/app.js', { cache: 'no-store' }); const r = await fetch('/api/version', { cache: 'no-store' });
const serverText = await serverResp.text(); const { version: serverVersion } = await r.json();
const match = serverText.match(/APP_VERSION\s*=\s*'([^']+)'/); const localVersion = typeof APP_VER !== 'undefined' ? APP_VER : '0';
const serverVersion = match?.[1] || null;
const localVersion = typeof APP_VERSION !== 'undefined' ? APP_VERSION : '0';
// SW update anstoßen
const reg = await navigator.serviceWorker.getRegistration(); const reg = await navigator.serviceWorker.getRegistration();
await reg?.update(); reg?.update().catch(() => {}); // kein await — kann hängen
if (serverVersion && serverVersion !== localVersion) { if (serverVersion && serverVersion !== localVersion) {
// Neuere Version verfügbar — Seite neu laden
if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' }); if (reg?.waiting) reg.waiting.postMessage({ type: 'SKIP_WAITING' });
UI.toast.info(`Update auf v${serverVersion} verfügbar — Seite wird neu geladen…`); UI.toast.info(`Update auf v${serverVersion} — Seite wird neu geladen…`);
setTimeout(() => location.reload(), 1500); setTimeout(() => location.replace('/?_t=' + Date.now()), 1500);
} else if (reg?.waiting) { } else if (reg?.waiting) {
reg.waiting.postMessage({ type: 'SKIP_WAITING' }); reg.waiting.postMessage({ type: 'SKIP_WAITING' });
UI.toast.success('Update wird installiert…'); UI.toast.success('Update wird installiert…');
setTimeout(() => location.replace('/?_t=' + Date.now()), 1500);
} else { } else {
UI.toast.success(`Ban Yaro ist aktuell — v${localVersion}`); UI.toast.success(`Ban Yaro ist aktuell — Build ${localVersion}`);
} }
} catch { } catch {
UI.toast.error('Update-Prüfung fehlgeschlagen.'); UI.toast.error('Update-Prüfung fehlgeschlagen.');
@ -1039,12 +1026,7 @@ window.Page_settings = (() => {
slot.innerHTML = ` slot.innerHTML = `
<div class="card" style="margin-bottom:var(--space-4)"> <div class="card" style="margin-bottom:var(--space-4)">
<div style="padding:var(--space-3) var(--space-4); <div class="by-card-section-header">Züchter-Profil</div>
font-size:var(--text-xs);font-weight:600;
color:var(--c-text-secondary);text-transform:uppercase;
letter-spacing:0.05em;border-bottom:1px solid var(--c-border)">
Züchter-Profil
</div>
<div style="padding:var(--space-4)"> <div style="padding:var(--space-4)">
${statusBadge} ${statusBadge}
${actionBlock} ${actionBlock}
@ -1266,7 +1248,7 @@ window.Page_settings = (() => {
</form> </form>
`, `,
footer: ` footer: `
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%"> <div class="w3-btn-stack">
<button type="submit" form="breeder-apply-form" class="btn btn-primary" id="breeder-apply-submit" <button type="submit" form="breeder-apply-form" class="btn btn-primary" id="breeder-apply-submit"
style="width:100%">Antrag einreichen</button> style="width:100%">Antrag einreichen</button>
<button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button> <button type="button" class="btn btn-secondary" data-modal-close>Abbrechen</button>

View file

@ -450,7 +450,7 @@ window.Page_wetter = (() => {
</span> </span>
<div style="flex:1"> <div style="flex:1">
<div style="font-size:var(--text-sm);font-weight:600"> <div style="font-size:var(--text-sm);font-weight:600">
${_esc(compass)} · ${Math.round(d.windspeed_max ?? 0)} km/h ${_esc(compass)} · ${Math.round(d.wind_kmh ?? 0)} km/h
</div> </div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(bft)}</div> <div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(bft)}</div>
</div> </div>
@ -803,7 +803,7 @@ window.Page_wetter = (() => {
let score = 10; let score = 10;
const temp = d.temp_max ?? 20; const temp = d.temp_max ?? 20;
const precip = d.precip_prob ?? 0; const precip = d.precip_prob ?? 0;
const wind = d.windspeed_max ?? 0; const wind = d.wind_kmh ?? 0;
const asphalt = d.asphalt_temp ?? 0; const asphalt = d.asphalt_temp ?? 0;
// Temperatur (ideal: 1020°C) // Temperatur (ideal: 1020°C)
@ -998,7 +998,7 @@ window.Page_wetter = (() => {
const temp = d.temp_max ?? 20; const temp = d.temp_max ?? 20;
const tempMin = d.temp_min ?? temp; const tempMin = d.temp_min ?? temp;
const precip = d.precip_prob ?? 0; const precip = d.precip_prob ?? 0;
const wind = d.windspeed_max ?? 0; const wind = d.wind_kmh ?? 0;
const asphalt = d.asphalt_temp ?? 0; const asphalt = d.asphalt_temp ?? 0;
const wcode = d.weathercode ?? 0; const wcode = d.weathercode ?? 0;
const isSnow = wcode >= 71 && wcode <= 77; const isSnow = wcode >= 71 && wcode <= 77;

View file

@ -274,28 +274,20 @@ 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:460;display:flex;flex-direction:column;justify-content:flex-end'; ov.className = 'w3-sheet-overlay';
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" class="w3-backdrop"></div>
<div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0; <div class="w3-sheet-panel">
padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px); <div class="w3-sheet-header">
box-shadow:0 -8px 32px rgba(0,0,0,0.2)"> <div class="w3-sheet-title">${options.length ? title : 'Schnellzugriff'}</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px"> <button id="fab-close" class="w3-close-btn">
<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%;
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>
</button> </button>
</div> </div>
<div style="display:flex;flex-direction:column;gap:10px"> <div style="display:flex;flex-direction:column;gap:10px">
${options.map(o => ` ${options.map(o => `
<button class="fab-option" data-page="${o.page}" data-action="${o.action || ''}" <button class="fab-option w3-fab-option" data-page="${o.page}" data-action="${o.action || ''}">
style="display:flex;align-items:center;gap:14px;width:100%; <div class="w3-icon-dot" style="background:${o.color}18">
background:var(--c-bg-card);border:1px solid var(--c-border);
border-radius:14px;padding:14px 16px;cursor:pointer;text-align:left;
transition:background .12s">
<div style="width:40px;height:40px;border-radius:12px;background:${o.color}18;
display:flex;align-items:center;justify-content:center;flex-shrink:0">
<svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:${o.color}"> <svg class="ph-icon" style="width:1.2rem;height:1.2rem;color:${o.color}">
<use href="/icons/phosphor.svg#${o.icon}"></use> <use href="/icons/phosphor.svg#${o.icon}"></use>
</svg> </svg>
@ -307,9 +299,7 @@ window.Worlds = (() => {
</button> </button>
`).join('')} `).join('')}
</div> </div>
<button id="fab-all-btn" style="display:flex;align-items:center;justify-content:center;gap:8px; <button id="fab-all-btn" class="w3-all-btn" style="margin-top:${options.length ? '14px' : '0'}">
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> <svg class="ph-icon" style="width:16px;height:16px"><use href="/icons/phosphor.svg#squares-four"></use></svg>
Weitere Funktionen Weitere Funktionen
</button> </button>
@ -353,20 +343,14 @@ window.Worlds = (() => {
if (!chips.length) return ''; if (!chips.length) return '';
return ` return `
<div style="margin-bottom:20px"> <div style="margin-bottom:20px">
<div style="font-size:var(--text-xs);font-weight:700;color:var(--c-text-secondary); <div class="w3-section-label">${worldLabels[w]}</div>
letter-spacing:0.08em;margin-bottom:10px">${worldLabels[w]}</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px"> <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
${chips.map(c => ` ${chips.map(c => `
<button class="all-chip-btn" data-page="${c.page}" <button class="all-chip-btn w3-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)"> <svg class="ph-icon" style="width:1.4rem;height:1.4rem;color:var(--c-primary)">
<use href="/icons/phosphor.svg#${c.icon}"></use> <use href="/icons/phosphor.svg#${c.icon}"></use>
</svg> </svg>
<span style="font-size:10px;font-weight:600;color:var(--c-text);text-align:center; <span class="w3-chip-label">${c.label}</span>
line-height:1.2;word-break:break-word">${c.label}</span>
</button> </button>
`).join('')} `).join('')}
</div> </div>
@ -376,16 +360,13 @@ 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:460;display:flex;flex-direction:column;justify-content:flex-end'; ov.className = 'w3-sheet-overlay';
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" class="w3-backdrop"></div>
<div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0; <div class="w3-sheet-panel w3-sheet-panel--scroll">
padding:20px 16px calc(env(safe-area-inset-bottom,16px) + 16px); <div class="w3-sheet-header w3-sheet-header--mb20">
box-shadow:0 -8px 32px rgba(0,0,0,0.2);max-height:82vh;overflow-y:auto"> <div class="w3-sheet-title">Ausgeblendete Funktionen</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px"> <button id="fab-close" class="w3-close-btn">
<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> <svg class="ph-icon" style="width:14px;height:14px"><use href="/icons/phosphor.svg#x"></use></svg>
</button> </button>
</div> </div>
@ -430,7 +411,8 @@ window.Worlds = (() => {
const ov = document.createElement('div'); const ov = document.createElement('div');
ov.id = 'quick-gassi-overlay'; ov.id = 'quick-gassi-overlay';
ov.style.cssText = 'position:fixed;inset:0;z-index:400;display:flex;flex-direction:column;justify-content:flex-end'; ov.className = 'w3-sheet-overlay';
ov.style.zIndex = '400';
const weatherLine = weatherData const weatherLine = weatherData
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:6px"> ? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:6px">
@ -438,20 +420,17 @@ window.Worlds = (() => {
</div>` : ''; </div>` : '';
ov.innerHTML = ` ov.innerHTML = `
<div id="qg-backdrop" style="position:absolute;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(3px)"></div> <div id="qg-backdrop" class="w3-backdrop" style="background:rgba(0,0,0,0.6);backdrop-filter:blur(3px)"></div>
<div style="position:relative;z-index:1;background:var(--c-bg);border-radius:24px 24px 0 0; <div class="w3-sheet-panel" style="padding:24px 16px calc(env(safe-area-inset-bottom,16px) + 20px)">
padding:24px 16px calc(env(safe-area-inset-bottom,16px) + 20px); <div class="w3-sheet-header w3-sheet-header--mb20">
box-shadow:0 -8px 32px rgba(0,0,0,0.25)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px">
<div> <div>
<div style="font-size:var(--text-base);font-weight:700">🐾 Schnell-Gassi</div> <div class="w3-sheet-title">🐾 Schnell-Gassi</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px"> <div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
${_esc(dog.name)} · ohne GPS ${_esc(dog.name)} · ohne GPS
</div> </div>
${weatherLine} ${weatherLine}
</div> </div>
<button id="qg-close" style="background:var(--c-border);border:none;border-radius:50%; <button id="qg-close" class="w3-close-btn--lg">
width:32px;height:32px;cursor:pointer;display:flex;align-items:center;justify-content:center">
<svg class="ph-icon" style="width:16px;height:16px"><use href="/icons/phosphor.svg#x"></use></svg> <svg class="ph-icon" style="width:16px;height:16px"><use href="/icons/phosphor.svg#x"></use></svg>
</button> </button>
</div> </div>
@ -459,20 +438,13 @@ window.Worlds = (() => {
<div style="font-size:var(--text-sm);font-weight:600;margin-bottom:10px">Dauer</div> <div style="font-size:var(--text-sm);font-weight:600;margin-bottom:10px">Dauer</div>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:24px"> <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:24px">
${durations.map(d => ` ${durations.map(d => `
<button class="qg-dur" data-min="${d}" <button class="qg-dur w3-dur-btn${d === selectedMin ? ' active' : ''}" data-min="${d}">
style="padding:12px 6px;border-radius:12px;border:2px solid ${d === selectedMin ? 'var(--c-primary)' : 'var(--c-border)'};
background:${d === selectedMin ? 'var(--c-primary-subtle)' : 'var(--c-bg-card)'};
cursor:pointer;font-weight:700;font-size:var(--text-sm);
color:${d === selectedMin ? 'var(--c-primary)' : 'var(--c-text)'}">
${d} min ${d} min
</button> </button>
`).join('')} `).join('')}
</div> </div>
<button id="qg-submit" style="width:100%;padding:16px;border-radius:14px; <button id="qg-submit" class="w3-submit-btn">
background:var(--c-primary);color:white;border:none;cursor:pointer;
font-size:var(--text-base);font-weight:700;
display:flex;align-items:center;justify-content:center;gap:8px">
<svg class="ph-icon" style="width:1.2rem;height:1.2rem"><use href="/icons/phosphor.svg#check"></use></svg> <svg class="ph-icon" style="width:1.2rem;height:1.2rem"><use href="/icons/phosphor.svg#check"></use></svg>
Eintragen Eintragen
</button> </button>
@ -490,10 +462,7 @@ window.Worlds = (() => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
selectedMin = parseInt(btn.dataset.min); selectedMin = parseInt(btn.dataset.min);
ov.querySelectorAll('.qg-dur').forEach(b => { ov.querySelectorAll('.qg-dur').forEach(b => {
const active = parseInt(b.dataset.min) === selectedMin; b.classList.toggle('active', parseInt(b.dataset.min) === selectedMin);
b.style.borderColor = active ? 'var(--c-primary)' : 'var(--c-border)';
b.style.background = active ? 'var(--c-primary-subtle)' : 'var(--c-bg-card)';
b.style.color = active ? 'var(--c-primary)' : 'var(--c-text)';
}); });
}); });
}); });
@ -505,15 +474,8 @@ window.Worlds = (() => {
submitBtn.textContent = 'Wird eingetragen…'; submitBtn.textContent = 'Wird eingetragen…';
try { try {
const payload = { // Kein Tagebucheintrag — nur Streak pingen
typ: 'gassi', await API.post(`/streak/${dog.id}/ping`);
titel: 'Schnell-Gassi 🐾',
text: `Kurze Runde, ${selectedMin} Minuten`,
};
if (weatherData) {
payload.weather_json = JSON.stringify(weatherData);
}
await API.post(`/dogs/${dog.id}/diary`, payload);
_close(); _close();
UI.toast?.success(`Gassi eingetragen! ${selectedMin} min 🐾`); UI.toast?.success(`Gassi eingetragen! ${selectedMin} min 🐾`);
// Streak-Cache invalidieren // Streak-Cache invalidieren
@ -1103,7 +1065,7 @@ window.Worlds = (() => {
${alertHtml} ${alertHtml}
${user && dog ? ` ${user && dog ? `
<div class="wj-chip-row"> <div class="wj-chip-row">
<div class="wj-chip" data-wnav="wetter" style="${gassiScore ? `border-color:${gassiColor}44;background:${gassiColor}12;` : ''}"> <div class="wj-chip" data-wnav="wetter">
<div style="display:flex;align-items:center;gap:6px;width:100%"> <div style="display:flex;align-items:center;gap:6px;width:100%">
<span style="font-size:1.4rem;line-height:1;flex-shrink:0">${weatherEmoji}</span> <span style="font-size:1.4rem;line-height:1;flex-shrink:0">${weatherEmoji}</span>
<div style="flex:1;min-width:0"> <div style="flex:1;min-width:0">
@ -1112,7 +1074,7 @@ window.Worlds = (() => {
<span style="font-size:1.25rem;font-weight:800;color:${gassiColor};line-height:1">${gassiScore ?? '—'}</span> <span style="font-size:1.25rem;font-weight:800;color:${gassiColor};line-height:1">${gassiScore ?? '—'}</span>
${gassiScore ? `<span style="font-size:var(--text-xs);color:rgba(255,255,255,0.4);font-weight:600">/10</span>` : ''} ${gassiScore ? `<span style="font-size:var(--text-xs);color:rgba(255,255,255,0.4);font-weight:600">/10</span>` : ''}
</div> </div>
${w ? `<div style="font-size:9px;color:rgba(255,255,255,0.5);margin-top:1px">${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</div>` : ''} ${w ? `<span style="font-size:9px;color:rgba(255,255,255,0.75);font-weight:500;margin-top:1px;white-space:nowrap">${Math.round(w.temp_c ?? 0)}° · ${w.precip_prob ?? 0}% Regen</span>` : ''}
</div> </div>
</div> </div>
</div> </div>

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v760'; const CACHE_VERSION = 'by-v774';
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
@ -292,7 +292,8 @@ self.addEventListener('fetch', event => {
} }
return response; return response;
}) })
.catch(() => caches.match(event.request)) .catch(() => caches.match(event.request)
.then(cached => cached || new Response('', { status: 503 })))
); );
return; return;
} }
@ -324,9 +325,8 @@ self.addEventListener('fetch', event => {
}) })
) )
.catch(() => { .catch(() => {
if (event.request.mode === 'navigate') { if (event.request.mode === 'navigate') return caches.match('/');
return caches.match('/'); return new Response('', { status: 503 });
}
}) })
); );
}); });
@ -344,6 +344,10 @@ self.addEventListener('sync', event => {
// MESSAGE — Tile-Vorausladung (Offline-Speicherung) + Queue-Steuerung // MESSAGE — Tile-Vorausladung (Offline-Speicherung) + Queue-Steuerung
// ---------------------------------------------------------- // ----------------------------------------------------------
self.addEventListener('message', event => { self.addEventListener('message', event => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
return;
}
if (event.data?.type === 'PROCESS_QUEUE') { if (event.data?.type === 'PROCESS_QUEUE') {
event.waitUntil(_processQueue()); event.waitUntil(_processQueue());
return; return;