Skip to content

UN-4290 [FEAT] Add the v1 account menu and organisation switcher - #2395

Open
athul-rs wants to merge 70 commits into
UN-4286-plugin-manifestfrom
UN-4290-v1-account-menu
Open

athul-rs wants to merge 70 commits into
UN-4286-plugin-manifestfrom
UN-4290-v1-account-menu

Conversation

@athul-rs

@athul-rs athul-rs commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

  • The v1 account menu and organisation switcher (P2-T02, pages 01 and 19).
  • Account menu: name and e-mail, Profile & preferences, Help & docs, Sign out, and a account-menu.plan-billing slot that cloud fills.
  • Organisation switcher: the user's organisations with role, member count and region (from PB-T15), search, and a confirm step before switching.

Why

How

  • Sign out calls POST /logout, resets analytics and opens the URL the backend returns (the identity provider's logout). If the POST fails it falls back to the GET logout, which still ends the session.
  • A user with no name sees the e-mail once, as the title line.
  • lib/http.ts is a small axios client with the CSRF header. It is a stand-in for P0-T05 (UN-4313, the shared HTTP client) and is replaced when that lands.
  • Styling uses v0's CSS variables for now and moves to the P0-T04 tokens (and their #v1-portal container) when those land.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. All files are new and under frontend/src/v1/; nothing in v0 imports them, and no route renders them until the v1 shell (P2-T01) mounts them.

Database Migrations

  • None.

Env Config

  • None.

Relevant Docs

  • Design: frames NEW09 (account menu), O01 to O04 (switcher, confirm, single organisation, member), designs repo at 83e4fff.
  • Deferred, with owners: create organisation (the plan rule that forbids it is undecided); avatar photo; region labels (codes are shown as the backend sends them).

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

Screenshots

To add from the dev run.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

athul-rs and others added 22 commits October 27, 2026 18:05
P2-T02 (pages 01 and 19). The account menu from the avatar: identity,
Profile & preferences, the account-menu.plan-billing slot for cloud,
Help & docs and Sign out. Sign out POSTs /api/v1/logout with the CSRF
token, resets analytics and opens the returned URL; if the POST fails it
falls back to the GET logout. No "Switch to old version" item (D14).

The organisation switcher from the pill: search, a row per organisation
with role, member count and region (absent fields left out), the current
one marked, a confirmation before switching, the one-organisation and
member states, and a load error with retry. After a switch the app does a
full load of the same section in the new organisation, else its
dashboard. Create organisation is left for a decision (plan gap 6).

lib/http.ts is the shared client: session cookie, X-CSRFToken on unsafe
methods. Invisible until the shell (P2-T01) renders these components.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… has no name

Found on the dev preview: a user without a name got an empty title line
above the e-mail.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…i 5.2 both declare containSubset

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… per-browser preference

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… match the v1 Tailwind root

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…heme mapping and the focus utility

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…so biome ci passes

src/v1/styles/tokens.css is written by scripts/build-v1-tokens.mjs and checked byte-for-byte by styles/tokens.test.ts; the formatter would rewrap it and break the check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e and guard the CSRF cookie

signOut now throws when the reply has no redirect URL so the GET logout
fallback runs. signOutAndLeave in services/session.ts owns the whole
sequence (POST, analytics reset, navigate, fallback) and AccountMenu calls
it. A malformed CSRF cookie sends no header instead of throwing. The
account trigger shows one letter, and the Help & docs link announces that
it opens in a new tab.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A switch in flight can no longer be cancelled, a 412 from the list sends the
user to sign-in, an empty search says so, the dialog's from side comes from
the current organisation, and overlapping list loads keep only the latest.
The loading text is a live region and the current row carries aria-current.
The Switch button gets the arrows icon, and avatars show two letters for a
one-word name while the pill shows one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…sts and on missing variants

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…line styles, CSS imports and !important fail lint

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with one remaining non-blocking issue: reopening the switcher does not refresh memberships.

Fix All in Claude CodeFindings

  1. P2 New organisations stay hidden ▶
Fix with agent prompt
### Issue 1
frontend/src/v1/shell/OrganizationSwitcher.tsx:288-289
Once the list loads, reopening the switcher never refreshes it because `state.status` stays `"ready"`. If someone adds the user to another organisation while this page stays open, it will not appear until a full page reload. Refresh the list when the switcher reopens, or give the cached list an expiry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds the v1 account menu, organisation search and switch confirmation, and sign-out services.

  • The account menu shows account details and signs users out.
  • The organisation switcher searches, describes, and changes organisations.

Reviews (16) · Last reviewed commit: "UN-4290 [FIX] Address review: list shape..." · Reviewed by Greptile

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 9, 2026
@greptile-apps
greptile-apps Bot dismissed their stale review November 9, 2026 19:15

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 9, 2026
The dev-spec board sets shadow-md for dropdown menus and shadow-lg for
popovers. The account menu is a dropdown menu.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps
greptile-apps Bot dismissed their stale review November 10, 2026 07:58

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 10, 2026
athul-rs added a commit that referenced this pull request Nov 10, 2026

@ritwik-g ritwik-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Athul. This is close. Two things need fixing before merge: the README merge loss (#1) and the 412 handling (#2). The rest are small.

The account menu now matches the #2466 select panel exactly: shadow-md, rounded-lg, border, bg-popover and p-1.5. The organisation switcher panel and the confirm dialog do not follow the same scale yet (#7).

On the spec:

  • Leaving out "Create organisation" matches the later Figma note on page 19: "The product cannot create an organisation, so the dropdown does not offer it." The spec page still lists it, so it's worth asking Hari to update it.
  • The E2E acceptance item (switch and sign out) has to land with the PR that mounts these components (P2-T01).

Ran locally: typecheck:v1 and lint:v1 are clean, and the 65 tests in this PR pass.

Comment thread frontend/src/v1/README.md Resolved
Comment thread frontend/src/v1/services/session.ts Resolved
athul-rs and others added 2 commits November 11, 2026 10:57
…2, return focus, handle a removed organisation

- Restore the v1 README's Plugins section, lost in the #2394 merges.
- A 412 with a code goes to /error?code=&domain= as v0 does; an empty body still goes to /landing.
- Closing the confirm dialog puts focus back on the organisation pill.
- A 403 on switch says access was removed, keeps Switch disabled and reloads the list; other failures are logged.
- Loading and load errors use Skeleton and InlineAlert; the dialog error uses InlineAlert.
- Switcher panel shadow-md (dropdown), confirm dialog shadow-xl (dialog).
- Log when sign-out falls back to the GET logout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps
greptile-apps Bot dismissed their stale review November 11, 2026 09:50

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@athul-rs

Copy link
Copy Markdown
Contributor Author

Review body: asked Hari separately about dropping "Create organisation" from the P2-T02 page (draft below). E2E for switch and sign out lands with P2-T01.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 11, 2026

@ritwik-g ritwik-g left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All 8 points are fixed in cacc2e2, and the tests now catch each one. Approving. Two small non-blocking follow-ups on the new 403 path are inline.

athul-rs and others added 2 commits November 11, 2026 16:00
…reload

The switch dialog captures the current organisation when Switch is clicked,
so the list reload after a 403 cannot blank its role and member count. A
test rejects with an axios 500 to pin the 403-only branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps
greptile-apps Bot dismissed their stale review November 11, 2026 17:35

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

greptile-apps[bot]
greptile-apps Bot previously approved these changes Nov 11, 2026

@vishnuszipstack vishnuszipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR 2395 — UN-4290 Add the v1 account menu and organisation switcher

Mode: INITIAL, at the PR's current head. Reviewed head 85797a2d116bdc24f74c7e7544fac2f94ec6016e, merge base 5cc28d787200f53196f14c238ef894310dbffb39, base freshened (base_fetch: ok), 10 files, 1535 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
How this was done: a full fan-out (code reviewer, silent-failure hunter, type design, comments, lifecycle and a whole-diff cold read) ran at an earlier head a29fc096a. Before posting, every finding was re-checked against this head. Findings already fixed, already raised by another reviewer, or wrong on a second read were dropped (14 dropped: 9 already raised by ritwik-g and fixed in cacc2e2, 1 fixed, 4 withdrawn). The author's own commits since a29fc096a were reviewed for new defects, and the tests were re-run here.
Security deep-dive: degraded — the vendored security prompt ran once at this head, its filter step inline instead of as independent passes; no findings at confidence >= 8.

Verdict — APPROVE WITH NITS

Summary — Critical: 0 · High: 0 · Medium: 1 · Low: 7 · Lenses run: 19/19 (1 not covered, 6 N/A; see checklist).

The earlier rounds' fixes hold at this head, and cacc2e2 and f37d0d9 add no new defects. One robustness gap is left: the organisation list reply is trusted without a shape check, and a wrong body takes the top bar down instead of showing the alert. The rest are Lows: v1 conventions (icon roles, keeping URLs and status codes in services/), two edge cases in the panel copy, and a test that cannot fail. Tests at this head, from frontend/: the shell, organisation and service tests pass (4 files, 70/70); typecheck:v1 and lint:v1 pass; biome ci is clean on the 10 changed files. The full test:v1 suite and E2E were not run, because nothing mounts these components in a route yet.

Findings

Posted inline on the diff; index:

    1. [Medium] [Lens 3, 8] — A 200 reply without an organizations array crashes the top bar — frontend/src/v1/services/organizations.ts:12
    1. [Low] [Lens 8] — The shared client has no timeout, so a hung switch locks the dialog — frontend/src/v1/lib/http.ts:27
    1. [Low] [Lens 4] — The CSRF header is attached whatever the request's origin — frontend/src/v1/lib/http.ts:29
    1. [Low] [Lens 13] — The "outside the current organisation's paths" test never reaches the org check — frontend/src/v1/lib/organizations.test.ts:104
    1. [Low] [Lens 2] — The shell imports Lucide glyphs directly instead of by role from lib/icons.ts — frontend/src/v1/shell/OrganizationSwitcher.tsx:21
    1. [Low] [Lens 3] — The panel contradicts itself when the current organisation is not in the list — frontend/src/v1/shell/OrganizationSwitcher.tsx:345
    1. [Low] [Lens 7, 16] — Region codes are shown raw, but the backend contract says the client maps them — frontend/src/v1/lib/organizations.ts:36
    1. [Low] [Lens 2] — URL and HTTP-status handling sit in shell/, not services/ — frontend/src/v1/shell/OrganizationSwitcher.tsx:84

Lens checklist

  1. Spec & intent — Clean
  2. Architectural fit & precedent — See findings #5, #8
  3. Correctness & edge cases — See findings #1, #6
  4. Security — See findings #3
  5. Data integrity & migrations — N/A — no persisted data or schema
  6. Concurrency — Clean
  7. API & contract compatibility — See findings #7
  8. Reliability & resilience — See findings #1, #2
  9. Performance & cost — Clean
  10. Observability — Clean
  11. Operational safety — N/A — the components are not mounted in any route yet; no flag or rollout config
  12. LLM/agent-specific — N/A — no model, prompt or agent path
  13. Testing — See findings #4
  14. Dependencies & build — N/A — no dependency or lockfile change
  15. Code quality — Clean
  16. Doc & comment accuracy — See findings #7
  17. Cross-document rule consistency — Not covered — merge-result sweep unavailable: git 2.34 here, the script needs 2.38
  18. On-prem deployment experience — N/A — no chart, values or on-prem build path touched
  19. Scaling & workload lifecycle — N/A — no worker or scaling config

Comment on lines +12 to +17
export async function listOrganizations(): Promise<Organization[]> {
const { data } = await http.get<{ organizations: Organization[] }>(
"/api/v1/organization",
);
return data.organizations;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. [Medium] [Lens 3, 8] — A 200 reply without an organizations array crashes the top bar

  • Location: frontend/src/v1/services/organizations.ts:L12-L17
  • listOrganizations returns data.organizations without checking it. A 200 with some other body resolves to undefined. That can come from a proxy or SPA fallback serving index.html, or from a backend shape change. The switcher then stores {status: "ready", organizations: undefined}, and organizations.filter(...) at OrganizationSwitcher.tsx:L270 throws during render. The top bar goes down when it should show the "Could not load your organisations." alert with Retry. signOut in this same PR already validates its reply shape, so the convention is there. Fix: if (!Array.isArray(data?.organizations)) throw new Error("Organization list reply carried no organizations");. Add a service test that a {} body rejects.
  • Verification: VERIFIED — read at this head: listOrganizations returns data.organizations unchecked and OrganizationSwitcher.tsx:L270 calls .filter on it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05f055a4: listOrganizations now throws "Organization list reply carried no organizations" when the body has no array, so the switcher shows its load error with Retry. Service tests cover {}, {organizations: null} and an HTML body, and a switcher test checks the alert and Retry.

* than through axios's xsrf options, which only the browser adapter applies.
* ponytail: no 401 or error-toast handling yet; add it when a page needs it.
*/
export const http = axios.create({ withCredentials: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2. [Low] [Lens 8] — The shared client has no timeout, so a hung switch locks the dialog

  • Location: frontend/src/v1/lib/http.ts:L27
  • axios.create({ withCredentials: true }) leaves timeout at 0, which means no client-side limit. While POST /organization/<id>/set is pending, ConfirmSwitch has busy set. Cancel is disabled, and Escape and overlay clicks are ignored (OrganizationSwitcher.tsx:L174-L176, L227). If the request never settles, only a reload gets the user out. Sign-out is similar: the GET fallback in signOutAndLeave runs only on rejection, so a hung POST makes "Sign out" look dead. Any upstream proxy timeout would turn this into a 504, so the impact depends on the deployment. A default timeout of 15 to 30 s on http turns a hang into a rejection, and both paths already handle that.
  • Verification: UNVERIFIED — read only; does any proxy in front of these calls impose its own timeout?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left as is in this PR. UN-4313 replaces lib/http.ts with the shared client in services/http.ts right after this merges, and its owner asked that nothing new build on this file. The timeout is passed to that ticket's owner to carry into the shared client.

Comment on lines +29 to +38
http.interceptors.request.use((config) => {
const token = cookie("csrftoken");
if (
token !== undefined &&
!SAFE_METHODS.has((config.method ?? "get").toLowerCase())
) {
config.headers.set("X-CSRFToken", token);
}
return config;
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3. [Low] [Lens 4] — The CSRF header is attached whatever the request's origin

  • Location: frontend/src/v1/lib/http.ts:L29-L38
  • The interceptor adds X-CSRFToken to every unsafe request and checks only the method. The comment says axios's xsrf options were bypassed on purpose, but those options also apply a same-origin check, and this replacement drops it. Every current caller uses a relative URL, so nothing leaks today. This is documented as "the one HTTP client v1 services use", though. The first service that POSTs to an absolute third-party URL through it will send the Django CSRF token to that host. Skip the header unless new URL(config.url ?? "", config.baseURL ?? window.location.origin).origin === window.location.origin, and add a test that a cross-origin POST carries no header.
  • Verification: VERIFIED — read at this head; no current caller is cross-origin

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left as is in this PR for the same reason: the shared client from UN-4313 replaces lib/http.ts and already owns CSRF handling. The same-origin check, with its cross-origin test, is passed to that ticket's owner.

Comment on lines +104 to +108
test("falls back to the dashboard outside the current organisation's paths", () => {
expect(landingPath("/landing", "zipstack", "tanushree")).toBe(
"/tanushree/dashboard",
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4. [Low] [Lens 13] — The "outside the current organisation's paths" test never reaches the org check

  • Location: frontend/src/v1/lib/organizations.test.ts:L104-L108
  • The only input is /landing, which has one segment, so section is undefined and the result is the dashboard either way. Delete org === fromOrg && from landingPath (lib/organizations.ts:L78) and the suite still passes. That was mutation-verified at the old head, and the test is unchanged at the new one. A switch started from /<other>/<sub> would then land on /<toOrg>/<sub>. Add landingPath("/otherorg/workflows", "zipstack", "tanushree") -> "/tanushree/dashboard". The Help link has a similar gap: the AccountMenu test checks only href. If target="_blank" (AccountMenu.tsx:L34) were lost, the sr-only "(opens in a new tab)" text would be wrong and no test would fail.
  • Verification: VERIFIED — mutation run at the earlier head (removing org === fromOrg && stays green); the test is unchanged here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05f055a4: added landingPath("/otherorg/workflows", "zipstack", "tanushree") -> /tanushree/dashboard, which fails with org === fromOrg && removed. The AccountMenu test now asserts the Help link's target="_blank" and rel="noreferrer" next to the sr-only text, and fails if they are dropped.

Comment on lines +21 to +26
import {
ArrowRightLeft,
ChevronRight,
ChevronsUpDown,
Search,
} from "lucide-react";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5. [Low] [Lens 2] — The shell imports Lucide glyphs directly instead of by role from lib/icons.ts

  • Location: frontend/src/v1/shell/OrganizationSwitcher.tsx:L21-L26
  • The v1 README says "Shared glyphs come by role from lib/icons.ts". SHELL_ICONS already defines orgSwitcher (ChevronsUpDown), search (Search), accountMenu (ChevronDown) and signOut (LogOut), and ACTION_ICONS has next (ChevronRight) and openExternal (ExternalLink). Both shell files import those glyphs from lucide-react directly (also AccountMenu.tsx:L5-L11). check-v1-icons does not catch this. These are the first shell components, so a glyph swap in the role map will skip the top bar, and later authors get a precedent for going around the map. Use the roles. Keep direct imports only for ArrowRightLeft, CircleUserRound and CircleQuestionMark, which have no role.
  • Verification: VERIFIED — read at this head against lib/icons.ts

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05f055a4: both shell files take orgSwitcher, search, accountMenu, signOut, next and openExternal from SHELL_ICONS / ACTION_ICONS. Only ArrowRightLeft, CircleUserRound and CircleQuestionMark, which have no role, are imported from lucide-react.

Comment on lines +345 to +400
{shown.length === 0 ? (
<p className="ui:px-3 ui:py-4 ui:text-body-small ui:text-muted-foreground">
No organisations match
</p>
) : (
<p className="ui:px-1.5 ui:pt-1 ui:pb-2 ui:text-overline ui:uppercase ui:text-muted-foreground">
You belong to {count} organisation{count === 1 ? "" : "s"}
</p>
)}
<ul className="ui:flex ui:min-h-0 ui:flex-col ui:overflow-y-auto">
{shown.map((org) => {
const isCurrent = org.id === current.id;
return (
<li
key={org.id}
{...(isCurrent ? { "aria-current": "true" } : {})}
className={`ui:flex ui:h-14 ui:items-center ui:gap-2.5 ui:rounded-md ui:px-2.5 ${isCurrent ? "ui:bg-muted" : ""}`}
>
<OrgAvatar name={org.display_name} size="md" />
<OrgLine org={org} />
{isCurrent ? (
<span className="ui:rounded-sm ui:bg-primary-muted ui:px-2 ui:py-0.75 ui:text-caption ui:font-medium ui:text-primary">
Current
</span>
) : (
<button
type="button"
aria-label={`Switch to ${org.display_name}`}
className={`${BUTTON} ui:px-2.5`}
onClick={() =>
setPending({
from: {
...organizations.find(
(o) => o.id === current.id,
),
id: current.id,
name: current.name,
display_name: current.displayName,
},
to: org,
})
}
>
<ArrowRightLeft aria-hidden className="ui:size-4" />
Switch
</button>
)}
</li>
);
})}
</ul>
{count === 1 && (
<p className="ui:px-3 ui:pt-1.5 ui:text-caption ui:text-muted-foreground">
Nothing to switch to.
</p>
)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6. [Low] [Lens 3] — The panel contradicts itself when the current organisation is not in the list

  • Location: frontend/src/v1/shell/OrganizationSwitcher.tsx:L345-L400
  • The dialog is built to handle a current organisation that is missing from the list: from comes from current props, and a test covers that case. Take that case with one other organisation listed. The panel says "You belong to 1 organisation", shows a Switch row, and also shows "Nothing to switch to." In fact the user belongs to two. "Nothing to switch to." is keyed on count === 1, not on there being no row other than current.id. Also, with an empty list and an empty query, the panel says "No organisations match". Fix: base "Nothing to switch to." on organizations.every((o) => o.id === current.id), and show "No organisations match" only when query.trim() !== "".
  • Verification: VERIFIED — traced by hand through the panel's render branches at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05f055a4: "Nothing to switch to." now shows when every listed organisation is the current one, "No organisations match" only for a non-empty query, and the count includes a current organisation missing from the list. New tests cover the current organisation missing from a list with one other, and an empty list with an empty query.

member_count === undefined
? undefined
: `${member_count} member${member_count === 1 ? "" : "s"}`;
return [roleLabel(role), members, region].filter(Boolean).join(" · ");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7. [Low] [Lens 7, 16] — Region codes are shown raw, but the backend contract says the client maps them

  • Location: frontend/src/v1/lib/organizations.ts:L36
  • backend/account_v2/ReadMe.md documents region as a code (US, EU; STAGING, DEV or INTEGRATION outside production) and says "The client maps it to a label." describeOrganization adds the code to the line as is. On cloud's non-production deployments, rows then read "Admin · 3 members · INTEGRATION". Either map codes to labels (an unknown code can fall back to the raw value), or add a comment that showing the code is the intended v1 behaviour and update the ReadMe line.
  • Verification: VERIFIED — read against backend/account_v2/ReadMe.md at this head

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v1 keeps showing the code. In e05f055a4 a comment on describeOrganization says so, and the region row in backend/account_v2/ReadMe.md now says the client shows the code as is.

Comment on lines +84 to +88
// 412: the backend logged the session out while building the list.
if (axios.isAxiosError(error) && error.response?.status === 412) {
window.location.assign(sessionEndedPath(error.response.data));
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8. [Low] [Lens 2] — URL and HTTP-status handling sit in shell/, not services/

  • Location: frontend/src/v1/shell/OrganizationSwitcher.tsx:L84-L88
  • The README folder table says services/ is "the only place a URL appears". The switcher imports axios to read error.response?.status for the 412 and the 403. It holds SIGN_IN_PATH = "/landing" (L34) and builds /error?code=… itself, and AccountMenu.tsx:L15 holds HELP_URL. A 412 meaning "the session was logged out", and a 403 on /set meaning "access removed", are facts of the backend contract, so services/organizations.ts should own them. Have it return or throw a typed outcome, so the next caller of listOrganizations or switchOrganization does not copy this status logic. These are the first shell modules, so they set the pattern.
  • Verification: VERIFIED — read at this head against the v1 README folder table

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e05f055a4: services/organizations.ts turns the 412 on the list and the 403 on set into an OrganizationError with kind session-ended or access-removed, and the switcher no longer imports axios. SIGN_IN_PATH, the /error?code= path and HELP_URL stay in shell/, since they are app routes and a docs link, not API URLs.

Comment on lines +279 to +280
if (open && state.status !== "ready") {
load();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 New organisations stay hidden

Once the list loads, reopening the switcher never refreshes it because state.status stays "ready". If someone adds the user to another organisation while this page stays open, it will not appear until a full page reload. Refresh the list when the switcher reopens, or give the cached list an expiry.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/v1/shell/OrganizationSwitcher.tsx
Line: 279-280

Comment:
**New organisations stay hidden**

Once the list loads, reopening the switcher never refreshes it because `state.status` stays `"ready"`. If someone adds the user to another organisation while this page stays open, it will not appear until a full page reload. Refresh the list when the switcher reopens, or give the cached list an expiry.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

… copy, icon roles

- listOrganizations rejects a 200 whose body has no organizations array, so
  the switcher shows its load error with Retry instead of crashing.
- services/organizations.ts reads the 412 on the list (session ended) and the
  403 on set (access removed) and throws an OrganizationError with a kind; the
  switcher no longer imports axios.
- "Nothing to switch to." shows when every listed organisation is the current
  one; "No organisations match" shows only for a non-empty query.
- Shell glyphs with a role come from SHELL_ICONS / ACTION_ICONS.
- Tests: landingPath from another organisation's path, the Help link's
  target and rel, the new service and panel cases.
- Region codes are shown as sent; the comment and account_v2 ReadMe say so.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@greptile-apps
greptile-apps Bot dismissed their stale review November 12, 2026 13:53

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

main-only Stays on main: not backported to a release line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants