chore: update submodule refs for heartbeat fix + server column
server-core: start heartbeat collector goroutine, expose wg_server_id dashboard-ui: add server column to devices table Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] `StartHeartbeatCollector` running as goroutine in production
|
||||
- [ ] Device API returns `wg_server_id` in JSON
|
||||
- [ ] Devices table in UI shows server name
|
||||
- [ ] `go build ./...` passes
|
||||
+1
-1
Submodule apps/dashboard-ui updated: 0dbafae09d...09a8348055
+1
-1
Submodule apps/server-core updated: bc8dd981f3...179b3e3696
Reference in New Issue
Block a user