Close DV-01–04,07,09,13,14: unify operator-deck UX in server/web.
Standardize neon cyan on #00e8f5, wire SacredPageHeader across Builds/Path Tracer/LOTL, dedupe Pages.css, align Path Tracer chrome, drop dead wealth-deck CSS, add prefers-color-scheme shell + HelpTip, sidebar version from package.json build inject, and Vitest CSS regression guards.
This commit is contained in:
20
server/web/src/help/architectureDeferred.test.ts
Normal file
20
server/web/src/help/architectureDeferred.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const crucibleSrc = readFileSync(resolve(__dirname, '../pages/CruciblePage.tsx'), 'utf8');
|
||||
const wsProviderSrc = readFileSync(resolve(__dirname, '../context/WebSocketProvider.tsx'), 'utf8');
|
||||
|
||||
describe('architecture deferred (frontend)', () => {
|
||||
it('CruciblePage remains monolithic until section split lands', () => {
|
||||
expect(crucibleSrc.split('\n').length).toBeGreaterThan(1500);
|
||||
});
|
||||
|
||||
it('WebSocketProvider is still a single shared context', () => {
|
||||
expect(wsProviderSrc).toContain('WebSocketContext');
|
||||
});
|
||||
|
||||
it('terminal render cap documents virtualization ceiling', () => {
|
||||
expect(crucibleSrc).toContain('visibleTerminalLines');
|
||||
});
|
||||
});
|
||||
58
server/web/src/help/authStoragePolicy.test.ts
Normal file
58
server/web/src/help/authStoragePolicy.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
AUTH_CROSS_TAB_STORAGE_SYNC,
|
||||
AUTH_STORAGE_KEY,
|
||||
AUTH_SYNC_EVENT,
|
||||
describeAuthStoragePolicy,
|
||||
} from './authStoragePolicy';
|
||||
import { clearStoredAuth, getStoredAuth, setStoredAuth } from '../api/auth';
|
||||
|
||||
describe('authStoragePolicy', () => {
|
||||
beforeEach(() => {
|
||||
sessionStorage.clear();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('documents dual storage with same-tab event sync only', () => {
|
||||
expect(describeAuthStoragePolicy()).toMatch(/dual-write/);
|
||||
expect(describeAuthStoragePolicy()).toMatch(/aetherforge-auth/);
|
||||
expect(AUTH_CROSS_TAB_STORAGE_SYNC).toBe(false);
|
||||
expect(AUTH_STORAGE_KEY).toBe('aetherforge_auth');
|
||||
expect(AUTH_SYNC_EVENT).toBe('aetherforge-auth');
|
||||
});
|
||||
|
||||
it('dispatches aetherforge-auth on login for same-tab listeners', () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener(AUTH_SYNC_EVENT, handler);
|
||||
setStoredAuth('ops', 'secret');
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
window.removeEventListener(AUTH_SYNC_EVENT, handler);
|
||||
});
|
||||
|
||||
it('dispatches aetherforge-auth on logout for same-tab listeners', () => {
|
||||
setStoredAuth('ops', 'secret');
|
||||
const handler = vi.fn();
|
||||
window.addEventListener(AUTH_SYNC_EVENT, handler);
|
||||
clearStoredAuth();
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
window.removeEventListener(AUTH_SYNC_EVENT, handler);
|
||||
});
|
||||
|
||||
it('keeps sessionStorage auth when localStorage cleared externally (no cross-tab logout)', () => {
|
||||
setStoredAuth('ops', 'secret');
|
||||
const token = getStoredAuth();
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY);
|
||||
window.dispatchEvent(
|
||||
new StorageEvent('storage', {
|
||||
key: AUTH_STORAGE_KEY,
|
||||
oldValue: token,
|
||||
newValue: null,
|
||||
storageArea: localStorage,
|
||||
})
|
||||
);
|
||||
expect(getStoredAuth()).toBe(token);
|
||||
});
|
||||
});
|
||||
24
server/web/src/help/authStoragePolicy.ts
Normal file
24
server/web/src/help/authStoragePolicy.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Cross-tab auth storage policy (document-only).
|
||||
*
|
||||
* Credentials mirror in sessionStorage and localStorage under `aetherforge_auth`.
|
||||
* Reads prefer sessionStorage, then fall back to localStorage (survives tab close).
|
||||
*
|
||||
* Same-tab sync: login/logout dispatch the `aetherforge-auth` CustomEvent so SessionGate,
|
||||
* WebSocketProvider, and PresenceContext re-read credentials without a full reload.
|
||||
*
|
||||
* Cross-tab: intentionally NOT synced via the `storage` event. Logout in tab A clears
|
||||
* localStorage but tab B keeps in-memory session until refresh or a 401. Each tab owns
|
||||
* its WS lifecycle after auth changes in that tab.
|
||||
*/
|
||||
|
||||
export const AUTH_STORAGE_KEY = 'aetherforge_auth';
|
||||
export const AUTH_EXPIRED_FLAG_KEY = 'aetherforge_auth_expired';
|
||||
export const AUTH_SYNC_EVENT = 'aetherforge-auth';
|
||||
|
||||
/** By policy we never listen to cross-tab `storage` events for auth. */
|
||||
export const AUTH_CROSS_TAB_STORAGE_SYNC = false;
|
||||
|
||||
export function describeAuthStoragePolicy(): string {
|
||||
return 'dual-write session+local; same-tab aetherforge-auth; no storage-event cross-tab sync';
|
||||
}
|
||||
@@ -373,7 +373,10 @@ describe('getForgeFieldMeta', () => {
|
||||
|
||||
describe('getForgeLiveNotices', () => {
|
||||
it('returns empty array for a plain windows form', () => {
|
||||
const notices = getForgeLiveNotices(baseForm({ target_os: 'windows', spread_kit: false }), false);
|
||||
const notices = getForgeLiveNotices(
|
||||
baseForm({ target_os: 'windows', spread_kit: false, miner_execution: 'inprocess' }),
|
||||
false
|
||||
);
|
||||
expect(notices).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -450,4 +453,15 @@ describe('getForgeLiveNotices', () => {
|
||||
);
|
||||
expect(notices.some((n) => n.includes('without Spread Kit or Fusion'))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns when container/auto execution needs worker image', () => {
|
||||
const auto = getForgeLiveNotices(baseForm({ miner_execution: 'auto' }), false);
|
||||
expect(auto.some((n) => n.includes('agent-worker'))).toBe(true);
|
||||
|
||||
const container = getForgeLiveNotices(baseForm({ miner_execution: 'container' }), false);
|
||||
expect(container.some((n) => n.includes('Dockerfile.agent'))).toBe(true);
|
||||
|
||||
const inprocess = getForgeLiveNotices(baseForm({ miner_execution: 'inprocess' }), false);
|
||||
expect(inprocess.some((n) => n.includes('agent-worker'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -315,6 +315,12 @@ export function getForgeFieldMeta(form: BuildRequest): Record<string, ForgeField
|
||||
max_memory_percent: { disabled: false, badge: 'baked' },
|
||||
min_free_ram_mb: { disabled: false, badge: 'baked' },
|
||||
mining_mode: { disabled: false, badge: 'baked' },
|
||||
miner_execution: {
|
||||
disabled: false,
|
||||
badge: 'baked',
|
||||
hint:
|
||||
'Auto/container tiers need a worker image on the host: docker build -f docker/Dockerfile.agent -t aetherforge/agent-worker:latest . Override tag with AETHERFORGE_MINER_IMAGE.',
|
||||
},
|
||||
idle_threshold_pct: {
|
||||
disabled: !isIdle,
|
||||
badge: 'requires',
|
||||
@@ -627,6 +633,12 @@ export function getForgeLiveNotices(form: BuildRequest, fusionPrepSelected: bool
|
||||
if (form.target_os === 'universal' && !form.fusion_enabled && !form.spread_kit) {
|
||||
notices.push('Universal without Spread Kit or Fusion — pick a deliverable type above.');
|
||||
}
|
||||
const execMode = form.miner_execution ?? 'auto';
|
||||
if (execMode === 'auto' || execMode === 'container') {
|
||||
notices.push(
|
||||
'Container/auto execution needs aetherforge/agent-worker:latest on the operator host — build with docker/Dockerfile.agent (set AETHERFORGE_MINER_IMAGE to override).'
|
||||
);
|
||||
}
|
||||
|
||||
return notices;
|
||||
}
|
||||
|
||||
17
server/web/src/help/routeEffects.test.ts
Normal file
17
server/web/src/help/routeEffects.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { isDashboardRoute } from './routeEffects';
|
||||
|
||||
describe('isDashboardRoute', () => {
|
||||
it('matches Command Deck paths', () => {
|
||||
expect(isDashboardRoute('/dashboard')).toBe(true);
|
||||
expect(isDashboardRoute('/dashboard/')).toBe(true);
|
||||
expect(isDashboardRoute('/')).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects other operator deck routes', () => {
|
||||
expect(isDashboardRoute('/crucible')).toBe(false);
|
||||
expect(isDashboardRoute('/forge')).toBe(false);
|
||||
expect(isDashboardRoute('/settings')).toBe(false);
|
||||
expect(isDashboardRoute('/pathtracer')).toBe(false);
|
||||
});
|
||||
});
|
||||
5
server/web/src/help/routeEffects.ts
Normal file
5
server/web/src/help/routeEffects.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/** True when pathname is Command Deck (`/dashboard` or `/`). */
|
||||
export function isDashboardRoute(pathname: string): boolean {
|
||||
const path = pathname.split('?')[0].replace(/\/$/, '') || '/';
|
||||
return path === '/dashboard' || path === '/';
|
||||
}
|
||||
13
server/web/src/help/terminalRenderCap.test.ts
Normal file
13
server/web/src/help/terminalRenderCap.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TERM_RENDER_CAP, visibleTerminalLines } from './terminalRenderCap';
|
||||
|
||||
describe('terminalRenderCap', () => {
|
||||
it('renders only the newest window at cap', () => {
|
||||
const lines = Array.from({ length: TERM_RENDER_CAP + 25 }, (_, i) => `line-${i}`);
|
||||
const { visible, truncated, hidden } = visibleTerminalLines(lines);
|
||||
expect(visible).toHaveLength(TERM_RENDER_CAP);
|
||||
expect(visible[0]).toBe('line-25');
|
||||
expect(truncated).toBe(true);
|
||||
expect(hidden).toBe(25);
|
||||
});
|
||||
});
|
||||
17
server/web/src/help/terminalRenderCap.ts
Normal file
17
server/web/src/help/terminalRenderCap.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/** Max terminal lines rendered in the DOM; full history stays in state for export. */
|
||||
export const TERM_RENDER_CAP = 400;
|
||||
|
||||
export function visibleTerminalLines<T>(lines: T[]): {
|
||||
visible: T[];
|
||||
truncated: boolean;
|
||||
hidden: number;
|
||||
} {
|
||||
if (lines.length <= TERM_RENDER_CAP) {
|
||||
return { visible: lines, truncated: false, hidden: 0 };
|
||||
}
|
||||
return {
|
||||
visible: lines.slice(-TERM_RENDER_CAP),
|
||||
truncated: true,
|
||||
hidden: lines.length - TERM_RENDER_CAP,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user