2026-04-01 11:47:03 +04:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
2026-04-03 09:55:36 +04:00
|
|
|
"strconv"
|
2026-04-01 11:47:03 +04:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-04-03 09:55:36 +04:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 11:47:03 +04:00
|
|
|
func (a *App) readSettings() (AppSettings, error) {
|
2026-04-03 09:55:36 +04:00
|
|
|
live := defaultLiveModeSettings()
|
|
|
|
|
|
|
|
|
|
viewProofRaw, err := a.readSettingValue(settingViewProofInView)
|
2026-04-01 11:47:03 +04:00
|
|
|
if err != nil {
|
2026-04-03 09:55:36 +04:00
|
|
|
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
|
|
|
|
|
}
|
2026-04-01 11:47:03 +04:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-03 09:55:36 +04:00
|
|
|
|
|
|
|
|
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
|
2026-04-01 11:47:03 +04:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 09:55:36 +04:00
|
|
|
func upsertSetting(tx *sql.Tx, key string, value string) error {
|
|
|
|
|
_, err := tx.Exec(`
|
2026-04-01 11:47:03 +04:00
|
|
|
INSERT INTO app_settings(key, value, updated_at)
|
2026-04-03 09:55:36 +04:00
|
|
|
VALUES(?, ?, CURRENT_TIMESTAMP)
|
2026-04-01 11:47:03 +04:00
|
|
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
2026-04-03 09:55:36 +04:00
|
|
|
`, key, value)
|
2026-04-01 11:47:03 +04:00
|
|
|
if err != nil {
|
2026-04-03 09:55:36 +04:00
|
|
|
return fmt.Errorf("update app setting %s: %w", key, err)
|
2026-04-01 11:47:03 +04:00
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-03 09:55:36 +04:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 11:47:03 +04:00
|
|
|
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"
|
|
|
|
|
}
|
2026-04-03 09:55:36 +04:00
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
}
|