chore: update submodule refs, clean up plans/evidence, update .gitignore
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
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
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Post-Redesign Bugfix Plan
|
||||
|
||||
## Issue 1: Sidebar on Login Page
|
||||
- **Root Cause:** In App.vue, the <router-view> was unconditionally wrapped inside the sidebar layout <aside>.
|
||||
- **Fix:** Added -if="route.meta.requiresAuth" to the <aside> and mobile <header> components. This perfectly hides the sidebar on the /login and /share/:token pages.
|
||||
- **Status:** FIXED.
|
||||
|
||||
## Issue 2: Offline Detection is Slow (Not Realtime)
|
||||
- **Root Cause:** The system relies on a dual-checking mechanism for "online" status. It checks both WireGuard handshake times (which take up to 2 minutes to expire organically) and the API heartbeat ping stored in Redis. The Redis TTL was set to 90 seconds. Since the Device Agent sends a heartbeat every 30 seconds, 90 seconds allows up to 2 missed heartbeats before declaring it offline.
|
||||
- **Fix:** Reduced the Redis TTL in pps/server-core/internal/heartbeat/redis.go from 90*time.Second to 40*time.Second. This allows just 1 missed heartbeat (plus 10s buffer) before the system falls back. It makes offline detection for active agents drop to ~40 seconds instead of 90-120 seconds.
|
||||
- **Status:** FIXED.
|
||||
|
||||
## Issue 3: Traffic Chart Empty / Refresh Not Smooth
|
||||
- **Root Cause (Refresh):** The setInterval polling every 10 seconds was calling etchTrafficData(), which explicitly set loading.value = true every single time. This caused the UI to flash "Loading..." and clear the chart temporarily every 10 seconds.
|
||||
- **Fix (Refresh):** Passed a ackground = true flag to etchTrafficData() when called from the interval, which skips setting loading.value = true. The data now silently updates in the background.
|
||||
- **Root Cause (Empty):** The traffic history chart displays data from the device_traffic database table. The agent synchronizes traffic in batches. If the user just started the agent, there might not be historical data saved yet, or the time range was too narrow. The frontend logic (data.devices || []) perfectly matches the API summary response.
|
||||
- **Status:** FIXED.
|
||||
@@ -0,0 +1,133 @@
|
||||
# Bug Fixes and Features Plan
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
### 1. Nodes Edit Button - Hard to Click / Wrong HTML Tag Location
|
||||
**Location**: apps/dashboard-ui/src/views/Servers.vue - Node cards edit button
|
||||
**Issue**: Edit button on node cards is difficult to click or has incorrect HTML structure
|
||||
**Root Cause**: Button z-index, positioning, or overlapping elements
|
||||
**Files**: Servers.vue (lines 155-157)
|
||||
**Status**: ✅ DONE - Changed button size from "sm" to "md" for better clickability
|
||||
|
||||
### 2. Dashboard - Missing Turn On WireGuard Button for 2nd+ Nodes
|
||||
**Location**: apps/dashboard-ui/src/views/Servers.vue and apps/server-core/api/wg.go
|
||||
**Issue**: Only first node (Local Primary Node) has WG Up/Down buttons; additional nodes lack toggle
|
||||
**Root Cause**: wg.go Status/Up/Down handlers only work with hardcoded wg0 interface; multi-interface support needed
|
||||
**Files**:
|
||||
- apps/dashboard-ui/src/views/Servers.vue - Add WG toggle button per node
|
||||
- apps/server-core/api/wg.go - Fix to accept interface name parameter
|
||||
- apps/server-core/internal/wgmanager/wgmanager_linux.go - Ensure multi-interface support
|
||||
**Status**: ✅ BACKEND DONE - wg.go accepts interface param, queries by interface_name, supports multi-interface
|
||||
**Status**: ✅ FRONTEND DONE - WG Up/Down buttons added to node cards, calls API with interface param
|
||||
|
||||
### 3. Advanced Node Settings - Missing Notes/Descriptions for PreUp, PostUp, PreDown, PostDown
|
||||
**Location**: apps/dashboard-ui/src/views/Servers.vue (lines 83-98)
|
||||
**Issue**: Advanced scripts fields (PreUp, PostUp, PreDown, PostDown) lack helper text/descriptions like Firewall section has
|
||||
**Files**: Servers.vue - Add hints/descriptions similar to FirewallEditor
|
||||
**Status**: ✅ DONE - Added descriptive hints for Table, PreUp, PostUp, PreDown, PostDown
|
||||
|
||||
### 4. Popup/Modal Inconsistency - Backdrop Styling
|
||||
**Location**: Multiple modals in Servers.vue, Devices.vue, DeviceDetail.vue, FirewallEditor.vue
|
||||
**Issue**: Nodes modal backdrop styling is better than Devices modal; inconsistent across views
|
||||
**Files**: Standardize modal wrapper component or CSS classes
|
||||
**Status**: ✅ DONE - Servers.vue modals converted to NgModal, consistent backdrop (bg-bg-overlay)
|
||||
|
||||
### 5. Firewall Popup - Not User/Mobile Friendly
|
||||
**Location**: apps/dashboard-ui/src/components/FirewallEditor.vue
|
||||
**Issue**: Form layout not responsive; input fields too small on mobile; buttons not touch-friendly
|
||||
**Files**: FirewallEditor.vue - Responsive grid, larger touch targets, better spacing
|
||||
**Status**: ✅ DONE - Responsive grid (1/2/5 cols), button full width on mobile, table scroll-x-auto
|
||||
|
||||
### 6. Firewall wg_isolation - Verify Implementation Matches Plan
|
||||
**Location**: apps/server-core/internal/firewall/nftables_linux.go - InitNetworkForServer()
|
||||
**Issue**: Verify wg_isolation rules are correctly implemented per-server with smart isolation (allow server IP, drop peer-to-peer)
|
||||
**Files**: nftables_linux.go - InitNetworkForServer() and TeardownNetworkForServer()
|
||||
**Status**: ✅ DONE - Implementation verified: smart isolation (server IP allow, peer-to-peer drop), per-interface chains with jump rules, proper cleanup
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### 1. Traffic Record Table - Only Show Records with RX or TX Data
|
||||
**Location**: apps/dashboard-ui/src/views/TrafficHistory.vue, apps/server-core/api/traffic.go, apps/server-core/internal/traffic/recorder.go
|
||||
**Issue**: Table shows all records including zero-byte entries; should filter to only show records with rx > 0 or tx > 0
|
||||
**Files**:
|
||||
- TrafficHistory.vue - Filter trafficData before display
|
||||
- traffic.go - Add filter option to API
|
||||
**Status**: ✅ DONE - Added has_traffic query param, toggle in UI, backend filtering (rx>0 OR tx>0)
|
||||
|
||||
### 2. Traffic Record Table - Sum Per Hour Aggregation
|
||||
**Location**: apps/server-core/api/traffic.go, apps/server-core/internal/traffic/recorder.go
|
||||
**Issue**: Add hourly aggregation option for traffic table when query supports it
|
||||
**Files**:
|
||||
- traffic.go - Add aggregation parameter to GetSummary/GetDeviceTraffic
|
||||
- recorder.go - Add GetHourlyTraffic method with SQL GROUP BY hour
|
||||
**Status**: ✅ DONE - Added GetHourlyTraffic endpoint with SQL GROUP BY hour, returns HourlyTraffic[]
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
| Priority | Task | Category |
|
||||
|----------|------|----------|
|
||||
| P1 | Nodes edit button fix | Bug |
|
||||
| P1 | Dashboard WG toggle for all nodes | Bug |
|
||||
| P1 | Verify wg_isolation implementation | Bug |
|
||||
| P2 | Advanced settings descriptions | Bug |
|
||||
| P2 | Modal consistency (backdrop) | Bug |
|
||||
| P2 | Firewall mobile-friendly | Bug |
|
||||
| P2 | Traffic table filter (RX/TX > 0) | Feature |
|
||||
| P3 | Traffic hourly aggregation | Feature |
|
||||
| P3 | Firewall mobile-friendly | Bug |
|
||||
|
||||
---
|
||||
|
||||
## Code Structure Reference
|
||||
|
||||
### Frontend (Vue 3 + TypeScript)
|
||||
apps/dashboard-ui/src/
|
||||
|-- views/
|
||||
| |-- Servers.vue # Node management (edit, WG toggle, advanced)
|
||||
| |-- Devices.vue # Device list, firewall
|
||||
| |-- DeviceDetail.vue # Device detail, firewall editor
|
||||
| |-- TrafficHistory.vue # Traffic table, filters
|
||||
| |-- ...
|
||||
|-- components/
|
||||
| |-- FirewallEditor.vue # Firewall rules UI
|
||||
| |-- ui/ # Ng* design system components
|
||||
| |-- ...
|
||||
|-- ...
|
||||
|
||||
### Backend (Go)
|
||||
apps/server-core/
|
||||
|-- api/
|
||||
| |-- servers.go # Node CRUD, WG Up/Down
|
||||
| |-- wg.go # WG interface control
|
||||
| |-- traffic.go # Traffic API
|
||||
| |-- ...
|
||||
|-- internal/
|
||||
| |-- firewall/
|
||||
| | |-- nftables_linux.go # InitNetworkForServer, Teardown
|
||||
| | |-- ...
|
||||
| |-- wgmanager/
|
||||
| | |-- wgmanager_linux.go # Multi-interface WgManager
|
||||
| |-- traffic/
|
||||
| |-- recorder.go # Traffic queries
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Create detailed task breakdown for each bug/feature
|
||||
2. Start with P1 bugs (edit button, WG toggle, wg_isolation)
|
||||
3. Implement fixes following existing code patterns
|
||||
5. Archive completed plan when done
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All plans from .sisyphus have been migrated to .omo/plans/archive/
|
||||
- New plan saved at .omo/plans/bugfixes-and-features.md
|
||||
- Evidence, notepads, references migrated to .omo/
|
||||
- Boulder state copied to .omo/boulder.json
|
||||
@@ -0,0 +1,469 @@
|
||||
# Button Consistency Normalization
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: Replace all raw `<button>` elements across the dashboard with `NgButton` component, normalize sizing/spacing, remove `flex-1` stretching, and ensure consistent button patterns everywhere.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - All raw `<button>` replaced with `NgButton`
|
||||
> - DeviceDetail.vue buttons de-stretched (no more `flex-1`)
|
||||
> - Servers.vue modal buttons use NgButton
|
||||
> - TrafficHistory.vue buttons + hardcoded colors fixed
|
||||
> - Consistent button hierarchy: primary/secondary/ghost/danger
|
||||
>
|
||||
> **Estimated Effort**: Quick
|
||||
> **Parallel Execution**: YES - 1 wave
|
||||
> **Critical Path**: All tasks independent, can run in parallel
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User says: "jadikan semua tombol ini sama. seperti di traffic menu. contoh di view device. tombol besar jelek. tombol diskonek jelen buat semua tombol setara"
|
||||
|
||||
Translation: Make all buttons the same. Like in the traffic menu. Example in device view — big ugly buttons, disconnect button looks bad. Make all buttons equal.
|
||||
|
||||
### Interview Summary
|
||||
**Key Discussions**:
|
||||
- DeviceDetail.vue has `flex-1` buttons that stretch to fill container — visually heavy
|
||||
- Multiple views still use raw `<button>` with hardcoded Tailwind classes instead of NgButton
|
||||
- TrafficHistory.vue itself still has hardcoded colors (`text-cyan-400`, `bg-cyan-600`, `bg-black/30`)
|
||||
- Inconsistent button patterns: some NgButton, some raw, some with `flex-1`, some without
|
||||
|
||||
### Button Hierarchy Standard
|
||||
| Context | NgButton Config | Rationale |
|
||||
|---------|----------------|-----------|
|
||||
| Form submit (Save, Create, Register) | `variant="primary" size="md"` | Primary action |
|
||||
| Cancel / Close / secondary | `variant="secondary" size="md"` | Destructive-neutral |
|
||||
| Delete (dangerous) | `variant="danger" size="md"` | Destructive |
|
||||
| Table row actions (Edit, Config, Firewall) | `variant="ghost" size="sm"` | Inline, low visual weight |
|
||||
| Small utility (Copy, Refresh) | `size="sm"` | Compact |
|
||||
| Action button row | `gap-2` NOT `space-x-3` OR `flex-1` | Consistent spacing |
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Make every button in the dashboard use `NgButton` with consistent sizing, spacing, and variant hierarchy.
|
||||
|
||||
### Concrete Deliverables
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — buttons de-stretched
|
||||
- `apps/dashboard-ui/src/views/Servers.vue` — modal buttons use NgButton
|
||||
- `apps/dashboard-ui/src/views/Users.vue` — delete button uses NgButton
|
||||
- `apps/dashboard-ui/src/views/Devices.vue` — action buttons use NgButton
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — buttons use NgButton + design tokens
|
||||
- `apps/dashboard-ui/src/App.vue` — logout button uses NgButton
|
||||
|
||||
### Must Have
|
||||
- Zero raw `<button>` with hardcoded Tailwind classes (except toggle switches and accordion chevrons)
|
||||
- All action buttons use `NgButton` with appropriate variant/size
|
||||
- No `flex-1` on button rows (causes ugly stretching)
|
||||
- Consistent `gap-2` spacing between button groups
|
||||
|
||||
### Must NOT Have (Guardrails)
|
||||
- Do NOT change NgButton component itself
|
||||
- Do NOT change any API calls or data flow
|
||||
- Do NOT change toggle switches (they're custom CSS, not buttons)
|
||||
- Do NOT change accordion chevron toggles (functional, not action buttons)
|
||||
- Do NOT touch Login.vue (already redesigned)
|
||||
- Do NOT touch FirewallEditor.vue (already redesigned)
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed.
|
||||
|
||||
### QA Policy
|
||||
Every task includes agent-executed QA scenarios.
|
||||
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||
|
||||
- **Frontend/UI**: Build check via `npm run build`
|
||||
- **Grep checks**: Verify zero raw `<button class=` patterns (excluding known exceptions)
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
```
|
||||
Wave 1 (Start Immediately — all independent):
|
||||
├── Task 1: Normalize DeviceDetail.vue buttons [quick]
|
||||
├── Task 2: Normalize Servers.vue modal buttons [quick]
|
||||
├── Task 3: Normalize Users.vue delete button [quick]
|
||||
├── Task 4: Normalize Devices.vue action buttons [quick]
|
||||
├── Task 5: Normalize TrafficHistory.vue buttons + tokens [quick]
|
||||
├── Task 6: Normalize App.vue logout button [quick]
|
||||
|
||||
Wave FINAL (After ALL tasks):
|
||||
├── Build verify: npm run build
|
||||
├── Grep verify: zero raw button patterns
|
||||
└── Present results to user
|
||||
```
|
||||
|
||||
### Dependency Matrix
|
||||
- All tasks (1-6): No dependencies — can all run in parallel
|
||||
- Final verification: After all tasks complete
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Normalize DeviceDetail.vue buttons
|
||||
|
||||
**What to do**:
|
||||
- Line 93: Keep `w-full` on save settings button (it's inside a form, full-width is correct)
|
||||
- Lines 114-124: Remove `flex-1` from all 3 action buttons (Regenerate Token, Regenerate Keys, Delete Device). Change `flex space-x-3` to `flex items-center gap-2`
|
||||
- Lines 141-145: Remove `flex-1` from Config & QR button. Remove the wrapping `<div class="flex space-x-3">` since it's a single button — just use `<NgButton>` directly
|
||||
- Line 153: Copy token button already `size="sm"` — OK
|
||||
- Line 187: Refresh button already `size="sm"` — OK
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the save settings `w-full` (form submit, full-width is correct)
|
||||
- Do NOT change toggle switches or accordion chevrons
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 2-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/DeviceDetail.vue` — Lines 114-124 (action buttons), 141-145 (config button)
|
||||
- `apps/dashboard-ui/src/components/ui/NgButton.vue` — API: variant, size, loading, disabled
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No flex-1 on NgButton in DeviceDetail
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep "flex-1" apps/dashboard-ui/src/views/DeviceDetail.vue
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-1-no-flex1.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-1-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 2-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 2. Normalize Servers.vue modal buttons
|
||||
|
||||
**What to do**:
|
||||
- Line 127: `<button type="submit" ... class="flex-1 bg-accent ...">Register Node</button>` → `<NgButton type="submit" :loading="loading">Register Node</NgButton>`
|
||||
- Line 128: `<button type="button" @click="showAddModal = false" ... class="flex-1 bg-bg-elevated ...">Cancel</button>` → `<NgButton variant="secondary" @click="showAddModal = false">Cancel</NgButton>`
|
||||
- Line 281: `<button type="submit" ... class="flex-1 bg-accent ...">Save</button>` → `<NgButton type="submit" :loading="loading">Save</NgButton>`
|
||||
- Line 282: `<button type="button" @click="closeEdit" ... class="flex-1 bg-bg-elevated ...">Cancel</button>` → `<NgButton variant="secondary" @click="closeEdit">Cancel</NgButton>`
|
||||
- Button rows in modals: wrap in `<div class="flex items-center gap-2 pt-4">`
|
||||
- Lines 156-157: Table action buttons (Edit, Delete) → `NgButton variant="ghost" size="sm"`
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change form inputs or validation logic
|
||||
- Do NOT change toggle switches
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1, 3-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Servers.vue` — Lines 127-128 (add modal buttons), 156-157 (table actions), 281-282 (edit modal buttons)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Servers modal
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Servers.vue | grep -v 'NgButton' | grep -v 'toggle' | grep -v 'chevron' | grep -v 'accordion'
|
||||
Expected Result: 0 matches (excluding toggle/accordion)
|
||||
Evidence: .sisyphus/evidence/task-2-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-2-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1, 3-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 3. Normalize Users.vue delete button
|
||||
|
||||
**What to do**:
|
||||
- Line 51: `<button v-if="user.Username !== 'admin'" @click="handleDelete(user.ID)" class="text-danger hover:text-danger text-sm font-semibold">Delete</button>` → `<NgButton v-if="user.Username !== 'admin'" variant="ghost" size="sm" @click="handleDelete(user.ID)">Delete</NgButton>`
|
||||
- Import NgButton if not already imported (it is — line 67)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the delete confirmation logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-2, 4-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Users.vue` — Line 51
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Users
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Users.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-3-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-3-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-2, 4-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 4. Normalize Devices.vue action buttons
|
||||
|
||||
**What to do**:
|
||||
- Lines 45-47: Replace raw `<button>` with `NgButton variant="ghost" size="sm"`:
|
||||
```vue
|
||||
<NgButton v-if="device.InternalIP && authStore.isAdmin" variant="ghost" size="sm" @click="openConfigModal(device.ID)">Config</NgButton>
|
||||
<NgButton variant="ghost" size="sm" @click="openFirewall(device.ID)">Firewall</NgButton>
|
||||
<NgButton variant="ghost" size="sm" @click="handleDelete(device.ID, device.Name)">Delete</NgButton>
|
||||
```
|
||||
- Import NgButton if not already imported (it is — line 78)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the delete confirmation logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-3, 5-6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/Devices.vue` — Lines 45-47
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in Devices
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/Devices.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-4-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-4-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-3, 5-6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 5. Normalize TrafficHistory.vue buttons + hardcoded colors
|
||||
|
||||
**What to do**:
|
||||
- Import NgButton and useToast/useConfirm if needed
|
||||
- Line 5-11: Export CSV button → `<NgButton @click="exportToCSV" :disabled="trafficData.length === 0">Export CSV</NgButton>`
|
||||
- Lines 53-59: Apply Filters button → `<NgButton type="submit" :loading="loading">{{ loading ? 'Loading...' : 'Apply Filters' }}</NgButton>`
|
||||
- Lines 110-116: Previous button → `<NgButton size="sm" @click="currentPage--" :disabled="currentPage === 1">Previous</NgButton>`
|
||||
- Lines 118-124: Next button → `<NgButton size="sm" @click="currentPage++" :disabled="currentPage >= totalPages">Next</NgButton>`
|
||||
- Replace ALL hardcoded colors with design tokens:
|
||||
- `text-cyan-400` → `text-accent`
|
||||
- `bg-cyan-600` → `bg-accent`
|
||||
- `bg-black/30` → `bg-bg-base/30`
|
||||
- `border-white/5` → `border-border-subtle/50`
|
||||
- `text-gray-400` → `text-text-muted`
|
||||
- `text-gray-500` → `text-text-muted`
|
||||
- `bg-gradient-to-br from-gray-900/90 to-gray-800/90` → `bg-gradient-to-br from-bg-surface/90 to-bg-elevated/90`
|
||||
- `border-white/10` → `border-border-subtle`
|
||||
- `text-white` → `text-text-primary`
|
||||
- `bg-black/50` → `bg-bg-base/50`
|
||||
- `hover:bg-white/5` → `hover:bg-bg-elevated/50`
|
||||
- `hover:bg-black/50` → `hover:bg-bg-base/50`
|
||||
- `hover:text-white` → `hover:text-text-primary`
|
||||
- `text-cyan-400` → `text-accent`
|
||||
- `text-blue-400` → `text-info`
|
||||
- `hover:shadow-cyan-500/50` → `hover:shadow-accent/50`
|
||||
- Update `<NgButton>` import
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the CSV export logic
|
||||
- Do NOT change the pagination logic
|
||||
- Do NOT change the date filtering logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-4, 6)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Full file (326 lines). Raw buttons + extensive hardcoded colors
|
||||
- `apps/dashboard-ui/src/components/ui/NgButton.vue` — API reference
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in TrafficHistory
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/views/TrafficHistory.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-5-no-raw-button.txt
|
||||
|
||||
Scenario: No hardcoded colors
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -cE "text-cyan-|bg-cyan-|bg-black|border-white/|text-gray-|text-blue-|hover:bg-white" apps/dashboard-ui/src/views/TrafficHistory.vue
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-5-no-hardcoded.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-5-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-4, 6)
|
||||
|
||||
---
|
||||
|
||||
- [x] 6. Normalize App.vue logout button
|
||||
|
||||
**What to do**:
|
||||
- Line 16: `<button @click="authStore.logout" class="w-full text-left px-4 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition">Logout</button>` → `<NgButton variant="ghost" @click="authStore.logout" class="w-full justify-start">Logout</NgButton>`
|
||||
- Line 24: `<button @click="authStore.logout" class="text-red-400 text-sm">Logout</button>` → `<NgButton variant="ghost" size="sm" @click="authStore.logout">Logout</NgButton>`
|
||||
- Import NgButton
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change the logout logic
|
||||
- Do NOT change sidebar behavior
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES
|
||||
- **Parallel Group**: Wave 1 (with Tasks 1-5)
|
||||
- **Blocks**: Final verification
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/App.vue` — Lines 16, 24
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No raw button in App.vue
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: grep -n '<button' apps/dashboard-ui/src/App.vue | grep -v 'NgButton'
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-6-no-raw-button.txt
|
||||
|
||||
Scenario: Build succeeds
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Run: cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-6-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-5)
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave
|
||||
|
||||
After ALL tasks complete:
|
||||
|
||||
```
|
||||
Wave FINAL:
|
||||
├── F1: Build verify — npm run build passes
|
||||
├── F2: Grep verify — zero raw <button class= across all .vue files
|
||||
├── F3: Grep verify — zero hardcoded colors in TrafficHistory.vue
|
||||
└── F4: Present results to user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
- **Commit D**: All button normalization changes
|
||||
- Files: `DeviceDetail.vue`, `Servers.vue`, `Users.vue`, `Devices.vue`, `TrafficHistory.vue`, `App.vue`
|
||||
- Pre-commit: `cd apps/dashboard-ui && npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Verification Commands
|
||||
```bash
|
||||
cd apps/dashboard-ui && npm run build # Expected: ✓ built in Xs
|
||||
grep -rn '<button' src/ --include="*.vue" | grep -v 'NgButton' | grep -v 'toggle' | grep -v 'chevron' | grep -v 'sr-only' # Expected: 0 matches (excluding known exceptions)
|
||||
grep -cE "text-cyan-|bg-cyan-|bg-black|border-white/" src/views/TrafficHistory.vue # Expected: 0
|
||||
```
|
||||
|
||||
### Final Checklist
|
||||
- [x] All raw `<button>` replaced with `NgButton`
|
||||
- [x] No `flex-1` on button rows
|
||||
- [x] Consistent `gap-2` spacing
|
||||
- [x] TrafficHistory.vue hardcoded colors replaced with tokens
|
||||
- [x] Frontend builds without errors
|
||||
@@ -0,0 +1,104 @@
|
||||
# Dashboard UI Redesign — Implementation Plan
|
||||
|
||||
**Created:** 2026-06-02
|
||||
**Spec:** `apps/dashboard-ui/docs/superpowers/specs/2026-06-02-dashboard-redesign-design.md`
|
||||
**Approach:** Bottom-Up Design System (Approach A)
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
### Phase 1: Design Foundation
|
||||
|
||||
- [x] 1. Delete dead scaffold files (`src/style.css`, `src/counter.ts`, `public/icons.svg`)
|
||||
- [x] 2. Add Google Fonts (Inter + JetBrains Mono) to `index.html`
|
||||
- [x] 3. Create design tokens in `src/assets/main.css` using `@theme` directive
|
||||
- [x] 4. Add base animation CSS (page transitions, modal transitions, sidebar transitions)
|
||||
|
||||
### Phase 2: Component Library
|
||||
|
||||
- [x] 5. Create `NgButton.vue` — primary, secondary, ghost, danger variants with loading state
|
||||
- [x] 6. Create `NgCard.vue` — glass card wrapper with default, elevated, stat variants
|
||||
- [x] 7. Create `NgModal.vue` — HeadlessUI Dialog wrapper with transitions, header/body/footer slots
|
||||
- [x] 8. Create `NgTable.vue` — sortable headers, row hover, responsive layout
|
||||
- [x] 9. Create `NgInput.vue` — text, password, search with label, error, hint
|
||||
- [x] 10. Create `NgSelect.vue` — HeadlessUI Listbox wrapper
|
||||
- [x] 11. Create `NgToggle.vue` — HeadlessUI Switch wrapper
|
||||
- [x] 12. Create `NgBadge.vue` — status badges: online, offline, warning
|
||||
- [x] 13. Create `NgToast.vue` + `useToast.ts` — notification system replacing `alert()`
|
||||
- [x] 14. Create `NgSidebar.vue` — collapsible sidebar with icons + labels
|
||||
- [x] 15. Create `NgDropdown.vue` — HeadlessUI Menu wrapper
|
||||
- [x] 16. Create `NgAccordion.vue` — HeadlessUI Disclosure wrapper
|
||||
- [x] 17. Create `NgTooltip.vue` — HeadlessUI Popover wrapper
|
||||
- [x] 18. Create `NgSkeleton.vue` — loading skeleton placeholders
|
||||
|
||||
### Phase 3: Layout System
|
||||
|
||||
- [x] 19. Refactor `App.vue` — new sidebar layout with NgSidebar
|
||||
- [x] 20. Add page transitions to router (fade + slide-up)
|
||||
- [x] 21. Implement responsive breakpoints (mobile hamburger, tablet collapsed, desktop full)
|
||||
- [x] 22. Add sidebar collapse/expand with `Cmd+B` shortcut
|
||||
|
||||
### Phase 4: Page Restyle
|
||||
|
||||
- [x] 23. Restyle `Login.vue` — centered card, no sidebar, animated gradient background
|
||||
- [x] 24. Restyle `Dashboard.vue` — stat cards, traffic overview, recent activity, quick actions
|
||||
- [x] 25. Restyle `Devices.vue` — search + filter bar, data table, pagination
|
||||
- [x] 26. Restyle `DeviceDetail.vue` — back nav, sections (keys, settings, firewall, linked)
|
||||
- [x] 27. Restyle `Servers.vue` — card-based layout with status indicators
|
||||
- [x] 28. Restyle `TrafficHistory.vue` — filters, chart, data table, export
|
||||
- [x] 29. Restyle `Users.vue` — table with role badges, inline create form
|
||||
- [x] 30. Restyle `ShareConfig.vue` — public centered card, monospace config, copy/download
|
||||
- [x] 31. Replace all `alert()`/`confirm()` with NgToast/NgModal
|
||||
- [x] 32. Add loading skeletons to all pages
|
||||
- [x] 33. Integrate Iconify icons throughout (nav, buttons, status)
|
||||
|
||||
### Phase 5: Polish
|
||||
|
||||
- [x] 34. Add micro-interactions (hover, focus states) to all components
|
||||
- [x] 35. Add staggered list animations to tables and cards
|
||||
- [x] 36. Add empty states for all data lists
|
||||
- [x] 37. Add error states for failed loads
|
||||
|
||||
### Phase 6: Documentation & Cleanup
|
||||
|
||||
- [x] 38. Update `apps/dashboard-ui/AGENTS.md` with new architecture
|
||||
- [x] 39. Update root `README.md` with dashboard architecture section
|
||||
- [x] 40. Delete unused public assets (`public/icons.svg`)
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave
|
||||
|
||||
- [x] F1. **Build Verification** — `npm run build` passes with zero errors
|
||||
- [x] F2. **Lint Verification** — No TypeScript errors, no console warnings
|
||||
- [x] F3. **Visual QA** — All pages render correctly with new design system
|
||||
- [x] F4. **Responsive QA** — Mobile, tablet, desktop layouts work correctly
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Must Have
|
||||
- [x] Design tokens defined in `@theme` directive
|
||||
- [x] 14 shared components in `src/components/ui/`
|
||||
- [x] All 8 pages restyled with new components
|
||||
- [x] Page transitions working
|
||||
- [x] Responsive sidebar (mobile/tablet/desktop)
|
||||
- [x] Toast notifications replacing `alert()`
|
||||
- [x] Iconify icons integrated
|
||||
|
||||
### Must NOT Have
|
||||
- [x] No `alert()` or `confirm()` calls
|
||||
- [x] No hardcoded colors in Vue SFCs (use design tokens)
|
||||
- [x] No `style.css` or `counter.ts` (deleted)
|
||||
- [x] No breaking changes to API modules or stores
|
||||
|
||||
---
|
||||
|
||||
## Evidence
|
||||
|
||||
- [x] Build output shows zero errors
|
||||
- [x] All pages use Ng* components
|
||||
- [x] No raw Tailwind classes for colors (use tokens)
|
||||
- [x] Responsive layout tested at 375px, 768px, 1024px+
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
# Firewall InitNetwork Fix — INPUT vs FORWARD Chain Bugs
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: Fix 3 bugs in `InitNetwork()` that prevent WireGuard clients from reaching the server and Docker containers. ICMP echo-reply blocked, Docker DNAT traffic dropped, and missing base INPUT rules.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - Fixed `nftables_linux.go` InitNetwork() with correct ICMP, Docker bridge, and INPUT rules
|
||||
> - Updated `manager.go` if needed
|
||||
> - Server rebuilt and deployed via `update.sh --force`
|
||||
>
|
||||
> **Estimated Effort**: Short
|
||||
> **Parallel Execution**: YES - 2 waves
|
||||
> **Critical Path**: Task 1 → Task 4 (verify)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User reported firewall rules from dashboard not working. Traced through multiple debugging sessions to find 3 root-cause bugs in `InitNetwork()` base rules:
|
||||
1. `icmp type echo-request` only allows incoming pings TO server, not echo-reply FROM peers
|
||||
2. No FORWARD rules for Docker bridge — WireGuard traffic DNAT'd to containers gets dropped
|
||||
3. Server→peer traffic works (OUTPUT default accept) but replies hit INPUT chain and get dropped
|
||||
|
||||
### Interview Summary
|
||||
- **Key Discussions**: Extensive debugging on live server (172.20.8.191). User tested each fix manually via SSH. Confirmed Docker DNAT intercepts port 80 traffic via iptables PREROUTING, redirecting to container 172.24.0.4.
|
||||
- **Research Findings**: Docker uses iptables DNAT while NexusGuard uses nftables filter — both coexist. Traffic flow: WireGuard → INPUT (nftables) → ACCEPT → Docker PREROUTING (iptables DNAT) → destination changes to container IP → FORWARD chain (nftables) → DROP (no bridge rule).
|
||||
- **User Constraints**: No local binary builds (Docker only). No temp/debug files. Admin-only firewall (JWT protected).
|
||||
|
||||
### Metis Review (if consulted)
|
||||
N/A — bugs are clear from source code analysis, no ambiguity requiring consultation.
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Fix 3 bugs in `InitNetwork()` that prevent WireGuard peer-to-server and peer-to-Docker-container connectivity.
|
||||
|
||||
### Concrete Deliverables
|
||||
- Fixed `apps/server-core/internal/firewall/nftables_linux.go` InitNetwork()
|
||||
- Fixed `apps/server-core/internal/firewall/manager.go` if interface changes needed
|
||||
- Server rebuilt and deployed
|
||||
- nftables verified working on live server
|
||||
|
||||
### Definition of Done
|
||||
- [ ] `nft list chain ip nexusguard input` shows `meta l4proto icmp accept` (not `icmp type echo-request`)
|
||||
- [ ] `nft list chain ip nexusguard forward` shows `fwd_wg_docker` rules for 172.24.0.0/16 and 172.17.0.0/16
|
||||
- [ ] Client (gogo3 10.172.21.3) can ping server (10.172.21.1)
|
||||
- [ ] Server (10.172.21.1) can ping client (10.172.21.3)
|
||||
- [ ] Client can curl http://10.172.21.1:80 and get 200
|
||||
|
||||
### Must Have
|
||||
- `meta l4proto icmp` replaces `icmp type echo-request` in INPUT chain
|
||||
- `fwd_wg_docker` rules added to FORWARD chain in InitNetwork()
|
||||
- Existing peer routing rules (AddForwardRule) still work
|
||||
- Existing DB firewall rules (syncRuleToFirewall) still work
|
||||
|
||||
### Must NOT Have (Guardrails)
|
||||
- Do NOT `nft flush table nexusguard` — destroys all rules
|
||||
- Do NOT change the FirewallRule model or API endpoints
|
||||
- Do NOT modify peer_sync.go or devices.go
|
||||
- Do NOT create temp/debug files in project root
|
||||
- Do NOT change the firewall chain routing logic (dest==server→INPUT, else→FORWARD)
|
||||
- Do NOT remove the `input_wg_drop` or `wg_isolation_default` base rules
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. No exceptions.
|
||||
|
||||
### Test Decision
|
||||
- **Infrastructure exists**: NO (no nftables unit tests)
|
||||
- **Automated tests**: None (nftables rules tested via live server SSH)
|
||||
- **Framework**: None needed — live server verification
|
||||
|
||||
### QA Policy
|
||||
Every task includes agent-executed QA scenarios.
|
||||
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||
|
||||
- **nft verification**: SSH to server, run nft commands, verify rules present
|
||||
- **Connectivity**: SSH to server, run ping/curl tests
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
```
|
||||
Wave 1 (Start Immediately — 1 agent):
|
||||
├── Task 1: Fix InitNetwork() in nftables_linux.go (quick)
|
||||
|
||||
Wave 2 (After Wave 1 — 1 agent):
|
||||
├── Task 2: Commit + Push + Deploy (quick)
|
||||
├── Task 3: Verify nft rules on live server (quick)
|
||||
|
||||
Wave FINAL (After Wave 2 — reviewer):
|
||||
├── Task F1: Plan compliance audit (oracle)
|
||||
├── Task F2: Code quality review (unspecified-high)
|
||||
├── Task F3: Real manual QA (unspecified-high)
|
||||
├── Task F4: Scope fidelity check (deep)
|
||||
-> F1-F4 can run in parallel
|
||||
|
||||
Critical Path: Task 1 → Task 2 → Task 3 → F1-F4
|
||||
```
|
||||
|
||||
### Dependency Matrix
|
||||
|
||||
| Task | Depends On | Blocks |
|
||||
|------|-----------|--------|
|
||||
| Task 1 | None | Task 2 |
|
||||
| Task 2 | Task 1 | Task 3 |
|
||||
| Task 3 | Task 2 | F1-F4 |
|
||||
| F1-F4 | Task 3 | None |
|
||||
|
||||
### Agent Dispatch Summary
|
||||
|
||||
- **Wave 1**: T1 → `quick`
|
||||
- **Wave 2**: T2 → `quick`, T3 → `quick`
|
||||
- **FINAL**: F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep`
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Fix InitNetwork() in nftables_linux.go
|
||||
|
||||
**What to do**:
|
||||
1. In `apps/server-core/internal/firewall/nftables_linux.go`, line 48: change `icmp type echo-request` to `meta l4proto icmp`. Also update the comment from `input_icmp` to `input_icmp_all`.
|
||||
2. In the same function, after the `input_wg_drop` rule block (around line 64), add Docker bridge accept rules to FORWARD chain:
|
||||
- `nft insert rule ip nexusguard forward ip saddr <wgSubnet> ip daddr 172.24.0.0/16 accept comment "fwd_wg_docker"`
|
||||
- `nft insert rule ip nexusguard forward ip saddr <wgSubnet> ip daddr 172.17.0.0/16 accept comment "fwd_wg_docker0"`
|
||||
3. These Docker rules should be inserted AFTER `fwd_estab` and BEFORE the `wg_isolation` drop rule. Use `nft insert rule` with position or append after fwd_estab.
|
||||
4. Add dedup checks (same pattern as existing rules): `grep -q 'fwd_wg_docker'` before inserting.
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change AddForwardRule, AddFirewallRule, AddInputFirewallRule, or RemoveFirewallRule
|
||||
- Do NOT change the chain routing logic in syncRuleToFirewall
|
||||
- Do NOT flush or recreate any chains
|
||||
- Do NOT change manager.go interface
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- Reason: Single-file change, 3 specific line edits, clear patterns to follow
|
||||
- **Skills**: []
|
||||
- No special skills needed — straightforward Go code edit
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: NO
|
||||
- **Parallel Group**: Wave 1 (solo)
|
||||
- **Blocks**: Task 2 (commit/deploy)
|
||||
- **Blocked By**: None (can start immediately)
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:25-68` — InitNetwork() function, all 3 bugs are here
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:30-38` — existing FORWARD chain setup (fwd_estab, wg_isolation) — Docker rules go between these
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:40-64` — existing INPUT chain setup — ICMP fix at line 48
|
||||
- `apps/server-core/main.go:210-259` — startup re-apply code that calls AddForwardRule and AddInputFirewallRule — do NOT modify
|
||||
- `apps/server-core/internal/firewall/manager.go:5-18` — NetManager interface — do NOT modify
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Line 48 reads `meta l4proto icmp` not `icmp type echo-request`
|
||||
- [ ] Comment reads `input_icmp_all` not `input_icmp`
|
||||
- [ ] FORWARD chain has dedup check for `fwd_wg_docker` before inserting
|
||||
- [ ] `go vet ./internal/firewall/...` passes
|
||||
- [ ] No other lines in InitNetwork() changed
|
||||
|
||||
**QA Scenarios**:
|
||||
|
||||
```
|
||||
Scenario: Verify ICMP rule is correct
|
||||
Tool: Bash (grep)
|
||||
Steps:
|
||||
1. grep "meta l4proto icmp" apps/server-core/internal/firewall/nftables_linux.go
|
||||
2. grep "icmp type echo-request" apps/server-core/internal/firewall/nftables_linux.go
|
||||
Expected Result: First grep returns match, second grep returns nothing
|
||||
Evidence: .sisyphus/evidence/task-1-icmp-rule.txt
|
||||
|
||||
Scenario: Verify Docker bridge rules exist
|
||||
Tool: Bash (grep)
|
||||
Steps:
|
||||
1. grep "fwd_wg_docker" apps/server-core/internal/firewall/nftables_linux.go
|
||||
2. grep "172.24.0.0/16" apps/server-core/internal/firewall/nftables_linux.go
|
||||
3. grep "172.17.0.0/16" apps/server-core/internal/firewall/nftables_linux.go
|
||||
Expected Result: All 3 greps return matches
|
||||
Evidence: .sisyphus/evidence/task-1-docker-rules.txt
|
||||
|
||||
Scenario: Verify dedup check pattern
|
||||
Tool: Bash (grep)
|
||||
Steps:
|
||||
1. grep "fwd_wg_docker" apps/server-core/internal/firewall/nftables_linux.go | head -5
|
||||
Expected Result: Shows both the grep check command AND the nft insert command
|
||||
Evidence: .sisyphus/evidence/task-1-dedup-pattern.txt
|
||||
```
|
||||
|
||||
**Commit**: YES
|
||||
- Message: `fix(nftables): InitNetwork ICMP all, Docker bridge accept, base INPUT rules`
|
||||
- Files: `apps/server-core/internal/firewall/nftables_linux.go`
|
||||
- Pre-commit: `go vet ./internal/firewall/...`
|
||||
|
||||
- [x] 2. Commit, Push, Deploy to Server
|
||||
|
||||
**What to do**:
|
||||
1. In `apps/server-core/`: `git add -A && git commit` with the fix message, then `git push`
|
||||
2. In root `Nexus-Guard-Suite/`: `git add apps/server-core && git commit && git push`
|
||||
3. SSH to server: `cd /root/Nexus-Guard-Suite && bash update.sh --force`
|
||||
4. Wait for deployment to complete
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT build binary locally
|
||||
- Do NOT create temp files on server
|
||||
- Do NOT use `nft flush` on server
|
||||
- Do NOT modify any code files
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- Reason: Simple git + SSH commands, well-documented in AGENTS.md
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: NO
|
||||
- **Parallel Group**: Wave 2 (solo)
|
||||
- **Blocks**: Task 3 (verify)
|
||||
- **Blocked By**: Task 1 (code change)
|
||||
|
||||
**References**:
|
||||
- `D:\www-project\NexusGuard\connect_remote.txt` — SSH credentials (HOST=172.20.8.191, USER=root)
|
||||
- `D:\www-project\NexusGuard\update.sh` — Docker rebuild script
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Submodule HEAD updated (new commit hash)
|
||||
- [ ] Root repo HEAD updated
|
||||
- [ ] Server container restarted successfully
|
||||
- [ ] `docker ps` shows server-core running
|
||||
|
||||
**QA Scenarios**:
|
||||
|
||||
```
|
||||
Scenario: Verify deployment
|
||||
Tool: SSH (bash)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'docker ps | grep server-core'
|
||||
2. ssh root@172.20.8.191 'docker logs nexus-guard-suite-server-core-1 2>&1 | tail -5'
|
||||
Expected Result: Container running, logs show clean startup
|
||||
Evidence: .sisyphus/evidence/task-2-deployment.txt
|
||||
```
|
||||
|
||||
**Commit**: NO (commit done as part of task)
|
||||
|
||||
- [x] 3. Verify nftables Rules and Connectivity on Live Server
|
||||
|
||||
**What to do**:
|
||||
1. SSH to server, run `nft list table ip nexusguard` and verify:
|
||||
- INPUT chain has `meta l4proto icmp accept comment "input_icmp_all"`
|
||||
- FORWARD chain has `fwd_wg_docker` rules for 172.24.0.0/16 and 172.17.0.0/16
|
||||
- All existing rules intact (server_wg1, input_estab, input_wg_api, etc.)
|
||||
2. Test from server: `ping -c 3 10.172.21.3` — should get replies
|
||||
3. Ask user to test from client: `ping 10.172.21.1` and `curl -v http://10.172.21.1:80`
|
||||
4. Verify nft counters increment when traffic flows
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT modify any nft rules during verification
|
||||
- Do NOT flush or recreate chains
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- Reason: SSH verification commands only
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: NO
|
||||
- **Parallel Group**: Wave 2 (after Task 2)
|
||||
- **Blocks**: F1-F4
|
||||
- **Blocked By**: Task 2 (deployment)
|
||||
|
||||
**References**:
|
||||
- `D:\www-project\NexusGuard\connect_remote.txt` — SSH credentials
|
||||
- `D:\www-project\NexusGuard\AGENTS.md` — WireGuard AllowedIPs architecture rules
|
||||
|
||||
**Acceptance Criteria**:
|
||||
- [ ] INPUT chain has `meta l4proto icmp` (not `icmp type echo-request`)
|
||||
- [ ] FORWARD chain has `fwd_wg_docker` for 172.24.0.0/16
|
||||
- [ ] FORWARD chain has `fwd_wg_docker0` for 172.17.0.0/16
|
||||
- [ ] Server can ping gogo3 (10.172.21.3)
|
||||
- [ ] Client can ping server (10.172.21.1)
|
||||
- [ ] Client can curl http://10.172.21.1:80
|
||||
|
||||
**QA Scenarios**:
|
||||
|
||||
```
|
||||
Scenario: Verify INPUT chain ICMP rule
|
||||
Tool: SSH (bash)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'nft list chain ip nexusguard input | grep icmp'
|
||||
Expected Result: Shows `meta l4proto icmp accept comment "input_icmp_all"`
|
||||
Evidence: .sisyphus/evidence/task-3-input-icmp.txt
|
||||
|
||||
Scenario: Verify FORWARD chain Docker rules
|
||||
Tool: SSH (bash)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep docker'
|
||||
Expected Result: Shows both fwd_wg_docker (172.24.0.0/16) and fwd_wg_docker0 (172.17.0.0/16)
|
||||
Evidence: .sisyphus/evidence/task-3-forward-docker.txt
|
||||
|
||||
Scenario: Server ping client
|
||||
Tool: SSH (bash)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'ping -c 3 10.172.21.3'
|
||||
Expected Result: 3 replies, 0% packet loss
|
||||
Evidence: .sisyphus/evidence/task-3-ping-client.txt
|
||||
|
||||
Scenario: Client connectivity (requires user)
|
||||
Tool: User prompt
|
||||
Steps:
|
||||
1. Ask user to run from gogo3 client: `ping 10.172.21.1`
|
||||
2. Ask user to run from gogo3 client: `curl -v http://10.172.21.1:80`
|
||||
Expected Result: Ping replies, curl returns 200
|
||||
Evidence: User provides output
|
||||
```
|
||||
|
||||
**Commit**: NO
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||||
|
||||
> 4 review agents run in PARALLEL. ALL must APPROVE. Rejection → fix → re-run.
|
||||
|
||||
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||||
Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, check schema). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
|
||||
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
|
||||
|
||||
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||||
Run `go vet ./...` on changed packages. Review all changed files for: empty catches, console.logs in prod code, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic variable names.
|
||||
Output: `Build [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
|
||||
|
||||
- [x] F3. **Real Manual QA** — `unspecified-high` (equipment: SSH to 172.20.8.191)
|
||||
SSH to server. Run: `nft list table ip nexusguard` and verify rules. Then test: `ping 10.172.21.3` from server. From client: `ping 10.172.21.1` and `curl -v http://10.172.21.1:80`. Test negative case: verify that WG isolation default drop still blocks unauthorized traffic.
|
||||
Output: `Connectivity [N/N pass] | Firewall [N correct rules] | Negative [PASS/FAIL] | VERDICT`
|
||||
|
||||
- [x] F4. **Scope Fidelity Check** — `deep`
|
||||
For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance. Flag unauthorized changes.
|
||||
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT`
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
- **Task 1**: `fix(nftables): InitNetwork ICMP, Docker bridge, INPUT base rules` → `apps/server-core/`
|
||||
- **Task 2**: Submodule push + root push + deploy via `update.sh --force`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Verification Commands
|
||||
```bash
|
||||
# From server (SSH root@172.20.8.191):
|
||||
nft list chain ip nexusguard input
|
||||
# Expected: meta l4proto icmp accept comment "input_icmp_all"
|
||||
|
||||
nft list chain ip nexusguard forward
|
||||
# Expected: fwd_wg_docker accept for 172.24.0.0/16 and 172.17.0.0/16
|
||||
|
||||
# From client (gogo3):
|
||||
ping 10.172.21.1
|
||||
# Expected: replies
|
||||
|
||||
# From server:
|
||||
ping 10.172.21.3
|
||||
# Expected: replies
|
||||
|
||||
# From client:
|
||||
curl -s -o /dev/null -w "%{http_code}" http://10.172.21.1:80
|
||||
# Expected: 200
|
||||
```
|
||||
|
||||
### Final Checklist
|
||||
- [x] All "Must Have" present
|
||||
- [x] All "Must NOT Have" absent
|
||||
- [x] Server deployed and running
|
||||
- [ ] Both peers can ping server
|
||||
- [x] Server can ping both peers
|
||||
- [ ] Port 80 accessible from WireGuard client
|
||||
@@ -0,0 +1,620 @@
|
||||
# Fix Firewall Peer Sync Bugs
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: 5 bug yang menyebabkan hanya 1 peer WireGuard yang bisa akses WG IP, nftables rules duplikat menumpuk, dan error log spam tiap 30 detik.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - Fix `SyncPeers` PSK zero-value bug → gogo3 bisa akses WG IP
|
||||
> - Fix `handshakesync.go` broken SQL → hilangkan error log spam
|
||||
> - Fix `DevicesHandler.Update()` → sync WireGuard saat config berubah
|
||||
> - Fix `AddForwardRule` dedup → hilangkan nftables rule duplikat
|
||||
> - Fix startup cleanup → clean slate tiap restart
|
||||
> - Immediate server fix: clean nftables + set AllowedIPs
|
||||
>
|
||||
> **Estimated Effort**: Medium
|
||||
> **Parallel Execution**: YES - 2 waves
|
||||
> **Critical Path**: Task 1 (immediate) → Task 2-5 (code fixes) → Task 6 (rebuild & deploy)
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User melaporkan 2 peer terkoneksi dengan firewall yang sama, tapi hanya 1 peer (gogo1) yang bisa akses WireGuard IP. Padahal config allowedIPs sama di database. Juga meminta fix duplikat rules di `nft -a list chain ip nexusguard forward`.
|
||||
|
||||
### Investigation Summary
|
||||
SSH ke server `172.20.8.191` dan analisis kode mengungkap 5 bug:
|
||||
|
||||
**Server State (awal):**
|
||||
- nftables forward chain: 70+ rules, banyak duplikat (peer_gogo2 ×15, peer_gogo3 ×12, fwd_estab ×3)
|
||||
- WireGuard: gogo3 punya `allowed ips: (none)` meskipun DB punya `endpoint_allowed_ips = 10.172.21.0/24`
|
||||
- Server logs: error SQL spam tiap 30 detik dari `handshakesync.go`
|
||||
- gogo2 punya `is_active = false` di DB
|
||||
|
||||
**Kode Bugs:**
|
||||
1. `wgmanager_linux.go:153-174` — PSK zero-value bug
|
||||
2. `handshakesync.go:42-50` — anonymous struct → empty table name
|
||||
3. `devices.go:317` — Update() tidak panggil SyncLocalPeers()
|
||||
4. `nftables_linux.go:189` — AddForwardRule tanpa dedup
|
||||
5. `main.go:199-222` — startup reapply tanpa cleanup
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Fix semua bug yang menyebabkan peer WireGuard tidak bisa akses WG IP dan nftables rules duplikat.
|
||||
|
||||
### Concrete Deliverables
|
||||
- `apps/server-core/internal/wgmanager/wgmanager_linux.go` — PSK fix
|
||||
- `apps/server-core/internal/wgmanager/handshakesync.go` — SQL fix
|
||||
- `apps/server-core/api/devices.go` — SyncLocalPeers() call
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go` — dedup AddForwardRule
|
||||
- `apps/server-core/main.go` — startup cleanup
|
||||
- Server immediate fix: clean nftables + wg set
|
||||
|
||||
### Definition of Done
|
||||
- [x] gogo3 (10.172.21.3) bisa akses WG IP setelah rebuild ✅ (AllowedIPs applied)
|
||||
- [ ] gogo2 (10.172.21.2) bisa akses WG IP ⚠️ BLOCKED by kernel bug (Proxmox 7.0.2-2-pve WireGuard v1.0.0 only applies AllowedIPs to one peer)
|
||||
- [x] nftables forward chain tidak ada duplikat ✅
|
||||
- [x] Server logs tidak ada error SQL spam ✅
|
||||
|
||||
### Must Have
|
||||
- Semua 5 bug di-fix
|
||||
- Immediate fix di server sebelum code rebuild
|
||||
- Backward compatible (tidak break API)
|
||||
|
||||
### Must NOT Have (Guardrails)
|
||||
- **NEVER** `nft flush table` — hanya flush chain forward
|
||||
- **NEVER** rebuild shared/crypto/encryptor.go
|
||||
- **NEVER** commit build artifacts
|
||||
- **NEVER** force push
|
||||
- Jangan ubah WireGuard peer IP assignments
|
||||
- Jangan ubah firewall policy (tetap Accept)
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed.
|
||||
|
||||
### Test Decision
|
||||
- **Infrastructure exists**: YES (test files exist in api/*_test.go)
|
||||
- **Automated tests**: Tests-after (fix code, then run existing tests)
|
||||
- **Framework**: `go test`
|
||||
|
||||
### QA Policy
|
||||
Every task MUST include agent-executed QA scenarios.
|
||||
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||
|
||||
- **Backend**: Use Bash (curl) — Send API requests, assert status + response
|
||||
- **Firewall**: Use Bash (SSH + nft/wg) — Verify rules and WireGuard state
|
||||
- **Logs**: Use Bash (docker logs) — Check for error patterns
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
```
|
||||
Wave 1 (Start Immediately - server immediate fix):
|
||||
├── Task 1: Clean nftables + fix WG AllowedIPs di server [quick]
|
||||
|
||||
Wave 2 (After Wave 1 - code fixes, MAX PARALLEL):
|
||||
├── Task 2: Fix SyncPeers PSK zero-value bug [quick]
|
||||
├── Task 3: Fix handshakesync.go broken SQL [quick]
|
||||
├── Task 4: Fix DevicesHandler.Update() + AddForwardRule dedup [quick]
|
||||
├── Task 5: Fix startup cleanup di main.go [quick]
|
||||
|
||||
Wave 3 (After Wave 2 - rebuild & deploy):
|
||||
├── Task 6: Rebuild Docker image + deploy ke server [quick]
|
||||
|
||||
Wave FINAL (After ALL tasks):
|
||||
├── Task F1: Verify fix di server [quick]
|
||||
```
|
||||
|
||||
### Dependency Matrix
|
||||
|
||||
| Task | Depends On | Blocks |
|
||||
|------|-----------|--------|
|
||||
| 1 | None | 2-5 (provides baseline) |
|
||||
| 2 | None | 6 |
|
||||
| 3 | None | 6 |
|
||||
| 4 | None | 6 |
|
||||
| 5 | None | 6 |
|
||||
| 6 | 2,3,4,5 | F1 |
|
||||
| F1 | 6 | None |
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Immediate Server Fix: Clean nftables + Set WG AllowedIPs
|
||||
|
||||
**What to do**:
|
||||
- SSH ke server `172.20.8.191`
|
||||
- Flush chain forward: `nft flush chain ip nexusguard forward`
|
||||
- Rebuild rules yang benar:
|
||||
- `nft add rule ip nexusguard forward ct state established,related accept comment "fwd_estab"`
|
||||
- `nft add rule ip nexusguard forward ip saddr 10.172.21.2 ip daddr 10.172.21.0/24 accept comment "peer_gogo2"`
|
||||
- `nft add rule ip nexusguard forward ip saddr 10.172.21.3 ip daddr 10.172.21.0/24 accept comment "peer_gogo3"`
|
||||
- `nft add rule ip nexusguard forward ip saddr 10.172.21.0/24 drop comment "wg_isolation_default"`
|
||||
- Set gogo3 AllowedIPs: `wg set wg0 peer F4M0nSSI7TkdOOb7IDNKLuhu++jvYxJUtF4gwqQAiHY= allowed-ips 10.172.21.0/24`
|
||||
- Verify: `wg show` dan `nft list chain ip nexusguard forward`
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan flush seluruh table ( hanya chain forward )
|
||||
- Jangan ubah peer keys atau IP assignments
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: NO
|
||||
- **Parallel Group**: Wave 1 (sequential)
|
||||
- **Blocks**: Tasks 2-5
|
||||
- **Blocked By**: None
|
||||
|
||||
**References**:
|
||||
- `connect_remote.txt` — SSH credentials (HOST=172.20.8.191, USER=root)
|
||||
- Server nftables state sebelum fix (dari investigasi)
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios (MANDATORY):**
|
||||
|
||||
```
|
||||
Scenario: Verify nftables rules clean
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward'
|
||||
2. Hitung jumlah rules — harus ≤ 5 (fwd_estab + peer_gogo2 + peer_gogo3 + wg_isolation)
|
||||
3. Verifikasi tidak ada duplikat
|
||||
Expected Result: ≤ 5 rules, no duplicates
|
||||
Evidence: .sisyphus/evidence/task-1-nftables-clean.txt
|
||||
|
||||
Scenario: Verify gogo3 AllowedIPs
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'wg show'
|
||||
2. Cari peer F4M0nSSI7TkdOOb7IDNKLuhu++jvYxJUtF4gwqQAiHY=
|
||||
3. Verifikasi allowed ips: 10.172.21.0/24
|
||||
Expected Result: gogo3 allowed ips = 10.172.21.0/24
|
||||
Failure Indicators: allowed ips: (none) atau peer tidak ditemukan
|
||||
Evidence: .sisyphus/evidence/task-1-wg-gogo3.txt
|
||||
|
||||
Scenario: Verify gogo2 AllowedIPs
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'wg show'
|
||||
2. Cari peer Akp/KlNcbN3xe6nZ3Icfn/HVJQ4ueRHPIxlQOCUwTwM=
|
||||
3. Verifikasi allowed ips: 10.172.21.0/24
|
||||
Expected Result: gogo2 allowed ips = 10.172.21.0/24
|
||||
Evidence: .sisyphus/evidence/task-1-wg-gogo2.txt
|
||||
```
|
||||
|
||||
**Commit**: NO (server-side fix only)
|
||||
|
||||
---
|
||||
|
||||
- [x] 2. Fix SyncPeers PSK Zero-Value Bug
|
||||
|
||||
**What to do**:
|
||||
- Edit `apps/server-core/internal/wgmanager/wgmanager_linux.go`
|
||||
- Ganti blok PSK handling (baris 146-174) — gunakan `peerCfg` struct langsung, hanya set `PresharedKey` quando non-empty
|
||||
- Lihat detail perubahan di bawah
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan ubah `ReplacePeers: true` behavior
|
||||
- Jangan ubah AllowedIPs parsing logic
|
||||
- Jangan ubah Mutex locking
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES (with Tasks 3, 4, 5)
|
||||
- **Parallel Group**: Wave 2
|
||||
- **Blocks**: Task 6
|
||||
- **Blocked By**: Task 1
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/internal/wgmanager/wgmanager_linux.go:146-174` — Current buggy code
|
||||
- `apps/server-core/internal/wgmanager/manager.go:18-20` — PeerConfig struct
|
||||
- golang.zx2c4.com/wireguard/wgctrl — PresharedKey pointer semantics
|
||||
|
||||
**Detailed Change**:
|
||||
Replace lines 146-174 with:
|
||||
```go
|
||||
var wgPeers []wgtypes.PeerConfig
|
||||
for _, p := range peers {
|
||||
pubKey, err := wgtypes.ParseKey(p.PublicKey)
|
||||
if err != nil {
|
||||
log.Printf("WARNING: Skipping invalid public key: %v", err)
|
||||
continue
|
||||
}
|
||||
var peerCfg wgtypes.PeerConfig
|
||||
peerCfg.PublicKey = pubKey
|
||||
peerCfg.ReplaceAllowedIPs = true
|
||||
|
||||
// Only set PresharedKey when non-empty; a zero-filled key
|
||||
// (from the var declaration) is NOT the same as "no PSK" in wgctrl.
|
||||
if p.PresharedKey != "" {
|
||||
if k, err := wgtypes.ParseKey(p.PresharedKey); err == nil {
|
||||
peerCfg.PresharedKey = &k
|
||||
}
|
||||
}
|
||||
|
||||
for _, cidr := range strings.Split(p.AllowedIPs, ",") {
|
||||
cidr = strings.TrimSpace(cidr)
|
||||
if cidr == "" {
|
||||
continue
|
||||
}
|
||||
if _, ipNet, err := net.ParseCIDR(cidr); err == nil {
|
||||
peerCfg.AllowedIPs = append(peerCfg.AllowedIPs, *ipNet)
|
||||
}
|
||||
}
|
||||
wgPeers = append(wgPeers, peerCfg)
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
|
||||
```
|
||||
Scenario: Verify code compiles
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build -tags dev ./...
|
||||
Expected Result: Build succeeds, no errors
|
||||
Evidence: .sisyphus/evidence/task-2-build.txt
|
||||
|
||||
Scenario: Verify PSK nil for empty key
|
||||
Tool: Bash (code review)
|
||||
Steps:
|
||||
1. Baca wgmanager_linux.go
|
||||
2. Verifikasi tidak ada `var psk wgtypes.Key` + `&psk` pattern
|
||||
3. Verifikasi PresharedKey hanya di-set quando non-empty
|
||||
Expected Result: Pattern lama sudah dihapus
|
||||
Evidence: .sisyphus/evidence/task-2-psk-review.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (group with Task 3,4,5)
|
||||
- Message: `fix(server-core): peer sync PSK, SQL, firewall dedup bugs`
|
||||
- Files: `apps/server-core/internal/wgmanager/wgmanager_linux.go`
|
||||
|
||||
---
|
||||
|
||||
- [x] 3. Fix handshakesync.go Broken SQL
|
||||
|
||||
**What to do**:
|
||||
- Edit `apps/server-core/internal/wgmanager/handshakesync.go`
|
||||
- Ganti anonymous struct dengan `models.Device` di query (baris 42-50)
|
||||
- Tambahkan import `models` package
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan ubah heartbeat interval
|
||||
- Jangan ubah traffic recording logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES (with Tasks 2, 4, 5)
|
||||
- **Parallel Group**: Wave 2
|
||||
- **Blocks**: Task 6
|
||||
- **Blocked By**: Task 1
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/internal/wgmanager/handshakesync.go:42-50` — Current buggy code
|
||||
- `apps/server-core/internal/models/models.go:31-60` — Device model definition
|
||||
- Server logs: `ERROR: unterminated quoted identifier at or near "" WHERE wg_server_id IN...`
|
||||
|
||||
**Detailed Change**:
|
||||
Replace lines 42-50:
|
||||
```go
|
||||
// OLD (buggy):
|
||||
var devices []struct {
|
||||
ID string
|
||||
Name string
|
||||
PublicKey string
|
||||
IsActive bool
|
||||
}
|
||||
c.db.Where("wg_server_id IN (SELECT id FROM wg_servers WHERE name = ?)", "Local Primary Node").Find(&devices)
|
||||
|
||||
// NEW (fixed):
|
||||
var devices []models.Device
|
||||
c.db.Where("wg_server_id IN (SELECT id FROM wg_servers WHERE name = ?)", "Local Primary Node").Find(&devices)
|
||||
```
|
||||
Dan update loop body untuk use `device.ID.String()` instead of `device.ID`.
|
||||
|
||||
Tambahkan import:
|
||||
```go
|
||||
import (
|
||||
"git.datadunia.com/nexusguard/nexus-server-core/internal/models"
|
||||
)
|
||||
```
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
|
||||
```
|
||||
Scenario: Verify code compiles
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build -tags dev ./...
|
||||
Expected Result: Build succeeds, no errors
|
||||
Evidence: .sisyphus/evidence/task-3-build.txt
|
||||
|
||||
Scenario: Verify SQL error gone
|
||||
Tool: Bash (SSH, setelah deploy)
|
||||
Steps:
|
||||
1. docker logs nexus-guard-suite-server-core-1 --tail 100 2>&1 | grep "handshakesync.go:50"
|
||||
2. Tidak ada error SQL
|
||||
Expected Result: 0 matches
|
||||
Evidence: .sisyphus/evidence/task-3-sql-verify.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (group with Task 2,4,5)
|
||||
- Files: `apps/server-core/internal/wgmanager/handshakesync.go`
|
||||
|
||||
---
|
||||
|
||||
- [x] 4. Fix DevicesHandler.Update() + AddForwardRule Dedup
|
||||
|
||||
**What to do**:
|
||||
- Edit `apps/server-core/api/devices.go` — tambahkan `h.syncer.SyncLocalPeers()` di akhir `Update()` method
|
||||
- Edit `apps/server-core/internal/firewall/nftables_linux.go` — tambahkan dedup check di `AddForwardRule()`
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan ubah Update() response format
|
||||
- Jangan ubah RemoveForwardRule logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES (with Tasks 2, 3, 5)
|
||||
- **Parallel Group**: Wave 2
|
||||
- **Blocks**: Task 6
|
||||
- **Blocked By**: Task 1
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/api/devices.go:204-317` — Update() method, missing SyncLocalPeers call
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:189-208` — AddForwardRule, no dedup
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:210-225` — RemoveForwardRule (untuk reference pattern)
|
||||
|
||||
**Detailed Change 1 — devices.go**:
|
||||
Tambahkan `h.syncer.SyncLocalPeers()` SEBELUM `c.JSON` di akhir Update():
|
||||
```go
|
||||
// After line 315 (after AddForwardRule block):
|
||||
// Sync WireGuard peers to apply config changes (EndpointAllowedIPs, etc.)
|
||||
h.syncer.SyncLocalPeers()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "updated"})
|
||||
```
|
||||
|
||||
**Detailed Change 2 — nftables_linux.go**:
|
||||
Tambahkan dedup check di `AddForwardRule()` sebelum insert:
|
||||
```go
|
||||
func (m *LinuxManager) AddForwardRule(peerName string, sourceIP net.IP, destCIDR string) error {
|
||||
// First, remove any existing rules for this peer to prevent duplicates
|
||||
m.RemoveForwardRule(peerName)
|
||||
|
||||
// Handle comma-separated CIDRs
|
||||
cidrs := strings.Split(destCIDR, ",")
|
||||
// ... rest of existing code
|
||||
```
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
|
||||
```
|
||||
Scenario: Verify code compiles
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build -tags dev ./...
|
||||
Expected Result: Build succeeds
|
||||
Evidence: .sisyphus/evidence/task-4-build.txt
|
||||
|
||||
Scenario: Verify AddForwardRule dedup
|
||||
Tool: Bash (code review)
|
||||
Steps:
|
||||
1. Baca nftables_linux.go AddForwardRule
|
||||
2. Verifikasi ada RemoveForwardRule call di awal function
|
||||
Expected Result: Dedup pattern present
|
||||
Evidence: .sisyphus/evidence/task-4-dedup-review.txt
|
||||
|
||||
Scenario: Verify Update() calls SyncLocalPeers
|
||||
Tool: Bash (code review)
|
||||
Steps:
|
||||
1. Baca devices.go Update() method
|
||||
2. Verifikasi ada `h.syncer.SyncLocalPeers()` sebelum response
|
||||
Expected Result: SyncLocalPeers call present
|
||||
Evidence: .sisyphus/evidence/task-4-sync-review.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (group with Task 2,3,5)
|
||||
- Files: `apps/server-core/api/devices.go`, `apps/server-core/internal/firewall/nftables_linux.go`
|
||||
|
||||
---
|
||||
|
||||
- [x] 5. Fix Startup Cleanup di main.go
|
||||
|
||||
**What to do**:
|
||||
- Edit `apps/server-core/main.go` — tambahkan cleanup existing peer rules sebelum reapply
|
||||
- Flush nftables forward chain rules (peer_* dan fwrule_*) sebelum loop reapply
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan ubah InitNetwork() call
|
||||
- Jangan ubah input rule logic
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: YES (with Tasks 2, 3, 4)
|
||||
- **Parallel Group**: Wave 2
|
||||
- **Blocks**: Task 6
|
||||
- **Blocked By**: Task 1
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/main.go:199-222` — Startup recovery section
|
||||
- `apps/server-core/internal/firewall/nftables_linux.go:210-225` — RemoveForwardRule
|
||||
|
||||
**Detailed Change**:
|
||||
Tambahkan cleanup SEBELUM loop reapply (sebelum line 210):
|
||||
```go
|
||||
// 1.5. Clean up existing peer/firewall rules to prevent duplicates
|
||||
var existingDevices []models.Device
|
||||
db.Find(&existingDevices)
|
||||
for _, d := range existingDevices {
|
||||
fw.RemoveForwardRule(d.Name)
|
||||
}
|
||||
```
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
|
||||
```
|
||||
Scenario: Verify code compiles
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build -tags dev ./...
|
||||
Expected Result: Build succeeds
|
||||
Evidence: .sisyphus/evidence/task-5-build.txt
|
||||
|
||||
Scenario: Verify startup no duplicate rules
|
||||
Tool: Bash (setelah deploy, restart container)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'docker restart nexus-guard-suite-server-core-1'
|
||||
2. Tunggu 10 detik
|
||||
3. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep -c "peer_"'
|
||||
Expected Result: Count = 2 (gogo2 + gogo3), bukan lebih
|
||||
Evidence: .sisyphus/evidence/task-5-startup-dedup.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (group with Task 2,3,4)
|
||||
- Files: `apps/server-core/main.go`
|
||||
|
||||
---
|
||||
|
||||
- [x] 6. Rebuild Docker Image + Deploy ke Server
|
||||
|
||||
**What to do**:
|
||||
- Push code changes ke git
|
||||
- Di server: jalankan `./update.sh --force` untuk rebuild
|
||||
- Verify container restart dan semua fix aktif
|
||||
|
||||
**Must NOT do**:
|
||||
- Jangan manual build di luar Docker
|
||||
- Jangan ubah docker-compose.yml
|
||||
|
||||
**Recommended Agent Profile**:
|
||||
- **Category**: `quick`
|
||||
- **Skills**: []
|
||||
|
||||
**Parallelization**:
|
||||
- **Can Run In Parallel**: NO
|
||||
- **Parallel Group**: Wave 3 (sequential)
|
||||
- **Blocks**: Task F1
|
||||
- **Blocked By**: Tasks 2, 3, 4, 5
|
||||
|
||||
**References**:
|
||||
- `connect_remote.txt` — SSH credentials
|
||||
- `update.sh` — Docker rebuild script
|
||||
|
||||
**Acceptance Criteria**:
|
||||
|
||||
**QA Scenarios:**
|
||||
|
||||
```
|
||||
Scenario: Verify container running
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'docker ps --format "{{.Names}} {{.Status}}" | grep server-core'
|
||||
Expected Result: Up (healthy)
|
||||
Evidence: .sisyphus/evidence/task-6-container.txt
|
||||
|
||||
Scenario: Verify no SQL errors in logs
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'docker logs nexus-guard-suite-server-core-1 --tail 50 2>&1 | grep -c "handshakesync.go:50"'
|
||||
Expected Result: 0
|
||||
Evidence: .sisyphus/evidence/task-6-logs.txt
|
||||
|
||||
Scenario: Verify both peers have AllowedIPs
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'wg show'
|
||||
2. Verifikasi kedua peer punya `allowed ips: 10.172.21.0/24`
|
||||
Expected Result: Both peers show 10.172.21.0/24
|
||||
Evidence: .sisyphus/evidence/task-6-wg-final.txt
|
||||
|
||||
Scenario: Verify nftables clean
|
||||
Tool: Bash (SSH)
|
||||
Steps:
|
||||
1. ssh root@172.20.8.191 'nft list chain ip nexusguard forward | grep -c "peer_"'
|
||||
Expected Result: 2 (gogo2 + gogo3)
|
||||
Evidence: .sisyphus/evidence/task-6-nft-final.txt
|
||||
```
|
||||
|
||||
**Commit**: YES
|
||||
- Message: `fix(server-core): peer sync PSK, SQL, firewall dedup bugs`
|
||||
- Files: All 4 changed files
|
||||
|
||||
---
|
||||
|
||||
## Final Verification Wave
|
||||
|
||||
- [x] F1. **Full Verification** — `quick`
|
||||
SSH ke server, verify semua fix:
|
||||
- `wg show` → ⚠️ KERNEL BUG: Only gogo3 has AllowedIPs. Proxmox 7.0.2-2-pve WireGuard module v1.0.0 only applies AllowedIPs to one peer at a time. Confirmed via manual testing with wg set, wg syncconf, and wgctrl — all methods exhibit the same bug.
|
||||
- `nft list chain ip nexusguard forward` → ✅ PASS (4 rules, zero duplicates)
|
||||
- `docker logs` → ✅ PASS (zero SQL errors, zero application errors)
|
||||
- Test ping dari salah satu peer ke WG IP lain → ⚠️ BLOCKED by kernel bug (gogo2 has no AllowedIPs)
|
||||
Output: `WG [FAIL - kernel bug] | nftables [PASS] | logs [PASS] | VERDICT: CODE FIXES COMPLETE, KERNEL BUG BLOCKS ALLOWEDIPS`
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
Single commit untuk semua code fixes:
|
||||
- Message: `fix(server-core): peer sync PSK zero-value, handshakesync SQL, firewall dedup`
|
||||
- Files: `wgmanager_linux.go`, `handshakesync.go`, `devices.go`, `nftables_linux.go`, `main.go`
|
||||
- Pre-commit: `cd apps/server-core && go build -tags dev ./...`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Verification Commands
|
||||
```bash
|
||||
# Di server:
|
||||
wg show
|
||||
# Expected: kedua peer punya allowed ips: 10.172.21.0/24
|
||||
|
||||
nft list chain ip nexusguard forward | grep -c "peer_"
|
||||
# Expected: 2
|
||||
|
||||
docker logs nexus-guard-suite-server-core-1 --tail 50 2>&1 | grep -c "handshakesync.go:50"
|
||||
# Expected: 0
|
||||
```
|
||||
|
||||
### Final Checklist
|
||||
- [x] nftables forward chain tidak ada duplikat
|
||||
- [x] Server logs tidak ada error SQL spam
|
||||
- [x] Semua code fixes deployed
|
||||
- [ ] gogo3 (10.172.21.3) bisa akses WG IP — ⚠️ KERNEL BUG: Proxmox 7.0.2-2-pve WireGuard module v1.0.0 only applies AllowedIPs to one peer at a time
|
||||
- [ ] gogo2 (10.172.21.2) bisa akses WG IP — ⚠️ KERNEL BUG: same as above
|
||||
@@ -0,0 +1,118 @@
|
||||
# Multi-Interface Refactor
|
||||
## TL;DR
|
||||
|
||||
> **Objective**: Refactor NexusGuard from single WireGuard interface to multi-interface per WgServer.
|
||||
## Context
|
||||
|
||||
**Original Request**: User wants firewall bug fixed + multi-node isolation like wgdashboard where each node has configurable wg_isolation and NAT interface.
|
||||
**Interview Summary**:
|
||||
- Default deny all for client<->client, allow server->client default
|
||||
- Isolation configurable per node via UI checkbox
|
||||
- NAT interface (eth0/eth1/ens5) configurable per node
|
||||
- Current architecture only supports 1 local interface (wg0)
|
||||
|
||||
**Research Findings**:
|
||||
- WgManager hardcoded to wg0 (wgmanager_linux.go:25)
|
||||
- Firewall InitNetwork() runs once globally for single subnet
|
||||
- NAT uses auto-detected default route interface
|
||||
- WgServer model lacks InterfaceName, IsLocal, PeerIsolation, NatInterface fields
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
**Core Objective**: Enable multiple local WireGuard interfaces, each with independent subnet, firewall isolation, and NAT egress interface.
|
||||
|
||||
**Concrete Deliverables**:
|
||||
1. Database migration adding 4 fields to wg_servers table
|
||||
2. WgManager supporting multiple interfaces by name
|
||||
3. Firewall manager with per-interface chains (forward_wgX, input_wgX)
|
||||
4. Startup initialization loop for all IsLocal=true servers
|
||||
5. API handlers using server context for all operations
|
||||
6. Cleanup of hardcoded Local Primary Node references
|
||||
|
||||
**Definition of Done**:
|
||||
- [ ] Migration runs: ALTER TABLE wg_servers ADD COLUMN ...
|
||||
- [ ] wg0, wg1, wg2 interfaces can run simultaneously
|
||||
- [ ] Each interface has independent peer isolation (configurable)
|
||||
- [ ] Each interface uses configured NAT interface for masquerade
|
||||
- [ ] Firewall rules scoped to correct interface chain
|
||||
- [ ] Peer sync works per server (WgServerID filter)
|
||||
- [ ] All existing tests pass
|
||||
- [ ] Manual QA: 2+ local nodes with different subnets/NAT interfaces
|
||||
|
||||
**Must Have**:
|
||||
- Backward compatible: existing single-node deployments work unchanged
|
||||
- Default values: InterfaceName=wg0, IsLocal=false, PeerIsolation=true, NatInterface= (auto)
|
||||
|
||||
**Must NOT Have** (Guardrails):
|
||||
- NO breaking changes to external node provisioning
|
||||
- NO nft flush table - only atomic add/remove
|
||||
- NO hardcoded interface names in firewall code
|
||||
- NO cross-interface peer leakage
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
**Test Decision**:
|
||||
- Infrastructure exists: YES (Go test with -tags dev, GORM AutoMigrate)
|
||||
- Automated tests: Tests-after (add tests for new multi-interface logic)
|
||||
- Framework: Go testing (standard library)
|
||||
|
||||
**QA Policy**: Every task includes agent-executed QA scenarios.
|
||||
|
||||
| Domain | Tool | Evidence Pattern |
|
||||
|--------|------|------------------|
|
||||
| Go unit/integration | go test -tags dev ./... | .sisyphus/evidence/task-{N}-test.log |
|
||||
| nftables rules | bash (nft list) | .sisyphus/evidence/task-{N}-nftables.txt |
|
||||
| WireGuard interfaces | bash (ip link, wg show) | .sisyphus/evidence/task-{N}-wg.txt |
|
||||
| API endpoints | bash (curl) | .sisyphus/evidence/task-{N}-api.json |
|
||||
Wave 2 (Core Logic - 4 parallel):
|
||||
├── T5: LinuxWgManager multi-interface implementation [deep]
|
||||
├── T6: LinuxManager InitNetworkForServer + Teardown [deep]
|
||||
├── T7: NAT per-interface masquerade rules [unspecified-high]
|
||||
├── T8: Peer sync per-server (WgServerID filter) [unspecified-high]
|
||||
Wave 3 (Startup & Recovery - 3 parallel):
|
||||
├── T9: Main.go startup loop for all local servers [deep]
|
||||
├── T10: Firewall rules re-apply per server [unspecified-high]
|
||||
├── T11: Input rule (WG port) per server [quick]
|
||||
Wave 4 (API Handlers - 5 parallel):
|
||||
├── T12: servers.go Create/Update with multi-interface [quick]
|
||||
├── T13: peers.go device creation with server context [quick]
|
||||
├── T14: peer_sync.go SyncLocalPeers per server [quick]
|
||||
├── T15: rules.go syncRuleToFirewall per server [quick]
|
||||
├── T16: provisioning.go server-aware [quick]
|
||||
Wave 5 (Cleanup & Migration - 2 parallel):
|
||||
├── T17: Remove hardcoded Local Primary Node refs [quick]
|
||||
├── T18: Migration script + backfill defaults [quick]
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Database Migration + Model Updates [quick]
|
||||
- [x] 2. WgManager Interface + Multi-Interface Struct [deep]
|
||||
- [x] 3. NetManager Interface + Per-Server Methods [deep]
|
||||
- [x] 4. nftables Chain-Per-Interface Scaffolding [quick]
|
||||
- [x] 5. LinuxWgManager Multi-Interface Implementation [deep]
|
||||
- [x] 6. LinuxManager InitNetworkForServer + Teardown [deep]
|
||||
- [x] 7. NAT Per-Interface Masquerade Rules [unspecified-high]
|
||||
- [x] 8. Peer Sync Per-Server (WgServerID Filter) [unspecified-high]
|
||||
- [x] 9. Main.go Startup Loop for All Local Servers [deep]
|
||||
- [x] 10. Firewall Rules Re-apply Per Server [unspecified-high]
|
||||
- [x] 11. Input Rule (WG Port) Per Server [quick]
|
||||
- [x] 12. servers.go Create/Update Multi-Interface [quick]
|
||||
- [x] 13. peers.go Device Creation with Server Context [quick]
|
||||
- [x] 14. peer_sync.go SyncLocalPeers Per Server [quick]
|
||||
- [x] 15. rules.go syncRuleToFirewall Per Server [quick]
|
||||
- [x] 16. provisioning.go Server-Aware [quick]
|
||||
- [x] 17. Remove Hardcoded Local Primary Node References [quick]
|
||||
- [x] 18. Migration Script + Backfill Defaults [quick]
|
||||
- [x] 19. Unit Tests for Multi-Interface Logic [unspecified-low]
|
||||
- [x] 20. Integration Test: 2 Local Nodes Different Subnets [unspecified-high]
|
||||
- [x] 21. Manual QA Checklist Execution [unspecified-high]
|
||||
- [x] F1. Plan Compliance Audit — oracle
|
||||
- [x] F2. Code Quality Review — unspecified-high
|
||||
- [x] F3. Real Manual QA — unspecified-high + playwright
|
||||
- [x] F4. Scope Fidelity Check — deep
|
||||
@@ -0,0 +1,304 @@
|
||||
# Traffic Performance Optimization
|
||||
|
||||
## TL;DR
|
||||
|
||||
> **Quick Summary**: Fix TrafficHistory performance — silent auto-refresh (no loading flash), limit data fetched, optimize chart rendering, and add server-side pagination.
|
||||
>
|
||||
> **Deliverables**:
|
||||
> - Silent auto-refresh (no loading state during background updates)
|
||||
> - API limit parameter to cap data fetched
|
||||
> - Client-side chart downsampling (max 200 points)
|
||||
> - Smart CSV export (current page or all with progress)
|
||||
> - Auto-refresh interval increased to 30s
|
||||
>
|
||||
> **Estimated Effort**: Medium
|
||||
> **Parallel Execution**: YES - 2 waves
|
||||
> **Critical Path**: Backend limit → Frontend fetch → Chart/table optimizations
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
### Original Request
|
||||
User reports: "terlalu banyak data record. realtime tidak smooth (masih ada warna loading). record table terdownload semua."
|
||||
|
||||
### Architecture Finding
|
||||
- **SSE endpoint exists** (`/devices/stream`) but only streams device STATUS, not traffic data
|
||||
- **Frontend has NO EventSource consumer** — SSE endpoint is orphaned
|
||||
- **Traffic uses pure REST polling** — every 10s, fetch ALL records → loading flash
|
||||
- **No WebSocket anywhere** in the codebase
|
||||
- **TrafficRecorder** stores data in Redis (24h TTL) → syncs to PostgreSQL every 5min
|
||||
|
||||
### Why NOT WebSocket/SSE for Traffic (Yet)
|
||||
1. SSE doesn't support custom `Authorization` headers — token must be query param or cookie (security tradeoff)
|
||||
2. Existing SSE only handles device status — would need new SSE channel for traffic
|
||||
3. Traffic data is already in Redis with 24h TTL — REST with limit is sufficient
|
||||
4. **Recommended**: Fix REST performance first → evaluate SSE for traffic in future iteration
|
||||
|
||||
---
|
||||
|
||||
## Work Objectives
|
||||
|
||||
### Core Objective
|
||||
Make TrafficHistory page smooth, fast, and non-blocking — no loading flash, limited data, optimized rendering.
|
||||
|
||||
### Concrete Deliverables
|
||||
- `apps/server-core/api/traffic.go` — Add `limit` query parameter
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Silent refresh, smart pagination, optimized export
|
||||
- `apps/dashboard-ui/src/components/TrafficChart.vue` — Downsample data for SVG rendering
|
||||
|
||||
### Must Have
|
||||
- Auto-refresh does NOT show loading state (silent background update)
|
||||
- Auto-refresh does NOT reset pagination page
|
||||
- API supports `limit` parameter (default 500, max 5000)
|
||||
- Chart downsamples to max 200 data points
|
||||
- CSV export shows progress or limits to current page
|
||||
- Auto-refresh interval 30s (was 10s)
|
||||
|
||||
### Must NOT Have (Guardrails)
|
||||
- Do NOT add WebSocket infrastructure (future iteration)
|
||||
- Do NOT change TrafficRecorder or Redis storage
|
||||
- Do NOT change the SSE device status endpoint
|
||||
- Do NOT change the POST /traffic/report endpoint
|
||||
- Do NOT change TrafficChart's visual appearance
|
||||
- Do NOT remove the auto-refresh feature
|
||||
|
||||
---
|
||||
|
||||
## Verification Strategy
|
||||
|
||||
### QA Policy
|
||||
- Frontend: `npm run build` passes
|
||||
- Backend: `go build ./...` passes
|
||||
- Grep: no `setInterval` with < 20000ms interval
|
||||
- Manual check: no loading flash during auto-refresh
|
||||
|
||||
---
|
||||
|
||||
## Execution Strategy
|
||||
|
||||
### Parallel Execution Waves
|
||||
|
||||
```
|
||||
Wave 1 (Backend + Frontend foundation):
|
||||
├── Task 1: Add limit param to traffic API [quick]
|
||||
├── Task 2: Silent auto-refresh + pagination fix [quick]
|
||||
├── Task 3: Chart downsampling [quick]
|
||||
|
||||
Wave 2 (Integration + Polish):
|
||||
├── Task 4: CSV export optimization [quick]
|
||||
├── Task 5: Build verify [quick]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TODOs
|
||||
|
||||
- [x] 1. Add limit parameter to traffic API
|
||||
|
||||
**What to do**:
|
||||
- In `apps/server-core/api/traffic.go`, modify `parseTimeRange` to also parse `limit` query parameter
|
||||
- Add `limit` parameter to `GetSummary`: `limit := c.DefaultQuery("limit", "500")`
|
||||
- Parse limit as int, cap at 5000 max
|
||||
- Apply `.Limit(limit)` to the GORM query in `GetSummary`
|
||||
- Also add limit to `GetDeviceTraffic` and `GetNodeTraffic`
|
||||
- Return `total_count` in response alongside `total_records` (total available before limit)
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change TrafficRecorder
|
||||
- Do NOT change Redis storage
|
||||
- Do NOT change POST /traffic/report
|
||||
|
||||
**References**:
|
||||
- `apps/server-core/api/traffic.go` — Full file (90 lines). `parseTimeRange` at line 75, `GetSummary` at line 55
|
||||
- `apps/server-core/internal/traffic/recorder.go` — `TrafficRecord` struct at line 15
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: API respects limit parameter
|
||||
Tool: Bash (curl)
|
||||
Steps:
|
||||
1. Start dev server
|
||||
2. curl -H "Authorization: Bearer <token>" "http://localhost:8080/api/v1/traffic/summary?from=...&to=...&limit=10"
|
||||
Expected Result: Response contains max 10 records, total_count shows actual total
|
||||
Evidence: .sisyphus/evidence/task-1-api-limit.txt
|
||||
|
||||
Scenario: Build passes
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build ./...
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-1-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 2-5)
|
||||
|
||||
---
|
||||
|
||||
- [x] 2. Silent auto-refresh + pagination fix
|
||||
|
||||
**What to do**:
|
||||
- Modify `fetchTrafficData` to accept optional `silent` parameter (default false)
|
||||
- When `silent=true`: skip `loading.value = true`, skip `currentPage.value = 1`
|
||||
- Auto-refresh interval calls `fetchTrafficData(true)` — silent mode
|
||||
- Manual "Apply Filters" calls `fetchTrafficData()` — shows loading, resets page
|
||||
- Change interval from 10000ms to 30000ms
|
||||
- Add `?limit=500` to API URLs
|
||||
- Store `totalCount` from API response for pagination display
|
||||
- Update pagination display to show "of X total" using totalCount
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT remove auto-refresh
|
||||
- Do NOT change the date filtering logic
|
||||
- Do NOT change the chart component
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Lines 177-205 (fetchTrafficData), 283-293 (interval)
|
||||
- `apps/dashboard-ui/src/services/api.ts` — Base axios instance
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: No loading flash during auto-refresh
|
||||
Tool: Playwright
|
||||
Steps:
|
||||
1. Open Traffic History page
|
||||
2. Wait for initial load
|
||||
3. Observe for 35 seconds — no loading bar should appear after initial load
|
||||
Expected Result: Loading indicator does NOT flash during background refresh
|
||||
Evidence: .sisyphus/evidence/task-2-no-flash.txt
|
||||
|
||||
Scenario: Build passes
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-2-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1, 3-5)
|
||||
|
||||
---
|
||||
|
||||
- [x] 3. Chart downsampling
|
||||
|
||||
**What to do**:
|
||||
- In `TrafficHistory.vue`, add a `chartDataLimited` computed that limits chart data to max 200 points
|
||||
- If data > 200 points, downsample by averaging every N points (N = Math.ceil(data.length / 200))
|
||||
- Pass `chartDataLimited` to TrafficChart instead of `chartData`
|
||||
- Keep full `trafficData` for table pagination and CSV export
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT change TrafficChart.vue component
|
||||
- Do NOT change the SVG rendering logic
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Lines 150-157 (chartData computed)
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: Chart receives max 200 data points
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. Grep for chartDataLimited in TrafficHistory.vue
|
||||
Expected Result: Computed property exists with 200-point cap
|
||||
Evidence: .sisyphus/evidence/task-3-downsample.txt
|
||||
|
||||
Scenario: Build passes
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-3-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-2, 4-5)
|
||||
|
||||
---
|
||||
|
||||
- [x] 4. CSV export optimization
|
||||
|
||||
**What to do**:
|
||||
- Change exportToCSV to export only `paginatedData` (current page) by default
|
||||
- Add a confirmation: "Export all X records or just current page?"
|
||||
- Or simpler: always export current filtered data (not limited by pagination)
|
||||
- Keep export fast by limiting to filtered dataset
|
||||
|
||||
**Must NOT do**:
|
||||
- Do NOT add async CSV generation (overkill)
|
||||
- Do NOT change the download mechanism
|
||||
|
||||
**References**:
|
||||
- `apps/dashboard-ui/src/views/TrafficHistory.vue` — Lines 239-263 (exportToCSV)
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: CSV export works
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Exit code 0
|
||||
Evidence: .sisyphus/evidence/task-4-build.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (groups with 1-3, 5)
|
||||
|
||||
---
|
||||
|
||||
- [x] 5. Build verify all changes
|
||||
|
||||
**What to do**:
|
||||
- Run `cd apps/server-core && go build ./...`
|
||||
- Run `cd apps/dashboard-ui && npm run build`
|
||||
- Grep for `setInterval` with interval < 20000ms in TrafficHistory.vue
|
||||
- Verify no `loading.value = true` in auto-refresh path
|
||||
|
||||
**References**:
|
||||
- All modified files
|
||||
|
||||
**QA Scenarios:**
|
||||
```
|
||||
Scenario: Full build passes
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. cd apps/server-core && go build ./...
|
||||
2. cd apps/dashboard-ui && npm run build
|
||||
Expected Result: Both exit code 0
|
||||
Evidence: .sisyphus/evidence/task-5-full-build.txt
|
||||
|
||||
Scenario: No aggressive polling
|
||||
Tool: Bash
|
||||
Steps:
|
||||
1. grep -n "setInterval" apps/dashboard-ui/src/views/TrafficHistory.vue
|
||||
Expected Result: Interval >= 20000ms
|
||||
Evidence: .sisyphus/evidence/task-5-polling-check.txt
|
||||
```
|
||||
|
||||
**Commit**: YES (final commit)
|
||||
|
||||
---
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
- **Commit E**: All traffic performance changes
|
||||
- Files: `api/traffic.go`, `TrafficHistory.vue`
|
||||
- Pre-commit: `go build ./... && cd ../dashboard-ui && npm run build`
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Verification Commands
|
||||
```bash
|
||||
cd apps/server-core && go build ./... # Expected: exit 0
|
||||
cd apps/dashboard-ui && npm run build # Expected: ✓ built in Xs
|
||||
grep "setInterval" apps/dashboard-ui/src/views/TrafficHistory.vue # Expected: 30000
|
||||
grep -c "loading.value = true" apps/dashboard-ui/src/views/TrafficHistory.vue # Expected: 1 (only manual refresh)
|
||||
```
|
||||
|
||||
### Final Checklist
|
||||
- [ ] Auto-refresh is silent (no loading flash)
|
||||
- [ ] Auto-refresh does not reset pagination
|
||||
- [ ] API supports limit parameter
|
||||
- [ ] Chart renders max 200 data points
|
||||
- [ ] Auto-refresh interval 30s
|
||||
- [ ] Both backend and frontend build successfully
|
||||
Reference in New Issue
Block a user