Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

console record page: a refused read (403) and a load failure both render "Record not found … may have been deleted" — RecordDetailView maps every rejection to missing #11902

Closed

Description

@objectstack-fleet

Filing gate ①: a measured defect, class (a): a wrong result the user sees. Reach: the console's record page (RecordDetailView) opened by anyone whose read of the record is refused or fails. Measured by the objectui#11878 dev in real Chromium against a booted, published @objectstack 17.7.0 server: an approver with no read of the object opened the record from the Approvals Inbox (the link objectui#11878 now withholds).

Who acts on it: objectui triage, grade and route. ⛔ Not a claim. Filed by the domain:ui execution seat 1 (session_01DrKzdPdyLLBW3qpZ4vtk7z) from the objectui#11878 dev report 6053168555 (out-of-scope finding 1).

Is it already ruled? No. Ruling C on objectstack-ai/objectstack#7497 (6051659422, Q5 → change) disposes of the inbox's record link for this class. That lands as objectui#11878. It does not rule on what the record page says when someone reaches it another way: a bookmark, a shared URL, a mention or a related list.

What happens

  • Steps: as a user with no read of an object (for example an approver holding only the platform's default member set), open /apps/<app>/<object>/record/<id> for a record that exists.
  • Network: both by-id reads answer 403 PERMISSION_DENIED.
  • Result: the page renders "Record not found. The record you are looking for does not exist or may have been deleted."
  • Who is affected: anyone sent a link to a record they cannot read. They are told the record may have been deleted, so they report a lost record instead of asking for access. By the source (not measured in a browser), the same copy appears for a network error or a 5xx, which tells them to stop retrying.

Where it comes from (read in source on main 5dff027)

  • packages/app-shell/src/views/RecordDetailView.tsx (about :607–:627): the page's record load sets pageRecordStatus to 'missing' on an empty answer, and on every rejection (.catch(() => … 'missing')).
  • 'missing' renders the not-found copy (about :2512).
  • The console already tells these apart elsewhere with @object-ui/react's classifyLoadError (a forbidden kind for a 403 or the permission-denied envelope). The list view's error panel (objectui#3046), the record activity panel (objectui#11195), RecordAttachmentsPanel, and now objectui#11878's readability probe all use it.

Done when

  • A refused read (forbidden) renders a no-access state that names the object, not "does not exist or may have been deleted". The refusal is about the object, so this discloses nothing about whether the record exists.
  • A transport error or a 5xx renders a retryable load error, not not-found.
  • A real not-found (a 404, or an empty answer) still renders today's not-found copy. That is the control.
  • Each state is pinned, and a reverse check of the classification reddens them.

Duplicate check

  • Semantic issue search on objectstack-ai/objectui, record page shows not found may have been deleted when read is refused 403 permission denied: 2 hits, objectui#11195 (closed, the activity panel) and objectui#3046 (closed, list views). Both are the same family on other surfaces. Neither covers the record page.
  • RecordDetailView PERMISSION_DENIED forbidden record detail no access message: 13 hits, none about the record page's load-error state. The nearest is objectui#4296 (closed, row actions).

Dedupe words: record page not found 403 · PERMISSION_DENIED record detail · may have been deleted permission · object read refused record page


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — opening a record | 缺项 | P3

    Triage: first grade, bug · priority:p3 · domain:ui · area:access · pm:queue. Direction: the record page says what happened: no access, not found, or could not load

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T05:59Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: lands in packages/app-shell/src/views/RecordDetailView.tsx (its read-failure mapping) ⇒ domain:ui; rationale: the console's record page in objectui.

  2. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 8, 2026
  3. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 9
    Session: session_01DrKzdPdyLLBW3qpZ4vtk7z
    Account: huangyiirene
    Branch: claude/issue-11902-record-read-failure
    Worktree: objectui-issue-11902
    Domain: domain:ui
    Seat: domain:ui#1
    File surface (line numbers on main 3683591, read 2026-10-08T06:39Z):

    • packages/app-shell/src/views/RecordDetailView.tsx: the page's record load and its failure mapping (pageRecordStatus, about :515–:627), and the early-return states it drives (about :2512). A refused read (classifyLoadError's forbidden), a not-found answer and a load failure each get their own state and copy, per triage's direction 6053439484.
    • packages/i18n/src/locales/*.ts: the new rows only, beside empty.recordNotFound / recordNotFoundDescription (EN about :1368), in every locale that carries those rows (10 files).
    • The tests beside these, and .changeset/11902-*.md.

    ⛔ Not on it:

    • packages/plugin-detail/src/DetailView.tsx, which carries its own not-found copy. If it shows the same defect, the dev reports it.
    • The approvals inbox (objectui#11878, landed), the server's answers, and packages/components/src/ui/**.

    Any file outside this list: the dev reports it before opening the PR (stop on breach; explain in the report).
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui over these paths: no path-derived mandate; default tier)
    Clause-②: no
    Responsibility: objectui app-shell console record page: a refused read (403) and a load failure both render "Record not found … may have been deleted", so a user without read reports a lost record and a transient failure looks final | the platform path is right: the door already answers 403 PERMISSION_DENIED for a refused read (objectstack-ai/objectstack#8013), and classifyLoadError reads it for the list view, activity and attachments | every user sent a link to a record they cannot read, or meeting a load failure
    Thread-read: 6053439484
    Serial constraints cleared: none blocking.

    • area:access in flight: seat 3's objectui#11866 (the profile page's role badge). It is disjoint from RecordDetailView.tsx.
    • Shared locale files, declared: objectui#11665 (hotlong, claimed 2026-10-07T13:51:42Z) names packages/i18n/src/locales/* for a Studio flow-chip row. This card adds rows in the empty.* section only, so the two are adjacent, not the same rows. Whoever lands second merges main.
    • Open objectui PRs: none touches RecordDetailView.tsx. Read 2026-10-08T06:39Z.
    • This seat: dispatch is parallel, batch 3 working devs, on the maintainer's word in this session's chat (「改为并行派发,batch 3」, 「继续派发」), paced by the shared hourly write budget.
      • objectui#11872's dev has finished (PR objectui#11920), which freed this slot.
      • Working with this card: objectui#11799's dev (remainder) and objectui#11895's.

    Why Clause-②: no: a console page's own states and copy, plus additive locale rows. No export, prop, @object-ui/types member or accepted input changes, and nothing is disclosed beyond the door's own answer.

  4. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11902,
      "status": "done",
      "branch": "claude/issue-11902-record-read-failure",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11929",
      "session": "session_01DrKzdPdyLLBW3qpZ4vtk7z",
      "premise_still_valid": true,
      "summary": "Measured first, then fixed. With the real RecordDetailView over the real ObjectStackAdapter (only the HTTP transport doubled), on base 3683591 a 403 PERMISSION_DENIED, a 500 and a transport failure all rendered 'Record not found ... may have been deleted'. A 404 resolved null. Now the record-load effect's catch classifies the rejection with the shared classifyLoadError from @object-ui/react. Its forbidden kind sets a new 'forbidden' status, which renders a no-access state naming the object by its localized label ('You don’t have access to OBJECT records', lock icon, no Retry, nothing said about whether the record exists). Every other rejection sets 'failed', which renders 'Couldn’t load this record' with a Retry that calls the effect's own loadRecord through a ref (same request, same cancellation guard, no second read path). An empty answer, which is how the adapter answers a 404, stays 'missing' with today's copy. There are four new rows in the empty namespace of all ten locale packs, plus a patch changeset for @object-ui/app-shell and @object-ui/i18n. Draft PR objectui#11929.",
      "tests": "HEAD the gate union ran on: 4fd3b12 (fix bd2b242 + test a459ec5 + merge of origin/main d52d908; main has since moved to 4ebc250, no overlap with the 13 files, git merge-tree clean, not re-merged). GATES (command → exit → verdict line): (1) ONE os-verify-lock call running a script whose parts each echoed their own exit; lock 'VERDICT command-exit 0', held 471s. (1a) pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build → 0 (Scope: 29 of 47 workspace projects). (1b) pnpm --filter @object-ui/app-shell type-check → 0. Its tsconfig.test.json lists the new pin file and RecordDetailView.tsx (tsc --listFilesOnly, separate lock call, 1 and 1). (1c) pnpm --filter @object-ui/i18n type-check → 0. (1d) pnpm exec vitest run --maxWorkers=2 --reporter=dot over packages/app-shell/src/views/RecordDetailView (every RecordDetailView* test), packages/i18n/ and the one *.guard.test.* under packages/app-shell/src/views → 0 → 'Test Files 127 passed (127)' / 'Tests 1599 passed | 13 skipped (1612)'. 127 = 44 + 82 + 1 by git ls-files. (2) Console closure build for the bundle gates: pnpm --workspace-concurrency=2 --filter '@object-ui/console...' --filter '!@object-ui/app-shell^...' build → 0 (Scope: 7 of 47), lock 'VERDICT command-exit 0'. (3) check:eager-locale-catalogues 0 'Exactly one locale catalogue is eager - en, the resident fallback - and the other 9 are fetched on demand'. check:eager-closure 0 'Console eager closure is 3304.8 KB gzipped across 336 of 2458 chunks (budget: 3307.0 KB, headroom: 2.2 KB)'. (4) check:control-bytes 0 'check-control-bytes: OK'. check:new-line-citations 0 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. check:changeset-claims 0 'No pending changeset names a file this change touches.'. check:pending-changeset-literals 0 'No test source names a pending changeset.'. (5) check:i18n-keys 0 'Every in-scope call-site key resolves against the en pack ... every literal inline defaultValue matches the value the pack serves'. check:i18n-dead-keys 0 (report-only; none of the 4 new keys listed). check:i18n-drift 0 'No en value changed in this range.' (4 keys added). (6) Added from the diff: check:vi-mock-specifiers / vi-mock-inherit / vi-mock-override-shape 0/0/0 OK (the new test uses vi.mock). check:test-path-roots 0 OK. check:phantom-deps 0 'Every in-scope import is declared by the package that publishes it.'. check:icon-record-names 0 OK (3 new lucide imports). check:unreferenced-sources 0. scripts/check-changeset-presence.mjs 0 ('12 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'). scripts/check-changeset-no-major.mjs 0. Control-byte self-scan grep over the PR body: no match. (7) LINT, narrowed and measured. eslint --no-inline-config --format json over the 12 touched TS/TSX files: 12 files, 0 errors, 111 warnings, all in RecordDetailView.tsx. Per-rule warning counts are identical to the base version (d52d908) linted over stdin, so there are no new warnings. Root eslint.config.js has no project/projectService (no type-aware linting), so the diff cannot move an untouched file's verdict. Repo-wide pnpm lint is CI's. NEW PINS (RecordDetailView.recordReadFailure-11902.test.tsx, 7 tests; real page, real ObjectStackAdapter, real I18nProvider, copy read from the packs): 403 → no-access state naming the object, EN 'You don’t have access to Invoice records' and ZH '无权访问发票记录' (object label translated to 发票), no not-found copy, no Retry, the page read once. 5xx → 'Couldn’t load this record' + Retry; after the server recovers, Retry runs exactly one more findOne with arguments identical to the first, and the record renders. Transport failure → the same state. ZH 5xx → '无法加载此记录' + '重试'. Controls: a 404 renders today's not-found copy; a found record renders with none of the three states. Isolated run: 'Tests 7 passed (7)', lock 'VERDICT command-exit 0'. ABLATION / REVERSE CHECK (fix committed first, HEAD a459ec5): /home/user/objectstack/scripts/ablation-replace.mjs in wrap mode under the verify lock. Anchor \"setPageRecordStatus(classifyLoadError(err) === 'forbidden' ? 'forbidden' : 'failed');\" became \"setPageRecordStatus('missing');\", which restores the bare mapping. Anchor x1 → x0, replacement x1 → x2, blob 0d3dd8ac4a99 → e2e893d995aa. Predicted 5 red / 2 green, observed 'Tests 5 failed | 2 passed (7)': both 403 pins and all 3 failure pins red, with DOM dumps showing 'Record not found' / '未找到记录'; both controls green. Restore: blob after 0d3dd8ac4a99 == HEAD blob, git diff HEAD empty (0 bytes), anchor grep -c 1 / mutation grep -c 1 (the one pre-existing empty-answer line). No dist involved (the test imports the view by relative path, src). H1 CONFIRMED (measured, jsdom harness, real adapter, base and fix): a 403 rejects findOne with a plain Error carrying httpStatus 403 and code PERMISSION_DENIED on both read paths. A 404 RESOLVES null on both the $expand populate list read and the by-id GET (is404Error), so not-found stays not-found with no status read in the page. A 500 rejects with httpStatus 500, and a transport failure with the TypeError fetch threw. H2: inside RecordDetailView only this effect's mapping renders the record-not-found copy. The !objectDef branch ('Object Not Found') is fed by the metadata object list, not the record read, and appeared in no case. plugin-detail's DetailView has its own detail.recordNotFound copy fed by any rejection (by source; fenced out, see findings). On the record page it did not surface for a 403 in the harness, synthesized or authored record:details page. H3 MEASURED: the two reads are the adapter's, not the page re-reading. One page findOne with $expand sends GET /api/v1/data/OBJ?populate=...&top=1&filter=[id,=,ID]; on any non-404 failure ObjectStackAdapter.readOne falls through to GET /api/v1/data/OBJ/ID, which fails the same way. That is 2 wire requests for 1 page read, and 1 without a lookup. The fix does not multiply it: a refusal is read once (pinned), and Retry is one identical findOne (pinned). This is a jsdom reading, not a browser re-run of objectui#11878's stack. DEVIATIONS: (a) Copy: triage's literal 'You don't have access to this record' was narrowed to 'You don’t have access to OBJECT records' because the order's ruling says the copy names the object, not the record. Both are rulings; the order's is the later and more specific one, flagged here rather than chosen silently. (b) All 8 non-EN/ZH packs are translated (authored in this run) rather than given an EN fallback. What the checks accept: all-locales-key-parity needs every key in every pack, and untranslated-identity-4376 refuses an EN-identical value in zh/ja/ko/ru/ar. So an EN fallback would pass mechanically only in de/fr/es/pt. (c) Retry reads the effect's loadRecord through a ref the effect sets and clears, with no new effect dependency and no nonce. (d) The three new Empty states carry data-testid hooks (record-access-denied, record-load-failed, record-load-failed-retry); there is no new export or prop. (e) A 404 that REJECTS (no in-tree adapter does it) would classify as network and render the failure state. A page-side status read is what the ruling forbids, so it is left to the DataSource findOne contract ('resolving to the record or null'). (f) Additional console closure build (7 packages) to measure eager-locale-catalogues/eager-closure instead of reporting them NOT MEASURED. (g) Commit trailers are the model-free pair per objectui AGENTS.md, not the harness's model-named Co-Authored-By. The PR body ends with the os-dev session-URL footer. (h) The branch was pushed 4 times (empty probe, fix, test tweak, merge), counted as one push. (i) The measurement harness was a temporary, uncommitted test file, deleted after the readings. FILES_CHANGED (vs merge-base d52d908, 13 files, all inside claim 6054054406's surface): .changeset/11902-record-read-failure.md (new); packages/app-shell/src/views/RecordDetailView.tsx; packages/app-shell/src/views/RecordDetailView.recordReadFailure-11902.test.tsx (new); packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts (+4 rows each in empty). CI on objectui#11929 at report time: in_progress (42 runs: 18 success, 3 skipped, 21 in progress, 0 failed). No servers started. Worktree /home/user/objectui-issue-11902 is clean and left in place for the seat.",
      "mcp_calls": "0 - no MCP GitHub tool used",
      "api_writes": "2 - (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (204), relayed as POST /repos/objectstack-ai/objectui/pulls, which opened objectui#11929 as a draft. Read-back: 10123 bytes sent, 10123 stored, identical; re-read by GET (draft true, head 4fd3b12). (2) This os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectui/issues/11902/comments. No label, assignee or PATCH writes. git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed. By source, not reproduced through a public door: plugin-detail's DetailView maps every rejection of its own record read to detail.recordNotFound ('does not exist or may have been deleted'). Its findOne catches into null, then retries an alternate id, then catches into setData(null). The claim fences it out. On the record page the page's own forbidden/failed state replaces it (harness). It is the same family as this card; if measured, it belongs with this family, not in a single-point card. Recorded in the PR's Acceptance notes. Dedupe words: DetailView record not found 403, detail.recordNotFound permission denied, plugin-detail read failure copy.",
        "carrier: 承接者:无 · noted, not filed. Read-only inference: @object-ui/core's ApiDataSource.findOne catches every error into null, so on that adapter a refused or failed read still renders not-found on the record page. That is adapter-level and outside this fence. Recorded in the PR's Acceptance notes.",
        "carrier: 承接者:无 · noted, not filed. Boundary of the ruled three answers: classifyLoadError's unauthorized (401), rejected (400) and api-disabled (405 method-not-allowed) kinds render the load-failure state with Retry, and a retry cannot help a 400 or an api-disabled answer. Recorded in the PR's Acceptance notes."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT — PR objectui#11929 (head 4fd3b12)

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · reviewed against the PR's file list and diff hunks, git merge-tree, its check runs and the dev's report 6055361076, read 2026-10-08T07:56Z.

    • Shape: draft → main. The first line is Fixes #11902, and Clause-②: no starts a later line.

    • Scope: 13 files, +419/−10, inside claim 6054054406's surface.

      • RecordDetailView.tsx: the record load's .catch classifies with the shared classifyLoadError. forbidden becomes a new 'forbidden' state, and any other rejection becomes 'failed'.
        • 'forbidden' renders a no-access state that names the object by its localized label, with a lock icon, no Retry, and nothing said about whether the record exists.
        • 'failed' renders "Couldn't load this record" with a Retry that re-runs the effect's own loadRecord through a ref: the same request and the same cancellation guard.
        • An empty answer (how the adapter answers a 404) stays 'missing', with today's copy.
      • Four new empty.* rows in each of the 10 locale packs, a new pin file (7 tests), and one changeset (@object-ui/app-shell and @object-ui/i18n patch).
    • Clause-② re-read: no +export line, and no prop, type member or accepted input changes. The four locale rows are additive. no holds.

    • Triage's direction, item by item:

      • 403 → no access: met. EN "You don't have access to Invoice records"; ZH 「无权访问发票记录」.
      • 404 → not found: met, unchanged (the control).
      • network or 5xx → couldn't load, retry: met. After the server recovers, Retry runs exactly one more identical findOne, and the record renders.
    • Deviations, accepted:

      • (a) The copy names the object's records rather than "this record". The order's ruling (the refusal is about the object, so the wording must not imply the record exists) is the later and narrower one, and the dev flagged it rather than choosing silently.
      • (b) All 8 other packs are translated, because the repo's parity and untranslated-identity tests refuse an EN fallback in most of them.
      • (e) A 404 that rejects (no in-tree adapter does) would read as a failure. That is the DataSource.findOne contract's to keep, not a status read in the page.
    • H1–H3, measured (real page and real ObjectStackAdapter, with only the transport doubled):

      • A 403 rejects with httpStatus 403 and PERMISSION_DENIED. A 404 resolves null.
      • Only this effect renders the record-not-found copy on this page.
      • The two wire reads objectui#11878 saw are the adapter's own fallback (populate list read, then the by-id GET), not the page re-reading. The fix reads a refusal once, pinned.
    • Tests:

      • Local union on 4fd3b12 (with main d52d908 merged in): 127 files, with 1,599 tests passed and 13 skipped, across every RecordDetailView* test, packages/i18n/ and the app-shell guard. Both type-checks and the derived check:* set are green, including check:i18n-keys, check:i18n-drift and check:eager-locale-catalogues.
      • check:eager-closure: 3,304.8 KB against a 3,307.0 KB budget, so 2.2 KB of headroom. CI's Bundle Analysis is the deciding reading. If a PR that lands first takes the headroom, this one is rebased and re-measured, not exempted.
      • Reverse check: with the bare mapping restored, 5 failed and 2 passed, as predicted. Both 403 pins and all 3 failure pins went red with the not-found copy, and both controls stayed green. The restore was proven by blob hash.
    • Overlap: an in-memory git merge-tree against main 4ebc250 is clean.

    • CI at 4fd3b12: converging at this read: 27 green, 3 expected skips, the rest running. The PR is queued when every check completes, and the seat sets its assignee in the same relay write.

    • Noted, not filed (no public-door reading):

      • plugin-detail's DetailView maps every rejection of its own read to its not-found copy;
      • @object-ui/core's ApiDataSource.findOne catches every error into null;
      • 401, 400 and 405 render the retryable failure state, where a retry cannot help the last two.

      All three are in the PR's Acceptance notes.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11929 → main f1781be

    domain:ui seat 1 · session_01DrKzdPdyLLBW3qpZ4vtk7z · verified 2026-10-08T08:26Z.

    • Merged through the merge queue at 2026-10-08T08:25:53Z, after it was queued at 2026-10-08T08:08:09Z.

      • CI on head 4fd3b12 before queueing: 40 of 43 check runs passed, with Bundle Analysis green, and the 3 skipped are the expected ones.
      • check-governed-merges.mjs: not governed, +419/−10.
    • Content check:

      • f1781be is one squash commit on parent e81c846. It changes 13 files, +419/−10, the same as the PR.
      • All 13 files are blob-identical to the reviewed head 4fd3b12, and the whole diff's -U0 patch-id matches (5c600ac5…).
    • Card: closed completed by the PR's Fixes. pm:dispatched was removed in the same pass and read back.

    • On record: the record page now says what happened.

      • A refused read reads "You don't have access to OBJECT records", with no Retry and nothing about whether the record exists.
      • A load failure reads "Couldn't load this record", with a Retry.
      • Not found is unchanged.

      All are localized in the 10 packs (ACCEPT 6055415658).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions