312 lines
11 KiB
HTML
312 lines
11 KiB
HTML
{% 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">
|
||
{% 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 "–"),
|
||
] %}
|
||
{% 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 %}
|
||
· 🌅 {{ sunrise }} · 🌇 {{ sunset }}
|
||
{% endif %}
|
||
{% if current.uv_index is not none %}
|
||
· UV {{ current.uv_index }}
|
||
{% 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.description %}<p class="warn-desc">{{ w.description[:140] }}{% if w.description|length > 140 %}…{% endif %}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- STÜNDLICH -->
|
||
<section class="section">
|
||
<h2 class="section-title">Stundenweise</h2>
|
||
<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 %}
|
||
{% if h.uv_index is not none and h.uv_index > 0 %}
|
||
<div class="hcard-uv">UV {{ h.uv_index }}</div>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- CHART -->
|
||
<section class="section">
|
||
<div class="chart-head">
|
||
<h2 class="section-title">Temperatur & Niederschlag</h2>
|
||
<span class="chart-range">48 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-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 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: "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 %}
|