Bündel 2: Zentrale Helper für DRY-Cleanup, SW by-v1114
NEUE BACKEND-MODULE:
math_utils.py
- haversine_km(lat1, lon1, lat2, lon2) — Distanz in km
- haversine_m(...) — Convenience-Wrapper in Metern
- bbox_deg_from_km(lat, radius_km) — Bounding-Box-Approximation
für SQL-Vorfilter (statt Haversine im Python-Loop)
config.py
- DB_PATH, MEDIA_DIR, BREEDER_DOCS_DIR, SCANINPUT_DIR
- API_TIMEOUT_SHORT (5s) / DEFAULT (10s) / LONG (30s)
- HTTP_USER_AGENT, HTTP_HEADERS
errors.py
- not_found(msg), forbidden(msg), bad_request(msg), unauthorized(msg)
- conflict(msg), too_many_requests(msg, retry_after), service_unavailable(msg)
- require_or_404(row, msg) — Convenience-Helper
UI.JS ERWEITERUNGEN:
UI.time erweitert:
- formatDate(d) → "15.03.2026"
- formatDateTime(d) → "15.03.2026, 14:30"
- weekday(d) → "Di"
- parseISO(str) → {year, month, day}
UI.text (neu):
- truncate(str, maxLen, ellipsis='…')
- slug(str) — URL-Slug aus String (mit DE-Umlauten)
UI.money (neu):
- format(value) → "12,34 €" (de-DE, EUR)
- formatWithSuffix(value, '/Jahr')
HAVERSINE-MIGRATION (13 Backend-Routen):
alerts.py, services.py, places.py, events.py, diary.py, playdate.py,
lost.py, poison.py, adoption.py, gassi_zeiten.py, sitting.py, routen.py,
walks.py
- Alle lokalen def _haversine/haversine_km entfernt
- Aufrufe ersetzt durch haversine_km/haversine_m je nach Einheit
- from math_utils import haversine_km|haversine_m in jeder Datei
Tests 19/19 grün.
Hinweis: Migrationen für MEDIA_DIR (19 Stellen), API-Timeouts (12),
Date-Formatter im Frontend (24) und UI.text.truncate (5) sind als
Folge-Sprints möglich. Helper sind verfügbar.
This commit is contained in:
parent
c517c9281d
commit
297bd22f96
22 changed files with 225 additions and 202 deletions
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1113"></script>
|
||||
<script src="/js/boot-early.js?v=1114"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1113">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1113">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1113">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1113">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1113">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1114">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1114">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1114">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1114">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1114">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -617,11 +617,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1113"></script>
|
||||
<script src="/js/ui.js?v=1113"></script>
|
||||
<script src="/js/app.js?v=1113"></script>
|
||||
<script src="/js/worlds.js?v=1113"></script>
|
||||
<script src="/js/offline-indicator.js?v=1113"></script>
|
||||
<script src="/js/api.js?v=1114"></script>
|
||||
<script src="/js/ui.js?v=1114"></script>
|
||||
<script src="/js/app.js?v=1114"></script>
|
||||
<script src="/js/worlds.js?v=1114"></script>
|
||||
<script src="/js/offline-indicator.js?v=1114"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -631,7 +631,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1113"></script>
|
||||
<script src="/js/boot.js?v=1114"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1113'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1114'; // ← 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;
|
||||
|
|
|
|||
|
|
@ -490,13 +490,73 @@ const UI = (() => {
|
|||
return fmtDate.format(new Date(dateStr));
|
||||
}
|
||||
|
||||
// Datum: "15.03.2026" / "15.03.2026, 14:30"
|
||||
const _fmtDateNumeric = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||||
});
|
||||
const _fmtDateTimeNumeric = new Intl.DateTimeFormat('de-DE', {
|
||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit'
|
||||
});
|
||||
// Wochentag: "Di." / Tag im Monat: "15"
|
||||
const _fmtWeekday = new Intl.DateTimeFormat('de-DE', { weekday: 'short' });
|
||||
|
||||
return {
|
||||
relative,
|
||||
format: d => fmtDate.format(new Date(d)),
|
||||
formatShort: d => fmtDateShort.format(new Date(d)),
|
||||
format: d => fmtDate.format(new Date(d)),
|
||||
formatShort: d => fmtDateShort.format(new Date(d)),
|
||||
formatDate: d => _fmtDateNumeric.format(new Date(d)), // 15.03.2026
|
||||
formatDateTime:d => _fmtDateTimeNumeric.format(new Date(d)), // 15.03.2026, 14:30
|
||||
weekday: d => _fmtWeekday.format(new Date(d)).replace('.', ''),
|
||||
// ISO-Parser: "2026-03-15" → { year, month, day }
|
||||
parseISO(str) {
|
||||
if (!str) return null;
|
||||
const m = String(str).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
return m ? { year: +m[1], month: +m[2], day: +m[3] } : null;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// TEXT — String-Helper
|
||||
// ----------------------------------------------------------
|
||||
const text = {
|
||||
/** Schneidet str auf maxLen ab und hängt ellipsis an. */
|
||||
truncate(str, maxLen = 80, ellipsis = '…') {
|
||||
if (!str) return '';
|
||||
const s = String(str);
|
||||
return s.length <= maxLen ? s : s.slice(0, maxLen - ellipsis.length) + ellipsis;
|
||||
},
|
||||
/** Slug aus String — für URL-Pfade. */
|
||||
slug(str) {
|
||||
return String(str || '')
|
||||
.toLowerCase()
|
||||
.replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// MONEY — Currency-Formatierung (de-DE, EUR)
|
||||
// ----------------------------------------------------------
|
||||
const _fmtEur = new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency', currency: 'EUR',
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2,
|
||||
});
|
||||
const money = {
|
||||
/** Formatiert Zahl als "12,34 €" — null/undefined ergibt "—". */
|
||||
format(value) {
|
||||
if (value == null || value === '' || isNaN(value)) return '—';
|
||||
return _fmtEur.format(Number(value));
|
||||
},
|
||||
/** Mit Suffix wie "/Jahr". */
|
||||
formatWithSuffix(value, suffix = '') {
|
||||
if (value == null || value === '' || isNaN(value)) return '—';
|
||||
return _fmtEur.format(Number(value)) + (suffix ? ' ' + suffix : '');
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// FOTO-VORSCHAU (Input[type=file] → img)
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -1272,7 +1332,7 @@ const UI = (() => {
|
|||
toast, modal,
|
||||
setLoading, asyncButton,
|
||||
formData, setFormError, clearFormErrors,
|
||||
emptyState, errorState, time,
|
||||
emptyState, errorState, time, text, money,
|
||||
previewUrl, previewFallback,
|
||||
setupPhotoPreview, scrollTop, skeleton, skeletonList,
|
||||
moneyInput, parseMoney, datePicker,
|
||||
|
|
|
|||
|
|
@ -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=1113"></script>
|
||||
<script src="/js/landing-init.js?v=1114"></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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1113';
|
||||
const VER = '1114';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue