Files
pulse-signage/src/web/routes/settings/background-tasks-page.js
T
2026-08-05 19:38:16 +01:00

345 lines
12 KiB
JavaScript

// Background tasks settings page renderer.
const { renderView } = require('#src/web/view');
const { hasAnyPermission } = require('#src/rbac');
const { buildPagination, buildQueryString } = require('#src/web/lib/pagination');
const { parsePageNumber, normalizeSortDirection, sortRows, createSearchMatcher } = require('#src/web/lib/list-query');
const TASKS_PER_PAGE = 25;
const QUEUE_PAGE_URL = '/settings/tasks-background';
const SCHEDULED_PAGE_URL = '/settings/tasks-scheduled';
const DATE_FILTER_FIELDS = {
createdAt: 'queued',
startedAt: 'started',
finishedAt: 'finished'
};
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
if (value % 60000 === 0) {
const minutes = Math.max(1, value / 60000);
return minutes === 1 ? 'Every minute' : `Every ${minutes} minutes`;
}
if (value % 1000 === 0) {
const seconds = Math.max(1, value / 1000);
return seconds === 1 ? 'Every second' : `Every ${seconds} seconds`;
}
return `${value} ms`;
}
function normalizeSourceFilter(sourceType, sourceId) {
const normalizedSourceType = String(sourceType || '').trim();
const normalizedSourceId = Math.floor(Number(sourceId) || 0);
if (!normalizedSourceType || !Number.isFinite(normalizedSourceId) || normalizedSourceId <= 0) {
return { sourceType: '', sourceId: '' };
}
return {
sourceType: normalizedSourceType,
sourceId: normalizedSourceId
};
}
function normalizeDateInput(value) {
const normalizedValue = String(value || '').trim();
return /^\d{4}-\d{2}-\d{2}$/.test(normalizedValue) ? normalizedValue : '';
}
function normalizeDateField(value) {
const normalizedField = String(value || '').trim();
return DATE_FILTER_FIELDS[normalizedField] ? normalizedField : 'createdAt';
}
function getIsoDateString(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '';
}
return date.toISOString().slice(0, 10);
}
function getTaskDateValue(task, dateField) {
if (!task || !dateField) {
return '';
}
return task[dateField] || '';
}
function taskMatchesDateFilter(task, dateFilter) {
if (!dateFilter || (!dateFilter.from && !dateFilter.to)) {
return true;
}
const taskDate = getIsoDateString(getTaskDateValue(task, dateFilter.field));
if (!taskDate) {
return false;
}
if (dateFilter.from && taskDate < dateFilter.from) {
return false;
}
if (dateFilter.to && taskDate > dateFilter.to) {
return false;
}
return true;
}
function taskMatchesFilters(task, sourceFilter) {
if (!sourceFilter || !sourceFilter.sourceType || !sourceFilter.sourceId) {
return true;
}
const metadata = task.metadata || {};
return String(metadata.sourceType || '').trim() === sourceFilter.sourceType && Number(metadata.sourceId || 0) === sourceFilter.sourceId;
}
const taskMatchesSearch = createSearchMatcher;
function formatStatusLabel(status) {
const normalized = String(status || '').trim().toLowerCase();
if (!normalized) {
return '';
}
if (normalized === 'queued') {
return 'Queued';
}
if (normalized === 'running') {
return 'Running';
}
if (normalized === 'completed') {
return 'Completed';
}
if (normalized === 'failed') {
return 'Failed';
}
if (normalized === 'canceled') {
return 'Canceled';
}
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
}
function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
const metadata = task.metadata || {};
const sourceType = String(metadata.sourceType || '').trim();
const sourceId = Math.floor(Number(metadata.sourceId) || 0);
if (!sourceType || !Number.isFinite(sourceId) || sourceId <= 0) {
return '';
}
const isActive = sourceFilter && sourceFilter.sourceType === sourceType && sourceFilter.sourceId === sourceId;
const nextQuery = Object.assign({}, queryState, {
page: 1,
sourceType: isActive ? '' : sourceType,
sourceId: isActive ? '' : sourceId
});
return buildQueryString(nextQuery);
}
function buildQueuePageViewModel(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0 };
const canManage = hasAnyPermission(currentUser, ['background-tasks.allow']);
const sort = String(data && data.sort || '').trim() || 'queued';
const direction = normalizeSortDirection(data && data.direction || 'desc');
const queryState = {
page: parsePageNumber(data && data.page),
sourceType: '',
sourceId: '',
search: String(data && data.search || '').trim(),
sort: sort,
direction: direction
};
const sourceFilter = normalizeSourceFilter(data && data.sourceType, data && data.sourceId);
queryState.sourceType = sourceFilter.sourceType;
queryState.sourceId = sourceFilter.sourceId;
const filteredTasks = tasks.filter(function (task) {
return taskMatchesFilters(task, sourceFilter) && taskMatchesDateFilter(task, { field: 'createdAt', from: '', to: '' }) && taskMatchesSearch(queryState.search, [
'title',
'category',
'status',
'errorMessage',
'createdAt',
'startedAt',
'finishedAt',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
])(task);
});
const sortedTasks = sortRows(filteredTasks, function (task) {
if (sort === 'task') {
return task && task.title;
}
if (sort === 'category') {
return task && task.category;
}
if (sort === 'status') {
return task && task.status;
}
if (sort === 'queued') {
return task && task.createdAt;
}
if (sort === 'started') {
return task && (task.startedAt || '0001-01-01T00:00:00.000Z');
}
if (sort === 'finished') {
return task && task.finishedAt;
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
return task && task.createdAt;
}, direction);
const pagination = buildPagination(sortedTasks.length, queryState.page, 'page', queryState, TASKS_PER_PAGE, 'tasks', 'Background task pages');
const visibleTasks = sortedTasks.slice((pagination.currentPage - 1) * TASKS_PER_PAGE, pagination.currentPage * TASKS_PER_PAGE);
const statusCards = [
{
status: 'queued',
label: 'Queued',
count: summary.counts.queued,
variant: 'primary'
},
{
status: 'running',
label: 'Running',
count: summary.counts.running,
variant: 'info'
},
{
status: 'failed',
label: 'Failed',
count: summary.counts.failed,
variant: 'danger'
},
{
status: 'completed',
label: 'Finished',
count: summary.counts.completed,
variant: 'success'
}
].map(function (card) {
return Object.assign({}, card, {
active: false
});
});
const visibleTasksWithSources = visibleTasks.map(function (task) {
return Object.assign({}, task, {
sourceUrl: buildTaskSourceFilterUrl(queryState, task, sourceFilter),
sourceLabel: task.metadata && task.metadata.sourceName ? String(task.metadata.sourceName).trim() : '',
sourceTypeLabel: task.metadata && task.metadata.sourceType ? String(task.metadata.sourceType).trim() : '',
sourceIdLabel: task.metadata && task.metadata.sourceId ? Number(task.metadata.sourceId) : ''
});
});
const hasActiveFilters = Boolean(queryState.sourceType || queryState.sourceId);
return renderView('settings/tasks-background/index', {
title: 'Background tasks',
active: 'background-tasks',
bodyClass: 'background-tasks-page',
message: message,
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
backgroundTasksMenuOpen: true,
tasks: visibleTasksWithSources,
summary: summary,
statusCards: statusCards,
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
search: queryState.search,
sort: sort,
direction: direction,
sourceType: queryState.sourceType,
sourceId: queryState.sourceId,
clearFiltersUrl: QUEUE_PAGE_URL,
hasActiveFilters: hasActiveFilters,
pagination: pagination,
canManage: canManage,
canClearFinished: tasks.some(function (task) {
return task.status !== 'queued' && task.status !== 'running';
})
});
}
function buildScheduledPageViewModel(data, message, currentUser) {
const recurringTasks = (data && data.recurringTasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0, scheduledCount: 0 };
const canManage = hasAnyPermission(currentUser, ['scheduled-tasks.allow']);
const search = String(data && data.search || '').trim();
const sort = String(data && data.sort || '').trim() || 'task';
const direction = normalizeSortDirection(data && data.direction);
const filteredRecurringTasks = recurringTasks.filter(function (task) {
return taskMatchesSearch(search, [
'title',
'intervalLabel',
'nextRunAt',
'lastRunAt',
'lastStatus',
'lastError',
function (item) { return item && item.metadata && item.metadata.sourceName; },
function (item) { return item && item.metadata && item.metadata.sourceType; },
function (item) { return item && item.metadata && item.metadata.sourceId; }
])(task);
});
const sortedRecurringTasks = sortRows(filteredRecurringTasks, function (task) {
if (sort === 'task') {
return task && task.title;
}
if (sort === 'interval') {
return task && task.intervalMs;
}
if (sort === 'next_run') {
return task && task.nextRunAt;
}
if (sort === 'last_run') {
return task && task.lastRunAt;
}
if (sort === 'last_result') {
return [task && task.lastStatus, task && task.lastError].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
if (sort === 'source') {
return [task && task.metadata && task.metadata.sourceName, task && task.metadata && task.metadata.sourceType, task && task.metadata && task.metadata.sourceId].map(function (value) {
return String(value || '').trim();
}).join(' ');
}
return task && task.title;
}, direction);
return renderView('settings/tasks-scheduled/index', {
title: 'Scheduled tasks',
active: 'background-tasks-scheduled',
message: message,
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
backgroundTasksMenuOpen: true,
recurringTasks: sortedRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
});
}),
summary: summary,
canManage: canManage,
scheduledCount: Number(summary.scheduledCount || filteredRecurringTasks.length || 0),
recurringPagination: buildPagination(sortedRecurringTasks.length, parsePageNumber(data && data.recurringPage), 'recurringPage', { recurringPage: parsePageNumber(data && data.recurringPage), search: search, sort: sort, direction: direction })
});
}
module.exports = {
renderBackgroundTasksPage: function (data, message, currentUser) {
return buildQueuePageViewModel(data, message, currentUser);
},
renderBackgroundTasksScheduledPage: function (data, message, currentUser) {
return buildScheduledPageViewModel(data, message, currentUser);
}
};