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
@@ -76,6 +76,21 @@ describe('remote proxy mount order', () => {
expect(res.status).not.toBe(502);
});
it('short-circuits /api/stacks/statuses (the sidebar status poll) for remote nodes', async () => {
// The sidebar polls /api/stacks/statuses every few seconds; if a future
// refactor accidentally mounted a local fast-path before the proxy, every
// operator viewing a remote node would silently see the central instance's
// own statuses. Asserts the same 502 short-circuit as /api/stacks.
const res = await request(app)
.get('/api/stacks/statuses')
.set('Authorization', authHeader)
.set('x-node-id', String(remoteNodeId));
expect(res.status).toBe(502);
expect(res.headers['x-sencho-proxy']).toBeUndefined();
expect(res.body?.error).toMatch(/unreachable/i);
});
it('handles proxy-exempt paths locally even when x-node-id targets a remote', async () => {
// /api/nodes/:id is in PROXY_EXEMPT_PREFIXES. The proxy must never catch
// gateway-level concerns; otherwise a user whose default node is remote