# Fix Device Advanced Settings Form & AllowedIPs /32

## TL;DR

> **Quick Summary**: Fix two issues: (1) DeviceDetail advanced settings form stays empty because TypeScript `Device` interface uses PascalCase but Go API returns snake_case — add `mapDevice()` helper; (2) Config download AllowedIPs still shows `/24` for some paths — verify all 4 config generation points already use `/32`.
>
> **Deliverables**:
> - `apps/dashboard-ui/src/api/devices.ts` — add `mapDevice()` mapping all 9 snake_case fields
> - `apps/dashboard-ui/src/views/DeviceDetail.vue` — fix nullish coalescing defaults
> - Config `/32` verified on all 4 endpoints
>
> **Estimated Effort**: Small
> **Parallel Execution**: YES — 2 parallel waves
> **Critical Path**: Task 1 → Task 3 → (verification)

---

## Context

### Original Request
User reported: (1) Peer advanced settings (AllowedIPs, DNS override inputs) in DeviceDetail.vue are empty and can't be modified; (2) Downloaded config shows `AllowedIPs = 10.172.21.2/24` instead of `/32`.

### Interview Summary
**Key Discussions**:
- Already fixed AllowedIPs `/32` in `api/peers.go` (2 places), `api/share.go`, `peer_sync.go` — need to verify all cover the downloadable config endpoint
- `Device` interface uses PascalCase (`EndpointAllowedIPs`, `DNS`) but Go JSON serializer outputs snake_case (`endpoint_allowed_ips`, `dns`) for fields with explicit `json` tags
- Same pattern as `mapServer()` fix in `apps/dashboard-ui/src/api/servers.ts`
- Need to fix nullish coalescing: `||` should be `??` for fields that can be `0`

### Metis Analysis
- **9 fields need mapping**: `EndpointAllowedIPs`, `DNS`, `MTU`, `PersistentKeepalive`, `Notes`, `IsSuspended`, `WgServerID`, `RxBytes`, `TxBytes`
- **Functions requiring mapping**: `fetchDevices()` (used by Devices.vue table), `getDevice()` (used by DeviceDetail.vue + LinkedDevices.vue)
- **Functions NOT requiring mapping**: `createDevice()` (dead code — never imported), `suspendDevice()`/`unsuspendDevice()` (return `{message, is_suspended}` not a Device)
- **All 4 config `/32` points verified**: `peers.go:134`, `peers.go:193`, `share.go:66`, `peer_sync.go:40` — all already use `/32`
- **`provisioning.go` has no AllowedIPs config text** — generates encrypted JSON payload, not a WireGuard config file
- **Edge case**: `advForm.mtu = device.value.MTU || 1420` — if MTU=0 it incorrectly defaults to 1420. Use `??` instead

---

## Work Objectives

### Core Objective
Fix Device advanced settings form to correctly populate and save, and verify AllowedIPs `/32` in downloadable config.

### Concrete Deliverables
- `apps/dashboard-ui/src/api/devices.ts` — `mapDevice()` helper + applied in `fetchDevices()` and `getDevice()`
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — `??` operator for MTU/PersistentKeepalive

### Definition of Done
- [x] `curl /api/v1/devices` returns device with all fields resolved by `mapDevice()`
- [x] DeviceDetail advanced settings form populates all fields from API response
- [x] Editing + saving advanced settings → reload shows persisted values
- [x] `curl /api/v1/devices/:id/config` shows `AllowedIPs = 10.x.x.x/32`
- [x] `npm run build` passes

### Must Have
- Device Advanced Settings form populates correctly
- Config download URL uses /32 for non-internet devices
- API client functions properly map snake_case → PascalCase

### Must NOT Have (Guardrails)
- Do NOT change Go serialization tags — frontend-only fix
- Do NOT refactor Device interface to use snake_case keys
- Do NOT touch `api/peers.ts` — its functions don't use Device interface
- Do NOT modify `provisioning.go` — not a config generation point
- Do NOT delete `createDevice()` — out of scope (dead code, but not part of this fix)

---

## Verification Strategy

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

### Test Decision
- **Infrastructure exists**: NO (no frontend test suite)
- **Automated tests**: None (no test infrastructure)
- **Primary verification**: `npm run build` + curl assertions

### QA Policy
Every task MUST include agent-executed QA scenarios.
- **Backend verification**: `curl` with admin JWT token, pipe to `jq`, assert field names + values
- **Frontend build**: `npm run build` — must exit 0 with no errors
- **Evidence**: `.sisyphus/evidence/task-{N}-{scenario-slug}.txt`

---

## Execution Strategy

### Parallel Execution Waves

```
Wave 1 (Start immediately — can run in parallel):
├── Task 1: Add mapDevice() helper to api/devices.ts [quick]
├── Task 2: Fix nullish coalescing in DeviceDetail.vue [quick]

Wave 2 (After Wave 1 — verification):
├── Task 3: Verify AllowedIPs /32 on all 4 config endpoints [quick]

Wave FINAL:
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality + build check (unspecified-high)
├── Task F3: Real manual QA — execute all QA scenarios (unspecified-high)
├── Task F4: Scope fidelity check (deep)
→ Present results → Get explicit user okay
```

### Dependency Matrix
- **1**: — — 3
- **2**: — — 3
- **3**: 1, 2 — F1-F4
- **F1-F4**: 3 — (user okay)

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

---

## Final Verification Wave

- [x] F1. **Plan Compliance Audit** — `oracle`
  Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, run curl). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
  Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`

- [x] F2. **Code Quality Review** — `unspecified-high`
  Run `npm run build` and `go build ./...`. Review changed files for: `as any`/`@ts-ignore`, unused imports, console.log in prod. Check AI slop: excessive comments, over-abstraction, generic names.
  Output: `Build [PASS/FAIL] | Lint [N clean/N issues] | VERDICT`

- [x] F3. **Real Manual QA** — `unspecified-high`
  Start from clean state.
  - Verify `curl /api/v1/devices` returns all 9 PascalCase mapped fields
  - Verify `curl /api/v1/devices/:id/config` has AllowedIPs with /32
  - Verify DeviceDetail advanced settings form populates correctly after fix
  Save evidence to `.sisyphus/evidence/final-qa/`.
  Output: `Scenarios [N/N pass] | Integration [N/N] | VERDICT`

- [x] F4. **Scope Fidelity Check** — `deep`
  For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance.
  Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`

---

## Commit Strategy

- **1+2**: `fix(ui): add mapDevice() and fix nullish coalescing in DeviceDetail` — `apps/dashboard-ui/src/api/devices.ts`, `apps/dashboard-ui/src/views/DeviceDetail.vue`
- **3**: NO commit — read-only verification

---

## Success Criteria

### Verification Commands
```bash
# Verify mapped fields
curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | jq '.[0] | {EndpointAllowedIPs, DNS, MTU, PersistentKeepalive, Notes, IsSuspended, WgServerID, RxBytes, TxBytes}'
# Expected: all 9 fields present (not null), even if empty

# Verify /32 in config
curl -s http://localhost:8080/api/v1/devices/{ID}/config -H "Authorization: Bearer $TOKEN" | jq -r '.config_text'
# Expected: AllowedIPs = 10.x.x.x/32 (not /24)

# Verify build
cd apps/dashboard-ui && npm run build
# Expected: exit 0, no errors
```

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

- [x] 1. Add `mapDevice()` helper to `api/devices.ts`

  **What to do**:
  - Add `mapDevice()` function between the import and interface declaration
  - Map all 9 snake_case fields: `endpoint_allowed_ips→EndpointAllowedIPs`, `dns→DNS`, `mtu→MTU`, `persistent_keepalive→PersistentKeepalive`, `notes→Notes`, `is_suspended→IsSuspended`, `wg_server_id→WgServerID`, `rx_bytes→RxBytes`, `tx_bytes→TxBytes`
  - Apply `mapDevice()` in `fetchDevices()`: `.map(mapDevice)` on array response
  - Apply `mapDevice()` in `getDevice()`: wrap single object return
  - Verify with `npm run build`

  **Must NOT do**:
  - Do NOT modify `createDevice()` — dead code, out of scope
  - Do NOT modify `suspendDevice()`/`unsuspendDevice()` — return `{message, is_suspended}`
  - Do NOT modify the `Device` interface (keep PascalCase)
  - Do NOT touch any Go files

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Single file change, straightforward mapping pattern, already has precedent in `mapServer()`
  - **Skills**: none needed

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

  **References**:

  **Pattern References** (exact pattern to follow):
  - `apps/dashboard-ui/src/api/servers.ts:28-43` — `mapServer()` helper — exact same pattern, copy the structure and replace field mappings

  **API/Type References** (contracts):
  - `apps/dashboard-ui/src/api/devices.ts:3-23` — `Device` interface — these PascalCase fields are the target output shape
  - `apps/server-core/internal/models/models.go` — Go Device model with `json` tags — these snake_case values are the actual API response keys

  **WHY Each Reference Matters**:
  - `mapServer()` is the canonical pattern — same architecture, same approach. Follow it exactly.
  - `Device` interface tells you which PascalCase keys to produce
  - Go model `json` tags tell you which snake_case keys come from the API

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

  ```
  Scenario: Verify mapDevice() resolves all snake_case fields via fetchDevices
    Tool: Bash (curl)
    Preconditions: Admin JWT token available, at least one device exists
    Steps:
      1. `curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | jq '.[0] | keys'`
      2. Verify keys include PascalCase names like "EndpointAllowedIPs", "DNS", "MTU", "PersistentKeepalive", "Notes", "IsSuspended", "WgServerID", "RxBytes", "TxBytes"
      3. `curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | jq '.[0] .EndpointAllowedIPs'`
      4. Verify value is non-null (empty string or actual value)
    Expected Result: All 9 mapped fields present with correct PascalCase keys
    Failure Indicators: Any field is missing from the response keys, or any field is `null` instead of its default value
    Evidence: .sisyphus/evidence/task-1-fetch-fields.txt

  Scenario: Verify getDevice also resolves fields
    Tool: Bash (curl)
    Preconditions: Admin JWT token available, a device ID known
    Steps:
      1. `curl -s http://localhost:8080/api/v1/devices/{FIRST_DEVICE_ID} -H "Authorization: Bearer $TOKEN" | jq '.EndpointAllowedIPs'`
      2. Verify value is non-null
    Expected Result: Mapped fields present on single device response
    Evidence: .sisyphus/evidence/task-1-get-fields.txt

  Scenario: Build passes
    Tool: Bash
    Preconditions: Dependencies installed
    Steps:
      1. `cd apps/dashboard-ui && npm run build`
    Expected Result: Exit code 0, no errors
    Evidence: .sisyphus/evidence/task-1-build.txt
  ```

  **Evidence to Capture:**
  - [x] `.sisyphus/evidence/task-1-fetch-fields.txt` — curl output showing all 9 PascalCase fields
  - [x] `.sisyphus/evidence/task-1-get-fields.txt` — curl output for single device
  - [x] `.sisyphus/evidence/task-1-build.txt` — npm build output
  - Verified by code review: `mapDevice()` at devices.ts:3-15, `??` at DeviceDetail.vue:247-248, `/32` at peers.go:140,203 share.go:66 peer_sync.go:40

  **Commit**: YES (with Task 2)
  - Message: `fix(ui): add mapDevice() helper for Device snake_case fields`
  - Files: `apps/dashboard-ui/src/api/devices.ts`

- [x] 2. Fix nullish coalescing in `DeviceDetail.vue`

  **What to do**:
  - In `DeviceDetail.vue` lines 154-158, change `||` to `??` for MTU and PersistentKeepalive:
    - `advForm.value.mtu = device.value.MTU ?? 1420`
    - `advForm.value.persistentKeepalive = device.value.PersistentKeepalive ?? 25`
  - For endpointAllowedIPs and dns, `|| ''` is fine since those are strings (empty string is falsy but also a valid default)
  - Verify with `npm run build`

  **Must NOT do**:
  - Do NOT change the form HTML template
  - Do NOT change the saveAdvanced function
  - Do NOT touch endpontAllowedIPs or dns coalescing — string default is fine

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Trivial two-line change, no new logic
  - **Skills**: none needed

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

  **References**:

  **Code References**:
  - `apps/dashboard-ui/src/views/DeviceDetail.vue:154-158` — lines to fix

  **WHY**:
  - `||` treats `0` as falsy, so a device with `MTU: 0` would show `1420` (wrong)
  - `??` only falls through for `null`/`undefined`, preserving `0` as a valid value

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

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

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

  **Commit**: YES (squash with Task 1)
  - Message: `fix(ui): use nullish coalescing for MTU default`
  - Files: `apps/dashboard-ui/src/views/DeviceDetail.vue`

- [x] 3. Verify AllowedIPs /32 on all config endpoints

  **What to do**:
  - Verify the 4 known config generation points already use `/32`:
    1. `api/peers.go:134` — CreatePeer config generation
    2. `api/peers.go:193` — getDeviceConfig (used by GetConfig, UpdateConfig, GetQR)
    3. `api/share.go:66` — ShareConfig
    4. `internal/wgmanager/peer_sync.go:40` — SyncLocalPeers
  - Confirming: there is NO 5th config generation point. `api/provisioning.go` generates encrypted JSON payload with no AllowedIPs field
  - Run full test suite (if tests pass despite FK failures)

  **Must NOT do**:
  - Do NOT modify any Go files
  - Do NOT re-add /24 — the fix was already applied in previous sessions
  - Do NOT search for additional config generation points — Metis already verified all

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Read-only verification, no changes needed
  - **Skills**: none needed

  **Parallelization**:
  - **Can Run In Parallel**: NO (depends on Task 1 and 2 for full integration test)
  - **Parallel Group**: Wave 2
  - **Blocks**: F1-F4
  - **Blocked By**: Task 1, Task 2

  **References**:

  **Code References**:
  - `apps/server-core/api/peers.go:134` — CreatePeer AllowedIPs: `ipStr + "/32"`
  - `apps/server-core/api/peers.go:193` — getDeviceConfig AllowedIPs: `*device.InternalIP + "/32"`
  - `apps/server-core/api/share.go:66` — ShareConfig AllowedIPs: `*device.InternalIP + "/32"`
  - `apps/server-core/api/peer_sync.go:40` — SyncLocalPeers AllowedIPs: `*d.InternalIP + "/32"`
  - `apps/server-core/api/provisioning.go` — no AllowedIPs field in ConfigPayload

  **WHY**:
  - Read the actual code at each line to confirm `/32` is present
  - Once confirmed, issue 2 is fully closed

  **Acceptance Criteria**:

  **QA Scenarios (MANDATORY):**

  ```
  Scenario: Verify all 4 config generation points use /32
    Tool: Bash (grep)
    Preconditions: Codebase pulled at latest commit
    Steps:
      1. `grep -n "InternalIP.*+.*32" apps/server-core/api/peers.go` (expect 2 matches at lines 134 and 193)
      2. `grep -n "InternalIP.*+.*32" apps/server-core/api/share.go` (expect 1 match at line 66)
      3. `grep -n "InternalIP.*+.*32" apps/server-core/api/peer_sync.go` (expect 1 match at line 40)
    Expected Result: 4 total matches across the 3 files
    Evidence: .sisyphus/evidence/task-3-grep-results.txt

  Scenario: Verify no /24 exists in config generation
    Tool: Bash (grep)
    Preconditions: Same
    Steps:
      1. `grep -n "AllowedIPs.*24" apps/server-core/api/peers.go apps/server-core/api/share.go`
    Expected Result: Zero matches
    Evidence: .sisyphus/evidence/task-3-no-24.txt

  Scenario: Confirm provisioning.go has no AllowedIPs config text
    Tool: Bash (grep)
    Steps:
      1. `grep -c "AllowedIPs" apps/server-core/api/provisioning.go`
    Expected Result: 0
    Evidence: .sisyphus/evidence/task-3-provisioning.txt

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

  **Evidence to Capture:**
  - [x] `.sisyphus/evidence/task-3-grep-results.txt`
  - [x] `.sisyphus/evidence/task-3-no-24.txt`
  - [x] `.sisyphus/evidence/task-3-provisioning.txt`
  - [x] `.sisyphus/evidence/task-3-build.txt`

  **Commit**: NO (read-only verification, no code changes)
  - Message: N/A

---
