cli/pkg/lab/collector/forgejo.go
Claude 50da0adcb7 feat: integrate lab dashboard as core lab serve
Port the standalone lab dashboard (lab.lthn.io) into the core CLI as
pkg/lab/ with collectors, handlers, and HTML templates. The dashboard
monitors machines, Docker containers, Forgejo, HuggingFace models,
training runs, and InfluxDB metrics with SSE live updates.

New command: core lab serve --bind :8080

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 05:53:52 +00:00

130 lines
2.7 KiB
Go

package collector
import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
"forge.lthn.ai/core/cli/pkg/lab"
)
type Forgejo struct {
url string
token string
store *lab.Store
}
func NewForgejo(forgeURL, token string, s *lab.Store) *Forgejo {
return &Forgejo{url: forgeURL, token: token, store: s}
}
func (f *Forgejo) Name() string { return "forgejo" }
func (f *Forgejo) Collect(ctx context.Context) error {
if f.token == "" {
return nil
}
commits, err := f.recentActivity(ctx)
if err != nil {
f.store.SetError("forgejo", err)
return err
}
f.store.SetCommits(commits)
f.store.SetError("forgejo", nil)
return nil
}
type forgeRepo struct {
FullName string `json:"full_name"`
UpdatedAt time.Time `json:"updated_at"`
}
type forgeCommit struct {
SHA string `json:"sha"`
Commit struct {
Message string `json:"message"`
Author struct {
Name string `json:"name"`
Date time.Time `json:"date"`
} `json:"author"`
} `json:"commit"`
}
func (f *Forgejo) recentActivity(ctx context.Context) ([]lab.Commit, error) {
// Get recently updated repos
repos, err := f.apiGet(ctx, "/api/v1/repos/search?sort=updated&order=desc&limit=5")
if err != nil {
return nil, err
}
var repoList []forgeRepo
if err := json.Unmarshal(repos, &repoList); err != nil {
// The search API wraps in {"data": [...], "ok": true}
var wrapped struct {
Data []forgeRepo `json:"data"`
}
if err2 := json.Unmarshal(repos, &wrapped); err2 != nil {
return nil, err
}
repoList = wrapped.Data
}
var commits []lab.Commit
for _, repo := range repoList {
if len(commits) >= 10 {
break
}
data, err := f.apiGet(ctx, fmt.Sprintf("/api/v1/repos/%s/commits?limit=2", repo.FullName))
if err != nil {
continue
}
var fc []forgeCommit
if err := json.Unmarshal(data, &fc); err != nil {
continue
}
for _, c := range fc {
msg := c.Commit.Message
if len(msg) > 80 {
msg = msg[:77] + "..."
}
commits = append(commits, lab.Commit{
SHA: c.SHA[:8],
Message: msg,
Author: c.Commit.Author.Name,
Repo: repo.FullName,
Timestamp: c.Commit.Author.Date,
})
}
}
return commits, nil
}
func (f *Forgejo) apiGet(ctx context.Context, path string) (json.RawMessage, error) {
req, err := http.NewRequestWithContext(ctx, "GET", f.url+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "token "+f.token)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, fmt.Errorf("forgejo %s returned %d", path, resp.StatusCode)
}
var raw json.RawMessage
if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
return nil, err
}
return raw, nil
}