459 lines
20 KiB
Markdown
459 lines
20 KiB
Markdown
# 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
|
|
- [x] IPAM tidak lagi mengalokasikan IP yang sama dengan server's interface_address
|
|
- [x] Edit node dengan mengubah IPPoolCIDR → InterfaceAddress otomatis recalculate
|
|
- [x] Single input IP/Prefix menolak network address (x.x.x.0/24) dan broadcast
|
|
- [x] Peer Address di config menggunakan netmask dari pool, bukan /32
|
|
- [x] Device baru muncul sebagai "Offline" sampai heartbeat pertama
|
|
- [x] Advanced Overrides collapsible (default tertutup) di create + edit form
|
|
- [x] Table default "Off" untuk server baru
|
|
- [x] 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
|
|
|
|
- [x] 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-133` — `IsAvailable` 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
|
|
```
|
|
|
|
- [x] 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)
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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 `getDeviceConfig` — `Address = %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)
|
|
|
|
- [x] 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}`
|
|
|
|
- [x] 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:
|
|
```html
|
|
<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
|
|
|
|
- [x] 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
|
|
|
|
- [x] 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
|
|
|
|
- [x] F1. **Plan Compliance Audit** — `oracle`
|
|
Verify: IPAM fix applied, all 5 bugs addressed, no scope creep
|
|
|
|
- [x] F2. **Code Quality Review** — `unspecified-high`
|
|
Run `tsc --noEmit` + `bun test`, check for unused imports, console.log
|
|
|
|
- [x] F3. **Real Manual QA** — `unspecified-high` (+ playwright)
|
|
Execute QA scenarios for all 10 tasks. Test cross-task integration.
|
|
|
|
- [x] F4. **Scope Fidelity Check** — `deep`
|
|
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
|
|
- [x] Server yang InterfaceAddress-nya `10.8.0.1/24` — peer tidak dapat IP `10.8.0.1`
|
|
- [x] Single input `10.172.20.1/24` → IP Pool `10.172.20.0/24`, Interface `10.172.20.1/24`
|
|
- [x] Peer Address di config: `10.172.20.2/24` (mengikuti pool netmask)
|
|
- [x] Advanced Overrides collapsible + Table default "Off"
|
|
- [x] Editable wg.conf view dengan validasi
|
|
- [x] Device baru: "Offline" sampai heartbeat pertama
|
|
- [x] `10.172.20.0/24` — tolak sebagai network address
|