MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-1312

8.15 Switching org leaves stale old-org content in the page body (switchers refresh in place; no navigation)

Done
Description

Type: bug · Parent: Epic 8 (Launch readiness) · Discovered in: manual dogfooding (out-of-band, not a motir run)

Symptom

After switching the active organization (OrgControl), the page body still shows the previous org's content — the chrome (org name) updates but the main content does not. The same class affects the workspace switcher (the org switch also re-points the workspace cookie, so it cascades to project-scoped surfaces).

Root cause (verified against shipped code)

All three switchers update in place via router.refresh() only — they never navigate:

  • app/(authed)/_components/OrgControl.tsx:60-69handleSwitchOrgawait switchOrganizationAction(orgId); router.refresh();
  • app/(authed)/_components/WorkspaceSwitcher.tsx:34-44 — same shape.
  • app/(authed)/_components/ProjectSwitcher.tsx:45-55 — same shape.

switchOrganizationAction (app/(authed)/_actions.ts:102-130) sets the motir.org cookie, re-points the workspace_id cookie to the first workspace in the new org, and records that workspace's active project — so after the switch the active workspace/project are different.

router.refresh() re-runs Server Components only. Per this repo's own contract — motir-core/CLAUDE.md § "Page state after a mutation — server refresh vs client-island refetch" — it provably cannot update a client island that seeds useState(initialProps) at mount and has no re-seed trigger. Two consequences the switchers do not handle:

  1. Stale client-island bodies. Confirmed instance: DashboardGrid (app/(authed)/dashboard/_components/DashboardGrid.tsx:94-97) seeds useState(detail.name/access/layout/widgets) at mount and does not import useEffect (line 3) — there is no path to re-seed from a refreshed detail prop. So router.refresh() after a switch leaves the OLD org's dashboard on screen. (Contrast: the board AVOIDS this by watching activeProjectId as a fetch dep and refetching — BoardContainer.tsx:99-104,237-242 — proving the team already knows this class but applied the fix only to the board, not generally / not to the switch path.)
  2. Dead/old deep URLs. A switch keeps the user on the current URL. URL-scoped routes pointing at an old-org entity — /items/[key], /sprints/[id]/report, /dashboard/[dashboardId], project-settings sub-pages — no longer belong to the new active context after the cookie cascade, so a same-URL refresh shows mismatched/stale content or 404s instead of landing somewhere valid.

Server-rendered, identity-derived surfaces (e.g. /items via getActiveProject()) DO update on refresh — which is why the bug looks intermittent (depends on which page you're on when you switch).

Fix direction

On switch, navigate to a neutral default surface — the work-items list (/items) of the now-active project — instead of refreshing in place: a router.push('/items') abandons the stale deep URL AND remounts client islands so they re-seed from new-org props. When the user is already on that default surface, router.push to the same route is a no-op, so fall back to router.refresh() there. Apply the same post-switch navigation to the org and workspace switchers (the cascade makes a workspace switch equivalent for project-scoped surfaces); the project switcher already refetches the board but should adopt the same rule for the other client-island surfaces. This matches the established surface-routing contract in motir-core/CLAUDE.md (a full context switch is more invalidating than a single mutation — navigation, not in-place refresh, is the right tool).

Resolution

Open — the closing-out type: code subtask fills this (write a failing repro first: switch org while on the dashboard / a deep item URL → assert the body reflects the new org).