Release 2.9.0
This commit is contained in:
@@ -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
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user