fix: add headroom to dashboard bar scales
This commit is contained in:
@@ -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.
|
||||
@@ -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)}`),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#edf2f4">
|
||||
<title>AI操作台 · LianSyn-platform</title>
|
||||
<link rel="stylesheet" href="styles.css?v=20260902-dashboard-bar-chart-1">
|
||||
<link rel="stylesheet" href="styles.css?v=20260902-dashboard-nice-scale-1">
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
@@ -397,6 +397,6 @@
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<script src="app.js?v=20260902-dashboard-bar-chart-1"></script>
|
||||
<script src="app.js?v=20260902-dashboard-nice-scale-1"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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, /原始输入/);
|
||||
|
||||
@@ -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\(\)\}`/);
|
||||
|
||||
Reference in New Issue
Block a user