config

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: May 25, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package config loads and validates bridge.yaml (library roots, listen address, TLS paths, scan interval).

Relative paths in the config file resolve against the config file's own directory, matching how most Unix tools handle config-relative paths.

Index

Constants

View Source
const (
	DefaultListenAddress = ":7788"
	DefaultAdminAddress  = "127.0.0.1:7789"
	DefaultDataDir       = "./data"
	// DefaultScanIntervalSec is 6h, not 1h. A 50k-track library on
	// mechanical NAS with the prior 1h cadence spun the disks every
	// hour preventing idle spindown — operator-facing wear hazard.
	// Operators with quiet libraries should set this higher; admin
	// console exposes on-demand triggers for ad-hoc rescans.
	// fsnotify integration would let us drop this further but has
	// cross-platform pitfalls (Windows path semantics, recursive
	// watch fan-out on large libraries) and deserves its own design
	// pass (PR #N).
	DefaultScanIntervalSec     = 21600
	DefaultLibraryName         = "1-bit Bridge"
	DefaultBackupIntervalHours = 24

	// DefaultDeleteAfterMissingScans is the grace period before the
	// scanner deletes a row that's been missing from successive scans.
	// 3 is a balance: short enough that a user-deleted track disappears
	// from search inside a typical day's rescan cadence (6h × 3 = 18h),
	// long enough to absorb the silent-empty-enumeration failure modes
	// network mounts produce occasionally. Operators on local-disk-only
	// deployments can override to 1 to preserve pre-resilience behaviour.
	DefaultDeleteAfterMissingScans = 3

	// DefaultManifestRequestsPerMinute / DefaultManifestBurst configure
	// the per-token /v1/manifest rate limit. 6 rpm + 3 burst lets the
	// first three calls fire instant (typical paginated scan: pull
	// page, process, pull next) and then paces the steady state at one
	// call every ~10 s. Tuned for the realistic worst case — an iOS
	// client doing a full-manifest re-pull after a long offline window
	// where it caches nothing on disk. Operators with bursty traffic
	// can raise; operators tightening defence-in-depth can lower.
	// Setting RequestsPerMinute to a negative value (or zero with
	// burst > 0) disables the limit entirely — see manifestRateLimiter.
	DefaultManifestRequestsPerMinute = 6
	DefaultManifestBurst             = 3
	DefaultBackupKeep                = 7
	// DefaultLibraryWatchDebounceSeconds is the per-directory
	// event coalesce window when fsnotify-based watching is on.
	// 10 seconds matches the documented default and is long
	// enough that a large-folder copy doesn't trigger a scan-
	// per-file storm.
	DefaultLibraryWatchDebounceSeconds = 10
	// DefaultUpscaleQueueCap bounds the pending-job channel of the
	// long-lived transcode worker pool inside `bridge serve`. 5000
	// covers 2-3 average user libraries; smaller deployments stay
	// well under the cap, the user-spam-the-button case bounces
	// against a clean 503 instead of exhausting memory.
	DefaultUpscaleQueueCap = 5000
	// DefaultBootstrapTargetRate is the integer Hz value used to seed
	// scan_state.upscale_target_hz on first run when the YAML field
	// is unset or "auto". 192000 covers most 44.1- and 48-family
	// sources without resampling artifacts; admin Settings can edit
	// at runtime via the v1.3 Library Inspector. Per
	// `UpscaleConfig.EffectiveBootstrapTargetRate`.
	DefaultBootstrapTargetRate = 192000
)

Defaults applied when a field is absent or zero-valued.

View Source
const DefaultVariantSweepIntervalSec = 3600

DefaultVariantSweepIntervalSec is the default cadence (1 h) for the upscale-variant integrity watcher. See IntegrityConfig docstring for the rationale (closes the operator-rm / backup- software / disk-failure cases without exhausting filesystem IO).

Variables

This section is empty.

Functions

func IsInQuietHours added in v0.1.1

func IsInQuietHours(startMin, endMin, now int) bool

IsInQuietHours returns true when minute-of-day `now` falls inside [startMin, endMin]. Handles midnight-wrap windows (start > end = the window crosses midnight, so the "inside" is [start, 1440) ∪ [0, end]).

func ParseQuietHours added in v0.1.1

func ParseQuietHours(s string) (startMin, endMin int, err error)

ParseQuietHours parses a "HH:MM-HH:MM" window into start and end minute-of-day values (0-1439 each). Returns an error for malformed input. The window may wrap midnight (start > end means "from start until midnight, then midnight until end").

Used by Validate to catch bad config at load time, and by the updater's auto-install scheduler to decide whether the current wall-clock minute is inside the window.

func ValidateCustomEndpoints added in v0.1.2

func ValidateCustomEndpoints(in []string) (kept []string, warnings []error)

ValidateCustomEndpoints filters the input to entries that parse as absolute HTTPS URLs with a non-empty host. Returns (kept, warnings) where `warnings` is one error per dropped entry. Used by Validate() to scrub the persisted list and by the admin patch handler to surface per-entry errors back to the operator.

Why HTTPS-only: iOS clients won't speak plain-HTTP to the bridge (ATS rejects it before our pinning runs even on a local-network allowlisted host), and the bridge itself only listens TLS. Allowing HTTP entries would just produce a confusing "advertised but unreachable" row in the Devices panel.

Types

type AutocertConfig added in v0.1.4

type AutocertConfig struct {
	// Enabled toggles the autocert auto-pilot. False (default)
	// means the bridge serves only the self-signed cert. True
	// (in public mode + with a configured Domain + Email) means
	// the bridge mints + serves an LE cert for Domain.
	Enabled bool `yaml:"enabled,omitempty"`

	// Domain is the publicly-routable hostname the operator's iOS
	// clients (and the operator's browser) dial. Required when
	// `deployment.mode: public` is set; ignored otherwise.
	//
	// Consumed by:
	//   - Admin Origin allowlist (PR 2)
	//   - autocert.Manager.HostPolicy (PR 3) — only Domain is
	//     accepted as a host for cert minting
	//   - tls.Manager SNI gate (PR 3) — only requests whose SNI
	//     matches Domain hit the autocert path
	Domain string `yaml:"domain,omitempty"`

	// Email is the operator's contact address registered with the
	// ACME directory. LE uses this to send expiry warnings and
	// service-disruption notices. Required when Enabled.
	Email string `yaml:"email,omitempty"`

	// CacheDir is the on-disk directory where autocert stores the
	// account key, issued certs, and pending challenge state.
	// Empty defaults to `<dataDir>/acme` at load time. Persistence
	// across restarts is load-bearing: wiping this dir between
	// restarts burns the LE duplicate-cert rate-limit budget
	// (5/week per registered domain).
	CacheDir string `yaml:"cacheDir,omitempty"`

	// UseStaging routes the ACME client at LE's staging directory
	// instead of production. Untrusted cert (browsers warn) but
	// no rate limits — useful during deployment-tuning when the
	// production rate limits would otherwise burn fast. Default
	// false (production).
	UseStaging bool `yaml:"useStaging,omitempty"`

	// External443Mapping documents that the operator runs an
	// external port-forward / load balancer that maps `WAN:443`
	// to the bridge's `listenAddress`. Required when `Enabled`
	// AND `listenAddress` doesn't already end in `:443` — without
	// it, LE's TLS-ALPN-01 challenge cannot reach the bridge.
	// True is the operator's promise that the mapping is in
	// place; Validate cannot probe the WAN side.
	External443Mapping bool `yaml:"external443Mapping,omitempty"`
}

AutocertConfig wires native Let's Encrypt provisioning via `golang.org/x/crypto/acme/autocert` (TLS-ALPN-01 challenge on the same listener as the public API). When enabled in public mode, the bridge auto-mints + auto-renews an LE cert for `Domain` and serves it on matching SNI through the same `internal/tls.Manager` that already fronts the Tailscale magic-DNS cert path.

**Hard ACME constraint**: TLS-ALPN-01 is validated by LE *only* on TCP/443. Either set `listenAddress: ":443"` directly OR configure an external port-forward / load-balancer mapping `WAN:443 → bridge:<listenPort>` and opt-in via `external443Mapping: true`. Validate enforces this in public mode + autocert.enabled.

type BackupConfig added in v0.1.1

type BackupConfig struct {
	// IntervalHours is the cadence for automatic snapshots.
	// nil/omitted → DefaultBackupIntervalHours.
	// 0 → disabled.
	// >0 → cadence in hours (negative values rejected at Validate).
	IntervalHours *int `yaml:"intervalHours,omitempty"`

	// Keep is the maximum number of snapshots to retain after a
	// rotation. Older snapshots beyond this count are deleted on
	// each periodic snapshot. Zero or negative disables pruning;
	// the operator manages backup-disk usage by hand in that case.
	// Defaults to DefaultBackupKeep when the entire backup section
	// is omitted; an operator who wants no pruning sets `keep: -1`
	// (a negative value won't trip Validate).
	Keep int `yaml:"keep,omitempty"`
}

BackupConfig configures the periodic state-snapshot ticker that `bridge serve` runs alongside the manifest scanner. The CLI `bridge backup` / `bridge restore` work regardless of this section — these knobs only govern the in-process automatic schedule.

Default state (section absent): IntervalHours=24, Keep=7 (one snapshot per day, a week retained). Setting `intervalHours: 0` explicitly disables the periodic ticker; the operator can still snapshot on-demand via the CLI or the admin console. To preserve the "omitted vs explicit-zero" distinction across YAML round-trips, `IntervalHours` is a `*int` — nil means "absent, use default", a pointer to 0 means "explicitly disabled".

func (BackupConfig) EffectiveIntervalHours added in v0.1.1

func (b BackupConfig) EffectiveIntervalHours() int

EffectiveIntervalHours resolves the runtime cadence from the pointer-typed config field. Caller code uses this rather than dereferencing IntervalHours directly so the nil-vs-zero contract stays in one place.

func (BackupConfig) EffectiveKeep added in v0.1.1

func (b BackupConfig) EffectiveKeep() int

EffectiveKeep returns the rotation count to apply on each periodic snapshot. Mirrors EffectiveIntervalHours so call sites have one helper per field.

type Config

type Config struct {
	LibraryRoots    []string `yaml:"libraryRoots"`
	ListenAddress   string   `yaml:"listenAddress"`
	AdminAddress    string   `yaml:"adminAddress,omitempty"`
	DataDir         string   `yaml:"dataDir"`
	TLSCertPath     string   `yaml:"tlsCertPath,omitempty"`
	TLSKeyPath      string   `yaml:"tlsKeyPath,omitempty"`
	ScanIntervalSec int      `yaml:"scanIntervalSec"`
	LibraryName     string   `yaml:"libraryName"`
	// CustomEndpoints lets operators advertise URLs that aren't
	// discoverable from the host's interface table — e.g. a reverse
	// proxy, a port-forwarded WAN URL, or a non-default Tailscale
	// MagicDNS that the auto-detector didn't pick up. Each entry is
	// a complete URL ("https://my-bridge.example.com:7788") and is
	// surfaced to iOS via /v1/health and the admin endpoints panel.
	//
	// Validation: each entry must parse as an absolute URL with
	// scheme="https" — entries failing validation are dropped at
	// load with a `.warn` log and don't fail the whole config.
	//
	// Operators add entries via the admin console's Settings page;
	// hand-edits to bridge.yaml work too. Used as input to the cert
	// SAN gather (PR feat/tls-broader-sans) so that adding a new
	// custom DNS hostname here, then rotating the cert, makes that
	// URL TLS-handshake-compatible from iOS.
	CustomEndpoints []string           `yaml:"customEndpoints,omitempty"`
	Update          UpdateConfig       `yaml:"update,omitempty"`
	Backup          BackupConfig       `yaml:"backup,omitempty"`
	LibraryWatch    LibraryWatchConfig `yaml:"libraryWatch,omitempty"`
	Upscale         UpscaleConfig      `yaml:"upscale,omitempty"`
	Tailscale       TailscaleConfig    `yaml:"tailscale,omitempty"`
	Scanner         ScannerConfig      `yaml:"scanner,omitempty"`
	Limits          LimitsConfig       `yaml:"limits,omitempty"`
	Integrity       IntegrityConfig    `yaml:"integrity,omitempty"`
	Deployment      DeploymentConfig   `yaml:"deployment,omitempty"`
	Autocert        AutocertConfig     `yaml:"autocert,omitempty"`
	MDNS            MDNSConfig         `yaml:"mdns,omitempty"`

	// DisableHTTP3 prevents the server from binding UDP ports and
	// advertising Alt-Svc headers for HTTP/3 upgrades. Defaults to false.
	DisableHTTP3 bool `yaml:"disableHttp3,omitempty"`
}

Config mirrors the on-disk bridge.yaml shape. See config/bridge.yaml.example.

func Clone added in v0.1.3

func Clone(cfg *Config) *Config

Clone returns a deep clone of cfg. Nil input returns nil.

IMPORTANT: When adding pointer or slice fields to Config, update this function to deep-copy them. TestConfigCloneIsDeep verifies coverage via the reflection-based assertNoSharedPointers walker over a fillNonZero-populated fixture.

func Load

func Load(path string) (*Config, error)

Load parses a bridge.yaml file, fills defaults, resolves relative paths against the config file's directory, and validates. A returned *Config is ready to hand to downstream subsystems.

func (*Config) EffectiveAutocertCacheDir added in v0.1.4

func (c *Config) EffectiveAutocertCacheDir() string

EffectiveAutocertCacheDir returns the autocert account + cert cache directory. When Autocert.CacheDir is set, it's used verbatim; when empty, defaults to <dataDir>/acme. Caller resolves DataDir before consulting this so the default tracks the actual data root across deployments.

func (*Config) EffectiveMDNSEnabled added in v0.1.4

func (c *Config) EffectiveMDNSEnabled() bool

EffectiveMDNSEnabled returns the resolved mDNS posture: explicit operator value when set, otherwise the deployment-mode default (true for loopback, false for public).

Tolerates either form at the YAML layer; the pointer indirection is the single source of truth for missing-vs-explicit.

func (*Config) IsPublic added in v0.1.4

func (c *Config) IsPublic() bool

IsPublic reports whether the configured deployment mode is "public". Errors during EffectiveMode resolution (unknown values) are treated as non-public — Validate surfaces the typo separately at load time, and we don't want a typo to silently relax the loopback gate.

func (*Config) OrphanSidecarSweepInterval added in v0.1.4

func (c *Config) OrphanSidecarSweepInterval() time.Duration

OrphanSidecarSweepInterval returns the configured forward-sweep cadence (orphan sidecar files on disk → unlink). Default zero (DISABLED); explicit zero or negative also map to disabled. Symmetric to VariantSweepInterval but with a different default because the forward sweep is opt-in (see IntegrityConfig. OrphanSidecarSweepIntervalSec docstring for the rationale).

Negative values are clamped to zero — a typo'd negative would otherwise create a busy-loop ticker. Same hardening as VariantSweepInterval.

func (*Config) Save

func (c *Config) Save(path string) error

Save atomically writes c as YAML to path (temp file + rename). Parent directory must exist. Comments and fields unknown to this schema are not preserved — callers that want to keep hand-authored comments should not use Save. `bridge init` and admin-console edits are the intended callers.

func (*Config) ScanInterval

func (c *Config) ScanInterval() time.Duration

ScanInterval returns scanIntervalSec as a time.Duration.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks invariants the server relies on. Called automatically by Load; exposed for tests and for callers that construct Config in memory.

func (*Config) VariantSweepInterval added in v0.1.3

func (c *Config) VariantSweepInterval() time.Duration

VariantSweepInterval returns the configured sweep cadence as a time.Duration. Missing field (pointer nil) returns the default (1 h); an explicit zero opt-out is preserved verbatim and the integrity package treats ≤ 0 as "watcher disabled".

Negative values are clamped to zero (disabled) — a typo'd negative would otherwise create a busy-loop ticker that stat()s every variant row continuously and saturates IOPS.

type DeploymentConfig added in v0.1.4

type DeploymentConfig struct {
	// Mode selects the posture: "loopback" (default) or "public".
	// Empty value falls back to "loopback" at load time.
	Mode string `yaml:"mode,omitempty"`

	// AdminTLSTerminatedByProxy lets operators run the bridge in
	// public mode behind a TLS-terminating reverse proxy (Caddy /
	// nginx) without having the bridge mint its own admin TLS cert.
	// When true: the admin listener serves plain HTTP on its bind
	// address (operator's responsibility to keep that bind on a
	// private interface or restrict it via firewall); session
	// cookies still carry Secure (so the operator's proxy MUST
	// front them with HTTPS — browsers refuse to send Secure
	// cookies over plain HTTP, surfacing a misconfigured setup as
	// a visible login failure rather than a silent leak). When
	// false (and Mode == public): the bridge wraps its admin
	// listener in tls.NewListener against the same Manager that
	// fronts the public API, so the admin console gets the LE
	// cert for the operator's domain SNI and a self-signed cert
	// for direct-IP / unknown SNI.
	AdminTLSTerminatedByProxy bool `yaml:"adminTLSTerminatedByProxy,omitempty"`
}

DeploymentConfig selects the overall deployment posture. `loopback` (default) preserves the historical single-operator-on-trusted-LAN shape: admin console is loopback-only with no auth, mDNS advertises freely, Tailscale auto-pilot runs by default, the self-signed TLS cert is the pinning anchor for iOS clients.

`public` opts into the public-VPS posture: admin console can bind non-loopback (auth-protected, see internal/adminauth — gated on this flag), mDNS suppressed by default, Tailscale defaults to disabled, the iOS pinning anchor shifts to a publicly-trusted LE cert minted via autocert on the operator's domain.

The flip is deliberately coarse — a single posture switch rather than a cascade of independent knobs — and is RestartRequired (the admin bind address, TLS listener composition, and auto-pilot wiring all change shape; hot-applying mid-process is asking for incidents).

func (DeploymentConfig) EffectiveMode added in v0.1.4

func (d DeploymentConfig) EffectiveMode() (DeploymentMode, error)

EffectiveMode resolves the configured posture, applying the "loopback" default for empty values. Returns one of the two known modes or an error when the yaml carries something unrecognised — surfaces the typo at config-load time rather than as silent loopback-fallback that masks public-mode intent.

Tolerant of leading/trailing whitespace and case differences in the YAML value (mirrors TailscaleConfig.EffectiveMode).

type DeploymentMode added in v0.1.4

type DeploymentMode string

DeploymentMode is the typed representation of DeploymentConfig.Mode.

const (
	DeploymentModeLoopback DeploymentMode = "loopback"
	DeploymentModePublic   DeploymentMode = "public"
)

type IntegrityConfig added in v0.1.3

type IntegrityConfig struct {
	// VariantSweepIntervalSec controls how often the
	// integrity.VariantWatcher walks `track_variants` and
	// stat()s each sidecar to detect external deletions.
	// Default 3600 s (1 h). Explicit zero disables the
	// watcher entirely — operators on minimal deploys who
	// run `bridge upscale --gc` manually opt out via this
	// knob.
	//
	// Pointer-typed so applyDefaults can distinguish
	// "missing field, use default" from "explicit zero,
	// disable". Same pattern LimitsConfig.RequestsPerMinute
	// uses for the same reason. Always read via
	// Config.VariantSweepInterval() below — never
	// dereference directly.
	//
	// Pair with the reactive serve-side cleanup in
	// internal/api.serveVariant: that path closes the
	// active-playback case the moment a client requests a
	// missing sidecar. The watcher catches the
	// not-currently-playing case at most VariantSweepInterval
	// later. Both publish the same `upscale.deleted` SSE
	// event so iOS reconciliation is uniform.
	VariantSweepIntervalSec *int `yaml:"variantSweepIntervalSec,omitempty"`

	// OrphanSidecarSweepIntervalSec controls how often the
	// integrity.OrphanSidecarSweeper walks `<variantsDir>/transcoded/`
	// for sidecar files that have no matching `track_variants` row
	// and unlinks them. The forward-sweep half of what the operator-
	// triggered `bridge upscale --gc` does today; the existing
	// VariantWatcher above handles the reverse half (DB rows whose
	// sidecar file no longer exists).
	//
	// **Default zero (disabled)** — opt-in feature. Operators on
	// minimal deploys or low-disk-pressure libraries can keep running
	// `--gc` manually; operators on libraries that churn variants
	// (frequent rip / re-tag passes that delete + re-add tracks under
	// the SAME relative path but produce different `track_variants.id`
	// rows) opt in via a non-zero value here.
	//
	// **Chunked + low-priority**: each tick processes at most
	// `gcChunkSize` filesystem entries (100, defined in the integrity
	// package). The operator-triggered `--gc` keeps its existing
	// unbounded-sweep semantics; this knob exists for the
	// hands-off-operator profile where chunking + cadence-spacing
	// matters.
	//
	// **Snapshot semantics**: each tick takes a `BEGIN DEFERRED`
	// snapshot of `track_variants.sidecar_path` BEFORE the
	// filesystem walk so a concurrent `UpsertVariant` writer can't
	// produce a false-positive orphan (the new sidecar lands on disk
	// before its row commits to the snapshot; pre-snapshot the
	// reverse race would have the sweeper unlink the file before the
	// row caught up).
	//
	// Pointer-typed to distinguish "missing field → use default
	// (disabled)" from "explicit zero → also disabled" — kept
	// symmetric with VariantSweepIntervalSec above even though they
	// resolve to the same behaviour here, so a future migration that
	// flips the default doesn't silently change operator-zeroed
	// configs.
	//
	// Always read via Config.OrphanSidecarSweepInterval() below.
	OrphanSidecarSweepIntervalSec *int `yaml:"orphanSidecarSweepIntervalSec,omitempty"`
}

IntegrityConfig controls the proactive consistency watchers — today just the upscale-variant sweep. The library-scanner's own scheduling lives at the top-level `scanIntervalSec` for back-compat with v1.0 deploys; this block exists so future orthogonal integrity surfaces (artwork-cache reconcile, sidecar freshness re-validate) can join the same YAML node without scattering top-level fields.

type LibraryWatchConfig added in v0.1.1

type LibraryWatchConfig struct {
	// Enabled is the master toggle. Default false.
	Enabled bool `yaml:"enabled,omitempty"`
	// DebounceSeconds is the per-directory event coalesce window.
	// 10 seconds is the documented default — long enough that a
	// large-folder copy doesn't trigger a scan-per-file storm,
	// short enough that the perceived "instant" feel survives.
	// Zero or omitted → DefaultLibraryWatchDebounceSeconds.
	DebounceSeconds int `yaml:"debounceSeconds,omitempty"`
}

LibraryWatchConfig governs the optional fsnotify-based instant-update watcher. Off by default — the periodic scan (ScanIntervalSec) remains the safety net in either case. Power users with local-disk libraries flip this on to get Roon/Plex-style "drop a file in, it appears" responsiveness; NAS / spinning-disk users keep the periodic-only path so a flapping server can't trigger a thrash of incremental scans.

Linux deployments with very large libraries should also raise `fs.inotify.max_user_watches` — `bridge doctor` warns when the kernel limit looks too low for the configured roots.

func (LibraryWatchConfig) EffectiveDebounceSeconds added in v0.1.1

func (l LibraryWatchConfig) EffectiveDebounceSeconds() int

EffectiveDebounceSeconds resolves the runtime debounce window from the YAML field. Centralised so the watcher and the doctor check can't disagree.

type LimitsConfig added in v0.1.3

type LimitsConfig struct {
	Manifest ManifestLimitsConfig `yaml:"manifest,omitempty"`
}

LimitsConfig groups operator-facing throttle knobs. Today: just the /v1/manifest rate limit. Lives at the top of the YAML so future per-endpoint or per-resource limits can join the same block instead of scattering across the config surface.

type MDNSConfig added in v0.1.4

type MDNSConfig struct {
	Enabled *bool `yaml:"enabled,omitempty"`
}

MDNSConfig controls the Bonjour `_onebit-bridge._tcp` service advertisement. Defaults differ by deployment posture:

  • Loopback (default): mDNS is ON. Home LAN deployments rely on it for iOS auto-discovery — without mDNS, every new device would need a manual pair URL.
  • Public: mDNS is OFF. A VPS has no local LAN to advertise on (the bridge's interfaces are routable WAN addresses); emitting Bonjour records would be a no-op at best and a small attack-surface leak at worst (TXT records expose the bridge's protocol version + library name).

`Enabled` is a pointer so applyDefaults can distinguish "missing field, apply posture-default" from "explicit operator override" — same shape `IntegrityConfig.VariantSweepIntervalSec` uses for the same reason. Always read via `EffectiveMDNSEnabled()`; never dereference Enabled directly (nil-deref hazard).

type ManifestLimitsConfig added in v0.1.3

type ManifestLimitsConfig struct {
	RequestsPerMinute *int `yaml:"requestsPerMinute,omitempty"`
	Burst             *int `yaml:"burst,omitempty"`
}

ManifestLimitsConfig controls the per-token-ID token bucket applied to /v1/manifest. See internal/api.manifestRateLimiter for the runtime shape.

Pointer-typed RequestsPerMinute / Burst preserve the operator's intent across the (missing-field) vs (explicit-zero) distinction Go's bare `int` collapses. The documented opt-out path is `limits.manifest.requestsPerMinute: 0` — explicit zero MUST disable the limiter, while a missing field MUST pick up the default. Without the pointer, applyDefaults can't tell those apart and silently overrides operator-supplied zeros (Gemini HIGH + Greptile P1 on PR #194). Same shape as Backup.IntervalHours uses for the same reason. Always read via EffectiveRPM / EffectiveBurst below — never dereference the pointer fields directly.

func (ManifestLimitsConfig) EffectiveBurst added in v0.1.3

func (m ManifestLimitsConfig) EffectiveBurst() int

EffectiveBurst returns the configured burst capacity. A missing field returns the default. An explicit zero / negative gets clamped to 1 by the limiter constructor — burst=0 in the rate package means "deny everything", which would lock out legitimate clients on a config typo. Operators opt out of the limiter via RequestsPerMinute=0, not via Burst.

func (ManifestLimitsConfig) EffectiveRPM added in v0.1.3

func (m ManifestLimitsConfig) EffectiveRPM() int

EffectiveRPM returns the configured requests-per-minute. A missing field (pointer == nil) returns the default; an explicit zero is preserved verbatim — the limiter's constructor maps zero to "no budget" so callers fall open. That's the documented opt-out path.

type RuntimeConfig added in v0.1.3

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

RuntimeConfig is the process-wide holder for the live config snapshot. Readers call Load() per-request; writers clone, mutate, validate, save, run hooks, and atomically swap with Store().

func NewRuntimeConfig added in v0.1.3

func NewRuntimeConfig(cfg *Config) *RuntimeConfig

NewRuntimeConfig initialises a RuntimeConfig from cfg.

func (*RuntimeConfig) Clone added in v0.1.3

func (r *RuntimeConfig) Clone() *Config

Clone returns a deep clone of the current snapshot.

func (*RuntimeConfig) Load added in v0.1.3

func (r *RuntimeConfig) Load() *Config

Load returns the current immutable snapshot.

func (*RuntimeConfig) Store added in v0.1.3

func (r *RuntimeConfig) Store(cfg *Config)

Store atomically swaps the current snapshot. Nil is ignored.

type ScannerConfig added in v0.1.3

type ScannerConfig struct {
	DeleteAfterMissingScans int `yaml:"deleteAfterMissingScans,omitempty"`
}

ScannerConfig controls the library scanner's resilience knobs.

DeleteAfterMissingScans is the consecutive-missing-scan grace period before a row is deleted. The scanner increments per-row `missing_count` on every pass where the row is in the before-snapshot but NOT in the seen-set AND not under an errorSubtree. Rows reach the threshold only after that many CLEAN scans (no surfaced error) all failed to see them — defending against silent-empty-enumeration modes on network mounts (SMB re-auth flap, NFS brownout, libsmb2 timeout returning an empty Readdir) that errorSubtrees can't catch because no error fired.

Default is 3. Minimum is 1 (preserves the pre-resilience immediate- delete behaviour — useful for local-disk-only deployments where the failure modes don't apply). Maximum isn't enforced but values > 10 only make sense in heavily flaky-mount environments; the trade-off is that a user-deleted track lingers in search until the threshold expires.

type TailscaleConfig added in v0.1.2

type TailscaleConfig struct {
	// Mode selects the integration: "cli" (default), "tsnet", or
	// "disabled". Empty value falls back to "cli" at load time.
	Mode string `yaml:"mode,omitempty"`

	// AuthKey is the Tailscale auth key used by tsnet on first run.
	//
	// Precedence (matches Tailscale's standard idiom):
	//   1. TS_AUTHKEY environment variable (preferred — keeps
	//      secrets out of yaml on disk)
	//   2. This field (fallback for ops who can't set env vars)
	//   3. Empty → triggers interactive OAuth (`bridge tsnet auth`
	//      prints an AuthURL the operator visits in a browser)
	//
	// Unused once tsnet has persisted state.
	AuthKey string `yaml:"authKey,omitempty"`

	// Hostname is the magic-DNS hostname tsnet will register with.
	// Empty falls back to the bridge's deviceName / library name.
	Hostname string `yaml:"hostname,omitempty"`
}

TailscaleConfig selects how the bridge integrates with Tailscale. `cli` (default) preserves the historical CLI-shell-out flow: `tailscale status` for endpoint detection + `tailscale cert` for LE-on-magic-DNS cert minting. The CLI path requires the operator's host to have `tailscaled` running and `tailscale` in $PATH, and ships LE cert/key files to disk for the SNI cert switcher to read.

`tsnet` makes the bridge its own tailnet node via the embedded `tailscale.com/tsnet` library — no external daemon needed, no on-disk LE cert dance (tsnet's ListenTLS terminates LE in-process). State (machine identity + control-plane keys) persists under `<dataDir>/tailscale/`. First-time auth uses an interactive browser flow; subsequent boots load persisted state. Headless deploys can pre-seed the state by setting AuthKey or the TS_AUTHKEY environment variable.

`disabled` skips both — useful for LAN-only deployments where the operator doesn't want tailscale code paths exercised.

Default is `cli` to preserve back-compat. Operators opt into `tsnet` per-device; the default may be flipped in a future release after the tsnet path soaks.

func (TailscaleConfig) EffectiveMode added in v0.1.2

func (t TailscaleConfig) EffectiveMode() (TailscaleMode, error)

EffectiveMode resolves the configured mode, applying the "cli" default for empty values. Returns one of the three known modes or an error when the yaml carries something unrecognised — the safer shape than silently falling through to the default on a typo (e.g. `mode: tnset` would otherwise look like it worked).

Tolerant of leading/trailing whitespace and case differences in the YAML value — hand-edited config files frequently pick up trailing spaces after a merge / format-on-save, and the resulting "unknown value" error trapped operators who couldn't see the invisible character. Normalisation is one-way: the error message preserves the ORIGINAL untrimmed `t.Mode` so the operator sees their actual input verbatim, which matters most for a typo path where the invisible-whitespace explanation would mislead.

type TailscaleMode added in v0.1.2

type TailscaleMode string

TailscaleMode is the typed representation of TailscaleConfig.Mode.

const (
	TailscaleModeCLI      TailscaleMode = "cli"
	TailscaleModeTsnet    TailscaleMode = "tsnet"
	TailscaleModeDisabled TailscaleMode = "disabled"
)

type UpdateConfig added in v0.1.1

type UpdateConfig struct {
	// AutoInstall enables the poll-loop's automatic install attempt.
	// Off by default — the operator must opt in. Even when on, the
	// stream-active gate refuses if any /v1/download is in flight,
	// and quiet-hours (when set) restrict to the configured window.
	AutoInstall bool `yaml:"autoInstall,omitempty"`

	// QuietHours restricts auto-install to a daily window in
	// "HH:MM-HH:MM" form using the server's local time. Empty means
	// "any time". The window may wrap midnight (e.g.
	// "23:00-06:00") — see config_test.go for the wrap-around test
	// matrix. Any-clock format that fails to parse rejects at
	// Validate time so a bad config doesn't silently disable the
	// auto-installer.
	QuietHours string `yaml:"quietHours,omitempty"`

	// CheckIntervalHours overrides the default poll cadence (6 h).
	// Operator-tunable for installations on metered or rate-limited
	// uplinks. Values below 1h are clamped at runtime by the updater.
	// Zero = use the package default.
	CheckIntervalHours int `yaml:"checkIntervalHours,omitempty"`
}

UpdateConfig configures the Phase C opt-in auto-installer. The safeties from Phase B (stream-active gate, signature verification, rollback marker) ALWAYS apply — these toggles only decide whether auto-install is attempted at all and within what time window.

Default state: AutoInstall=false (operator-triggered only). The admin Settings UI exposes these for hand-edit; YAML-direct edits are also supported.

type UpscaleConfig added in v0.1.2

type UpscaleConfig struct {
	// Enabled is the master toggle. Default false.
	Enabled bool `yaml:"enabled,omitempty"`

	// OptimizeEnabled gates the CarPlay-optimize feature
	// (`optimized-*` variant class — downsample to 16-bit /
	// 44.1k or 48k for fast cellular CarPlay playback).
	// Pointer-bool so the default is true when the field is
	// absent (`nil` → enabled); operators on storage-constrained
	// hosts can opt out via `optimizeEnabled: false`. Ignored
	// when `Enabled` is false (the master toggle covers both).
	OptimizeEnabled *bool `yaml:"optimizeEnabled,omitempty"`

	// Workers is the size of the long-lived transcode worker pool
	// instantiated by `bridge serve` when Enabled. Zero (the default)
	// resolves to min(NumCPU-1, 4) at startup. Operators with beefy
	// hosts can raise; small-box deployments (Pi) should leave at
	// the default to avoid starving downloads / pairing requests.
	Workers int `yaml:"workers,omitempty"`

	// QueueCap is the hard cap on the worker pool's pending-job
	// channel. POST /v1/upscale enqueues are non-blocking and drop
	// to a 503 `queue_full` response when the channel can't accept
	// more — protects against a user spamming "Generate" on their
	// 50k-track library exhausting memory or wedging the HTTP path.
	// Zero (the default) resolves to DefaultUpscaleQueueCap.
	QueueCap int `yaml:"queueCap,omitempty"`

	// TargetRate selects the resampler output rate. "auto" (the
	// default) picks 176400 Hz for 44.1-family sources and 192000
	// Hz for 48-family sources. Explicit numeric values override
	// the auto pick (e.g. "352800" for DSD-friendly DACs that
	// prefer the next octave up). Sources at or above the chosen
	// rate are skipped — never downsample.
	TargetRate string `yaml:"targetRate,omitempty"`

	// TargetBits is the output bit depth. 24 (the default) is the
	// sweet spot for FLAC: lower ceiling than 32-bit float but
	// 96 dB above the audible noise floor and well below any
	// realistic dither quantisation noise. 32-bit-int output is
	// supported for completeness but rarely a sonic improvement.
	TargetBits int `yaml:"targetBits,omitempty"`

	// VariantsDir lets operators relocate upscaled FLAC sidecars off
	// the bridge's data partition onto any writable mount they have
	// space on — external drive, secondary disk, dedicated NAS subdir.
	// Empty (the default) falls through to `<dataDir>/transcoded/`
	// (preserved historical layout — no existing install breaks).
	//
	// MUST be an absolute path. MUST NOT resolve under any library
	// root (catastrophic — variants tangled with source files would
	// confuse the scanner AND collide with the read-only library
	// invariant from PR #75). Validation in `Config.Validate` enforces
	// both constraints; the directory itself is created on first
	// upscale via `os.MkdirAll`.
	//
	// On-disk layout under VariantsDir is source-path-mirrored
	// (`<artist>/<album>/<basename>.<variantID>.flac` style) so
	// operators with write access to their library can `mv` the
	// variants dir contents into the library and slot variants next
	// to source files. The DB lookup remains the only path-resolution
	// mechanism — moves do NOT change the wire shape; iOS doesn't
	// care.
	VariantsDir string `yaml:"variantsDir,omitempty"`
}

UpscaleConfig governs the optional offline PCM-upscaling feature introduced in v1.2. Disabled by default — operators must explicitly opt in via `upscale.enabled: true`. When disabled:

  • the `bridge upscale` CLI exits with a friendly "feature is disabled" error.
  • the `POST /v1/upscale` endpoint returns 503 `upscale_disabled`.
  • the manifest provider does not splice `Variants` into Track responses even if `track_variants` rows exist on disk.
  • `/v1/health` reports `upscaleEnabled: false`.

When enabled, additional safety: `bridge serve` runs an `exec.LookPath("sox")` probe at startup. Missing `sox` logs an error and overrides Enabled to false in-memory — feature gracefully degrades, the rest of the server keeps running.

`track_variants` table is created unconditionally (additive schema, no harm in empty); only the read/write paths are gated. This means a user can: enable → run `bridge upscale` → variants populate → disable → variants disappear from manifest → re-enable → variants reappear without re-conversion.

func (UpscaleConfig) EffectiveBootstrapTargetBits added in v0.1.3

func (u UpscaleConfig) EffectiveBootstrapTargetBits() int

EffectiveBootstrapTargetBits is the int counterpart to EffectiveBootstrapTargetRate for the scan_state seed. Reuses the existing EffectiveTargetBits resolver (16/24/32 with 24 default) so the operator-facing default matches the legacy CLI behaviour.

func (UpscaleConfig) EffectiveBootstrapTargetRate added in v0.1.3

func (u UpscaleConfig) EffectiveBootstrapTargetRate() int

EffectiveBootstrapTargetRate resolves a concrete integer Hz value for seeding the DB-backed operator-controlled upscale target on first run. The legacy `TargetRate` field accepts "auto" (the source-rate-aware default) and arbitrary strings for the per-track CLI / HTTP path; the new v1.3 operator-driven model stores a fixed integer in scan_state that admin Settings edits.

Resolution order:

  • explicit numeric string (e.g. "192000") → parsed and returned
  • "auto" or unset → DefaultBootstrapTargetRate (192000 Hz)
  • parse failure → DefaultBootstrapTargetRate (don't fail boot on a malformed YAML; admin Settings can correct via UI)

Called once by cmd/bridge/main.go at boot: if scan_state has no upscale_target_hz row, seed it via Store.SetUpscaleTarget(this, EffectiveBootstrapTargetBits()).

func (UpscaleConfig) EffectiveOptimizeEnabled added in v0.1.4

func (u UpscaleConfig) EffectiveOptimizeEnabled() bool

EffectiveOptimizeEnabled returns the optimize-feature gate value with nil-defaulting-to-true. Operators who never touch the field inherit the on-by-default behavior; `optimizeEnabled: false` in YAML explicitly opts out (storage-constrained hosts who only want upscale).

func (UpscaleConfig) EffectiveQueueCap added in v0.1.2

func (u UpscaleConfig) EffectiveQueueCap() int

EffectiveQueueCap resolves the pending-job channel cap, defaulting to DefaultUpscaleQueueCap when the YAML field is zero.

func (UpscaleConfig) EffectiveTargetBits added in v0.1.2

func (u UpscaleConfig) EffectiveTargetBits() int

EffectiveTargetBits resolves the output bit depth. Defaults to 24.

func (UpscaleConfig) EffectiveTargetRate added in v0.1.2

func (u UpscaleConfig) EffectiveTargetRate() string

EffectiveTargetRate returns the YAML field's literal value when set, or "auto" (the default) when empty. Validation happens at the call site in the transcode package, where the source rate is in scope.

func (UpscaleConfig) EffectiveVariantsDir added in v0.1.3

func (u UpscaleConfig) EffectiveVariantsDir(dataDir string) string

EffectiveVariantsDir resolves the absolute on-disk directory the transcode pool writes variant sidecars to. Explicit YAML setting wins; empty falls through to the legacy `<dataDir>/transcoded/` path so existing installs without a `variantsDir` setting are unaffected.

Centralised so every consumer (transcode pool, admin "Stored at" surface, free-space probe, CLI `bridge variants move`) reads from one helper and can't drift. Mirrors the `EffectiveBootstrapTargetRate` convention.

Pure / no I/O. Validation (must be absolute, must not be under a library root) lives in `Config.Validate` so a misconfiguration fails fast at boot — by the time this helper is called, the path has already been vetted.

func (UpscaleConfig) EffectiveWorkers added in v0.1.2

func (u UpscaleConfig) EffectiveWorkers() int

EffectiveWorkers resolves the runtime worker count from the YAML field, applying the min(NumCPU-1, 4) default at zero. Centralised so the CLI and the serve-side pool can't disagree on the floor.

Jump to

Keyboard shortcuts

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