Release 1.5.13

This commit is contained in:
2026-07-26 20:26:01 +01:00
parent 4afaeaafb1
commit 815c808d73
59 changed files with 1418 additions and 3042 deletions
@@ -1,10 +1,11 @@
const { renderView } = require('../../view');
const { hasAnyPermission } = require('../../../rbac');
const { buildPaginationPages } = require('../../lib/pagination');
const { buildPagination, buildQueryString } = require('../../lib/pagination');
const { parsePageNumber, normalizeSortDirection, sortRows, createSearchMatcher } = require('../../lib/list-query');
const TASKS_PER_PAGE = 10;
const QUEUE_PAGE_URL = '/settings/background-tasks';
const SCHEDULED_PAGE_URL = '/settings/background-tasks/scheduled';
const QUEUE_PAGE_URL = '/settings/tasks-background';
const SCHEDULED_PAGE_URL = '/settings/tasks-scheduled';
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
@@ -19,11 +20,6 @@ function formatIntervalLabel(intervalMs) {
return `${value} ms`;
}
function parsePageNumber(value) {
const pageNumber = Math.floor(Number(value) || 1);
return Math.max(1, pageNumber);
}
function normalizeSourceFilter(sourceType, sourceId) {
const normalizedSourceType = String(sourceType || '').trim();
const normalizedSourceId = Math.floor(Number(sourceId) || 0);
@@ -38,48 +34,6 @@ function normalizeSourceFilter(sourceType, sourceId) {
};
}
function buildQueryString(query) {
const searchParams = new URLSearchParams();
['page', 'recurringPage', 'status', 'sourceType', 'sourceId'].forEach(function (key) {
const value = query && query[key];
if (value === undefined || value === null || value === '') {
return;
}
searchParams.set(key, String(value));
});
const queryString = searchParams.toString();
return queryString ? `?${queryString}` : '';
}
function buildPagination(totalItems, currentPage, pageParam, queryState) {
const normalizedPageParam = String(pageParam || 'page').trim() || 'page';
const totalPages = Math.max(1, Math.ceil(totalItems / TASKS_PER_PAGE));
const safeCurrentPage = Math.min(Math.max(1, currentPage), totalPages);
const startIndex = (safeCurrentPage - 1) * TASKS_PER_PAGE;
const endIndex = Math.min(totalItems, startIndex + TASKS_PER_PAGE);
const pages = buildPaginationPages(totalPages, safeCurrentPage, normalizedPageParam, queryState, 7);
const previousQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage - 1 });
const nextQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage + 1 });
return {
currentPage: safeCurrentPage,
totalPages: totalPages,
totalItems: totalItems,
hasMultiplePages: totalPages > 1,
startItem: totalItems === 0 ? 0 : startIndex + 1,
endItem: endIndex,
hasPrevious: safeCurrentPage > 1,
hasNext: safeCurrentPage < totalPages,
previousUrl: buildQueryString(previousQuery),
nextUrl: buildQueryString(nextQuery),
pages: pages,
pageSize: TASKS_PER_PAGE,
pageParam: normalizedPageParam
};
}
function taskMatchesFilters(task, sourceFilter) {
if (!sourceFilter || !sourceFilter.sourceType || !sourceFilter.sourceId) {
return true;
@@ -89,6 +43,8 @@ function taskMatchesFilters(task, sourceFilter) {
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) {
@@ -133,25 +89,61 @@ function buildTaskSourceFilterUrl(queryState, task, sourceFilter) {
function buildQueuePageViewModel(data, message, currentUser) {
const tasks = (data && data.tasks) || [];
const recurringTasks = (data && data.recurringTasks) || [];
const summary = (data && data.summary) || { counts: {}, total: 0, activeCount: 0 };
const canManage = hasAnyPermission(currentUser, ['background-tasks.allow']);
const sort = String(data && data.sort || '').trim();
const direction = normalizeSortDirection(data && data.direction);
const queryState = {
page: parsePageNumber(data && data.page),
recurringPage: parsePageNumber(data && data.recurringPage),
sourceType: '',
sourceId: ''
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);
return taskMatchesFilters(task, sourceFilter) && taskMatchesSearch(queryState.search, [
'title',
'key',
'category',
'status',
'errorMessage',
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 pagination = buildPagination(filteredTasks.length, queryState.page, 'page', queryState);
const recurringPagination = buildPagination(recurringTasks.length, queryState.recurringPage, 'recurringPage', queryState);
const visibleTasks = filteredTasks.slice((pagination.currentPage - 1) * TASKS_PER_PAGE, pagination.currentPage * TASKS_PER_PAGE);
const visibleRecurringTasks = recurringTasks.slice((recurringPagination.currentPage - 1) * TASKS_PER_PAGE, recurringPagination.currentPage * TASKS_PER_PAGE);
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;
}
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',
@@ -199,16 +191,14 @@ function buildQueuePageViewModel(data, message, currentUser) {
message: message,
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
stateVersion: data && data.stateVersion ? String(data.stateVersion) : '',
backgroundTasksMenuOpen: true,
tasks: visibleTasksWithSources,
summary: summary,
statusCards: statusCards,
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
clearFiltersUrl: buildQueryString({ recurringPage: queryState.recurringPage }),
clearFiltersUrl: QUEUE_PAGE_URL,
hasActiveFilters: hasActiveFilters,
pagination: pagination,
recurringPagination: recurringPagination,
canManage: canManage,
canClearFinished: tasks.some(function (task) {
return task.status !== 'queued' && task.status !== 'running';
@@ -220,6 +210,47 @@ 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();
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.nextRunAt;
}, direction);
return renderView('settings/tasks-scheduled/index', {
title: 'Scheduled tasks',
@@ -227,17 +258,16 @@ function buildScheduledPageViewModel(data, message, currentUser) {
message: message,
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
stateVersion: data && data.stateVersion ? String(data.stateVersion) : '',
backgroundTasksMenuOpen: true,
recurringTasks: recurringTasks.map(function (task) {
recurringTasks: sortedRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
});
}),
summary: summary,
canManage: canManage,
scheduledCount: Number(summary.scheduledCount || recurringTasks.length || 0),
recurringPagination: buildPagination(recurringTasks.length, parsePageNumber(data && data.recurringPage), 'recurringPage', { recurringPage: parsePageNumber(data && data.recurringPage) })
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 })
});
}
+24 -69
View File
@@ -42,65 +42,18 @@ module.exports = function registerAdminSettingsRoutes(app, deps) {
throw new Error('registerAdminSettingsRoutes requires pages and backgroundTaskQueue.');
}
function buildQueueVersion(tasks, recurringTasks, summary) {
return [
'q:' + Number(summary && summary.counts && summary.counts.queued || 0),
'r:' + Number(summary && summary.counts && summary.counts.running || 0),
'f:' + Number(summary && summary.counts && summary.counts.failed || 0),
'c:' + Number(summary && summary.counts && summary.counts.completed || 0),
'a:' + Number(summary && summary.activeCount || 0),
't:' + Number(summary && summary.total || 0),
'tasks:' + (tasks || []).map(function (task) {
return [task.id, task.status, task.startedAt || '', task.finishedAt || '', task.errorMessage || '', task.attempts || 0].join(':');
}).join('|'),
'recurring:' + (recurringTasks || []).map(function (task) {
return [task.key, task.nextRunAt || '', task.lastRunAt || '', task.lastStatus || '', task.lastError || ''].join(':');
}).join('|')
].join('~');
}
app.get('/settings/background-tasks', requireSettingsAccess(), function (req, res) {
app.get('/settings/tasks-background', requireSettingsAccess(), function (req, res) {
const tasks = backgroundTaskQueue.listTasks();
const recurringTasks = backgroundTaskQueue.listRecurringTasks();
const summary = backgroundTaskQueue.getSummary();
const version = buildQueueVersion(tasks, recurringTasks, summary);
res.send(pages.renderBackgroundTasksPage({ tasks: tasks, recurringTasks: recurringTasks, summary: summary, stateVersion: version, page: req.query.page, recurringPage: req.query.recurringPage }, req.query.message ? String(req.query.message) : '', req.currentUser));
res.send(pages.renderBackgroundTasksPage({ tasks: tasks, recurringTasks: recurringTasks, summary: summary, page: req.query.page, recurringPage: req.query.recurringPage, search: req.query.search, sort: req.query.sort, direction: req.query.direction }, req.query.message ? String(req.query.message) : '', req.currentUser));
});
app.get('/settings/background-tasks/scheduled', requireScheduledSettingsAccess(), function (req, res) {
app.get('/settings/tasks-scheduled', requireScheduledSettingsAccess(), function (req, res) {
const tasks = backgroundTaskQueue.listTasks();
const recurringTasks = backgroundTaskQueue.listRecurringTasks();
const summary = backgroundTaskQueue.getSummary();
const version = buildQueueVersion(tasks, recurringTasks, summary);
res.send(pages.renderBackgroundTasksScheduledPage({ tasks: tasks, recurringTasks: recurringTasks, summary: summary, stateVersion: version, page: req.query.page, recurringPage: req.query.recurringPage }, req.query.message ? String(req.query.message) : '', req.currentUser));
});
app.get('/settings/background-tasks/state', function (req, res, next) {
if (!req.currentUser) {
return res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.'));
}
if (!hasAnyPermission(req.currentUser, ['background-tasks.read', 'background-tasks.allow', 'scheduled-tasks.read', 'scheduled-tasks.allow'])) {
const error = new Error('You do not have permission to access this area.');
error.statusCode = 403;
error.expose = true;
return next(error);
}
return next();
}, function (req, res) {
const tasks = backgroundTaskQueue.listTasks();
const recurringTasks = backgroundTaskQueue.listRecurringTasks();
const summary = backgroundTaskQueue.getSummary();
const version = buildQueueVersion(tasks, recurringTasks, summary);
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private');
res.json({
version: version,
summary: summary,
taskCount: tasks.length,
recurringCount: recurringTasks.length
});
res.send(pages.renderBackgroundTasksScheduledPage({ tasks: tasks, recurringTasks: recurringTasks, summary: summary, page: req.query.page, recurringPage: req.query.recurringPage, search: req.query.search, sort: req.query.sort, direction: req.query.direction }, req.query.message ? String(req.query.message) : '', req.currentUser));
});
function requireSettingsManageAccess() {
@@ -120,22 +73,7 @@ module.exports = function registerAdminSettingsRoutes(app, deps) {
};
}
app.post('/settings/background-tasks/clear-finished', requireSettingsManageAccess(), function (req, res) {
const removedCount = backgroundTaskQueue.clearFinishedTasks();
res.redirect('/settings/background-tasks?message=' + encodeURIComponent(removedCount ? 'Cleared ' + removedCount + ' finished task' + (removedCount === 1 ? '' : 's') + '.' : 'No finished tasks to clear.'));
});
app.post('/settings/background-tasks/:id/cancel', requireSettingsManageAccess(), function (req, res) {
const canceled = backgroundTaskQueue.cancelTask(Number(req.params.id));
res.redirect('/settings/background-tasks?message=' + encodeURIComponent(canceled ? 'Task canceled.' : 'Unable to cancel that task.'));
});
app.post('/settings/background-tasks/:id/retry', requireSettingsManageAccess(), function (req, res) {
const retryTask = backgroundTaskQueue.retryTask(Number(req.params.id));
res.redirect('/settings/background-tasks?message=' + encodeURIComponent(retryTask ? 'Task requeued.' : 'Unable to retry that task.'));
});
app.post('/settings/background-tasks/recurring/:key/run', function (req, res, next) {
function requireRecurringTaskAccess(req, res, next) {
if (!req.currentUser) {
return res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.'));
}
@@ -148,8 +86,25 @@ module.exports = function registerAdminSettingsRoutes(app, deps) {
}
return next();
}, function (req, res) {
}
app.post('/settings/tasks-background/clear-finished', requireSettingsManageAccess(), function (req, res) {
const removedCount = backgroundTaskQueue.clearFinishedTasks();
res.redirect('/settings/tasks-background?message=' + encodeURIComponent(removedCount ? 'Cleared ' + removedCount + ' finished task' + (removedCount === 1 ? '' : 's') + '.' : 'No finished tasks to clear.'));
});
app.post('/settings/tasks-background/:id/cancel', requireSettingsManageAccess(), function (req, res) {
const canceled = backgroundTaskQueue.cancelTask(Number(req.params.id));
res.redirect('/settings/tasks-background?message=' + encodeURIComponent(canceled ? 'Task canceled.' : 'Unable to cancel that task.'));
});
app.post('/settings/tasks-background/:id/retry', requireSettingsManageAccess(), function (req, res) {
const retriedTask = backgroundTaskQueue.retryTask(Number(req.params.id));
res.redirect('/settings/tasks-background?message=' + encodeURIComponent(retriedTask ? 'Task requeued.' : 'Unable to retry that task.'));
});
app.post('/settings/tasks-scheduled/recurring/:key/run', requireRecurringTaskAccess, function (req, res) {
const ran = backgroundTaskQueue.runRecurringTask(String(req.params.key || '').trim());
res.redirect('/settings/background-tasks/scheduled?message=' + encodeURIComponent(ran ? 'Scheduled refresh run queued.' : 'Unable to run that scheduled refresh.'));
res.redirect('/settings/tasks-scheduled?message=' + encodeURIComponent(ran ? 'Scheduled refresh run queued.' : 'Unable to run that scheduled refresh.'));
});
};