# 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
