Release 2.9.0
This commit is contained in:
@@ -4,6 +4,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { loadFontLibrary } = require('#src/web/lib/media/font-library');
|
||||
const { fetchAppSettings } = require('#src/data/app-settings');
|
||||
const { convertWeatherSnapshot } = require('#src/data/weather-units');
|
||||
const { buildAuditChanges } = require('#src/data/audit-log');
|
||||
const { PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
|
||||
|
||||
@@ -65,6 +66,7 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
const rssData = await common.fetchRssFeedsData(pool);
|
||||
const apiData = typeof common.fetchApiSourcesData === 'function' ? await common.fetchApiSourcesData(pool) : { apiSources: [] };
|
||||
const timetableData = typeof common.fetchTimetablesData === 'function' ? await common.fetchTimetablesData(pool) : { timetableGroups: [] };
|
||||
const weatherData = typeof common.fetchWeatherLocationsData === 'function' ? await common.fetchWeatherLocationsData(pool) : { weatherLocations: [] };
|
||||
const appSettings = await fetchAppSettings(pool);
|
||||
const apiSources = Array.isArray(apiData.apiSources) ? apiData.apiSources.map(function (source) {
|
||||
return Object.assign({}, source, {
|
||||
@@ -80,10 +82,16 @@ module.exports = function registerContentRoutes(app, deps) {
|
||||
}) });
|
||||
}));
|
||||
|
||||
return Object.assign(data, rssData, apiData, timetableData, {
|
||||
const weatherLocations = Array.isArray(weatherData.weatherLocations) ? weatherData.weatherLocations.map(function (location) {
|
||||
const responseJson = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(location.last_response_json) : null;
|
||||
return Object.assign({}, location, { responseJson: convertWeatherSnapshot(responseJson, { temperature: location.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius', wind: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'ms' : 'kmh', precipitation: location.precipitation_unit === 'inch' ? 'inch' : 'mm' }) });
|
||||
}) : [];
|
||||
|
||||
return Object.assign(data, rssData, apiData, timetableData, weatherData, {
|
||||
rssFeeds: rssFeeds,
|
||||
apiSources: apiSources,
|
||||
timetableGroups: timetableData.timetableGroups || [],
|
||||
weatherLocations: weatherLocations,
|
||||
fontLibrary: loadFontLibrary(deps.uploadDir),
|
||||
uploadLimits: {
|
||||
imageMaxBytes: Number(appSettings['uploads.image_max_bytes']) || IMAGE_UPLOAD_MAX_BYTES,
|
||||
|
||||
@@ -15,13 +15,16 @@ module.exports = function registerDataSourceRoutes(app, deps) {
|
||||
return res.redirect('/login');
|
||||
}
|
||||
|
||||
if (hasAnyPermission(req.currentUser, ['timetables.read', 'rss-feeds.read', 'api-sources.read'])) {
|
||||
if (hasAnyPermission(req.currentUser, ['timetables.read', 'rss-feeds.read', 'api-sources.read', 'weather.read'])) {
|
||||
if (hasAnyPermission(req.currentUser, ['timetables.read'])) {
|
||||
return res.redirect('/data-sources/timetables');
|
||||
}
|
||||
if (hasAnyPermission(req.currentUser, ['rss-feeds.read'])) {
|
||||
return res.redirect('/data-sources/rss-feeds');
|
||||
}
|
||||
if (hasAnyPermission(req.currentUser, ['weather.read'])) {
|
||||
return res.redirect('/data-sources/weather');
|
||||
}
|
||||
return res.redirect('/data-sources/api-sources');
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
function buildDefaultApiSource() {
|
||||
return {
|
||||
id: null,
|
||||
enabled: 1,
|
||||
name: '',
|
||||
apiUrl: '',
|
||||
requestMethod: 'GET',
|
||||
|
||||
@@ -288,6 +288,19 @@ module.exports = function registerApiSourceRoutes(app, deps) {
|
||||
return res.status(404).send('API source not found');
|
||||
}
|
||||
|
||||
if (String(req.body && req.body.data_source_action || '').trim() === 'toggle') {
|
||||
const enabled = apiSource.enabled === 0 || apiSource.enabled === false ? 1 : 0;
|
||||
await connection.query('UPDATE i_api_sources SET enabled = ?, modified_by = ? WHERE id = ?', [enabled, getAuditUserId(req), apiSource.id]);
|
||||
if (enabled) {
|
||||
dataSourceTasks.registerRecurringRefresh('api-source', apiSource.id, apiSource.name, apiSource.update_interval_value, apiSource.update_interval_unit, function () {
|
||||
return dataSourceTasks.refreshApiSourceInBackground(apiSource.id, getAuditUserId(req));
|
||||
});
|
||||
} else {
|
||||
dataSourceTasks.removeRecurringRefresh('api-source', apiSource.id);
|
||||
}
|
||||
return res.redirect('/data-sources/api-sources/' + apiSource.id + '/edit?message=' + encodeURIComponent(enabled ? 'API source enabled.' : 'API source disabled.'));
|
||||
}
|
||||
|
||||
const payload = common.buildApiSourcePayload(req, apiSource);
|
||||
if (await common.fetchDuplicateName(pool, 'i_api_sources', payload.name, apiSource.id)) {
|
||||
return res.redirect('/data-sources/api-sources/' + apiSource.id + '/edit?message=' + encodeURIComponent('An API source with that name already exists.'));
|
||||
@@ -295,35 +308,21 @@ module.exports = function registerApiSourceRoutes(app, deps) {
|
||||
const actorId = getAuditUserId(req);
|
||||
await connection.beginTransaction();
|
||||
await connection.query(
|
||||
'UPDATE i_api_sources SET name = ?, api_url = ?, request_method = ?, request_body_json = ?, auth_method = ?, auth_username = ?, auth_password = ?, auth_bearer_token = ?, auth_header_name = ?, auth_header_value = ?, token_url = ?, token_request_body_json = ?, token_response_path = ?, token_header_name = ?, token_header_prefix = ?, items_path = ?, update_interval_value = ?, update_interval_unit = ?, last_pulled_at = ?, last_pull_error = ?, last_response_status = ?, last_response_content_type = ?, last_response_json = ?, modified_by = ? WHERE id = ?',
|
||||
[payload.name, payload.apiUrl, payload.requestMethod, payload.requestBodyJson || null, payload.authMethod, payload.authUsername || null, payload.authPassword || null, payload.authBearerToken || null, payload.authHeaderName || null, payload.authHeaderValue || null, payload.tokenUrl || null, payload.tokenRequestBodyJson || null, payload.tokenResponsePath || null, payload.tokenHeaderName || null, payload.tokenHeaderPrefix || null, payload.itemsPath || null, payload.updateIntervalValue, payload.updateIntervalUnit, null, null, null, null, null, actorId, apiSource.id]
|
||||
'UPDATE i_api_sources SET name = ?, api_url = ?, request_method = ?, request_body_json = ?, auth_method = ?, auth_username = ?, auth_password = ?, auth_bearer_token = ?, auth_header_name = ?, auth_header_value = ?, token_url = ?, token_request_body_json = ?, token_response_path = ?, token_header_name = ?, token_header_prefix = ?, items_path = ?, update_interval_value = ?, update_interval_unit = ?, modified_by = ? WHERE id = ?',
|
||||
[payload.name, payload.apiUrl, payload.requestMethod, payload.requestBodyJson || null, payload.authMethod, payload.authUsername || null, payload.authPassword || null, payload.authBearerToken || null, payload.authHeaderName || null, payload.authHeaderValue || null, payload.tokenUrl || null, payload.tokenRequestBodyJson || null, payload.tokenResponsePath || null, payload.tokenHeaderName || null, payload.tokenHeaderPrefix || null, payload.itemsPath || null, payload.updateIntervalValue, payload.updateIntervalUnit, actorId, apiSource.id]
|
||||
);
|
||||
await connection.commit();
|
||||
dataSourceTasks.registerRecurringRefresh('api-source', apiSource.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
||||
return dataSourceTasks.refreshApiSourceInBackground(apiSource.id, actorId);
|
||||
});
|
||||
const message = 'API source updated. Refresh is running in the background.';
|
||||
const refreshTask = await backgroundTaskQueue.enqueueTask({
|
||||
key: 'api-source-refresh:' + apiSource.id,
|
||||
title: 'API source refresh',
|
||||
category: 'data-source',
|
||||
taskType: 'data-source-refresh',
|
||||
payload: {
|
||||
sourceType: 'api-source',
|
||||
sourceId: apiSource.id,
|
||||
sourceName: payload.name,
|
||||
actorId: actorId
|
||||
},
|
||||
metadata: {
|
||||
sourceType: 'api-source',
|
||||
sourceId: apiSource.id,
|
||||
sourceName: payload.name
|
||||
}
|
||||
});
|
||||
redirectAfterSave(req, res, '/data-sources/api-sources/' + apiSource.id + '/edit?refresh_task_id=' + encodeURIComponent(refreshTask && refreshTask.id ? refreshTask.id : ''), {
|
||||
if (apiSource.enabled === 0 || apiSource.enabled === false) {
|
||||
dataSourceTasks.removeRecurringRefresh('api-source', apiSource.id);
|
||||
} else {
|
||||
dataSourceTasks.registerRecurringRefresh('api-source', apiSource.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
||||
return dataSourceTasks.refreshApiSourceInBackground(apiSource.id, actorId);
|
||||
});
|
||||
}
|
||||
redirectAfterSave(req, res, '/data-sources/api-sources/' + apiSource.id + '/edit', {
|
||||
closeUrl: '/data-sources/api-sources',
|
||||
newUrl: '/data-sources/api-sources/new',
|
||||
message: message
|
||||
message: 'API source updated.'
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
@@ -337,6 +336,18 @@ module.exports = function registerApiSourceRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/data-sources/api-sources/:id/refresh', requirePermission('api-sources.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const apiSource = await common.fetchApiSourceById(pool, Number(req.params.id));
|
||||
if (!apiSource) return res.status(404).send('API source not found');
|
||||
if (apiSource.enabled === 0 || apiSource.enabled === false) {
|
||||
return res.redirect('/data-sources/api-sources/' + apiSource.id + '/edit?message=' + encodeURIComponent('API source is disabled.'));
|
||||
}
|
||||
await backgroundTaskQueue.enqueueTask({ key: 'api-source-refresh:' + apiSource.id, title: 'API source refresh', category: 'data-source', taskType: 'data-source-refresh', payload: { sourceType: 'api-source', sourceId: apiSource.id, sourceName: apiSource.name, actorId: getAuditUserId(req) }, metadata: { sourceType: 'api-source', sourceId: apiSource.id, sourceName: apiSource.name } });
|
||||
res.redirect('/data-sources/api-sources/' + apiSource.id + '/edit?message=' + encodeURIComponent('API source refresh queued.'));
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
app.post('/data-sources/api-sources/:id/delete', requirePermission('api-sources.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const apiSource = await common.fetchApiSourceById(pool, Number(req.params.id));
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
function buildDefaultRssFeed() {
|
||||
return {
|
||||
id: null,
|
||||
enabled: 1,
|
||||
name: '',
|
||||
feedUrl: '',
|
||||
updateIntervalValue: 60,
|
||||
|
||||
@@ -249,6 +249,19 @@ module.exports = function registerRssFeedRoutes(app, deps) {
|
||||
return res.status(404).send('RSS feed not found');
|
||||
}
|
||||
|
||||
if (String(req.body && req.body.data_source_action || '').trim() === 'toggle') {
|
||||
const enabled = rssFeed.enabled === 0 || rssFeed.enabled === false ? 1 : 0;
|
||||
await connection.query('UPDATE i_rss_feeds SET enabled = ?, modified_by = ? WHERE id = ?', [enabled, getAuditUserId(req), rssFeed.id]);
|
||||
if (enabled) {
|
||||
dataSourceTasks.registerRecurringRefresh('rss-feed', rssFeed.id, rssFeed.name, rssFeed.update_interval_value, rssFeed.update_interval_unit, function () {
|
||||
return dataSourceTasks.refreshRssFeedInBackground(rssFeed.id, rssFeed.feed_url, rssFeed.item_limit, getAuditUserId(req));
|
||||
});
|
||||
} else {
|
||||
dataSourceTasks.removeRecurringRefresh('rss-feed', rssFeed.id);
|
||||
}
|
||||
return res.redirect('/data-sources/rss-feeds/' + rssFeed.id + '/edit?message=' + encodeURIComponent(enabled ? 'RSS feed enabled.' : 'RSS feed disabled.'));
|
||||
}
|
||||
|
||||
const payload = common.buildRssFeedPayload(req, rssFeed);
|
||||
if (await common.fetchDuplicateName(pool, 'i_rss_feeds', payload.name, rssFeed.id)) {
|
||||
return res.redirect('/data-sources/rss-feeds/' + rssFeed.id + '/edit?message=' + encodeURIComponent('An RSS feed with that name already exists.'));
|
||||
@@ -260,31 +273,17 @@ module.exports = function registerRssFeedRoutes(app, deps) {
|
||||
[payload.name, payload.feedUrl, payload.updateIntervalValue, payload.updateIntervalUnit, payload.itemLimit, actorId, rssFeed.id]
|
||||
);
|
||||
await connection.commit();
|
||||
dataSourceTasks.registerRecurringRefresh('rss-feed', rssFeed.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
||||
return dataSourceTasks.refreshRssFeedInBackground(rssFeed.id, payload.feedUrl, payload.itemLimit, actorId);
|
||||
});
|
||||
const message = 'RSS feed updated. Refresh is running in the background.';
|
||||
const refreshTask = await backgroundTaskQueue.enqueueTask({
|
||||
key: 'rss-feed-refresh:' + rssFeed.id,
|
||||
title: 'RSS feed refresh',
|
||||
category: 'data-source',
|
||||
taskType: 'data-source-refresh',
|
||||
payload: {
|
||||
sourceType: 'rss-feed',
|
||||
sourceId: rssFeed.id,
|
||||
sourceName: payload.name,
|
||||
actorId: actorId
|
||||
},
|
||||
metadata: {
|
||||
sourceType: 'rss-feed',
|
||||
sourceId: rssFeed.id,
|
||||
sourceName: payload.name
|
||||
}
|
||||
});
|
||||
redirectAfterSave(req, res, '/data-sources/rss-feeds/' + rssFeed.id + '/edit?refresh_task_id=' + encodeURIComponent(refreshTask && refreshTask.id ? refreshTask.id : ''), {
|
||||
if (rssFeed.enabled === 0 || rssFeed.enabled === false) {
|
||||
dataSourceTasks.removeRecurringRefresh('rss-feed', rssFeed.id);
|
||||
} else {
|
||||
dataSourceTasks.registerRecurringRefresh('rss-feed', rssFeed.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
||||
return dataSourceTasks.refreshRssFeedInBackground(rssFeed.id, payload.feedUrl, payload.itemLimit, actorId);
|
||||
});
|
||||
}
|
||||
redirectAfterSave(req, res, '/data-sources/rss-feeds/' + rssFeed.id + '/edit', {
|
||||
closeUrl: '/data-sources/rss-feeds',
|
||||
newUrl: '/data-sources/rss-feeds/new',
|
||||
message: message
|
||||
message: 'RSS feed updated.'
|
||||
});
|
||||
} catch (error) {
|
||||
try {
|
||||
@@ -298,6 +297,18 @@ module.exports = function registerRssFeedRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/data-sources/rss-feeds/:id/refresh', requirePermission('rss-feeds.allow'), async function (req, res, next) {
|
||||
try {
|
||||
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
||||
if (!rssFeed) return res.status(404).send('RSS feed not found');
|
||||
if (rssFeed.enabled === 0 || rssFeed.enabled === false) {
|
||||
return res.redirect('/data-sources/rss-feeds/' + rssFeed.id + '/edit?message=' + encodeURIComponent('RSS feed is disabled.'));
|
||||
}
|
||||
await backgroundTaskQueue.enqueueTask({ key: 'rss-feed-refresh:' + rssFeed.id, title: 'RSS feed refresh', category: 'data-source', taskType: 'data-source-refresh', payload: { sourceType: 'rss-feed', sourceId: rssFeed.id, sourceName: rssFeed.name, actorId: getAuditUserId(req) }, metadata: { sourceType: 'rss-feed', sourceId: rssFeed.id, sourceName: rssFeed.name } });
|
||||
res.redirect('/data-sources/rss-feeds/' + rssFeed.id + '/edit?message=' + encodeURIComponent('RSS feed refresh queued.'));
|
||||
} catch (error) { next(error); }
|
||||
});
|
||||
|
||||
app.post('/data-sources/rss-feeds/:id/delete', requirePermission('rss-feeds.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
// 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 { 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.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); }
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const buildWeatherLocationFormViewModel = require('./form-view-model');
|
||||
module.exports = function renderWeatherLocationAddPage(location, message, currentUser, options) { return renderView('data-sources/weather/form', buildWeatherLocationFormViewModel(location, message, currentUser, false, options)); };
|
||||
@@ -0,0 +1,3 @@
|
||||
const { renderView } = require('../../../view');
|
||||
const buildWeatherLocationFormViewModel = require('./form-view-model');
|
||||
module.exports = function renderWeatherLocationEditPage(location, message, currentUser, options) { return renderView('data-sources/weather/form', buildWeatherLocationFormViewModel(location, message, currentUser, true, options)); };
|
||||
@@ -0,0 +1,69 @@
|
||||
const { convertWeatherSnapshot } = require('../../../../data/weather-units');
|
||||
|
||||
function buildDefaultWeatherLocation() {
|
||||
return { id: null, enabled: 1, name: '', location_label: '', latitude: '', longitude: '', timezone: 'Europe/London', provider: 'open-meteo', temperature_unit: 'celsius', wind_unit: 'kmh', precipitation_unit: 'mm', update_interval_value: 30, update_interval_unit: 'minutes' };
|
||||
}
|
||||
|
||||
function weatherIconForCode(code) {
|
||||
const value = Number(code);
|
||||
if (!Number.isFinite(value)) return 'bi-cloud';
|
||||
if (value === 0) return 'bi-sun';
|
||||
if (value <= 3) return 'bi-cloud-sun';
|
||||
if (value <= 48) return 'bi-cloud-fog';
|
||||
if (value <= 67) return 'bi-cloud-rain';
|
||||
if (value <= 77) return 'bi-cloud-snow';
|
||||
if (value <= 82) return 'bi-cloud-rain';
|
||||
return 'bi-cloud-lightning-rain';
|
||||
}
|
||||
|
||||
function buildWeatherPreview(location) {
|
||||
const preview = {
|
||||
hasSnapshot: false,
|
||||
forecast: [{ day: 'Today', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Tomorrow', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Day 3', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Day 4', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Day 5', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Day 6', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }, { day: 'Day 7', condition: '-', icon: 'bi-cloud', high: '-', low: '-', rain: '-' }],
|
||||
hourly: Array.from({ length: 24 }, function () { return { day: '--:--', icon: 'bi-cloud', temperature: '-', rain: '-' }; }),
|
||||
};
|
||||
let snapshot;
|
||||
try {
|
||||
snapshot = JSON.parse(location && location.last_response_json || '');
|
||||
} catch (_error) {
|
||||
return preview;
|
||||
}
|
||||
if (!snapshot || typeof snapshot !== 'object') return preview;
|
||||
const displaySnapshot = convertWeatherSnapshot(snapshot, { temperature: location.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius', wind: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'ms' : 'kmh', precipitation: location.precipitation_unit === 'inch' ? 'inch' : 'mm' });
|
||||
const current = displaySnapshot.current || {};
|
||||
const daily = displaySnapshot.daily || {};
|
||||
const hourly = displaySnapshot.hourly || {};
|
||||
const weatherCode = Number(current.weather_code);
|
||||
preview.hasSnapshot = true;
|
||||
preview.location = location.location_label;
|
||||
preview.temperatureUnit = location.temperature_unit === 'fahrenheit' ? '°F' : '°C';
|
||||
preview.windUnit = location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h';
|
||||
preview.precipitationUnit = location.precipitation_unit === 'inch' ? 'in' : 'mm';
|
||||
preview.temperature = current.temperature_2m ?? current.temperature ?? '-';
|
||||
preview.feelsLike = current.apparent_temperature ?? current.feels_like ?? '-';
|
||||
preview.icon = weatherIconForCode(weatherCode);
|
||||
preview.condition = weatherCode === 0 ? 'Clear sky' : weatherCode <= 3 ? 'Partly cloudy' : weatherCode <= 48 ? 'Foggy' : weatherCode <= 67 ? 'Rain' : weatherCode <= 77 ? 'Snow' : weatherCode <= 82 ? 'Showers' : 'Thunderstorm';
|
||||
preview.humidity = current.relative_humidity_2m === undefined ? '-' : current.relative_humidity_2m + '%';
|
||||
preview.wind = current.wind_speed_10m ?? current.windSpeed ?? '-';
|
||||
preview.precipitation = current.precipitation ?? '-';
|
||||
preview.uvIndex = current.uv_index ?? '-';
|
||||
preview.cloudCover = current.cloud_cover ?? '-';
|
||||
preview.sunrise = daily.sunrise && daily.sunrise[0] ? String(daily.sunrise[0]).slice(11, 16) : '-';
|
||||
preview.sunset = daily.sunset && daily.sunset[0] ? String(daily.sunset[0]).slice(11, 16) : '-';
|
||||
preview.fetchedAt = location.last_pulled_at || '';
|
||||
preview.forecast = (daily.time || []).slice(0, 7).map(function (day, index) {
|
||||
const code = Number(daily.weather_code && daily.weather_code[index]);
|
||||
return { day: index === 0 ? 'Today' : day, condition: code <= 3 ? 'Partly cloudy' : code <= 67 ? 'Rain' : code <= 77 ? 'Snow' : 'Showers', icon: weatherIconForCode(code), high: daily.temperature_2m_max && daily.temperature_2m_max[index] !== undefined ? daily.temperature_2m_max[index] : '-', low: daily.temperature_2m_min && daily.temperature_2m_min[index] !== undefined ? daily.temperature_2m_min[index] : '-', rain: daily.precipitation_sum && daily.precipitation_sum[index] !== undefined ? daily.precipitation_sum[index] : '-', wind: daily.wind_speed_10m_max && daily.wind_speed_10m_max[index] !== undefined ? daily.wind_speed_10m_max[index] : '-', uvIndex: daily.uv_index_max && daily.uv_index_max[index] !== undefined ? daily.uv_index_max[index] : '-' };
|
||||
});
|
||||
preview.hourly = (hourly.time || []).slice(0, 24).map(function (time, index) {
|
||||
return { day: String(time).slice(11, 16), icon: weatherIconForCode(hourly.weather_code && hourly.weather_code[index]), temperature: hourly.temperature_2m && hourly.temperature_2m[index] !== undefined ? hourly.temperature_2m[index] : '-', rain: hourly.precipitation_probability && hourly.precipitation_probability[index] !== undefined ? hourly.precipitation_probability[index] + '%' : '-' };
|
||||
});
|
||||
return preview;
|
||||
}
|
||||
|
||||
module.exports = function buildWeatherLocationFormViewModel(location, message, currentUser, isEdit, options) {
|
||||
const weatherLocation = Object.assign(buildDefaultWeatherLocation(), location || {});
|
||||
const providerAvailability = options && options.providerAvailability ? options.providerAvailability : { openMeteo: true, pirateWeather: false };
|
||||
const formAction = isEdit ? '/data-sources/weather/' + weatherLocation.id : '/data-sources/weather';
|
||||
return { title: isEdit ? 'Edit weather location' : 'Add weather location', active: 'weather', message, currentUser: currentUser || null, weatherLocation, weatherPreview: buildWeatherPreview(weatherLocation), providerAvailability, isEdit: Boolean(isEdit), formAction: formAction, formAttrs: isEdit ? 'data-async-save data-async-save-close-url="/data-sources/weather" data-async-save-new-url="/data-sources/weather/new"' : 'data-async-save data-async-save-new-redirect="response-url" data-async-save-close-url="/data-sources/weather" data-async-save-new-url="/data-sources/weather/new"', cancelUrl: '/data-sources/weather', deleteUrl: isEdit ? '/data-sources/weather/' + weatherLocation.id + '/delete' : '', deleteDisabled: !isEdit || Boolean(weatherLocation.inUse), showSaveSecondaryActions: true, scripts: ['js/data-sources/weather-location-form.js'] };
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const { renderView } = require('../../../view');
|
||||
|
||||
function formatInterval(value, unit) {
|
||||
const amount = Math.max(1, Number(value) || 0);
|
||||
const normalized = String(unit || 'minutes');
|
||||
return 'Every ' + amount + ' ' + normalized.replace(/s$/, '') + (amount === 1 ? '' : 's');
|
||||
}
|
||||
|
||||
module.exports = function renderWeatherLocationsPage(data, message, currentUser) {
|
||||
return renderView('data-sources/weather/list', {
|
||||
title: 'Weather locations', active: 'weather', message, currentUser: currentUser || null,
|
||||
weatherLocations: (data.weatherLocations || []).map(function (location) {
|
||||
return Object.assign({}, location, { intervalLabel: formatInterval(location.update_interval_value, location.update_interval_unit) });
|
||||
}), pagination: data.pagination || null
|
||||
});
|
||||
};
|
||||
@@ -13,6 +13,7 @@ const registerRootDataSourceRoutes = require('./admin/data-sources');
|
||||
const registerApiSourceRoutes = require('./data-sources/api-sources/routes');
|
||||
const registerRssFeedRoutes = require('./data-sources/rss-feeds/routes');
|
||||
const registerTimetableRoutes = require('./data-sources/timetables/routes');
|
||||
const registerWeatherRoutes = require('./data-sources/weather');
|
||||
const registerSettingsRoutes = require('./settings/background-tasks');
|
||||
const registerFontRoutes = require('./settings/fonts');
|
||||
const registerSettingsPageRoutes = require('./settings/routes');
|
||||
@@ -282,6 +283,16 @@ function registerSettingsAndContentRoutes(app, deps) {
|
||||
requirePermission: deps.requirePermission
|
||||
});
|
||||
|
||||
registerWeatherRoutes(app, {
|
||||
pool: deps.pool,
|
||||
common: deps.common,
|
||||
getAuditUserId: deps.getAuditUserId,
|
||||
redirectAfterSave: deps.redirectAfterSave,
|
||||
dataSourceTasks: deps.dataSourceTasks,
|
||||
backgroundTaskQueue: deps.backgroundTaskQueue,
|
||||
requirePermission: deps.requirePermission
|
||||
});
|
||||
|
||||
registerSettingsRoutes(app, {
|
||||
pages: deps.pages,
|
||||
backgroundTaskQueue: deps.backgroundTaskQueue,
|
||||
|
||||
@@ -58,9 +58,18 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
],
|
||||
audit: ['audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days'],
|
||||
media: ['uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types'],
|
||||
weather: ['weather.open_meteo_api_key', 'weather.pirate_weather_api_key'],
|
||||
icons: ['announcements.suggested_icons']
|
||||
};
|
||||
|
||||
function maskApiKey(value) {
|
||||
const key = String(value || '');
|
||||
if (!key) {
|
||||
return '';
|
||||
}
|
||||
return key.length <= 8 ? 'Configured' : key.slice(0, 4) + '...' + key.slice(-4);
|
||||
}
|
||||
|
||||
function buildSettingChanges(previousSettings, nextSettings, keys) {
|
||||
const changes = {};
|
||||
(Array.isArray(keys) ? keys : []).forEach(function (key) {
|
||||
@@ -157,6 +166,10 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
,rssDefaultIntervalUnit: String(settings['data-sources.rss_default_interval_unit'] || 'minutes')
|
||||
,apiDefaultIntervalValue: Number(settings['data-sources.api_default_interval_value']) || 60
|
||||
,apiDefaultIntervalUnit: String(settings['data-sources.api_default_interval_unit'] || 'minutes')
|
||||
,weatherPirateWeatherConfigured: Boolean(settings['weather.pirate_weather_api_key'])
|
||||
,weatherPirateWeatherKeyLabel: maskApiKey(settings['weather.pirate_weather_api_key'])
|
||||
,weatherOpenMeteoConfigured: Boolean(settings['weather.open_meteo_api_key'])
|
||||
,weatherOpenMeteoKeyLabel: maskApiKey(settings['weather.open_meteo_api_key'])
|
||||
,auditRetentionDays: Number(settings['audit.retention_days']) || 0
|
||||
,auditEnabled: Boolean(settings['audit.enabled'])
|
||||
,auditIncludeRequestMetadata: Boolean(settings['audit.include_request_metadata'])
|
||||
@@ -231,7 +244,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
app.post('/settings/system', deps.requirePermission('system-settings.update'), async function (req, res, next) {
|
||||
try {
|
||||
const section = String(req.body && req.body.settings_section || '').trim().toLowerCase();
|
||||
if (section !== 'icons' && section !== 'media' && section !== 'security' && section !== 'defaults' && section !== 'audit') {
|
||||
if (section !== 'icons' && section !== 'media' && section !== 'security' && section !== 'defaults' && section !== 'audit' && section !== 'weather') {
|
||||
const error = new Error('Unknown settings section.');
|
||||
error.statusCode = 400;
|
||||
error.expose = true;
|
||||
@@ -358,7 +371,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'uploads.image_max_bytes': imageMaxMb * 1024 * 1024,
|
||||
'uploads.video_max_bytes': videoMaxMb * 1024 * 1024,
|
||||
@@ -369,6 +382,17 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Media settings saved.') + '#media-uploads');
|
||||
}
|
||||
|
||||
if (section === 'weather') {
|
||||
const submittedOpenMeteoKey = String(req.body && req.body.open_meteo_api_key || '').trim();
|
||||
const submittedPirateWeatherKey = String(req.body && req.body.pirate_weather_api_key || '').trim();
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'weather.open_meteo_api_key': submittedOpenMeteoKey || previousSettings['weather.open_meteo_api_key'],
|
||||
'weather.pirate_weather_api_key': submittedPirateWeatherKey || previousSettings['weather.pirate_weather_api_key']
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
await recordSettingsAuditEvent(req, previousSettings, savedSettings, section);
|
||||
return res.redirect('/settings/system?message=' + encodeURIComponent('Weather provider settings saved.') + '#weather-providers');
|
||||
}
|
||||
|
||||
const selectedIcons = req.body && (req.body['suggested_icons[]'] !== undefined ? req.body['suggested_icons[]'] : req.body.suggested_icons);
|
||||
const catalogKeys = new Set(ANNOUNCEMENT_ICON_CATALOG.map(function (option) {
|
||||
return option.value;
|
||||
@@ -384,7 +408,7 @@ module.exports = function registerSettingsPageRoutes(app, deps) {
|
||||
error.expose = true;
|
||||
throw error;
|
||||
}
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
await pool.query('DELETE FROM o_app_settings WHERE setting_key NOT IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ['announcements.suggested_icons', 'announcements.default_icon', 'announcements.default_duration_value', 'announcements.default_duration_unit', 'player.default_slide_duration_seconds', 'player.default_fade_between_slides', 'player.skip_unavailable_rtmp', 'data-sources.rss_default_interval_value', 'data-sources.rss_default_interval_unit', 'data-sources.api_default_interval_value', 'data-sources.api_default_interval_unit', 'weather.open_meteo_api_key', 'weather.pirate_weather_api_key', 'uploads.image_max_bytes', 'uploads.video_max_bytes', 'uploads.wysiwyg_image_max_bytes', 'uploads.allowed_mime_types', 'security.session_lifetime_days', 'security.allow_user_session_revocation', 'security.max_active_sessions', 'security.login_max_attempts', 'security.login_lockout_minutes', 'security.login_rate_limit_scope', 'security.password_min_length', 'security.password_min_categories', 'security.password_require_lowercase', 'security.password_require_uppercase', 'security.password_require_number', 'security.password_require_symbol', 'security.require_password_change_for_new_users', 'security.require_password_change_after_admin_reset', 'audit.enabled', 'audit.categories', 'audit.include_request_metadata', 'audit.retention_days']);
|
||||
const savedSettings = await saveAppSettings(pool, {
|
||||
'announcements.suggested_icons': suggestedIcons
|
||||
}, req.currentUser && req.currentUser.id);
|
||||
|
||||
@@ -25,6 +25,7 @@ function buildSlideFormViewModel(data, slide, message, currentUser, isEdit) {
|
||||
const rssFeeds = data && data.rssFeeds ? data.rssFeeds : [];
|
||||
const apiSources = data && data.apiSources ? data.apiSources : [];
|
||||
const timetableGroups = data && data.timetableGroups ? data.timetableGroups : [];
|
||||
const weatherLocations = data && data.weatherLocations ? data.weatherLocations : [];
|
||||
const fontLibrary = data && data.fontLibrary ? data.fontLibrary : null;
|
||||
const uploadLimits = data && data.uploadLimits ? data.uploadLimits : {};
|
||||
const templates = buildTemplates(data && data.templates ? data.templates : [], templateRegions);
|
||||
@@ -48,6 +49,7 @@ function buildSlideFormViewModel(data, slide, message, currentUser, isEdit) {
|
||||
rssFeeds: rssFeeds,
|
||||
apiSources: apiSources,
|
||||
timetableGroups: timetableGroups,
|
||||
weatherLocations: weatherLocations,
|
||||
fontStylesheetHref: fontLibrary && fontLibrary.stylesheetHref ? fontLibrary.stylesheetHref : '',
|
||||
fontFamilyFormats: fontLibrary && fontLibrary.fontFamilyFormats ? fontLibrary.fontFamilyFormats : '',
|
||||
existingTemplateId: viewSlide && viewSlide.template_id ? viewSlide.template_id : null,
|
||||
|
||||
@@ -6,6 +6,7 @@ const crypto = require('crypto');
|
||||
const path = require('path');
|
||||
const { verifyRequestAuth, verifyPageAuthToken } = require('#src/request-auth');
|
||||
const { getFontStylesheetHref } = require('#src/web/lib/media/font-library');
|
||||
const { convertWeatherSnapshot } = require('#src/data/weather-units');
|
||||
const { buildThumbnailPreviewPayload } = require('#src/web/lib/media/slide-thumbnails');
|
||||
|
||||
function safeJsonForScript(value) {
|
||||
@@ -80,6 +81,20 @@ module.exports = function registerSlidesRoutes(app, deps) {
|
||||
const apiSources = (apiData.apiSources || []).map(function (source) {
|
||||
return Object.assign({}, source, { responseJson: common.parseJsonSafe ? common.parseJsonSafe(source.last_response_json) : null });
|
||||
});
|
||||
const weatherData = typeof common.fetchWeatherLocationsData === 'function' ? await common.fetchWeatherLocationsData(pool) : { weatherLocations: [] };
|
||||
const weatherLocations = (weatherData.weatherLocations || []).map(function (location) {
|
||||
const snapshot = common.parseJsonSafe ? common.parseJsonSafe(location.last_response_json) : null;
|
||||
if (!snapshot || typeof snapshot !== 'object') return null;
|
||||
const item = Object.assign({}, convertWeatherSnapshot(snapshot, {
|
||||
temperature: location.temperature_unit === 'fahrenheit' ? 'fahrenheit' : 'celsius',
|
||||
wind: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'ms' : 'kmh',
|
||||
precipitation: location.precipitation_unit === 'inch' ? 'inch' : 'mm'
|
||||
}), { location_label: location.location_label || '', name: location.name || '', timezone: location.timezone || '', temp_unit: location.temperature_unit === 'fahrenheit' ? '°F' : '°C', wind_unit: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h', precip_unit: location.precipitation_unit === 'inch' ? 'in' : 'mm' });
|
||||
item.current = Object.assign({}, item.current || {}, { temperature_unit: location.temperature_unit === 'fahrenheit' ? '°F' : '°C', wind_speed_unit: location.wind_unit === 'mph' ? 'mph' : location.wind_unit === 'ms' ? 'm/s' : 'km/h', precipitation_unit: location.precipitation_unit === 'inch' ? 'in' : 'mm' });
|
||||
item.daily = Object.assign({}, item.daily || {}, { temperature_unit: location.temperature_unit === 'fahrenheit' ? '°F' : '°C' });
|
||||
item.hourly = Object.assign({}, item.hourly || {}, { temperature_unit: location.temperature_unit === 'fahrenheit' ? '°F' : '°C' });
|
||||
return { id: location.id, data: item };
|
||||
}).filter(Boolean);
|
||||
const getApiItems = function (source) {
|
||||
const response = source && source.responseJson;
|
||||
const itemsPath = String(source && source.items_path || '').trim();
|
||||
@@ -103,6 +118,10 @@ module.exports = function registerSlidesRoutes(app, deps) {
|
||||
const feed = rssFeeds.find(function (entry) { return Number(entry.id) === Number(regionContent.feed_id); });
|
||||
return feed && Array.isArray(feed.items) ? feed.items[index] || null : null;
|
||||
}
|
||||
if (type === 'weather') {
|
||||
const location = weatherLocations.find(function (entry) { return Number(entry.id) === Number(regionContent.weather_location_id); });
|
||||
return location ? location.data : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const getCachedImagePath = function (remoteUrl) {
|
||||
|
||||
Reference in New Issue
Block a user