fix(web): load every page of organizations, not just the first 50 (#3446) - #3447
Merged
ToddHebebrand merged 1 commit intoAug 11, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3446. Verified @_jrush's report against
origin/mainbefore changing anything — it reproduces exactly as described, and the cause is client-side.Why it happens
GET /orgs/organizationspaginates correctly and returns{ data, pagination: { page, limit, total } }. The page never asks for more than the first page:getPaginationdefaults tolimit=50, so 50 rows come back andpagination.totalis discarded. Search isorganizations.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.
getPaginationisMath.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
fetchAllOrganizationswalks 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:
paginationblock;totalreached — the fast path when the server reports it.Plus a
MAX_PAGESstop at 100 (10k orgs). A wrong or missingtotalcombined 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.nullfrom 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, theMAX_PAGESstop against a server that never stops,nullpropagation 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 returnedunknown[]wheresetOrganizationswantsOrganization[], so it is generic and the call site isfetchAllOrganizations<Organization>.vitest run(apps/web, full suite) — 559 files / 5392 passed, exit 0.Not addressed
The API side is untouched:
limitstill 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.