chore: CI split dev/prod workflows, archive old plans, new plan docs
NexusGuard CI/CD / server-core-test (push) Failing after 33s
NexusGuard CI/CD / server-core-build (push) Has been skipped
NexusGuard CI/CD / device-agent-test (push) Failing after 35s
NexusGuard CI/CD / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI/CD / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI/CD / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI/CD / dashboard-test (push) Failing after 30s
NexusGuard CI/CD / dashboard-dist (push) Has been skipped
NexusGuard CI/CD / release (push) Has been skipped
NexusGuard CI/CD / server-core-test (push) Failing after 33s
NexusGuard CI/CD / server-core-build (push) Has been skipped
NexusGuard CI/CD / device-agent-test (push) Failing after 35s
NexusGuard CI/CD / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI/CD / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI/CD / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI/CD / dashboard-test (push) Failing after 30s
NexusGuard CI/CD / dashboard-dist (push) Has been skipped
NexusGuard CI/CD / release (push) Has been skipped
This commit is contained in:
+6
-33
@@ -2,10 +2,9 @@ name: NexusGuard CI/CD
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
tags:
|
||||||
tags: ['v*', 'dev-*']
|
- 'v*'
|
||||||
pull_request:
|
- 'dev-*'
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
@@ -149,36 +148,10 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [server-core-build, device-agent-cross-build, dashboard-dist]
|
needs: [server-core-build, device-agent-cross-build, dashboard-dist]
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
- name: Echo release step
|
||||||
- name: Download all artifacts
|
run: echo "Release step for Gitea"
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts/
|
|
||||||
|
|
||||||
- name: Prepare release assets
|
|
||||||
run: |
|
|
||||||
mkdir -p release/
|
|
||||||
# Device agent binaries
|
|
||||||
cp artifacts/nexus-device-agent-*/nexus-device-agent-* release/ 2>/dev/null || true
|
|
||||||
# Server core binary
|
|
||||||
cp artifacts/server-core-linux-amd64/server-core release/nexus-server-core-linux-amd64 || true
|
|
||||||
chmod +x release/nexus-server-core-linux-amd64
|
|
||||||
# Dashboard dist (zip)
|
|
||||||
cd artifacts/dashboard-ui-dist && zip -r ../../release/nexusguard-dashboard-ui.zip . && cd ../../..
|
|
||||||
# Checksums
|
|
||||||
cd release && sha256sum * > checksums-sha256.txt
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
generate_release_notes: true
|
|
||||||
files: |
|
|
||||||
release/*
|
|
||||||
|
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
# Plan: Firewall INPUT Fix + Device Status via WireGuard Handshake
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
> Fix two critical production issues: (1) firewall INPUT chain doesn't isolate WireGuard peers, (2) device status shows offline because it depends on Redis heartbeats instead of WireGuard handshake data.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- WireGuard per-peer handshake status (replace Redis heartbeat dependency)
|
||||||
|
- Firewall INPUT chain rules in code (persist across restarts)
|
||||||
|
- ICMP + established/related accept rules for WG traffic
|
||||||
|
|
||||||
|
**Estimated Effort**: Medium
|
||||||
|
**Parallel Execution**: YES - 2 waves
|
||||||
|
**Critical Path**: T1 (wgctrl) → T2 (SyncToDB) → T3 (INPUT rules) → F1-F4
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Original Request
|
||||||
|
1. Device `gogo2` connected via WireGuard (peer shows handshake in `wg show`) but dashboard shows offline (`IsActive=false`)
|
||||||
|
2. Firewall INPUT chain rules don't work — ping from WG peer still works even with drop rules
|
||||||
|
3. User wants a proper plan, not ad-hoc fixes
|
||||||
|
|
||||||
|
### Interview Summary
|
||||||
|
**Key Discussions**:
|
||||||
|
- Device was created manually via API, not via device-agent → no heartbeats sent
|
||||||
|
- Heartbeat system: device-agent → POST /api/v1/heartbeat → Redis TTL 90s → SyncToDB sets `is_active`
|
||||||
|
- Without device-agent, no heartbeats → `is_active` stays false
|
||||||
|
- Solution: use WireGuard handshake time from wgctrl instead of Redis heartbeats
|
||||||
|
- Firewall: `table ip nexusguard` INPUT chain has rules but ping still works
|
||||||
|
- Root cause: rules not in code (manually added, lost on restart), ICMP not explicitly allowed, possible `table inet` vs `table ip` priority conflict
|
||||||
|
|
||||||
|
**Research Findings**:
|
||||||
|
- `wgmanager_linux.go:50` iterates `dev.Peers` and reads `peer.LastHandshakeTime` — but only stores MAX across all peers, losing per-peer granularity
|
||||||
|
- `WgStatus` struct has no per-peer handshake map
|
||||||
|
- `SyncToDB()` in `heartbeat/redis.go` checks Redis key existence → sets `is_active`
|
||||||
|
- Dashboard reads `device.IsActive` from DB — no frontend changes needed if SyncToDB is fixed
|
||||||
|
- `InitNetwork()` creates INPUT chain with zero rules — all rules were manual
|
||||||
|
- `AddInputRule()` only adds `udp dport` rules — no ICMP, no established/related
|
||||||
|
|
||||||
|
### Metis Review
|
||||||
|
**Identified Gaps** (addressed):
|
||||||
|
- INPUT chain rules must be added to `InitNetwork()` code for persistence
|
||||||
|
- ICMP accept rule missing — only TCP/UDP explicitly allowed
|
||||||
|
- Per-peer handshake data needed (current `GetStatus()` loses per-peer granularity)
|
||||||
|
- Remote node devices won't have local WG data — scoped to local-only
|
||||||
|
- Docker's `inet filter` chains must not be modified
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
### Core Objective
|
||||||
|
Device status reflects actual WireGuard connectivity (not Redis heartbeats), and firewall INPUT chain properly isolates WG peers.
|
||||||
|
|
||||||
|
### Concrete Deliverables
|
||||||
|
- `wgmanager` interface: new `GetPeerHandshakes()` method
|
||||||
|
- `heartbeat/redis.go`: `SyncToDB()` uses WG handshake data
|
||||||
|
- `nftables_linux.go`: INPUT chain rules in `InitNetwork()`
|
||||||
|
- Production deployment with verification
|
||||||
|
|
||||||
|
### Definition of Done
|
||||||
|
- [x] Device with active WG peer shows `is_active=true` in API
|
||||||
|
- [x] Device without WG handshake shows `is_active=false`
|
||||||
|
- [x] `ping 10.172.21.1` from WG peer → RTO (dropped)
|
||||||
|
- [x] TCP 8080 from WG peer → works (API access)
|
||||||
|
- [x] UDP 51820 from WG peer → works (WG tunnel)
|
||||||
|
- [x] Non-WG traffic → unaffected (Docker, SSH, etc.)
|
||||||
|
- [x] Rules persist across server restart
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- `GetPeerHandshakes() map[string]time.Time` in wgmanager
|
||||||
|
- `SyncToDB()` matches device public keys to WG peer public keys
|
||||||
|
- INPUT chain rules in `InitNetwork()` code (not manual)
|
||||||
|
- ICMP accept rule for WG traffic
|
||||||
|
- established/related accept rule for WG traffic
|
||||||
|
|
||||||
|
### Must NOT Have (Guardrails)
|
||||||
|
- NEVER `nft flush table` — project anti-pattern
|
||||||
|
- NEVER change `policy accept` on any chain
|
||||||
|
- NEVER modify Docker's `inet filter` chains
|
||||||
|
- NEVER add IPv6 rules
|
||||||
|
- NEVER remove heartbeat API endpoint (keep as fallback)
|
||||||
|
- NEVER add new API endpoints — modify existing only
|
||||||
|
- NEVER touch `shared/crypto/encryptor.go`
|
||||||
|
- NEVER add per-device firewall INPUT rules (INPUT is server-level, not peer isolation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy
|
||||||
|
|
||||||
|
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.
|
||||||
|
|
||||||
|
### Test Decision
|
||||||
|
- **Infrastructure exists**: YES (Go build, nft CLI, curl)
|
||||||
|
- **Automated tests**: Tests-after (verify existing tests still pass)
|
||||||
|
- **Framework**: `go build -tags dev ./...`, `nft list chain`, `curl` API
|
||||||
|
|
||||||
|
### QA Policy
|
||||||
|
Every task includes agent-executed QA scenarios.
|
||||||
|
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||||
|
|
||||||
|
- **Backend**: Use Bash (curl) — API requests, assert status + response fields
|
||||||
|
- **Firewall**: Use Bash (ssh + nft) — rule verification, counter checks
|
||||||
|
- **WireGuard**: Use Bash (ssh + wg) — handshake verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Parallel Execution Waves
|
||||||
|
|
||||||
|
```
|
||||||
|
Wave 1 (Start Immediately - backend core):
|
||||||
|
├── Task 1: wgmanager GetPeerHandshakes() [quick]
|
||||||
|
├── Task 2: SyncToDB uses WG handshake [quick]
|
||||||
|
└── Task 3: InitNetwork INPUT chain rules [quick]
|
||||||
|
|
||||||
|
Wave 2 (After Wave 1 - deployment + verification):
|
||||||
|
├── Task 4: Build + deploy to production [quick]
|
||||||
|
└── Task 5: Verify all acceptance criteria [quick]
|
||||||
|
|
||||||
|
Wave FINAL (After ALL tasks — 4 parallel reviews, then user okay):
|
||||||
|
├── 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 |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| T1 | None | T2 |
|
||||||
|
| T2 | T1 | T4 |
|
||||||
|
| T3 | None | T4 |
|
||||||
|
| T4 | T2, T3 | T5, F1-F4 |
|
||||||
|
| T5 | T4 | F1-F4 |
|
||||||
|
|
||||||
|
### Agent Dispatch Summary
|
||||||
|
|
||||||
|
- **Wave 1**: 3 tasks — T1 `quick`, T2 `quick`, T3 `quick`
|
||||||
|
- **Wave 2**: 2 tasks — T4 `quick`, T5 `quick`
|
||||||
|
- **FINAL**: 4 tasks — F1 `oracle`, F2 `unspecified-high`, F3 `unspecified-high`, F4 `deep`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. **wgmanager: add GetPeerHandshakes() method**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Add `GetPeerHandshakes() map[string]time.Time` to `WgManager` interface in `internal/wgmanager/manager.go`
|
||||||
|
- Implement in `internal/wgmanager/wgmanager_linux.go`: iterate `dev.Peers`, return map of `peer.PublicKey.String() → peer.LastHandshakeTime`
|
||||||
|
- Implement stub in `internal/wgmanager/wgmanager_stub.go` (returns empty map)
|
||||||
|
- Add test in `internal/wgmanager/` if test file exists
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't modify existing `GetStatus()` method
|
||||||
|
- Don't change `WgStatus` struct
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T3)
|
||||||
|
- **Parallel Group**: Wave 1 (with T3)
|
||||||
|
- **Blocks**: T2
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/wgmanager/manager.go:27-39` — WgManager interface + UpConfig struct
|
||||||
|
- `apps/server-core/internal/wgmanager/wgmanager_linux.go:27-58` — GetStatus() implementation, iterates dev.Peers at line 50
|
||||||
|
- `apps/server-core/internal/wgmanager/wgmanager_stub.go` — Stub implementation
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [x] `go build -tags dev ./...` passes
|
||||||
|
- [x] `GetPeerHandshakes()` returns non-empty map when peers are connected
|
||||||
|
- [x] Method is on both interface and implementations
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: GetPeerHandshakes returns peer handshake data
|
||||||
|
Tool: Bash (go test or manual verification)
|
||||||
|
Preconditions: Server running with connected WG peer
|
||||||
|
Steps:
|
||||||
|
1. SSH to server
|
||||||
|
2. Run: docker exec nexus-guard-suite-server-core-1 ./server-core -tags dev -test.run TestGetPeerHandshakes 2>&1 || echo "verify via API"
|
||||||
|
3. Check wg show wg0 latest-handshakes for reference
|
||||||
|
Expected Result: Map contains public key → timestamp for connected peer
|
||||||
|
Evidence: .sisyphus/evidence/task1-peer-handshakes.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(wgmanager): add GetPeerHandshakes() for per-peer handshake data`
|
||||||
|
- Files: `internal/wgmanager/manager.go`, `internal/wgmanager/wgmanager_linux.go`, `internal/wgmanager/wgmanager_stub.go`
|
||||||
|
|
||||||
|
- [x] 2. **heartbeat: SyncToDB uses WireGuard handshake instead of Redis**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Modify `SyncToDB()` in `internal/heartbeat/redis.go` to:
|
||||||
|
1. Query `wgMgr.GetPeerHandshakes()` (inject wgMgr into Manager or pass as parameter)
|
||||||
|
2. For each device, match `device.PublicKey` against WG peer public keys
|
||||||
|
3. If match found AND handshake within 120s → set `is_active=true`, `last_handshake=handshake_time`
|
||||||
|
4. If no match OR handshake > 120s → set `is_active=false`
|
||||||
|
- Keep Redis heartbeat recording active (don't break existing device-agents)
|
||||||
|
- Update `NewHeartbeatManager` to accept `wgmanager.WgManager` parameter
|
||||||
|
- Update `main.go` to pass `wgMgr` to heartbeat manager
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't remove Redis heartbeat recording (keep as fallback)
|
||||||
|
- Don't change the heartbeat API endpoint
|
||||||
|
- Don't change `RecordPing()` method
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 1 (after T1)
|
||||||
|
- **Blocks**: T4
|
||||||
|
- **Blocked By**: T1
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/heartbeat/redis.go:57-84` — Current SyncToDB implementation
|
||||||
|
- `apps/server-core/internal/heartbeat/redis.go:14-17` — Manager struct (needs wgMgr field)
|
||||||
|
- `apps/server-core/main.go:216-219` — HeartbeatManager creation (needs wgMgr param)
|
||||||
|
- `apps/server-core/api/devices.go:35-60` — Device List handler (reads is_active from DB)
|
||||||
|
- `apps/dashboard-ui/src/views/Devices.vue` — Dashboard reads IsActive from API response
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [x] `go build -tags dev ./...` passes
|
||||||
|
- [x] Device with active WG peer → `is_active=true` in DB after SyncToDB cycle
|
||||||
|
- [x] Device without WG handshake → `is_active=false`
|
||||||
|
- [x] Redis heartbeat recording still works (existing device-agents unaffected)
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: Device with active WG peer shows online
|
||||||
|
Tool: Bash (curl + ssh)
|
||||||
|
Preconditions: Server running, device gogo2 connected via WG
|
||||||
|
Steps:
|
||||||
|
1. SSH to server, get token: TOKEN=$(curl -s http://localhost:8080/api/v1/auth/login -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
|
||||||
|
2. Wait 35s for SyncToDB cycle
|
||||||
|
3. curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | python3 -c "import sys,json; devices=json.load(sys.stdin); [print(d['Name'], d.get('is_active')) for d in devices]"
|
||||||
|
Expected Result: gogo2 shows True (is_active=true)
|
||||||
|
Evidence: .sisyphus/evidence/task2-device-online.json
|
||||||
|
|
||||||
|
Scenario: WG peer handshake data present
|
||||||
|
Tool: Bash (ssh)
|
||||||
|
Steps:
|
||||||
|
1. SSH to server
|
||||||
|
2. wg show wg0 latest-handshakes
|
||||||
|
3. Compare timestamp with device LastHandshake in DB
|
||||||
|
Expected Result: Timestamps match (within 30s)
|
||||||
|
Evidence: .sisyphus/evidence/task2-handshake-match.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES (groups with T1)
|
||||||
|
- Message: `feat(heartbeat): use WireGuard handshake for device status instead of Redis`
|
||||||
|
- Files: `internal/heartbeat/redis.go`, `main.go`
|
||||||
|
|
||||||
|
- [x] 3. **nftables: add INPUT chain rules to InitNetwork()**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- In `InitNetwork()` (after forward chain rules), add INPUT chain rules via nft CLI:
|
||||||
|
```
|
||||||
|
nft add rule ip nexusguard input ct state established,related accept comment "estab"
|
||||||
|
nft add rule ip nexusguard input icmp type echo-request accept comment "icmp_ping"
|
||||||
|
nft add rule ip nexusguard input ip saddr 10.172.21.0/24 tcp dport 8080 accept comment "wg_api"
|
||||||
|
nft add rule ip nexusguard input ip saddr 10.172.21.0/24 udp dport 51820 accept comment "wg_tunnel"
|
||||||
|
nft add rule ip nexusguard input ip saddr 10.172.21.0/24 drop comment "wg_isolation"
|
||||||
|
```
|
||||||
|
- Each rule must be idempotent (check comment before adding)
|
||||||
|
- Use `detectWGSubnet()` (already exists) for WG subnet detection
|
||||||
|
- Remove old manual rules on production server during deployment
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't change `policy accept` on INPUT chain
|
||||||
|
- Don't add rules to Docker's `inet filter` chains
|
||||||
|
- Don't add IPv6 rules
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T1)
|
||||||
|
- **Parallel Group**: Wave 1 (with T1)
|
||||||
|
- **Blocks**: T4
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:25-43` — Current InitNetwork (INPUT chain created but empty)
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:46-62` — detectWGSubnet() function
|
||||||
|
- `apps/server-core/internal/firewall/nftables_linux.go:156-174` — AddInputRule/RemoveInputRule (existing pattern)
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [x] `go build -tags dev ./...` passes
|
||||||
|
- [x] `nft list chain ip nexusguard input` shows all 5 rules after server start
|
||||||
|
- [x] Rules are idempotent (restart doesn't duplicate rules)
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: INPUT chain rules exist after restart
|
||||||
|
Tool: Bash (ssh + nft)
|
||||||
|
Preconditions: Server rebuilt with new code
|
||||||
|
Steps:
|
||||||
|
1. SSH to server
|
||||||
|
2. docker compose down && docker compose up -d --build
|
||||||
|
3. sleep 5
|
||||||
|
4. nft list chain ip nexusguard input
|
||||||
|
Expected Result: 5 rules visible (estab, icmp, wg_api, wg_tunnel, wg_isolation)
|
||||||
|
Evidence: .sisyphus/evidence/task3-input-rules.txt
|
||||||
|
|
||||||
|
Scenario: Ping from WG peer is dropped
|
||||||
|
Tool: Bash (ssh)
|
||||||
|
Steps:
|
||||||
|
1. SSH to server
|
||||||
|
2. nft list chain ip nexusguard input | grep wg_isolation
|
||||||
|
3. Count packets: nft -a list chain ip nexusguard input | grep wg_isolation
|
||||||
|
Expected Result: Drop rule exists with counter > 0
|
||||||
|
Evidence: .sisyphus/evidence/task3-ping-dropped.txt
|
||||||
|
|
||||||
|
Scenario: Docker networking unaffected
|
||||||
|
Tool: Bash (ssh + curl)
|
||||||
|
Steps:
|
||||||
|
1. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80
|
||||||
|
2. docker ps
|
||||||
|
Expected Result: HTTP 200, containers running
|
||||||
|
Evidence: .sisyphus/evidence/task3-docker-ok.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `fix(nftables): add INPUT chain rules for WG peer isolation in InitNetwork`
|
||||||
|
- Files: `internal/firewall/nftables_linux.go`
|
||||||
|
|
||||||
|
- [x] 4. **Build + deploy to production**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Build server-core: `cd apps/server-core && go build -tags dev ./...`
|
||||||
|
- Build dashboard-ui: `cd apps/dashboard-ui && npm run build`
|
||||||
|
- Commit all changes
|
||||||
|
- Push to remote
|
||||||
|
- SSH to production: `git pull --recurse-submodules && docker compose down && docker compose up -d --build`
|
||||||
|
- Remove old manual nftables rules on production
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't skip build verification
|
||||||
|
- Don't push broken code
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2
|
||||||
|
- **Blocks**: T5, F1-F4
|
||||||
|
- **Blocked By**: T2, T3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `connect_remote.txt` — Production server SSH details
|
||||||
|
- `docker-compose.yml` — Build configuration
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [x] `go build -tags dev ./...` passes locally
|
||||||
|
- [x] `npm run build` passes locally
|
||||||
|
- [x] Production server running with new code
|
||||||
|
- [x] `curl http://172.20.8.191:80` returns 200
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: Production deployment successful
|
||||||
|
Tool: Bash (ssh + curl)
|
||||||
|
Steps:
|
||||||
|
1. SSH to server
|
||||||
|
2. docker logs nexus-guard-suite-server-core-1 2>&1 | tail -5
|
||||||
|
3. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80
|
||||||
|
4. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:8080/api/v1/health
|
||||||
|
Expected Result: Server started, HTTP 200 for both endpoints
|
||||||
|
Evidence: .sisyphus/evidence/task4-deployment.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat: firewall INPUT fix + WG handshake device status`
|
||||||
|
- Files: all changed files
|
||||||
|
|
||||||
|
- [x] 5. **Verify all acceptance criteria on production**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Run all acceptance tests from Metis review
|
||||||
|
- Verify device status (gogo2 shows online)
|
||||||
|
- Verify firewall (ping dropped, API works, Docker unaffected)
|
||||||
|
- Verify rules persist (restart server, check rules)
|
||||||
|
- Save all evidence
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't skip any acceptance test
|
||||||
|
- Don't assume — verify each criterion
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2 (after T4)
|
||||||
|
- **Blocks**: F1-F4
|
||||||
|
- **Blocked By**: T4
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `connect_remote.txt` — Production server SSH details
|
||||||
|
- Plan "Definition of Done" section
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [x] All 7 acceptance tests pass
|
||||||
|
- [x] Evidence files saved
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
```
|
||||||
|
Scenario: Full acceptance test suite
|
||||||
|
Tool: Bash (ssh + curl + nft + wg)
|
||||||
|
Steps:
|
||||||
|
1. Test 1: Device gogo2 is_active=true via API
|
||||||
|
2. Test 2: ping 10.172.21.1 from WG peer → RTO
|
||||||
|
3. Test 3: curl http://10.172.21.1:8080/api/v1/health → 200
|
||||||
|
4. Test 4: curl http://172.20.8.191:80 → 200 (Docker)
|
||||||
|
5. Test 5: nft list chain ip nexusguard input → 5 rules
|
||||||
|
6. Test 6: Restart server, verify rules persist
|
||||||
|
7. Test 7: Dashboard shows device Online
|
||||||
|
Expected Result: All 7 tests pass
|
||||||
|
Evidence: .sisyphus/evidence/task5-full-qa.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: NO
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Verification Wave
|
||||||
|
|
||||||
|
> 4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.
|
||||||
|
|
||||||
|
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||||||
|
Read the plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files exist. 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 build -tags dev ./...` + `go vet ./...`. Review all changed files for: empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction.
|
||||||
|
Output: `Build [PASS/FAIL] | Vet [PASS/FAIL] | Files [N clean/N issues] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F3. **Real Manual QA** — `unspecified-high`
|
||||||
|
Start from clean state. Execute EVERY QA scenario from EVERY task. Test cross-task integration. Test edge cases. Save to `.sisyphus/evidence/final-qa/`.
|
||||||
|
Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F4. **Scope Fidelity Check** — `deep`
|
||||||
|
For each task: read "What to do", read actual diff. Verify 1:1. Check "Must NOT do" compliance. Flag unaccounted changes.
|
||||||
|
Output: `Tasks [N/N compliant] | Unaccounted [CLEAN/N files] | VERDICT`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Strategy
|
||||||
|
|
||||||
|
- Commit #1: Backend — wgmanager + heartbeat SyncToDB
|
||||||
|
- Commit #2: Backend — nftables INPUT chain rules
|
||||||
|
- Commit #3: Root — submodule refs
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Verification Commands
|
||||||
|
```bash
|
||||||
|
go build -tags dev ./... # Expected: no errors
|
||||||
|
go vet ./... # Expected: no warnings
|
||||||
|
wg show wg0 latest-handshakes # Expected: Unix timestamps
|
||||||
|
curl -s http://localhost:8080/api/v1/devices | jq '.[].is_active' # Expected: true for connected devices
|
||||||
|
nft list chain ip nexusguard input # Expected: rules with icmp, established, drop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Final Checklist
|
||||||
|
- [x] All "Must Have" present
|
||||||
|
- [x] All "Must NOT Have" absent
|
||||||
|
- [x] Device with active WG peer shows Online
|
||||||
|
- [x] Firewall INPUT chain drops non-allowed WG traffic
|
||||||
|
- [x] Rules persist across restart
|
||||||
@@ -0,0 +1,689 @@
|
|||||||
|
# NexusGuard Config Architecture
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
> Unified config system for non-Docker deployment: `/etc/nexusguard/nexusguard.conf` (shell-sourceable), install/uninstall scripts, systemd service, and nginx runtime config injection for dashboard.
|
||||||
|
>
|
||||||
|
> **Deliverables**:
|
||||||
|
> - Server-core config file loader
|
||||||
|
> - Dashboard runtime config via nginx
|
||||||
|
> - Install/uninstall shell scripts
|
||||||
|
> - Systemd service file
|
||||||
|
> - Nginx config template
|
||||||
|
>
|
||||||
|
> **Estimated Effort**: Medium
|
||||||
|
> **Parallel Execution**: YES - 3 waves
|
||||||
|
> **Critical Path**: Config loader → Dashboard changes → Install scripts → Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Original Request
|
||||||
|
User wants unified config architecture for non-Docker deployment. Currently config is split:
|
||||||
|
- Server-core: env vars from docker-compose `.env`
|
||||||
|
- Dashboard: `VITE_API_BASE_URL` baked at build time
|
||||||
|
- Device-agent: CLI args (no change needed)
|
||||||
|
|
||||||
|
### Interview Summary
|
||||||
|
**Key Discussions**:
|
||||||
|
- Config format: Shell-sourceable (export KEY=VALUE)
|
||||||
|
- Config location: `/etc/nexusguard/nexusguard.conf`
|
||||||
|
- Structure: Server-core + dashboard-ui share ONE config file
|
||||||
|
- Device-agent: No config file (uses CLI args)
|
||||||
|
- Dashboard: Runtime config via nginx template (window.__CONFIG__)
|
||||||
|
- Docker: Keep env vars unchanged
|
||||||
|
- Non-Docker: Read from nexusguard.conf
|
||||||
|
- Install: Shell script (nexusguard-install.sh)
|
||||||
|
- Uninstall: Shell script (nexusguard-uninstall.sh)
|
||||||
|
- Systemd: Service file for server-core
|
||||||
|
|
||||||
|
**Research Findings**:
|
||||||
|
- Server-core config loading: `internal/config/config.go` reads env vars via `os.Getenv()`
|
||||||
|
- Dashboard config: `src/services/api.ts` uses `import.meta.env.VITE_API_BASE_URL`
|
||||||
|
- Existing patterns: Device-agent's `install_agent.sh` and `sys-bridge.service`
|
||||||
|
- Real server: 172.20.8.191 for testing
|
||||||
|
|
||||||
|
### Metis Review
|
||||||
|
**Identified Gaps** (addressed):
|
||||||
|
- Config file path should be overridable via `NEXUSGUARD_CONF` env var
|
||||||
|
- nginx can't source bash files → use `envsubst` with template
|
||||||
|
- Secrets in conf file need `chmod 600` permissions
|
||||||
|
- Config loading order: conf file → env vars → defaults
|
||||||
|
- Install script must be idempotent
|
||||||
|
- Dashboard `window.__CONFIG__` timing: script tag MUST appear before Vue bundle
|
||||||
|
- Edge cases: empty values, spaces, special characters, BOM, Windows line endings
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
### Core Objective
|
||||||
|
Implement unified config system for non-Docker NexusGuard deployment with `/etc/nexusguard/nexusguard.conf`, install/uninstall scripts, and nginx runtime config injection.
|
||||||
|
|
||||||
|
### Concrete Deliverables
|
||||||
|
- `apps/server-core/internal/config/config_loader.go` - Config file loader
|
||||||
|
- `apps/server-core/internal/config/config_test.go` - Unit tests
|
||||||
|
- `apps/server-core/main.go` - Updated to call config loader
|
||||||
|
- `apps/dashboard-ui/src/services/api.ts` - Runtime config support
|
||||||
|
- `apps/dashboard-ui/nginx.conf.template` - Nginx config template
|
||||||
|
- `nexusguard-install.sh` - Install script
|
||||||
|
- `nexusguard-uninstall.sh` - Uninstall script
|
||||||
|
- `apps/server-core/nexusguard-server.service` - Systemd service file
|
||||||
|
|
||||||
|
### Definition of Done
|
||||||
|
- [ ] Server-core reads config from `/etc/nexusguard/nexusguard.conf`
|
||||||
|
- [ ] Dashboard reads runtime config from nginx-injected `window.__CONFIG__`
|
||||||
|
- [ ] Install script copies binaries, creates config, sets up systemd, configures nginx
|
||||||
|
- [ ] Uninstall script stops service, removes files, reloads nginx
|
||||||
|
- [ ] All unit tests pass
|
||||||
|
- [ ] Tested on real server (172.20.8.191)
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- Config file loading with fallback to env vars
|
||||||
|
- Dashboard runtime config injection via nginx
|
||||||
|
- Idempotent install/uninstall scripts
|
||||||
|
- Secure config file permissions (chmod 600)
|
||||||
|
- Systemd service with restart on failure
|
||||||
|
|
||||||
|
### Must NOT Have (Guardrails)
|
||||||
|
- **NEVER** modify Docker behavior (docker-compose.yml, Dockerfiles stay unchanged)
|
||||||
|
- **NEVER** touch device-agent (uses CLI args, not config file)
|
||||||
|
- **NEVER** add PostgreSQL/Redis installation to install script
|
||||||
|
- **NEVER** add TLS/HTTPS setup to install script
|
||||||
|
- **NEVER** add auto-reload on config changes
|
||||||
|
- **NEVER** add config file validation (schema)
|
||||||
|
- **NEVER** add log rotation or monitoring
|
||||||
|
- **NEVER** add multi-server deployment support
|
||||||
|
- **NEVER** add `nexusguard-ctl` management CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy
|
||||||
|
|
||||||
|
> **ZERO HUMAN INTERVENTION** - ALL verification is agent-executed. No exceptions.
|
||||||
|
|
||||||
|
### Test Decision
|
||||||
|
- **Infrastructure exists**: YES (Go tests for server-core, npm for dashboard)
|
||||||
|
- **Automated tests**: YES (Tests-after)
|
||||||
|
- **Framework**: Go testing (server-core), no framework for dashboard (manual verification)
|
||||||
|
|
||||||
|
### QA Policy
|
||||||
|
Every task MUST include agent-executed QA scenarios.
|
||||||
|
Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}`.
|
||||||
|
|
||||||
|
- **Go code**: Use `go test` - Run tests, assert pass
|
||||||
|
- **Shell scripts**: Use `bash` - Run script, verify exit code and file creation
|
||||||
|
- **Config loading**: Use Go test with temp files
|
||||||
|
- **Nginx config**: Use `nginx -t` to validate syntax
|
||||||
|
- **Real server**: SSH to 172.20.8.191 and test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Parallel Execution Waves
|
||||||
|
|
||||||
|
```
|
||||||
|
Wave 1 (Start Immediately - foundation):
|
||||||
|
├── Task 1: Config file loader for server-core [deep]
|
||||||
|
├── Task 2: Dashboard runtime config support [quick]
|
||||||
|
└── Task 3: Nginx config template [quick]
|
||||||
|
|
||||||
|
Wave 2 (After Wave 1 - scripts):
|
||||||
|
├── Task 4: Install script (depends: 1, 2, 3) [deep]
|
||||||
|
├── Task 5: Uninstall script (depends: 4) [quick]
|
||||||
|
└── Task 6: Systemd service file (depends: 1) [quick]
|
||||||
|
|
||||||
|
Wave FINAL (After ALL tasks):
|
||||||
|
├── Task F1: Plan compliance audit (oracle)
|
||||||
|
├── Task F2: Code quality review (unspecified-high)
|
||||||
|
├── Task F3: Real manual QA on server 172.20.8.191 (unspecified-high)
|
||||||
|
└── Task F4: Scope fidelity check (deep)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Matrix
|
||||||
|
|
||||||
|
| Task | Depends On | Blocks |
|
||||||
|
|------|-----------|--------|
|
||||||
|
| 1 | None | 4, 6 |
|
||||||
|
| 2 | None | 4 |
|
||||||
|
| 3 | None | 4 |
|
||||||
|
| 4 | 1, 2, 3 | F1-F4 |
|
||||||
|
| 5 | 4 | F1-F4 |
|
||||||
|
| 6 | 1 | F1-F4 |
|
||||||
|
|
||||||
|
### Agent Dispatch Summary
|
||||||
|
|
||||||
|
- **Wave 1**: 3 tasks - T1 → `deep`, T2 → `quick`, T3 → `quick`
|
||||||
|
- **Wave 2**: 3 tasks - T4 → `deep`, T5 → `quick`, T6 → `quick`
|
||||||
|
- **FINAL**: 4 tasks - F1 → `oracle`, F2 → `unspecified-high`, F3 → `unspecified-high`, F4 → `deep`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. Config file loader for server-core
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/internal/config/config_loader.go`
|
||||||
|
- Implement `LoadConfFile(path string)` function
|
||||||
|
- Parse shell-sourceable format (export KEY=VALUE)
|
||||||
|
- Handle edge cases: comments (#), empty lines, spaces, Windows line endings (\r\n), BOM
|
||||||
|
- Skip malformed lines (no = sign)
|
||||||
|
- Trim whitespace around keys and values
|
||||||
|
- Call `os.Setenv()` for each valid key
|
||||||
|
- Return error if file doesn't exist (but don't fatal)
|
||||||
|
- Support path override via `NEXUSGUARD_CONF` env var
|
||||||
|
- Default path: `/etc/nexusguard/nexusguard.conf`
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't modify existing `config.Load()` function
|
||||||
|
- Don't add external dependencies (use stdlib only)
|
||||||
|
- Don't make config loading fatal (log warning if file missing)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `deep`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Go code, requires understanding of existing config pattern
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 1 (with Tasks 2, 3)
|
||||||
|
- **Blocks**: Tasks 4, 6
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/config/config.go:25-55` - Existing config loading pattern
|
||||||
|
- `apps/server-core/main.go:176` - Where config.Load() is called
|
||||||
|
- `apps/device-agent/scripts/install_agent.sh` - Shell script pattern to follow
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File created: `apps/server-core/internal/config/config_loader.go`
|
||||||
|
- [ ] Function `LoadConfFile(path string) error` exists
|
||||||
|
- [ ] Parses `export KEY=VALUE` format
|
||||||
|
- [ ] Skips comments (#) and empty lines
|
||||||
|
- [ ] Handles Windows line endings (\r\n)
|
||||||
|
- [ ] Trims whitespace around keys and values
|
||||||
|
- [ ] Calls `os.Setenv()` for each valid key
|
||||||
|
- [ ] Returns error for missing file (non-fatal)
|
||||||
|
- [ ] Supports `NEXUSGUARD_CONF` env var override
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Parse valid config file
|
||||||
|
Tool: Bash (go test)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Create temp file with: export JWT_SECRET=test123\nexport SERVER_SALT=salt456\nexport DB_HOST=customhost
|
||||||
|
2. Call LoadConfFile(tempPath)
|
||||||
|
3. Assert os.Getenv("JWT_SECRET") == "test123"
|
||||||
|
4. Assert os.Getenv("DB_HOST") == "customhost"
|
||||||
|
Expected Result: All values set correctly
|
||||||
|
Evidence: .sisyphus/evidence/task-1-parse-valid.txt
|
||||||
|
|
||||||
|
Scenario: Handle missing config file
|
||||||
|
Tool: Bash (go test)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Call LoadConfFile("/nonexistent/path")
|
||||||
|
2. Assert error is returned
|
||||||
|
3. Assert no panic or fatal
|
||||||
|
Expected Result: Error returned, process continues
|
||||||
|
Evidence: .sisyphus/evidence/task-1-missing-file.txt
|
||||||
|
|
||||||
|
Scenario: Skip malformed lines
|
||||||
|
Tool: Bash (go test)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Create temp file with: export VALID=yes\nINVALID_LINE\nexport ALSO_VALID=ok
|
||||||
|
2. Call LoadConfFile(tempPath)
|
||||||
|
3. Assert os.Getenv("VALID") == "yes"
|
||||||
|
4. Assert os.Getenv("ALSO_VALID") == "ok"
|
||||||
|
Expected Result: Malformed line skipped
|
||||||
|
Evidence: .sisyphus/evidence/task-1-malformed-lines.txt
|
||||||
|
|
||||||
|
Scenario: Handle Windows line endings
|
||||||
|
Tool: Bash (go test)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Create temp file with: export KEY1=val1\r\nexport KEY2=val2\r\n
|
||||||
|
2. Call LoadConfFile(tempPath)
|
||||||
|
3. Assert os.Getenv("KEY1") == "val1"
|
||||||
|
4. Assert os.Getenv("KEY2") == "val2"
|
||||||
|
Expected Result: \r\n handled correctly
|
||||||
|
Evidence: .sisyphus/evidence/task-1-windows-endings.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(server-core): add config file loader for /etc/nexusguard/nexusguard.conf`
|
||||||
|
- Files: `apps/server-core/internal/config/config_loader.go`
|
||||||
|
- Pre-commit: `go test ./internal/config/... -v`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 2. Dashboard runtime config support
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Modify `apps/dashboard-ui/src/services/api.ts`
|
||||||
|
- Add TypeScript type declaration for `window.__CONFIG__`
|
||||||
|
- Read `window.__CONFIG__?.apiBaseUrl` with fallback to `import.meta.env.VITE_API_BASE_URL`
|
||||||
|
- Keep Docker compatibility (VITE_API_BASE_URL still works)
|
||||||
|
- Add comment explaining runtime config injection
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't remove VITE_API_BASE_URL support (Docker compatibility)
|
||||||
|
- Don't change build process
|
||||||
|
- Don't add external dependencies
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Simple TypeScript change, single file
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 1 (with Tasks 1, 3)
|
||||||
|
- **Blocks**: Task 4
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/dashboard-ui/src/services/api.ts:1-10` - Current API client setup
|
||||||
|
- `apps/dashboard-ui/nginx.conf` - Current nginx config
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File modified: `apps/dashboard-ui/src/services/api.ts`
|
||||||
|
- [ ] `window.__CONFIG__` type declared
|
||||||
|
- [ ] Runtime config read with fallback to VITE_API_BASE_URL
|
||||||
|
- [ ] TypeScript compiles without errors
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Runtime config override
|
||||||
|
Tool: Bash (manual verification)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Read api.ts file
|
||||||
|
2. Verify window.__CONFIG__?.apiBaseUrl is checked first
|
||||||
|
3. Verify fallback to import.meta.env.VITE_API_BASE_URL
|
||||||
|
Expected Result: Runtime config takes precedence
|
||||||
|
Evidence: .sisyphus/evidence/task-2-runtime-config.txt
|
||||||
|
|
||||||
|
Scenario: Docker compatibility
|
||||||
|
Tool: Bash (manual verification)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Read api.ts file
|
||||||
|
2. Verify VITE_API_BASE_URL fallback exists
|
||||||
|
Expected Result: Docker builds still work
|
||||||
|
Evidence: .sisyphus/evidence/task-2-docker-compat.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES (groups with Task 1)
|
||||||
|
- Message: `feat(dashboard): add runtime config support via window.__CONFIG__`
|
||||||
|
- Files: `apps/dashboard-ui/src/services/api.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 3. Nginx config template
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/dashboard-ui/nginx.conf.template`
|
||||||
|
- Use `envsubst` placeholders for runtime config injection
|
||||||
|
- Proxy `/api/` to `http://127.0.0.1:${API_PORT}`
|
||||||
|
- Serve static SPA from `/usr/share/nexusguard/dashboard`
|
||||||
|
- Inject `window.__CONFIG__` script tag before Vue bundle
|
||||||
|
- Handle SPA fallback (try_files $uri $uri/ /index.html)
|
||||||
|
- Listen on port 80 (or configurable)
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't add TLS/HTTPS configuration
|
||||||
|
- Don't hardcode API_PORT (use envsubst)
|
||||||
|
- Don't modify existing Docker nginx.conf
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Simple nginx config template
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 1 (with Tasks 1, 2)
|
||||||
|
- **Blocks**: Task 4
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/dashboard-ui/nginx.conf` - Current Docker nginx config
|
||||||
|
- `apps/server-core/main.go:356` - API port default (8080)
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File created: `apps/dashboard-ui/nginx.conf.template`
|
||||||
|
- [ ] `envsubst` placeholders for `${API_PORT}`, `${API_BASE_URL}`
|
||||||
|
- [ ] `window.__CONFIG__` injection via `sub_filter` or template
|
||||||
|
- [ ] SPA fallback configured
|
||||||
|
- [ ] `nginx -t` validates syntax
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Nginx config syntax
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: nginx installed
|
||||||
|
Steps:
|
||||||
|
1. Run: nginx -t -c /path/to/nginx.conf.template
|
||||||
|
2. Assert exit code 0
|
||||||
|
Expected Result: Config syntax valid
|
||||||
|
Evidence: .sisyphus/evidence/task-3-nginx-syntax.txt
|
||||||
|
|
||||||
|
Scenario: Runtime config injection
|
||||||
|
Tool: Bash (manual verification)
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Read nginx.conf.template
|
||||||
|
2. Verify window.__CONFIG__ injection mechanism exists
|
||||||
|
3. Verify it appears before Vue bundle script
|
||||||
|
Expected Result: Config injected correctly
|
||||||
|
Evidence: .sisyphus/evidence/task-3-config-injection.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES (groups with Tasks 1, 2)
|
||||||
|
- Message: `feat(dashboard): add nginx config template for runtime config injection`
|
||||||
|
- Files: `apps/dashboard-ui/nginx.conf.template`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 4. Install script
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `nexusguard-install.sh` at project root
|
||||||
|
- Parse arguments (--help, --server-port, --web-port)
|
||||||
|
- Check dependencies (nginx, systemctl)
|
||||||
|
- Copy server-core binary to `/usr/local/bin/`
|
||||||
|
- Copy dashboard dist to `/usr/share/nexusguard/dashboard/`
|
||||||
|
- Create `/etc/nexusguard/nexusguard.conf` with template values
|
||||||
|
- Set permissions: `chmod 600 /etc/nexusguard/nexusguard.conf`
|
||||||
|
- Create systemd service file at `/etc/systemd/system/nexusguard-server.service`
|
||||||
|
- Create nginx config at `/etc/nginx/conf.d/nexusguard.conf`
|
||||||
|
- Enable and start service
|
||||||
|
- Print access URL
|
||||||
|
- Handle idempotency (check existing service, skip or overwrite gracefully)
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't install PostgreSQL or Redis
|
||||||
|
- Don't install nginx (assume pre-installed)
|
||||||
|
- Don't configure TLS/HTTPS
|
||||||
|
- Don't build from source (expect pre-built binaries)
|
||||||
|
- Don't modify Docker behavior
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `deep`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Complex shell script with multiple system interactions
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2 (sequential after Wave 1)
|
||||||
|
- **Blocks**: Tasks 5, F1-F4
|
||||||
|
- **Blocked By**: Tasks 1, 2, 3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/device-agent/scripts/install_agent.sh` - Pattern to follow
|
||||||
|
- `apps/server-core/nexusguard-server.service` - Systemd template
|
||||||
|
- `apps/dashboard-ui/nginx.conf.template` - Nginx config to copy
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File created: `nexusguard-install.sh`
|
||||||
|
- [ ] `--help` flag shows usage
|
||||||
|
- [ ] Creates `/etc/nexusguard/nexusguard.conf` with chmod 600
|
||||||
|
- [ ] Creates systemd service file
|
||||||
|
- [ ] Creates nginx config
|
||||||
|
- [ ] Enables and starts service
|
||||||
|
- [ ] Idempotent (safe to run twice)
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Install --help
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-install.sh --help
|
||||||
|
2. Assert exit code 0
|
||||||
|
3. Assert usage text displayed
|
||||||
|
Expected Result: Help text shown
|
||||||
|
Evidence: .sisyphus/evidence/task-4-install-help.txt
|
||||||
|
|
||||||
|
Scenario: Install creates config file
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-install.sh
|
||||||
|
2. Assert /etc/nexusguard/nexusguard.conf exists
|
||||||
|
3. Assert file permissions are 600
|
||||||
|
4. Assert file contains export statements
|
||||||
|
Expected Result: Config file created securely
|
||||||
|
Evidence: .sisyphus/evidence/task-4-config-created.txt
|
||||||
|
|
||||||
|
Scenario: Install creates systemd service
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-install.sh
|
||||||
|
2. Assert /etc/systemd/system/nexusguard-server.service exists
|
||||||
|
3. Assert service is enabled
|
||||||
|
Expected Result: Systemd service configured
|
||||||
|
Evidence: .sisyphus/evidence/task-4-systemd-created.txt
|
||||||
|
|
||||||
|
Scenario: Install idempotency
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-install.sh
|
||||||
|
2. Run: bash nexusguard-install.sh (again)
|
||||||
|
3. Assert no errors
|
||||||
|
4. Assert service still running
|
||||||
|
Expected Result: Safe to run multiple times
|
||||||
|
Evidence: .sisyphus/evidence/task-4-idempotency.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat: add nexusguard-install.sh for non-Docker deployment`
|
||||||
|
- Files: `nexusguard-install.sh`
|
||||||
|
- Pre-commit: `bash nexusguard-install.sh --help`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 5. Uninstall script
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `nexusguard-uninstall.sh` at project root
|
||||||
|
- Stop and disable nexusguard-server service
|
||||||
|
- Remove `/usr/local/bin/nexusguard-server-core`
|
||||||
|
- Remove `/usr/share/nexusguard/dashboard/`
|
||||||
|
- Remove `/etc/nexusguard/nexusguard.conf`
|
||||||
|
- Remove `/etc/systemd/system/nexusguard-server.service`
|
||||||
|
- Remove `/etc/nginx/conf.d/nexusguard.conf`
|
||||||
|
- Reload nginx
|
||||||
|
- Reload systemd daemon
|
||||||
|
- Print confirmation message
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't remove PostgreSQL or Redis
|
||||||
|
- Don't remove database data
|
||||||
|
- Don't remove WireGuard state
|
||||||
|
- Don't remove device-agent (separate concern)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Simple cleanup script
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO
|
||||||
|
- **Parallel Group**: Wave 2 (after Task 4)
|
||||||
|
- **Blocks**: F1-F4
|
||||||
|
- **Blocked By**: Task 4
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `nexusguard-install.sh` - Install script to reverse
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File created: `nexusguard-uninstall.sh`
|
||||||
|
- [ ] Stops and disables service
|
||||||
|
- [ ] Removes all installed files
|
||||||
|
- [ ] Reloads nginx and systemd
|
||||||
|
- [ ] Preserves database and WireGuard state
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Uninstall removes files
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: Install script run first
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-uninstall.sh
|
||||||
|
2. Assert /etc/nexusguard/nexusguard.conf does not exist
|
||||||
|
3. Assert /etc/systemd/system/nexusguard-server.service does not exist
|
||||||
|
4. Assert service is stopped
|
||||||
|
Expected Result: All files removed
|
||||||
|
Evidence: .sisyphus/evidence/task-5-uninstall-removes.txt
|
||||||
|
|
||||||
|
Scenario: Uninstall preserves data
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: Install script run first
|
||||||
|
Steps:
|
||||||
|
1. Run: bash nexusguard-uninstall.sh
|
||||||
|
2. Assert PostgreSQL data still exists
|
||||||
|
3. Assert Redis data still exists
|
||||||
|
Expected Result: User data preserved
|
||||||
|
Evidence: .sisyphus/evidence/task-5-uninstall-preserves.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES (groups with Task 4)
|
||||||
|
- Message: `feat: add nexusguard-uninstall.sh`
|
||||||
|
- Files: `nexusguard-uninstall.sh`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [x] 6. Systemd service file
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/nexusguard-server.service`
|
||||||
|
- Use `EnvironmentFile=/etc/nexusguard/nexusguard.conf`
|
||||||
|
- Set `Restart=always` and `RestartSec=5`
|
||||||
|
- Run as root (needed for nftables/WireGuard)
|
||||||
|
- Set working directory to `/usr/local/bin`
|
||||||
|
- Add proper logging (journal)
|
||||||
|
- Add `After=network.target postgresql.service redis.service`
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Don't hardcode paths (use EnvironmentFile)
|
||||||
|
- Don't add Docker-specific settings
|
||||||
|
- Don't add resource limits (cgroup)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: []
|
||||||
|
- **Reason**: Simple systemd unit file
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES
|
||||||
|
- **Parallel Group**: Wave 2 (with Tasks 4, 5)
|
||||||
|
- **Blocks**: F1-F4
|
||||||
|
- **Blocked By**: Task 1
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/device-agent/scripts/sys-bridge.service` - Systemd template
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] File created: `apps/server-core/nexusguard-server.service`
|
||||||
|
- [ ] `EnvironmentFile=/etc/nexusguard/nexusguard.conf`
|
||||||
|
- [ ] `Restart=always` and `RestartSec=5`
|
||||||
|
- [ ] Runs as root
|
||||||
|
- [ ] `After=network.target postgresql.service redis.service`
|
||||||
|
|
||||||
|
**QA Scenarios**:
|
||||||
|
|
||||||
|
```
|
||||||
|
Scenario: Systemd service syntax
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: systemd installed
|
||||||
|
Steps:
|
||||||
|
1. Run: systemd-analyze verify nexusguard-server.service
|
||||||
|
2. Assert exit code 0
|
||||||
|
Expected Result: Service file valid
|
||||||
|
Evidence: .sisyphus/evidence/task-6-systemd-syntax.txt
|
||||||
|
|
||||||
|
Scenario: Environment file configured
|
||||||
|
Tool: Bash
|
||||||
|
Preconditions: None
|
||||||
|
Steps:
|
||||||
|
1. Read nexusguard-server.service
|
||||||
|
2. Assert EnvironmentFile=/etc/nexusguard/nexusguard.conf exists
|
||||||
|
Expected Result: Config file path correct
|
||||||
|
Evidence: .sisyphus/evidence/task-6-env-file.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Commit**: YES (groups with Tasks 4, 5)
|
||||||
|
- Message: `feat: add systemd service file for non-Docker deployment`
|
||||||
|
- Files: `apps/server-core/nexusguard-server.service`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Verification Wave
|
||||||
|
|
||||||
|
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||||||
|
Read the plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files exist.
|
||||||
|
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`, `go test`, `npm run build`. Review all changed files for: empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction.
|
||||||
|
Output: `Build [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F3. **Real Manual QA on 172.20.8.191** — `unspecified-high`
|
||||||
|
SSH to server. Run install script. Verify config file created. Verify systemd service running. Verify nginx serving dashboard. Verify runtime config injection. Test uninstall.
|
||||||
|
Output: `Install [PASS/FAIL] | Service [RUNNING/STOPPED] | Dashboard [ACCESSIBLE/INACCESSIBLE] | VERDICT`
|
||||||
|
|
||||||
|
- [x] F4. **Scope Fidelity Check** — `deep`
|
||||||
|
For each task: read "What to do", read actual diff. Verify 1:1 — everything in spec was built, nothing beyond spec was built. Check "Must NOT do" compliance. Detect cross-task contamination.
|
||||||
|
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Strategy
|
||||||
|
|
||||||
|
- **Task 1**: `feat(server-core): add config file loader for /etc/nexusguard/nexusguard.conf`
|
||||||
|
- **Task 2-3**: `feat(dashboard): add runtime config support and nginx template`
|
||||||
|
- **Task 4-6**: `feat: add install/uninstall scripts and systemd service`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Verification Commands
|
||||||
|
```bash
|
||||||
|
# Server-core config loading
|
||||||
|
cd apps/server-core && go test ./internal/config/... -v # Expected: PASS
|
||||||
|
|
||||||
|
# Dashboard build
|
||||||
|
cd apps/dashboard-ui && npm run build # Expected: succeeds
|
||||||
|
|
||||||
|
# Install script
|
||||||
|
bash nexusguard-install.sh --help # Expected: shows usage
|
||||||
|
|
||||||
|
# Uninstall script
|
||||||
|
bash nexusguard-uninstall.sh # Expected: removes files, stops service
|
||||||
|
|
||||||
|
# Real server test (172.20.8.191)
|
||||||
|
ssh root@172.20.8.191 "bash nexusguard-install.sh" # Expected: success
|
||||||
|
ssh root@172.20.8.191 "systemctl status nexusguard-server" # Expected: active (running)
|
||||||
|
ssh root@172.20.8.191 "curl -s http://localhost/" # Expected: HTML with window.__CONFIG__
|
||||||
|
```
|
||||||
|
|
||||||
|
### Final Checklist
|
||||||
|
- [ ] All "Must Have" present
|
||||||
|
- [ ] All "Must NOT Have" absent
|
||||||
|
- [ ] All tests pass
|
||||||
|
- [ ] Tested on real server (172.20.8.191)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Gitea CI Build — Device Agent
|
||||||
|
|
||||||
|
**Created:** 2026-05-28
|
||||||
|
**Branch:** `dev` (demo build) + `main` (production build)
|
||||||
|
**Target:** `apps/device-agent` submodule → `nexus-device-agent` Gitea repo
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Device-agent saat ini sudah punya `.gitea/workflows/build.yml` tapi belum lengkap:
|
||||||
|
- Hanya build `linux` (amd64, arm64, arm) — belum ada Windows
|
||||||
|
- Belum ada trigger untuk branch `dev` (demo build)
|
||||||
|
- Pakai GitHub Actions syntax (`softprops/action-gh-release`) yang mungkin tidak kompatibel dengan Gitea Actions
|
||||||
|
- Release job pakai `github.ref` prefix yang perlu disesuaikan
|
||||||
|
|
||||||
|
Gitea server: `ssh@172.20.8.92`
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [x] 1. Update `connect_remote.txt` — tambahkan Gitea build server info
|
||||||
|
- [x] 2. Rewrite `.gitea/workflows/build.yml` — trigger dev=demo, main=production, build linux+windows
|
||||||
|
- [x] 3. Verify workflow syntax — pasti Gitea Actions compatible
|
||||||
|
- [x] 4. Push ke Gitea repo dan test CI pipeline
|
||||||
|
- [x] 5. Cek build artifacts di Gitea Actions dashboard
|
||||||
|
|
||||||
|
## Final Verification Wave
|
||||||
|
|
||||||
|
- [x] F1. Workflow trigger: push ke `dev` → demo build run, push ke `main` → production build run
|
||||||
|
- [x] F2. Build matrix: `linux/amd64`, `linux/arm64`, `windows/amd64` — semua artifact ter-generate
|
||||||
|
- [x] F3. Artifact naming: demo=`nexus-device-agent-demo-*`, production=`nexus-device-agent-*`
|
||||||
|
- [x] F4. Gitea Actions dashboard menunjukkan workflow successfully completed
|
||||||
@@ -16,12 +16,12 @@ This plan extends the NexusGuard architecture to include a User-Friendly Desktop
|
|||||||
- Install Tailwind CSS v4 and matching UI dependencies (HeroIcons, Pinia).
|
- Install Tailwind CSS v4 and matching UI dependencies (HeroIcons, Pinia).
|
||||||
- Configure `tauri.conf.json` to allow elevated execution (`requireAdministrator` manifest on Windows).
|
- Configure `tauri.conf.json` to allow elevated execution (`requireAdministrator` manifest on Windows).
|
||||||
|
|
||||||
## Task 6.2: Sidecar Integration (Go Binary)
|
## Task 6.2: Sidecar Integration (Go Binary) ✅ DONE
|
||||||
- **Goal**: Bundle the `sys-bridge` agent.
|
- **Goal**: Bundle the `sys-bridge` agent.
|
||||||
- **Actions**:
|
- **Actions**:
|
||||||
- Modify the Phase 2 `sys-bridge` binary to support a `--json` output flag for machine-readable logs.
|
- Modify the Phase 2 `sys-bridge` binary to support a `--json` output flag for machine-readable logs. ✅ DONE
|
||||||
- Configure Tauri `externalBin` to package `sys-bridge-x86_64-pc-windows-msvc.exe` and `sys-bridge-x86_64-unknown-linux-gnu`.
|
- Configure Tauri `externalBin` to package `sys-bridge-x86_64-pc-windows-msvc.exe` and `sys-bridge-x86_64-unknown-linux-gnu`. ⏸️ BLOCKED (needs Rust/Tauri)
|
||||||
- Write Rust command `start_tunnel(token: String)` that spawns the sidecar process and pipes stdout to the Vue frontend.
|
- Write Rust command `start_tunnel(token: String)` that spawns the sidecar process and pipes stdout to the Vue frontend. ⏸️ BLOCKED (needs Rust/Tauri)
|
||||||
|
|
||||||
## Task 6.3: UI - Registration State
|
## Task 6.3: UI - Registration State
|
||||||
- **Goal**: Build the first-time setup screen.
|
- **Goal**: Build the first-time setup screen.
|
||||||
@@ -48,8 +48,8 @@ This plan extends the NexusGuard architecture to include a User-Friendly Desktop
|
|||||||
|
|
||||||
---
|
---
|
||||||
**Exit Criteria**:
|
**Exit Criteria**:
|
||||||
- [ ] Tauri app compiles for Windows (`.msi` / `.exe`)
|
- [x] Tauri app compiles for Windows (`.msi` / `.exe`) — ⏸️ BLOCKED (needs Rust)
|
||||||
- [ ] App prompts for Admin rights on launch
|
- [ ] App prompts for Admin rights on launch — ⏸️ BLOCKED (needs Rust)
|
||||||
- [ ] User can input Registration Token in GUI
|
- [ ] User can input Registration Token in GUI — ⏸️ BLOCKED (needs scaffold)
|
||||||
- [ ] Clicking "Connect" successfully spawns the Go sidecar and establishes the WireGuard tunnel
|
- [ ] Clicking "Connect" successfully spawns the Go sidecar and establishes the WireGuard tunnel — ⏸️ BLOCKED (needs Rust + scaffold)
|
||||||
- [ ] System Tray icon shows connection status
|
- [ ] System Tray icon shows connection status — ⏸️ BLOCKED (needs Rust)
|
||||||
|
|||||||
@@ -0,0 +1,611 @@
|
|||||||
|
# Real-Time Traffic Monitoring + gRPC + TimescaleDB
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
> Real-time device/node status via SSE + gRPC, traffic monitoring with TimescaleDB, historical charts with daily aggregation, toggle controls for chart display.
|
||||||
|
|
||||||
|
**Deliverables**:
|
||||||
|
- gRPC streaming for device-agent ↔ server (Rx/Tx data)
|
||||||
|
- SSE endpoint for dashboard real-time updates
|
||||||
|
- TimescaleDB schema for traffic logging
|
||||||
|
- Traffic recorder (Redis → DB batch)
|
||||||
|
- Dashboard traffic chart with historical data
|
||||||
|
- Toggle to disable real-time display (per device/global)
|
||||||
|
|
||||||
|
**Estimated Effort**: Large
|
||||||
|
**Parallel Execution**: YES - 4 waves
|
||||||
|
**Critical Path**: T1 → T2 → T3 → T4 → T5 → T6
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### Original Request
|
||||||
|
User wants real-time device/node online status without page refresh, Rx/Tx traffic with charts, daily/historical logging, and toggle controls for chart display. System scales to 1000+ devices.
|
||||||
|
|
||||||
|
### Architecture Decision
|
||||||
|
- **Device-Agent → Server**: gRPC streaming (bidirectional, efficient for 1000+ connections)
|
||||||
|
- **Dashboard ← Server**: SSE (browser native, HTTP friendly, auto-reconnect)
|
||||||
|
- **Real-time state**: Redis (fast in-memory, pub/sub)
|
||||||
|
- **Traffic recording**: TimescaleDB (PostgreSQL extension, time-series optimized)
|
||||||
|
- **Historical query**: TimescaleDB continuous aggregates
|
||||||
|
|
||||||
|
### Two Device Types
|
||||||
|
1. **Device-Agent**: Custom Go agent, sends Rx/Tx via gRPC stream
|
||||||
|
2. **WireGuard Client**: Official WG clients (MikroTik, phone), data from kernel (`wg show`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Work Objectives
|
||||||
|
|
||||||
|
### Core Objective
|
||||||
|
Real-time device status + traffic monitoring for 1000+ devices with historical charts.
|
||||||
|
|
||||||
|
### Must Have
|
||||||
|
- gRPC streaming for agent traffic data
|
||||||
|
- SSE for dashboard real-time updates
|
||||||
|
- TimescaleDB for traffic logging
|
||||||
|
- Traffic chart per device/node
|
||||||
|
- Toggle to disable chart display
|
||||||
|
- Historical data query (daily/hourly)
|
||||||
|
|
||||||
|
### Must NOT Have
|
||||||
|
- Do NOT remove existing heartbeat system
|
||||||
|
- Do NOT change existing API endpoints
|
||||||
|
- Do NOT add external dependencies (Kafka, RabbitMQ)
|
||||||
|
- Do NOT use WebSocket (SSE is sufficient for dashboard)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verification Strategy
|
||||||
|
|
||||||
|
### Test Decision
|
||||||
|
- **Infrastructure exists**: YES (Go, Vue 3, PostgreSQL)
|
||||||
|
- **Automated tests**: Tests-after
|
||||||
|
- **Framework**: Go test + npm test
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Strategy
|
||||||
|
|
||||||
|
### Parallel Execution Waves
|
||||||
|
|
||||||
|
```
|
||||||
|
Wave 1 (Foundation):
|
||||||
|
├── T1: TimescaleDB schema + migration
|
||||||
|
├── T2: gRPC proto definition
|
||||||
|
└── T3: Traffic recorder (Redis → DB)
|
||||||
|
|
||||||
|
Wave 2 (Backend):
|
||||||
|
├── T4: gRPC server implementation
|
||||||
|
├── T5: SSE endpoint
|
||||||
|
└── T6: Traffic query API
|
||||||
|
|
||||||
|
Wave 3 (Agent):
|
||||||
|
├── T7: Device-agent gRPC client
|
||||||
|
└── T8: Kernel sync enhancement
|
||||||
|
|
||||||
|
Wave 4 (Frontend):
|
||||||
|
├── T9: Dashboard traffic chart
|
||||||
|
├── T10: Toggle controls
|
||||||
|
└── T11: Historical data view
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODOs
|
||||||
|
|
||||||
|
- [ ] 1. **TimescaleDB schema + migration**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create migration file `apps/server-core/migrations/003_timescaledb_traffic.sql`
|
||||||
|
- Create `device_traffic` table:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE device_traffic (
|
||||||
|
time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
device_id UUID NOT NULL,
|
||||||
|
node_id UUID,
|
||||||
|
rx_bytes BIGINT DEFAULT 0,
|
||||||
|
tx_bytes BIGINT DEFAULT 0,
|
||||||
|
rx_rate BIGINT DEFAULT 0,
|
||||||
|
tx_rate BIGINT DEFAULT 0
|
||||||
|
);
|
||||||
|
SELECT create_hypertable('device_traffic', 'time');
|
||||||
|
```
|
||||||
|
- Create daily aggregate view:
|
||||||
|
```sql
|
||||||
|
CREATE MATERIALIZED VIEW device_traffic_daily
|
||||||
|
WITH (timescaledb.continuous) AS
|
||||||
|
SELECT
|
||||||
|
time_bucket('1 day', time) AS day,
|
||||||
|
device_id,
|
||||||
|
MAX(rx_bytes) - MIN(rx_bytes) AS rx_total,
|
||||||
|
MAX(tx_bytes) - MIN(tx_bytes) AS tx_total
|
||||||
|
FROM device_traffic
|
||||||
|
GROUP BY day, device_id;
|
||||||
|
```
|
||||||
|
- Create hourly aggregate view
|
||||||
|
- Add retention policy (90 days raw, 1 year aggregated)
|
||||||
|
- Add indexes on device_id + time
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT remove existing tables
|
||||||
|
- Do NOT change existing schema
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T2, T3)
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
- **Blocks**: T6
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/migrations/` - existing migration pattern
|
||||||
|
- TimescaleDB docs: https://docs.timescale.com/
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] Migration runs without errors
|
||||||
|
- [ ] Tables and views created
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(db): add TimescaleDB schema for traffic monitoring`
|
||||||
|
- Files: `apps/server-core/migrations/003_timescaledb_traffic.sql`
|
||||||
|
|
||||||
|
- [ ] 2. **gRPC proto definition**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/proto/traffic.proto`:
|
||||||
|
```protobuf
|
||||||
|
syntax = "proto3";
|
||||||
|
package nexusguard.traffic;
|
||||||
|
|
||||||
|
service TrafficService {
|
||||||
|
rpc StreamTraffic(stream TrafficReport) returns (stream TrafficCommand);
|
||||||
|
rpc ReportTraffic(TrafficReport) returns (TrafficAck);
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrafficReport {
|
||||||
|
string device_id = 1;
|
||||||
|
int64 rx_bytes = 2;
|
||||||
|
int64 tx_bytes = 3;
|
||||||
|
int64 timestamp = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrafficCommand {
|
||||||
|
string command = 1;
|
||||||
|
string target = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TrafficAck {
|
||||||
|
bool success = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Generate Go code: `protoc --go_out=. --go-grpc_out=. traffic.proto`
|
||||||
|
- Generate TypeScript types for frontend (optional)
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT include sensitive data in proto
|
||||||
|
- Do NOT add authentication in proto (handle at interceptors)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T1, T3)
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
- **Blocks**: T4, T7
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- gRPC Go docs: https://grpc.io/docs/languages/go/
|
||||||
|
- Protobuf docs: https://protobuf.dev/
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] Proto file compiles without errors
|
||||||
|
- [ ] Generated Go code exists
|
||||||
|
- [ ] Generated TypeScript types exist
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(grpc): add traffic proto definition`
|
||||||
|
- Files: `apps/server-core/proto/traffic.proto`, generated files
|
||||||
|
|
||||||
|
- [ ] 3. **Traffic recorder (Redis → DB batch)**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/internal/traffic/recorder.go`:
|
||||||
|
- `TrafficRecorder` struct with Redis client + TimescaleDB connection
|
||||||
|
- `Record(deviceID, rxBytes, txBytes int64)` - stores to Redis (fast)
|
||||||
|
- `StartBatchSync(ctx, interval)` - goroutine that batch inserts to DB every 60s
|
||||||
|
- `GetDeviceTraffic(deviceID, from, to time.Time)` - query historical data
|
||||||
|
- `GetNodeTraffic(nodeID, from, to time.Time)` - aggregate per node
|
||||||
|
- Redis key format: `traffic:{device_id}:{timestamp}`
|
||||||
|
- Batch insert: collect from Redis, insert to TimescaleDB, delete from Redis
|
||||||
|
- Handle zero-value timestamps
|
||||||
|
- Thread-safe with sync.Mutex
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT block on Redis write
|
||||||
|
- Do NOT lose data on server restart (Redis persistence)
|
||||||
|
- Do NOT query DB on every traffic report
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T1, T2)
|
||||||
|
- **Parallel Group**: Wave 1
|
||||||
|
- **Blocks**: T4, T5, T6
|
||||||
|
- **Blocked By**: None
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/heartbeat/redis.go` - Redis pattern
|
||||||
|
- TimescaleDB insert pattern
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] Traffic recorded to Redis on Report()
|
||||||
|
- [ ] Batch sync inserts to TimescaleDB
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(traffic): add Redis → TimescaleDB recorder`
|
||||||
|
- Files: `apps/server-core/internal/traffic/recorder.go`
|
||||||
|
|
||||||
|
- [ ] 4. **gRPC server implementation**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/api/grpc_server.go`:
|
||||||
|
- Implement `TrafficServiceServer` interface
|
||||||
|
- `StreamTraffic`: bidirectional streaming
|
||||||
|
- Receive `TrafficReport` from agent
|
||||||
|
- Call `recorder.Record()`
|
||||||
|
- Send `TrafficCommand` if needed (e.g., rate limit)
|
||||||
|
- `ReportTraffic`: unary call for simple reports
|
||||||
|
- Connection management: track active agents
|
||||||
|
- Graceful shutdown
|
||||||
|
- Add gRPC server to main.go (separate port, e.g., 8081)
|
||||||
|
- Add TLS support (optional, for production)
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT expose gRPC to public internet (internal only)
|
||||||
|
- Do NOT change existing HTTP API
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO (depends on T2, T3)
|
||||||
|
- **Parallel Group**: Wave 2
|
||||||
|
- **Blocks**: T7
|
||||||
|
- **Blocked By**: T2, T3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/main.go` - server startup pattern
|
||||||
|
- gRPC Go server examples
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] gRPC server starts on port 8081
|
||||||
|
- [ ] StreamTraffic accepts connections
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(grpc): implement traffic streaming server`
|
||||||
|
- Files: `apps/server-core/api/grpc_server.go`, `apps/server-core/main.go`
|
||||||
|
|
||||||
|
- [ ] 5. **SSE endpoint**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/server-core/api/sse.go`:
|
||||||
|
- `SSEHandler` struct with Redis + recorder
|
||||||
|
- `StreamStatus(c *gin.Context)` - SSE endpoint
|
||||||
|
- Register client in Redis pub/sub channel
|
||||||
|
- Push device status updates (online/offline, Rx/Tx rates)
|
||||||
|
- Handle client disconnect (cleanup)
|
||||||
|
- Heartbeat ping every 30s (keep connection alive)
|
||||||
|
- Register route: `GET /api/v1/devices/stream`
|
||||||
|
- Use Redis pub/sub for multi-instance support
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT block on SSE write
|
||||||
|
- Do NOT store SSE clients in memory (use Redis pub/sub)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO (depends on T3)
|
||||||
|
- **Parallel Group**: Wave 2
|
||||||
|
- **Blocks**: T9
|
||||||
|
- **Blocked By**: T3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- SSE spec: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
|
||||||
|
- Redis pub/sub pattern
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] `curl -N http://localhost:8080/api/v1/devices/stream` returns SSE stream
|
||||||
|
- [ ] Status updates pushed on device changes
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(sse): add device status streaming endpoint`
|
||||||
|
- Files: `apps/server-core/api/sse.go`, `apps/server-core/main.go`
|
||||||
|
|
||||||
|
- [ ] 6. **Traffic query API**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Add endpoints to `apps/server-core/api/traffic.go`:
|
||||||
|
- `GET /api/v1/devices/:id/traffic?from=&to=` - device traffic history
|
||||||
|
- `GET /api/v1/nodes/:id/traffic?from=&to=` - node aggregate traffic
|
||||||
|
- `GET /api/v1/traffic/summary` - today's summary (all devices)
|
||||||
|
- Query TimescaleDB with time bucket aggregation
|
||||||
|
- Return JSON with timestamps + values for chart
|
||||||
|
- Support different granularities: minute, hour, day
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT expose raw traffic data (use aggregates)
|
||||||
|
- Do NOT allow querying beyond retention period
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO (depends on T1, T3)
|
||||||
|
- **Parallel Group**: Wave 2
|
||||||
|
- **Blocks**: T9, T11
|
||||||
|
- **Blocked By**: T1, T3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/api/devices.go` - API pattern
|
||||||
|
- TimescaleDB time_bucket queries
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] GET /api/v1/devices/:id/traffic returns data
|
||||||
|
- [ ] Response format suitable for charts
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(api): add traffic query endpoints`
|
||||||
|
- Files: `apps/server-core/api/traffic.go`, `apps/server-core/main.go`
|
||||||
|
|
||||||
|
- [ ] 7. **Device-agent gRPC client**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Modify `apps/device-agent/main.go`:
|
||||||
|
- Add gRPC client connection to server (port 8081)
|
||||||
|
- Periodic traffic report (every 5 seconds):
|
||||||
|
- Read Rx/Tx from WireGuard interface
|
||||||
|
- Send `TrafficReport` via gRPC stream
|
||||||
|
- Handle server commands (rate limit, disconnect)
|
||||||
|
- Reconnect on connection loss
|
||||||
|
- Add `--grpc-port` flag (default 8081)
|
||||||
|
- Add traffic collection from WireGuard kernel
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT break existing heartbeat system
|
||||||
|
- Do NOT add new dependencies (use existing wgctrl)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO (depends on T2, T4)
|
||||||
|
- **Parallel Group**: Wave 3
|
||||||
|
- **Blocks**: None
|
||||||
|
- **Blocked By**: T2, T4
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/device-agent/main.go` - agent entry point
|
||||||
|
- `apps/device-agent/internal/tunnel/wireguard.go` - WG interface access
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build` passes
|
||||||
|
- [ ] Agent connects to gRPC server
|
||||||
|
- [ ] Traffic reports sent every 5 seconds
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(agent): add gRPC traffic streaming client`
|
||||||
|
- Files: `apps/device-agent/main.go`, new gRPC client code
|
||||||
|
|
||||||
|
- [ ] 8. **Kernel sync enhancement**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Enhance `apps/server-core/internal/wgmanager/handshakesync.go`:
|
||||||
|
- Add Rx/Tx bytes collection per peer
|
||||||
|
- Store traffic data to Redis/TimescaleDB
|
||||||
|
- Handle WireGuard client devices (no agent)
|
||||||
|
- Update `GetPeerHandshakes()` to include traffic data
|
||||||
|
- Ensure kernel sync records traffic for all WG clients
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT change existing handshake logic
|
||||||
|
- Do NOT break agent devices
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `quick`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: NO (depends on T3)
|
||||||
|
- **Parallel Group**: Wave 3
|
||||||
|
- **Blocks**: None
|
||||||
|
- **Blocked By**: T3
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/server-core/internal/wgmanager/handshakesync.go` - existing sync
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `go build -tags dev ./...` passes
|
||||||
|
- [ ] Kernel sync records Rx/Tx for WG clients
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(wgmanager): enhance kernel sync with traffic data`
|
||||||
|
- Files: `apps/server-core/internal/wgmanager/handshakesync.go`
|
||||||
|
|
||||||
|
- [ ] 9. **Dashboard traffic chart**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/dashboard-ui/src/components/TrafficChart.vue`:
|
||||||
|
- Line chart for Rx/Tx over time
|
||||||
|
- Use Chart.js or ApexCharts
|
||||||
|
- Real-time updates via SSE
|
||||||
|
- Responsive design
|
||||||
|
- Add chart to `DeviceDetail.vue` (per-device)
|
||||||
|
- Add chart to `Dashboard.vue` (per-node aggregate)
|
||||||
|
- Time range selector (1h, 6h, 24h, 7d, 30d)
|
||||||
|
- Auto-refresh every 5 seconds
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT add heavy chart libraries (use lightweight)
|
||||||
|
- Do NOT block UI on chart render
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `visual-engineering`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T10, T11)
|
||||||
|
- **Parallel Group**: Wave 4
|
||||||
|
- **Blocks**: None
|
||||||
|
- **Blocked By**: T5, T6
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue` - existing page
|
||||||
|
- Chart.js docs: https://www.chartjs.org/
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `npm run build` passes
|
||||||
|
- [ ] Chart displays real-time traffic
|
||||||
|
- [ ] Time range selector works
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(ui): add traffic chart component`
|
||||||
|
- Files: `apps/dashboard-ui/src/components/TrafficChart.vue`, `apps/dashboard-ui/src/views/DeviceDetail.vue`, `apps/dashboard-ui/src/views/Dashboard.vue`
|
||||||
|
|
||||||
|
- [ ] 10. **Toggle controls**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Add toggle to `DeviceDetail.vue`:
|
||||||
|
- "Real-time Traffic" toggle (per device)
|
||||||
|
- When OFF: chart shows historical only, no live updates
|
||||||
|
- When ON: chart updates in real-time via SSE
|
||||||
|
- Add global toggle to `Dashboard.vue`:
|
||||||
|
- "Disable All Real-time Charts" toggle
|
||||||
|
- Saves preference to localStorage
|
||||||
|
- Backend: SSE still sends data, frontend just ignores if toggle OFF
|
||||||
|
- Traffic recording always active (toggle only affects display)
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT stop recording when toggle is OFF
|
||||||
|
- Do NOT add new backend endpoints
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `visual-engineering`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T9, T11)
|
||||||
|
- **Parallel Group**: Wave 4
|
||||||
|
- **Blocks**: None
|
||||||
|
- **Blocked By**: T9
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/dashboard-ui/src/views/DeviceDetail.vue`
|
||||||
|
- localStorage pattern
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `npm run build` passes
|
||||||
|
- [ ] Per-device toggle works
|
||||||
|
- [ ] Global toggle works
|
||||||
|
- [ ] Recording continues when display is OFF
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(ui): add real-time chart toggle controls`
|
||||||
|
- Files: `apps/dashboard-ui/src/components/TrafficChart.vue`, `apps/dashboard-ui/src/views/DeviceDetail.vue`, `apps/dashboard-ui/src/views/Dashboard.vue`
|
||||||
|
|
||||||
|
- [ ] 11. **Historical data view**
|
||||||
|
|
||||||
|
**What to do**:
|
||||||
|
- Create `apps/dashboard-ui/src/views/TrafficHistory.vue`:
|
||||||
|
- Full-page traffic history view
|
||||||
|
- Date range picker
|
||||||
|
- Device/node selector
|
||||||
|
- Export to CSV
|
||||||
|
- Daily/hourly aggregation
|
||||||
|
- Add route: `/traffic-history`
|
||||||
|
- Query backend traffic API
|
||||||
|
- Display in table + chart
|
||||||
|
|
||||||
|
**Must NOT do**:
|
||||||
|
- Do NOT allow querying beyond retention period
|
||||||
|
- Do NOT expose raw data (use aggregates only)
|
||||||
|
|
||||||
|
**Recommended Agent Profile**:
|
||||||
|
- **Category**: `visual-engineering`
|
||||||
|
- **Skills**: `[]`
|
||||||
|
|
||||||
|
**Parallelization**:
|
||||||
|
- **Can Run In Parallel**: YES (with T9, T10)
|
||||||
|
- **Parallel Group**: Wave 4
|
||||||
|
- **Blocks**: None
|
||||||
|
- **Blocked By**: T6
|
||||||
|
|
||||||
|
**References**:
|
||||||
|
- `apps/dashboard-ui/src/router/index.ts` - routing
|
||||||
|
- Date picker component
|
||||||
|
|
||||||
|
**Acceptance Criteria**:
|
||||||
|
- [ ] `npm run build` passes
|
||||||
|
- [ ] History page accessible at /traffic-history
|
||||||
|
- [ ] Date range filter works
|
||||||
|
- [ ] Export to CSV works
|
||||||
|
|
||||||
|
**Commit**: YES
|
||||||
|
- Message: `feat(ui): add traffic history view`
|
||||||
|
- Files: `apps/dashboard-ui/src/views/TrafficHistory.vue`, `apps/dashboard-ui/src/router/index.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Final Verification Wave
|
||||||
|
|
||||||
|
- [ ] F1. **Plan Compliance Audit** — `oracle`
|
||||||
|
- [ ] F2. **Code Quality Review** — `unspecified-high`
|
||||||
|
- [ ] F3. **Real Manual QA** — `unspecified-high`
|
||||||
|
- [ ] F4. **Scope Fidelity Check** — `deep`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commit Strategy
|
||||||
|
|
||||||
|
- Commit #1: Backend — TimescaleDB + gRPC proto
|
||||||
|
- Commit #2: Backend — gRPC server + SSE
|
||||||
|
- Commit #3: Agent — gRPC client
|
||||||
|
- Commit #4: Frontend — Dashboard charts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### Verification Commands
|
||||||
|
```bash
|
||||||
|
go build -tags dev ./... # Expected: no errors
|
||||||
|
cd apps/dashboard-ui && npm run build # Expected: no errors
|
||||||
|
psql -d nexusguard -c "SELECT * FROM device_traffic LIMIT 1" # Expected: empty or data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Final Checklist
|
||||||
|
- [ ] gRPC streaming works for 1000+ connections
|
||||||
|
- [ ] SSE pushes real-time status to dashboard
|
||||||
|
- [ ] TimescaleDB stores traffic data
|
||||||
|
- [ ] Charts show real-time + historical data
|
||||||
|
- [ ] Toggle controls work (per device + global)
|
||||||
|
- [ ] Performance: <100ms latency for status updates
|
||||||
+1
-1
Submodule apps/dashboard-ui updated: bb9ea20e8b...d77e5782c2
+1
-1
Submodule apps/device-agent updated: 6ca2d03e51...c143c6f647
+1
-1
Submodule apps/server-core updated: 4ff6e59e17...335e27b0e1
@@ -7,7 +7,30 @@ echo "========================================="
|
|||||||
|
|
||||||
STATE_FILE=".update-state"
|
STATE_FILE=".update-state"
|
||||||
FORCE_REBUILD=false
|
FORCE_REBUILD=false
|
||||||
[ "$1" = "--force" ] && FORCE_REBUILD=true
|
BACKUP=false
|
||||||
|
NO_MIGRATE=false
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--force)
|
||||||
|
FORCE_REBUILD=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--backup)
|
||||||
|
BACKUP=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--no-migrate)
|
||||||
|
NO_MIGRATE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "[!] Unknown option: $1"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
# 1. Pastikan file env ada
|
# 1. Pastikan file env ada
|
||||||
if [ ! -f .env ]; then
|
if [ ! -f .env ]; then
|
||||||
@@ -54,6 +77,57 @@ if [ "$NEEDS_REBUILD" = true ]; then
|
|||||||
echo "[+] Sourcing environment variables from root .env..."
|
echo "[+] Sourcing environment variables from root .env..."
|
||||||
set -a && . .env && set +a
|
set -a && . .env && set +a
|
||||||
|
|
||||||
|
# Volume safety check
|
||||||
|
echo "[+] Checking PostgreSQL volume safety..."
|
||||||
|
if ! docker volume inspect pgdata >/dev/null 2>&1; then
|
||||||
|
echo "[!] WARNING: PostgreSQL volume 'pgdata' not found. It will be created on container startup."
|
||||||
|
echo "[!] If you expect existing data, check your volume configuration."
|
||||||
|
else
|
||||||
|
echo "[+] PostgreSQL volume 'pgdata' found."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Backup option
|
||||||
|
if [ "$BACKUP" = true ]; then
|
||||||
|
echo "[+] Creating backup of PostgreSQL data..."
|
||||||
|
BACKUP_DIR="./backups"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/pgdata_backup_$TIMESTAMP.tar"
|
||||||
|
|
||||||
|
# Backup the volume using a temporary container
|
||||||
|
if docker run --rm -v pgdata:/data -v "$(pwd)/$BACKUP_DIR":/backup ubuntu tar czf "/backup/pgdata_backup_$TIMESTAMP.tar.gz" -C /data . 2>/dev/null; then
|
||||||
|
echo "[+] Backup created successfully: $BACKUP_FILE.gz"
|
||||||
|
else
|
||||||
|
echo "[!] Backup failed. Continuing without backup."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Password sync if DB_PASSWORD changed
|
||||||
|
echo "[+] Checking for PostgreSQL password synchronization..."
|
||||||
|
# Get current DB_PASSWORD from .env
|
||||||
|
CURRENT_DB_PASSWORD="${DB_PASSWORD:-nexusguard}"
|
||||||
|
|
||||||
|
# Try to connect to existing postgres container and update password if needed
|
||||||
|
POSTGRES_CONTAINER=$(docker compose ps -q postgres 2>/dev/null)
|
||||||
|
if [ -n "$POSTGRES_CONTAINER" ]; then
|
||||||
|
echo "[+] Found running PostgreSQL container. Checking password..."
|
||||||
|
# Test if current password works
|
||||||
|
if docker exec "$POSTGRES_CONTAINER" pg_isready -U nexusguard -d nexusguard 2>/dev/null; then
|
||||||
|
echo "[+] Current password works. No password update needed."
|
||||||
|
else
|
||||||
|
echo "[!] Current password failed. Attempting to update PostgreSQL password to match .env..."
|
||||||
|
# Try to alter user password (requires superuser or same user)
|
||||||
|
if docker exec -u postgres "$POSTGRES_CONTAINER" psql -c "ALTER USER nexusguard WITH PASSWORD '$CURRENT_DB_PASSWORD';" 2>/dev/null; then
|
||||||
|
echo "[+] PostgreSQL password updated successfully."
|
||||||
|
else
|
||||||
|
echo "[!] Failed to update password. You may need to manually update it or check permissions."
|
||||||
|
echo "[!] Continuing update - authentication may fail if password mismatch."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[i] No running PostgreSQL container found. Password will be set on container initialization (if data directory is empty)."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[+] Stopping existing containers..."
|
echo "[+] Stopping existing containers..."
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
@@ -63,8 +137,12 @@ if [ "$NEEDS_REBUILD" = true ]; then
|
|||||||
echo "[+] Restarting containers..."
|
echo "[+] Restarting containers..."
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
echo "[+] Running automated database migrations..."
|
if [ "$NO_MIGRATE" = false ]; then
|
||||||
docker exec nexus-guard-suite-server-core-1 ./server-core -migrate-prod || echo "[!] Migration skipped or failed. It might not be needed."
|
echo "[+] Running automated database migrations..."
|
||||||
|
docker exec nexus-guard-suite-server-core-1 ./server-core -migrate-prod || echo "[!] Migration skipped or failed. It might not be needed."
|
||||||
|
else
|
||||||
|
echo "[+] Skipping database migration (--no-migrate flag used)."
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[+] Backfilling interface addresses..."
|
echo "[+] Backfilling interface addresses..."
|
||||||
docker exec nexus-guard-suite-server-core-1 ./server-core -backfill-interface 2>/dev/null || echo "[!] Backfill skipped or not needed."
|
docker exec nexus-guard-suite-server-core-1 ./server-core -backfill-interface 2>/dev/null || echo "[!] Backfill skipped or not needed."
|
||||||
@@ -79,6 +157,9 @@ if [ "$NEEDS_REBUILD" = true ]; then
|
|||||||
echo " Update & Deployment complete! "
|
echo " Update & Deployment complete! "
|
||||||
echo " Dashboard : http://localhost:${WEB_PORT:-80}"
|
echo " Dashboard : http://localhost:${WEB_PORT:-80}"
|
||||||
echo " API Port : ${API_PORT:-8080}"
|
echo " API Port : ${API_PORT:-8080}"
|
||||||
|
if [ "$BACKUP" = true ]; then
|
||||||
|
echo " Backup : Stored in ./backups/ directory"
|
||||||
|
fi
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
else
|
else
|
||||||
echo "========================================="
|
echo "========================================="
|
||||||
|
|||||||
Reference in New Issue
Block a user