Skip to content

Give the frontline a progress phrase, attachments and a card worth showing - #115

Merged
os-warren merged 4 commits into
mainfrom
claude/issue-108-frontline-ui
Sep 2, 2026
Merged

Give the frontline a progress phrase, attachments and a card worth showing#115
os-warren merged 4 commits into
mainfrom
claude/issue-108-frontline-ui

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #108

The three things deck p7 promises the frontline, plus the p16/p17 list and card. All metadata — no action, no handler, no customization.

Verified in a browser against a live pnpm demo (own port 4108, Chromium 1194). Gates below were run on the merged head 3b717e0.


1 · A progress phrase, one tap

duly_task.progress — four preset phrases (on_time / distributed / awaiting_feedback / in_hand) beside the free-text note, which stays as "write your own". No default: a dispatched task has reported nothing yet, and blank is the honest value for that.

task.hook.ts now stamps last_update_at when it changes. That was the point of putting it on the list — reporting progress is the product's headline gesture, and if it did not count as movement then a task nudged every week would still drift into "Not moving", which is the one list a manager is told to trust.

Red-first, and measured as such. With the hook's field list reverted to ['status','note','skip_reason'] (mutation confirmed on disk both ways) the two new assertions fail and the other 62 in the file stay green:

=== MUTATED: real spelling now 0 (want 0) · mutated spelling now 1 (want 1)
     × advances when a progress phrase is picked
     × advances when the phrase CHANGES, and not when it is re-sent
      Tests  2 failed | 62 passed (64)
--- RESTORE: mutated spelling present? 0 (want 0) · real spelling present? 1 (want 1)

Live proof through the console, not just the unit test — inline-select on a row of My week, then Save All:

BEFORE:     progress: null      last_update_at: 2026-08-03T07:00:00.000Z   ← stalled, ~30d
AFTER SAVE: progress: "in_hand" last_update_at: 2026-09-02T05:00:35.098Z   ← the hook stamped it

Inline select on a list row

Not a metric. The on-time verdict is completed_late, stamped by the server from completed_at against late_after. on_time here is a person's own words about their own work; reading it as evidence would make the phrase a scored field, and then nobody picks the honest one. Said in the object header, and no dataset names the column.

2 · Attachments, and never a gate

duly_task.attachments — the platform's own file field, multiple: true, no accept list.

It works with zero configuration, measured rather than assumed. file is a first-class FieldType, it is in MULTI_CAPABLE_TYPES so multiple makes it an array, and storage is in PLATFORM_ALWAYS_ON_CAPABILITIES — the CLI's serve command mounts StorageServicePlugin from @objectstack/service-storage whether or not a stack asks for it. The boot banner lists it among the 41 plugins with nothing declared in objectstack.config.ts. A real upload through the record form:

"attachments": [{ "id": "6060ff67-…", "name": "audit-cover-sheet.txt",
                  "size": 73, "mimeType": "text/plain",
                  "url": "/api/v1/storage/files/6060ff67-…" }]

Attachment uploaded

Never a completion gate — the product invariant, pinned twice rather than asserted once. test/invariants.test.ts covers the three shapes the mistake would take (required, a requiredWhen on done, a validation rule naming the column); test/task-hook.test.ts completes a fileless task against a booted engine, completes one that has files without treating them as evidence, and strips the last file off a done task without a refusal.

And an attachment deliberately does NOT move the stagnation clock. progress is on the hook's list, attachments is not. An upload that silently refreshed the clock would make "attach something" the cheapest way to look busy — one step from the evidence gate the invariant forbids. Confirmed live: after the upload above, last_update_at was still the timestamp the earlier phrase edit left.

3 · The list, the card, the record page

my_week carries the deck's columns in the deck's order — 状态 · 工作事项 · 来源 · 到期 · 最新进展 · 附件 — and is inline-editable. The manager lenses (late, stalled, by_unit) deliberately are not: managers do not enter status is a product invariant, and an editable status cell on a team lens breaks it one row at a time.

Relative dates need no key. Measured on console 17.2.0: the date cell defaults to format: 'relative' and derives "due-like" from the field NAME (a regex due_date matches), so it already renders Overdue 7d in red, In 3 days, Tomorrow, and an absolute date outside a ±7-day window. The card's fallback — a late_after column standing in for 逾期 N 天 — is therefore not needed, and late_after stays on the late lens that filters by it.

The board shows subject · due · owner · source · progress, with real column counts.

Board card face

The record page is 基本 / 进展与附件 / 历史, via fieldGroups + Field.group. history is exactly the set of readonly server-owned stamps — a rule, not a taste, so test/invariants.test.ts fails if a new stamp is filed anywhere else (it would otherwise appear mid-form, reading as a field somebody forgot to make editable).

Record field groups

Chinese

#18's coverage gate is green, and green there only means the keys exist — so the screens were opened in a zh-CN console as well. Column headers 最新进展 / 附件, phrase chips 待反馈 / 已下发各部门 / 处理中, relative dates 逾期 7 天 / 3天后, section headings 基本 / 进展与附件 / 历史, board columns 待办 / 进行中 / 已完成.

zh-CN My week
zh-CN board

A field-group heading turned out to have a bundle slot the walk did not know about, and the two halves of the platform disagree about it — recorded in authored-text.ts because only one half has a reader:

  • translateObject(obj, bundle, { locale }) does not rewrite fieldGroups[].label. Fed _sections.basics.label it returns the authored English untouched, while the object's own label and every field label beside it do translate.
  • The console does: its form runs the spec's deriveFieldGroupLayout, emits one section per group with name set to the group key, and resolves the heading through sectionLabelobjects.<object>._sections.<name>.label. So the key is real, and 基本 / 进展与附件 / 历史 render.

Three measurements worth keeping

kanban.cardFields is not authorable; kanban.columns is. KanbanConfigSchema is a strict object with exactly groupByField, summarizeField, columns, so cardFields is refused by pnpm validate rather than silently ignored — the good failure, and the opposite of the gantt block's passthrough trap. The relay reads cardFields: kanban.cardFields || kanban.columns || …, so columns lands on the card. Nothing to file.

Swimlanes are authorable, and are deliberately left off — objectstack-ai/objectui#7303. The key is the view-level grouping, not kanban.swimlaneField (strict schema rejects that one). Authored, it works: lanes rendered, headed ▼ CATALOG (19). It also takes the column headers with it — in swimlane mode the status header row measures height 0, the titles sit in the DOM at y=233 inside a collapsed flex … pl-36 overflow-x-auto, and a hit test at a title's own centre returns the lane's collapse button behind it. With the key absent the same board renders them as real <h3>s (OPEN 5 · IN PROGRESS 1 · DONE 20 · SKIPPED 0). A kanban whose columns are unlabelled is not a board, and this is a p0 pre-sales screen — so lanes off, source on every card face instead, one line in the view comment to flip the day #7303 lands.

⚠️ The first draft of that comment said swimlanes were impossible and would have filed a bug that is not there. It was written off the console's other relay (ObjectView, whose kanban branch forwards no grouping), and these views do not go through it. The browser is what corrected it.

"最新进展 (= progress 或 note)" is two things, not one. No authorable way to say "this or that" in a column: a stored latest_progress is the maintained-flag shape AGENTS.md rule 5 forbids, and a formula field is virtual and would render the stored on_time rather than the label. So the grid carries progress (short, tappable) and note stays on the record in the same group.

Two changes the card did not ask for

Both are named here because neither is invisible, and both are one line to revert.

  1. The board is scoped to its viewer (owner = {current_user_id}). Dragging a card writes status on the task under it, so a board of other people's rows hands every viewer a one-gesture way past managers do not enter status — the board already sits under "My work" for that reason, and this makes its data agree with its placement. It also fixes a live defect: unscoped, the first 100 of 186 rows sorted by due date were all done, so Open and In-progress rendered "No cards / 0" on a board whose whole job is live work. Scoped, it is 26 rows, one page, every count true.
  2. The demo's in-flight tasks carry a phrase, spread deterministically by position like every other variation in that fixture. A column that is empty on all 186 rows demonstrates nothing. Stalled rows and done rows stay blank on purpose, and each blank says something: "untouched since dispatch" and "the owner reported progress" are contradictory claims, and a completed task's report is its status — stamping on_time on 151 finished rows would put a self-reported phrase beside completed_late: true wherever the drift ran over.

Gates

Run through the shared verify lock on the merged head 3b717e0, after merging origin/main (#110 and #112 landed first; no conflicts):

✓ Validation passed (499ms)      Data: 5 Objects  61 Fields
✓ tsc --noEmit                   exit 0
  Test Files  31 passed (31)
       Tests  789 passed (789)
✓ Build complete (755ms)

Screenshots live on the throwaway branch screenshots/issue-108 — delete it with the PR.

🤖 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:38
`duly_task.progress` is four preset phrases plus the free-text `note` beside
it, so reporting progress is one tap instead of a typed sentence; the hook
stamps `last_update_at` when it changes, because picking a phrase is somebody
working the task. `duly_task.attachments` is the platform file field, optional
on every path and never a completion gate — pinned in the metadata and against
a booted engine. The list, the board card and the record page's three sections
are the deck's p7/p16/p17 layout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
…finding

The board is the owner's own: dragging a card writes `status`, and "managers
do not enter status" has to be true of the data and not only of where the nav
puts the link. It also keeps the lens inside one fetched page — unscoped, the
first 100 of 186 rows sorted by due date were all `done` and the Open and
In-progress columns rendered empty.

Swimlanes by source are authorable (`grouping`, not `kanban.swimlaneField`)
and are deliberately left off: measured in a browser, turning them on renders
the status column-header row at height 0.

The demo's in-flight tasks carry a phrase so the new column and card face show
something; stalled and done rows stay blank, and each blank says something.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
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:24

Copy link
Copy Markdown
Collaborator Author

Reviewed — merging. Open question: A, no swimlanes.

Gates, re-run by me on the head merged with current main: validate 0, typecheck 0, test 0 (Test Files 31 passed, Tests 789 passed), build 0. Verified in the source: the stamp list is ['status', 'progress', 'note', 'skip_reason'] with attachments deliberately absent and the asymmetry explained at the site; attachments is not required, has no requiredWhen, and no rule names it; the board filters owner = {current_user_id}; swimlanes are off with the one-line re-enable in the view comment.

A. A board whose column headers render at height 0 is not a board — on a pre-sales screen a viewer must be able to tell Open from Done without dragging a card. source on the card face carries the deck's p17 intent until objectui#7303 lands. And the correction you recorded on yourself is the part worth keeping: your first draft called swimlanes impossible and would have filed a bug that does not exist, because you were reading the ObjectView relay these views do not go through. The browser overruled the reading. That is the discipline this repo runs on.

The attachments asymmetry is the best decision in the PR. An upload does not move last_update_at, so uploading cannot be the cheap way to look busy — and you confirmed it in the running app, not only in the hook test (last_update_at unchanged after a real upload through the record form). That closes a hole the deck's "附件自主上传" would otherwise have opened in the stagnation signal.

Two changes the card did not ask for, both accepted:

  • Board scoped to the current user. Right, and for a reason beyond the empty-columns symptom: dragging a card writes status, and an unscoped board lets anyone move anyone's task. This is "my board". The deck's p17 department board ("部门负责人一眼看出堆在哪个环节") is a read surface and a different view; the manager has by_unit and the dashboard for that today. If a department board is wanted for the demo, it is a separate card with a read-only kanban, not a wider filter on this one.
  • Demo tasks carry a phrase so the new column is not blank on every row. Fine — and consistent with the seed's rule that history comes from the engine, since progress is authored input, not a stamp.

The zh-CN 1 file beside 附件 is the console's own string; leave it unfiled until the key is identified, as you did.


Generated by Claude Code

@os-warren
os-warren merged commit 45ebcb3 into main Sep 2, 2026
1 check passed
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.

一线界面:进展预设短语、任务附件,列表与看板卡片按方案 p7/p16/p17 补齐

1 participant