Repository navigation
Settings: an overview and three hubs, one home per feature - #55
Merged
Merged
Conversation
…ard does The route guard lets the administrator into a Settings hub by asking the auth context whether they hold settings.view, where a super administrator (or the * permission) holds everything. A page inside the hub then asked a different authority - useRBAC(), whose in-memory service creates no roles outside demo mode - and told the same administrator "you don't have permission to view workflow settings". - accessRules.ts holds the rule as pure functions (userHasPermission, isUserRowActive); authService uses them, so the guard and the pages cannot disagree. - useSettingsAccess() gives Settings pages can() and canAny() from the signed-in user. - The Workflows page uses it, and the five workflows.* permissions it checks are now declared by the module; nobody could hold them before.
…s what they are The setup wizard writes the first login with the schema's own columns (is_active, role, role_id). The Users tab read isActive and userType, which that row does not carry, so the administrator showed a blank status and a raw "super_admin"; the old Unified page read the same fields and called them Inactive. - userDisplay.ts reads a login row either way: Active or Inactive by the sign-in's own rule, the role as the named role it points at or the stored role in words, and whether the login administers the school. - Status is a badge and Role a readable name. Logins the page writes now carry is_active, so a login it creates is one the sign-in will accept. - The Roles grid keyed two columns "id" (ID and Actions), which React warned about on every render; Actions has its own key. The new test fails against the page as it was (all three cases) and passes now.
…se they read Both read the canonical database, and neither opened it. The conflict log reported "the canonical database is not open yet" and the integrity page "the data layer is not connected" unless some other page had opened the database first; the integrity page was routed with no adapter at all, so it could never connect. The end-to-end suite works around this by visiting a page that does open it. - useCanonicalAdapter() opens the database (once for the app, shared) and returns its adapter; adapter and error are both null while it opens, so a page can say it is waiting. - SyncConflictsPanel uses it, and says "loading" while the database opens and why if it cannot; a test can still hand it an adapter, and then nothing is opened. - DataIntegrityTab opens the database and runs the integrity checks against it as the signed-in user.
The three hubs each held the same code: a tab list, the active tab copied from ?tab= into state once, and a handler that wrote it back. Because it was copied once, a link to another tab of the hub you were already on changed the address and left the old tab open (seen in a real browser: hash-navigating from ?tab=settings to ?tab=unified showed the first tab). SettingsHub reads the tab from the address on every render, so such a link works in place; choosing a tab replaces the entry (the default tab leaves the address bare) and keeps the rest of the query. An unknown tab opens the default; an old tab name listed in movedTabs redirects to the page that holds it now. The tab body has no header of its own, and a banner can sit above the body of every tab.
Overview, School & People, Modules & Features and Data & System, in all four locales, ahead of the hubs that use them. French and Kiswahili are unreviewed drafts of four short labels and need a native speaker's check. Kinyarwanda is left in English, as the existing Kinyarwanda hub names are; it is not guessed.
…pecial cases of the router The router imported three hub pages by name and routed them, and repeated four of the module's legacy redirects. Both now live where every other module keeps them: the hubs in the module definition's `routes`, the redirects as its deprecated navigation entries. A hub is routable as soon as the module is registered, not only once navigation has loaded. The four router tests that mocked those special cases are removed. The coverage test, which registers the real module definitions, now covers the hubs: it failed while the router's explicit routes shadowed them, and passes once they are gone.
The automatic JSX runtime needs no React in scope, and the typecheck flags the import as unused.
One row of four tabs: school profile, users and roles, workflows, audit log. It is built from SettingsHub, so the address decides the tab, and the tab ids come from one shared list (hubTabs) that the page is typed against: a tab added to or removed from the list fails to compile until the page follows, and the redirect tests can check their targets against it. Gone from the hub: the Dashboard tab (the Settings overview is a page of its own), the Sync Conflicts tab (it moves to Data & System with the rest of sync; its old address is sent there) and Tab Settings (an empty placeholder whose component no longer exists). Each old address lands on a page that exists.
It is a tab of a hub that supplies the title, so a second title under the tab row was a duplicate. What the header carried stays, as a toolbar of the body: the line saying what the checks look for, and the "Check again" action.
getEnabledModules() returns module ids, but the report looked for an `id` on each of them and the graph tested `m.id`. Given a registry, every required dependency was reported missing and every module was drawn disabled. They now test the ids, and the version of a dependency is read from its metadata. The graph takes the registry as an optional argument, defaulting to the application's own, so a test can hand it one. Not changed: validateDependencies, which runs at registration, makes the same assumption about getEnabledModules(); it is left for a follow-up.
…n do The page had three tabs and the plugin manager backed one. Gone: the marketplace (a hard-coded list behind a button that flipped a flag), the policy form (saved to localStorage and never given to the plugin manager), the three-dots button and the README menu item (both only closed a menu), the Discussion button (no handler) and the README section (a placeholder). Kept, and made true: - Settings of a plugin are saved through the plugin manager when a switch, list or colour changes and when a text or number field is left. They were uncontrolled inputs that saved nothing, and read `default` where the schema says `defaultValue`. Kinds the form cannot edit (file, multiselect) are said so rather than drawn. - The access a plugin declares is listed from its privileges. The page read a field they do not have, so every plugin was reported as needing no special permissions. - The button that enables an installed plugin says Enable, not Install. - Switching a plugin, uninstalling it and changing its settings need settings.manage or plugins.manage; anyone else sees them read only. The page only checked the route before. usePlugins gains getPluginSettings and setPluginSetting, which pass through to the manager.
A row for each module the registry knows: running or not, what it cannot run without, what it can use when it is there, and which modules need it, with the problems the dependency check finds. It reads the registry and changes nothing; it says in a line that turning a module on or off for a school is not available yet. It replaces two pages. The module management page asked useRBAC() with no user, so it denied everyone, and its toggles wrote a localStorage configuration that nothing in the app reads. The dependency report called the validator without a registry and so always showed zero modules.
…ething Network status and module health in the header, the connection and sync notices, and the performance monitor. A switch writes only its own flag, and needs settings.manage; anyone else sees the switches but cannot change them. The old Feature Settings page had seven inner tabs and listed every flag in the configuration. Ten of those are read by nothing (enableAdvancedReporting, enableBulkOperations, enableDataExport, enableNotifications, enableAuditLog, enableMultiTenant, enableRealTimeSync, enablePerformanceMonitoring, syncStatusComponent, advancedErrorBoundaries), and the data layer and tabbed mode flags are not casual preferences. Its application mode switch, sync panel, conflict screen, performance dashboards and tooltip demo each duplicated something elsewhere.
One row of three tabs - modules, features, plugins - at /settings/modules, with its place in the sidebar and in the module's declared routes. The tab ids come from the shared list in hubTabs. The module management page and the dependency report page are gone (the Modules tab replaces both), and the old System hub no longer carries the modules, dependencies and plugins tabs. /settings/dependency-report, /settings/plugins and /settings/features now redirect into the new hub, and /settings/modules, which used to redirect out, is the hub itself.
One row of five tabs - data layer, sync, backup and restore, data integrity, system health - at /settings/system. The old System hub's modules, dependencies and plugins tabs went to Modules & Features in the previous commit, and their old ?tab= values are sent there. The Data Integrity page, which was its own sidebar entry, is a tab, and its old address redirects to it. Sync is one tab. It used to be an inner tab strip of the Data Layer page, with a second four-tab strip inside the sync panel and a third copy under Feature Settings. Its conflict log is the canonical one, and its settings (when, how often, retries, how a conflict is decided) are the ones that write a setting. The queue's pause, resume, delete and priority controls and the conflict Resolve button only logged a line, so they are not carried over. Browser storage is shown once. The hub had a permanent Browser storage panel above every tab and System Health had its own Storage card reading the same store. System Health keeps the card; the hub shows a warning only when the browser is not keeping the data durably (held in memory, unavailable, or persistence refused), with a link to the details. The Data Layer page lost its title and its inner strip, and the test that rendered a mock of it and asserted against the mock is gone.
…labels The list that names each hub's tabs now also holds the hub's title, its line of description and the label of each tab, with the translation key each is shown under. The three hub pages take all of it from there and keep only their icons and bodies, so the overview can list the hubs from the same place and a label cannot drift between the two. A tab added to or removed from the list still fails to compile in its hub until the page follows. No behaviour changes: the hub tests pass unchanged apart from reading the ids through tabIdsOf.
Everything it shows now comes from something real, and every link lands on a page that exists.
Four tiles: the logins that can sign in, the modules running out of the modules there are (and
whether any has a dependency problem), whether a school profile is set, and whether the browser keeps
the data durably. A "Needs attention" list says what is wrong and links to where to fix it: no
administrator can sign in, the browser storage is at risk, no school profile, modules with problems.
A card for each hub lists its tabs, linking to each, from the shared hub list. Recent activity is the
audit log's own latest entries.
Gone: a System Status that read "Active" whatever the installation ("All systems active"), four
invented recent-activity entries ("30 minutes ago"), student and staff counts that belong to
Administration, admins counted as role === 'admin' (a super administrator is not one), module counts
from a collection that does not say what runs, and links to /settings/unified (three), to
/settings/features, /settings/school-profile and /settings/audit-log (old addresses), and to
/settings/data-management, which never existed.
The storage rule is now one function, shared with the warning on the Data & System hub.
…laced Gone from Settings: the Configuration hub, Main Settings, Unified Settings, General Settings, Tab Settings and the old Feature Settings page. Each was a duplicate of something that now has one home, or never worked: - Unified Settings: its user, role and workflow management only said "not yet implemented". - The user permissions panel: showed "created successfully" and kept nothing. - The module management panel and its toggles: wrote a localStorage configuration nothing reads. - Tab Settings: an empty placeholder whose component no longer existed. - Feature Settings: seven inner tabs; its application mode switch, sync panel, conflict screen and tooltip demo each duplicated something elsewhere, and the root copy of the mode switch was an exact duplicate of the settings one that only its own test imported. The module definition is now the overview and three hubs, in that order. Fifteen old addresses stay, each redirecting to where its page lives now, including /settings/data-management, which the old dashboard linked to and which never existed. settingsNavigation.test.ts holds that to account: it checks that each redirect lands on a live page and, for a hub, on a tab that hub has; that no redirect leads to another; that every page named exists; and that every sidebar label is in all four languages. The barrels, the dead lazy exports in routeConfig and the resolver's explicit page map no longer name the deleted pages. Kept on purpose: the performance and memory dashboards (they were only reachable from the Feature Settings page, which no administrator could open) and MarketplaceDiscovery (a real client of the plugin marketplace service, simply not shown anywhere yet).
…holds A tab for /settings/school used to read "School", from the last segment of its address, and /settings/modules and /settings/system read "Modules" and "System". They now read School & People, Modules & Features and Data & System, whichever tab of the hub is open, and stay one workspace tab. The names apply to the Settings module only: /administration/school is still "School".
Every module dashboard logged "A props object containing a "key" prop is being spread into JSX": the shell spread each metric and each quick action, which carry their own key, into its card components. It now takes the key out and gives it to React directly, and the two cards say in their types that they do not receive one.
…ependency The cycle detector built its graph from required and optional dependencies together. The shipped modules are laid out with administration as the root: it optionally integrates with finance and HR, and finance, HR and the rest require it. Every healthy install therefore reported nine circular dependencies on administration, which showed in a real browser as "1 module with problems" and would have made the Settings overview raise a permanent warning. Only a cycle made of required links can stop modules from loading: an optional link is used when the module is there and done without when it is not. The graph is now built from required dependencies only. A cycle of required links is still reported, and the real registry now reports no problems.
… writes it The first real-browser run showed "School profile: Not set" on a school the setup wizard had just configured. The overview read `schoolProfile` from app data, which does not carry it; the profile is the school_details document (what the School Profile tab edits), read with useDatabaseDocument. The overview now waits for that document before judging, instead of calling a profile that is still loading "not set". Also: one login reads "1 account" and one module with a problem reads "1 with a problem", not "1 accounts" and "1 with problems".
The real-browser run showed the overview and all three hubs reading "Settings" in the workspace tab bar
after a page reload. A reload rebuilds the tab list from the address alone, and the sync engine names the
tabs with a stand-in translator that read every navigation key as its first segment:
navigation:settings.schoolPeople became "Settings". The live tabs were right (the real translator names
them), which is why the unit tests never saw it.
The engine's translator now answers only for a module's title (navigation:finance.title) and hands any
other navigation key back, so the deriver names the page itself. The deriver falls back to the hub's own
title from hubTabs when there is no translation to look the key up in, instead of the last word of the
address ("School", "Modules", "System").
… tab The layout mirrors the router into the tab bar by opening a tab for any address that has none. It does that for the address the router is about to leave, so every old address that redirects (the fifteen old Settings addresses and every deprecated address of the other modules) left a tab named for itself behind, "Unified", "General", "Configuration", beside the tab for where it landed. A real-browser run showed one visit to each old Settings address filling the bar to its twenty-tab limit, and at the limit the oldest real tab is closed to make room. resolveRedirect answers from the two places the router builds its redirect routes from, in the router's order (a deprecated navigation item that names its page's new home, then the shared map). The mirror skips an address it resolves; the tab for where the router lands opens when it gets there.
…fers
The popup listed every child of the Settings module, deprecated ones included, so after the hubs it
offered the four entries and also the fifteen old addresses that only redirect ("Configuration",
"Unified", "General", "Features", ...): the same feature under several names, which is what the hubs
exist to end. The sidebars already hide a deprecated item; the popup now does too.
Both tables of the Workflows tab (the workflows and the states of one) gave their Actions column the key of the ID column, so React logged "two children with the same key, id" each time the tab was drawn. The real-browser run caught it in its last check, which watches the console. The Actions column now has a key of its own; a test draws both tables and reads the console.
DataLayerSettingsPage.test.tsx imported backendArchitecture and never used the binding (the vi.mock of the same module is independent of it). The import was already unused before the Settings hubs; the file is in this change, so the gate's ESLint check on changed files reports it.
…hat outlives the page After a sign-in the login page scheduled navigate(redirectPath) one second later and never cancelled it. The page is gone as soon as the sign-in is seen (an effect already sends the visitor to the dashboard), but the timer still fired a second later and sent a visitor who had meanwhile clicked into a module back to the dashboard. The real-browser Settings spec hit it on a fast machine: it clicked Settings within the second, the router landed on /settings, and half a second later the stale timer took it back. The redirect now happens straight away and honours the page that sent the visitor to sign in, in the effect and in both handlers; no timer is left to fire later. Tests: a sign-in redirects without a timer, honours the originating page, and does not pull a visitor who has moved on back to the dashboard.
…dering again does not remount the page The route table called React.lazy while it rendered, for every page, every time. Each render therefore gave every page a new component type, and React unmounted the page on screen and mounted it again whenever the navigation reloaded (a module loading is enough). A page loses what was typed into it when that happens, and a page whose job is an effect never gets to run it: a Settings hub redirecting a moved tab rendered, ran its layout effect, and never ran the passive effect that navigates, so the address stayed put and the body stayed blank. A real browser reproduced it every time (3 stalls in 3 rounds of visiting a moved tab straight after another) and showed the layout effect running with the passive one missing. With one lazy component per page loader (lazyPage, a WeakMap on the loader) the same loop of 30 rounds of 4 moved tabs, 120 redirects, had no stall. getPageComponent and the dashboard and module-route lookups in DynamicRoutes use lazyPage. Tests: the same loader gives the same component, getPageComponent returns the same one on every ask, a page stays mounted while the routes render again, and a control shows a component made anew on every render does remount it.
Runs on demand (it is not in CI's golden-path list): pnpm exec playwright test tests/e2e/settings-hubs.spec.ts One real-browser journey on a fresh SQLite-WASM database, signed in as the first administrator the setup wizard made: the sidebar offers the overview and three hubs and none of the old entries; the header's Settings popup offers the same four; the overview's figures are true (1 account, every module running with no problems, the profile set) and every link on it goes to a hub or one of its tabs; each hub has one row of tabs and every tab opens with its own content for the administrator; a feature switch survives a reload; the 22 addresses the old Settings answered (and the moved ?tab= values) land where their page lives now, the moved tabs again and again; a link to another tab of the hub you are on switches in place; a full load of an old address lands too; the workspace tabs are the dashboard, the overview and the three hubs, named for what they hold, after a reload, with no tab left for an old address; a hub with five tabs does not push a phone-width page sideways; and the console shows no page error and no React warning. It found seven faults the unit tests had not (see the commits above it), including the two that made it flake: a sign-in timer that outlived the login page, and a route table that remounted every page when it rendered.
What was done in the 34 commits since consolidation, the eight faults the real-browser spec found that the unit tests had not, the final task gate (GATE PASSED, 8 checks), what was not verified, the follow-ups, and the exact file list. States plainly that no pull request was opened or merged: this machine has no GitHub login, and nothing in the harness opens or merges one.
…add the four sidebar labels to pt and ar Both sides edited fr and sw navigation.json: the Settings branch added labels for its four entries, the locale work completed the catalogue. The merge keeps the translated values and the four labels, and adds them to pt and ar too, which the locale completeness check then requires.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on the pull request for
prd/ui-brand-home-module-names: until that merges, this shows its four commits too. Merge that one first, with a merge commit.Settings was five sidebar entries and 16 hub tabs, with nested tab strips, the same feature in several places, controls that did nothing and pages that refused the administrator. It is now an overview and three hubs, each with one row of tabs: School & People, Modules & Features, Data & System. Every old address redirects.
It also fixes eight faults a real-browser run found that the unit tests had not, three of them in the app outside Settings: a sign-in timer that sent a quick visitor back to the dashboard, a route table that remounted every page whenever navigation reloaded (a moved Settings tab never redirected), and old addresses leaving a stray workspace tab each.
Verified: task gate GATE PASSED on the head (tsc 2252/2500, 898 related tests, lint, secrets); the browser spec
tests/e2e/settings-hubs.spec.tspasses. Not verified: fr and sw labels are unreviewed drafts, rw is English; the hubs' own labels are not in the catalogue yet.Full account:
docs/prd/workflow-log/UI-brand-and-settings-hubs-2026-10-26.md.