Save worktree changes

This commit is contained in:
2026-07-25 02:29:19 +01:00
parent 8d3b7d557b
commit db9d718cd8
170 changed files with 11719 additions and 3414 deletions
@@ -0,0 +1,88 @@
const { renderView } = require('../../view');
const { hasAnyPermission } = require('../../../rbac');
const TASKS_PER_PAGE = 10;
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 parsePageNumber(value) {
const pageNumber = Math.floor(Number(value) || 1);
return Math.max(1, pageNumber);
}
function buildPagination(totalItems, currentPage, pageParam) {
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 = [];
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
pages.push({
number: pageNumber,
active: pageNumber === safeCurrentPage,
url: `?${normalizedPageParam}=${pageNumber}`
});
}
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: `?${normalizedPageParam}=${safeCurrentPage - 1}`,
nextUrl: `?${normalizedPageParam}=${safeCurrentPage + 1}`,
pages: pages,
pageSize: TASKS_PER_PAGE,
pageParam: normalizedPageParam
};
}
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 visibleRecurringTasks = recurringTasks.slice((recurringPagination.currentPage - 1) * TASKS_PER_PAGE, recurringPagination.currentPage * TASKS_PER_PAGE);
return renderView('settings/background-tasks/index', {
title: 'Background tasks',
active: 'background-tasks',
message: message,
currentUser: currentUser || null,
scripts: ['js/settings/background-tasks.js'],
stateVersion: data && data.stateVersion ? String(data.stateVersion) : '',
tasks: visibleTasks,
recurringTasks: visibleRecurringTasks.map(function (task) {
return Object.assign({}, task, {
intervalLabel: formatIntervalLabel(task.intervalMs)
});
}),
summary: summary,
pagination: pagination,
recurringPagination: recurringPagination,
canManage: canManage,
canClearFinished: tasks.some(function (task) {
return task.status !== 'queued' && task.status !== 'running';
})
});
};
@@ -0,0 +1,99 @@
const { hasAnyPermission } = require('../../../rbac');
function requireSettingsAccess() {
return 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'])) {
return next();
}
const error = new Error('You do not have permission to access this area.');
error.statusCode = 403;
error.expose = true;
next(error);
};
}
module.exports = function registerAdminSettingsRoutes(app, deps) {
const pages = deps.pages;
const backgroundTaskQueue = deps.backgroundTaskQueue;
if (!pages || !backgroundTaskQueue) {
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) {
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));
});
app.get('/settings/background-tasks/state', requireSettingsAccess(), 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
});
});
function requireSettingsManageAccess() {
return function (req, res, next) {
if (!req.currentUser) {
return res.redirect('/login?message=' + encodeURIComponent('Please sign in to continue.'));
}
if (hasAnyPermission(req.currentUser, ['background-tasks.allow'])) {
return next();
}
const error = new Error('You do not have permission to access this area.');
error.statusCode = 403;
error.expose = true;
next(error);
};
}
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.'));
});
};
+14
View File
@@ -0,0 +1,14 @@
const { renderView } = require('../../../view');
module.exports = function renderRbacAddPage(message, currentUser, formValues, permissionGroups, messageVariant) {
return renderView('settings/rbac/add', {
title: 'Create role',
active: 'rbac',
message: message,
messageVariant: messageVariant || 'primary',
currentUser: currentUser || null,
formValues: formValues || {},
permissionGroups: permissionGroups || [],
scripts: ['js/rbac-permissions.js']
});
};
+14
View File
@@ -0,0 +1,14 @@
const { renderView } = require('../../../view');
module.exports = function renderRbacEditPage(role, message, currentUser, permissionGroups, users) {
return renderView('settings/rbac/edit', {
title: 'Edit role',
active: 'rbac',
message: message,
currentUser: currentUser || null,
role: role,
permissionGroups: permissionGroups || [],
users: users || [],
scripts: ['js/rbac-permissions.js']
});
};
+11
View File
@@ -0,0 +1,11 @@
const { renderView } = require('../../../view');
module.exports = function renderRbacPage(data, message, currentUser) {
return renderView('settings/rbac/list', {
title: 'Roles and permissions',
active: 'rbac',
message: message,
currentUser: currentUser || null,
roles: data.roles || []
});
};
+13
View File
@@ -0,0 +1,13 @@
const { renderView } = require('../../../view');
module.exports = function renderUsersAddPage(message, currentUser, roles, formValues, messageVariant) {
return renderView('settings/users/add', {
title: 'Add user',
active: 'users',
message: message,
messageVariant: messageVariant || 'primary',
currentUser: currentUser || null,
roles: roles || [],
formValues: formValues || {}
});
};
+12
View File
@@ -0,0 +1,12 @@
const { renderView } = require('../../../view');
module.exports = function renderUsersEditPage(user, message, currentUser, roles) {
return renderView('settings/users/edit', {
title: 'Edit user',
active: 'users',
message: message,
currentUser: currentUser || null,
user: user,
roles: roles || []
});
};
+11
View File
@@ -0,0 +1,11 @@
const { renderView } = require('../../../view');
module.exports = function renderUsersPage(data, message, currentUser) {
return renderView('settings/users/list', {
title: 'Users',
active: 'users',
message: message,
currentUser: currentUser || null,
users: data.users || []
});
};