refactor(settings/service): split into URL-routed sub-routes with side-nav layout #829
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!829
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/settings-service-subroutes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Picks up the "rethink relation between settings page and its subpages" feedback from PR #827. Implements
specs/settings-service-subroutes.mdend-to-end.Operator complaints addressed:
max-w-220(matches sister settings pages: labels / repos / secrets)./settings/hub already had a card; subtitle updated to drop the dead "auth, node flows" copy left over from the pre-PR-A tab list./settings/service/{container,watchdogs,design}) routed by TanStack Router file-based routing. Bookmarkable, browser back navigates between sections.New layout
The layout owns the shared
service-config-settingsquery, the save / reset / delete-all mutations, the destructive-fields confirm dialog (today: onlycontainer_image_default), and the scope chips. Section forms read fromuseServiceConfig()so they don't need to thread props through.File map
apps/web/src/lib/service-config-context.tsxServiceConfigProvider+useServiceConfig()hook +ScopeIdtypeapps/web/src/components/settings-side-nav.tsx<Link>activeProps)apps/web/src/features/service-config/primitives.tsxINPUT_CLS,<Field>,<Toolbar>extractedapps/web/src/features/service-config/{container,watchdogs,design}-section.tsxapps/web/src/routes/settings.service.tsx<Outlet>+ ServiceConfigProviderapps/web/src/routes/settings.service.index.tsxbeforeLoadredirect →/settings/service/containerapps/web/src/routes/settings.service.{container,watchdogs,design}.tsxapps/web/src/routes/settings.index.tsxapps/web/src/routeTree.gen.tsTest plan
just qa— 3135/3135 server tests + web typecheck greenjust restart— service running, SPA bundle rebuilt/app/settings/service→ redirects to/container. Click each side-nav item → URL changes + content swaps. Browser back navigates between sections./app/settings/service/designdirectly → renders Design section without going through the redirect.Follow-up
PR D (
specs/service-config-url-consolidation.md) — surface per-forge OAuth client/secret editors. The Forge sub-route comes back assettings.service.forge.tsx, drops into the side-nav, hosts three forge cards (Forgejo / GitLab / GitHub) with URL + client_id + client_secret editor each.🤖 Generated with Claude Code
Fieldlabel association):htmlFor={id}inapps/web/src/features/service-config/primitives.tsxpoints at the wrapper<div id={id}>, not the<input>inside it. Clicking the label does not focus the input; screen readers announce the wrong target. Fix: passidas a prop fromFielddown to the child input (or expose it via a render-prop /cloneElement), or drop the generated id and require callers to wire their ownid+htmlFor. The<input>elements already havearia-labelso a11y is not broken, but the brokenhtmlForis still misleading and non-compliant.ServiceConfigProvidermemo):useMemoinapps/web/src/lib/service-config-context.tsxdepends on[value]but the call site insettings.service.tsxpasses an inline object literal, sovalueis always a new reference every render and the memo never short-circuits. Not behavior-breaking, but the abstraction is broken. Fix: destructure the individual fields in the dependency array, or stabilise the object withuseMemoat the call site.Reviewer flagged two issues: 1. **`Field` `htmlFor={id}` pointed at the wrapper `<div>`, not the input.** Click-on-label didn't focus the control; screen readers announced the wrong target. Switched `Field` to a render-prop: `children` is now `(id: string) => ReactNode` and every caller applies the id to the actual `<input>`. The wrapper `<div id={id}>` is gone, so the `<label htmlFor>` now targets the real control. Side-effect: dropped the now-redundant `aria-label` on each input (the `<label>` does the labelling). 2. **`ServiceConfigProvider` `useMemo([value])` busted every render.** The deps array referenced an inline-object literal, so the memo never short-circuited. Refactored: - Provider now takes individual props (spread at the call site) instead of a single `value` object. - `useMemo` deps list the underlying primitives so the memo only fires when one of them actually changes. - `requestSave` / `requestReset` wrapped in `useCallback` in the layout so their identities are stable across renders (mutation objects are stable, so empty-dep callbacks are safe). Affected files: - `apps/web/src/features/service-config/primitives.tsx` — `Field` becomes render-prop. - `apps/web/src/features/service-config/{container,watchdogs,design}-section.tsx` — every `<Field>{<input>}</Field>` rewritten as `<Field>{(id) => <input id={id} />}</Field>`. - `apps/web/src/lib/service-config-context.tsx` — provider props + proper memo deps. - `apps/web/src/routes/settings.service.tsx` — request callbacks via `useCallback`; provider invocation switches from `value={…}` to spread. `just qa` green (3135/3135 server tests + web typecheck). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>