Help us shape the future of Go! Take the 2026 Go Developer Survey

admin

package
v0.2.12 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: GPL-3.0 Imports: 26 Imported by: 0

Documentation

Overview

Package admin is Ether's owner-facing HTTP surface: the JSON API under /ether/api/ and the embedded React SPA (setup wizard, dashboard, settings, logs, resolver traces) served under /ether/.

The package deliberately depends only on internal/config, internal/core (for types) and the small interfaces/funcs declared in Deps; internal/app wires the real implementations. The SPA is embedded in the same binary, so every endpoint it calls is always present: the component deps in Deps are required, and a handler only reaches the deps its routes use.

Mounting:

mux.Handle("/ether/", admin.New(admin.Deps{Config: store, ...}))

The handler accepts both "/ether/..." paths and paths already stripped of the prefix (http.StripPrefix style).

Logging: pass a shared ring buffer so the Logs page can tail the server:

buf := admin.NewLogBuffer(2000)
logger := slog.New(admin.TeeHandler(slog.NewTextHandler(os.Stderr, nil), buf))
h := admin.New(admin.Deps{Config: store, Logs: buf, Logger: logger, ...})

Index

Constants

View Source
const RedactedSecret = "••••"

RedactedSecret is the placeholder shown in place of a stored secret. A PUT that sends it back unchanged preserves the stored value.

Variables

This section is empty.

Functions

func DiskFree added in v0.2.4

func DiskFree(path string) (int64, error)

DiskFree reports available bytes on the filesystem containing path.

func ImageURL

func ImageURL(path string, w int) string

ImageURL is the browser-loadable proxy URL of a TMDB image path.

func New

func New(deps Deps) http.Handler

New returns the admin handler per the work-package contract.

func RedactConfig

func RedactConfig(cfg config.Config) config.Config

RedactConfig returns a copy of cfg with every secret replaced by RedactedSecret ("••••"). Empty secrets stay empty so the UI can tell "unset" from "set". The input is not modified.

func TeeHandler

func TeeHandler(next slog.Handler, buf *LogBuffer) slog.Handler

TeeHandler returns an slog.Handler that writes to next and records into buf (e.g. console + in-memory ring for the Logs page).

func UnredactIndexer

func UnredactIndexer(ix config.Indexer, prev config.Config) config.Indexer

UnredactIndexer restores an indexer's API key and secret settings that the UI sent back as placeholders, from the stored config (for tests of unsaved edits run outside this package). A placeholder that is not bound to the stored entry (see UnredactConfig) comes back empty.

Types

type Access added in v0.2.4

type Access struct {
	Owner         bool
	SetupComplete bool
}

Access is how requireAuth admitted a request: with a validated owner token, or (Owner false) through tokenless local-network setup access.

func RequestAccess added in v0.2.4

func RequestAccess(ctx context.Context) (a Access, ok bool)

RequestAccess reports how the admin API admitted the request whose context is ctx. Only requireAuth sets it, from its own checks, so extension handlers can demand more than admission (an owner token, a finished setup); ok is false for a request that did not pass through requireAuth.

type CatalogBrowser added in v0.2.9

type CatalogBrowser interface {
	BrowseTitles(context.Context, core.BrowseQuery) (core.Page, error)
}

CatalogBrowser exposes paginated local browsing without a remote TMDB search.

type CatalogHelper

type CatalogHelper interface {
	SearchTitles(ctx context.Context, q string, t core.MediaType, limit int) ([]core.Title, error)
	// Facets: genre, decade, contentRating, studio, country, language.
	Facets(ctx context.Context, t core.MediaType, facet string) ([]core.FacetValue, error)
	// Lookup kinds: person, keyword, company, network, collection, watchProvider.
	Lookup(ctx context.Context, kind, q, region string) ([]core.LookupResult, error)
	TitleDetail(ctx context.Context, key core.RatingKey) (*core.Title, error)
}

CatalogHelper backs /ether/api/catalog/*. Implemented by internal/collections.

type CollectionStudio

type CollectionStudio interface {
	ListSpecs(ctx context.Context) ([]core.CollectionSpec, error)
	GetSpec(ctx context.Context, id string) (*core.CollectionSpec, error)
	CreateSpec(ctx context.Context, spec core.CollectionSpec) (*core.CollectionSpec, error)
	UpdateSpec(ctx context.Context, id string, spec core.CollectionSpec) (*core.CollectionSpec, error)
	DeleteSpec(ctx context.Context, id string) error
	Preview(ctx context.Context, spec core.CollectionSpec, n int) (*core.CollectionPreview, error)
	Rebuild(ctx context.Context, id string) (*core.CollectionSpec, error)
	Templates() []core.CollectionTemplate
	Generate(ctx context.Context, prompt, mediaType string) (*core.CollectionSpec, string, error)
	// Artwork returns the collection's poster at width w.
	Artwork(ctx context.Context, id string, w int) ([]byte, string, time.Time, error)
}

CollectionStudio backs /ether/api/collections*. Implemented by internal/collections.

Errors are mapped by behaviour, so implementations need not import this package: core.ErrNotFound → 404; an error with InvalidFields() map[string]string → 400 with per-field messages; NotConfigured() bool (the owner has not enabled the LLM that Generate drafts with) → 501; Conflict() bool → 409; Upstream() bool → 502.

type Deps

type Deps struct {
	// Extensions are owner-authenticated API subtrees keyed by relative path.
	Extensions    map[string]http.Handler
	ValidateExtra func(config.Config) []FieldError
	// Config is the owner settings store. Required.
	Config *config.Store
	// Logger defaults to slog.Default().
	Logger *slog.Logger
	// Logs is the in-memory log ring served at /ether/api/logs. A fresh
	// 2000-entry buffer is created when nil (retrieve it via
	// Handler.Logs()); pass a shared one to tail server logs.
	Logs *LogBuffer
	// Version is reported by /ether/api/bootstrap ("dev" when empty).
	Version string

	// PlexAuth drives wizard step 1 (plex.tv PIN sign-in and claim).
	PlexAuth PlexAuth

	// SignIn powers "Sign in with Plex" on the admin login screen.
	SignIn SignIn

	// IsOwnerToken reports whether a bearer token belongs to the owner.
	IsOwnerToken func(ctx context.Context, token string) bool

	// Provider/integration testers used by the wizard and settings pages.
	TestUsenet        func(ctx context.Context, p config.UsenetProvider) error
	TestIndexer       func(ctx context.Context, ix config.Indexer) error
	TestDebrid        func(ctx context.Context, p config.DebridProvider) error
	TestTMDB          func(ctx context.Context, key string) error
	TestOpenSubtitles func(ctx context.Context, cfg config.OpenSubtitles) error
	TestLLM           func(ctx context.Context, cfg config.LLM) error
	// LLMModels lists the models the provider in cfg offers.
	LLMModels func(ctx context.Context, cfg config.LLM) ([]LLMModel, error)
	TestTrakt func(ctx context.Context, cfg config.Trakt) error

	// Dashboard status providers. Each non-nil provider appears on the
	// dashboard and at /ether/api/status/{name}.
	SessionsStatus  StatusFunc // "sessions" — now playing
	CacheStatus     StatusFunc // "cache" — usage; {used,budget} renders as a bar
	ProvidersStatus StatusFunc // "providers" — Usenet provider health
	IndexersStatus  StatusFunc // "indexers" — budgets remaining
	CatalogStatus   StatusFunc // "catalog" — catalogue sync/stats
	RemoteStatus    StatusFunc // "remote" — remote access
	HWAccelStatus   StatusFunc // "hwaccel" — transcoding hardware
	// SystemLoad is this machine's recent CPU, memory, network and GPU
	// load, served on its own route because only the Overview polls it.
	SystemLoad StatusFunc
	// ExtraStatus adds/overrides named status providers generically. The
	// "widget" entry is required: it backs GET /ether/api/status.
	ExtraStatus map[string]StatusFunc

	// ResolverTraces backs GET /ether/api/traces.
	ResolverTraces StatusFuncE

	// Users backs the user-administration and viewer endpoints.
	Users UserStore

	// ViewerDirectory lists stored identities for owner administration, including
	// imported or inactive viewers. It must never be used for authentication.
	// Nil preserves the active-user fallback for smaller integrations.
	ViewerDirectory func(context.Context) ([]ViewerRecord, error)

	// TestTimeout bounds each wizard test call (default 20s).
	TestTimeout time.Duration
	// StatusTimeout bounds each status provider call (default 5s).
	StatusTimeout time.Duration

	// DiskUsage overrides free-space reporting (tests). Nil → OS statfs.
	DiskUsage func(path string) (total, free int64, err error)

	// Personalization backs /api/viewers/*, /api/layout and
	// /api/insights/* (internal/personalize.Engine). Viewers are listed
	// from Users.
	Personalization Personalization

	// Collections backs /api/collections* (Collection Studio).
	Collections CollectionStudio
	// CatalogHelper backs /api/catalog/*.
	CatalogHelper     CatalogHelper
	CatalogVisibility func(context.Context, []core.LibraryExclusion) (any, error)
	// Images backs the TMDB image proxy at /api/image.
	Images ImageProxy
}

Deps are the dependencies of the admin handler. The component dependencies (Plex sign-in, testers, traces, users, personalization, collections, catalogue helper, image proxy) are required. Only the tuning knobs, Logger/Logs, the dashboard status providers, Extensions, ValidateExtra and DiskUsage have defaults or may be left empty.

type FieldError

type FieldError struct {
	Field   string `json:"field"`
	Message string `json:"message"`
}

FieldError is one validation problem, addressed by dotted field path.

func UnredactConfig

func UnredactConfig(next, prev config.Config) (config.Config, []FieldError)

UnredactConfig restores secrets in next that the client sent back as the redaction placeholder, using prev (the currently stored config). List entries are matched by stable ID only, so reordering a list does not swap secrets. A placeholder whose entry is new, has no ID, or now points somewhere else is not restored: it is reported as a FieldError and left empty.

func ValidateConfig

func ValidateConfig(cfg config.Config) []FieldError

ValidateConfig checks a candidate config for the problems the owner can fix in the UI. It is intentionally permissive about empty optional sections (an unconfigured install must remain saveable mid-wizard) and strict about structurally broken values.

func (FieldError) Error

func (e FieldError) Error() string

type Handler

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

Handler is the admin HTTP handler. Use NewHandler when you need access to the log buffer the handler created.

func NewHandler

func NewHandler(deps Deps) *Handler

NewHandler builds the admin handler. It panics when Deps.Config is nil.

func (*Handler) Logs

func (h *Handler) Logs() *LogBuffer

Logs returns the log ring served by this handler.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler.

type ImageProxy

type ImageProxy interface {
	Get(ctx context.Context, path string, w int) ([]byte, string, error)
}

ImageProxy serves TMDB artwork by path at the TMDB size covering w. Implemented by internal/artwork. It must reject anything that is not a TMDB image path.

type LLMModel added in v0.2.2

type LLMModel struct {
	ID    string `json:"id"`
	Label string `json:"label,omitempty"`
}

LLMModel is one model a language-model provider offers.

type LogBuffer

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

LogBuffer is a concurrency-safe ring of recent log records. It backs the Logs page; install its Handler (usually teed with the console handler via TeeHandler) as the server logger.

func NewLogBuffer

func NewLogBuffer(capacity int) *LogBuffer

NewLogBuffer creates a ring holding at most capacity entries (default 2000 when capacity <= 0).

func (*LogBuffer) Add

func (b *LogBuffer) Add(e LogEntry)

Add appends an entry, evicting the oldest when full.

func (*LogBuffer) Clear

func (b *LogBuffer) Clear()

Clear drops all entries.

func (*LogBuffer) Entries

func (b *LogBuffer) Entries(limit int, level, q string) []LogEntry

Entries returns up to limit entries, oldest first. level filters to that level or more severe ("info" includes warn+error); q is a case-insensitive substring over message and attrs. Empty filters match all.

func (*LogBuffer) Handler

func (b *LogBuffer) Handler() slog.Handler

Handler returns an slog.Handler that records into the buffer at slog.LevelDebug and above.

func (*LogBuffer) Len

func (b *LogBuffer) Len() int

Len reports how many entries are retained.

type LogEntry

type LogEntry struct {
	Time    time.Time         `json:"time"`
	Level   string            `json:"level"`
	Message string            `json:"message"`
	Attrs   map[string]string `json:"attrs,omitempty"`
}

LogEntry is one captured log record.

type Personalization

type Personalization interface {
	ViewerStats(ctx context.Context, userIDs []int64) (map[int64]core.ViewerStats, error)
	RecentActivity(ctx context.Context, userID int64, limit int) ([]core.ViewerActivity, error)
	Taste(ctx context.Context, user *core.User) (*core.TasteProfile, error)
	SetTasteOverrides(ctx context.Context, user *core.User, overrides []core.TasteOverride) (*core.TasteProfile, error)
	// PreviewHome is exactly what /hubs (or /hubs/sections/{id} when
	// section is "movies", "shows" or a section id) serves the viewer in
	// the context.
	PreviewHome(ctx context.Context, user *core.User, hc core.HubContext, section string, limit int) (*core.HomePreview, error)
	Refresh(ctx context.Context, user *core.User) error
	// HomeLayout is the saved Home layout document; SetHomeLayout
	// validates and replaces it, applying from the next /hubs request.
	HomeLayout(ctx context.Context) (core.HomeLayout, error)
	SetHomeLayout(ctx context.Context, d core.HomeLayout) (core.HomeLayout, error)
	// PreviewLayout assembles what the viewer would see under a draft
	// document without saving it.
	PreviewLayout(ctx context.Context, user *core.User, hc core.HubContext, section string, limit int, d core.HomeLayout) (*core.HomePreview, error)
	// LayoutDefaults is the default row count per section (home, movies,
	// shows).
	LayoutDefaults() map[string]int
	AvailableRows(ctx context.Context) ([]core.RowDescriptor, error)
	RowInsights(ctx context.Context, days int) ([]core.RowInsight, error)
	// RankerInsight describes the learned ranking model.
	RankerInsight(ctx context.Context) (core.RankerInsight, error)
	Activity(ctx context.Context, days int) ([]core.ActivityDay, error)
}

Personalization is the engine behind the viewers, Home Designer and insights endpoints. Implemented by internal/personalize.Engine.

type PlexAuth

type PlexAuth interface {
	// StartPIN begins the PIN flow: id is the poll handle, code is the
	// 4-character code shown to the owner, authURL is the plex.tv page to open.
	StartPIN(ctx context.Context) (id, code, authURL string, err error)
	// PollPIN reports whether the owner has approved the PIN yet. When done
	// is true the implementation has stored the owner token in config.
	PollPIN(ctx context.Context, id string) (done bool, err error)
	// Claim claims this server to the signed-in account.
	Claim(ctx context.Context) error
	// Status returns a JSON-able snapshot (claimed, owner, connections...).
	Status() any
}

PlexAuth is the plex.tv sign-in/claim surface the wizard needs. Implemented by internal/plextv.

type SetupStep

type SetupStep struct {
	ID       string `json:"id"`
	Title    string `json:"title"`
	Complete bool   `json:"complete"`
	Required bool   `json:"required"`
	Detail   string `json:"detail,omitempty"`
}

SetupStep is one wizard step's completion state.

func SetupStatus

func SetupStatus(cfg config.Config) (steps []SetupStep, complete bool)

SetupStatus derives wizard progress from the stored config.

type SignIn

type SignIn interface {
	// StartPIN creates a PIN; forwardURL is where plex.tv sends the browser
	// after approval.
	StartPIN(ctx context.Context, forwardURL string) (id, code, authURL string, err error)
	// PINToken returns the account token once approved, "" while pending.
	PINToken(ctx context.Context, id string) (string, error)
}

SignIn is a plex.tv PIN sign-in that, unlike PlexAuth, changes nothing on the server: it only yields the signed-in account's token so the admin UI can authenticate as that account.

type StatusFunc

type StatusFunc func(ctx context.Context) any

StatusFunc returns a JSON-able status blob for a dashboard card. It may return (any, error) shaped values via StatusFuncE; StatusFunc never fails.

type StatusFuncE

type StatusFuncE func(ctx context.Context) (any, error)

StatusFuncE is a StatusFunc that can fail.

type TitleCard

type TitleCard struct {
	Key            string   `json:"key"`
	Type           string   `json:"type"`
	Title          string   `json:"title"`
	Year           int      `json:"year,omitempty"`
	Summary        string   `json:"summary,omitempty"`
	Poster         string   `json:"poster,omitempty"`
	Backdrop       string   `json:"backdrop,omitempty"`
	Rating         float64  `json:"rating,omitempty"`
	ContentRating  string   `json:"contentRating,omitempty"`
	RuntimeMinutes int      `json:"runtimeMinutes,omitempty"`
	Genres         []string `json:"genres,omitempty"`
	ShowTitle      string   `json:"showTitle,omitempty"`
	Season         int      `json:"season,omitempty"`
	Episode        int      `json:"episode,omitempty"`
}

TitleCard is the shared title shape of the owner API.

func CardFor

func CardFor(t core.Title) TitleCard

CardFor converts a catalogue title to a TitleCard.

type UserStore

type UserStore interface {
	Users(ctx context.Context) ([]core.User, error)
	UpdateUser(ctx context.Context, u core.User) error
}

UserStore is the user-administration surface (satisfied by core.Auth implementations; only the two admin methods are required here).

type ViewerRecord added in v0.2.9

type ViewerRecord struct {
	core.User
	Active bool
	Origin string
}

ViewerRecord is a stored identity plus read-only Plex membership metadata. Imported history does not grant server access.

Jump to

Keyboard shortcuts

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