8.4 KiB
Fix: Device Online/Offline Status & Server Column
TL;DR
Quick Summary: Fix 3 issues: (1)
StartHeartbeatCollectordead 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:
heartbeat/redis.go:38-55definesStartHeartbeatCollector()— a goroutine that periodically callsSyncToDB()to sync Redis heartbeat keys → deviceis_activein DBStartHeartbeatCollectoris NEVER CALLED frommain.go:176—hbMgris created but onlyhbHandler(HTTP endpoint) is wired up- Result:
SyncToDBnever runs →IsActiveis permanently stuck at whatever value the device was created with - Existing devices (created before the
IsActive:truehardcode removal) show "Online" forever; new devices show "Offline" forever
Device → Server relationship missing in UI:
- Device model has
WgServerIDuuid FK (models.go:61) — but nojsontag, so it's omitted from API response - Device model has no GORM relation to WgServer (no
WgServer WgServerfield) - Devices.vue table columns: Owner, Name, IP, Status, Actions — no Server column
- Device TypeScript interface (devices.ts:3-21) omits
WgServerID
Key Decisions
- Simple fix: just add
jsontag toWgServerIDin the model + addWgServerGORM relation + Preload - Frontend can display server name directly from API data
- No need for complex response restructuring
Work Objectives
Concrete Deliverables
main.go— addhbMgr.StartHeartbeatCollector()call to start periodic Redis→DB syncinternal/models/models.go— addjson:"wg_server_id"tag +WgServerrelation fieldapi/devices.go— add.Preload("WgServer")to List handlersrc/api/devices.ts— addWgServerID+WgServerfields to Device interfacesrc/views/Devices.vue— add Server column to table
Must Have
StartHeartbeatCollectorcalled from main.go (periodic 30s sync)- Device API response includes
wg_server_idandwg_server.name - Devices table shows server name column
Must NOT Have
- JANGAN ubah heartbeat interval (90s TTL, 30s collector — existing values)
- JANGAN hapus
isActive := falsefallback 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
-
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:if rdb != nil { hbMgr.StartHeartbeatCollector(context.Background(), 30*time.Second) } - This starts the periodic goroutine that syncs Redis heartbeat keys → device
IsActivein 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 creationapps/server-core/main.go:141-144— rdb conditional creation (nil if Redis not configured)
Acceptance Criteria:
StartHeartbeatCollectorcalled conditionally (only if Redis configured)go build ./...passes
- In
-
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)
- Line 61: Add
Before:
WgServerID uuid.UUID `gorm:"type:uuid;not null;index"`After:
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_idappears in JSON response from GET /api/v1/deviceswg_serverobject appears in JSON response when preloadedgo build ./...passes
- In
-
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
- Change line 41:
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_serverobject withname,public_endpointetc. go build ./...passes
- In
-
4. Update Device TS interface + add Server column in Devices.vue
What to do:
-
In
apps/dashboard-ui/src/api/devices.ts:- Add
WgServerID: stringto Device interface - Add
WgServer?: { ID: string; Name: string; PublicEndpoint: string }to Device interface
- Add
-
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):
<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):
<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 interfaceapps/dashboard-ui/src/views/Devices.vue:14-22— Table headersapps/dashboard-ui/src/views/Devices.vue:25-42— Table rows
Acceptance Criteria:
- Device type includes
WgServerIDandWgServerfield - Devices table shows server name column with data
-
Final Verification Wave
- F1. Build Verification —
go build ./...passes for server-core - 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
StartHeartbeatCollectorrunning as goroutine in production- Device API returns
wg_server_idin JSON - Devices table in UI shows server name
go build ./...passes