package app import ( "encoding/json" "io" "net/http" "strings" "time" ) func handleTransportClientCardGet(w http.ResponseWriter, r *http.Request, id string) { if handleTransportVirtualClientCardGet(w, r, id) { return } transportMu.Lock() st := loadTransportClientsState() transportMu.Unlock() idx := findTransportClientIndex(st.Items, id) if idx < 0 { writeJSON(w, http.StatusNotFound, TransportClientsResponse{OK: false, Message: "not found"}) return } item := st.Items[idx] writeJSON(w, http.StatusOK, TransportClientsResponse{OK: true, Message: "ok", Item: &item}) } func handleTransportClientCardPatch(w http.ResponseWriter, r *http.Request, id string) { if isTransportPolicyVirtualClientID(id) { status, resp := transportVirtualClientReadOnlyResponse() writeJSON(w, status, resp) return } var body TransportClientPatchRequest if r.Body != nil { defer r.Body.Close() if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil && err != io.EOF { http.Error(w, "bad json", http.StatusBadRequest) return } } lockIDs, ok := resolveTransportClientCardPatchLockIDs(id, body) if !ok { writeJSON(w, http.StatusNotFound, TransportClientsResponse{OK: false, Message: "not found"}) return } status := http.StatusOK resp := TransportClientsResponse{} withTransportIfaceLocks(lockIDs, func() { status, resp = executeTransportClientCardPatchLocked(id, body) }) if resp.OK { publishTransportRuntimeObservabilitySnapshotChanged("transport_client_updated", []string{id}, lockIDs) } writeJSON(w, status, resp) } func resolveTransportClientCardPatchLockIDs(id string, body TransportClientPatchRequest) ([]string, bool) { transportMu.Lock() st := loadTransportClientsState() idx := findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return nil, false } lockIDs := []string{st.Items[idx].IfaceID} if body.IfaceID != nil { lockIDs = append(lockIDs, normalizeTransportIfaceID(*body.IfaceID)) } transportMu.Unlock() return lockIDs, true } func executeTransportClientCardPatchLocked(id string, body TransportClientPatchRequest) (int, TransportClientsResponse) { transportMu.Lock() defer transportMu.Unlock() st := loadTransportClientsState() idx := findTransportClientIndex(st.Items, id) if idx < 0 { return http.StatusNotFound, TransportClientsResponse{OK: false, Message: "not found"} } it := st.Items[idx] if body.Name != nil { it.Name = strings.TrimSpace(*body.Name) } if body.IfaceID != nil { it.IfaceID = normalizeTransportIfaceID(*body.IfaceID) } if body.Enabled != nil { it.Enabled = *body.Enabled } if body.Config != nil { it.Config = cloneMap(body.Config) } if normCfg, _ := normalizeTransportClientConfig(it.Kind, it.Config); normCfg != nil || it.Config != nil { it.Config = normCfg } now := time.Now().UTC() it.UpdatedAt = now.Format(time.RFC3339) st.Items[idx] = it ifaces, err := syncTransportInterfacesWithClientsLocked(st.Items) if err != nil { return http.StatusOK, TransportClientsResponse{OK: false, Message: "interfaces sync failed: " + err.Error()} } it, _ = applyTransportIfaceBinding(st.Items[idx], ifaces, now) st.Items[idx] = it if err := saveTransportClientsState(st); err != nil { return http.StatusOK, TransportClientsResponse{OK: false, Message: "save failed: " + err.Error()} } return http.StatusOK, TransportClientsResponse{OK: true, Message: "updated", Item: &it} } func handleTransportClientCardDelete(w http.ResponseWriter, r *http.Request, id string) { if isTransportPolicyVirtualClientID(id) { status, resp := transportVirtualClientReadOnlyResponse() writeJSON(w, status, resp) return } force := strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("force")), "true") cleanupArtifacts := !strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("cleanup")), "false") lockIDs, ok := resolveTransportClientDeleteLockIDs(id) if !ok { writeJSON(w, http.StatusNotFound, TransportClientsResponse{OK: false, Message: "not found"}) return } status := http.StatusOK resp := TransportClientsResponse{} withTransportIfaceLocks(lockIDs, func() { status, resp = executeTransportClientCardDeleteLocked(id, force, cleanupArtifacts) }) if resp.OK { publishTransportRuntimeObservabilitySnapshotChanged("transport_client_deleted", []string{id}, lockIDs) } writeJSON(w, status, resp) } func resolveTransportClientDeleteLockIDs(id string) ([]string, bool) { transportMu.Lock() st := loadTransportClientsState() idx := findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return nil, false } lockIDs := []string{st.Items[idx].IfaceID} transportMu.Unlock() return lockIDs, true } func executeTransportClientCardDeleteLocked(id string, force bool, cleanupArtifacts bool) (int, TransportClientsResponse) { transportMu.Lock() st := loadTransportClientsState() idx := findTransportClientIndex(st.Items, id) if idx < 0 { transportMu.Unlock() return http.StatusNotFound, TransportClientsResponse{OK: false, Message: "not found"} } pol := loadTransportPolicyState() if !force { for _, it := range pol.Intents { if strings.TrimSpace(it.ClientID) == id { transportMu.Unlock() return http.StatusOK, TransportClientsResponse{ OK: false, Message: "client is used by active policy; set force=true to remove", } } } } removed := st.Items[idx] st.Items = append(st.Items[:idx], st.Items[idx+1:]...) if err := saveTransportClientsState(st); err != nil { transportMu.Unlock() return http.StatusOK, TransportClientsResponse{OK: false, Message: "save failed: " + err.Error()} } transportMu.Unlock() msg := "deleted" if cleanupArtifacts { cleanup := selectTransportBackend(removed).Cleanup(removed) if !cleanup.OK { cleanupErr := strings.TrimSpace(cleanup.Stderr) if cleanupErr == "" { cleanupErr = strings.TrimSpace(cleanup.Message) } if cleanupErr == "" { cleanupErr = "cleanup failed" } msg = msg + "; cleanup warning: " + cleanupErr } } return http.StatusOK, TransportClientsResponse{OK: true, Message: msg} }