# Fix InterfaceAddress Override Bug (wg/up + Edit Form)

## TL;DR

> **Quick Summary**: Two bugs prevent custom WireGuard server InterfaceAddress from sticking: (1) `wg/up` endpoint always recalculates from IPPoolCIDR instead of using stored DB value; (2) Server edit form always pre-fills `ipInput` as network+1 instead of showing stored InterfaceAddress.
>
> **Deliverables**:
> - `apps/server-core/api/wg.go` — use `wgServer.InterfaceAddress` from DB first, fallback to calc
> - `apps/dashboard-ui/src/views/Servers.vue` — use `srv.InterfaceAddress` for edit form pre-fill
>
> **Estimated Effort**: Small (2 files, ~10 lines changed)
> **Parallel Execution**: YES — 2 parallel tasks
> **Critical Path**: Task 1 → (build verification)

---

## Context

### Original Request
User reported: Interface Address (CIDR) for nodes cannot be changed from `.1` to `.4`. WireGuard itself has no such limitation — this is a NexusGuard bug.

### Root Cause
**Bug 1 — `api/wg.go:60-71`**: The `Up()` handler always recalculates `interfaceAddr` from `IPPoolCIDR` using `ip[3]++` (network+1). The stored `wgServer.InterfaceAddress` from the database is completely ignored — it's never read.

**Bug 2 — `Servers.vue:315-323`**: The `openEdit()` function always reconstructs `ipInput` as network+1 from `IPPoolCIDR`. Even though `srv.InterfaceAddress` is read at line 333, it's immediately overwritten by `parseIpInput()` at line 364 which resets it. The stored value is ignored.

### Data Flow (the bug path)
```
User sets InterfaceAddress = 10.172.21.4/24 → DB stores .4 ✅
                                                  │
                    ┌──────────────────────────────┤
                    │                              │
                    ▼                              ▼
           openEdit() (Servers.vue)         wg Up() (wg.go)
                    │                              │
           ipParts[3]++ = .1                ip[3]++ = .1
           (ignores srv.InterfaceAddress)   (ignores wgServer.InterfaceAddress)
                    │                              │
                    ▼                              ▼
              Shows .1 ❌                   Tunnel uses .1 ❌
```

### Metis Analysis
- Backend create/update handlers (`api/servers.go`) correctly store `InterfaceAddress` to DB — no changes needed there
- `parseIpInput()` in `Servers.vue` correctly computes `interfaceAddress` from `ipInput` — bug is what feeds it, not how it works
- IPAM (`internal/ipam/manager.go`) correctly queries `interface_address` from DB for peer allocation — no changes needed
- `calcInterfaceAddress()` in `servers.go` is used by fallback/create paths — no changes needed

---

## Work Objectives

### Core Objective
Fix custom InterfaceAddress to persist through wg/up and display correctly in the edit form.

### Concrete Deliverables
- `apps/server-core/api/wg.go:60-71` — use stored `wgServer.InterfaceAddress` first, fallback to calc from pool if empty
- `apps/dashboard-ui/src/views/Servers.vue:315-323` — use `srv.InterfaceAddress` for `ipInput` pre-fill, fallback to network+1 if empty

### Definition of Done
- [x] Setting InterfaceAddress to custom value via API → wg/up uses that value (not network+1)
- [x] Setting InterfaceAddress to custom value → edit form shows that value (not network+1)
- [x] Empty InterfaceAddress + IPPoolCIDR → fallback to network+1 still works
- [x] Malformed InterfaceAddress in DB → wg/up falls back to calc (doesn't crash)
- [x] `npm run build` passes

### Must Have
- Custom InterfaceAddress survives wg/up call
- Edit form displays stored InterfaceAddress

### Must NOT Have (Guardrails)
- Do NOT touch `parseIpInput()` in Servers.vue (lines 360-425) — it works correctly
- Do NOT touch create/update handlers in `api/servers.go` — they correctly store InterfaceAddress
- Do NOT touch IPAM (`internal/ipam/manager.go`) — it correctly excludes InterfaceAddress from peer allocation
- Do NOT touch `calcInterfaceAddress` in `api/servers.go`
- Do NOT touch wg_test.go — existing tests cover only the fallback path
- Do NOT refactor the unified `ipInput` → `ipPoolCidr` + `interfaceAddress` form pattern

---

## Verification Strategy

> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.

### Test Decision
- **Infrastructure exists**: No test suite for this specific path
- **Automated tests**: None for custom InterfaceAddress path
- **Primary verification**: `npm run build` + `go build ./...` + grep assertions

### QA Policy
Every task MUST include agent-executed QA scenarios.
- **Backend**: Build check + grep verification
- **Frontend**: Build check
- **Evidence**: `.sisyphus/evidence/task-{N}-{scenario-slug}.txt`

---

## Execution Strategy

### Parallel Execution Waves

```
Wave 1 (Start immediately — parallel):
├── Task 1: Fix api/wg.go — use stored InterfaceAddress [quick]
├── Task 2: Fix Servers.vue — use stored InterfaceAddress for pre-fill [quick]

Wave FINAL:
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality + build check (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
├── Task F4: Scope fidelity check (deep)
```

### Agent Dispatch Summary
- **Wave 1**: 2 parallel tasks
- **FINAL**: 4 parallel reviews

---

## TODOs

- [x] 1. Fix `api/wg.go` — use stored InterfaceAddress for wg/up

  **What to do**:
  - In `apps/server-core/api/wg.go` lines 60-71:
    - Change `interfaceAddr := ""` to `interfaceAddr := wgServer.InterfaceAddress`
    - Change the `if` condition from `if wgServer.IPPoolCIDR != ""` to `if interfaceAddr == "" && wgServer.IPPoolCIDR != ""`
    - This way: stored value wins; if empty, fall back to pool calculation

  **Current code block (lines 60-71)**:
  ```go
  interfaceAddr := ""
  if wgServer.IPPoolCIDR != "" {
      if ip, ipnet, err := net.ParseCIDR(wgServer.IPPoolCIDR); err == nil {
          ip4 := ip.To4()
          if ip4 != nil {
              ip4[3]++
              if ones, _ := ipnet.Mask.Size(); ones > 0 {
                  interfaceAddr = fmt.Sprintf("%s/%d", ip4.String(), ones)
              }
          }
      }
  }
  ```

  **Changed to**:
  ```go
  interfaceAddr := wgServer.InterfaceAddress
  if interfaceAddr == "" && wgServer.IPPoolCIDR != "" {
      if ip, ipnet, err := net.ParseCIDR(wgServer.IPPoolCIDR); err == nil {
          ip4 := ip.To4()
          if ip4 != nil {
              ip4[3]++
              if ones, _ := ipnet.Mask.Size(); ones > 0 {
                  interfaceAddr = fmt.Sprintf("%s/%d", ip4.String(), ones)
              }
          }
      }
  }
  ```

  - Verify with `go build ./...`

  **Must NOT do**:
  - Do NOT touch `calcInterfaceAddress` in `api/servers.go`
  - Do NOT touch create/update handlers in `api/servers.go`
  - Do NOT touch IPAM or wgmanager

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Single file, 6-line change, minimal logic
  - **Skills**: none needed

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 1 (with Task 2)
  - **Blocks**: F1-F4
  - **Blocked By**: None

  **References**:
  - `apps/server-core/api/wg.go:55-78` — context: wgServer variable is already loaded from DB, so `wgServer.InterfaceAddress` is available
  - `apps/server-core/api/wg.go:60-71` — the exact lines to change

  **WHY**:
  - The stored `InterfaceAddress` is the user's explicit choice. The pool calculation was always meant to be a fallback for empty values.
  - No CIDR validation needed — if stored value is malformed, `ip addr add` will fail which is acceptable (the DB should have valid data)

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

  ```
  Scenario: Verify code uses stored InterfaceAddress
    Tool: Bash (grep)
    Preconditions: Codebase clean
    Steps:
      1. `Select-String -Path "apps/server-core/api/wg.go" -Pattern 'interfaceAddr := wgServer.InterfaceAddress'`
    Expected Result: Match found — the new code is in place
    Evidence: .sisyphus/evidence/task-1-code-check.txt

  Scenario: Verify fallback still exists
    Tool: Bash (grep)
    Steps:
      1. `Select-String -Path "apps/server-core/api/wg.go" -Pattern 'interfaceAddr == "" &&'`
    Expected Result: Match found — fallback to pool calc still works
    Evidence: .sisyphus/evidence/task-1-fallback.txt

  Scenario: Build passes
    Tool: Bash
    Steps:
      1. `cd apps/server-core && go build ./...`
    Expected Result: Exit 0
    Evidence: .sisyphus/evidence/task-1-build.txt
  ```

  **Evidence to Capture:**
  - [ ] `.sisyphus/evidence/task-1-code-check.txt`
  - [ ] `.sisyphus/evidence/task-1-fallback.txt`
  - [ ] `.sisyphus/evidence/task-1-build.txt`

  **Commit**: YES (with Task 2)
  - Message: `fix(api): respect stored InterfaceAddress in wg/up instead of always recalculating`
  - Files: `apps/server-core/api/wg.go`

- [x] 2. Fix `Servers.vue` — use stored InterfaceAddress for edit form pre-fill

  **What to do**:
  - In `apps/dashboard-ui/src/views/Servers.vue` lines 315-323, change `openEdit()` to use `srv.InterfaceAddress` first, fallback to pool network+1

  **Current code block (lines 315-323)**:
  ```ts
  let ipInput = ''
  if (srv.IPPoolCIDR) {
      const parts = srv.IPPoolCIDR.split('/')
      const poolPrefix = parts[1] || ''
      const poolIp = parts[0]
      const ipParts = poolIp.split('.').map(Number)
      ipParts[3]++
      ipInput = poolPrefix ? `${ipParts.join(".")}/${poolPrefix}` : ''
  }
  ```

  **Changed to**:
  ```ts
  let ipInput = ''
  if (srv.InterfaceAddress) {
      ipInput = srv.InterfaceAddress
  } else if (srv.IPPoolCIDR) {
      const parts = srv.IPPoolCIDR.split('/')
      const poolPrefix = parts[1] || ''
      const poolIp = parts[0]
      const ipParts = poolIp.split('.').map(Number)
      ipParts[3]++
      ipInput = poolPrefix ? `${ipParts.join(".")}/${poolPrefix}` : ''
  }
  ```

  - **Critical note**: Line 333 (`interfaceAddress: srv.InterfaceAddress || ''`) will be overwritten by `parseIpInput` at line 364 (which resets it to `''`) and then line 420 (which sets it from `ipInput`). This is correct behavior — the stored value feeds `ipInput`, `parseIpInput` derives everything from `ipInput`.
  - Verify with `npm run build`

  **Must NOT do**:
  - Do NOT touch `parseIpInput()` (lines 360-425) — works correctly
  - Do NOT change line 333 (`interfaceAddress: srv.InterfaceAddress || ''`) — it's overwritten by parseIpInput, harmless
  - Do NOT change create flow — only edit flow

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Single file, 5-line change, straightforward
  - **Skills**: none needed

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 1 (with Task 1)
  - **Blocks**: F1-F4
  - **Blocked By**: None

  **References**:
  - `apps/dashboard-ui/src/views/Servers.vue:315-323` — exact lines to change
  - `apps/dashboard-ui/src/views/Servers.vue:360-425` — `parseIpInput` function (read-only reference)

  **WHY**:
  - `parseIpInput` derives `ipPoolCidr` and `interfaceAddress` from `ipInput`. Pre-filling `ipInput` with the stored value makes it flow correctly through the existing logic.

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

  ```
  Scenario: Verify code uses stored InterfaceAddress
    Tool: Bash (grep)
    Steps:
      1. `Select-String -Path "apps/dashboard-ui/src/views/Servers.vue" -Pattern "srv.InterfaceAddress"`
    Expected Result: Match at line ~315 (before the else if for IPPoolCIDR)
    Evidence: .sisyphus/evidence/task-2-code-check.txt

  Scenario: Verify fallback still exists
    Tool: Bash (grep)
    Steps:
      1. `Select-String -Path "apps/dashboard-ui/src/views/Servers.vue" -Pattern "ipParts\[3\]\+\+" -SimpleMatch`
    Expected Result: Match found — fallback to network+1 still works
    Evidence: .sisyphus/evidence/task-2-fallback.txt

  Scenario: Build passes
    Tool: Bash
    Steps:
      1. `cd apps/dashboard-ui && npm run build`
    Expected Result: Exit 0
    Evidence: .sisyphus/evidence/task-2-build.txt
  ```

  **Evidence to Capture:**
  - [ ] `.sisyphus/evidence/task-2-code-check.txt`
  - [ ] `.sisyphus/evidence/task-2-fallback.txt`
  - [ ] `.sisyphus/evidence/task-2-build.txt`

  **Commit**: YES (with Task 1)
  - Message: `fix(ui): use stored InterfaceAddress in edit form instead of always reconstructing`
  - Files: `apps/dashboard-ui/src/views/Servers.vue`

---

## Final Verification Wave

- [x] F1. **Plan Compliance Audit** — `oracle`
  Read the plan end-to-end. For each Must Have: verify implementation exists. For each Must NOT Have: search codebase for forbidden patterns.
  Output: `VERDICT: APPROVE/REJECT`

- [x] F2. **Code Quality Review** — `unspecified-high`
  Run `npm run build` and `go build ./...`. Check for AI slop.
  Output: `Build [PASS/FAIL] | VERDICT`

- [x] F3. **Real Manual QA** — `unspecified-high`
  Verify both fix scenarios. No integration testing — these are compile-time/logic fixes.
  Output: `Scenarios [N/N pass] | VERDICT`

- [x] F4. **Scope Fidelity Check** — `deep`
  For each task: read "What to do", read actual diff. No scope creep.
  Output: `Tasks [N/N compliant] | VERDICT`

---

## Commit Strategy

- **1+2**: `fix: respect stored InterfaceAddress in wg/up and edit form`
  - `apps/server-core/api/wg.go`
  - `apps/dashboard-ui/src/views/Servers.vue`

---

## Success Criteria

### Verification Commands
```bash
cd apps/server-core && go build ./...     # Backend builds
cd apps/dashboard-ui && npm run build     # Frontend builds
```

### Final Checklist
- [x] All "Must Have" present
- [x] All "Must NOT Have" absent
- [x] All builds pass
