diff --git a/internal/config/config.go b/internal/config/config.go index 6b9df8c..24877f7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,6 +13,7 @@ import ( "os" "path/filepath" "sort" + "strconv" "strings" "time" ) @@ -79,6 +80,13 @@ type Config struct { // pre-recency behaviour). Default ~7 days. AutoSummarizeWindow time.Duration + // OnboardSummarizeCount caps how many of a freshly-connected user's newest + // videos are summarized immediately on connect (the onboarding "it works" + // burst). HARD-capped at maxOnboardSummarizeCount so onboarding can never + // bulk-fetch; 0 disables the burst. Every fetch still flows through the shared + // caption rate gate (ADR-014) — the cap bounds count, never the pacing. Default 3. + OnboardSummarizeCount int + // DiscoveryInterval, when > 0, makes `serve` run in-process scheduled discovery // for ALL users on that cadence (ADR-018). Zero/unset = disabled, so dev and // tests never auto-fetch. Single-replica assumption — see cmdServe. @@ -108,17 +116,19 @@ func (c Config) DexConfigured() bool { return strings.TrimSpace(c.OIDCIssuer) != // Defaults (see docs/homelab-integration.md). All overridable via env. const ( - defaultGatewayURL = "http://koala:30401/v1" - defaultSummarizerModel = "koala/phi4-mini" - defaultSummarizerTimeout = 5 * time.Minute - defaultYTTokenRef = "youtube/refresh_token" - defaultYTConnectRedirectURL = "https://tapir.d-ma.be/oauth/youtube/callback" - defaultOAuthRedirectAddr = "localhost:8080" - defaultHTTPAddr = ":8080" - defaultFetchBackoff = time.Hour - defaultFetchRate = 2 * time.Second - defaultPublicURL = "https://tapir.d-ma.be" - defaultAutoSummarizeWindow = 7 * 24 * time.Hour + defaultGatewayURL = "http://koala:30401/v1" + defaultSummarizerModel = "koala/phi4-mini" + defaultSummarizerTimeout = 5 * time.Minute + defaultYTTokenRef = "youtube/refresh_token" + defaultYTConnectRedirectURL = "https://tapir.d-ma.be/oauth/youtube/callback" + defaultOAuthRedirectAddr = "localhost:8080" + defaultHTTPAddr = ":8080" + defaultFetchBackoff = time.Hour + defaultFetchRate = 2 * time.Second + defaultPublicURL = "https://tapir.d-ma.be" + defaultAutoSummarizeWindow = 7 * 24 * time.Hour + defaultOnboardSummarizeCount = 3 + maxOnboardSummarizeCount = 5 ) // Load reads the environment into a Config, applying defaults. It does not @@ -183,6 +193,18 @@ func Load() (Config, error) { } c.AutoSummarizeWindow = autoWindow + onboard, err := intOr("TAPIR_ONBOARD_SUMMARIZE_COUNT", defaultOnboardSummarizeCount) + if err != nil { + return Config{}, err + } + if onboard < 0 { + onboard = 0 + } + if onboard > maxOnboardSummarizeCount { + onboard = maxOnboardSummarizeCount + } + c.OnboardSummarizeCount = onboard + return c, nil } @@ -247,6 +269,18 @@ func envOr(key, fallback string) string { return fallback } +func intOr(key string, fallback int) (int, error) { + v := os.Getenv(key) + if v == "" { + return fallback, nil + } + n, err := strconv.Atoi(v) + if err != nil { + return 0, fmt.Errorf("config: %s=%q: %w", key, v, err) + } + return n, nil +} + func durationOr(key string, fallback time.Duration) (time.Duration, error) { v := os.Getenv(key) if v == "" { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 8b2451b..ca0c99a 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -53,6 +53,38 @@ func TestLoad_AppliesDefaults(t *testing.T) { } } +func TestLoad_OnboardSummarizeCount(t *testing.T) { + cases := []struct { + name, env string + want int + }{ + {"default", "", defaultOnboardSummarizeCount}, + {"explicit", "4", 4}, + {"zero disables", "0", 0}, + {"clamped to hard cap", "50", maxOnboardSummarizeCount}, + {"negative clamps to zero", "-3", 0}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + setEnv(t, map[string]string{"TAPIR_ONBOARD_SUMMARIZE_COUNT": c.env}) + cfg, err := Load() + if err != nil { + t.Fatalf("Load: %v", err) + } + if cfg.OnboardSummarizeCount != c.want { + t.Fatalf("OnboardSummarizeCount = %d, want %d", cfg.OnboardSummarizeCount, c.want) + } + }) + } +} + +func TestLoad_OnboardSummarizeCountInvalid(t *testing.T) { + setEnv(t, map[string]string{"TAPIR_ONBOARD_SUMMARIZE_COUNT": "three"}) + if _, err := Load(); err == nil { + t.Fatal("Load: want error for non-numeric TAPIR_ONBOARD_SUMMARIZE_COUNT") + } +} + func TestLoad_ParsesValues(t *testing.T) { setEnv(t, map[string]string{ "TAPIR_USER_ID": "11111111-1111-1111-1111-111111111111",