package settings import ( "context" "database/sql" "encoding/json" "fmt" "math" "os" "strconv" "strings" "sync" "time" ) type Runtime struct { GuessMinIntervalSec int `json:"guess_min_interval_sec"` ClientSubmitIntervalSec int `json:"client_submit_interval_sec"` GuessLotteryWindowSec int `json:"guess_lottery_window_sec"` GuessLotteryMaxAccepted int `json:"guess_lottery_max_accepted"` BeaconHuntEnabled int `json:"beacon_hunt_enabled"` BeaconBonusWeight int `json:"beacon_bonus_weight"` SybilProofOfWorkBits int `json:"sybil_pow_bits"` SybilWarmupSec int `json:"sybil_warmup_sec"` OpenAIMaxCalls1H int `json:"openai_max_calls_1h"` OpenAIMaxCalls24H int `json:"openai_max_calls_24h"` OpenAIMaxCost24HUSD float64 `json:"openai_max_cost_24h_usd"` OpenAIBudgetReserveUSD float64 `json:"openai_budget_reserve_usd"` TaskRangeBits int `json:"task_range_bits"` ActiveTaskCount int `json:"active_task_count"` PresenceTTLSec int `json:"presence_ttl_sec"` DefaultMaxNodes int `json:"default_max_nodes"` PublicScorePrecision int `json:"public_score_precision"` PlaceEnabled int `json:"place_enabled"` PlaceWidth int `json:"place_width"` PlaceHeight int `json:"place_height"` PlacePointsPerScore int `json:"place_points_per_score"` PlaceDrawPoints int `json:"place_draw_points"` PlaceDrawBeaconMultiplier int `json:"place_draw_beacon_multiplier"` PlaceTimePoints int `json:"place_time_points"` PlaceTimeIntervalSec int `json:"place_time_interval_sec"` PlaceTimeMaxGapSec int `json:"place_time_max_gap_sec"` PlacePixelCost int `json:"place_pixel_cost"` ArtifactPreset string `json:"artifact_preset"` ArtifactProvider string `json:"artifact_provider"` ArtifactModel string `json:"artifact_model"` ArtifactPrompt string `json:"artifact_prompt"` ArtifactNegativePrompt string `json:"artifact_negative_prompt"` ArtifactWidth int `json:"artifact_width"` ArtifactHeight int `json:"artifact_height"` ArtifactSteps int `json:"artifact_steps"` ArtifactQuality string `json:"artifact_quality"` ArtifactRarityCommonPct float64 `json:"artifact_rarity_common_pct"` ArtifactRarityUncommonPct float64 `json:"artifact_rarity_uncommon_pct"` ArtifactRarityRarePct float64 `json:"artifact_rarity_rare_pct"` ArtifactRarityUltraRarePct float64 `json:"artifact_rarity_ultra_rare_pct"` ArtifactRaritySpecialIllustrationPct float64 `json:"artifact_rarity_special_illustration_pct"` } type Manager struct { db *sql.DB mu sync.RWMutex v Runtime } func envInt(k string, def int) int { if s := os.Getenv(k); s != "" { if n, err := strconv.Atoi(s); err == nil { return n } } return def } func envString(k, def string) string { if s := strings.TrimSpace(os.Getenv(k)); s != "" { return s } return def } func envFloat(k string, def float64) float64 { if s := strings.TrimSpace(os.Getenv(k)); s != "" { if n, err := strconv.ParseFloat(s, 64); err == nil { return n } } return def } func Defaults() Runtime { return Runtime{ GuessMinIntervalSec: envInt("DEFAULT_GUESS_MIN_INTERVAL_SEC", 10), ClientSubmitIntervalSec: envInt("DEFAULT_CLIENT_SUBMIT_INTERVAL_SEC", 11), GuessLotteryWindowSec: envInt("DEFAULT_GUESS_LOTTERY_WINDOW_SEC", 60), GuessLotteryMaxAccepted: envInt("DEFAULT_GUESS_LOTTERY_MAX_ACCEPTED", 0), BeaconHuntEnabled: envInt("DEFAULT_BEACON_HUNT_ENABLED", 0), BeaconBonusWeight: envInt("DEFAULT_BEACON_BONUS_WEIGHT", 2), SybilProofOfWorkBits: envInt("DEFAULT_SYBIL_POW_BITS", 15), SybilWarmupSec: envInt("DEFAULT_SYBIL_WARMUP_SEC", 15), OpenAIMaxCalls1H: envInt("DEFAULT_OPENAI_MAX_CALLS_1H", 20), OpenAIMaxCalls24H: envInt("DEFAULT_OPENAI_MAX_CALLS_24H", 100), OpenAIMaxCost24HUSD: envFloat("DEFAULT_OPENAI_MAX_COST_24H_USD", 5.00), OpenAIBudgetReserveUSD: envFloat("DEFAULT_OPENAI_BUDGET_RESERVE_USD", 0.20), TaskRangeBits: envInt("DEFAULT_TASK_RANGE_BITS", 28), ActiveTaskCount: envInt("DEFAULT_ACTIVE_TASK_COUNT", 1), PresenceTTLSec: envInt("DEFAULT_PRESENCE_TTL_SEC", 35), DefaultMaxNodes: envInt("DEFAULT_MAX_NODES", 2000), PublicScorePrecision: envInt("DEFAULT_PUBLIC_SCORE_PRECISION", 2), PlaceEnabled: envInt("DEFAULT_PLACE_ENABLED", 1), PlaceWidth: envInt("DEFAULT_PLACE_WIDTH", 256), PlaceHeight: envInt("DEFAULT_PLACE_HEIGHT", 256), PlacePointsPerScore: envInt("DEFAULT_PLACE_POINTS_PER_SCORE", 100), PlaceDrawPoints: envInt("DEFAULT_PLACE_DRAW_POINTS", 25), PlaceDrawBeaconMultiplier: envInt("DEFAULT_PLACE_DRAW_BEACON_MULTIPLIER", 1), PlaceTimePoints: envInt("DEFAULT_PLACE_TIME_POINTS", 20), PlaceTimeIntervalSec: envInt("DEFAULT_PLACE_TIME_INTERVAL_SEC", 60), PlaceTimeMaxGapSec: envInt("DEFAULT_PLACE_TIME_MAX_GAP_SEC", 120), PlacePixelCost: envInt("DEFAULT_PLACE_PIXEL_COST", 100), ArtifactPreset: strings.ToLower(envString("ARTIFACT_PRESET", "raccoon_full_art_v1")), ArtifactProvider: strings.ToLower(envString("ARTIFACT_PROVIDER", "openai")), ArtifactModel: envString("ARTIFACT_MODEL", "gpt-image-2"), ArtifactPrompt: envString("ARTIFACT_PROMPT", "Optional global creative override for legacy artifact providers. The raccoon_full_art_v1 preset uses its built-in structured master prompt and task-specific NFT instructions."), ArtifactNegativePrompt: envString("ARTIFACT_NEGATIVE_PROMPT", "text, watermark, logo, signature, low quality, blurry, malformed"), ArtifactWidth: envInt("ARTIFACT_WIDTH", 1024), ArtifactHeight: envInt("ARTIFACT_HEIGHT", 1536), ArtifactSteps: envInt("ARTIFACT_STEPS", 28), ArtifactQuality: strings.ToLower(envString("ARTIFACT_QUALITY", "medium")), ArtifactRarityCommonPct: envFloat("DEFAULT_ARTIFACT_RARITY_COMMON_PCT", 58.00), ArtifactRarityUncommonPct: envFloat("DEFAULT_ARTIFACT_RARITY_UNCOMMON_PCT", 29.50), ArtifactRarityRarePct: envFloat("DEFAULT_ARTIFACT_RARITY_RARE_PCT", 9.90), ArtifactRarityUltraRarePct: envFloat("DEFAULT_ARTIFACT_RARITY_ULTRA_RARE_PCT", 2.25), ArtifactRaritySpecialIllustrationPct: envFloat("DEFAULT_ARTIFACT_RARITY_SPECIAL_ILLUSTRATION_PCT", 0.35), } } func New(ctx context.Context, db *sql.DB) (*Manager, error) { m := &Manager{db: db, v: Defaults()} b, _ := json.Marshal(m.v) now := time.Now().UTC().UnixMilli() _, err := db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES('runtime',?,?) ON CONFLICT(key) DO NOTHING`, string(b), now) if err != nil { return nil, err } if err := m.Refresh(ctx); err != nil { return nil, err } if err := m.migrateRIFTMediumDefault(ctx); err != nil { return nil, err } return m, nil } // migrateRIFTMediumDefault upgrades deployments created by the first RIFT // release, where high quality was hard-coded as the collection default. The // migration runs once only; after that an operator can still deliberately set // high (or low) without it being overwritten on restart. func (m *Manager) migrateRIFTMediumDefault(ctx context.Context) error { const marker = "migration_rift_medium_quality_v1" var n int if err := m.db.QueryRowContext(ctx, `SELECT count(*) FROM settings WHERE key=?`, marker).Scan(&n); err != nil { return err } if n > 0 { return nil } // A deployment that explicitly configured ARTIFACT_QUALITY keeps that // operator choice. The migration only changes the former implicit RIFT // default. if strings.TrimSpace(os.Getenv("ARTIFACT_QUALITY")) != "" { _, err := m.db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO NOTHING`, marker, "explicit-env-preserved", time.Now().UTC().UnixMilli()) return err } v := m.Get() if strings.EqualFold(strings.TrimSpace(v.ArtifactPreset), "raccoon_full_art_v1") && strings.EqualFold(strings.TrimSpace(v.ArtifactQuality), "high") { v.ArtifactQuality = "medium" b, _ := json.Marshal(v) now := time.Now().UTC().UnixMilli() if _, err := m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), now); err != nil { return err } m.mu.Lock() m.v = v m.mu.Unlock() } _, err := m.db.ExecContext(ctx, `INSERT INTO settings(key,value,updated_at) VALUES(?,?,?) ON CONFLICT(key) DO NOTHING`, marker, "done", time.Now().UTC().UnixMilli()) return err } func (m *Manager) Get() Runtime { m.mu.RLock() defer m.mu.RUnlock() return m.v } func (m *Manager) Refresh(ctx context.Context) error { var raw string if err := m.db.QueryRowContext(ctx, `SELECT value FROM settings WHERE key='runtime'`).Scan(&raw); err != nil { return err } // Start from current defaults so databases created by older Neural Hunt // versions automatically receive newly introduced settings. var stored map[string]json.RawMessage _ = json.Unmarshal([]byte(raw), &stored) _, hadArtifactPreset := stored["artifact_preset"] v := Defaults() if err := json.Unmarshal([]byte(raw), &v); err != nil { return err } // One-time upgrade for the bundled pre-collection database: older builds // shipped the offline square SVG preset. The new collection is designed to // work after only OPENAI_API_KEY + model are configured, so that exact legacy // default is migrated to OpenAI portrait generation automatically. Explicit // non-local provider choices are preserved. if !hadArtifactPreset { if strings.EqualFold(v.ArtifactProvider, "local") && v.ArtifactWidth == 1024 && v.ArtifactHeight == 1024 { v.ArtifactPreset = "raccoon_full_art_v1" v.ArtifactProvider = "openai" v.ArtifactWidth = 1024 v.ArtifactHeight = 1536 v.ArtifactQuality = "medium" } else { // Existing deployments that deliberately chose another provider keep // their old behavior until they explicitly opt into the collection preset. v.ArtifactPreset = "legacy" } if b, err := json.Marshal(v); err == nil { _, _ = m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), time.Now().UTC().UnixMilli()) } } if err := Validate(v); err != nil { return err } m.mu.Lock() m.v = v m.mu.Unlock() return nil } func (m *Manager) Update(ctx context.Context, v Runtime) error { if err := Validate(v); err != nil { return err } b, _ := json.Marshal(v) if _, err := m.db.ExecContext(ctx, `UPDATE settings SET value=?,updated_at=? WHERE key='runtime'`, string(b), time.Now().UTC().UnixMilli()); err != nil { return err } m.mu.Lock() m.v = v m.mu.Unlock() return nil } func Validate(v Runtime) error { if v.GuessMinIntervalSec < 1 || v.GuessMinIntervalSec > 3600 { return fmt.Errorf("guess_min_interval_sec must be 1..3600") } if v.ClientSubmitIntervalSec <= v.GuessMinIntervalSec || v.ClientSubmitIntervalSec > 7200 { return fmt.Errorf("client_submit_interval_sec must be > guess_min_interval_sec and <= 7200") } if v.GuessLotteryWindowSec < 1 || v.GuessLotteryWindowSec > 3600 { return fmt.Errorf("guess_lottery_window_sec must be 1..3600") } if v.GuessLotteryMaxAccepted < 0 || v.GuessLotteryMaxAccepted > 100000 { return fmt.Errorf("guess_lottery_max_accepted must be 0..100000 (0 disables lottery)") } if v.BeaconHuntEnabled < 0 || v.BeaconHuntEnabled > 1 { return fmt.Errorf("beacon_hunt_enabled must be 0 or 1") } if v.BeaconBonusWeight < 1 || v.BeaconBonusWeight > 10 { return fmt.Errorf("beacon_bonus_weight must be 1..10") } if v.SybilProofOfWorkBits < 0 || v.SybilProofOfWorkBits > 22 { return fmt.Errorf("sybil_pow_bits must be 0..22") } if v.SybilWarmupSec < 0 || v.SybilWarmupSec > 86400 { return fmt.Errorf("sybil_warmup_sec must be 0..86400") } if v.OpenAIMaxCalls1H < 0 || v.OpenAIMaxCalls1H > 100000 { return fmt.Errorf("openai_max_calls_1h must be 0..100000") } if v.OpenAIMaxCalls24H < 0 || v.OpenAIMaxCalls24H > 1000000 { return fmt.Errorf("openai_max_calls_24h must be 0..1000000") } if v.OpenAIMaxCost24HUSD < 0 || v.OpenAIMaxCost24HUSD > 100000 { return fmt.Errorf("openai_max_cost_24h_usd must be 0..100000") } if v.OpenAIBudgetReserveUSD < 0 || v.OpenAIBudgetReserveUSD > 10000 { return fmt.Errorf("openai_budget_reserve_usd must be 0..10000") } if v.TaskRangeBits < 8 || v.TaskRangeBits > 128 { return fmt.Errorf("task_range_bits must be 8..128") } if v.ActiveTaskCount < 1 || v.ActiveTaskCount > 64 { return fmt.Errorf("active_task_count must be 1..64") } if v.PresenceTTLSec < 15 || v.PresenceTTLSec > 600 { return fmt.Errorf("presence_ttl_sec must be 15..600") } if v.DefaultMaxNodes < 50 || v.DefaultMaxNodes > 100000 { return fmt.Errorf("default_max_nodes must be 50..100000") } if v.PublicScorePrecision < 0 || v.PublicScorePrecision > 6 { return fmt.Errorf("public_score_precision must be 0..6") } if v.PlaceEnabled < 0 || v.PlaceEnabled > 1 { return fmt.Errorf("place_enabled must be 0 or 1") } if v.PlaceWidth < 32 || v.PlaceWidth > 1024 || v.PlaceHeight < 32 || v.PlaceHeight > 1024 { return fmt.Errorf("place dimensions must be 32..1024") } if v.PlacePointsPerScore < 1 || v.PlacePointsPerScore > 1000000 { return fmt.Errorf("place_points_per_score must be 1..1000000") } if v.PlaceDrawPoints < 0 || v.PlaceDrawPoints > 100000000 { return fmt.Errorf("place_draw_points must be 0..100000000") } if v.PlaceDrawBeaconMultiplier < 0 || v.PlaceDrawBeaconMultiplier > 1 { return fmt.Errorf("place_draw_beacon_multiplier must be 0 or 1") } if v.PlaceTimePoints < 0 || v.PlaceTimePoints > 100000000 { return fmt.Errorf("place_time_points must be 0..100000000") } if v.PlaceTimeIntervalSec < 5 || v.PlaceTimeIntervalSec > 86400 { return fmt.Errorf("place_time_interval_sec must be 5..86400") } if v.PlaceTimeMaxGapSec < 5 || v.PlaceTimeMaxGapSec > 86400 { return fmt.Errorf("place_time_max_gap_sec must be 5..86400") } if v.PlacePixelCost < 1 || v.PlacePixelCost > 100000000 { return fmt.Errorf("place_pixel_cost must be 1..100000000") } switch strings.ToLower(strings.TrimSpace(v.ArtifactPreset)) { case "legacy", "raccoon_full_art_v1": default: return fmt.Errorf("artifact_preset must be legacy or raccoon_full_art_v1") } switch strings.ToLower(v.ArtifactProvider) { case "local", "openai", "comfyui", "a1111", "auto": default: return fmt.Errorf("artifact_provider must be local, openai, comfyui, a1111 or auto") } if strings.TrimSpace(v.ArtifactModel) == "" { return fmt.Errorf("artifact_model must not be empty") } if len(v.ArtifactPrompt) < 10 || len(v.ArtifactPrompt) > 12000 { return fmt.Errorf("artifact_prompt length must be 10..12000") } if len(v.ArtifactNegativePrompt) > 8000 { return fmt.Errorf("artifact_negative_prompt too long") } if v.ArtifactWidth < 256 || v.ArtifactWidth > 4096 || v.ArtifactHeight < 256 || v.ArtifactHeight > 4096 { return fmt.Errorf("artifact dimensions must be 256..4096") } if v.ArtifactSteps < 1 || v.ArtifactSteps > 200 { return fmt.Errorf("artifact_steps must be 1..200") } switch strings.ToLower(v.ArtifactQuality) { case "low", "medium", "high", "auto": default: return fmt.Errorf("artifact_quality must be low, medium, high or auto") } rarityValues := []struct { name string v float64 }{ {"artifact_rarity_common_pct", v.ArtifactRarityCommonPct}, {"artifact_rarity_uncommon_pct", v.ArtifactRarityUncommonPct}, {"artifact_rarity_rare_pct", v.ArtifactRarityRarePct}, {"artifact_rarity_ultra_rare_pct", v.ArtifactRarityUltraRarePct}, {"artifact_rarity_special_illustration_pct", v.ArtifactRaritySpecialIllustrationPct}, } total := 0.0 for _, x := range rarityValues { if x.v < 0 || x.v > 100 { return fmt.Errorf("%s must be 0..100", x.name) } total += x.v } if math.Abs(total-100.0) > 0.001 { return fmt.Errorf("artifact rarity percentages must sum to exactly 100 (currently %.4f)", total) } return nil } func (m *Manager) Run(ctx context.Context) { t := time.NewTicker(5 * time.Second) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: _ = m.Refresh(ctx) } } }