Skip to content

fix(cli): home heartbeat fetches one client, not the whole account (#338)#339

Open
saadqbal wants to merge 4 commits into
developfrom
fix/338-heartbeat-single-client
Open

fix(cli): home heartbeat fetches one client, not the whole account (#338)#339
saadqbal wants to merge 4 commits into
developfrom
fix/338-heartbeat-single-client

Conversation

@saadqbal

@saadqbal saadqbal commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Symptom

Bare tracebloc/tb home screen shows a false negative:

⚠ Secure environment "…" · running — couldn't confirm it's connected to tracebloc — run tb doctor

…while tb doctor is fully green (Backend auth ✔, egress ✔, active client resolved). The backend IS reachable — the home probe just can't confirm it in time.

Root cause

realHeartbeatlookupClientStatusapi.Client.ListClients, which does GET /edge-device/ and paginates through every client in the account (up to maxListPages=100), then filters client-side for the active id — all under homeProbeTimeout=1200ms / homeDetectBudget=1500ms. Enough clients to need multiple pages (or slow prod round-trips) → context deadline → beatUnknown → "couldn't confirm". tb doctor is green because its Backend-auth check is a single GET /userinfo/ with no tight timeout.

Fix

  • Add api.Client.GetClient(ctx, id) → GET /edge-device/{id}/ — the O(1) detail route (same one PatchClientClusterID/RevokeClient use); PatchClientClusterID already decodes a single ProvisionedClient from it. 404 → (nil, nil) (not-found, not an error).
  • Switch lookupClientStatus to fetch the single active client directly instead of listing the whole account.
  • Remove the now-unused findClientByID (blocking deadcode gate). ListClients still backs client list / use / create-collision.

One fast round-trip fits comfortably in the 1.2s budget → the heartbeat returns the honest Online / not-online instead of spurious "couldn't confirm".

Verification

  • go build/go vet/gofmt clean; GetClient contract test (2xx decode / 404→nil,nil / 500→APIError, pins the detail path) + the heartbeat/home tests pass. Full internal/cli suite left to CI.

Fixes #338.

🤖 Generated with Claude Code


Note

Medium Risk
Touches the shared status lookup used by home heartbeat, client status --wait, and delete guards; behavior is intended to match prior semantics with a different API shape, backed by broad test updates.

Overview
Fixes false “couldn't confirm it's connected” on the bare home screen when the backend is fine but the probe times out (~1.2s).

lookupClientStatus (home heartbeat, client status, delete pre-offboard guard) no longer calls ListClients and scan the account. It uses a new api.Client.GetClientGET /edge-device/{id}/ — for one round-trip. findClientByID is removed. ListClients is unchanged for client list, create collision, and adopt flows.

GetClient decodes a single ProvisionedClient; 404 → (nil, nil) (missing client, not an error). A non-numeric active client id is treated as not found (no API call) so --wait fails fast instead of polling to timeout.

Tests add TestGetClient and TestClientStatus_WaitFailsFastOnNonNumericID; status/delete stubs expect /edge-device/5/ and object JSON instead of list payloads.

Reviewed by Cursor Bugbot for commit c1f50ea. Bugbot is set up for automated code reviews on this repo. Configure here.

)

The bare-CLI home screen showed a false "running — couldn't confirm it's
connected to tracebloc" while `tb doctor` was fully green. Root cause:
realHeartbeat → lookupClientStatus called api.Client.ListClients, which pages
through EVERY client in the account (GET /edge-device/, up to maxListPages=100)
just to read one client's status — all under the ~1.2s home-probe budget. On
accounts with enough clients to need multiple pages, the list can't finish in
time → context deadline → beatUnknown → the false "couldn't confirm". doctor is
green because its Backend-auth check is a single GET /userinfo/ with no leash.

Add api.Client.GetClient (GET /edge-device/{id}/ — the detail route
PatchClientClusterID/RevokeClient already use) and switch lookupClientStatus to
fetch the single active client directly (404 → not-found, not an error). One
O(1) round-trip fits comfortably in the budget → honest Online/not-online.
Removes the now-unused findClientByID (blocking deadcode gate). ListClients
still backs list / use / create-collision.

Fixes #338.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@LukasWodka

Copy link
Copy Markdown
Contributor

👋 Heads-up — Code review queue is at 40 / 30

Above the WIP limit. The team convention is to review existing PRs before opening new work.

Open PRs currently in Code review (oldest first):

Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.)

@saadqbal saadqbal requested a review from aptracebloc July 15, 2026 14:35
@saadqbal saadqbal self-assigned this Jul 15, 2026
saadqbal and others added 2 commits July 15, 2026 19:43
… route

lookupClientStatus now fetches the single client (GetClient) instead of
ListClients, so the TestClientStatus_* backend stubs must serve
GET /edge-device/5/ (single object) rather than GET /edge-device/ (array).
Without this the status path decoded an empty body ('unexpected end of JSON
input') and the --wait fail-fast tests (426/401/missing) looped to the 10m
test timeout. Create/list stubs are unchanged — ListClients still backs those.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delete's offboard guard also checks the client via lookupClientStatus, which
now fetches GET /edge-device/{id}/. The TestDelete_* status stubs served the
list route → the guard hit an unhandled path (empty body / 'unexpected GET
/edge-device/5/') and the online/426/401 guards stopped firing. Redirect the
status-guard GETs to /edge-device/5/ (single object). The --force revoke-fail
tests skip the guard and only stub /revoke/, so they're untouched.

Full internal/cli package green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4d633c7. Configure here.

Comment thread internal/cli/client.go
…error (Bugbot #338)

lookupClientStatus returned a permanent error when ActiveClientID wasn't
numeric. --wait only fail-fasts on 401/403/426/404, so it treated that parse
error as transient and polled to the timeout — a regression vs the old
ListClients+match path, which returned found=false (a non-numeric id can never
match). Return (0, false, nil) so --wait fail-fasts on the missing client.
Adds TestClientStatus_WaitFailsFastOnNonNumericID.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants