20 KiB
Bug Fix: Node Form, Device Status, & Peer Config
TL;DR
Quick Summary: Fix 5 bugs in NexusGuard dashboard-ui + server-core terkait node registration form, device online/offline status, peer address netmask, advanced options, dan editable wg.conf view. Plus fix 3 critical bugs uncovered by Metis review (IPAM string comparison, share.go prefix, Update handler gap).
Deliverables:
- Single input IP Pool + Interface Address dengan available IP counter
- Fix peer Address menggunakan pool netmask (bukan /32)
- Advanced Options collapsible + Table default "Off"
- Editable wg.conf view (safe, validated)
- Device status akurat (Online hanya jika benar-benar konek)
- IPAM exclusion bug fixed (server IP tidak di-override peer)
Estimated Effort: Medium (10-14 tasks across 3 waves) Parallel Execution: YES — 3 waves Critical Path: IPAM fix → Backend fixes → Frontend fixes → wg.conf feature
Context
Original Request
5 bugs diidentifikasi user:
- IP Pool CIDR + Interface Address jadi 1 input (dengan available IP count)
- Peer Address tidak pakai netmask pool (selalu /32)
- Advanced Options tidak collapsible + Table default "auto"
- Tidak ada editable raw wg.conf view
- Device status selalu Online meskipun tidak konek
Metis Review — Critical Findings
3 bugs uncovered yang harus diperbaiki bersamaan:
| # | Bug | Lokasi | Dampak |
|---|---|---|---|
| C1 | IPAM string comparison mismatch | ipam/manager.go:87-94 vs 107 |
interface_address tersimpan sebagai "10.8.0.1/24" tapi dibandingkan dengan "10.8.0.1" (tanpa prefix). Server's own IP tidak pernah dikecualikan → peer bisa dapat IP yang sama dengan server → IP conflict |
| C2 | share.go juga hardcode /32 | share.go:63 |
Sama seperti peers.go, share link juga generate Address dengan /32 |
| C3 | Update handler tidak recalculate InterfaceAddress | servers.go:321-326 |
Create handler auto-calc InterfaceAddress, tapi Update handler tidak. Ganti IPPoolCIDR saat edit → InterfaceAddress stale |
Konfirmasi dari User
- IP single input: ✅ Sepakat.
10.172.20.1/24→ pool=10.172.20.0/24, interface=10.172.20.1/24 - Advanced collapsible + Table=Off: ✅ Keduanya
- Editable wg.conf: ✅ Editable, harus aman. wg.conf tidak ada sebagai file fisik (generated config via wgctrl)
- Device status: User tidak tahu apakah pakai Redis. Device-agent pakai official WG client
Work Objectives
Core Objective
Memperbaiki 5 bugs dashboard-ui + server-core + 3 critical bugs dari Metis review.
Concrete Deliverables
- Servers.vue: Form register/edit node — single IP input + available count
- peers.go + share.go: Peer Address pakai pool netmask (bukan /32)
- ipam/manager.go: Fix string comparison untuk interface_address exclusion
- Servers.vue: Advanced Overrides collapsible + Table default "Off"
- PeerConfigModal.vue: Editable wg.conf + safe Apply
- models.go + peers.go + devices.go + heartbeat/redis.go: IsActive default false
Must Have
- IPAM tidak lagi mengalokasikan IP yang sama dengan server's interface_address
- Edit node dengan mengubah IPPoolCIDR → InterfaceAddress otomatis recalculate
- Single input IP/Prefix menolak network address (x.x.x.0/24) dan broadcast
- Peer Address di config menggunakan netmask dari pool, bukan /32
- Device baru muncul sebagai "Offline" sampai heartbeat pertama
- Advanced Overrides collapsible (default tertutup) di create + edit form
- Table default "Off" untuk server baru
- Editable wg.conf view dengan validasi keamanan
Must NOT Have (Guardrails)
- JANGAN ubah struktur kolom DB WgServer (merge hanya UI-level, backend tetap 2 field)
- JANGAN ubah AllowedIPs /32 di [Peer] section (hanya Address di [Interface])
- JANGAN deduplikasi config generator (scope creep)
- JANGAN izinkan edit PrivateKey/PresharedKey tanpa warning
- JANGAN tambahkan DB migration untuk backfill Table/IsActive existing
- JANGAN
nft flush tableatau ubah behavior firewall - JANGAN log plaintext keys
Verification Strategy
Test Decision
- Infrastructure exists: YES (bun test + vitest di dashboard-ui)
- Automated tests: NO (bug fix, verification via QA scenarios)
- Framework: vitest (existing)
QA Policy
Setiap task diverifikasi oleh agent (agent-executed QA). Tidak ada verifikasi manual.
- Frontend: Playwright — navigasi form, input data, assert DOM
- API: Bash (curl) — send requests, assert JSON response
- Backend: Bash (go run) — test specific functions if needed
Execution Strategy
Parallel Execution Waves
Wave 1 (Backend Foundation — IPAM + Model Fixes):
├── Task 1: Fix IPAM string comparison (ipam/manager.go) [quick]
├── Task 2: Fix Update handler recalculate InterfaceAddress (servers.go) [quick]
├── Task 3: Change GORM defaults — IsActive=false + Table=off (models.go) [quick]
├── Task 4: Fix SyncToDB isActive default (heartbeat/redis.go) [quick]
├── Task 5: Remove IsActive:true hardcode from CreatePeer + Devices.Create (peers.go, devices.go) [quick]
└── Task 6: Fix peer Address to use pool netmask (peers.go + share.go) [quick]
Wave 2 (Frontend — Form + UI Changes):
├── Task 7: Merge IP Pool + Interface Address jadi 1 input + available IP count (Servers.vue) [visual-engineering]
├── Task 8: Advanced Overrides collapsible + Table default "Off" (Servers.vue) [visual-engineering]
├── Task 9: Device status UI — handle IsActive=false untuk new device (Devices.vue, DeviceDetail.vue) [quick]
Wave 3 (wg.conf Editable View):
├── Task 10: Editable wg.conf textarea + backend validation endpoint (PeerConfigModal.vue + peers.go) [unspecified-high]
TODOs
-
1. Fix IPAM string comparison — strip prefix dari interface_address
What to do:
- Di
ipam/manager.go:AllocateIPFromCIDR()(line 87-107): saatPluck("interface_address", &serverIPs), data yang didapat format"10.8.0.1/24"(dengan prefix) - Saat dimasukkan ke
usedMap(line 94):usedMap["10.8.0.1/24"] = true - Tapi perbandingan (line 107):
if !usedMap[ip.String()]—ip.String()="10.8.0.1"(tanpa prefix) - String
"10.8.0.1/24"≠"10.8.0.1"→ server IP tidak pernah match → peer bisa dapat IP server - Fix: Parse
interface_addressambil IP saja sebelum masukusedMap - Juga fix di
AllocateIP()(line 39-46) — masalah yang sama
Must NOT do:
- Jangan ubah logika increment IP atau skip broadcast
- Jangan tambah field baru ke model
Recommended Agent Profile:
- Category:
quick - Skills: []
Parallelization:
- Can Run In Parallel: YES
- Blocks: Tasks 7 (backend change untuk single input), Task 6 (peer fix)
- Blocked By: None
References:
apps/server-core/internal/ipam/manager.go:73-116— AllocateIPFromCIDR functionapps/server-core/internal/ipam/manager.go:30-71— AllocateIP function (same bug)apps/server-core/internal/ipam/manager.go:123-133—IsAvailablealready handles prefix correctly (pattern to follow)apps/server-core/internal/models/models.go:35-36— IPPoolCIDR and InterfaceAddress field definitions
Acceptance Criteria:
- Server with InterfaceAddress
10.8.0.1/24→ IPAM tidak mengalokasikan10.8.0.1 - Server with InterfaceAddress
10.8.0.1/24→ IPAM bisa alokasikan10.8.0.2(tersedia)
QA Scenarios:
Scenario: Verify server interface IP excluded from allocation Tool: Bash (curl) Preconditions: DB has server with IPPoolCIDR="10.8.0.0/24", InterfaceAddress="10.8.0.1/24" Steps: 1. curl -s -X POST /api/v1/peers -H "Authorization: Bearer $TOKEN" -d '{"name":"test-peer","wg_server_id":"$SERVER_ID"}' 2. Parse response JSON → get device.InternalIP Expected Result: InternalIP != "10.8.0.1" (server IP not allocated to peer) Failure Indicators: Peer gets 10.8.0.1 Evidence: .sisyphus/evidence/task-1-ipam-fix.json - Di
-
2. Fix Update handler — recalculate InterfaceAddress saat IPPoolCIDR berubah
What to do:
- Di
api/servers.go:321-326(Update handler): - Saat ini hanya:
if req.InterfaceAddress != nil { server.InterfaceAddress = *req.InterfaceAddress } - Tambahkan: jika
req.IPPoolCIDR != nil && req.InterfaceAddress == nil→ auto-calculate seperti Create handler (line 162-171) - Extract logika auto-calc ke helper function untuk reuse
Must NOT do:
- Jangan trigger auto-calc jika InterfaceAddress juga dikirim (user ingin override manual)
Parallelization:
- Can Run In Parallel: YES
- Blocks: Task 7 (single input tergantung backend)
- Blocked By: None
References:
apps/server-core/api/servers.go:162-171— Create handler auto-calc logic (pattern to copy)apps/server-core/api/servers.go:278-365— Update handler (current behavior to fix)
Acceptance Criteria:
- PUT /servers/{id} with
{"ip_pool_cidr": "10.9.0.0/24"}→ InterfaceAddress berubah jadi10.9.0.1/24 - PUT /servers/{id} with
{"ip_pool_cidr": "10.9.0.0/24", "interface_address": "10.9.0.5/24"}→ InterfaceAddress =10.9.0.5/24(manual override)
- Di
-
3. Change GORM defaults — IsActive=false + Table=off
What to do:
models.go:37:Table string \...default:'auto'`→default:'off'`models.go:70:IsActive bool \gorm:"default:true"`→default:false`
Parallelization:
- Can Run In Parallel: YES
- Blocks: Tasks 4, 5, 9
- Blocked By: None
References:
apps/server-core/internal/models/models.go:37— Table fieldapps/server-core/internal/models/models.go:70— IsActive field
-
4. Fix SyncToDB — isActive default false instead of true
What to do:
heartbeat/redis.go:67— changeisActive := truetoisActive := false- Device dianggap offline sampai Redis membuktikan online
Parallelization:
- Can Run In Parallel: YES (with Task 5)
- Blocked By: Task 3 (model default change)
References:
apps/server-core/internal/heartbeat/redis.go:57-84— SyncToDB function
-
5. Remove IsActive:true hardcode dari CreatePeer + Devices.Create
What to do:
peers.go:99: HapusIsActive: truedari struct literal (gunakan default dari model)devices.go:118(perlu cek): HapusIsActive: trueyang hardcode
Must NOT do:
- Jangan set IsActive: false secara eksplisit — biarkan GORM default (yang sudah diubah ke false)
Parallelization:
- Can Run In Parallel: YES (with Task 4)
- Blocked By: Task 3 (model default change)
References:
apps/server-core/api/peers.go:89-103— Device creation structapps/server-core/api/devices.go— Devices handler
-
6. Fix peer Address — gunakan pool netmask (bukan /32)
What to do:
peers.go:133:Address = %s/32→ parsewgServer.IPPoolCIDR, ambilones(netmask bits), gunakan%s/%dpeers.go:205: Sama digetDeviceConfig—Address = %s/32→ pool netmaskshare.go:63: Sama —Address = %s/32→ pool netmask- Fallback: jika
IPPoolCIDRkosong, tetap gunakan/32 - Hanya ubah
Addressdi[Interface], jangan ubahAllowedIPsdi[Peer]
Must NOT do:
- JANGAN ubah AllowedIPs
/32untuk non-internet peers (itu untuk routing, bukan interface address)
Parallelization:
- Can Run In Parallel: YES
- Blocked By: None (tapi idealnya setelah Task 1)
References:
apps/server-core/api/peers.go:131-147— CreatePeer config generationapps/server-core/api/peers.go:203-225— getDeviceConfig config generationapps/server-core/api/share.go— Share link config generation
Acceptance Criteria:
- Peer dengan server pool
10.8.0.0/24→ Address =10.8.0.2/24(bukan /32) - Server tanpa pool (kosong) → Address =
x.x.x.x/32(fallback)
-
7. Merge IP Pool + Interface Address jadi 1 input + available IP count
What to do:
-
Create form (
Servers.vue):- Hapus 2 field terpisah (IP Pool CIDR + Interface Address)
- Tambah 1 field baru:
Interface IP / Prefixdengan placeholder10.172.20.1/24 - Saat user mengetik, parse: extract IP untuk interface, extract network untuk pool
- Update
v-modeldanhandleAdduntuk kirimip_pool_cidr+interface_addresske API (backend tetap 2 field) - Tampilkan info: "254 Available IPs" (dihitung dari prefix)
-
Edit form (
Servers.vueopenEdit):- Reconstruct single input dari existing
srv.IPPoolCIDR+srv.InterfaceAddress - Format: extract IP dari InterfaceAddress + prefix dari IPPoolCIDR
- Contoh: pool=
10.8.0.0/24, interface=10.8.0.1/24→ input=10.8.0.1/24
- Reconstruct single input dari existing
-
Validasi:
- Tolak jika IP adalah network address (
10.172.20.0/24) - Tolak jika IP adalah broadcast (
10.172.20.255/24) - Tolak jika prefix < /24 (too large) atau > /32 (no usable IPs)
- Tampilkan error message jelas di form
- Tolak jika IP adalah network address (
-
Available IP Count:
- Hitung:
2^(32-prefix) - 2(network + broadcast) - Tampilkan sebagai teks di bawah input: "254 available IP addresses"
- Update otomatis saat user mengubah prefix
- Hitung:
-
Backend: Tidak ada perubahan model — UI memparse dan mengirim ke 2 field yang ada
Must NOT do:
- JANGAN ubah model
WgServer(tetap 2 kolom terpisah di DB) - JANGAN hapus API backward compatibility (endpoint masih terima
ip_pool_cidr+interface_address)
Parallelization:
- Can Run In Parallel: NO (dengan Task 1-6)
- Blocked By: Task 2 (backend recalculate handler)
References:
apps/dashboard-ui/src/views/Servers.vue:48-55,186-192— Current form fieldsapps/dashboard-ui/src/views/Servers.vue:265-284,295-300— Form data modelsapps/dashboard-ui/src/views/Servers.vue:368-404— handleAdd functionapps/dashboard-ui/src/views/Servers.vue:302-358— openEdit + handleEditSaveapps/dashboard-ui/src/api/servers.ts:33-36— createServer API call
Acceptance Criteria:
- Input
10.172.20.1/24→ IP Pool =10.172.20.0/24, Interface =10.172.20.1/24 - Input
10.172.20.0/24→ error "Network address tidak valid untuk interface" - Input
10.172.20.255/24→ error "Broadcast address tidak valid" - Tampilkan "254 available IP addresses" untuk /24, "126" untuk /25
- Edit form menunjukkan existing server sebagai
{ip}/{prefix}
-
-
8. Advanced Overrides collapsible + Table default "Off"
What to do:
- Tambahkan
showAdvancedref (defaultfalse) seperti di DeviceDetail.vue - Bungkus section Advanced Overrides (
Servers.vue:63-91dan195-222) dengan toggle:<button @click="showAdvanced = !showAdvanced" class="..."> <span>Advanced Overrides</span> <span :class="showAdvanced ? 'rotate-180' : ''">▼</span> </button> <div v-if="showAdvanced" class="..."> <!-- existing advanced fields --> </div> - Create form: Ubah
form.value.table = 'auto'→'off'(line 275) - Edit form: Ubah
editForm.value.table = 'auto'→'off'(line 297, 313)srv.Table || 'auto'→srv.Table || 'off'
- handleEditSave: Kirim table hanya jika ada perubahan:
table: editForm.value.table === 'off' ? undefined : 'auto'- Sebenarnya: kalau default 'off', tidak perlu kirim jika 'off' (biar undefined)
- Tapi kalau user pilih 'auto', kirim 'auto'
Must NOT do:
- JANGAN tambah migration untuk backfill existing server
- JANGAN ubah validasi form yang sudah ada
Parallelization:
- Can Run In Parallel: YES (with Task 7)
- Blocked By: None
References:
apps/dashboard-ui/src/views/Servers.vue:63-91— Create form advanced sectionapps/dashboard-ui/src/views/Servers.vue:195-222— Edit form advanced sectionapps/dashboard-ui/src/views/DeviceDetail.vue:42-72— Collapsible pattern to followapps/dashboard-ui/src/views/Servers.vue:275,297,313— Current table defaults
Acceptance Criteria:
- Advanced Overrides section collapsible dengan toggle chevron
- Section tertutup secara default
- Nilai di dalam section tetap terkirim saat save meski section tertutup
- Create server baru → Table = "off" secara default
- Edit server → Table = "off" jika belum pernah diubah
- Tambahkan
-
9. Device status UI — handle IsActive=false untuk device baru
What to do:
- Devices.vue:29-33: Status badge sudah menggunakan
device.IsActive ? 'Online' : 'Offline' - Dengan fix di Task 3-5, device baru akan muncul sebagai "Offline" — ini yang benar
- Opsional: Tambah deteksi "Unknown" untuk device tanpa heartbeat sama sekali
- Cek
device.LastHandshake— jika zero value (0001-01-01ataunull) → "Unknown" / "Pending" - Tapi ini nice-to-have, minimal fix cukup dengan IsActive=false
- Cek
Parallelization:
- Can Run In Parallel: YES (with Tasks 7, 8)
- Blocked By: Tasks 3, 4, 5 (model default + remove hardcode)
References:
apps/dashboard-ui/src/views/Devices.vue:29-33— Status displayapps/dashboard-ui/src/views/DeviceDetail.vue:12-14— Status badge
- Devices.vue:29-33: Status badge sudah menggunakan
-
10. Editable wg.conf view + backend validation endpoint
What to do:
-
Frontend (
PeerConfigModal.vue):- Ubah textarea dari
readonlymenjadi editable - Hapus
resize-none+select-all, tambah class agar bisa diedit - Tambah tombol "Save & Apply" + "Cancel" (reset ke generated config)
- Tambah confirmation dialog jika edit PrivateKey/PresharedKey
- Ubah textarea dari
-
Backend (new endpoint atau extend existing):
PUT /devices/{id}/configatau extendPUT /devices/{id}- Validate:
- Parse WireGuard INI format
- Valid base64 keys
- Valid CIDR untuk Address
- Valid endpoint format
- Safe fields-only update:
- Parse config → extract: DNS, MTU, PersistentKeepalive, AllowedIPs
- JANGAN izinkan update PrivateKey, PresharedKey, Address (internal IP)
- Jika user mengubah field terlarang → reject with error message
- Update DB fields → regenerate config
- Return updated config_text
-
Error handling:
- Parse error → tampilkan "Invalid config format: {detail}"
- Rejected field → "Cannot change PrivateKey via config editor. Use 'Regenerate Keys' feature."
- Network error → "Failed to save config. Check your connection."
Must NOT do:
- JANGAN izinkan edit PrivateKey/PresharedKey tanpa warning + confirmation
- JANGAN simpan raw config text (parse ke DB fields)
- JANGAN trigger restart WireGuard interface
Parallelization:
- Can Run In Parallel: NO
- Blocked By: Task 6 (pool netmask fix — config format berubah)
References:
apps/dashboard-ui/src/components/PeerConfigModal.vue— Config modal (read-only saat ini)apps/dashboard-ui/src/components/AddPeerModal.vue:20-24— Config text area patternapps/server-core/api/peers.go:155-225— getDeviceConfig (config generation)apps/server-core/api/peers.go:131-147— CreatePeer config pattern
Acceptance Criteria:
- Textarea bisa diedit (tidak readonly)
- Edit AllowedIPs → Apply → config berubah
- Edit PrivateKey → warning muncul → user bisa cancel
- Config tidak valid → error message muncul
-
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleVerify: IPAM fix applied, all 5 bugs addressed, no scope creep -
F2. Code Quality Review —
unspecified-highRuntsc --noEmit+bun test, check for unused imports, console.log -
F3. Real Manual QA —
unspecified-high(+ playwright) Execute QA scenarios for all 10 tasks. Test cross-task integration. -
F4. Scope Fidelity Check —
deepVerify: Must Have checklist complete, Must NOT compliance
Commit Strategy
- 1-5:
fix(server-core): ipam fix + isactive default + update handler - 6:
fix(server-core): peer address uses pool netmask - 7-9:
fix(dashboard-ui): node form merge + advanced collapse + status - 10:
feat(dashboard-ui): editable wg.conf view with safe apply
Success Criteria
Final Checklist
- Server yang InterfaceAddress-nya
10.8.0.1/24— peer tidak dapat IP10.8.0.1 - Single input
10.172.20.1/24→ IP Pool10.172.20.0/24, Interface10.172.20.1/24 - Peer Address di config:
10.172.20.2/24(mengikuti pool netmask) - Advanced Overrides collapsible + Table default "Off"
- Editable wg.conf view dengan validasi
- Device baru: "Offline" sampai heartbeat pertama
10.172.20.0/24— tolak sebagai network address