# 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::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 Owner Name Server IP Address Status Actions ``` And in tbody (after Name cell): ```vue {{ device.WgServer?.Name || 'Unknown' }} ``` **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