Documentation
¶
Overview ¶
Package admin serves the local-only web console for operating a running bridge instance — adding/removing library roots, pairing/revoking client devices, and surfacing scan + uptime stats.
Trust model: the admin listener binds a loopback address (default 127.0.0.1:7789). Anyone on the host already has read access to the token store and sqlite DB, so adding an auth layer on top would be theatre. Loopback binding is enforced in two places — config.validateLoopbackAddress at load time and a RemoteAddr check in the Handler as a belt-and-braces runtime guard so a future misconfiguration (e.g. forgetting to bind the listener to 127.0.0.1) still refuses LAN traffic.
Mutations (add root, pair device, revoke, settings edit) go through a single mutex on the Server so two operators hitting the UI simultaneously can't interleave a config.Save against each other. In practice the admin surface is single-user, so the mutex is a correctness guard rather than a performance concern.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrUpdateNoUpdate = errors.New("no update available") ErrUpdateActiveSessions = errors.New("active downloads in flight") ErrUpdateNotSupported = errors.New("self-install not supported on this platform") ErrUpdatePathNotWritable = errors.New("binary path not writable") )
Sentinel errors for the install / rollback paths. Defined in admin/ rather than re-exported from internal/updater so the wire shape lives entirely in this package — handlers_api.go classifies via errors.Is, not string-substring (which was the original implementation; PR #42 review flagged the fragility).
The adapter in cmd/bridge/main.go translates internal/updater's equivalent errors to these via fmt.Errorf("%w: %s", ErrXxx, ...) so the admin handler can switch on errors.Is without reaching into the updater package.
Functions ¶
This section is empty.
Types ¶
type Deps ¶
type Deps struct {
Cfg *config.Config // mutable — admin edits in place and calls Save(CfgPath)
CfgPath string // path to bridge.yaml, for Cfg.Save()
Auth *auth.Store // token list / mint / revoke
Manifest *manifest.Store
Scanner *manifest.Scanner
Resolver *bridgefs.Resolver
// Fingerprint is the TLS cert SHA-256 in colon-hex form. Shown in the
// pairing modal and the dashboard.
Fingerprint string
// StartedAt is used to render uptime. Typically time.Now().UTC() at
// the moment the serve command completes its init.
StartedAt time.Time
// Restart is called when the operator clicks "Restart now" on a
// restart-required settings edit. Nil means os.Exit(0) — fine when
// running under launchd/systemd which will relaunch.
Restart func()
// ScanCtx is the parent context for admin-triggered scans. serveCmd
// should pass the same context it passes to scanner.RunPeriodic so a
// shutdown cancels any admin-triggered scan along with the periodic
// one. Nil defaults to context.Background() — only acceptable for
// tests that don't care about goroutine cleanup.
ScanCtx context.Context
// Updater is the optional read-side of the update poller. Wired via
// an adapter in cmd/bridge/main.go so this package doesn't import
// internal/updater. Nil-safe — when absent, the dashboard's update
// tile shows "not configured" and the /api/updates endpoint
// returns the same fallback shape.
Updater UpdateProvider
// BackupSources is the resolved set of state-file paths the
// admin's "Snapshot now" button hands to `backup.Snapshot`.
// Injected from `cmd/bridge/main.go` via the same
// `buildBackupSources` helper the CLI uses, so paths can't drift
// between the two surfaces (CodeRabbit + Gemini both flagged the
// prior local helper as a divergence risk on PR #44). Zero-value
// is treated as "feature not wired" and the API endpoints return
// 503 — fine for tests that don't construct the admin server
// with backup wiring.
BackupSources backup.Sources
// Tailscale is the read+refresh side of the Tailscale HTTPS
// auto-pilot. Nil-safe — when absent, the dashboard's Tailscale
// tile shows "not configured" and the /api/tailscale endpoints
// return the same fallback shape. Wired via an adapter in
// cmd/bridge/main.go so this package doesn't import
// internal/tailscale or the cmd/bridge auto-pilot type.
Tailscale TailscaleProvider
// Pairing backs the admin-approval pairing flow. Optional — when
// nil, /api/pairing returns an empty list and the approve / decline
// handlers reply 503 (so a misconfigured deployment surfaces a
// distinct error rather than silently dropping operator clicks).
// The iOS-facing /v1/pairing/* endpoints are gated by the api
// package's own pairing wiring; both sides receive the same Store
// from cmd/bridge/main.go.
Pairing *pairing.Store
// UpscalePrecheck probes whether the upscale feature can run on
// this host (sox on PATH, --version returns within 2 s). Wired
// to `transcode.PrecheckSox` via a closure in cmd/bridge/main.go
// so this package doesn't import internal/transcode (matches the
// MBIDProbe / UpdateProvider decoupling pattern). Nil-safe — when
// absent the Settings response omits the `upscaleSoxAvailable`
// field and the UI hides the warning banner.
UpscalePrecheck func() error
// UpscaleStats returns a snapshot of the long-lived
// transcode pool's counters (workers, queue length, in-
// flight jobs, lifetime totals). Wired via a closure in
// cmd/bridge/main.go so the admin package stays decoupled
// from internal/transcode. The closure returns nil when
// the feature is off (Pool isn't instantiated); the admin
// endpoint then omits the `pool` field instead of
// surfacing zero-padded clutter ("0/0 queue, 0 inflight"
// would suggest the pool exists but is idle, which is
// semantically wrong).
UpscaleStats func() *UpscalePoolStats
// IsSupervised reports whether the current process is running
// under launchd / systemd / Windows SCM — i.e. whether
// `os.Exit(0)` will trigger an automatic relaunch. Threaded
// through to `settingsResponse.IsSupervised` so the admin UI
// can show "Restart now" (auto-relaunch promised) versus
// "Stop now (manual restart required)" (operator must run the
// service back up themselves). Wired in `cmd/bridge/main.go`
// from `supervision.IsSupervised()`. Defaults to false in
// test harnesses, which yields the conservative — never-
// promise-relaunch — UI wording, never the lying one.
IsSupervised bool
}
Deps bundles the runtime state the admin console reads and mutates. All fields are required unless marked optional.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server owns the admin listener + mux. One per process.
func New ¶
New constructs an admin Server. Call Handler to get the http.Handler for ListenAndServe, or Serve to run a background listener with graceful shutdown.
type TailscaleProvider ¶ added in v0.1.2
type TailscaleProvider interface {
Status() TailscaleStatus
RefreshNow(ctx context.Context) TailscaleStatus
}
TailscaleProvider is the read+refresh side of the Tailscale auto-pilot the admin tile reads. The adapter in cmd/bridge/main.go wraps the process-scoped autopilot so the wire shape lives entirely in this package — keeps internal/admin decoupled from cmd/bridge's implementation choices (background renewer cadence, mint trigger strings, etc.).
type TailscaleStatus ¶ added in v0.1.2
type TailscaleStatus struct {
CLIAvailable bool `json:"cliAvailable"`
NodeName string `json:"nodeName,omitempty"`
MagicDNSName string `json:"magicDNSName,omitempty"`
HTTPSCertsEnabled bool `json:"httpsCertsEnabled"`
CertPresent bool `json:"certPresent"`
CertNotAfter *time.Time `json:"certNotAfter,omitempty"`
CertPath string `json:"certPath,omitempty"`
MagicDNSURL string `json:"magicDNSURL,omitempty"`
LastError string `json:"lastError,omitempty"`
LastChecked *time.Time `json:"lastChecked,omitempty"`
}
TailscaleStatus is the JSON shape /api/tailscale/status returns. Mirrors `cmd/bridge/tailscaleStatus` but lives here so the admin package compiles without importing cmd/bridge.
Optional time fields are pointers (Qodo on PR #102): a non-pointer `time.Time` with `json:",omitempty"` still serialises the zero value `"0001-01-01T00:00:00Z"` because `omitempty` doesn't recognise time-zero. Pointer form honours `omitempty` correctly. Matches the `tokenRow.ExpiresAt *time.Time` precedent.
`MagicDNSURL` is the operator-facing bridge URL on the magic-DNS endpoint, including the configured listen port (NOT a hard-coded `:7788` — operators using non-default `cfg.ListenAddress` need the right URL surfaced for manual recovery, CodeRabbit on PR #102).
type UpdateProvider ¶ added in v0.1.1
type UpdateProvider interface {
Status() UpdateStatus
CheckNow(ctx context.Context) UpdateStatus
Install(ctx context.Context, force bool) (UpdateStatus, error)
Rollback(force bool) error
}
UpdateProvider is the read-side of the updater used by the admin console. Implemented by the adapter in cmd/bridge/main.go around internal/updater.Updater. CheckNow takes a context so a slow GitHub response can be cancelled if the operator's browser disconnects.
Install and Rollback return errors the admin handler classifies via errors.Is against the package-level sentinels below (ErrNoUpdate / ErrActiveSessions / ErrInstallNotSupported / ErrPathNotWritable). The adapter is responsible for mapping internal/updater's typed errors onto these admin-facing sentinels so this package stays decoupled from internal/updater's API.
type UpdateStatus ¶ added in v0.1.1
type UpdateStatus struct {
CurrentVersion string `json:"currentVersion"`
LatestVersion string `json:"latestVersion,omitempty"`
UpdateAvailable bool `json:"updateAvailable"`
ReleaseNotesURL string `json:"releaseNotesURL,omitempty"`
Channel string `json:"channel"`
LastCheck time.Time `json:"lastCheck,omitempty"`
LastError string `json:"lastError,omitempty"`
MinClientVersion string `json:"minClientVersion,omitempty"`
CanInstall bool `json:"canInstall"`
// DeferredReason is the most-recent gate-refusal explanation
// from the auto-installer. Empty when the previous cycle
// either installed the candidate, found no candidate, or
// hadn't yet polled. Currently the only populated reason is
// the MinClientVersion compat gate ("would orphan device(s):
// X"); future gates can extend the same field. Surfaced in
// the dashboard as a yellow "held update" card.
DeferredReason string `json:"deferredReason,omitempty"`
}
UpdateStatus is the wire shape /api/updates returns. Decoupled from internal/updater so the admin package compiles without importing it.
CanInstall is the platform-capability flag the dashboard template uses to gate the "Install & restart" button. False on Windows (and any future platform where the swap path is unimplemented) so the operator never sees a button that returns 501. The adapter in cmd/bridge/main.go fills this from runtime.GOOS at construction time — capability is fixed for the lifetime of the process.
type UpscalePoolStats ¶ added in v0.1.2
type UpscalePoolStats struct {
Workers int `json:"workers"`
QueueCap int `json:"queueCap"`
QueueLen int `json:"queueLen"`
Inflight int `json:"inflight"`
Enqueued uint64 `json:"enqueued"`
Done uint64 `json:"done"`
Failed uint64 `json:"failed"`
}
UpscalePoolStats mirrors `transcode.PoolStats` field-for- field but lives here so the admin package compiles without importing internal/transcode. The wiring closure in cmd/bridge/main.go translates between the two value types.