diff --git a/trainpulse/server.py b/trainpulse/server.py index 418b6cf..cfe46a4 100644 --- a/trainpulse/server.py +++ b/trainpulse/server.py @@ -29,6 +29,7 @@ class State: rt_version: str | None = None rt_at: float | None = None rt_error: str | None = None + rt_tried_at: float | None = None vehicles: bytes = b'{"type":"FeatureCollection","features":[]}' vehicles_gz: bytes = gzip.compress(vehicles) count = 0 @@ -64,6 +65,11 @@ def watch_feeds(): t = time.time() S.feed = Feed(C.FEEDS_DIR / want) log.info("Fahrplan %s geladen in %.0f s (%d Fahrten)", want, time.time() - t, len(S.feed.trip_ids)) + try: # Echtzeit sofort holen statt auf den nächsten Abruf zu warten + poll_realtime() + except Exception: + log.exception("Erster Echtzeit-Abruf fehlgeschlagen") + compute_positions() def poll_realtime(): @@ -77,6 +83,8 @@ def poll_realtime(): if S.rt_at and time.time() - S.rt_at > 600: S.rt = {} raise + finally: + S.rt_tried_at = time.time() def compute_positions(): @@ -142,6 +150,8 @@ def status(): "rt_error": S.rt_error, "rt_trips": len(S.rt), "vehicles": S.count, + # erst "bereit", wenn Positionen mit (dem ersten Versuch) Echtzeit berechnet sind + "ready": bool(f and S.computed_at and (not C.RT_KEY or (S.rt_tried_at and S.computed_at >= S.rt_tried_at))), } diff --git a/web/app.js b/web/app.js index 09a9de2..69a012a 100644 --- a/web/app.js +++ b/web/app.js @@ -6,7 +6,7 @@ maplibregl.addProtocol("pmtiles", protocol.tile); // Drei Grundkarten-Sätze im selben Style, umgeschaltet per Sichtbarkeit: // hell, dunkel, und Beschriftung für Satellit (weisse Schrift mit dunklem Rand, wie bei Google Maps) -const prefixed = (layers, pre, extra = {}) => layers.map((l) => ({ ...l, id: pre + l.id, +const prefixed = (layers, pre, extra = {}) => layers.filter((l) => l.id !== "roads_shields").map((l) => ({ ...l, id: pre + l.id, layout: { ...l.layout, visibility: "none" }, paint: { ...l.paint, ...(l.type === "symbol" ? extra : {}) } })); const LAYERS = { light: prefixed(basemaps.layers("osm", basemaps.namedFlavor("light"), { lang: "de" }), "l-"), @@ -114,8 +114,10 @@ for (const m of MODES) document.getElementById("btn-" + m).onclick = () => setMo let trains = { type: "FeatureCollection", features: [] }; let ts = 0, step = 2, clockOffset = 0; +let ready = false; // erst Züge zeigen, wenn der Server fertige Daten (inkl. Echtzeit) hat + async function refreshTrains() { - if (document.hidden) return; + if (document.hidden || !ready) return; try { const r = await fetch("api/vehicles"); if (!r.ok) return; @@ -228,25 +230,34 @@ function animate(ms) { } async function refreshStatus() { - const el = document.getElementById("status"); + const el = document.getElementById("status"), loading = document.getElementById("loading"); try { const s = await (await fetch("api/status")).json(); - if (!s.feed_version) { el.textContent = "Noch kein Fahrplan geladen …"; el.className = "warn"; return; } - const v = s.feed_version; - const parts = [`Fahrplan ${v.slice(6, 8)}.${v.slice(4, 6)}.${v.slice(0, 4)}`, `${s.vehicles} Züge`]; - let warn = !s.shapes; - if (!s.realtime) { parts.push("keine Echtzeit (API-Key fehlt)"); warn = true; } - else if (s.rt_error || s.rt_age === null || s.rt_age > 180) { parts.push("Echtzeit gestört"); warn = true; } - else parts.push(`Echtzeit vor ${s.rt_age} s`); - if (!s.shapes) parts.push("Strecken: Luftlinie"); - el.textContent = parts.join(" · "); - el.className = warn ? "warn" : ""; + if (!s.ready) { + loading.textContent = s.feed_version ? "Züge werden geladen …" + : "Fahrplan wird vorbereitet … beim ersten Start dauert das 15–20 Minuten."; + loading.hidden = false; + return; + } + if (!ready) { + ready = true; + await refreshTrains(); + loading.hidden = true; + } + const rtOk = s.realtime && !s.rt_error && s.rt_age !== null && s.rt_age < 180; + el.textContent = `${s.vehicles} Züge unterwegs` + (rtOk ? "" : " · Verspätungen zurzeit nicht verfügbar"); + el.className = rtOk ? "" : "warn"; } catch (e) { - el.textContent = "Server nicht erreichbar"; - el.className = "warn"; + loading.textContent = "Keine Verbindung zum Server …"; + loading.hidden = false; } } +async function statusLoop() { // bis alles bereit ist alle 2 s nachfragen, danach alle 30 s + await refreshStatus(); + setTimeout(statusLoop, ready ? 30000 : 2000); +} + // ---------- Zugdetails ---------- let openId = null; @@ -315,10 +326,8 @@ map.on("load", () => { let mode = "light"; try { mode = localStorage.getItem("mode") || (localStorage.getItem("sat") === "1" ? "sat" : "light"); } catch (e) { /* egal */ } setMode(mode); - refreshTrains(); - refreshStatus(); + statusLoop(); setInterval(refreshTrains, 5000); requestAnimationFrame(animate); - setInterval(refreshStatus, 30000); setInterval(() => openId && showTrip(openId), 30000); }); diff --git a/web/index.html b/web/index.html index 2a16320..1830fda 100644 --- a/web/index.html +++ b/web/index.html @@ -28,9 +28,11 @@
Lade …
+ +