chore: archive old plans, add new plan docs, update submodules
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user