Skip to content

webui: hide disabled feature pages from the admin nav (closes #437)#453

Merged
lonix merged 2 commits into
mainfrom
claude/hide-disabled-nav
May 26, 2026
Merged

webui: hide disabled feature pages from the admin nav (closes #437)#453
lonix merged 2 commits into
mainfrom
claude/hide-disabled-nav

Conversation

@lonix
Copy link
Copy Markdown
Owner

@lonix lonix commented May 20, 2026

Closes #437.

Problem

The admin sidebar (NAV_ITEMS / renderNav in src/web/admin-layout.ts) listed every page unconditionally. So Notices, Polls, Reaction Roles, Announcements and Voice Channels appeared even with their feature flag off — an operator would click through to an inert page for a feature they hadn't enabled.

Changes

src/web/admin-layout.ts

  • NavItem gains an optional featureKey — the <feature>.enabled config key gating the page. Dashboard, Settings, Permissions, Setup Wizard, Database and Bootstrap have none and are always shown.
  • NAV_ITEMS annotates the five gated entries.
  • renderNav filters out items whose gating feature resolves to false. An unknown featureKey (absent from the status map) is treated as enabled — a wiring gap fails open rather than blanking the nav.
  • AdminPageOptions gains an optional navFeatureStatus map; when omitted the nav shows everything (keeps existing direct callers + tests working).
  • New resolveNavFeatureStatus helper builds the status map from an injected async boolean reader, so admin-layout stays free of a direct ConfigService dependency and remains unit-testable.

src/web/admin-views.ts

  • CommonProps carries navFeatureStatus; all 14 renderAdminPage calls thread it through.

src/web/read-only-routes.ts

  • commonFromReq is now async and resolves navFeatureStatus via ConfigService.getBoolean. All 10 call sites updated to await.

src/web/write-routes.ts

  • The four write-router page renders (import preview, wizard start/step/confirm) resolve navFeatureStatus too, via a small navStatusForPage helper, so their sidebar matches the read-only pages.

Scope decision

The issue left the direct-navigation behaviour open ("the existing page or a 'feature disabled' notice"). Feature pages stay reachable by direct URL — they already render a sensible read-only/disabled view, so no per-route guard or redirect is added. #437 only suppresses the nav advertisement, which is the lighter, lower-risk of the two options.

Verification

  • npm test — 744 passed, 1 skipped, 0 failed (5 new tests).
  • npx tsc --noEmit — clean.
  • npm run lint — 0 errors.
  • npm run format:check — clean.
  • Manual: disable notices.enabled in Settings → Notices disappears from the sidebar on the next page load; re-enable → it returns. /admin/notices typed directly still renders.

Test plan

  • Unit: nav shows all when status map omitted; disabled hidden / enabled kept / ungated always shown; missing-key fails open.
  • Unit: resolveNavFeatureStatus queries exactly the gated keys.
  • Manual sidebar check across a few feature toggles.

Generated by Claude Code

The sidebar listed every page unconditionally, so Notices, Polls,
Reaction Roles, Announcements and Voice Channels showed up even when
their feature flag was off — an operator would click through to an
inert page for a feature they hadn't enabled.

admin-layout.ts:

- NavItem gains an optional `featureKey` — the `<feature>.enabled`
  config key that gates the page. Dashboard, Settings, Permissions,
  Setup Wizard, Database and Bootstrap have none and are always shown.
- NAV_ITEMS annotates the five gated entries (announcements, polls,
  reactionroles, notices, voicechannels).
- renderNav filters out items whose gating feature resolves to false.
  An unknown featureKey (absent from the status map) is treated as
  enabled — a wiring gap fails open rather than blanking the nav.
- AdminPageOptions gains an optional navFeatureStatus map; when omitted
  the nav shows everything (keeps existing direct callers and tests
  working).
- New resolveNavFeatureStatus helper builds the status map from an
  injected async boolean reader, so admin-layout stays free of a direct
  ConfigService dependency and remains unit-testable.

admin-views.ts:

- CommonProps carries navFeatureStatus; all 14 renderAdminPage calls
  thread it through.

read-only-routes.ts:

- commonFromReq is now async and resolves navFeatureStatus via
  ConfigService.getBoolean. All 10 call sites updated to await it.

write-routes.ts:

- The four write-router page renders (import preview, wizard
  start/step/confirm) resolve navFeatureStatus too, via a small
  navStatusForPage helper, so their sidebar matches the read-only
  pages.

Scope note: feature pages stay reachable by direct URL — they already
render a sensible read-only/disabled view. #437 only suppresses the
nav advertisement, which is the lighter of the two options the issue
left open.

Tests: 5 new cases in admin-layout.test.ts covering nav filtering
(omitted map shows all, disabled hidden / enabled kept / ungated
always shown, missing-key fails open) and resolveNavFeatureStatus
querying exactly the gated keys.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the WebUI admin layout so the sidebar navigation only advertises pages for features that are currently enabled, while still allowing direct URL access to disabled feature pages (per the scope decision in #437).

Changes:

  • Added optional feature gating metadata to admin nav items and filtered the rendered nav based on a per-request feature-status map.
  • Threaded navFeatureStatus through admin view renderers so every page can render a consistent sidebar.
  • Resolved navFeatureStatus in both read-only and write routes via ConfigService.getBoolean, and added unit tests around nav filtering/status resolution.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/web/admin-layout.ts Adds featureKey to nav items, introduces resolveNavFeatureStatus, and filters sidebar items based on navFeatureStatus.
src/web/admin-views.ts Extends common page props with optional navFeatureStatus and passes it through to renderAdminPage.
src/web/read-only-routes.ts Makes commonFromReq async to compute navFeatureStatus and updates all call sites to await it.
src/web/write-routes.ts Adds a helper to compute navFeatureStatus for write-rendered pages (wizard/import) to keep sidebar consistent.
__tests__/web/admin-layout.test.ts Adds coverage for nav filtering behavior and resolveNavFeatureStatus key resolution.

Comment thread src/web/admin-layout.ts Outdated
* are always shown. The page URL itself stays reachable by direct
* navigation; only the nav advertisement is suppressed.
*/
featureKey?: string;
Comment thread src/web/admin-layout.ts Outdated
Comment on lines +112 to +115
* omitted, every nav item is shown (keeps direct callers and tests that
* don't care about nav filtering working unchanged).
*/
navFeatureStatus?: Record<string, boolean>;
Comment thread src/web/admin-layout.ts Outdated
Comment on lines +95 to +101
const status: Record<string, boolean> = {};
for (const item of NAV_ITEMS) {
if (item.featureKey) {
status[item.featureKey] = await isEnabled(item.featureKey);
}
}
return status;
Comment on lines +116 to 125
const config = ConfigService.getInstance();
const navFeatureStatus = await resolveNavFeatureStatus((key) =>
config.getBoolean(key, false),
);
return {
guildId: session.guildId,
csrfToken: getCsrfToken(req),
remainingMs: getDisplayedRemainingMs(session),
navFeatureStatus,
};
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this one deliberately. The duplicate getBoolean is a ConfigService in-memory cache hit on a request that already does Discord API round-trips and Mongo queries — the cost is noise. Against that, reusing navFeatureStatus for page-level enabled flags would couple ~5 page handlers to a map that exists specifically for sidebar filtering: a future change to which nav items are feature-gated (or to the NavFeatureStatus shape) would silently alter page-render logic in unrelated handlers. The two reads are intentionally independent concerns that happen to share a key.

The "drift if defaults change" point doesn't apply here either — both call sites pass the same explicit false default, matching defaultConfig. Addressed the other three comments in 85fc420.


Generated by Claude Code

Three type-safety / efficiency improvements from review:

1. featureKey is now typed `keyof ConfigSchema` instead of plain
   `string`, so a typo in NAV_ITEMS fails at compile time rather than
   silently never matching at runtime.

2. New exported `NavFeatureStatus` type — `Partial<Record<keyof
   ConfigSchema, boolean>>`. The map intentionally allows missing keys
   (renderNav fails open on an absent key); the previous
   `Record<string, boolean>` typed every index access as a non-optional
   boolean, which didn't model that. Threaded through AdminPageOptions,
   CommonProps, commonFromReq, and navStatusForPage.

3. resolveNavFeatureStatus now resolves feature keys in parallel via
   Promise.all and de-duplicates them first, so two nav items sharing a
   key (none today, but defensively) trigger a single read.

Not changed: the reviewer also suggested route handlers reuse
common.navFeatureStatus for their page-level `enabled` flags instead of
re-reading the *.enabled key. Skipped — see PR reply. The duplicate read
is a ConfigService cache hit, and reusing the nav map would couple five
page handlers to a structure built for sidebar filtering.
@lonix lonix merged commit 06696a3 into main May 26, 2026
8 checks passed
@lonix lonix deleted the claude/hide-disabled-nav branch May 26, 2026 06:49
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.

WebUI: hide disabled feature pages from the admin navigation

3 participants