Repository navigation
Closed
Description
The screen that removes the "can you send me a status update" mail. A manager opens a person and sees everything, having entered nothing.
Blocked-by: #8 (the scopes that decide what they may see).
Files you own
src/pages/member.page.ts(new), pushed intodulyPagesinsrc/pages/index.ts- a nav entry in the Team group of
src/apps/duly.app.ts test/member-page.test.ts(new)
Content, in reading order
- Header — name, position, unit, manager.
- Right now — open tasks, of which late, of which not moving. Three numbers, the third emphasised.
- This period — their governed duties and the state of each. Grouped by frequency so a monthly rhythm reads differently from an annual one.
- Standing duties — listed, with no tick affordance anywhere near them. They never complete; a control that implies otherwise is a bug.
- Recent activity — last 20 task events by
last_update_at. - Assigned to them — tasks with
source: 'assigned', showing who assigned each.
Not on this page
- Their work log.
duly_log_entryisreadScope: 'own'for every position; the only widening is a record's ownvisibility: 'manager'flag via a sharing rule. Do not query the object here at all — not filtered, not counted, not "0 entries". A manager seeing a log count learns the log exists and that it is being watched, which is enough to stop people keeping one. - Any editable control. Read-only page. A manager's only write in this product is assigning, and the assign action lives on its own object.
- Any comparison to other people. No percentile, no "vs team average", no rank. Their own trend over time is fine and useful; a position in a distribution is a performance score.
Rules
- Everything must read correctly for a manager whose scope resolves through the ADR-0057 depths. In this open-edition checkout those fall back to owner-only, so the page will look empty for anyone but yourself — expected, not a bug (see Security model — positions, permission sets, sharing rules #8). Assert authored scopes and filters in tests rather than resolved rows.
- Standing duties come from
duly_dutywhereform = 'standing'; they have no tasks by construction, so do not join through tasks to find them.
Acceptance
- the page renders from
duly_task/duly_dutyonly; grep the file — no reference toduly_log_entryanywhere - standing duties appear with no completion control
- nothing on the page is editable
- no comparison-to-peers element exists
- reachable from the Team nav group
Gates
pnpm validate && pnpm typecheck && pnpm test && pnpm build.
Activity
os-warren commentedon Sep 2, 2026
Claiming this card.
01SqkTcrxUFci7nqXdbBSe2pclaude/issue-13-member-page../duly-issue-13offorigin/main@dd3c619Files I intend to own:
src/pages/member.page.ts(new),src/pages/index.ts, the Team nav group insrc/apps/duly.app.ts,test/member-page.test.ts(new), pluszh-CN/enentries for the new page + nav labels (shared with #107/#108/#109 — whoever lands second mergesmainand re-runs the gates).#8 landed (PR #53), so the
pm:blockedpremise is lifted; I'll verify that againstorigin/mainbefore writing code.Generated by Claude Code
os-warren commentedon Sep 2, 2026
{ "issue": 13, "status": "done", "branch": "claude/issue-13-member-page", "pr": "https://github.com/objectstack-ai/duly/pull/112", "premise_still_valid": true, "summary": "`src/pages/member.page.ts` ships `duly_member`, a read-only `type: 'record'` page over `sys_user` carrying the card's six sections in reading order, wired into `dulyPages` and reachable from a new `People` entry in the Team nav group. The three forbidden things are pinned by `test/member-page.test.ts` (25 assertions): the work-log object's machine name is greped out of the page source AND walked out of the parsed page, no writing component type is declared, every highlight chip is `readonly`, and no aggregate or peer comparison exists. The card asked for the page schema to be MEASURED — seven gaps were found against installed 17.2.0 and all seven are filed at objectstack-ai/objectui rather than worked around. Two were found only in the browser. ONE LINE OF THE CARD IS NOT DELIVERED: 'showing who assigned each' is unauthorable — the assigner is one lookup hop from the task (related-list columns cannot cross a lookup) and the alternative binding on `duly_assignment.assignees` was authored, run, and refused by the driver with 400 INVALID_FILTER because `RelatedList` composes its parent filter as bare equality and `assignees` is `multiple: true`. That list was deleted rather than left rendering an error; the task list carries the `assignment` column, one click from the answer. Separately, an auto-appended discussion panel with a comment box, reactions and threaded replies was found on the rendered page — `sys_user` is a locked platform object and the renderer's `disableDiscussion` opt-out is not a `PageSchema` key, so the page now declares an explicit `record:discussion` with all three writes off, which is the only authorable way to shut it (verified: 0 composers on the page). Reached `main` at 56b6aae (#110) via merge and re-ran all four gates after it; also converged onto #110's `node:fs` shim, deleting this branch's Vite `?raw` declaration rather than shipping two ways to read a file in a test.", "tests": "All four gates re-run ON THE FINAL PUSHED COMMIT e399e78 (`git status --porcelain` = 0 lines), each captured with the exit code taken before any pipe:\n\n pnpm validate -> 0 \"✓ Validation passed (483ms)\" · \"UI: 1 Apps 5 Views 1 Pages 1 Dashboards 6 Actions\"\n one warning, the expected open-edition state naming @objectstack/security-enterprise (AGENTS.md rule 7 — not silenced)\n pnpm typecheck -> 0 tsc --noEmit, no diagnostics\n pnpm test -> 0 \"Test Files 31 passed (31)\" · \"Tests 775 passed (775)\"\n pnpm build -> 0 \"✓ Build complete (1003ms)\"\n\nABLATION — both new guards were proven able to fail. Script carried `trap '<restore>' EXIT INT TERM`; each mutation's anchor was asserted present exactly once by the editing script BEFORE the edit (a python `assert s.count(old) == 1`, so a zero-hit edit aborts instead of reporting a green no-op); no rebuild step applies (vitest reads src directly, no dist in the resolution path).\n A. injected a `duly_log_entry` related list -> on-disk proof `grep -c duly_log_entry` = 1 -> \"Tests 3 failed | 22 passed (25)\", red on `is not named anywhere in the source`, `binds no component to it`, `reads in the order the card asks for`.\n B. dropped `readonly: true` from one highlight chip -> \"Tests 1 failed | 24 passed (25)\", red on `marks every highlight chip readonly` and nothing else. (The shell-echoed grep counts for leg B were mangled by heredoc quoting and are NOT evidence; the anchor assertion and the targeted red are.)\n RESTORE leg verified byte-identical: `diff -q member.page.ts.orig src/pages/member.page.ts` => identical, `grep -c 'readonly: true'` = 5, `grep -c duly_log_entry` = 0.\n\nBROWSER — `pnpm demo -- --port 4913` (own port; killed by recorded PID only, never by name; port confirmed closed afterwards). 465 seeded rows, signed in as the demo admin, Playwright 1.56.1 at 1400x1000 @2x. Two seeded people (Rosa Delgado; Priya Raman, who has a standing duty). Measured on the rendered page: all six sections present with real data; counts Open 5 / Late 1 / Not moving 1, Governed duties 3 sorted Fortnightly-Monthly-Monthly, Held permanently 1, Last touched 28, Assigned work 2; `duly_log_entry`/'Work log'/'percentile'/'rank'/'vs team' all ABSENT; 0 textareas or contenteditables (no comment box); 0 console errors and 0 4xx on /api after the fix. The ADR-0065 emphasis was confirmed by computed style, not by eye: objectui emitted `.os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; }` and the node computes `3px rgb(140, 101, 18)` / `padding-left: 12px`. Page-assignment collision measured live: `GET /api/v1/meta/page` returns BOTH `duly_member` and the platform's `sys_user_detail` for `sys_user`, `duly_member` first, and it wins by list position and a stable sort only. Screenshots pushed to throwaway branch `shots-issue-13` and embedded in the PR.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectui#7297: no record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the VIEWER, so element:number cannot be scoped to the record and would silently count the whole org", "filed as objectstack-ai/objectui#7298: two page keys the renderer reads that PageSchema (a strictObject) does not declare — `priority` in usePageAssignment and `disableDiscussion` in RecordDetailView; both are unwritable, and the second leaves a comment box on a read-only page", "filed as objectstack-ai/objectui#7299: record:related_list cannot bind to a multi-value relationship field — RelatedList composes bare equality, the driver refuses with 400 INVALID_FILTER and prescribes $contains, which is unreachable from RecordRelatedListProps", "filed as objectstack-ai/objectui#7300: a page cannot declare its related lists read-only — '+ New'/edit/delete are host-resolved from the CHILD object, so this read-only page renders 8 '+ New' buttons it has no key to decline", "filed as objectstack-ai/objectui#7301: record:related_list has no groupBy, and its columns cannot cross a lookup — the two limits that kept 'grouped by frequency' and 'who assigned each' from being written", "NOT filed, recorded here for PM triage: `PageTranslation.components` (@objectstack/spec 17.2.0) declares title|description|label|placeholder|emptyText|submitLabel and has NO key for `content` — the one string `element:text` renders. Its own comment says the key face was derived from the copy props components declare and names its two deliberate exclusions (`help`, `subtitle`), so this reads as a missed one. The blessed alternative (an inline `{en,'zh-CN'}` I18nLabel map, which the platform's own sys-user.page.ts uses) is what this page does, and the 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count. It belongs at objectstack-ai/objectstack, not objectui, which is why I did not file it under the card's stated destination without a PM call." ] }Generated by Claude Code
os-warren commentedon Sep 2, 2026
os-dev-report
Supersedes the previous comment on this issue. Two things the GitHub body sanitizer ate there, both worth knowing about: the leading
<!-- os-dev-report -->HTML-comment marker was stripped entirely (so that comment is invisible to a marker scan), and inside the evidence the short<restore>fragment was eaten fromtrap '...' EXIT INT TERM, which made the ablation's restore leg read as an empty trap. Same mechanism both times — short angle-bracket fragments do not survive, backticks included. This comment leads with the marker as literal text and spells the trap out.{ "issue": 13, "status": "done", "branch": "claude/issue-13-member-page", "pr": "https://github.com/objectstack-ai/duly/pull/112", "premise_still_valid": true, "summary": "`src/pages/member.page.ts` ships `duly_member`, a read-only `type: record` page over `sys_user` carrying the card's six sections in reading order, wired into `dulyPages` and reachable from a new `People` entry in the Team nav group. The three forbidden things are pinned by `test/member-page.test.ts` (25 assertions): the work-log object's machine name is greped out of the page source AND walked out of the parsed page, no writing component type is declared, every highlight chip is `readonly`, and no aggregate or peer comparison exists. The card asked for the page schema to be MEASURED — seven gaps were found against installed 17.2.0 and all seven are filed at objectstack-ai/objectui rather than worked around. Two were found only in the browser. ONE LINE OF THE CARD IS NOT DELIVERED: 'showing who assigned each' is unauthorable — the assigner is one lookup hop from the task (related-list columns cannot cross a lookup) and the alternative binding on `duly_assignment.assignees` was authored, run, and refused by the driver with 400 INVALID_FILTER because `RelatedList` composes its parent filter as bare equality and `assignees` is `multiple: true`. That list was deleted rather than left rendering an error; the task list carries the `assignment` column, one click from the answer. Separately, an auto-appended discussion panel with a comment box, reactions and threaded replies was found on the rendered page — `sys_user` is a locked platform object and the renderer's `disableDiscussion` opt-out is not a `PageSchema` key, so the page now declares an explicit `record:discussion` with all three writes off, which is the only authorable way to shut it (verified: 0 composers on the page). Reached `main` at 56b6aae (#110) via merge and re-ran all four gates after it; also converged onto #110's `node:fs` shim, deleting this branch's Vite `?raw` declaration rather than shipping two ways to read a file in a test.", "tests": "All four gates re-run ON THE FINAL PUSHED COMMIT e399e78 (`git status --porcelain` = 0 lines), each exit code captured before any pipe:\n\n pnpm validate -> 0 '✓ Validation passed (483ms)' · 'UI: 1 Apps 5 Views 1 Pages 1 Dashboards 6 Actions'\n one warning, the expected open-edition state naming @objectstack/security-enterprise (AGENTS.md rule 7 — not silenced)\n pnpm typecheck -> 0 tsc --noEmit, no diagnostics\n pnpm test -> 0 'Test Files 31 passed (31)' · 'Tests 775 passed (775)'\n pnpm build -> 0 '✓ Build complete (1003ms)'\n\nABLATION — both new guards were proven able to fail. The script carried a restore trap on EXIT, INT and TERM (a `cp` of the pristine file back over the page, registered before the first mutation), so a foreground-cap SIGTERM mid-mutation could cost a reading but never leave a mutated tree. Each mutation's anchor was asserted present exactly once by the editing script BEFORE the edit (a python `assert s.count(old) == 1`), so a zero-hit edit aborts instead of reporting a green no-op. No rebuild step applies here: vitest reads src directly, with no dist in the resolution path.\n A. injected a `duly_log_entry` related list -> on-disk proof `grep -c duly_log_entry` = 1 -> 'Tests 3 failed | 22 passed (25)', red on `is not named anywhere in the source`, `binds no component to it`, `reads in the order the card asks for`.\n B. dropped `readonly: true` from one highlight chip -> 'Tests 1 failed | 24 passed (25)', red on `marks every highlight chip readonly` and nothing else. (The shell-echoed grep counts for leg B were mangled by heredoc quoting and are NOT evidence; the anchor assertion and the targeted red are.)\n RESTORE leg verified byte-identical: `diff -q member.page.ts.orig src/pages/member.page.ts` => identical, `grep -c 'readonly: true'` = 5, `grep -c duly_log_entry` = 0.\n\nBROWSER — `pnpm demo -- --port 4913` (own port; killed by recorded PID only, never by name; port confirmed closed afterwards). 465 seeded rows, signed in as the demo admin, Playwright 1.56.1 at 1400x1000 @2x. Two seeded people (Rosa Delgado; Priya Raman, who has a standing duty). Measured on the rendered page: all six sections present with real data; counts Open 5 / Late 1 / Not moving 1, Governed duties 3 sorted Fortnightly-Monthly-Monthly, Held permanently 1, Last touched 28, Assigned work 2; `duly_log_entry` / 'Work log' / 'percentile' / 'rank' / 'vs team' all ABSENT; 0 textareas or contenteditables (no comment box); 0 console errors and 0 4xx on /api after the fix. The ADR-0065 emphasis was confirmed by computed style, not by eye: objectui emitted `.os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; }` and the node computes `3px rgb(140, 101, 18)` / `padding-left: 12px`. Page-assignment collision measured live: `GET /api/v1/meta/page` returns BOTH `duly_member` and the platform's `sys_user_detail` for `sys_user`, `duly_member` first, and it wins by list position and a stable sort only. Screenshots pushed to throwaway branch `shots-issue-13` and embedded in the PR.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectui#7297: no record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the VIEWER, so element:number cannot be scoped to the record and would silently count the whole org", "filed as objectstack-ai/objectui#7298: two page keys the renderer reads that PageSchema (a strictObject) does not declare — `priority` in usePageAssignment and `disableDiscussion` in RecordDetailView; both are unwritable, and the second leaves a comment box on a read-only page", "filed as objectstack-ai/objectui#7299: record:related_list cannot bind to a multi-value relationship field — RelatedList composes bare equality, the driver refuses with 400 INVALID_FILTER and prescribes $contains, which is unreachable from RecordRelatedListProps", "filed as objectstack-ai/objectui#7300: a page cannot declare its related lists read-only — '+ New' / edit / delete are host-resolved from the CHILD object, so this read-only page renders 8 '+ New' buttons it has no key to decline", "filed as objectstack-ai/objectui#7301: record:related_list has no groupBy, and its columns cannot cross a lookup — the two limits that kept 'grouped by frequency' and 'who assigned each' from being written", "NOT filed, recorded here for PM triage: PageTranslation.components (@objectstack/spec 17.2.0) declares title|description|label|placeholder|emptyText|submitLabel and has NO key for `content` — the one string element:text renders. Its own comment says the key face was derived from the copy props components declare and names its two deliberate exclusions (help, subtitle), so this reads as a missed one. The blessed alternative (an inline en/zh-CN I18nLabel map, which the platform's own sys-user.page.ts uses) is what this page does, and the 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count. It belongs at objectstack-ai/objectstack, not objectui, which is why I did not file it under the card's stated destination without a PM call." ] }Generated by Claude Code