fix: make dashboard metrics display-only
This commit is contained in:
1 parent
d034f649c4
commit
b1fe53386c
7 files changed
+82
-45
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
# Task: Make dashboard metrics display-only
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
- Task ID: 20260902-dashboard-metrics-static-a91c
|
||||||
|
- Mode: Feature
|
||||||
|
- Branch: codex/20260902-dashboard-metrics-static-a91c-dashboard-metrics-static-a91c
|
||||||
|
- Worktree: /Users/inmanx/Documents/lwltAPI-dashboard-metrics-static-a91c
|
||||||
|
- Base commit: d034f649c4e7c5d0856f22053b92d2e5a63be5eb
|
||||||
|
- Owner: codex
|
||||||
|
- Status: Ready for integration
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- Make every leadership-dashboard metric card display-only and remove card-driven status filtering/selection state.
|
||||||
|
- Keep the task-result filter as the explicit status control, defaulting to `all` so the task list initially shows all matching data.
|
||||||
|
- Remove obsolete interactive/selected metric-card styles and add focused regression coverage.
|
||||||
|
- Bump the platform asset cache token so the JavaScript and CSS behavior is not masked by a previously cached dashboard build.
|
||||||
|
|
||||||
|
## Intent And Constraints
|
||||||
|
|
||||||
|
- Preserve the accepted read-only leadership-dashboard authorization, business-facing projection, mobile layout, and ranking drill-through behavior.
|
||||||
|
- Preserve the existing presentation rule that internal attention/waiting states are exposed as “进行中”; do not change task lifecycle storage or server classification.
|
||||||
|
- Do not change dashboard APIs, permissions, task data, ERP behavior, runtime services, deployment, or canonical project memory in this Feature task.
|
||||||
|
- Work in the isolated task worktree and reconcile the concurrent main-worktree kanban-filter task before integration; do not adopt or modify its uncommitted files.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
- `renderOperationsDashboardSummary()` now emits five uniform, non-interactive `article` cards with no status metadata, selected-state computation, `aria-pressed`, or card-driven filter mutation.
|
||||||
|
- Removed the summary-container click handler and the obsolete metric-card pointer, hover, and `.is-active` styles.
|
||||||
|
- Preserved the explicit task-result filter and its `all` initialization, so the dashboard task list opens on all matching results while status changes remain opt-in through the filter form.
|
||||||
|
- Updated focused platform and authorization regressions, and advanced the dashboard asset cache token to `20260902-dashboard-metrics-static-1`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Passed: bundled Node `--check LianSyn-platform/app.js`.
|
||||||
|
- Passed: focused dashboard regression, 4/4 tests.
|
||||||
|
- Passed: focused account-authorization regression, 8/8 tests.
|
||||||
|
- Passed: repository hygiene, 10/10 tests.
|
||||||
|
- Passed: TypeScript no-emit check.
|
||||||
|
- Passed: control-plane regression, 156/156 tests.
|
||||||
|
- Passed: legacy/platform/tool regression, 267/267 tests.
|
||||||
|
- Passed: TypeScript build.
|
||||||
|
- Passed: `git diff --check`.
|
||||||
|
- Note: the initial `node --run ...` wrappers did not enter the checks because this host has no `node` on the child-shell `PATH`; the same scripts were rerun successfully with the bundled Node executable and direct equivalent commands.
|
||||||
|
|
||||||
|
## Follow-ups
|
||||||
|
|
||||||
|
- Reconcile this focused change with the concurrently active main-worktree kanban-filter task before integrating; do not overwrite its unknown changes.
|
||||||
|
|
||||||
|
## Promotion Candidates
|
||||||
|
|
||||||
|
- Target canonical document: `.project-docs/40-domain/business-rules.md` during Integration Gate review.
|
||||||
|
- Proposal: leadership-dashboard metric cards are display-only summaries and do not carry selection/filter behavior; task results initialize to all data, while explicit status filtering remains in the filter section.
|
||||||
|
- Evidence: the user's explicit correction plus focused and full repository verification recorded above.
|
||||||
|
- Future impact: dashboard metric-card rendering and mobile presentation should not reintroduce button semantics, selected-state styles, or implicit status filtering unless a later product decision explicitly restores them.
|
||||||
|
- Semantic conflicts: none with AUTH-001 or the existing business-facing merge of internal attention states into “进行中”. The concurrent kanban-filter task may overlap code paths but its scope was still undefined when this task planned.
|
||||||
|
- Human confirmation: provided directly by the user in this task request.
|
||||||
@@ -9,12 +9,21 @@ const [app, index, styles, taskService] = await Promise.all([
|
|||||||
readFile(new URL('../control-plane/src/task-service.ts', import.meta.url), 'utf8')
|
readFile(new URL('../control-plane/src/task-service.ts', import.meta.url), 'utf8')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('dashboard summary cards share one visual treatment and expose an accessible selected state', () => {
|
test('dashboard summary cards are uniform display-only totals and leave filtering to the explicit controls', () => {
|
||||||
|
const summaryRenderer = app.slice(
|
||||||
|
app.indexOf('function renderOperationsDashboardSummary()'),
|
||||||
|
app.indexOf('function operationsDashboardRankScale')
|
||||||
|
);
|
||||||
assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/);
|
assert.match(app, /label: '操作次数'[^\n]+tone: 'total'/);
|
||||||
assert.doesNotMatch(app, /tone: 'primary'/);
|
assert.doesNotMatch(app, /tone: 'primary'/);
|
||||||
assert.match(app, /card\.setAttribute\('aria-pressed', String\(isSelected\)\)/);
|
assert.match(summaryRenderer, /const card = el\('article', `operations-dashboard-stat operations-dashboard-stat-\$\{item\.tone\}`\)/);
|
||||||
|
assert.doesNotMatch(summaryRenderer, /selectedStatus|isSelected|operationsStatus|aria-pressed/);
|
||||||
|
assert.doesNotMatch(app, /\$\('#operationsDashboardSummary'\)\?\.addEventListener\('click'/);
|
||||||
|
assert.match(index, /<option value="all">全部结果<\/option>/);
|
||||||
|
assert.match(app, /function resetOperationsDashboardFilters\(\)[\s\S]*?\$\('#operationsDashboardStatus'\)\.value = 'all';/);
|
||||||
assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/);
|
assert.match(styles, /\.operations-dashboard-summary\s*{[\s\S]*?grid-template-columns: repeat\(5, minmax\(0, 1fr\)\)/);
|
||||||
assert.match(styles, /\.operations-dashboard-stat\.is-active\s*{[\s\S]*?background: var\(--dashboard-accent-soft\);[\s\S]*?transform: none;/);
|
assert.doesNotMatch(styles, /\.operations-dashboard-stat\[data-operations-status\]/);
|
||||||
|
assert.doesNotMatch(styles, /\.operations-dashboard-stat\.is-active/);
|
||||||
assert.doesNotMatch(styles, /operations-dashboard-stat-primary/);
|
assert.doesNotMatch(styles, /operations-dashboard-stat-primary/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+6
-20
@@ -1363,22 +1363,14 @@ function renderOperationsDashboardSummary() {
|
|||||||
container.replaceChildren();
|
container.replaceChildren();
|
||||||
const summary = operationsDashboardData?.summary || {};
|
const summary = operationsDashboardData?.summary || {};
|
||||||
const cards = [
|
const cards = [
|
||||||
{ label: '操作次数', value: summary.total || 0, hint: '全部任务操作', status: 'all', tone: 'total' },
|
{ label: '操作次数', value: summary.total || 0, hint: '全部任务操作', tone: 'total' },
|
||||||
{ label: '成功次数', value: summary.completed || 0, hint: '已成功完成', status: 'completed', tone: 'success' },
|
{ label: '成功次数', value: summary.completed || 0, hint: '已成功完成', tone: 'success' },
|
||||||
{ label: '进行中', value: Number(summary.active || 0) + Number(summary.attention || 0), hint: '正在处理', status: 'active', tone: 'active' },
|
{ label: '进行中', value: Number(summary.active || 0) + Number(summary.attention || 0), hint: '正在处理', tone: 'active' },
|
||||||
{ label: '未成功', value: summary.failed || 0, hint: '需要继续处理', status: 'failed', tone: 'failed' },
|
{ label: '未成功', value: summary.failed || 0, hint: '需要继续处理', tone: 'failed' },
|
||||||
{ label: '操作员工', value: summary.user_count || 0, hint: '参与操作人数', status: '', tone: 'people' }
|
{ label: '操作员工', value: summary.user_count || 0, hint: '参与操作人数', tone: 'people' }
|
||||||
];
|
];
|
||||||
const selectedStatus = String($('#operationsDashboardStatus')?.value || 'all');
|
|
||||||
for (const item of cards) {
|
for (const item of cards) {
|
||||||
const isSelected = Boolean(item.status && selectedStatus === item.status);
|
const card = el('article', `operations-dashboard-stat operations-dashboard-stat-${item.tone}`);
|
||||||
const card = el(item.status ? 'button' : 'article', `operations-dashboard-stat operations-dashboard-stat-${item.tone}${isSelected ? ' is-active' : ''}`);
|
|
||||||
if (item.status) {
|
|
||||||
card.type = 'button';
|
|
||||||
card.dataset.operationsStatus = item.status;
|
|
||||||
card.setAttribute('aria-label', `按${item.label}筛选任务`);
|
|
||||||
card.setAttribute('aria-pressed', String(isSelected));
|
|
||||||
}
|
|
||||||
card.append(el('span', '', item.label));
|
card.append(el('span', '', item.label));
|
||||||
card.append(el('strong', '', String(item.value)));
|
card.append(el('strong', '', String(item.value)));
|
||||||
card.append(el('small', '', item.hint));
|
card.append(el('small', '', item.hint));
|
||||||
@@ -5579,12 +5571,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
$('#operationsDashboardActor').value = button.dataset.operationsActorId || '';
|
$('#operationsDashboardActor').value = button.dataset.operationsActorId || '';
|
||||||
refreshOperationsDashboardAfterDrilldown();
|
refreshOperationsDashboardAfterDrilldown();
|
||||||
});
|
});
|
||||||
$('#operationsDashboardSummary')?.addEventListener('click', (event) => {
|
|
||||||
const button = event.target.closest('[data-operations-status]');
|
|
||||||
if (!button) return;
|
|
||||||
$('#operationsDashboardStatus').value = button.dataset.operationsStatus || 'all';
|
|
||||||
refreshOperationsDashboardAfterDrilldown();
|
|
||||||
});
|
|
||||||
$('#operationsDashboardBusinesses')?.addEventListener('click', (event) => {
|
$('#operationsDashboardBusinesses')?.addEventListener('click', (event) => {
|
||||||
const button = event.target.closest('[data-operations-business]');
|
const button = event.target.closest('[data-operations-business]');
|
||||||
if (!button?.dataset.operationsBusiness) return;
|
if (!button?.dataset.operationsBusiness) return;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="theme-color" content="#edf2f4">
|
<meta name="theme-color" content="#edf2f4">
|
||||||
<title>AI操作台 · LianSyn-platform</title>
|
<title>AI操作台 · LianSyn-platform</title>
|
||||||
<link rel="stylesheet" href="styles.css?v=20260902-dashboard-mobile-share-1">
|
<link rel="stylesheet" href="styles.css?v=20260902-dashboard-metrics-static-1">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="app-shell">
|
<main class="app-shell">
|
||||||
@@ -405,6 +405,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
<script src="app.js?v=20260902-dashboard-mobile-share-1"></script>
|
<script src="app.js?v=20260902-dashboard-metrics-static-1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3244,23 +3244,6 @@ textarea {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operations-dashboard-stat[data-operations-status] {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operations-dashboard-stat[data-operations-status]:not(.is-active):hover {
|
|
||||||
border-color: #8daf9f;
|
|
||||||
box-shadow: 0 9px 24px rgba(32, 79, 67, 0.09);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.operations-dashboard-stat.is-active {
|
|
||||||
border-color: var(--dashboard-accent);
|
|
||||||
background: var(--dashboard-accent-soft);
|
|
||||||
box-shadow: inset 0 0 0 1px var(--dashboard-accent), 0 9px 24px rgba(32, 79, 67, 0.09);
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.operations-dashboard-stat > span {
|
.operations-dashboard-stat > span {
|
||||||
color: var(--dashboard-muted);
|
color: var(--dashboard-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ test('operator UI exposes role-aware accounts, executive drill-through, original
|
|||||||
assert.match(app, /\/api\/operations-dashboard\?/);
|
assert.match(app, /\/api\/operations-dashboard\?/);
|
||||||
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
|
assert.match(app, /\/api\/operations-dashboard\/tasks\/\$\{encodeURIComponent\(taskId\)\}/);
|
||||||
assert.match(app, /business_route_id/);
|
assert.match(app, /business_route_id/);
|
||||||
assert.match(app, /data-operations-status|dataset\.operationsStatus/);
|
assert.doesNotMatch(app, /data-operations-status|dataset\.operationsStatus/);
|
||||||
|
assert.match(app, /\$\('#operationsDashboardStatus'\)\.value = 'all'/);
|
||||||
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
|
assert.match(app, /data-operations-business|dataset\.operationsBusiness/);
|
||||||
assert.match(app, /function operationsDashboardRankScale/);
|
assert.match(app, /function operationsDashboardRankScale/);
|
||||||
assert.match(app, /operationsDashboardRankBar/);
|
assert.match(app, /operationsDashboardRankBar/);
|
||||||
|
|||||||
@@ -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');
|
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="loginPanel"/);
|
||||||
assert.match(index, /id="workbench"[^>]*hidden/);
|
assert.match(index, /id="workbench"[^>]*hidden/);
|
||||||
assert.match(index, /styles\.css\?v=20260902-dashboard-mobile-share-1/);
|
assert.match(index, /styles\.css\?v=20260902-dashboard-metrics-static-1/);
|
||||||
assert.match(index, /app\.js\?v=20260902-dashboard-mobile-share-1/);
|
assert.match(index, /app\.js\?v=20260902-dashboard-metrics-static-1/);
|
||||||
assert.match(index, /id="statusDetailsPopover"/);
|
assert.match(index, /id="statusDetailsPopover"/);
|
||||||
assert.match(index, /id="statusDetailsRefresh"/);
|
assert.match(index, /id="statusDetailsRefresh"/);
|
||||||
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
|
assert.match(app, /apiRequest\(`\/api\/tasks\?\$\{params\.toString\(\)\}`/);
|
||||||
|
|||||||
Reference in new issue
Block a user