Uh oh!
There was an error while loading. Please reload this page.
Member detail page — open one person's duties without asking them - #112
Conversation
`duly_member`: a read-only `type: 'record'` page over `sys_user` that shows a
manager one person's whole picture with nothing entered — header, what is open
right now, what this period asks of them, what they hold permanently, what has
moved lately, and what was put on them by someone else.
Reached by opening a person: the Team nav group gains a `People` entry, since a
`type: 'page'` nav item routes through objectui's `PageView`, which mounts no
record context. `kind: 'full'` rather than `slotted` is a product decision — the
default-page synthesizer emits a related list per object with an FK to
`sys_user`, which on this stack includes the work-log object this page must
never show.
Five platform gaps were measured against the installed 17.2.0 packages and are
recorded on the page rather than worked around; the page's header comment names
each one and what it does instead. The load-bearing one: no record-context token
exists for filter values (`CONTEXT_TOKENS` is `{current_user_id}` +
`{current_org_id}`, both the VIEWER), so `element:number` cannot be scoped to
the person and "Right now" is three record-bound related lists with their own
count badges instead of three org-wide figures.
`test/member-page.test.ts` pins the four "not on this page" invariants, all of
which fail silently: the work-log object is greped out of the 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 anywhere.
Gates: validate 0, typecheck 0, test 0 (768 passed / 30 files), build 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p…omment box Two findings from driving the seeded demo as the admin, neither visible to any gate: `assigned_by_whom` — a `duly_assignment` related list bound on `assignees` — was REFUSED by the driver, 400 INVALID_FILTER: `RelatedList` builds its parent filter as bare equality and `assignees` is `multiple: true`, which this driver stores as a JSON TEXT column. `$contains` is the working spelling and it is not reachable from `RecordRelatedListProps`. The list is removed rather than left rendering an error; "showing who assigned each" is the one line of the card that is not fully delivered, and it is recorded as gap §6 and filed. `RecordDetailView` auto-appends a chatter panel — comment box, reactions, threaded replies, all hard-coded on — to every record page whose object does not set `enable.feeds: false`. `sys_user` is a locked platform object, and the renderer's own opt-out (`disableDiscussion`) is not a `PageSchema` key, so it cannot be written. Placing an EXPLICIT `record:discussion` suppresses the append and takes its config back: all three writes off, leaving the record's read-only history. Verified in the browser — the composer is gone (0 textareas / contenteditables on the page). Also measured and now recorded: the page wins the `sys_user` assignment against the platform's `sys_user_detail` on this checkout, but by list position only (both `priority ?? 0`, stable sort) — gap §5 stands. Gates: validate 0, typecheck 0, test 0 (769 passed / 30 files), build 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…numbers #110 landed `test/node-builtins.d.ts` — a narrow `declare module 'node:fs'` exposing `readFileSync`, with the reasoning that `@types/node` stays out of a metadata package. That is the same problem `test/raw-import.d.ts` solved here with Vite's `?raw`, and one way to read a file in a test beats two, so this drops the `?raw` declaration and uses the merged shim. Its `URL` overload is what makes `node:url` unnecessary. Also stamps every gap in the page header with the issue it was filed as, so a gap without a number reads as a workaround wearing a comment: §1 no record-context filter token objectui#7297 §2 related list cannot group objectui#7301 §3 no page-level read-only for a related list objectui#7300 §4 related-list columns cannot cross a lookup objectui#7301 §5 `priority` read but not declared objectui#7298 §6 related list cannot bind a multi-value field objectui#7299 §7 `disableDiscussion` read but not declared objectui#7298 Gates re-run on the merge: validate 0, typecheck 0, test 0 (775 passed / 31 files), build 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
os-warren
commented
Sep 2, 2026
Reviewed — merging.Gates, re-run by me on the head merged with current The three hard rules, checked in the source rather than taken from the report: The one line not delivered is the right kind of not-delivered. "Showing who assigned each" was authored two ways, run, and refused by the driver ( The discussion panel was the important catch. An auto-appended comment box with reactions and threaded replies, on a page whose whole premise is that the manager enters nothing — and on a person's record, which is precisely where a performance note would go. Seven platform gaps measured against 17.2.0 and filed at objectui (#7297–#7301 plus the two above) instead of being worked around; the one you left for a PM call — Recording one measurement as a watch item rather than a fix: Generated by Claude Code |
Fixes#13
A manager opens a person and sees everything, having entered nothing.
duly_memberis a read-onlytype: 'record'page oversys_userwith the card's six sections in reading order, reachable from the Team nav group.Gates green on
e399e78(the pushed head):validate0 ·typecheck0 ·test0 (775 passed / 31 files) ·build0. The onevalidatewarning is the expected open-edition state naming@objectstack/security-enterprise— AGENTS.md rule 7 says not to silence it.The page, in the browser
Driven against
pnpm demoon a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branchshots-issue-13(not for merge).Header, and "Right now" — name, unit, manager off the user record; position from the
sys_user_positionjunction; then open work, the part past its grace, and the part nobody has touched in a fortnight."Not moving" emphasised, and "This period" — the third number is the one that fires before a deadline exists, so it carries the rule. Governed duties lead with
frequencyand sort on it.Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.
"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.
The Team nav group, with the People entry that opens a person.
The emphasis on "Not moving" is ADR-0065 scoped styles, not a
className— the build-time Tailwind never scans runtime metadata. Confirmed by computed style rather than by eye: objectui emitted.os-s-right_now_stalled { border-left: 3px solid #8C6512 !important; padding-left: 12px !important; }and the node computes3px rgb(140, 101, 18)/padding-left: 12px.The three things that must not be on the page
Each fails silently if broken — a log count reads as a helpful number, a "+ New" reads as a feature, a peer comparison reads as a dashboard. None turns anything red on its own, which is what
test/member-page.test.tsis for.The work log. The object's machine name appears nowhere in
src/pages/member.page.ts— including in its comments, because the card's acceptance is a grep and a grep cannot read intent. The name, the reasoning and the guard live together in the test, which greps the source and walks the parsed page (objectNameresolves to exactlyduly_duty,duly_task,sys_user_position).Editable controls. No
record:details(itsinlineEditdefaults ON where the object is editable — declaring the component is enough), norecord:quick_actions, noelement:form/button/text_input/record_picker, noaddpicker, noactions. Highlight chips carryreadonly: true, whichHeaderHighlightenforces. One residue remains and it is not ours to close — §3.Comparison to peers. No aggregate component of any kind, and every list is bound to this record through
relationshipFieldwith no filter naming a user — not even{current_user_id}, which is the viewer rather than the person being read.Both guards were ablated to prove they can fail. Injecting a
duly_log_entryrelated list turned 3 assertions red; droppingreadonlyfrom one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restoretrap, and the tree was confirmed byte-identical afterwards.What the platform can and cannot express here
The card asked for this to be measured rather than assumed. Seven gaps, each measured against the installed 17.2.0 packages, each filed at
objectstack-ai/objectuirather than worked around, each carrying its number in the page header. Two were found only in the browser.CONTEXT_TOKENSis{current_user_id}+{current_org_id}, both the viewerelement:numberhere would have counted the whole org under one person's namerecord:related_listhas nogroupByfrequencyleads the columns and the sort. Weaker than grouping; not pretended otherwiseassignment— one click from the assigner, not zeropriorityis read byusePageAssignmentbut not declared onPageSchemarecord:related_listcannot bind a multi-value fieldThree of these deserve more than a table row.
§6 — "showing who assigned each" is not delivered
This is the one requirement from the card that is missing, and it is missing because it cannot be written.
The assigner lives on
duly_assignment.assigner. Two routes, both closed. A column on the task list would have to cross a lookup (§4). A related list onduly_assignmentbound toassignees— the field that actually names this person — was authored, run against the seeded demo, and refused:The driver is behaving perfectly — loud, named, with the working spelling. But
RelatedListcomposes its parent filter as bare equality and$containsis not reachable fromRecordRelatedListProps, so the list cannot be written correctly at all. It was deleted rather than left rendering a 400.§7 — a comment box that had to be closed by opening it
Not in the schema; found by reading the rendered page.
RecordDetailViewappends a chatter panel to every record page whose object does not setenable.feeds: false, hard-codedshowCommentInput: true, enableReactions: true, enableThreading: true.sys_useris a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not aPageSchemakey — andPageSchemais astrictObject, so writing it is a parse error.A comment box on a person's record is worse than merely editable here: it is where a performance note would go, on a page whose whole premise is that the manager enters nothing.
The fix is authorable but backwards — declaring an explicit
record:discussionsuppresses the append and hands its config to the author, wherefeed.showCommentInput: falseis honoured (RecordActivityTimeline.tsx:215, with objectui's own test pinning "hides it even when the host CAN persist a comment"). So the page declares one, with all three writes off. Verified: 0 textareas / contenteditables on the rendered page. The test pins this as a positive assertion, because deleting the node does not remove the panel — it hands it back to the host with the writes on.§5 — which page wins, and why that is luck
@objectstack/platform-objectsshipssys_user_detailfor the same object withisDefault: true. Measured on this checkout:GET /api/v1/meta/pagereturns both,duly_memberfirst, and it wins — by list position and a stable sort, sinceusePageAssignmentbreaks ties on aprioritykey nobody can author.isDefaultis not consulted at all, which is why this page leaves itfalserather than making the flag a lie on whichever page loses.Notes for review
kind: 'full', notslotted, is a product decision. A slotted page falls through tobuildDefaultPageSchema, whosetabssynthesizer emits one related list per object with an FK tosys_user— including the work-log object. The one thing this page must never show is the thing the default layout adds for free.The nav entry is a people list, not a page link. A
type: 'page'nav item routes throughPageView, which mounts noRecordContextProvider— every related list would have a null parent and render nothing. It also needsrequiresObject: 'sys_user': without itdefineStackrefuses the whole stack, because the nav cross-reference check resolvesobjectNameagainstconfig.objectsonly. That is also the correct runtime behaviour (hide, don't 404).Lateness and stagnation are asked the same way the existing views ask them —
late_after < {today}andlast_update_at < {14_days_ago}— so one person cannot be late on this page and on time on the Late list (#48).Shared files.
zh-CN.tsandauthored-text.tsare touched, as flagged for the #107/#108/#109 batch;mainwas merged at56b6aae(#110) and all four gates re-run after it. That merge also broughttest/node-builtins.d.ts, a narrowdeclare module 'node:fs'— this branch had solved the same problem with Vite's?raw, and converged on the merged shim rather than ship two ways to read a file in a test.element:textcopy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut:PageTranslation.componentsdeclarestitle | description | label | placeholder | emptyText | submitLabeland has no key forcontent— the one stringelement:textrenders.I18nLabelSchemaaccepts an inline{ en, 'zh-CN' }map and that is the route the platform's ownsys-user.page.tsuses. The 16 strings are declared in the walk's untranslatable ledger with that reason and pinned by count, so the day the key lands they move into the bundles and the count drops to zero.🤖 Generated with Claude Code
https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Generated by Claude Code