Files
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

518 lines
18 KiB
Markdown

# Nodes Form Fields Fix — Listen Binding & UI Inconsistencies
## TL;DR
> **Quick Summary**: Fix 6 inconsistencies in the Nodes Register/Edit form (Servers.vue): missing Listen Address in edit modal, misleading "Listen Binding" column header, display column that doesn't show port, and inconsistent input types for script hooks. Frontend-only changes, no backend modification needed.
> **Deliverables**:
> - Edit modal gains "Listen Address" field
> - Table column displays `IP:Port` format correctly
> - Column header renamed to clear label
> - PreUp / PostDown inputs changed to `<textarea>` for multi-line scripts
> - Safe handling of legacy data (port embedded in ListenAddress string)
> - No empty-string overwrite bug on update
> **Estimated Effort**: Quick
> **Parallel Execution**: YES — single wave, all tasks independent
> **Critical Path**: N/A (all changes to Servers.vue)
---
## Context
### Original Request
Fix masalah labeling di Nodes Register/Edit form: "Listen Binding" vs "Listen Address" tidak konsisten, Edit modal hilang field Listen Address, dan tipe input script hooks tidak seragam.
### Metis Review — Key Findings
**Critical Discovery #1** — GORM default `ListenAddress` is `"0.0.0.0:51820"` (IP:Port), but frontend always sends IP-only. Legacy records may have port embedded in the string.
**Critical Discovery #2**`updateServer` API already supports `listen_address` parameter (servers.ts:45). Only Servers.vue needs the field wired up.
**Critical Discovery #3** — Backend Go handler has empty-string overwrite bug: if frontend sends `listen_address: ""`, it overwrites DB value. **Must omit field from payload if unchanged.**
**Critical Discovery #4** — No validation on listen_address format. Backend accepts any string. Out of scope for this fix but worth noting.
---
## Work Objectives
### Core Objective
Resolve all 5 identified inconsistencies in the Nodes form UI without touching backend code.
### Concrete Deliverables
- `apps/dashboard-ui/src/views/Servers.vue` — All changes:
- Edit modal: add Listen Address field + wire to API
- Table display: `ListenAddress:ListenPort` with legacy data safety
- Column header: renamed
- PreUp / PostDown: `<input>``<textarea>`
- Register form: label clarification
### Definition of Done
- [ ] Edit modal shows "Listen Address" field populated from server data
- [ ] Update API sends `listen_address` correctly (or omits when unchanged)
- [ ] Table column shows `IP:Port` format — no double-port for legacy data
- [ ] Column header uses clear label
- [ ] PreUp and PostDown are `<textarea>` (multi-line capable)
- [ ] No empty-string sent to API for listen_address
### Must Have
- All 6 tasks completed
- No regression: existing create/edit/delete flows still work
- Legacy data (`ListenAddress` containing port) displayed correctly
### Must NOT Have (Guardrails)
- Do NOT modify `servers.ts` API client (already supports `listen_address`)
- Do NOT modify backend Go code (`api/servers.go`, `internal/models/`)
- Do NOT touch other views (DeviceDetail.vue, Dashboard.vue, etc.)
- Do NOT add IP validation or IPv6 handling (out of scope)
- Do NOT send `listen_address: ""` to update API
---
## Verification Strategy
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.
### Test Decision
- **Infrastructure exists**: YES (Vue 3 + TypeScript)
- **Automated tests**: None (no frontend test suite exists)
- **Primary verification**: Agent-executed QA via Playwright (browser automation)
### QA Policy
Every task MUST include agent-executed QA scenarios using Playwright:
- Navigate to Nodes page
- Open Register / Edit modal
- Fill fields, submit, verify results
- Capture screenshots as evidence
---
## Execution Strategy
### Parallel Execution Waves
```
Wave 1 (ALL tasks in parallel — single file edits):
├── Task 1: Add listenAddress to editForm reactive state + openEdit()
├── Task 2: Wire listen_address to handleEditSave() payload
├── Task 3: Fix table display column — ListenAddress:ListenPort with legacy safety
├── Task 4: Rename column header from "Listen Binding"
├── Task 5: Fix PreUp and PostDown — <input> → <textarea>
└── Task 6: Clarify Register form "Listen Address" label
Wave FINAL (verification):
├── Task F1: Verify all changes via Playwright QA scenarios
```
---
## TODOs
- [x] 1. Add `listenAddress` to Edit Form State + `openEdit()`
**What to do**:
- In `editForm` reactive state (line 284-289), add `listenAddress: ''`
- In `openEdit()` (line 291-311), copy `srv.ListenAddress` to `editForm.value.listenAddress`
- **Critical**: If `srv.ListenAddress` contains a port (e.g., `"0.0.0.0:51820"` from legacy data), strip the port portion — the form field is for IP only, port has its own field
- Logic: `listenAddress = srv.ListenAddress.includes(':') ? srv.ListenAddress.split(':')[0] : srv.ListenAddress`
- Add the input field in the edit modal template, after "Public Endpoint" (line 168):
```html
<div>
<label class="block text-xs text-gray-500 mb-1">Listen Address</label>
<input v-model="editForm.listenAddress" placeholder="0.0.0.0" class="w-full bg-black/50 border border-white/10 rounded p-2 text-white focus:border-cyan-500 focus:outline-none" />
</div>
```
**Must NOT do**:
- Do NOT modify `servers.ts` API client
- Do NOT send empty string if field is cleared
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Simple reactive state + template addition
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1 (with Tasks 2-6)
- **Blocks**: Task 2 (needs the state variable)
- **Blocked By**: None
**References**:
- `apps/dashboard-ui/src/views/Servers.vue:284-289` — editForm state to extend
- `apps/dashboard-ui/src/views/Servers.vue:291-311` — openEdit() to update
- `apps/dashboard-ui/src/views/Servers.vue:168-172` — After "Public Endpoint" field (insert point)
**Acceptance Criteria**:
- [ ] editForm has `listenAddress` field
- [ ] openEdit() populates listenAddress from srv.ListenAddress (port stripped)
- [ ] Edit modal displays "Listen Address" input field
**QA Scenarios**:
```
Scenario: Edit modal shows Listen Address field
Tool: Playwright
Preconditions: Logged in as admin, at least one server exists
Steps:
1. Navigate to Nodes page (/servers)
2. Click "Edit" on any server row
3. Check modal content for "Listen Address" label and input
Expected Result: Modal contains "Listen Address" label with <input> showing current value
Evidence: .sisyphus/evidence/task-1-edit-listen-address.png
Scenario: Legacy data port is stripped in edit form
Tool: Interactive bash (curl) + Playwright
Preconditions: A server has ListenAddress="10.0.0.1:51820" (legacy data)
Steps:
1. Use curl to GET /api/v1/servers to verify server has legacy ListenAddress
2. Open edit modal in Playwright
3. Check listenAddress input value
Expected Result: Input shows "10.0.0.1", not "10.0.0.1:51820"
Evidence: .sisyphus/evidence/task-1-legacy-port-stripped.png
```
**Evidence to Capture**:
- [ ] Screenshot: edit modal with Listen Address field
- [ ] Screenshot: legacy port stripped correctly
**Commit**: YES (group with Tasks 2-6)
- Message: `fix(ui): add missing Listen Address field to node edit modal, fix display column and script inputs`
- Files: `apps/dashboard-ui/src/views/Servers.vue`
---
- [x] 2. Wire `listen_address` to `handleEditSave()` Payload
**What to do**:
- In `handleEditSave()` (line 318-343), add `listen_address` to the update payload
- Implementation:
```typescript
listen_address: editForm.value.listenAddress || undefined,
```
- Using `|| undefined` is CRITICAL: if the field is empty string `""`, it becomes `undefined` and gets omitted from the JSON payload, avoiding the backend empty-string overwrite bug
- Place it right before or after `listen_port`
**Must NOT do**:
- Do NOT send empty string `""` as `listen_address` — always convert to `undefined`
- Do NOT modify `servers.ts`
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Single line addition in existing function
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES (but must be after Task 1 for state variable)
- **Parallel Group**: Wave 1
- **Blocked By**: Task 1
**References**:
- `Servers.vue:318-343` — handleEditSave function
- `servers.ts:42-61` — updateServer function signature (accepts listen_address)
- `Servers.vue:328` — existing `listen_port: editForm.value.listenPort` line
**Acceptance Criteria**:
- [ ] handleEditSave sends `listen_address` in payload
- [ ] Empty listen_address field results in `undefined` (omitted from payload)
- [ ] Backend does NOT receive `listen_address: ""`
**QA Scenarios**:
```
Scenario: Update listen address via edit modal
Tool: Playwright
Preconditions: Logged in as admin, server exists
Steps:
1. Open edit modal, change Listen Address to "0.0.0.1"
2. Click Save
3. Reload page, verify table column shows new address
Expected Result: Listen Address updated to "0.0.0.1"
Evidence: .sisyphus/evidence/task-2-update-listen-address.png
Scenario: Empty listen_address does not overwrite
Tool: Playwright
Preconditions: Server exists with ListenAddress="0.0.0.0"
Steps:
1. Open edit modal, clear Listen Address field
2. Click Save
3. Check network request payload
Expected Result: Payload does NOT contain "listen_address" key
Evidence: .sisyphus/evidence/task-2-omit-empty.txt
```
**Evidence to Capture**:
- [ ] Screenshot: after updating listen address
- [ ] Network request log: payload verification
**Commit**: YES (group with Task 1)
---
- [x] 3. Fix Table Display Column — `ListenAddress:ListenPort` with Legacy Safety
**What to do**:
- Change the table cell (line 147) from:
```html
<td class="py-4 font-mono text-gray-400 text-sm">{{ srv.ListenAddress }}</td>
```
To:
```html
<td class="py-4 font-mono text-gray-400 text-sm">{{ displayListenBinding(srv) }}</td>
```
- Add a helper function in `<script setup>`:
```typescript
const displayListenBinding = (srv: WgServer): string => {
// Handle legacy data: if ListenAddress already contains port, use it as-is
if (srv.ListenAddress.includes(':')) {
return srv.ListenAddress
}
return `${srv.ListenAddress}:${srv.ListenPort}`
}
```
**Must NOT do**:
- Do NOT produce double-port (e.g., `0.0.0.0:51820:51820`)
- Do NOT crash if `ListenPort` is 0
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Template change + 5-line helper function
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocked By**: None
**References**:
- `Servers.vue:147` — Current display to change
- `Servers.vue:252` — `import { type WgServer }` already exists
**Acceptance Criteria**:
- [ ] Table column shows `0.0.0.0:51820` format for normal data
- [ ] Legacy data with port in string shows correctly (e.g., `192.168.1.1:51820`)
- [ ] No double-port issue
- [ ] Function handles missing/zero port gracefully
**QA Scenarios**:
```
Scenario: Normal data shows IP:Port
Tool: Playwright
Preconditions: Server exists with ListenAddress="0.0.0.0" and ListenPort=51820
Steps:
1. Navigate to Nodes page
2. Check the "Listen Binding" column
Expected Result: Cell shows "0.0.0.0:51820"
Evidence: .sisyphus/evidence/task-3-display-normal.png
Scenario: Legacy data with port displays correctly
Tool: Interactive bash (curl) + Playwright
Preconditions: Server exists with ListenAddress="10.0.0.1:51820" (legacy)
Steps:
1. Navigate to Nodes page
2. Check the column for that server
Expected Result: Cell shows "10.0.0.1:51820" (no double port)
Evidence: .sisyphus/evidence/task-3-display-legacy.png
```
**Evidence to Capture**:
- [ ] Screenshot: normal IP:Port display
- [ ] Screenshot: legacy data display
**Commit**: YES (group with Task 1)
---
- [x] 4. Rename Column Header from "Listen Binding"
**What to do**:
- Change line 134:
```html
<th class="pb-3">Listen Binding</th>
```
→ choose one of:
- `"Bind Address"` (clear, standard)
- `"Listen Address"` (matches form label)
- `"Listen Port"` (if showing port only)
- Since we're now displaying `IP:Port` in the cell, `"Bind Address"` is the most descriptive
**Must NOT do**:
- Do NOT use "Listen Binding" — it's ambiguous
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: One-line text change
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocked By**: None
**References**:
- `Servers.vue:134` — Current header text
**Acceptance Criteria**:
- [ ] Column header changed to clear label
- [ ] No broken layout
**QA Scenarios**:
```
Scenario: Column header displays new label
Tool: Playwright
Steps: Navigate to Nodes page, capture screenshot of table header
Expected Result: Header shows new label (e.g., "Bind Address")
Evidence: .sisyphus/evidence/task-4-column-header.png
```
**Evidence to Capture**:
- [ ] Screenshot: table header
**Commit**: YES (group with Task 1)
---
- [x] 5. Fix PreUp and PostDown — `<input>` → `<textarea>`
**What to do**:
- In Register form (line 75-76): Change PreUp from `<input>` to `<textarea rows="2">`
- In Register form (line 87-88): Change PostDown from `<input>` to `<textarea rows="2">`
- In Edit form (line 201-203): Change PreUp from `<input>` to `<textarea rows="2">`
- In Edit form (line 213-215): Change PostDown from `<input>` to `<textarea rows="2">`
- Keep the same TailwindCSS styling classes
- Match the existing `<textarea>` pattern from PostUp/PreDown (lines 79-84, 206-211)
**Must NOT do**:
- Do NOT change PostUp or PreDown (already are `<textarea>`)
- Do NOT change any other field types
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: 4 HTML element tag changes, identical pattern
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocked By**: None
**References**:
- `Servers.vue:75-76` — Register form PreUp (input → textarea)
- `Servers.vue:79-84` — Register form PostUp (textarea — existing pattern)
- `Servers.vue:87-88` — Register form PostDown (input → textarea)
- `Servers.vue:201-203` — Edit form PreUp
- `Servers.vue:213-215` — Edit form PostDown
**Acceptance Criteria**:
- [ ] PreUp and PostDown are `<textarea>` in both Register and Edit forms
- [ ] Multi-line text can be entered
- [ ] No visual regression (same styling as PostUp/PreDown)
**QA Scenarios**:
```
Scenario: All 4 script hook fields are textareas
Tool: Playwright
Steps:
1. Navigate to Nodes page
2. Click "Register Node"
3. Check PreUp, PostUp, PreDown, PostDown are all textareas
4. Cancel, click Edit on a server
5. Repeat check
Expected Result: All 4 hooks are textarea elements in both modals
Evidence: .sisyphus/evidence/task-5-script-textareas.png
```
**Evidence to Capture**:
- [ ] Screenshot: register modal with all 4 textareas
- [ ] Screenshot: edit modal with all 4 textareas
**Commit**: YES (group with Task 1)
---
- [x] 6. Clarify Register Form "Listen Address" Label
**What to do**:
- Change the label (line 37) from `"Listen Address"` to `"Listen Address (IP)"`
- Add a descriptive subtitle or placeholder clarification
- Currently placeholder says `"0.0.0.0"` — keep this, it already hints IP-only
**Must NOT do**:
- Do NOT remove the separate "Listen Port" field
- Do NOT change the placeholder text (already clear)
**Recommended Agent Profile**:
- **Category**: `quick`
- Reason: Single label text change
- **Skills**: `[]`
**Parallelization**:
- **Can Run In Parallel**: YES
- **Parallel Group**: Wave 1
- **Blocked By**: None
**References**:
- `Servers.vue:37` — Current label "Listen Address"
**Acceptance Criteria**:
- [ ] Label updated to clarify it's IP-only
**Evidence to Capture**:
- [ ] Screenshot: register form showing updated label
**Commit**: YES (group with Task 1)
---
## Final Verification Wave
- [x] F1. **Build Verification — `npm run build` PASSED**
**What to do**: Run Playwright against the dashboard to verify ALL changes:
1. Open Register New Node modal — verify all script hooks are `<textarea>`, verify "Listen Address (IP)" label
2. Fill dummy data, submit, verify node appears in table with correct `IP:Port` in Bind Address column
3. Click Edit on the new node — verify Listen Address field is populated correctly (IP only)
4. Change Listen Address, save — verify table updates
5. Clear Listen Address, save — verify it doesn't break
6. Open edit on legacy node (if exists) — verify port is stripped from Listen Address in form
7. Verify all 4 script hooks are textareas in edit modal too
8. Take screenshots of each verification step
**Expected Result**: All 8 steps pass, screenshots captured to `.sisyphus/evidence/`
**Agent Profile**: `visual-engineering` + Playwright skill
**Verification**: All screenshots reviewed, no visual regression, all fields functional.
---
## Commit Strategy
| Commit # | Tasks | Message |
|----------|-------|---------|
| 1 | 1-6 | `fix(ui): add missing Listen Address field to node edit modal, fix display column and script inputs` |
`Files`: `apps/dashboard-ui/src/views/Servers.vue`
---
## Success Criteria
### Verification Commands
```bash
cd apps/dashboard-ui
npx vue-tsc --noEmit # Expected: PASS (no type errors)
npm run build # Expected: PASS (build succeeds)
```
### Final Checklist
- [x] All 6 tasks complete
- [x] Edit modal has "Listen Address" field
- [x] Table column displays `IP:Port` correctly
- [x] Column header "Bind Address"
- [x] All 4 script hooks are `<textarea>`
- [x] Register form label clarified
- [x] No empty-string sent to API
- [x] `npm run build` passes
- [x] `vue-tsc --noEmit` passes