mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
TASK-conversation-console-workflow: compact Overview
This commit is contained in:
1 parent
4fa9bd7d49
commit
fcd9b049e8
6 files changed
+466
-46
No files matched your search
@@ -2764,3 +2764,50 @@ test_integrity_note:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
|
||||
# Compact Console Overview density (2026-10-01)
|
||||
|
||||
```yaml
|
||||
test_integrity_note:
|
||||
change_type: new_tests
|
||||
scenarios:
|
||||
- id: SCN-console-overview-owner-decisions-and-urgent-hazards-lead-with-distinct-counts
|
||||
given: "An owner-addressed pending decision, unsafe Fleet findings, a manager-wake reservation, and advisory evidence"
|
||||
when: "The Console renders its Overview attention summary"
|
||||
then: "Owner decisions and urgent hazards lead, exact owner/action/advisory counts stay distinct, and only the guarded owner decision shortcuts appear"
|
||||
- id: SCN-console-overview-clean-unique-commits-stay-informational-and-unsafe-workstays-reviewable
|
||||
given: "One clean branch-ahead worktree and a separate dirty worktree"
|
||||
when: "The Overview projects worktree evidence"
|
||||
then: "Unique commits remain informational and inspectable while dirty work remains a named review finding"
|
||||
- id: SCN-console-overview-crowded-preview-preserves-every-fleet-and-advisory-record-at-desktop-and-mobile
|
||||
given: "A fixed 49-reference advisory cohort with one conflicting observation and 17 Fleet findings including unknown ownership and long evidence"
|
||||
when: "The Overview is measured at 1440x1000 and 390x844 and View all Fleet opens the full queue"
|
||||
then: "Shown/total/remaining counts are exact, urgent findings stay visible, every Fleet record and all advisory provenance/conflicts/unknowns remain reachable, and no horizontal overflow or clipped record occurs"
|
||||
- id: SCN-console-overview-refresh-and-navigation-preserve-owner-drafts-without-adding-unsafe-actions
|
||||
given: "An unsent task draft and a pending owner decision in the isolated demo"
|
||||
when: "The owner navigates away and refreshes the Console"
|
||||
then: "The draft remains intact, the owner shortcut boundary remains explicit, and no acknowledgement, close, recovery, or non-owner decision action is introduced"
|
||||
- id: SCN-console-overview-retains-ten-destinations-keyboard-routing-and-manager-wake-deep-links
|
||||
given: "The ten existing Console view destinations and an urgent manager-wake finding on a mobile viewport"
|
||||
when: "The owner uses keyboard navigation and opens the manager-wakes route directly and through browser history"
|
||||
then: "Each route selects its owning view, nested targets receive focus, Back/Forward remain coherent, and the urgent manager-wakes target is reachable"
|
||||
observable_outcomes:
|
||||
- "Owner and urgent counts are separate from advisory counts and clean unique commits do not inflate hazards"
|
||||
- "A bounded Fleet preview states exact shown, total, and remaining values and links to every original Fleet record"
|
||||
- "The full queue retains long evidence, conflicting advisory observations, exact task references, source provenance, and unknown ownership"
|
||||
- "The ten existing destinations, guarded owner action, drafts, mobile keyboard focus, and manager-wakes deep link remain observable"
|
||||
- "Fixed crowded data has no horizontal overflow or clipped urgent/evidence content at 1440x1000 and 390x844"
|
||||
determinism_controls:
|
||||
- "Hermetic loopback Console server with the isolated in-memory demo snapshot replaced by fixed task, owner, finding, evidence, and commit values"
|
||||
- "Fixed Playwright viewport dimensions and deterministic route/history actions; no live project mutation, external service, retry, or sleep"
|
||||
anti_cheat_rationale:
|
||||
prevents:
|
||||
- "Hard-coded preview/count labels that omit original findings"
|
||||
- "Hiding urgent or unknown-owner evidence behind advisory totals"
|
||||
- "Classifying clean unique commits as unsafe work"
|
||||
- "Losing conflicting source observations or inventing provenance"
|
||||
- "Adding owner actions that bypass the existing decision preview boundary"
|
||||
relaxation:
|
||||
did_relax_any_assertion: false
|
||||
if_true_explain_spec_basis: ""
|
||||
```
|
||||
@@ -192,5 +192,39 @@ function attentionGroups(snapshot) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
global.TorchAttentionProjection = Object.freeze({ groups: attentionGroups });
|
||||
function attentionSummary(groups) {
|
||||
const fleet = groups.fleet ?? [];
|
||||
const owner = groups.owner ?? [];
|
||||
const arbiter = groups.arbiter ?? [];
|
||||
const advisory = groups.advisory ?? [];
|
||||
const actionableFleet = fleet.filter((item) => item.tone !== 'info');
|
||||
const advisoryReferences = advisory.reduce((count, item) => count
|
||||
+ (Array.isArray(item.taskReferences) ? item.taskReferences.length : item.observations?.length ?? 0), 0);
|
||||
return {
|
||||
ownerDecisions: owner.length,
|
||||
urgentHazards: fleet.filter((item) => item.tone === 'urgent').length,
|
||||
actionable: owner.length + actionableFleet.length + arbiter.filter((item) => item.tone !== 'info').length,
|
||||
fleetTotal: fleet.length,
|
||||
fleetInformational: fleet.length - actionableFleet.length,
|
||||
advisoryCohorts: advisory.length,
|
||||
advisoryReferences,
|
||||
};
|
||||
}
|
||||
|
||||
function fleetPreview(groups, additionalLimit = 3) {
|
||||
const fleet = groups.fleet ?? [];
|
||||
const urgent = fleet.filter((item) => item.tone === 'urgent');
|
||||
const remaining = fleet.filter((item) => item.tone !== 'urgent')
|
||||
.sort((left, right) => {
|
||||
const rank = { decision: 0, review: 1, info: 2 };
|
||||
return (rank[left.tone] ?? 3) - (rank[right.tone] ?? 3);
|
||||
});
|
||||
return [...urgent, ...remaining.slice(0, Math.max(0, additionalLimit))];
|
||||
}
|
||||
|
||||
global.TorchAttentionProjection = Object.freeze({
|
||||
groups: attentionGroups,
|
||||
summary: attentionSummary,
|
||||
fleetPreview,
|
||||
});
|
||||
})(globalThis);
|
||||
+18
-1
@@ -67,8 +67,17 @@
|
||||
</div>
|
||||
</div>
|
||||
<aside class="attention-panel" aria-labelledby="attention-title">
|
||||
<div class="panel-heading"><h2 id="attention-title">Needs your attention</h2><span id="attention-count">0 items</span></div>
|
||||
<div class="panel-heading"><h2 id="attention-title">Decisions and risks</h2><span id="attention-count">0 actionable</span></div>
|
||||
<div class="attention-counts" aria-label="Actionable and advisory counts">
|
||||
<p><strong id="owner-decision-count">0 owner decisions</strong><span>Only decisions addressed to you</span></p>
|
||||
<p><strong id="urgent-hazard-count">0 urgent hazards</strong><span>Unsafe or lost-work evidence</span></p>
|
||||
<p><strong id="advisory-count">0 advisory cohorts</strong><span>Rechecks are not owner actions</span></p>
|
||||
</div>
|
||||
<div id="attention-list" class="attention-list"></div>
|
||||
<div class="attention-footer">
|
||||
<span id="fleet-preview-count" role="status" aria-live="polite">Showing 0 of 0 Fleet findings; 0 remaining</span>
|
||||
<a href="#fleet-attention-queue">View all Fleet</a>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
@@ -199,6 +208,14 @@
|
||||
</article>
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Ownership</h3><span>Implementation stays with specialists</span></div><div id="ownership-list" class="console-list"></div></article>
|
||||
</div>
|
||||
<section id="fleet-attention-queue" class="fleet-attention-queue" aria-labelledby="fleet-attention-title" tabindex="-1">
|
||||
<div class="section-heading"><div><p class="section-kicker">Every finding remains inspectable</p><h3 id="fleet-attention-title">Fleet review queue</h3></div><span>Full evidence and provenance</span></div>
|
||||
<label class="fleet-attention-filter" for="fleet-attention-filter">Filter Fleet findings
|
||||
<input id="fleet-attention-filter" type="search" autocomplete="off" placeholder="Search owner, task, evidence, or reference">
|
||||
</label>
|
||||
<p id="fleet-attention-result-count" class="fleet-attention-result-count" role="status" aria-live="polite">Loading findings…</p>
|
||||
<div id="fleet-attention-list" class="fleet-attention-list"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section id="organization" class="console-section organization-section" data-console-view="organization" aria-labelledby="organization-title">
|
||||
|
||||
+77
-36
@@ -954,9 +954,59 @@ function renderFlowWatch(snapshot) {
|
||||
function attentionGroups(snapshot) {
|
||||
return globalThis.TorchAttentionProjection.groups(snapshot);
|
||||
}
|
||||
function attentionItemCount(groups) {
|
||||
return ['owner', 'fleet', 'advisory', 'arbiter']
|
||||
.reduce((count, key) => count + (groups[key]?.length ?? 0), 0);
|
||||
function attentionSummary(groups) {
|
||||
return globalThis.TorchAttentionProjection.summary(groups);
|
||||
}
|
||||
function renderAttentionItem(item) {
|
||||
const severity = ({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown';
|
||||
return `<article class="attention-item tone-${escapeHtml(item.tone)}" data-attention-record>
|
||||
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
|
||||
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
|
||||
<small>Severity: ${escapeHtml(severity)} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}</small>
|
||||
${item.evidence.length > 120
|
||||
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
|
||||
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
|
||||
${item.observations ? `<details class="attention-cohort-details"><summary>Recheck observations (${item.observations.length})</summary><ul>${item.observations.map((observation) => `<li>
|
||||
<code>${escapeHtml(observation.taskId ?? 'Task reference not recorded')}</code> · ${escapeHtml(observation.code)}
|
||||
· Owner: ${escapeHtml(observation.owner ?? 'Owner not recorded')}
|
||||
· Observed source/revision: ${escapeHtml(observation.observedAt ?? 'Not recorded')}
|
||||
· Current observed commit: ${escapeHtml(observation.currentObservedCommit ?? 'Not recorded')}
|
||||
· Reproduction: ${escapeHtml(observation.reproductionStatus ?? 'UNKNOWN')}
|
||||
· Sources: ${escapeHtml(observation.sources.join(', ') || 'Not recorded')}
|
||||
</li>`).join('')}</ul></details>` : ''}
|
||||
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
|
||||
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
|
||||
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
|
||||
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
|
||||
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
|
||||
</div>` : ''}
|
||||
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
|
||||
</div></article>`;
|
||||
}
|
||||
function renderAttentionGroup(key, title, description, items, idPrefix) {
|
||||
const headingId = `${idPrefix}-${key}-title`;
|
||||
return `<section class="attention-group attention-group-${key}" aria-labelledby="${headingId}">
|
||||
<div class="attention-group-heading"><h3 id="${headingId}">${title}</h3><span>${items.length}</span></div>
|
||||
${items.length ? items.map(renderAttentionItem).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
|
||||
</section>`;
|
||||
}
|
||||
function applyFleetAttentionFilter(query = '') {
|
||||
const list = $('#fleet-attention-list');
|
||||
if (!list) return;
|
||||
const normalized = query.trim().toLocaleLowerCase();
|
||||
const records = [...list.querySelectorAll('[data-attention-record]')];
|
||||
let visible = 0;
|
||||
for (const record of records) {
|
||||
const matches = !normalized || record.textContent.toLocaleLowerCase().includes(normalized);
|
||||
record.hidden = !matches;
|
||||
if (matches) visible += 1;
|
||||
}
|
||||
for (const group of list.querySelectorAll('.attention-group')) {
|
||||
group.hidden = ![...group.querySelectorAll('[data-attention-record]')].some((record) => !record.hidden);
|
||||
}
|
||||
setText('#fleet-attention-result-count', normalized
|
||||
? `${visible} of ${records.length} findings match.`
|
||||
: `${records.length} Fleet and advisory findings available.`);
|
||||
}
|
||||
function renderAttention(groups) {
|
||||
const definitions = [
|
||||
@@ -965,37 +1015,27 @@ function renderAttention(groups) {
|
||||
['advisory', 'Advisory / recheck cohorts', 'Observed evidence grouped for rechecking; these findings do not assign an owner action.'],
|
||||
['arbiter', 'Arbiter handling', 'No observed item has structured evidence assigning its next step exclusively to an arbiter.'],
|
||||
];
|
||||
const total = attentionItemCount(groups);
|
||||
setText('#attention-count', `${total} ${total === 1 ? 'item' : 'items'}`);
|
||||
setHtml('#attention-list', definitions.map(([key, title, description]) => {
|
||||
const items = groups[key] ?? [];
|
||||
return `<section class="attention-group attention-group-${key}" aria-labelledby="attention-${key}-title">
|
||||
<div class="attention-group-heading"><h3 id="attention-${key}-title">${title}</h3><span>${items.length}</span></div>
|
||||
${items.length ? items.map((item) => `<article class="attention-item tone-${escapeHtml(item.tone)}">
|
||||
<span class="attention-mark" aria-hidden="true"></span><div class="attention-copy">
|
||||
<strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p>
|
||||
<small>Severity: ${escapeHtml(({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown')} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''}</small>
|
||||
${item.evidence.length > 120
|
||||
? `<details class="attention-evidence"><summary>Evidence and references</summary><p>${escapeHtml(item.evidence)}</p></details>`
|
||||
: `<small class="attention-evidence-short">Evidence: ${escapeHtml(item.evidence)}</small>`}
|
||||
${item.observations ? `<details class="attention-cohort-details"><summary>Recheck observations (${item.observations.length})</summary><ul>${item.observations.map((observation) => `<li>
|
||||
<code>${escapeHtml(observation.taskId ?? 'Task reference not recorded')}</code> · ${escapeHtml(observation.code)}
|
||||
· Owner: ${escapeHtml(observation.owner ?? 'Owner not recorded')}
|
||||
· Observed source/revision: ${escapeHtml(observation.observedAt ?? 'Not recorded')}
|
||||
· Current observed commit: ${escapeHtml(observation.currentObservedCommit ?? 'Not recorded')}
|
||||
· Reproduction: ${escapeHtml(observation.reproductionStatus ?? 'UNKNOWN')}
|
||||
· Sources: ${escapeHtml(observation.sources.join(', ') || 'Not recorded')}
|
||||
</li>`).join('')}</ul></details>` : ''}
|
||||
<a href="${escapeHtml(item.href)}">${escapeHtml(item.action)}</a>
|
||||
${item.decisionApprovalId ? `<div class="attention-quick-actions" role="group" aria-label="Owner decision shortcuts for ${escapeHtml(item.title)}">
|
||||
<button type="button" data-attention-decision="approved" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Approve</button>
|
||||
<button type="button" class="quiet-action" data-attention-decision="rejected" data-attention-approval="${escapeHtml(item.decisionApprovalId)}">Reject</button>
|
||||
<p class="attention-action-status" role="status" aria-live="polite" hidden></p>
|
||||
</div>` : ''}
|
||||
${item.requestOwner ? `<a href="#owner-request-form" data-attention-recipient="${escapeHtml(item.requestOwner)}">Request review from ${escapeHtml(item.owner)}</a>` : ''}
|
||||
</div></article>`).join('') : `<p class="attention-empty">${escapeHtml(description)}</p>`}
|
||||
</section>`;
|
||||
}).join(''));
|
||||
const summary = attentionSummary(groups);
|
||||
const fleetPreview = globalThis.TorchAttentionProjection.fleetPreview(groups);
|
||||
const remaining = Math.max(0, summary.fleetTotal - fleetPreview.length);
|
||||
const priorQuery = $('#fleet-attention-filter')?.value ?? '';
|
||||
setText('#attention-count', `${summary.actionable} actionable`);
|
||||
setText('#owner-decision-count', `${summary.ownerDecisions} owner decision${summary.ownerDecisions === 1 ? '' : 's'}`);
|
||||
setText('#urgent-hazard-count', `${summary.urgentHazards} urgent hazard${summary.urgentHazards === 1 ? '' : 's'}`);
|
||||
setText('#advisory-count', summary.advisoryReferences
|
||||
? `${summary.advisoryCohorts} cohort${summary.advisoryCohorts === 1 ? '' : 's'} · ${summary.advisoryReferences} task references`
|
||||
: `${summary.advisoryCohorts} advisory cohort${summary.advisoryCohorts === 1 ? '' : 's'}`);
|
||||
setText('#fleet-preview-count', `Showing ${fleetPreview.length} of ${summary.fleetTotal} Fleet findings; ${remaining} remaining`);
|
||||
setHtml('#attention-list', definitions.map(([key, title, description]) => renderAttentionGroup(
|
||||
key, title, description, key === 'fleet' ? fleetPreview : groups[key] ?? [], 'overview-attention',
|
||||
)).join(''));
|
||||
setHtml('#fleet-attention-list', [
|
||||
renderAttentionGroup('fleet', 'Fleet handling', 'No Fleet-owned findings are recorded.', groups.fleet ?? [], 'fleet-queue'),
|
||||
renderAttentionGroup('advisory', 'Advisory / recheck cohorts', 'No advisory cohorts are recorded.', groups.advisory ?? [], 'fleet-queue'),
|
||||
].join(''));
|
||||
const filter = $('#fleet-attention-filter');
|
||||
if (filter) filter.value = priorQuery;
|
||||
applyFleetAttentionFilter(priorQuery);
|
||||
}
|
||||
|
||||
function render(snapshot) {
|
||||
@@ -1025,9 +1065,9 @@ function render(snapshot) {
|
||||
const parts = [`${agents.length} persistent ${agents.length === 1 ? 'identity' : 'identities'}`,
|
||||
`${working} ${working === 1 ? 'task' : 'tasks'} in progress`];
|
||||
if (blocked) parts.push(`${blocked} blocked`);
|
||||
const attentionCount = attentionItemCount(attention);
|
||||
const attentionCounts = attentionSummary(attention);
|
||||
setText('#project-summary', snapshot.mode === 'installed'
|
||||
? `${parts.join(' · ')}. ${attentionCount ? `${attentionCount} item${attentionCount === 1 ? '' : 's'} need a named next step.` : 'No owner decision is currently waiting in the observed queues.'}`
|
||||
? `${parts.join(' · ')}. ${attentionCounts.ownerDecisions} owner decision${attentionCounts.ownerDecisions === 1 ? '' : 's'} · ${attentionCounts.urgentHazards} urgent hazard${attentionCounts.urgentHazards === 1 ? '' : 's'} · ${attentionCounts.actionable} actionable across the observed queues.`
|
||||
: 'This repository has not been installed as a TORCH Fleet. The view is showing repository intake only.');
|
||||
renderTaskCreateOptions(agents, tasks, snapshot.mode === 'installed');
|
||||
renderOwnerRequestOptions(agents, tasks, snapshot.mode === 'installed');
|
||||
@@ -2136,6 +2176,7 @@ $('#attention-list')?.addEventListener('click', (event) => {
|
||||
});
|
||||
|
||||
$('#refresh-console')?.addEventListener('click', () => refresh());
|
||||
$('#fleet-attention-filter')?.addEventListener('input', (event) => applyFleetAttentionFilter(event.currentTarget.value));
|
||||
$('#toggle-live-refresh')?.addEventListener('click', () => {
|
||||
liveUpdatesPaused = !liveUpdatesPaused;
|
||||
$('#toggle-live-refresh').textContent = liveUpdatesPaused ? 'Resume updates' : 'Pause updates';
|
||||
|
||||
+28
-8
@@ -166,30 +166,30 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.task-reference { margin-top: .55rem; color: var(--muted); font-size: .75rem; overflow-wrap: anywhere; }
|
||||
.task-reference code { display: inline-block; margin-top: .3rem; color: var(--ink); }
|
||||
.console-error { margin: 1.2rem 0 0; background: var(--warning-surface); color: var(--warning-ink); border-left: 4px solid var(--signal); padding: 1rem; }
|
||||
.pulse-zone { display: grid; grid-template-columns: minmax(0, 1.45fr) minmax(270px, .8fr); border-bottom: 1px solid var(--ink); }
|
||||
.pulse-main { min-width: 0; padding: 2rem 2rem 1.6rem 0; }
|
||||
.pulse-zone { display: grid; grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); gap: clamp(1.25rem, 2.8vw, 2.6rem); border-bottom: 1px solid var(--ink); }
|
||||
.pulse-main { min-width: 0; align-self: start; padding: 1.55rem 0 1.25rem; }
|
||||
.pulse-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 1.25rem; }
|
||||
.section-kicker { margin: 0 0 .35rem; color: var(--muted); font-size: .78rem; font-weight: 700; }
|
||||
.pulse-heading h1, .section-heading h2 { max-width: none; font-size: clamp(1.75rem, 2.8vw, 2.5rem); line-height: 1.05; letter-spacing: -.035em; }
|
||||
.health-flag { display: inline-flex; align-items: center; border: 1px solid var(--field); color: var(--field); padding: .25rem .55rem; font-size: .78rem; font-weight: 700; }
|
||||
.pulse-summary { max-width: 60ch; margin: 1rem 0 1.65rem; color: var(--muted); font-size: 1.05rem; }
|
||||
.pulse-summary { max-width: 50ch; margin: .75rem 0 1.15rem; color: var(--muted); font-size: .9rem; line-height: 1.5; }
|
||||
.pulse-readouts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); }
|
||||
.pulse-readouts div { min-width: 0; display: grid; align-content: start; gap: .3rem; padding: .8rem .8rem .2rem 0; }
|
||||
.pulse-readouts div + div { padding-left: .8rem; border-left: 1px solid var(--line); }
|
||||
.pulse-readouts span { color: var(--muted); font-size: .74rem; }
|
||||
.pulse-readouts strong { overflow-wrap: anywhere; font-family: var(--display); font-size: .95rem; }
|
||||
.attention-panel { padding: 1.45rem 0 1rem 1.35rem; border-left: 1px solid var(--line); }
|
||||
.attention-panel { min-width: 0; align-self: start; padding: 1.15rem 0 1rem 1.2rem; border-left: 1px solid var(--line); }
|
||||
.panel-heading, .section-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; }
|
||||
.panel-heading { padding-bottom: .65rem; border-bottom: 1px solid var(--line); }
|
||||
.panel-heading h2, .panel-heading h3 { margin: 0; font: 650 1rem/1.2 var(--display); }
|
||||
.panel-heading span, .section-heading > span { color: var(--muted); font-size: .76rem; text-align: right; }
|
||||
.attention-list { display: grid; gap: 1rem; }
|
||||
.attention-group { min-width: 0; padding-top: .6rem; border-top: 1px solid var(--line); }
|
||||
.attention-list { display: grid; gap: .6rem; }
|
||||
.attention-group { min-width: 0; padding-top: .4rem; border-top: 1px solid var(--line); }
|
||||
.attention-group-heading { display: flex; align-items: baseline; justify-content: space-between; gap: .8rem; }
|
||||
.attention-group-heading h3 { margin: 0; font: 650 .9rem/1.25 var(--display); }
|
||||
.attention-group-heading span, .attention-empty { color: var(--muted); font-size: .72rem; }
|
||||
.attention-empty { margin: .35rem 0 0; line-height: 1.4; }
|
||||
.attention-item { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: .7rem; padding: .75rem 0; border-bottom: 1px solid var(--line); }
|
||||
.attention-item { display: grid; grid-template-columns: 8px minmax(0, 1fr); gap: .55rem; padding: .5rem 0; border-bottom: 1px solid var(--line); }
|
||||
.attention-mark { width: 7px; height: 7px; margin-top: .45rem; background: var(--field); }
|
||||
.tone-urgent .attention-mark { background: var(--signal); }
|
||||
.tone-review .attention-mark { background: var(--route); }
|
||||
@@ -197,7 +197,7 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.tone-info .attention-mark { background: var(--field); }
|
||||
.attention-copy { min-width: 0; }
|
||||
.attention-item strong { display: block; font-size: .86rem; overflow-wrap: anywhere; }
|
||||
.attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; }
|
||||
.attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; overflow-wrap: anywhere; }
|
||||
.attention-item small { display: block; color: var(--muted); font-size: .69rem; line-height: 1.35; overflow-wrap: anywhere; }
|
||||
.attention-item a { display: inline-block; margin: .35rem .8rem 0 0; color: var(--route); font-size: .76rem; font-weight: 650; }
|
||||
.attention-evidence { margin-top: .28rem; color: var(--muted); font-size: .69rem; }
|
||||
@@ -207,6 +207,25 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.attention-quick-actions button { min-height: 2rem; padding: .3rem .55rem; }
|
||||
.attention-action-status { flex-basis: 100%; margin: .1rem 0 0; color: var(--muted); font-size: .7rem; line-height: 1.4; }
|
||||
.attention-action-status[hidden] { display: none; }
|
||||
.attention-counts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .45rem; padding: .6rem 0 .25rem; }
|
||||
.attention-counts p { min-width: 0; margin: 0; padding-left: .55rem; border-left: 2px solid var(--line); }
|
||||
.attention-counts p:first-child { border-left-color: #f3bd55; }
|
||||
.attention-counts p:nth-child(2) { border-left-color: var(--signal); }
|
||||
.attention-counts p:last-child { border-left-color: var(--field); }
|
||||
.attention-counts strong, .attention-counts span { display: block; overflow-wrap: anywhere; }
|
||||
.attention-counts strong { font-size: .74rem; line-height: 1.25; }
|
||||
.attention-counts span { margin-top: .12rem; color: var(--muted); font-size: .65rem; line-height: 1.3; }
|
||||
.attention-footer { display: flex; justify-content: space-between; align-items: baseline; gap: .75rem; padding-top: .6rem; color: var(--muted); font-size: .7rem; }
|
||||
.attention-footer a { color: var(--route); font-weight: 700; white-space: nowrap; }
|
||||
.fleet-attention-queue { min-width: 0; margin-top: 1.2rem; padding-top: 1.2rem; border-top: 1px solid var(--ink); scroll-margin-top: 1rem; }
|
||||
.fleet-attention-filter { display: grid; gap: .3rem; max-width: 34rem; color: var(--muted); font-size: .75rem; font-weight: 650; }
|
||||
.fleet-attention-filter input { width: 100%; min-width: 0; min-height: 2.5rem; padding: .45rem .6rem; border: 1px solid var(--line); border-radius: 0; background: var(--deep); color: var(--ink); font: 500 .82rem/1.3 var(--body); }
|
||||
.fleet-attention-result-count { color: var(--muted); font-size: .75rem; }
|
||||
.fleet-attention-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.1rem; }
|
||||
.fleet-attention-list > .attention-group { padding: .7rem; border: 1px solid var(--line); background: var(--paper); }
|
||||
.fleet-attention-list .attention-evidence[open] p { max-width: 80ch; }
|
||||
.attention-cohort-details li, .attention-cohort-details code { overflow-wrap: anywhere; }
|
||||
.fleet-attention-queue [hidden] { display: none !important; }
|
||||
.board-section, .console-section { padding-block: 1.65rem; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; }
|
||||
.section-heading { margin-bottom: 1rem; }
|
||||
.section-heading h2 { margin: 0; }
|
||||
@@ -629,6 +648,7 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.console-workspace { padding-inline: .8rem; }
|
||||
.console-rail { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.pulse-zone, .fleet-columns, .evidence-columns, .delivery-columns, .flow-watch-grid { grid-template-columns: 1fr; }
|
||||
.fleet-attention-list { grid-template-columns: 1fr; }
|
||||
.owner-request-fields, .runtime-profile-fields { grid-template-columns: 1fr; }
|
||||
.schedule-launcher-actions { align-items: stretch; flex-direction: column; }
|
||||
.lifecycle-heading { flex-direction: column; gap: .25rem; }
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import test, { after, before } from 'node:test';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { createConsoleServer } from '../../src/console/server.mjs';
|
||||
import '../../site/attention-projection.js';
|
||||
|
||||
let server;
|
||||
let browser;
|
||||
let origin;
|
||||
|
||||
before(async () => {
|
||||
server = createConsoleServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
origin = `http://127.0.0.1:${server.address().port}`;
|
||||
browser = await chromium.launch({ headless: true });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser?.close();
|
||||
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
|
||||
async function openOverview(viewport = { width: 1440, height: 1000 }) {
|
||||
const page = await browser.newPage({ viewport });
|
||||
await page.goto(`${origin}/console?demo=1#overview`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
const snapshot = await page.evaluate(() => structuredClone(globalThis.TorchConsoleDemo.snapshot()));
|
||||
return { page, snapshot };
|
||||
}
|
||||
|
||||
async function renderSnapshot(page, snapshot) {
|
||||
await page.evaluate((fixed) => { globalThis.TorchConsoleDemo.snapshot = () => structuredClone(fixed); }, snapshot);
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
}
|
||||
|
||||
function unsafeSnapshot(snapshot) {
|
||||
snapshot.mode = 'installed';
|
||||
snapshot.approvalRequests = { available: true, pendingCount: 1, items: [{
|
||||
id: 'owner-review-1', revision: 4, status: 'pending', approver: 'owner', requester: 'qa',
|
||||
title: 'Review the exact owner decision', task: 'TASK-CONSOLE-OVERVIEW', evidence: 'same-origin preview and current revision',
|
||||
}] };
|
||||
snapshot.fleetChanges = [];
|
||||
snapshot.hierarchyProposals = [];
|
||||
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
|
||||
{ severity: 'error', code: 'BACKLOG_HEALTH_INVALID', message: 'Current schema validation failed; inspect the evidence before action.' },
|
||||
{ severity: 'error', code: 'WORKTREE_PROBLEM', area: 'qa', problem: 'git-operation:merge', path: '/work/qa' },
|
||||
{ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE', taskId: 'TASK-ADVISORY-1', owner: 'provider-runtime', observedAt: 'a'.repeat(40) },
|
||||
] };
|
||||
snapshot.backlogHealth = { findings: [] };
|
||||
snapshot.worktrees = [{ area: 'qa', branch: 'qa/review', path: '/work/qa', ahead: 2, behind: 0 }];
|
||||
snapshot.agents = [...(snapshot.agents ?? []), { areaId: 'qa', title: 'QA' }];
|
||||
snapshot.managerWakes = { blockingCount: 1, reservations: [{ managerId: 'qa', scheduleId: 'wake-1', reservedAt: '2026-10-01T00:00:00Z', outcome: 'unknown', blocksManagerWake: true }] };
|
||||
snapshot.deliveryOperations = [{ operation: 'apply', state: 'unknown', actor: 'qa', commit: 'c'.repeat(40) }];
|
||||
snapshot.backlog = [...(snapshot.backlog ?? []), { id: 'TASK-ADVISORY-1', owner: 'provider-runtime', observedAt: 'a'.repeat(40), state: 'in_progress' }];
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
test('SCN-console-overview-owner-decisions-and-urgent-hazards-lead-with-distinct-counts', async (t) => {
|
||||
const { page, snapshot } = await openOverview();
|
||||
t.after(() => page.close());
|
||||
await renderSnapshot(page, unsafeSnapshot(snapshot));
|
||||
const projected = globalThis.TorchAttentionProjection.summary(globalThis.TorchAttentionProjection.groups(unsafeSnapshot(snapshot)));
|
||||
|
||||
const groups = page.locator('#attention-list > .attention-group');
|
||||
assert.match(await groups.nth(0).locator('h3').innerText(), /Waiting on you/);
|
||||
assert.match(await groups.nth(1).locator('h3').innerText(), /Fleet handling/);
|
||||
assert.equal(await page.locator('#owner-decision-count').innerText(), `${projected.ownerDecisions} owner decision${projected.ownerDecisions === 1 ? '' : 's'}`);
|
||||
assert.equal(await page.locator('#urgent-hazard-count').innerText(), `${projected.urgentHazards} urgent hazard${projected.urgentHazards === 1 ? '' : 's'}`);
|
||||
assert.equal(await page.locator('#attention-count').innerText(), `${projected.actionable} actionable`);
|
||||
assert.match(await page.locator('#advisory-count').innerText(), /1 cohort/);
|
||||
assert.equal(await page.locator('#attention-list [data-attention-decision]').count(), 2);
|
||||
assert.equal(await page.locator('#attention-list button').filter({ hasText: /Acknowledge|Close|Recover/ }).count(), 0);
|
||||
assert.ok(await page.locator('#attention-list .tone-urgent').count() >= 3);
|
||||
assert.equal(await page.locator('#attention-list a[href="#manager-wakes"]').count(), 1);
|
||||
});
|
||||
|
||||
test('SCN-console-overview-clean-unique-commits-stay-informational-and-unsafe-workstays-reviewable', async (t) => {
|
||||
const { page, snapshot } = await openOverview();
|
||||
t.after(() => page.close());
|
||||
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
|
||||
{ severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'project-kernel', problem: 'unique-commits:3', path: '/work/kernel' },
|
||||
{ severity: 'warning', code: 'WORKTREE_PROBLEM', area: 'provider-runtime', problem: 'worktree-dirty', path: '/work/provider' },
|
||||
] };
|
||||
snapshot.worktrees = [
|
||||
{ area: 'project-kernel', branch: 'kernel/progress', path: '/work/kernel', ahead: 3, behind: 0 },
|
||||
{ area: 'provider-runtime', branch: 'provider/changes', path: '/work/provider', ahead: 0, behind: 0 },
|
||||
];
|
||||
snapshot.agents = [...(snapshot.agents ?? []), { areaId: 'project-kernel', title: 'Project Kernel' }, { areaId: 'provider-runtime', title: 'Provider Runtime' }];
|
||||
await renderSnapshot(page, snapshot);
|
||||
|
||||
const records = page.locator('#fleet-attention-list [data-attention-record]');
|
||||
const clean = records.filter({ hasText: 'has work ahead of main' });
|
||||
const dirty = records.filter({ hasText: 'worktree needs review' });
|
||||
assert.equal(await clean.count(), 1);
|
||||
assert.ok(await clean.evaluate((item) => item.classList.contains('tone-info')));
|
||||
assert.match(await clean.innerText(), /3 unique commit\(s\) are ahead/);
|
||||
assert.equal(await dirty.count(), 1);
|
||||
assert.ok(await dirty.evaluate((item) => item.classList.contains('tone-review')));
|
||||
assert.match(await dirty.innerText(), /Uncommitted changes are present/);
|
||||
});
|
||||
|
||||
function crowdedSnapshot(snapshot) {
|
||||
const references = Array.from({ length: 49 }, (_, index) => `TASK-DENSITY-${String(index + 1).padStart(2, '0')}`);
|
||||
const tasks = references.map((id, index) => ({
|
||||
id, owner: index === 47 ? null : `domain-${String(index + 1).padStart(2, '0')}`,
|
||||
observedAt: index === 47 ? null : String(index + 1).padStart(40, '0'), state: 'in_progress',
|
||||
}));
|
||||
const doctor = tasks.map((task, index) => ({ severity: 'warning', code: 'BACKLOG_OBSERVED_COMMIT_STALE',
|
||||
taskId: task.id, owner: task.owner, observedAt: task.observedAt,
|
||||
...(index === 47 ? { currentObservedCommit: null } : {}),
|
||||
}));
|
||||
const backlogHealth = structuredClone(doctor);
|
||||
backlogHealth[48].owner = 'conflicting-owner';
|
||||
snapshot.backlog = tasks;
|
||||
snapshot.fleetChanges = [];
|
||||
snapshot.hierarchyProposals = [];
|
||||
snapshot.approvalRequests = { available: true, pendingCount: 0, items: [] };
|
||||
snapshot.messages = { ...(snapshot.messages ?? {}), unacknowledged: 0, recent: [] };
|
||||
snapshot.integration = [];
|
||||
snapshot.worktrees = [];
|
||||
snapshot.backlogActivity = { tasks: [] };
|
||||
snapshot.managerWakes = { blockingCount: 0, reservations: [] };
|
||||
snapshot.deliveryOperations = [];
|
||||
snapshot.repository = { ...(snapshot.repository ?? {}), head: 'd'.repeat(40) };
|
||||
snapshot.doctor = { ...(snapshot.doctor ?? {}), findings: [
|
||||
...doctor,
|
||||
...Array.from({ length: 3 }, (_, index) => ({ severity: 'error', code: `FIXED_UNSAFE_${index + 1}`,
|
||||
area: index === 0 ? null : `domain-${index + 1}`, message: `Urgent evidence ${index + 1}: ${'e'.repeat(180)}` })),
|
||||
...Array.from({ length: 14 }, (_, index) => ({ severity: 'warning', code: `FIXED_REVIEW_${index + 1}`,
|
||||
area: index === 0 ? null : `domain-${index + 1}`, message: `Review evidence ${index + 1}: ${'f'.repeat(140)}` })),
|
||||
] };
|
||||
snapshot.backlogHealth = { findings: backlogHealth };
|
||||
return { snapshot, references };
|
||||
}
|
||||
|
||||
test('SCN-console-overview-crowded-preview-preserves-every-fleet-and-advisory-record-at-desktop-and-mobile', async (t) => {
|
||||
const desktop = await openOverview();
|
||||
t.after(() => desktop.page.close());
|
||||
const { snapshot, references } = crowdedSnapshot(desktop.snapshot);
|
||||
await renderSnapshot(desktop.page, snapshot);
|
||||
const projected = globalThis.TorchAttentionProjection.summary(globalThis.TorchAttentionProjection.groups(snapshot));
|
||||
assert.equal(await desktop.page.locator('#advisory-count').innerText(), '1 cohort · 49 task references');
|
||||
const previewCount = globalThis.TorchAttentionProjection.fleetPreview(globalThis.TorchAttentionProjection.groups(snapshot)).length;
|
||||
assert.equal(await desktop.page.locator('#fleet-preview-count').innerText(), `Showing ${previewCount} of ${projected.fleetTotal} Fleet findings; ${projected.fleetTotal - previewCount} remaining`);
|
||||
assert.equal(projected.fleetTotal, 17, 'the crowded data has exactly 17 original Fleet findings');
|
||||
assert.equal(await desktop.page.locator('#attention-list .tone-urgent').count(), 3);
|
||||
const desktopWidth = await desktop.page.evaluate(() => ({ viewport: document.documentElement.clientWidth,
|
||||
document: document.documentElement.scrollWidth,
|
||||
layout: ['body', '.console-layout', '.console-workspace', '#overview', '.attention-panel', '#attention-list'].map((selector) => {
|
||||
const item = document.querySelector(selector); const rect = item.getBoundingClientRect();
|
||||
return `${selector}: ${Math.round(rect.left)}..${Math.round(rect.right)} scroll=${item.scrollWidth}/${item.clientWidth}`;
|
||||
}),
|
||||
offenders: [...document.querySelectorAll('body *')].map((item) => ({ item, rect: item.getBoundingClientRect() }))
|
||||
.filter(({ item, rect }) => !item.closest('[hidden]') && rect.width > 0 && (rect.right > document.documentElement.clientWidth + 1 || rect.left < -1 || item.scrollWidth > item.clientWidth + 2))
|
||||
.slice(0, 12).map(({ item, rect }) => `${item.tagName.toLowerCase()}${item.id ? `#${item.id}` : ''}.${String(item.className?.baseVal ?? item.className ?? '').split(' ').filter(Boolean).join('.')} ${Math.round(rect.left)}..${Math.round(rect.right)} scroll=${item.scrollWidth}/${item.clientWidth}`) }));
|
||||
assert.ok(desktopWidth.document <= desktopWidth.viewport, JSON.stringify(desktopWidth));
|
||||
const overviewUrgentBounds = await desktop.page.locator('#attention-list .tone-urgent').evaluateAll((items) => items.map((item) => {
|
||||
const rect = item.getBoundingClientRect(); return [rect.left, rect.right];
|
||||
}));
|
||||
assert.ok(overviewUrgentBounds.every(([left, right]) => left >= 0 && right <= 1440));
|
||||
|
||||
await desktop.page.locator('.attention-footer a[href="#fleet-attention-queue"]').click();
|
||||
await desktop.page.waitForFunction(() => location.hash === '#fleet-attention-queue' && document.querySelector('.console-workspace').dataset.currentView === 'fleet');
|
||||
assert.equal(await desktop.page.locator('#fleet-attention-list .attention-group-fleet [data-attention-record]').count(), 17);
|
||||
assert.equal(await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details li').count(), 50,
|
||||
'the conflicting owner remains a separate observation instead of overwriting provenance');
|
||||
assert.deepEqual(await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details li code').allTextContents(),
|
||||
[...references, references[48]]);
|
||||
assert.equal(await desktop.page.evaluate(() => document.activeElement.id), 'fleet-attention-queue');
|
||||
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Owner: Responsible domain not recorded/);
|
||||
await desktop.page.locator('#fleet-attention-list .attention-group-advisory .attention-cohort-details summary').click();
|
||||
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Owner: Owner not recorded/);
|
||||
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Sources: doctor, backlogHealth/);
|
||||
assert.match(await desktop.page.locator('#fleet-attention-list').innerText(), /Urgent evidence 1/);
|
||||
const queueFilter = desktop.page.locator('#fleet-attention-filter');
|
||||
await queueFilter.fill(references[0]);
|
||||
assert.equal(await desktop.page.locator('#fleet-attention-result-count').innerText(), '1 of 18 findings match.');
|
||||
await queueFilter.fill('');
|
||||
assert.equal(await desktop.page.locator('#fleet-attention-list [data-attention-record]:visible').count(), 18);
|
||||
await desktop.page.locator('#fleet-attention-list details').evaluateAll((details) => details.forEach((detail) => { detail.open = true; }));
|
||||
assert.ok(await desktop.page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth),
|
||||
'expanded full evidence and cohort references remain horizontally contained');
|
||||
const queueBounds = await desktop.page.locator('#fleet-attention-list [data-attention-record]').evaluateAll((items) => items.map((item) => {
|
||||
const rect = item.getBoundingClientRect(); return [rect.left, rect.right];
|
||||
}));
|
||||
assert.ok(queueBounds.every(([left, right]) => left >= 0 && right <= 1440));
|
||||
|
||||
const mobile = await openOverview({ width: 390, height: 844 });
|
||||
t.after(() => mobile.page.close());
|
||||
const mobileFixture = crowdedSnapshot(mobile.snapshot);
|
||||
await renderSnapshot(mobile.page, mobileFixture.snapshot);
|
||||
const mobileWidth = await mobile.page.evaluate(() => ({ viewport: document.documentElement.clientWidth,
|
||||
document: document.documentElement.scrollWidth,
|
||||
offenders: [...document.querySelectorAll('body *')].map((item) => ({ item, rect: item.getBoundingClientRect() }))
|
||||
.filter(({ item, rect }) => !item.closest('[hidden]') && rect.width > 0 && rect.right > document.documentElement.clientWidth + 1)
|
||||
.slice(0, 8).map(({ item, rect }) => `${item.tagName.toLowerCase()}${item.id ? `#${item.id}` : ''}.${String(item.className?.baseVal ?? item.className ?? '').split(' ').filter(Boolean).join('.')} right=${Math.round(rect.right)}`) }));
|
||||
assert.ok(mobileWidth.document <= mobileWidth.viewport, JSON.stringify(mobileWidth));
|
||||
await mobile.page.locator('#attention-list .attention-group-advisory summary').click();
|
||||
const lastReference = mobile.page.locator('#attention-list .attention-group-advisory .attention-cohort-details li').last();
|
||||
const mobileBounds = await lastReference.boundingBox();
|
||||
assert.ok(mobileBounds && mobileBounds.x >= 0 && mobileBounds.x + mobileBounds.width <= 390,
|
||||
'expanded exact references remain within the mobile viewport');
|
||||
assert.ok(await mobile.page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth),
|
||||
'expanded advisory provenance does not overflow the mobile viewport');
|
||||
assert.equal(await mobile.page.locator('#fleet-preview-count').innerText(), `Showing ${previewCount} of ${projected.fleetTotal} Fleet findings; ${projected.fleetTotal - previewCount} remaining`);
|
||||
});
|
||||
|
||||
test('SCN-console-overview-refresh-and-navigation-preserve-owner-drafts-without-adding-unsafe-actions', async (t) => {
|
||||
const { page, snapshot } = await openOverview();
|
||||
t.after(() => page.close());
|
||||
const unreadBefore = snapshot.messages?.unacknowledged ?? 0;
|
||||
await renderSnapshot(page, unsafeSnapshot(snapshot));
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
await page.locator('.task-create-panel > summary').click();
|
||||
const title = page.locator('#task-create-form [name="title"]');
|
||||
await title.fill('Unsent route-change draft for the current revision.');
|
||||
await page.locator('.console-rail a[href="#overview"]').click();
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="3"]').waitFor();
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
assert.equal(await title.inputValue(), 'Unsent route-change draft for the current revision.');
|
||||
assert.equal(await page.locator('#attention-list [data-attention-decision]').count(), 2);
|
||||
assert.equal(await page.locator('#attention-list button').filter({ hasText: /Acknowledge|Close|Recover/ }).count(), 0);
|
||||
assert.equal(await page.evaluate(() => globalThis.TorchConsoleDemo.snapshot().messages?.unacknowledged ?? 0), unreadBefore,
|
||||
'rendering and refresh do not acknowledge durable messages');
|
||||
});
|
||||
|
||||
test('SCN-console-overview-retains-ten-destinations-keyboard-routing-and-manager-wake-deep-links', async (t) => {
|
||||
const { page, snapshot } = await openOverview({ width: 390, height: 844 });
|
||||
t.after(() => page.close());
|
||||
await renderSnapshot(page, unsafeSnapshot(snapshot));
|
||||
const links = page.locator('.console-rail a[href^="#"]');
|
||||
assert.equal(await links.count(), 10);
|
||||
const destinations = ['overview', 'owner-briefing', 'flow-watch', 'work', 'initiative-progress', 'fleet',
|
||||
'communications', 'organization', 'evidence', 'delivery'];
|
||||
for (const destination of destinations) {
|
||||
const link = page.locator(`.console-rail a[href="#${destination}"]`);
|
||||
if (destination === 'fleet') {
|
||||
await link.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
} else await link.click();
|
||||
await page.waitForFunction((view) => location.hash === `#${view}`
|
||||
&& document.querySelector('.console-workspace').dataset.currentView === view, destination);
|
||||
}
|
||||
await page.locator('.console-rail a[href="#fleet"]').click();
|
||||
await page.waitForFunction(() => location.hash === '#fleet'
|
||||
&& document.querySelector('.console-workspace').dataset.currentView === 'fleet');
|
||||
await page.goto(`${origin}/console?demo=1#manager-wakes`);
|
||||
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
|
||||
await page.waitForFunction(() => document.activeElement.id === 'manager-wakes');
|
||||
assert.equal(await page.locator('#manager-wakes').isVisible(), true);
|
||||
await page.goBack();
|
||||
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
|
||||
await page.goForward();
|
||||
await page.waitForFunction(() => document.querySelector('.console-workspace').dataset.currentView === 'fleet');
|
||||
await page.waitForFunction(() => document.activeElement.id === 'manager-wakes');
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth));
|
||||
});
|
||||
Reference in new issue
Block a user