Files
wetter/templates/weather.html
simon dc0c555684 modified: app.py
modified:   templates/weather.html
2026-04-24 09:34:33 +02:00

402 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ display_name.split(',')[0] }} Skywatcher{% endblock %}
{% block head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
{% endblock %}
{% block content %}
{# ── Wetterklasse für den Hero-Gradient ─────────────────────────── #}
{% if current.icon == "🌧️" %}{% set wclass = "w-rain" %}
{% elif current.icon == "❄️" %}{% set wclass = "w-snow" %}
{% elif current.icon == "☁️" %}{% set wclass = "w-cloudy" %}
{% elif current.icon == "⛅" %}{% set wclass = "w-partcloud" %}
{% else %}{% set wclass = "w-clear" %}{% endif %}
<!-- HERO -->
<header class="hero {{ wclass }}">
<div class="hero-inner">
<div class="hero-meta">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
{{ display_name.split(',')[0] }}
<span class="hero-meta-sub">{{ display_name.split(',')[1:3]|join(',') if ',' in display_name else '' }}</span>
<span class="hero-meta-time">{{ now_local }} Uhr</span>
</div>
<div class="hero-main">
<div class="hero-temp">{{ current.temp_c if current.temp_c is not none else "" }}°</div>
<div class="hero-desc">
<div class="hero-icon-big">{{ current.icon or "☁️" }}</div>
<div class="hero-stats-mini">
{% if current.wind_kmh is not none %}
<span>{{ current.wind_kmh }} km/h {{ wind_dir_name(current.wind_dir) }}</span>
{% endif %}
{% if current.cloud_pct is not none %}
<span>{{ current.cloud_pct }}% Bedeckung</span>
{% endif %}
{% if current.pressure_hpa is not none %}
<span>{{ current.pressure_hpa }} hPa</span>
{% endif %}
</div>
</div>
</div>
<div class="hero-metrics">
{% if current.uv_index is not none %}
{% set _uv_l, _uv_lv = uv_risk_info(current.uv_index) %}
{% set uv_curr_str = current.uv_index|string + " " + _uv_l %}
{% else %}
{% set uv_curr_str = "" %}
{% endif %}
{% set items = [
("Gefühlt wie", (current.feels_like|string + " °C") if current.feels_like is not none else ""),
("Böen", (current.gust_kmh|string + " km/h") if current.gust_kmh is not none else ""),
("Niederschlag", (current.precip_mm|string + " mm") if (current.precip_mm is not none and current.precip_mm > 0) else ((current.rain_prob|string + " %") if (current.rain_prob is not none and current.rain_prob > 0) else "0 mm")),
("Sonne", (current.sun_min|string + " min/h") if (current.sun_min is not none and current.sun_min > 0) else ""),
("UV-Index", uv_curr_str),
] %}
{% for label, val in items %}
<div class="hero-metric">
<span class="hm-val">{{ val }}</span>
<span class="hm-label">{{ label }}</span>
</div>
{% endfor %}
</div>
</div>
<div class="hero-station">
📡 Station <strong>{{ station_name }}</strong> ({{ station_dist }} km)
{% if sunrise %}
&nbsp;·&nbsp; 🌅 {{ sunrise }} &nbsp;·&nbsp; 🌇 {{ sunset }}
{% endif %}
{% if current.uv_index is not none %}
{% set uv_label, uv_level = uv_risk_info(current.uv_index) %}
&nbsp;·&nbsp; UV {{ current.uv_index }} ({{ uv_label }})
{% endif %}
</div>
</header>
{% if warnings %}
<div class="warnings">
{% for w in warnings %}
<div class="warn-item warn-lvl-{{ w.level }}">
<span class="warn-icon">{{ '⚠️' if w.level == 1 else ('🟠' if w.level == 2 else ('🔴' if w.level >= 3 else '⚠️')) }}</span>
<div>
<strong>{{ w.headline }}</strong>
{% if w.onset_dt or w.expires_dt %}
<p class="warn-time">
{% if w.onset_dt %}{{ w.onset_dt.strftime('%d.%m. %H:%M') }}{% endif %}
{% if w.expires_dt %} {{ w.expires_dt.strftime('%d.%m. %H:%M') }}{% endif %}
</p>
{% endif %}
{% if w.description %}
{% set short_desc = w.description.split('. ')[0] %}
<p class="warn-desc">{{ short_desc }}{% if short_desc|length < w.description|length %}.{% endif %}</p>
{% if short_desc|length < w.description|length %}
<details class="warn-more">
<summary>Details</summary>
<p>{{ w.description }}</p>
</details>
{% endif %}
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
<section class="section insights">
<h2 class="section-title">Kurzfazit</h2>
<div class="insight-grid">
<div class="insight-card">
<span class="insight-label">Temperaturtrend (6h)</span>
<strong class="insight-value">
{% if temp_delta_6h is not none %}
{% if temp_delta_6h > 0 %}+{% endif %}{{ temp_delta_6h }}° · {{ temp_trend_6h }}
{% else %}
{% endif %}
</strong>
</div>
<div class="insight-card">
<span class="insight-label">Drucktrend (6h)</span>
<strong class="insight-value">
{% if pressure_delta is not none %}
{% if pressure_delta > 0 %}+{% endif %}{{ pressure_delta }} hPa · {{ pressure_trend }}
{% else %}
{% endif %}
</strong>
</div>
<div class="insight-card">
<span class="insight-label">Bestes Aktivitätsfenster</span>
<strong class="insight-value">
{% if best_window %}
{{ best_window.start.strftime('%H:%M') }}{{ best_window.end.strftime('%H:%M') }} · Score {{ best_window.score }}
{% else %}
{% endif %}
</strong>
</div>
</div>
</section>
<!-- STÜNDLICH -->
<section class="section">
<h2 class="section-title">Stundenweise</h2>
<div class="hourly-legend">
<span class="hl-label">Legende:</span>
<span class="hl-badge hl-conf hl-conf--hoch">Konfidenz hoch</span>
<span class="hl-badge hl-conf hl-conf--mittel">mittel</span>
<span class="hl-badge hl-conf hl-conf--niedrig">niedrig</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-act">Aktivität 0100</span>
<span class="hl-sep">·</span>
<span class="hl-badge hl-uv">UV-Index</span>
</div>
<div class="hourly-strip-wrap">
<div class="hourly-strip">
{% for h in forecast %}
<div class="hcard {% if loop.first %}hcard--now{% endif %}">
<div class="hcard-time">
{% if loop.first %}Jetzt
{% elif h.datetime is string %}{{ h.datetime[11:16] }}
{% else %}{{ h.datetime.strftime('%H:%M') }}{% endif %}
</div>
<div class="hcard-date">
{% if h.datetime is string %}{{ h.datetime[8:10] }}.{{ h.datetime[5:7] }}.
{% else %}{{ h.datetime.strftime('%d.%m.') }}{% endif %}
</div>
<div class="hcard-icon">{{ h.icon }}</div>
<div class="hcard-temp">
{% if h.temp_c is not none %}{{ h.temp_c }}°{% else %}{% endif %}
</div>
{% if h.precip_mm and h.precip_mm > 0 %}
<div class="hcard-precip">{{ h.precip_mm }} mm</div>
{% elif h.rain_prob is not none and h.rain_prob > 0 %}
<div class="hcard-precip hcard-precip--prob">{{ h.rain_prob }}%</div>
{% else %}
<div class="hcard-precip hcard-precip--none"></div>
{% endif %}
{% if h.wind_kmh is not none %}
<div class="hcard-wind">{{ h.wind_kmh }}<small>km/h</small></div>
{% endif %}
<div class="hcard-confidence hcard-confidence--{{ h.confidence_label }}">{{ h.confidence }}%</div>
<div class="hcard-activity">Aktiv {{ h.activity_score }}</div>
{% if h.uv_index is not none and h.uv_index > 0 %}
<div class="hcard-uv hcard-uv--{{ h.uv_level }}">UV {{ h.uv_index }} {{ h.uv_label }}</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</section>
<!-- CHART -->
<section class="section">
<div class="chart-head">
<h2 class="section-title">Temperatur, Gefühlt &amp; Niederschlag</h2>
<span class="chart-range">72 Stunden</span>
</div>
<div class="chart-box">
<canvas id="wxChart"></canvas>
</div>
</section>
<!-- TAGESÜBERSICHT -->
<section class="section">
<h2 class="section-title">Tagesübersicht</h2>
<div class="daily-header">
<span>Tag</span>
<span>Temperaturbereich</span>
<span>Min &nbsp;·&nbsp; Max &nbsp;·&nbsp; Nieder. &nbsp;·&nbsp; UV</span>
</div>
<div class="daily-list">
{# calc global min/max for bar scaling do it in a loop to stay Jinja2-safe #}
{% set ns = namespace(g_min=99, g_max=-99) %}
{% for d in daily %}
{% if d.temp_min is not none and d.temp_min < ns.g_min %}{% set ns.g_min = d.temp_min %}{% endif %}
{% if d.temp_max is not none and d.temp_max > ns.g_max %}{% set ns.g_max = d.temp_max %}{% endif %}
{% endfor %}
{% set g_min = ns.g_min %}
{% set g_max = ns.g_max %}
{% set g_range = (g_max - g_min) if (g_max - g_min) > 0 else 1 %}
{% for d in daily %}
<div class="drow">
<div class="drow-left">
<span class="drow-icon">{{ d.icon }}</span>
<div class="drow-date-wrap">
<span class="drow-dow">
{% if d.date is string %}
{% set y = d.date[:4]|int %}{% set mo = d.date[5:7]|int %}{% set dy = d.date[8:10]|int %}
{% set days = ["Mo","Di","Mi","Do","Fr","Sa","So"] %}
{{ dy }}.{{ d.date[5:7] }}.
{% else %}
{{ d.date.strftime('%a') }}
{% endif %}
</span>
</div>
</div>
<div class="drow-bar-wrap">
{% if d.temp_min is not none and d.temp_max is not none %}
{% set left_pct = ((d.temp_min - g_min) / g_range * 100)|round(1) %}
{% set width_pct = ((d.temp_max - d.temp_min) / g_range * 100)|round(1) %}
<div class="drow-bar-track">
<div class="drow-bar" style="left:{{ left_pct }}%; width:{{ [width_pct, 4]|max }}%"></div>
</div>
{% endif %}
</div>
<div class="drow-right">
<span class="drow-min">{{ d.temp_min }}°</span>
<span class="drow-max">{{ d.temp_max }}°</span>
{% if d.precip > 0 %}
<span class="drow-precip">🌧 {{ d.precip }}mm</span>
{% elif d.rain_prob is not none and d.rain_prob > 0 %}
<span class="drow-precip">💧 {{ d.rain_prob }}%</span>
{% endif %}
{% if d.uv_max is not none and d.uv_max > 0 %}
<span class="drow-uv">UV {{ d.uv_max }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</section>
<p class="data-note">
Wetterdaten: <a href="https://opendata.dwd.de" target="_blank" rel="noopener noreferrer">Deutscher Wetterdienst Open Data (MOSMIX)</a>
</p>
{% endblock %}
{% block scripts %}
<script>
// ── Letzte Suchen speichern ──────────────────────────────────────────────────
(function(){
const key = "sw_recent";
const ort = {{ ort | tojson }};
let recent = JSON.parse(localStorage.getItem(key) || "[]");
recent = [ort, ...recent.filter(x => x !== ort)].slice(0, 6);
localStorage.setItem(key, JSON.stringify(recent));
})();
</script>
<script>
(function() {
const labels = {{ chart_labels | tojson }};
const temps = {{ chart_temps | tojson }};
const feels = {{ chart_feels | tojson }};
const precip = {{ chart_precip | tojson }};
const rainProb = {{ chart_rain_prob | tojson }};
// Prüfen ob echte Niederschlagsmengen vorhanden sind
const hasRealPrecip = precip.some(v => v > 0);
const barData = hasRealPrecip ? precip : rainProb;
const barLabel = hasRealPrecip ? "Niederschlag (mm)" : "Regenwahrsch. (%)";
const barMax = hasRealPrecip ? undefined : 100;
// Nur jeden 3. Label anzeigen, Rest leer lassen
const sparseLabels = labels.map((l, i) => i % 3 === 0 ? l : "");
const ctx = document.getElementById("wxChart").getContext("2d");
// Gradient für Temperatur-Linie
const grad = ctx.createLinearGradient(0, 0, 0, 300);
grad.addColorStop(0, "rgba(255,140,50,0.35)");
grad.addColorStop(1, "rgba(255,140,50,0)");
new Chart(ctx, {
data: {
labels,
datasets: [
{
type: "line",
label: "Temperatur (°C)",
data: temps,
borderColor: "#ff8c32",
backgroundColor: grad,
borderWidth: 2.5,
tension: 0.45,
fill: true,
yAxisID: "yT",
pointRadius: 0,
pointHoverRadius: 5,
pointHoverBackgroundColor: "#ff8c32",
},
{
type: "line",
label: "Gefühlt (°C)",
data: feels,
borderColor: "#ffd4a8",
borderDash: [5, 4],
borderWidth: 1.8,
tension: 0.45,
fill: false,
yAxisID: "yT",
pointRadius: 0,
pointHoverRadius: 4,
},
{
type: "bar",
label: barLabel,
data: barData,
backgroundColor: "rgba(80,180,255,0.55)",
borderColor: "rgba(80,180,255,0.9)",
borderWidth: 1,
borderRadius: 3,
yAxisID: "yR",
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: "index", intersect: false },
plugins: {
legend: {
labels: { color: "#9aa8b8", font: { family: "Inter", size: 12 }, boxWidth: 14 }
},
tooltip: {
backgroundColor: "rgba(10,12,18,0.92)",
borderColor: "rgba(255,255,255,0.1)",
borderWidth: 1,
titleColor: "#fff",
bodyColor: "#9aa8b8",
padding: 12,
callbacks: {
title: items => labels[items[0].dataIndex],
}
}
},
scales: {
x: {
ticks: {
color: "#5a6a7a",
font: { family: "Inter", size: 11 },
maxRotation: 0,
callback: (_, i) => i % 6 === 0 ? labels[i] : "",
autoSkip: false,
},
grid: { color: "rgba(255,255,255,0.04)" }
},
yT: {
position: "left",
ticks: { color: "#ff8c32", font: { family: "Inter", size: 11 }, callback: v => v + "°" },
grid: { color: "rgba(255,255,255,0.06)" }
},
yR: {
position: "right",
min: 0,
...(barMax !== undefined ? { max: barMax } : {}),
ticks: { color: "#50b4ff", font: { family: "Inter", size: 11 }, callback: v => hasRealPrecip ? v + "mm" : v + "%" },
grid: { display: false }
}
}
}
});
})();
</script>
{% endblock %}