Feature: Fell-Typ Einstellung am Hundeprofil — personalisierte Wetter-Hinweise (SW by-v693)
- DB-Migration: dogs.fell_typ (kurz|mittel|lang|drahtaar|doppel|nackt)
- Hund-Profil Formular: Dropdown "Felltyp" mit 6 Optionen, wird via PATCH /api/dogs/{id} gespeichert
- Wetter: _dogWeatherLabel(d, felltyp) mit fell-spezifischen Hitze-/Kälteschwellen
- Wetter: Fell-spezifische Hinweise (doppel + Hitze, nackt + Kälte, kurz + Frost)
This commit is contained in:
parent
471633867c
commit
af1508c0de
6 changed files with 80 additions and 12 deletions
|
|
@ -540,6 +540,9 @@ def _migrate(conn_factory):
|
||||||
("pflege_tipps", "fell_pflege_art", "TEXT"),
|
("pflege_tipps", "fell_pflege_art", "TEXT"),
|
||||||
# Wiki-Foto-Einreichungen: Bildrechte-Bestätigung
|
# Wiki-Foto-Einreichungen: Bildrechte-Bestätigung
|
||||||
("wiki_foto_submissions", "rights_confirmed", "INTEGER NOT NULL DEFAULT 0"),
|
("wiki_foto_submissions", "rights_confirmed", "INTEGER NOT NULL DEFAULT 0"),
|
||||||
|
# Tagebuch-Medien: Bildmaße für Querformat-Filter
|
||||||
|
("diary_media", "img_width", "INTEGER"),
|
||||||
|
("diary_media", "img_height", "INTEGER"),
|
||||||
# Tagebuch: Wetter + POI-Metadaten beim Eintrag
|
# Tagebuch: Wetter + POI-Metadaten beim Eintrag
|
||||||
("diary", "weather_json", "TEXT"),
|
("diary", "weather_json", "TEXT"),
|
||||||
("diary", "poi_json", "TEXT"),
|
("diary", "poi_json", "TEXT"),
|
||||||
|
|
@ -568,6 +571,8 @@ def _migrate(conn_factory):
|
||||||
# Passwort-Zurücksetzen
|
# Passwort-Zurücksetzen
|
||||||
("users", "password_reset_token", "TEXT"),
|
("users", "password_reset_token", "TEXT"),
|
||||||
("users", "password_reset_expires", "TEXT"),
|
("users", "password_reset_expires", "TEXT"),
|
||||||
|
# Fell-Typ für personalisierte Wetter-Hinweise
|
||||||
|
("dogs", "fell_typ", "TEXT"), # kurz|mittel|lang|drahtaar|doppel|nackt
|
||||||
]
|
]
|
||||||
with conn_factory() as conn:
|
with conn_factory() as conn:
|
||||||
for table, column, col_type in migrations:
|
for table, column, col_type in migrations:
|
||||||
|
|
@ -1940,6 +1945,12 @@ def _migrate(conn_factory):
|
||||||
CREATE INDEX IF NOT EXISTS idx_dq_kategorie ON daily_quotes(kategorie);
|
CREATE INDEX IF NOT EXISTS idx_dq_kategorie ON daily_quotes(kategorie);
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Goldene Gassi-Stunde: User-Einstellung
|
||||||
|
existing_u = [row[1] for row in conn.execute("PRAGMA table_info(users)").fetchall()]
|
||||||
|
if 'gassi_stunde_push' not in existing_u:
|
||||||
|
conn.execute("ALTER TABLE users ADD COLUMN gassi_stunde_push INTEGER NOT NULL DEFAULT 0")
|
||||||
|
logger.info("Migration: users.gassi_stunde_push bereit.")
|
||||||
|
|
||||||
# Wiederkehrende Ausgaben (Daueraufträge)
|
# Wiederkehrende Ausgaben (Daueraufträge)
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS recurring_expenses (
|
CREATE TABLE IF NOT EXISTS recurring_expenses (
|
||||||
|
|
|
||||||
|
|
@ -93,9 +93,9 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css?v=690">
|
<link rel="stylesheet" href="/css/design-system.css?v=693">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=690">
|
<link rel="stylesheet" href="/css/layout.css?v=693">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=690">
|
<link rel="stylesheet" href="/css/components.css?v=693">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -562,7 +562,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=690"></script>
|
<script src="/js/worlds.js?v=693"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '692'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '693'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -970,6 +970,23 @@ window.Page_dog_profile = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Felltyp
|
||||||
|
<span style="color:var(--c-text-secondary)">(optional)</span>
|
||||||
|
${UI.help('Der Felltyp wird für personalisierte Wetter-Hinweise genutzt.')}
|
||||||
|
</label>
|
||||||
|
<select class="form-control" name="fell_typ">
|
||||||
|
<option value="" ${!dog?.fell_typ ? 'selected' : ''}>– nicht angegeben –</option>
|
||||||
|
<option value="kurz" ${dog?.fell_typ === 'kurz' ? 'selected' : ''}>Kurzhaar (Labrador, Boxer)</option>
|
||||||
|
<option value="mittel" ${dog?.fell_typ === 'mittel' ? 'selected' : ''}>Mittellang (Spaniel, Husky)</option>
|
||||||
|
<option value="lang" ${dog?.fell_typ === 'lang' ? 'selected' : ''}>Langhaar (Collie, Berner Senne)</option>
|
||||||
|
<option value="drahtaar" ${dog?.fell_typ === 'drahtaar' ? 'selected' : ''}>Drahthaar (Terrier, Schnauzer)</option>
|
||||||
|
<option value="doppel" ${dog?.fell_typ === 'doppel' ? 'selected' : ''}>Doppeltes Unterfell (Husky, Malamute, Samojede)</option>
|
||||||
|
<option value="nackt" ${dog?.fell_typ === 'nackt' ? 'selected' : ''}>Nackthund (Chinese Crested, Xolo)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Bio / Steckbrief
|
Bio / Steckbrief
|
||||||
|
|
@ -1136,6 +1153,7 @@ window.Page_dog_profile = (() => {
|
||||||
chip_nr: fd.chip_nr || null,
|
chip_nr: fd.chip_nr || null,
|
||||||
bio: fd.bio || null,
|
bio: fd.bio || null,
|
||||||
is_public: 'is_public' in fd,
|
is_public: 'is_public' in fd,
|
||||||
|
fell_typ: fd.fell_typ || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
let saved;
|
let saved;
|
||||||
|
|
|
||||||
|
|
@ -531,7 +531,8 @@ window.Page_wetter = (() => {
|
||||||
if (!d) return;
|
if (!d) return;
|
||||||
|
|
||||||
const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' };
|
const _POLLEN_NAMES = { erle:'Erle', birke:'Birke', graeser:'Gräser', beifuss:'Beifuß', ambrosia:'Ambrosia' };
|
||||||
const _wl = _dogWeatherLabel(d);
|
const felltyp = (_appState?.activeDog ?? _appState?.dogs?.[0])?.fell_typ || null;
|
||||||
|
const _wl = _dogWeatherLabel(d, felltyp);
|
||||||
let html = `
|
let html = `
|
||||||
<div style="border-radius:var(--radius);padding:var(--space-4);
|
<div style="border-radius:var(--radius);padding:var(--space-4);
|
||||||
background:${_wl.color}18;border:1px solid ${_wl.color}44;
|
background:${_wl.color}18;border:1px solid ${_wl.color}44;
|
||||||
|
|
@ -653,6 +654,35 @@ window.Page_wetter = (() => {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fell-spezifische Hinweise
|
||||||
|
if (felltyp) {
|
||||||
|
const tempNow = d.temp_max ?? 20;
|
||||||
|
let fellHint = null;
|
||||||
|
if (felltyp === 'doppel' && tempNow > 20) {
|
||||||
|
fellHint = { icon: 'thermometer-hot', color: '#F97316',
|
||||||
|
text: 'Doppeltes Fell — heute besonders auf Überhitzung achten.' };
|
||||||
|
} else if (felltyp === 'nackt' && tempNow < 15) {
|
||||||
|
fellHint = { icon: 'coat-hanger', color: '#60A5FA',
|
||||||
|
text: 'Nackthund braucht heute eine Hundejacke oder einen -pullover.' };
|
||||||
|
} else if (felltyp === 'kurz' && tempNow < 5) {
|
||||||
|
fellHint = { icon: 'snowflake', color: '#38BDF8',
|
||||||
|
text: 'Kurzhaar friert schnell — Hundemantel empfohlen.' };
|
||||||
|
}
|
||||||
|
if (fellHint) {
|
||||||
|
html += `
|
||||||
|
<div style="display:flex;align-items:center;gap:var(--space-3);
|
||||||
|
padding:var(--space-3);border-radius:var(--radius);
|
||||||
|
background:${fellHint.color}1a;border:1px solid ${fellHint.color}55;
|
||||||
|
margin-bottom:var(--space-3)">
|
||||||
|
<svg class="ph-icon" style="width:1.3rem;height:1.3rem;flex-shrink:0;color:${fellHint.color}">
|
||||||
|
<use href="/icons/phosphor.svg#${fellHint.icon}"></use>
|
||||||
|
</svg>
|
||||||
|
<div style="font-size:var(--text-sm)">${_esc(fellHint.text)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Schnüffel-Index + Hunde-Alter Chips
|
// Schnüffel-Index + Hunde-Alter Chips
|
||||||
const ageYears = _dogAgeYears();
|
const ageYears = _dogAgeYears();
|
||||||
html += _dogAgeChip(ageYears);
|
html += _dogAgeChip(ageYears);
|
||||||
|
|
@ -874,7 +904,7 @@ window.Page_wetter = (() => {
|
||||||
return '#F44336'; // level 4+
|
return '#F44336'; // level 4+
|
||||||
}
|
}
|
||||||
|
|
||||||
function _dogWeatherLabel(d) {
|
function _dogWeatherLabel(d, felltyp) {
|
||||||
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;
|
||||||
|
|
@ -882,6 +912,15 @@ window.Page_wetter = (() => {
|
||||||
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;
|
||||||
|
|
||||||
|
// Fell-spezifische Temperaturschwellen
|
||||||
|
const heatLimit = {
|
||||||
|
kurz: 25, mittel: 27, lang: 22, drahtaar: 26, doppel: 30, nackt: 20
|
||||||
|
}[felltyp] ?? 28;
|
||||||
|
const coldLimit = {
|
||||||
|
kurz: 8, mittel: 5, lang: 3, drahtaar: 5, doppel: -5, nackt: 15
|
||||||
|
}[felltyp] ?? 5;
|
||||||
|
|
||||||
if (d.thunderstorm)
|
if (d.thunderstorm)
|
||||||
return { label:'Gewitterangst-Wetter', sub:'Angsthasen lieber zu Hause lassen', emoji:'⛈️', color:'#7C3AED' };
|
return { label:'Gewitterangst-Wetter', sub:'Angsthasen lieber zu Hause lassen', emoji:'⛈️', color:'#7C3AED' };
|
||||||
if (isSnow && temp < 3)
|
if (isSnow && temp < 3)
|
||||||
|
|
@ -890,13 +929,13 @@ window.Page_wetter = (() => {
|
||||||
return { label:'Matschpfoten-Wetter', sub:'Pfoten nach der Runde gut abtrocknen', emoji:'🌨️', color:'#60A5FA' };
|
return { label:'Matschpfoten-Wetter', sub:'Pfoten nach der Runde gut abtrocknen', emoji:'🌨️', color:'#60A5FA' };
|
||||||
if (tempMin < 0 && precip < 30)
|
if (tempMin < 0 && precip < 30)
|
||||||
return { label:'Kristallklare Nasenluft', sub:'Kalt aber herrlich — Schnüffeln auf Maximum', emoji:'🌡️', color:'#60A5FA' };
|
return { label:'Kristallklare Nasenluft', sub:'Kalt aber herrlich — Schnüffeln auf Maximum', emoji:'🌡️', color:'#60A5FA' };
|
||||||
if (temp < 5 && precip > 50)
|
if (temp < coldLimit && precip > 50)
|
||||||
return { label:'Kuschelwetter', sub:'Kurze Runde, dann ab auf das Sofa', emoji:'🏠', color:'#6B7280' };
|
return { label:'Kuschelwetter', sub:'Kurze Runde, dann ab auf das Sofa', emoji:'🏠', color:'#6B7280' };
|
||||||
if (temp < 5)
|
if (temp < coldLimit)
|
||||||
return { label:'Fellkuschelwetter', sub:'Frisch und klar — ideal für aktive Rassen', emoji:'🧣', color:'#93C5FD' };
|
return { label:'Fellkuschelwetter', sub:'Frisch und klar — ideal für aktive Rassen', emoji:'🧣', color:'#93C5FD' };
|
||||||
if (temp > 28 && asphalt > 50)
|
if (temp > heatLimit && asphalt > 50)
|
||||||
return { label:'Pfoten-Alarm!', sub:'Asphalt zu heiß — früh morgens oder abends raus', emoji:'🔥', color:'#EF4444' };
|
return { label:'Pfoten-Alarm!', sub:'Asphalt zu heiß — früh morgens oder abends raus', emoji:'🔥', color:'#EF4444' };
|
||||||
if (temp > 28)
|
if (temp > heatLimit)
|
||||||
return { label:'Schwimm-Wetter', sub:'Bach oder See suchen — Hunde überhitzen schnell', emoji:'🏊', color:'#F97316' };
|
return { label:'Schwimm-Wetter', sub:'Bach oder See suchen — Hunde überhitzen schnell', emoji:'🏊', color:'#F97316' };
|
||||||
if (precip > 70 && temp < 15)
|
if (precip > 70 && temp < 15)
|
||||||
return { label:'Nass-Hund-Wetter', sub:'Handtuch bereit? Der Geruch kommt garantiert', emoji:'💧', color:'#3B82F6' };
|
return { label:'Nass-Hund-Wetter', sub:'Handtuch bereit? Der Geruch kommt garantiert', emoji:'💧', color:'#3B82F6' };
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v692';
|
const CACHE_VERSION = 'by-v693';
|
||||||
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue