diff --git a/.sisyphus/plans/firewall-and-legacy-cleanup.md b/.sisyphus/plans/firewall-and-legacy-cleanup.md
new file mode 100644
index 0000000..ee62cac
--- /dev/null
+++ b/.sisyphus/plans/firewall-and-legacy-cleanup.md
@@ -0,0 +1,1360 @@
+# Firewall System Fix + Legacy Styling Cleanup
+
+## TL;DR
+
+> **Quick Summary**: Fix broken nftables firewall rule system (rules from UI never actually filter traffic), remove dead LinkedDevices component, and modernize ALL remaining legacy-styled views/components to use the Ng* design system with proper @theme tokens.
+>
+> **Deliverables**:
+> - Working nftables firewall rules (UI rules actually filter kernel traffic)
+> - fwd_estab duplicate bug fixed
+> - RemoveForwardRule multi-CIDR fix
+> - @theme design tokens defined in main.css
+> - 11 files restyled with Ng* components + design tokens
+> - All 19 alert() → useToast(), all 6 confirm() → useConfirm()
+> - LinkedDevices.vue deleted
+> - Backend go tests for firewall rule CRUD
+>
+> **Estimated Effort**: Large
+> **Parallel Execution**: YES - 3 waves
+> **Critical Path**: Task 1 (@theme) → Wave 2 frontend tasks → Final verification
+
+---
+
+## Context
+
+### Original Request
+User identified LinkedDevices.vue ("Perangkat Tertaut") as broken/redundant and asked:
+1. What is it for? → Answered: legacy component showing device's own data, not actual linked devices
+2. Firewall needs fixing — conflicts with AllowedIPs and nftables → Investigated: found critical backend bugs
+3. Fix with recommendations + check all legacy styling across all menus
+
+### Interview Summary
+**Key Discussions**:
+- LinkedDevices.vue: DELETE — broken, redundant, uses emoji/alert/confirm
+- FirewallEditor.vue: Redesign with Ng* design system
+- Backend bugs: fwd_estab 3x duplicate, syncRuleToFirewall completely broken (rules never actually filter), RemoveForwardRule single-handle bug
+- AllowedIPs vs nftables: Separate concerns (routing vs filtering) — UI already separates them correctly
+- Styling: ALL 7 views + 4 components need cleanup (19 alert, 6 confirm, hardcoded colors, emoji)
+
+**Research Findings**:
+- Ng* components use @theme tokens (`bg-bg-surface`, `text-text-primary`, etc.) but main.css has NO @theme block — tokens undefined
+- `syncRuleToFirewall()` calls `AddRangeRule()` which creates orphaned nftables SETs with no chain RULE referencing them — UI firewall rules are 100% non-functional
+- `AddRangeRule` interface inadequate: no protocol, no action, CIDR parsing wrong
+- 3 callers of AddRangeRule: rules.go:133, devices.go:144, peers.go:128
+- useToast() and useConfirm() composables already exist and ready to use
+- All 16 Ng* components created during redesign, ready to use
+
+### Metis Review
+**Identified Gaps** (addressed):
+- @theme tokens missing — added as Wave 0 prerequisite task
+- NetManager interface needs new method for proper firewall rules — planned
+- devices.go:144 and peers.go:128 SSH auto-provisioning also use broken AddRangeRule — included in scope
+- RemoveForwardRule multi-CIDR awk bug — included in scope
+- Existing orphaned nftables sets in production — noted, InitNetwork will clean
+- alert/confirm count correction: 21/7 raw → 19/6 after LinkedDevices deletion — confirmed
+
+---
+
+## Work Objectives
+
+### Core Objective
+Fix the completely non-functional firewall rule system so UI-created rules actually filter traffic in the Linux kernel, and modernize all remaining legacy UI to the Ng* design system.
+
+### Concrete Deliverables
+- `apps/server-core/internal/firewall/manager.go` — new `AddFirewallRule`/`RemoveFirewallRule` methods
+- `apps/server-core/internal/firewall/nftables_linux.go` — fwd_estab fix, new methods, RemoveForwardRule fix
+- `apps/server-core/internal/firewall/nftables_stub.go` — matching stub methods
+- `apps/server-core/api/rules.go` — rewritten `syncRuleToFirewall` + delete cleanup
+- `apps/server-core/api/devices.go` + `peers.go` — updated SSH auto-provisioning callers
+- `apps/dashboard-ui/src/assets/main.css` — @theme design tokens
+- `apps/dashboard-ui/src/components/LinkedDevices.vue` — DELETED
+- `apps/dashboard-ui/src/components/FirewallEditor.vue` — redesigned with Ng*
+- 9 more .vue files restyled (see TODOs)
+
+### Definition of Done
+- [x] `cd apps/server-core && go build ./...` passes
+- [x] `cd apps/server-core && go test ./... -tags dev -count=1` all pass
+- [x] `cd apps/dashboard-ui && npm run build` passes with 0 errors
+- [x] `grep -r "alert(" --include="*.vue" src/` returns 0 matches
+- [x] `grep -r "LinkedDevices" --include="*.vue" --include="*.ts" src/` returns 0 matches
+- [x] `grep -r "confirm(" --include="*.vue" src/ | grep -v useConfirm | grep -v handleConfirm | grep -v ConfirmModal | grep -v "\/\/"` returns 0 matches
+
+### Must Have
+- Firewall rules created via UI must produce actual nft rules in FORWARD chain
+- All alert()/confirm() replaced with useToast()/useConfirm()
+- LinkedDevices.vue deleted with all references
+- @theme tokens defined so Ng* components render correctly
+- Port range support (e.g., 80-443)
+- Protocol selection (tcp/udp/both) honored in nft rules
+- Action (accept/drop) honored in nft rules
+
+### Must NOT Have (Guardrails)
+- NEVER use `nft flush table` — destroys all peer isolation
+- NEVER touch `AddForwardRule`, `AddInputRule`, `RemoveInputRule`, `AddUserIsolation` — they work correctly
+- NEVER introduce new Ng* components — use existing 16 only
+- NEVER touch Login.vue — already redesigned
+- NEVER touch style.css — dead scaffold code
+- NEVER add logging/audit trails to firewall backend
+- NEVER refactor admin-only checks (known debt, out of scope)
+- NEVER add sorting/filtering/pagination to FirewallEditor rule table
+- Port format: single port or dash-range ONLY (e.g., `80` or `8000-9000`). No comma-separated.
+
+---
+
+## Verification Strategy
+
+> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions.
+
+### Test Decision
+- **Infrastructure exists**: YES (Go: `go test`, Frontend: `npm run build` / vue-tsc)
+- **Automated tests**: YES (backend tests after implementation)
+- **Framework**: Go `testing` package for backend; no frontend unit tests
+- **Test approach**: Tests-after for backend; agent QA for frontend
+
+### QA Policy
+Every task MUST include agent-executed QA scenarios.
+Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
+
+- **Backend**: Use Bash (`go test`, `go build`) — compile, run tests, verify output
+- **Frontend/UI**: Use Bash (`npm run build`, `grep`) — type-check, verify no legacy patterns remain
+
+---
+
+## Execution Strategy
+
+### Parallel Execution Waves
+
+```
+Wave 0 (Prerequisite — must complete first):
+└── Task 1: Define @theme design tokens in main.css [quick]
+
+Wave 1 (Backend firewall — all parallel):
+├── Task 2: Fix InitNetwork fwd_estab duplicates [quick]
+├── Task 3: Add AddFirewallRule/RemoveFirewallRule to NetManager interface [quick]
+├── Task 4: Rewrite syncRuleToFirewall + fix rule delete cleanup [quick]
+├── Task 5: Fix RemoveForwardRule multi-CIDR handling [quick]
+├── Task 6: Update SSH auto-provisioning callers [quick]
+└── Task 7: Add backend tests for firewall rule CRUD [quick]
+
+Wave 2 (Frontend — all parallel, depends on Task 1):
+├── Task 8: Delete LinkedDevices.vue + clean references [quick]
+├── Task 9: Redesign FirewallEditor.vue with Ng* design system [quick]
+├── Task 10: Restyle DeviceDetail.vue [quick]
+├── Task 11: Restyle Devices.vue [quick]
+├── Task 12: Restyle Dashboard.vue [quick]
+├── Task 13: Restyle Servers.vue modals/forms [quick]
+├── Task 14: Restyle Users.vue [quick]
+├── Task 15: Restyle AddPeerModal.vue + PeerConfigModal.vue [quick]
+├── Task 16: Restyle ShareConfig.vue [quick]
+└── Task 17: Restyle TrafficChart.vue SVG colors [quick]
+
+Wave FINAL (After ALL tasks — 4 parallel reviews):
+├── 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)
+-> Present results -> Get explicit user okay
+```
+
+### Dependency Matrix
+
+| Task | Depends On | Blocks | Wave |
+|------|-----------|--------|------|
+| 1 | - | 8-17 | 0 |
+| 2 | - | 7 | 1 |
+| 3 | - | 4, 5, 6, 7 | 1 |
+| 4 | 3 | 7 | 1 |
+| 5 | - | 7 | 1 |
+| 6 | 3 | 7 | 1 |
+| 7 | 2, 3, 4, 5, 6 | FINAL | 1 |
+| 8 | 1 | FINAL | 2 |
+| 9 | 1 | FINAL | 2 |
+| 10 | 1, 8 | FINAL | 2 |
+| 11-17 | 1 | FINAL | 2 |
+| F1-F4 | ALL | - | FINAL |
+
+### Agent Dispatch Summary
+
+- **Wave 0**: **1** — T1 → `quick`
+- **Wave 1**: **6** — T2-T6 → `quick`, T7 → `quick`
+- **Wave 2**: **10** — T8-T17 → `quick`
+- **FINAL**: **4** — F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep`
+
+---
+
+## TODOs
+
+- [x] 1. Define @theme design tokens in main.css
+
+ **What to do**:
+ - Add `@theme` block to `apps/dashboard-ui/src/assets/main.css` after `@import "tailwindcss";`
+ - Define ALL color tokens used by existing Ng* components (found by grepping `src/components/ui/*.vue`):
+ - Background: `--color-bg-base`, `--color-bg-surface`, `--color-bg-elevated`, `--color-bg-overlay`
+ - Text: `--color-text-primary`, `--color-text-secondary`, `--color-text-muted`
+ - Border: `--color-border-subtle`, `--color-border-default`, `--color-border-strong`
+ - Accent: `--color-accent`, `--color-accent-hover`
+ - Semantic: `--color-danger`, `--color-success`, `--color-warning`
+ - Shadow: `--shadow-glow`
+ - Color palette: dark glassmorphism theme — `bg-base` ~`#0a0a14`, `bg-surface` ~`#12121e`, `bg-elevated` ~`#1a1a2e`, accent = cyan (#06b6d4), danger = red (#ef4444)
+ - Verify existing Ng* components render correctly after tokens are defined
+
+ **Must NOT do**:
+ - Do NOT modify any Ng* component files
+ - Do NOT touch `style.css` (dead code)
+ - Do NOT add custom utility classes beyond what @theme provides
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: NO (prerequisite for all frontend tasks)
+ - **Parallel Group**: Wave 0
+ - **Blocks**: Tasks 8-17
+ - **Blocked By**: None
+
+ **References**:
+ - `apps/dashboard-ui/src/assets/main.css` — Current file (17 lines: @import + fadeInUp animation only)
+ - `apps/dashboard-ui/src/components/ui/NgCard.vue:14-17` — Uses `bg-bg-surface`, `bg-bg-elevated`, `border-border-subtle`, `border-border-default`, `shadow-glow`
+ - `apps/dashboard-ui/src/components/ui/NgButton.vue:18-22` — Uses `bg-accent`, `bg-accent-hover`, `bg-bg-elevated`, `bg-bg-surface`, `text-text-primary`, `text-text-secondary`, `bg-danger`, `ring-accent`, `ring-offset-bg-base`
+ - `apps/dashboard-ui/src/components/ui/NgInput.vue:49-51` — Uses `bg-bg-base`, `text-text-primary`, `placeholder-text-muted`, `border-border-default`, `border-border-strong`, `ring-accent`, `border-danger`, `ring-danger`
+ - `apps/dashboard-ui/src/components/ui/NgModal.vue:46,63,70,77` — Uses `bg-bg-overlay`, `from-bg-surface`, `to-bg-elevated`, `border-border-subtle`, `text-text-primary`, `text-text-muted`, `text-text-secondary`
+ - `apps/dashboard-ui/vite.config.ts` — Vite 8 + `@tailwindcss/vite` plugin (TailwindCSS v4 syntax, `@theme` directive)
+ - TailwindCSS v4 docs: `@theme` defines custom design tokens as CSS custom properties that generate utility classes
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: @theme tokens compile and generate utilities
+ Tool: Bash
+ Preconditions: apps/dashboard-ui has node_modules installed
+ Steps:
+ 1. Run: cd apps/dashboard-ui && npm run build
+ 2. Check exit code is 0
+ 3. Run: grep -c "@theme" src/assets/main.css
+ Expected Result: Build succeeds. grep returns 1 (one @theme block exists)
+ Evidence: .sisyphus/evidence/task-1-theme-build.txt
+
+ Scenario: All required token names defined
+ Tool: Bash
+ Steps:
+ 1. Run: grep -E "bg-base|bg-surface|bg-elevated|bg-overlay|text-primary|text-secondary|text-muted|border-subtle|border-default|border-strong|accent|accent-hover|danger|success|warning|shadow-glow" apps/dashboard-ui/src/assets/main.css | wc -l
+ Expected Result: At least 15 matches (all tokens present)
+ Evidence: .sisyphus/evidence/task-1-token-audit.txt
+ ```
+
+ **Commit**: YES (Commit A)
+ - Message: `feat(dashboard): add @theme design tokens to main.css`
+ - Files: `src/assets/main.css`
+ - Pre-commit: `npm run build`
+
+- [x] 2. Fix InitNetwork fwd_estab duplicates in nftables_linux.go
+
+ **What to do**:
+ - DELETE lines 67, 75, 83 in `nftables_linux.go` — these are copy-paste duplicates that insert `fwd_estab` into FORWARD chain from inside the INPUT chain section
+ - Keep ONLY line 43 (the correct one inside the FORWARD `wg_isolation` guard)
+ - Verify the idempotency guard: line 43 is inside `if exec.Command("sh", "-c", checkWgDrop).Run() != nil` — this means `fwd_estab` is only added when `wg_isolation` doesn't exist yet. This is correct behavior.
+
+ **Must NOT do**:
+ - Do NOT touch the INPUT chain rules (lines 48-84 excluding the fwd_estab duplicates)
+ - Do NOT change `wg_isolation` rule
+ - Do NOT use `nft flush table`
+ - Do NOT modify `AddForwardRule`, `AddInputRule`, or any method outside `InitNetwork`
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: YES
+ - **Parallel Group**: Wave 1 (with Tasks 3, 4, 5, 6)
+ - **Blocks**: Task 7
+ - **Blocked By**: None
+
+ **References**:
+ - `apps/server-core/internal/firewall/nftables_linux.go:25-87` — Full `InitNetwork()` function
+ - Line 43: Correct `fwd_estab` insert (inside FORWARD wg_isolation guard)
+ - Line 67: DUPLICATE — inside INPUT section, inserts into FORWARD
+ - Line 75: DUPLICATE — inside INPUT section, inserts into FORWARD
+ - Line 83: DUPLICATE — inside INPUT section, inserts into FORWARD
+ - All 3 duplicates lack idempotency guards (no grep check), so every `InitNetwork()` call creates new handles
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: fwd_estab appears exactly once in InitNetwork
+ Tool: Bash
+ Steps:
+ 1. Run: grep -c "fwd_estab" apps/server-core/internal/firewall/nftables_linux.go
+ Expected Result: Exactly 1 match (line 43 area only)
+ Evidence: .sisyphus/evidence/task-2-fwd-estab-count.txt
+
+ Scenario: Build compiles successfully
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go build ./...
+ Expected Result: Exit code 0, no errors
+ Evidence: .sisyphus/evidence/task-2-go-build.txt
+ ```
+
+ **Commit**: NO (groups with Commit B)
+
+- [x] 3. Add AddFirewallRule/RemoveFirewallRule to NetManager interface
+
+ **What to do**:
+ - Add new method to `manager.go` interface:
+ ```go
+ AddFirewallRule(ruleName string, sourceIP net.IP, destCIDR string, portRange string, protocol string, action string) error
+ RemoveFirewallRule(ruleName string) error
+ ```
+ - Implement in `nftables_linux.go`:
+ - `AddFirewallRule`: Generate proper `nft insert rule ip nexusguard forward ip saddr {sourceIP} ip daddr {destCIDR} {protocol} dport {portRange} {action} comment "fwrule_{ruleName}"`
+ - Handle protocol: `tcp`, `udp`, or omit for `both`
+ - Handle portRange: empty = all ports (no dport match), single port `80`, range `80-443`
+ - Handle action: `accept` or `drop`
+ - Handle destCIDR: single IP auto-appended `/32`, CIDR passed as-is
+ - `RemoveFirewallRule`: Find and delete ALL handles matching `comment "fwrule_{ruleName}"` (loop over awk output)
+ - Implement stubs in `nftables_stub.go` (return nil)
+ - Do NOT remove old `AddRangeRule`/`RemoveRangeRule` yet — keep for backward compatibility until callers are updated
+
+ **Must NOT do**:
+ - Do NOT remove `AddRangeRule`/`RemoveRangeRule` from interface (callers still reference them until Task 4/6)
+ - Do NOT touch `AddForwardRule`, `AddInputRule`, or any other existing methods
+ - Do NOT use `nft flush table`
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: YES
+ - **Parallel Group**: Wave 1 (with Tasks 2, 5)
+ - **Blocks**: Tasks 4, 6, 7
+ - **Blocked By**: None
+
+ **References**:
+ - `apps/server-core/internal/firewall/manager.go` — Full interface (17 lines). New methods follow existing pattern.
+ - `apps/server-core/internal/firewall/nftables_linux.go:181-200` — `AddForwardRule` as PATTERN to follow: uses `nft insert rule`, handles comma-separated CIDRs, uses comment for identification, uses `exec.Command`
+ - `apps/server-core/internal/firewall/nftables_linux.go:202-211` — `RemoveForwardRule` as PATTERN but with KNOWN BUG (single handle). New `RemoveFirewallRule` must loop ALL handles.
+ - `apps/server-core/internal/firewall/nftables_stub.go` — All methods return nil, one-liner pattern
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: Interface compiles with new methods
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go build ./...
+ Expected Result: Exit code 0
+ Evidence: .sisyphus/evidence/task-3-go-build.txt
+
+ Scenario: New methods exist in all 3 files
+ Tool: Bash
+ Steps:
+ 1. Run: grep -c "AddFirewallRule" apps/server-core/internal/firewall/manager.go apps/server-core/internal/firewall/nftables_linux.go apps/server-core/internal/firewall/nftables_stub.go
+ Expected Result: 1 match per file (3 total)
+ Evidence: .sisyphus/evidence/task-3-method-check.txt
+ ```
+
+ **Commit**: NO (groups with Commit B)
+
+- [x] 4. Rewrite syncRuleToFirewall + fix rule delete in rules.go
+
+ **What to do**:
+ - Rewrite `syncRuleToFirewall()` in `rules.go` to use new `AddFirewallRule()`:
+ ```go
+ func (h *RulesHandler) syncRuleToFirewall(rule models.FirewallRule, deviceName string) {
+ // Get device's internal IP for source matching
+ var device models.Device
+ if err := h.db.Where("id = ?", rule.DeviceID).First(&device).Error; err != nil {
+ return
+ }
+ sourceIP := net.ParseIP(device.InternalIP)
+ if sourceIP == nil {
+ return
+ }
+
+ destCIDR := rule.DestIPRange
+ // Single IP → append /32
+ if !strings.Contains(destCIDR, "/") {
+ destCIDR += "/32"
+ }
+
+ h.fw.AddFirewallRule(
+ rule.ID.String(),
+ sourceIP,
+ destCIDR,
+ rule.DestPortRange, // "" = all ports, "80" = single, "80-443" = range
+ rule.Protocol, // "tcp", "udp", "both"
+ rule.Action, // "accept", "drop"
+ )
+ }
+ ```
+ - Update `Delete()` handler to use `RemoveFirewallRule()` instead of `RemoveRangeRule()`:
+ ```go
+ h.fw.RemoveFirewallRule(rule.ID.String())
+ ```
+ - Add input validation in `Create()`: validate `DestPortRange` format (empty, single number, or `N-N` range)
+ - Add input validation: validate `Action` is `accept` or `drop` only
+ - Add input validation: validate `Protocol` is `tcp`, `udp`, or `both` only
+
+ **Must NOT do**:
+ - Do NOT change the API request/response shape (keep `CreateRuleRequest` struct)
+ - Do NOT add new fields to the model
+ - Do NOT add logging or audit trails
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: YES (after Task 3)
+ - **Parallel Group**: Wave 1
+ - **Blocks**: Task 7
+ - **Blocked By**: Task 3
+
+ **References**:
+ - `apps/server-core/api/rules.go` — Full file (135 lines). `syncRuleToFirewall` at line 114-135, `Delete` at line 83-112, `Create` at line 49-81
+ - `apps/server-core/api/helpers.go` — Has `validateAllowedIPs()` pattern to follow for validation
+ - `apps/server-core/internal/models/models.go:95-112` — `FirewallRule` model: ID, DeviceID, DestIPRange, DestPortRange, Protocol, Action
+ - `apps/server-core/api/rules.go:109` — Current delete uses `RemoveRangeRule("rule_" + rule.ID.String())` → change to `RemoveFirewallRule(rule.ID.String())`
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: syncRuleToFirewall uses new AddFirewallRule
+ Tool: Bash
+ Steps:
+ 1. Run: grep "AddFirewallRule" apps/server-core/api/rules.go
+ 2. Run: grep "AddRangeRule" apps/server-core/api/rules.go
+ Expected Result: AddFirewallRule found, AddRangeRule NOT found
+ Evidence: .sisyphus/evidence/task-4-sync-rewrite.txt
+
+ Scenario: Delete uses RemoveFirewallRule
+ Tool: Bash
+ Steps:
+ 1. Run: grep "RemoveFirewallRule" apps/server-core/api/rules.go
+ 2. Run: grep "RemoveRangeRule" apps/server-core/api/rules.go
+ Expected Result: RemoveFirewallRule found, RemoveRangeRule NOT found
+ Evidence: .sisyphus/evidence/task-4-delete-fix.txt
+
+ Scenario: Build succeeds
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go build ./...
+ Expected Result: Exit code 0
+ Evidence: .sisyphus/evidence/task-4-build.txt
+ ```
+
+ **Commit**: NO (groups with Commit B)
+
+- [x] 5. Fix RemoveForwardRule multi-CIDR handling
+
+ **What to do**:
+ - Fix `RemoveForwardRule()` in `nftables_linux.go` to handle multiple handles (multi-CIDR peers)
+ - Current bug: `awk '/comment \\"peer_xxx\\"/ {print $NF}'` returns only the LAST match when piped to a single string. Only one handle is deleted.
+ - Fix: Loop over ALL matching handles. Use approach:
+ ```go
+ func (m *LinuxManager) RemoveForwardRule(peerName string) error {
+ // Match both exact "peer_xxx" and suffixed "peer_xxx_0", "peer_xxx_1" patterns
+ cmdStr := fmt.Sprintf(`nft -a list chain ip nexusguard forward | grep -E 'comment "peer_%s(_[0-9]+)?"' | awk '{print $NF}'`, peerName)
+ out, err := exec.Command("sh", "-c", cmdStr).Output()
+ if err != nil || len(out) == 0 {
+ return nil
+ }
+ for _, handle := range strings.Split(strings.TrimSpace(string(out)), "\n") {
+ handle = strings.TrimSpace(handle)
+ if handle == "" {
+ continue
+ }
+ exec.Command("sh", "-c", fmt.Sprintf("nft delete rule ip nexusguard forward handle %s", handle)).Run()
+ }
+ return nil
+ }
+ ```
+ - Note: `AddForwardRule` creates rules with comments `peer_xxx` (single CIDR) or `peer_xxx_0`, `peer_xxx_1` (multi-CIDR). The grep pattern must match all variants.
+
+ **Must NOT do**:
+ - Do NOT modify `AddForwardRule` — it works correctly
+ - Do NOT use `nft flush table`
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: YES
+ - **Parallel Group**: Wave 1 (with Tasks 2, 3)
+ - **Blocks**: Task 7
+ - **Blocked By**: None
+
+ **References**:
+ - `apps/server-core/internal/firewall/nftables_linux.go:202-211` — Current buggy `RemoveForwardRule` (single handle)
+ - `apps/server-core/internal/firewall/nftables_linux.go:181-200` — `AddForwardRule` creates comments `peer_{name}` and `peer_{name}_{i}` for multi-CIDR
+ - Line 190-192: Suffix logic — `suffix = fmt.Sprintf("_%d", i)` for multi-CIDR, empty string for single CIDR
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: RemoveForwardRule loops over all handles
+ Tool: Bash
+ Steps:
+ 1. Run: grep -A 15 "func.*RemoveForwardRule" apps/server-core/internal/firewall/nftables_linux.go
+ 2. Verify the output contains a loop (for/range or while) over handles
+ 3. Verify grep pattern matches both "peer_xxx" and "peer_xxx_N" suffixes
+ Expected Result: Function contains loop + regex pattern for suffixed comments
+ Evidence: .sisyphus/evidence/task-5-remove-forward.txt
+
+ Scenario: Build succeeds
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go build ./...
+ Expected Result: Exit code 0
+ Evidence: .sisyphus/evidence/task-5-build.txt
+ ```
+
+ **Commit**: NO (groups with Commit B)
+
+- [x] 6. Update SSH auto-provisioning callers (devices.go + peers.go)
+
+ **What to do**:
+ - Update `apps/server-core/api/devices.go:144` — replace `AddRangeRule` call with `AddFirewallRule`:
+ ```go
+ // Before: _ = h.fw.AddRangeRule("rule_"+rule.ID.String(), ip, ip, 22, 22)
+ // After:
+ _ = h.fw.AddFirewallRule(rule.ID.String(), sourceIP, rule.DestIPRange, "22", "tcp", "accept")
+ ```
+ Where `sourceIP` is the device's InternalIP (already available in the handler context)
+ - Update `apps/server-core/api/peers.go:128` — same pattern as devices.go
+ - Read surrounding code context to get the correct `sourceIP` variable name (the device's internal IP is available in the handler)
+ - After updating both callers, `AddRangeRule` and `RemoveRangeRule` can be removed from the interface + all implementations (cleanup)
+
+ **Must NOT do**:
+ - Do NOT change the SSH auto-provisioning logic (still creates port 22 TCP accept rule)
+ - Do NOT change the FirewallRule model or DB operations
+ - Do NOT remove `AddRangeRule`/`RemoveRangeRule` until AFTER both callers are updated
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: YES (after Task 3)
+ - **Parallel Group**: Wave 1
+ - **Blocks**: Task 7
+ - **Blocked By**: Task 3
+
+ **References**:
+ - `apps/server-core/api/devices.go:144` — Current: `_ = h.fw.AddRangeRule("rule_"+rule.ID.String(), ip, ip, 22, 22)`. Read lines 130-150 for full context to find the sourceIP variable.
+ - `apps/server-core/api/peers.go:128` — Current: `_ = h.fw.AddRangeRule("rule_"+rule.ID.String(), ip, ip, 22, 22)`. Read lines 115-135 for full context.
+ - `apps/server-core/internal/firewall/manager.go` — After cleanup, remove `AddRangeRule`/`RemoveRangeRule` from interface
+ - `apps/server-core/internal/firewall/nftables_linux.go:154-177` — Remove `AddRangeRule`/`RemoveRangeRule` implementations
+ - `apps/server-core/internal/firewall/nftables_stub.go:22-23` — Remove stub methods
+ - `apps/server-core/internal/firewall/nftables_test.go:40-52` — Remove or update `TestRangeRule`
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: No more AddRangeRule callers in API
+ Tool: Bash
+ Steps:
+ 1. Run: grep -r "AddRangeRule" apps/server-core/
+ Expected Result: 0 matches (removed from interface, impls, callers, and tests)
+ Evidence: .sisyphus/evidence/task-6-no-rangerule.txt
+
+ Scenario: AddFirewallRule used in devices.go and peers.go
+ Tool: Bash
+ Steps:
+ 1. Run: grep "AddFirewallRule" apps/server-core/api/devices.go apps/server-core/api/peers.go
+ Expected Result: 1 match per file (2 total)
+ Evidence: .sisyphus/evidence/task-6-new-callers.txt
+
+ Scenario: Build succeeds after cleanup
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go build ./...
+ Expected Result: Exit code 0
+ Evidence: .sisyphus/evidence/task-6-build.txt
+ ```
+
+ **Commit**: NO (groups with Commit B)
+
+- [x] 7. Add backend tests for firewall rule CRUD
+
+ **What to do**:
+ - Add/update tests in `apps/server-core/api/rules_test.go` (if exists) or create it:
+ - Test `Create` handler: valid rule with IP/CIDR, port range, protocol, action → 201 Created
+ - Test `Create` handler: invalid port format → 400
+ - Test `Create` handler: invalid action → 400
+ - Test `Create` handler: invalid protocol → 400
+ - Test `Delete` handler: existing rule → 200, verify rule removed from DB
+ - Test `List` handler: returns rules for a device
+ - Update `apps/server-core/internal/firewall/nftables_test.go`:
+ - Replace `TestRangeRule` with `TestFirewallRule` testing new `AddFirewallRule`/`RemoveFirewallRule` via stub
+ - Run all tests: `go test ./... -tags dev -count=1`
+
+ **Must NOT do**:
+ - Do NOT write tests that require a running Linux nftables kernel — use stub manager
+ - Do NOT mock the database — use GORM in-memory SQLite (pattern from existing tests)
+
+ **Recommended Agent Profile**:
+ - **Category**: `quick`
+ - **Skills**: []
+
+ **Parallelization**:
+ - **Can Run In Parallel**: NO (depends on all Wave 1 tasks)
+ - **Parallel Group**: Wave 1 (sequential after Tasks 2-6)
+ - **Blocks**: Final verification
+ - **Blocked By**: Tasks 2, 3, 4, 5, 6
+
+ **References**:
+ - `apps/server-core/internal/firewall/nftables_test.go` — Existing test file (52 lines), has `TestRangeRule` to replace
+ - `apps/server-core/internal/models/models_test.go` — Pattern for GORM in-memory SQLite test setup (lines 1-30)
+ - `apps/server-core/api/rules.go` — Handler code to test
+ - Check if `apps/server-core/api/rules_test.go` exists — if so, read it for existing patterns
+
+ **Acceptance Criteria**:
+
+ **QA Scenarios (MANDATORY):**
+
+ ```
+ Scenario: All tests pass
+ Tool: Bash
+ Steps:
+ 1. Run: cd apps/server-core && go test ./... -tags dev -count=1 -v 2>&1 | tail -30
+ Expected Result: All tests PASS, exit code 0
+ Evidence: .sisyphus/evidence/task-7-test-results.txt
+
+ Scenario: New firewall tests exist
+ Tool: Bash
+ Steps:
+ 1. Run: grep -c "TestFirewallRule\|TestCreate.*Rule\|TestDelete.*Rule\|TestList.*Rule" apps/server-core/internal/firewall/nftables_test.go apps/server-core/api/rules_test.go 2>/dev/null || echo "no test files"
+ Expected Result: At least 3 test functions found
+ Evidence: .sisyphus/evidence/task-7-test-count.txt
+ ```
+
+ **Commit**: YES (Commit B)
+ - Message: `fix(firewall): rewrite nftables rule system + fix fwd_estab duplicates`
+ - Files: `internal/firewall/manager.go`, `internal/firewall/nftables_linux.go`, `internal/firewall/nftables_stub.go`, `internal/firewall/nftables_test.go`, `api/rules.go`, `api/rules_test.go`, `api/devices.go`, `api/peers.go`
+ - Pre-commit: `go test ./... -tags dev -count=1`
+
+- [x] 8. Delete LinkedDevices.vue + clean all references
+
+ **What to do**:
+ - Delete `apps/dashboard-ui/src/components/LinkedDevices.vue`
+ - Remove import and template usage from `DeviceDetail.vue`:
+ - Line 175: `