@@ -1418,10 +1504,13 @@ export default function EVAArrivees() {
@@ -1429,11 +1518,15 @@ export default function EVAArrivees() {
{/* SECTION DROITE : MODE / VOIE & BLOC OPERATEUR/NUMERO */}
- {isSub || isCarTER ? (
+ {isSuppressed ? (
+
+
+
+ ) : (isSub || isCarTER) ? (
- ) : (rawPlatform && !isSuppressed) ? (
+ ) : rawPlatform ? (
Voie
{rawPlatform}
@@ -1442,7 +1535,7 @@ export default function EVAArrivees() {
{op}
- {number}
+ {formatTrainNumber(number)}
diff --git a/src/pages/afficheurs/eva/EVADeparts.js b/src/pages/afficheurs/eva/EVADeparts.js
index 54a2f1c..83fc65e 100644
--- a/src/pages/afficheurs/eva/EVADeparts.js
+++ b/src/pages/afficheurs/eva/EVADeparts.js
@@ -805,7 +805,12 @@ export default function EVADeparts() {
}
if (isSuppressed) return { label: 'supprimé', detail: '', delayMinutes: null, causeDetails };
- if (maxDelay != null) return { label: `retardé +${maxDelay}min.`, detail: '', delayMinutes: maxDelay, causeDetails };
+ if (maxDelay != null) {
+ const dH = Math.floor(maxDelay / 60);
+ const dRem = maxDelay % 60;
+ const dStr = maxDelay >= 60 ? (dRem > 0 ? `${dH}h${String(dRem).padStart(2, '0')}` : `${dH}h`) : `${maxDelay}min.`;
+ return { label: `retardé +${dStr}`, detail: '', delayMinutes: maxDelay, causeDetails };
+ }
return { label: "à l'heure", detail: '', delayMinutes: null, causeDetails };
}, [buildPerturbMap, getPerturbationJours, parsePerturbArretsObject, parseDelayMinutesFromArret, normalizeName, getStops]);
@@ -1243,7 +1248,7 @@ export default function EVADeparts() {
fetchData();
const intervalId = setInterval(fetchData, 15000);
return () => { mounted = false; clearInterval(intervalId); };
- }, [gareDisplay, gareParam, gareIdParam, buildPerturbMap, computeStatusForHoraireAtStop, normalizeName, applyPerturbationsToHoraires, getStops, getPerturbationJours]);
+ }, [gareDisplay, gareParam, gareIdParam]);
// Alternance retard / texte toutes les 2s
const [alternateDelay, setAlternateDelay] = useState(false);
diff --git a/src/services/sncfApi.js b/src/services/sncfApi.js
index cf65ba9..49d052d 100644
--- a/src/services/sncfApi.js
+++ b/src/services/sncfApi.js
@@ -1,27 +1,34 @@
let isRealtimeRefreshing = false;
-// Fonctions de persistance du cache temps réel dans sessionStorage
+// Fonctions de persistance du cache temps réel dans localStorage et sessionStorage
function saveRealtimeCacheToStorage() {
- if (typeof window === 'undefined' || !window.sessionStorage) return;
+ if (typeof window === 'undefined') return;
try {
if (siriLiteCache.platforms && siriLiteCache.platforms.size > 0) {
const arr = Array.from(siriLiteCache.platforms.entries()).slice(0, 4000);
- window.sessionStorage.setItem('sncf_siri_platforms', JSON.stringify({
+ const payload = JSON.stringify({
ts: siriLiteCache.timestamp,
data: arr
- }));
+ });
+ if (window.localStorage) {
+ window.localStorage.setItem('sncf_siri_platforms', payload);
+ }
+ if (window.sessionStorage) {
+ window.sessionStorage.setItem('sncf_siri_platforms', payload);
+ }
}
} catch (_) {}
}
function restoreRealtimeCacheFromStorage() {
- if (typeof window === 'undefined' || !window.sessionStorage) return;
+ if (typeof window === 'undefined') return;
try {
- const raw = window.sessionStorage.getItem('sncf_siri_platforms');
+ const raw = (window.localStorage && window.localStorage.getItem('sncf_siri_platforms')) ||
+ (window.sessionStorage && window.sessionStorage.getItem('sncf_siri_platforms'));
if (raw) {
const parsed = JSON.parse(raw);
- if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 900000)) {
+ if (parsed && Array.isArray(parsed.data) && (Date.now() - parsed.ts < 1800000)) {
siriLiteCache.timestamp = parsed.ts;
siriLiteCache.platforms = new Map(parsed.data);
}
@@ -29,32 +36,43 @@ function restoreRealtimeCacheFromStorage() {
} catch (_) {}
}
-export async function triggerRealtimeBackgroundRefresh() {
- if (isRealtimeRefreshing) return;
- const now = Date.now();
+let lastRealtimeTriggerTime = 0;
- const needRt = (now - gtfsRtCache.timestamp > 30000) || gtfsRtCache.updates.size === 0;
- const needSiri = (now - siriLiteCache.timestamp > 60000) || siriLiteCache.platforms.size === 0;
- const needSx = (now - situationExchangeCache.timestamp > 60000) || situationExchangeCache.platforms.size === 0;
+let activeRealtimePromise = null;
+
+export async function triggerRealtimeBackgroundRefresh(force = false) {
+ const now = Date.now();
+ if (activeRealtimePromise) return activeRealtimePromise;
+ if (!force && (now - lastRealtimeTriggerTime < 30000)) return;
+ lastRealtimeTriggerTime = now;
+
+ const needRt = force || (now - gtfsRtCache.timestamp > 30000) || gtfsRtCache.updates.size === 0;
+ const needSiri = force || (now - siriLiteCache.timestamp > 60000) || siriLiteCache.platforms.size === 0;
+ const needSx = force || (now - situationExchangeCache.timestamp > 60000) || situationExchangeCache.platforms.size === 0;
if (!needRt && !needSiri && !needSx) return;
- isRealtimeRefreshing = true;
- try {
- const promises = [];
- if (needRt) promises.push(fetchGtfsRealtimeData());
- if (needSiri) promises.push(fetchSiriLiteData());
- if (needSx) promises.push(fetchSituationExchangeData());
+ activeRealtimePromise = (async () => {
+ isRealtimeRefreshing = true;
+ try {
+ const promises = [];
+ if (needRt) promises.push(fetchGtfsRealtimeData());
+ if (needSiri) promises.push(fetchSiriLiteData());
+ if (needSx) promises.push(fetchSituationExchangeData());
- await Promise.allSettled(promises);
- } catch (_) {
- // ignore
- } finally {
- isRealtimeRefreshing = false;
- if (typeof window !== 'undefined') {
- window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
+ await Promise.allSettled(promises);
+ } catch (_) {
+ // ignore
+ } finally {
+ isRealtimeRefreshing = false;
+ activeRealtimePromise = null;
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
+ }
}
- }
+ })();
+
+ return activeRealtimePromise;
}
/* eslint-disable */
@@ -204,9 +222,124 @@ function extractAllTrainNumbers(journeyXml) {
return Array.from(nums);
}
+
+function processJourneyXml(j, platformsMap, updatesMap) {
+ // 1. Extraction exhaustive des identifiants et numéros de train
+ const trainNums = extractAllTrainNumbers(j);
+
+ const journeyRefMatch = j.match(/
([^<]+)<\/DatedVehicleJourneyRef>/) ||
+ j.match(/([^<]+)<\/VehicleJourneyRef>/) ||
+ j.match(/[\s\S]*?([^<]+)<\/DatedVehicleJourneyRef>/);
+ const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
+
+ const isCanceledJourney = j.includes('true') || j.includes('cancelled');
+
+ // 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
+ const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
+ for (const call of callMatches) {
+ // Extraction du code UIC de l'arrêt
+ const stopRefMatch = call.match(/([^<]+)<\/StopPointRef>/);
+ let uic = '';
+ let uic7 = '';
+ if (stopRefMatch) {
+ const rawRef = stopRefMatch[1];
+ const uicDigits = rawRef.match(/\d{7,8}/);
+ uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
+ if (uic.length === 8 && uic.startsWith('87')) {
+ uic7 = uic.slice(2);
+ }
+ }
+
+ const stopNameMatch = call.match(/([^<]+)<\/StopPointName>/);
+ const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
+
+ // Extraction des quais (Départ, Arrivée, Prévu ou Réel)
+ const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
+ const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
+ const quayMatch = call.match(/([^<]+)<\/QuayRef>/);
+
+ let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
+ const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
+
+ // Extraction des heures et calcul du retard
+ const expDepMatch = call.match(/([^<]+)<\/ExpectedDepartureTime>/);
+ const aimDepMatch = call.match(/([^<]+)<\/AimedDepartureTime>/);
+ const expArrMatch = call.match(/([^<]+)<\/ExpectedArrivalTime>/);
+ const aimArrMatch = call.match(/([^<]+)<\/AimedArrivalTime>/);
+
+ let delayMin = 0;
+ const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
+ const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
+
+ if (expTimeStr && aimTimeStr) {
+ const expD = new Date(expTimeStr).getTime();
+ const aimD = new Date(aimTimeStr).getTime();
+ if (!isNaN(expD) && !isNaN(aimD)) {
+ delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
+ }
+ }
+
+ const isStopCancelled = isCanceledJourney || call.includes('cancelled') || call.includes('cancelled') || call.includes('true');
+
+ if (platform) {
+ for (const tNum of trainNums) {
+ platformsMap.set(tNum, platform);
+ const ct = cleanNum(tNum);
+ if (ct) platformsMap.set(ct, platform);
+ if (uic) {
+ platformsMap.set(`${tNum}_${uic}`, platform);
+ platformsMap.set(`${uic}_${tNum}`, platform);
+ if (ct) {
+ platformsMap.set(`${ct}_${uic}`, platform);
+ platformsMap.set(`${uic}_${ct}`, platform);
+ }
+ }
+ if (uic7) {
+ platformsMap.set(`${tNum}_${uic7}`, platform);
+ platformsMap.set(`${uic7}_${tNum}`, platform);
+ if (ct) {
+ platformsMap.set(`${ct}_${uic7}`, platform);
+ platformsMap.set(`${uic7}_${ct}`, platform);
+ }
+ }
+ if (stopNameClean) {
+ platformsMap.set(`${tNum}_${stopNameClean}`, platform);
+ if (ct) platformsMap.set(`${ct}_${stopNameClean}`, platform);
+ }
+ }
+ if (journeyRef) {
+ platformsMap.set(journeyRef, platform);
+ if (uic) {
+ platformsMap.set(`${journeyRef}_${uic}`, platform);
+ platformsMap.set(`${uic}_${journeyRef}`, platform);
+ }
+ if (stopNameClean) {
+ platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
+ }
+ }
+ }
+
+ const info = {
+ platform: platform,
+ delayMinutes: delayMin,
+ isCanceled: isStopCancelled
+ };
+
+ for (const tNum of trainNums) {
+ if (uic) updatesMap.set(`${tNum}_${uic}`, info);
+ updatesMap.set(tNum, info);
+ }
+ if (journeyRef) {
+ if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
+ updatesMap.set(journeyRef, info);
+ }
+ }
+}
+
/**
* Récupère et décode le flux SIRI Lite SNCF officiel en temps réel (circulations dans les 60 prochaines minutes)
* Source : https://proxy.transport.data.gouv.fr/resource/sncf-siri-lite-estimated-timetable
+ * Décodage en streaming pour apparition instantanée des quais (sans attendre les 22 Mo complets)
*/
async function fetchSiriLiteData() {
const now = Date.now();
@@ -214,131 +347,83 @@ async function fetchSiriLiteData() {
return siriLiteCache;
}
- const platformsMap = new Map();
- const updatesMap = new Map();
+ // Conserver les quais déjà connus pour enrichir progressivement
+ const platformsMap = new Map(siriLiteCache.platforms || []);
+ const updatesMap = new Map(siriLiteCache.updates || []);
try {
const res = await fetchWithTimeout(SNCF_SIRI_LITE_TIMETABLE, {
headers: {
'Accept': 'application/xml, text/xml, */*'
}
- }, 35000);
+ }, 45000);
if (res && res.ok) {
- const xml = await res.text();
- const journeys = xml.split('');
+ let lastDispatchTime = Date.now();
+ let newPlatformsCount = 0;
- for (let i = 1; i < journeys.length; i++) {
- const j = journeys[i];
+ if (res.body && typeof res.body.getReader === 'function') {
+ const reader = res.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
- // 1. Extraction exhaustive des identifiants et numéros de train
- const trainNums = extractAllTrainNumbers(j);
+ try {
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
- const journeyRefMatch = j.match(/([^<]+)<\/DatedVehicleJourneyRef>/) || j.match(/([^<]+)<\/VehicleJourneyRef>/) || j.match(/[\s\S]*?([^<]+)<\/DatedVehicleJourneyRef>/);
- const journeyRef = journeyRefMatch ? journeyRefMatch[1].trim() : '';
+ buffer += decoder.decode(value, { stream: true });
- const isCanceledJourney = j.includes('true') || j.includes('cancelled');
-
- // 2. Extraire tous les arrêts desservis (RecordedCall et EstimatedCall)
- const callMatches = j.match(/<(?:RecordedCall|EstimatedCall)>[\s\S]*?<\/(?:RecordedCall|EstimatedCall)>/g) || [];
- for (const call of callMatches) {
- // Extraction du code UIC de l'arrêt
- const stopRefMatch = call.match(/([^<]+)<\/StopPointRef>/);
- let uic = '';
- let uic7 = '';
- if (stopRefMatch) {
- const rawRef = stopRefMatch[1];
- const uicDigits = rawRef.match(/\d{7,8}/);
- uic = uicDigits ? uicDigits[0] : rawRef.replace(/\D/g, '');
- if (uic.length === 8 && uic.startsWith('87')) {
- uic7 = uic.slice(2);
- }
- }
-
- const stopNameMatch = call.match(/([^<]+)<\/StopPointName>/);
- const stopNameClean = stopNameMatch ? formatStationName(stopNameMatch[1]).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[-_/.,()'&]/g, ' ').replace(/\s+/g, ' ').trim() : '';
-
- // Extraction des quais (Départ, Arrivée, Prévu ou Réel)
- const depPlatMatch = call.match(/<(?:DeparturePlatformName|PlannedDeparturePlatformName)>([^<]+)<\/(?:DeparturePlatformName|PlannedDeparturePlatformName)>/);
- const arrPlatMatch = call.match(/<(?:ArrivalPlatformName|PlannedArrivalPlatformName)>([^<]+)<\/(?:ArrivalPlatformName|PlannedArrivalPlatformName)>/);
- const quayMatch = call.match(/([^<]+)<\/QuayRef>/);
-
- let rawPlatform = (depPlatMatch ? depPlatMatch[1] : (arrPlatMatch ? arrPlatMatch[1] : (quayMatch ? quayMatch[1] : ''))).trim();
- const platform = rawPlatform.replace(/^(?:voie|quai|track|platform)\s*/i, '').trim();
-
- // Extraction des heures et calcul du retard
- const expDepMatch = call.match(/([^<]+)<\/ExpectedDepartureTime>/);
- const aimDepMatch = call.match(/([^<]+)<\/AimedDepartureTime>/);
- const expArrMatch = call.match(/([^<]+)<\/ExpectedArrivalTime>/);
- const aimArrMatch = call.match(/([^<]+)<\/AimedArrivalTime>/);
-
- let delayMin = 0;
- const expTimeStr = (expDepMatch && expDepMatch[1]) || (expArrMatch && expArrMatch[1]);
- const aimTimeStr = (aimDepMatch && aimDepMatch[1]) || (aimArrMatch && aimArrMatch[1]);
-
- if (expTimeStr && aimTimeStr) {
- const expD = new Date(expTimeStr).getTime();
- const aimD = new Date(aimTimeStr).getTime();
- if (!isNaN(expD) && !isNaN(aimD)) {
- delayMin = Math.max(0, Math.round((expD - aimD) / 60000));
- }
- }
-
- const isStopCancelled = isCanceledJourney || call.includes('cancelled') || call.includes('cancelled') || call.includes('true');
-
- if (platform) {
- for (const tNum of trainNums) {
- if (uic) {
- platformsMap.set(`${tNum}_${uic}`, platform);
- platformsMap.set(`${uic}_${tNum}`, platform);
- }
- if (uic7) {
- platformsMap.set(`${tNum}_${uic7}`, platform);
- platformsMap.set(`${uic7}_${tNum}`, platform);
- }
- if (stopNameClean) {
- platformsMap.set(`${tNum}_${stopNameClean}`, platform);
+ let idx;
+ while ((idx = buffer.indexOf('')) !== -1) {
+ const journeyXml = buffer.slice(0, idx + 26);
+ buffer = buffer.slice(idx + 26);
+ const beforeSize = platformsMap.size;
+ processJourneyXml(journeyXml, platformsMap, updatesMap);
+ if (platformsMap.size > beforeSize) {
+ newPlatformsCount += (platformsMap.size - beforeSize);
}
}
- if (journeyRef) {
- if (uic) {
- platformsMap.set(`${journeyRef}_${uic}`, platform);
- platformsMap.set(`${uic}_${journeyRef}`, platform);
- }
- if (stopNameClean) {
- platformsMap.set(`${journeyRef}_${stopNameClean}`, platform);
- }
+
+ // Mise à jour continue du cache mémoire et stockage sans provoquer de re-fetch intempestif
+ const nowTime = Date.now();
+ if (newPlatformsCount > 0 && (nowTime - lastDispatchTime > 1500)) {
+ lastDispatchTime = nowTime;
+ newPlatformsCount = 0;
+ siriLiteCache = {
+ timestamp: nowTime,
+ platforms: new Map(platformsMap),
+ updates: new Map(updatesMap)
+ };
+ saveRealtimeCacheToStorage();
}
}
+ } finally {
+ try { reader.releaseLock(); } catch (_) {}
+ }
+ } else {
+ // Fallback sans Web Streams
+ const xml = await res.text();
+ const journeys = xml.split('');
+ for (let i = 1; i < journeys.length; i++) {
+ processJourneyXml(journeys[i], platformsMap, updatesMap);
+ }
+ }
- const info = {
- platform: platform,
- delayMinutes: delayMin,
- isCanceled: isStopCancelled
- };
-
- for (const tNum of trainNums) {
- if (uic) updatesMap.set(`${tNum}_${uic}`, info);
- updatesMap.set(tNum, info);
- }
- if (journeyRef) {
- if (uic) updatesMap.set(`${journeyRef}_${uic}`, info);
- updatesMap.set(journeyRef, info);
- }
+ if (platformsMap.size > 0 || updatesMap.size > 0) {
+ siriLiteCache = {
+ timestamp: Date.now(),
+ platforms: platformsMap,
+ updates: updatesMap
+ };
+ saveRealtimeCacheToStorage();
+ if (typeof window !== 'undefined') {
+ window.dispatchEvent(new CustomEvent('sncf-realtime-updated'));
}
}
}
-
- if (platformsMap.size > 0 || updatesMap.size > 0) {
- siriLiteCache = {
- timestamp: now,
- platforms: platformsMap,
- updates: updatesMap
- };
- saveRealtimeCacheToStorage();
- }
} catch (err) {
- console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable refresh error/timeout:', err);
+ console.warn('[sncfApi] Notice: SIRI-Lite Estimated Timetable stream refresh:', err);
}
return siriLiteCache;
@@ -669,32 +754,32 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
const publicUrl = (typeof process !== 'undefined' && process.env && process.env.PUBLIC_URL) ? process.env.PUBLIC_URL : '';
const stationUrl = `${publicUrl}/data/stations/${uic}.json`;
- // Récupération avec cache mémoire immédiat pour affichage instantané en dev
- let data = stationJsonCache.get(String(uic));
- if (!data) {
+ // Chargement simultané de toutes les données : Horaires + GTFS-RT + SIRI-LITE
+ const fetchStationJson = async () => {
+ let cached = stationJsonCache.get(String(uic));
+ if (cached) return cached;
const jsonRes = await fetch(stationUrl).catch(err => {
console.warn(`[sncfApi] Notice: fetch station ${uic} error:`, err);
return null;
});
if (jsonRes && jsonRes.ok) {
- data = await jsonRes.json();
- stationJsonCache.set(String(uic), data);
+ cached = await jsonRes.json();
+ stationJsonCache.set(String(uic), cached);
+ return cached;
}
- }
+ return null;
+ };
+
+ const [stationData] = await Promise.all([
+ fetchStationJson(),
+ triggerRealtimeBackgroundRefresh()
+ ]);
+ let data = stationData;
if (!data) {
console.warn(`[sncfApi] Fichier de gare introuvable : ${stationUrl}`);
return null;
}
-
- // Utilisation immédiate du cache temps réel & déclenchement non-bloquant en tâche de fond
- const rtPromise = triggerRealtimeBackgroundRefresh();
- if (gtfsRtCache.updates.size === 0 && siriLiteCache.platforms.size === 0) {
- await Promise.race([
- rtPromise,
- new Promise(resolve => setTimeout(resolve, 800))
- ]);
- }
const rtData = gtfsRtCache || { updates: new Map(), alerts: [] };
const siriData = siriLiteCache || { platforms: new Map(), updates: new Map() };
const sxData = situationExchangeCache || { platforms: new Map(), updates: new Map(), alerts: [] };
@@ -789,6 +874,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
(uicClean && cNum && sxData.platforms.get(`${uicClean}_${cNum}`)) ||
(cNum && cName && sxData.platforms.get(`${cNum}_${cName}`)) ||
(rawNum && cName && sxData.platforms.get(`${rawNum}_${cName}`)) ||
+ // 3. Fallback sur le numéro de train seul si connu dans SIRI-Lite / Situation Exchange
+ (cNum && siriData.platforms.get(cNum)) ||
+ (rawNum && siriData.platforms.get(rawNum)) ||
+ (cNum && siriData.updates.get(cNum)?.platform) ||
+ (rawNum && siriData.updates.get(rawNum)?.platform) ||
+ (cNum && sxData.updates.get(cNum)?.platform) ||
+ (rawNum && sxData.updates.get(rawNum)?.platform) ||
''
);
};
@@ -841,7 +933,13 @@ async function fetchGtfsStationTimetable(uic, mode = 'departs') {
statusLabel = 'supprimé';
etat = 'supprime';
} else if (delayMinutes > 0) {
- statusLabel = `retardé +${delayMinutes}min.`;
+ if (delayMinutes >= 60) {
+ const h = Math.floor(delayMinutes / 60);
+ const rem = delayMinutes % 60;
+ statusLabel = rem > 0 ? `retardé +${h}h${String(rem).padStart(2, '0')}` : `retardé +${h}h`;
+ } else {
+ statusLabel = `retardé +${delayMinutes}min.`;
+ }
etat = 'retarde';
}
diff --git a/src/utils/gtfsRegionalRanges.js b/src/utils/gtfsRegionalRanges.js
index f48fa53..89e6ada 100644
--- a/src/utils/gtfsRegionalRanges.js
+++ b/src/utils/gtfsRegionalRanges.js
@@ -187,8 +187,8 @@ export function resolveCirculationIdentity(trainNumStr, options = {}) {
isSubstitution: true,
regionCode: best.region,
brand: meta.brand,
- typeLabel: 'CAR',
- logoFile: 'track-car.svg',
+ typeLabel: 'CAR TER',
+ logoFile: 'sncf-voyageurs-logo.png',
cssClass: meta.cssClass,
color: meta.color
};