Files
SignageHTML/server/routes/departures.js
kyle cdfd32dc69 Items 16-22: Icon classification, accessibility, responsive design, API params
- Consolidate 5 weather icon methods into classifyWeatherIcon/applyWeatherIconClasses
- Add focus-visible styles, ARIA attributes, keyboard nav on config button/modal
- Add responsive breakpoints for departure cards, weather widget, config modal
- Simplify CSS selectors: replace :not(:is(...)) chains with body.normal
- Fix upsidedown layout margin assumption with transform-based centering
- Upgrade weather icons from @2x to @4x for high-DPI displays
- Add forecast window and transport filter params to SL departures API

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 14:39:45 +01:00

121 lines
3.7 KiB
JavaScript

/**
* Departures route handler
* Handles fetching and returning transit departure data
*/
const https = require('https');
/**
* Fetch departures for a specific site from SL Transport API
* @param {string} siteId - The site ID to fetch departures for
* @param {Object} options - Query options
* @param {number} options.forecast - Time window in minutes (default: 60)
* @param {string} options.transport - Transport mode filter (e.g. 'BUS', 'METRO')
* @returns {Promise<Object>} - Departure data
*/
function fetchDeparturesForSite(siteId, options = {}) {
return new Promise((resolve, reject) => {
const params = new URLSearchParams();
params.set('forecast', options.forecast || 60);
if (options.transport) {
params.set('transport', options.transport);
}
const apiUrl = `https://transport.integration.sl.se/v1/sites/${siteId}/departures?${params}`;
console.log(`Fetching data from: ${apiUrl}`);
https.get(apiUrl, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const parsedData = JSON.parse(data);
resolve(parsedData);
} catch (error) {
console.error('Error parsing departures API response:', error);
resolve({
departures: [],
error: 'Failed to parse API response: ' + error.message
});
}
});
}).on('error', (error) => {
console.error('Error fetching departures:', error);
reject(error);
});
});
}
/**
* Fetch departures for all enabled sites
* @param {Array} enabledSites - Array of enabled site configurations
* @returns {Promise<Object>} - Object with sites array containing departure data
*/
async function fetchAllDepartures(enabledSites, globalOptions = {}) {
if (enabledSites.length === 0) {
return { sites: [], error: 'No enabled sites configured' };
}
try {
const sitesPromises = enabledSites.map(async (site) => {
try {
const siteOptions = {
forecast: site.forecast || globalOptions.forecast || 60,
transport: site.transport || globalOptions.transport || ''
};
const departureData = await fetchDeparturesForSite(site.id, siteOptions);
return {
siteId: site.id,
siteName: site.name,
data: departureData
};
} catch (error) {
console.error(`Error fetching departures for site ${site.id}:`, error);
return {
siteId: site.id,
siteName: site.name,
error: error.message
};
}
});
const results = await Promise.all(sitesPromises);
return { sites: results };
} catch (error) {
console.error('Error fetching all departures:', error);
return { sites: [], error: error.message };
}
}
/**
* Handle departures API endpoint
* @param {http.IncomingMessage} req - HTTP request object
* @param {http.ServerResponse} res - HTTP response object
* @param {Object} config - Application configuration
*/
async function handleDepartures(req, res, config) {
try {
const enabledSites = config.sites.filter(site => site.enabled);
const globalOptions = {
forecast: config.forecast || 60,
transport: config.transport || ''
};
const data = await fetchAllDepartures(enabledSites, globalOptions);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
} catch (error) {
console.error('Error handling departures request:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: error.message }));
}
}
module.exports = {
handleDepartures,
fetchDeparturesForSite,
fetchAllDepartures
};