Skip to content

Retire the "Oldest untouched task" tile: the top row is two stagnation numbers - #126

Closed
os-warren wants to merge 2 commits into
mainfrom
claude/issue-122-oldest-touch-tile
Closed

Retire the "Oldest untouched task" tile: the top row is two stagnation numbers#126
os-warren wants to merge 2 commits into
mainfrom
claude/issue-122-oldest-touch-tile

Conversation

@os-warren

@os-warren os-warren commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #122

Route taken: the card's fallback — the tile goes, not_moving_30d takes the row. The preferred route (a days-since integer) was measured against the real analytics door and is not expressible; so was a third route the card did not know about (format: 'relative'). Both measurements are below.

⚠️ Read first: half the card's premise no longer reproduces

The card measured Console @objectstack/cli 17.2.0 and reported the tile printing 2026-07-04T07:00:00.000Z verbatim and overflowing at both viewports. On main today (17.3.0) that symptom is gone — the console runs the value through a locale date formatter (objectui#7178, fixed and shipped):

zh-CN en-US overflow
Console 17.2.0 (card) 2026-07-04T07:00:00.000Z same yes, both viewports
Console 17.3.0 (measured here) 2026年7月4日 07:00 Jul 4, 2026, 07:00 AM none — scrollWidth == clientWidth at 1440 and 390

What survives is the half the card's "done" section is actually written against: the tile answers with an instant, carrying a time-of-day that is the dispatch clock, in a row where every other tile is a count or a rate — and "days since" cannot replace it. That is what this PR fixes. If the maintainer now prefers keeping a formatted-date tile, this PR is one revert — say so and I will restore it rather than argue.

Why the preferred route is impossible (measured, not assumed)

Against @objectstack/spec 17.3.0 and a real pnpm demo:zh boot:

  1. AggregationFunction is count | sum | avg | min | max | count_distinctno date-difference member.
  2. A derived measure is { op, of } with op: ratio | sum | difference | product and of: z.array(SnakeCaseIdentifierSchema)other measure NAMES only. No literal operand, no now measure, so today − min(last_update_at) has nothing to put on the left of the minus.
  3. Even measure-to-measure arithmetic over instants is dead. A temporary measure { op: 'difference', of: ['newest_last_update_at', 'oldest_last_update_at'] } was added to the real dataset, booted, and queried through POST /api/v1/analytics/dataset/query (reverted before any commit — git checkout confirmed clean):
{"rows":[{"oldest_last_update_at":"2026-07-04T07:00:00.000Z",
          "newest_last_update_at":"2026-09-05T06:36:02.452Z",
          "untouched_over_14d":3,"untouched_over_30d":3,
          "days_untouched_attempt":null}],
 "fields":[{"name":"oldest_last_update_at","type":"number","label":"Oldest touch"}, ]}

null, because the executor's computeDerived coerces every operand with Number() and Number('2026-07-04T07:00:00.000Z') is NaN. Note the fields[] entry: the door types the measure number and hands back an ISO string — that mismatch is the defect underneath the symptom.

  1. The remaining way to a days number is a stored column recomputed nightly — AGENTS.md rule 5's banned shape, and the card refuses it by name.

5. And the obvious question — "why not just format it?" — was probed too

objectstack#14933 records that a date measure's format is read as a display style, with 'relative' rendering "3 days ago". That would have answered this card without removing anything, so it was tried rather than reasoned about: format: 'relative' added to the real measure, rebuilt, booted, tile read in both locales.

measure.format zh-CN en-US
absent 2026年7月4日 07:00 Jul 4, 2026, 07:00 AM
'relative' 2026年7月4日 07:00unchanged Jul 4, 2026, 07:00 AMunchanged

The style is not dropped on the wire — the door carries it ("fields":[{"name":"oldest_last_update_at","type":"number","label":"Oldest touch","format":"relative"}]) — the dashboard metric tile simply does not honour it on the console shipped with cli 17.3.0. Probe reverted; tree clean.

Filed upstream as objectstack-ai/objectstack#15768 with both halves (the type: "number" mislabel on an instant, and the ignored style), including the note that it makes #14933's proposed one-line wording wrong for the tile path. AGENTS.md rule 9: the gap is reported, not written around.

What changed

  • src/dashboards/duty-health.dashboard.ts — the oldest_touch widget is gone, replaced in place by a removal note carrying the measurements above. not_moving_30d widens into the row.
  • The row is 7/5, not 6/6, and that second half was measured in the browser. Six columns at h: 4 equals the headline's area and fails the pin that no other number may compete with it. The first attempt — w: 6, h: 3 — passed every gate and broke the screen: the console's grid compacts vertically, so the shorter tile left a gap, the on-time rate was pulled into the right column beside the headline, and the overdue and completeness tiles rose to fill the left. A row of tiles must be uniform in height, so width is the only free dimension: headline 7, subset tile 5 — the same split the two charts below already use. Area 28 vs 20, pin green with room.
  • src/datasets/stagnation.dataset.ts — the measure stays. It is a legitimate semantic-layer value (a table column, a report, an API read), and this dataset already ships measures no widget binds (open_tasks, untouched_over_7d). What is banned — in a comment, and in a test — is binding an instant to a single-value tile.
  • test/dashboard.test.ts — a new walk over the dashboards barrel: no metric/kpi widget may bind a value-preserving aggregate (min/max/sum/avg) over a date/datetime/time field. It resolves the measure's field type through the objects barrel rather than matching a name, records a boundary (not a pass) for a joined path or an unknown field, and ships nine synthetic self-tests — a guard written to stay green forever has no other evidence that it can fail.
  • src/translations/zh-CN.ts — the widget's bundle keys are removed with the widget (test/i18n-coverage.test.ts checks both directions); the measure's label key stays with the measure. The >30d description gains the pointer below, in both languages.
  • The worst single case stays reachable. src/views/task.view.ts's stalled view already sorts last_update_at ascending and shows that column — its first row is the answer, with the task and the owner beside it, which a bare date on a tile never had. The >30d tile's description now says so.

Reverse-verification: the retired tile was reinstated on the real dashboard (mutation confirmed on disk by grep -c, restored by an EXIT/INT/TERM trap, restore confirmed by git diff --quiet). test/dashboard.test.ts went to 2 failed | 50 passed, exit 1 — the two failures being the new guard and the "tile is gone" pin.

Gates — all four green at c22d68b, the final commit

✓ Validation passed (538ms)        # pnpm validate  (+ the expected hierarchy-security warning AGENTS.md documents)
  (tsc --noEmit, silent)           # pnpm typecheck
  Test Files  35 passed (35)
       Tests  897 passed (897)     # pnpm test
✓ Build complete (879ms)           # pnpm build

Run as one chain through the shared verify lock: VERDICT command-exit 0 · held the lock 43s.

Browser proof

pnpm demo:zh -- --port 4320 on a clean .objectstack/data, Chromium at locale: zh-CN with accept-language: zh-CN,zh;q=0.9, signed in through POST /api/v1/auth/sign-in/email. Both committed:

  • 1440×2300docs/i18n/01-dashboard-zh.png. Re-captured rather than added: it showed the retired tile, and this PR is what made it stale. Top row is two numbers; the reading order below it is unchanged.
  • 390×844docs/i18n/04-dashboard-zh-390.png. New; the docs had no mobile capture.

No ISO timestamp in the page text at either width, and document.scrollWidth == clientWidth at both — no horizontal overflow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p

os-warren and others added 2 commits September 5, 2026 06:45
`duly_stagnation.oldest_last_update_at` is `min(last_update_at)`, and the
"Oldest untouched task" metric tile bound it. A metric widget renders one
value as it arrives, and for that measure what arrives is an instant:
`POST /api/v1/analytics/dataset/query` answers `"2026-07-04T07:00:00.000Z"`
under a `fields[]` entry typed `number` (measured on a real boot, 17.3.0).

#122 asked for days-since instead. It is not expressible in the dataset
layer, measured rather than assumed:

  - `AggregationFunction` is count | sum | avg | min | max | count_distinct
    — no date-difference member.
  - A derived measure is `{ op, of }` with `of: z.array(SnakeCaseIdentifier)`
    — other MEASURE NAMES only. No literal, no `now`, so `today - min(...)`
    has nothing on the left of the minus.
  - Measure-to-measure arithmetic over instants is dead too: a temporary
    `{ op: 'difference', of: [max_ts, min_ts] }` added to the real dataset,
    booted and queried, answered `null` — `computeDerived` coerces operands
    with `Number()` and an ISO string is `NaN`.

A stored `days_stalled` is AGENTS.md rule 5's banned shape, so the card's
own fallback ships: the tile goes and `not_moving_30d` takes the row. Its
`h` drops 4 -> 3 in the same edit — at `w: 6, h: 4` it would equal the
headline's area and fail the pin that no other number may compete with it.

The MEASURE stays: it is a legitimate semantic-layer value (table column,
report, API read) and this dataset already ships measures no widget binds.
What is banned is putting an instant on a single-value tile, and
test/dashboard.test.ts now walks the dashboards barrel for exactly that,
resolving the measure's field type through the objects barrel rather than
matching a name. The worst single case stays reachable in task.view.ts's
`stalled` view, which already sorts `last_update_at` ascending.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The first attempt at the widened subset tile was `w: 6, h: 3` — six columns
because the card asked for them, three rows tall because six-by-four would
equal the headline's area and fail the pin that no other number may compete
with it.

Screenshotted at 1440, that reordered the screen. The console's grid
compacts vertically: the shorter tile left a gap, the on-time rate was
pulled up into the right column beside the headline, and the overdue and
completeness tiles rose to fill the left — so the documented reading order
(the on-time rate directly under the headline) silently stopped being what
the screen showed, with every gate still green.

A row of tiles has to be uniform in height, which leaves width as the only
free dimension: the headline takes seven columns and the subset tile five,
the same 7/5 split the two charts below already use. The area rule holds
with room to spare (28 vs 20).

docs/i18n/01-dashboard-zh.png is re-captured from this build — it showed
the retired tile — and 04-dashboard-zh-390.png is the same screen at 390px,
which the docs did not have. Both are `pnpm demo:zh`, Chromium at
locale zh-CN, signed in as the demo admin.

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

Copy link
Copy Markdown
Collaborator Author

Closing without merging — and thank you for verifying the premise first, which is what decides this.

Re-measured on main (74342a1, @objectstack/* 17.3.0) with a fresh pnpm demo:zh boot: the oldest_touch tile renders 2026年7月4日 07:00, no overflow at 1440, sub-label Chinese. The defect #122 was filed against (a raw ISO string overflowing the tile) is gone with objectui#7178, exactly as your report says. What is left is the design question — an instant among counts — and the dashboard's own docblock argued for a date there ("the worst single case, as a DATE… a date, never a magnitude and never a person"). With the platform now formatting it, that design holds and the tile stays.

Not taking the guard test either, since it would forbid the tile we are keeping. The 390px capture and the analytics-door measurements are recorded here and on #122; objectstack#15768 stays open as the upstream follow-up.


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.

Dashboard "Oldest untouched task" tile renders a raw ISO timestamp — replace the measure with a number a manager can read

1 participant