17 KiB
Fix Device Advanced Settings Form & AllowedIPs /32
TL;DR
Quick Summary: Fix two issues: (1) DeviceDetail advanced settings form stays empty because TypeScript
Deviceinterface uses PascalCase but Go API returns snake_case — addmapDevice()helper; (2) Config download AllowedIPs still shows/24for some paths — verify all 4 config generation points already use/32.Deliverables:
apps/dashboard-ui/src/api/devices.ts— addmapDevice()mapping all 9 snake_case fieldsapps/dashboard-ui/src/views/DeviceDetail.vue— fix nullish coalescing defaults- Config
/32verified on all 4 endpointsEstimated 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
/32inapi/peers.go(2 places),api/share.go,peer_sync.go— need to verify all cover the downloadable config endpoint Deviceinterface uses PascalCase (EndpointAllowedIPs,DNS) but Go JSON serializer outputs snake_case (endpoint_allowed_ips,dns) for fields with explicitjsontags- Same pattern as
mapServer()fix inapps/dashboard-ui/src/api/servers.ts - Need to fix nullish coalescing:
||should be??for fields that can be0
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
/32points verified:peers.go:134,peers.go:193,share.go:66,peer_sync.go:40— all already use/32 provisioning.gohas 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 infetchDevices()andgetDevice()apps/dashboard-ui/src/views/DeviceDetail.vue—??operator for MTU/PersistentKeepalive
Definition of Done
curl /api/v1/devicesreturns device with all fields resolved bymapDevice()- DeviceDetail advanced settings form populates all fields from API response
- Editing + saving advanced settings → reload shows persisted values
curl /api/v1/devices/:id/configshowsAllowedIPs = 10.x.x.x/32npm run buildpasses
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:
curlwith admin JWT token, pipe tojq, 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
-
F1. Plan Compliance Audit —
oracleRead 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 -
F2. Code Quality Review —
unspecified-highRunnpm run buildandgo 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 -
F3. Real Manual QA —
unspecified-highStart from clean state.- Verify
curl /api/v1/devicesreturns all 9 PascalCase mapped fields - Verify
curl /api/v1/devices/:id/confighas 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
- Verify
-
F4. Scope Fidelity Check —
deepFor 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
# 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
-
All "Must Have" present
-
All "Must NOT Have" absent
-
All builds pass
-
1. Add
mapDevice()helper toapi/devices.tsWhat 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()infetchDevices():.map(mapDevice)on array response - Apply
mapDevice()ingetDevice(): 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
Deviceinterface (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()
- Reason: Single file change, straightforward mapping pattern, already has precedent in
- 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—Deviceinterface — these PascalCase fields are the target output shapeapps/server-core/internal/models/models.go— Go Device model withjsontags — 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.Deviceinterface tells you which PascalCase keys to produce- Go model
jsontags 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.txtEvidence to Capture:
.sisyphus/evidence/task-1-fetch-fields.txt— curl output showing all 9 PascalCase fields.sisyphus/evidence/task-1-get-fields.txt— curl output for single device.sisyphus/evidence/task-1-build.txt— npm build output
Commit: YES (with Task 2)
- Message:
fix(ui): add mapDevice() helper for Device snake_case fields - Files:
apps/dashboard-ui/src/api/devices.ts
- Add
-
2. Fix nullish coalescing in
DeviceDetail.vueWhat to do:
- In
DeviceDetail.vuelines 154-158, change||to??for MTU and PersistentKeepalive:advForm.value.mtu = device.value.MTU ?? 1420advForm.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:
||treats0as falsy, so a device withMTU: 0would show1420(wrong)??only falls through fornull/undefined, preserving0as 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.txtEvidence 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
- In
-
3. Verify AllowedIPs /32 on all config endpoints
What to do:
- Verify the 4 known config generation points already use
/32:api/peers.go:134— CreatePeer config generationapi/peers.go:193— getDeviceConfig (used by GetConfig, UpdateConfig, GetQR)api/share.go:66— ShareConfiginternal/wgmanager/peer_sync.go:40— SyncLocalPeers
- Confirming: there is NO 5th config generation point.
api/provisioning.gogenerates 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
/32is 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.txtEvidence to Capture:
.sisyphus/evidence/task-3-grep-results.txt.sisyphus/evidence/task-3-no-24.txt.sisyphus/evidence/task-3-provisioning.txt.sisyphus/evidence/task-3-build.txt
Commit: NO (read-only verification, no code changes)
- Message: N/A
- Verify the 4 known config generation points already use