Files
harbor-net/docs/superpowers/plans/2026-05-24-vpn-proxy-client-route-console-redesign.md
Dmitriy Petrov a0f41baa36
All checks were successful
Build and Deploy Gateway / build-and-push (push) Successful in 14s
Build and Deploy Gateway / deploy (push) Successful in 1s
Simplify client proxy port handling
2026-06-04 10:24:33 +03:00

13 KiB

VPN Proxy Client Route Console Redesign 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: Replace the current macOS client overview with a route-first console that makes the active traffic path, local proxy address, selected mode, and next action obvious at a glance.

Architecture: Keep resolveClientRoute() as the single source of truth and keep ClientOverviewPage as the orchestrator. Split the screen into small presentational components inside src/web/components/ClientOverviewPage.jsx, then replace only the client-mode CSS block in src/web/styles.css so gateway and Windows work stay untouched.

Tech Stack: React 19, Vite, Node.js node:test, existing CSS variables, Open Design static HTML artifact.

Design Artifact: docs/design/open-design/vpn-proxy-route-console-redesign.html


Current Findings

  • src/web/components/ClientOverviewPage.jsx already has the right model: one overview screen, mutually exclusive Gateway, VPN, and Direct modes, and route state from resolveClientRoute().
  • src/web/styles.css makes the client screen visually separate, but it uses a dark blue-green palette that reads as a monitoring dashboard rather than a macOS setup tool.
  • The current status panel, route line, mode grid, and proxy panel have similar visual weight. The user must scan several boxes to answer the primary question: where does my traffic go right now?
  • Copyable proxy addresses sit in the side panel. They are useful, but they are visually separated from the route story.
  • The three mode buttons look like cards. They work, but they do not communicate that mode selection changes the middle segment of the route.

Target Design

Use a light, restrained operational UI for a normal macOS desktop context: a user has Docker running, a browser open, and is checking why an app uses a certain proxy path. The interface should feel closer to a compact network control console than a server dashboard.

The first viewport should show:

  • top status: service running, restart, apply route;
  • left mode rail: Gateway, Local VPN, Direct;
  • main route strip: Mac apps > local proxy > selected route > Internet;
  • right utility panel: copy proxy addresses, proxy port, recent activity;
  • settings below route: only the form for the selected mode.

File Structure

  • Modify src/web/components/ClientOverviewPage.jsx: reorganize render structure into route console subcomponents while preserving props and handlers.
  • Modify src/web/styles.css: replace .client-* layout styles from .client-mode .app-main through the final client media query.
  • Test test/web/client-route.test.js: extend route state coverage so UI changes do not hide incorrect mode/status combinations.
  • Keep docs/design/open-design/vpn-proxy-route-console-redesign.html: reference artifact for visual decisions.

Task 1: Lock Route Contract Before UI Changes

Files:

  • Modify: test/web/client-route.test.js

  • Step 1: Add tests for all user-visible route statuses

Add these cases to test/web/client-route.test.js:

test('resolves running local VPN route', () => {
  const route = resolveClientRoute({
    state: {
      singboxRunning: true,
      configExists: true,
      proxyPort: 8082,
      selectedTag: 'finland-02',
      clientSettings: { homeBypassEnabled: false, sharedProxyEnabled: false },
    },
    activeServer: { tag: 'finland-02' },
  });

  assert.equal(route.mode, 'vpn');
  assert.equal(route.status, 'connected');
  assert.equal(route.localProxy, '127.0.0.1:8082');
  assert.deepEqual(route.path, ['Mac apps', '127.0.0.1:8082', 'VPN finland-02', 'Internet']);
});

test('resolves gateway route when shared proxy is enabled', () => {
  const route = resolveClientRoute({
    state: {
      singboxRunning: true,
      configExists: true,
      proxyPort: 8082,
      clientSettings: {
        sharedProxyEnabled: true,
        sharedProxy: { host: '192.168.50.111', port: 8080 },
      },
    },
  });

  assert.equal(route.mode, 'gateway');
  assert.equal(route.status, 'connected');
  assert.equal(route.target, '192.168.50.111:8080');
  assert.deepEqual(route.path, ['Mac apps', '127.0.0.1:8082', 'Gateway 192.168.50.111:8080', 'Internet']);
});

test('resolves direct route when home bypass is enabled', () => {
  const route = resolveClientRoute({
    state: {
      singboxRunning: true,
      configExists: true,
      clientSettings: { homeBypassEnabled: true, sharedProxyEnabled: false, proxyPort: 8084 },
    },
  });

  assert.equal(route.mode, 'direct');
  assert.equal(route.status, 'connected');
  assert.equal(route.localProxy, '127.0.0.1:8084');
  assert.deepEqual(route.path, ['Mac apps', '127.0.0.1:8084', 'Direct', 'Internet']);
});
  • Step 2: Run the route tests

Run:

npm test -- test/web/client-route.test.js

Expected: all existing and new route tests pass.

  • Step 3: Commit
git add test/web/client-route.test.js
git commit -m "test: lock client route display contract"

Task 2: Restructure Client Overview Markup

Files:

  • Modify: src/web/components/ClientOverviewPage.jsx

  • Step 1: Replace the route line with route nodes

Replace RouteLine with:

function RouteStrip({ route }) {
  const nodes = [
    { label: 'Источник', value: route.path[0], detail: 'приложения Mac' },
    { label: 'Локальный proxy', value: route.localProxy, detail: 'HTTP и SOCKS5' },
    { label: 'Режим', value: route.target, detail: route.targetDetail, active: route.status === 'connected' },
    { label: 'Выход', value: 'Internet', detail: route.status === 'connected' ? 'маршрут активен' : 'ожидает запуска' },
  ];

  return (
    <div className="client-route-strip">
      {nodes.map((node) => (
        <div className={`client-route-node ${node.active ? 'active' : ''}`} key={node.label}>
          <small>{node.label}</small>
          <strong>{node.value}</strong>
          <span>{node.detail}</span>
        </div>
      ))}
    </div>
  );
}

function RoutePath({ route }) {
  return (
    <div className="client-route-path">
      {route.path.map((item, index) => (
        <React.Fragment key={`${item}-${index}`}>
          <strong>{item}</strong>
          {index < route.path.length - 1 && <span>{'>'}</span>}
        </React.Fragment>
      ))}
    </div>
  );
}
  • Step 2: Add a mode rail component

Add:

function ModeRail({ route, setupMode, clientSettings, state, busy, onGateway, onVpn, onDirect }) {
  const modes = [
    {
      id: 'gateway',
      title: 'Общий gateway',
      subtitle: clientSettings?.sharedProxy
        ? `${clientSettings.sharedProxy.host}:${clientSettings.sharedProxy.port}`
        : 'серверная proxy',
      onClick: onGateway,
    },
    {
      id: 'vpn',
      title: 'Локальный VPN',
      subtitle: state?.selectedTag || 'выбрать сервер',
      onClick: onVpn,
    },
    {
      id: 'direct',
      title: 'Напрямую',
      subtitle: 'без VPN',
      onClick: onDirect,
    },
  ];

  return (
    <aside className="client-mode-rail">
      <div className="client-section-label">Режим подключения</div>
      <div className="client-mode-list">
        {modes.map((mode) => (
          <button
            key={mode.id}
            type="button"
            className={`client-rail-mode ${setupMode === mode.id ? 'selected' : ''} ${route.mode === mode.id ? 'active' : ''}`}
            disabled={busy}
            onClick={mode.onClick}
          >
            <span className="client-mode-dot" />
            <span>
              <strong>{mode.title}</strong>
              <small>{mode.subtitle}</small>
            </span>
          </button>
        ))}
      </div>
    </aside>
  );
}
  • Step 3: Replace the top-level JSX

Use this layout in ClientOverviewPage:

return (
  <div className="client-console">
    <ModeRail
      route={route}
      setupMode={setupMode}
      clientSettings={clientSettings}
      state={state}
      busy={busy}
      onGateway={selectGateway}
      onVpn={selectVpn}
      onDirect={() => {
        setSetupMode('direct');
        enableDirect();
      }}
    />

    <section className="client-route-workspace">
      <StatusPanel route={route} state={state} />
      <RouteStrip route={route} />
      <RoutePath route={route} />

      <section className="client-mode-panel">
        {setupMode === 'gateway' && (
          <GatewaySettings settings={clientSettings} busy={busy} onCheck={onCheckSharedProxy} />
        )}
        {setupMode === 'vpn' && (
          <VpnSettings
            state={state}
            servers={servers}
            subscriptionUrl={subscriptionUrl}
            setSubscriptionUrl={setSubscriptionUrl}
            pendingTag={pendingTag}
            setPendingTag={setPendingTag}
            busy={busy}
            onFetchSubscription={onFetchSubscription}
            onApply={onApply}
          />
        )}
        {setupMode === 'direct' && <DirectSettings busy={busy} onEnable={enableDirect} />}
      </section>
    </section>

    <ProxySettings state={state} settings={clientSettings} busy={busy} onSave={onSaveClientSettings} />
  </div>
);
  • Step 4: Run build

Run:

npm run build

Expected: Vite build succeeds.

  • Step 5: Commit
git add src/web/components/ClientOverviewPage.jsx
git commit -m "refactor: reshape client overview around route console"

Task 3: Replace Client Visual System

Files:

  • Modify: src/web/styles.css

  • Step 1: Replace only the client CSS block

Replace the CSS from .client-mode .app-main through the client media query with the style direction from docs/design/open-design/vpn-proxy-route-console-redesign.html. Keep selectors scoped to .client-* so gateway screens keep the existing palette.

Use these token values for the client block:

.app-body.client-mode {
  grid-template-columns: 1fr;
  background: oklch(0.965 0.008 232);
}

.client-mode .topbar {
  background: oklch(0.978 0.007 232);
  border-bottom-color: oklch(0.835 0.018 232);
}

.client-mode .app-main {
  max-width: 1320px;
  width: 100%;
  margin: 0 auto;
  padding: 18px;
  color: oklch(0.238 0.028 238);
}

.client-console {
  min-height: calc(100vh - var(--topbar-h) - 36px);
  display: grid;
  grid-template-columns: 264px minmax(0, 1fr) 312px;
  overflow: hidden;
  background: oklch(0.986 0.006 232);
  border: 1px solid oklch(0.835 0.018 232);
  border-radius: 8px;
  box-shadow: 0 18px 42px oklch(0.36 0.035 238 / 0.13);
}
  • Step 2: Add responsive behavior

Add:

@media (max-width: 1080px) {
  .client-console {
    grid-template-columns: 220px minmax(0, 1fr);
  }

  .client-side-panel {
    grid-column: 1 / -1;
    border-left: 0;
    border-top: 1px solid oklch(0.835 0.018 232);
  }

  .client-route-strip {
    grid-template-columns: 1fr 1fr;
  }
}

@media (max-width: 760px) {
  .client-console,
  .client-route-strip,
  .client-inline-form,
  .client-port-row {
    grid-template-columns: 1fr;
  }

  .client-mode-rail {
    border-right: 0;
    border-bottom: 1px solid oklch(0.835 0.018 232);
  }
}
  • Step 3: Verify no banned patterns were introduced

Run:

rg -n "background-clip:\\s*text|border-left:\\s*[2-9]|border-right:\\s*[2-9]|backdrop-filter|letter-spacing:\\s*-" src/web/styles.css

Expected: no matches.

  • Step 4: Run build

Run:

npm run build

Expected: Vite build succeeds.

  • Step 5: Commit
git add src/web/styles.css
git commit -m "style: apply light route console client theme"

Task 4: Browser Verification

Files:

  • No file changes expected.

  • Step 1: Start the dev server

Run:

npm run dev -- --host 127.0.0.1 --port 4567

Expected: Vite listens on http://127.0.0.1:4567.

  • Step 2: Open client mode with representative state

Use the browser to open:

http://127.0.0.1:4567

Expected: the first viewport shows the mode rail, route strip, route path, selected-mode form, and copyable proxy addresses without overlap at desktop width.

  • Step 3: Check mobile width

Resize to 390px wide.

Expected: rail, route workspace, and proxy panel stack vertically; long proxy URLs truncate inside their containers; action buttons remain readable.

  • Step 4: Run final verification

Run:

npm test
npm run build
git diff --check

Expected: all commands pass.

  • Step 5: Commit
git add src/web/components/ClientOverviewPage.jsx src/web/styles.css test/web/client-route.test.js
git commit -m "feat: redesign client overview as route console"

Self-Review

Spec coverage:

  • Current UX assessment is captured in Current Findings.
  • New design direction is captured in Target Design.
  • Open Design artifact is referenced explicitly.
  • Implementation tasks cover route contract, markup, scoped CSS, and browser verification.

Placeholder scan:

  • No TBD, TODO, or unspecified validation steps remain.

Type consistency:

  • Route fields match resolveClientRoute(): mode, status, localProxy, target, targetDetail, path.