Move distributed bug fixing app from core/cli internal/bugseti/ and cmd/bugseti/ into its own module. Library code at package root, app entry point in cmd/, design docs in docs/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
5 KiB
Markdown
150 lines
5 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|