dashboard

package
v0.41.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 15, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package dashboard holds the domain model both forge clients produce and the aggregator that merges them into one snapshot. Neither forge client depends on the other; both only depend on this package's types.

Index

Constants

View Source
const DefaultMaxConcurrency = 8

DefaultMaxConcurrency bounds how many repositories a GenericSource fetches at once. Generous enough to make quick work of a ~100-repo account without opening that many sockets at once for no benefit.

Variables

This section is empty.

Functions

This section is empty.

Types

type Aggregator

type Aggregator struct {
	// contains filtered or unexported fields
}

Aggregator holds the most recently fetched Snapshot and refreshes it in the background. Get never blocks on a forge: it always returns whatever was last successfully assembled, even if a refresh is running right now or every source just failed.

func NewAggregator

func NewAggregator(sources []Source) *Aggregator

NewAggregator returns an Aggregator whose Get answers an empty snapshot until the first Refresh (or Run) completes.

func (*Aggregator) Get

func (a *Aggregator) Get() Snapshot

Get returns the current snapshot. Safe to call concurrently with Refresh.

func (*Aggregator) Refresh

func (a *Aggregator) Refresh(ctx context.Context)

Refresh fetches every source concurrently and replaces the snapshot with the merged result, and does not return until a pass that started at or after this call has completed — see coalescer's doc comment for the guarantee and the incident behind it. A source's own Fetch never returns an error — a forge it can't reach at all shows up as an unreachable ForgeHealth entry instead, so one broken forge never drops the other's data.

func (*Aggregator) RefreshRepo added in v0.39.0

func (a *Aggregator) RefreshRepo(ctx context.Context, forge Forge, owner, name, fullName string) bool

RefreshRepo refreshes just the named repository's pull requests and issues, merging the result into the current snapshot in place of that repo's previous entries rather than replacing the whole snapshot — the scoped counterpart to Refresh, for a webhook delivery that already knows exactly which repo changed. Reports false if no configured Source drives forge, or if the one that does can't do a scoped fetch (RepoRefresher, the same optional-capability pattern as RateLimiter) — the caller's own signal to fall back to a full Refresh, so a repo never silently goes unrefreshed just because its Source can't do this yet.

Concurrent calls for the same repo coalesce the same way Refresh's own calls do — see coalescer — keyed so an unrelated repo's own refresh never waits on this one.

func (*Aggregator) Run

func (a *Aggregator) Run(ctx context.Context, interval time.Duration)

Run refreshes immediately, then again every interval, until ctx is canceled. Meant to run in its own goroutine for the lifetime of the process.

func (*Aggregator) Subscribe added in v0.23.0

func (a *Aggregator) Subscribe() (<-chan Snapshot, func())

Subscribe returns a channel that receives the new Snapshot after every completed Refresh, and a function to stop receiving them. The channel is buffered by exactly one — a subscriber that falls behind (an SSE client whose write is blocked on a slow network) never makes Refresh itself block; it just misses an intermediate update and catches up on the next one. Call the returned function when done, typically via defer: it closes the channel, so a range over it ends cleanly.

type CIStatus

type CIStatus string

CIStatus is the combined result across every check reported against a pull request's head commit. Matches components.schemas.CIStatus.

const (
	CISuccess CIStatus = "success"
	CIFailure CIStatus = "failure"
	CIPending CIStatus = "pending"
	CINone    CIStatus = "none"
)

The combined result across every check on a pull request's head commit.

type Forge

type Forge string

Forge names one of the two forges this service talks to. Matches components.schemas.Forge in api/openapi.yaml.

const (
	ForgeGitHub  Forge = "github"
	ForgeForgejo Forge = "forgejo"
)

The only two forges this service knows how to talk to.

type ForgeClient

type ForgeClient interface {
	ListRepos(ctx context.Context) ([]RepoRef, error)
	ListOpenPullRequests(ctx context.Context, owner, name, repo string) ([]PullRequest, error)
	ListOpenIssues(ctx context.Context, owner, name, repo string) ([]Issue, error)
}

ForgeClient is what a forge-specific client (internal/github, internal/forgejo) provides for GenericSource to drive. Both clients' method sets already match this shape, so neither needs an adapter.

type ForgeHealth

type ForgeHealth struct {
	Forge     Forge      `json:"forge"`
	Reachable bool       `json:"reachable"`
	Error     string     `json:"error,omitempty"`
	RepoCount int        `json:"repoCount"`
	RateLimit *RateLimit `json:"rateLimit,omitempty"`
}

ForgeHealth matches components.schemas.ForgeHealth.

type GenericSource

type GenericSource struct {
	// contains filtered or unexported fields
}

GenericSource drives a ForgeClient the same way regardless of which forge it talks to: list repositories, then fetch each one's open pull requests and issues concurrently, bounded, skipping a single repo's failure rather than failing the whole forge.

func NewGenericSource

func NewGenericSource(forge Forge, client ForgeClient, maxConcurrency int) *GenericSource

NewGenericSource returns a Source reporting as forge, driving client with at most maxConcurrency repositories fetched at once.

func (*GenericSource) Fetch

func (s *GenericSource) Fetch(ctx context.Context) Result

Fetch implements Source by listing repos, then fetching each one's open pull requests and issues concurrently, bounded by maxConcurrency. A single repo's failure is logged and skipped, not fatal to the forge.

func (*GenericSource) FetchRepo added in v0.38.0

func (s *GenericSource) FetchRepo(ctx context.Context, owner, name, fullName string) ([]PullRequest, []Issue, error)

FetchRepo implements RepoRefresher: the same per-repo calls Fetch already makes for every tracked repo, but for just the one a caller (a webhook delivery) already knows the identity of — no ListRepos call needed first.

func (*GenericSource) Forge added in v0.38.0

func (s *GenericSource) Forge() Forge

Forge implements Source.

type Issue

type Issue struct {
	Forge     Forge     `json:"forge"`
	Repo      string    `json:"repo"`
	Number    int       `json:"number"`
	Title     string    `json:"title"`
	URL       string    `json:"url"`
	Author    string    `json:"author"`
	Labels    []Label   `json:"labels"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Issue matches components.schemas.Issue in api/openapi.yaml.

type Label added in v0.22.0

type Label struct {
	Name  string `json:"name"`
	Color string `json:"color"`
}

Label matches components.schemas.Label — a label's name and its real colour from the forge, not just the name.

type Manager added in v0.8.0

type Manager struct {
	// contains filtered or unexported fields
}

Manager owns one Aggregator per user, each refreshing itself in the background on its own schedule — the per-user equivalent of a single global Aggregator, now that each user's dashboard reflects their own saved forge credentials rather than one shared environment-configured set.

func NewManager added in v0.8.0

func NewManager(refreshInterval time.Duration) *Manager

NewManager returns a Manager whose per-user Aggregators refresh every refreshInterval.

func (*Manager) Ensure added in v0.8.0

func (m *Manager) Ensure(ctx context.Context, userID []byte, sources []Source)

Ensure (re)builds userID's Aggregator from sources and starts its background refresh, stopping whatever was running for that user before. Call it once after login (using their currently saved credentials) and again every time those credentials are saved — there's no way to add or remove a Source from a running Aggregator, so a changed configuration means a new one.

func (*Manager) EnsureIfAbsent added in v0.38.1

func (m *Manager) EnsureIfAbsent(ctx context.Context, userID []byte, sources []Source)

EnsureIfAbsent is Ensure, but only takes effect if userID has no Aggregator running yet — the check and the insert happen under the same lock, unlike a separate Running() check followed by a call to Ensure(), which races when several requests for the same user land in the same instant (several browser tabs and an SSE reconnect, all arriving right after a restart wipes the Manager clean): each would see "not running" and each would create its own Aggregator with its own immediate Refresh. Concurrent callers here still each do their own harmless work building sources beforehand; only one gets to actually create the Aggregator and kick off its first Refresh.

func (*Manager) Get added in v0.8.0

func (m *Manager) Get(userID []byte) Snapshot

Get returns userID's current snapshot — empty, not nil arrays, if Ensure was never called for them (no credentials saved yet).

func (*Manager) RefreshNow added in v0.23.0

func (m *Manager) RefreshNow(ctx context.Context, userID []byte) bool

RefreshNow fetches userID's sources immediately, out of band from their regular refreshInterval ticker — the webhook handler's entry point for turning a forge event into an up-to-date snapshot within seconds rather than waiting for the next scheduled refresh. Reports false if userID has no running Aggregator (Ensure was never called for them), rather than silently doing nothing.

func (*Manager) RefreshRepo added in v0.39.0

func (m *Manager) RefreshRepo(ctx context.Context, userID []byte, forge Forge, owner, name, fullName string) bool

RefreshRepo delegates to userID's Aggregator — see Aggregator.RefreshRepo. Reports false if userID has no running Aggregator, the same as RefreshNow, and also false wherever Aggregator.RefreshRepo itself would — a caller should fall back to RefreshNow either way, without needing to tell the two apart.

func (*Manager) Remove added in v0.9.0

func (m *Manager) Remove(userID []byte)

Remove stops userID's refresh loop and evicts their snapshot — a no-op if they had none running. Used when an admin revokes or deletes an account, so a locked-out user's background refresh doesn't keep polling either forge for no one.

func (*Manager) Running added in v0.27.1

func (m *Manager) Running(userID []byte) bool

Running reports whether userID currently has an Aggregator refreshing in the background. False right after a process restart for every user — the Manager holds no state across one — until something re-Ensures them; a caller can use this to tell "nothing saved in Settings" apart from "just needs rewarming from what's already saved."

func (*Manager) Stop added in v0.8.0

func (m *Manager) Stop()

Stop cancels every running per-user refresh loop.

func (*Manager) Subscribe added in v0.23.0

func (m *Manager) Subscribe(userID []byte) (<-chan Snapshot, func(), bool)

Subscribe delegates to userID's Aggregator — see Aggregator.Subscribe. Reports false if userID has no running Aggregator.

type PullRequest

type PullRequest struct {
	Forge     Forge     `json:"forge"`
	Repo      string    `json:"repo"`
	Number    int       `json:"number"`
	Title     string    `json:"title"`
	URL       string    `json:"url"`
	Author    string    `json:"author"`
	Draft     bool      `json:"draft"`
	Labels    []Label   `json:"labels"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
	CI        CIStatus  `json:"ci"`
}

PullRequest matches components.schemas.PullRequest in api/openapi.yaml.

type RateLimit added in v0.36.0

type RateLimit struct {
	Limit     int       `json:"limit"`
	Remaining int       `json:"remaining"`
	ResetsAt  time.Time `json:"resetsAt"`
}

RateLimit matches components.schemas.RateLimit — the forge API's own request budget for the credential the last refresh used. Only some forges expose this (GitHub does, Forgejo doesn't by default), so it's always a pointer: nil means "this forge doesn't report one," not "the budget is zero."

type RateLimiter added in v0.36.0

type RateLimiter interface {
	RateLimit(ctx context.Context) (RateLimit, error)
}

RateLimiter is implemented by a ForgeClient that can report its own API rate-limit status — GitHub can; Forgejo can't, since it has no rate limiting of its own by default. GenericSource checks for this via a type assertion rather than adding it to ForgeClient itself, so a client with nothing to report doesn't need a no-op method.

type RepoRef

type RepoRef struct {
	FullName string
	Owner    string
	Name     string
}

RepoRef names one repository a ForgeClient is configured to track — every repo the configured credential has write access to, or, with no credential at all, every public repo a configured username owns.

type RepoRefresher added in v0.38.0

type RepoRefresher interface {
	FetchRepo(ctx context.Context, owner, name, fullName string) (prs []PullRequest, issues []Issue, err error)
}

RepoRefresher is implemented by a Source that can refresh just one named repository instead of its whole configured scope — checked via a type assertion, the same optional-capability pattern RateLimiter uses, so a Source with no such shortcut doesn't need a no-op method. A webhook delivery already names the exact repo that changed; without this, the only way to pick up that change is Fetch's full scope, which for a large tracked-repo count is real, avoidable cost on every single delivery.

type Result

type Result struct {
	Health       ForgeHealth
	PullRequests []PullRequest
	Issues       []Issue
}

Result is what one forge contributes to a Snapshot. Health.Reachable is false only for a fetch that failed outright (couldn't list repositories at all); a single repository's own fetch failing is logged and skipped by the Source, not surfaced here — a good forge with one broken repo still reports Reachable: true.

type Snapshot

type Snapshot struct {
	GeneratedAt  time.Time     `json:"generatedAt"`
	Forges       []ForgeHealth `json:"forges"`
	PullRequests []PullRequest `json:"pullRequests"`
	Issues       []Issue       `json:"issues"`
}

Snapshot matches components.schemas.Dashboard — the whole body GET /api/dashboard answers with.

type Source

type Source interface {
	Fetch(ctx context.Context) Result
	// Forge reports which forge this Source drives — how RefreshRepo
	// picks the one matching a webhook delivery's own forge out of an
	// Aggregator's sources.
	Forge() Forge
}

Source is one forge's half of the aggregate. github.Source and forgejo.Source both implement it; the aggregator knows nothing about either forge beyond this.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL