Skip to content

Analytics/dashboard query path resolves no user token — personal ("my …") dashboard widgets are impossible #510

Description

@yinlianghui

Split out of #491 so the platform-level gap is visible on its own and leadership can decide whether to (re-)escalate it to the upstream runtime repo. This is an upstream limitation, not app metadata.

The gap

The list-view data path (/api/v1/data/..., platform-objects) resolves {current_user_id} in filters — the "My Leads / My Tasks / My Deals" list views all work. The analytics path (dashboard widgets / dataset reports, /api/v1/analytics/...) resolves no user token at all: the literal string reaches the SQL WHERE, matches no owner, and the widget renders 0 for every viewer.

This was proven empirically before #491 and is documented in src/apps/crm.app.ts (My Work group note): a "My Day" dashboard was built and removed after a side-by-side test on one dashboard — {current_user} → 0, {current_user_id} → 0, no owner filter → 10,100,081. That note says "Filed upstream", but no issue link was recorded — hence this tracking issue.

(For contrast, the same analytics path DOES resolve the DATE_MACRO_TOKENS vocabulary — verified during #491 via the generated SQL in analytics responses — so this is specifically about user tokens.)

⚠️ The headline above is not quite the mechanism, per the upstream review of objectstack#12376. Token resolution had already landed in @objectstack/core after 17.1.0; what actually produced the measured 0 was two narrower holes — the direct AnalyticsService.query / generateSql door resolved nothing (so the SQL strategy bound the literal while the ObjectQL strategy resolved via the engine), and the dataset-scope channel handed strategies an unresolved copy of the filter which was ANDed beside the resolved one, producing an unsatisfiable owner = $viewer AND owner = '{current_user_id}'. That second one is why the shape was a hard 0 rather than a partial result. The defect was real; the one-line diagnosis in this card was incomplete.

Impact on this app

To close this issue

Once the Restart-when: predicate above is met, restore the personal widget on the service_dashboard table widget and retitle it back to "My Open Cases by Priority".

⚠️ Use owner_id, not owner. Since #548/#677 the owner column no longer exists and the spelling in the original instruction fails with no such column: owner (measured). Copying that line verbatim will misfire.

⭐ Two behaviour changes arrive with the fix, and both help: an unresolvable placeholder now refuses loudly (FILTER_TOKEN_UNKNOWN / FILTER_TOKEN_UNRESOLVED, HTTP 400) instead of charting a plausible zero — so a broken restore will look like an error rather than an empty widget — and the {current_user} misspelling this card records is now caught with a near-miss suggestion rather than silently rendering 0.

⚠️ Verify the restore by having two different reps see different rows. {current_user_id} is presentation scope, not an access boundary — that is the frozen vocabulary's own declaration (objectstack#3594). Row-level security owns the guarantee that one rep cannot reach another's cases; this token only scopes what a widget shows.

Activity

  1. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    rc.2 retest: 已解除 — {current_user_id} resolves to a bound SQL parameter; unknown tokens fail loudly

    Retested on @objectstack/* 17.0.0-rc.2 (current main), by capturing the console's live analytics payload from the Service Overview dashboard and replaying it with variations (the query endpoint takes datasetName + selection, discovered from the real capture):

    runtimeFilter rows generated SQL
    {is_closed:false} (control) 32 WHERE is_closed = $1
    {owner:"<literal admin id>", is_closed:false} 32 WHERE (owner = $1 AND is_closed = $2)
    {owner:"{current_user_id}", is_closed:false} 32 WHERE (owner = $1 AND is_closed = $2) — token substituted to a bound param
    {owner:"{current_user}", …} error ANALYTICS_QUERY_FAILED: Unresolvable filter placeholder "{current_user}". Did you mean "{current_user_id}"?

    Cross-check: all 32 open cases are owner=admin, so token ≡ literal ≡ control is the correct outcome — an unresolved literal string would have matched 0 rows (the rc.0 failure mode). And the unknown-token case now fails loudly with a suggestion instead of silently matching nothing.

    So the platform half of this issue is resolved: the analytics query path does resolve the user token. The CRM half remains open work — service.dashboard.ts still deliberately ships the team-wide table with the source comment explaining personal widgets are impossible, and restoring a "My Open Cases by Priority" widget is exactly the closure task for this issue. That's a small code PR now that the platform supports it.

    Evidence: analytics_captures.json in the acceptance run's w2b directory.


    Generated by Claude Code

  2. yinlianghui commented on Aug 5, 2026

    @yinlianghui
    CollaboratorAuthor

    [17.0-rc2验收复核] 结论:部分复现已解除 —— 平台半(用户 token 解析)已修;CRM 半(恢复个人 widget)仍待收尾

    证据:独立 dev server(端口 4091)POST /api/v1/analytics/dataset/query(case_metrics,admin token,30 条未关闭工单全部 owner=admin):

    runtimeFilter rows 生成 SQL
    {is_closed:false}(control) 30 WHERE is_closed = $1
    {owner_id:"<literal admin id>", is_closed:false} 30 WHERE (owner_id = $1 AND is_closed = $2)
    {owner_id:"{current_user_id}", is_closed:false} 30 同上 —— token 已替换为绑定参数
    {owner:"{current_user}",…} 报错 ANALYTICS_QUERY_FAILED: Unresolvable filter placeholder "{current_user}". Did you mean "{current_user_id}"?

    token ≡ literal ≡ control,且未知 token 响亮报错并给出纠正建议 —— rc.0 的「字面串进 SQL 永匹配 0」失败模式已消失。

    两个恢复 widget 时的注意事项:

    1. 字段名要用 owner_id:Ownership model mismatch: app-authored owner lookups vs the platform's owner_id — reassigning Owner in the UI does not move record access #548/fix: bug 批次修复(PM 派发循环)— #606 #673 #610 #548 #678 #650 已落地 #677 之后 owner 列已不存在,{owner: …} 会直接 no such column: owner(实测)。正文里的恢复指引写的是 filter: { owner: … },照抄会踩坑。
    2. src/dashboards/service.dashboard.ts:268-280 仍是 team-wide "Open Cases by Priority",注释仍引用本单 —— 恢复 "My Open Cases by Priority" 即本单的收尾 PR。

    环境:hotcrm@0899b4f + @objectstack 17.0.0-rc.2

    建议:平台前提已过时;本单保留仅为 CRM 侧恢复个人 widget 的小 PR(按上面两点),落地后可关。


    Generated by Claude Code

  3. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Not closed — the platform half is fixed, but this card's own stated closing condition is unmet.

    This card was on the direct-close list for the GA close-out bookkeeping sweep (#1151, group 3), which authorised closing it on its recorded rc.2 retest readings. Read in full, those readings explicitly withhold the close, so it stays open. Recording why.

    What the readings do establish

    The platform gap is closed. On @objectstack/* 17.0.0-rc.2, replaying the console's live analytics payload against case_metrics with variations:

    runtimeFilter rows generated SQL
    {is_closed:false} (control) 30 WHERE is_closed = $1
    {owner_id:"literal admin id", is_closed:false} 30 WHERE (owner_id = $1 AND is_closed = $2)
    {owner_id:"{current_user_id}", is_closed:false} 30 same — token substituted to a bound parameter
    {owner:"{current_user}", …} error ANALYTICS_QUERY_FAILED: Unresolvable filter placeholder "{current_user}". Did you mean "{current_user_id}"?

    Token ≡ literal ≡ control is the correct outcome (all open cases are owner=admin); an unresolved literal string would have matched 0 rows, which was the rc.0 failure mode. Unknown tokens now fail loudly with a suggestion.

    Source: rc.2 retest · independent re-read

    Why that is not enough to close this card

    1. This card's body defines its own closing condition, and it is a code change that has not happened. The "To close this issue" section reads:

    When the analytics service learns to substitute {current_user_id} (parity with the list path), restore the personal widget: re-add filter: { owner: '{current_user_id}', is_closed: false } on the service_dashboard table widget and retitle it back to "My Open Cases by Priority".

    The antecedent is now true. The consequent has not been done — src/dashboards/service.dashboard.ts:268-280 still ships the team-wide "Open Cases by Priority", with a source comment still pointing at this issue.

    2. Both readings say so themselves, and neither recommends closing now. 「平台半(用户 token 解析)已修;CRM 半(恢复个人 widget)仍待收尾 … 落地后可关」 — close after it lands. The other: "The CRM half remains open work … restoring a 'My Open Cases by Priority' widget is exactly the closure task for this issue."

    Closing on the platform reading alone would discard a named, unfinished deliverable that no other card carries — the source comment in service.dashboard.ts would then point at a closed issue.

    3. The restore instruction in the body is itself stale and will misfire if copied. The rc.2 reading flagged this: since #548/#677 the owner column no longer exists, and {owner: …} fails with no such column: owner (measured). Whoever does the restore must use owner_id, not the owner spelling written in this card's body.

    What would close it

    The small CRM PR the readings describe: restore the personal widget on service_dashboard using owner_id, retitle it, drop the stale source comment, and confirm it renders per-viewer. That is a code change and therefore outside #1151's bookkeeping mandate.

    This card is no longer an upstream platform tracker — it is a one-PR repo-side task with the platform precondition satisfied. Worth re-triaging as such rather than leaving it labelled upstream:objectstack / prio:p1; that relabel is the PM's call, and nothing was changed here.

    Nothing on this card was changed: still open, labels untouched.


    Generated by Claude Code


    Generated by Claude Code

  4. hotlong commented on Aug 14, 2026

    @hotlong
    Contributor

    Transfer sweep (#1156, part 1) — outcome 2: the fix lands in hotcrm. Relabelled and kept here.

    The outcome test that was applied

    Not "what does the label say" but where would the fix land today? The upstream:objectstack label records what someone believed when this card was filed on 2026-07-28; it is a claim, not a verdict, and on this card it is now wrong.

    Verdict: the platform half is fixed, and this card's own closing condition is CRM code

    Platform half — fixed. Two independent measurements on @objectstack/* 17.0.0-rc.2 agree that the analytics query path resolves {current_user_id} to a bound SQL parameter, and that an unknown token now fails loudly with a suggestion rather than reaching the WHERE clause as a literal:

    runtimeFilter rows generated SQL
    {is_closed:false} (control) 30 WHERE is_closed = $1
    {owner_id:"literal admin id", is_closed:false} 30 WHERE (owner_id = $1 AND is_closed = $2)
    {owner_id:"{current_user_id}", is_closed:false} 30 same — token substituted to a bound parameter
    {owner:"{current_user}", …} error ANALYTICS_QUERY_FAILED: Unresolvable filter placeholder "{current_user}". Did you mean "{current_user_id}"?

    Token ≡ literal ≡ control is the correct outcome (all open cases are owner=admin); the rc.0 failure mode was an unresolved literal matching 0 rows. Sources: rc.2 retest · independent re-read.

    CRM half — outstanding, and it is this card's stated closing condition. The body's "To close this issue" section names a code change in this repo: restore the personal widget on service_dashboard. src/dashboards/service.dashboard.ts:268-280 still ships the team-wide "Open Cases by Priority", with a source comment still pointing at this issue.

    So there is no platform need left to route anywhere. Under the ruling below, that means this is not a transfer.

    Confirming #1156's stated expectation, independently

    #1156 flagged this card as expected-outcome-2 on the strength of #1151's review, and asked for that to be verified rather than inherited. It holds. The two rc.2 readings above are on-card and mutually independent, and neither recommends closing now — both explicitly withhold the close pending the CRM-side widget restore (「平台半(用户 token 解析)已修;CRM 半(恢复个人 widget)仍待收尾 … 落地后可关」).

    Actions taken on this card

    • upstream:objectstack — removed. The remaining work is a one-PR change in this repo.
    • prio:p1 → prio:p2. P1 was graded when this was a platform blocker making personal dashboard widgets impossible product-wide. What is left is restoring one widget on one dashboard whose precondition is already met — triaged backlog, no near-term schedule.
    • bug — kept. It is still a defect from the user's point of view until the widget is back.
    • Left open. Nothing about this card is finished.

    ⚠️ Carried forward for whoever picks this up: the body's restore instruction is stale

    The body says to re-add filter: { owner: '{current_user_id}', is_closed: false }. Since #548/#677 the owner column no longer exists and that spelling fails with no such column: owner (measured). Use owner_id. Copying the body verbatim will misfire.

    What was NOT done here

    No re-measurement on 17.0.0 GA. The platform readings above are rc.2-era and this repo has since moved to GA (#1146 / PR #1147). This sweep is routing and bookkeeping only — no app boot, no probe scaffolding, no code. The widget restore should confirm per-viewer rendering as its own acceptance step.

    Authority

    hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台

    The ruling routes platform needs to the platform. This card no longer carries one, so the companion disposition 「关掉,只留指向镜像的指针」 does not apply: there is nothing to mirror and nothing to point at. Keeping it here is what the ruling requires, not an exception to it.


    Generated by Claude Code


    Generated by Claude Code

  5. added and removed
    upstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstream
    on Aug 14, 2026
  6. hotlong commented on Aug 25, 2026

    @hotlong
    Contributor

    Triage (代扫, vacancy clause) → pm:blocked, Blocked-by: objectstack-ai/objectstack#12230 (filed by this seat just now).

    Filed 2026-07-28 with bug + prio:p2 and no pm-state, so it has been invisible to every candidate query for four weeks. Vacancy clause: triage actions only.

    The premise was re-measured before blocking, and it survives — but its old blocker does not

    The card is right that this is upstream, so the only real triage question is what is it waiting on. The answer was not what the paper trail said.

    An upstream card for exactly this existed — objectstack#3574, "Dashboard widget filters do not interpolate {current_user_id}" — and it is closed as completed. Taken at face value, this card would have gone back to pm:queue and burned a dispatch on a premise that is still true. It was closed by PR objectstack#3594, "gate unresolvable filter placeholders; freeze the {current_user_id} vocabulary" — an author-time lint, not a substitution in the analytics query path. Real work, and not the thing this card needs.

    So I measured the published artefact this repo installs rather than trusting either issue state or release notes. @objectstack/service-analytics@17.1.0, dist/index.js, 226,078 bytes:

    target term hits control term hits
    current_user 0 filter 173
    current_user_id 0 dataset 95
    currentUserId 0 measure 167

    The controls are stated so the zeros count — a zero result is not a finding until something proves the search reached the code. It does. The gap is intact in the version this repo pins.

    Disposition

    pm:blocked on the fresh card objectstack#12230, which carries the measurement above and asks for the substitution on its own terms. I filed new rather than reopening #3574 so the lint that did land keeps its closure.

    ⚠️ The unlock predicate is a read of the published package, not the upstream issue's state. This lane has now been bitten by that gap twice — here, and on #1206, where the blocker closed nine hours after the release that could have carried it. When objectstack#12230 closes, probe @objectstack/service-analytics at whatever version is then pinnable for a non-zero current_user_id count with a control term beside it, and only then return this card to the queue.

    prio:p2 retained — the personal use-case is served through list views today, so the app is not broken, only less capable than it reads.


    Generated by Claude Code

  7. 2 remaining items

  8. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    Contributor

    repo:hotcrm seat, session_01X8U3asekbiC7yWoEPWR4Dg · stock re-triage group 1 (maintainer-confirmed ten-card group; maintainer reply verbatim: 「批1 同意」) · 2026-09-24T23:03Z

    Hold released → pm:queue (p2 unchanged)

    The Restart-when: condition is met on the install surface, not on issue state: @objectstack/service-analytics@17.4.0 (the version this repo pins) carries objectstack#12230's fix — dist/index.js contains resolveQueryTokens ×3, resolvedDatasetScopeGetter ×2 and the 12230 marker ×2 (control: filter ×747). No merged PR on this card after the hold.

    Work: restore the personal widget in src/service/dashboards/service.dashboard.ts (the comment at :333 still cites this card) and retitle it back to "My Open Cases by Priority". Use owner_id, not owner. Acceptance: two different reps see different rows. The body's Restart-when: / Restart-touch: lines are removed in this pass (a released hold keeps no hold line).


    Generated by Claude Code

  9. added
    pm:queueReady for the PM dispatch loop
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 24, 2026
  10. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round R64 (serial dispatch, maintainer instruction verbatim: 「串行派发」) · 2026-09-25T12:11Z
    Session: session_01X8U3asekbiC7yWoEPWR4Dg
    Branch: claude/issue-510-my-open-cases-widget
    Worktree: hotcrm-issue-510
    Domain: repo:hotcrm (single-lane repo — no domain:* taxonomy)
    Seat: repo:hotcrm#1
    File surface: src/service/dashboards/service.dashboard.ts, the My Work note in the app file that cites the "proven" gap (locate it under src/sales/apps/), the four locale packs only if the widget title is translated there, a test that pins the widget if one exists, one .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — default judgement tier, hand-picked (dispatch-gates --tier refuses a hotcrm path from the objectstack checkout, by design); a metadata restore whose acceptance is a two-user measurement
    Clause-②: no
    Thread-read: 5823666103
    Serial constraints cleared: no open PR touches src/service/dashboards/** (open: #1956 — src/revenue/objects, src/sales/profiles, src/sales/objects/account.hook.ts, docs, tests; #1950 — src/sales/**; #1953 — ratchet script; #1058 — deps). #1956 edits content/docs/administration/* and several test/* files; this card declares none of them. In-flight lane cards: #549 (dev done, PR #1956 held for the maintainer).

  11. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 510,
      "status": "blocked",
      "branch": "claude/issue-510-my-open-cases-widget",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1957",
      "session": "session_01X8U3asekbiC7yWoEPWR4Dg",
      "premise_still_valid": true,
      "summary": "The platform half works on the 17.4.0 pin, so A1 holds. Two staffed reps on one booted app each saw only their own cases on the restored widget: A 3 cases, B 2, admin 0 against a control of 30. The in-surface restore is committed at 065af37d and open as a draft (widget id, title and filter, the four locale packs, the stale My Work and calendar notes in src/sales/apps/crm.app.ts, the analytics token rule in test/view-references.test.ts, and a changeset). BLOCKED on a file-surface breach, per the dispatch's stop-on-breach: 9 more files are needed and were not touched. (1) test/dashboard-agent-global-filter.test.ts and test/dashboard-date-range-window.test.ts run every widget with an isSystem context, so the token throws UnresolvedFilterTokenError; they need a user in the context and owner_id in the fixtures. (2) Six docs pages still name the old tile title: content/docs/analytics/dashboards{,.zh-Hans,.zh-Hant}.mdx and content/docs/service/index{,.zh-Hans,.zh-Hant}.mdx. That turns test/docs-dashboard-tiles.test.ts and test/docs-service-index-analytics.test.ts red. (3) The stale comment in src/service/views/case.view.ts (my_open_cases docblock). No open lane PR touches any of these files (read the file lists of #1956, #1950 and #1953). A3: case_metrics has no owner dimension, but a widget filter on the raw owner_id column works, as measured. Mechanism note: the console substitutes the viewer id client-side before the request, and the server door resolves the literal token too; both were measured.",
      "tests": "Gates at 065af37d (each run separately, output redirected, exit code captured before any pipe): validate 0 · typecheck 0 · lint 0 (7 warnings, 14 suggestions) · lint:i18n-gate 0 ('0 i18n/missing-* issues') · hygiene 0 ('source hygiene clean') · hygiene:tokens 0: before (4db37cd4) src/service interaction ~6,228, service total ~21,127, sales total ~99,340; after ~6,239 / ~21,138 / ~99,340 · build 0 · test 1: 'Test Files 4 failed | 168 passed (172); Tests 12 failed | 3673 passed | 1 skipped'. All 12 failures are in the 4 out-of-surface test files named in summary; test/view-references.test.ts passes. Two-user measurement: dev server on port 4510, @objectstack 17.4.0, pnpm demo:staff exit 0. Admin PATCHed 5 open cases (all 38 cases were ownerless): service.agent got CASE-00002 critical, CASE-00036 medium and CASE-00009 low; service.manager got CASE-00007 critical and CASE-00029 medium. CONSOLE DOOR (headless Chromium, per-user sign-in, /_console/apps/app.objectstack.hotcrm/dashboard/service_dashboard, captured POST /api/v1/analytics/dataset/query for this widget): user A rows Medium 1, Low 1, Critical 1; user B rows Medium 1, Critical 1; admin 'No data yet' (rows []). runtimeFilter sent = {$and:[{is_closed:false, owner_id:VIEWER_ID},{created_date window}]}. SQL: WHERE ((is_closed = $1 AND owner_id = $2) AND window) AND (RLS: owner_id = $5 OR id IN (...)). SERVER DOOR (REST replay with the literal token): owner_id '{current_user_id}' gives A 200/3 cases, B 200/2, admin 200/0. Control {is_closed:false} gives A 22 (RLS-visible), B 9, admin 30. The '{current_user}' misspelling gives 400 FILTER_TOKEN_UNKNOWN with a did-you-mean '{current_user_id}' for all three users. Scratch evidence: scratchpad issue-510/browse.json and replay.txt (local only). No ablation: there is no new guard.",
      "mcp_calls": "0",
      "api_writes": "2 relay/REST writes + 1 refused: git push x2 (empty branch probe, then 065af37d; not REST) · fleet-write relay pr_create, dispatched as repository_dispatch to objectstack-ai/objectstack (run 36134938642, success), which ran POST /repos/objectstack-ai/hotcrm/pulls as objectstack-fleet[bot] → PR #1957 draft, body read back byte-equal (4652 chars) · label-write --assign os-warren on PR #1957 was REFUSED by the auto-mode classifier (External System Writes). Not retried, no alternate route. The seat needs to set the PR assignee. · the os-dev-report comment on #510 (this report) goes through the relay as a comment op, see deviations.",
      "open_questions": [
        {
          "question": "File-surface extension: may this card edit 9 more files: test/dashboard-agent-global-filter.test.ts, test/dashboard-date-range-window.test.ts, content/docs/analytics/dashboards{,.zh-Hans,.zh-Hant}.mdx, content/docs/service/index{,.zh-Hans,.zh-Hant}.mdx and src/service/views/case.view.ts?",
          "options": ["A: extend the surface and send a patch round on this branch: user context plus owner_id fixtures in the two dashboard tests, retitle the tile in the six docs pages (English tile title in every locale), rewrite the case.view.ts docblock", "B: land the restore as-is: not possible, because CI stays red"],
          "recommendation": "A. The edits are mechanical consequences of the card's own retitle and token. No open PR touches these files."
        },
        {
          "question": "The service dashboard's Agent global filter binds owner_id as well. At runtime it is ANDed into My Open Cases, so choosing any agent other than yourself empties the widget. Should the personal widget opt out of that binding?",
          "options": ["A: filterBindings: { owner_id: false } on my_open_cases_by_priority. The widget always means mine, using the opt-out mechanism the volume trend already uses for dateRange, which test/dashboard-agent-global-filter.test.ts already honours", "B: leave it bound: an agent pick other than yourself shows an empty personal widget"],
          "recommendation": "A. With the binding, the widget answers 'owned by me AND by X', which is empty unless X is me. Measure the console's handling of a field-keyed filterBindings opt-out in the patch round before claiming it."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the patch round of this card (#510), for the stale 'analytics path resolves no user token' docblock on my_open_cases in src/service/views/case.view.ts. Noted, not filed.",
        "carrier: none. Observation: all 38 seeded crm_case rows have owner_id null after demo:staff, because the demo ownership routes cover opportunity, lead, task and event only. A fresh demo org's My Open Cases widget and My Cases view are therefore empty for every persona. This is demo-staffing coverage, not a defect in a public surface. Noted in Acceptance notes only."
      ],
      "files_changed": [".changeset/my-open-cases-by-priority.md", "src/service/dashboards/service.dashboard.ts", "src/sales/apps/crm.app.ts", "src/sales/translations/en/app.ts", "src/sales/translations/zh-CN/app.ts", "src/sales/translations/es-ES/app.ts", "src/sales/translations/ja-JP/app.ts", "test/view-references.test.ts"],
      "deviations": [
        "test/view-references.test.ts: I read it as the dispatch's 'test pinning the widget'. It pins which tokens widget filters may use, and it had to widen for the restore.",
        "Stopped at the file-surface breach instead of editing the 9 files listed in open_questions. The PR is open as a draft with CI expected red, and the gap is named in its body.",
        "The PR assignee write (label-write --assign os-warren on #1957) was refused by the classifier and is left for the seat.",
        "The PR footer uses the role file's session-URL form instead of the harness's own attribution lines (role-file precedence)."
      ]
    }

    Generated by Claude Code

  12. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    PM answers to the dev's two open questions (report 5832363413) · 2026-09-25T12:30Z

    repo:hotcrm seat, session_01X8U3asekbiC7yWoEPWR4Dg. Both answers fall in the seat's no-escalation class (mechanical consequences and a reversible presentation default). The maintainer can veto either.

    1. File-surface extension — A, granted. The 9 files are direct consequences of this card's own retitle and token:

      • test/dashboard-agent-global-filter.test.ts, test/dashboard-date-range-window.test.ts: user context plus owner_id fixtures. ⛔ Do not weaken what they assert.
      • content/docs/analytics/dashboards{,.zh-Hans,.zh-Hant}.mdx, content/docs/service/index{,.zh-Hans,.zh-Hant}.mdx: the tile's new title.
      • src/service/views/case.view.ts: the stale my_open_cases docblock.

      No open PR touches any of them (checked against feat(revenue): quotes and contracts follow the account (controlled_by_parent) — #549 #1956, feat(opportunity): REQ-0006 qualification, the customer calendar, narrative and approval on status change #1950, chore(ratchet): raise the two src/sales token ceilings to their anchor() (59,000 / 107,000) #1953).

    2. Agent global filter — A: filterBindings: { owner_id: false } on my_open_cases_by_priority. A personal widget answers "mine"; ANDing the agent pick makes it empty for any pick but yourself. This reuses the existing opt-out mechanism. Measure first: confirm the console honours a field-keyed opt-out, with a control. If it does not, stop and report — ⛔ no workaround.

    Patch round on the same branch (claude/issue-510-my-open-cases-widget, PR #1957). Card stays pm:dispatched.

  13. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 510,
      "status": "done",
      "branch": "claude/issue-510-my-open-cases-widget",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1957",
      "session": "session_01X8U3asekbiC7yWoEPWR4Dg",
      "premise_still_valid": true,
      "summary": "Patch round per ruling 5832389289. Both answers are A. (1) Surface extended to the 9 files. test/dashboard-date-range-window.test.ts now runs widgets as a signed-in VIEWER, with owner_id fixtures (the rows at offsets 2 and 29 belong to another owner) and a ground truth that models owner_id '{current_user_id}' explicitly and throws on any other owner spelling. test/dashboard-agent-global-filter.test.ts gains a viewer option on run() and a new section 7 pinning the personal widget: the opt-out is present; viewer ALPHA with agent BETA gets ALPHA's truth, and vice versa; the two answers differ and together cover every open case; a ghost viewer gets 0. No existing assertion was removed or loosened. The six docs pages now name My Open Cases by Priority, English tile title in every locale, and say it ignores the Agent filter. The case.view.ts docblock is rewritten. (2) filterBindings: { owner_id: false } was measured in the console before it was committed, and ablated there too. The PR body written at open time is now stale (see open_questions).",
      "tests": "Final head 5089afd3, clean tree, verify chain run step by step with exit codes captured: validate 0 · typecheck 0 · lint 0 · lint:i18n-gate 0 ('0 i18n/missing-* issues') · hygiene 0 · hygiene:tokens 0 · build 0 (dist widget filterBindings read back as {owner_id:false}) · test 0: 'Test Files 172 passed (172); Tests 3687 passed | 1 skipped (3688)'. Token ratchet: before (4db37cd4) src/service interaction ~6,228, service total ~21,127, sales total ~99,340; after ~6,250 / ~21,149 / ~99,340, all under ceiling. CONSOLE MEASUREMENT of the opt-out: dev server on port 4510, 17.4.0, demo:staff, the same 5 cases assigned. Signed in as service.agent (Priya Raman), opened service_dashboard, picked Agent = Tomas Okafor (service.manager) via data-testid dashboard-filter-owner_id. WITH the opt-out, committed at 25ccc30f: the personal widget was not re-queried after the pick and still rendered Medium 1, Low 1, Critical 1. CONTROL: the bound case_count KPI widgets were re-queried with owner_id = the manager's id ANDed in, going 22 → 0, 5 → 0 and 3 → 0 (the manager's cases are outside the agent's RLS). ABLATION in the console: the opt-out was removed with ablation-replace (anchor 1 → 0), dist then read filterBindings null, and the server was rebuilt and restarted. The personal widget was then re-queried with {$and:[{is_closed:false, owner_id:AGENT},{window, owner_id:MANAGER}]}, returned rows [] and rendered 'No data yet'. Restore: blob equal to HEAD, git diff HEAD empty, dist rebuilt by the verify chain. TEST ABLATIONS (one-shot, auto-restored, git diff HEAD empty afterwards): M1, deleting the opt-out, turns 3 agent-filter tests red (the sweep partition, 'opts out … by its field name', 'shows each viewer their own open cases'). M2, making the truth ignore owner, turns 'counts exactly the rows…' red with 'my_open_cases_by_priority @ last_7_days: engine 1, truth 2' — the engine really applies the resolved owner predicate.",
      "mcp_calls": "0",
      "api_writes": "This round: git push x2 (25ccc30f, 5089afd3; not REST) · 1 relay write: a comment op on #510 carrying this os-dev-report. No PR body PATCH (the dev writes the body once), no assignee or label writes. Previous round: pr_create #1957 and comment 5832363413.",
      "open_questions": [
        {
          "question": "The PR #1957 body written at open time still says 'DRAFT, incomplete by a declared file-surface breach' and 'CI expected red', and its gates table is at 065af37d. The dev does not PATCH bodies. Should the seat rewrite it?",
          "options": [
            "A: the seat replaces the Status section with: 'Complete. Surface extended per 5832389289; Agent-filter opt-out measured in the console with a bound-widget control and an ablation (the widget empties without it).' It also replaces the gates table with the 5089afd3 readings from this report (all 8 exit 0; tests 172 files, 3687 passed)",
            "B: leave the body and let this comment stand as the record"
          ],
          "recommendation": "A: reviewers read the body, and it currently contradicts the head."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none. Observation carried from round 1: all 38 seeded crm_case rows are ownerless after demo:staff, because the ownership routes cover opportunity, lead, task and event only. A fresh demo org's My Open Cases widget and My Cases view are therefore empty for every persona. Demo coverage, not a public-surface defect. Acceptance notes only."
      ],
      "files_changed": [
        ".changeset/my-open-cases-by-priority.md",
        "src/service/dashboards/service.dashboard.ts",
        "src/sales/apps/crm.app.ts",
        "src/sales/translations/en/app.ts",
        "src/sales/translations/zh-CN/app.ts",
        "src/sales/translations/es-ES/app.ts",
        "src/sales/translations/ja-JP/app.ts",
        "src/service/views/case.view.ts",
        "test/view-references.test.ts",
        "test/dashboard-agent-global-filter.test.ts",
        "test/dashboard-date-range-window.test.ts",
        "content/docs/analytics/dashboards.mdx",
        "content/docs/analytics/dashboards.zh-Hans.mdx",
        "content/docs/analytics/dashboards.zh-Hant.mdx",
        "content/docs/service/index.mdx",
        "content/docs/service/index.zh-Hans.mdx",
        "content/docs/service/index.zh-Hant.mdx"
      ],
      "deviations": [
        "test/dashboard-agent-global-filter.test.ts already passed once the opt-out landed, because its sweep skips opted-out widgets by design. The viewer context was added anyway, as a new section 7 that pins the personal widget positively. No existing assertion changed except truthMatches, which now throws on an unresolved owner placeholder instead of silently mis-matching.",
        "The Agent filter is left unbolded in the docs, to match how the same pages already write it in prose.",
        "Build-level dist ablation was done by rebuilding and restarting the dev server rather than with ablation-dist-preflight.mjs (an objectstack script that does not apply to a hotcrm artifact). Landing on disk was proven by the anchor count and by the dist widget read back through JSON.parse.",
        "Cleanup: the dev server was killed (none left on port 4510). The worktree node_modules and the worktree itself were removed after the clean-tree check. The branch is on the remote at 5089afd3."
      ]
    }

    Generated by Claude Code

  14. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    Contributor

    PM review — ACCEPT → landing · 2026-09-25T12:43Z

    Reviewer of record: repo:hotcrm seat, session_01X8U3asekbiC7yWoEPWR4Dg. Reviewed against GitHub.

    • PR feat(service): restore the personal My Open Cases by Priority widget #1957 at 5089afd3: Fixes #510, a real changeset, 17 files (+132 −52). CI 10/10 green, mergeable clean.
    • Path face: no governed-surface path (.claude/**, skills/**, docs/adr/**, AGENTS.md, CLAUDE.md). {current_user_id} is presentation scope, not an access boundary (RLS still applies). ⇒ seat-landed through the merge queue.
    • Acceptance (card's own): two reps see different rows — A 3, B 2, admin 0 vs control 30 — at both the console door and the server door. {current_user} refuses with FILTER_TOKEN_UNKNOWN and a did-you-mean suggestion.
    • Agent-filter opt-out (answer 2, 5832389289): measured in the console with a bound-widget control (22 → 0) and an ablation (widget empties without it).
    • Declared surface extension: the 9 files granted in 5832389289, and nothing beyond them.
    • Out-of-scope note, not filed (no carrier): demo:staff leaves all 38 seeded cases ownerless, so a fresh demo org's "My" case views are empty for every persona.

    PR body Status section refreshed by the seat to match the head.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions