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>
130 lines
2.7 KiB
Go
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
|
|
}
|