Skip to content

Settings Page Architecture

/settings is a React surface inside the SPA, not a server-rendered page. Every tab is a child route of one layout, so revisiting a tab is a state change rather than a request.

Layout

SettingsRoute (campuscore_app/web/src/routes/SettingsRoute.tsx) replaces the app shell rather than nesting inside it. Rendered inside the shell, the settings nav became a third column beside the chat sidebar; "‹ Back to app" returns to chat.

The route owns one content column, and owns it alone:

<section className={`mx-auto w-full ${COLUMN_CLASS[activeColumn]} px-4 py-8`}>
  <Outlet />
</section>

A page must not declare a width, centring, or page padding of its own. Five tabs once did, nesting a second identical <section> and paying the padding twice, which left them 32px lower and 32px narrower than their neighbours. SettingsShell supplies the header and vertical rhythm; the column is the route's.

Width follows content, and a tab names the column it needs rather than setting a boolean. A boolean could not describe the knowledge tab, which holds a list and a table.

Column Width For
form (the default) 768px Label-and-input pages. Stretched wider, hint text runs long and a three-field card looks half-empty.
list 1024px A list of rows: Connectors, Workspace Connectors, Knowledge Folders, SSO, Roles, Feature Management.
table 1152px A dense table needing columns: User Management, Analytics, Audit Log.

One logical surface gets one width. The knowledge folder list and detail are both list, in the settings tab and on the /knowledge route, so moving between them does not resize the page.

On mobile the sidebar collapses to a horizontally scrolling strip of the same tab links.

The tabs

SETTINGS_TABS in campuscore_app/web/src/routes/settingsTabs.tsx is the single declaration of what exists. Each entry carries a slug, label, group, icon, its element, and its gates.

Group Tab Gate
Account My Account none
Account Connectors none
Workspace General & Branding workspace admin
Workspace Sample Questions workspace admin
Workspace User Management workspace admin
Workspace Knowledge Folders workspace admin + cc_knowledge_folders
Workspace Workspace Connectors workspace admin
Workspace Single Sign-On workspace admin
Workspace Roles & Permissions workspace admin
Workspace Feature Management workspace admin
Workspace Custom Domain workspace admin
Workspace Analytics workspace admin
Workspace Audit Log workspace admin

General & Branding edits the institution's name, assistant identity, logo, login background image (minimum 1920x1080, see Branding & Theming), and the two brand colours - which style only the sign-in page and the sidebar's initials badge, never app controls.

Analytics and Audit Log are deliberately not behind a feature flag: usage visibility and audit visibility are accountability surfaces and must not be silently switched off. Analytics is the only lazily imported tab, because it carries the Recharts chunk that must not ride in the bundle every chat user downloads.

visibleSettingsTabs({ canManageWorkspace, features }) decides what a given user is offered. It gates on the server's can_manage_workspace answer and the server's feature list - never on a role slug spelled in the client, which is how nine tabs once became invisible to the administrator who owned them.

Where the pieces live

Settings-specific pieces live in components/settings/:

  • SettingsPanel.tsx - SettingsHeader (title, subtitle, optional action), SettingsPanel (the card), SettingsShell (header plus content).
  • primitives.tsx - ServiceAvatar, Tag.

App-wide pieces live in components/ui/ and are used by the knowledge routes too:

  • Button.tsx - Button, ButtonLink, buttonClass. One primary, one secondary, one danger. No other file may declare a brand-filled button of its own (bg-brand still tints avatars and progress fills, which are not buttons); --color-brand is a per-university runtime value, so hover has to be expressed once, centrally.
  • StatusPill.tsx - tones named for meaning (success, warning, danger, neutral, info, brand), not for colour.
  • Toggle.tsx - the on/off switch. Emerald, not brand, so a client with a red brand does not get an "on" state that reads as an error.
  • DataTable.tsx, FilterBar.tsx, Drawer.tsx - the control bar, dense row surface and detail panel the data-heavy tabs share.
  • StatCards.tsx, EmptyState.tsx, Modal.tsx, ConfirmDialog.tsx, SearchInput.tsx, Spinner.tsx, and the dropdown family Popover.tsx/Select.tsx/Menu.tsx (SPA design system).

DataTable has no sorting on purpose. Every list behind it is keyset-paginated, so ordering the loaded pages alone would present a partial view as though it were the whole one. Ordering is the server's.

The API behind it

Each tab reads a typed endpoint module under campuscore_app/apps/main_app/apis/, wrapped by the @api_endpoint decorator and reached through a hook in campuscore_app/web/src/lib/api/.

Tab Endpoint module
My Account account_api.py
Connectors connectors_api.py
General & Branding workspace_branding_api.py
Sample Questions workspace_faqs_api.py
User Management workspace_users_api.py
Knowledge Folders workspace_folders_api.py
Workspace Connectors workspace_connectors_api.py
Single Sign-On workspace_sso_api.py
Roles & Permissions workspace_roles_api.py (the catalog read plus rename, review, and mapping-CRUD routes)
Feature Management workspace_features_api.py
Custom Domain workspace_domains_api.py
Analytics analytics_api.py
Audit Log audit_log_api.py

Authorization is the decorator's, not the component's. Every workspace endpoint declares auth="workspace_admin", so a non-admin who somehow reached one of these components would see empty lists and 403s rather than data.

See spa-and-api-framework.md for the decorator, the generated TypeScript contract, and the rule that a Pydantic model change becomes a TypeScript compile error.

Knowledge folders

The knowledge folder surfaces are shared components rendered in two places, because they exist both as a personal route (/knowledge) and as a workspace admin tab.

  • components/knowledge/FolderListPanel.tsx backs both folder lists.
  • components/knowledge/FolderDetail.tsx backs both folder detail views.

The two mount points differ by a FolderSource: which endpoints to read, and how to word a deletion warning. They previously existed as two independent implementations and had already drifted on loading state and delete copy.