200 lines
14 KiB
JavaScript
200 lines
14 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');
|
|
const { buildAuditChanges } = require('../../../data/audit-log');
|
|
|
|
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 recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
|
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, next) {
|
|
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 } });
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'weather', eventType: 'weather-location.created', actorUserId: actorId, targetType: 'weather-location', targetId: result.insertId, targetLabel: 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);
|
|
}
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'weather', eventType: enabled ? 'weather-location.enabled' : 'weather-location.disabled', actorUserId: getAuditUserId(req), targetType: 'weather-location', targetId: location.id, targetLabel: location.name });
|
|
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);
|
|
});
|
|
}
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'weather', eventType: 'weather-location.updated', actorUserId: getAuditUserId(req), targetType: 'weather-location', targetId: location.id, targetLabel: payload.name, details: { changes: buildAuditChanges({ name: location.name, locationLabel: location.location_label, latitude: location.latitude, longitude: location.longitude, timezone: location.timezone, provider: location.provider, temperatureUnit: location.temperature_unit, windUnit: location.wind_unit, precipitationUnit: location.precipitation_unit, updateIntervalValue: location.update_interval_value, updateIntervalUnit: location.update_interval_unit }, payload) } });
|
|
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 } });
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'weather', eventType: 'weather-location.refresh_requested', actorUserId: getAuditUserId(req), targetType: 'weather-location', targetId: location.id, targetLabel: 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);
|
|
if (typeof recordRequestAuditEvent === 'function') await recordRequestAuditEvent(pool, req, { category: 'weather', eventType: 'weather-location.deleted', actorUserId: getAuditUserId(req), targetType: 'weather-location', targetId: location.id, targetLabel: location.name });
|
|
res.redirect('/data-sources/weather?message=' + encodeURIComponent('Weather location deleted.'));
|
|
} catch (error) { next(error); }
|
|
});
|
|
}; |