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 ¶
- Variables
- type ActiveWorkerView
- type AdminBatchCoordinator
- type AdminBatchInsufficientDiskSpace
- type AdminBatchRow
- type AdminBatchSubmitResult
- type AdminBatchThroughput
- type AdminVariantDeleteRequest
- type AdminVariantDeleteResponse
- type AdminVariantDeleter
- type AutocertStatusSnapshot
- type Deps
- type Server
- type TailscaleProvider
- type TailscaleStatus
- type UPnPDiscoveredServer
- type UPnPServerAddRequest
- type UPnPServerUpdateRequest
- type UPnPUpstreamProvider
- type UPnPUpstreamServerState
- type UpdateProvider
- type UpdateStatus
- type UpscalePoolStats
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.
ErrAdminVariantDeleterUnavailable is the sentinel error the `DELETE /api/upscale/variants` handler matches against to emit 503 — wired to wrap `api.ErrVariantDeleteUnavailable` in the cmd/bridge adapter so the admin package stays decoupled from the api package's error definitions.
var ErrUPnPDuplicateUDN = errors.New("admin: upnp upstream UDN or manualDescriptionURL already configured")
ErrUPnPDuplicateUDN is returned by AddServer when the requested UDN (or ManualDescriptionURL) collides with an existing configured row. Two configured rows sharing identity would race on the same routing path namespace + manifest entries on every walk.
var ErrUPnPNoSuchServer = errors.New("admin: no such upnp upstream server")
ErrUPnPNoSuchServer is returned by ForceRescan / RemoveServer / UpdateServer when the caller passed a UDN that doesn't match any configured server.
var ErrUPnPRescanInFlight = errors.New("admin: upnp rescan already in flight")
ErrUPnPRescanInFlight is returned by ForceRescan when an ingest run is already executing — we don't overlap runs (the cmd/bridge ticker uses a single goroutine).
var ErrUPnPValidation = errors.New("admin: upnp upstream validation failed")
ErrUPnPValidation is returned by AddServer / UpdateServer when the payload fails shape validation (empty name, no UDN AND no manualURL, path-prefix shape, etc.). The handler maps to 400 Bad Request.
Functions ¶
This section is empty.
Types ¶
type ActiveWorkerView ¶ added in v0.1.7
type ActiveWorkerView struct {
WorkerID int `json:"workerId"`
Busy bool `json:"busy"`
SourceRel string `json:"sourceRel,omitempty"`
SourceSampleRate int `json:"sourceSampleRate,omitempty"`
SourceBits int `json:"sourceBits,omitempty"`
TargetSampleRate int `json:"targetSampleRate,omitempty"`
TargetBits int `json:"targetBits,omitempty"`
Quality string `json:"quality,omitempty"`
Kind string `json:"kind,omitempty"`
StartedAtUnixMs int64 `json:"startedAtUnixMs,omitempty"`
}
ActiveWorkerView is one worker's live slot in the upscale pool — the SSE worker grid on the Jobs page renders one row per entry. Mirror of transcode.ActiveJobView; cmd/bridge maps between the two so internal/admin doesn't import internal/transcode (same decoupling as the UpscaleStats closure).
type AdminBatchCoordinator ¶ added in v0.1.3
type AdminBatchCoordinator interface {
Submit(ctx context.Context, libraryRelPath string, targetRate, targetBits int) (AdminBatchSubmitResult, error)
SubmitOptimize(ctx context.Context, libraryRelPath string) (AdminBatchSubmitResult, error)
Cancel(idHex string) error
ListBatches(limit int) ([]AdminBatchRow, error)
Throughput() AdminBatchThroughput
}
AdminBatchCoordinator is the admin-side interface the Library Inspector's "Upscale this folder" button + the Jobs page consume. Implemented by an adapter in cmd/bridge/main.go around transcode.Coordinator — same UpscaleEnqueuer / UpscaleStats decoupling pattern. The methods mirror `api.BatchCoordinator` but use admin-package wire shapes (`AdminBatchRow`, etc.) so the admin package stays free of internal/api.
`Submit` takes a `context.Context` so the HTTP request's cancellation propagates through to the coordinator and any downstream listings the coordinator does internally (manifest projection walk). Per Gemini high on PR #202.
`SubmitOptimize` is the kind="optimize" sibling — it enrolls a path into a CarPlay-optimized batch (16-bit, family-preserving 44.1k/48k FLAC). The target params are auto-derived per-track (see transcode.TargetRateForOptimize) so there's no rate/bits argument; the result struct's `TargetRate`/`TargetBits` reflect the most common per-track resolution (mixed-family scopes use 0 to signal "varies"). Added in the Library Inspector tile-redesign PR alongside the per-tile "Generate CarPlay-optimized variants" affordance.
type AdminBatchInsufficientDiskSpace ¶ added in v0.1.3
type AdminBatchInsufficientDiskSpace struct {
ProjectedBytes int64
RequiredBytes int64
AvailableBytes int64
}
AdminBatchInsufficientDiskSpace is the typed-error shape returned when the coordinator's pre-flight refuses a submit.
func (*AdminBatchInsufficientDiskSpace) Error ¶ added in v0.1.3
func (e *AdminBatchInsufficientDiskSpace) Error() string
type AdminBatchRow ¶ added in v0.1.3
type AdminBatchRow struct {
ID string `json:"id"`
Path string `json:"path"`
TargetRate int `json:"targetRate"`
TargetBits int `json:"targetBits"`
Status string `json:"status"`
TotalFiles int `json:"totalFiles"`
ProcessedFiles int `json:"processedFiles"`
FailedFiles int `json:"failedFiles"`
// SkippedFiles is the count of projection-eligible tracks that
// Submit/SubmitOptimize did NOT enqueue (already at target,
// lossy, DSD, unknown format, etc.). Distinct from FailedFiles
// (per-job SoX failures during the run). The Jobs page renders
// "X tracks skipped" as a sub-line whenever this is > 0.
SkippedFiles int `json:"skippedFiles,omitempty"`
Error string `json:"error,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
AdminBatchRow is the per-row wire shape returned by the admin list endpoint (Jobs page consumer).
`CreatedAt` / `UpdatedAt` are `time.Time` (RFC 3339-encoded on the wire) rather than int64 nanoseconds. JavaScript's Number type can't safely represent 64-bit ns timestamps — values above 2^53 round, causing the Jobs page to render a date a few hundred milliseconds off. The string form parses safely via `new Date(...)`. Per Gemini high on PR #202.
type AdminBatchSubmitResult ¶ added in v0.1.3
type AdminBatchSubmitResult struct {
BatchID string `json:"batchID"`
Path string `json:"path"`
TargetRate int `json:"targetRate"`
TargetBits int `json:"targetBits"`
TotalFiles int `json:"totalFiles"`
AlreadyCovered int `json:"alreadyCovered"`
ProjectedSizeBytes int64 `json:"projectedSizeBytes"`
AvailableBytes int64 `json:"availableBytes"`
EnqueuedCount int `json:"enqueuedCount"`
}
AdminBatchSubmitResult is the wire shape returned by the admin Submit endpoint. Mirrors transcode.SubmitResult + api.BatchSubmitResult field-for-field but lives here so the admin templates can JSON-decode without importing either of those packages.
type AdminBatchThroughput ¶ added in v0.1.3
type AdminBatchThroughput struct {
JobsPerHour float64 `json:"jobsPerHour"`
EtaSeconds float64 `json:"etaSeconds"`
Samples int `json:"samples"`
}
AdminBatchThroughput is the wire shape returned by the throughput proxy. Mirrors transcode.ThroughputSnapshot.
type AdminVariantDeleteRequest ¶ added in v0.1.3
type AdminVariantDeleteRequest struct {
All bool
Prefix string
Path string
// Kind narrows the deletion to one variant kind ("upscale" /
// "optimize"); empty preserves pre-feature behaviour (deletes
// BOTH kinds matching the path scope). Wire-shape mirror of
// `api.VariantDeleteRequest.Kind`; the adapter in
// cmd/bridge/main.go translates the field across the
// admin↔api boundary. The Library Inspector's per-kind drawer
// Delete buttons set this to scope the destructive action.
Kind string
}
AdminVariantDeleteRequest is the parsed-and-validated input to the admin handler's call into the deleter. Exactly one of `All` / `Prefix` / `Path` is set on any valid request; the handler short-circuits the unscoped form behind a typed-phrase confirmation in the UI (matches the `bridge artwork --gc` `--confirm` CLI convention) plus the existing `?confirm=true` query gate for direct callers.
type AdminVariantDeleteResponse ¶ added in v0.1.3
type AdminVariantDeleteResponse struct {
DeletedCount int `json:"deletedCount"`
FreedBytes int64 `json:"freedBytes"`
DeletedPaths []string `json:"deletedPaths"`
}
AdminVariantDeleteResponse is the wire shape returned on success. Same field set as the public endpoint's response so the admin UI can render "deleted N variants, freed Y bytes" directly from this body.
type AdminVariantDeleter ¶ added in v0.1.3
type AdminVariantDeleter interface {
Delete(ctx context.Context, req AdminVariantDeleteRequest) (AdminVariantDeleteResponse, error)
}
AdminVariantDeleter is the admin-side interface the `DELETE /api/upscale/variants` handler consumes. The single `Delete` method takes a parsed admin-package request and returns a parsed admin-package response — the adapter in cmd/bridge/main.go translates to/from the `api` package's `VariantDeleteRequest` / `VariantDeleteResponse` shapes so the admin package stays free of `internal/api` (same decoupling pattern as `AdminBatchCoordinator`).
Errors:
- `ErrAdminVariantDeleterUnavailable` (sentinel) → handler emits 503 service_unavailable. Distinguishes "feature off on this bridge" from "feature on but listing failed".
- Any other error → handler emits 500 internal, with the error surface text in the response body.
type AutocertStatusSnapshot ¶ added in v0.1.4
type AutocertStatusSnapshot struct {
Domain string `json:"domain,omitempty"`
CertPresent bool `json:"certPresent"`
NotAfter time.Time `json:"notAfter,omitempty"`
LastError string `json:"lastError,omitempty"`
LastCheck time.Time `json:"lastCheck,omitempty"`
}
AutocertStatusSnapshot mirrors tlsacme.Status for the admin surface, defined here so the admin package stays decoupled from internal/tlsacme. Wired via the AutocertStatus closure.
type Deps ¶
type Deps struct {
CfgHolder *config.RuntimeConfig // live config snapshot holder
CfgPath string // path to bridge.yaml, for Config.Save()
Auth *auth.Store // token list / mint / revoke
Manifest *manifest.Store
Scanner *manifest.Scanner
Resolver *bridgefs.Resolver
// Fingerprint is the self-signed TLS cert SHA-256 in colon-hex form.
// Shown in the dashboard cert tile (the LAN pin operators verify) and
// used as the pairing-QR fallback fingerprint.
Fingerprint string
// FingerprintForHost returns the cert fingerprint a device will capture
// when it dials the given hostname (SNI), routed through the same SNI
// cert switcher the listener serves with. The pairing-QR baker uses it
// so the QR advertises the cert the device ACTUALLY sees — the public
// domain's autocert/LE fingerprint for a public dial URL, the
// self-signed LAN fingerprint otherwise. Pre-fix the QR always baked
// the self-signed fingerprint, which a public-mode device (connecting
// over the LE-served public endpoint) could never match. Wired in
// cmd/bridge/main.go to certManager.FingerprintForServerName. Nil in
// loopback wiring / tests — callers fall back to Fingerprint.
FingerprintForHost func(host string) 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
// UpscaleSoxFLAC reports whether the host sox build has FLAC
// support (hasFLAC) and whether that could be determined at all
// (known). The bridge forces `-t flac` for every conversion, so a
// sox WITHOUT FLAC passes UpscalePrecheck but fails every job at
// runtime — this lets the Settings tile warn about that narrower
// case. Wired to a transcode.ProbeSox closure in cmd/bridge/main.go
// (same decoupling as UpscalePrecheck). Nil-safe: absent → the
// FLAC field is omitted and no FLAC warning renders. known=false
// (closure absent, or `sox --help` unparseable) is treated
// conservatively as "don't assert" rather than "FLAC missing".
UpscaleSoxFLAC func() (hasFLAC, known bool)
// 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
// UpscaleBusy is a CHEAP "is the pool actively processing" probe
// (inflight or queued > 0) — atomic counters + a map-len, NO DB. The
// SSE loop uses it to gate the live worker grid onto the fast (500 ms)
// tick WHILE a batch runs, so sub-5s jobs are visible at per-second
// resolution; idle bridges fall back to the 5 s medium tick (no cost).
// Nil-safe: absent → never fast-tick the upscale frame.
UpscaleBusy func() bool
// AnalysisActive reports the LIVE runtime state of the audio-
// analysis feature — i.e. the startup-computed `analysisActive`
// (config flag AND sox-precheck outcome), NOT the persisted config
// flag. The two diverge after a restart-required PATCH: the config
// holder reflects the new value immediately, but the runtime stays
// at its startup value until restart. Wiring this lets
// /api/analysis/stats.enabled agree with /v1/health's `waveform`
// flag (also startup-wired). Nil-safe: when absent the handler
// falls back to the persisted config + sox derivation (test
// harnesses). Mirrors the intent of the upscale tile's pool-derived
// `enabled`. Wired in cmd/bridge/main.go.
AnalysisActive func() bool
// ProjectedSize estimates the on-disk size of a FLAC
// variant produced from (sourceSize, sourceRate, sourceBits)
// at (targetRate, targetBits). Wired to
// `transcode.ProjectedSize` (with `DefaultCompressionFactor`
// baked in) via a closure in cmd/bridge/main.go so the admin
// package doesn't import internal/transcode. Mirrors the
// UpscaleStats / UpscalePrecheck decoupling pattern.
//
// Nil when upscale is disabled — the projection endpoint
// surfaces a clean 503 in that case.
ProjectedSize func(sourceSize int64, sourceRate, sourceBits, targetRate, targetBits int) int64
// AvailableDiskSpace probes free bytes on the volume holding
// `dir`. Wired to `transcode.AvailableDiskSpace`. Nil-safe
// alongside ProjectedSize (both wired together when upscale
// is enabled, both nil when disabled).
AvailableDiskSpace func(dir string) (int64, error)
// OptimizeEligible is the per-track gate for kind="optimize"
// projections / batches. Wired to `transcode.OptimizeEligible`
// in cmd/bridge/main.go. Tracks failing the gate (DSD, lossy
// codecs, or already-at-CarPlay-floor like 16/44.1) fold into
// the projection's `unknownFormatFiles` counter so the UI's
// "X tracks skipped" copy reconciles with the JSON payload.
// Nil-safe: when absent the projection endpoint serves only
// the upscale kind and surfaces a 503 for kind=optimize.
OptimizeEligible func(sourcePath, codec string, sourceRate, sourceBits int) bool
// TargetRateForOptimize returns the family-preserving target
// rate (44100 or 48000) for a given source rate. Wired to
// `transcode.TargetRateForOptimize` in cmd/bridge/main.go.
// Called per-track inside the optimize projection loop so
// mixed-family folders (44.1k + 96k FLAC sharing one album)
// produce honest per-track size estimates.
// Nil-safe alongside OptimizeEligible.
TargetRateForOptimize func(sourceRate int) int
// BatchCoordinator is the v1.3 admin Library Inspector's gateway
// to the transcode.Coordinator. Wired to a closure-based adapter
// in cmd/bridge/main.go (same decoupling pattern as
// UpscaleStats / UpscaleEnqueuer). Nil-safe — when absent the
// admin Library Inspector renders the folder tree but the
// "Upscale this folder" trigger surfaces a 503.
BatchCoordinator AdminBatchCoordinator
// VariantDeleter is the admin-side gateway to the same
// list/unlink/DeleteVariant/SSE-publish pipeline that powers
// the public `DELETE /v1/upscale/variants` endpoint. Wired in
// cmd/bridge to an adapter around `api.Server.RunVariantDelete`
// so the admin console and the iOS app go through exactly one
// code path on the way out — no risk of drift between the two
// destructive surfaces. Nil-safe: when absent (upscale disabled
// on this bridge OR pre-feature build) the admin handler
// surfaces 503 service_unavailable, matching the
// `BatchCoordinator == nil` shape on the same page.
VariantDeleter AdminVariantDeleter
// 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
// AdminAuth is the admin credentials + session store. Required
// in public mode (the boundary middleware refuses to start
// without it); ignored in loopback mode where there's no auth
// layer at all. Wired in cmd/bridge/main.go.
//
// When non-nil AND IsPublic() is true, the session middleware
// is active: pages require a valid session cookie and the API
// returns JSON 401 on missing/invalid credentials. When nil,
// the historical loopback-only behaviour is preserved.
AdminAuth *adminauth.Store
// LoginLimiter is the failed-login rate limiter. Required
// alongside AdminAuth in public mode; ignored in loopback. The
// limiter's own goroutine is managed by its NewRateLimiter /
// Stop API — admin doesn't own its lifecycle.
LoginLimiter *adminauth.RateLimiter
// TLSConfig, when non-nil, makes Serve wrap the underlying
// net.Listener in tls.NewListener using this config — i.e. the
// admin console serves HTTPS instead of plain HTTP. Required
// when the bridge terminates TLS for the admin console
// itself (public mode + autocert.enabled). Nil when:
// - loopback mode (historical no-TLS contract); OR
// - public mode with AdminTLSTerminatedByProxy=true
// (reverse proxy fronts TLS, bridge serves plain HTTP on
// a private interface).
//
// Wired in cmd/bridge/main.go via certManager.AdminTLSConfig().
TLSConfig *tls.Config
// AutocertStatus surfaces a per-request snapshot of the
// autocert.Manager's live state for the dashboard tile.
// Nil-safe — when absent the tile renders "not configured"
// and /api/autocert/status returns the same disabled shape.
// Wired via a closure in cmd/bridge/main.go so this package
// doesn't import internal/tlsacme.
AutocertStatus func() AutocertStatusSnapshot
// MDNSToggle is the hot-reload callback for the mDNS
// advertiser. The Settings PATCH handler fires it after
// persisting a `mdns.enabled` change, with the new resolved
// value. main.go wires it to its mdnsLifecycle.Set; nil
// (test wiring) means the change persists but the runtime
// state doesn't flip — operator restart picks it up.
MDNSToggle func(enabled bool)
// TailscaleDisable is the hot-reload callback for the
// Tailscale auto-pilot. The Settings PATCH handler fires it
// on the any→disabled transition so the running auto-pilot
// cancels its ctx + clears the LE cert from certManager.
// Transitions INTO cli/tsnet still require restart (auto-
// pilot + listener composition need a clean boot).
TailscaleDisable func()
// UPnPUpstream is the admin-side gateway to the upstream
// MediaServer feature (bridge PR E). Wired to an adapter around
// the cmd/bridge upnpUpstreamLifecycle so the Devices page can
// surface per-server discovery state + ingest stats + a
// force-rescan button. Nil-safe — when absent (operator hasn't
// enabled `upnpUpstream.enabled` in bridge.yaml), the relevant
// admin endpoints return a stable error envelope the frontend
// uses to hide the UPnP card.
UPnPUpstream UPnPUpstreamProvider
}
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"`
// Mode is the configured tailscale.mode ("cli" / "tsnet" /
// "disabled"). Threaded into the snapshot so the admin tile can
// distinguish "operator set mode=disabled" from "mode=cli but the
// tailscale CLI isn't on this host" — the two used to collapse into
// an identical misleading "Disabled" badge because the tile read
// only runtime CLI detection. Empty only in legacy snapshots.
Mode string `json:"mode,omitempty"`
// PublicMode reports whether the bridge is in public/autocert mode,
// where the Tailscale auto-pilot doesn't apply at all. The tile
// greys out / hides itself in that case rather than showing a
// "Disabled" badge that reads as a misconfiguration.
PublicMode bool `json:"publicMode"`
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"`
// BackendState is the upstream `ipnstate.Status.BackendState`
// surfaced for consumers that need to gate on whether the embedded
// tsnet node is fully up. One of: "NoState", "NeedsLogin",
// "NeedsMachineAuth", "Stopped", "Starting", "Running". Empty when
// the source is the CLI auto-pilot (cli-mode adapter) — that path
// drives its own check via `CLIAvailable` + `MagicDNSName` for
// existing consumers; new consumers consult this explicit field.
BackendState string `json:"backendState,omitempty"`
// TailscaleIPs are the tsnet node's tailnet-assigned addresses
// (CGNAT 100.x IPv4 + ULA fd7a:115c:a1e0::/48 IPv6) as strings,
// surfaced so the api layer can advertise them in
// `/v1/health.endpoints` for iOS clients that need an IP fallback
// when MagicDNS resolution misses. Empty in cli-mode; populated
// only by the embedded-tsnet path. `omitempty` keeps the existing
// admin-tile JSON shape unchanged when the source doesn't populate.
TailscaleIPs []string `json:"tailscaleIPs,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 UPnPDiscoveredServer ¶ added in v0.1.6
type UPnPDiscoveredServer struct {
UDN string `json:"udn"`
FriendlyName string `json:"friendlyName,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
ModelName string `json:"modelName,omitempty"`
ModelDescription string `json:"modelDescription,omitempty"`
ControlURL string `json:"contentDirectoryControlURL,omitempty"`
LastSeenAt time.Time `json:"lastSeenAt"`
}
UPnPDiscoveredServer is the wire shape of one row in the "Discovered on LAN" surface. Carries enough info to pre-fill the add form when the operator clicks "Configure…" — friendlyName + manufacturer + modelName for human identification; UDN for the stored config row; controlURL is informational (the bridge derives it at runtime from SSDP, not the YAML row).
type UPnPServerAddRequest ¶ added in v0.1.6
type UPnPServerAddRequest struct {
Name string `json:"name"`
UDN string `json:"udn,omitempty"`
ManualDescriptionURL string `json:"manualDescriptionURL,omitempty"`
PathPrefix string `json:"pathPrefix,omitempty"`
RootObjectID string `json:"rootObjectID,omitempty"`
SkipTopLevelContainers []string `json:"skipTopLevelContainers,omitempty"`
}
UPnPServerAddRequest is the wire shape of POST /api/upnp/servers. Either UDN OR ManualDescriptionURL is required; supplying both is allowed (SSDP-paired manual fallback) and the handler stores both. PathPrefix defaults to a sanitized form of Name when omitted (same rule the YAML loader applies). RootObjectID defaults to "64" (the MiniDLNA Browse Folders convention).
type UPnPServerUpdateRequest ¶ added in v0.1.6
type UPnPServerUpdateRequest struct {
Name *string `json:"name,omitempty"`
PathPrefix *string `json:"pathPrefix,omitempty"`
RootObjectID *string `json:"rootObjectID,omitempty"`
SkipTopLevelContainers *[]string `json:"skipTopLevelContainers,omitempty"`
}
UPnPServerUpdateRequest is the wire shape of PATCH /api/upnp/servers/{udn}. All fields are pointers so omitted fields preserve the current value (mirrors the `apiSettingsPatch` pointer-discriminator convention). Empty-string is a valid "clear this field" value for PathPrefix / RootObjectID — the YAML loader fills the defaults.
type UPnPUpstreamProvider ¶ added in v0.1.6
type UPnPUpstreamProvider interface {
// ConfiguredServers returns the operator-configured servers
// merged with their live discovery state. Order is the YAML
// config order so the UI is deterministic.
ConfiguredServers() []UPnPUpstreamServerState
// DiscoveredServers returns SSDP-cached MediaServers that are
// NOT in the operator's configured list — i.e. candidates the
// user can one-click "Configure…" into the manifest ingest
// path. Each row carries enough info to prefill the add form
// (UDN, friendlyName, manufacturer, modelName, controlURL,
// lastSeenAt). Order is friendlyName ASC for deterministic
// rendering; ties broken by UDN.
DiscoveredServers() []UPnPDiscoveredServer
// ForceRescan triggers an immediate Ingester.Run for the matching
// UDN (or "" for all configured servers). Returns nil on success
// + a typed error otherwise; the handler maps to JSON.
ForceRescan(ctx context.Context, udn string) error
// AddServer appends a new server entry to upnpUpstream.servers and
// persists bridge.yaml via Config.Save. Returns ErrUPnPDuplicateUDN
// when the UDN OR ManualDescriptionURL collides with an existing
// configured row, ErrUPnPValidation on shape errors (missing name,
// no UDN/manualURL, etc.), or the underlying save error on
// persistence failure. The added entry's live discovery state
// surfaces via the next ConfiguredServers read; the actual ingest
// walk requires a bridge restart (caller writes
// `restartRequired: true` in the response).
AddServer(ctx context.Context, req UPnPServerAddRequest) error
// RemoveServer drops the server entry whose UDN matches and
// persists bridge.yaml. Returns ErrUPnPNoSuchServer when no row
// matches. The routed tracks the removed server contributed to
// the manifest remain until the next restart's reconcile sweep —
// caller writes `restartRequired: true`.
RemoveServer(ctx context.Context, udn string) error
// UpdateServer edits the operator-visible fields of an existing
// row (Name / PathPrefix / RootObjectID / SkipTopLevelContainers).
// UDN is identity and NOT editable. Returns ErrUPnPNoSuchServer
// when no row matches; ErrUPnPValidation on shape errors.
UpdateServer(ctx context.Context, udn string, req UPnPServerUpdateRequest) error
}
UPnPUpstreamProvider is the operator-facing interface for the upstream-MediaServer feature. Production wiring is the bridge's upnpUpstreamLifecycle; tests pass a stub. Nil-safe — when the provider isn't wired (operator hasn't enabled the feature) the admin handlers all surface 404 with a stable error code so the frontend can hide the surface cleanly.
**Read vs write semantics**: `ConfiguredServers` and `DiscoveredServers` are cheap reads safe under polling cadence. `ForceRescan` debounces concurrent calls via inFlight. The CRUD trio (`AddServer` / `RemoveServer` / `UpdateServer`) writes bridge.yaml via the same `Config.Save` path the rest of the admin PATCH surface uses; callers must surface `restartRequired: true` in the response so the operator knows to restart for the new server set to take effect (v1 matches the established Sox/DLNA/Tailscale-mode precedent).
type UPnPUpstreamServerState ¶ added in v0.1.6
type UPnPUpstreamServerState struct {
Name string `json:"name"`
ConfiguredUDN string `json:"configuredUDN,omitempty"`
ManualURL string `json:"manualDescriptionURL,omitempty"`
Discovered bool `json:"discovered"`
ResolvedUDN string `json:"resolvedUDN,omitempty"`
FriendlyName string `json:"friendlyName,omitempty"`
Manufacturer string `json:"manufacturer,omitempty"`
ControlURL string `json:"contentDirectoryControlURL,omitempty"`
LastSeenAt time.Time `json:"lastSeenAt,omitempty"`
LastWalkStarted time.Time `json:"lastWalkStartedAt,omitempty"`
LastWalkFinished time.Time `json:"lastWalkFinishedAt,omitempty"`
LastWalked int `json:"lastWalkedCount,omitempty"`
LastReaped int `json:"lastReapedCount,omitempty"`
LastWalkErr string `json:"lastWalkErr,omitempty"`
RoutedTracks int `json:"routedTracks"`
}
UPnPUpstreamServerState is one row in the per-server status surface. Wire fields mirror the operator's mental model: "did the bridge see my 2Go? did the last walk succeed? how many tracks did it ingest?"
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"`
// ActiveWorkers is the live per-worker grid (one entry per worker
// slot, Busy=false for idle). Drives the Jobs page's "Workers" panel.
// Carries StartedAtUnixMs (not a ticking elapsed) so the SSE upscale
// frame stays diff-stable while a job runs — the browser ticks the
// elapsed display locally.
ActiveWorkers []ActiveWorkerView `json:"activeWorkers,omitempty"`
}
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.
Source Files
¶
- admin.go
- handlers_api.go
- handlers_autocert.go
- handlers_backups.go
- handlers_cert.go
- handlers_covers.go
- handlers_devices.go
- handlers_events.go
- handlers_library_browse.go
- handlers_library_data.go
- handlers_library_inspector.go
- handlers_library_search.go
- handlers_login.go
- handlers_pages.go
- handlers_pairing.go
- handlers_smartplaylists.go
- handlers_tailscale.go
- handlers_token_lifecycle.go
- handlers_upnp.go
- handlers_upscale_delete.go
- handlers_variants_dir.go
- middleware_auth.go
- pairing.go