Deutschland: Karte erst bereit wenn beide Länder geladen, alte Schweizer Daten übernehmen, Hardware-Hinweise

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:35:09 +02:00
parent 2a4962c379
commit c07a309045
5 changed files with 24 additions and 8 deletions
+4 -1
View File
@@ -37,7 +37,8 @@ die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml`
1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*): 1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*):
Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim Debian 12 oder 13 Template, unprivileged, 2 Kerne, **4 GB RAM** (pfaedle braucht beim
Aufbereiten bis ~2 GB, der Server ~0,5 GB), **20 GB Disk**, Netz per DHCP. Aufbereiten bis ~2 GB, der Server ~1 GB), **30 GB Disk** (Kartenkacheln ~4,5 GB, beim
monatlichen OSM-Update kurzzeitig ~5 GB Download für Deutschland), Netz per DHCP.
Unter *Options → Features* **Nesting** und **keyctl** aktivieren – ohne das läuft Docker Unter *Options → Features* **Nesting** und **keyctl** aktivieren – ohne das läuft Docker
im LXC nicht. im LXC nicht.
2. **Installieren** (im Container als root): 2. **Installieren** (im Container als root):
@@ -47,6 +48,8 @@ die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml`
bash Trainpulse/deploy/install.sh bash Trainpulse/deploy/install.sh
``` ```
Das Skript installiert Docker, falls es fehlt, baut das Image und startet beide Container. Das Skript installiert Docker, falls es fehlt, baut das Image und startet beide Container.
Der erste Start dauert ca. 30–45 Minuten: Kartenkacheln (~4,5 GB) und OpenStreetMap für
Deutschland (~5 GB) herunterladen, dann beide Fahrpläne aufbereiten.
3. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren, 3. **Echtzeit freischalten:** Auf <https://api-manager.opentransportdata.swiss> registrieren,
eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in eine Applikation anlegen, das Produkt *GTFS-RT* abonnieren und den API-Key in
`Trainpulse/.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `docker compose up -d`. `Trainpulse/.env` bei `TRAINPULSE_RT_KEY=` eintragen. Dann `docker compose up -d`.
+4 -2
View File
@@ -8,6 +8,8 @@ cd "$(dirname "$0")/.."
[ "$(id -u)" = 0 ] || { echo "Bitte als root ausführen" >&2; exit 1; } [ "$(id -u)" = 0 ] || { echo "Bitte als root ausführen" >&2; exit 1; }
command -v apt-get >/dev/null || { echo "Nur für Debian/Ubuntu (apt) gedacht" >&2; exit 1; } command -v apt-get >/dev/null || { echo "Nur für Debian/Ubuntu (apt) gedacht" >&2; exit 1; }
mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo) mem=$(awk '/MemTotal/ {print int($2/1024)}' /proc/meminfo)
disk=$(df -Pm . | awk 'NR==2 {print $4}')
[ "$disk" -ge 25000 ] || echo "WARNUNG: nur ${disk} MB frei – empfohlen sind ~30 GB" >&2
[ "$mem" -ge 3500 ] || echo "WARNUNG: nur ${mem} MB RAM – für die Fahrplan-Aufbereitung werden ~4 GB empfohlen" >&2 [ "$mem" -ge 3500 ] || echo "WARNUNG: nur ${mem} MB RAM – für die Fahrplan-Aufbereitung werden ~4 GB empfohlen" >&2
if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then if ! command -v docker >/dev/null || ! docker compose version >/dev/null 2>&1; then
@@ -35,8 +37,8 @@ ip=$(hostname -I | awk '{print $1}')
cat <<EOF cat <<EOF
Fertig. Die Karte läuft unter http://$ip:8080 Fertig. Die Karte läuft unter http://$ip:8080
Der Updater lädt jetzt Kartenkacheln (~1,2 GB) und den Fahrplan und berechnet die Der Updater lädt jetzt Kartenkacheln (~4,5 GB), OpenStreetMap und die Fahrpläne der Schweiz
Streckenverläufe. Bis die ersten Züge erscheinen, dauert es ca. 15 Minuten. und Deutschlands und berechnet die Streckenverläufe. Beim ersten Start dauert das ca. 30–45 Minuten.
Fortschritt: docker compose logs -f updater Fortschritt: docker compose logs -f updater
Echtzeit: API-Key in $(pwd)/.env eintragen, dann docker compose up -d Echtzeit: API-Key in $(pwd)/.env eintragen, dann docker compose up -d
EOF EOF
+7 -2
View File
@@ -329,8 +329,13 @@ def main() -> None:
ap.error("--zip braucht --source") ap.error("--zip braucht --source")
C.FEEDS_DIR.mkdir(parents=True, exist_ok=True) C.FEEDS_DIR.mkdir(parents=True, exist_ok=True)
for p in C.FEEDS_DIR.iterdir(): # Ordner der alten Version (feeds/<version>) und abgeschaltete Quellen for p in C.FEEDS_DIR.iterdir():
if p.is_dir() and p.name not in C.SOURCES: if not p.is_dir() or p.name in C.SOURCES:
continue
if p.name.isdigit() and (p / "meta.json").exists(): # alte Version: feeds/<version> war die Schweiz
(C.FEEDS_DIR / "ch").mkdir(exist_ok=True)
p.rename(C.FEEDS_DIR / "ch" / p.name)
else:
shutil.rmtree(p) shutil.rmtree(p)
if not args.zip: if not args.zip:
ensure_tiles() ensure_tiles()
+8 -2
View File
@@ -34,6 +34,7 @@ class Source:
self.rt_at: float | None = None self.rt_at: float | None = None
self.rt_error: str | None = None self.rt_error: str | None = None
self.rt_tried_at: float | None = None self.rt_tried_at: float | None = None
self.on_disk = False # ein aufbereiteter Fahrplan liegt bereit (wird gleich geladen)
@property @property
def has_rt(self) -> bool: def has_rt(self) -> bool:
@@ -67,8 +68,12 @@ def watch_feeds():
"""Lädt je Quelle den passenden Fahrplan: den, auf den sich die Echtzeitdaten beziehen (Schweiz: """Lädt je Quelle den passenden Fahrplan: den, auf den sich die Echtzeitdaten beziehen (Schweiz:
die schalten erst um 15 Uhr auf einen neuen Fahrplan um), sonst den neusten.""" die schalten erst um 15 Uhr auf einen neuen Fahrplan um), sonst den neusten."""
today = dt.datetime.now(TZ).date() today = dt.datetime.now(TZ).date()
found = {}
for src in S.sources.values(): # erst alle markieren, damit die Karte nicht mit einem Land allein startet
found[src.name] = sorted(m.parent.name for m in (C.FEEDS_DIR / src.name).glob("*/meta.json"))
src.on_disk = bool(found[src.name])
for src in S.sources.values(): for src in S.sources.values():
ready = sorted(m.parent.name for m in (C.FEEDS_DIR / src.name).glob("*/meta.json")) ready = found[src.name]
if not ready: if not ready:
continue continue
want = src.rt_version if src.rt_version in ready else ready[-1] want = src.rt_version if src.rt_version in ready else ready[-1]
@@ -171,12 +176,13 @@ def trip(source: str, date: str, ti: int):
@app.get("/api/status") @app.get("/api/status")
def status(): def status():
loaded = [s for s in S.sources.values() if s.feed] loaded = [s for s in S.sources.values() if s.feed]
pending = [s for s in S.sources.values() if s.on_disk and not s.feed] # nicht halbe Karte zeigen
rt = [s for s in loaded if s.has_rt] rt = [s for s in loaded if s.has_rt]
ok = [s for s in rt if not s.rt_error and s.rt_at and time.time() - s.rt_at < 180] ok = [s for s in rt if not s.rt_error and s.rt_at and time.time() - s.rt_at < 180]
return { return {
"loaded": bool(loaded), "loaded": bool(loaded),
# erst "bereit", wenn Positionen mit (dem ersten Versuch) Echtzeit berechnet sind # erst "bereit", wenn Positionen mit (dem ersten Versuch) Echtzeit berechnet sind
"ready": bool(loaded and S.computed_at and all(s.rt_tried_at and S.computed_at >= s.rt_tried_at for s in rt)), "ready": bool(loaded and not pending and S.computed_at and all(s.rt_tried_at and S.computed_at >= s.rt_tried_at for s in rt)),
"vehicles": S.count, "vehicles": S.count,
"rt_ok": bool(ok) and len(ok) == len(loaded), "rt_ok": bool(ok) and len(ok) == len(loaded),
"rt_some": bool(ok), "rt_some": bool(ok),
+1 -1
View File
@@ -236,7 +236,7 @@ async function refreshStatus() {
const s = await (await fetch("api/status")).json(); const s = await (await fetch("api/status")).json();
if (!s.ready) { if (!s.ready) {
loading.textContent = s.loaded ? "Züge werden geladen …" loading.textContent = s.loaded ? "Züge werden geladen …"
: "Fahrplan wird vorbereitet … beim ersten Start dauert das 15–20 Minuten."; : "Fahrplan wird vorbereitet … beim ersten Start dauert das 30–45 Minuten.";
loading.hidden = false; loading.hidden = false;
return; return;
} }