package main import ( "encoding/csv" "net/http" "os" "path/filepath" "strings" "sync" ) var ( rulesMu sync.RWMutex rules = map[string]map[string]Rule{} ) func loadRules(dir string) error { entries, err := os.ReadDir(dir) if err != nil { return err } out := map[string]map[string]Rule{} for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".csv") { continue } lang := strings.TrimSuffix(e.Name(), ".csv") f, err := os.Open(filepath.Join(dir, e.Name())) if err != nil { continue } r := csv.NewReader(f) r.FieldsPerRecord = -1 records, err := r.ReadAll() f.Close() if err != nil { continue } langMap := map[string]Rule{} for i, rec := range records { if len(rec) < 4 { continue } if i == 0 { lower := strings.ToLower(strings.TrimSpace(rec[0])) if lower == "number" || lower == "rule_number" || lower == "rule" { continue } } ru := Rule{ Number: strings.TrimSpace(rec[0]), Text: strings.TrimSpace(rec[1]), SuggestedPenalty: strings.TrimSpace(rec[2]), EscalationMode: strings.TrimSpace(rec[3]), } if ru.EscalationMode == "" { ru.EscalationMode = "same" } if strings.HasPrefix(ru.EscalationMode, "escalate:") { tiers := strings.Split(strings.TrimPrefix(ru.EscalationMode, "escalate:"), "|") for j, t := range tiers { tiers[j] = strings.TrimSpace(t) } ru.EscalationTiers = tiers ru.EscalationMode = "escalate" } langMap[ru.Number] = ru } out[lang] = langMap } rulesMu.Lock() rules = out rulesMu.Unlock() return nil } func getRules(lang string) []Rule { rulesMu.RLock() defer rulesMu.RUnlock() m, ok := rules[lang] if !ok { m = rules["en"] } out := make([]Rule, 0, len(m)) for _, r := range m { out = append(out, r) } return out } func registerRuleRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /api/rules", requireAuth(handleListRules)) mux.HandleFunc("GET /api/rules/languages", requireAuth(handleListRuleLanguages)) mux.HandleFunc("POST /api/rules/reload", requireAdmin(handleReloadRules)) } func handleListRuleLanguages(w http.ResponseWriter, r *http.Request) { rulesMu.RLock() defer rulesMu.RUnlock() out := make([]string, 0, len(rules)) for lang := range rules { out = append(out, lang) } writeJSON(w, http.StatusOK, out) } func handleListRules(w http.ResponseWriter, r *http.Request) { lang := r.URL.Query().Get("lang") if lang == "" { u := userFromCtx(r) lang = u.Language } writeJSON(w, http.StatusOK, getRules(lang)) } func handleReloadRules(w http.ResponseWriter, r *http.Request) { dir := os.Getenv("RULES_DIR") if dir == "" { dir = "rules" } if err := loadRules(dir); err != nil { writeError(w, http.StatusInternalServerError, "load_error") return } w.WriteHeader(http.StatusNoContent) }