package httpapi import ( "context" "encoding/json" "fmt" "time" "github.com/dtoro/oikos/internal/db/sqlcgen" "github.com/dtoro/oikos/internal/domain" "github.com/dtoro/oikos/internal/httpapi/gen" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) const ( defaultLimit = 50 maxLimit = 200 // graphNodeCap bounds the whole-graph view. The cognition transactional // types (execution, task) are audit records, not topology, and previously // crowded out every host/lxc/service; the default whole-graph view below // excludes them so the cap is spent on the actual fleet graph. Operators // still reach executions/tasks via list_entities. graphNodeCap = 2000 ) // actorInfo returns the caller's (type, label) from the request context, // falling back to operator/unknown when unset. func actorInfo(ctx context.Context) (string, string) { if a := GetActor(ctx); a != nil { typ := a.Type if typ == "" { typ = "operator" } label := a.Label if label == "" { label = a.ID } return typ, label } return "operator", "unknown" } func clampLimit(l *int) int { if l == nil { return defaultLimit } if *l < 1 { return 1 } if *l > maxLimit { return maxLimit } return *l } // resolveEntityID resolves a UUID-or-slug path/query value to the entity UUID. func (s *Server) resolveEntityID(ctx context.Context, idOrSlug string) (uuid.UUID, error) { if id, err := uuid.Parse(idOrSlug); err == nil { entity, err := sqlcgen.New(s.pool).GetEntityByID(ctx, id) if err != nil { return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug) } return entity.ID, nil } entity, err := sqlcgen.New(s.pool).GetEntityBySlug(ctx, idOrSlug) if err != nil { return uuid.Nil, fmt.Errorf("%w: %s", domain.ErrNotFound, idOrSlug) } return entity.ID, nil } // entityCols requires the entities table to be aliased as `e`, with // entity_status left-joined and aliased as `st` (see withEntityStatus). const entityCols = `e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at, st.health, st.last_check_at` func scanEntity(row pgx.Row) (gen.Entity, error) { var e gen.Entity var state *string var attrsJSON []byte var maint *time.Time var health *string var lastCheckAt *time.Time err := row.Scan(&e.Id, &e.Slug, &e.Type, &e.Name, &state, &attrsJSON, &maint, &e.Version, &e.CreatedAt, &e.UpdatedAt, &health, &lastCheckAt) if err != nil { return e, err } e.State = state e.MaintenanceUntil = maint if health != nil { h := gen.EntityHealth(*health) e.Health = &h } e.LastCheckAt = lastCheckAt var attrs map[string]any if len(attrsJSON) > 0 && json.Unmarshal(attrsJSON, &attrs) == nil && len(attrs) > 0 { e.Attributes = &attrs } return e, nil }