fix(sidebar): require admin role for Schedule task and debounce search input (#1243)

The right-click Schedule task menu item and its keyboard shortcut were gated
only on isPaid, but the backend write routes under /api/scheduled-tasks
enforce requireAdmin + requirePaid on every action. Non-admin Skipper or
Admiral users would see the menu item and hit a 403 on click. The frontend
now mirrors the backend by gating Schedule task on isPaid && isAdmin so the
affordance only renders for users whose action will actually succeed.

Also adds a 120ms keystroke debounce to the sidebar search input. The
useStackListState filter rebuild was previously running on every keystroke
because <Command shouldFilter={false}> disables cmdk's own filter and the
existing 250ms timer only debounces state-invalidate events. Visible input
stays immediate via local state; the debounced emit drives the filter pass.

Adds a regression guard that /api/stacks/statuses is short-circuited by the
remote-node proxy (covers the sidebar status poll path) and updates the
sidebar feature docs to reflect the admin role requirement on Schedule task.
This commit is contained in:
Anso
2026-05-28 14:16:46 -04:00
committed by GitHub
parent 265fece988
commit 979181875d
9 changed files with 164 additions and 7 deletions
@@ -21,6 +21,7 @@ interface UseSidebarContextMenuOptions {
activeNode: Node | null | undefined;
isPaid: boolean;
isAdmiral: boolean;
isAdmin: boolean;
can: (action: PermissionAction, resourceType?: string, resourceId?: string) => boolean;
}
@@ -32,6 +33,7 @@ export function useSidebarContextMenu({
activeNode,
isPaid,
isAdmiral,
isAdmin,
can,
}: UseSidebarContextMenuOptions) {
const buildMenuCtx = useCallback((file: string): StackMenuCtx => {
@@ -42,6 +44,7 @@ export function useSidebarContextMenu({
isBusy: stackListState.isStackBusy(file),
isPaid,
isAdmiral,
isAdmin,
canDelete: can('stack:delete', 'stack', sName),
canEditLabels: can('stack:edit', 'stack', sName),
// POST /api/labels (the inline "New label" entry) is guarded by the
@@ -121,9 +124,13 @@ export function useSidebarContextMenu({
navState.setActiveView('scheduled-ops');
},
};
// Handlers from useStackActions, useOverlayState, useViewNavigationState are
// useCallback-stabilized at their owner hooks, so listing the menu surface
// values (status maps, role/tier flags, pin state) is sufficient. Exhaustive
// deps would force a rebuild on every parent render and defeat the memo.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
stackListState.stackStatuses, stackListState.stackPorts, isPaid, isAdmiral,
stackListState.stackStatuses, stackListState.stackPorts, isPaid, isAdmiral, isAdmin,
stackListState.isPinned, stackListState.labels, stackListState.stackLabelMap,
stackListState.pin, stackListState.unpin,
]);