go/docs/plans/2026-02-13-bugseti-hub-service-design.md
Snider 9c25d39570 docs: add BugSETI HubService design doc
Thin HTTP client for portal coordination API — issue claiming,
stats sync, leaderboard, auto-register via forge token.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-16 05:53:52 +00:00

5 KiB

BugSETI HubService Design

Overview

A thin HTTP client service in the BugSETI desktop app that coordinates with the agentic portal's /api/bugseti/* endpoints. Prevents duplicate work across the 11 community testers, aggregates stats for leaderboard, and registers client instances.

Decisions

Decision Choice Rationale
Target Direct to portal API Endpoints built for this purpose
Auth Auto-register via forge token No manual key management for users
Sync strategy Lazy/manual User-triggered claims, manual stats sync
Offline mode Offline-first Queue failed writes, retry on reconnect
Approach Thin HTTP client (net/http) Matches existing patterns, no deps

Architecture

File: internal/bugseti/hub.go + hub_test.go

HubService
├── HTTP client (net/http, 10s timeout)
├── Auth: auto-register via forge token → cached ak_ token
├── Config: HubURL, HubToken, ClientID in ConfigService
├── Offline-first: queue failed writes, drain on next success
└── Lazy sync: user-triggered, no background goroutines

Dependencies: ConfigService only.

Integration:

  • QueueService calls hub.ClaimIssue() when user picks an issue
  • SubmitService calls hub.UpdateStatus("completed") after PR
  • TrayService calls hub.GetLeaderboard() from UI
  • main.go calls hub.Register() on startup

Data Types

type HubClient struct {
    ClientID  string    // UUID, generated once, persisted in config
    Name      string    // e.g. "Snider's MacBook"
    Version   string    // bugseti.GetVersion()
    OS        string    // runtime.GOOS
    Arch      string    // runtime.GOARCH
}

type HubClaim struct {
    IssueID     string  // "owner/repo#123"
    Repo        string
    IssueNumber int
    Title       string
    URL         string
    Status      string  // claimed|in_progress|completed|skipped
    ClaimedAt   time.Time
    PRUrl       string
    PRNumber    int
}

type LeaderboardEntry struct {
    Rank            int
    ClientName      string
    IssuesCompleted int
    PRsSubmitted    int
    PRsMerged       int
    CurrentStreak   int
}

type GlobalStats struct {
    TotalParticipants  int
    ActiveParticipants int
    TotalIssuesCompleted int
    TotalPRsMerged     int
    ActiveClaims       int
}

API Mapping

Method HTTP Endpoint Trigger
Register() POST /register App startup
Heartbeat() POST /heartbeat Manual / periodic if enabled
ClaimIssue(issue) POST /issues/claim User picks issue
UpdateStatus(id, status) PATCH /issues/{id}/status PR submitted, skip
ReleaseClaim(id) DELETE /issues/{id}/claim User abandons
IsIssueClaimed(id) GET /issues/{id} Before showing issue
ListClaims(filters) GET /issues/claimed UI active claims view
SyncStats(stats) POST /stats/sync Manual from UI
GetLeaderboard(limit) GET /leaderboard UI leaderboard view
GetGlobalStats() GET /stats UI stats dashboard

Auto-Register Flow

New endpoint on portal:

POST /api/bugseti/auth/forge
Body: { "forge_url": "https://forge.lthn.io", "forge_token": "..." }

Portal validates token against Forgejo API (/api/v1/user), creates an AgentApiKey with bugseti.read + bugseti.write scopes, returns { "api_key": "ak_..." }.

HubService caches the ak_ token in config.json. On 401, clears cached token and re-registers.

Error Handling

Error Behaviour
Network unreachable Log, queue write ops, return cached reads
401 Unauthorised Clear token, re-register via forge
409 Conflict (claim) Return "already claimed" — not an error
404 (claim not found) Return nil
429 Rate limited Back off, queue the op
5xx Server error Log, queue write ops

Pending operations queue:

  • Failed writes stored in []PendingOp, persisted to $DataDir/hub_pending.json
  • Drained on next successful user-triggered call (no background goroutine)
  • Each op has: method, path, body, created_at

Config Changes

New fields in Config struct:

HubURL    string `json:"hubUrl,omitempty"`    // portal API base URL
HubToken  string `json:"hubToken,omitempty"`  // cached ak_ token
ClientID  string `json:"clientId,omitempty"`  // UUID, generated once
ClientName string `json:"clientName,omitempty"` // display name

Files Changed

File Action
internal/bugseti/hub.go New — HubService
internal/bugseti/hub_test.go New — httptest-based tests
internal/bugseti/config.go Edit — add Hub* + ClientID fields
cmd/bugseti/main.go Edit — create + register HubService
cmd/bugseti/tray.go Edit — leaderboard/stats menu items
Laravel: auth controller New — /api/bugseti/auth/forge

Testing

  • httptest.NewServer mocks for all endpoints
  • Test success, network error, 409 conflict, 401 re-auth flows
  • Test pending ops queue: add when offline, drain on reconnect
  • _Good, _Bad, _Ugly naming convention