# Post-Deploy Fixes + Config Delivery Plan ## TL;DR > **Quick Summary**: Fix 3 immediate bugs (DNS clear, peer detection debug, peer IP change) and design secure config delivery for devices. > > **Deliverables**: > - [x] DNS override can be cleared to empty (frontend fix) > - [ ] Debug procedure documented for peer detection failures > - [x] InternalIP editable from DeviceDetail.vue > - [x] Firewall port range can be empty (apply to all ports) > - [ ] Encrypted config delivery endpoint + companion plaintext endpoint > > **Estimated Effort**: Medium > **Parallel Execution**: YES - 2 waves > **Critical Path**: T1 → [T2] → T3 --- ## Context ### Original Issues 1. **DNS tidak bisa dikosongkan**: Ketika user kosongkan field DNS Override di DeviceDetail.vue lalu save, `advForm.value.dns` jadi `""`, tapi `"" || undefined` di JavaScript menghasilkan `undefined`. Akibatnya field `dns` tidak terkirim ke API, nilai DB tidak berubah. 2. **Peer tidak mendeteksi koneksi**: Device official WG client tetap Offline meski terkoneksi. Perlu debugging multi-layer: (1) cek collector jalan, (2) cek kernel handshake, (3) cek frontend isDeviceOnline(). 3. **Tidak bisa ganti peer IP**: `UpdateDeviceRequest` tidak memiliki field `InternalIP`. IP hanya dialokasikan sekali oleh IPAM saat provisioning. 4. **Config delivery**: Ingin endpoint aman untuk device ambil config, tapi juga bisa debug plaintext untuk WireGuard client manual. ### Current State - **DNS backend**: `devices.go:236-238` — sudah handle `req.DNS != nil`, termasuk empty string ✅ - **DNS frontend**: `DeviceDetail.vue:300` — `dns: advForm.value.dns || undefined` ❌ bug - **Peer IP**: `UpdateDeviceRequest` di `devices.go:190` — TIDAK ada field `InternalIP` - **Config endpoints**: - `GET /devices/:id/config` → plaintext (admin only) - `POST /provision` → `encrypted_config` AES-256-GCM (token-based) - `GET /devices/:id/config/qr` → QR image (admin only) - **Tidak ada endpoint publik untuk device mengambil config sendiri** --- ## Work Objectives ### Objective A: Fix Bugs (Issue 1) **Must Have**: - [ ] DNS override bisa dikosongkan dan tersimpan sebagai `""` di DB - [ ] `npm run build` passes **Must NOT Have**: - [ ] Tidak mengubah struktur API atau backend ### Objective B: Debug Guide (Issue 2) **Must Have**: - [ ] Step-by-step debugging procedure untuk peer detection - [ ] Commands untuk tiap layer (kernel, collector, DB, frontend) ### Objective C: Peer IP Change (Issue 3) **Must Have**: - [ ] Admin bisa mengubah `internal_ip` device via API - [ ] API memvalidasi format IP sebelum update - [ ] API mengecek duplikasi IP di IPAM sebelum update - [ ] Firewall rule re-sync jika IP berubah - [ ] Frontend input field untuk InternalIP di DeviceDetail.vue **Must NOT Have**: - [ ] Jangan override alokasi IPAM untuk device yang belum provisioning - [ ] Jangan izinkan non-admin mengganti IP ### Objective D: Config Delivery (Issue 4) **Questions to resolve** (akan ditanyakan ke user): - Apakah device perlu endpoint autentikasi sendiri (token/API key) untuk ambil config? - Atau cukup admin ambilkan seperti sekarang? - Encrypted payload (AES-GCM) atau cukup HTTPS saja? - Perlu QR code juga atau config text saja? **Must Have** (asumsi awal): - [ ] Endpoint untuk device mengambil config sendiri (setelah provisioning) - [ ] Opsi plaintext untuk debugging - [ ] Opsi encrypted payload untuk production --- ## Verification Strategy > **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. - **Frontend**: `npm run build` + Playwright (cek DNS save + IP input) - **Backend**: `go build -tags dev ./...` + curl API test - **Config**: curl endpoint + validasi response format --- ## Execution Strategy ``` Wave 1 (Parallel — all remaining work): ├── T1: Fix DNS clear bug (frontend) [DONE] ├── T2: Write debugging docs [INFORMATIONAL] ├── T3: Add InternalIP change to backend + frontend [DONE] ├── T4: Fix firewall port range to allow empty (models + API + frontend + nftables) [DONE] └── F1-F3: Build verification + code review Wave 2 (Future — separate plan): ├── Device-Agent Registration + Approval flow └── Encrypted config delivery endpoint ``` --- ## TODOs - [ ] 1. Fix DNS clear bug **What to do**: - File: `apps/dashboard-ui/src/views/DeviceDetail.vue` baris 300 - Ubah `dns: advForm.value.dns || undefined` menjadi `dns: advForm.value.dns === '' ? '' : advForm.value.dns || undefined` - Run `npm run build` untuk verifikasi **Must NOT do**: - Jangan ubah backend — sudah support empty string **Parallelization**: Wave 1, parallel with T2, T3 **Acceptance Criteria**: - [ ] `npm run build` passes - [ ] Set DNS → kosongkan → save → reload → DNS tetap kosong **QA Scenarios**: ``` Scenario: Clear DNS override saves correctly Tool: Bash (curl) Preconditions: Device with existing DNS Steps: 1. curl -X PUT /api/v1/devices/$ID -H "Content-Type: application/json" -d '{"dns": ""}' -H "Authorization: Bearer $TOKEN" 2. curl -X GET /api/v1/devices/$ID -H "Authorization: Bearer $TOKEN" Expected Result: Response JSON includes "dns": "" or null Evidence: .sisyphus/evidence/postdeploy-t1-dns-clear.txt Scenario: Build passes Tool: Bash Steps: cd apps/dashboard-ui && npm run build Expected Result: vue-tsc + vite build succeeds Evidence: .sisyphus/evidence/postdeploy-t1-build.txt ``` **Commit**: YES — `fix(ui): allow clearing DNS override to empty` - [ ] 2. Document peer detection debugging procedure **What to do**: Create debugging doc covering 5 layers: 1. Kernel: `sudo wg show` — peer handshake dari kernel 2. Collector: `docker logs` grep handshake — apakah collector jalan 3. Node type: query DB `wg_servers.name` — local node atau remote 4. DB data: `psql` cek `is_active`, `last_handshake`, `client_info` 5. Status API: `curl /devices/:id/status` — realtime status dari API **Parallelization**: Wave 1, parallel **Acceptance Criteria**: - [ ] Setiap langkah punya command exact yang bisa di-copy-paste - [ ] Setiap langkah punya expected output **Commit**: NO (informational) - [x] 3. Add InternalIP to UpdateDeviceRequest + handler + UI **What to do**: **Backend** — `apps/server-core/api/devices.go`: - Tambah `InternalIP *string json:"internal_ip"` ke `UpdateDeviceRequest` struct - Di Update handler, setelah blok DNS (line ~238), tambah: ```go if req.InternalIP != nil { ip := net.ParseIP(*req.InternalIP) if ip == nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid internal_ip format"}) return } // Check duplicate IP across all devices + servers var count int64 h.db.Model(&models.Device{}).Where("internal_ip = ? AND id != ?", *req.InternalIP, id).Count(&count) if count > 0 { c.JSON(http.StatusConflict, gin.H{"error": "internal_ip already in use"}) return } updates["internal_ip"] = *req.InternalIP // Re-sync firewall if endpoint_allowed_ips also changed or existed if req.EndpointAllowedIPs != nil && *req.EndpointAllowedIPs != "" { destCIDR := *req.EndpointAllowedIPs // Remove old rule, add new rule h.fw.AddForwardRule(device.Name, ip, destCIDR) } } ``` **Frontend** — `apps/dashboard-ui/src/views/DeviceDetail.vue`: - Di advanced settings section, tambah input field: ```html