From de9f33dc45b137d1e48f3853904e45d1589b4b2d Mon Sep 17 00:00:00 2001 From: inman Date: Wed, 2 Sep 2026 11:08:04 +0800 Subject: [PATCH] feat: add bars to dashboard rankings --- .../20260902-dashboard-ranking-bars-c37a9e.md | 58 ++++++++++++++++++ LianSyn-platform/app.js | 29 ++++++++- LianSyn-platform/index.html | 4 +- LianSyn-platform/styles.css | 60 ++++++++++++++++++- .../test/account-authorization.test.ts | 2 + control-plane/test/control-plane.test.ts | 4 +- 6 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 .project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md diff --git a/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md b/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md new file mode 100644 index 0000000..f71db7c --- /dev/null +++ b/.project-docs/30-worklog/tasks/20260902-dashboard-ranking-bars-c37a9e.md @@ -0,0 +1,58 @@ +# Task: Add horizontal bars to dashboard rankings + +## Identity + +- Task ID: 20260902-dashboard-ranking-bars-c37a9e +- Mode: Feature +- Branch: main +- Worktree: /Users/inmanx/Documents/lwltAPI +- Base commit: f11aa6416b1e88c00fc6985b71130f1e3ad48b63 +- Owner: codex +- Status: Ready for integration + +## Scope + +- Add horizontal bar visualization to both the task-type ranking and employee-operation ranking on `/operations-dashboard`. +- Preserve the compact fixed-height ranking panels, operation/success count labels, ordering, scrolling, and click-through filters. +- Keep completion rate and all percentage labels out of the leadership presentation. +- Update browser cache versions and focused dashboard regression assertions. + +## Intent And Constraints + +- Use the existing vanilla HTML/CSS/JavaScript stack and dashboard response; do not introduce a chart library or change the operations API. +- Scale bars independently within each ranking so the leading operation count occupies the available width and the remaining rows are immediately comparable. +- Encode total operations as a muted outer bar and successful operations as a darker inner bar on the same scale; keep the exact counts visible as text. +- Preserve leadership-only read access, business-safe copy, task drill-through, employee filtering, and all task/ERP authorization boundaries. +- Do not restart, deploy, mutate runtime tasks, access ERP, or add an organization concept. + +## Outcome + +- Added one reusable ranking-bar renderer that clamps numeric inputs and normalizes total/success widths against the largest operation count in the current ranking. +- Added the same two-layer horizontal bar to every task-type and employee row: the light fill shows operation volume and the dark-green inner fill shows successful volume. +- Retained explicit “操作 N / 成功 N” labels and accessible button names; no percentage or completion-rate text was reintroduced. +- Expanded the compact row grid to include the bar without changing the fixed 304px panel height or internal scrolling. On narrow screens, the bar moves to a second row beneath the identity and counts. +- Preserved task-type and employee click-through behavior, filters, task detail, ranking order, and business-safe dashboard language. +- Updated static asset cache versions and regression assertions for the ranking-bar renderer. +- The running 8786 service serves the static change directly. No restart, deployment, database/task mutation, ERP access, or external send was performed. + +## Verification + +- `node --check LianSyn-platform/app.js`: passed with the bundled Node runtime. +- Focused account/dashboard regression: 8/8 passed. +- `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. +- Authenticated browser verification on `http://127.0.0.1:8786/operations-dashboard?bars=1` loaded 359 operations, 251 successes, 23 task types, and 1 participating employee. +- Visual verification confirmed that the task-type ranking visibly compares 43/27/26/24 operation counts, the success layer remains readable inside each total bar, and the employee row shows 359 total operations against 251 successes. +- Interaction verification confirmed that task-type click-through selected “删除订单”, narrowed the table to 43 operations, and kept completion-rate language absent. + +## Follow-ups + +- The current standard data contains one employee, so employee-to-employee bar comparison will become visible once additional accounts have task activity; the same normalization is already applied. + +## Promotion Candidates + +- None. This is a focused visual encoding of the already accepted count-based leadership rankings and does not change product, authorization, data, or API semantics. diff --git a/LianSyn-platform/app.js b/LianSyn-platform/app.js index 8ffbbd1..0de0115 100644 --- a/LianSyn-platform/app.js +++ b/LianSyn-platform/app.js @@ -1322,6 +1322,25 @@ function renderOperationsDashboardSummary() { } } +function operationsDashboardRankBar(total, completed, maximum) { + const safeMaximum = Math.max(1, Number(maximum || 0)); + const safeTotal = Math.max(0, Number(total || 0)); + const safeCompleted = Math.min(safeTotal, Math.max(0, Number(completed || 0))); + const bar = el('span', 'operations-dashboard-rank-bar'); + bar.setAttribute('aria-hidden', 'true'); + if (safeTotal > 0) { + const totalFill = el('i', 'is-total'); + totalFill.style.width = `${Math.min(100, (safeTotal / safeMaximum) * 100)}%`; + bar.append(totalFill); + } + if (safeCompleted > 0) { + const successFill = el('i', 'is-success'); + successFill.style.width = `${Math.min(100, (safeCompleted / safeMaximum) * 100)}%`; + bar.append(successFill); + } + return bar; +} + function renderOperationsDashboardBusinesses() { const container = $('#operationsDashboardBusinesses'); if (!container) return; @@ -1331,6 +1350,7 @@ function renderOperationsDashboardBusinesses() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有任务类型数据。')); return; } + const maximum = Math.max(...businesses.map((item) => Number(item.total || 0)), 1); for (const [index, item] of businesses.entries()) { const button = el('button', 'operations-dashboard-business-row'); button.type = 'button'; @@ -1340,9 +1360,10 @@ function renderOperationsDashboardBusinesses() { el('span', 'operations-dashboard-rank-number', String(index + 1).padStart(2, '0')), el('strong', 'operations-dashboard-rank-name', operationsDashboardBusinessLabel(item.label)) ); + button.append(operationsDashboardRankBar(item.total, item.completed, maximum)); const metrics = el('span', 'operations-dashboard-rank-metrics'); metrics.append( - el('span', '', `操作 ${Number(item.total || 0)}`), + el('span', 'is-total', `操作 ${Number(item.total || 0)}`), el('span', 'is-success', `成功 ${Number(item.completed || 0)}`) ); button.append(metrics); @@ -1364,6 +1385,7 @@ function renderOperationsDashboardUsers() { container.append(el('p', 'operations-dashboard-empty', '当前范围内还没有人员工作数据。')); return; } + const maximum = Math.max(...users.map((item) => Number(item.total || 0)), 1); for (const [index, item] of users.entries()) { const button = el('button', 'operations-dashboard-user-row'); button.type = 'button'; @@ -1379,12 +1401,13 @@ function renderOperationsDashboardUsers() { el('strong', '', item.actor.username), el('small', '', accountRoleLabel(item.actor.role)) ); + const bar = operationsDashboardRankBar(item.total, item.completed, maximum); const metrics = el('div', 'operations-dashboard-user-metrics'); metrics.append( - el('span', '', `操作 ${Number(item.total || 0)}`), + el('span', 'is-total', `操作 ${Number(item.total || 0)}`), el('span', 'is-success', `成功 ${Number(item.completed || 0)}`) ); - button.append(identity, metrics); + button.append(identity, bar, metrics); container.append(button); } } diff --git a/LianSyn-platform/index.html b/LianSyn-platform/index.html index 54e3883..3ab5e93 100644 --- a/LianSyn-platform/index.html +++ b/LianSyn-platform/index.html @@ -5,7 +5,7 @@ AI操作台 · LianSyn-platform - +
@@ -398,6 +398,6 @@
- + diff --git a/LianSyn-platform/styles.css b/LianSyn-platform/styles.css index 02b36e1..20261d5 100644 --- a/LianSyn-platform/styles.css +++ b/LianSyn-platform/styles.css @@ -4042,7 +4042,7 @@ textarea { min-width: 0; min-height: 42px; display: grid; - grid-template-columns: 34px minmax(0, 1fr) auto; + grid-template-columns: 34px minmax(110px, 0.9fr) minmax(96px, 1.15fr) auto; align-items: center; gap: 10px; padding: 7px 10px; @@ -4089,6 +4089,45 @@ textarea { white-space: nowrap; } +.operations-dashboard-rank-bar { + position: relative; + width: 100%; + height: 7px; + display: block; + overflow: hidden; + border-radius: 3px; + background: #e1e7e3; + box-shadow: inset 0 0 0 1px rgba(62, 91, 81, 0.08); +} + +.operations-dashboard-rank-bar i { + position: absolute; + left: 0; + display: block; + min-width: 2px; + border-radius: inherit; + transition: width 220ms ease; +} + +.operations-dashboard-rank-bar .is-total { + top: 0; + height: 100%; + background: #b6c6bf; +} + +.operations-dashboard-rank-bar .is-success { + top: 2px; + height: 3px; + background: var(--dashboard-accent); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.42); +} + +.operations-dashboard-rank-metrics .is-total, +.operations-dashboard-user-metrics .is-total { + color: #63736d; + font-weight: 650; +} + .operations-dashboard-rank-metrics .is-success, .operations-dashboard-user-metrics .is-success { color: var(--dashboard-accent); @@ -4096,7 +4135,7 @@ textarea { } .operations-dashboard-user-row { - grid-template-columns: 34px minmax(0, 1fr) auto; + grid-template-columns: 34px minmax(110px, 0.9fr) minmax(96px, 1.15fr) auto; } .operations-dashboard-user-identity { @@ -4363,6 +4402,23 @@ textarea { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .operations-dashboard-business-row, + .operations-dashboard-user-row { + min-height: 52px; + grid-template-columns: 28px minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 6px 8px; + } + + .operations-dashboard-rank-number { + grid-row: 1 / span 2; + } + + .operations-dashboard-rank-bar { + grid-column: 2 / -1; + grid-row: 2; + } + .operations-dashboard-filters { grid-template-columns: minmax(0, 1fr); } diff --git a/control-plane/test/account-authorization.test.ts b/control-plane/test/account-authorization.test.ts index f32e99b..9756910 100644 --- a/control-plane/test/account-authorization.test.ts +++ b/control-plane/test/account-authorization.test.ts @@ -244,6 +244,8 @@ 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, /operationsDashboardRankBar/); + assert.match(app, /operations-dashboard-rank-bar/); assert.doesNotMatch(app, /完成率|operationsDashboardCompletionRate|operationsDashboardDays/); assert.match(app, /function operationsDashboardReadableResult/); assert.match(app, /function operationsDashboardReadableInput/); diff --git a/control-plane/test/control-plane.test.ts b/control-plane/test/control-plane.test.ts index b755219..1e41d8c 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-compact-dashboard-1/); - assert.match(index, /app\.js\?v=20260902-compact-dashboard-1/); + assert.match(index, /styles\.css\?v=20260902-dashboard-ranking-bars-1/); + assert.match(index, /app\.js\?v=20260902-dashboard-ranking-bars-1/); assert.match(index, /id="statusDetailsPopover"/); assert.match(index, /id="statusDetailsRefresh"/); assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);