package app import ( "sort" "strings" "time" ) func normalizeTransportClientsState(st transportClientsState, forceRebalance bool) (transportClientsState, bool) { changed := false st.Version = transportStateVersion if st.Items == nil { st.Items = nil return st, false } out := make([]TransportClient, 0, len(st.Items)) byID := map[string]int{} for _, raw := range st.Items { it := raw id := sanitizeID(it.ID) if id == "" { changed = true continue } if it.ID != id { it.ID = id changed = true } ifaceID := normalizeTransportIfaceID(it.IfaceID) if it.IfaceID != ifaceID { it.IfaceID = ifaceID changed = true } kind := normalizeTransportKind(it.Kind) if kind == "" { kind = TransportClientSingBox changed = true } it.Kind = kind if normCfg, cfgChanged := normalizeTransportClientConfig(it.Kind, it.Config); cfgChanged { it.Config = normCfg changed = true } if strings.TrimSpace(it.Name) == "" { it.Name = id changed = true } status := normalizeTransportStatus(it.Status) if status != it.Status { it.Status = status changed = true } if !equalStringSlices(it.Capabilities, defaultTransportCapabilities(it.Kind)) { it.Capabilities = defaultTransportCapabilities(it.Kind) changed = true } if normRuntime, runtimeChanged := normalizeTransportRuntimeStored(it.Runtime, it.Kind, it.Config); runtimeChanged { it.Runtime = normRuntime changed = true } if strings.TrimSpace(it.Health.LastCheck) == "" { if strings.TrimSpace(it.UpdatedAt) != "" { it.Health.LastCheck = it.UpdatedAt } else { it.Health.LastCheck = time.Now().UTC().Format(time.RFC3339) } changed = true } if idx, ok := byID[it.ID]; ok { // Keep deterministic winner for duplicate IDs. if preferTransportClient(it, out[idx]) { out[idx] = it } changed = true continue } byID[it.ID] = len(out) out = append(out, it) } sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID }) usedTables := map[string]struct{}{} for i := range out { if strings.TrimSpace(out[i].RoutingTable) == "" { continue } wantTable := normalizeTransportRoutingTable(out[i].RoutingTable, transportRoutingTableForID(out[i].ID)) if out[i].RoutingTable != wantTable { out[i].RoutingTable = wantTable changed = true } usedTables[wantTable] = struct{}{} } for i := range out { if strings.TrimSpace(out[i].RoutingTable) != "" { continue } wantTable := transportRoutingTableForIDUnique(out[i].ID, usedTables) if out[i].RoutingTable != wantTable { out[i].RoutingTable = wantTable changed = true } } norm, allocChanged := reconcileTransportAllocations(out, forceRebalance) if allocChanged { changed = true } st.Items = norm return st, changed } func normalizeTransportStatus(st TransportClientStatus) TransportClientStatus { switch st { case TransportClientStarting, TransportClientUp, TransportClientDegraded, TransportClientDown: return st default: return TransportClientDown } } func equalStringSlices(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func preferTransportClient(cand, cur TransportClient) bool { cu := strings.TrimSpace(cand.UpdatedAt) ou := strings.TrimSpace(cur.UpdatedAt) if cu != ou { if cu == "" { return false } if ou == "" { return true } return cu > ou } if cand.Enabled != cur.Enabled { return cand.Enabled } return strings.TrimSpace(cand.Name) > strings.TrimSpace(cur.Name) }