Compare commits

...

4 commits

Author SHA1 Message Date
0967623342 Hotfix: Routen-Filter oeffnete nicht — .hidden hat !important, style.display verlor immer
Kaputt seit 27a3f95 (2026-06-05, 'Filter standardmaessig zu' setzte die
hidden-KLASSE ins Markup, der Toggle blieb aber auf style.display='').
.hidden = display:none !important (design-system.css) -> Klick wirkte nie.
Import funktionierte weiter (Label+File-Input nativ) — dadurch gemeldet
als 'Filter geht nicht mehr' (Angie + Rene).
Fix: classList.toggle('hidden') im Toggle + konsistent im Suggest-Tab.
Bump v1242
2026-06-06 19:05:34 +02:00
ca23b3ec46 Navi-Sounds: 2x Wuff = links, 1x Wuff = rechts, Klaeffen = falscher Weg (Idee Rene)
- NavSound (routes.js): WebAudio-Wuff-Synthese (Saegezahn-Sweep + Tiefpass,
  kein Asset, laeuft offline); echte Aufnahmen unter /sounds/wuff.mp3 +
  klaeffen.mp3 (z.B. von Yaro) werden automatisch bevorzugt
- Abbiege-Erkennung: Turns einmalig aus dem Track (Peilung ueber 15-m-
  Stuetzpunkte gegen GPS-Zickzack, >=40 Grad), Ansage bei <=45 m davor,
  einmal pro Abbiegepunkt
- Falscher Weg (>50 m, bestehende Warnung): Klaeffen beim Abkommen +
  Erinnerung alle 30 s; zurueck auf Route reset
- Toggle im Navi-Header (Lautsprecher-Icon, by_nav_sound, Default AN),
  iOS-Audio-Unlock beim Navi-Start + Toggle (Probe-Wuff als Bestaetigung)
Bump v1241
2026-06-06 18:57:20 +02:00
a1b2644cce DWD-PoC: Binaer-Artefakte aus dem Repo (nur decode_poc.py bleibt) 2026-06-06 18:34:41 +02:00
502f0f4921 DWD-Compose: container_name entfernt — Staging+Prod teilen den Docker-Host (Namenskonflikt) 2026-06-06 18:33:05 +02:00
13 changed files with 164 additions and 4212 deletions

View file

@ -1 +1 @@
1240
1242

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1240"></script>
<script src="/js/boot-early.js?v=1242"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1240">
<link rel="stylesheet" href="/css/layout.css?v=1240">
<link rel="stylesheet" href="/css/components.css?v=1240">
<link rel="stylesheet" href="/css/utilities.css?v=1240">
<link rel="stylesheet" href="/css/lists.css?v=1240">
<link rel="stylesheet" href="/css/design-system.css?v=1242">
<link rel="stylesheet" href="/css/layout.css?v=1242">
<link rel="stylesheet" href="/css/components.css?v=1242">
<link rel="stylesheet" href="/css/utilities.css?v=1242">
<link rel="stylesheet" href="/css/lists.css?v=1242">
</head>
<body>
@ -612,11 +612,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1240"></script>
<script src="/js/ui.js?v=1240"></script>
<script src="/js/app.js?v=1240"></script>
<script src="/js/worlds.js?v=1240"></script>
<script src="/js/offline-indicator.js?v=1240"></script>
<script src="/js/api.js?v=1242"></script>
<script src="/js/ui.js?v=1242"></script>
<script src="/js/app.js?v=1242"></script>
<script src="/js/worlds.js?v=1242"></script>
<script src="/js/offline-indicator.js?v=1242"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -626,7 +626,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1240"></script>
<script src="/js/boot.js?v=1242"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1240'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1242'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -68,6 +68,81 @@ window.Page_routes = (() => {
let _navCompassHeading = null;
let _navHeadingSmoothed = null;
// ----------------------------------------------------------
// NAVI-SOUNDS (Idee René 2026-06-06): links = 2× Wuff, rechts = 1× Wuff,
// falscher Weg = Kläffen. WebAudio-Synthese (kein Asset, läuft offline) —
// liegen echte Aufnahmen unter /sounds/wuff.mp3 + /sounds/klaeffen.mp3
// (z.B. von Yaro 🐕), werden DIE bevorzugt. iOS: Audio braucht eine User-Geste
// → unlock() beim Navi-Start/Toggle.
// ----------------------------------------------------------
const NavSound = (() => {
let ctx = null;
let files = null; // { wuff, klaeffen } HTMLAudio | leer = Synthese
const enabled = () => { try { return localStorage.getItem('by_nav_sound') !== '0'; } catch (e) { return true; } };
function _ctx() {
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume().catch(() => {});
return ctx;
}
// Ein synthetischer „Wuff": Sägezahn-Sweep durch Tiefpass, kurzer Attack, schneller Decay.
function _wuff(at, pitch = 1) {
const c = _ctx(), t = c.currentTime + at;
const o = c.createOscillator(), g = c.createGain(), f = c.createBiquadFilter();
o.type = 'sawtooth';
o.frequency.setValueAtTime(240 * pitch, t);
o.frequency.exponentialRampToValueAtTime(75 * pitch, t + 0.16);
f.type = 'lowpass'; f.frequency.value = 900 * pitch; f.Q.value = 3;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(0.9, t + 0.02);
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
o.connect(f); f.connect(g); g.connect(c.destination);
o.start(t); o.stop(t + 0.25);
}
function _barks(n, pitch, gap) {
if (!enabled()) return;
const sample = files && (pitch > 1.3 ? files.klaeffen : files.wuff);
if (sample) { // echte Aufnahme: n-mal hintereinander
let i = 0;
const play = () => {
if (i++ >= n) return;
sample.currentTime = 0;
sample.play().catch(() => {});
setTimeout(play, gap * 1000 + 200);
};
play();
return;
}
try { for (let i = 0; i < n; i++) _wuff(i * gap, pitch); } catch (e) {}
}
return {
enabled,
unlock() { // in User-Geste aufrufen (iOS-Autoplay-Policy)
try {
const c = _ctx();
const b = c.createBuffer(1, 1, 22050), s = c.createBufferSource();
s.buffer = b; s.connect(c.destination); s.start(0);
} catch (e) {}
// Echte Samples einmalig anfragen (404 → Synthese bleibt)
if (files === null) {
files = {};
['wuff', 'klaeffen'].forEach(name => {
const a = new Audio(`/sounds/${name}.mp3`);
a.preload = 'auto';
a.addEventListener('canplaythrough', () => { files[name] = a; }, { once: true });
a.addEventListener('error', () => {}, { once: true });
});
}
},
links() { _barks(2, 1.0, 0.30); }, // 2× Wuff
rechts() { _barks(1, 1.0, 0.30); }, // 1× Wuff
klaeffen() { _barks(4, 1.7, 0.16); }, // schnelles, höheres Bellen
};
})();
let _navSndAnnouncedIdx = -1; // bis zu welchem Track-Index Abbiegungen angesagt wurden
let _navSndOffRoute = false; // Off-Route-Zustand (Kläffen beim Eintritt + alle 30 s)
let _navSndLastKlaeff = 0;
// Recording-Overlay state
let _recOvl = null, _recMap = null;
let _recFollow = true; // Karte folgt dem Standort bei Aufzeichnung (Drag pausiert)
@ -329,7 +404,10 @@ window.Page_routes = (() => {
_filterOpen = !_filterOpen;
const panel = document.getElementById('rk-filter-panel');
const btn = document.getElementById('rk-filter-btn');
if (panel) panel.style.display = _filterOpen ? '' : 'none';
// KLASSE toggeln, nicht style.display: .hidden hat display:none !important
// (design-system.css) — Inline-Style kommt dagegen nie an. Kaputt seit 27a3f95
// („Filter standardmäßig zu" setzte die Klasse ins Markup, Toggle blieb auf style).
if (panel) panel.classList.toggle('hidden', !_filterOpen);
if (btn) btn.classList.toggle('active', _filterOpen);
}
@ -362,7 +440,8 @@ window.Page_routes = (() => {
if (searchRow) searchRow.style.display = 'none';
if (actRow) actRow.style.display = 'none';
const filterPanel = document.getElementById('rk-filter-panel');
if (filterPanel) filterPanel.style.display = 'none';
if (filterPanel) { filterPanel.classList.add('hidden'); _filterOpen = false; }
document.getElementById('rk-filter-btn')?.classList.remove('active');
if (!App.hasPro(_appState?.user)) {
document.getElementById('rk-list')?.replaceChildren();
const gate = document.createElement('div');
@ -1670,6 +1749,12 @@ window.Page_routes = (() => {
const track = route.gps_track || [];
if (track.length < 2) return;
// Navi-Sounds: Audio in der User-Geste freischalten (iOS) + Ansage-Status zurücksetzen
NavSound.unlock();
_navSndAnnouncedIdx = -1;
_navSndOffRoute = false;
_navSndLastKlaeff = 0;
_navMaxIdx = 0;
_navRecorded = false;
_navLastBearing = null;
@ -1689,6 +1774,7 @@ window.Page_routes = (() => {
background:var(--c-surface);border-bottom:1px solid var(--c-border);flex-shrink:0">
<button id="rk-nav-back" style="background:none;border:none;color:var(--c-primary);font-size:16px;cursor:pointer;padding:4px 0"> Zurück</button>
<span style="font-weight:700;font-size:14px;flex:1;text-align:center;margin:0 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${UI.escape(route.name)}</span>
<button id="rk-nav-sound-btn" style="background:none;border:none;color:${NavSound.enabled() ? 'var(--c-primary)' : 'var(--c-text-secondary)'};cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Navi-Sounds (2× Wuff = links, 1× = rechts, Kläffen = falscher Weg)">${UI.icon(NavSound.enabled() ? 'speaker-high' : 'speaker-none')}</button>
<button id="rk-nav-center-btn" style="background:none;border:none;color:var(--c-primary);cursor:pointer;padding:4px 6px;display:flex;align-items:center" title="Auf Standort zentrieren">${UI.icon('crosshair')}</button>
</div>
@ -1894,6 +1980,27 @@ window.Page_routes = (() => {
return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
};
// Abbiegepunkte EINMALIG aus dem Track ableiten: Peilung über ~15-m-Stützpunkte
// (sonst macht GPS-Zickzack aus jeder Geraden eine Kurve), Richtungsänderung ≥ 40°
// = Abbiegung, > 0 = rechts. Mindestabstand zwischen Ansagen ~25 m.
const _navTurns = (() => {
const out = [];
const distM = (a, b) => _haversineKm(a.lat, a.lon, b.lat, b.lon) * 1000;
let lastIdx = -1;
for (let i = 1; i < track.length - 1; i++) {
let p = i - 1, accP = distM(track[p], track[i]);
while (p > 0 && accP < 15) { p--; accP += distM(track[p], track[p + 1]); }
let n = i + 1, accN = distM(track[i], track[n]);
while (n < track.length - 1 && accN < 15) { n++; accN += distM(track[n - 1], track[n]); }
const d = ((_bearingTo(track[i], track[n]) - _bearingTo(track[p], track[i]) + 540) % 360) - 180;
if (Math.abs(d) >= 40 && (lastIdx < 0 || distM(track[lastIdx], track[i]) > 25)) {
out.push({ idx: i, right: d > 0 });
lastIdx = i;
}
}
return out;
})();
const _updateStats = (idx, distToRoute, userLat, userLon) => {
if (idx > _navMaxIdx) {
_navMaxIdx = idx;
@ -1924,12 +2031,33 @@ window.Page_routes = (() => {
_navLastBearing = _bearingTo({ lat: userLat, lon: userLon }, track[idx + 1]);
_updateDimArrow();
}
// Abbiege-Ansage (René: 2× Wuff = links, 1× = rechts): nächster Turn vor uns,
// angesagt sobald ≤ 45 m entfernt — einmal pro Abbiegepunkt.
if (userLat != null && distToRoute < 0.1) {
const next = _navTurns.find(t => t.idx > idx && t.idx > _navSndAnnouncedIdx);
if (next) {
const dM = _haversineKm(userLat, userLon, track[next.idx].lat, track[next.idx].lon) * 1000;
if (dM <= 45) {
_navSndAnnouncedIdx = next.idx;
if (next.right) NavSound.rechts(); else NavSound.links();
}
}
}
const offWarn = document.getElementById('rk-nav-offwarn');
if (distToRoute * 1000 > 50) {
offWarn.style.display = '';
if (navigator.vibrate) navigator.vibrate([200, 100, 200]);
// Falscher Weg = Kläffen (beim Abkommen + Erinnerung alle 30 s)
const _now = Date.now();
if (!_navSndOffRoute || _now - _navSndLastKlaeff > 30000) {
_navSndOffRoute = true;
_navSndLastKlaeff = _now;
NavSound.klaeffen();
}
} else {
offWarn.style.display = 'none';
_navSndOffRoute = false;
}
// Polylines aktualisieren
doneLine.setLatLngs(track.slice(0, idx + 1).map(p => [p.lat, p.lon]));
@ -2057,6 +2185,19 @@ window.Page_routes = (() => {
if (locMarker) _navMap.setView(locMarker.getLatLng(), 16);
});
// Navi-Sounds an/aus (Klick = User-Geste → unlock + Probe-Wuff als Bestätigung)
document.getElementById('rk-nav-sound-btn')?.addEventListener('click', e => {
const on = !NavSound.enabled();
try { localStorage.setItem('by_nav_sound', on ? '1' : '0'); } catch (err) {}
const btn = e.currentTarget;
btn.style.color = on ? 'var(--c-primary)' : 'var(--c-text-secondary)';
btn.querySelector('use')?.setAttribute('href', `/icons/phosphor.svg#${on ? 'speaker-high' : 'speaker-none'}`);
if (on) { NavSound.unlock(); NavSound.rechts(); }
UI.toast.info(on
? 'Navi-Sounds an: 2× Wuff = links, 1× Wuff = rechts, Kläffen = falscher Weg 🐕'
: 'Navi-Sounds aus.');
});
document.getElementById('rk-nav-pois')?.addEventListener('click', () => {
if (!_navPois.length) { UI.toast.info('Keine POIs entlang dieser Route.'); return; }
const byType = {};

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1240"></script>
<script src="/js/landing-init.js?v=1242"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1240';
const VER = '1242';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten

View file

@ -10,7 +10,8 @@ services:
dwd-radar:
build: ./tools/dwd-radar
image: banyaro-dwd-radar
container_name: banyaro-dwd-radar
# KEIN container_name: Staging + Prod teilen sich den Docker-Host — der Compose-
# Projektname (= Verzeichnis banyaro / banyaro-staging) hält die Container auseinander.
mem_limit: 1g
volumes:
- ./data/radar:/out

3
tools/dwd-radar/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
poc/*.tif
poc/*.tar.bz2
poc/DE1200_RV*

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.