Skip to content

fix(app-shell): Home's action centre needs an ANSWER before it says "all caught up" (#4235) - #4315

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4235-home-inbox-source
Aug 11, 2026
Merged

fix(app-shell): Home's action centre needs an ANSWER before it says "all caught up" (#4235)#4315
yinlianghui merged 1 commit into
mainfrom
claude/issue-4235-home-inbox-source

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4235

Home's action centre told a user with nine unread messages that they were all
caught up, with no badge. This makes that pair unreachable: the affirmative copy
now renders only after the inbox read has actually answered.

The card's premise did not survive measurement — the source is right

#4235 reads the symptom as a WRONG SOURCE (sys_inbox_message rather than
/api/v1/notifications) and asks for the panel to be re-pointed. Measured, the
table read is the sanctioned consumer channel and re-pointing it would be a
regression:

  • ADR-0030 names it, three times. Its object-model table (L5 row): "the
    bell reads sys_inbox_message
    ". Its P0 phase: "UI bell reads
    sys_inbox_message". Its cross-repo cut-over section: "Repoint the Console
    bell (AppHeader/InboxPopover/record views) from sys_notification to
    sys_inbox_message (the mine view), joining sys_notification_receipt
    for read-state."
  • The API is a projection of the same rows.
    MessagingService.listInbox (packages/services/service-messaging/src/messaging-service.ts,
    the block headed "Inbox read API (ADR-0030) — backs /api/v1/notifications")
    reads sys_inbox_message where user_id, ordered created_at desc, joined
    with the same receipts. Reading the table is reading the API's own source one
    hop earlier, not a second opinion.
  • The alternative was already ruled on. objectstack#7344's disposition 3 was
    exactly "serve the personal inbox from a dedicated authenticated route instead
    of the generic data API". The maintainer ruling of 2026-08-11 chose Option A
    instead — grant the table read — and objectstack#7586 landed it.

So this PR keeps the source and fixes what actually produced the symptom. A pin
was added for the query shape, so a future re-point has to argue with ADR-0030
rather than slip past it.

The cross-run contradiction, resolved: it is the signed-in user

The card records two QA runs on the same console pin 09987b68 disagreeing —
objectstack#7514 saw this panel empty with 9 unread, objectstack#7517 used it as
the working control against the dead bell — and asks which is stale. Neither
is. objectstack#7344 measured the mechanism in a browser on 2026-08-10:

[Security] Access denied: operation 'find' on object 'sys_inbox_message'
is not permitted for positions [org_member, contributor, finance, everyone]

Every inbox read returned 403 PERMISSION_DENIED for a plain member, because no
shipped permission set granted the object — while /api/v1/notifications, a
dedicated authenticated route, answered those same users with their unread rows.
An admin session read the table and the card worked. One build, one pin,
opposite screenshots, decided by who was signed in.
Both QA reports are true.

The 403 itself is closed server-side by objectstack#7586. What was not closed —
and is what this PR fixes — is that the console converted that denial into good
news, so the next denial, outage or malformed answer reproduces the identical
silent lie.

What changed

useHomeInbox caught every failed read to [], handing HomeActionCenter an
empty array indistinguishable from an empty inbox. It now reports
notificationsStatus:

status meaning
idle not asked yet — no adapter, or no signed-in user
loading asked, in flight
ready answered; only here does empty mean empty
error failed; the empty array is the absence of an answer

Those are MetadataProvider's four words on purpose — #4300 landed this same
rule for the app list ("an unloadable app list is UNKNOWN, not 'no default
app'") and ruled one status dialect, no second one.

HomeActionCenter gates the affirmative copy on ready, and renders a quiet,
non-affirmative notice otherwise — alongside the approvals row when only
that half answered, so the panel never silently drops the half it failed to
read. The prop is required rather than optional-with-a-default: a call site that
cannot say whether its rows are an answer should not reach the affirmative copy
by staying silent.

A missing object stays an answer. A 404 / OBJECT_NOT_FOUND means this
deployment has no inbox pipeline, so nothing is waiting — it degrades to the
caught-up state exactly as before, using the same isMissingResource split
sharedUserFeeds and AppHeader already apply. Classifying it as an error
would have put a permanent error line on Home for every community build without
service-messaging. Both polarities are pinned.

No new i18n key. The quiet notice reuses errors.unknown and
common.loading, which exist in all ten packs; check:i18n-keys confirms both
resolve and both inline defaults match their en values byte for byte.
packages/i18n is held by #4040 tranche 2 and is untouched.

Scope

The bell is untouched — #4230 ruled TWO independent panels, and its raw-table
read is ADR-0030 by design. PR #4284's tripwire suite is unchanged and green.
The #4225 shared-feed refactor is deliberately NOT done here: that card is
sequenced after this one, it touches mark-read, and sharedUserFeeds.ts carries
no change in this PR.

Verification

  • pnpm exec vitest run packages/app-shell — the whole affected package: 345 files, 3303 passed, 1 skipped. PR test(app-shell): pin the bell panel's Unread/All render oracle — #4230's console predates #4199 (#4230) #4284's bell tripwire suite is in that run, unchanged and green.
  • pnpm exec vitest run packages/app-shell/src/console/home/__tests__/ — 6 files, 34 tests, all green.
  • pnpm --filter @object-ui/app-shell type-check — green (both tsc --noEmit and tsc -p tsconfig.typetests.json).
  • check:i18n-keys, check:control-bytes, check-changeset-presence, check-changeset-no-major — all green.

Reverse verification, predictions written before the run, fix reverted by
git checkout origin/main -- on the two source files with the tests untouched:
predicted 4 red / 5 green, measured 4 failed | 5 passed, and the four are
exactly the unanswered-read cases:

× ... > does not claim "all caught up" when the inbox read was DENIED
× ... > does not claim it while the session is still settling (no user yet)
× ... > does not claim it while no adapter has been provided yet
× ... > says so even when the approvals half DID answer
✓ ... > CONTROL: a successful, genuinely empty inbox still says "all caught up"
✓ ... > CONTROL: a deployment with no inbox object at all is an ANSWER, not an error
✓ ... > lists the QA payload and badges it, instead of an empty panel
✓ ... > reads the ADR-0030 `mine` window — user-scoped, newest first
✓ ... > badges the approvals-only case without the inbox contributing (#4197 control)

Two of the greens are declared non-discriminating rather than dressed up as
evidence: the happy path was never broken (measured before any edit — nine rows
render and no caught-up copy appears at origin/main), and the 404 control
guards this change's blast radius rather than reproducing the defect.


Generated by Claude Code

…all caught up" (#4235)

useHomeInbox swallowed every failed sys_inbox_message read to [], so a denial
reached HomeActionCenter wearing the shape of an empty inbox. objectstack#7344
measured the mechanism: 403 PERMISSION_DENIED on that object for every non-admin
session, while /api/v1/notifications - a projection of the same rows - answered.

The hook now reports notificationsStatus; the affirmative copy renders only on
'ready'. Source unchanged: ADR-0030 names the table read as the consumer channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 11, 2026 1:45pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 29.6 KB 350 KB
Entry file index-DLxFjnQD.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.88KB 3.25KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 488.96KB 108.42KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 150.04KB 39.79KB
fields (index.js) 228.45KB 56.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 16.38KB 5.47KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.98KB 10.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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.73KB 17.54KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 121.07KB 31.39KB
plugin-designer (index.js) 210.91KB 42.67KB
plugin-detail (index.js) 238.98KB 59.76KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 164.14KB 39.98KB
plugin-grid (index.js) 187.97KB 49.90KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 109.93KB 26.65KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.60KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.96KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.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 (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant