From 031dc6b99551d61bf0ceb9f3c0ad50ab94c9daba Mon Sep 17 00:00:00 2001 From: "n.siegfried" Date: Thu, 20 Aug 2026 18:14:15 -0700 Subject: [PATCH] D12 - the productivity factor, on the dashboard (was BL-023) Nick's decision: 'find a spot on the dashboard.' The spot: an eighth metric card beside Est./Actual hrs - actual/estimated to two decimals, green at or under 1.0, red over. Both hour fields are optional (CR-017), so with nothing to divide the card shows an em dash rather than vanishing: a metric that disappears reads as 'no such measure', not 'nothing logged yet'. Server sums (B4), the same m.est_hours/actual_hours its neighbours already render - zero new fetches, and the card stays inside the block the metrics-failure path skips, so an outage still shows the error panel and no cards. aggregates_check gains the pin (16 -> 17): the card must equal the quotient of the SERVER's sums, or the em dash when either sum is zero - derived, not hardcoded. Backlog entry corrected in passing where it credited /api/projects/{id}/summary with hour sums it never carried. Items: D12 (decisions-2026-08-20.md), CR-017 read, B4 discipline. Co-Authored-By: Claude Fable 5 --- docs/reference/file-map.md | 2 +- docs/waves/backlog.md | 5 +++-- html/wp-creation-app.js | 11 +++++++++++ tests/aggregates_check.py | 12 ++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/reference/file-map.md b/docs/reference/file-map.md index 0f98039..7c9af22 100644 --- a/docs/reference/file-map.md +++ b/docs/reference/file-map.md @@ -247,7 +247,7 @@ Wave 4 added three more, each written because its task's done-when could not be anything that already existed: ```bash -python tests/aggregates_check.py # B4 — do the counts come from the server? 16 checks +python tests/aggregates_check.py # B4 — do the counts come from the server? 17 checks python tests/url_state_check.py # S3 — does the app's state have an address? 23 checks python tests/autosave_check.py # S2/B5 — does unsaved work survive? 34 checks python tests/a11y_check.py # S10/S11/S12 — announce, legible, focus 22 checks diff --git a/docs/waves/backlog.md b/docs/waves/backlog.md index 863d685..34c6d44 100644 --- a/docs/waves/backlog.md +++ b/docs/waves/backlog.md @@ -518,8 +518,9 @@ deliberately deferred. hours exist on every package; nothing yet compares them. A productivity factor (actual ÷ estimated, rolled up by discipline / building / type the way CR-018 rolls cost) is the measurement Marlena's tracking exists to enable. - The rollup endpoints (`/api/wps/metrics`, `/api/projects/{id}/summary`) - already carry both sums, so this is a presentation task, not a data one. + `/api/wps/metrics` already carries both sums, so this is a presentation + task, not a data one. (Corrected at D12: the entry originally credited + `/api/projects/{id}/summary` too, which carries no hours at all.) - **Why not now:** new scope — needs its own item id per the working rules, and a product conversation about where it displays and who reads it. - **Suggested wave or follow-up:** next revision; needs Nick for placement. diff --git a/html/wp-creation-app.js b/html/wp-creation-app.js index eeeb083..61c3797 100644 --- a/html/wp-creation-app.js +++ b/html/wp-creation-app.js @@ -3777,6 +3777,17 @@ function renderDashboard(){ ${card('Overdue', overdue, overdue?'dm-red':'', 'overdue')} ${card('Est. hrs', Math.round(estH))} ${card('Actual hrs', Math.round(actH))} + ${(()=>{ + // D12 (was BL-023): the productivity factor - actual against estimated, + // the measure CR-017's tracking exists to enable. At or under 1.0 the + // work beat the estimate (green); over it (red). Both hour fields are + // optional, so with nothing to divide the card shows an em dash rather + // than vanishing - a metric that disappears reads as "no such measure", + // not "nothing logged yet". Server sums (B4), same as its neighbours. + const pf = (estH > 0 && actH > 0) ? actH / estH : null; + return card('Productivity (act/est)', pf === null ? '—' : pf.toFixed(2), + pf === null ? '' : (pf <= 1 ? 'dm-green' : 'dm-red')); + })()} `; // status + discipline breakdown chips (status chips also filter the board) diff --git a/tests/aggregates_check.py b/tests/aggregates_check.py index 70c6c40..2d3a45f 100644 --- a/tests/aggregates_check.py +++ b/tests/aggregates_check.py @@ -155,6 +155,18 @@ def main(): str(shown) == str(as_root["total"]), "shown=%r server=%r (poisoned cache said 2)" % (shown, as_root["total"])) chk("...and is therefore not the poisoned cache's 2", str(shown) != "2", shown) + # D12: the productivity factor card, computed from the SAME server + # sums as its neighbours. Both hour fields are optional (CR-017), + # so the expected value is derived, not hardcoded: a real quotient + # when both sums exist, an em dash when either is zero. + pf_shown = page.eval( + "(()=>{const e=[...document.querySelectorAll('.dash-metric')]" + ".find(x=>/Productivity/i.test(x.textContent));" + "return e?e.querySelector('.dm-val').textContent.trim():null})()") + est, act = as_root.get("est_hours") or 0, as_root.get("actual_hours") or 0 + pf_want = ("%.2f" % (act / est)) if est > 0 and act > 0 else "—" + chk("the D12 productivity card shows actual/estimated from the server sums", + pf_shown == pf_want, "shown=%r want=%r (est=%r act=%r)" % (pf_shown, pf_want, est, act)) print("\n3. a failed aggregate request is an error, not a zero") page.eval("""(() => {