Release v2.15.4
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m50s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s

This commit is contained in:
lzstealth committed 2026-10-17 16:01:00 +01:00
1 parent 64aeb1b47e
commit 05c9341b08
25 files changed
+595 -31

No files matched your search

+1 -1
View File
@@ -250,7 +250,7 @@ async function countMediaAssetReferences(pool, mediaPath) {
const [slideRows] = await pool.query(
`SELECT COUNT(*) AS ref_count
FROM c_slides
WHERE LOCATE(?, COALESCE(content_json, '')) > 0`,
WHERE LOCATE(?, CAST(COALESCE(content_json, '') AS CHAR)) > 0`,
[normalizedPath]
);
const [templateRows] = await pool.query(
+3
View File
@@ -2,6 +2,7 @@
const express = require('express');
const fs = require('fs');
const registerErrorPageMiddleware = require('#src/web/middleware/error-page');
module.exports = function registerMiddleware(app, deps) {
const pool = deps.pool;
@@ -28,6 +29,8 @@ module.exports = function registerMiddleware(app, deps) {
}
});
registerErrorPageMiddleware(app, deps.pages);
app.use(function (req, res, next) {
if (req.path === '/' || req.path === '/login' || req.path === '/logout' || req.path === '/forgot-password' || req.path === '/reset-password' || req.path === '/verify-email' || req.path === '/slides/popup-preview' || req.path.indexOf('/api/internal/slide-thumbnails/') === 0 || req.path === '/api/internal/sync/player-media' || req.path === '/api/internal/sync/player-font' || req.path === '/api/internal/sync/player-control') {
return next();
+48
View File
@@ -0,0 +1,48 @@
function wantsHtmlErrorPage(req, res) {
const isXhr = String(req.get && req.get('X-Requested-With') || '').toLowerCase() === 'xmlhttprequest';
const requestPath = String(req.originalUrl || req.path || '');
const acceptsHtml = !req.accepts || req.accepts('html');
const contentType = String(res.get && res.get('Content-Type') || '').toLowerCase();
return !isXhr && requestPath.indexOf('/api/') !== 0 && acceptsHtml && (!contentType || contentType.indexOf('text/html') === 0);
}
function createErrorPage(pages, req, statusCode, message) {
const isServerError = statusCode >= 500;
return pages.renderErrorPage({
statusCode: statusCode,
message: isServerError ? '' : String(message || ''),
detail: isServerError ? 'The server could not complete the request.' : '',
backUrl: req.currentUser ? '/dashboard' : '/login',
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
}, req.currentUser);
}
module.exports = function registerErrorPageMiddleware(app, pages) {
app.use(function (req, res, next) {
const send = res.send;
const sendStatus = res.sendStatus;
res.send = function (body) {
const statusCode = Number(res.statusCode) || 200;
const isHtml = typeof body === 'string' && /^\s*</.test(body);
if (statusCode >= 400 && statusCode < 600 && !isHtml && wantsHtmlErrorPage(req, res)) {
return send.call(res, createErrorPage(pages, req, statusCode, body));
}
return send.apply(res, arguments);
};
res.sendStatus = function (statusCode) {
if (statusCode >= 400 && statusCode < 600 && wantsHtmlErrorPage(req, res)) {
res.statusCode = statusCode;
return res.send(createErrorPage(pages, req, statusCode));
}
return sendStatus.call(res, statusCode);
};
next();
});
};
+93 -14
View File
@@ -98,15 +98,40 @@
return Boolean(start && now < start);
}
function toLocalInput(value) {
var date = toDate(value);
if (!date) {
return '';
}
function pad(part) { return String(part).padStart(2, '0'); }
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
}
function getDateRangeConfig(regionId, card, current) {
var config = {};
['date_filter', 'date_from', 'date_to'].forEach(function (field) {
var input = card && card.querySelector ? card.querySelector('[name="region_' + field + '_' + regionId + '"]') : null;
config[field] = input ? input.value : current[field] || (field === 'date_filter' ? 'all' : '');
});
return config;
}
function isLive(entry, now) {
var start = toDate(entry && entry.start_datetime);
var end = toDate(entry && entry.end_datetime);
return Boolean(start && end && now >= start && now < end);
}
function getVisibleEntries(groupId, displayMode, maxItems, groups) {
function getVisibleEntries(groupId, displayMode, maxItems, groups, config) {
var now = new Date();
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
var range = config || {};
var from = toDate(range.date_filter === 'between' || range.date_filter === 'after' ? range.date_from : '');
var to = toDate(range.date_filter === 'between' || range.date_filter === 'before' ? range.date_to : '');
var candidates = getEntries(groupId, groups).filter(function (entry) {
var start = toDate(entry && entry.start_datetime);
return (!from && !to) || Boolean(start && (!from || start >= from) && (!to || start <= to));
});
var entries = candidates.slice().sort(function (left, right) {
var leftStart = toDate(left && left.start_datetime);
var rightStart = toDate(right && right.start_datetime);
return (leftStart ? leftStart.getTime() : 0) - (rightStart ? rightStart.getTime() : 0) || Number(left.id || 0) - Number(right.id || 0);
@@ -124,7 +149,7 @@
});
if (!entries.length && mode === 'upcoming') {
entries = getEntries(groupId, groups).filter(function (entry) {
entries = candidates.filter(function (entry) {
return isLive(entry, now);
});
}
@@ -167,6 +192,9 @@
display_mode: String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
value: String(current.text !== undefined ? current.text : current.value !== undefined ? current.value : ''),
max_items: Math.max(1, Number(current.max_items || current.timetable_max_items || 5)),
date_filter: ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all',
date_from: current.date_from || '',
date_to: current.date_to || '',
font_family: current.font_family || region.font_family || getDefaultStyle().font_family,
font_size: current.font_size || region.font_size || getDefaultStyle().font_size,
font_color: current.font_color || region.font_color || getDefaultStyle().font_color
@@ -230,8 +258,9 @@
var displayMode = timetableDisplayModeInput ? timetableDisplayModeInput.value : String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var maxItems = Math.max(1, Number(timetableMaxItemsInput ? timetableMaxItemsInput.value : current.max_items || current.timetable_max_items || 5));
var group = getGroupById(groupId, timetableGroups);
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups);
if (!entries.length) {
var range = getDateRangeConfig(regionId, card, current);
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups, range);
if (!entries.length && range.date_filter === 'all') {
entries = getEntries(groupId, timetableGroups).slice(0, maxItems);
}
@@ -267,9 +296,10 @@
var currentGroupId = current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id);
var currentDisplayMode = String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var currentMaxItems = Math.max(1, Number(current.max_items || current.timetable_max_items || 5));
var dateFilter = ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all';
var currentGroup = getGroupById(currentGroupId, timetableGroups);
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups);
if (!currentEntries.length) {
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups, current);
if (!currentEntries.length && dateFilter === 'all') {
currentEntries = getEntries(currentGroupId, timetableGroups).slice(0, currentMaxItems);
}
var groupOptions = timetableGroups.map(function (group) {
@@ -295,7 +325,7 @@
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(currentValue) + '" />' +
'</div>' +
'<div class="row g-3">' +
'<div class="col-12 col-lg-6">' +
'<div class="col-12 col-lg-4">' +
'<label class="form-label" for="region_timetable_group_id_' + region.id + '">Timetable group</label>' +
'<select id="region_timetable_group_id_' + region.id + '" name="region_timetable_group_id_' + region.id + '" class="form-select">' +
'<option value="">Select a group</option>' +
@@ -310,12 +340,34 @@
'<option value="both"' + (currentDisplayMode === 'both' ? ' selected' : '') + '>Both</option>' +
'</select>' +
'</div>' +
'<div class="col-6 col-lg-3">' +
'<div class="col-6 col-lg-2">' +
'<label class="form-label" for="region_timetable_max_items_' + region.id + '">Max items</label>' +
'<input id="region_timetable_max_items_' + region.id + '" type="number" min="1" step="1" name="region_timetable_max_items_' + region.id + '" class="form-control" value="' + escapeHtml(currentMaxItems || 5) + '" />' +
'</div>' +
'<div class="col-12 col-lg-3">' +
'<label class="form-label" for="region_date_filter_' + region.id + '">Date filter</label>' +
'<select id="region_date_filter_' + region.id + '" name="region_date_filter_' + region.id + '" class="form-select">' +
'<option value="all"' + (dateFilter === 'all' ? ' selected' : '') + '>All</option>' +
'<option value="between"' + (dateFilter === 'between' ? ' selected' : '') + '>Between Dates</option>' +
'<option value="before"' + (dateFilter === 'before' ? ' selected' : '') + '>Before Date</option>' +
'<option value="after"' + (dateFilter === 'after' ? ' selected' : '') + '>After Date</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div data-timetable-date-range' + (dateFilter === 'all' ? ' hidden' : '') + '>' +
'<div class="row g-3">' +
['from', 'to'].map(function (boundary) {
var field = 'date_' + boundary;
var enabled = dateFilter === 'between' || dateFilter === (boundary === 'from' ? 'after' : 'before');
var label = boundary === 'from' ? 'Starts on or after (local time)' : 'Starts on or before (local time)';
return '<div class="col-12 col-md-6" data-timetable-boundary' + (!enabled ? ' hidden' : '') + '>' +
'<label class="form-label" for="region_' + field + '_' + region.id + '">' + label + '</label>' +
'<input id="region_' + field + '_' + region.id + '" type="datetime-local" step="1" class="form-control" data-timetable-date="' + field + '" value="' + escapeHtml(toLocalInput(current[field])) + '"' + (!enabled ? ' disabled' : '') + ' />' +
'<input type="hidden" name="region_' + field + '_' + region.id + '" value="' + escapeHtml(current[field] || '') + '" />' +
'</div>';
}).join('') +
'</div>' +
'</div>' +
'<div class="text-body-secondary small">Use the selected group and display mode to choose which timetable entry fields are available.</div>' +
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
'<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</div>' +
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
@@ -350,24 +402,50 @@
panel.innerHTML = renderSchedulePlaceholderChips(current.group, current.entries);
}
function updateDateRange(card) {
var regionId = card.getAttribute('data-region-id');
var config = getDateRangeConfig(regionId, card, {});
var panel = card.querySelector('[data-timetable-date-range]');
if (!panel) {
return;
}
panel.hidden = config.date_filter === 'all';
panel.querySelectorAll('[data-timetable-date]').forEach(function (input) {
var field = input.getAttribute('data-timetable-date');
var enabled = config.date_filter === 'between' || config.date_filter === (field === 'date_from' ? 'after' : 'before');
input.closest('[data-timetable-boundary]').hidden = !enabled;
input.disabled = !enabled;
var date = toDate(input.value);
card.querySelector('[name="region_' + input.getAttribute('data-timetable-date') + '_' + regionId + '"]').value = date ? date.toISOString() : '';
});
var fromInput = panel.querySelector('[data-timetable-date="date_from"]');
var toInput = panel.querySelector('[data-timetable-date="date_to"]');
var from = toDate(fromInput.value);
var to = toDate(toInput.value);
toInput.setCustomValidity(config.date_filter === 'between' && from && to && from > to ? 'The end of the date range must be on or after its start.' : '');
}
if (!window.__scheduleRegionPlaceholderRefreshInstalled) {
window.__scheduleRegionPlaceholderRefreshInstalled = true;
document.addEventListener('change', function (event) {
function handleTimetableInput(event) {
var target = event.target;
if (!target || !target.closest) {
return;
}
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"]')) {
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"], select[name^="region_date_filter_"], [data-timetable-date]')) {
return;
}
var card = target.closest('[data-region-id]');
if (card) {
updateDateRange(card);
updateSchedulePlaceholderPanel(card);
}
}, true);
}
document.addEventListener('input', handleTimetableInput, true);
document.addEventListener('change', handleTimetableInput, true);
}
function buildEditorCardContext(context) {
@@ -410,6 +488,7 @@
tz_long: timezoneValues.tz_long,
timeZone: timezoneValues.tz_long,
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
...getDateRangeConfig(region.id, card, current),
existingContent: existingContent || {},
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
};
@@ -423,7 +502,7 @@
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
var group = getGroupById(groupId, groups);
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups);
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups, regionContent);
if (!value) {
return '<div class="slide-preview-region timetable" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '"></div>';
+2
View File
@@ -334,6 +334,7 @@ function registerSettingsAndContentRoutes(app, deps) {
uploadDir: deps.uploadDir,
backgroundTaskQueue: deps.backgroundTaskQueue,
setAuthMessageCookie: deps.setAuthMessageCookie,
recordRequestAuditEvent: deps.recordRequestAuditEvent,
requirePermission: deps.requirePermission
});
@@ -344,6 +345,7 @@ function registerSettingsAndContentRoutes(app, deps) {
uploadDir: deps.uploadDir,
getAuditUserId: deps.getAuditUserId,
setAuthMessageCookie: deps.setAuthMessageCookie,
recordRequestAuditEvent: deps.recordRequestAuditEvent,
requirePermission: deps.requirePermission,
syncPlaylistUploadsOnChange: deps.syncPlaylistUploadsOnChange
});
+24
View File
@@ -123,6 +123,7 @@ module.exports = function registerFontRoutes(app, deps) {
const uploadDir = deps.uploadDir;
const backgroundTaskQueue = deps.backgroundTaskQueue;
const setAuthMessageCookie = deps.setAuthMessageCookie;
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
const FONT_FAMILY_MAX_LENGTH = 255;
@@ -146,6 +147,13 @@ module.exports = function registerFontRoutes(app, deps) {
}
const result = await fontLibrary.addFontFile(uploadDir, req.file, String(req.body && req.body.font_family || '').trim().slice(0, FONT_FAMILY_MAX_LENGTH));
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'media-assets', eventType: 'font.uploaded', actorUserId: req.currentUser && req.currentUser.id,
targetType: 'font', targetId: result.font.id, targetLabel: result.font.family,
details: { fileName: result.font.fileName, format: result.font.format }
});
}
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:${result && result.font && result.font.id ? result.font.id : Date.now()}`,
@@ -186,6 +194,14 @@ module.exports = function registerFontRoutes(app, deps) {
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
}
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'media-assets', eventType: enabled ? 'font.enabled' : 'font.disabled', actorUserId: req.currentUser && req.currentUser.id,
targetType: 'font', targetId: updated.id, targetLabel: updated.family || updated.name,
details: { enabled: Boolean(enabled) }
});
}
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:toggle:${updated && updated.id ? updated.id : Date.now()}`,
@@ -222,6 +238,14 @@ module.exports = function registerFontRoutes(app, deps) {
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
}
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(pool, req, {
category: 'media-assets', eventType: 'font.deleted', actorUserId: req.currentUser && req.currentUser.id,
targetType: 'font', targetId: removed.id, targetLabel: removed.family || removed.name,
details: { fileName: removed.fileName, format: removed.format }
});
}
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:delete:${removed && removed.id ? removed.id : Date.now()}`,
+29 -5
View File
@@ -117,6 +117,7 @@ function filterAndSortAssets(assets, query) {
module.exports = function registerMediaLibraryRoutes(app, deps) {
const requireAccess = requireMediaAccess(deps.setAuthMessageCookie);
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
const pageSize = 24;
if (!deps.pool || !deps.pages || !deps.upload || !deps.uploadDir) {
throw new Error('registerMediaLibraryRoutes requires pool, pages, upload, and uploadDir.');
@@ -196,7 +197,19 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
return res.redirect('/media-library?message=' + encodeURIComponent('File must be ' + limitLabel + ' or smaller.'));
}
await mediaLibrary.createImageVariants(req.file);
await mediaLibrary.registerMediaAsset(deps.pool, Object.assign({}, req.file, { mediaType: mediaType }), '/media/uploads/' + req.file.filename, deps.getAuditUserId(req));
const mediaPath = '/media/uploads/' + req.file.filename;
await mediaLibrary.registerMediaAsset(deps.pool, Object.assign({}, req.file, { mediaType: mediaType }), mediaPath, deps.getAuditUserId(req));
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(deps.pool, req, {
category: 'media-assets',
eventType: 'media.uploaded',
actorUserId: deps.getAuditUserId(req),
targetType: 'media-asset',
targetId: mediaPath,
targetLabel: req.file.originalname || req.file.filename,
details: { mediaPath: mediaPath, mediaType: mediaType, mimeType: mimeType, fileSize: Number(req.file.size) || 0 }
});
}
res.redirect('/media-library?message=' + encodeURIComponent('Media uploaded.'));
} catch (error) {
if (req.file && req.file.path) {
@@ -206,8 +219,8 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
}
});
async function deleteMediaAsset(assetId) {
const [rows] = await deps.pool.query('SELECT id, media_path FROM c_media_assets WHERE id = ?', [Number(assetId)]);
async function deleteMediaAsset(assetId, req) {
const [rows] = await deps.pool.query('SELECT id, media_path, original_name, media_type, file_size FROM c_media_assets WHERE id = ?', [Number(assetId)]);
const asset = rows && rows[0];
if (!asset) {
return 'missing';
@@ -218,6 +231,17 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
}
const filePath = path.join(path.dirname(deps.uploadDir), asset.media_path.replace(/^\/media\//, ''));
await deps.pool.query('DELETE FROM c_media_assets WHERE id = ?', [asset.id]);
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(deps.pool, req, {
category: 'media-assets',
eventType: 'media.deleted',
actorUserId: deps.getAuditUserId(req),
targetType: 'media-asset',
targetId: asset.id,
targetLabel: asset.original_name || asset.media_path,
details: { mediaPath: asset.media_path, mediaType: asset.media_type || null, fileSize: Number(asset.file_size) || 0 }
});
}
await fs.promises.unlink(filePath).catch(function (error) {
if (error && error.code !== 'ENOENT') {
throw error;
@@ -250,7 +274,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
let deletedCount = 0;
let inUseCount = 0;
for (let index = 0; index < assetIds.length; index += 1) {
const result = await deleteMediaAsset(assetIds[index]);
const result = await deleteMediaAsset(assetIds[index], req);
if (result === 'deleted') deletedCount += 1;
if (result === 'in-use') inUseCount += 1;
}
@@ -263,7 +287,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
app.post('/media-library/:id/delete', requireAccess, deps.requirePermission('media-library.delete'), async function (req, res, next) {
try {
const result = await deleteMediaAsset(req.params.id);
const result = await deleteMediaAsset(req.params.id, req);
if (result === 'missing') {
return res.redirect('/media-library?message=' + encodeURIComponent('Media asset not found.'));
}
+25 -1
View File
@@ -6,6 +6,7 @@ const { buildPagination } = require('#src/web/lib/pagination');
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
module.exports = function registerWeatherIconRoutes(app, deps) {
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024, files: library.CONDITIONS.length * 2, fields: 2 } });
const fields = library.CONDITIONS.flatMap(function (condition) {
return [{ name: 'icon_' + condition.key, maxCount: 1 }, { name: 'night_icon_' + condition.key, maxCount: 1 }];
@@ -57,8 +58,17 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
async function save(req, res, next) {
try {
const previousSets = library.readWeatherIconSets(deps.mediaDir);
await library.saveWeatherIconSet(deps.mediaDir, req.body.name, Object.values(req.files || {}).flat(), req.params.id);
const uploadedFiles = Object.values(req.files || {}).flat();
const savedSet = await library.saveWeatherIconSet(deps.mediaDir, req.body.name, uploadedFiles, req.params.id);
req.weatherIconRemovalOperations = library.collectWeatherIconRemovalOperations(previousSets, library.readWeatherIconSets(deps.mediaDir));
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(deps.pool, req, {
category: 'media-assets', eventType: req.params.id ? 'weather-icon-set.updated' : 'weather-icon-set.created',
actorUserId: req.currentUser && req.currentUser.id, targetType: 'weather-icon-set',
targetId: savedSet.id, targetLabel: savedSet.name,
details: { uploadedFileCount: uploadedFiles.length, iconCount: Object.keys(savedSet.icons || {}).length, nightIconCount: Object.keys(savedSet.nightIcons || {}).length }
});
}
} catch (error) {
try { return await page(req, res, error.message, 400); } catch (pageError) { return next(pageError); }
}
@@ -74,6 +84,13 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
const enabled = String(req.body && req.body.enabled || '').trim() !== '0';
const updated = library.setWeatherIconSetEnabled(deps.mediaDir, req.params.id, enabled);
if (!updated) return res.redirect('/settings/weather-icons?message=' + encodeURIComponent('That weather icon set was not found.'));
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(deps.pool, req, {
category: 'media-assets', eventType: enabled ? 'weather-icon-set.enabled' : 'weather-icon-set.disabled',
actorUserId: req.currentUser && req.currentUser.id, targetType: 'weather-icon-set',
targetId: updated.id, targetLabel: updated.name, details: { enabled: Boolean(enabled) }
});
}
await sync();
res.redirect('/settings/weather-icons?message=' + encodeURIComponent(enabled ? 'Weather icon set enabled.' : 'Weather icon set disabled.'));
} catch (error) { next(error); }
@@ -89,6 +106,13 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
});
if (used) return await page(req, res, 'That icon set is still used by one or more slides.', 400);
const removed = library.deleteWeatherIconSet(deps.mediaDir, req.params.id);
if (typeof recordRequestAuditEvent === 'function') {
await recordRequestAuditEvent(deps.pool, req, {
category: 'media-assets', eventType: 'weather-icon-set.deleted', actorUserId: req.currentUser && req.currentUser.id,
targetType: 'weather-icon-set', targetId: removed.id, targetLabel: removed.name,
details: { iconCount: Object.keys(removed.icons || {}).length, nightIconCount: Object.keys(removed.nightIcons || {}).length }
});
}
await sync(library.collectWeatherIconRemovalOperations([removed], []));
res.redirect('/settings/weather-icons?message=' + encodeURIComponent('Weather icon set deleted.'));
} catch (error) { next(error); }