Skip to content

仪表盘对齐方案 p20:逾期项、清单完备度、各部门逾期分布、本月工作构成 #109

Description

@os-warren

售前演示优先级 p0。 方案 v7 p20 是「领导端的第一屏」:四个指标卡 + 两张图。现在 duly_duty_health 有停滞三卡 + 按期率 + 两张图,但方案的四卡里缺两张、两张图都不是方案的。全部是数据集 + 组件元数据。

四个指标卡(p8、p20 同一组)

卡 现状 做法
停滞项 ✔ 保留
逾期项 ✗ #52 落地后 late_after 是普通日期列:late_after < {today} AND status IN (open, in_progress),治理口径
按期完成率 ✔ (#52) 保留;format 问题见 #101,不要绕
清单完备度 ✗ 按 review_status 计数、不按条目数:approved 占全部治理职责的比例,附「N 条待确认 / M 条待审定」副标。字段由同批「清单确认与审定」卡片提供;先落地的一方定义数据集,后落地的合 main。绝不做「谁条目数偏低」——AGENTS.md 不变量

两张图(p20)

  • 各部门逾期项分布:柱图,维度业务单元(按名字排序,永远不按值),度量逾期项。
  • 本月工作构成:饼图 重复事项 / 单次事项 / 常设职责——按 duly_duty.form 计数(recurring / one_off / standing)。注意常设职责没有任务,这张图数的是职责不是任务;图下一句口径说明「完成率只统计重复事项」(p20 原文)。

规则(沿用现有仪表盘文件头)

  • 每个组件绑数据集,不绑对象;不出现任何按人排序的维度。
  • 停滞桶是累进的,不叠不堆。
  • 颜色沿用现有两色,showDataLabels: false 的对比度理由在文件头,别改。
  • 度量标签的中文等平台 #14253 发布(duly#106),本卡不绕。

验收

  • 浏览器截图:四卡两图,用种子数据有真实数字(种子里要有几条逾期、几条待确认——和同批两张卡对齐)
  • test/dashboard.test.ts 的「不按人排序」属性守卫仍然通过并覆盖新组件
  • 四道门绿;中文 bundle 补齐新组件标题/描述

Activity

  1. self-assigned this
    on Sep 2, 2026
  2. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor

    Claiming this issue.

    Scope: the four KPI tiles + two charts of deck p20 on duly_duty_health — datasets + widget metadata only. Note the review_status dependency on the parallel 清单确认与审定 card (#107): I will define the 清单完备度 measure against review_status regardless and merge main if #107 lands first / report the gate refusal if it has not.


    Generated by Claude Code

  3. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor
    {
      "issue": 109,
      "status": "done",
      "branch": "claude/issue-109-dashboard-p20",
      "pr": "https://github.com/objectstack-ai/duly/pull/111",
      "premise_still_valid": true,
      "summary": "Added the two missing p20 KPI cards and both p20 charts to duly_duty_health, all as metadata: a new measure duly_duty_health.tasks_overdue (late_after < {today} AND status IN (open,in_progress) — a plain date filter on #52's dispatch stamp, never a due_date window), a new fourth dataset duly_duty_register based on duly_duty (the only object that can see standing and unapproved duties), and four widgets — Overdue tile, List completeness tile, Overdue-by-unit bar ordered by unit name, and a Work mix pie over duly_duty.form. TWO DEVIATIONS, both documented in the module header rather than silently taken. (1) The completeness tile ships WITHOUT the card's 'N to confirm / M to approve' sub-caption: a dataset-bound KPI renders values[0] and drops the rest, and the authored sub-caption slot (options.description, translation key ...subCaption) is read only on the inline metric path, which no spec-valid widget can reach because `dataset` is required. Measured in objectui and filed as objectstack-ai/objectui#7293; not faked as static text, and the two counts are deliberately not declared on the dataset until the renderer can show them. (2) The deck's 'four cards, two charts' is delivered as six tiles and four charts: the three stagnation tiles ARE the deck's 停滞项 card (the file header's nested-threshold reasoning, pinned by test/dashboard.test.ts), and the two existing charts were not retired to hit a slide count. No seed change was needed — the fixture already produces 4 overdue tasks across 3 units.",
      "tests": "Gates at f3047e8 under the shared verify lock (scripts/pm/os-verify-lock.sh), exit codes captured before any pipe. validate: 'os-verify-lock: VERDICT command-exit 0' / '✓ Validation passed (545ms)' with the one expected hierarchy-security warning. typecheck: VERDICT command-exit 0, tsc --noEmit silent. build: VERDICT command-exit 0 / '✓ Build complete (723ms)'. test: VERDICT command-exit 1 — 'Test Files 1 failed | 28 passed (29) / Tests 1 failed | 753 passed (754)'. The single failure is the #107 dependency and nothing else: test/metadata-bindings.test.ts prints \"dataset duly_duty_register · measures 'duties_approved'.filter key: \\\"review_status\\\" — \\\"review_status\\\" is not a field on duly_duty\". NOTE the card predicted `pnpm validate` would refuse it; measured, validate exits 0 because it does not resolve dataset field references at all (objectstack#14105) — the unit gate is the only one that sees it. MERGE MEASURED, not assumed: merging #107's pushed branch head 868c8a5 into mine in a throwaway worktree (953bf9e, 17 files / 985 insertions, no conflicts) gives all four gates green — validate 0, typecheck 0, 'Test Files 30 passed (30) / Tests 787 passed (787)', build 0. So it is landing order only; whoever lands second merges main and re-runs. BROWSER: pnpm demo on port 4109, fresh DB, own PIDs killed by recorded pid (never by name). Live off POST /api/v1/analytics/dataset/query — tasks_overdue = 4; by business_unit = Northgate Operations 1, Riverside Plant 1, Northgate Quality 2 (the largest bar renders in the MIDDLE, so the by-name order is visible on screen); duties_in_register by form = Recurring 23, One-off 1, Standing 3. The completeness tile shows the runtime's own 400 naming review_status — refused loudly at query time, not silently zero. SIMULATION of #107's field (local, uncommitted, labelled as such in the PR): mutation confirmed on disk by anchored grep -c counts BEFORE any reading (review_status lines 0->1, to_confirm 0->1) and restored by an EXIT/INT/TERM trap with git status clean after; the tile then rendered 0.85 from {duties_approved: 23, duties_in_register: 27}. A first simulation run left every duty at the approved default and rendered 1.00 — which independently re-confirms #101's finding that '0.0%' would have printed 1.0% on a perfect register. New unit coverage: test/datasets.test.ts gains the task-vs-duty base-object split, the overdue measure's late_after/{today} threshold plus 'reads no due_date', and five duly_duty_register assertions; test/dashboard.test.ts gains a block pinning the four p20 numbers by the measure each binds. test/i18n-coverage.test.ts's untranslatable dataset-label count moved 29 -> 36 (the new measure label + the new dataset's six strings), updated with the reason in the comment.",
      "open_questions": [
        {
          "question": "The deck's p20 is 'four KPI cards and two charts'. This PR ADDS the two missing cards and the two deck charts, leaving the screen at six tiles and four charts. Is that the intended end state, or should the two pre-existing charts (Not moving by unit / Coming up) be retired so the screen matches the slide exactly?",
          "options": [
            "A — keep all four charts (what shipped). The issue's own table marks the three stagnation tiles as 保留 and never asks for a removal; the file header defends both existing charts (earliest actionable signal per unit, and the forward look); and test/dashboard.test.ts pins the >30d tile's 'subset' wording plus needs a widget filter to exist at all, so removals would mean weakening pinned guards.",
              "B — retire 'Not moving, by unit' and 'Coming up' so the screen is literally four tiles and two charts for the demo.",
            "C — keep them but move them below the two deck charts so the first screenful is exactly p20."
          ],
          "recommendation": "A, because nothing on the card asks for a removal and both existing charts carry measured product reasoning in the module header that this task was told to extend rather than contradict. If the demo needs a literal p20 screenful, C is the cheap version (a layout-only change to two `layout.y` values) and does not delete anything."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectui#7293: a dataset-bound KPI tile can render no sub-caption at all — DatasetWidget's isMetric branch drops every measure after values[0], and the authored options.description sub-caption (translation key ...subCaption, resolved by tWidgetSubCaption, overlaid server-side by translateDashboard) reaches only the inline object-metric path, which no spec-valid widget can take because `dataset` is required on DashboardWidgetSchema — a declared-and-unenforced key with four layers of live plumbing behind it"
      ]
    }

    Generated by Claude Code

  4. os-warren commented on Sep 2, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    (Replaces the comment above: GitHub's body sanitizer ate the HTML-comment marker on that one, so this is the same report with a literal-text marker the PM scan can see. There is no edit-comment tool available in this session, hence a replacement rather than a fix in place.)

    {
      "issue": 109,
      "status": "done",
      "branch": "claude/issue-109-dashboard-p20",
      "pr": "https://github.com/objectstack-ai/duly/pull/111",
      "premise_still_valid": true,
      "summary": "Added the two missing p20 KPI cards and both p20 charts to duly_duty_health, all as metadata: a new measure duly_duty_health.tasks_overdue (late_after earlier than {today} AND status IN (open,in_progress) — a plain date filter on #52's dispatch stamp, never a due_date window), a new fourth dataset duly_duty_register based on duly_duty (the only object that can see standing and unapproved duties), and four widgets — Overdue tile, List completeness tile, Overdue-by-unit bar ordered by unit name, and a Work mix pie over duly_duty.form. TWO DEVIATIONS, both documented in the module header rather than silently taken. (1) The completeness tile ships WITHOUT the card's 'N to confirm / M to approve' sub-caption: a dataset-bound KPI renders values[0] and drops the rest, and the authored sub-caption slot (options.description, translation key ending .subCaption) is read only on the inline metric path, which no spec-valid widget can reach because `dataset` is required. Measured in objectui and filed as objectstack-ai/objectui#7293; not faked as static text, and the two counts are deliberately not declared on the dataset until the renderer can show them. (2) The deck's 'four cards, two charts' is delivered as six tiles and four charts: the three stagnation tiles ARE the deck's 停滞项 card (the file header's nested-threshold reasoning, pinned by test/dashboard.test.ts), and the two existing charts were not retired to hit a slide count. No seed change was needed — the fixture already produces 4 overdue tasks across 3 units.",
      "tests": "Gates at f3047e8 under the shared verify lock (scripts/pm/os-verify-lock.sh), exit codes captured before any pipe. validate: 'os-verify-lock: VERDICT command-exit 0' / '✓ Validation passed (545ms)' with the one expected hierarchy-security warning. typecheck: VERDICT command-exit 0, tsc --noEmit silent. build: VERDICT command-exit 0 / '✓ Build complete (723ms)'. test: VERDICT command-exit 1 — 'Test Files 1 failed | 28 passed (29) / Tests 1 failed | 753 passed (754)'. The single failure is the #107 dependency and nothing else: test/metadata-bindings.test.ts prints [dataset duly_duty_register · measures 'duties_approved'.filter key: \"review_status\" — \"review_status\" is not a field on duly_duty]. NOTE the card predicted `pnpm validate` would refuse it; measured, validate exits 0 because it does not resolve dataset field references at all (objectstack#14105) — the unit gate is the only one that sees it. MERGE MEASURED, not assumed: merging #107's pushed branch head 868c8a5 into mine in a throwaway worktree (953bf9e, 17 files / 985 insertions, no conflicts) gives all four gates green — validate 0, typecheck 0, 'Test Files 30 passed (30) / Tests 787 passed (787)', build 0. So it is landing order only; whoever lands second merges main and re-runs. BROWSER: pnpm demo on port 4109, fresh DB, own PIDs killed by recorded pid (never by name). Live off POST /api/v1/analytics/dataset/query — tasks_overdue = 4; by business_unit = Northgate Operations 1, Riverside Plant 1, Northgate Quality 2 (the largest bar renders in the MIDDLE, so the by-name order is visible on screen); duties_in_register by form = Recurring 23, One-off 1, Standing 3. The completeness tile shows the runtime's own 400 naming review_status — refused loudly at query time, not silently zero. SIMULATION of #107's field (local, uncommitted, labelled as such in the PR): mutation confirmed on disk by anchored grep -c counts BEFORE any reading (review_status lines 0 to 1, to_confirm 0 to 1) and restored by an EXIT/INT/TERM trap with git status clean after; the tile then rendered 0.85 from {duties_approved: 23, duties_in_register: 27}. A first simulation run left every duty at the approved default and rendered 1.00 — which independently re-confirms #101's finding that '0.0%' would have printed 1.0% on a perfect register. New unit coverage: test/datasets.test.ts gains the task-vs-duty base-object split, the overdue measure's late_after/{today} threshold plus 'reads no due_date', and five duly_duty_register assertions; test/dashboard.test.ts gains a block pinning the four p20 numbers by the measure each binds. test/i18n-coverage.test.ts's untranslatable dataset-label count moved 29 to 36 (the new measure label + the new dataset's six strings), updated with the reason in the comment.",
      "open_questions": [
        {
          "question": "The deck's p20 is 'four KPI cards and two charts'. This PR ADDS the two missing cards and the two deck charts, leaving the screen at six tiles and four charts. Is that the intended end state, or should the two pre-existing charts (Not moving by unit / Coming up) be retired so the screen matches the slide exactly?",
          "options": [
            "A — keep all four charts (what shipped). The issue's own table marks the three stagnation tiles as 保留 and never asks for a removal; the file header defends both existing charts (earliest actionable signal per unit, and the forward look); and test/dashboard.test.ts pins the 30d tile's 'subset' wording and needs a widget filter to exist at all, so removals would mean weakening pinned guards.",
            "B — retire 'Not moving, by unit' and 'Coming up' so the screen is literally four tiles and two charts for the demo.",
            "C — keep them but move them below the two deck charts so the first screenful is exactly p20."
          ],
          "recommendation": "A, because nothing on the card asks for a removal and both existing charts carry measured product reasoning in the module header that this task was told to extend rather than contradict. If the demo needs a literal p20 screenful, C is the cheap version (a layout-only change to two `layout.y` values) and does not delete anything."
        }
      ],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectui#7293: a dataset-bound KPI tile can render no sub-caption at all — DatasetWidget's isMetric branch drops every measure after values[0], and the authored options.description sub-caption (translation key ending .subCaption, resolved by tWidgetSubCaption, overlaid server-side by translateDashboard) reaches only the inline object-metric path, which no spec-valid widget can take because `dataset` is required on DashboardWidgetSchema — a declared-and-unenforced key with four layers of live plumbing behind it"
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions