From 63b387f67998410d02c2c3493c3e1b2ee8f2f108 Mon Sep 17 00:00:00 2001 From: inman Date: Wed, 2 Sep 2026 11:27:34 +0800 Subject: [PATCH] fix: add headroom to dashboard bar scales --- .../20260902-dashboard-nice-scale-d8c31a.md | 55 +++++++++++++++++++ LianSyn-platform/app.js | 32 +++++++++-- LianSyn-platform/index.html | 4 +- .../test/account-authorization.test.ts | 11 ++++ control-plane/test/control-plane.test.ts | 4 +- 5 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260902-dashboard-nice-scale-d8c31a.md diff --git a/.project-docs/30-worklog/tasks/20260902-dashboard-nice-scale-d8c31a.md b/.project-docs/30-worklog/tasks/20260902-dashboard-nice-scale-d8c31a.md new file mode 100644 index 0000000..3f4fa51 --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-dashboard-nice-scale-d8c31a.md @@ -0,0 +1,55 @@ +# Task: Add dynamic nice scale to dashboard bars + +## Identity + +- Task ID: 20260902-dashboard-nice-scale-d8c31a +- Mode: Feature +- Branch: codex/20260902-dashboard-nice-scale-d8c31a-dashboard-nice-scale-d8c31a +- Worktree: /Users/inmanx/Documents/lwltAPI-dashboard-nice-scale-d8c31a +- Base commit: e530b7a3489c8f2be2f15f69de2149658aaa8ca3 +- Owner: codex +- Status: Ready for integration + +## Scope + +- Replace max-item normalization in both dashboard rankings with an independent dynamic “nice” scale that leaves visible headroom above the leading bar. +- Keep row-as-bar presentation, descending operation-count order, exact operation/success labels, fixed layout, scrolling, and drill-through behavior unchanged. +- Update static asset cache versions and focused dashboard regression coverage. + +## Intent And Constraints + +- Choose readable count intervals from `1 / 2 / 5 / 10` steps over roughly five intervals, round the observed maximum upward, and advance one further interval when the maximum already lands exactly on a scale boundary. +- Expected reference cases include `43 → 50`, `359 → 400`, and exact-boundary `100 → 120`; empty data remains safe. +- Compute task-type and employee scales independently. Operation count alone controls bar length; success count remains text and no completion rate or percentage is introduced. +- Preserve leadership-only access, business-safe language, employee/task-type drill-through, and all task/ERP authorization boundaries. +- Do not change the dashboard API, restart or deploy services, mutate runtime data, access ERP, or introduce an organization concept. + +## Outcome + +- Added a reusable dynamic ranking scale that divides the observed maximum into roughly five readable intervals using `1 / 2 / 5 / 10` steps. +- The scale rounds upward and advances one additional interval when the maximum already lands exactly on a boundary, so the leading bar always keeps visible headroom. +- Reference values now resolve as `43 → 50`, `359 → 400`, `100 → 120`, `1 → 2`, and empty data → `1`. +- Task-type and employee rankings compute their scales independently. Existing descending operation-count order, row-as-bar rendering, exact operation/success counts, fixed panel layout, scrolling, and drill-through remain unchanged. +- Updated the static asset cache version and added executable regression assertions for the dynamic-scale reference cases. +- No API, authorization, runtime data, ERP, deployment, or service-restart behavior changed. + +## Verification + +- `node --check LianSyn-platform/app.js`: passed. +- Focused account/dashboard regression: 8/8 passed, including dynamic-scale cases `0 → 1`, `1 → 2`, `43 → 50`, `100 → 120`, and `359 → 400`. +- `node --run check:repo`: 10/10 passed. +- `node --run check`: passed. +- `node --run test:control-plane`: 153/153 passed. +- `node --run test:legacy`: 256/256 passed. +- `node --run build`: passed. +- `git diff --check`: passed. +- Isolated browser preview at `http://127.0.0.1:8892/operations-dashboard?nice-scale=1` used a non-persistent mock dashboard response and the actual feature source/CSS. It rendered task-type widths `86% / 54% / 52% / 48% / 48%` for totals `43 / 27 / 26 / 24 / 24`, and employee width `89.75%` for total `359`. +- Visual inspection confirmed both leading bars retain right-side headroom. DOM and interaction verification confirmed descending order, exact operation/success text, and task-type click-through selecting `order_delete`; no completion-rate or percentage label was shown. + +## Follow-ups + +- Integrate the isolated commit only after the active main-branch password-flow integration releases its ownership gate, then reload the standard 8786 dashboard for a final runtime check. + +## Promotion Candidates + +- None. This is a focused visual-scale correction within the accepted count-based ranking behavior and does not change product, authorization, API, or data semantics. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index 670f263..86ea0a6 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -1385,8 +1385,24 @@ function renderOperationsDashboardSummary() { } } -function operationsDashboardRankBar(total, maximum) { - const safeMaximum = Math.max(1, Number(maximum || 0)); +function operationsDashboardRankScale(maximum) { + const numericMaximum = Number(maximum || 0); + const safeMaximum = Number.isFinite(numericMaximum) ? Math.max(0, numericMaximum) : 0; + if (safeMaximum <= 0) return 1; + + const roughStep = safeMaximum / 5; + const magnitude = 10 ** Math.floor(Math.log10(Math.max(1, roughStep))); + const normalizedStep = roughStep / magnitude; + const niceStep = Math.max( + 1, + (normalizedStep <= 1 ? 1 : normalizedStep <= 2 ? 2 : normalizedStep <= 5 ? 5 : 10) * magnitude + ); + const roundedMaximum = Math.ceil(safeMaximum / niceStep) * niceStep; + return roundedMaximum <= safeMaximum ? roundedMaximum + niceStep : roundedMaximum; +} + +function operationsDashboardRankBar(total, scaleMaximum) { + const safeMaximum = Math.max(1, Number(scaleMaximum || 0)); const safeTotal = Math.max(0, Number(total || 0)); const bar = el('span', 'operations-dashboard-rank-bar'); bar.setAttribute('aria-hidden', 'true'); @@ -1408,7 +1424,9 @@ function renderOperationsDashboardBusinesses() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有任务类型数据。')); return; } - const maximum = Math.max(...businesses.map((item) => Number(item.total || 0)), 1); + const scaleMaximum = operationsDashboardRankScale( + Math.max(...businesses.map((item) => Number(item.total || 0)), 0) + ); for (const [index, item] of businesses.entries()) { const button = el('button', 'operations-dashboard-business-row'); button.type = 'button'; @@ -1418,7 +1436,7 @@ function renderOperationsDashboardBusinesses() { el('span', 'operations-dashboard-rank-number', String(index + 1).padStart(2, '0')), el('strong', 'operations-dashboard-rank-name', operationsDashboardBusinessLabel(item.label)) ); - button.prepend(operationsDashboardRankBar(item.total, maximum)); + button.prepend(operationsDashboardRankBar(item.total, scaleMaximum)); const metrics = el('span', 'operations-dashboard-rank-metrics'); metrics.append( el('span', 'is-total', `操作 ${Number(item.total || 0)}`), @@ -1444,7 +1462,9 @@ function renderOperationsDashboardUsers() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有人员工作数据。')); return; } - const maximum = Math.max(...users.map((item) => Number(item.total || 0)), 1); + const scaleMaximum = operationsDashboardRankScale( + Math.max(...users.map((item) => Number(item.total || 0)), 0) + ); for (const [index, item] of users.entries()) { const button = el('button', 'operations-dashboard-user-row'); button.type = 'button'; @@ -1460,7 +1480,7 @@ function renderOperationsDashboardUsers() { el('strong', '', item.actor.username), el('small', '', accountRoleLabel(item.actor.role)) ); - const bar = operationsDashboardRankBar(item.total, maximum); + const bar = operationsDashboardRankBar(item.total, scaleMaximum); const metrics = el('div', 'operations-dashboard-user-metrics'); metrics.append( el('span', 'is-total', `操作 ${Number(item.total || 0)}`), diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index a698d3f..ba8144c 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -5,7 +5,7 @@ AI操作台 · LianSyn-platform - +
@@ -397,6 +397,6 @@
- + diff --git a/control-plane/test/account-authorization.test.ts b/control-plane/test/account-authorization.test.ts index ddd6b90..3644c91 100644 --- a/control-plane/test/account-authorization.test.ts +++ b/control-plane/test/account-authorization.test.ts @@ -248,10 +248,21 @@ test('operator UI exposes role-aware accounts, executive drill-through, original assert.match(app, /business_route_id/); assert.match(app, /data-operations-status|dataset\.operationsStatus/); assert.match(app, /data-operations-business|dataset\.operationsBusiness/); + assert.match(app, /function operationsDashboardRankScale/); assert.match(app, /operationsDashboardRankBar/); assert.match(app, /operations-dashboard-rank-bar/); assert.match(app, /sort\(\(left, right\) => Number\(right\.total/); assert.doesNotMatch(app, /完成率|operationsDashboardCompletionRate|operationsDashboardDays/); + const rankScaleSource = app.slice( + app.indexOf('function operationsDashboardRankScale('), + app.indexOf('function operationsDashboardRankBar(') + ); + const rankScale = Function(`${rankScaleSource}\nreturn operationsDashboardRankScale;`)() as (maximum: number) => number; + assert.equal(rankScale(0), 1); + assert.equal(rankScale(1), 2); + assert.equal(rankScale(43), 50); + assert.equal(rankScale(100), 120); + assert.equal(rankScale(359), 400); assert.match(app, /function operationsDashboardReadableResult/); assert.match(app, /function operationsDashboardReadableInput/); assert.match(app, /原始输入/); diff --git a/control-plane/test/control-plane.test.ts b/control-plane/test/control-plane.test.ts index 85213dc..f46fb65 100644 --- a/control-plane/test/control-plane.test.ts +++ b/control-plane/test/control-plane.test.ts @@ -1225,8 +1225,8 @@ test('operator page has a login gate and uses the durable task API', async () => const inpage = await readFile(new URL('../../chrome-extension/ltjt-order-assistant/inpage.js', import.meta.url), 'utf8'); assert.match(index, /id="loginPanel"/); assert.match(index, /id="workbench"[^>]*hidden/); - assert.match(index, /styles\.css\?v=20260902-dashboard-bar-chart-1/); - assert.match(index, /app\.js\?v=20260902-dashboard-bar-chart-1/); + assert.match(index, /styles\.css\?v=20260902-dashboard-nice-scale-1/); + assert.match(index, /app\.js\?v=20260902-dashboard-nice-scale-1/); assert.match(index, /id="statusDetailsPopover"/); assert.match(index, /id="statusDetailsRefresh"/); assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);