package main import ( "database/sql" "fmt" "strconv" "strings" ) const ( settingViewProofInView = "view_proof_in_view" settingLiveActiveView = "live_active_view" settingLiveShowGroupLive = "live_show_group_live" settingLiveGroupDisplayMode = "live_group_display_mode" settingLiveGroupFixedCode = "live_group_fixed_code" settingLiveShowPrelimTie = "live_show_prelim_tie" settingLiveShowPrelimOverall = "live_show_prelim_overall" settingLiveShowFinalGroups = "live_show_final_groups" settingLiveFinalDisplayMode = "live_final_display_mode" settingLiveFinalFixedGroup = "live_final_fixed_group" settingLiveShowFinalTie = "live_show_final_tie" settingLiveShowPodium = "live_show_podium" settingLiveRotationInterval = "live_rotation_interval_sec" settingLiveRotationPlayers = "live_rotation_player_count" ) func defaultLiveModeSettings() LiveModeSettings { return LiveModeSettings{ ActiveView: "group_live", ShowGroupLive: true, GroupDisplayMode: "rotate", GroupFixedCode: "", ShowPrelimTie: true, ShowPrelimOverall: true, ShowFinalGroups: true, FinalDisplayMode: "rotate", FinalFixedGroup: 1, ShowFinalTie: true, ShowPodium: true, RotationIntervalSec: 5, RotationPlayerCount: 12, } } func (a *App) readSettings() (AppSettings, error) { live := defaultLiveModeSettings() viewProofRaw, err := a.readSettingValue(settingViewProofInView) if err != nil { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingViewProofInView, err) } if raw, err := a.readSettingValue(settingLiveShowGroupLive); err == nil { live.ShowGroupLive = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowGroupLive, err) } if raw, err := a.readSettingValue(settingLiveActiveView); err == nil { live.ActiveView = normalizeLiveActiveView(raw, live.ActiveView) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveActiveView, err) } if raw, err := a.readSettingValue(settingLiveGroupDisplayMode); err == nil { live.GroupDisplayMode = normalizeLiveDisplayMode(raw, live.GroupDisplayMode) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveGroupDisplayMode, err) } if raw, err := a.readSettingValue(settingLiveGroupFixedCode); err == nil { live.GroupFixedCode = strings.ToUpper(strings.TrimSpace(raw)) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveGroupFixedCode, err) } if raw, err := a.readSettingValue(settingLiveShowPrelimTie); err == nil { live.ShowPrelimTie = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPrelimTie, err) } if raw, err := a.readSettingValue(settingLiveShowPrelimOverall); err == nil { live.ShowPrelimOverall = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPrelimOverall, err) } if raw, err := a.readSettingValue(settingLiveShowFinalGroups); err == nil { live.ShowFinalGroups = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowFinalGroups, err) } if raw, err := a.readSettingValue(settingLiveFinalDisplayMode); err == nil { live.FinalDisplayMode = normalizeLiveDisplayMode(raw, live.FinalDisplayMode) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveFinalDisplayMode, err) } if raw, err := a.readSettingValue(settingLiveFinalFixedGroup); err == nil { live.FinalFixedGroup = normalizeLiveFinalFixedGroup(raw, live.FinalFixedGroup) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveFinalFixedGroup, err) } if raw, err := a.readSettingValue(settingLiveShowFinalTie); err == nil { live.ShowFinalTie = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowFinalTie, err) } if raw, err := a.readSettingValue(settingLiveShowPodium); err == nil { live.ShowPodium = settingBool(raw) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveShowPodium, err) } if raw, err := a.readSettingValue(settingLiveRotationInterval); err == nil { live.RotationIntervalSec = normalizeRotationInterval(raw, live.RotationIntervalSec) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveRotationInterval, err) } if raw, err := a.readSettingValue(settingLiveRotationPlayers); err == nil { live.RotationPlayerCount = normalizeRotationPlayerCount(raw, live.RotationPlayerCount) } else { return AppSettings{}, fmt.Errorf("read app setting %s: %w", settingLiveRotationPlayers, err) } return AppSettings{ ViewProofInView: settingBool(viewProofRaw), LiveMode: live, }, nil } func (a *App) updateSettings(req AdminSettingsUpdateRequest) error { tx, err := a.db.Begin() if err != nil { return fmt.Errorf("begin settings transaction: %w", err) } defer tx.Rollback() hasUpdate := false if req.ViewProofInView != nil { hasUpdate = true if err := upsertSetting(tx, settingViewProofInView, settingString(*req.ViewProofInView)); err != nil { return err } } if req.LiveMode != nil { patch := req.LiveMode if patch.ActiveView != nil { hasUpdate = true value := normalizeLiveActiveView(*patch.ActiveView, "group_live") if err := upsertSetting(tx, settingLiveActiveView, value); err != nil { return err } } if patch.ShowGroupLive != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowGroupLive, settingString(*patch.ShowGroupLive)); err != nil { return err } } if patch.GroupDisplayMode != nil { hasUpdate = true value := normalizeLiveDisplayMode(*patch.GroupDisplayMode, "rotate") if err := upsertSetting(tx, settingLiveGroupDisplayMode, value); err != nil { return err } } if patch.GroupFixedCode != nil { hasUpdate = true value := strings.ToUpper(strings.TrimSpace(*patch.GroupFixedCode)) if err := upsertSetting(tx, settingLiveGroupFixedCode, value); err != nil { return err } } if patch.ShowPrelimTie != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowPrelimTie, settingString(*patch.ShowPrelimTie)); err != nil { return err } } if patch.ShowPrelimOverall != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowPrelimOverall, settingString(*patch.ShowPrelimOverall)); err != nil { return err } } if patch.ShowFinalGroups != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowFinalGroups, settingString(*patch.ShowFinalGroups)); err != nil { return err } } if patch.FinalDisplayMode != nil { hasUpdate = true value := normalizeLiveDisplayMode(*patch.FinalDisplayMode, "rotate") if err := upsertSetting(tx, settingLiveFinalDisplayMode, value); err != nil { return err } } if patch.FinalFixedGroup != nil { hasUpdate = true value := normalizeLiveFinalFixedGroupInt(*patch.FinalFixedGroup) if err := upsertSetting(tx, settingLiveFinalFixedGroup, strconv.Itoa(value)); err != nil { return err } } if patch.ShowFinalTie != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowFinalTie, settingString(*patch.ShowFinalTie)); err != nil { return err } } if patch.ShowPodium != nil { hasUpdate = true if err := upsertSetting(tx, settingLiveShowPodium, settingString(*patch.ShowPodium)); err != nil { return err } } if patch.RotationIntervalSec != nil { hasUpdate = true value := normalizeRotationIntervalInt(*patch.RotationIntervalSec) if err := upsertSetting(tx, settingLiveRotationInterval, strconv.Itoa(value)); err != nil { return err } } if patch.RotationPlayerCount != nil { hasUpdate = true value := normalizeRotationPlayerCountInt(*patch.RotationPlayerCount) if err := upsertSetting(tx, settingLiveRotationPlayers, strconv.Itoa(value)); err != nil { return err } } } if !hasUpdate { return fmt.Errorf("no settings to update") } if err := tx.Commit(); err != nil { return fmt.Errorf("commit settings transaction: %w", err) } return nil } func upsertSetting(tx *sql.Tx, key string, value string) error { _, err := tx.Exec(` INSERT INTO app_settings(key, value, updated_at) VALUES(?, ?, CURRENT_TIMESTAMP) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP `, key, value) if err != nil { return fmt.Errorf("update app setting %s: %w", key, err) } return nil } func (a *App) readSettingValue(key string) (string, error) { var raw string err := a.db.QueryRow(`SELECT value FROM app_settings WHERE key = ?`, key).Scan(&raw) if err != nil { if err == sql.ErrNoRows { return "", nil } return "", err } return raw, nil } func settingBool(value string) bool { switch strings.TrimSpace(strings.ToLower(value)) { case "1", "true", "yes", "on": return true default: return false } } func settingString(value bool) string { if value { return "1" } return "0" } func normalizeLiveDisplayMode(value string, fallback string) string { normalized := strings.TrimSpace(strings.ToLower(value)) if normalized == "fixed" { return "fixed" } if normalized == "rotate" { return "rotate" } if strings.TrimSpace(strings.ToLower(fallback)) == "fixed" { return "fixed" } return "rotate" } func normalizeLiveFinalFixedGroup(value string, fallback int) int { parsed, err := strconv.Atoi(strings.TrimSpace(value)) if err != nil { return normalizeLiveFinalFixedGroupInt(fallback) } return normalizeLiveFinalFixedGroupInt(parsed) } func normalizeLiveFinalFixedGroupInt(value int) int { if value == 2 { return 2 } return 1 } func normalizeRotationInterval(raw string, fallback int) int { parsed, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil { return normalizeRotationIntervalInt(fallback) } return normalizeRotationIntervalInt(parsed) } func normalizeRotationIntervalInt(value int) int { if value < 3 { return 3 } if value > 30 { return 30 } return value } func normalizeRotationPlayerCount(raw string, fallback int) int { parsed, err := strconv.Atoi(strings.TrimSpace(raw)) if err != nil { return normalizeRotationPlayerCountInt(fallback) } return normalizeRotationPlayerCountInt(parsed) } func normalizeRotationPlayerCountInt(value int) int { if value < 3 { return 3 } if value > 40 { return 40 } return value } func normalizeLiveActiveView(value string, fallback string) string { switch strings.TrimSpace(strings.ToLower(value)) { case "group_live", "prelim_tie", "prelim_overall", "final_groups", "final_tie", "podium": return strings.TrimSpace(strings.ToLower(value)) default: if strings.TrimSpace(strings.ToLower(fallback)) != "" { return strings.TrimSpace(strings.ToLower(fallback)) } return "group_live" } }