Skip to content

feat: Settings → Menu Options, per role, with an admin's own view on top - #131

Open
MikeBerryCC wants to merge 1 commit into
ovexro:mainfrom
MikeBerryCC:Hide-Menu-Options
Open

MikeBerryCC wants to merge 1 commit into
ovexro:mainfrom
MikeBerryCC:Hide-Menu-Options

Conversation

@MikeBerryCC

Copy link
Copy Markdown

What

A Menu Options tab in Settings with a role selector and a checkbox per menu entry. Unticking one removes it from that role's side menu, top bar and Ctrl-K palette.

Why

The panel does DNS, mail, Docker, git deploys, WordPress, backups, resellers and telemetry, and shows every one of those doors to an operator who uses four of them — 26 sidebar rows across five groups, with no way to put away the ones a given box will never use. It also gives an operator standing a box up for a client no way to make that client's menu right before they first sign in.

Model

The hidden set is stored panel-side as menu_hidden_{role} and applies to every account with that role, on every browser.

The admin row is a default rather than a decision, and that asymmetry is deliberate. For reseller, user and client the stored array is what the role sees. For admin it is the starting point, and any administrator may override it for their own browser — an operator debugging a box has to be able to get every door back without another account's help, and there is nobody above an admin to restore a row they can no longer find. The scope control on the tab says which of the two is being edited.

This is presentation, not permission, and the tab says so where the ticking happens rather than only in a commit message. A hidden row's route still resolves and every page still applies the access rules it always did. The hidden set is kept out of isNavVisible, which answers the different question of whether an account may be offered a page — folding them together would make a checkbox in Settings look like a security control, and would invite someone to hide a row instead of restricting it.

Four entries can never be hidden, and are listed in the tab as ticked-and-disabled rather than omitted so an operator can see why: Dashboard (where every layout's logo links), Settings (the only way back to these checkboxes), My Account (the sole entry every role can see) and the signed-in user with its Logout button.

New endpoint

GET /api/settings is AdminUser, so the accounts this shapes are exactly the ones that cannot read it. GET /api/menu-options answers only for claims.role, the way branding already does for a single other key, and returns the role alongside the set — the caller has to know whether what it received is binding or a default, and deriving that separately in the frontend is how the two would drift.

Notes for review

  • The checkbox list is derived from navGroups, not restated, so a row added to the registry gets a checkbox with no second list to update. A hardcoded copy is how the command palette came to offer pages the sidebar did not.
  • Top-bar controls have no registry of their own, so they are enumerated in menuOptions.ts and keyed chrome:* to keep one flat array free of collisions with paths. The bell and the /notifications nav row are separate checkboxes.
  • The dashboard's "+ Deploy App" and "+ Add Site" are gated on the same checkboxes as the Docker Apps and Sites rows — leaving a shortcut there would put the hidden thing back on the first screen the account sees.
  • A malformed menu_hidden_* is rejected on save. All three readers (backend, checkboxes, layouts) treat an unparseable value as "hide nothing", so a bad array would otherwise fail silently on somebody else's screen.
  • The Ctrl-K palette honours the hidden set. Happy to back that out if you would rather it stay a full escape hatch — it is one condition in CommandPalette.tsx.

New env vars / migrations / dependencies

None. Four new ALLOWED_KEYS entries (menu_hidden_admin|reseller|user|client), one new route, no new crates or packages, no migration — the settings table already holds arbitrary keys.

Testing

  • tests/settings-controls-pin-e2e.sh: 19 passed, 0 failed with the four new keys. §1 credits them through the format!("menu_hidden_{ prefix and §9 through the menu_hidden_${menuRole} template in Settings.tsx, so neither arm needed an exemption. Worth knowing §9 scans .tsx only, which is why that template has to stay there.
  • Backend cargo test: 487 passed, 0 failed. No new clippy findings; new lines are rustfmt-clean. I did not run cargo fmt across the crate, as the tree has pre-existing drift that would bury the change.
  • Frontend tsc -b and npm run build: clean.
  • Verified in a running panel, not only compiled: hiding a row drops it from the sidebar live and from the Ctrl-K palette; the setting persists to the settings table and survives a reload; "+ Deploy App" disappears with the Docker Apps row while "+ Add Site" stays; the top-bar bell and the Notifications nav row toggle independently; the always-shown four render disabled; re-ticking restores everything; and an admin's own override writes localStorage while leaving the stored default untouched.

Size

10 files, +633 / −23, one commit.

I did not add a FEATURES.md row — happy to if you would like it recorded; it did not obviously fit the existing table shape.

🤖 Generated with Claude Code

A panel that does DNS, mail, Docker, git deploys, WordPress, backups, resellers
and telemetry shows every one of those doors to an operator who uses four of
them. The sidebar has grown to 26 rows across five groups and there was no way
to put away the ones a given box will never use — nor for the operator standing
a box up for a client to have that client's menu be right the first time they
sign in.

Adds a Menu Options tab to Settings with a role selector and a checkbox per
menu entry. Unticking one removes it from that role's side menu, top bar and
Ctrl-K palette. The hidden set is stored panel-side as `menu_hidden_{role}` and
applies to every account with that role, on every browser.

The admin row is a DEFAULT rather than a decision, and that asymmetry is the
point. For reseller, user and client the stored array is what the role sees.
For admin it is the starting point, and any administrator may override it for
their own browser — because an operator debugging a box has to be able to get
every door back without another account's help, and there is nobody above an
admin to restore a row they can no longer find. The scope control says which of
the two is being edited, so an admin cannot set "the admin menu" and then be
unable to explain why a colleague's differs.

This is presentation, NOT permission, and the tab says so where the ticking
happens rather than only here. A hidden row's route still resolves and every
page still applies the access rules it always did, so the hidden set is kept out
of `isNavVisible`, which answers the different question of whether an account
may be offered a page at all. Folding the two together would make a checkbox in
Settings look like a security control, and would invite someone to hide a row
INSTEAD of restricting it.

Four entries can never be hidden, and are listed in the tab as ticked-and-
disabled rather than omitted so an operator can see why: Dashboard (where every
layout's logo links), Settings (the only way back to these checkboxes), My
Account (the sole entry every role can see) and the signed-in user with its
Logout button.

Reads need their own endpoint. `GET /api/settings` is `AdminUser`, so the
accounts this shapes are exactly the ones that cannot read it: `GET
/api/menu-options` answers only for `claims.role`, the way `branding` already
does for a single other key. It returns the role alongside the set, because the
caller has to know whether what it received is binding or a default — deriving
that separately in the frontend is how the two would drift.

The checkbox list is derived from `navGroups` rather than restated, so a row
added to the registry gets a checkbox with no second list to update; a hardcoded
copy is exactly how the command palette came to offer pages the sidebar did not.
The top-bar controls have no registry of their own, so they are enumerated in
`menuOptions.ts` and keyed `chrome:*` to keep one flat stored array free of
collisions with paths. The dashboard's "+ Deploy App" and "+ Add Site" shortcuts
are gated on the same checkboxes as the Docker Apps and Sites rows: leaving a
shortcut on the dashboard would put the thing that was hidden back on the first
screen the account sees.

A malformed `menu_hidden_*` is rejected on save. Three readers — the backend,
the Settings checkboxes and the layouts — all treat an unparseable value as
"hide nothing", so a bad array fails silently on somebody else's screen rather
than anywhere the person who caused it would see it. The same defensiveness runs
through the read path: a blocked localStorage, a failed fetch, or an always-
visible path somehow present in the stored array all fall back to showing
everything, because a cluttered menu is a better failure than a missing one.

tests/settings-controls-pin-e2e.sh passes with the four new keys: 19/19. §1
credits them through the `format!("menu_hidden_{` prefix and §9 through the
`menu_hidden_${menuRole}` template in Settings.tsx, so neither arm needed an
exemption. Note §9 scans .tsx only, which is why that template has to stay
there.

Verified in a running panel: hiding a row drops it from the sidebar live and
from the Ctrl-K palette, "+ Deploy App" disappears with the Docker Apps row
while "+ Add Site" stays, the top-bar bell and the Notifications nav row toggle
independently, the always-shown four render disabled, re-ticking restores
everything, and an admin's own override writes localStorage while leaving the
stored default untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant