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');
+});