Skip to content

Member detail page — open one person's duties without asking them - #112

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page
Sep 2, 2026
Merged

Member detail page — open one person's duties without asking them#112
os-warren merged 4 commits into
mainfrom
claude/issue-13-member-page

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #13

A manager opens a person and sees everything, having entered nothing. duly_member is a read-only type: 'record' page over sys_user with the card's six sections in reading order, reachable from the Team nav group.

Gates green on e399e78 (the pushed head): validate 0 · typecheck 0 · test 0 (775 passed / 31 files) · build 0. The one validate warning 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 demo on a private port with the 465-row demo seed, signed in as the demo admin, viewport 1400×1000 @2x. Screenshots live on the throwaway branch shots-issue-13 (not for merge).

Header, and "Right now" — name, unit, manager off the user record; position from the sys_user_position junction; then open work, the part past its grace, and the part nobody has touched in a fortnight.

Header and Right now

"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 frequency and sort on it.

Not moving and This period

Standing duties (a different person, who has one) — listed, with no status column and nothing to tick.

Standing duties

"Assigned to them", and the bottom of the page — note the Discussion panel has no comment box. That took work; see §7.

Assigned to them

The Team nav group, with the People entry that opens a person.

Team nav

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 computes 3px 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.ts is 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 (objectName resolves to exactly duly_duty, duly_task, sys_user_position).

Editable controls. No record:details (its inlineEdit defaults ON where the object is editable — declaring the component is enough), no record:quick_actions, no element:form / button / text_input / record_picker, no add picker, no actions. Highlight chips carry readonly: true, which HeaderHighlight enforces. 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 relationshipField with 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_entry related list turned 3 assertions red; dropping readonly from one chip turned 1 red. Each mutation's anchor was asserted present exactly once before the run, the script carried a restore trap, 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/objectui rather than worked around, each carrying its number in the page header. Two were found only in the browser.

Gap Filed What the page does instead
§1 No record-context token for filter values — CONTEXT_TOKENS is {current_user_id} + {current_org_id}, both the viewer #7297 "Right now" is three related lists with their own count badges. An element:number here would have counted the whole org under one person's name
§2 record:related_list has no groupBy #7301 frequency leads the columns and the sort. Weaker than grouping; not pretended otherwise
§3 A page cannot declare its related lists read-only — "+ New" is host-resolved from the child object #7300 Nothing. 8 "+ New" buttons render. Closing it object-side would close creation everywhere, which is a different and wrong change
§4 Related-list columns cannot cross a lookup #7301 The task list carries assignment — one click from the assigner, not zero
§5 priority is read by usePageAssignment but not declared on PageSchema #7298 Nothing authorable. See below
§6 record:related_list cannot bind a multi-value field #7299 Nothing. The one line of the card not delivered
§7 The auto-appended discussion panel cannot be declined, and carries a comment box #7298 Closed — by declaring the component that opens it

Three 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 on duly_assignment bound to assignees — the field that actually names this person — was authored, run against the seeded demo, and refused:

GET /api/v1/data/duly_assignment?filter=["assignees","=","ql8NO1TcKsuFrrH-"]
400 INVALID_FILTER — The bare equality spelling { "assignees": value } WAS NOT
APPLIED: "assignees" is a multi-value … field, stored by this driver as a JSON
TEXT column … Use "$contains" for membership …

The driver is behaving perfectly — loud, named, with the working spelling. But RelatedList composes its parent filter as bare equality and $contains is not reachable from RecordRelatedListProps, 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. RecordDetailView appends a chatter panel to every record page whose object does not set enable.feeds: false, hard-coded showCommentInput: true, enableReactions: true, enableThreading: true. sys_user is a locked platform object, so that switch is not ours, and the renderer's own documented escape (assignedPage.disableDiscussion = true) is not a PageSchema key — and PageSchema is a strictObject, 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:discussion suppresses the append and hands its config to the author, where feed.showCommentInput: false is 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-objects ships sys_user_detail for the same object with isDefault: true. Measured on this checkout: GET /api/v1/meta/page returns both, duly_member first, and it wins — by list position and a stable sort, since usePageAssignment breaks ties on a priority key nobody can author. isDefault is not consulted at all, which is why this page leaves it false rather than making the flag a lie on whichever page loses.


Notes for review

kind: 'full', not slotted, is a product decision. A slotted page falls through to buildDefaultPageSchema, whose tabs synthesizer emits one related list per object with an FK to sys_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 through PageView, which mounts no RecordContextProvider — every related list would have a null parent and render nothing. It also needs requiresObject: 'sys_user': without it defineStack refuses the whole stack, because the nav cross-reference check resolves objectName against config.objects only. That is also the correct runtime behaviour (hide, don't 404).

Lateness and stagnation are asked the same way the existing views ask themlate_after < {today} and last_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.ts and authored-text.ts are touched, as flagged for the #107/#108/#109 batch; main was merged at 56b6aae (#110) and all four gates re-run after it. That merge also brought test/node-builtins.d.ts, a narrow declare 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:text copy is localized inline, not through the bundle, and that is a spec gap rather than a shortcut: PageTranslation.components declares title | description | label | placeholder | emptyText | submitLabel and has no key for content — the one string element:text renders. I18nLabelSchema accepts an inline { en, 'zh-CN' } map and that is the route the platform's own sys-user.page.ts uses. 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

os-warren and others added 4 commits September 2, 2026 04:43
`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
os-warren marked this pull request as ready for review September 2, 2026 05:09

Copy link
Copy Markdown
Collaborator Author

Reviewed — merging.

Gates, re-run by me on the head merged with current main: validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 775 passed), build 0.

The three hard rules, checked in the source rather than taken from the report: duly_log_entry — 0 occurrences in the page; the single hit for percentile|rank|average is the header comment stating the prohibition, not an element; all 5 highlight chips readonly: true; the record:discussion block turns off showCommentInput, enableReactions, enableThreading, the filter and the subscription toggles. Reachable from the Team group as People.

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 (400 INVALID_FILTERRelatedList composes bare equality and assignees is multiple: true), so the list was deleted rather than left rendering an error, and the task list carries the assignment column one click from the answer. A page that shows an error box on a demo is worse than a page that shows one fewer section. Filed as objectui#7299 with the exact refusal.

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. disableDiscussion is read by the renderer but is not a PageSchema key (objectui#7298), so the explicit record:discussion with every write off is the only authorable way to shut it, and it was verified as 0 composers on the rendered page. That is the difference between "read-only" as an intention and as a measurement.

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 — PageTranslation.components has no content key for element:text — belongs at objectstack and I have filed it as objectstack#14412. Your reasoning was right: the schema comment names its two deliberate exclusions and this is not one of them.

Recording one measurement as a watch item rather than a fix: GET /api/v1/meta/page returns both duly_member and the platform's sys_user_detail for sys_user, and ours wins by list position and a stable sort only. A platform reordering would make this page vanish with no error. Filed as a finding so it is not re-derived.


Generated by Claude Code

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Member detail page — open one person's duties without asking them

1 participant