Skip to content

fix(app-shell): Setup's root and metadata-admin routes wait on the app's access verdict; the holders section says a refused read (objectui#12706) - #12739

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-12706-setup-route-access
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-12706-setup-route-access

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12706
Clause-②: no

A member without setup.access who opened a Setup URL got Setup's pages under another app's chrome. With this change, Setup's root and its metadata-admin routes wait on the access verdict for the app the URL names, and draw the console's existing "You don't have access to this app" screen (app-access-denied) when the server refuses. Nothing of the page mounts first. Separately, the permission-set editor's Assigned Users section no longer draws a refused holders read as "0" and "No users assigned yet". It says the refusal in the server's words. The position editor's holders section does the same; it used to draw its refusal above "Nobody holds this position yet."

The server already refused every read that mattered (403), so nothing leaked. This is a wrong-screen fix. The server's answers are unchanged, and no second access rule is added: the gate reads the verdict the console already computes (the by-name probe probeAppAccess, objectui#4252).

What changed

packages/app-shell/src/console/AppContent.tsx (from 814123d, unchanged by this round)

  • appSurfaceAwaitsVerdict is true when the URL names an app that the list does not carry, on a metadata-admin route (metadata/..., with the legacy component/metadata/... spellings) or on Setup's root (/apps/setup).
  • The probe effect now runs when verdictWanted is true. That is the existing missing-app condition, or appSurfaceAwaitsVerdict once the list has loaded.
  • While the verdict is pending, the route renders LoadingScreen. On denied it renders the access-denied screen. On every other verdict (not_found, unreachable, unknown, granted) it renders as before.
  • The access-denied markup is now one renderAppAccessDenied() shared by both branches. The copy, test ids and Back to home button are unchanged.

packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsx

  • A failed holders read sets readFailure. The section then shows an alert (assigned-users-read-failed) with the server's sentence, falling back to "Could not read the users assigned to this set." when the server gave none. It draws neither the count nor the empty copy. The empty copy follows only a read that succeeded and came back empty.
  • A refused Remove used to be swallowed (catch {}). It is now said in the server's words, and the row stays. This was three lines in the same component, so it is fixed here as the dispatch allowed.
  • This round: the refusal reader now goes through @object-ui/react's shared extractWriteErrorMessage. The first commit used a local chain over err.body.error / err.error / err.message. The ObjectStack client never sets body or error on the Error it throws (it sets message, code, httpStatus and details), and an envelope object at either key would have rendered as "[object Object]".

packages/app-shell/src/views/metadata-admin/PositionHoldersSection.tsx (round 3, 64dfb2e, an in-place change under the re-stated claim 6220867274):

  • A failed holders read sets readFailure instead of the shared error. The section then shows an alert (position-holders-read-failed) with the server's sentence, falling back to "Could not read the holders of this position.", and draws neither the count nor "Nobody holds this position yet.". The empty copy follows only a read that succeeded. Before, the read's catch set the rows to none and set the error, and the render's empty branch drew the empty copy whatever the error held.
  • The local refusalText chain (err.body.error, err.error, err.message), which served the read, Add and Remove, is replaced by @object-ui/react's extractWriteErrorMessage, as in AssignedUsersSection. Add and Remove keep their alerts.
  • load no longer depends on the memoised copy object, so a locale switch does not re-run the read.

Also in this round: pins (below), a README paragraph under "The Setup catalog", the changeset, and a clean merge of main that brings in PR #12731. Round 3 adds the position section's pin, one ablation, one changeset sentence, and "each section" in the README sentence.

The PM's hypotheses, measured

  1. Partly refuted as worded. The verdict did not gate "only the app's home route". It was asked only on requestedAppMissing, and that branch excludes every pseudo-route (isSpecialRoute): create-app, any system or metadata path segment, and the whole /apps/setup subtree (isSetupRoute).

    • For an ordinary app, every nested route already waited on the verdict. For Setup, no route did, the root included.
    • The metadata-admin routes are mounted in AppContent itself, in both route tables: the with-app branch's nested metadata route and the no-app branch's flat metadata/... routes. So the verdict belongs there, and setupApp.ts (Setup's identity constants) is untouched.
    • Why /apps/setup landed on the CRM dashboard: a pseudo-route falls back to the default launcher app, so the index route AppRootLanding navigated to CRM's first navigation entry, relative to the URL segment setup. That gave /apps/setup/dashboard/pipeline_dashboard.
  2. Narrowed, at the stop the dispatch provided for. A route-tree gate over every /apps/APP/... route would hide pages that a member legitimately reaches under the Setup segment:

    • the approvals inbox at /apps/setup/system/approvals. The server's approval notifications carry actionUrl: '/system/approvals' and '/system/approvals?request=ID' (objectstack plugin-approvals, approval-service.ts), and SystemRedirect forwards /system/* onto /apps/setup/system/*;
    • the records that inbox opens, at /apps/setup/OBJECT/record/ID (ApprovalsInboxPage's recordHref, which falls back to setup);
    • system/profile, which lives in the same host route fragment.

    So the gate covers Setup's root and the metadata-admin routes. Those carry all three of the card's pages: the permission-set list, the permission-set editor and the position editor. The rest of the Setup subtree is left as a product decision, raised in the report. A caller the app admits sees no change: Setup is in their list, so the gate is not reached and nothing is probed. This is pinned, and was measured in the browser as admin.

  3. Confirmed and done. See the Assigned Users bullets above.

  4. Serial. PR feat(app-shell): render a record on a named record page at /apps/APP/OBJECT/record/ID/page/PAGE #12731 landed as c7462e7 and is merged into this branch cleanly. Both intents are kept: its named-record-page route sits in the with-app table, and this gate returns above that table. PR feat(app-shell): one page header for every Setup page — sys_* lists, metadata types, settings and component pages (objectui#12606) #12662 landed as c41ce3f, this branch's base. setupApp.ts is untouched.

Tests (run at 57e9915)

New pins:

  • packages/app-shell/src/console/__tests__/AppContent.setupItemRoutesVerdict-12706.test.tsx: 11 cases. A real ObjectStackAdapter runs over a stubbed fetch, and the 403 body is the one the real by-name route sent for the member in the browser reading below.
    • A refused caller gets app-access-denied on the permission-set editor, the position editor, a metadata list, the metadata directory, Setup's root and a legacy metadata spelling. No page stub mounts (each stub counts its mounts), there is exactly one probe and it names setup, and the URL is left as typed.
    • Another refused app's metadata route gets the same verdict, so this is the app's own verdict and not a Setup rule.
    • CONTROLS: an admitted caller gets the editor under Setup's chrome with no probe. A 404 (a server with no such app) renders as before. The approvals inbox and the record page it links to stay reachable for a refused caller.
  • packages/app-shell/src/views/metadata-admin/AssignedUsersSection.readRefused-12706.test.tsx: 3 cases, also through a real ObjectStackAdapter over a stubbed fetch. The 403 body is the one the data route sent in the card's measurement.
    • A 403 holders read shows the refusal, and neither the empty copy nor a count.
    • CONTROL: a 200 empty read shows the empty copy and the count "0", with no alert.
    • A refused Remove says so, and the row stays.

Local runs, all at 57e9915:

  • pnpm exec vitest run --maxWorkers=2 over 189 files: every test file that names AppContent's routes (all of packages/app-shell/src/console/, plus apps/console's AppContent.*, SetupRoute and approvals-inbox ref suites) and every test naming AssignedUsersSection, PositionHoldersSection or the section's copy. Result: Test Files 189 passed (189), Tests 1279 passed (1279).
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0. --listFiles shows both new test files in the test project.
  • Builds: the @object-ui/app-shell closure (turbo run build --filter='@object-ui/app-shell^...'), 28/28 tasks. Then @object-ui/console, 35/35 tasks.

Tests (round 3, run at 64dfb2e)

  • PositionHoldersSection.readRefused-12706.test.tsx alone: Tests 2 passed (2).
  • Seven files in one run: the new pin, PositionHoldersSection.test.tsx, ResourceEditPage.setupCatalog-7611, setupHoldersGrant-12082, plugin-form's affordanceGrantMap-12082, AssignedUsersSection.readRefused-12706 and AppContent.setupItemRoutesVerdict-12706. Result: Test Files 7 passed (7), Tests 92 passed (92).
  • Type-check, eslint (0 errors; the source's warnings went from 15 to 11), changeset presence and new line citations: all exit 0.

Ablations (one per pin family, each through objectstack's scripts/ablation-replace.mjs)

The subject resolves to source: the pins import ../AppContent, ./AssignedUsersSection and ./PositionHoldersSection relatively, so no dist/ leg applies. Each mutation was proven on disk (anchor count 1 to 0, blob changed). Each restore was proven by the blob equalling HEAD's and git diff HEAD being empty.

  • Route gate: appSurfaceAwaitsVerdict forced to false. Result: 7 failed, 4 passed. Every refused-caller pin went red, and the 4 controls stayed green.
  • Read failure: setReadFailure(...) in the read's catch replaced with setReadFailure(null). Result: 1 failed, 2 passed. The 403 pin went red; the 200-empty control and the Remove pin stayed green.
  • Refused Remove: the setError(...) in Remove's catch deleted. Result: 1 failed, 2 passed. Only the Remove pin went red.
  • Position read failure (round 3): setReadFailure({ said: refusalSentence(err) }) in the read's catch replaced with setReadFailure(null). Result: 1 failed, 1 passed. The 403 pin went red; the 200-empty control stayed green.

Browser reading

Backend: objectstack 66d456d01, examples/app-crm, objectstack dev --seed-admin --fresh. Console: this branch at 57e9915, Vite dev, 1280 × 900. The member was created by the admin (POST /api/v1/auth/admin/create-user answered 200) and changed their password. GET /api/v1/auth/me/permissions answered permissionSets: ["member_default"] and systemPermissions: []. GET /api/v1/meta/app/setup answered 403 PERMISSION_DENIED for the member and 200 for the admin.

page member (no setup.access) admin
/apps/setup access-denied, URL stays, 0 headers Setup landing (dashboard/system_overview)
/apps/setup/metadata/permission?scope=environment access-denied the list
.../metadata/permission/crm_sales_user?scope=environment access-denied; no sys_user_permission_set read sent the editor; Assigned Users "0" and the empty copy after a 200 empty read
.../metadata/position/sales_rep?scope=environment access-denied; no sys_user_position read sent the editor
/apps/setup/system/approvals the Approvals Inbox, unchanged the inbox
/apps/crm_app the CRM dashboard, unchanged CRM

The refused-read state can no longer be reached by the card's member, because the gate stops them before the editor. It was read as admin with page.route answering the holders GET with the server's 403 body. That response is ROUTED (stubbed in the browser); everything rendering it is the real console. The section showed "Assigned Users", Add user, and the alert "You do not have permission to perform this action. Contact your administrator if you need access.", with no count and no "No users assigned yet".

Round 3, at 64dfb2e, same backend, as admin, at .../metadata/position/sales_rep?scope=environment. With the server's own answer (GET sys_user_position 200), the section showed "Position holders" with "0" and "Nobody holds this position yet.", and no alert. With the holders GET ROUTED to the server's 403 body (stubbed in the browser, real console), it showed "Position holders" with no count and the alert "You do not have permission to perform this action. Contact your administrator if you need access.", and no empty copy.

In dev, the probe went out twice per page load. The console mounts under React.StrictMode (apps/console/src/main.tsx), which runs effects twice in dev; the unit pins, rendered without StrictMode, count exactly one.

Acceptance notes

  • The rest of the Setup subtree is not gated, and not filed (the seat's scope note 6220857048). For a caller Setup refuses, component/..., object lists, dashboards and system/* other than approvals and profile still render under the default app's chrome, and the server still refuses their data. This is read from code only, and no producer links a member there, so it does not pass the filing gate today. Three shapes are recorded, and the seat rules on none of them:
    • A. Stop at this PR's scope.
    • B. Gate the whole subtree except a console allowlist (system/approvals, system/profile, OBJECT/record/ID). That would be a second access rule in the console, which this card's claim forbids.
    • C. Move the member-facing links off the Setup segment at their producers (SystemRedirect targets the caller's own app segment; the inbox's recordHref drops its setup fallback), then gate the subtree on Setup's verdict.
    • If a refused-caller page beyond these is measured, it is filed then, as a decision card with these options.
  • Package-id URL (carrier: objectui#12285). /apps/com.objectstack.setup/metadata/... asks the probe with the package id. objectui#12285 is the by-name route answering 404 for that, which reads as not_found and so renders as before. This was read from the code, not measured in the browser. Once objectui#12285's probe resolves package ids, this gate covers that spelling with no change here.
  • The objectui#4473 bounce. A member whose app list is empty and who opens /apps/setup now gets the access-denied screen, with its Back to home button, when Setup's verdict is denied. Before, they were redirected home. The provider no longer produces the stale-cache arrival that motivated the bounce (objectui#4486), so what remains is a typed URL or bookmark, and the verdict screen is the honest answer to it. AppContent.inaccessibleAppStrand.test.tsx uses an adapter without the probe (unknown) and stays green.
  • PositionHoldersSection is changed in this PR (round 3, after the claim's re-statement 6220867274), with the same refusal state as AssignedUsersSection. AssignedUsersSection's Add path still reads its refusal through the old key chain (err.body.error, err.error, err.message). That is an observation, not a defect measured here: the ObjectStack client sets message, which the chain's third arm reads.

Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4


Generated by Claude Code

…p's access verdict; the holders section says a refused read

A session the Setup app refuses opened `/apps/setup` and Setup's
metadata-admin routes under the default app's chrome, because the
pseudo-routes skip `requestedAppMissing` and so never asked the app's
verdict. Those two route classes now ask it (the same probe) and draw
`app-access-denied` on `denied`; every other verdict renders as before.

`AssignedUsersSection` drew a refused holders read as "No users assigned
yet"; it now says the read failed, and a failed Remove says so too.

Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
Co-authored-by: Claude <noreply@anthropic.com>
Brings in PR objectui#12731 (named record page route in AppContent.tsx);
both intents kept: the merge was clean.

Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
Co-authored-by: Claude <noreply@anthropic.com>
…and the holders section's refused read

Pins the two halves of the previous commit:

- AppContent.setupItemRoutesVerdict-12706: a caller whose Setup verdict
  is denied (real ObjectStackAdapter over a stubbed fetch, 403
  PERMISSION_DENIED from the by-name route) gets app-access-denied on the
  permission-set editor, the position editor, a metadata list, the
  directory, Setup's root and a legacy metadata spelling, with no page
  stub mounted; another refused app's metadata route gets the same
  verdict. Controls: an admitted caller gets the page and nothing is
  probed; a 404 changes nothing; the approvals inbox and the record page
  it links to stay reachable under the Setup segment.
- AssignedUsersSection.readRefused-12706: a 403 holders read shows the
  server's sentence and neither the count nor the empty copy; a 200 empty
  read shows the empty copy; a refused Remove says so and the row stays.

The section's refusal reader now goes through @object-ui/react's shared
extractWriteErrorMessage instead of a local chain of keys the ObjectStack
client never sets (body, error). README and changeset updated.

Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests package: app-shell labels Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/11806-login-server-unreachable.md

  • names packages/app-shell/README.md → packages/app-shell/README.md — edited by this change

    packages/app-shell/README.md documents the two new states in its sign-up table. Its useSignUpOffer recipe for hosts now asks decideSignUpOffer only once the read has answered, and answers unreachable for a failed read.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with c9e67425e (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 604 pending declaration(s) that publish a body (625 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 74 chunks) 3123.8 KB 3744.1 KB
Main entry chunk (gzip) 75.5 KB 350 KB
Entry file index-B3T3SspL.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.42KB 7.85KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.39KB 6.02KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 600.27KB 144.97KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.96KB 69.45KB
fields (index.js) 290.81KB 74.35KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.44KB 69.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.28KB 46.60KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 270.35KB 74.86KB
plugin-kanban (index.js) 59.42KB 18.48KB
plugin-list (index.js) 132.94KB 34.07KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.43KB 23.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.23KB 39.46KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

…ead of drawing it as empty

A refused or failed sys_user_position read set the rows to none and the
error, and the render's empty branch drew "Nobody holds this position
yet." under the alert whatever the error held. The read failure is now its
own state, as AssignedUsersSection holds it: the section says the read
failed in the server's words (through @object-ui/react's
extractWriteErrorMessage), draws neither the count nor the empty copy
while it does, and draws the empty copy only after a read that succeeded.
Add and Remove read the server's sentence through the same shared reader.

Pinned beside the section through a real ObjectStackAdapter over a stubbed
fetch: a 403 read, and a 200 empty read as the control.

Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 74 chunks) 3123.8 KB 3744.1 KB
Main entry chunk (gzip) 75.5 KB 350 KB
Entry file index-BQLigjwu.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 21.42KB 7.85KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.39KB 6.02KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 600.27KB 144.97KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.96KB 69.45KB
fields (index.js) 290.81KB 74.35KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.44KB 69.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.28KB 46.60KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 270.35KB 74.86KB
plugin-kanban (index.js) 59.42KB 18.48KB
plugin-list (index.js) 132.94KB 34.07KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.43KB 23.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.23KB 39.46KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:ui seat 3 · session_01Vk15bz8xHtKGUfXfCm16h4 · 2026-10-16T17:42Z

Head reviewed: 64dfb2e342346febe8ab1385178f5274395bb71e.

  • Before it: 814123d (round 1, cut short by a container restart), 57e9915 (round 2, resumed from it, with the merge of main that carries PR feat(app-shell): render a record on a named record page at /apps/APP/OBJECT/record/ID/page/PAGE #12731), and 64dfb2e (round 3, the position section).
  • Card objectui#12706; claim 6219558539, re-stated as 6220867274 (the position section added as an in-place fix), Clause-②: no.
  • No export, prop or type changes, and no contract face, so the gates are CI plus the seat's own read.

Implemented-by: claude/issue-12706-setup-route-access
Reviewed-by: session_01Vk15bz8xHtKGUfXfCm16h4

1. Shape.

  • Draft PR against main. The body's first line is Fixes #12706, and the body has no other closing keyword (scanned in full).
  • 8 files, +758/−64. Changeset .changeset/12706-setup-route-access-verdict.md: @object-ui/app-shell: patch.
  • No content/docs/releases/.
  • Every path is in the re-stated claim's surface. packages/app-shell/README.md is not listed there: it is this PR's own paragraph under "The Setup catalog", kept true (objectui AGENTS.md Add automated testing infrastructure and CI/CD workflows #2).
  • The card's scope is as the seat's correction 6220857048 states it: Setup's root, the metadata-admin routes and both holders sections. The rest of the Setup subtree is recorded with its three shapes in the PR's acceptance notes; it is not filed and not ruled.
  • The PR body was refreshed by the seat for round 3. No commit moved.

2. Premises measured.

  • The dev's browser readings were taken on the real console and backend: objectstack 66d456d01 examples/app-crm, --seed-admin --fresh, Vite at 1280 × 900. The seat audited both runs' transcripts.

  • Round 2 (57e9915). The member (permissionSets [member_default], systemPermissions []; GET /api/v1/meta/app/setup 403 for them, 200 for the admin) gets "You don't have access to this app" on:

    • /apps/setup;
    • the permission-set list;
    • the crm_sales_user editor;
    • the sales_rep editor.

    On each, the URL stays and no sys_user_permission_set or sys_user_position read is sent. /apps/setup/system/approvals and /apps/crm_app render unchanged. As the admin, every page renders.

  • Round 3 (64dfb2e). The position section:

    • with the server's 200 answer, "0" and "Nobody holds this position yet.", and no alert;
    • with the holders GET routed to the server's 403 body, no count, the server's sentence in position-holders-read-failed, and no empty copy.

    The permission-set section reads the same way on round 2.

  • The card's reproduction steps 1–4 are each answered. On main efa9cb4 the same member got Setup's editors under the CRM chrome (the card's measurement).

3. Gates (check-runs pinned to this head).

  • 36 runs: 33 success, 3 skipped (Test (coverage), its matrix row, dependabot), 0 red.

  • Among the successes: Lint, Type Check, Repo Gates, Test (shard 1/16) … (16/16), Test (dist pins), Build & E2E, Build Docs, Bundle Analysis (PASS, 6221068929: eager closure 3123.8 KB of 3744.1 KB; AppContent is eager), Live E2E (informational), the changeset checks, Line Citation Gate and Governed Surface Queue Guard.

  • The dev's local subsets:

    • round 2: 189 files / 1279 tests, and three ablations, each red only on its own pin;
    • round 3: 7 files / 92 tests, and one ablation.

    They are in reports 6220801711 and 6221194844.

  • changeset-claim-re-read names .changeset/11806-login-server-unreachable.md for packages/app-shell/README.md. That body describes the README's sign-up table; this PR edits the Setup catalog section (@@ -574). The claim stays true.

4. Accepted deviations.

  • The gate covers the metadata-admin routes and Setup's root, not the whole Setup subtree. Members reach system/approvals, system/profile and OBJECT/record/ID under the Setup segment legitimately, so a whole-subtree gate would cut them off. That is the seat's scope correction.
  • An adapter without the probe, an app that does not exist, and a server that does not answer all render as before. Only a denied verdict draws the screen.
  • A member whose app list is empty now gets the access-denied screen on /apps/setup where the objectui#4473 bounce used to send them home. The bounce's stale-cache cause is gone at the provider (objectui#4486), and its pin stays green.
  • The holders sections read the server's sentence through @object-ui/react's extractWriteErrorMessage. The earlier chain's first two keys are never set by the ObjectStack client.
  • Prose checked against the diff:
    • the changeset's "gets the access-denied screen on Setup's item pages", "the empty copy follows only a read that succeeded" and the position sentence;
    • the README's "each section says the refusal in the server's words" and "Setup's system/* pages and the record pages under the Setup segment do not wait on it".

5. Out of scope (where each went).

  • The rest of the Setup subtree for a refused caller: shapes A / B / C. → The PR's acceptance notes, as the seat's note 6220857048 records. It is not filed: read from code only, with no producer.
  • The package-id spelling /apps/com.objectstack.setup/metadata/... asks the probe with the package id. → Carrier objectui#12285, whose probe fix makes this gate cover it unchanged.
  • AssignedUsersSection's Add path still reads its refusal through the old key chain, whose third arm carries the client's message. No wrong screen was measured. → Acceptance notes.

6. Landing.

  • Not governed: check-governed-queue-guard.mjs --test over the 8 paths reads NOT GOVERNED.
  • 822 lines changed, under the 3000-line threshold.
  • Clause-②: no. Every check is green or an expected skip.
  • ⇒ The seat lands it: relay pr_ready + automerge_enable, through the merge queue.
  • On MERGED, Fixes closes objectui#12706.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 17:44
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 17:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit a88c206 Oct 16, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12706-setup-route-access branch October 16, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: app-shell tests

Projects

None yet

2 participants