Files
pulse-signage/src/web/routes/data-sources/weather.js
T
lzstealth 3960931ebe
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m53s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s
Add onboarding weather and template gradients
2026-08-28 20:05:23 +01:00

193 lines
12 KiB
JavaScript

// Weather location source CRUD routes.
const { buildPagination } = require('../../lib/pagination');
const renderWeatherLocationsPage = require('./weather/list');
const renderWeatherLocationAddPage = require('./weather/add');
const renderWeatherLocationEditPage = require('./weather/edit');
const { buildDuplicateWeatherLocationName, buildDuplicateWeatherLocation } = require('./weather/duplicate');
const { fetchAppSettings } = require('../../../data/app-settings');
async function getWeatherLocationUsageIds(pool, common) {
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
const weatherLocationIds = new Set();
slides.forEach(function (slide) {
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(slide.content_json) : null;
if (!content || typeof content !== 'object') return;
(function walk(value) {
if (!value || typeof value !== 'object') return;
if (Array.isArray(value)) {
value.forEach(walk);
return;
}
if (Object.prototype.hasOwnProperty.call(value, 'weather_location_id')) {
const locationId = Number(value.weather_location_id);
if (Number.isFinite(locationId)) weatherLocationIds.add(locationId);
}
Object.keys(value).forEach(function (key) { walk(value[key]); });
}(content));
});
return weatherLocationIds;
}
async function isWeatherLocationInUse(pool, common, locationId) {
const usageIds = await getWeatherLocationUsageIds(pool, common);
return usageIds.has(Number(locationId));
}
module.exports = function registerWeatherRoutes(app, deps) {
const pool = deps.pool;
const common = deps.common;
const getAuditUserId = deps.getAuditUserId;
const redirectAfterSave = deps.redirectAfterSave;
const dataSourceTasks = deps.dataSourceTasks;
const backgroundTaskQueue = deps.backgroundTaskQueue;
const requirePermission = deps.requirePermission;
const listPageSize = 25;
async function getProviderAvailability() {
const settings = await fetchAppSettings(pool);
return { openMeteo: true, pirateWeather: Boolean(String(settings['weather.pirate_weather_api_key'] || '').trim()) };
}
app.get('/data-sources/weather', requirePermission('weather.read'), async function (req, res, next) {
try {
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
const search = common.getSearchQuery(req);
const sort = common.getSortQuery(req);
const direction = common.getSortDirectionQuery(req);
const data = await common.fetchWeatherLocationsPage(pool, page, listPageSize, search, sort, direction);
const usageIds = await getWeatherLocationUsageIds(pool, common);
res.send(renderWeatherLocationsPage({
weatherLocations: data.rows.map(function (location) { return Object.assign({}, location, { inUse: usageIds.has(Number(location.id)) }); }),
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search, sort, direction }, listPageSize, 'weather locations', 'weather location pages')
}, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) { next(error); }
});
app.get('/data-sources/weather/new', requirePermission('weather.create'), function (req, res) {
getProviderAvailability().then(function (providerAvailability) {
res.send(renderWeatherLocationAddPage({}, req.query.message ? String(req.query.message) : '', req.currentUser, { providerAvailability: providerAvailability }));
}).catch(next);
});
app.get('/data-sources/weather/geocode', requirePermission('weather.read'), async function (req, res, next) {
try {
const results = await common.fetchWeatherLocationSuggestions(req.query.q);
res.json({ results: results });
} catch (error) { next(error); }
});
app.get('/data-sources/weather/:id/edit', requirePermission('weather.update'), async function (req, res, next) {
try {
const location = await common.fetchWeatherLocationById(pool, Number(req.params.id));
if (!location) return res.status(404).send('Weather location not found');
const providerAvailability = await getProviderAvailability();
const inUse = await isWeatherLocationInUse(pool, common, location.id);
res.send(renderWeatherLocationEditPage(Object.assign({}, location, { inUse: inUse }), req.query.message ? String(req.query.message) : '', req.currentUser, { providerAvailability: providerAvailability }));
} catch (error) { next(error); }
});
app.get('/data-sources/weather/:id/duplicate', requirePermission('weather.read'), requirePermission('weather.create'), async function (req, res, next) {
try {
const location = await common.fetchWeatherLocationById(pool, Number(req.params.id));
if (!location) return res.status(404).send('Weather location not found');
let duplicateName = buildDuplicateWeatherLocationName(location.name);
let duplicateIndex = 2;
while (await common.fetchDuplicateName(pool, 'i_weather_locations', duplicateName)) {
duplicateName = buildDuplicateWeatherLocationName(location.name) + ' (' + duplicateIndex + ')';
duplicateIndex += 1;
}
res.send(renderWeatherLocationAddPage(buildDuplicateWeatherLocation(location, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
providerAvailability: await getProviderAvailability(),
messageVariant: 'info',
showSaveSecondaryActions: true
}));
} catch (error) { next(error); }
});
app.post('/data-sources/weather', requirePermission('weather.create'), async function (req, res, next) {
const connection = await pool.getConnection();
try {
const payload = common.buildWeatherLocationPayload(req, null);
if (await common.fetchDuplicateName(pool, 'i_weather_locations', payload.name)) {
return res.redirect('/data-sources/weather/new?message=' + encodeURIComponent('A weather location with that name already exists.'));
}
const actorId = getAuditUserId(req);
await connection.beginTransaction();
const [result] = await connection.query('INSERT INTO i_weather_locations (name, location_label, latitude, longitude, timezone, provider, temperature_unit, wind_unit, precipitation_unit, update_interval_value, update_interval_unit, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [payload.name, payload.locationLabel, payload.latitude, payload.longitude, payload.timezone, payload.provider, payload.temperatureUnit, payload.windUnit, payload.precipitationUnit, payload.updateIntervalValue, payload.updateIntervalUnit, actorId, actorId]);
await connection.commit();
dataSourceTasks.registerRecurringRefresh('weather-location', result.insertId, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
return dataSourceTasks.refreshWeatherLocationInBackground(result.insertId, null);
});
await backgroundTaskQueue.enqueueTask({ key: 'weather-location-refresh:' + result.insertId, title: 'Weather location refresh', category: 'data-source', taskType: 'data-source-refresh', payload: { sourceType: 'weather-location', sourceId: result.insertId, sourceName: payload.name, actorId: actorId }, metadata: { sourceType: 'weather-location', sourceId: result.insertId, sourceName: payload.name } });
redirectAfterSave(req, res, '/data-sources/weather/' + result.insertId + '/edit', { closeUrl: '/data-sources/weather', newUrl: '/data-sources/weather/new', message: 'Weather location created.' });
} catch (error) {
try { await connection.rollback(); } catch (_rollbackError) { }
next(error);
} finally { connection.release(); }
});
app.post('/data-sources/weather/:id', requirePermission('weather.update'), async function (req, res, next) {
const connection = await pool.getConnection();
try {
const location = await common.fetchWeatherLocationById(pool, Number(req.params.id));
if (!location) return res.status(404).send('Weather location not found');
if (String(req.body && req.body.data_source_action || '').trim() === 'toggle') {
const enabled = location.enabled === 0 || location.enabled === false ? 1 : 0;
await connection.query('UPDATE i_weather_locations SET enabled = ?, modified_by = ? WHERE id = ?', [enabled, getAuditUserId(req), location.id]);
if (enabled) {
dataSourceTasks.registerRecurringRefresh('weather-location', location.id, location.name, location.update_interval_value, location.update_interval_unit, function () {
return dataSourceTasks.refreshWeatherLocationInBackground(location.id, getAuditUserId(req));
});
} else {
dataSourceTasks.removeRecurringRefresh('weather-location', location.id);
}
return res.redirect('/data-sources/weather/' + location.id + '/edit?message=' + encodeURIComponent(enabled ? 'Weather location enabled.' : 'Weather location disabled.'));
}
const payload = common.buildWeatherLocationPayload(req, location);
if (await common.fetchDuplicateName(pool, 'i_weather_locations', payload.name, location.id)) {
return res.redirect('/data-sources/weather/' + location.id + '/edit?message=' + encodeURIComponent('A weather location with that name already exists.'));
}
await connection.beginTransaction();
await connection.query('UPDATE i_weather_locations SET name = ?, location_label = ?, latitude = ?, longitude = ?, timezone = ?, provider = ?, temperature_unit = ?, wind_unit = ?, precipitation_unit = ?, update_interval_value = ?, update_interval_unit = ?, modified_by = ? WHERE id = ?', [payload.name, payload.locationLabel, payload.latitude, payload.longitude, payload.timezone, payload.provider, payload.temperatureUnit, payload.windUnit, payload.precipitationUnit, payload.updateIntervalValue, payload.updateIntervalUnit, getAuditUserId(req), location.id]);
await connection.commit();
if (location.enabled === 0 || location.enabled === false) {
dataSourceTasks.removeRecurringRefresh('weather-location', location.id);
} else {
dataSourceTasks.registerRecurringRefresh('weather-location', location.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
return dataSourceTasks.refreshWeatherLocationInBackground(location.id, null);
});
}
redirectAfterSave(req, res, '/data-sources/weather/' + location.id + '/edit', { closeUrl: '/data-sources/weather', newUrl: '/data-sources/weather/new', message: 'Weather location updated.' });
} catch (error) {
try { await connection.rollback(); } catch (_rollbackError) { }
next(error);
} finally { connection.release(); }
});
app.post('/data-sources/weather/:id/refresh', requirePermission('weather.allow'), async function (req, res, next) {
try {
const location = await common.fetchWeatherLocationById(pool, Number(req.params.id));
if (!location) return res.status(404).send('Weather location not found');
if (location.enabled === 0 || location.enabled === false) {
return res.redirect('/data-sources/weather/' + location.id + '/edit?message=' + encodeURIComponent('Weather location is disabled.'));
}
await backgroundTaskQueue.enqueueTask({ key: 'weather-location-refresh:' + location.id, title: 'Weather location refresh', category: 'data-source', taskType: 'data-source-refresh', payload: { sourceType: 'weather-location', sourceId: location.id, sourceName: location.name, actorId: getAuditUserId(req) }, metadata: { sourceType: 'weather-location', sourceId: location.id, sourceName: location.name } });
res.redirect('/data-sources/weather/' + location.id + '/edit?message=' + encodeURIComponent('Weather refresh queued.'));
} catch (error) { next(error); }
});
app.post('/data-sources/weather/:id/delete', requirePermission('weather.delete'), async function (req, res, next) {
try {
const location = await common.fetchWeatherLocationById(pool, Number(req.params.id));
if (!location) return res.status(404).send('Weather location not found');
if (await isWeatherLocationInUse(pool, common, location.id)) {
return res.redirect('/data-sources/weather?message=' + encodeURIComponent('This weather location is still used by one or more slides.'));
}
await pool.query('DELETE FROM i_weather_locations WHERE id = ?', [location.id]);
dataSourceTasks.removeRecurringRefresh('weather-location', location.id);
res.redirect('/data-sources/weather?message=' + encodeURIComponent('Weather location deleted.'));
} catch (error) { next(error); }
});
};