Standortdaten: Nur nach expliziter Browser-Freigabe â fĂŒr Karte, Gassi-Treffen,
Giftköder-Meldungen, Nearby-Alerts und Routenaufzeichnung. Standortdaten werden nicht dauerhaft
gespeichert, auĂer du speicherst selbst eine Route oder Meldung.
@@ -94,6 +94,9 @@
Fotos & EXIF-Daten: Beim Hochladen von Bildern können GPS-Koordinaten
in den EXIF-Metadaten enthalten sein. Diese werden serverseitig ausgelesen, um Fotos auf der
Karte zu verorten â sofern vorhanden. Die Rohdaten werden nicht separat gespeichert.
+
Mikrofon (Sprachnachrichten): Nur nach expliziter Browser-Freigabe und nur,
+ wĂ€hrend du in einer Notiz aktiv eine Sprachnachricht aufnimmst. Die Aufnahme wird ausschlieĂlich
+ auf unseren eigenen Servern gespeichert (kein Drittanbieter), ist privat und nur fĂŒr dich sichtbar.
`;
+ lb.querySelector('#by-lb-close').addEventListener('click', () => lb.remove());
+ lb.querySelector('#by-lb-prev')?.addEventListener('click', () => { if (idx > 0) { idx--; render(); } });
+ lb.querySelector('#by-lb-next')?.addEventListener('click', () => { if (idx < list.length - 1) { idx++; render(); } });
+ };
+ render();
+ document.body.appendChild(lb);
+ }
+ return { show };
+ })();
+
// Ăffentliche API
return {
toast, modal,
- noteModal,
+ noteModal, noteMediaAttacher, lightbox,
setLoading, asyncButton,
formData, setFormError, clearFormErrors,
emptyState, errorState, time, text, money,
diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js
index da1e422..2c44e93 100644
--- a/backend/static/js/worlds.js
+++ b/backend/static/js/worlds.js
@@ -1791,6 +1791,146 @@ window.Worlds = (() => {
{ t:'Kein Psychiater der Welt kann so gut zuhören wie ein Hund.', a:'Unbekannt' },
{ t:'Wo Hunde sind, da ist das Zuhause.', a:'Unbekannt' },
{ t:'Der Hund hat keinen Begriff von Vergangenheit oder Zukunft. Er lebt.', a:'Milan Kundera' },
+ { t:"Wenn du einen verhungernden Hund aufnimmst und es ihm gutgehen lĂ€sst, wird er dich nicht beiĂen. Das ist der wesentliche Unterschied zwischen Hund und Mensch.", a:"Mark Twain" },
+ { t:"Der Hund ist ein Gentleman. Ich hoffe, in seinen Himmel zu kommen, nicht in den der Menschen.", a:"Mark Twain" },
+ { t:"Je mehr ich ĂŒber die Menschen lerne, desto mehr liebe ich meinen Hund.", a:"Mark Twain" },
+ { t:"Der Himmel vergibt nach Gunst. Ginge es nach Verdienst, bliebest du drauĂen und dein Hund kĂ€me hinein.", a:"Mark Twain" },
+ { t:"Die Treue eines Hundes ist ein kostbares Geschenk, das nicht weniger bindende Pflichten auferlegt als die Freundschaft eines Menschen.", a:"Konrad Lorenz" },
+ { t:"Es gibt keine Treue, die nicht schon einmal gebrochen worden wĂ€re, auĂer der eines wahrhaft treuen Hundes.", a:"Konrad Lorenz" },
+ { t:"Die Bindung an einen echten Hund ist so bestÀndig, wie es Bande auf dieser Erde nur sein können.", a:"Konrad Lorenz" },
+ { t:"Der Wunsch, einen Hund zu halten, entspringt der uralten Sehnsucht des zivilisierten Menschen nach dem verlorenen Paradies.", a:"Konrad Lorenz" },
+ { t:"Wenn ich daran denke, dass mein Hund mich mehr liebt, als ich ihn, dann beschÀmt mich das.", a:"Konrad Lorenz" },
+ { t:"Der Hund ist, mit Recht, das Sinnbild der Treue.", a:"Arthur Schopenhauer" },
+ { t:"Woran sollte man sich von der Falschheit der Menschen erholen, wenn die Hunde nicht wÀren, in deren ehrliches Gesicht man ohne Misstrauen blicken kann.", a:"Arthur Schopenhauer" },
+ { t:"Der Anblick jedes Tieres erfreut mich unmittelbar und mir geht dabei das Herz auf, am meisten bei dem der Hunde.", a:"Arthur Schopenhauer" },
+ { t:"Hunde lieben ihre Freunde und beiĂen ihre Feinde, ganz anders als Menschen, die niemals rein lieben, sondern stets Liebe und Hass vermengen.", a:"Sigmund Freud" },
+ { t:"Hunde schenken Zuneigung ohne Zwiespalt und die Schönheit eines Daseins, das ganz in sich ruht.", a:"Sigmund Freud" },
+ { t:"Gibt es im Himmel keine Hunde, dann will ich, wenn ich sterbe, dorthin gehen, wo sie hingekommen sind.", a:"Will Rogers" },
+ { t:"FĂŒr seinen Hund ist jeder Mensch ein Napoleon, daher die anhaltende Beliebtheit der Hunde.", a:"Aldous Huxley" },
+ { t:"Bevor du einen Hund hast, kannst du dir kaum vorstellen, wie das Leben mit ihm wÀre. Danach kannst du dir kein anderes Leben mehr vorstellen.", a:"Caroline Knapp" },
+ { t:"Wer einmal einen wundervollen Hund hatte, dessen Leben ist ohne einen Àrmer.", a:"Dean Koontz" },
+ { t:"Einen Hund zu streicheln und zu kraulen kann den Geist so beruhigen wie tiefe Meditation und ist fast so gut fĂŒr die Seele wie ein Gebet.", a:"Dean Koontz" },
+ { t:"Tausendmal hat er mir gesagt, dass ich sein Grund zu leben bin, durch die Art, wie er sich an mein Bein lehnt.", a:"Gene Hill" },
+ { t:"Hunde sind unser Bindeglied zum Paradies. Sie kennen weder Bosheit noch Neid noch Unzufriedenheit.", a:"Milan Kundera" },
+ { t:"Mit einem Hund an einem schönen Nachmittag auf einem HĂŒgel zu sitzen ist wie eine RĂŒckkehr nach Eden.", a:"Milan Kundera" },
+ { t:"Hunde sprechen sehr wohl, doch nur zu denen, die zu lauschen verstehen.", a:"Orhan Pamuk" },
+ { t:"Tiere sind so angenehme Freunde, sie stellen keine Fragen und ĂŒben keine Kritik.", a:"George Eliot" },
+ { t:"Das gröĂte VergnĂŒgen mit einem Hund ist, dass man sich vor ihm zum Narren machen kann und er nicht nur nicht tadelt, sondern selbst mitmacht.", a:"Samuel Butler" },
+ { t:"Bedenke, dass der AllmÀchtige, der uns den Hund zum GefÀhrten gab, ihm ein edles Wesen verlieh, das des Betrugs unfÀhig ist.", a:"Sir Walter Scott" },
+ { t:"Ich habe oft ĂŒber den Grund nachgedacht, warum Hunde so kurz leben, und bin ĂŒberzeugt, es geschieht aus Mitleid mit dem Menschen.", a:"Sir Walter Scott" },
+ { t:"Hunde beiĂen mich nie. Nur Menschen.", a:"Marilyn Monroe" },
+ { t:"Wer hĂ€lt dich fĂŒr so groĂartig wie dein Hund.", a:"Audrey Hepburn" },
+ { t:"Ich gehe mit meinen Hunden, das hÀlt mich fit. Ich rede mit meinen Hunden, das hÀlt mich gesund.", a:"Audrey Hepburn" },
+ { t:"Sobald er seinen Herrn erblickte, lieĂ Argos die Ohren sinken und wedelte mit dem Schwanz, doch zu ihm hin zu kommen vermochte er nicht mehr.", a:"Homer" },
+ { t:"Hunde und Philosophen tun das meiste Gute und erhalten den geringsten Lohn.", a:"Diogenes" },
+ { t:"Hunde sind besser als Menschen, denn sie wissen, doch sie verraten es nicht.", a:"Emily Dickinson" },
+ { t:"Bei Schlachten, die das Schicksal von Völkern entschieden, blieb ich ungerĂŒhrt. Hier aber, beim Kummer eines einzigen Hundes, war ich zu TrĂ€nen gerĂŒhrt.", a:"Napoleon Bonaparte" },
+ { t:"Wenn Hunde in den Himmel kommen, brauchen sie keine FlĂŒgel, denn Gott weiĂ, dass Hunde das Laufen am meisten lieben.", a:"Cynthia Rylant" },
+ { t:"Meine ganze Bestimmung war es, ihn zu lieben, bei ihm zu sein und ihn glĂŒcklich zu machen.", a:"W. Bruce Cameron" },
+ { t:"Meine Hundefreunde scheinen meine Grenzen zu verstehen und bleiben immer dicht an meiner Seite.", a:"Helen Keller" },
+ { t:"Gib einem Hund dein Herz, das er zerreiĂen kann.", a:"Rudyard Kipling" },
+ { t:"Ein Hund lehrt einen Jungen Treue, Beharrlichkeit und sich dreimal zu drehen, bevor man sich hinlegt.", a:"Robert Benchley" },
+ { t:"Du kannst zu einem Hund den gröĂten Unsinn sagen, und er schaut dich an, als wollte er sagen: Donnerwetter, du hast recht, darauf wĂ€re ich nie gekommen.", a:"Dave Barry" },
+ { t:"Wenn ich an die Unsterblichkeit glaube, dann daran, dass gewisse Hunde in den Himmel kommen, und nur sehr, sehr wenige Menschen.", a:"James Thurber" },
+ { t:"Eine TĂŒr ist das, auf deren falscher Seite ein Hund sich stĂ€ndig befindet.", a:"Ogden Nash" },
+ { t:"NatĂŒrlich kann man ohne Hund leben, es lohnt sich nur nicht.", a:"Heinz RĂŒhmann" },
+ { t:"Ein Leben ohne Mops ist möglich, aber sinnlos.", a:"Loriot" },
+ { t:"Die Welt wÀre ein schönerer Ort, wenn jeder so bedingungslos lieben könnte wie ein Hund.", a:"M.K. Clinton" },
+ { t:"Der durchschnittliche Hund ist ein netterer Mensch als der durchschnittliche Mensch.", a:"Andy Rooney" },
+ { t:"Niemand schÀtzt das ganz besondere Genie deiner Unterhaltung so sehr wie dein Hund.", a:"Christopher Morley" },
+ { t:"Ich habe meinem Schmerz einen Namen gegeben und nenne ihn Hund, denn er ist ebenso treu und klug wie jeder andere Hund.", a:"Friedrich Nietzsche" },
+ { t:"Dem Hunde, wenn er gut gezogen, wird selbst ein weiser Mann gewogen.", a:"Johann Wolfgang von Goethe" },
+ { t:"VerstöĂt ein Herr seinen Hund, weil er ihm das Brot nicht mehr verdienen kann, so zeigt das stets eine sehr kleine Seele des Herrn an.", a:"Immanuel Kant" },
+ { t:"Wenn du keinen Hund besitzt, ist nicht unbedingt etwas mit dir verkehrt, aber vielleicht stimmt etwas mit deinem Leben nicht.", a:"Roger Caras" },
+ { t:"Alte Hunde sind wie alte Schuhe, bequem. Sie sind vielleicht etwas aus der Form, aber sie passen einfach gut.", a:"Bonnie Wilcox" },
+ { t:"Kommt ein Hund nicht zu dir, nachdem er dir ins Gesicht gesehen hat, so solltest du heimgehen und dein Gewissen prĂŒfen.", a:"Woodrow Wilson" },
+ { t:"Springt ein Hund auf deinen SchoĂ, dann weil er dich mag. Tut eine Katze dasselbe, ist es nur, weil dein SchoĂ wĂ€rmer ist.", a:"Alfred North Whitehead" },
+ { t:"Geld kann dir einen feinen Hund kaufen, aber nur Liebe bringt ihn dazu, mit dem Schwanz zu wedeln.", a:"Kinky Friedman" },
+ { t:"Wenn ich fĂŒr eine Reise den Koffer hervorhole, weiĂ er es lange vorher und gerĂ€t in einen Zustand milder Aufregung.", a:"John Steinbeck" },
+ { t:"Ein Hund ist ein Band zwischen Fremden.", a:"John Steinbeck" },
+ { t:"Mein kleiner Hund, ein Herzschlag zu meinen FĂŒĂen.", a:"Edith Wharton" },
+ { t:"Geschaffen wurde der Hund eigens fĂŒr die Kinder. Er ist der Gott des Ăbermuts.", a:"Henry Ward Beecher" },
+ { t:"Hunde sind klug. Sie kriechen in eine stille Ecke und lecken ihre Wunden und kehren erst in die Welt zurĂŒck, wenn sie wieder heil sind.", a:"Agatha Christie" },
+ { t:"Schönheit ohne Eitelkeit, StĂ€rke ohne Ăbermut, Mut ohne Wildheit und alle Tugenden des Menschen ohne seine Laster.", a:"Lord Byron" },
+ { t:"Gott dreht Wolken um und um, um den Hunden im Hundehimmel flauschige Betten zu bereiten.", a:"Cynthia Rylant" },
+ { t:"Hunde besitzen eine Eigenschaft, die unter Menschen selten ist, nÀmlich zu erkennen, wer Hilfe braucht, und sie zu geben.", a:"Caroline Knapp" },
+ { t:"In manchen Dingen ist mein Hund klĂŒger als ich, in anderen ist er bodenlos unwissend.", a:"John Steinbeck" },
+ { t:"Ein Hund zur Hand ist besser als ein Bruder in der Ferne.", a:"Persisches Sprichwort" },
+ { t:"Wenn du bei jedem bellenden Hund stehen bleibst, beendest du deine Reise nie.", a:"Arabisches Sprichwort" },
+ { t:"Es ist schwer, einen so treuen GefÀhrten zu finden wie einen Hund.", a:"Mongolisches Sprichwort" },
+ { t:"Ein Hund ohne Schwanz kann nicht zeigen, dass er sich freut.", a:"Albanisches Sprichwort" },
+ { t:"Ein Hund, der mit dem Schwanz wedelt, bezieht keine PrĂŒgel.", a:"Japanisches Sprichwort" },
+ { t:"Der hungrige Hund fĂŒrchtet den Stock nicht.", a:"Japanisches Sprichwort" },
+ { t:"Treffen sich im Paradies eine Menschenseele und eine Hundeseele, verneigt sich der Mensch vor dem Hund.", a:"Sibirisches Sprichwort" },
+ { t:"Solange der Mensch denkt, Tiere fĂŒhlten nicht, fĂŒhlen Tiere, dass der Mensch nicht denkt.", a:"Indianische Weisheit" },
+ { t:"Mit Hunden zu leben tut dem Menschen gut.", a:"Tibetisches Sprichwort" },
+ { t:"Ein Hund ist ein Herz auf vier Beinen.", a:"Irisches Sprichwort" },
+ { t:"Der Hund vergisst den einen Bissen nicht, und wirfst du ihm auch hundert Steine nach.", a:"Chinesisches Sprichwort" },
+ { t:"Sei der Freund meines Hundes, dann bist du auch der meine.", a:"Indianische Weisheit" },
+ { t:"HĂŒte dich vor dem Menschen, der nicht spricht, und vor dem Hund, der nicht bellt.", a:"Indianische Weisheit" },
+ { t:"Ein kluger Hund bellt nicht ohne Grund.", a:"Französisches Sprichwort" },
+ { t:"In seiner eigenen HĂŒtte ist jeder Hund ein Löwe.", a:"Französisches Sprichwort" },
+ { t:"Hunde, die sich beiĂen, halten gegen den Wolf zusammen.", a:"Armenisches Sprichwort" },
+ { t:"Der Hund bellt, doch die Karawane zieht weiter.", a:"TĂŒrkisches Sprichwort" },
+ { t:"Hat ein Armer den Hund groĂgezogen, folgt er keinem Reichen mehr.", a:"Mongolisches Sprichwort" },
+ { t:"Hat der Hund zu viele Herren, schlÀft er hungrig ein.", a:"Afrikanisches Sprichwort" },
+ { t:"Ich hoffe, einmal der Mensch zu werden, fĂŒr den mein Hund mich hĂ€lt.", a:"Ungarisches Sprichwort" },
+ { t:"Eines Hundes Treue wÀhrt ein ganzes Leben lang.", a:"Spanisches Sprichwort" },
+ { t:"Faule SchÀfer haben die besten Hunde.", a:"Deutsches Sprichwort" },
+ { t:"Hunde, die viel bellen, beiĂen selten.", a:"Italienisches Sprichwort" },
+ { t:"Wer einen guten Hund hat, braucht keinen WĂ€chter.", a:"Italienisches Sprichwort" },
+ { t:"Wo der Hund frei laufen darf, ist das GlĂŒck nicht weit.", a:"Unbekannt" },
+ { t:"Ein Hund schaut nicht auf deinen Stand, nur auf dein Herz.", a:"Unbekannt" },
+ { t:"Dem Hund ist gleich, ob du reich bist; ihm reicht, dass du heimkommst.", a:"Unbekannt" },
+ { t:"Ein Hund braucht keine Worte, um dich zu trösten.", a:"Unbekannt" },
+ { t:"Der beste Platz der Welt ist neben einem Hund.", a:"Unbekannt" },
+ { t:"Ein Tag mit Hund ist nie ganz verloren.", a:"Unbekannt" },
+ { t:"Ein Hund fĂŒllt die Stille im Haus mit leisem GlĂŒck.", a:"Unbekannt" },
+ { t:"Hunde messen die Zeit nicht in Stunden, sondern in SpaziergÀngen.", a:"Unbekannt" },
+ { t:"Ein Hund findet zum GlĂŒck immer den kĂŒrzesten Weg.", a:"Unbekannt" },
+ { t:"Mit einem Hund an der Seite lÀuft man nie allein.", a:"Unbekannt" },
+ { t:"Ein Hund vergibt schneller, als wir uns entschuldigen können.", a:"Unbekannt" },
+ { t:"Wer die Sprache der Hunde lernt, hört auf zu reden und beginnt zu fĂŒhlen.", a:"Unbekannt" },
+ { t:"Ein Hund kennt deinen Namen nicht, aber er kennt dein Herz.", a:"Unbekannt" },
+ { t:"Hunde sind die PĂŒnktlichsten, wenn es ums GlĂŒcklichsein geht.", a:"Unbekannt" },
+ { t:"Ein Hund wartet nicht auf morgen, um dich heute zu lieben.", a:"Unbekannt" },
+ { t:"Manche Engel haben Fell und kalte Pfoten.", a:"Unbekannt" },
+ { t:"Ein Hund nimmt dich, wie du bist, und macht dich trotzdem besser.", a:"Unbekannt" },
+ { t:"Was ein Hund ĂŒber Freundschaft weiĂ, lernt der Mensch ein Leben lang.", a:"Unbekannt" },
+ { t:"Hunde haben kurze Leben, weil sie das Lieben so gut können, dass sie keine Zeit verschwenden.", a:"Unbekannt" },
+ { t:"GlĂŒck hat vier Pfoten und einen wedelnden Schwanz.", a:"Unbekannt" },
+ { t:"Ein Hund teilt dein Schweigen, ohne es zu fĂŒllen.", a:"Unbekannt" },
+ { t:"Der treueste Blick der Welt kommt von unten und wedelt dabei.", a:"Unbekannt" },
+ { t:"Ein Hund braucht keinen Sonntag, jeder Tag mit dir ist ihm Feiertag.", a:"Unbekannt" },
+ { t:"Wo ein Hund die Stiefel bringt, fehlt es nie an Liebe.", a:"Unbekannt" },
+ { t:"Hunde rechnen nicht nach, wie viel du gibst; sie geben einfach alles zurĂŒck.", a:"Unbekannt" },
+ { t:"Ein Hund sieht nicht, wie du aussiehst, sondern wer du bist.", a:"Unbekannt" },
+ { t:"Ein Hund macht aus einem Spaziergang ein kleines Abenteuer.", a:"Unbekannt" },
+ { t:"Wer einem Hund ins Auge sieht, schaut der Ehrlichkeit beim Atmen zu.", a:"Unbekannt" },
+ { t:"Ein Hund spĂŒrt deinen schlechten Tag und bleibt trotzdem.", a:"Unbekannt" },
+ { t:"Das Schwierigste am Hundeleben ist, dass es zu kurz fĂŒr so viel Liebe ist.", a:"Unbekannt" },
+ { t:"Ein Hund braucht wenig und schenkt davon das Meiste.", a:"Unbekannt" },
+ { t:"Hunde sind der Beweis, dass Treue keine Worte braucht.", a:"Unbekannt" },
+ { t:"Ein wedelnder Schwanz hat schon manchen Tag gerettet.", a:"Unbekannt" },
+ { t:"Wer einen Hund versteht, braucht den Menschen weniger zu erklÀren.", a:"Unbekannt" },
+ { t:"Ein Hund spart seine Liebe nicht auf, er verschenkt sie sofort und vollstÀndig.", a:"Unbekannt" },
+ { t:"Die kĂŒrzeste Verbindung zwischen zwei Menschen ist manchmal eine Hundeleine.", a:"Unbekannt" },
+ { t:"Ein Hund kennt keinen Stolz, nur Wiedersehensfreude.", a:"Unbekannt" },
+ { t:"Wer mit einem Hund alt wird, lernt das GlĂŒck im Kleinen.", a:"Unbekannt" },
+ { t:"Ein Hund hÀlt dir keine Reden, er hÀlt dir die Treue.", a:"Unbekannt" },
+ { t:"Vor einem Hund muss man nichts vorgeben; er liebt das Echte.", a:"Unbekannt" },
+ { t:"Ein Hund schreibt keine Briefe, doch sein Schwanz erzÀhlt alles.", a:"Unbekannt" },
+ { t:"Hunde nehmen die kleinen Dinge ernst, darum sind sie so groĂ im Lieben.", a:"Unbekannt" },
+ { t:"Ein Hund verzeiht dir den Regen, solange du mit ihm hinausgehst.", a:"Unbekannt" },
+ { t:"Wer das Vertrauen eines Hundes gewinnt, hat etwas Selteneres als Gold.", a:"Unbekannt" },
+ { t:"Ein Hund macht stille Tage warm und laute Tage leiser.", a:"Unbekannt" },
+ { t:"Die treueste Uhr im Haus ist der Hund vor dem Futternapf.", a:"Unbekannt" },
+ { t:"Ein Hund fragt nicht, wie der Tag war; er macht ihn einfach besser.", a:"Unbekannt" },
+ { t:"Wer einen Hund an der Seite hat, ist nirgends ganz fremd.", a:"Unbekannt" },
+ { t:"Ein Hund kennt nur ein Tempo bei der Liebe: sofort.", a:"Unbekannt" },
+ { t:"Ein Hund am Feuer wÀrmt mehr als das Feuer selbst.", a:"Unbekannt" },
+ { t:"Wer den Hund gut behandelt, dem öffnet sich das Herz von selbst.", a:"Unbekannt" },
+ { t:"Ein Hund braucht keinen Kalender, er weiĂ genau, wann du nach Hause kommst.", a:"Unbekannt" },
];
function _renderWelt() {
diff --git a/backend/static/landing.html b/backend/static/landing.html
index 272122c..9cb3d3f 100644
--- a/backend/static/landing.html
+++ b/backend/static/landing.html
@@ -4,7 +4,7 @@
-
+
Ban Yaro â Die Hunde-App fĂŒr Deutschland, Ăsterreich & Schweiz
diff --git a/backend/static/sw.js b/backend/static/sw.js
index 119cc71..c7e474e 100644
--- a/backend/static/sw.js
+++ b/backend/static/sw.js
@@ -4,7 +4,7 @@
============================================================ */
// â EINZIGE Stelle fĂŒr die Version â STATIC_ASSETS und CACHE_VERSION leiten sich ab
-const VER = '1278';
+const VER = '1292';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt ĂŒber SW-Updates erhalten
diff --git a/docker-compose.yml b/docker-compose.yml
index d984dcd..240f881 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -14,6 +14,7 @@ services:
- DB_PATH=/data/banyaro.db
- MEDIA_DIR=/data/media
- UMAMI_URL=https://umami.motocamp.de
+ - KI_MODE=cloud
# VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY / VAPID_CONTACT
# â kommen aus .env (nicht in Git)
healthcheck:
diff --git a/tests/js/README.md b/tests/js/README.md
index 1b5675e..f0428e7 100644
--- a/tests/js/README.md
+++ b/tests/js/README.md
@@ -13,5 +13,13 @@ for f in tests/js/test-map-offline*.js; do node "$f" backend/static/js/map-offli
- r6: Standort-Grundversorgung (ensureHomeArea: lĂ€dt/skippt/Cap, ĂŒberlebt clear)
- r7: selektives Löschen (Korridor-Keep via keepTracks, manuelle Gebiete weg, Komplett-Wipe-Fallback)
+EigenstÀndig (kein Stub-Argument nötig):
+
+```
+node tests/js/test-nav-loop-closestidx.js
+```
+
+- nav-loop-closestidx: Navi-Erst-Fix bei Runden springt nicht ans Track-Ende (spiegelt `_closestIdx` aus `js/pages/routes.js`) â Bugfix Angie/Deining 09.06.2026
+
â ïž Node 21+: eingebautes `navigator`-Global â Stubs via `Object.defineProperty(globalThis, 'navigator', âŠ)`,
ein einfaches `global.navigator =` wird still verschluckt.
diff --git a/tests/js/test-nav-loop-closestidx.js b/tests/js/test-nav-loop-closestidx.js
new file mode 100644
index 0000000..df45372
--- /dev/null
+++ b/tests/js/test-nav-loop-closestidx.js
@@ -0,0 +1,98 @@
+// Navi-Erst-Fix bei RUNDEN: der Startindex darf nicht ans Track-Ende springen.
+//
+// Spiegelt die _closestIdx-Erst-Fix-Logik aus js/pages/routes.js (_startNav). An einem
+// Start/Ende-Knoten einer Runde ist der ENDPUNKT oft ein paar Meter nÀher als der
+// Startpunkt; die alte globale Suche sprang dann sofort ans Track-Ende â 100 % / 0 km ab
+// Sekunde 1 (Angie, Deining-Runde 09.06.2026). Bei Ănderung BEIDE Stellen anpassen.
+//
+// Hinweis: bewusst eine Nachbildung â die echte Funktion ist eine Closure in _startNav
+// und nicht exportierbar, ohne routes.js umzubauen.
+
+const _haversineKm = (lat1, lon1, lat2, lon2) => {
+ const R = 6371, dLat = (lat2 - lat1) * Math.PI / 180, dLon = (lon2 - lon1) * Math.PI / 180;
+ const a = Math.sin(dLat / 2) ** 2 +
+ Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon / 2) ** 2;
+ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+};
+
+// Erst-Fix-Index fĂŒr gegebenen track + Userposition (1:1 aus routes.js).
+function firstFixIdx(track, lat, lon) {
+ const search = (from, to) => {
+ let best = from, bestD = Infinity;
+ for (let i = from; i <= to; i++) {
+ const d = _haversineKm(lat, lon, track[i].lat, track[i].lon);
+ if (d < bestD) { bestD = d; best = i; }
+ }
+ return { best, bestD };
+ };
+ const isLoop = track.length > 2 &&
+ _haversineKm(track[0].lat, track[0].lon,
+ track[track.length - 1].lat, track[track.length - 1].lon) < 0.06;
+ const g = search(0, track.length - 1);
+ if (isLoop) {
+ const win = Math.min(track.length - 1, Math.max(30, Math.floor(track.length * 0.15)));
+ const s = search(0, win);
+ return { idx: s.bestD < 0.15 ? s.best : g.best, isLoop };
+ }
+ const s = search(0, Math.min(track.length - 1, 30));
+ return { idx: (s.bestD - g.bestD) * 1000 < 25 ? s.best : g.best, isLoop };
+}
+
+// Die ALTE Logik (vor dem Fix) â nur zum Beweis, dass der Fix wirklich etwas Ă€ndert.
+function firstFixIdxOld(track, lat, lon) {
+ const search = (from, to) => {
+ let best = from, bestD = Infinity;
+ for (let i = from; i <= to; i++) {
+ const d = _haversineKm(lat, lon, track[i].lat, track[i].lon);
+ if (d < bestD) { bestD = d; best = i; }
+ }
+ return { best, bestD };
+ };
+ const g = search(0, track.length - 1);
+ const s = search(0, Math.min(track.length - 1, 30));
+ return (s.bestD - g.bestD) * 1000 < 25 ? s.best : g.best;
+}
+
+// --- Synthetische Deining-artige Runde -------------------------------------
+const C = { lat: 48.07, lon: 11.50 };
+const mLat = m => m / 111320;
+const mLon = (m, lat) => m / (111320 * Math.cos(lat * Math.PI / 180));
+// Punkt auf einem Kreis: Winkel von Nord, im Uhrzeigersinn.
+const onCircle = (deg, r) => {
+ const rad = deg * Math.PI / 180;
+ return { lat: C.lat + mLat(r * Math.cos(rad)), lon: C.lon + mLon(r * Math.sin(rad), C.lat) };
+};
+
+const N = 40, R = 80; // 40 Punkte auf 80-m-Kreis, lange Runde von 0°â329°
+const track = [];
+for (let i = 0; i < N; i++) track.push(onCircle(i / (N - 1) * 329, R));
+// User steht 3 m auĂerhalb des ENDpunkts (329°) â nĂ€her am Ende als am Start.
+const user = onCircle(329, R + 3);
+
+const startEndM = _haversineKm(track[0].lat, track[0].lon,
+ track[N - 1].lat, track[N - 1].lon) * 1000;
+const dStart = _haversineKm(user.lat, user.lon, track[0].lat, track[0].lon) * 1000;
+const dEnd = _haversineKm(user.lat, user.lon, track[N - 1].lat, track[N - 1].lon) * 1000;
+console.log(`Runde: StartâEnde ${startEndM.toFixed(0)} m | UserâStart ${dStart.toFixed(0)} m, UserâEnde ${dEnd.toFixed(0)} m`);
+
+// 1. Loop wird erkannt (Start â Ende < 60 m)
+const res = firstFixIdx(track, user.lat, user.lon);
+if (!res.isLoop) throw new Error('Runde nicht als Loop erkannt');
+
+// 2. Erst-Fix landet im STARTbereich, NICHT am Track-Ende
+console.log('Erst-Fix-Index:', res.idx, '(von', N - 1 + ')');
+if (res.idx > Math.floor(N * 0.15)) throw new Error(`Erst-Fix sprang weg vom Start (idx ${res.idx})`);
+
+// 3. Beweis: die alte Logik wÀre hier ans Ende gesprungen (100 %)
+const old = firstFixIdxOld(track, user.lat, user.lon);
+console.log('Alte Logik-Index:', old);
+if (old !== N - 1) throw new Error('Erwartet: alte Logik springt ans Ende â Testfall trifft den Bug nicht mehr');
+
+// 4. Punkt-zu-Punkt-Route (kein Loop): User am Start â 0 %, am Ende â bleibt sinnvoll
+const ptp = [];
+for (let i = 0; i < N; i++) ptp.push({ lat: C.lat + mLat(i * 25), lon: C.lon }); // 25-m-Schritte nach Norden
+const ptpRes = firstFixIdx(ptp, ptp[0].lat, ptp[0].lon);
+if (ptpRes.isLoop) throw new Error('Gerade Route fÀlschlich als Loop erkannt');
+if (ptpRes.idx !== 0) throw new Error(`Punkt-zu-Punkt am Start sollte idx 0 sein, war ${ptpRes.idx}`);
+
+console.log('\nALLE NAV-LOOP-TESTS BESTANDEN');
diff --git a/tests/test_account_deletion.py b/tests/test_account_deletion.py
index e016966..63df268 100644
--- a/tests/test_account_deletion.py
+++ b/tests/test_account_deletion.py
@@ -59,3 +59,30 @@ def test_delete_account_minimal_user(client):
assert resp.status_code == 200, resp.text
with db() as conn:
assert conn.execute("SELECT 1 FROM users WHERE id=?", (uid,)).fetchone() is None
+
+
+def test_delete_account_purges_note_media(client):
+ """Account-Löschung entfernt Notiz-Medien â DB-Zeilen UND Dateien auf Disk."""
+ import io, os
+ from database import db
+ from PIL import Image
+
+ uid, headers = _make_user(client)
+ nid = client.post("/api/notes/diary/1", headers=headers,
+ json={"text": "Mit Foto", "parent_label": "X"}).json()["id"]
+
+ buf = io.BytesIO(); Image.new("RGB", (10, 10), (1, 2, 3)).save(buf, format="JPEG")
+ up = client.post(f"/api/notes/{nid}/media", headers=headers,
+ files={"file": ("f.jpg", buf.getvalue(), "image/jpeg")})
+ assert up.status_code == 200, up.text
+ url = up.json()["url"]
+ fpath = os.path.join(os.getenv("MEDIA_DIR", "/data/media"), url[len("/media/"):])
+ assert os.path.exists(fpath)
+
+ resp = client.delete("/api/profile/account", headers=headers)
+ assert resp.status_code == 200, resp.text
+
+ with db() as conn:
+ assert conn.execute("SELECT COUNT(*) c FROM note_media WHERE note_id=?", (nid,)).fetchone()["c"] == 0
+ assert conn.execute("SELECT COUNT(*) c FROM notes WHERE user_id=?", (uid,)).fetchone()["c"] == 0
+ assert not os.path.exists(fpath), "note_media-Datei blieb als Leiche auf Disk"
diff --git a/tests/test_notes_media.py b/tests/test_notes_media.py
new file mode 100644
index 0000000..a1003f9
--- /dev/null
+++ b/tests/test_notes_media.py
@@ -0,0 +1,129 @@
+"""Tests fĂŒr Notiz-Medien: Bild-/Audio-Upload, GET liefert media_items,
+Löschen entfernt DB-Zeile + Datei, Notiz-Delete rÀumt Dateien, Validierung."""
+
+import io
+import os
+
+import pytest
+
+
+def _jpeg_bytes(color=(200, 100, 50)):
+ from PIL import Image
+ buf = io.BytesIO()
+ Image.new("RGB", (12, 12), color).save(buf, format="JPEG")
+ return buf.getvalue()
+
+
+# Minimaler MP4/M4A-Header (ftyp-Box) â genĂŒgt fĂŒr validate_audio(audio/mp4).
+_M4A_BYTES = b"\x00\x00\x00\x18ftypM4A \x00\x00\x00\x00M4A mp42isom" + b"\x00" * 32
+
+
+def _create_note(client, user, text="Notiz mit Medien"):
+ r = client.post("/api/notes/diary/1", headers=user["headers"],
+ json={"text": text, "parent_label": "Testobjekt"})
+ assert r.status_code == 201, r.text
+ return r.json()
+
+
+def _media_path(url):
+ media_dir = os.getenv("MEDIA_DIR", "/data/media")
+ rel = url[len("/media/"):] if url.startswith("/media/") else url.lstrip("/")
+ return os.path.join(media_dir, rel)
+
+
+def test_upload_image_to_note(client, user):
+ note = _create_note(client, user)
+ r = client.post(
+ f"/api/notes/{note['id']}/media",
+ headers=user["headers"],
+ files={"file": ("foto.jpg", _jpeg_bytes(), "image/jpeg")},
+ )
+ assert r.status_code == 200, r.text
+ m = r.json()
+ assert m["media_type"] == "image"
+ assert m["url"].startswith("/media/notes/")
+ assert os.path.exists(_media_path(m["url"]))
+
+
+def test_note_get_includes_media_items(client, user):
+ note = _create_note(client, user)
+ client.post(f"/api/notes/{note['id']}/media", headers=user["headers"],
+ files={"file": ("a.jpg", _jpeg_bytes(), "image/jpeg")})
+ r = client.get("/api/notes/diary/1", headers=user["headers"])
+ assert r.status_code == 200
+ target = next(n for n in r.json() if n["id"] == note["id"])
+ assert len(target["media_items"]) == 1
+ assert target["media_items"][0]["media_type"] == "image"
+
+
+def test_upload_audio_to_note(client, user):
+ # Reine Sprachnotiz: leerer Text ist erlaubt, das Medium trÀgt die Notiz.
+ note = _create_note(client, user, text="")
+ r = client.post(
+ f"/api/notes/{note['id']}/media",
+ headers=user["headers"],
+ files={"file": ("sprachnachricht.m4a", _M4A_BYTES, "audio/mp4")},
+ )
+ assert r.status_code == 200, r.text
+ m = r.json()
+ assert m["media_type"] == "audio"
+ assert m["url"].endswith(".m4a")
+ assert os.path.exists(_media_path(m["url"]))
+
+
+def test_delete_media_removes_row_and_file(client, user):
+ note = _create_note(client, user)
+ up = client.post(f"/api/notes/{note['id']}/media", headers=user["headers"],
+ files={"file": ("x.jpg", _jpeg_bytes(), "image/jpeg")}).json()
+ path = _media_path(up["url"])
+ assert os.path.exists(path)
+
+ r = client.delete(f"/api/notes/{note['id']}/media/{up['id']}", headers=user["headers"])
+ assert r.status_code == 204
+ assert not os.path.exists(path)
+
+ g = client.get("/api/notes/diary/1", headers=user["headers"]).json()
+ target = next(n for n in g if n["id"] == note["id"])
+ assert target["media_items"] == []
+
+
+def test_delete_note_removes_media_files(client, user):
+ note = _create_note(client, user)
+ up = client.post(f"/api/notes/{note['id']}/media", headers=user["headers"],
+ files={"file": ("y.jpg", _jpeg_bytes(), "image/jpeg")}).json()
+ path = _media_path(up["url"])
+ assert os.path.exists(path)
+
+ r = client.delete(f"/api/notes/{note['id']}", headers=user["headers"])
+ assert r.status_code == 204
+ assert not os.path.exists(path)
+
+
+def test_upload_rejects_corrupt_image(client, user):
+ note = _create_note(client, user)
+ r = client.post(
+ f"/api/notes/{note['id']}/media",
+ headers=user["headers"],
+ files={"file": ("fake.jpg", b"this is not a jpeg", "image/jpeg")},
+ )
+ assert r.status_code == 415
+
+
+def test_upload_media_requires_own_note(client, user):
+ r = client.post(
+ "/api/notes/999999/media",
+ headers=user["headers"],
+ files={"file": ("z.jpg", _jpeg_bytes(), "image/jpeg")},
+ )
+ assert r.status_code == 404
+
+
+def test_audio_utils_unit():
+ """to_m4a reicht bereits-AAC durch; validate_audio prĂŒft Magic-Bytes."""
+ from media_utils import to_m4a, validate_audio
+ data, ext = to_m4a(_M4A_BYTES, ".m4a")
+ assert ext == ".m4a"
+ assert data == _M4A_BYTES # schon AAC â keine Transkodierung
+ validate_audio(_M4A_BYTES, "audio/mp4") # kein Raise
+ with pytest.raises(ValueError):
+ validate_audio(b"xxxx", "audio/mp4")