package app import ( "context" "fmt" "net/http" "strings" "sub2api-cn-relay-manager/internal/store/sqlite" ) type PortalLogicalGroupInfo struct { LogicalGroupID string `json:"logical_group_id"` DisplayName string `json:"display_name"` Description string `json:"description,omitempty"` Status string `json:"status"` StickyMode string `json:"sticky_mode,omitempty"` RoutePolicy string `json:"route_policy,omitempty"` PublicModels []PortalLogicalGroupModel `json:"public_models,omitempty"` RouteCount int `json:"route_count"` ActiveRouteCount int `json:"active_route_count"` CreatedAt string `json:"created_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"` } type PortalLogicalGroupModel struct { PublicModel string `json:"public_model"` Status string `json:"status,omitempty"` } func handleListPortalLogicalGroups(w http.ResponseWriter, r *http.Request, fn func(context.Context) ([]PortalLogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-portal-logical-groups action is not configured"}) return } groups, err := fn(r.Context()) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"logical_groups": groups}) } func handleGetPortalLogicalGroup(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) (PortalLogicalGroupInfo, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "get-portal-logical-group action is not configured"}) return } group, err := fn(r.Context(), strings.TrimSpace(r.PathValue("groupID"))) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"logical_group": group}) } func handleListPortalLogicalGroupModels(w http.ResponseWriter, r *http.Request, fn func(context.Context, string) ([]PortalLogicalGroupModel, error)) { if fn == nil { writeHTTPError(w, &httpError{StatusCode: http.StatusInternalServerError, Code: "server_misconfigured", Message: "list-portal-logical-group-models action is not configured"}) return } models, err := fn(r.Context(), strings.TrimSpace(r.PathValue("groupID"))) if err != nil { writeHTTPError(w, classifyError(err)) return } writeJSON(w, http.StatusOK, map[string]any{"models": models}) } func buildListPortalLogicalGroupsAction(sqliteDSN string) func(context.Context) ([]PortalLogicalGroupInfo, error) { return func(ctx context.Context) ([]PortalLogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() rows, err := store.LogicalGroups().List(ctx) if err != nil { return nil, err } items := make([]PortalLogicalGroupInfo, 0, len(rows)) for _, row := range rows { if !isPortalLogicalGroupVisible(row.Status) { continue } item, buildErr := buildPortalLogicalGroupInfo(ctx, store, row) if buildErr != nil { return nil, buildErr } items = append(items, item) } return items, nil } } func buildGetPortalLogicalGroupAction(sqliteDSN string) func(context.Context, string) (PortalLogicalGroupInfo, error) { return func(ctx context.Context, logicalGroupID string) (PortalLogicalGroupInfo, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return PortalLogicalGroupInfo{}, err } defer store.Close() group, err := getLogicalGroupRow(ctx, store, logicalGroupID) if err != nil { return PortalLogicalGroupInfo{}, err } if !isPortalLogicalGroupVisible(group.Status) { return PortalLogicalGroupInfo{}, fmt.Errorf("logical group %q not found", strings.TrimSpace(logicalGroupID)) } return buildPortalLogicalGroupInfo(ctx, store, group) } } func buildListPortalLogicalGroupModelsAction(sqliteDSN string) func(context.Context, string) ([]PortalLogicalGroupModel, error) { return func(ctx context.Context, logicalGroupID string) ([]PortalLogicalGroupModel, error) { store, err := sqlite.Open(ctx, sqliteDSN) if err != nil { return nil, err } defer store.Close() group, err := getLogicalGroupRow(ctx, store, logicalGroupID) if err != nil { return nil, err } if !isPortalLogicalGroupVisible(group.Status) { return nil, fmt.Errorf("logical group %q not found", strings.TrimSpace(logicalGroupID)) } rows, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, group.LogicalGroupID) if err != nil { return nil, err } return portalLogicalGroupModelsFromRows(rows), nil } } func buildPortalLogicalGroupInfo(ctx context.Context, store *sqlite.DB, group sqlite.LogicalGroup) (PortalLogicalGroupInfo, error) { modelRows, err := store.LogicalGroupModels().ListByLogicalGroupID(ctx, group.LogicalGroupID) if err != nil { return PortalLogicalGroupInfo{}, err } routeRows, err := store.LogicalGroupRoutes().ListByLogicalGroupID(ctx, group.LogicalGroupID) if err != nil { return PortalLogicalGroupInfo{}, err } routeCount := 0 activeRouteCount := 0 for _, route := range routeRows { routeCount++ if strings.EqualFold(strings.TrimSpace(route.Status), "active") { activeRouteCount++ } } return PortalLogicalGroupInfo{ LogicalGroupID: group.LogicalGroupID, DisplayName: group.DisplayName, Description: group.Description, Status: group.Status, StickyMode: group.StickyMode, RoutePolicy: group.RoutePolicy, PublicModels: portalLogicalGroupModelsFromRows(modelRows), RouteCount: routeCount, ActiveRouteCount: activeRouteCount, CreatedAt: group.CreatedAt, UpdatedAt: group.UpdatedAt, }, nil } func portalLogicalGroupModelsFromRows(rows []sqlite.LogicalGroupModel) []PortalLogicalGroupModel { result := make([]PortalLogicalGroupModel, 0, len(rows)) for _, row := range rows { if !isPortalLogicalGroupModelVisible(row.Status) { continue } result = append(result, PortalLogicalGroupModel{ PublicModel: row.PublicModel, Status: row.Status, }) } return result } func isPortalLogicalGroupVisible(status string) bool { return strings.EqualFold(strings.TrimSpace(status), "active") } func isPortalLogicalGroupModelVisible(status string) bool { status = strings.TrimSpace(status) return status == "" || strings.EqualFold(status, "active") }