# 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`:

```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:

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

Expected: all existing and new route tests pass.

- [ ] **Step 3: Commit**

```bash
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:

```jsx
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:

```jsx
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`:

```jsx
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:

```bash
npm run build
```

Expected: Vite build succeeds.

- [ ] **Step 5: Commit**

```bash
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:

```css
.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:

```css
@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:

```bash
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:

```bash
npm run build
```

Expected: Vite build succeeds.

- [ ] **Step 5: Commit**

```bash
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:

```bash
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:

```text
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:

```bash
npm test
npm run build
git diff --check
```

Expected: all commands pass.

- [ ] **Step 5: Commit**

```bash
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`.
