Repository navigation
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
Conversation
…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>
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
…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>
✅ Console Performance Budget
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
Size Limits
|
席位核对 — ACCEPT ·
|
Fixes #12706
Clause-②: no
A member without
setup.accesswho 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(from814123d, unchanged by this round)appSurfaceAwaitsVerdictis true when the URL names an app that the list does not carry, on a metadata-admin route (metadata/..., with the legacycomponent/metadata/...spellings) or on Setup's root (/apps/setup).verdictWantedis true. That is the existing missing-app condition, orappSurfaceAwaitsVerdictonce the list has loaded.LoadingScreen. Ondeniedit renders the access-denied screen. On every other verdict (not_found,unreachable,unknown,granted) it renders as before.renderAppAccessDenied()shared by both branches. The copy, test ids and Back to home button are unchanged.packages/app-shell/src/views/metadata-admin/AssignedUsersSection.tsxreadFailure. 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.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.@object-ui/react's sharedextractWriteErrorMessage. The first commit used a local chain overerr.body.error/err.error/err.message. The ObjectStack client never setsbodyorerroron the Error it throws (it setsmessage,code,httpStatusanddetails), 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 claim6220867274):readFailureinstead of the sharederror. 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'scatchset the rows to none and set the error, and the render's empty branch drew the empty copy whatever the error held.refusalTextchain (err.body.error,err.error,err.message), which served the read, Add and Remove, is replaced by@object-ui/react'sextractWriteErrorMessage, as inAssignedUsersSection. Add and Remove keep their alerts.loadno 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
mainthat 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
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, anysystemormetadatapath segment, and the whole/apps/setupsubtree (isSetupRoute).AppContentitself, in both route tables: the with-app branch's nestedmetadataroute and the no-app branch's flatmetadata/...routes. So the verdict belongs there, andsetupApp.ts(Setup's identity constants) is untouched./apps/setuplanded on the CRM dashboard: a pseudo-route falls back to the default launcher app, so the index routeAppRootLandingnavigated to CRM's first navigation entry, relative to the URL segmentsetup. That gave/apps/setup/dashboard/pipeline_dashboard.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:/apps/setup/system/approvals. The server's approval notifications carryactionUrl: '/system/approvals'and'/system/approvals?request=ID'(objectstackplugin-approvals,approval-service.ts), andSystemRedirectforwards/system/*onto/apps/setup/system/*;/apps/setup/OBJECT/record/ID(ApprovalsInboxPage'srecordHref, which falls back tosetup);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.
Confirmed and done. See the Assigned Users bullets above.
Serial. PR feat(app-shell): render a record on a named record page at /apps/APP/OBJECT/record/ID/page/PAGE #12731 landed as
c7462e7and 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 asc41ce3f, this branch's base.setupApp.tsis untouched.Tests (run at
57e9915)New pins:
packages/app-shell/src/console/__tests__/AppContent.setupItemRoutesVerdict-12706.test.tsx: 11 cases. A realObjectStackAdapterruns over a stubbedfetch, and the 403 body is the one the real by-name route sent for the member in the browser reading below.app-access-deniedon 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 namessetup, and the URL is left as typed.packages/app-shell/src/views/metadata-admin/AssignedUsersSection.readRefused-12706.test.tsx: 3 cases, also through a realObjectStackAdapterover a stubbedfetch. The 403 body is the one the data route sent in the card's measurement.Local runs, all at
57e9915:pnpm exec vitest run --maxWorkers=2over 189 files: every test file that namesAppContent's routes (all ofpackages/app-shell/src/console/, plusapps/console'sAppContent.*,SetupRouteand approvals-inbox ref suites) and every test namingAssignedUsersSection,PositionHoldersSectionor 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.--listFilesshows both new test files in the test project.@object-ui/app-shellclosure (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.tsxalone:Tests 2 passed (2).PositionHoldersSection.test.tsx,ResourceEditPage.setupCatalog-7611,setupHoldersGrant-12082, plugin-form'saffordanceGrantMap-12082,AssignedUsersSection.readRefused-12706andAppContent.setupItemRoutesVerdict-12706. Result:Test Files 7 passed (7),Tests 92 passed (92).Ablations (one per pin family, each through objectstack's
scripts/ablation-replace.mjs)The subject resolves to source: the pins import
../AppContent,./AssignedUsersSectionand./PositionHoldersSectionrelatively, so nodist/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 andgit diff HEADbeing empty.appSurfaceAwaitsVerdictforced tofalse. Result: 7 failed, 4 passed. Every refused-caller pin went red, and the 4 controls stayed green.setReadFailure(...)in the read'scatchreplaced withsetReadFailure(null). Result: 1 failed, 2 passed. The 403 pin went red; the 200-empty control and the Remove pin stayed green.setError(...)in Remove'scatchdeleted. Result: 1 failed, 2 passed. Only the Remove pin went red.setReadFailure({ said: refusalSentence(err) })in the read'scatchreplaced withsetReadFailure(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 at57e9915, Vite dev, 1280 × 900. The member was created by the admin (POST /api/v1/auth/admin/create-useranswered 200) and changed their password.GET /api/v1/auth/me/permissionsansweredpermissionSets: ["member_default"]andsystemPermissions: [].GET /api/v1/meta/app/setupanswered 403PERMISSION_DENIEDfor the member and 200 for the admin.setup.access)/apps/setupdashboard/system_overview)/apps/setup/metadata/permission?scope=environment.../metadata/permission/crm_sales_user?scope=environmentsys_user_permission_setread sent.../metadata/position/sales_rep?scope=environmentsys_user_positionread sent/apps/setup/system/approvals/apps/crm_appThe 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.routeanswering the holdersGETwith 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_position200), the section showed "Position holders" with "0" and "Nobody holds this position yet.", and no alert. With the holdersGETROUTED 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
6220857048). For a caller Setup refuses,component/..., object lists, dashboards andsystem/*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:system/approvals,system/profile,OBJECT/record/ID). That would be a second access rule in the console, which this card's claim forbids.SystemRedirecttargets the caller's own app segment; the inbox'srecordHrefdrops itssetupfallback), then gate the subtree on Setup's verdict./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 asnot_foundand 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./apps/setupnow gets the access-denied screen, with its Back to home button, when Setup's verdict isdenied. 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.tsxuses an adapter without the probe (unknown) and stays green.PositionHoldersSectionis changed in this PR (round 3, after the claim's re-statement6220867274), with the same refusal state asAssignedUsersSection.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 setsmessage, which the chain's third arm reads.Session:
https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4Generated by Claude Code