package main import ( "database/sql" "fmt" "strconv" "strings" "time" ) const ( stagePhasePreliminary = "preliminary" stagePhasePrelimTie = "prelim_tiebreak" stagePhaseFinal = "final" stagePhaseFinalTie = "final_tiebreak" ) var allowedStagePhases = map[string]bool{ stagePhasePreliminary: true, stagePhasePrelimTie: true, stagePhaseFinal: true, stagePhaseFinalTie: true, } func normalizeStagePhase(raw string) (string, bool) { phase := strings.ToLower(strings.TrimSpace(raw)) if !allowedStagePhases[phase] { return "", false } return phase, true } func normalizeStageGroup(phase string, raw string) (string, bool) { group := strings.TrimSpace(raw) switch phase { case stagePhasePreliminary: if strings.EqualFold(group, positionBoardUnassigned) { return positionBoardUnassigned, true } group = normalizePreliminaryGroupKey(group) if group == "" { return "", false } return group, true case stagePhaseFinal: parsed, err := strconv.Atoi(group) if err != nil || (parsed != 1 && parsed != 2) { return "", false } return strconv.Itoa(parsed), true case stagePhasePrelimTie, stagePhaseFinalTie: parsed, err := strconv.Atoi(group) if err != nil || parsed <= 0 { return "", false } return strconv.Itoa(parsed), true default: return "", false } } func normalizeStageRound(phase string, round int) int { switch phase { case stagePhasePreliminary: if round < 1 { return 1 } if round > 3 { return 3 } return round case stagePhaseFinal: if round < 1 { return 1 } if round > 2 { return 2 } return round default: return 1 } } func normalizeTimestamp(raw string) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return "" } if parsed, err := time.Parse(time.RFC3339Nano, trimmed); err == nil { return parsed.UTC().Format(time.RFC3339) } if parsed, err := time.Parse("2006-01-02 15:04:05", trimmed); err == nil { return parsed.UTC().Format(time.RFC3339) } return trimmed } func scanStageSession(scanner interface{ Scan(dest ...any) error }) (*StageSessionState, error) { var item StageSessionState var ended sql.NullString if err := scanner.Scan(&item.ID, &item.Phase, &item.GroupKey, &item.Round, &item.StartedAt, &ended); err != nil { return nil, err } item.Phase = strings.ToLower(strings.TrimSpace(item.Phase)) item.GroupKey = strings.TrimSpace(item.GroupKey) item.StartedAt = normalizeTimestamp(item.StartedAt) if ended.Valid { item.EndedAt = normalizeTimestamp(ended.String) } return &item, nil } func (a *App) readCurrentStage() (*StageSessionState, error) { row := a.db.QueryRow(` SELECT id, phase, group_key, round, started_at, ended_at FROM stage_sessions WHERE ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1 `) item, err := scanStageSession(row) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("read current stage: %w", err) } return item, nil } func (a *App) readLastStage() (*StageSessionState, error) { row := a.db.QueryRow(` SELECT id, phase, group_key, round, started_at, ended_at FROM stage_sessions ORDER BY started_at DESC, id DESC LIMIT 1 `) item, err := scanStageSession(row) if err != nil { if err == sql.ErrNoRows { return nil, nil } return nil, fmt.Errorf("read last stage: %w", err) } return item, nil } func (a *App) readStageHistory() ([]StageSessionState, error) { rows, err := a.db.Query(` SELECT id, phase, group_key, round, started_at, ended_at FROM stage_sessions ORDER BY started_at DESC, id DESC `) if err != nil { return nil, fmt.Errorf("query stage history: %w", err) } defer rows.Close() items := make([]StageSessionState, 0) for rows.Next() { item, scanErr := scanStageSession(rows) if scanErr != nil { return nil, fmt.Errorf("scan stage history: %w", scanErr) } items = append(items, *item) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("iterate stage history: %w", err) } return items, nil }