feat(settings): introduce dynamic-route shell + redirect /settings → /settings/account (#139)

* feat(settings): introduce dynamic-route shell + redirect /settings → /settings/account

Splits the 3218-LOC /settings monolith into a routing surface only — no
section content moves in this PR. Each of the eight slugs (account,
integrations, notifications, dashboard, ai, api, advanced, about) now lives
at its own /settings/[section] route and renders inside a new
<SettingsShell> sidebar+main layout. Every section currently shows a
placeholder ("Moving here with the next release update") so reviewers can
validate the URL structure in isolation. The historic monolith is preserved
verbatim as page.legacy.tsx for section-by-section extraction by the
follow-up PRs (A2-account, A2-about, A2-ai, A2-integrations,
A2-notifications, A2-rest).

Behaviour change: existing entry points (sidebar nav, topbar dropdown,
onboarding hand-off, dashboard checklist, Withings callback, Codex
callback) now point at /settings/<slug> instead of /settings#anchor. The
/settings root is a 308 (permanentRedirect) to /settings/account so old
bookmarks and external links keep working.

Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): route every Codex OAuth callback redirect to /settings/integrations

Review-pass H1: only the late success/failure paths in the Codex OAuth
callback were updated to point at the new section route. The four early
branches — rate-limited, provider-supplied `?error=...`, missing params,
invalid state — still redirected to `/settings?codex_error=...`.

After the section split that path 308-redirects to `/settings/account`,
where the Codex error toast/banner doesn't live. Users hitting any of
those edge cases would land on the wrong screen with the error code
silently lost.

All six branches now go to `/settings/integrations?codex_error=...`,
matching the success and token-exchange-failure paths and the existing
Withings callback shape.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 authored and GitHub committed 2026-05-08 14:15:15 +02:00
1 parent ddab92e42b
commit 3221f86eea
18 files changed
+3879 -3253

No files matched your search

@@ -83,11 +83,11 @@ describe("buildChecklist", () => {
it("attaches deep-link hrefs", () => {
const items = buildChecklist(inputs());
const hrefs = Object.fromEntries(items.map((i) => [i.id, i.href]));
expect(hrefs.profile).toBe("/settings#profile");
expect(hrefs.profile).toBe("/settings/account");
expect(hrefs.measurement).toBe("/measurements");
expect(hrefs.medication).toBe("/medications");
expect(hrefs.withings).toBe("/settings#withings");
expect(hrefs.notifications).toBe("/settings#notifications");
expect(hrefs.withings).toBe("/settings/integrations");
expect(hrefs.notifications).toBe("/settings/notifications");
});
});
+9 -4
View File
@@ -19,7 +19,12 @@ export type ChecklistItemId = (typeof CHECKLIST_ITEM_IDS)[number];
export interface ChecklistItem {
id: ChecklistItemId;
done: boolean;
/** Pre-built href the CTA should jump to. Hash anchors target settings sections. */
/**
* Pre-built href the CTA should jump to. The settings entries point at
* the v1.4 split routes under `/settings/[section]` (introduced by PR
* A2-shell), so the CTA lands directly on the right section instead of
* relying on a hash anchor on the legacy monolith.
*/
href: string;
/** True if the user explicitly hid this row. */
dismissed: boolean;
@@ -58,7 +63,7 @@ export function buildChecklist(inputs: ChecklistInputs): ChecklistItem[] {
{
id: "profile",
done: profileDone,
href: "/settings#profile",
href: "/settings/account",
dismissed: inputs.dismissedIds.has("profile"),
},
{
@@ -76,13 +81,13 @@ export function buildChecklist(inputs: ChecklistInputs): ChecklistItem[] {
{
id: "withings",
done: inputs.withingsConnected,
href: "/settings#withings",
href: "/settings/integrations",
dismissed: inputs.dismissedIds.has("withings"),
},
{
id: "notifications",
done: inputs.notificationsConfigured,
href: "/settings#notifications",
href: "/settings/notifications",
dismissed: inputs.dismissedIds.has("notifications"),
},
];