381 lines
13 KiB
JavaScript
381 lines
13 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 buildTaskPlayerLabel(metadata) {
|
|
const playerLabel = String(metadata && metadata.playerLabel || '').trim();
|
|
const playerIdentifier = String(metadata && metadata.playerIdentifier || '').trim();
|
|
const playerPublicBaseUrl = String(metadata && metadata.playerPublicBaseUrl || '').trim();
|
|
|
|
if (playerLabel) {
|
|
return playerLabel;
|
|
}
|
|
|
|
if (playerIdentifier && playerPublicBaseUrl) {
|
|
return `${playerIdentifier} (${playerPublicBaseUrl})`;
|
|
}
|
|
|
|
if (playerIdentifier) {
|
|
return playerIdentifier;
|
|
}
|
|
|
|
if (playerPublicBaseUrl) {
|
|
return playerPublicBaseUrl;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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; },
|
|
function (item) { return item && item.metadata && item.metadata.playerLabel; },
|
|
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
|
|
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
|
|
])(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, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].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) {
|
|
const playerLabel = buildTaskPlayerLabel(task.metadata);
|
|
const taskDescription = [playerLabel, String(task && task.key || '').trim()].filter(Boolean).join(':');
|
|
|
|
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) : '',
|
|
playerLabel: playerLabel,
|
|
taskDescription: taskDescription
|
|
});
|
|
});
|
|
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; },
|
|
function (item) { return item && item.metadata && item.metadata.playerLabel; },
|
|
function (item) { return item && item.metadata && item.metadata.playerIdentifier; },
|
|
function (item) { return item && item.metadata && item.metadata.playerPublicBaseUrl; }
|
|
])(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, task && task.metadata && task.metadata.playerLabel, task && task.metadata && task.metadata.playerIdentifier, task && task.metadata && task.metadata.playerPublicBaseUrl].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),
|
|
playerLabel: buildTaskPlayerLabel(task.metadata)
|
|
});
|
|
}),
|
|
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);
|
|
}
|
|
}; |