Documentation
¶
Overview ¶
Package api implements the HTTP/2 handlers for the v1 wire protocol (see PROTOCOL.md at the repo root).
v1 endpoints exposed by this package:
GET /v1/health — no auth, liveness / pairing probe
GET /v1/list — directory listing (authed)
GET /v1/stat — file metadata (authed)
GET /v1/read — byte-range read; Range header required (authed)
GET /v1/download — full-file streaming download (authed)
GET /v1/manifest — library track manifest (authed)
GET /v1/artwork/{mbid} — album artwork blob (authed)
GET /v1/artist-image/{mbid} — artist portrait by MusicBrainz ID (authed)
Pairing itself is handled by the admin console (see internal/admin), not by a /v1 endpoint — the iOS client posts the bearer token and pinned fingerprint to the admin host once, then talks to /v1 only.
Every response carries the X-Bridge-Protocol header. Authenticated endpoints run through the authed() middleware, which requires Authorization: Bearer <token> and validates via the auth.Store.
Index ¶
- Variables
- type ArtworkDirProvider
- type Entry
- type ErrorResponse
- type EventPublisher
- type HealthResponse
- type MBIDProbe
- type ManifestPage
- type ManifestProvider
- type PairingStateEvent
- type ScanState
- type Server
- func (s *Server) EventPublisher() EventPublisher
- func (s *Server) Handler() http.Handler
- func (s *Server) Resolver() *bridgefs.Resolver
- func (s *Server) StartEventBroker() (stopFn func())
- func (s *Server) StartPairingRateLimitGC() (stopFn func())
- func (s *Server) StartedAt() time.Time
- func (s *Server) WithArtworkDirs(ad ArtworkDirProvider) *Server
- func (s *Server) WithCertExpiry(notAfter time.Time) *Server
- func (s *Server) WithMBIDProbe(p MBIDProbe) *Server
- func (s *Server) WithPairing(p *pairing.Store) *Server
- func (s *Server) WithSessionTracker(t SessionTracker) *Server
- func (s *Server) WithUpdater(u UpdaterStatus) *Server
- func (s *Server) WithUpscale(enabled bool, vs VariantStore) *Server
- func (s *Server) WithUpscaleEnqueuer(e UpscaleEnqueuer) *Server
- func (s *Server) WithUpscaleStats(p UpscaleStatsProvider) *Server
- type SessionTracker
- type StatResponse
- type UpdateInfo
- type UpdaterStatus
- type UpscaleEnqueuer
- type UpscalePoolStats
- type UpscaleRequest
- type UpscaleResponse
- type UpscaleStats
- type UpscaleStatsProvider
- type VariantRecord
- type VariantStore
Constants ¶
This section is empty.
Variables ¶
var Copy = io.Copy
Copy is exposed for test shims; wraps io.Copy so tests can swap it out. (Currently unused — kept as a seam for benchmarks.)
var ErrUpscaleIneligible = errors.New("upscale source is ineligible")
ErrUpscaleIneligible is returned by EnqueueOne when the source is DSD, already above the target rate, or otherwise not a candidate for upscaling. Silent rejection — the user has no remediation, so we don't surface it in the wire response beyond the rejected count.
var ErrUpscaleQueueFull = errors.New("upscale queue is full")
ErrUpscaleQueueFull is the typed sentinel UpscaleEnqueuer returns when the underlying worker pool can't accept another job. The HTTP handler maps it to a 503 `queue_full` response. Defined in the api package so the handler can `errors.Is` without importing internal/transcode (which would invert the dependency direction — api defines abstractions, transcode is an implementation detail consumed via the adapter at cmd/bridge wiring).
var ErrUpscaleSourceMissing = errors.New("upscale source path missing on disk")
ErrUpscaleSourceMissing is returned by EnqueueOne when the resolved source path doesn't exist on disk. Distinguished from ErrUpscaleQueueFull so the handler can report it separately in `rejected` counts and so tests can assert the typed branch.
Functions ¶
This section is empty.
Types ¶
type ArtworkDirProvider ¶
type ArtworkDirProvider interface {
ArtworkCacheDir() string
}
ArtworkDirProvider is the minimal interface api needs to serve cached artwork. Implemented by cmd/bridge's serveCmd (via the Enricher's CacheDir). Split out so api tests don't have to import internal/enrich.
type Entry ¶
type Entry struct {
Name string `json:"name"`
Path string `json:"path"`
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModTime time.Time `json:"mtime"`
}
Entry is the JSON shape of a single directory entry returned by /v1/list. The path field is library-root-relative, not the server's absolute path.
type ErrorResponse ¶
ErrorResponse matches the shape documented in PROTOCOL.md ("short-code" + human detail). Keep this struct in sync with the error table there.
type EventPublisher ¶ added in v0.1.2
type EventPublisher interface {
Publish(topic string, payload interface{})
}
EventPublisher is the interface upstream services (transcode pool, pairing store) consume to publish events without taking a hard dependency on the api package. Wired at cmd/bridge boundary.
type HealthResponse ¶
type HealthResponse struct {
ProtocolVersion int `json:"protocolVersion"`
ServerVersion string `json:"serverVersion"`
LibraryName string `json:"libraryName"`
LibraryRoots []string `json:"libraryRoots"`
CertFingerprint string `json:"certFingerprint"`
StartedAt time.Time `json:"startedAt"`
ScanState ScanState `json:"scanState"`
Endpoints []string `json:"endpoints,omitempty"`
LatestServerVersion string `json:"latestServerVersion,omitempty"`
UpdateAvailable bool `json:"updateAvailable,omitempty"`
UpdateReleaseNotesURL string `json:"updateReleaseNotesURL,omitempty"`
MinClientVersion string `json:"minClientVersion,omitempty"`
// UpscaleEnabled mirrors the runtime config flag
// `cfg.Upscale.Enabled` AND the result of the sox-on-PATH
// startup probe (a true config setting whose probe failed
// reports false here — see cmd/bridge/main.go's serve-side
// degradation). iOS uses this to gate every variant-related
// UI surface for that bridge: when false, the picker / glyph
// / context menu items are all hidden so SMB and bridge rows
// look identical. Pre-v1.2 servers omit the field entirely;
// iOS treats absence as `false` (no variant chrome). Pointer
// type so a true `false` value distinguishes "feature
// supported but disabled" from "no field on the wire".
UpscaleEnabled *bool `json:"upscaleEnabled,omitempty"`
// CertNotAfter is the on-disk TLS certificate's `NotAfter` (UTC).
// Lets iOS surface a "Bridge cert expires in X days — re-pair to
// refresh" warning before the cert actually expires and TLS
// handshakes start failing at Apple's ATS layer (Apple's 397-day
// cap means operators must re-pair roughly annually). Additive
// field; pre-bridge-with-this-PR servers omit it and iOS treats
// absence as "no expiry info, never warn". Pointer (not bare
// `time.Time`) because Go's `omitempty` doesn't treat the zero
// time as empty — and emitting `0001-01-01T00:00:00Z` from a
// test harness or a parse failure would actively confuse clients.
CertNotAfter *time.Time `json:"certNotAfter,omitempty"`
// Features advertises capability flags the server supports.
// Additive over the wire (omitempty); iOS consults this list to
// skip belt-and-braces recovery paths when the server already
// provides the underlying guarantee. Stable string keys, never
// repurpose. Pre-feature-flag bridges omit the field entirely;
// iOS treats absence as "feature absent" so any client-side
// recovery path runs unconditionally on those bridges.
//
// Current keys:
// - "variantBumpsIndex": UpsertVariant / DeleteVariant bump
// `tracks.indexed_at` for the parent row, so iOS delta-sync
// surfaces variant changes without needing a full rescan.
// iOS gates its +600s "silent fullRescan recovery" rung on
// absence of this flag.
Features []string `json:"features,omitempty"`
}
HealthResponse is the /v1/health JSON body. Field ordering must stay stable because the iOS side golden-decodes this shape (see PROTOCOL.md).
`Endpoints` is the additive v1 extension that lets iOS self-discover LAN ↔ Tailscale alternates without re-pairing. Empty when the bridge can't enumerate interfaces (e.g. test harness) — iOS falls back to the URL it was paired on. Adding fields here is backwards-compatible: the iOS decoder uses Codable with default values for anything it doesn't know about.
The four `*Update*` / `MinClientVersion` fields are the Phase A additive extension (see PROTOCOL.md "Updates" section). They are only populated when an UpdaterStatus has been wired via WithUpdater AND the updater has at least one successful poll cached; absent otherwise so the wire shape stays identical to the pre-update response on a bridge that doesn't yet have an updater. iOS treats all four as optional and falls back to "no update info" rather than a hard error if any are missing.
type MBIDProbe ¶ added in v0.1.1
type MBIDProbe interface {
HasTrackWithArtworkMBID(mbid string) bool
HasTrackWithArtistMBID(mbid string) bool
}
MBIDProbe is an optional interface the artwork + artist-image handlers use to distinguish "MBID known to the server but not cached yet" (return 202 + Retry-After so iOS retries) from "genuinely unknown MBID" (return 404 so iOS treats as terminal).
Nil-safe — when `s.mbidProbe` is nil the handlers fall back to the pre-v1.1 behaviour of 404-on-miss. `internal/manifest.Provider` satisfies this interface in production.
type ManifestPage ¶ added in v0.1.2
ManifestPage is the typed JSON-serializable shape returned by `BuildManifestPage`. Aliased to `manifest.Manifest` so the wire shape stays defined in exactly one place; the alias keeps the `api` package's interface contract type-safe without forcing callers to learn about `internal/manifest`.
The `internal/api → internal/manifest` import is unidirectional: manifest never imports api (the existing `VariantLookup` / `VariantRecord` mirror pattern at api.go:105 / provider.go:26 is what defends the OTHER direction; `Manifest` doesn't need a mirror because nothing in manifest reaches into api for it).
type ManifestProvider ¶
type ManifestProvider interface {
// WriteManifest streams the legacy non-paginated manifest as JSON
// straight to w. Bounding peak memory is mandatory: the prior
// in-memory builder OOM-killed Pi-class hosts on 50k-track
// libraries. Mid-stream errors are returned but unrecoverable on
// the wire (headers and prefix already sent); the handler logs and
// the truncated body fails iOS-side decode, which retries.
//
// `ctx` is checked inside the per-row stream loop so a client
// disconnect mid-response (slow network, iOS app backgrounded)
// terminates the SQLite scan instead of running it to EOF holding
// connection resources.
WriteManifest(ctx context.Context, w io.Writer, since time.Time) error
// BuildManifestPage is the v1.1 paginated-manifest variant used
// when the client asks for `?limit=`. `cursor=""` requests the
// first page. Callers iterate until the returned page's
// `NextCursor` is nil. Returns a fully-materialised value because
// per-page output is bounded by the page-size cap (5000) and the
// JSON writer can buffer the whole page without OOM risk.
BuildManifestPage(cursor string, limit int) (*ManifestPage, error)
IsScanning() bool
LastFullScan() time.Time
TracksIndexed() int
}
ManifestProvider is the interface /v1/manifest and /v1/health use to read the indexed library state. internal/manifest implements it via the Scanner + Store pair; tests can pass a small stub.
type PairingStateEvent ¶ added in v0.1.2
type PairingStateEvent struct {
Status string `json:"status"`
TTLSecondsRemaining int `json:"ttlSecondsRemaining"`
BridgeStartedAt int64 `json:"bridgeStartedAt"`
VerificationCode string `json:"verificationCode,omitempty"`
Token string `json:"token,omitempty"`
TokenID string `json:"tokenId,omitempty"`
}
PairingStateEvent is the wire shape the bridge publishes on the SSE `pairing.<requestID>` topic AND returns from `GET /v1/pairing/{id}`. Single source of truth — iOS reuses one decoder across both transports.
JSON tags match the polling contract; do NOT rename without bumping `ProtocolVersion` and a Mirror-PR pair on the iOS side.
type ScanState ¶
type ScanState struct {
IsScanning bool `json:"isScanning"`
LastFullScan time.Time `json:"lastFullScan,omitempty"`
TracksIndexed int `json:"tracksIndexed"`
}
ScanState reports the scanner's current status. Real fields populate once the manifest package lands; the shape is stubbed in for iOS decoder stability.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server owns the http.Handler and the per-request state it needs.
func New ¶
New constructs a Server. fingerprint is the SHA-256 of the TLS cert, used for display in /v1/health (iOS pins by this value). mp can be nil during early boot / tests — /v1/manifest will return 503 until it's populated.
func (*Server) EventPublisher ¶ added in v0.1.2
func (s *Server) EventPublisher() EventPublisher
EventPublisher returns the broker as the Server-facing `EventPublisher` interface so upstream services (transcode pool, pairing store) can publish events without taking a hard dependency on the api package. When the broker isn't wired (test harnesses, pre-this-PR bridges) the returned publisher silently drops publishes — same back-compat shape every upstream path already handles.
func (*Server) Handler ¶
Handler returns the root http.Handler, pre-wrapped with the X-Bridge-Protocol middleware.
func (*Server) Resolver ¶
Resolver returns the internal path Resolver so the admin console can call SetRoots when adding or removing a library root at runtime. The api handlers and admin handlers deliberately share the same Resolver instance — otherwise a hot add/remove would update one without the other and /v1/list would serve stale top-level entries.
func (*Server) StartEventBroker ¶ added in v0.1.2
func (s *Server) StartEventBroker() (stopFn func())
StartEventBroker spins up the in-process event-bus goroutine that backs GET /v1/events. Same pattern as `StartPairingRateLimitGC` — returns a stopFn that drains the broker on shutdown. Wired from cmd/bridge after `apiSrv := api.New(...)`.
func (*Server) StartPairingRateLimitGC ¶ added in v0.1.2
func (s *Server) StartPairingRateLimitGC() (stopFn func())
StartPairingRateLimitGC kicks off the per-IP rate-limiter's background sweep on a hidden goroutine. Called once from `cmd/bridge` after `apiSrv := api.New(...).WithPairing(...)`. The returned func, called on shutdown, signals the goroutine to exit cleanly. Tests don't need to call this — the GC interval is hours, so leaving it idle is harmless, and table-driven tests construct fresh limiters via `New` anyway.
func (*Server) StartedAt ¶ added in v0.1.2
StartedAt returns the server's construction timestamp. Used by the SSE pairing-event publisher in cmd/bridge to populate the `bridgeStartedAt` field, matching the polling endpoint's contract: iOS observes a value change between events and treats it as "bridge restarted, abandon this in-flight pairing request."
func (*Server) WithArtworkDirs ¶
func (s *Server) WithArtworkDirs(ad ArtworkDirProvider) *Server
WithArtworkDirs attaches an artwork cache dir provider. Separate from New to avoid churning every call site when optional features are added.
func (*Server) WithCertExpiry ¶ added in v0.1.2
WithCertExpiry attaches the on-disk TLS cert's NotAfter date so /v1/health can surface it to iOS. Wired from cmd/bridge after `tls.Inspect(certPath)`. Test harnesses can omit this; the HealthResponse field is `omitempty` (pointer) so the zero-time stays off the wire.
func (*Server) WithMBIDProbe ¶ added in v0.1.1
WithMBIDProbe attaches an MBIDProbe so the artwork / artist-image handlers can return 202 + Retry-After for known-but-uncached MBIDs instead of a flat 404. Optional — omitting it preserves v1.0 behaviour (404 on every cache miss). `internal/manifest.Provider` satisfies the interface in production.
func (*Server) WithPairing ¶ added in v0.1.2
WithPairing attaches the in-memory pairing.Store that backs the admin-approval pairing flow (POST /v1/pairing/requests, GET/DELETE /v1/pairing/{id}). Optional — when nil the routes return 404 `pairing_not_supported` and iOS falls back to manual token entry. Older bridges that don't ship this package keep the same wire behaviour for free (404 from the unregistered route).
func (*Server) WithSessionTracker ¶ added in v0.1.1
func (s *Server) WithSessionTracker(t SessionTracker) *Server
WithSessionTracker attaches the inflight-download counter the updater consults before swapping the binary and restarting. Optional — omitting it disables the gate entirely (tests). The counter is incremented around `serveFile` so /v1/read and /v1/download both count.
func (*Server) WithUpdater ¶ added in v0.1.1
func (s *Server) WithUpdater(u UpdaterStatus) *Server
WithUpdater attaches an UpdaterStatus so /v1/health can advertise the latest available bridge release to iOS clients. Optional — omitting it preserves the pre-Phase-A wire shape (the four new `omitempty` fields stay absent from the JSON response).
func (*Server) WithUpscale ¶ added in v0.1.2
func (s *Server) WithUpscale(enabled bool, vs VariantStore) *Server
WithUpscale wires the v1.2 PCM-upscaling feature into the server. `enabled` mirrors `cfg.Upscale.Enabled` AND the result of the cmd/bridge sox-on-PATH startup probe (a true config setting whose probe failed lands here as false — graceful degradation, the rest of the server keeps running). `vs` may be nil when enabled=false; when enabled=true it MUST be the VariantStore implementation that knows where the sidecars live.
Effect on the wire:
- /v1/health reports `upscaleEnabled` matching `enabled`.
- /v1/manifest emits per-Track `variants` slices iff `enabled` (cleared in the manifest provider otherwise).
- /v1/download honors `?variant=<id>` iff `enabled` AND `vs != nil`; 404 `variant_not_found` otherwise.
func (*Server) WithUpscaleEnqueuer ¶ added in v0.1.2
func (s *Server) WithUpscaleEnqueuer(e UpscaleEnqueuer) *Server
WithUpscaleEnqueuer attaches the long-lived transcode worker pool's job-submit interface so the v1.2 `POST /v1/upscale` endpoint can hand off track / folder requests. Optional — when nil the endpoint returns `503 upscale_disabled`.
Wired in cmd/bridge serve startup IFF `cfg.Upscale.Enabled && sox-on-PATH probe passed`. The adapter at the wiring point translates `transcode.ErrQueueFull` into the api package's `ErrUpscaleQueueFull` sentinel so the handler can map cleanly to the wire response.
func (*Server) WithUpscaleStats ¶ added in v0.1.2
func (s *Server) WithUpscaleStats(p UpscaleStatsProvider) *Server
WithUpscaleStats attaches the snapshot provider for GET /v1/upscale/stats. Optional — when nil the endpoint returns the zero-value UpscaleStats (`enabled=false`, no pool, no sox availability), which iOS treats as "feature off" without distinguishing missing-endpoint from disabled-feature. Lets older bridges expose the route without the wiring overhead.
Wired in cmd/bridge serve startup with the same closure the admin `/api/upscale/stats` tile already consumes — the two surfaces stay in lockstep so the admin operator and the paired iOS client see the same numbers.
type SessionTracker ¶ added in v0.1.1
type SessionTracker interface {
Begin()
End()
}
SessionTracker is the optional interface serveFile uses to record active file-serving requests so the updater can refuse to swap- and-restart while a download is in flight. Nil-safe — when `s.sessions` is nil serveFile skips the bookkeeping (test harnesses, pre-Phase-B bridges).
`internal/updater.Tracker` satisfies this interface in production.
type StatResponse ¶
type StatResponse struct {
IsDir bool `json:"isDir"`
Size int64 `json:"size"`
ModTime time.Time `json:"mtime"`
}
StatResponse is the JSON shape returned by /v1/stat.
type UpdateInfo ¶ added in v0.1.1
type UpdateInfo struct {
LatestVersion string
UpdateAvailable bool
ReleaseNotesURL string
MinClientVersion string
}
UpdateInfo is the wire shape /v1/health embeds. Lives in this package (not internal/updater) so the iOS-facing fields don't import the updater type machinery and tests can hand-craft an UpdaterStatus stub.
type UpdaterStatus ¶ added in v0.1.1
type UpdaterStatus interface {
UpdateInfo() UpdateInfo
}
UpdaterStatus is the optional interface the /v1/health handler uses to populate the additive `latestServerVersion` / `updateAvailable` / `updateReleaseNotesURL` / `minClientVersion` fields. Nil-safe — when `s.updater` is nil those fields stay omitempty-empty and the wire shape matches the pre-Phase-A response exactly.
`internal/updater.Updater` satisfies this interface in production. The `MinClientVersion` returned here is read from `version.MinClientVersion` so it advertises the floor THIS bridge needs from its iOS clients (not the floor a candidate update would require — that's a Phase B / C concern).
type UpscaleEnqueuer ¶ added in v0.1.2
UpscaleEnqueuer is the interface POST /v1/upscale uses to hand off a track or folder of tracks to the long-lived transcode worker pool. Mirrors the api package's other "feature glue" interfaces (ManifestProvider, MBIDProbe, VariantStore) so the api package doesn't import internal/transcode directly.
The cmd/bridge wiring constructs an adapter that wraps a transcode.Pool. Nil-safe: when WithUpscaleEnqueuer wasn't called (or was called with nil because the feature gate / sox probe failed), the handler returns 503 `upscale_disabled`.
`libraryRelativePath` is the wire-shape path the manifest emits and that iOS sends back. The adapter resolves it to an absolute on-disk path via `bridgefs.Resolver` (canonical resolution — handles single-root + multi-root layouts uniformly), stat()s the source for freshness fields, and hands a `transcode.JobSpec` to the Pool.
Errors:
- `ErrUpscaleQueueFull` (typed): all queue slots taken; handler returns 503 `queue_full`.
- `ErrUpscaleSourceMissing` (typed): the path resolves but stat fails; handler counts as "rejected" with a clear log line. Not a 5xx because a missing source is a scanner-reconciliation issue, not a server fault.
- `ErrUpscaleIneligible` (typed): the source is DSD, already at/above target rate, or already has a fresh sidecar. Counted as "rejected" silently — the user can't take action.
- any other error: treated as a server fault; the handler logs and counts as rejected.
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 api package compiles without importing internal/transcode. The wiring closure in cmd/bridge/main.go translates between the two value types — same indirection the admin package already uses.
type UpscaleRequest ¶ added in v0.1.2
type UpscaleRequest struct {
Path string `json:"path"`
}
UpscaleRequest is the wire shape POST /v1/upscale accepts. `path` may be a track file or a folder; the handler stat()s to decide. Folder requests recursively enqueue every regular file under the folder; the enqueuer's per-track eligibility check filters non-PCM / already-at-target / already-cached candidates.
type UpscaleResponse ¶ added in v0.1.2
type UpscaleResponse struct {
Enqueued int `json:"enqueued"`
Rejected int `json:"rejected,omitempty"`
Eligible int `json:"eligible,omitempty"`
QueueFull bool `json:"queueFull,omitempty"`
}
UpscaleResponse is the wire shape POST /v1/upscale returns on 202 Accepted.
- `enqueued`: number of jobs that successfully landed on the worker pool's queue.
- `rejected`: number of candidates that were considered but not queued (queue full, ineligible, source missing).
- `eligible`: total number of regular files the handler considered (folder walk surface).
- `queueFull`: true when at least one rejection was due to queue capacity (vs. eligibility / missing-source). iOS surfaces this in the toast so the user knows to retry.
When `enqueued == 0 && queueFull`, the handler returns 503 `queue_full` instead of 202 — every candidate that was eligible bounced.
type UpscaleStats ¶ added in v0.1.2
type UpscaleStats struct {
Enabled bool `json:"enabled"`
SoxAvailable *bool `json:"soxAvailable,omitempty"`
Pool *UpscalePoolStats `json:"pool,omitempty"`
CachedVariants int `json:"cachedVariants"`
CachedBytes int64 `json:"cachedBytes"`
}
UpscaleStats is the wire shape GET /v1/upscale/stats returns.
Field-for-field compatible with the admin /api/upscale/stats payload — the JSON shapes intentionally match so an operator inspecting the bridge with `curl -k https://…/v1/upscale/stats` (with bearer token) sees the same body the Settings tile is already showing them. iOS uses it for the "Upscaling" management section inside BridgeEditorView (counts of cached / in-flight / failed jobs).
- Enabled mirrors live runtime state, NOT the persisted `cfg.Upscale.Enabled` flag. The two diverge in two real cases the admin handler documents (see CLAUDE.md / PR #110): (a) startup demoted the feature when sox-precheck failed even though the flag was on; (b) the operator just PATCHed the flag off but the long-lived Pool is still alive until restart. Both surface as `pool == nil` from the wiring closure, and we report `enabled = (pool != nil)` so the iOS-facing /v1/health.upscaleEnabled and this endpoint agree about what "active" means.
- Pool is omitted when the feature is off (no pool to query).
- SoxAvailable is omitted when the test harness didn't wire a precheck closure.
type UpscaleStatsProvider ¶ added in v0.1.2
type UpscaleStatsProvider interface {
UpscaleStatsSnapshot() UpscaleStats
}
UpscaleStatsProvider is the interface GET /v1/upscale/stats reads. Mirrors the admin tile's data sources but lives here so the api package doesn't import internal/admin or internal/transcode.
The cmd/bridge wiring constructs an adapter that returns the same pool snapshot the admin handler consumes — the two surfaces stay in lockstep.
Nil-safe: when WithUpscaleStats wasn't called (test harness, or builds without the upscale wiring), the handler returns the zero-value UpscaleStats — `enabled=false, cachedVariants=0, cachedBytes=0, pool=nil, soxAvailable=nil`. iOS renders that as "feature off" without distinguishing a missing endpoint from a disabled feature, which matches the /v1/health.upscaleEnabled contract iOS already gates on.
type VariantRecord ¶ added in v0.1.2
VariantRecord is the minimum metadata the api needs to (a) decide freshness and (b) serve the sidecar bytes. Mirrors the on-disk columns the manifest package writes. Pointer return from LookupVariant lets `nil` distinguish "no such row" from "row exists but freshness fails" — caller can still surface a targeted 404 vs 410 from the same return shape.
type VariantStore ¶ added in v0.1.2
type VariantStore interface {
LookupVariant(sourcePath, variantID string) (*VariantRecord, error)
}
VariantStore is the optional interface the `?variant=<id>` branch of /v1/download uses to look up a variant's cached metadata. Nil-safe — when `s.variantStore` is nil the download handler returns 404 `variant_not_found` for any request that carries the parameter (matches the "feature unavailable" behaviour iOS already handles for pre-v1.2 bridges).
**Freshness check happens in the api**, not here. The api has the canonical `bridgefs.Resolver` (path validation + traversal guard already exercised on every other handler), and uses the `os.FileInfo` it already stat'd for the source file in serveFile. This avoids a duplicate path-resolution code path in the manifest package — which Gemini bot review on PR #108 identified as broken in single-root mode (the manifest's hand-rolled basename-stripping assumed multi-root layout) AND flagged by CodeQL as "uncontrolled data used in path expression". Both go away when the api owns the source-side stat call and only asks the variant store for "do you have this row, and what's its recorded provenance".
`internal/manifest.Provider` satisfies this in production via a thin LookupVariant wrapper around the SQLite row read.