From b1d9fb4f54e8598ca1bc853eb13ce4239afadbc0 Mon Sep 17 00:00:00 2001 From: rene Date: Mon, 4 May 2026 20:30:06 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Wetter-Verbesserung=20im=20Tagebuch?= =?UTF-8?q?=20=E2=80=94=20Auto-Wetter,=20Chip-Fix,=20Detail-Fix=20(SW=20by?= =?UTF-8?q?-v695)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - diary.js: Weather-Chip in der Liste nutzt jetzt temp_c (korrekter Feldname) - diary.js: Detail-View zeigt "emoji temp · X km/h Wind · Y% Regen" (precip_prob statt Luftfeuchtigkeit) - diary.js: Bei neuem Eintrag ohne GPS → Wetter wird via GPS-API vorgeholt und als weather_json mitgesendet - diary.py: DiaryCreate-Modell um weather_json-Feld erweitert; client-geliefertes Wetter wird gespeichert wenn kein GPS-basiertes Wetter verfügbar - SW by-v695, APP_VER 695 --- backend/routes/diary.py | 27 ++++++++++++++++++++++----- backend/static/js/app.js | 2 +- backend/static/js/pages/diary.js | 28 +++++++++++++++++++--------- backend/static/sw.js | 2 +- 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/backend/routes/diary.py b/backend/routes/diary.py index a3dee2b..6f6cd12 100644 --- a/backend/routes/diary.py +++ b/backend/routes/diary.py @@ -9,7 +9,7 @@ from auth import get_current_user, require_admin import ki as KI import httpx import weather as weather_mod -from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from +from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from, get_image_size from timeutils import safe_client_time logger = logging.getLogger(__name__) @@ -30,6 +30,7 @@ class DiaryCreate(BaseModel): location_name: Optional[str] = None is_milestone: bool = False dog_ids: Optional[list[int]] = None # alle Hunde inkl. primär; None = nur primary + weather_json: Optional[str] = None # Client-seitig vorab geholtes Wetter (Fallback wenn kein GPS) class DiaryUpdate(BaseModel): @@ -350,6 +351,19 @@ async def create_diary(dog_id: int, data: DiaryCreate, ) entry = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() + elif data.weather_json: + # Client hat Wetter vorab geholt (kein GPS-Standort gesetzt) → direkt speichern + try: + json.loads(data.weather_json) # Validierung + with db() as conn: + conn.execute( + "UPDATE diary SET weather_json=? WHERE id=?", + (data.weather_json, entry_id) + ) + entry = conn.execute("SELECT * FROM diary WHERE id=?", (entry_id,)).fetchone() + except Exception as exc: + logger.warning("Client-weather_json ungültig: %s", exc) + return _entry_dict(entry, dogs_map, media_map) @@ -692,10 +706,12 @@ async def upload_media(dog_id: int, entry_id: int, media_url = f"/media/diary/{filename}" - # EXIF-GPS aus Bild extrahieren (nur bei Bilddateien) - exif_gps = None + # Bildmaße + EXIF-GPS (nur bei Bilddateien) + exif_gps = None + img_size = None if media_type == "image": exif_gps = extract_gps_from_exif(raw_data) + img_size = get_image_size(raw_data) with db() as conn: # sort_order = nächste freie Position @@ -706,8 +722,9 @@ async def upload_media(dog_id: int, entry_id: int, # Erstes Item eines Eintrags wird automatisch Cover is_cover = 1 if max_order == -1 else 0 conn.execute( - "INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover) VALUES (?,?,?,?,?)", - (entry_id, media_url, media_type, max_order + 1, is_cover) + "INSERT INTO diary_media (diary_id, url, media_type, sort_order, is_cover, img_width, img_height) VALUES (?,?,?,?,?,?,?)", + (entry_id, media_url, media_type, max_order + 1, is_cover, + img_size[0] if img_size else None, img_size[1] if img_size else None) ) new_id = conn.execute( "SELECT id FROM diary_media WHERE diary_id=? ORDER BY id DESC LIMIT 1", diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 393bf53..4b5071c 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '694'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '695'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.3.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index a39eccd..66d9150 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -868,9 +868,9 @@ window.Page_diary = (() => { if (e.weather_json) { try { const w = typeof e.weather_json === 'string' ? JSON.parse(e.weather_json) : e.weather_json; - const temp = w?.temperature_2m ?? w?.temp_c; + const temp = w?.temp_c ?? w?.temperature_2m; if (temp != null) { - metaParts.push(`${_weatherEmoji(w.weather_code ?? w.weathercode, w.is_day)} ${Math.round(temp)}°`); + metaParts.push(`${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°`); } } catch (_) {} } @@ -1073,15 +1073,14 @@ window.Page_diary = (() => { if (entry.weather_json) { try { const w = typeof entry.weather_json === 'string' ? JSON.parse(entry.weather_json) : entry.weather_json; - const temp = w?.temperature_2m ?? w?.temp_c; + const temp = w?.temp_c ?? w?.temperature_2m; if (w && temp != null) { - const feels = w.apparent_temperature ?? w.feels_like_c; - const wind = w.wind_speed_10m ?? w.wind_kmh; + const wind = w.wind_kmh ?? w.wind_speed_10m; + const precip = w.precip_prob; const parts = [ - `${_weatherEmoji(w.weather_code ?? w.weathercode, w.is_day)} ${Math.round(temp)}°C`, - feels != null ? `gefühlt ${Math.round(feels)}°` : null, - wind != null ? `💨 ${Math.round(wind)} km/h` : null, - w.relative_humidity_2m != null ? `💧 ${w.relative_humidity_2m}%` : null, + `${_weatherEmoji(w.weathercode ?? w.weather_code, w.is_day)} ${Math.round(temp)}°C`, + wind != null ? `${Math.round(wind)} km/h Wind` : null, + precip != null ? `${precip}% Regen` : null, ].filter(Boolean).join(' · '); metaItems.push(`${parts}`); } @@ -1728,6 +1727,16 @@ window.Page_diary = (() => { }); await UI.asyncButton(submitBtn, async () => { + // Auto-Wetter: nur bei neuem Eintrag ohne GPS-Standort + let _clientWeather = null; + if (!isEdit && _locLat == null) { + try { + const pos = await API.getLocation(); + const wd = await API.weather.get(pos.lat, pos.lon); + if (wd && wd.temp_c != null) _clientWeather = JSON.stringify(wd); + } catch (_) { /* GPS oder Wetter nicht verfügbar → kein Problem */ } + } + const payload = { datum: fd.datum || null, typ: fd.typ, @@ -1739,6 +1748,7 @@ window.Page_diary = (() => { gps_lon: _locLon, location_name: _locName, client_time: API.clientNow(), + weather_json: _clientWeather, }; async function _uploadNewFiles(entryId) { diff --git a/backend/static/sw.js b/backend/static/sw.js index 7ce0992..28edf7a 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v694'; +const CACHE_VERSION = 'by-v695'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache