Repository navigation
fix(app-shell): the activity feed asks only a caller who may read sys_activity, and a failed read is not an empty feed (objectui#12081 item 8) - #12095
Conversation
…_activity, and a failed read is not an empty feed (objectui#12081 item 8)
The header bell's Activity tab, the global:notifications block and Home's
activity card share one sys_activity read. It went out for every signed-in
user; a caller whose sets grant no read on sys_activity (objectstack's
member_default names it deliberately not) got 403 on every page, and the
hook handed its consumers `.value` alone, so all three said "No recent
activity".
- the caller's object grant decides before asking, through
usePermissions().can('sys_activity', 'read'); unknown permissions (no
provider, not loaded) still read, and only a loaded refusal sends nothing.
The consumers render no widget for it.
- the feed returns its snapshot with its status (ActivityFeedReading), and
the Activity tab and Home's card render error / loading instead of the
empty copy when the read has not answered.
- the cache key carries the signed-in user beside the adapter, as the inbox
feed's does: the server narrows these rows per caller and a sign-out keeps
the SPA running.
Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…ion (objectui#12081 item 8) Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z 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
|
|
✅ 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
|
Contract reviewServed-tier: Reviewed on the landing head (the branch merged ① Derived judgments
② Semver level
③ Boundary flagsFrom the dev's
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #12081 (item 8)
Clause-②: no
Item 8 only, as triage directed. Items 2, 3, 4 and 7 stay on the card, and #12081 remains open.
Measured first
objectui
mainat023f00d4.useSharedActivityFeedsentfind('sys_activity', { $orderby: { timestamp: 'desc' }, $top: 20 })for every signed-in user. A failure other than 404 went tomarkFailed(), so the store's status becameerror. The hook still returned.valuealone. Three surfaces read that value: the header bell's Activity tab, theglobal:notificationspage block (the same popover), and Home's activity card (throughuseHumanActivityFeed). Each received[]and showed "No recent activity", followed by a "View all activity" link to a list page that refuses the same user. The card says the widget "shows nothing". It actually shows the empty-state message.Reproduced on
023f00d4with only the test file changed. The new block inAppHeader.inboxVariant.test.tsxwas 2 red and 2 green:expected [ { object: 'sys_activity', … } ] to have a length of +0 but got 1.objectstack
origin/mainat86da1949, read only.member_defaultin plugin-security'sdefault-permission-sets.tsgrants read on the two inbox objects. Its own comment says "sys_activityis deliberately NOT included: it is not a per-user-scoped shape".git grep sys_activity 86da1949 -- examples/returns nothing, so no shipped example app grants it either. The admin sets reach it through their*entries.activity-read-visibility.tsadds a parent-record filter to every non-systemfind,findOne,countandaggregateonsys_activity. A reader who holds the object grant sees only rows about records they can open.find('sys_activity', …), filtered byobject_nameandrecord_id. It hits the same object check, and for that user it shows its refusal state (objectui#11195), not rows.PM mechanism assumption 2 holds. That is why this PR takes triage's second branch: decide from the user's grant before asking.
Changes
1. The grant is checked before the read (
sharedUserFeeds.ts). The hook readsusePermissions().can('sys_activity', 'read'). This is the permission check the shell already uses for reads:ObjectDataPage's route gate andUnifiedSidebar's nav gate read it the same way. I looked at the affordance map from objectui#12084 and did not use it. Its rows are write affordances only, and its census explicitly leavescan(x, 'read')read gates outside its family. The check has three outcomes, beside the unchanged presence rule:readable: false.2. The status reaches the surfaces. The hook now returns
ActivityFeedReading, which isvalue,statusandreadabletogether.readableis a separate field rather than a fifth status value, so the four-value status set (#4300) is unchanged.3. The surfaces.
InboxPopovertakes a requiredactivityprop in place ofactivities. Withreadable: falseit shows no Activity tab trigger, no tab body and no "View all activity" link, and it ignores a remembered Activity tab choice. Witherrorit shows "An unexpected error occurred.". Withidleorloadingit shows "Loading…". It shows "No recent activity" only when the status isready.AppHeaderandglobal-notifications-rendererpass the new value through.AppHeader's exported props are unchanged: rows a host passes asactivitiesare treated asready.useHomeInboxreturnsactivityinstead ofactivities.HomeActivityrenders nothing whenreadableis false and shows the same three states otherwise.HomePageremoves the 360px grid column when the card is absent.4. The cache key includes the signed-in user. This is a small, bounded fix in the same file; the reasons are below.
The messages reuse existing locale keys,
errors.unknownandcommon.loading, the same pairHomeActionCentershows for an inbox read that has not answered. No locale key is added. Nothing is added to the package entry:sharedUserFeeds,InboxPopover,HomeRailanduseHomeInboxare not re-exported fromsrc/index.ts.AppHeaderis re-exported, and its props are unchanged. So Clause-② staysno.The fix in the same file: the activity cache key
The store keyed the activity feed on the adapter alone. Two facts make that wrong:
AuthProvidernotes that "no sign-out call site reloads the page"), so this module-level store keeps running.As a result, the next user to sign in on the same browser tab was shown the previous user's rows as
ready, within the 30s freshness window and until a refetch landed. The key is now the adapter plus the signed-in user id, as the inbox feed's key already is. The user id is part of the key only: the query names no user, so a host without auth still reads. The test "re-keys on the signed-in user" pins it.Why this fits in this PR rather than a separate card:
Evidence
Head. All runs below are on
f1f0ba557:023f00d4plus this change, merged withorigin/mainatde302c73. That merge brought in objectui#12088 and #12090, and neither touches any file in this diff.Type-check.
pnpm --filter @object-ui/app-shell type-check(tsc --noEmit && tsc -p tsconfig.test.json) exits 0.Tests, narrowed. I ran every test file that names any of the seven changed modules or the components built on them (
sharedUserFeeds,useHomeInbox,AppHeader,InboxPopover,global-notifications-renderer/GlobalNotificationsRenderer,HomeRail,HomePage,HomeLayout,ConsoleLayout,global:notifications), selected bygit grep -l, plusaffordanceGrantMap-12082.test.tsx. That is 121 files, in two runs.ApiConsolePage.requestPreset-10591. It hit a 10s timeout while the page still showed "Initializing application…", under a full shared box. That happens before the shell renders anything this PR touches. Run alone on the same head it passes 12 of 12.The rest of app-shell's 1,250 test files are left to CI: no vi.mock of the changed modules exists outside the files above.
Reverse checks. Each check was run from the committed
d74a507withablation-replace.mjs, which checks that the anchor matched and that the restore leaves the file identical to HEAD (git diff HEADempty after each leg). The tests run were the newobjectui#12081cases in the hook suite and theAppHeadersuite.readable = true || …): 3 red, namely the two refusal cases in the hook suite and theAppHeadermember case.ready: 1 red, the 403 case.Gates.
check:new-line-citationscheck:control-bytescheck:i18n-keyscheck-changeset-presencecheck-changeset-no-majorcheck-vi-mock-inherit/-override-shape/-specifierscheck-test-path-rootscheck:esm-specifiers/check:self-import/check:phantom-depsLint. I ran eslint on the 27 touched
.ts/.tsxfiles only:--format json): 27 files, 0 errors, 96 warnings, and none of the warnings is on a line this PR adds;eslint.config.jsforprojectService,parserOptions.projectorTypeCheckedfinds nothing, so type-aware linting is not enabled, and this diff cannot change the lint result of a file it does not touch.Eager closure.
check:eager-closurepasses atd74a507: 3169.0 KB gzipped, headroom 35.6 KB. Base023f00d4, built in a separate worktree, measures 3168.6 KB. The difference is +349 bytes gzipped (+1,202 raw), and the eager chunk count is 290 at both. The head figure was taken before the merge.Acceptance notes
useHomeInboxmocks in sevenHomePage.*tests never carriednotificationsStatus. This PR only replaces theiractivitieskey. The missing key is fixture drift; it was not changed here and no card was filed.MePermissionsProviderrefetches when a different user signs in on the same tab. If it does not, the next user's grant check reads the previous user's grants. The result is then either a refused read, which now shows as an error, or a missing Activity tab until reload. This is unmeasured, so no card was filed. Who picks it up: no one.content/docs/guide/notifications.mdcovers only the notifications half.Session:
https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7zGenerated by Claude Code