chore: archive old plans, add new plan docs, update submodules
NexusGuard CI / server-core-test (push) Failing after 47s
NexusGuard CI / device-agent-test (push) Failing after 35s
NexusGuard CI / dashboard-ui-build (push) Failing after 31s

This commit is contained in:
datadunia
2026-05-28 02:50:43 +07:00
parent cf6ffba404
commit 075f915a66
17 changed files with 1199 additions and 3 deletions
@@ -0,0 +1,226 @@
# Fix: Device Online/Offline Status & Server Column
## TL;DR
> **Quick Summary**: Fix 3 issues: (1) `StartHeartbeatCollector` dead code — periodic Redis→DB sync never runs, so IsActive is never updated; (2) Devices table doesn't show which WireGuard server a device belongs to; (3) Frontend Devices UI missing server info.
>
> **Deliverables**:
> - Heartbeat collector goroutine started in main.go (1-line fix)
> - WgServerID exposed in Device API response
> - Server name column in Devices table
>
> **Estimated Effort**: Quick (3-4 tasks, 1 wave)
> **Parallel Execution**: YES — 2 tracks
---
## Context
### Root Cause Analysis
**Device Online/Offline status (IsActive) mechanism is broken:**
1. `heartbeat/redis.go:38-55` defines `StartHeartbeatCollector()` — a goroutine that periodically calls `SyncToDB()` to sync Redis heartbeat keys → device `is_active` in DB
2. **`StartHeartbeatCollector` is NEVER CALLED** from `main.go:176``hbMgr` is created but only `hbHandler` (HTTP endpoint) is wired up
3. Result: `SyncToDB` never runs → `IsActive` is permanently stuck at whatever value the device was created with
4. Existing devices (created before the `IsActive:true` hardcode removal) show "Online" forever; new devices show "Offline" forever
**Device → Server relationship missing in UI:**
1. Device model has `WgServerID` uuid FK (models.go:61) — but **no `json` tag**, so it's omitted from API response
2. Device model has **no GORM relation** to WgServer (no `WgServer WgServer` field)
3. Devices.vue table columns: Owner, Name, IP, Status, Actions — **no Server column**
4. Device TypeScript interface (devices.ts:3-21) omits `WgServerID`
### Key Decisions
- Simple fix: just add `json` tag to `WgServerID` in the model + add `WgServer` GORM relation + Preload
- Frontend can display server name directly from API data
- No need for complex response restructuring
---
## Work Objectives
### Concrete Deliverables
1. `main.go` — add `hbMgr.StartHeartbeatCollector()` call to start periodic Redis→DB sync
2. `internal/models/models.go` — add `json:"wg_server_id"` tag + `WgServer` relation field
3. `api/devices.go` — add `.Preload("WgServer")` to List handler
4. `src/api/devices.ts` — add `WgServerID` + `WgServer` fields to Device interface
5. `src/views/Devices.vue` — add Server column to table
### Must Have
- [ ] `StartHeartbeatCollector` called from main.go (periodic 30s sync)
- [ ] Device API response includes `wg_server_id` and `wg_server.name`
- [ ] Devices table shows server name column
### Must NOT Have
- **JANGAN** ubah heartbeat interval (90s TTL, 30s collector — existing values)
- **JANGAN** hapus `isActive := false` fallback di SyncToDB
- **JANGAN** tambah migration baru (AutoMigrate handles new relation column)
---
## Verification Strategy
### Test Decision
- **Infrastructure exists**: YES
- **Automated tests**: NO (quick fix, QA via curl/code review)
- **Agent-Executed QA**: Each task verified by reading the modified files
---
## Execution Strategy
```
Wave 1 (Parallel — ALL tasks independent):
├── Task 1: Start heartbeat collector in main.go [quick]
├── Task 2: Add WgServerID json tag + relation to Device model [quick]
├── Task 3: Preload WgServer in Devices List handler [quick]
├── Task 4: Update Device TS interface + add Server column in Devices.vue [quick]
Wave FINAL: Build verification + code review
├── F1: go build ./... passes
├── F2: Verify endpoints return wg_server_id
```
---
## TODOs
- [x] 1. Start heartbeat collector in main.go
**What to do**:
- In `apps/server-core/main.go`, after line 176 (`hbMgr := heartbeat.NewHeartbeatManager(rdb, db)`), add:
```go
if rdb != nil {
hbMgr.StartHeartbeatCollector(context.Background(), 30*time.Second)
}
```
- This starts the periodic goroutine that syncs Redis heartbeat keys → device `IsActive` in DB
- The collector reads Redis key `device:<ID>:ping` (90s TTL, written by device agent heartbeat)
- If key exists → IsActive = true; if TTL expired → IsActive = false
**Parallelization**:
- Can Run In Parallel: YES (with Tasks 2, 3, 4)
**References**:
- `apps/server-core/internal/heartbeat/redis.go:38-55` — StartHeartbeatCollector definition (no callers currently)
- `apps/server-core/main.go:175-176` — hbMgr creation
- `apps/server-core/main.go:141-144` — rdb conditional creation (nil if Redis not configured)
**Acceptance Criteria**:
- [ ] `StartHeartbeatCollector` called conditionally (only if Redis configured)
- [ ] `go build ./...` passes
- [x] 2. Add WgServerID json tag + WgServer relation to Device model
**What to do**:
- In `apps/server-core/internal/models/models.go`, modify the Device struct:
- Line 61: Add `json:"wg_server_id"` tag to WgServerID field
- Add new field after line 61: `WgServer WgServer \`gorm:"foreignKey:WgServerID"\``
- Use TAB indentation (Go standard)
**Before**:
```go
WgServerID uuid.UUID `gorm:"type:uuid;not null;index"`
```
**After**:
```go
WgServerID uuid.UUID `json:"wg_server_id" gorm:"type:uuid;not null;index"`
WgServer WgServer `gorm:"foreignKey:WgServerID"`
```
**Parallelization**:
- Can Run In Parallel: YES (with Tasks 1, 4)
**References**:
- `apps/server-core/internal/models/models.go:57-83` — Device struct (WgServerID at line 61)
- `apps/server-core/internal/models/models.go:59-60` — User relation pattern (User + UserID, to follow)
- `apps/server-core/internal/models/models.go:25-48` — WgServer struct (already defined)
**Acceptance Criteria**:
- [ ] `wg_server_id` appears in JSON response from GET /api/v1/devices
- [ ] `wg_server` object appears in JSON response when preloaded
- [ ] `go build ./...` passes
- [x] 3. Preload WgServer in Devices List handler
**What to do**:
- In `apps/server-core/api/devices.go`, DeviceList handler (line 35-56):
- Change line 41: `q := h.db.Preload("User")` → `q := h.db.Preload("User").Preload("WgServer")`
- For non-admin users (line 49-53): add `.Preload("WgServer")` too
**Parallelization**:
- Can Run In Parallel: YES (with Tasks 1, 4)
**References**:
- `apps/server-core/api/devices.go:35-56` — List handler (current Preload("User") at line 41)
**Acceptance Criteria**:
- [ ] GET /api/v1/devices returns `wg_server` object with `name`, `public_endpoint` etc.
- [ ] `go build ./...` passes
- [x] 4. Update Device TS interface + add Server column in Devices.vue
**What to do**:
- In `apps/dashboard-ui/src/api/devices.ts`:
- Add `WgServerID: string` to Device interface
- Add `WgServer?: { ID: string; Name: string; PublicEndpoint: string }` to Device interface
- In `apps/dashboard-ui/src/views/Devices.vue`:
- Add a "Server" column header after "Status" (or between Name and Status)
- Add server name cell: `{{ device.WgServer?.Name || 'Unknown' }}`
- Keep the existing columns intact
**Template change (Devices.vue:17-21)**:
```vue
<thead>
<tr class="text-gray-400 border-b border-white/10">
<th v-if="authStore.isAdmin" class="pb-3">Owner</th>
<th class="pb-3">Name</th>
<th class="pb-3">Server</th>
<th class="pb-3">IP Address</th>
<th class="pb-3">Status</th>
<th class="pb-3">Actions</th>
</tr>
</thead>
```
And in tbody (after Name cell):
```vue
<td class="py-4 text-gray-400 text-sm">{{ device.WgServer?.Name || 'Unknown' }}</td>
```
**Parallelization**:
- Can Run In Parallel: YES (with Tasks 1, 2, 3)
**References**:
- `apps/dashboard-ui/src/api/devices.ts:3-21` — Device interface
- `apps/dashboard-ui/src/views/Devices.vue:14-22` — Table headers
- `apps/dashboard-ui/src/views/Devices.vue:25-42` — Table rows
**Acceptance Criteria**:
- [ ] Device type includes `WgServerID` and `WgServer` field
- [ ] Devices table shows server name column with data
---
## Final Verification Wave
- [x] F1. **Build Verification** — `go build ./...` passes for server-core
- [x] F2. **Review changes** — All 4 files modified correctly
---
## Commit Strategy
- **1**: `fix(core): start heartbeat collector goroutine in main.go`
- **2-4**: `fix(api): expose wg_server_id in device response, add server column`
---
## Success Criteria
- [ ] `StartHeartbeatCollector` running as goroutine in production
- [ ] Device API returns `wg_server_id` in JSON
- [ ] Devices table in UI shows server name
- [ ] `go build ./...` passes