From 1b4ff73d3fb07e44ab55cb7d0e5796ac9d586856 Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Thu, 7 May 2026 08:51:50 +0200 Subject: [PATCH 1/7] AB#81253 fix missing qr code in svgs --- src/components/stopPoster/footer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/stopPoster/footer.js b/src/components/stopPoster/footer.js index 92f26d70..f1f62b3a 100644 --- a/src/components/stopPoster/footer.js +++ b/src/components/stopPoster/footer.js @@ -42,7 +42,7 @@ function getSvgElementPosition($element, widthModifier = 0, heightModifier = 0) function getDynamicAreas(svg, widthModifier, heightModifier) { const $ = cheerioLoad(svg); - const dynamicAreas = $('.dynamic-area'); + const dynamicAreas = $('[data-area-type]'); const areas = []; dynamicAreas.each((_, element) => { From c4649ff6cdf92a17705ce516198598e512f12343 Mon Sep 17 00:00:00 2001 From: Avi Date: Thu, 7 May 2026 09:05:16 +0200 Subject: [PATCH 2/7] Fix memory leak for poster generation (#570) --- scripts/generator.js | 194 ++++++++++++++++++++++++++++--------------- scripts/worker.js | 21 +++-- 2 files changed, 144 insertions(+), 71 deletions(-) diff --git a/scripts/generator.js b/scripts/generator.js index b53616f5..bd9fd8c1 100644 --- a/scripts/generator.js +++ b/scripts/generator.js @@ -14,7 +14,10 @@ const PDF_TIMEOUT = 5 * 60 * 1000; const MAX_RENDER_ATTEMPTS = 3; const SCALE = 96 / 72; +const BROWSER_RECYCLE_AFTER = 50; + let browser = null; +let renderCount = 0; const cwd = process.cwd(); const fileOutputDir = path.join(cwd, 'output'); @@ -22,6 +25,18 @@ const fileOutputDir = path.join(cwd, 'output'); const pdfPath = id => path.join(fileOutputDir, `${id}.pdf`); const csvPath = id => path.join(fileOutputDir, `${id}.csv`); +async function closeBrowser() { + if (browser) { + const b = browser; + browser = null; // null first so the 'disconnected' handler is a no-op + try { + await b.close(); + } catch (err) { + console.error(`[browser] Error closing browser: ${err.message}`); + } + } +} + async function initialize() { const launchOptions = { args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'], @@ -30,7 +45,9 @@ async function initialize() { launchOptions.executablePath = process.env.PUPPETEER_EXECUTABLE_PATH; } browser = await puppeteer.launch(launchOptions); + renderCount = 0; browser.on('disconnected', () => { + console.log('[browser] Browser disconnected unexpectedly, will re-launch on next job.'); browser = null; }); } @@ -72,19 +89,37 @@ async function waitFile(filePath) { } /** - * Renders component to PDF or CSV file - * @returns {Promise} + * Renders component to PDF or CSV file. + * + * The caller is responsible for passing an AbortSignal-like token via + * `options.abortSignal` so this function can close its page when a timeout + * fires from outside. + * + * @returns {Promise} posterUploaded */ async function renderComponent(options) { const { id, component, template, props, onInfo, onError } = options; const page = await browser.newPage(); + let pageClosed = false; + async function safeClosePage() { + if (!pageClosed) { + pageClosed = true; + try { + await page.close(); + } catch (err) { + console.error(`[page] Error closing page for ${id}: ${err.message}`); + } + } + } + await page.exposeFunction('serverLog', log); - page.on('error', error => { - page.close(); - browser.close(); + page.on('error', async error => { + console.error(`[page] Page crashed for ${id}: ${error.message}`); + await safeClosePage(); + await closeBrowser(); onError(error); }); @@ -99,79 +134,80 @@ async function renderComponent(options) { const pageUrl = generateRenderUrl(component, template, props, id); - console.log(`Opening ${pageUrl} in Puppeteer.`); + console.log(`[render] Opening ${pageUrl} in Puppeteer.`); - if (component === 'StopRoutePlate' && (props.downloadTable || props.downloadSummary)) { - // Allow the downloading of CSV file since the component just sends it to the client instead of actually rendering - const client = await page.createCDPSession(); - await client.send('Page.setDownloadBehavior', { - behavior: 'allow', - downloadPath: fileOutputDir, - }); + try { + if (component === 'StopRoutePlate' && (props.downloadTable || props.downloadSummary)) { + // Allow the downloading of CSV file since the component just sends it to the client instead of actually rendering + const client = await page.createCDPSession(); + await client.send('Page.setDownloadBehavior', { + behavior: 'allow', + downloadPath: fileOutputDir, + }); - const csvFilePath = props.downloadSummary ? csvPath(`summary-${id}`) : csvPath(id); + const csvFilePath = props.downloadSummary ? csvPath(`summary-${id}`) : csvPath(id); - try { await page.goto(pageUrl); await waitFile(csvFilePath); const posterUploaded = await uploadPosterToCloud(csvFilePath); - await page.close(); + await safeClosePage(); return posterUploaded; - } catch (err) { - throw new Error('StopRoutePlate CSV rendering failed'); } - } - await page.goto(pageUrl, { - timeout: RENDER_TIMEOUT, - }); + await page.goto(pageUrl, { + timeout: RENDER_TIMEOUT, + }); - const { error = null, width, height } = await page.evaluate( - () => - new Promise(resolve => { - window.callPhantom = opts => resolve(opts); - }), - ); + const { error = null, width, height } = await page.evaluate( + () => + new Promise(resolve => { + window.callPhantom = opts => resolve(opts); + }), + ); - if (error) { - throw new Error(error); - } + if (error) { + throw new Error(error); + } - await page.emulateMediaType('screen'); - - let printOptions = {}; - if (props.printTimetablesAsA4 || component === 'CoverPage') { - printOptions = { - printBackground: true, - format: 'A4', - margin: 0, - timeout: PDF_TIMEOUT, - }; - } else if (props.printAsA5) { - printOptions = { - printBackground: true, - format: 'A5', - margin: 0, - timeout: PDF_TIMEOUT, - }; - } else { - printOptions = { - printBackground: true, - width: width * SCALE, - height: height * SCALE, - pageRanges: '1', - scale: SCALE, - }; - } + await page.emulateMediaType('screen'); + + let printOptions = {}; + if (props.printTimetablesAsA4 || component === 'CoverPage') { + printOptions = { + printBackground: true, + format: 'A4', + margin: 0, + timeout: PDF_TIMEOUT, + }; + } else if (props.printAsA5) { + printOptions = { + printBackground: true, + format: 'A5', + margin: 0, + timeout: PDF_TIMEOUT, + }; + } else { + printOptions = { + printBackground: true, + width: width * SCALE, + height: height * SCALE, + pageRanges: '1', + scale: SCALE, + }; + } - const contents = await page.pdf(printOptions); + const contents = await page.pdf(printOptions); - const pdfFilePath = pdfPath(id); - await fs.outputFile(pdfFilePath, contents); - await page.close(); + const pdfFilePath = pdfPath(id); + await fs.outputFile(pdfFilePath, contents); + await safeClosePage(); - const posterUploaded = await uploadPosterToCloud(pdfFilePath); - return posterUploaded; + const posterUploaded = await uploadPosterToCloud(pdfFilePath); + return posterUploaded; + } catch (err) { + await safeClosePage(); + throw err; + } } async function generate(options) { @@ -185,15 +221,31 @@ async function generate(options) { onInfo('Creating new browser instance'); await initialize(); } + + // Recycle the browser periodically to reclaim Chromium-internal heap. + if (renderCount > 0 && renderCount % BROWSER_RECYCLE_AFTER === 0) { + await closeBrowser(); + await initialize(); + } + + let timeoutHandle; const timeout = new Promise((resolve, reject) => { - setTimeout(reject, RENDER_TIMEOUT, new Error('Render timeout')); + timeoutHandle = setTimeout(reject, RENDER_TIMEOUT, new Error('Render timeout')); }); - const posterUploaded = await Promise.race([renderComponent(options), timeout]); + let posterUploaded; + try { + posterUploaded = await Promise.race([renderComponent(options), timeout]); + } finally { + // Always clear the timeout so the closure is released immediately. + clearTimeout(timeoutHandle); + } + const uploadFailed = !posterUploaded && AZURE_STORAGE_ACCOUNT && AZURE_STORAGE_KEY; if (!uploadFailed) { onInfo('Rendered successfully.'); + renderCount += 1; } else { const err = { message: 'Rendered successfully but uploading poster failed.', stack: '' }; throw err; @@ -202,6 +254,17 @@ async function generate(options) { return { success: true, uploaded: !uploadFailed }; } catch (error) { onError(error); + if (browser) { + try { + const pages = await browser.pages(); + console.log(`[browser] Pages open after error: ${pages.length}`); + } catch (inspectErr) { + console.error( + `[browser] Cannot inspect pages after error, closing browser: ${inspectErr.message}`, + ); + await closeBrowser(); + } + } } } @@ -212,4 +275,5 @@ module.exports = { generate, generateRenderUrl, csvPath, + closeBrowser, }; diff --git a/scripts/worker.js b/scripts/worker.js index 69267586..8e8b3fe8 100644 --- a/scripts/worker.js +++ b/scripts/worker.js @@ -1,7 +1,7 @@ const { Worker, QueueScheduler } = require('bullmq'); const Redis = require('ioredis'); -const { generate } = require('./generator'); +const { generate, closeBrowser } = require('./generator'); const { addEvent, updatePoster } = require('./store'); const { REDIS_CONNECTION_STRING } = require('../constants'); @@ -75,9 +75,18 @@ worker.on('failed', (job, err) => { worker.on('drained', () => console.log('Job queue empty! Waiting for new jobs...')); -process.on('SIGINT', () => { - console.log('Shutting down worker...'); - worker.close(true); - queueScheduler.close(); +async function shutdown(signal) { + console.log(`Shutting down worker (${signal})...`); + try { + await worker.close(true); + await queueScheduler.close(); + await closeBrowser(); + } catch (err) { + console.error(`Error during shutdown: ${err.message}`); + } process.exit(0); -}); +} + +process.on('SIGINT', () => shutdown('SIGINT')); +// Docker / Kubernetes sends SIGTERM; handle it so the process drains cleanly. +process.on('SIGTERM', () => shutdown('SIGTERM')); From 175c138bfa049b48d2fb28d30454bccf4c485889 Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Mon, 1 Jun 2026 11:01:42 +0200 Subject: [PATCH 3/7] zone_E --- package-lock.json | 12 ++++++------ package.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98b3ed72..09545f29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hsl-map-publisher", - "version": "1.2.1", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hsl-map-publisher", - "version": "1.2.1", + "version": "1.2.2", "license": "AGPL-3.0-only", "dependencies": { "@azure/storage-blob": "^10.4.0", @@ -28,7 +28,7 @@ "graphql": "^0.11.7", "graphql-tag": "^2.5.0", "haversine": "^1.1.1", - "hsl-map-style": "hsldevcom/hsl-map-style#zone_e_map_styles", + "hsl-map-style": "hsldevcom/hsl-map-style#master", "html-webpack-plugin": "^5.6.3", "ioredis": "^5.0.6", "knex": "^2.0.0", @@ -10819,10 +10819,10 @@ } }, "node_modules/hsl-map-style": { - "version": "1.2.3", - "resolved": "git+ssh://git@github.com/hsldevcom/hsl-map-style.git#2e247924dde50d3e229ab5cb2650d0d2a56ff66e", + "version": "1.2.4", + "resolved": "git+ssh://git@github.com/hsldevcom/hsl-map-style.git#9345d342215f6acb124e868987bbe4891167fa10", "dependencies": { - "lodash": "^4.17.4" + "lodash": "^4.18.1" }, "bin": { "styletool": "bin/styletool-cli" diff --git a/package.json b/package.json index b208eb03..6d1c1510 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hsl-map-publisher", - "version": "1.2.1", + "version": "1.2.2", "description": "HSL Map Publisher", "main": "index.js", "scripts": { @@ -101,7 +101,7 @@ "graphql": "^0.11.7", "graphql-tag": "^2.5.0", "haversine": "^1.1.1", - "hsl-map-style": "hsldevcom/hsl-map-style#zone_e_map_styles", + "hsl-map-style": "hsldevcom/hsl-map-style#master", "html-webpack-plugin": "^5.6.3", "ioredis": "^5.0.6", "knex": "^2.0.0", From 0e31b0a28e9280bcb5c49e87ca6735684e7c287a Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Thu, 4 Jun 2026 10:21:06 +0200 Subject: [PATCH 4/7] AB#84213 fix routes being filtered out if their numbered variant has day departures --- .github/workflows/ci-cd.yml | 2 +- src/components/map/stopMapContainer.js | 25 ++++++++--------- src/components/stopPoster/routesContainer.js | 19 ++++++------- src/util/domain.js | 28 +++++++++++++++++++- 4 files changed, 49 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 45835755..a6dd556c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -19,5 +19,5 @@ jobs: with: checkAndTestOutsideDocker: true codeCoverageEnabled: true - performRelease: false + performRelease: true checkAndTestInsideDocker: false diff --git a/src/components/map/stopMapContainer.js b/src/components/map/stopMapContainer.js index a513a863..66d884ac 100644 --- a/src/components/map/stopMapContainer.js +++ b/src/components/map/stopMapContainer.js @@ -11,7 +11,7 @@ import haversine from 'haversine'; import apolloWrapper from 'util/apolloWrapper'; import config from 'util/config'; import promiseWrapper from 'util/promiseWrapper'; -import { isNumberVariant, trimRouteId, isDropOffOnly } from 'util/domain'; +import { trimRouteId, isDropOffOnly, filterRouteSegments } from 'util/domain'; import { calculateStopsViewport } from 'util/stopPoster'; import routeCompare from 'util/routeCompare'; @@ -100,14 +100,14 @@ const nearbyItemsQuery = gql` } `; -const stopsMapper = stopGroup => ({ - ...stopGroup, - // Assume all stops face the same way - calculatedHeading: stopGroup.stops.nodes[0].calculatedHeading, - routes: flatMap(stopGroup.stops.nodes, node => - node.routeSegments.nodes - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) +const stopsMapper = stopGroup => { + const allSegments = flatMap(stopGroup.stops.nodes, node => node.routeSegments.nodes); + + return { + ...stopGroup, + // Assume all stops face the same way + calculatedHeading: stopGroup.stops.nodes[0].calculatedHeading, + routes: filterRouteSegments(allSegments) .filter(routeSegment => !isDropOffOnly(routeSegment)) .map(routeSegment => { const mergedRouteSegment = mergeRouteSegments( @@ -141,9 +141,10 @@ const stopsMapper = stopGroup => ({ mode: mergedRouteSegment.route.nodes[0].mode, trunkRoute, }; - }), - ).sort(routeCompare), -}); + }) + .sort(routeCompare), + }; +}; const nearbyItemsMapper = mapProps(props => { const stops = props.data.stopGroups.nodes diff --git a/src/components/stopPoster/routesContainer.js b/src/components/stopPoster/routesContainer.js index ebee56bd..d366dda7 100644 --- a/src/components/stopPoster/routesContainer.js +++ b/src/components/stopPoster/routesContainer.js @@ -7,7 +7,7 @@ import flatMap from 'lodash/flatMap'; import groupBy from 'lodash/groupBy'; import compact from 'lodash/compact'; -import { isNumberVariant, trimRouteId, isDropOffOnly, filterRoute } from 'util/domain'; +import { trimRouteId, isDropOffOnly, filterRoute, filterRouteSegments } from 'util/domain'; import apolloWrapper from 'util/apolloWrapper'; import routeCompare from 'util/routeCompare'; @@ -47,18 +47,16 @@ const routesQuery = gql` const propsMapper = mapProps(props => { const { data, routeFilter, ...propsToForward } = props; - const stops = flatMap( + + const allSegments = flatMap( data.stop.siblings.nodes.map(s => - s.routeSegments.nodes - .map(routeSegment => ({ ...routeSegment, platform: s.platform })) - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) - .filter(routeSegment => !isDropOffOnly(routeSegment)) - .filter(routeSegment => - filterRoute({ routeId: routeSegment.routeId, filter: routeFilter }), - ), + s.routeSegments.nodes.map(routeSegment => ({ ...routeSegment, platform: s.platform })), ), ); + + const stops = filterRouteSegments(allSegments) + .filter(routeSegment => !isDropOffOnly(routeSegment)) + .filter(routeSegment => filterRoute({ routeId: routeSegment.routeId, filter: routeFilter })); const routes = stops.map(routeSegment => ({ ...routeSegment.route.nodes[0], viaFi: routeSegment.viaFi, @@ -69,7 +67,6 @@ const propsMapper = mapProps(props => { platform: routeSegment.platform, })); - // Group similar routes and place the platforminfo in the list const routesGrouped = Object.values(groupBy(routes, r => r.routeId + r.destinationFi)) .map(r => r.reduce((prev, curr) => ({ ...prev, platforms: prev.platforms.concat(curr.platform) }), { diff --git a/src/util/domain.js b/src/util/domain.js index 015e453e..ce9f8658 100644 --- a/src/util/domain.js +++ b/src/util/domain.js @@ -223,7 +223,7 @@ function groupOnConsecutive(groupedOnVersion) { } function getListOfVersionsAsString(versions) { - const alsoBasic = versions.some(version => version && version.trim() === ''); + const alsoBasic = versions.some(version => version != null && version.trim() === ''); const letters = versions.filter(version => version && version.trim() !== '').sort(); if (letters.length === 0) return ''; const letterString = letters.join(','); @@ -360,6 +360,31 @@ function getShelterText(stopType) { } } +/** + * Filters route segments, keeping only non-number-variant segments that either have regular day + * departures themselves, or have a numbered variant that does. + * @param {Array} segments + * @returns {Array} Filtered segments + */ +function filterRouteSegments(segments) { + const variantIdsWithDepartures = segments + .filter(s => isNumberVariant(s.routeId) && s.hasRegularDayDepartures === true) + .map(s => s.routeId); + + const baseRouteIdsWithVariantDepartures = new Set( + segments + .filter(s => !isNumberVariant(s.routeId)) + .filter(s => variantIdsWithDepartures.some(variantId => variantId.startsWith(s.routeId))) + .map(s => s.routeId), + ); + + return segments + .filter( + s => s.hasRegularDayDepartures === true || baseRouteIdsWithVariantDepartures.has(s.routeId), + ) + .filter(s => !isNumberVariant(s.routeId)); +} + export { isNumberVariant, isRailRoute, @@ -378,4 +403,5 @@ export { getFormattedRouteList, formatRouteString, getShelterText, + filterRouteSegments, }; From 3d6ae85536b54941cb9971a19a92988ce16dc521 Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Thu, 4 Jun 2026 11:23:55 +0200 Subject: [PATCH 5/7] promote variants if main has no day departures --- src/util/domain.js | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/util/domain.js b/src/util/domain.js index ce9f8658..06427446 100644 --- a/src/util/domain.js +++ b/src/util/domain.js @@ -367,22 +367,40 @@ function getShelterText(stopType) { * @returns {Array} Filtered segments */ function filterRouteSegments(segments) { - const variantIdsWithDepartures = segments - .filter(s => isNumberVariant(s.routeId) && s.hasRegularDayDepartures === true) - .map(s => s.routeId); + const variantsWithDepartures = segments.filter( + s => isNumberVariant(s.routeId) && s.hasRegularDayDepartures === true, + ); + + const variantIdssWithDepartures = variantsWithDepartures.map(s => s.routeId); const baseRouteIdsWithVariantDepartures = new Set( segments .filter(s => !isNumberVariant(s.routeId)) - .filter(s => variantIdsWithDepartures.some(variantId => variantId.startsWith(s.routeId))) + .filter(s => variantIdssWithDepartures.some(variantId => variantId.startsWith(s.routeId))) .map(s => s.routeId), ); - return segments - .filter( - s => s.hasRegularDayDepartures === true || baseRouteIdsWithVariantDepartures.has(s.routeId), - ) - .filter(s => !isNumberVariant(s.routeId)); + const filtered = segments.filter( + s => s.hasRegularDayDepartures === true || baseRouteIdsWithVariantDepartures.has(s.routeId), + ); + + return filtered + .filter(s => !isNumberVariant(s.routeId)) + .map(s => { + // If this base route was promoted (no own departures), copy destination/via from its variant + if (baseRouteIdsWithVariantDepartures.has(s.routeId) && s.hasRegularDayDepartures !== true) { + const variant = variantsWithDepartures.find(v => v.routeId.startsWith(s.routeId)); + if (variant) { + return { + ...s, + viaFi: variant.viaFi, + viaSe: variant.viaSe, + route: variant.route, + }; + } + } + return s; + }); } export { From fe5096b058953244059049d394f228f6b84bf91b Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Thu, 4 Jun 2026 12:25:59 +0200 Subject: [PATCH 6/7] also promote routes for route diagram to work correctly --- .../routeDiagram/routeDiagramContainer.js | 14 ++++++-------- src/util/domain.js | 10 +++++----- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/routeDiagram/routeDiagramContainer.js b/src/components/routeDiagram/routeDiagramContainer.js index 8f5d5273..eda9a7b8 100644 --- a/src/components/routeDiagram/routeDiagramContainer.js +++ b/src/components/routeDiagram/routeDiagramContainer.js @@ -5,7 +5,7 @@ import compose from 'recompose/compose'; import mapProps from 'recompose/mapProps'; import flatMap from 'lodash/flatMap'; import sortBy from 'lodash/sortBy'; -import { isNumberVariant, trimRouteId, isDropOffOnly, filterRoute } from 'util/domain'; +import { trimRouteId, isDropOffOnly, filterRoute, filterRouteSegments } from 'util/domain'; import apolloWrapper from 'util/apolloWrapper'; import { routesToTree } from 'util/routes'; @@ -100,11 +100,9 @@ const nodeToStop = ({ stopByStopId }) => { const propsMapper = mapProps(props => { const routes = flatMap(props.data.stops.nodes, s => - flatMap(s.siblings.nodes, stop => - stop.routeSegments.nodes - // Select regular routes that allow boarding from current stop - .filter(routeSegment => routeSegment.hasRegularDayDepartures === true) - .filter(routeSegment => !isNumberVariant(routeSegment.routeId)) + flatMap(s.siblings.nodes, stop => { + const allSegments = stop.routeSegments.nodes; + return filterRouteSegments(allSegments) .filter(routeSegment => !isDropOffOnly(routeSegment)) .filter(routeSegment => filterRoute({ routeId: routeSegment.routeId, filter: props.routeFilter }), @@ -115,8 +113,8 @@ const propsMapper = mapProps(props => { trunkRoute: routeSegment.line.nodes[0].trunkRoute === '1', // List all stops (including drop-off only) for each route stops: sortBy(routeSegment.nextStops.nodes, node => node.stopIndex).map(nodeToStop), - })), - ), + })); + }), ); const treeMaxWidth = props.maxColumns ? props.maxColumns : props.printAsA3 ? 5 : 6; // Defaults 6 for normal posters and 5 for a3 posters. diff --git a/src/util/domain.js b/src/util/domain.js index 06427446..f6f6b649 100644 --- a/src/util/domain.js +++ b/src/util/domain.js @@ -387,15 +387,15 @@ function filterRouteSegments(segments) { return filtered .filter(s => !isNumberVariant(s.routeId)) .map(s => { - // If this base route was promoted (no own departures), copy destination/via from its variant if (baseRouteIdsWithVariantDepartures.has(s.routeId) && s.hasRegularDayDepartures !== true) { const variant = variantsWithDepartures.find(v => v.routeId.startsWith(s.routeId)); if (variant) { + // promote variant to main route + // but keep the base routeId and platform. return { - ...s, - viaFi: variant.viaFi, - viaSe: variant.viaSe, - route: variant.route, + ...variant, + routeId: s.routeId, + platform: s.platform, }; } } From b6ba5d7449b65518f8331e344e70f310736aec6f Mon Sep 17 00:00:00 2001 From: Sebastian Szeszko Date: Thu, 4 Jun 2026 14:37:49 +0200 Subject: [PATCH 7/7] v1.2.3 --- .github/workflows/ci-cd.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a6dd556c..45835755 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -19,5 +19,5 @@ jobs: with: checkAndTestOutsideDocker: true codeCoverageEnabled: true - performRelease: true + performRelease: false checkAndTestInsideDocker: false diff --git a/package.json b/package.json index 6d1c1510..7e945096 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hsl-map-publisher", - "version": "1.2.2", + "version": "1.2.3", "description": "HSL Map Publisher", "main": "index.js", "scripts": {