Skip to content

Add configuration for which pages are shown in UI#13

Merged
ScriptSmith merged 2 commits intomainfrom
page-config
Mar 18, 2026
Merged

Add configuration for which pages are shown in UI#13
ScriptSmith merged 2 commits intomainfrom
page-config

Conversation

@ScriptSmith
Copy link
Owner

No description provided.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 17, 2026

Greptile Summary

This PR introduces per-page visibility configuration for the Hadrian UI. Operators can now mark any page as enabled, disabled, or notice (with an optional message) in the server config. The Rust backend adds PagesConfig / AdminPagesConfig structs with an ergonomic TOML-friendly PageConfig enum (bare string or inline table), serialises them through the unauthenticated /admin/v1/ui/config endpoint, and applies sensible WASM-mode defaults that mark multi-user/server-only features as notices. On the frontend, a new PageGuard component gates every route, Header, AdminSidebar, and UserMenu filter navigation items, and the command palette omits shortcuts for disabled pages.

Key observations from the review:

  • getFirstEnabledRoute redirects to "notice" pages — when a disabled page triggers a redirect, the function returns the first page whose status is not "disabled", which may be a "notice" page rather than a fully functional one. Depending on intent, consider prioritising "enabled" pages first.
  • Flash of default content during config loadPageGuard and RootRedirect use the default config (all pages enabled) while the server response is still in-flight (isLoading = true). Pages that the server marks disabled briefly render before being redirected. Gating routing decisions behind isLoading would eliminate this.
  • /session route uses an admin-scoped page key — the route lives inside the main AppLayout but is controlled by AdminPagesConfig.session_info. The admin sidebar also links to this non-/admin path. Consolidating the route's home (either fully under /admin or with a top-level PagesConfig key) would reduce architectural confusion.

Confidence Score: 4/5

  • Safe to merge with minor UX concerns around the config loading flash and redirect semantics.
  • The Rust backend changes are well-structured, well-tested, and correctly wired into the existing config/response pipeline. The frontend implementation is comprehensive and consistent across all navigation surfaces. The two issues (loading-state flash and "notice" pages being redirect targets) are UX concerns rather than data-loss or security bugs. The architectural inconsistency of the /session route is pre-existing behaviour made more visible by this PR. No regressions or critical logic errors were found.
  • Pay close attention to ui/src/components/PageGuard/PageGuard.tsx (redirect semantics) and ui/src/config/ConfigProvider.tsx (loading state not guarded in routing).

Important Files Changed

Filename Overview
src/config/ui.rs Adds PageStatus, PageConfig (untagged serde enum), PagesConfig, and AdminPagesConfig structs with full test coverage. Defaults, serialization, and validation are all well-handled.
src/routes/admin/ui_config.rs Adds PagesResponse, AdminPagesResponse, and PageConfigResponse response types with correct From impls mapping config structs to API responses. No issues found in new code.
src/wasm.rs Uses the new PageConfig/PagesConfig types to mark WASM-unavailable features (teams, API keys, usage, several admin pages) with notice messages. Clean integration of the new config API.
ui/src/components/PageGuard/PageGuard.tsx Core routing guard; getFirstEnabledRoute returns "notice" pages as redirect targets, which may not match its name or the admin's intent. See inline comment.
ui/src/config/ConfigProvider.tsx Fetches and deep-merges page config from server; no loading guard is applied before routing decisions, causing a brief flash of default (all-enabled) config before the server response arrives.
ui/src/config/types.ts Adds PageStatus, PageConfig, PagesConfig, and AdminPagesConfig TypeScript interfaces, mirroring the Rust backend types accurately.
ui/src/routes/AppRoutes.tsx Wraps all page routes in PageGuard; the /session route uses an admin-scoped page key despite living in the main app layout, which is architecturally inconsistent.
ui/src/components/AdminLayout/AdminSidebar.tsx Filters admin nav items by page visibility (hiding "disabled" items, showing "notice" items). Correct integration of the new config.
ui/src/components/PageNotice/PageNotice.tsx New component that renders a centred info card when a page is in "notice" status. Simple and correct.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant ConfigProvider
    participant Backend as Backend (GET /admin/v1/ui/config)
    participant PageGuard
    participant Router

    Browser->>ConfigProvider: App mount (isLoading=true, defaultConfig)
    ConfigProvider->>Backend: fetch /admin/v1/ui/config
    Note over ConfigProvider: Renders with defaultPagesConfig (all enabled)
    Browser->>Router: Navigate to route (e.g. /chat)
    Router->>PageGuard: Render with defaultConfig
    PageGuard-->>Browser: Render page content (flash)

    Backend-->>ConfigProvider: UiConfigResponse { pages: PagesResponse }
    ConfigProvider->>ConfigProvider: deep-merge with defaultPagesConfig
    ConfigProvider->>PageGuard: Re-render with merged config

    alt page.status == "disabled"
        PageGuard->>Router: Navigate(getFirstEnabledRoute)
        Router-->>Browser: Redirect to first enabled/notice page
    else page.status == "notice"
        PageGuard-->>Browser: Render PageNotice (title + message)
    else page.status == "enabled"
        PageGuard-->>Browser: Render children
    end
Loading

Comments Outside Diff (1)

  1. ui/src/config/ConfigProvider.tsx, line 155-188 (link)

    P2 Flash of disabled content during config load

    ConfigProvider initialises with defaultConfig (all pages enabled) while isLoading is true. Before the server config arrives, PageGuard and RootRedirect use the default config, so a page that the server marks as "disabled" will briefly render before being redirected away. This is a jarring flash for end users.

    The isLoading flag is already part of the context value (ConfigContextValue). PageGuard and RootRedirect could suspend rendering until the config is ready:

    // In PageGuard
    export function PageGuard({ pageKey, pageTitle, children }: PageGuardProps) {
      const { config, isLoading } = useConfig();
      if (isLoading) return null; // or a skeleton/spinner
      ...
    }

    A similar guard in RootRedirect would prevent a spurious navigation to /chat (the default first route) before the real config is known.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: ui/src/config/ConfigProvider.tsx
    Line: 155-188
    
    Comment:
    **Flash of disabled content during config load**
    
    `ConfigProvider` initialises with `defaultConfig` (all pages enabled) while `isLoading` is `true`. Before the server config arrives, `PageGuard` and `RootRedirect` use the default config, so a page that the server marks as `"disabled"` will briefly render before being redirected away. This is a jarring flash for end users.
    
    The `isLoading` flag is already part of the context value (`ConfigContextValue`). `PageGuard` and `RootRedirect` could suspend rendering until the config is ready:
    
    ```tsx
    // In PageGuard
    export function PageGuard({ pageKey, pageTitle, children }: PageGuardProps) {
      const { config, isLoading } = useConfig();
      if (isLoading) return null; // or a skeleton/spinner
      ...
    }
    ```
    
    A similar guard in `RootRedirect` would prevent a spurious navigation to `/chat` (the default first route) before the real config is known.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/components/PageGuard/PageGuard.tsx
Line: 38-45

Comment:
**`getFirstEnabledRoute` redirects to "notice" pages**

`getFirstEnabledRoute` skips pages whose status is `"disabled"`, but it also returns the first page with status `"notice"`. When a `PageGuard` redirects a user away from a disabled page (line 58), the redirect target could be a `"notice"` page, meaning the user is silently bounced to a notice screen rather than a functional page. The function name says "enabled" but its behaviour includes `"notice"`.

If all main pages are either `"disabled"` or `"notice"`, the user will keep landing on notices, never reaching a working page. Consider only returning routes whose status is strictly `"enabled"`, and falling back to `"notice"` pages as a second tier, or at least document the intended behaviour:

```suggestion
export function getFirstEnabledRoute(pages: PagesConfig): string {
  // First, prefer a fully enabled page
  for (const key of mainPageOrder) {
    if (pages[key].status === "enabled") {
      return mainPageRoutes[key];
    }
  }
  // Fall back to a notice page if no enabled page exists
  for (const key of mainPageOrder) {
    if (pages[key].status !== "disabled") {
      return mainPageRoutes[key];
    }
  }
  return "/account";
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: ui/src/config/ConfigProvider.tsx
Line: 155-188

Comment:
**Flash of disabled content during config load**

`ConfigProvider` initialises with `defaultConfig` (all pages enabled) while `isLoading` is `true`. Before the server config arrives, `PageGuard` and `RootRedirect` use the default config, so a page that the server marks as `"disabled"` will briefly render before being redirected away. This is a jarring flash for end users.

The `isLoading` flag is already part of the context value (`ConfigContextValue`). `PageGuard` and `RootRedirect` could suspend rendering until the config is ready:

```tsx
// In PageGuard
export function PageGuard({ pageKey, pageTitle, children }: PageGuardProps) {
  const { config, isLoading } = useConfig();
  if (isLoading) return null; // or a skeleton/spinner
  ...
}
```

A similar guard in `RootRedirect` would prevent a spurious navigation to `/chat` (the default first route) before the real config is known.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: ui/src/routes/AppRoutes.tsx
Line: 236-244

Comment:
**Session route gated by an admin config key**

The `/session` route lives inside the `AppLayout` group (main user-facing app), but it is guarded with an `AdminPagesConfig` field key via the `"admin.*"` prefix. `getPageConfig` resolves this correctly at runtime, but the coupling is architecturally surprising: a main-app route is controlled by a field that belongs to admin page configuration.

`AdminSidebar.tsx` also lists a "Session Info" nav item pointing at `/session` (not an `/admin/...` path), using a key from `AdminPagesConfig`. Reading either file in isolation, the route appears admin-scoped while actually being served under the user layout.

Consider either moving the route under `/admin/session` so it lives consistently in the admin layout, or adding a top-level field in `PagesConfig` for it to keep the main-app and admin separation clear.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: b4522b5

Comment on lines +38 to +45
export function getFirstEnabledRoute(pages: PagesConfig): string {
for (const key of mainPageOrder) {
if (pages[key].status !== "disabled") {
return mainPageRoutes[key];
}
}
return "/account";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 getFirstEnabledRoute redirects to "notice" pages

getFirstEnabledRoute skips pages whose status is "disabled", but it also returns the first page with status "notice". When a PageGuard redirects a user away from a disabled page (line 58), the redirect target could be a "notice" page, meaning the user is silently bounced to a notice screen rather than a functional page. The function name says "enabled" but its behaviour includes "notice".

If all main pages are either "disabled" or "notice", the user will keep landing on notices, never reaching a working page. Consider only returning routes whose status is strictly "enabled", and falling back to "notice" pages as a second tier, or at least document the intended behaviour:

Suggested change
export function getFirstEnabledRoute(pages: PagesConfig): string {
for (const key of mainPageOrder) {
if (pages[key].status !== "disabled") {
return mainPageRoutes[key];
}
}
return "/account";
}
export function getFirstEnabledRoute(pages: PagesConfig): string {
// First, prefer a fully enabled page
for (const key of mainPageOrder) {
if (pages[key].status === "enabled") {
return mainPageRoutes[key];
}
}
// Fall back to a notice page if no enabled page exists
for (const key of mainPageOrder) {
if (pages[key].status !== "disabled") {
return mainPageRoutes[key];
}
}
return "/account";
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/components/PageGuard/PageGuard.tsx
Line: 38-45

Comment:
**`getFirstEnabledRoute` redirects to "notice" pages**

`getFirstEnabledRoute` skips pages whose status is `"disabled"`, but it also returns the first page with status `"notice"`. When a `PageGuard` redirects a user away from a disabled page (line 58), the redirect target could be a `"notice"` page, meaning the user is silently bounced to a notice screen rather than a functional page. The function name says "enabled" but its behaviour includes `"notice"`.

If all main pages are either `"disabled"` or `"notice"`, the user will keep landing on notices, never reaching a working page. Consider only returning routes whose status is strictly `"enabled"`, and falling back to `"notice"` pages as a second tier, or at least document the intended behaviour:

```suggestion
export function getFirstEnabledRoute(pages: PagesConfig): string {
  // First, prefer a fully enabled page
  for (const key of mainPageOrder) {
    if (pages[key].status === "enabled") {
      return mainPageRoutes[key];
    }
  }
  // Fall back to a notice page if no enabled page exists
  for (const key of mainPageOrder) {
    if (pages[key].status !== "disabled") {
      return mainPageRoutes[key];
    }
  }
  return "/account";
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 236 to 244
path="/session"
element={
<Suspense fallback={<PageLoader />}>
<SessionInfoPage />
<PageGuard pageKey="admin.session_info" pageTitle="Session Info">
<SessionInfoPage />
</PageGuard>
</Suspense>
}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Session route gated by an admin config key

The /session route lives inside the AppLayout group (main user-facing app), but it is guarded with an AdminPagesConfig field key via the "admin.*" prefix. getPageConfig resolves this correctly at runtime, but the coupling is architecturally surprising: a main-app route is controlled by a field that belongs to admin page configuration.

AdminSidebar.tsx also lists a "Session Info" nav item pointing at /session (not an /admin/... path), using a key from AdminPagesConfig. Reading either file in isolation, the route appears admin-scoped while actually being served under the user layout.

Consider either moving the route under /admin/session so it lives consistently in the admin layout, or adding a top-level field in PagesConfig for it to keep the main-app and admin separation clear.

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/routes/AppRoutes.tsx
Line: 236-244

Comment:
**Session route gated by an admin config key**

The `/session` route lives inside the `AppLayout` group (main user-facing app), but it is guarded with an `AdminPagesConfig` field key via the `"admin.*"` prefix. `getPageConfig` resolves this correctly at runtime, but the coupling is architecturally surprising: a main-app route is controlled by a field that belongs to admin page configuration.

`AdminSidebar.tsx` also lists a "Session Info" nav item pointing at `/session` (not an `/admin/...` path), using a key from `AdminPagesConfig`. Reading either file in isolation, the route appears admin-scoped while actually being served under the user layout.

Consider either moving the route under `/admin/session` so it lives consistently in the admin layout, or adding a top-level field in `PagesConfig` for it to keep the main-app and admin separation clear.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ScriptSmith ScriptSmith merged commit 0258558 into main Mar 18, 2026
20 checks passed
@ScriptSmith ScriptSmith deleted the page-config branch March 19, 2026 13:30
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