Files
multi-simadmin/docs/superpowers/plans/2026-07-30-settings-navigation-stream-performance.md

17 KiB

Settings Navigation And Stream Performance Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make top-level settings security-only, switch internal pages without document reloads, and let the browser observe an idle SSE connection immediately.

Architecture: Keep the existing pathname-based AppShell and add one focused browser-navigation Hook that owns pathname state, eligible link interception, history traversal, focus, and scroll restoration. Keep the instance editor route intact while turning the legacy top-level instance-settings route into the existing redirect route shape. Preserve streaming proxy behavior and explicitly flush downstream headers as soon as upstream headers arrive.

Tech Stack: React 19, TypeScript 5.9, Testing Library, Vitest/jsdom, Node HTTP, Chrome DevTools Protocol browser E2E

Global Constraints

  • Do not add a third-party router.
  • Top-level settings contains only password protection and HTTP/HTTPS security information.
  • /settings/instances resolves as a redirect to /settings/system.
  • /settings/instances/:instanceId remains available for instance editing from node details.
  • Eligible same-origin links use history.pushState; external, download, target, cross-origin, and modified-click links retain browser behavior.
  • Browser back and forward remain functional through popstate.
  • Page changes focus #main-content and scroll to the top.
  • AppShell remains mounted across top-navigation changes so the event subscription is not recreated.
  • SSE remains streaming and authenticated; do not replace it with polling or conceal genuine reconnect states.
  • Production code changes must follow an observed failing test.
  • Do not push the resulting commits unless the user explicitly requests it.

File Structure

  • apps/web/src/app-shell.tsx: route ownership and the three-entry global navigation; no browser event ownership.
  • apps/web/src/app-shell.integration.test.tsx: route, navigation, security-only settings, and instance-editor regression coverage.
  • apps/web/src/navigation/use-browser-pathname.ts: browser pathname state and document-level navigation interception.
  • apps/web/src/navigation/use-browser-pathname.test.tsx: jsdom behavior contract for same-page navigation.
  • apps/web/src/main.tsx: thin browser root that supplies the Hook pathname to the persistent AppShell.
  • apps/web/src/styles.css: remove now-unused top-level settings sub-navigation styles.
  • apps/api/src/canary-gateway.ts: downstream proxy header forwarding.
  • apps/api/src/canary-gateway.test.ts: idle SSE header-flush regression.
  • scripts/real-browser-e2e.mjs: browser-level assertions for navigation persistence, request volume, focus, and live status.

Task 1: Security-Only Top-Level Settings Route

Files:

  • Modify: apps/web/src/app-shell.integration.test.tsx
  • Modify: apps/web/src/app-shell.tsx
  • Modify: apps/web/src/styles.css

Interfaces:

  • Consumes: existing resolveRoute(input: string): ResolvedRoute and AppShellProps.

  • Produces: resolveRoute('/settings/instances') with { kind: 'redirect', to: '/settings/system' }; global settings link href="/settings/system".

  • Step 1: Write failing route and rendering tests

Add an explicit resolveRoute import and assertions to app-shell.integration.test.tsx:

expect(resolveRoute('/settings/instances')).toMatchObject({
  kind: 'redirect',
  to: '/settings/system',
});
expect(resolveRoute('/settings/instances/bravo')).toMatchObject({
  kind: 'settings-instance-detail',
  params: { instanceId: 'bravo' },
});

Update the global-workspace test so settings points directly to /settings/system, the system settings content renders, and there is no top-level settings sub-navigation:

expect(within(navigation).getByRole('link', { name: '设置' })).toHaveAttribute(
  'href',
  '/settings/system',
);
rerender(<AppShell pathname="/settings/system" eventStreamClient={quietEventStreamClient} />);
expect(screen.getByRole('heading', { name: '系统与安全' })).toBeTruthy();
expect(screen.queryByRole('navigation', { name: '设置导航' })).toBeNull();

Render /settings/instances with a spying Fleet source and assert it renders security settings without loading Fleet:

const load = vi.fn().mockResolvedValue(snapshot);
render(
  <AppShell
    pathname="/settings/instances"
    fleetDataSource={source(load)}
    eventStreamClient={quietEventStreamClient}
  />,
);
expect(screen.getByRole('heading', { name: '系统与安全' })).toBeTruthy();
expect(load).not.toHaveBeenCalled();
  • Step 2: Run the focused test and verify failure

Run: corepack pnpm exec vitest run apps/web/src/app-shell.integration.test.tsx

Expected: FAIL because settings still links to /settings/instances, renders the settings sub-navigation, and owns the Fleet-backed instance settings page.

  • Step 3: Implement the minimal settings route change

In resolveRoute, remove /settings/instances from staticRoutes and return the redirect before dynamic instance-edit matching:

if (pathname === '/settings/instances')
  return { kind: 'redirect', pathname, to: '/settings/system' };

Change the global item and remove the obsolete page branch and import:

{ section: 'settings', href: '/settings/system', label: '设置', icon: 'settings' }

Delete the conditional <nav className="settings-navigation"> block. Remove both .settings-navigation rule groups from styles.css; they no longer have a consumer.

  • Step 4: Run the focused test and verify success

Run: corepack pnpm exec vitest run apps/web/src/app-shell.integration.test.tsx

Expected: PASS, including the preserved /settings/instances/bravo editor route.

  • Step 5: Commit the settings slice
git add apps/web/src/app-shell.tsx apps/web/src/app-shell.integration.test.tsx apps/web/src/styles.css
git commit -m "fix: simplify top-level settings"

Task 2: Same-Page Browser Pathname Hook

Files:

  • Create: apps/web/src/navigation/use-browser-pathname.ts
  • Create: apps/web/src/navigation/use-browser-pathname.test.tsx
  • Modify: apps/web/src/main.tsx

Interfaces:

  • Consumes: browser window.location, window.history, document click events, and popstate.

  • Produces: useBrowserPathname(): string.

  • Step 1: Write failing Hook tests

Create a jsdom test harness:

// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useBrowserPathname } from './use-browser-pathname.js';

function Harness() {
  const pathname = useBrowserPathname();
  return (
    <>
      <a href="/automation">Automation</a>
      <main id="main-content" tabIndex={-1}>{pathname}</main>
    </>
  );
}

afterEach(() => {
  cleanup();
  window.history.replaceState(null, '', '/');
  vi.restoreAllMocks();
});

Cover these exact contracts:

it('pushes eligible same-origin links and updates pathname without reloading', () => {
  window.history.replaceState(null, '', '/fleet');
  const pushState = vi.spyOn(window.history, 'pushState');
  const scrollTo = vi.spyOn(window, 'scrollTo').mockImplementation(() => undefined);
  render(<Harness />);
  fireEvent.click(screen.getByRole('link', { name: 'Automation' }));
  expect(pushState).toHaveBeenCalledWith(null, '', '/automation');
  expect(screen.getByRole('main')).toHaveTextContent('/automation');
  expect(screen.getByRole('main')).toHaveFocus();
  expect(scrollTo).toHaveBeenCalledWith({ top: 0, left: 0, behavior: 'auto' });
});

Add a popstate test that replaces history with /fleet, dispatches new PopStateEvent('popstate'), and expects the rendered pathname to update. Add parameterized anchor tests proving no interception for target="_blank", download, an external origin, hash-only navigation, button: 1, and each of metaKey, ctrlKey, shiftKey, and altKey.

  • Step 2: Run Hook tests and verify failure

Run: corepack pnpm exec vitest run apps/web/src/navigation/use-browser-pathname.test.tsx

Expected: FAIL because use-browser-pathname.ts does not exist.

  • Step 3: Implement the minimal Hook

Implement a React Hook with this public shape:

export function useBrowserPathname(): string

The effect must register one document click listener and one window popstate listener, and clean up both. The click listener must locate event.target.closest('a[href]'), reject prevented/non-primary/modified clicks, reject download and non-_self targets, parse with new URL(anchor.href, window.location.href), require the current origin, reject same-path hash-only changes, then call:

event.preventDefault();
window.history.pushState(null, '', `${url.pathname}${url.search}${url.hash}`);
setPathname(window.location.pathname);
window.scrollTo({ top: 0, left: 0, behavior: 'auto' });
document.querySelector<HTMLElement>('#main-content')?.focus({ preventScroll: true });

The popstate listener updates state from window.location.pathname, scrolls, and focuses using the same local helper.

  • Step 4: Run Hook tests and verify success

Run: corepack pnpm exec vitest run apps/web/src/navigation/use-browser-pathname.test.tsx

Expected: PASS for interception, history traversal, exclusions, focus, and scroll.

  • Step 5: Keep the AppShell mounted in main.tsx

Add a thin component and pass its reactive pathname to the existing shell:

function BrowserApp() {
  const pathname = useBrowserPathname();
  return (
    <ConsoleAuthGate>
      <AppShell pathname={pathname} />
    </ConsoleAuthGate>
  );
}

createRoot(root).render(
  <StrictMode>
    <BrowserApp />
  </StrictMode>,
);

This changes only AppShell props during navigation; it does not recreate the root or shell component.

  • Step 6: Run all Web unit tests

Run: corepack pnpm --filter @multi-simadmin/web test

Expected: PASS.

  • Step 7: Commit the navigation slice
git add apps/web/src/navigation/use-browser-pathname.ts apps/web/src/navigation/use-browser-pathname.test.tsx apps/web/src/main.tsx
git commit -m "feat: navigate workspaces without page reloads"

Task 3: Flush Idle SSE Headers Through The Canary Gateway

Files:

  • Modify: apps/api/src/canary-gateway.test.ts
  • Modify: apps/api/src/canary-gateway.ts

Interfaces:

  • Consumes: upstream Node IncomingMessage headers and downstream ServerResponse.

  • Produces: downstream response headers observable before the first upstream body byte.

  • Step 1: Write the failing idle-stream regression test

Add a test whose upstream sends and flushes headers but deliberately sends no body:

it('flushes SSE response headers before the first upstream event', async () => {
  const upstream = createServer((_request, response) => {
    response.writeHead(200, {
      'content-type': 'text/event-stream',
      'cache-control': 'no-cache',
    });
    response.flushHeaders();
  });
  servers.push(upstream);
  const gateway = await startGateway(await fixtureDist(), await listen(upstream));

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 500);
  try {
    const response = await fetch(`${gateway.origin}/api/v1/events`, {
      signal: controller.signal,
    });
    expect(response.status).toBe(200);
    expect(response.headers.get('content-type')).toBe('text/event-stream');
    await response.body?.cancel();
  } finally {
    clearTimeout(timeout);
  }
});
  • Step 2: Run the gateway test and verify failure

Run: corepack pnpm exec vitest run apps/api/src/canary-gateway.test.ts -t "flushes SSE response headers"

Expected: FAIL by abort/timeout because the gateway has not committed downstream headers.

  • Step 3: Flush downstream headers after forwarding upstream headers

Immediately after the existing response.writeHead(...) in proxyRequest, add:

response.flushHeaders();

Keep the existing pipeline(upstreamResponse, response) unchanged.

  • Step 4: Run gateway tests and verify success

Run: corepack pnpm exec vitest run apps/api/src/canary-gateway.test.ts

Expected: PASS for both the idle-header and multi-chunk streaming cases.

  • Step 5: Commit the gateway slice
git add apps/api/src/canary-gateway.ts apps/api/src/canary-gateway.test.ts
git commit -m "fix: flush proxied event stream headers"

Task 4: Real Browser Navigation And Connection Regression

Files:

  • Modify: scripts/real-browser-e2e.mjs

Interfaces:

  • Consumes: built Web assets, isolated fixture server or E2E_ORIGIN, Chrome DevTools Protocol.

  • Produces: end-to-end proof that navigation is reload-free, does not recreate SSE, does not load Fleet from settings, and reaches the connected status.

  • Step 1: Add instrumentation and failing browser assertions

Change the fixture event endpoint to return an open SSE response:

if (pathname === '/api/v1/events') {
  response.writeHead(200, {
    'content-type': 'text/event-stream',
    'cache-control': 'no-cache',
  });
  response.flushHeaders();
  return;
}

Record request pathnames in the isolated server and install a page-lifetime marker before navigating:

await evaluate(cdp, `window.__navigationLifetime = crypto.randomUUID()`);
const lifetime = await evaluate(cdp, `window.__navigationLifetime`);

After keyboard navigation from Fleet to settings, assert:

assert.equal(await evaluate(cdp, `window.__navigationLifetime`), lifetime);
assert.equal(await evaluate(cdp, `location.pathname`), '/settings/system');
assert.equal(
  await evaluate(cdp, `document.activeElement?.id`),
  'main-content',
  'Settings navigation must focus the main content',
);

Use the recorded request counts before and after navigation to assert that /api/v1/events remains at one request and that settings navigation adds no /api/v1/instances or /resources request. Wait until .connection-status [data-state="open"] exists before asserting.

  • Step 2: Build and run browser E2E to expose remaining failures

Run: corepack pnpm run test:e2e:browser

Expected before all preceding tasks are applied: FAIL on the old settings path, lifetime marker, event request count, or open connection status. Expected after Tasks 1-3: PASS.

  • Step 3: Update existing E2E route expectations and diagnostics

Replace both settings navigation expectations from /settings/instances and heading 实例 to /settings/system and the system/security heading. Update the final PASS line to name same-page navigation and live SSE status. Keep the existing responsive Fleet and Automation assertions unchanged.

  • Step 4: Re-run browser E2E

Run: corepack pnpm run test:e2e:browser

Expected: PASS in Chrome/Edge with one event subscription, no document reload, no Fleet requests caused by settings, focused main content, and connected stream status.

  • Step 5: Commit the browser regression
git add scripts/real-browser-e2e.mjs
git commit -m "test: cover persistent browser navigation"

Task 5: Full Verification And Local Runtime

Files:

  • Verify only; modify a file only if a failing check demonstrates a defect in the scoped implementation.

Interfaces:

  • Consumes: all prior task outputs.

  • Produces: a verified release candidate at http://127.0.0.1:8789/.

  • Step 1: Run unit and integration tests

Run: corepack pnpm test

Expected: PASS.

  • Step 2: Run static checks

Run: corepack pnpm typecheck

Expected: PASS.

Run: corepack pnpm lint

Expected: PASS.

Run: corepack pnpm format:check

Expected: PASS.

  • Step 3: Run the production Web build and real browser suite

Run: corepack pnpm --filter @multi-simadmin/web build

Expected: PASS and emit apps/web/dist.

Run: corepack pnpm run test:e2e:browser

Expected: PASS.

  • Step 4: Restart the local production services using the repository's existing runtime commands

First inspect the current listeners and repository runtime documentation; stop only the exact API/gateway processes belonging to this workspace. Start the API on 127.0.0.1:8790 and the gateway on 127.0.0.1:8789 with the existing configured environment and hidden background windows.

  • Step 5: Verify the live gateway

Run:

curl.exe -fsS http://127.0.0.1:8789/readyz
curl.exe -sS -N --max-time 2 -D - http://127.0.0.1:8789/api/v1/events

Expected: readiness succeeds; the event request prints HTTP/1.1 200 OK and content-type: text/event-stream before curl times out waiting for an event body.

  • Step 6: Inspect final Git state

Run: git status --short --branch

Expected: branch contains only the planned commits and is ahead of origin/main; do not push.


Self-Review

  • Spec coverage: Tasks 1-5 cover security-only settings, legacy redirect semantics, retained instance editing, persistent same-page navigation, link exclusions, history traversal, focus/scroll behavior, idle SSE header delivery, genuine stream state, request-volume regression, and local 8789 verification.
  • Placeholder scan: no deferred implementation or unspecified error-handling steps remain.
  • Type consistency: useBrowserPathname(): string, existing ResolvedRoute, and existing EventStreamClient contracts are used consistently across tasks.