Swiss Bus Tracker — suivre en temps réel un bus, un train ou un métro suisse depuis un arrêt et une direction choisis, avec heure planifiée, heure réelle, retard annoncé et indication « déjà passé ». Le tout dans une interface qui tient dans la main, installable comme une vraie app sur iPhone ou Android.
👉 App live : https://bus.smarsys.com 👉 Code source : https://github.com/smarsys/swiss-bus-tracker
Cet article détaille la démarche, la stack technique et les raisons qui m’ont poussé à publier le projet en open source.
Le besoin
Comme beaucoup d’habitants du canton de Vaud, je fréquente régulièrement les Car Postaux, les CFF et les TL. Les applications officielles existent et font très bien leur travail, mais elles sont conçues pour répondre à des questions génériques : « comment aller de A à B ? », « quels sont les prochains départs de cette gare ? ».
Ce qui m’intéressait était différent : répondre en un coup d’œil à une question très précise, répétée plusieurs fois par semaine, avec un minimum de frictions :
Le bus 425 qui passe à Oulens-sous-Echallens, Collège en direction d’Échallens, il part à quelle heure réelle maintenant ?
Une question ultra-spécifique, mais dont la réponse nécessite de naviguer dans plusieurs écrans des apps existantes. Je voulais une app qui me donne cette réponse en une pastille colorée, sur une carte persistante sauvegardée en favori. Ouvrir l’app, regarder, fermer. Trois secondes.
Les données — OJP 2.0
La Suisse publie sur opentransportdata.swiss un écosystème de données ouvertes remarquablement complet pour un pays de cette taille. Plusieurs APIs cohabitent : GTFS pour les horaires statiques, GTFS-RT pour les mises à jour temps réel, SIRI-SX pour les perturbations, et surtout OJP 2.0 (Open Journey Planner) pour la planification multimodale.
OJP 2.0 expose notamment deux services très pratiques :
- LocationInformationRequest : résoudre un nom d’arrêt flou (« Oulens ») en un identifiant unique
- StopEventRequest : récupérer les prochains passages à un arrêt donné, avec à la fois les horaires planifiés (
TimetabledTime) et les prévisions temps réel (EstimatedTime) quand elles sont disponibles
Le format est du XML SIRI, l’authentification un Bearer token obtenu gratuitement après inscription. Les quotas gratuits (50 req/min, 20’000/jour) sont très confortables pour un usage personnel ou une app de poche.
À noter : toutes les lignes ne publient pas leurs positions temps réel. Les grandes lignes CFF et les transports urbains (TL, TPG) le font systématiquement. Certaines lignes rurales de CarPostal comme le 425 n’ont que des horaires planifiés. L’app reflète honnêtement cette nuance avec trois statuts distincts : « à l’heure » (vert), « retard de X min » (ambre/rouge), « planifié » (gris — pas de données temps réel disponibles).
L’architecture
La stack est volontairement simple, chaque choix ayant une justification précise.
Backend : FastAPI + httpx + lxml
Python 3.11, FastAPI pour le serveur web ASGI, httpx pour les appels HTTP asynchrones vers OJP, lxml pour parser les réponses XML SIRI. Pydantic v2 pour les modèles de données et la validation.
Deux endpoints principaux exposés en JSON :
GET /api/stops/search?q=<texte>— autocomplete d’arrêtGET /api/departures?stopRef=<id>&line=<num>&direction=<txt>&window_min=<min>&num_results=<n>— passages filtrés
Un cache in-memory TTL de 20 secondes par combinaison (arrêt, fenêtre) évite de marteler l’API OJP lorsque plusieurs favoris ciblent le même arrêt ou lors de refresh rapprochés.
Frontend : HTML + JavaScript vanilla + Tailwind CSS
Pas de framework JS (ni React, ni Vue, ni Svelte), pas de build step. Une page unique avec du vanilla JS et Tailwind CSS via CDN. Résultat : un premier paint sous les 200 ms sur 4G, un JavaScript total de moins de 20 Ko, zéro dépendance npm à maintenir.
Les favoris sont persistés dans localStorage. L’app fait un auto-refresh toutes les 30 secondes. Chaque ligne de transport affiche une pastille colorée selon son mode :
- Jaune CarPostal pour les bus
- Rouge CFF pour les trains
- Bleu TL pour les métros et tramways urbains
PWA — installable sur iPhone et Android
Un fichier manifest.json, un service worker minimal, deux icônes PNG (192 et 512 px) et quelques meta tags iOS spécifiques suffisent pour rendre l’app installable sur l’écran d’accueil. Sur iPhone iOS 16.4+, les notifications web natives fonctionnent également une fois l’app installée en PWA : l’utilisateur peut configurer « alerte 5 minutes avant le départ » sur n’importe quel favori.
Conteneurisation
Image Docker basée sur Ubuntu 22.04 avec Python 3.11 installé via apt. Un Dockerfile d’une vingtaine de lignes. Une image construite automatiquement par GitHub Actions à chaque push sur main et publiée sur le GitHub Container Registry public.
Déploiement — Jelastic Infomaniak
Topologie en deux niveaux :
Internet → NGINX Load Balancer → Custom Container Python
Le NGINX Jelastic fait la terminaison SSL avec un certificat Let’s Encrypt auto-renouvelé. Le container Python écoute sur le port 8080 et reçoit les requêtes proxifiées. Une IP publique IPv4 dédiée permet d’émettre un certificat pour le sous-domaine bus.smarsys.com.
CI/CD — entièrement automatisé
À chaque push sur main, GitHub Actions enchaîne :
- Lancement des tests unitaires pytest (parsing XML, cache TTL, logique de filtrage)
- Lint via ruff
- Build de l’image Docker
- Push de l’image sur le GitHub Container Registry
- Appel de l’API Jelastic pour déclencher un redéploiement du container
- Vérification que la nouvelle version est bien live sur
bus.smarsys.com
Temps total entre git push et la nouvelle version en production : environ six minutes, sans aucune intervention manuelle.
Le rôle de Claude Code dans la démarche
Une spécificité de ce projet mérite d’être mentionnée : il a été construit en pair-programming avec Claude Code (l’interface CLI agentique d’Anthropic pour le développement).
La démarche n’était pas de « demander à une IA de coder à ma place », mais plutôt d’orchestrer un agent qui exécute les étapes techniques précises pendant que je conservais le contrôle des décisions d’architecture, de produit et d’infrastructure. Concrètement :
- Je définissais le besoin métier (« je veux filtrer les passages par ligne et direction »)
- Je traduisais en spec technique exécutable (fichiers à créer, structure des réponses, conventions de nommage)
- Claude Code écrivait le code, les tests, les fichiers de configuration
- Je validais, testais en local, pointais les bugs observés avec des exemples concrets
- Claude Code corrigeait, j’approuvais le commit
Cette approche demande un effort réel de rédaction de prompts : plus ils sont précis (avec structure de fichiers, critères de succès, règles de conduite), plus l’agent produit un résultat utilisable du premier coup. Ce n’est pas de la « magie », c’est un outil qui amplifie considérablement la productivité lorsque l’on sait déjà ce qu’on veut construire.
Le ratio effort / résultat m’a surpris. Passer de l’idée à une application en production avec CI/CD complet, domaine custom, SSL, PWA installable et notifications push en une après-midi aurait représenté plusieurs journées de travail il y a deux ans. L’agent ne remplace pas l’expertise technique — il faut toujours savoir diagnostiquer « Illegal header value b’Bearer ‘ » ou identifier pourquoi un nodeGroup Jelastic refuse un redeploy — mais il supprime une large part du travail de saisie, de recherche de syntaxe et de copier-coller de boilerplate.
Stack technique complète
Pour ceux qui veulent reproduire ou s’inspirer :
- Langages : Python 3.11 (backend), JavaScript ES2022 (frontend), HTML5, CSS via Tailwind
- Framework backend : FastAPI, Pydantic v2, httpx async, lxml
- Frontend : vanilla JS, Tailwind CSS CDN, service worker natif
- Source de données : OJP 2.0 sur opentransportdata.swiss
- Tests : pytest, ruff
- Containerisation : Docker, base Ubuntu 22.04
- Registry : GitHub Container Registry (ghcr.io)
- CI/CD : GitHub Actions (tests + build + push + redeploy automatique)
- Hébergement : Jelastic Infomaniak (Suisse, Genève DC3)
- Topologie : NGINX Load Balancer + Custom Container + IPv4 publique
- SSL : Let’s Encrypt avec renouvellement automatique
- DNS : CNAME puis A record chez Infomaniak
- Domaine : bus.smarsys.com
Conclusion et perspectives
Ce projet illustre à petite échelle ce qu’il est devenu possible de construire en 2026 pour un développeur individuel : une application complète, conteneurisée, déployée avec un pipeline CI/CD professionnel, installable comme une PWA native, avec certificat SSL et domaine propre, pour un coût d’exploitation d’environ 20 CHF par mois.
Les données ouvertes suisses sont une ressource d’une qualité exceptionnelle, encore sous-exploitée par les développeurs indépendants. L’API OJP 2.0 en particulier offre une porte d’entrée simple vers l’ensemble du transport public helvétique.
Les pistes d’évolution sont nombreuses : géolocalisation (« arrêts près de moi »), partage de favoris via URL, widget iOS natif, mode sombre, ajout des perturbations via SIRI-SX, tri intelligent qui remonte en priorité les favoris dont le prochain passage est imminent. Le code étant open source sous licence MIT, toute contribution ou fork est bienvenu.
👉 App live : https://bus.smarsys.com 👉 Code source : https://github.com/smarsys/swiss-bus-tracker
Publication rédigée par Christophe Arn, SMARSYS — avril 2026
