Release 1.5.3

This commit is contained in:
2026-07-25 15:11:41 +01:00
parent 9ee938fd2f
commit 0e89892c94
89 changed files with 7660 additions and 542 deletions
@@ -2,6 +2,7 @@ const { renderView } = require('../../view');
const { hasAnyPermission } = require('../../../rbac');
const TASKS_PER_PAGE = 10;
const TASK_STATUSES = new Set(['queued', 'running', 'failed', 'completed', 'canceled']);
function formatIntervalLabel(intervalMs) {
const value = Math.max(1, Number(intervalMs) || 0);
@@ -21,7 +22,40 @@ function parsePageNumber(value) {
return Math.max(1, pageNumber);
}
function buildPagination(totalItems, currentPage, pageParam) {
function normalizeStatusFilter(value) {
const status = String(value || '').trim().toLowerCase();
return TASK_STATUSES.has(status) ? status : '';
}
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 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);
@@ -30,13 +64,17 @@ function buildPagination(totalItems, currentPage, pageParam) {
const pages = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
const nextQuery = Object.assign({}, queryState, { [normalizedPageParam]: pageNumber });
pages.push({
number: pageNumber,
active: pageNumber === safeCurrentPage,
url: `?${normalizedPageParam}=${pageNumber}`
url: buildQueryString(nextQuery)
});
}
const previousQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage - 1 });
const nextQuery = Object.assign({}, queryState, { [normalizedPageParam]: safeCurrentPage + 1 });
return {
currentPage: safeCurrentPage,
totalPages: totalPages,
@@ -46,23 +84,139 @@ function buildPagination(totalItems, currentPage, pageParam) {
endItem: endIndex,
hasPrevious: safeCurrentPage > 1,
hasNext: safeCurrentPage < totalPages,
previousUrl: `?${normalizedPageParam}=${safeCurrentPage - 1}`,
nextUrl: `?${normalizedPageParam}=${safeCurrentPage + 1}`,
previousUrl: buildQueryString(previousQuery),
nextUrl: buildQueryString(nextQuery),
pages: pages,
pageSize: TASKS_PER_PAGE,
pageParam: normalizedPageParam
};
}
function taskMatchesFilters(task, statusFilter, sourceFilter) {
if (statusFilter && task.status !== statusFilter) {
return false;
}
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;
}
function formatStatusLabel(status) {
const normalizedStatus = String(status || '').trim().toLowerCase();
if (!normalizedStatus) {
return 'All tasks';
}
return normalizedStatus.charAt(0).toUpperCase() + normalizedStatus.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 buildStatusFilterUrl(queryState, statusFilter) {
const nextQuery = Object.assign({}, queryState, {
page: 1,
status: statusFilter && statusFilter.status === statusFilter.targetStatus ? '' : statusFilter.targetStatus,
sourceType: statusFilter && statusFilter.sourceType ? statusFilter.sourceType : '',
sourceId: statusFilter && statusFilter.sourceId ? statusFilter.sourceId : ''
});
return buildQueryString(nextQuery);
}
module.exports = function renderBackgroundTasksPage(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 pagination = buildPagination(tasks.length, parsePageNumber(data && data.page), 'page');
const recurringPagination = buildPagination(recurringTasks.length, parsePageNumber(data && data.recurringPage), 'recurringPage');
const visibleTasks = tasks.slice((pagination.currentPage - 1) * TASKS_PER_PAGE, pagination.currentPage * TASKS_PER_PAGE);
const queryState = {
page: parsePageNumber(data && data.page),
recurringPage: parsePageNumber(data && data.recurringPage),
status: normalizeStatusFilter(data && data.status),
sourceType: '',
sourceId: ''
};
const sourceFilter = normalizeSourceFilter(data && data.sourceType, data && data.sourceId);
queryState.status = normalizeStatusFilter(data && data.status);
queryState.sourceType = sourceFilter.sourceType;
queryState.sourceId = sourceFilter.sourceId;
const filteredTasks = tasks.filter(function (task) {
return taskMatchesFilters(task, queryState.status, sourceFilter);
});
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 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) {
const isActive = queryState.status === card.status;
const statusFilter = {
status: queryState.status,
sourceType: queryState.sourceType,
sourceId: queryState.sourceId,
targetStatus: card.status
};
return Object.assign({}, card, {
active: isActive,
href: buildStatusFilterUrl(queryState, statusFilter),
ariaLabel: isActive ? `Clear ${card.label.toLowerCase()} filter` : `Filter tasks by ${card.label.toLowerCase()}`
});
});
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.status || queryState.sourceType || queryState.sourceId);
return renderView('settings/background-tasks/index', {
title: 'Background tasks',
@@ -71,13 +225,18 @@ module.exports = function renderBackgroundTasksPage(data, message, currentUser)
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
stateVersion: data && data.stateVersion ? String(data.stateVersion) : '',
tasks: visibleTasks,
tasks: visibleTasksWithSources,
recurringTasks: visibleRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
});
}),
summary: summary,
statusCards: statusCards,
activeStatusLabel: formatStatusLabel(queryState.status),
activeSourceLabel: queryState.sourceType && queryState.sourceId ? `${queryState.sourceType} #${queryState.sourceId}` : '',
clearFiltersUrl: buildQueryString({ recurringPage: queryState.recurringPage }),
hasActiveFilters: hasActiveFilters,
pagination: pagination,
recurringPagination: recurringPagination,
canManage: canManage,
+2 -1
View File
@@ -6,6 +6,7 @@ module.exports = function renderRbacPage(data, message, currentUser) {
active: 'rbac',
message: message,
currentUser: currentUser || null,
roles: data.roles || []
roles: data.roles || [],
pagination: data.pagination || null
});
};
+2 -1
View File
@@ -6,6 +6,7 @@ module.exports = function renderUsersPage(data, message, currentUser) {
active: 'users',
message: message,
currentUser: currentUser || null,
users: data.users || []
users: data.users || [],
pagination: data.pagination || null
});
};