Feature: Meine Wetterrekorde Sektion auf Wetter-Seite (SW by-v694)
- Backend: GET /api/weather/records — liest diary-Einträge mit weather_json und berechnet Kältester/Heißester Gassi, Stürmischster Tag, Regentage - Frontend: #wttr-records 2×2 Grid-Karten unterhalb Hunde-Wetter (nur für eingeloggte User mit ≥3 Tagebucheinträgen mit Wetterdaten) - SW-Version auf by-v694 erhöht, APP_VER auf 694
This commit is contained in:
parent
d081029618
commit
6152d6bf0e
5 changed files with 173 additions and 12 deletions
|
|
@ -3,9 +3,11 @@ BAN YARO — Wetter-API
|
||||||
GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort
|
GET /api/weather?lat=&lon= → aktuelles Wetter + Zecken-Warnung für Nutzerstandort
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
from fastapi import APIRouter, Query, HTTPException, Depends
|
from fastapi import APIRouter, Query, HTTPException, Depends
|
||||||
import weather as weather_module
|
import weather as weather_module
|
||||||
from auth import get_current_user
|
from auth import get_current_user
|
||||||
|
from database import db
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -31,3 +33,57 @@ async def get_weather_forecast(
|
||||||
return await weather_module.get_forecast(lat, lon)
|
return await weather_module.get_forecast(lat, lon)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise HTTPException(503, f'Wettervorhersage nicht verfügbar: {exc}')
|
raise HTTPException(503, f'Wettervorhersage nicht verfügbar: {exc}')
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/records')
|
||||||
|
async def weather_records(user=Depends(get_current_user)):
|
||||||
|
"""Persönliche Wetterrekorde aus diary-Einträgen mit weather_json."""
|
||||||
|
uid = user["id"]
|
||||||
|
with db() as conn:
|
||||||
|
rows = conn.execute("""
|
||||||
|
SELECT d.datum, d.weather_json, d.titel
|
||||||
|
FROM diary d
|
||||||
|
WHERE d.user_id = ? AND d.weather_json IS NOT NULL
|
||||||
|
ORDER BY d.datum ASC
|
||||||
|
""", (uid,)).fetchall()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
return {"records": None}
|
||||||
|
|
||||||
|
entries = []
|
||||||
|
for r in rows:
|
||||||
|
try:
|
||||||
|
w = json.loads(r["weather_json"])
|
||||||
|
entries.append({
|
||||||
|
"datum": r["datum"],
|
||||||
|
"titel": r["titel"],
|
||||||
|
"temp_c": w.get("temp_c"),
|
||||||
|
"wind_kmh": w.get("wind_kmh"),
|
||||||
|
"precip_prob": w.get("precip_prob"),
|
||||||
|
"desc": w.get("desc", ""),
|
||||||
|
"weathercode": w.get("weathercode"),
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not entries:
|
||||||
|
return {"records": None}
|
||||||
|
|
||||||
|
temps = [e for e in entries if e["temp_c"] is not None]
|
||||||
|
winds = [e for e in entries if e["wind_kmh"] is not None]
|
||||||
|
|
||||||
|
records = {}
|
||||||
|
if temps:
|
||||||
|
kaeltester = min(temps, key=lambda e: e["temp_c"])
|
||||||
|
heissester = max(temps, key=lambda e: e["temp_c"])
|
||||||
|
records["kaeltester"] = kaeltester
|
||||||
|
records["heissester"] = heissester
|
||||||
|
if winds:
|
||||||
|
stuermischster = max(winds, key=lambda e: e["wind_kmh"])
|
||||||
|
records["stuermischster"] = stuermischster
|
||||||
|
|
||||||
|
regen_count = sum(1 for e in entries if (e.get("precip_prob") or 0) > 60)
|
||||||
|
records["regen_eintraege"] = regen_count
|
||||||
|
records["gesamt_eintraege"] = len(entries)
|
||||||
|
|
||||||
|
return {"records": records}
|
||||||
|
|
|
||||||
|
|
@ -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=693">
|
<link rel="stylesheet" href="/css/design-system.css?v=694">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=693">
|
<link rel="stylesheet" href="/css/layout.css?v=694">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=693">
|
<link rel="stylesheet" href="/css/components.css?v=694">
|
||||||
</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=693"></script>
|
<script src="/js/worlds.js?v=694"></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 = '693'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '694'; // ← 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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,11 +55,12 @@ window.Page_wetter = (() => {
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// MODUL-STATE
|
// MODUL-STATE
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
let _container = null;
|
let _container = null;
|
||||||
let _appState = null;
|
let _appState = null;
|
||||||
let _data = null;
|
let _data = null;
|
||||||
let _selDay = 0;
|
let _selDay = 0;
|
||||||
let _loading = false;
|
let _loading = false;
|
||||||
|
let _recordsLoaded = false;
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// INIT
|
// INIT
|
||||||
|
|
@ -76,7 +77,8 @@ window.Page_wetter = (() => {
|
||||||
// REFRESH
|
// REFRESH
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
_selDay = 0;
|
_selDay = 0;
|
||||||
|
_recordsLoaded = false;
|
||||||
_renderShell();
|
_renderShell();
|
||||||
_tryAutoLocate();
|
_tryAutoLocate();
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +197,10 @@ window.Page_wetter = (() => {
|
||||||
<!-- Hunde-Wetter -->
|
<!-- Hunde-Wetter -->
|
||||||
<div id="wttr-dog" class="section-card">
|
<div id="wttr-dog" class="section-card">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Meine Wetterrekorde -->
|
||||||
|
<div id="wttr-records">
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Strip-Klick-Events
|
// Strip-Klick-Events
|
||||||
|
|
@ -211,6 +217,7 @@ window.Page_wetter = (() => {
|
||||||
_renderDetail();
|
_renderDetail();
|
||||||
_renderRainTimeline();
|
_renderRainTimeline();
|
||||||
_renderDog();
|
_renderDog();
|
||||||
|
_loadRecords();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
@ -972,6 +979,104 @@ window.Page_wetter = (() => {
|
||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
// MEINE WETTERREKORDE
|
||||||
|
// ----------------------------------------------------------
|
||||||
|
async function _loadRecords() {
|
||||||
|
// Nur wenn User eingeloggt
|
||||||
|
if (!_appState?.user) return;
|
||||||
|
// Nur einmal pro Seitenaufruf laden
|
||||||
|
if (_recordsLoaded) return;
|
||||||
|
_recordsLoaded = true;
|
||||||
|
try {
|
||||||
|
const res = await API.get('/weather/records');
|
||||||
|
_renderRecords(res?.records || null);
|
||||||
|
} catch {
|
||||||
|
// Stumm scheitern — Rekorde sind ein Nice-to-have
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _fmtDate(datum) {
|
||||||
|
if (!datum) return '';
|
||||||
|
try {
|
||||||
|
return new Date(datum + 'T12:00').toLocaleDateString('de', {
|
||||||
|
day: 'numeric', month: 'short', year: 'numeric'
|
||||||
|
});
|
||||||
|
} catch { return datum; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function _recordCard(emoji, title, value, subtitle, color) {
|
||||||
|
return `
|
||||||
|
<div style="background:var(--c-bg-card);border:1px solid var(--c-border);
|
||||||
|
border-radius:var(--radius);padding:var(--space-3) var(--space-3);
|
||||||
|
display:flex;flex-direction:column;gap:2px">
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||||
|
display:flex;align-items:center;gap:4px;font-weight:600">
|
||||||
|
<span>${emoji}</span>
|
||||||
|
<span>${_esc(title)}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-xl);font-weight:800;color:${color};line-height:1.1">
|
||||||
|
${_esc(value)}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
||||||
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
|
||||||
|
${_esc(subtitle)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderRecords(records) {
|
||||||
|
const el = _container.querySelector('#wttr-records');
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
// Mindestens 3 Einträge nötig
|
||||||
|
if (!records || (records.gesamt_eintraege || 0) < 3) {
|
||||||
|
el.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [];
|
||||||
|
|
||||||
|
if (records.kaeltester) {
|
||||||
|
const e = records.kaeltester;
|
||||||
|
const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum);
|
||||||
|
cards.push(_recordCard('🥶', 'Kältester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#60A5FA'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.heissester) {
|
||||||
|
const e = records.heissester;
|
||||||
|
const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum);
|
||||||
|
cards.push(_recordCard('🔥', 'Heißester Gassi', `${Math.round(e.temp_c)}°C`, sub, '#EF4444'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.stuermischster) {
|
||||||
|
const e = records.stuermischster;
|
||||||
|
const sub = e.titel ? `${e.titel} · ${_fmtDate(e.datum)}` : _fmtDate(e.datum);
|
||||||
|
cards.push(_recordCard('🌬️', 'Stürmischster Tag', `${Math.round(e.wind_kmh)} km/h`, sub, '#A78BFA'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const regenCount = records.regen_eintraege || 0;
|
||||||
|
const gesamt = records.gesamt_eintraege || 0;
|
||||||
|
cards.push(_recordCard('💧', 'Regentage', `${regenCount} Einträge`, `von ${gesamt} Tagebucheinträgen`, '#3B82F6'));
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
<div style="margin-top:var(--space-5)">
|
||||||
|
<h3 style="font-size:var(--text-base);font-weight:700;
|
||||||
|
margin-bottom:var(--space-3);
|
||||||
|
display:flex;align-items:center;gap:var(--space-2)">
|
||||||
|
<svg class="ph-icon" style="width:1.1em;height:1.1em;vertical-align:-2px;color:var(--c-primary)">
|
||||||
|
<use href="/icons/phosphor.svg#trophy"></use>
|
||||||
|
</svg>
|
||||||
|
Meine Wetterrekorde
|
||||||
|
</h3>
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2)">
|
||||||
|
${cards.join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
// PUBLIC API
|
// PUBLIC API
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v693';
|
const CACHE_VERSION = 'by-v694';
|
||||||
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