Ladeanzeige statt Zwischenständen, Echtzeit sofort nach Start, keine Autobahn-Schilder

- /api/status liefert ready, sobald Positionen mit Echtzeit berechnet sind
- Server holt Echtzeit direkt nach dem Laden des Fahrplans (statt nach ~35 s)
- Browser zeigt bis dahin nur 'Züge werden geladen …'
- Statuszeile ohne Technik: 'N Züge unterwegs', ggf. 'Verspätungen zurzeit nicht verfügbar'
- Autobahn-/Strassenschilder (roads_shields) ausgeblendet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
Claude
2026-10-21 21:02:35 +02:00
parent 4df29e7e6c
commit 2e3c10a6c4
4 changed files with 48 additions and 19 deletions
+10
View File
@@ -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))),
}
+27 -18
View File
@@ -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);
});
+3 -1
View File
@@ -28,9 +28,11 @@
<li><i style="--c:var(--d3)"></i>&gt;15′</li>
<li><i style="--c:var(--na)"></i>ohne Echtzeit</li>
</ul>
<p id="status">Lade …</p>
<p id="status"></p>
</header>
<div class="card" id="loading" role="status">Züge werden geladen …</div>
<aside class="card" id="panel" hidden>
<button id="close" aria-label="Schliessen">×</button>
<div id="detail"></div>
+8
View File
@@ -46,3 +46,11 @@ h1::before { content: ""; display: inline-block; width: 10px; height: 10px; bord
.hover .maplibregl-popup-content { background: var(--bg); color: var(--fg); padding: 6px 10px; border-radius: 8px; font-size: 13px; }
.hover .maplibregl-popup-tip { display: none; }
#loading { left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 14px 20px; font-size: 15px;
max-width: calc(100vw - 48px); text-align: center; z-index: 3; }
#loading[hidden] { display: none; }
#loading::before { content: ""; display: inline-block; width: 14px; height: 14px; margin-right: 10px; vertical-align: -2px;
border: 2px solid var(--line); border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { #loading::before { animation: none; } }