Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Manager dashboard: what has stopped, and what is coming up - #67

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard
Sep 1, 2026
Merged

Manager dashboard: what has stopped, and what is coming up#67
os-warren merged 2 commits into
mainfrom
claude/issue-10-manager-dashboard

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#10

duly_duty_health, bound entirely to the semantic layer, reachable from the Team nav group as its first entry.

Gates green on 2d1a27e (the tip of this branch): pnpm validate 0 · pnpm typecheck 0 · pnpm test 0 (457 passed / 15 files, up from 419 / 14) · pnpm build 0. The one validate warning is the expected hierarchy-security capability-provider line, which AGENTS.md rule 7 says not to silence.

The layout, and the item that is not in it

#cardshipped
1Not moving, >14d with >30d beside itnot_moving_14d (top-left, largest tile) + not_moving_30d, plus oldest_touch as a date
2Latenot built — see below
3On-time ratenot built — per the PM note (#52)
4By unitnot_moving_by_unit, ordered by the unit dimension
5Coming upcoming_up, due next 14 days by week

Items 2 and 3 are the same missing comparison, not two omissions. Both need due_date + duty.grace_days: there is no date arithmetic in the filter grammar, the {N_days_ago} vocabulary is relative to NOW rather than to another column, and here the offset is itself a column (objectstack#14104). No dataset declares an on-time or a lateness measure for a widget to bind, and src/datasets/ is not mine to change on this card.

A grace-free lateness is expressible without touching a dataset — a widget filter of due_date < {today} over tasks_due is two lines — and is deliberately not built. It marks late every task still inside the grace its own duty grants, so a customer who configured 7 days gets its people listed late the morning after the due date. That is the bug #48 already records against the late LIST view, and the product decision is open on #52. Deciding it in a widget would settle it by accident.

test/dashboard.test.ts pins the absence in both directions: no widget may bind an on_time / late / overdue measure, and no widget may rebuild one out of a due-date window bounded above by {today} or a past token (the forward window this dashboard does use is bounded above by a future token, which is a different question). When the tile arrives, both pins come off with it.

The absence is also stated on the screen, in the dashboard description, next to the caliber note — a manager reading "not moving: 3" on a screen silent about lateness concludes there are three problems. An unexplained absence is a wrong number with no digits.

What the platform actually guards — the card's second assumption was wrong too

The PM note said dataset bindings are guarded "above" and asked whether test/metadata-bindings.test.ts reaches dashboard widgets. It does not — its dulyDashboards import (added here) resolves the nav entry only. But the more useful answer came from measuring rather than assuming. Each mutation below was applied to the real dashboard, confirmed on disk with grep -F before the gates ran, and restored by a trap:

mutated referencevalidatebuildrule
widget datasetduly_stagnatoin11widget-dataset-unknown (+ did-you-mean)
widget dimensions[]business_unitt11widget-dimension-unknown
widget values[]untouched_over_1411widget-measure-unknown
{token} in widget filter → {14_days_hence}11filter-token-unknown, path-precise
dashboard dateRange.fieldnonexistent_column11dashboard-filter-field-unknown
nav dashboardNameduly_ghost12defineStack cross-reference
widget filter KEY → due_daet00
options.sortBynot_selected00

The last two are the real holes, and the first of them is sharp: on the same filter node, a bad {token} is caught with the exact path widgets[4].filter.due_date.$lte while a misspelt column is not — and dashboard-filter-field-unknown already resolves dataset → base object → field set per widget for the dashboard-level filter one key over. Filed upstream as objectstack-ai/objectstack#14148 with both halves and the repro. test/dashboard.test.ts's binding walk is the repo-local stopgap and is written to be deleted when that lands.

Every claim in the checked-in comments is one of these measurements; the first commit's comments asserted the opposite (inherited from the card) and the second commit corrects them.

Shapes the PM flagged, and where each one landed

  • Cumulative buckets>14d and >30d are separate tiles, never two series in one chart. Pinned: no widget may select two nested thresholds together, none may draw one as a pie/donut/funnel, and none may stack one.
  • due_week / due_monthcoming_up groups by due_week only.
  • tasks_due identical in both datasets — so the forward look is scoped by a date window and not narrowed by status: adding status IN (open,in_progress) would put a different number behind a name the semantic layer defines once.
  • oldest_last_update_at is a timestamp — a metric tile labelled as a date, never a bar length.
  • The governed filter — neither added nor removed; it rides on the measures.

No ranking of people

No widget selects owner. The rule is pinned as a property rather than a spot-check: a person dimension is detected by resolving the dataset dimension's field to a sys_user lookup, so a second one added later is covered without editing the test. Two further pins hold even if a per-person workload widget is ever added deliberately (duly_workload keeps owner for exactly that): it may not be ordered by a measure, and it may not be truncated to a top N.

sortBy on the unit chart names the dimension. Ordering a lookup used to sort by the opaque FK id — "sorted by unit" that reads as random (objectstack#3680) — which was fixed upstream in #3693 to sort by the resolved display label. That is what makes this the unit name order the card asked for, not merely a count-free one.

Contrast, both themes

No text is drawn on a fill anywhere: showDataLabels: false is stated explicitly on both charts, so every label renders as axis or legend text on the card background. That is not tidiness — it is forced. Both fills are mid-tones chosen to clear 3:1 as graphical objects (WCAG 1.4.11) against a white and a near-black card, since metadata carries no per-theme colour: #B07C17 (L≈0.237 → 3.7:1 light, 5.3:1 dark) and #2E7C8E (L≈0.169 → 4.8:1 light, 4.0:1 dark). At that lightness white label text on the amber fill is 3.7:1, a failure for text — the two constraints have no common solution in one hex, so the labels come off the fill. The app's darker palette entries were measured and rejected for fills on the same test (#16515F 2.2:1, #5A3F0C 2.0:1 on a dark card). Tiles use warning / orange and never danger: attention, not blame — asserted.

File surface — one breach, flagged

test/metadata-bindings.test.ts is outside the surface the card named, and the change is not optional: that file's two tripwires fail the moment a dashboard nav entry exists ("teach walkNav about it before trusting the nav check"), which is the guard working as designed. It is taught the type — dashboardName resolved against the dashboards barrel, with self-test fixtures in both directions — and its nav-type tripwire now excludes the type it reads. Its comment records that this one, unlike viewName (#14108), is not an unguarded reference: defineStack refuses it. Everything else is on-surface: src/dashboards/*, the Team nav entry in src/apps/duly.app.ts, test/dashboard.test.ts. objectstack.config.ts, src/datasets/ and src/views/ are untouched.

What to look at in the browser

Acceptance is a running app, so the three things I could not verify from here:

  1. oldest_touch — a min over a datetime. It should read as a date; if the KPI card renders a raw ISO string or an epoch number, that is a renderer formatting gap and I would rather file it than paper over it with an unverified options key.
  2. Unit order on the by-unit chart — should be alphabetical by unit name per #3693.
  3. coming_up bucket boundaries — ISO weeks (Monday start) wherever the driver does not truncate server-side; the window is [today, +14d], so the first and last buckets are partial weeks by construction.

In this open-edition checkout the hierarchy scopes resolve to owner-only (AGENTS.md rule 7), so every number will be scoped to the signed-in user rather than to their unit — that is the edition, not this dashboard.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 08:08
Adds `duly_duty_health`, bound entirely to the semantic layer, and reaches
it from the Team nav group.
Two of the card's five items — "Late" and "On-time rate" — are the same
missing comparison (`due_date + duty.grace_days`, objectstack#14104) and
are deliberately not approximated; the product decision is open on #52.
The absence is stated on the screen rather than left silent.
`test/dashboard.test.ts` resolves every widget binding against the datasets
barrel: nothing in the platform does, and an empty "not moving" tile reads
exactly like a healthy team. `test/metadata-bindings.test.ts` is taught the
`dashboard` nav type it deliberately fails on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The card and its PM note assumed the whole dashboard binding surface was
unguarded, carrying #14105 (datasets) up a layer. Measured instead, one
mutation at a time on cli 17.2.0, and most of it IS guarded: widget
dataset / dimensions / values and filter {tokens} all fail both gates with
a named rule, and an unresolvable nav dashboardName is refused by
defineStack itself.
Two references are not resolved and fail silently — a widget's own filter
KEYS, and options.sortBy naming something the widget does not select.
Filed as objectstack#14148, with the note that the identical resolution
already exists one key over (dashboard-filter-field-unknown, #3365).
Comments that claimed otherwise are rewritten; the stopgap now points at
the gap it actually stands in for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 08:25
@os-warren
os-warren merged commit b93951a into mainSep 1, 2026
1 check passed
Sign up for freeto 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.

Manager dashboard — what is late, what has stopped

1 participant

@os-warren