diff --git a/README.md b/README.md index 6be11b9..754ae6e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,8 @@ die Kartenkacheln). Alle Daten liegen im Ordner `data/` neben der `compose.yaml` 1. **Container anlegen** (Proxmox-Oberfläche → *Create CT*): 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 im LXC nicht. 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 ``` 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 registrieren, 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`. diff --git a/deploy/install.sh b/deploy/install.sh index 6e25acc..dc5d3e2 100644 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -8,6 +8,8 @@ cd "$(dirname "$0")/.." [ "$(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; } 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 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 < None: ap.error("--zip braucht --source") C.FEEDS_DIR.mkdir(parents=True, exist_ok=True) - for p in C.FEEDS_DIR.iterdir(): # Ordner der alten Version (feeds/) und abgeschaltete Quellen - if p.is_dir() and p.name not in C.SOURCES: + for p in C.FEEDS_DIR.iterdir(): + if not p.is_dir() or p.name in C.SOURCES: + continue + if p.name.isdigit() and (p / "meta.json").exists(): # alte Version: feeds/ war die Schweiz + (C.FEEDS_DIR / "ch").mkdir(exist_ok=True) + p.rename(C.FEEDS_DIR / "ch" / p.name) + else: shutil.rmtree(p) if not args.zip: ensure_tiles() diff --git a/trainpulse/server.py b/trainpulse/server.py index 0cf2b68..665fd68 100644 --- a/trainpulse/server.py +++ b/trainpulse/server.py @@ -34,6 +34,7 @@ class Source: self.rt_at: float | None = None self.rt_error: str | None = None self.rt_tried_at: float | None = None + self.on_disk = False # ein aufbereiteter Fahrplan liegt bereit (wird gleich geladen) @property 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: die schalten erst um 15 Uhr auf einen neuen Fahrplan um), sonst den neusten.""" 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(): - ready = sorted(m.parent.name for m in (C.FEEDS_DIR / src.name).glob("*/meta.json")) + ready = found[src.name] if not ready: continue 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") def status(): 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] ok = [s for s in rt if not s.rt_error and s.rt_at and time.time() - s.rt_at < 180] return { "loaded": bool(loaded), # 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, "rt_ok": bool(ok) and len(ok) == len(loaded), "rt_some": bool(ok), diff --git a/web/app.js b/web/app.js index 354de84..f7eb17a 100644 --- a/web/app.js +++ b/web/app.js @@ -236,7 +236,7 @@ async function refreshStatus() { const s = await (await fetch("api/status")).json(); if (!s.ready) { 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; return; }