# 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
