From 2752caab1cb79f45c5f82a7aa6f3ef43d0c7e837 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 06:26:25 -0400 Subject: [PATCH] TASK-conversation-console-workflow: add focused Console views --- site/console.html | 29 +++-- site/console.js | 126 +++++++++++++++++++- site/styles.css | 26 ++-- test/fleet/console-view-navigation.test.mjs | 111 +++++++++++++++++ 4 files changed, 271 insertions(+), 21 deletions(-) create mode 100644 test/fleet/console-view-navigation.test.mjs diff --git a/site/console.html b/site/console.html index 662156a..3df04ff 100644 --- a/site/console.html +++ b/site/console.html @@ -43,14 +43,19 @@ Organization Evidence Release gates -

Local project view
—

+
+ Snapshot provenance + Commit — + Branch — +

Waiting for the first snapshot.

+

-
+

Project pulse

Work in motion

Observing

TORCH is gathering the latest repository and fleet evidence.

@@ -67,12 +72,12 @@
-
+

Owner briefing

Published evidence

Reading the latest local publication…

-
+

Approvals and reporting lines

Flow watch

Owner decisions gated

See which teams need attention and who owns each decision. A configured cadence does not confirm that a system timer is installed.

@@ -88,7 +93,7 @@
-
+

Authoritative backlog

Work board

0 active
@@ -161,13 +166,13 @@
-
+

Derived from the single backlog

Feature and milestone progress

Task states only · gates remain separate

Progress is calculated from each linked task's current state. It does not mean checks passed, integration landed, or a release is ready.

-
+

Persistent identities

Fleet and ownership

Session state from latest observation

Sessions

One context per identity
@@ -196,19 +201,19 @@
-
+

Current approved structure

Organization map

No active graph
-
+

Owner review

Organization proposals

0 pending
-
+

Durable coordination

Conversations

0 unacknowledged
Request work from an agent @@ -244,7 +249,7 @@
-
+

Provenance first

Evidence and review

Exact commit receipts

Checks

Passes are commit-bound
@@ -252,7 +257,7 @@
-
+

Separate authority gates

Integration and release

Integration is not deployment

Integration

FIFO queue · source commit
diff --git a/site/console.js b/site/console.js index cba404d..fc016cf 100644 --- a/site/console.js +++ b/site/console.js @@ -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 `
-
${escapeHtml(task.id)}${escapeHtml(task.priority ?? 'normal')}
+
${escapeHtml(task.priority ?? 'normal')}

${escapeHtml(task.title ?? 'Untitled task')}

${escapeHtml(detail || 'No task description recorded.')}

+
Task reference${escapeHtml(task.id)}
${staleEvidence ? `
Evidence needs refresh

${escapeHtml(staleEvidence.recommendation)}

` : ''}
Owner
${escapeHtml(task.owner ?? 'Unassigned')}
@@ -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(); diff --git a/site/styles.css b/site/styles.css index 7ca7687..b6799b4 100644 --- a/site/styles.css +++ b/site/styles.css @@ -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; } diff --git a/test/fleet/console-view-navigation.test.mjs b/test/fleet/console-view-navigation.test.mjs new file mode 100644 index 0000000..9d8221d --- /dev/null +++ b/test/fleet/console-view-navigation.test.mjs @@ -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'); +});