Files
Nexus-Guard-Suite/.omo/plans/archived/bugfix-node-form-device-status.md
datadunia cbacfea7f2
NexusGuard CI / server-core-test (push) Failing after 3m6s
NexusGuard CI / server-core-build (push) Has been skipped
NexusGuard CI / device-agent-test (push) Failing after 4s
NexusGuard CI / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI / dashboard-test (push) Failing after 4s
NexusGuard CI / dashboard-dist (push) Has been skipped
chore: update submodule refs, clean up plans/evidence, update .gitignore
2026-06-07 23:53:15 +07:00

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:

  1. IP Pool CIDR + Interface Address jadi 1 input (dengan available IP count)
  2. Peer Address tidak pakai netmask pool (selalu /32)
  3. Advanced Options tidak collapsible + Table default "auto"
  4. Tidak ada editable raw wg.conf view
  5. 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

  1. Servers.vue: Form register/edit node — single IP input + available count
  2. peers.go + share.go: Peer Address pakai pool netmask (bukan /32)
  3. ipam/manager.go: Fix string comparison untuk interface_address exclusion
  4. Servers.vue: Advanced Overrides collapsible + Table default "Off"
  5. PeerConfigModal.vue: Editable wg.conf + safe Apply
  6. 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 table atau 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): saat Pluck("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_address ambil IP saja sebelum masuk usedMap
    • 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 function
    • apps/server-core/internal/ipam/manager.go:30-71 — AllocateIP function (same bug)
    • apps/server-core/internal/ipam/manager.go:123-133IsAvailable already 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 mengalokasikan 10.8.0.1
    • Server with InterfaceAddress 10.8.0.1/24 → IPAM bisa alokasikan 10.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
    
  • 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 jadi 10.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)
  • 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 field
    • apps/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 — change isActive := true to isActive := 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: Hapus IsActive: true dari struct literal (gunakan default dari model)
    • devices.go:118 (perlu cek): Hapus IsActive: true yang 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 struct
    • apps/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 → parse wgServer.IPPoolCIDR, ambil ones (netmask bits), gunakan %s/%d
    • peers.go:205: Sama di getDeviceConfigAddress = %s/32 → pool netmask
    • share.go:63: Sama — Address = %s/32 → pool netmask
    • Fallback: jika IPPoolCIDR kosong, tetap gunakan /32
    • Hanya ubah Address di [Interface], jangan ubah AllowedIPs di [Peer]

    Must NOT do:

    • JANGAN ubah AllowedIPs /32 untuk 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 generation
    • apps/server-core/api/peers.go:203-225 — getDeviceConfig config generation
    • apps/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 / Prefix dengan placeholder 10.172.20.1/24
      • Saat user mengetik, parse: extract IP untuk interface, extract network untuk pool
      • Update v-model dan handleAdd untuk kirim ip_pool_cidr + interface_address ke API (backend tetap 2 field)
      • Tampilkan info: "254 Available IPs" (dihitung dari prefix)
    • Edit form (Servers.vue openEdit):

      • 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
    • 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
    • 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
    • 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 fields
    • apps/dashboard-ui/src/views/Servers.vue:265-284,295-300 — Form data models
    • apps/dashboard-ui/src/views/Servers.vue:368-404 — handleAdd function
    • apps/dashboard-ui/src/views/Servers.vue:302-358 — openEdit + handleEditSave
    • apps/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 showAdvanced ref (default false) seperti di DeviceDetail.vue
    • Bungkus section Advanced Overrides (Servers.vue:63-91 dan 195-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 section
    • apps/dashboard-ui/src/views/Servers.vue:195-222 — Edit form advanced section
    • apps/dashboard-ui/src/views/DeviceDetail.vue:42-72 — Collapsible pattern to follow
    • apps/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
  • 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-01 atau null) → "Unknown" / "Pending"
      • Tapi ini nice-to-have, minimal fix cukup dengan IsActive=false

    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 display
    • apps/dashboard-ui/src/views/DeviceDetail.vue:12-14 — Status badge
  • 10. Editable wg.conf view + backend validation endpoint

    What to do:

    • Frontend (PeerConfigModal.vue):

      • Ubah textarea dari readonly menjadi 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
    • Backend (new endpoint atau extend existing):

      • PUT /devices/{id}/config atau extend PUT /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 pattern
    • apps/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 Auditoracle Verify: IPAM fix applied, all 5 bugs addressed, no scope creep

  • F2. Code Quality Reviewunspecified-high Run tsc --noEmit + bun test, check for unused imports, console.log

  • F3. Real Manual QAunspecified-high (+ playwright) Execute QA scenarios for all 10 tasks. Test cross-task integration.

  • F4. Scope Fidelity Checkdeep Verify: 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 IP 10.8.0.1
  • Single input 10.172.20.1/24 → IP Pool 10.172.20.0/24, Interface 10.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