Compare commits

..
10 Commits
Author SHA1 Message Date
stefankoelle 9789eedc9e README.md 2026-08-14 23:37:05 +02:00
Stefan Koelle 8472475a2c Merge pull request #13 from skoelle/renovate/python-3.14-slim
docker Update python:3.14-slim Docker digest to a7fb1e6
2026-08-10 20:19:08 +02:00
renovate[bot] 10d1a71ab7 docker Update python:3.14-slim Docker digest to a7fb1e6 2026-08-10 18:17:40 +00:00
stefankoelle 8260d1a098 renovate no docker-compoes 2026-08-10 20:15:59 +02:00
stefankoelle 523ee24b0b renovate 2026-08-10 19:56:34 +02:00
stefankoelle 3a2d803788 documentation: profiles feature 2026-08-07 21:36:08 +02:00
stefankoelle b90b2f9ba1 new feature: profiles 2026-08-07 21:18:42 +02:00
stefankoelle 1c3ea8fb87 LICENSE 2026-08-04 19:27:51 +02:00
stefankoelle 85a95a1aa4 LICENSE 2026-08-04 19:16:37 +02:00
stefankoelle c8b8f4031d added LICENSE 2026-08-04 19:05:31 +02:00
13 changed files with 231 additions and 103 deletions
+6
View File
@@ -0,0 +1,6 @@
title: "MVG Departures"
emoji: "🚇"
category: code
subcategory: "Smart Home Apps"
status: active
stack: [Python, FastAPI, Uvicorn, Jinja2]
+6 -1
View File
@@ -11,10 +11,11 @@ MVG Departures Monitor: FastAPI-App für Münchner U-/S-Bahn-Abfahrtsanzeige. Mo
| `app/main.py` | FastAPI-App, Endpunkte, Caching, MVG-API-Aufrufe | | `app/main.py` | FastAPI-App, Endpunkte, Caching, MVG-API-Aufrufe |
| `app/config.py` | Config-Loader: YAML → Dataclasses | | `app/config.py` | Config-Loader: YAML → Dataclasses |
| `app/templates/index.html` | Jinja2-HTML-Template (Mobile-UI) | | `app/templates/index.html` | Jinja2-HTML-Template (Mobile-UI) |
| `config.yaml` | Stationskonfiguration (User-Input) | | `config.yaml` | Profil- und Stationskonfiguration (User-Input) |
| `requirements.txt` | Python-Dependencies | | `requirements.txt` | Python-Dependencies |
| `Dockerfile` | Container-Build | | `Dockerfile` | Container-Build |
| `.github/workflows/build.yml` | CI/CD | | `.github/workflows/build.yml` | CI/CD |
| `LICENSE` | MIT-Lizenz |
## Code-Conventions ## Code-Conventions
@@ -24,6 +25,7 @@ MVG Departures Monitor: FastAPI-App für Münchner U-/S-Bahn-Abfahrtsanzeige. Mo
- **Logging**: `logging.getLogger("mvg-departures")` - **Logging**: `logging.getLogger("mvg-departures")`
- **Naming**: snake_case (Python), camelCase (HTML/CSS-Klassen) - **Naming**: snake_case (Python), camelCase (HTML/CSS-Klassen)
- **Keine Comments**: Nur wenn explizit angefordert - **Keine Comments**: Nur wenn explizit angefordert
- **Lizenz**: MIT — Copyright-Header in allen Quelldateien (`# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) - MIT License`), HTML-Kommentar im Template
## Entwicklung ## Entwicklung
@@ -58,6 +60,9 @@ Kein Linter konfiguriert. Bei Bedarf: `ruff check app/` oder `black app/`
### Neue Station hinzufügen ### Neue Station hinzufügen
→ Nur `config.yaml` editieren, kein Code nötig → Nur `config.yaml` editieren, kein Code nötig
### Neues Profil hinzufügen
`config.yaml` unter `profiles:` neuen Eintrag mit `name:` und `stations:` ergänzen
### Neuen Transporttyp hinzufügen ### Neuen Transporttyp hinzufügen
`TYPE_MAP` und `ICON_MAP` in `main.py` erweitern, CSS-Klasse `.icon-X` in `index.html` hinzufügen `TYPE_MAP` und `ICON_MAP` in `main.py` erweitern, CSS-Klasse `.icon-X` in `index.html` hinzufügen
+2 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.14-slim@sha256:cea0e6040540fb2b965b6e7fb5ffa00871e632eef63719f0ea54bca189ce14a6 FROM python:3.14-slim@sha256:a7fb1e634c4a578f9e0bd6327f11a3cde11b7a9395f48e24360c0988bcc5c2bc
WORKDIR /app WORKDIR /app
@@ -7,6 +7,7 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app COPY app ./app
COPY config.yaml ./config.yaml COPY config.yaml ./config.yaml
COPY LICENSE ./LICENSE
ENV CONFIG_PATH=/app/config.yaml ENV CONFIG_PATH=/app/config.yaml
ENV TZ=Europe/Berlin ENV TZ=Europe/Berlin
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+67 -52
View File
@@ -1,21 +1,24 @@
# mvg-departures # 🚇 mvg-departures
Kompakter Abfahrtsmonitor fuer MVG U-Bahn und S-Bahn (Muenchen), primaer fuer mobile Nutzung optimiert. Kompakter Abfahrtsmonitor fuer MVG U-Bahn und S-Bahn (Muenchen), primaer fuer mobile Nutzung optimiert.
## Features
- Konfigurierbare Stationsliste (`config.yaml`) mit Typ (UBAHN/SBAHN/TRAM/BUS)
- Ausschluss-Filter pro Station: nur unerwuenschte Zielrichtungen werden ausgeblendet,
neue/unbekannte Ziele werden automatisch weiter angezeigt
- Zwei Endpunkte:
- `/` — mobile-optimierte HTML-Ansicht mit Auto-Refresh (alle 60s per Meta-Refresh)
- `/api/departures` — JSON-Ausgabe derselben Daten
- Anzeige von Linie, Ziel, Abfahrtszeit, Verspaetung und Stoerungsmeldungen
- Visuelle Unterscheidung von U-Bahn (blau) und S-Bahn (gruen) per Icon
![Screenshot der Webseite](docs/screenshot.png) ![Screenshot der Webseite](docs/screenshot.png)
## Konfiguration ## ✨ Features
- 🗺️ Konfigurierbare Stationsliste (`config.yaml`) mit Typ (UBAHN/SBAHN/TRAM/BUS)
- 🚫 Ausschluss-Filter pro Station: nur unerwuenschte Zielrichtungen werden ausgeblendet,
neue/unbekannte Ziele werden automatisch weiter angezeigt
- 👤 Profile zur Gruppierung von Stationen (z.B. "Hinfahrt" und "Rückfahrt")
- 🖱️ Klickbare Kacheln zum Profilwechsel (nur bei >1 Profil)
- 🔗 URL-Param `?profile=<name>` + Auto-Refresh
- 🌐 Zwei Endpunkte:
- `/` — mobile-optimierte HTML-Ansicht mit Auto-Refresh (alle 60s per Meta-Refresh)
- `/api/departures` — JSON-Ausgabe derselben Daten
- 📊 Anzeige von Linie, Ziel, Abfahrtszeit, Verspaetung und Stoerungsmeldungen
- 🎨 Visuelle Unterscheidung von U-Bahn (blau) und S-Bahn (gruen) per Icon
## ⚙️ Konfiguration
Siehe `config.yaml`: Siehe `config.yaml`:
@@ -24,39 +27,45 @@ refresh_seconds: 60
cache_seconds: 20 cache_seconds: 20
departures_limit: 10 departures_limit: 10
stations: profiles:
- name: "Josephsburg, München" - name: "Hinfahrt"
type: "UBAHN" stations:
exclude_destinations: - name: "Josephsburg, München"
- "Messestadt Ost" type: "UBAHN"
- "Messestadt West" exclude_destinations:
- "Messestadt Ost"
- name: "Berg am Laim, München" - "Messestadt West"
type: "SBAHN" - name: "Berg am Laim, München"
exclude_destinations: type: "SBAHN"
- "Erding" exclude_destinations:
- "Markt Schwaben" - "Erding"
- "Markt Schwaben"
- name: "Rückfahrt"
stations:
- name: "Grosshadern, München"
type: "UBAHN"
exclude_destinations: []
``` ```
`exclude_destinations` filtert per exaktem `destination`-Stringvergleich. Alles was `exclude_destinations` filtert per exaktem `destination`-Stringvergleich. Alles was
NICHT in der Liste steht, wird angezeigt — so fallen Linienaenderungen/Umleitungen NICHT in der Liste steht, wird angezeigt — so fallen Linienaenderungen/Umleitungen
sofort auf, statt lautlos verschwunden zu sein. sofort auf, statt lautlos verschwunden zu sein.
## Lokal starten ## 🏃 Lokal starten
```bash ```bash
pip install -r requirements.txt pip install -r requirements.txt
uvicorn app.main:app --reload uvicorn app.main:app --reload
``` ```
## Docker ## 🐳 Docker
```bash ```bash
docker build -t mvg-departures . docker build -t mvg-departures .
docker run -p 8000:8000 -e TZ=Europe/Berlin -v $(pwd)/config.yaml:/app/config.yaml mvg-departures docker run -p 8000:8000 -e TZ=Europe/Berlin -v $(pwd)/config.yaml:/app/config.yaml mvg-departures
``` ```
## Deployment ## 🚀 Deployment
Siehe `docker-compose.yml` im Repo als Referenz. Beispiel (Owner anpassen): Siehe `docker-compose.yml` im Repo als Referenz. Beispiel (Owner anpassen):
@@ -76,36 +85,38 @@ services:
- "com.centurylinklabs.watchtower.enable=true" - "com.centurylinklabs.watchtower.enable=true"
``` ```
## CI/CD ## 🔄 CI/CD
`.github/workflows/build.yml`: `.github/workflows/build.yml`:
- Baut bei jedem Push auf `main` das Docker-Image und pusht es nach - 🏗️ Baut bei jedem Push auf `main` das Docker-Image und pusht es nach
`ghcr.io/<owner>/<repo>:latest` sowie mit Short-SHA-Tag `ghcr.io/<owner>/<repo>:latest` sowie mit Short-SHA-Tag
- Anschliessender Cleanup-Job loescht alte Image-Versionen in der GHCR-Package-Registry - 🧹 Anschliessender Cleanup-Job loescht alte Image-Versionen in der GHCR-Package-Registry
und behaelt nur die letzten 4 Versionen (`min-versions-to-keep: 4`) und behaelt nur die letzten 4 Versionen (`min-versions-to-keep: 4`)
### Voraussetzung ### 📋 Voraussetzung
Repo-Settings → Actions → General → Workflow permissions auf Repo-Settings → Actions → General → Workflow permissions auf
"Read and write permissions" stellen, sonst schlaegt der Push nach GHCR fehl. "Read and write permissions" stellen, sonst schlaegt der Push nach GHCR fehl.
## mvg api ## 📡 mvg api
https://www.mvg.de/api/bgw-pt/v3/locations?query=Josephsburg \ https://www.mvg.de/api/bgw-pt/v3/locations?query=Josephsburg \
https://www.mvg.de/api/bgw-pt/v3/locations?query=Berg%20am \ https://www.mvg.de/api/bgw-pt/v3/locations?query=Berg%20am \
https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:1220&limit=10&transportTypes=UBAHN \ https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:1220&limit=10&transportTypes=UBAHN \
https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:910&limit=10&transportTypes=SBAHN https://www.mvg.de/api/bgw-pt/v3/departures?globalId=de:09162:910&limit=10&transportTypes=SBAHN
## Endpunkte ## 🔌 Endpunkte
| Route | Beschreibung | | Route | Beschreibung |
|---|---| |---|---|
| `/` | HTML-Ansicht, mobile-optimiert, Auto-Refresh alle 60s | | `/` | HTML-Ansicht, mobile-optimiert, Auto-Refresh alle 60s |
| `/api/departures` | JSON-Liste aller gefilterten Abfahrten | | `/?profile=<name>` | HTML-Ansicht für ein bestimmtes Profil |
| `/api/departures` | JSON-Liste aller gefilterten Abfahrten (erstes Profil als Fallback) |
| `/api/departures?profile=<name>` | JSON-Liste für ein bestimmtes Profil |
| `/healthz` | Healthcheck fuer Docker/Watchtower | | `/healthz` | Healthcheck fuer Docker/Watchtower |
## Projektstruktur ## 📁 Projektstruktur
``` ```
mvg-departures/ mvg-departures/
@@ -128,24 +139,28 @@ mvg-departures/
└── build.yml # CI/CD: Docker-Build + GHCR-Push └── build.yml # CI/CD: Docker-Build + GHCR-Push
``` ```
## Tech-Stack ## 🛠️ Tech-Stack
- **Runtime**: Python 3.14 - **Runtime**: Python 3.14
- **Framework**: FastAPI + Uvicorn - 🚀 **Framework**: FastAPI + Uvicorn
- **Templating**: Jinja2 (server-seitig) - 🎨 **Templating**: Jinja2 (server-seitig)
- **MVG-API**: `mvg` Python-Client (oeffentlich, kein Auth noetig) - 🚇 **MVG-API**: `mvg` Python-Client (oeffentlich, kein Auth noetig)
- **Config**: YAML via `pyyaml` - ⚙️ **Config**: YAML via `pyyaml`
- **Container**: Docker (python:3.14-slim) - 🐳 **Container**: Docker (python:3.14-slim)
## Caching ## 💾 Caching
- **Station-ID**: In-memory Dict, lifetime=Session (loest Station-Namen auf) - 🆔 **Station-ID**: In-memory Dict, lifetime=Session (loest Station-Namen auf)
- **Abfahrten**: In-memory Dict, TTL=`cache_seconds` (Default: 20s) pro Station+Typ - ⏱️ **Abfahrten**: In-memory Dict, TTL=`cache_seconds` (Default: 20s) pro Station+Typ
- Keine Persistenz → Neustart = Cache-Verlust - 🔄 Keine Persistenz → Neustart = Cache-Verlust
## Einschraenkungen ## ⚠️ Einschraenkungen
- Kein WebSocket/Live-Updates (nur periodischer Meta-Refresh) - 📡 Kein WebSocket/Live-Updates (nur periodischer Meta-Refresh)
- Keine Datenbank (nur In-Memory) - 💽 Keine Datenbank (nur In-Memory)
- Kein Test-Framework vorhanden - 🧪 Kein Test-Framework vorhanden
- Ein `config.yaml` pro Deployment (kein Multi-Tenancy) - 📄 Ein `config.yaml` pro Deployment (kein Multi-Tenancy)
## 📜 Lizenz
MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
+19 -7
View File
@@ -42,7 +42,9 @@ config.yaml
load_config() → AppConfig (Dataclass) load_config() → AppConfig (Dataclass)
Für jede Station: Profile auswählen (?profile= param oder erstes Profil)
Für jede Station im aktiven Profil:
resolve_station_id(name) → station_id (mit Memory-Cache) resolve_station_id(name) → station_id (mit Memory-Cache)
MvgApi(station_id).departures(limit, transport_types) MvgApi(station_id).departures(limit, transport_types)
@@ -61,7 +63,9 @@ Response: JSON oder HTML-Template
| Route | Methode | Beschreibung | | Route | Methode | Beschreibung |
|---|---|---| |---|---|---|
| `/` | GET | Mobile HTML-Ansicht, Auto-Refresh alle 60s | | `/` | GET | Mobile HTML-Ansicht, Auto-Refresh alle 60s |
| `/api/departures` | GET | JSON-Liste aller gefilterten Abfahrten | | `/?profile=<name>` | GET | HTML-Ansicht für ein bestimmtes Profil |
| `/api/departures` | GET | JSON-Liste aller gefilterten Abfahrten (erstes Profil als Fallback) |
| `/api/departures?profile=<name>` | GET | JSON-Liste für ein bestimmtes Profil |
| `/healthz` | GET | Healthcheck (`{"status": "ok"}`) | | `/healthz` | GET | Healthcheck (`{"status": "ok"}`) |
## 6. Konfiguration (`config.yaml`) ## 6. Konfiguration (`config.yaml`)
@@ -71,11 +75,18 @@ refresh_seconds: 60 # Meta-Refresh-Intervall (HTML)
cache_seconds: 20 # API-Cache pro Station cache_seconds: 20 # API-Cache pro Station
departures_limit: 10 # Max. Abfahrten pro Station departures_limit: 10 # Max. Abfahrten pro Station
stations: profiles:
- name: "Station Name, München" - name: "Hinfahrt" # Profilname (einzigartig)
type: "UBAHN" # UBAHN | SBAHN | TRAM | BUS stations:
exclude_destinations: # Exakter Stringvergleich - name: "Station Name, München"
- "Zielstation" type: "UBAHN" # UBAHN | SBAHN | TRAM | BUS
exclude_destinations: # Exakter Stringvergleich
- "Zielstation"
- name: "Rückfahrt"
stations:
- name: "Andere Station"
type: "SBAHN"
exclude_destinations: []
``` ```
### Filter-Logik ### Filter-Logik
@@ -93,6 +104,7 @@ stations:
## 8. UI ## 8. UI
- Dunkles Theme (`#111417` Background) - Dunkles Theme (`#111417` Background)
- Profil-Kacheln (nur bei >1 Profil): 50% Breite, Basis `#1e242b`, aktiv `#005ca9` (MVG-Blau)
- Farbliche Unterscheidung: U-Bahn (blau `#005ca9`), S-Bahn (grün `#00933b`), Tram (rot `#e2001a`), Bus (grau `#55545a`) - Farbliche Unterscheidung: U-Bahn (blau `#005ca9`), S-Bahn (grün `#00933b`), Tram (rot `#e2001a`), Bus (grau `#55545a`)
- Anzeige: Linie, Ziel, Abfahrtszeit, Verspätung (gelb/rot), Entfall, Störungsmeldungen - Anzeige: Linie, Ziel, Abfahrtszeit, Verspätung (gelb/rot), Entfall, Störungsmeldungen
- Responsive, für Smartphone-First optimiert - Responsive, für Smartphone-First optimiert
+1
View File
@@ -0,0 +1 @@
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) - MIT License
+20 -10
View File
@@ -1,3 +1,4 @@
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) - MIT License
"""Config-Loader für die Stationsliste aus config.yaml.""" """Config-Loader für die Stationsliste aus config.yaml."""
import os import os
import yaml import yaml
@@ -12,12 +13,18 @@ class StationConfig:
exclude_destinations: List[str] = field(default_factory=list) exclude_destinations: List[str] = field(default_factory=list)
@dataclass
class ProfileConfig:
name: str
stations: List[StationConfig] = field(default_factory=list)
@dataclass @dataclass
class AppConfig: class AppConfig:
refresh_seconds: int refresh_seconds: int
cache_seconds: int cache_seconds: int
departures_limit: int departures_limit: int
stations: List[StationConfig] profiles: List[ProfileConfig]
def load_config(path: str = None) -> AppConfig: def load_config(path: str = None) -> AppConfig:
@@ -25,18 +32,21 @@ def load_config(path: str = None) -> AppConfig:
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
raw = yaml.safe_load(f) raw = yaml.safe_load(f)
stations = [ profiles = []
StationConfig( for p in raw.get("profiles", []):
name=s["name"], stations = [
type=s["type"].upper(), StationConfig(
exclude_destinations=s.get("exclude_destinations", []) or [], name=s["name"],
) type=s["type"].upper(),
for s in raw.get("stations", []) exclude_destinations=s.get("exclude_destinations", []) or [],
] )
for s in p.get("stations", [])
]
profiles.append(ProfileConfig(name=p["name"], stations=stations))
return AppConfig( return AppConfig(
refresh_seconds=int(raw.get("refresh_seconds", 60)), refresh_seconds=int(raw.get("refresh_seconds", 60)),
cache_seconds=int(raw.get("cache_seconds", 20)), cache_seconds=int(raw.get("cache_seconds", 20)),
departures_limit=int(raw.get("departures_limit", 10)), departures_limit=int(raw.get("departures_limit", 10)),
stations=stations, profiles=profiles,
) )
+21 -7
View File
@@ -1,3 +1,4 @@
# Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) - MIT License
"""MVG Departures Monitor - FastAPI App.""" """MVG Departures Monitor - FastAPI App."""
import time import time
import logging import logging
@@ -11,7 +12,7 @@ from fastapi.staticfiles import StaticFiles
from mvg import MvgApi, TransportType from mvg import MvgApi, TransportType
from app.config import load_config, StationConfig from app.config import load_config, StationConfig, ProfileConfig
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mvg-departures") logger = logging.getLogger("mvg-departures")
@@ -97,27 +98,40 @@ def fetch_departures_for_station(station_cfg: StationConfig) -> List[Dict[str, A
return result return result
def get_all_departures() -> List[Dict[str, Any]]: def get_profile_by_name(profile_name: str) -> ProfileConfig:
for profile in config.profiles:
if profile.name == profile_name:
return profile
return config.profiles[0] if config.profiles else None
def get_departures_for_profile(profile: ProfileConfig) -> List[Dict[str, Any]]:
all_deps: List[Dict[str, Any]] = [] all_deps: List[Dict[str, Any]] = []
for station_cfg in config.stations: for station_cfg in profile.stations:
all_deps.extend(fetch_departures_for_station(station_cfg)) all_deps.extend(fetch_departures_for_station(station_cfg))
all_deps.sort(key=lambda d: d["time_epoch"] or 0) all_deps.sort(key=lambda d: d["time_epoch"] or 0)
return all_deps return all_deps
@app.get("/api/departures") @app.get("/api/departures")
def api_departures(): def api_departures(profile: str = None):
return JSONResponse(content={"departures": get_all_departures()}) active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
if not active_profile:
return JSONResponse(content={"departures": []})
return JSONResponse(content={"departures": get_departures_for_profile(active_profile)})
@app.get("/") @app.get("/")
def index(request: Request): def index(request: Request, profile: str = None):
departures = get_all_departures() active_profile = get_profile_by_name(profile) if profile else config.profiles[0] if config.profiles else None
departures = get_departures_for_profile(active_profile) if active_profile else []
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"index.html", "index.html",
{ {
"departures": departures, "departures": departures,
"profiles": config.profiles,
"active_profile": active_profile,
"refresh_seconds": config.refresh_seconds, "refresh_seconds": config.refresh_seconds,
"generated_at": datetime.now().strftime("%H:%M:%S"), "generated_at": datetime.now().strftime("%H:%M:%S"),
}, },
+14 -1
View File
@@ -1,14 +1,27 @@
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) - MIT License -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abfahrten</title> <title>Abfahrten</title>
<meta http-equiv="refresh" content="{{ refresh_seconds }}"> <meta http-equiv="refresh" content="{{ refresh_seconds }};url=/?profile={{ active_profile.name }}">
</head> </head>
<body> <body>
<div style="max-width: 375px; margin: 0 auto; width: 100%;"> <div style="max-width: 375px; margin: 0 auto; width: 100%;">
<h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1> <h1>Abfahrten <span class="updated">{{ generated_at }}</span></h1>
{% if profiles|length > 1 %}
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
{% for p in profiles %}
<a href="/?profile={{ p.name }}"
style="flex: 1; padding: 12px; text-align: center; text-decoration: none; border-radius: 8px; font-weight: 600; font-size: 18px; color: {{ 'white' if p.name == active_profile.name else '#9aa5b1' }}; background: {{ '#005ca9' if p.name == active_profile.name else '#1e242b' }};">
{{ p.name }}
</a>
{% endfor %}
</div>
{% endif %}
{% if departures %} {% if departures %}
<table> <table>
{% for d in departures %} {% for d in departures %}
+15 -12
View File
@@ -7,15 +7,18 @@ refresh_seconds: 60
cache_seconds: 20 cache_seconds: 20
departures_limit: 10 departures_limit: 10
stations: profiles:
- name: "Josephsburg, München" - name: "Hinfahrt"
type: "UBAHN" stations:
exclude_destinations: - name: "Josephsburg, München"
- "Messestadt Ost" type: "UBAHN"
- "Messestadt West" exclude_destinations:
- "Messestadt Ost"
- name: "Berg am Laim, München" - "Messestadt West"
type: "SBAHN" - name: "Berg am Laim, München"
exclude_destinations: type: "SBAHN"
- "Erding" exclude_destinations:
- "Markt Schwaben" - "Erding"
- "Markt Schwaben"
- name: "Rückfahrt"
stations: []
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
mvg-departures: mvg-departures:
image: ghcr.io/skoelle/mvg-departures:latest@sha256:c93ca797aa5dea84611a44d9463dd557816b664d30cdc7e956335f3e7628dcb0 image: ghcr.io/skoelle/mvg-departures:latest
container_name: mvg-departures container_name: mvg-departures
ports: ports:
- "8000:8000" - "8000:8000"
+38 -11
View File
@@ -3,23 +3,50 @@
"extends": [ "extends": [
"config:recommended", "config:recommended",
"docker:pinDigests", "docker:pinDigests",
"helpers:pinGitHubActionDigests", "helpers:pinGitHubActionDigests"
":configMigration",
":pinDevDependencies",
"abandonments:recommended",
"security:minimumReleaseAgeNpm",
":dependencyDashboard"
], ],
"schedule": ["before 6am on monday"],
"timezone": "Europe/Berlin",
"automergeStrategy": "squash", "automergeStrategy": "squash",
"ignorePaths": ["docker-compose.yml"],
"packageRules": [ "packageRules": [
{ {
"matchUpdateTypes": ["minor", "patch"], "description": "GitHub Actions - grouped automerge",
"groupName": "{{depName}}", "matchManagers": ["github-actions"],
"commitMessagePrefix": "fix(deps)" "groupName": "github-actions",
"commitMessagePrefix": "ci",
"automerge": true
}, },
{ {
"description": "Docker Digests - automerge (security patches)",
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["digest"],
"commitMessagePrefix": "docker",
"automerge": true
},
{
"description": "Docker Major/Minor - manual review",
"matchManagers": ["dockerfile"],
"matchUpdateTypes": ["major", "minor"],
"groupName": "docker-base-images",
"commitMessagePrefix": "docker",
"automerge": false
},
{
"description": "Python minor/patch - automerge",
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["minor", "patch"],
"commitMessagePrefix": "fix(deps)",
"automerge": true
},
{
"description": "Python major - no automerge, label",
"matchManagers": ["pip_requirements"],
"matchUpdateTypes": ["major"], "matchUpdateTypes": ["major"],
"groupName": "major-{{depName}}" "commitMessagePrefix": "chore(deps)",
"automerge": false,
"labels": ["major-update"]
} }
] ],
"ignoreDeps": ["node", "express", "node-cron", "nodemailer", "actions/setup-node"]
} }