Skip to content

fix(web): load every page of organizations, not just the first 50 (#3446) - #3447

Merged
ToddHebebrand merged 1 commit into
LanternOps:mainfrom
bdunncompany:fix/3446-organizations-pagination
Aug 11, 2026
Merged

fix(web): load every page of organizations, not just the first 50 (#3446)#3447
ToddHebebrand merged 1 commit into
LanternOps:mainfrom
bdunncompany:fix/3446-organizations-pagination

Conversation

@bdunncompany

Copy link
Copy Markdown
Collaborator

Closes #3446. Verified @_jrush's report against origin/main before changing anything — it reproduces exactly as described, and the cause is client-side.

Why it happens

GET /orgs/organizations paginates correctly and returns { data, pagination: { page, limit, total } }. The page never asks for more than the first page:

const response = await fetchWithAuth('/orgs/organizations');   // no page, no limit

getPagination defaults to limit=50, so 50 rows come back and pagination.total is discarded. Search is organizations.filter(...) over whatever was loaded (OrganizationsPage.tsx:78-81), which is why an org past the first page returns "No organizations match your search" even though the API knows about it — the search is filtering a list that never contained it.

Worth stating because it rules out the obvious one-line fix: raising the limit cannot work. getPagination is Math.min(100, ...), so the server clamps to 100 per request regardless of what is asked for. Any tenant past 100 orgs still loses rows. The page has to walk the pages.

The fix

fetchAllOrganizations walks every page and is exported so it can be tested without mounting an 876-line component that pulls in routing, i18n, drag-and-drop and Pax8.

It requests limit=100 — the server's ceiling — so the common case costs one request and a 250-org tenant costs three.

Two termination conditions on purpose:

  • short page — the primary signal, and the only one that works for a legacy or unpaginated response that returns a bare array with no pagination block;
  • total reached — the fast path when the server reports it.

Plus a MAX_PAGES stop at 100 (10k orgs). A wrong or missing total combined with a server that keeps returning full pages would otherwise spin forever in a browser tab; a bounded stop degrades to "some orgs missing", which is the current behaviour, rather than a hung page.

null from the fetcher propagates so the existing 401-redirect path still aborts without rendering.

Verification

Control run: with the loop bounded to a single page — i.e. the pre-fix behaviour — 3 of 7 tests fail, including walks every page, not just the first. The other 4 cover the shapes and guards and pass either way, which is what they are for.

7 new tests: multi-page walk (250 orgs across 3 pages, asserting org-51 — the first row the old cap hid — is present), that it requests the server ceiling, short-page termination with no pagination block, the {organizations:[...]} shape, the MAX_PAGES stop against a server that never stops, null propagation for the 401 path, and that a mid-walk fetch error escapes rather than silently truncating the list.

  • tsc --noEmit (apps/web) — exit 0. It caught a real gap on the first pass: the helper returned unknown[] where setOrganizations wants Organization[], so it is generic and the call site is fetchAllOrganizations<Organization>.
  • vitest run (apps/web, full suite) — 559 files / 5392 passed, exit 0.
  • One file changed, +59/−15, plus the new test file. No API changes.

Not addressed

The API side is untouched: limit still clamps at 100, which is correct for an API. If you would rather the page show a paged table than load everything, that is a different change — this one keeps the existing render-all-and-filter-client-side design and just stops it lying about what "all" means.

@ToddHebebrand
ToddHebebrand merged commit 5e62d40 into LanternOps:main Aug 11, 2026
54 checks passed
ToddHebebrand pushed a commit that referenced this pull request Aug 11, 2026
…e first 50

#3447 fixed the first-50 truncation on the Organizations settings page but
missed the second reader with the identical defect: orgStore.fetchOrganizations
(the org switcher). It fetched /orgs/organizations with no page/limit, the
server default returned 50 rows, and the switcher's client-side search only
filters what was loaded — so a partner with 50+ orgs could never switch to an
org past the first page, and the switcher header under-reported the org count.

fetchAllOrganizations moves from OrganizationsPage to lib/ (re-exported from
the page so its pagination tests and any importers keep working; the store
can't import a page component without a cycle) and the store now walks pages.

Found by pre-release UI QA sweep with a 126-org seed: switcher showed
'50 organizations' and search for 'QA Org 125' returned nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ToddHebebrand added a commit that referenced this pull request Aug 11, 2026
…et all-orgs, org switcher pagination (#3449)

Four user-facing bugs found and fixed by the 2026-08-11 pre-release UI
QA sweep (evidence: docs/testing/FEATURE_TEST_LOG.md, section
'2026-08-11 pre-release gap-closure pass').

## 1. Device-group creation 400 for multi-org partners (apps/api)
`POST /device-groups` read `orgId` only from the JSON body; the
dashboard's CreateGroupModal sends none (fetchWithAuth appends
`?orgId=`). Any partner with 2+ orgs got a 400 on every group create.
Fix: UUID-validated query-param fallback, still gated by
`ensureOrgAccess`; body keeps precedence. 4 new tests incl. cross-tenant
403. **Verified live**: static group w/ 3 devices + dynamic group (30
matches, evaluated at create) as a 126-org partner.

## 2. Saved-filter create 400 with empty description (packages/shared)
Dashboard sends `description: null` on both verbs; only update accepted
null → every create without a description failed with a generic toast.
Same create/update schema drift as #3159. One shared field now serves
both verbs. **Verified live.**

## 3. Fleet findings "All organizations" silently scoped to one org
(apps/web)
FindingsFeed omitted `orgId`, fetchWithAuth re-injected the active org —
and the org dropdown builds from returned rows, so other orgs could
never appear. Fix: explicit `skipOrgIdInjection` opt-out on
fetchWithAuth (sibling of `skipUnauthorizedRetry`), used by
`listFindings` when no org filter is set (API already returns all
accessible orgs). 2 new service tests. **Verified live**: 5/5 findings
across 2 orgs.

## 4. Org switcher truncated at 50 organizations (apps/web)
#3447 fixed OrganizationsPage but missed the second reader:
`orgStore.fetchOrganizations` fetched one unpaginated page, so the
switcher could never reach org 51+ (its search filters client-side).
`fetchAllOrganizations` extracted to `lib/` (re-exported from the page;
a store can't import a page component without a cycle) and the store now
walks pages. **Verified live**: '126 organizations', QA Org 125
reachable.

## Tests
- `pnpm vitest run src/routes/groups` (api): 89 passed
- shared validators: 49 passed
- web: fleet components + services, orgStore, OrganizationsPage
pagination: all green
- `tsc --noEmit` (web): clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Todd Hebebrand <todd@lanternops.io>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Organizations page silently caps list at 50 orgs — no pagination, search is client-side only

2 participants