mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 12:51:54 +00:00
TASK-conversation-console-workflow: add focused Console views
This commit is contained in:
1 parent
cb145e56cb
commit
2752caab1c
4 files changed
+271
-21
No files matched your search
+17
-12
@@ -43,14 +43,19 @@
|
||||
<a href="#organization">Organization</a>
|
||||
<a href="#evidence">Evidence</a>
|
||||
<a href="#delivery">Release gates</a>
|
||||
<p class="rail-note">Local project view<br><span id="project-commit">—</span></p>
|
||||
<details class="rail-provenance">
|
||||
<summary>Snapshot provenance</summary>
|
||||
<span>Commit <code id="project-commit">—</code></span>
|
||||
<span>Branch <code id="project-branch-detail">—</code></span>
|
||||
</details>
|
||||
</nav>
|
||||
|
||||
<div class="console-workspace">
|
||||
<p class="console-error" id="console-error" role="alert" hidden></p>
|
||||
<p id="live-refresh-status" role="status" aria-live="polite">Waiting for the first snapshot.</p>
|
||||
<p id="console-view-status" class="screen-reader-only" role="status" aria-live="polite"></p>
|
||||
|
||||
<section id="overview" class="pulse-zone" aria-labelledby="pulse-title">
|
||||
<section id="overview" class="pulse-zone" data-console-view="overview" aria-labelledby="pulse-title">
|
||||
<div class="pulse-main">
|
||||
<div class="pulse-heading"><div><p class="section-kicker">Project pulse</p><h1 id="pulse-title">Work in motion</h1></div><span id="fleet-health" class="health-flag">Observing</span></div>
|
||||
<p id="project-summary" class="pulse-summary">TORCH is gathering the latest repository and fleet evidence.</p>
|
||||
@@ -67,12 +72,12 @@
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section id="owner-briefing" class="console-section owner-briefing" aria-labelledby="owner-briefing-title">
|
||||
<section id="owner-briefing" class="console-section owner-briefing" data-console-view="owner-briefing" aria-labelledby="owner-briefing-title">
|
||||
<div class="section-heading"><h2 id="owner-briefing-title">Owner briefing</h2><span>Published evidence</span></div>
|
||||
<div id="owner-briefing-content"><p>Reading the latest local publication…</p></div>
|
||||
</section>
|
||||
|
||||
<section id="flow-watch" class="console-section flow-watch-section" aria-labelledby="flow-watch-title">
|
||||
<section id="flow-watch" class="console-section flow-watch-section" data-console-view="flow-watch" aria-labelledby="flow-watch-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Approvals and reporting lines</p><h2 id="flow-watch-title">Flow watch</h2></div><span>Owner decisions gated</span></div>
|
||||
<p class="flow-watch-note">See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.</p>
|
||||
<div class="flow-watch-grid">
|
||||
@@ -88,7 +93,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="work" class="board-section" aria-labelledby="work-title">
|
||||
<section id="work" class="board-section" data-console-view="work" aria-labelledby="work-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Authoritative backlog</p><h2 id="work-title">Work board</h2></div><span id="task-count">0 active</span></div>
|
||||
<div class="work-view-tools" role="group" aria-label="Backlog views and filters">
|
||||
<div class="work-filter-row">
|
||||
@@ -161,13 +166,13 @@
|
||||
<p id="task-create-action-status" class="priority-action-status" role="status" aria-live="polite" hidden></p>
|
||||
</section>
|
||||
|
||||
<section id="initiative-progress" class="console-section initiative-section" aria-labelledby="initiative-title">
|
||||
<section id="initiative-progress" class="console-section initiative-section" data-console-view="initiative-progress" aria-labelledby="initiative-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Derived from the single backlog</p><h2 id="initiative-title">Feature and milestone progress</h2></div><span>Task states only · gates remain separate</span></div>
|
||||
<p class="initiative-note">Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.</p>
|
||||
<div id="initiative-rollup" class="initiative-rollup" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section id="fleet" class="console-section fleet-section" aria-labelledby="fleet-title">
|
||||
<section id="fleet" class="console-section fleet-section" data-console-view="fleet" aria-labelledby="fleet-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Persistent identities</p><h2 id="fleet-title">Fleet and ownership</h2></div><span id="agent-health-note">Session state from latest observation</span></div>
|
||||
<div class="fleet-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Sessions</h3><span>One context per identity</span></div><div id="agent-list" class="agent-list"></div>
|
||||
@@ -196,19 +201,19 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="organization" class="console-section organization-section" aria-labelledby="organization-title">
|
||||
<section id="organization" class="console-section organization-section" data-console-view="organization" aria-labelledby="organization-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Current approved structure</p><h2 id="organization-title">Organization map</h2></div><span id="organization-revision">No active graph</span></div>
|
||||
<div id="organization-map" class="organization-map" aria-live="polite"></div>
|
||||
<div id="organization-lifecycle" class="organization-lifecycle" aria-live="polite"></div>
|
||||
</section>
|
||||
|
||||
<section id="organization-proposals" class="console-section" aria-labelledby="proposal-title">
|
||||
<section id="organization-proposals" class="console-section" data-console-view="organization" aria-labelledby="proposal-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Owner review</p><h2 id="proposal-title">Organization proposals</h2></div><span id="change-count">0 pending</span></div>
|
||||
<p id="organization-action-status" class="organization-action-status" role="status" aria-live="polite" hidden></p>
|
||||
<div id="change-list" class="console-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="communications" class="console-section communication-section" aria-labelledby="communications-title">
|
||||
<section id="communications" class="console-section communication-section" data-console-view="communications" aria-labelledby="communications-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Durable coordination</p><h2 id="communications-title">Conversations</h2></div><span id="message-count">0 unacknowledged</span></div>
|
||||
<details class="owner-request-composer">
|
||||
<summary>Request work from an agent</summary>
|
||||
@@ -244,7 +249,7 @@
|
||||
<div id="message-list" class="conversation-stream"></div>
|
||||
</section>
|
||||
|
||||
<section id="evidence" class="console-section evidence-section" aria-labelledby="evidence-title">
|
||||
<section id="evidence" class="console-section evidence-section" data-console-view="evidence" aria-labelledby="evidence-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Provenance first</p><h2 id="evidence-title">Evidence and review</h2></div><span>Exact commit receipts</span></div>
|
||||
<div class="evidence-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Checks</h3><span>Passes are commit-bound</span></div><div id="check-list" class="console-list"></div></article>
|
||||
@@ -252,7 +257,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="delivery" class="console-section delivery-section" aria-labelledby="delivery-title">
|
||||
<section id="delivery" class="console-section delivery-section" data-console-view="delivery" aria-labelledby="delivery-title">
|
||||
<div class="section-heading"><div><p class="section-kicker">Separate authority gates</p><h2 id="delivery-title">Integration and release</h2></div><span>Integration is not deployment</span></div>
|
||||
<div class="delivery-columns">
|
||||
<article class="work-surface"><div class="panel-heading"><h3>Integration</h3><span>FIFO queue · source commit</span></div><div id="integration-list" class="console-list"></div></article>
|
||||
|
||||
+124
-2
@@ -1,4 +1,83 @@
|
||||
const consoleViewDefinitions = Object.freeze({
|
||||
overview: { title: 'Overview' },
|
||||
'owner-briefing': { title: 'Briefing' },
|
||||
'flow-watch': { title: 'Flow watch' },
|
||||
work: { title: 'Work' },
|
||||
'initiative-progress': { title: 'Progress' },
|
||||
fleet: { title: 'Fleet' },
|
||||
communications: { title: 'Conversations' },
|
||||
organization: { title: 'Organization' },
|
||||
evidence: { title: 'Evidence' },
|
||||
delivery: { title: 'Release gates' },
|
||||
});
|
||||
|
||||
function installConsoleViewRouting() {
|
||||
const nav = document.querySelector('.console-rail');
|
||||
const sections = [...document.querySelectorAll('.console-workspace > [data-console-view]')];
|
||||
const links = [...(nav?.querySelectorAll('a[href^="#"]') ?? [])];
|
||||
if (!nav || !sections.length || !links.length) return { sync: () => 'overview' };
|
||||
|
||||
let lastHash = null;
|
||||
const decodeHash = (hash) => {
|
||||
try { return decodeURIComponent(String(hash ?? '').replace(/^#/, '')); } catch { return ''; }
|
||||
};
|
||||
|
||||
function resolveView(hash) {
|
||||
const fragment = decodeHash(hash);
|
||||
if (consoleViewDefinitions[fragment]) return fragment;
|
||||
const target = document.getElementById(fragment);
|
||||
const containingView = target?.closest('[data-console-view]')?.dataset.consoleView;
|
||||
return consoleViewDefinitions[containingView] ? containingView : 'overview';
|
||||
}
|
||||
|
||||
function sync({ focus = false, force = false } = {}) {
|
||||
const hash = globalThis.location.hash || '#overview';
|
||||
if (!force && hash === lastHash) return resolveView(hash);
|
||||
lastHash = hash;
|
||||
const view = resolveView(hash);
|
||||
const definition = consoleViewDefinitions[view];
|
||||
const visible = sections.filter((section) => section.dataset.consoleView === view);
|
||||
sections.forEach((section) => { section.hidden = section.dataset.consoleView !== view; });
|
||||
links.forEach((link) => {
|
||||
const route = resolveView(link.getAttribute('href'));
|
||||
if (route === view) link.setAttribute('aria-current', 'page');
|
||||
else link.removeAttribute('aria-current');
|
||||
});
|
||||
const workspace = document.querySelector('.console-workspace');
|
||||
if (workspace) workspace.dataset.currentView = view;
|
||||
document.title = `${definition.title} | TORCH Local Fleet Console`;
|
||||
const announcement = document.querySelector('#console-view-status');
|
||||
if (announcement) announcement.textContent = `Viewing ${definition.title}.`;
|
||||
|
||||
if (focus) {
|
||||
const targetId = decodeHash(hash);
|
||||
const target = document.getElementById(targetId);
|
||||
if (target && target.dataset.consoleView !== view && target.closest('[data-console-view]')) {
|
||||
target.scrollIntoView?.({ block: 'start' });
|
||||
target.focus?.({ preventScroll: true });
|
||||
} else {
|
||||
const labelledBy = visible[0]?.getAttribute('aria-labelledby');
|
||||
const heading = (labelledBy && document.getElementById(labelledBy))
|
||||
?? visible[0]?.querySelector('h1, h2');
|
||||
if (heading) {
|
||||
heading.tabIndex = -1;
|
||||
heading.focus({ preventScroll: true });
|
||||
}
|
||||
globalThis.scrollTo?.({ top: 0, behavior: 'instant' });
|
||||
}
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
globalThis.addEventListener('hashchange', () => sync({ focus: true }));
|
||||
globalThis.addEventListener('popstate', () => sync({ focus: true }));
|
||||
const initialFragment = decodeHash(globalThis.location.hash);
|
||||
sync({ focus: Boolean(initialFragment && !consoleViewDefinitions[initialFragment]) });
|
||||
return { sync };
|
||||
}
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
const consoleViewRouter = installConsoleViewRouting();
|
||||
const demoWorkspace = globalThis.TorchConsoleDemo;
|
||||
const workViews = globalThis.TorchWorkViews;
|
||||
const workProgress = globalThis.TorchWorkProgress;
|
||||
@@ -45,6 +124,46 @@ function setText(selector, value) {
|
||||
if (element) element.textContent = value ?? '';
|
||||
}
|
||||
|
||||
function compactOwnerBriefingProvenance() {
|
||||
const publication = $('#owner-briefing-content')?.querySelector('.briefing-publication');
|
||||
if (!publication) return;
|
||||
|
||||
const timestamp = publication.querySelector('p time');
|
||||
const window = [...publication.querySelectorAll('.briefing-muted')]
|
||||
.find((paragraph) => paragraph.textContent.trim().startsWith('Window:'));
|
||||
if (timestamp || window) {
|
||||
const disclosure = document.createElement('details');
|
||||
disclosure.className = 'briefing-provenance';
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = 'Publication time and reporting window';
|
||||
disclosure.append(summary);
|
||||
if (timestamp) {
|
||||
const exactTime = timestamp.cloneNode(true);
|
||||
exactTime.textContent = timestamp.getAttribute('datetime') || timestamp.textContent;
|
||||
const published = document.createElement('p');
|
||||
published.append('Published ', exactTime);
|
||||
disclosure.append(published);
|
||||
timestamp.textContent = ageLabel(timestamp.getAttribute('datetime'));
|
||||
timestamp.title = exactTime.textContent;
|
||||
}
|
||||
if (window) disclosure.append(window);
|
||||
publication.append(disclosure);
|
||||
}
|
||||
|
||||
for (const code of publication.querySelectorAll('code')) {
|
||||
const commit = code.textContent.trim();
|
||||
if (!/^[a-f0-9]{40}$/i.test(commit)) continue;
|
||||
const disclosure = document.createElement('details');
|
||||
disclosure.className = 'briefing-provenance';
|
||||
const summary = document.createElement('summary');
|
||||
summary.textContent = `Commit ${shortCommit(commit)}`;
|
||||
const exact = document.createElement('code');
|
||||
exact.textContent = commit;
|
||||
disclosure.append(summary, exact);
|
||||
code.replaceWith(disclosure);
|
||||
}
|
||||
}
|
||||
|
||||
function shortCommit(value) {
|
||||
return value ? String(value).slice(0, 9) : '—';
|
||||
}
|
||||
@@ -69,9 +188,10 @@ function taskCard(task, healthFindings = []) {
|
||||
const staleEvidence = healthFindings.find((finding) => finding.taskId === task.id
|
||||
&& ['BACKLOG_OBSERVED_COMMIT_STALE', 'BACKLOG_OBSERVED_COMMIT_MISSING'].includes(finding.code));
|
||||
return `<article class="task-ticket">
|
||||
<div class="ticket-topline"><code>${escapeHtml(task.id)}</code><span class="priority priority-${escapeHtml(task.priority ?? 'normal')}">${escapeHtml(task.priority ?? 'normal')}</span></div>
|
||||
<div class="ticket-topline"><span class="priority priority-${escapeHtml(task.priority ?? 'normal')}">${escapeHtml(task.priority ?? 'normal')}</span></div>
|
||||
<h4>${escapeHtml(task.title ?? 'Untitled task')}</h4>
|
||||
<p>${escapeHtml(detail || 'No task description recorded.')}</p>
|
||||
<details class="task-reference"><summary>Task reference</summary><code>${escapeHtml(task.id)}</code></details>
|
||||
${staleEvidence ? `<div class="evidence-warning" role="status"><strong>Evidence needs refresh</strong><p>${escapeHtml(staleEvidence.recommendation)}</p></div>` : ''}
|
||||
<dl class="ticket-meta">
|
||||
<div><dt>Owner</dt><dd>${escapeHtml(task.owner ?? 'Unassigned')}</dd></div>
|
||||
@@ -772,6 +892,7 @@ function render(snapshot) {
|
||||
|
||||
setText('#project-name', project?.name ?? project?.id ?? 'Not installed');
|
||||
setText('#project-branch', branch);
|
||||
setText('#project-branch-detail', branch);
|
||||
setText('#project-commit', shortCommit(snapshot.repository?.head));
|
||||
setText('#fleet-health', snapshot.mode === 'installed'
|
||||
? (snapshot.doctor?.healthy ? 'Healthy' : 'Review findings') : 'Not installed');
|
||||
@@ -794,6 +915,7 @@ function render(snapshot) {
|
||||
renderRuntimeProfileEditor(agents, snapshot.runtimeAdapters ?? [], snapshot.mode === 'installed');
|
||||
renderFlowWatch(snapshot);
|
||||
setHtml('#owner-briefing-content', globalThis.TorchOwnerDigest.render(snapshot.ownerDigest));
|
||||
compactOwnerBriefingProvenance();
|
||||
|
||||
renderAttention(attention);
|
||||
|
||||
@@ -1910,11 +2032,11 @@ globalThis.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted && !liveUpdatesPaused && document.visibilityState === 'visible') refresh();
|
||||
});
|
||||
if (demoWorkspace) {
|
||||
document.title = 'TORCH Dashboard';
|
||||
$('#demo-banner').hidden = false;
|
||||
$('#reset-demo')?.addEventListener('click', () => {
|
||||
demoWorkspace.reset();
|
||||
globalThis.location.reload();
|
||||
});
|
||||
}
|
||||
consoleViewRouter.sync({ force: true });
|
||||
refresh();
|
||||
+19
-7
@@ -152,9 +152,19 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.console-rail { position: sticky; top: 0; align-self: start; display: grid; gap: .2rem; padding: 1.5rem .75rem; border-right: 1px solid var(--line); }
|
||||
.console-rail a { padding: .55rem .7rem; color: var(--muted); font-weight: 650; text-decoration: none; border-left: 3px solid transparent; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible { color: var(--ink); background: var(--paper); border-left-color: var(--route); }
|
||||
.rail-note { margin: 2rem .7rem 0; color: var(--muted); font-size: .76rem; line-height: 1.7; }
|
||||
.rail-note span { color: var(--ink); font-family: ui-monospace, monospace; }
|
||||
.console-rail a[aria-current="page"] { color: var(--ink); background: var(--paper); border-left-color: var(--route); }
|
||||
.rail-provenance { margin: 1.5rem .7rem 0; color: var(--muted); font-size: .72rem; line-height: 1.55; overflow-wrap: anywhere; }
|
||||
.rail-provenance summary, .briefing-provenance summary, .task-reference summary { cursor: pointer; color: var(--route); font-weight: 650; }
|
||||
.rail-provenance > span { display: block; margin-top: .35rem; }
|
||||
.rail-provenance code { color: var(--ink); overflow-wrap: anywhere; }
|
||||
.console-workspace { min-width: 0; padding: 0 clamp(1rem, 2.5vw, 2.5rem) 3rem; }
|
||||
.console-workspace > [data-console-view][hidden] { display: none !important; }
|
||||
.screen-reader-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||
.briefing-provenance { margin-top: .4rem; color: var(--muted); font-size: .78rem; overflow-wrap: anywhere; }
|
||||
.briefing-provenance p { margin: .3rem 0; }
|
||||
.briefing-provenance code { display: inline-block; max-width: 100%; overflow-wrap: anywhere; }
|
||||
.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; }
|
||||
@@ -578,10 +588,10 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.blueprint article { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.blueprint-index { margin-bottom: 2rem; }
|
||||
.console-layout { grid-template-columns: 1fr; }
|
||||
.console-rail { position: sticky; z-index: 5; top: 0; display: flex; overflow-x: auto; padding: .35rem .6rem; border-right: 0; border-bottom: 1px solid var(--line); background: var(--paper); }
|
||||
.console-rail a { flex: 0 0 auto; padding: .5rem .65rem; border-left: 0; border-bottom: 3px solid transparent; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible { border-left-color: transparent; border-bottom-color: var(--route); }
|
||||
.rail-note { display: none; }
|
||||
.console-rail { position: sticky; z-index: 5; top: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: .25rem; padding: .4rem .6rem; border-right: 0; border-bottom: 1px solid var(--line); background: var(--paper); }
|
||||
.console-rail a { min-width: 0; padding: .45rem .5rem; border-left: 0; border-bottom: 3px solid transparent; overflow-wrap: anywhere; }
|
||||
.console-rail a:hover, .console-rail a:focus-visible, .console-rail a[aria-current="page"] { border-left-color: transparent; border-bottom-color: var(--route); }
|
||||
.rail-provenance { grid-column: 1 / -1; margin: .25rem .5rem; }
|
||||
.console-workspace { padding-inline: clamp(1rem, 3vw, 2rem); }
|
||||
.pulse-zone { grid-template-columns: minmax(0, 1.15fr) minmax(240px, .85fr); }
|
||||
.work-filter-row { grid-template-columns: repeat(auto-fit, minmax(135px, 1fr)); }
|
||||
@@ -611,11 +621,13 @@ footer div { display: flex; gap: 1.5rem; }
|
||||
.console-nav { min-height: 62px; gap: .7rem; }
|
||||
.console-project { margin-right: 0; }
|
||||
.console-project span { display: none; }
|
||||
.console-tools > span { display: none; }
|
||||
.console-tools { flex-wrap: wrap; gap: .5rem; }
|
||||
.console-tools > span { display: inline-flex; flex-basis: 100%; }
|
||||
.console-rail { top: 0; }
|
||||
.section-heading { flex-wrap: wrap; }
|
||||
.section-heading > div, .section-heading > span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.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; }
|
||||
.owner-request-fields, .runtime-profile-fields { grid-template-columns: 1fr; }
|
||||
.schedule-launcher-actions { align-items: stretch; flex-direction: column; }
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { once } from 'node:events';
|
||||
import test from 'node:test';
|
||||
import { chromium } from '@playwright/test';
|
||||
import { createConsoleServer } from '../../src/console/server.mjs';
|
||||
|
||||
const views = [
|
||||
['overview', 'Overview'],
|
||||
['owner-briefing', 'Briefing'],
|
||||
['flow-watch', 'Flow watch'],
|
||||
['work', 'Work'],
|
||||
['initiative-progress', 'Progress'],
|
||||
['fleet', 'Fleet'],
|
||||
['communications', 'Conversations'],
|
||||
['organization', 'Organization'],
|
||||
['evidence', 'Evidence'],
|
||||
['delivery', 'Release gates'],
|
||||
];
|
||||
|
||||
async function openDemo(t, viewport) {
|
||||
const server = createConsoleServer();
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
let browser;
|
||||
t.after(async () => {
|
||||
await browser?.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport });
|
||||
const page = await context.newPage();
|
||||
await page.clock.install({ time: new Date('2026-10-01T12:00:00.000Z') });
|
||||
await page.goto(`http://127.0.0.1:${server.address().port}/console?demo=1#work`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
return { page, context };
|
||||
}
|
||||
|
||||
async function assertSelectedView(page, route, title) {
|
||||
const selected = await page.locator('.console-workspace > [data-console-view]:not([hidden])').evaluateAll((sections) =>
|
||||
[...new Set(sections.map((section) => section.dataset.consoleView))]);
|
||||
assert.deepEqual(selected, [route], `only the ${route} view should be visible`);
|
||||
assert.equal(await page.title(), `${title} | TORCH Local Fleet Console`);
|
||||
assert.equal(await page.locator(`.console-rail a[aria-current="page"][href="#${route}"]`).count(), 1);
|
||||
assert.equal(await page.locator('#project-name').isVisible(), true);
|
||||
assert.equal(await page.locator('#snapshot-time').isVisible(), true);
|
||||
}
|
||||
|
||||
test('SCN-console-view-navigation: fragments, direct loads, Back and Forward select one addressable view', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
await assertSelectedView(page, 'work', 'Work');
|
||||
|
||||
await page.locator('.console-rail a[href="#fleet"]').click();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
await page.locator('.console-rail a[href="#evidence"]').click();
|
||||
await assertSelectedView(page, 'evidence', 'Evidence');
|
||||
await page.goBack();
|
||||
await assertSelectedView(page, 'fleet', 'Fleet');
|
||||
await page.goForward();
|
||||
await assertSelectedView(page, 'evidence', 'Evidence');
|
||||
|
||||
await page.goto(`http://127.0.0.1:${new URL(page.url()).port}/console?demo=1#approval-request-list`);
|
||||
await page.locator('#live-refresh-status[data-generation="1"]').waitFor();
|
||||
await assertSelectedView(page, 'flow-watch', 'Flow watch');
|
||||
});
|
||||
|
||||
test('SCN-console-mobile-navigation: every destination is visible at 390x844 without horizontal hunting', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 390, height: 844 });
|
||||
assert.equal(await page.locator('.console-rail a').count(), views.length);
|
||||
|
||||
for (const [route, title] of views) {
|
||||
const link = page.locator(`.console-rail a[href="#${route}"]`);
|
||||
assert.equal(await link.isVisible(), true, `${title} navigation must remain visible`);
|
||||
await link.click();
|
||||
await assertSelectedView(page, route, title);
|
||||
const widths = await page.evaluate(() => ({
|
||||
viewport: innerWidth,
|
||||
document: document.documentElement.scrollWidth,
|
||||
navigation: document.querySelector('.console-rail').scrollWidth,
|
||||
navigationClient: document.querySelector('.console-rail').clientWidth,
|
||||
}));
|
||||
assert.ok(widths.document <= widths.viewport, `document overflows horizontally: ${JSON.stringify(widths)}`);
|
||||
assert.ok(widths.navigation <= widths.navigationClient, `navigation requires horizontal scrolling: ${JSON.stringify(widths)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('SCN-console-view-drafts: navigation and refresh retain unsent edits and the explicit owner preview boundary', async (t) => {
|
||||
const { page } = await openDemo(t, { width: 1280, height: 900 });
|
||||
const titleInput = page.locator('#task-create-form [name="title"]');
|
||||
await page.locator('.task-create-panel > summary').click();
|
||||
await titleInput.fill('Unsent route-change draft');
|
||||
await page.locator('.console-rail a[href="#flow-watch"]').click();
|
||||
const approval = page.locator('.approval-decision-form').first();
|
||||
await approval.locator('[name="decision"]').selectOption('approved');
|
||||
await approval.locator('[name="note"]').fill('Review remains explicit after navigation.');
|
||||
await approval.locator('[data-approval-preview]').click();
|
||||
await approval.locator('[data-approval-confirm]:visible').waitFor();
|
||||
const previewToken = await approval.getAttribute('data-preview-token');
|
||||
|
||||
await page.locator('.console-rail a[href="#work"]').click();
|
||||
await page.locator('#refresh-console').click();
|
||||
await page.locator('#live-refresh-status[data-generation="2"]').waitFor();
|
||||
assert.equal(await titleInput.inputValue(), 'Unsent route-change draft');
|
||||
|
||||
await page.locator('.console-rail a[href="#flow-watch"]').click();
|
||||
assert.equal(await approval.getAttribute('data-preview-token'), previewToken);
|
||||
assert.equal(await approval.locator('[data-approval-confirm]').isVisible(), true);
|
||||
assert.equal(await approval.locator('[name="decision"]').isDisabled(), true);
|
||||
assert.equal(await approval.locator('[data-approval-edit]').isVisible(), true);
|
||||
assert.match(await approval.locator('.approval-decision-status').innerText(), /bound to this request revision/);
|
||||
assert.equal(await page.locator('#approval-request-count').innerText(), '1 open');
|
||||
});
|
||||
Reference in new issue
Block a user