Files
datadunia cbacfea7f2
NexusGuard CI / server-core-test (push) Failing after 3m6s
NexusGuard CI / server-core-build (push) Has been skipped
NexusGuard CI / device-agent-test (push) Failing after 4s
NexusGuard CI / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI / dashboard-test (push) Failing after 4s
NexusGuard CI / dashboard-dist (push) Has been skipped
chore: update submodule refs, clean up plans/evidence, update .gitignore
2026-06-07 23:53:15 +07:00

8.4 KiB

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:176hbMgr 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:
      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:

    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_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):

    <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 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 Verificationgo 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