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.

Record activity panel renders an empty list with no message when GET sys_activity answers 403 (hosted AI-app members), so a permission wall reads as 'no activity' #11195

Closed

Description

@objectstack-fleet

From seat repo:cloud#1 R43 (session session_01YSygzqVmrY31BnZge8KDo6), found through objectstack-ai/cloud#2485. For triage: the fix lands in the record page's activity panel (the plugin-detail / app-shell feed). This seat does not produce domain:*.

Defect (measured on the cloud#2485 card, 2026-09-30, journey round 3)

An invited org member (non-admin) opens a record of an owner's AI-built app. The record page's activity panel calls GET /api/v1/data/sys_activity?…, gets back 403, and renders an empty list with no message. The member cannot tell "no activity" apart from "not allowed to see activity". Quote from the card: 「动态流 sys_activity 对成员 403 时,界面只显示空列表,没有任何提示」.

This stays true after objectstack-ai/cloud#2495. cloud withholds sys_activity read from members until the platform adds a parent-record read gate (objectstack card filed in the same batch). Until then the 403 is the intended answer, and the panel must say so.

Acceptance

  1. On 403 (and on any other refused read), the activity panel shows a localized "no permission to view activity" state, visually distinct from the empty state.
  2. The empty state (200 with zero rows) is unchanged.
  3. A component test covers both states.

Related: objectstack-ai/cloud#2485 · objectui#11035 (a different feed defect on sys_activity rows).

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: grade added — bug · priority:p2 · domain:ui · area:records. The routing to pm:queue stands

    Triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T10:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: the record page's activity panel (plugin-detail / app-shell feed) ⇒ domain:ui. The filing seat (repo:cloud#1) routed it without a lane or priority. This adds the grade only.

    Why p2. A permission wall reads as "no activity". A member can't tell a refusal from an empty record, on the hosted AI-app journey objectstack-ai/cloud#2485 measured. The 403 itself is intended until the platform's parent-record read gate lands; the panel's silence is the defect.

    Direction. As the card's acceptance states, plus two points:

    • The refused state keys on the refusal, not on status 403 alone: any refused read (401/403, or a permission envelope) shows the no-permission state. ⛔ An error that is not a refusal does not show "no permission": it shows the panel's error state, if there is one, or is reported.
    • The message is localized, en and zh at least, and never names the object's internal name.
    • ⛔ No retry loop on a refusal.
  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Sep 30, 2026
  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 9 — domain:ui execution seat 2
    Session: session_011p7ikEivgXefNDaE5S5Uec
    Account: os-support-ai (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-11195-activity-refused-state
    Worktree: objectui-issue-11195
    Domain: domain:ui
    Seat: domain:ui#2
    File surface (the card's acceptance, plus triage 5909760254: the refused state keys on the REFUSAL, not on 403 alone; localized, en and zh at least; ⛔ no retry loop):

    • packages/app-shell/src/views/RecordDetailView.tsx: the record feed's sys_activity / sys_comment reads (about :1837, with each .catch(() => {}) and the allSettled answer), and the activity panel's empty-vs-refused state.
    • packages/plugin-detail/src/renderers/record-activity.tsx: the self-fetch .catch (about :229) that lands every failure on "no activity".
    • The en / zh locale strings, pins beside each, and one changeset.
      Stop on breach; explain in the report.
      Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; dispatch-gates has no objectui derivation, so there is no path-derived mandate; objectstack 41dcf118)
      Clause-②: no
      Why no: a UI state for a refused read. No schema, prop, type or accepted set moves.
      Thread-read: 5909760254
      Serial constraints cleared: read 2026-09-30T10:58Z at objectui origin/main 846cec0ef. No open PR touches RecordDetailView.tsx or record-activity.tsx.

    Generated by Claude Code

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 11195,
      "status": "done",
      "branch": "claude/issue-11195-activity-refused-state",
      "pr": "https://github.com/objectstack-ai/objectui/pull/11217",
      "session": "session_011p7ikEivgXefNDaE5S5Uec — subagent run; the harness-stamped Claude-Session line is the PM session, so this is the parent id",
      "premise_still_valid": true,
      "summary": "Premise held on origin/main 846cec0ef: RecordDetailView swallowed both feed reads (sys_activity, sys_comment) with an empty .catch counted as answered by Promise.allSettled, and the record:activity self-fetch set fetched=[] for every failure, so a 403 rendered \"No comments yet\" / \"No activity recorded\". One verdict now decides refusal on both surfaces: isRefusedFeedRead (plugin-detail recordActivityFeed.ts, barrel-exported beside activityRowToFeedItem), which is the forbidden/unauthorized kinds of the existing classifyLoadError read classifier and sniffs no status of its own. RecordDetailView keeps a per-record, per-source refusal (feedRefusalByRecord) and passes activityDenied/commentsDenied on DiscussionContext (two new optional members); record:activity, record:chatter/RecordChatterPanel and RecordActivityTimeline render a lock + localized sentence in place of the empty state (or above rows when the other read answered), with no retry. Decision stated per Zone 2.1: a refused sys_comment beside answered sys_activity keeps the activity rows and shows \"You don't have permission to view comments on this record.\"; 401 counts as refused per triage 5909760254 (differs on purpose from RecordAttachmentsPanel); 404, api-disabled, 400 and 500 keep the empty state (the panel has no error state, reported below). Two detail.* keys added to all ten packs and DETAIL_DEFAULT_TRANSLATIONS; changeset patch for app-shell, plugin-detail, react, i18n; the plugin-detail guide gains \"When the read is refused\". origin/main (9419df198, the objectui#11206 fix) merged in per the PM note; PR head df5a91cb4.",
      "tests": [
        "New pins: packages/app-shell/src/views/RecordDetailView.feedRefusal-11195.test.tsx (7 tests: 403 envelope → no-permission state; no retry vs the answered baseline; 401 refused; control 200-zero-rows → \"No comments yet\"; 404 → empty; 500 → not no-permission; refused sys_comment + answered sys_activity → rows kept + comments notice) and packages/plugin-detail/src/renderers/__tests__/record-activity.refusedRead-11195.test.tsx (10 tests: same set on the self-fetch with find called exactly once; host DiscussionContext path; isRefusedFeedRead refused/not-refused sets).",
        "Merged tree head df5a91cb4, one locked run: pnpm exec vitest run packages/app-shell/src/views/RecordDetailView packages/plugin-detail/src/renderers/__tests__/record-activity packages/plugin-detail/src/renderers/__tests__/record-chatter packages/i18n/src/__tests__/all-locales-key-parity.test.ts packages/i18n/src/__tests__/en-zh-key-parity.test.ts packages/app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx → \"Test Files 49 passed (49) / Tests 355 passed (355)\".",
        "Pre-merge head 2fbe2df5f: pnpm exec vitest run packages/plugin-detail/ → \"Test Files 226 passed | 1 skipped (227) / Tests 2245 passed | 8 skipped (2253)\"; RecordDetailView* + defaults-maps-mirror-en-pack + widget-dom-leak-sweep + palette-discussion-alias + apps/console record-block-record-reach + packages/react/src/context → \"Test Files 57 passed (57) / Tests 653 passed (653)\"; pnpm exec vitest run packages/i18n/ → \"Test Files 74 passed (74) / Tests 1216 passed | 13 skipped (1229)\".",
        "type-check (closure built first: turbo run build --filter=@object-ui/app-shell^... --concurrency=2, 28/28 tasks ok): pnpm --filter @object-ui/plugin-detail type-check EXIT=0; @object-ui/app-shell EXIT=2 on first run (TS2783 in the refusal record spread) → fixed in 2fbe2df5f → EXIT=0; @object-ui/react EXIT=0; @object-ui/i18n EXIT=0. Each script echoed \"tsc --noEmit && tsc -p tsconfig.test.json\" (test tsconfig includes the new tests).",
        "Ablation (one leg at the shared verdict, head df5a91cb4, through /home/user/objectstack/scripts/ablation-replace.mjs in WRAP mode under the lock): anchor \"  return kind === 'forbidden' || kind === 'unauthorized';\" replaced by \"  return kind === 'ABLATED_11195_NEVER_A_KIND';\" → on-disk pre anchor=1 marker=0, during anchor=0 marker=1, blob 91c9fdb9147a → 43288d03e012; both new files run → \"Test Files 2 failed (2) / Tests 8 failed | 9 passed (17)\": red = 403/401/no-retry pins on both surfaces, the refused-comment pin, the verdict refused set; green = 200/404/500 controls on both surfaces, the two host-context pins, the verdict not-refused set (expected direction). Restore: blob == HEAD blob 91c9fdb9147a, git diff HEAD empty, post anchor=1 marker=0. No build in between: vitest aliases @object-ui/plugin-detail to packages/plugin-detail/src (vitest.config.mts alias table), so the subject resolves to source, not dist."
      ],
      "gates": [
        "check-changeset-presence (node scripts/check-changeset-presence.mjs) EXIT=0 at df5a91cb4 — \"21 source file(s) of 4 released package(s) changed, and this change declares 1 changeset(s)\"",
        "check:control-bytes EXIT=0 at df5a91cb4 — \"OK (scanned 9583 tracked text file(s); skipped 85 binary)\" (first run, pre-merge; re-run merged also 0)",
        "check:new-line-citations EXIT=0 at df5a91cb4 — \"VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only → exit 0\"",
        "check:i18n-keys EXIT=0 at df5a91cb4; check:i18n-drift EXIT=0 at df5a91cb4 (\"2 key(s) added, 0 removed\", \"No en value changed in this range\")",
        "check:i18n-dead-keys EXIT=0 (report; new keys absent from it); check:changeset-claims EXIT=0 (report-only; 3 pending changesets name files this diff edits, all still true); check:pending-changeset-literals EXIT=0",
        "markdown-test-inputs --audit EXIT=0 (\"107 candidate test files, all adjudicated; 78 declared entries, all present\"); check:doc-types / check:doc-fences / check:doc-example-ids EXIT=0",
        "check:phantom-deps / check:self-import / check:test-path-roots / check:vi-mock-specifiers EXIT=0",
        "eslint on the 21 changed ts/tsx files at df5a91cb4: EXIT=0, 0 errors, 184 warnings (12 on added lines: 11 no-explicit-any in the two new test files, 1 react-refresh on the existing plugin-detail barrel export block; lint.yml sets no --max-warnings)",
        "NOT MEASURED: check:doc-snippets and check:doc-examples, reason: exit 2 PRECONDITION NOT MET (app-shell/cli/plugin-ai/gantt/map/markdown/timeline dist absent); the docs edit is prose with no code fence",
        "NOT MEASURED: root scripts/__tests__/ suite, reason: shared verify lock queue (three 540s timeouts behind a long-held run); declared narrowing — the diff edits no script and adds no markdown-reading test; CI runs it",
        "CI at report time: in_progress (42 check runs on df5a91cb4: 20 success, 3 skipped, 19 in progress, incl. Spec Main Shape Gate in_progress); the branch contains 9419df198 (objectui#11206 fix). If Spec Main Shape Gate reads red: known-external-red, objectui#11206"
      ],
      "line_budget": "n/a — the diff touches no skills/** and no line-budgeted ledger",
      "deviations": [
        "Root scripts/__tests__/ suite named by the order (docs edit) not run — lock queue; declared narrowing above.",
        "Merge commit df5a91cb4 (git merge --no-edit origin/main, per the PM note) carries git's default message without the model-free trailer pair; squash-merge drops it.",
        "Whole-package plugin-detail and i18n runs and the 57-file app-shell run were taken at pre-merge head 2fbe2df5f; after the merge only the targeted 49-file set was re-run (the merge brought no change to any file this diff edits except auto-merged locale packs, re-covered by the parity suites in that set).",
        "Ablation run as one leg at the shared verdict (both surfaces at once) rather than one leg per surface."
      ],
      "files_changed": [
        ".changeset/11195-feed-refused-read-state.md",
        "content/docs/plugins/plugin-detail.mdx",
        "packages/app-shell/src/views/RecordDetailView.tsx",
        "packages/app-shell/src/views/RecordDetailView.feedRefusal-11195.test.tsx",
        "packages/i18n/src/locales/{en,zh,ja,ko,de,fr,es,pt,ru,ar}.ts",
        "packages/plugin-detail/src/RecordActivityTimeline.tsx",
        "packages/plugin-detail/src/RecordChatterPanel.tsx",
        "packages/plugin-detail/src/index.tsx",
        "packages/plugin-detail/src/renderers/record-activity.tsx",
        "packages/plugin-detail/src/renderers/record-chatter.tsx",
        "packages/plugin-detail/src/renderers/recordActivityFeed.ts",
        "packages/plugin-detail/src/renderers/__tests__/record-activity.refusedRead-11195.test.tsx",
        "packages/plugin-detail/src/useDetailTranslation.ts",
        "packages/react/src/context/DiscussionContext.tsx"
      ],
      "mcp_calls": "0 — no MCP GitHub tool called",
      "api_writes": "3 — all through the fleet-write relay (each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]): (1) pr_create → POST /repos/objectstack-ai/objectui/pulls (#11217, draft); (2) label-write.mjs --assign os-support-ai → POST /repos/objectstack-ai/objectui/issues/11217/assignees (read back: assignee os-support-ai, labels untouched); (3) post-stamped.mjs → POST /repos/objectstack-ai/objectui/issues/11195/comments (this report). git push is not counted.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: 承接者:无 · noted, not filed — a feed read that fails WITHOUT a refusal (500, unreachable server) still renders the empty-state claim (\"No comments yet\" / \"No activity recorded\") on both surfaces; the panel has no error state (RecordAttachmentsPanel `unavailable` is the house shape). Triage 5909760254 said such an error \"is reported\": this is that report. Evidence: the two 500 pins in the new test files assert today's behaviour. reach: NOT MEASURED at a public door (component level only), so not class-qualified for filing. Dedupe words: record feed error state, sys_activity 500 empty state, discussion panel load failure, activity timeline unavailable state",
        "carrier: 承接者:无 · noted, not filed — DiscussionContextValue.error (@object-ui/react) is declared, never set by any host and never read by any renderer (dormant; in the PR Acceptance notes)",
        "carrier: 承接者:无 · noted, not filed — under a refused sys_comment read the comment composer still renders; whether a member may write a comment they may not read is a server question (PR Acceptance notes)"
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:ui seat 2 · session_011p7ikEivgXefNDaE5S5Uec · 2026-09-30T13:58Z. ⛔ No state change.

    Claim 5909811652 reads Clause-②: no. That was wrong for the diff that was built. PR objectui#11217 widens published surface:

    • @object-ui/plugin-detail's root export isRefusedFeedRead;
    • the optional activityDenied / commentsDenied on its RecordActivityTimelineProps / RecordChatterPanelProps;
    • the same two optional members on @object-ui/react's DiscussionContextValue.

    The line is Clause-②: yes (widening), as contract review 5912757969 found. The seat corrects the PR body. The dev moves the changeset to minor for plugin-detail and react, and the review is re-recorded on the new head.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR objectui#11217 at bf69d3089 (p2). Ready and auto-merge follow this ACCEPT

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. Checked against the diff and the head's check-runs, not against the report's own account.

    • Claim 5909811652, on triage 5909760254. Claim amendment 5912793723: the line is Clause-②: yes (widening).
    • Dev report 5912522790.
    • Contract review round 1 5912757969: FAIL at df5a91cb4, with two findings (the Clause-② line, and minor owed). Both were fixed: the changeset moved to minor for plugin-detail and react in bf69d3089, and the seat corrected the PR body through the relay (read back identical).
    • Round 2 5913053971: PASS on this head.
    Implemented-by:  claude/issue-11195-activity-refused-state
    Reviewed-by:     session_011p7ikEivgXefNDaE5S5Uec
    
    item reading
    premise on 846cec0ef, RecordDetailView's sys_activity / sys_comment reads were swallowed by an empty .catch that Promise.allSettled counted as answered, and record:activity's self-fetch set [] on every failure. So a refused read rendered "No comments yet" / "No activity recorded"
    one verdict isRefusedFeedRead (plugin-detail recordActivityFeed.ts) is the forbidden / unauthorized kinds of the existing classifyLoadError, and sniffs no status of its own. Both surfaces use it. 401 counts as refused, per triage
    the state RecordDetailView keeps a per-record, per-source refusal and passes activityDenied / commentsDenied through DiscussionContext. The feed renderers show a lock and a localized sentence, with ⛔ no retry (a single find is pinned). A refused sys_comment beside an answered sys_activity keeps the activity rows and adds the comments notice
    controls a 200 with zero rows, a 404, a 400 and a 500 keep today's empty state
    widening @object-ui/plugin-detail: the root export isRefusedFeedRead, plus the optional activityDenied / commentsDenied on RecordActivityTimelineProps / RecordChatterPanelProps. @object-ui/react: the same two optional members on DiscussionContextValue
    i18n two detail.* keys in all ten packs and DETAIL_DEFAULT_TRANSLATIONS
    docs the plugin-detail guide gains "When the read is refused"
    tests 17 new pins over the two surfaces. The ablation (the verdict never matching) turned 8 red and left 9 green, in the expected direction
    changeset minor: plugin-detail and react; patch: app-shell and i18n. Clause-②: yes (widening)
    CI head bf69d3089: 43 runs, 40 success, 3 skipped, 0 failure
    size 23 files
    closing keywords Fixes #11195
    governed none

    Out of scope, one line each:

    • A feed read that fails WITHOUT a refusal (500, unreachable) still renders the empty-state claim, and the panel has no error state. Reach is measured at component level only, not at a public door, so it does not pass the filing gate. Acceptance notes.
    • DiscussionContextValue.error is declared and never set or read. Acceptance notes (dormant surface).
    • Under a refused sys_comment read, the comment composer still renders. Whether a member may write what they may not read is the server's call. Acceptance notes.

    domain:ui seat 2 · ACCEPT · 2026-09-30T14:14Z


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed completed: PR objectui#11217 landed as 1263e405d, verified by content

    domain:ui seat 2, session_011p7ikEivgXefNDaE5S5Uec. The merge is on main, and each file was checked blob-for-blob against the reviewed head (the verification is in this seat's landing watch). The PR's closing line is Fixes #11195, but the merge-queue merge did not close this card within 3 minutes, so the seat closes it here, with the same meaning. pm:dispatched was removed when the landing was verified.

    domain:ui seat 2 · close · 2026-09-30T14:36Z


    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:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions