diff --git a/.sisyphus/plans/device-status-heartbeat.md b/.sisyphus/plans/device-status-heartbeat.md new file mode 100644 index 0000000..de5b2b9 --- /dev/null +++ b/.sisyphus/plans/device-status-heartbeat.md @@ -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::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 + + + 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 + +- [ ] 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 diff --git a/apps/dashboard-ui b/apps/dashboard-ui index 0dbafae..09a8348 160000 --- a/apps/dashboard-ui +++ b/apps/dashboard-ui @@ -1 +1 @@ -Subproject commit 0dbafae09dc43690fc09b671203fa89d6deb5399 +Subproject commit 09a8348055c283037a336814206711a9d06d8b77 diff --git a/apps/server-core b/apps/server-core index bc8dd98..179b3e3 160000 --- a/apps/server-core +++ b/apps/server-core @@ -1 +1 @@ -Subproject commit bc8dd981f309eb2ac557aab061327176dbb7af42 +Subproject commit 179b3e36969385ce5c73bd9f4de9a54837f14f34