     1→package graph
     2→
     3→// This file will be automatically regenerated based on the schema, any resolver
     4→// implementations
     5→// will be copied through when generating and any unknown code will be moved to the end.
     6→// Code generated by github.com/99designs/gqlgen version v0.17.87
     7→
     8→import (
     9→	"context"
    10→	"errors"
    11→	"fmt"
    12→	"math"
    13→	"regexp"
    14→	"slices"
    15→	"strconv"
    16→	"strings"
    17→	"time"
    18→
    19→	"github.com/ClusterCockpit/cc-backend/internal/config"
    20→	"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
    21→	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
    22→	"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
    23→	"github.com/ClusterCockpit/cc-backend/internal/repository"
    24→	"github.com/ClusterCockpit/cc-backend/pkg/archive"
    25→	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
    26→	ccunit "github.com/ClusterCockpit/cc-lib/v2/ccUnits"
    27→	"github.com/ClusterCockpit/cc-lib/v2/schema"
    28→)
    29→
    30→// Partitions is the resolver for the partitions field.
    31→func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) ([]string, error) {
    32→	return r.Repo.Partitions(obj.Name)
    33→}
    34→
    35→// StartTime is the resolver for the startTime field.
    36→func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) {
    37→	timestamp := time.Unix(obj.StartTime, 0)
    38→	return &timestamp, nil
    39→}
    40→
    41→// Tags is the resolver for the tags field.
    42→func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) {
    43→	return r.Repo.GetTags(repository.GetUserFromContext(ctx), obj.ID)
    44→}
    45→
    46→// ConcurrentJobs is the resolver for the concurrentJobs field.
    47→func (r *jobResolver) ConcurrentJobs(ctx context.Context, obj *schema.Job) (*model.JobLinkResultList, error) {
    48→	// FIXME: Make the hardcoded duration configurable
    49→	if obj.Shared != "none" && obj.Duration > 600 {
    50→		return r.Repo.FindConcurrentJobs(ctx, obj)
    51→	}
    52→
    53→	return nil, nil
    54→}
    55→
    56→// Footprint is the resolver for the footprint field.
    57→func (r *jobResolver) Footprint(ctx context.Context, obj *schema.Job) ([]*model.FootprintValue, error) {
    58→	rawFootprint, err := r.Repo.FetchFootprint(obj)
    59→	if err != nil {
    60→		cclog.Warn("Error while fetching job footprint data")
    61→		return nil, err
    62→	}
    63→
    64→	res := []*model.FootprintValue{}
    65→	for name, value := range rawFootprint {
    66→
    67→		parts := strings.Split(name, "_")
    68→		statPart := parts[len(parts)-1]
    69→		nameParts := parts[:len(parts)-1]
    70→
    71→		res = append(res, &model.FootprintValue{
    72→			Name:  strings.Join(nameParts, "_"),
    73→			Stat:  statPart,
    74→			Value: value,
    75→		})
    76→	}
    77→
    78→	return res, err
    79→}
    80→
    81→// EnergyFootprint is the resolver for the energyFootprint field.
    82→func (r *jobResolver) EnergyFootprint(ctx context.Context, obj *schema.Job) ([]*model.EnergyFootprintValue, error) {
    83→	rawEnergyFootprint, err := r.Repo.FetchEnergyFootprint(obj)
    84→	if err != nil {
    85→		cclog.Warn("Error while fetching job energy footprint data")
    86→		return nil, err
    87→	}
    88→
    89→	res := []*model.EnergyFootprintValue{}
    90→	for name, value := range rawEnergyFootprint {
    91→		// Suboptimal: Nearly hardcoded metric name expectations
    92→		matchCPU := regexp.MustCompile(`cpu|Cpu|CPU`)
    93→		matchAcc := regexp.MustCompile(`acc|Acc|ACC`)
    94→		matchMem := regexp.MustCompile(`mem|Mem|MEM`)
    95→		matchCore := regexp.MustCompile(`core|Core|CORE`)
    96→
    97→		hwType := ""
    98→		switch test := name; { // NOtice ';' for var declaration
    99→		case matchCPU.MatchString(test):
   100→			hwType = "CPU"
   101→		case matchAcc.MatchString(test):
   102→			hwType = "Accelerator"
   103→		case matchMem.MatchString(test):
   104→			hwType = "Memory"
   105→		case matchCore.MatchString(test):
   106→			hwType = "Core"
   107→		default:
   108→			hwType = "Other"
   109→		}
   110→
   111→		res = append(res, &model.EnergyFootprintValue{
   112→			Hardware: hwType,
   113→			Metric:   name,
   114→			Value:    value,
   115→		})
   116→	}
   117→	return res, err
   118→}
   119→
   120→// MetaData is the resolver for the metaData field.
   121→func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (any, error) {
   122→	return r.Repo.FetchMetadata(obj)
   123→}
   124→
   125→// UserData is the resolver for the userData field.
   126→func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) {
   127→	return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User)
   128→}
   129→
   130→// Name is the resolver for the name field.
   131→func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue) (*string, error) {
   132→	panic(fmt.Errorf("not implemented: Name - name"))
   133→}
   134→
   135→// CreateTag is the resolver for the createTag field.
   136→func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) {
   137→	user := repository.GetUserFromContext(ctx)
   138→	if user == nil {
   139→		return nil, fmt.Errorf("no user in context")
   140→	}
   141→
   142→	// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
   143→	if user.HasRole(schema.RoleAdmin) && scope == "admin" ||
   144→		user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && scope == "global" ||
   145→		user.Username == scope {
   146→		// Create in DB
   147→		id, err := r.Repo.CreateTag(typeArg, name, scope)
   148→		if err != nil {
   149→			cclog.Warn("Error while creating tag")
   150→			return nil, err
   151→		}
   152→		return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
   153→	} else {
   154→		cclog.Warnf("Not authorized to create tag with scope: %s", scope)
   155→		return nil, fmt.Errorf("not authorized to create tag with scope: %s", scope)
   156→	}
   157→}
   158→
   159→// DeleteTag is the resolver for the deleteTag field.
   160→func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, error) {
   161→	// This Uses ID string <-> ID string, removeTagFromList uses []string <-> []int
   162→	panic(fmt.Errorf("not implemented: DeleteTag - deleteTag"))
   163→}
   164→
   165→// AddTagsToJob is the resolver for the addTagsToJob field.
   166→func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
   167→	user := repository.GetUserFromContext(ctx)
   168→	if user == nil {
   169→		return nil, fmt.Errorf("no user in context")
   170→	}
   171→
   172→	jid, err := strconv.ParseInt(job, 10, 64)
   173→	if err != nil {
   174→		cclog.Warn("Error while adding tag to job")
   175→		return nil, err
   176→	}
   177→
   178→	tags := []*schema.Tag{}
   179→	for _, tagID := range tagIds {
   180→		// Get ID
   181→		tid, err := strconv.ParseInt(tagID, 10, 64)
   182→		if err != nil {
   183→			cclog.Warn("Error while parsing tag id")
   184→			return nil, err
   185→		}
   186→
   187→		// Test Exists
   188→		_, _, tscope, exists := r.Repo.TagInfo(tid)
   189→		if !exists {
   190→			cclog.Warnf("Tag does not exist (ID): %d", tid)
   191→			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
   192→		}
   193→
   194→		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
   195→		if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
   196→			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
   197→			user.Username == tscope {
   198→			// Add to Job
   199→			if tags, err = r.Repo.AddTag(user, jid, tid); err != nil {
   200→				cclog.Warn("Error while adding tag")
   201→				return nil, err
   202→			}
   203→		} else {
   204→			cclog.Warnf("Not authorized to add tag: %d", tid)
   205→			return nil, fmt.Errorf("not authorized to add tag: %d", tid)
   206→		}
   207→	}
   208→
   209→	return tags, nil
   210→}
   211→
   212→// RemoveTagsFromJob is the resolver for the removeTagsFromJob field.
   213→func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
   214→	user := repository.GetUserFromContext(ctx)
   215→	if user == nil {
   216→		return nil, fmt.Errorf("no user in context")
   217→	}
   218→
   219→	jid, err := strconv.ParseInt(job, 10, 64)
   220→	if err != nil {
   221→		cclog.Warn("Error while parsing job id")
   222→		return nil, err
   223→	}
   224→
   225→	tags := []*schema.Tag{}
   226→	for _, tagID := range tagIds {
   227→		// Get ID
   228→		tid, err := strconv.ParseInt(tagID, 10, 64)
   229→		if err != nil {
   230→			cclog.Warn("Error while parsing tag id")
   231→			return nil, err
   232→		}
   233→
   234→		// Test Exists
   235→		_, _, tscope, exists := r.Repo.TagInfo(tid)
   236→		if !exists {
   237→			cclog.Warnf("Tag does not exist (ID): %d", tid)
   238→			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
   239→		}
   240→
   241→		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
   242→		if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
   243→			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
   244→			user.Username == tscope {
   245→			// Remove from Job
   246→			if tags, err = r.Repo.RemoveTag(user, jid, tid); err != nil {
   247→				cclog.Warn("Error while removing tag")
   248→				return nil, err
   249→			}
   250→		} else {
   251→			cclog.Warnf("Not authorized to remove tag: %d", tid)
   252→			return nil, fmt.Errorf("not authorized to remove tag: %d", tid)
   253→		}
   254→
   255→	}
   256→
   257→	return tags, nil
   258→}
   259→
   260→// RemoveTagFromList is the resolver for the removeTagFromList field.
   261→func (r *mutationResolver) RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error) {
   262→	// Needs Contextuser
   263→	user := repository.GetUserFromContext(ctx)
   264→	if user == nil {
   265→		return nil, fmt.Errorf("no user in context")
   266→	}
   267→
   268→	tags := []int{}
   269→	for _, tagID := range tagIds {
   270→		// Get ID
   271→		tid, err := strconv.ParseInt(tagID, 10, 64)
   272→		if err != nil {
   273→			cclog.Warn("Error while parsing tag id for removal")
   274→			return nil, err
   275→		}
   276→
   277→		// Test Exists
   278→		_, _, tscope, exists := r.Repo.TagInfo(tid)
   279→		if !exists {
   280→			cclog.Warnf("Tag does not exist (ID): %d", tid)
   281→			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
   282→		}
   283→
   284→		// Test Access: Admins && Admin Tag OR Everyone && Private Tag
   285→		if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope {
   286→			// Remove from DB
   287→			if err = r.Repo.RemoveTagByID(tid); err != nil {
   288→				cclog.Warn("Error while removing tag")
   289→				return nil, err
   290→			} else {
   291→				tags = append(tags, int(tid))
   292→			}
   293→		} else {
   294→			cclog.Warnf("Not authorized to remove tag: %d", tid)
   295→			return nil, fmt.Errorf("not authorized to remove tag: %d", tid)
   296→		}
   297→	}
   298→	return tags, nil
   299→}
   300→
   301→// UpdateConfiguration is the resolver for the updateConfiguration field.
   302→func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
   303→	if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil {
   304→		cclog.Warn("Error while updating user config")
   305→		return nil, err
   306→	}
   307→
   308→	return nil, nil
   309→}
   310→
   311→// ID is the resolver for the id field.
   312→func (r *nodeResolver) ID(ctx context.Context, obj *schema.Node) (string, error) {
   313→	panic(fmt.Errorf("not implemented: ID - id"))
   314→}
   315→
   316→// SchedulerState is the resolver for the schedulerState field.
   317→func (r *nodeResolver) SchedulerState(ctx context.Context, obj *schema.Node) (schema.SchedulerState, error) {
   318→	if obj.NodeState != "" {
   319→		return obj.NodeState, nil
   320→	} else {
   321→		return "", fmt.Errorf("resolver: no SchedulerState (NodeState) on node object")
   322→	}
   323→}
   324→
   325→// HealthState is the resolver for the healthState field.
   326→func (r *nodeResolver) HealthState(ctx context.Context, obj *schema.Node) (string, error) {
   327→	if obj.HealthState != "" {
   328→		return string(obj.HealthState), nil
   329→	} else {
   330→		return "", fmt.Errorf("resolver: no HealthState (NodeState) on node object")
   331→	}
   332→}
   333→
   334→// MetaData is the resolver for the metaData field.
   335→func (r *nodeResolver) MetaData(ctx context.Context, obj *schema.Node) (any, error) {
   336→	if obj.MetaData != nil {
   337→		return obj.MetaData, nil
   338→	} else {
   339→		cclog.Debug("resolver: no MetaData (NodeState) on node object")
   340→		emptyMeta := make(map[string]string, 0)
   341→		return emptyMeta, nil
   342→	}
   343→}
   344→
   345→// HealthData is the resolver for the healthData field.
   346→func (r *nodeResolver) HealthData(ctx context.Context, obj *schema.Node) (any, error) {
   347→	if obj.HealthData != nil {
   348→		return obj.HealthData, nil
   349→	} else {
   350→		cclog.Debug("resolver: no HealthData (NodeState) on node object")
   351→		emptyHealth := make(map[string][]string, 0)
   352→		return emptyHealth, nil
   353→	}
   354→}
   355→
   356→// Clusters is the resolver for the clusters field.
   357→func (r *queryResolver) Clusters(ctx context.Context) ([]*schema.Cluster, error) {
   358→	return archive.Clusters, nil
   359→}
   360→
   361→// Tags is the resolver for the tags field.
   362→func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
   363→	return r.Repo.GetTags(repository.GetUserFromContext(ctx), nil)
   364→}
   365→
   366→// GlobalMetrics is the resolver for the globalMetrics field.
   367→func (r *queryResolver) GlobalMetrics(ctx context.Context) ([]*schema.GlobalMetricListItem, error) {
   368→	user := repository.GetUserFromContext(ctx)
   369→
   370→	if user != nil {
   371→		if user.HasRole(schema.RoleUser) || user.HasRole(schema.RoleManager) {
   372→			return archive.GlobalUserMetricList, nil
   373→		}
   374→	}
   375→
   376→	return archive.GlobalMetricList, nil
   377→}
   378→
   379→// User is the resolver for the user field.
   380→func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) {
   381→	return repository.GetUserRepository().FetchUserInCtx(ctx, username)
   382→}
   383→
   384→// AllocatedNodes is the resolver for the allocatedNodes field.
   385→func (r *queryResolver) AllocatedNodes(ctx context.Context, cluster string) ([]*model.Count, error) {
   386→	data, err := r.Repo.AllocatedNodes(cluster)
   387→	if err != nil {
   388→		cclog.Warn("Error while fetching allocated nodes")
   389→		return nil, err
   390→	}
   391→
   392→	counts := make([]*model.Count, 0, len(data))
   393→	for subcluster, hosts := range data {
   394→		counts = append(counts, &model.Count{
   395→			Name:  subcluster,
   396→			Count: len(hosts),
   397→		})
   398→	}
   399→
   400→	return counts, nil
   401→}
   402→
   403→// Node is the resolver for the node field.
   404→func (r *queryResolver) Node(ctx context.Context, id string) (*schema.Node, error) {
   405→	repo := repository.GetNodeRepository()
   406→	numericID, err := strconv.ParseInt(id, 10, 64)
   407→	if err != nil {
   408→		cclog.Warn("Error while parsing job id")
   409→		return nil, err
   410→	}
   411→	return repo.GetNodeByID(numericID, false)
   412→}
   413→
   414→// Nodes is the resolver for the nodes field.
   415→func (r *queryResolver) Nodes(ctx context.Context, filter []*model.NodeFilter, order *model.OrderByInput) (*model.NodeStateResultList, error) {
   416→	repo := repository.GetNodeRepository()
   417→	nodes, err := repo.QueryNodes(ctx, filter, nil, order) // Ignore Paging, Order Unused
   418→	count := len(nodes)
   419→	return &model.NodeStateResultList{Items: nodes, Count: &count}, err
   420→}
   421→
   422→// NodesWithMeta is the resolver for the nodesWithMeta field.
   423→func (r *queryResolver) NodesWithMeta(ctx context.Context, filter []*model.NodeFilter, order *model.OrderByInput) (*model.NodeStateResultList, error) {
   424→	// Why Extra Handler? -> graphql.CollectAllFields(ctx) only returns toplevel fields (i.e.: items, count), and not subfields like item.metaData
   425→	repo := repository.GetNodeRepository()
   426→	nodes, err := repo.QueryNodesWithMeta(ctx, filter, nil, order) // Ignore Paging, Order Unused
   427→	count := len(nodes)
   428→	return &model.NodeStateResultList{Items: nodes, Count: &count}, err
   429→}
   430→
   431→// NodeStates is the resolver for the nodeStates field.
   432→func (r *queryResolver) NodeStates(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStates, error) {
   433→	repo := repository.GetNodeRepository()
   434→
   435→	stateCounts, serr := repo.CountStates(ctx, filter, "node_state")
   436→	if serr != nil {
   437→		cclog.Warnf("Error while counting nodeStates: %s", serr.Error())
   438→		return nil, serr
   439→	}
   440→
   441→	healthCounts, herr := repo.CountStates(ctx, filter, "health_state")
   442→	if herr != nil {
   443→		cclog.Warnf("Error while counting healthStates: %s", herr.Error())
   444→		return nil, herr
   445→	}
   446→
   447→	allCounts := append(stateCounts, healthCounts...)
   448→
   449→	return allCounts, nil
   450→}
   451→
   452→// NodeStatesTimed is the resolver for the nodeStatesTimed field.
   453→func (r *queryResolver) NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter, typeArg string) ([]*model.NodeStatesTimed, error) {
   454→	repo := repository.GetNodeRepository()
   455→
   456→	if typeArg == "node" {
   457→		stateCounts, serr := repo.CountStatesTimed(ctx, filter, "node_state")
   458→		if serr != nil {
   459→			cclog.Warnf("Error while counting nodeStates in time: %s", serr.Error())
   460→			return nil, serr
   461→		}
   462→		return stateCounts, nil
   463→	}
   464→
   465→	if typeArg == "health" {
   466→		healthCounts, herr := repo.CountStatesTimed(ctx, filter, "health_state")
   467→		if herr != nil {
   468→			cclog.Warnf("Error while counting healthStates in time: %s", herr.Error())
   469→			return nil, herr
   470→		}
   471→		return healthCounts, nil
   472→	}
   473→
   474→	return nil, errors.New("unknown Node State Query Type")
   475→}
   476→
   477→// Job is the resolver for the job field.
   478→func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error) {
   479→	numericID, err := strconv.ParseInt(id, 10, 64)
   480→	if err != nil {
   481→		cclog.Warn("Error while parsing job id")
   482→		return nil, err
   483→	}
   484→
   485→	job, err := r.Repo.FindByID(ctx, numericID)
   486→	if err != nil {
   487→		cclog.Warn("Error while finding job by id")
   488→		return nil, err
   489→	}
   490→
   491→	if user := repository.GetUserFromContext(ctx); user != nil &&
   492→		job.User != user.Username &&
   493→		user.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
   494→		return nil, errors.New("you are not allowed to see this job")
   495→	}
   496→
   497→	return job, nil
   498→}
   499→
   500→// JobMetrics is the resolver for the jobMetrics field.
   501→func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) {
   502→	if resolution == nil { // Load from Config
   503→		if config.Keys.EnableResampling != nil {
   504→			defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
   505→			resolution = &defaultRes
   506→		} else { // Set 0 (Loads configured metric timestep)
   507→			defaultRes := 0
   508→			resolution = &defaultRes
   509→		}
   510→	}
   511→
   512→	job, err := r.Query().Job(ctx, id)
   513→	if err != nil {
   514→		cclog.Warn("Error while querying job for metrics")
   515→		return nil, err
   516→	}
   517→
   518→	data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution)
   519→	if err != nil {
   520→		cclog.Warn("Error while loading job data")
   521→		return nil, err
   522→	}
   523→
   524→	res := []*model.JobMetricWithName{}
   525→	for name, md := range data {
   526→		for scope, metric := range md {
   527→			res = append(res, &model.JobMetricWithName{
   528→				Name:   name,
   529→				Scope:  scope,
   530→				Metric: metric,
   531→			})
   532→		}
   533→	}
   534→
   535→	return res, err
   536→}
   537→
   538→// JobStats is the resolver for the jobStats field.
   539→func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []string) ([]*model.NamedStats, error) {
   540→	job, err := r.Query().Job(ctx, id)
   541→	if err != nil {
   542→		cclog.Warnf("Error while querying job %s for metadata", id)
   543→		return nil, err
   544→	}
   545→
   546→	data, err := metricdispatch.LoadJobStats(job, metrics, ctx)
   547→	if err != nil {
   548→		cclog.Warnf("Error while loading jobStats data for job id %s", id)
   549→		return nil, err
   550→	}
   551→
   552→	res := []*model.NamedStats{}
   553→	for name, md := range data {
   554→		res = append(res, &model.NamedStats{
   555→			Name: name,
   556→			Data: &md,
   557→		})
   558→	}
   559→
   560→	return res, err
   561→}
   562→
   563→// ScopedJobStats is the resolver for the scopedJobStats field.
   564→func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.NamedStatsWithScope, error) {
   565→	job, err := r.Query().Job(ctx, id)
   566→	if err != nil {
   567→		cclog.Warnf("Error while querying job %s for metadata", id)
   568→		return nil, err
   569→	}
   570→
   571→	data, err := metricdispatch.LoadScopedJobStats(job, metrics, scopes, ctx)
   572→	if err != nil {
   573→		cclog.Warnf("Error while loading scopedJobStats data for job id %s", id)
   574→		return nil, err
   575→	}
   576→
   577→	res := make([]*model.NamedStatsWithScope, 0)
   578→	for name, scoped := range data {
   579→		for scope, stats := range scoped {
   580→
   581→			mdlStats := make([]*model.ScopedStats, 0)
   582→			for _, stat := range stats {
   583→				mdlStats = append(mdlStats, &model.ScopedStats{
   584→					Hostname: stat.Hostname,
   585→					ID:       stat.ID,
   586→					Data:     stat.Data,
   587→				})
   588→			}
   589→
   590→			res = append(res, &model.NamedStatsWithScope{
   591→				Name:  name,
   592→				Scope: scope,
   593→				Stats: mdlStats,
   594→			})
   595→		}
   596→	}
   597→
   598→	return res, nil
   599→}
   600→
   601→// Jobs is the resolver for the jobs field.
   602→func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) {
   603→	if page == nil {
   604→		page = &model.PageRequest{
   605→			ItemsPerPage: 50,
   606→			Page:         1,
   607→		}
   608→	}
   609→
   610→	jobs, err := r.Repo.QueryJobs(ctx, filter, page, order)
   611→	if err != nil {
   612→		cclog.Warn("Error while querying jobs")
   613→		return nil, err
   614→	}
   615→
   616→	count, err := r.Repo.CountJobs(ctx, filter)
   617→	if err != nil {
   618→		cclog.Warn("Error while counting jobs")
   619→		return nil, err
   620→	}
   621→
   622→	// Note: Even if App-Default 'config.Keys.UiDefaults["job_list_usePaging"]' is set, always return hasNextPage boolean.
   623→	// Users can decide in frontend to use continuous scroll, even if app-default is paging!
   624→	// Skip if page.ItemsPerPage == -1 ("Load All" -> No Next Page required, Status Dashboards)
   625→	/*
   626→	  Example Page 4 @ 10 IpP : Does item 41 exist?
   627→	  Minimal Page 41 @ 1 IpP : If len(result) is 1, Page 5 @ 10 IpP exists.
   628→	*/
   629→	hasNextPage := false
   630→	if page.ItemsPerPage != -1 {
   631→		nextPage := &model.PageRequest{
   632→			ItemsPerPage: 1,
   633→			Page:         ((page.Page * page.ItemsPerPage) + 1),
   634→		}
   635→		nextJobs, err := r.Repo.QueryJobs(ctx, filter, nextPage, order)
   636→		if err != nil {
   637→			cclog.Warn("Error while querying next jobs")
   638→			return nil, err
   639→		}
   640→		hasNextPage = len(nextJobs) == 1
   641→	}
   642→
   643→	return &model.JobResultList{Items: jobs, Count: &count, HasNextPage: &hasNextPage}, nil
   644→}
   645→
   646→// JobsStatistics is the resolver for the jobsStatistics field.
   647→func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) {
   648→	var err error
   649→	var stats []*model.JobsStatistics
   650→
   651→	// Top Level Defaults
   652→	defaultDurationBins := "1h"
   653→	defaultMetricBins := 10
   654→
   655→	// Build requested fields map for selective column computation
   656→	statsFields := []string{
   657→		"totalJobs", "totalUsers", "totalWalltime", "totalNodes", "totalCores",
   658→		"totalAccs", "totalNodeHours", "totalCoreHours", "totalAccHours", "runningJobs", "shortJobs",
   659→	}
   660→	reqFields := make(map[string]bool, len(statsFields))
   661→	fetchedMainStats := false
   662→	for _, f := range statsFields {
   663→		if requireField(ctx, f) {
   664→			reqFields[f] = true
   665→			if f != "runningJobs" && f != "shortJobs" {
   666→				fetchedMainStats = true
   667→			}
   668→		}
   669→	}
   670→
   671→	if fetchedMainStats {
   672→		if groupBy == nil {
   673→			stats, err = r.Repo.JobsStats(ctx, filter, reqFields)
   674→		} else {
   675→			startGrouped := time.Now()
   676→			stats, err = r.Repo.JobsStatsGrouped(ctx, filter, page, sortBy, groupBy, reqFields)
   677→			cclog.Infof("Timer JobsStatsGrouped call: %s", time.Since(startGrouped))
   678→		}
   679→	} else {
   680→		stats = make([]*model.JobsStatistics, 0, 1)
   681→		stats = append(stats, &model.JobsStatistics{})
   682→	}
   683→
   684→	// runningJobs and shortJobs are already inlined in JobsStats/JobsStatsGrouped.
   685→	// Only run separate count queries if main stats were not fetched.
   686→	if !fetchedMainStats {
   687→		if groupBy != nil {
   688→			if requireField(ctx, "shortJobs") {
   689→				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "short")
   690→			}
   691→			if requireField(ctx, "runningJobs") {
   692→				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "running")
   693→			}
   694→		} else {
   695→			if requireField(ctx, "shortJobs") {
   696→				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "short")
   697→			}
   698→			if requireField(ctx, "runningJobs") {
   699→				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "running")
   700→			}
   701→		}
   702→	}
   703→
   704→	if err != nil {
   705→		return nil, err
   706→	}
   707→
   708→	if requireField(ctx, "histDuration") || requireField(ctx, "histNumNodes") || requireField(ctx, "histNumCores") || requireField(ctx, "histNumAccs") {
   709→
   710→		if numDurationBins == nil {
   711→			numDurationBins = &defaultDurationBins
   712→		}
   713→
   714→		if groupBy == nil {
   715→			stats[0], err = r.Repo.AddHistograms(ctx, filter, stats[0], numDurationBins)
   716→			if err != nil {
   717→				return nil, err
   718→			}
   719→		} else {
   720→			return nil, errors.New("histograms only implemented without groupBy argument")
   721→		}
   722→	}
   723→
   724→	if requireField(ctx, "histMetrics") {
   725→
   726→		if numMetricBins == nil {
   727→			numMetricBins = &defaultMetricBins
   728→		}
   729→
   730→		if groupBy == nil {
   731→			stats[0], err = r.Repo.AddMetricHistograms(ctx, filter, metrics, stats[0], numMetricBins)
   732→			if err != nil {
   733→				return nil, err
   734→			}
   735→		} else {
   736→			return nil, errors.New("metric histograms only implemented without groupBy argument")
   737→		}
   738→	}
   739→
   740→	return stats, nil
   741→}
   742→
   743→// JobsMetricStats is the resolver for the jobsMetricStats field.
   744→func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.JobStats, error) {
   745→	// No Paging, Fixed Order by StartTime ASC
   746→	order := &model.OrderByInput{
   747→		Field: "startTime",
   748→		Type:  "col",
   749→		Order: "ASC",
   750→	}
   751→
   752→	jobs, err := r.Repo.QueryJobs(ctx, filter, nil, order)
   753→	if err != nil {
   754→		cclog.Warn("Error while querying jobs for comparison")
   755→		return nil, err
   756→	}
   757→
   758→	res := []*model.JobStats{}
   759→	for _, job := range jobs {
   760→		data, err := metricdispatch.LoadJobStats(job, metrics, ctx)
   761→		if err != nil {
   762→			cclog.Warnf("Error while loading comparison jobStats data for job id %d", job.JobID)
   763→			continue
   764→			// return nil, err
   765→		}
   766→
   767→		sres := []*model.NamedStats{}
   768→		for name, md := range data {
   769→			sres = append(sres, &model.NamedStats{
   770→				Name: name,
   771→				Data: &md,
   772→			})
   773→		}
   774→
   775→		numThreadsInt := int(job.NumHWThreads)
   776→		numAccsInt := int(job.NumAcc)
   777→		res = append(res, &model.JobStats{
   778→			ID:              int(*job.ID),
   779→			JobID:           strconv.Itoa(int(job.JobID)),
   780→			StartTime:       int(job.StartTime),
   781→			Duration:        int(job.Duration),
   782→			Cluster:         job.Cluster,
   783→			SubCluster:      job.SubCluster,
   784→			NumNodes:        int(job.NumNodes),
   785→			NumHWThreads:    &numThreadsInt,
   786→			NumAccelerators: &numAccsInt,
   787→			Stats:           sres,
   788→		})
   789→	}
   790→	return res, err
   791→}
   792→
   793→// JobsFootprints is the resolver for the jobsFootprints field.
   794→func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
   795→	// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
   796→	return r.jobsFootprints(ctx, filter, metrics)
   797→}
   798→
   799→// RooflineHeatmap is the resolver for the rooflineHeatmap field.
   800→func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) {
   801→	return r.rooflineHeatmap(ctx, filter, rows, cols, minX, minY, maxX, maxY)
   802→}
   803→
   804→// NodeMetrics is the resolver for the nodeMetrics field.
   805→func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
   806→	user := repository.GetUserFromContext(ctx)
   807→	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
   808→		return nil, errors.New("you need to be administrator or support staff for this query")
   809→	}
   810→
   811→	defaultMetrics := make([]string, 0)
   812→	for _, mc := range archive.GetCluster(cluster).MetricConfig {
   813→		defaultMetrics = append(defaultMetrics, mc.Name)
   814→	}
   815→	if metrics == nil {
   816→		metrics = defaultMetrics
   817→	} else {
   818→		metrics = slices.DeleteFunc(metrics, func(metric string) bool {
   819→			return !slices.Contains(defaultMetrics, metric) // Remove undefined metrics.
   820→		})
   821→	}
   822→
   823→	data, err := metricdispatch.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)
   824→	if err != nil {
   825→		cclog.Warn("error while loading node data")
   826→		return nil, err
   827→	}
   828→
   829→	nodeRepo := repository.GetNodeRepository()
   830→	stateMap, _ := nodeRepo.MapNodes(cluster)
   831→
   832→	nodeMetrics := make([]*model.NodeMetrics, 0, len(data))
   833→	for hostname, metrics := range data {
   834→		host := &model.NodeMetrics{
   835→			Host:    hostname,
   836→			State:   stateMap[hostname],
   837→			Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
   838→		}
   839→		host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
   840→		if err != nil {
   841→			cclog.Warnf("error in nodeMetrics resolver: %s", err)
   842→		}
   843→
   844→		for metric, scopedMetrics := range metrics {
   845→			for _, scopedMetric := range scopedMetrics {
   846→				host.Metrics = append(host.Metrics, &model.JobMetricWithName{
   847→					Name:   metric,
   848→					Scope:  schema.MetricScopeNode,
   849→					Metric: scopedMetric,
   850→				})
   851→			}
   852→		}
   853→
   854→		nodeMetrics = append(nodeMetrics, host)
   855→	}
   856→
   857→	return nodeMetrics, nil
   858→}
   859→
   860→// NodeMetricsList is the resolver for the nodeMetricsList field.
   861→func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) {
   862→	if resolution == nil { // Load from Config
   863→		if config.Keys.EnableResampling != nil {
   864→			defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
   865→			resolution = &defaultRes
   866→		} else { // Set 0 (Loads configured metric timestep)
   867→			defaultRes := 0
   868→			resolution = &defaultRes
   869→		}
   870→	}
   871→
   872→	user := repository.GetUserFromContext(ctx)
   873→	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
   874→		return nil, errors.New("you need to be administrator or support staff for this query")
   875→	}
   876→
   877→	nodeRepo := repository.GetNodeRepository()
   878→	// nodes -> array hostname
   879→	nodes, stateMap, countNodes, hasNextPage, nerr := nodeRepo.GetNodesForList(ctx, cluster, subCluster, stateFilter, nodeFilter, page)
   880→	if nerr != nil {
   881→		return nil, errors.New("could not retrieve node list required for resolving NodeMetricsList")
   882→	}
   883→
   884→	if metrics == nil {
   885→		for _, mc := range archive.GetCluster(cluster).MetricConfig {
   886→			metrics = append(metrics, mc.Name)
   887→		}
   888→	}
   889→
   890→	// data -> map hostname:jobdata
   891→	data, err := metricdispatch.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, *resolution, from, to, ctx)
   892→	if err != nil {
   893→		cclog.Warn("error while loading node data (Resolver.NodeMetricsList")
   894→		return nil, err
   895→	}
   896→
   897→	nodeMetricsList := make([]*model.NodeMetrics, 0, len(data))
   898→	for _, hostname := range nodes {
   899→		host := &model.NodeMetrics{
   900→			Host:    hostname,
   901→			State:   stateMap[hostname],
   902→			Metrics: make([]*model.JobMetricWithName, 0),
   903→		}
   904→		host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
   905→		if err != nil {
   906→			cclog.Warnf("error in nodeMetrics resolver: %s", err)
   907→		}
   908→
   909→		for metric, scopedMetrics := range data[hostname] {
   910→			for scope, scopedMetric := range scopedMetrics {
   911→				host.Metrics = append(host.Metrics, &model.JobMetricWithName{
   912→					Name:   metric,
   913→					Scope:  scope,
   914→					Metric: scopedMetric,
   915→				})
   916→			}
   917→		}
   918→
   919→		nodeMetricsList = append(nodeMetricsList, host)
   920→	}
   921→
   922→	nodeMetricsListResult := &model.NodesResultList{
   923→		Items: nodeMetricsList,
   924→		// TotalNodes depends on sum of nodes grouped on latest timestamp, see repo/node.go:357
   925→		TotalNodes:  &countNodes,
   926→		HasNextPage: &hasNextPage,
   927→	}
   928→
   929→	return nodeMetricsListResult, nil
   930→}
   931→
   932→// ClusterMetrics is the resolver for the clusterMetrics field.
   933→func (r *queryResolver) ClusterMetrics(ctx context.Context, cluster string, metrics []string, from time.Time, to time.Time) (*model.ClusterMetrics, error) {
   934→	user := repository.GetUserFromContext(ctx)
   935→	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
   936→		return nil, errors.New("you need to be administrator or support staff for this query")
   937→	}
   938→
   939→	if metrics == nil {
   940→		for _, mc := range archive.GetCluster(cluster).MetricConfig {
   941→			metrics = append(metrics, mc.Name)
   942→		}
   943→	}
   944→
   945→	// 'nodes' == nil -> Defaults to all nodes of cluster for existing query workflow
   946→	scopes := []schema.MetricScope{"node"}
   947→	data, err := metricdispatch.LoadNodeData(cluster, metrics, nil, scopes, from, to, ctx)
   948→	if err != nil {
   949→		cclog.Warn("error while loading node data")
   950→		return nil, err
   951→	}
   952→
   953→	clusterMetricData := make([]*model.ClusterMetricWithName, 0)
   954→	clusterMetrics := model.ClusterMetrics{NodeCount: 0, Metrics: clusterMetricData}
   955→
   956→	collectorTimestep := make(map[string]int)
   957→	collectorUnit := make(map[string]schema.Unit)
   958→	collectorData := make(map[string][]schema.Float)
   959→
   960→	for _, metrics := range data {
   961→		clusterMetrics.NodeCount += 1
   962→		for metric, scopedMetrics := range metrics {
   963→			for _, scopedMetric := range scopedMetrics {
   964→				// Collect Info Once
   965→				_, okTimestep := collectorTimestep[metric]
   966→				if !okTimestep {
   967→					collectorTimestep[metric] = scopedMetric.Timestep
   968→				}
   969→				_, okUnit := collectorUnit[metric]
   970→				if !okUnit {
   971→					collectorUnit[metric] = scopedMetric.Unit
   972→				}
   973→				// Collect Data
   974→				for _, ser := range scopedMetric.Series {
   975→					_, okData := collectorData[metric]
   976→					// Init With Datasize > 0
   977→					if !okData && len(ser.Data) != 0 {
   978→						collectorData[metric] = make([]schema.Float, len(ser.Data))
   979→					} else if !okData {
   980→						cclog.Debugf("[SCHEMARESOLVER] clusterMetrics skip init: no data -> %s at %s; size %d", metric, ser.Hostname, len(ser.Data))
   981→					}
   982→					// Sum if init'd and matching size
   983→					if okData && len(ser.Data) == len(collectorData[metric]) {
   984→						for i, val := range ser.Data {
   985→							if val.IsNaN() {
   986→								continue
   987→							} else {
   988→								collectorData[metric][i] += val
   989→							}
   990→						}
   991→					} else if okData {
   992→						cclog.Debugf("[SCHEMARESOLVER] clusterMetrics skip sum: data diff -> %s at %s; want size %d, have size %d", metric, ser.Hostname, len(collectorData[metric]), len(ser.Data))
   993→					}
   994→				}
   995→			}
   996→		}
   997→	}
   998→
   999→	for metricName, data := range collectorData {
  1000→		// use ccUnits for backend normalization to "Tera"
  1001→		p_old := ccunit.NewPrefix(collectorUnit[metricName].Prefix)
  1002→		p_new := ccunit.NewPrefix("T")
  1003→		convFunc := ccunit.GetPrefixPrefixFactor(p_old, p_new)
  1004→		u_new := schema.Unit{Prefix: p_new.Prefix(), Base: collectorUnit[metricName].Base}
  1005→
  1006→		roundedData := make([]schema.Float, 0)
  1007→		for _, v_old := range data {
  1008→			v_new := math.Round(convFunc(float64(v_old)).(float64)*100.0) / 100.0
  1009→			roundedData = append(roundedData, schema.Float(v_new))
  1010→		}
  1011→
  1012→		cm := model.ClusterMetricWithName{
  1013→			Name:     metricName,
  1014→			Unit:     &u_new,
  1015→			Timestep: collectorTimestep[metricName],
  1016→			Data:     roundedData,
  1017→		}
  1018→
  1019→		clusterMetrics.Metrics = append(clusterMetrics.Metrics, &cm)
  1020→	}
  1021→
  1022→	return &clusterMetrics, nil
  1023→}
  1024→
  1025→// NumberOfNodes is the resolver for the numberOfNodes field.
  1026→func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) {
  1027→	nodeList, err := archive.ParseNodeList(obj.Nodes)
  1028→	if err != nil {
  1029→		return 0, err
  1030→	}
  1031→	return nodeList.NodeCount(), nil
  1032→}
  1033→
  1034→// Cluster returns generated.ClusterResolver implementation.
  1035→func (r *Resolver) Cluster() generated.ClusterResolver { return &clusterResolver{r} }
  1036→
  1037→// Job returns generated.JobResolver implementation.
  1038→func (r *Resolver) Job() generated.JobResolver { return &jobResolver{r} }
  1039→
  1040→// MetricValue returns generated.MetricValueResolver implementation.
  1041→func (r *Resolver) MetricValue() generated.MetricValueResolver { return &metricValueResolver{r} }
  1042→
  1043→// Mutation returns generated.MutationResolver implementation.
  1044→func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
  1045→
  1046→// Node returns generated.NodeResolver implementation.
  1047→func (r *Resolver) Node() generated.NodeResolver { return &nodeResolver{r} }
  1048→
  1049→// Query returns generated.QueryResolver implementation.
  1050→func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
  1051→
  1052→// SubCluster returns generated.SubClusterResolver implementation.
  1053→func (r *Resolver) SubCluster() generated.SubClusterResolver { return &subClusterResolver{r} }
  1054→
  1055→type (
  1056→	clusterResolver     struct{ *Resolver }
  1057→	jobResolver         struct{ *Resolver }
  1058→	metricValueResolver struct{ *Resolver }
  1059→	mutationResolver    struct{ *Resolver }
  1060→	nodeResolver        struct{ *Resolver }
  1061→	queryResolver       struct{ *Resolver }
  1062→	subClusterResolver  struct{ *Resolver }
  1063→)
  1064→

package graph

// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.87

import (
	"context"
	"errors"
	"fmt"
	"math"
	"regexp"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/ClusterCockpit/cc-backend/internal/config"
	"github.com/ClusterCockpit/cc-backend/internal/graph/generated"
	"github.com/ClusterCockpit/cc-backend/internal/graph/model"
	"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
	"github.com/ClusterCockpit/cc-backend/internal/repository"
	"github.com/ClusterCockpit/cc-backend/pkg/archive"
	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	ccunit "github.com/ClusterCockpit/cc-lib/v2/ccUnits"
	"github.com/ClusterCockpit/cc-lib/v2/schema"
)

// Partitions is the resolver for the partitions field.
func (r *clusterResolver) Partitions(ctx context.Context, obj *schema.Cluster) ([]string, error) {
	return r.Repo.Partitions(obj.Name)
}

// StartTime is the resolver for the startTime field.
func (r *jobResolver) StartTime(ctx context.Context, obj *schema.Job) (*time.Time, error) {
	timestamp := time.Unix(obj.StartTime, 0)
	return &timestamp, nil
}

// Tags is the resolver for the tags field.
func (r *jobResolver) Tags(ctx context.Context, obj *schema.Job) ([]*schema.Tag, error) {
	return r.Repo.GetTags(repository.GetUserFromContext(ctx), obj.ID)
}

// ConcurrentJobs is the resolver for the concurrentJobs field.
func (r *jobResolver) ConcurrentJobs(ctx context.Context, obj *schema.Job) (*model.JobLinkResultList, error) {
	// FIXME: Make the hardcoded duration configurable
	if obj.Shared != "none" && obj.Duration > 600 {
		return r.Repo.FindConcurrentJobs(ctx, obj)
	}

	return nil, nil
}

// Footprint is the resolver for the footprint field.
func (r *jobResolver) Footprint(ctx context.Context, obj *schema.Job) ([]*model.FootprintValue, error) {
	rawFootprint, err := r.Repo.FetchFootprint(obj)
	if err != nil {
		cclog.Warn("Error while fetching job footprint data")
		return nil, err
	}

	res := []*model.FootprintValue{}
	for name, value := range rawFootprint {

		parts := strings.Split(name, "_")
		statPart := parts[len(parts)-1]
		nameParts := parts[:len(parts)-1]

		res = append(res, &model.FootprintValue{
			Name:  strings.Join(nameParts, "_"),
			Stat:  statPart,
			Value: value,
		})
	}

	return res, err
}

// EnergyFootprint is the resolver for the energyFootprint field.
func (r *jobResolver) EnergyFootprint(ctx context.Context, obj *schema.Job) ([]*model.EnergyFootprintValue, error) {
	rawEnergyFootprint, err := r.Repo.FetchEnergyFootprint(obj)
	if err != nil {
		cclog.Warn("Error while fetching job energy footprint data")
		return nil, err
	}

	res := []*model.EnergyFootprintValue{}
	for name, value := range rawEnergyFootprint {
		// Suboptimal: Nearly hardcoded metric name expectations
		matchCPU := regexp.MustCompile(`cpu|Cpu|CPU`)
		matchAcc := regexp.MustCompile(`acc|Acc|ACC`)
		matchMem := regexp.MustCompile(`mem|Mem|MEM`)
		matchCore := regexp.MustCompile(`core|Core|CORE`)

		hwType := ""
		switch test := name; { // NOtice ';' for var declaration
		case matchCPU.MatchString(test):
			hwType = "CPU"
		case matchAcc.MatchString(test):
			hwType = "Accelerator"
		case matchMem.MatchString(test):
			hwType = "Memory"
		case matchCore.MatchString(test):
			hwType = "Core"
		default:
			hwType = "Other"
		}

		res = append(res, &model.EnergyFootprintValue{
			Hardware: hwType,
			Metric:   name,
			Value:    value,
		})
	}
	return res, err
}

// MetaData is the resolver for the metaData field.
func (r *jobResolver) MetaData(ctx context.Context, obj *schema.Job) (any, error) {
	return r.Repo.FetchMetadata(obj)
}

// UserData is the resolver for the userData field.
func (r *jobResolver) UserData(ctx context.Context, obj *schema.Job) (*model.User, error) {
	return repository.GetUserRepository().FetchUserInCtx(ctx, obj.User)
}

// Name is the resolver for the name field.
func (r *metricValueResolver) Name(ctx context.Context, obj *schema.MetricValue) (*string, error) {
	panic(fmt.Errorf("not implemented: Name - name"))
}

// CreateTag is the resolver for the createTag field.
func (r *mutationResolver) CreateTag(ctx context.Context, typeArg string, name string, scope string) (*schema.Tag, error) {
	user := repository.GetUserFromContext(ctx)
	if user == nil {
		return nil, fmt.Errorf("no user in context")
	}

	// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
	if user.HasRole(schema.RoleAdmin) && scope == "admin" ||
		user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && scope == "global" ||
		user.Username == scope {
		// Create in DB
		id, err := r.Repo.CreateTag(typeArg, name, scope)
		if err != nil {
			cclog.Warn("Error while creating tag")
			return nil, err
		}
		return &schema.Tag{ID: id, Type: typeArg, Name: name, Scope: scope}, nil
	} else {
		cclog.Warnf("Not authorized to create tag with scope: %s", scope)
		return nil, fmt.Errorf("not authorized to create tag with scope: %s", scope)
	}
}

// DeleteTag is the resolver for the deleteTag field.
func (r *mutationResolver) DeleteTag(ctx context.Context, id string) (string, error) {
	// This Uses ID string <-> ID string, removeTagFromList uses []string <-> []int
	panic(fmt.Errorf("not implemented: DeleteTag - deleteTag"))
}

// AddTagsToJob is the resolver for the addTagsToJob field.
func (r *mutationResolver) AddTagsToJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
	user := repository.GetUserFromContext(ctx)
	if user == nil {
		return nil, fmt.Errorf("no user in context")
	}

	jid, err := strconv.ParseInt(job, 10, 64)
	if err != nil {
		cclog.Warn("Error while adding tag to job")
		return nil, err
	}

	tags := []*schema.Tag{}
	for _, tagID := range tagIds {
		// Get ID
		tid, err := strconv.ParseInt(tagID, 10, 64)
		if err != nil {
			cclog.Warn("Error while parsing tag id")
			return nil, err
		}

		// Test Exists
		_, _, tscope, exists := r.Repo.TagInfo(tid)
		if !exists {
			cclog.Warnf("Tag does not exist (ID): %d", tid)
			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
		}

		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
		if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
			user.Username == tscope {
			// Add to Job
			if tags, err = r.Repo.AddTag(user, jid, tid); err != nil {
				cclog.Warn("Error while adding tag")
				return nil, err
			}
		} else {
			cclog.Warnf("Not authorized to add tag: %d", tid)
			return nil, fmt.Errorf("not authorized to add tag: %d", tid)
		}
	}

	return tags, nil
}

// RemoveTagsFromJob is the resolver for the removeTagsFromJob field.
func (r *mutationResolver) RemoveTagsFromJob(ctx context.Context, job string, tagIds []string) ([]*schema.Tag, error) {
	user := repository.GetUserFromContext(ctx)
	if user == nil {
		return nil, fmt.Errorf("no user in context")
	}

	jid, err := strconv.ParseInt(job, 10, 64)
	if err != nil {
		cclog.Warn("Error while parsing job id")
		return nil, err
	}

	tags := []*schema.Tag{}
	for _, tagID := range tagIds {
		// Get ID
		tid, err := strconv.ParseInt(tagID, 10, 64)
		if err != nil {
			cclog.Warn("Error while parsing tag id")
			return nil, err
		}

		// Test Exists
		_, _, tscope, exists := r.Repo.TagInfo(tid)
		if !exists {
			cclog.Warnf("Tag does not exist (ID): %d", tid)
			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
		}

		// Test Access: Admins && Admin Tag OR Support/Admin and Global Tag OR Everyone && Private Tag
		if user.HasRole(schema.RoleAdmin) && tscope == "admin" ||
			user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) && tscope == "global" ||
			user.Username == tscope {
			// Remove from Job
			if tags, err = r.Repo.RemoveTag(user, jid, tid); err != nil {
				cclog.Warn("Error while removing tag")
				return nil, err
			}
		} else {
			cclog.Warnf("Not authorized to remove tag: %d", tid)
			return nil, fmt.Errorf("not authorized to remove tag: %d", tid)
		}

	}

	return tags, nil
}

// RemoveTagFromList is the resolver for the removeTagFromList field.
func (r *mutationResolver) RemoveTagFromList(ctx context.Context, tagIds []string) ([]int, error) {
	// Needs Contextuser
	user := repository.GetUserFromContext(ctx)
	if user == nil {
		return nil, fmt.Errorf("no user in context")
	}

	tags := []int{}
	for _, tagID := range tagIds {
		// Get ID
		tid, err := strconv.ParseInt(tagID, 10, 64)
		if err != nil {
			cclog.Warn("Error while parsing tag id for removal")
			return nil, err
		}

		// Test Exists
		_, _, tscope, exists := r.Repo.TagInfo(tid)
		if !exists {
			cclog.Warnf("Tag does not exist (ID): %d", tid)
			return nil, fmt.Errorf("tag does not exist (ID): %d", tid)
		}

		// Test Access: Admins && Admin Tag OR Everyone && Private Tag
		if user.HasRole(schema.RoleAdmin) && (tscope == "global" || tscope == "admin") || user.Username == tscope {
			// Remove from DB
			if err = r.Repo.RemoveTagByID(tid); err != nil {
				cclog.Warn("Error while removing tag")
				return nil, err
			} else {
				tags = append(tags, int(tid))
			}
		} else {
			cclog.Warnf("Not authorized to remove tag: %d", tid)
			return nil, fmt.Errorf("not authorized to remove tag: %d", tid)
		}
	}
	return tags, nil
}

// UpdateConfiguration is the resolver for the updateConfiguration field.
func (r *mutationResolver) UpdateConfiguration(ctx context.Context, name string, value string) (*string, error) {
	if err := repository.GetUserCfgRepo().UpdateConfig(name, value, repository.GetUserFromContext(ctx)); err != nil {
		cclog.Warn("Error while updating user config")
		return nil, err
	}

	return nil, nil
}

// ID is the resolver for the id field.
func (r *nodeResolver) ID(ctx context.Context, obj *schema.Node) (string, error) {
	panic(fmt.Errorf("not implemented: ID - id"))
}

// SchedulerState is the resolver for the schedulerState field.
func (r *nodeResolver) SchedulerState(ctx context.Context, obj *schema.Node) (schema.SchedulerState, error) {
	if obj.NodeState != "" {
		return obj.NodeState, nil
	} else {
		return "", fmt.Errorf("resolver: no SchedulerState (NodeState) on node object")
	}
}

// HealthState is the resolver for the healthState field.
func (r *nodeResolver) HealthState(ctx context.Context, obj *schema.Node) (string, error) {
	if obj.HealthState != "" {
		return string(obj.HealthState), nil
	} else {
		return "", fmt.Errorf("resolver: no HealthState (NodeState) on node object")
	}
}

// MetaData is the resolver for the metaData field.
func (r *nodeResolver) MetaData(ctx context.Context, obj *schema.Node) (any, error) {
	if obj.MetaData != nil {
		return obj.MetaData, nil
	} else {
		cclog.Debug("resolver: no MetaData (NodeState) on node object")
		emptyMeta := make(map[string]string, 0)
		return emptyMeta, nil
	}
}

// HealthData is the resolver for the healthData field.
func (r *nodeResolver) HealthData(ctx context.Context, obj *schema.Node) (any, error) {
	if obj.HealthData != nil {
		return obj.HealthData, nil
	} else {
		cclog.Debug("resolver: no HealthData (NodeState) on node object")
		emptyHealth := make(map[string][]string, 0)
		return emptyHealth, nil
	}
}

// Clusters is the resolver for the clusters field.
func (r *queryResolver) Clusters(ctx context.Context) ([]*schema.Cluster, error) {
	return archive.Clusters, nil
}

// Tags is the resolver for the tags field.
func (r *queryResolver) Tags(ctx context.Context) ([]*schema.Tag, error) {
	return r.Repo.GetTags(repository.GetUserFromContext(ctx), nil)
}

// GlobalMetrics is the resolver for the globalMetrics field.
func (r *queryResolver) GlobalMetrics(ctx context.Context) ([]*schema.GlobalMetricListItem, error) {
	user := repository.GetUserFromContext(ctx)

	if user != nil {
		if user.HasRole(schema.RoleUser) || user.HasRole(schema.RoleManager) {
			return archive.GlobalUserMetricList, nil
		}
	}

	return archive.GlobalMetricList, nil
}

// User is the resolver for the user field.
func (r *queryResolver) User(ctx context.Context, username string) (*model.User, error) {
	return repository.GetUserRepository().FetchUserInCtx(ctx, username)
}

// AllocatedNodes is the resolver for the allocatedNodes field.
func (r *queryResolver) AllocatedNodes(ctx context.Context, cluster string) ([]*model.Count, error) {
	data, err := r.Repo.AllocatedNodes(cluster)
	if err != nil {
		cclog.Warn("Error while fetching allocated nodes")
		return nil, err
	}

	counts := make([]*model.Count, 0, len(data))
	for subcluster, hosts := range data {
		counts = append(counts, &model.Count{
			Name:  subcluster,
			Count: len(hosts),
		})
	}

	return counts, nil
}

// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id string) (*schema.Node, error) {
	repo := repository.GetNodeRepository()
	numericID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		cclog.Warn("Error while parsing job id")
		return nil, err
	}
	return repo.GetNodeByID(numericID, false)
}

// Nodes is the resolver for the nodes field.
func (r *queryResolver) Nodes(ctx context.Context, filter []*model.NodeFilter, order *model.OrderByInput) (*model.NodeStateResultList, error) {
	repo := repository.GetNodeRepository()
	nodes, err := repo.QueryNodes(ctx, filter, nil, order) // Ignore Paging, Order Unused
	count := len(nodes)
	return &model.NodeStateResultList{Items: nodes, Count: &count}, err
}

// NodesWithMeta is the resolver for the nodesWithMeta field.
func (r *queryResolver) NodesWithMeta(ctx context.Context, filter []*model.NodeFilter, order *model.OrderByInput) (*model.NodeStateResultList, error) {
	// Why Extra Handler? -> graphql.CollectAllFields(ctx) only returns toplevel fields (i.e.: items, count), and not subfields like item.metaData
	repo := repository.GetNodeRepository()
	nodes, err := repo.QueryNodesWithMeta(ctx, filter, nil, order) // Ignore Paging, Order Unused
	count := len(nodes)
	return &model.NodeStateResultList{Items: nodes, Count: &count}, err
}

// NodeStates is the resolver for the nodeStates field.
func (r *queryResolver) NodeStates(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStates, error) {
	repo := repository.GetNodeRepository()

	stateCounts, serr := repo.CountStates(ctx, filter, "node_state")
	if serr != nil {
		cclog.Warnf("Error while counting nodeStates: %s", serr.Error())
		return nil, serr
	}

	healthCounts, herr := repo.CountStates(ctx, filter, "health_state")
	if herr != nil {
		cclog.Warnf("Error while counting healthStates: %s", herr.Error())
		return nil, herr
	}

	allCounts := append(stateCounts, healthCounts...)

	return allCounts, nil
}

// NodeStatesTimed is the resolver for the nodeStatesTimed field.
func (r *queryResolver) NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter, typeArg string) ([]*model.NodeStatesTimed, error) {
	repo := repository.GetNodeRepository()

	if typeArg == "node" {
		stateCounts, serr := repo.CountStatesTimed(ctx, filter, "node_state")
		if serr != nil {
			cclog.Warnf("Error while counting nodeStates in time: %s", serr.Error())
			return nil, serr
		}
		return stateCounts, nil
	}

	if typeArg == "health" {
		healthCounts, herr := repo.CountStatesTimed(ctx, filter, "health_state")
		if herr != nil {
			cclog.Warnf("Error while counting healthStates in time: %s", herr.Error())
			return nil, herr
		}
		return healthCounts, nil
	}

	return nil, errors.New("unknown Node State Query Type")
}

// Job is the resolver for the job field.
func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error) {
	numericID, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		cclog.Warn("Error while parsing job id")
		return nil, err
	}

	job, err := r.Repo.FindByID(ctx, numericID)
	if err != nil {
		cclog.Warn("Error while finding job by id")
		return nil, err
	}

	if user := repository.GetUserFromContext(ctx); user != nil &&
		job.User != user.Username &&
		user.HasNotRoles([]schema.Role{schema.RoleAdmin, schema.RoleSupport, schema.RoleManager}) {
		return nil, errors.New("you are not allowed to see this job")
	}

	return job, nil
}

// JobMetrics is the resolver for the jobMetrics field.
func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) {
	if resolution == nil { // Load from Config
		if config.Keys.EnableResampling != nil {
			defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
			resolution = &defaultRes
		} else { // Set 0 (Loads configured metric timestep)
			defaultRes := 0
			resolution = &defaultRes
		}
	}

	job, err := r.Query().Job(ctx, id)
	if err != nil {
		cclog.Warn("Error while querying job for metrics")
		return nil, err
	}

	data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution)
	if err != nil {
		cclog.Warn("Error while loading job data")
		return nil, err
	}

	res := []*model.JobMetricWithName{}
	for name, md := range data {
		for scope, metric := range md {
			res = append(res, &model.JobMetricWithName{
				Name:   name,
				Scope:  scope,
				Metric: metric,
			})
		}
	}

	return res, err
}

// JobStats is the resolver for the jobStats field.
func (r *queryResolver) JobStats(ctx context.Context, id string, metrics []string) ([]*model.NamedStats, error) {
	job, err := r.Query().Job(ctx, id)
	if err != nil {
		cclog.Warnf("Error while querying job %s for metadata", id)
		return nil, err
	}

	data, err := metricdispatch.LoadJobStats(job, metrics, ctx)
	if err != nil {
		cclog.Warnf("Error while loading jobStats data for job id %s", id)
		return nil, err
	}

	res := []*model.NamedStats{}
	for name, md := range data {
		res = append(res, &model.NamedStats{
			Name: name,
			Data: &md,
		})
	}

	return res, err
}

// ScopedJobStats is the resolver for the scopedJobStats field.
func (r *queryResolver) ScopedJobStats(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope) ([]*model.NamedStatsWithScope, error) {
	job, err := r.Query().Job(ctx, id)
	if err != nil {
		cclog.Warnf("Error while querying job %s for metadata", id)
		return nil, err
	}

	data, err := metricdispatch.LoadScopedJobStats(job, metrics, scopes, ctx)
	if err != nil {
		cclog.Warnf("Error while loading scopedJobStats data for job id %s", id)
		return nil, err
	}

	res := make([]*model.NamedStatsWithScope, 0)
	for name, scoped := range data {
		for scope, stats := range scoped {

			mdlStats := make([]*model.ScopedStats, 0)
			for _, stat := range stats {
				mdlStats = append(mdlStats, &model.ScopedStats{
					Hostname: stat.Hostname,
					ID:       stat.ID,
					Data:     stat.Data,
				})
			}

			res = append(res, &model.NamedStatsWithScope{
				Name:  name,
				Scope: scope,
				Stats: mdlStats,
			})
		}
	}

	return res, nil
}

// Jobs is the resolver for the jobs field.
func (r *queryResolver) Jobs(ctx context.Context, filter []*model.JobFilter, page *model.PageRequest, order *model.OrderByInput) (*model.JobResultList, error) {
	if page == nil {
		page = &model.PageRequest{
			ItemsPerPage: 50,
			Page:         1,
		}
	}

	jobs, err := r.Repo.QueryJobs(ctx, filter, page, order)
	if err != nil {
		cclog.Warn("Error while querying jobs")
		return nil, err
	}

	count, err := r.Repo.CountJobs(ctx, filter)
	if err != nil {
		cclog.Warn("Error while counting jobs")
		return nil, err
	}

	// Note: Even if App-Default 'config.Keys.UiDefaults["job_list_usePaging"]' is set, always return hasNextPage boolean.
	// Users can decide in frontend to use continuous scroll, even if app-default is paging!
	// Skip if page.ItemsPerPage == -1 ("Load All" -> No Next Page required, Status Dashboards)
	/*
	  Example Page 4 @ 10 IpP : Does item 41 exist?
	  Minimal Page 41 @ 1 IpP : If len(result) is 1, Page 5 @ 10 IpP exists.
	*/
	hasNextPage := false
	if page.ItemsPerPage != -1 {
		nextPage := &model.PageRequest{
			ItemsPerPage: 1,
			Page:         ((page.Page * page.ItemsPerPage) + 1),
		}
		nextJobs, err := r.Repo.QueryJobs(ctx, filter, nextPage, order)
		if err != nil {
			cclog.Warn("Error while querying next jobs")
			return nil, err
		}
		hasNextPage = len(nextJobs) == 1
	}

	return &model.JobResultList{Items: jobs, Count: &count, HasNextPage: &hasNextPage}, nil
}

// JobsStatistics is the resolver for the jobsStatistics field.
func (r *queryResolver) JobsStatistics(ctx context.Context, filter []*model.JobFilter, metrics []string, page *model.PageRequest, sortBy *model.SortByAggregate, groupBy *model.Aggregate, numDurationBins *string, numMetricBins *int) ([]*model.JobsStatistics, error) {
	var err error
	var stats []*model.JobsStatistics

	// Top Level Defaults
	defaultDurationBins := "1h"
	defaultMetricBins := 10

	// Build requested fields map for selective column computation
	statsFields := []string{
		"totalJobs", "totalUsers", "totalWalltime", "totalNodes", "totalCores",
		"totalAccs", "totalNodeHours", "totalCoreHours", "totalAccHours", "runningJobs", "shortJobs",
	}
	reqFields := make(map[string]bool, len(statsFields))
	fetchedMainStats := false
	for _, f := range statsFields {
		if requireField(ctx, f) {
			reqFields[f] = true
			if f != "runningJobs" && f != "shortJobs" {
				fetchedMainStats = true
			}
		}
	}

	if fetchedMainStats {
		if groupBy == nil {
			stats, err = r.Repo.JobsStats(ctx, filter, reqFields)
		} else {
			startGrouped := time.Now()
			stats, err = r.Repo.JobsStatsGrouped(ctx, filter, page, sortBy, groupBy, reqFields)
			cclog.Infof("Timer JobsStatsGrouped call: %s", time.Since(startGrouped))
		}
	} else {
		stats = make([]*model.JobsStatistics, 0, 1)
		stats = append(stats, &model.JobsStatistics{})
	}

	// runningJobs and shortJobs are already inlined in JobsStats/JobsStatsGrouped.
	// Only run separate count queries if main stats were not fetched.
	if !fetchedMainStats {
		if groupBy != nil {
			if requireField(ctx, "shortJobs") {
				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "short")
			}
			if requireField(ctx, "runningJobs") {
				stats, err = r.Repo.AddJobCountGrouped(ctx, filter, groupBy, stats, "running")
			}
		} else {
			if requireField(ctx, "shortJobs") {
				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "short")
			}
			if requireField(ctx, "runningJobs") {
				stats, err = r.Repo.AddJobCount(ctx, filter, stats, "running")
			}
		}
	}

	if err != nil {
		return nil, err
	}

	if requireField(ctx, "histDuration") || requireField(ctx, "histNumNodes") || requireField(ctx, "histNumCores") || requireField(ctx, "histNumAccs") {

		if numDurationBins == nil {
			numDurationBins = &defaultDurationBins
		}

		if groupBy == nil {
			stats[0], err = r.Repo.AddHistograms(ctx, filter, stats[0], numDurationBins)
			if err != nil {
				return nil, err
			}
		} else {
			return nil, errors.New("histograms only implemented without groupBy argument")
		}
	}

	if requireField(ctx, "histMetrics") {

		if numMetricBins == nil {
			numMetricBins = &defaultMetricBins
		}

		if groupBy == nil {
			stats[0], err = r.Repo.AddMetricHistograms(ctx, filter, metrics, stats[0], numMetricBins)
			if err != nil {
				return nil, err
			}
		} else {
			return nil, errors.New("metric histograms only implemented without groupBy argument")
		}
	}

	return stats, nil
}

// JobsMetricStats is the resolver for the jobsMetricStats field.
func (r *queryResolver) JobsMetricStats(ctx context.Context, filter []*model.JobFilter, metrics []string) ([]*model.JobStats, error) {
	// No Paging, Fixed Order by StartTime ASC
	order := &model.OrderByInput{
		Field: "startTime",
		Type:  "col",
		Order: "ASC",
	}

	jobs, err := r.Repo.QueryJobs(ctx, filter, nil, order)
	if err != nil {
		cclog.Warn("Error while querying jobs for comparison")
		return nil, err
	}

	res := []*model.JobStats{}
	for _, job := range jobs {
		data, err := metricdispatch.LoadJobStats(job, metrics, ctx)
		if err != nil {
			cclog.Warnf("Error while loading comparison jobStats data for job id %d", job.JobID)
			continue
			// return nil, err
		}

		sres := []*model.NamedStats{}
		for name, md := range data {
			sres = append(sres, &model.NamedStats{
				Name: name,
				Data: &md,
			})
		}

		numThreadsInt := int(job.NumHWThreads)
		numAccsInt := int(job.NumAcc)
		res = append(res, &model.JobStats{
			ID:              int(*job.ID),
			JobID:           strconv.Itoa(int(job.JobID)),
			StartTime:       int(job.StartTime),
			Duration:        int(job.Duration),
			Cluster:         job.Cluster,
			SubCluster:      job.SubCluster,
			NumNodes:        int(job.NumNodes),
			NumHWThreads:    &numThreadsInt,
			NumAccelerators: &numAccsInt,
			Stats:           sres,
		})
	}
	return res, err
}

// JobsFootprints is the resolver for the jobsFootprints field.
func (r *queryResolver) JobsFootprints(ctx context.Context, filter []*model.JobFilter, metrics []string) (*model.Footprints, error) {
	// NOTE: Legacy Naming! This resolver is for normalized histograms in analysis view only - *Not* related to DB "footprint" column!
	return r.jobsFootprints(ctx, filter, metrics)
}

// RooflineHeatmap is the resolver for the rooflineHeatmap field.
func (r *queryResolver) RooflineHeatmap(ctx context.Context, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) ([][]float64, error) {
	return r.rooflineHeatmap(ctx, filter, rows, cols, minX, minY, maxX, maxY)
}

// NodeMetrics is the resolver for the nodeMetrics field.
func (r *queryResolver) NodeMetrics(ctx context.Context, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) ([]*model.NodeMetrics, error) {
	user := repository.GetUserFromContext(ctx)
	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
		return nil, errors.New("you need to be administrator or support staff for this query")
	}

	defaultMetrics := make([]string, 0)
	for _, mc := range archive.GetCluster(cluster).MetricConfig {
		defaultMetrics = append(defaultMetrics, mc.Name)
	}
	if metrics == nil {
		metrics = defaultMetrics
	} else {
		metrics = slices.DeleteFunc(metrics, func(metric string) bool {
			return !slices.Contains(defaultMetrics, metric) // Remove undefined metrics.
		})
	}

	data, err := metricdispatch.LoadNodeData(cluster, metrics, nodes, scopes, from, to, ctx)
	if err != nil {
		cclog.Warn("error while loading node data")
		return nil, err
	}

	nodeRepo := repository.GetNodeRepository()
	stateMap, _ := nodeRepo.MapNodes(cluster)

	nodeMetrics := make([]*model.NodeMetrics, 0, len(data))
	for hostname, metrics := range data {
		host := &model.NodeMetrics{
			Host:    hostname,
			State:   stateMap[hostname],
			Metrics: make([]*model.JobMetricWithName, 0, len(metrics)*len(scopes)),
		}
		host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
		if err != nil {
			cclog.Warnf("error in nodeMetrics resolver: %s", err)
		}

		for metric, scopedMetrics := range metrics {
			for _, scopedMetric := range scopedMetrics {
				host.Metrics = append(host.Metrics, &model.JobMetricWithName{
					Name:   metric,
					Scope:  schema.MetricScopeNode,
					Metric: scopedMetric,
				})
			}
		}

		nodeMetrics = append(nodeMetrics, host)
	}

	return nodeMetrics, nil
}

// NodeMetricsList is the resolver for the nodeMetricsList field.
func (r *queryResolver) NodeMetricsList(ctx context.Context, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) (*model.NodesResultList, error) {
	if resolution == nil { // Load from Config
		if config.Keys.EnableResampling != nil {
			defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
			resolution = &defaultRes
		} else { // Set 0 (Loads configured metric timestep)
			defaultRes := 0
			resolution = &defaultRes
		}
	}

	user := repository.GetUserFromContext(ctx)
	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
		return nil, errors.New("you need to be administrator or support staff for this query")
	}

	nodeRepo := repository.GetNodeRepository()
	// nodes -> array hostname
	nodes, stateMap, countNodes, hasNextPage, nerr := nodeRepo.GetNodesForList(ctx, cluster, subCluster, stateFilter, nodeFilter, page)
	if nerr != nil {
		return nil, errors.New("could not retrieve node list required for resolving NodeMetricsList")
	}

	if metrics == nil {
		for _, mc := range archive.GetCluster(cluster).MetricConfig {
			metrics = append(metrics, mc.Name)
		}
	}

	// data -> map hostname:jobdata
	data, err := metricdispatch.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, *resolution, from, to, ctx)
	if err != nil {
		cclog.Warn("error while loading node data (Resolver.NodeMetricsList")
		return nil, err
	}

	nodeMetricsList := make([]*model.NodeMetrics, 0, len(data))
	for _, hostname := range nodes {
		host := &model.NodeMetrics{
			Host:    hostname,
			State:   stateMap[hostname],
			Metrics: make([]*model.JobMetricWithName, 0),
		}
		host.SubCluster, err = archive.GetSubClusterByNode(cluster, hostname)
		if err != nil {
			cclog.Warnf("error in nodeMetrics resolver: %s", err)
		}

		for metric, scopedMetrics := range data[hostname] {
			for scope, scopedMetric := range scopedMetrics {
				host.Metrics = append(host.Metrics, &model.JobMetricWithName{
					Name:   metric,
					Scope:  scope,
					Metric: scopedMetric,
				})
			}
		}

		nodeMetricsList = append(nodeMetricsList, host)
	}

	nodeMetricsListResult := &model.NodesResultList{
		Items: nodeMetricsList,
		// TotalNodes depends on sum of nodes grouped on latest timestamp, see repo/node.go:357
		TotalNodes:  &countNodes,
		HasNextPage: &hasNextPage,
	}

	return nodeMetricsListResult, nil
}

// ClusterMetrics is the resolver for the clusterMetrics field.
func (r *queryResolver) ClusterMetrics(ctx context.Context, cluster string, metrics []string, from time.Time, to time.Time) (*model.ClusterMetrics, error) {
	user := repository.GetUserFromContext(ctx)
	if user != nil && !user.HasAnyRole([]schema.Role{schema.RoleAdmin, schema.RoleSupport}) {
		return nil, errors.New("you need to be administrator or support staff for this query")
	}

	if metrics == nil {
		for _, mc := range archive.GetCluster(cluster).MetricConfig {
			metrics = append(metrics, mc.Name)
		}
	}

	// 'nodes' == nil -> Defaults to all nodes of cluster for existing query workflow
	scopes := []schema.MetricScope{"node"}
	data, err := metricdispatch.LoadNodeData(cluster, metrics, nil, scopes, from, to, ctx)
	if err != nil {
		cclog.Warn("error while loading node data")
		return nil, err
	}

	clusterMetricData := make([]*model.ClusterMetricWithName, 0)
	clusterMetrics := model.ClusterMetrics{NodeCount: 0, Metrics: clusterMetricData}

	collectorTimestep := make(map[string]int)
	collectorUnit := make(map[string]schema.Unit)
	collectorData := make(map[string][]schema.Float)

	for _, metrics := range data {
		clusterMetrics.NodeCount += 1
		for metric, scopedMetrics := range metrics {
			for _, scopedMetric := range scopedMetrics {
				// Collect Info Once
				_, okTimestep := collectorTimestep[metric]
				if !okTimestep {
					collectorTimestep[metric] = scopedMetric.Timestep
				}
				_, okUnit := collectorUnit[metric]
				if !okUnit {
					collectorUnit[metric] = scopedMetric.Unit
				}
				// Collect Data
				for _, ser := range scopedMetric.Series {
					_, okData := collectorData[metric]
					// Init With Datasize > 0
					if !okData && len(ser.Data) != 0 {
						collectorData[metric] = make([]schema.Float, len(ser.Data))
					} else if !okData {
						cclog.Debugf("[SCHEMARESOLVER] clusterMetrics skip init: no data -> %s at %s; size %d", metric, ser.Hostname, len(ser.Data))
					}
					// Sum if init'd and matching size
					if okData && len(ser.Data) == len(collectorData[metric]) {
						for i, val := range ser.Data {
							if val.IsNaN() {
								continue
							} else {
								collectorData[metric][i] += val
							}
						}
					} else if okData {
						cclog.Debugf("[SCHEMARESOLVER] clusterMetrics skip sum: data diff -> %s at %s; want size %d, have size %d", metric, ser.Hostname, len(collectorData[metric]), len(ser.Data))
					}
				}
			}
		}
	}

	for metricName, data := range collectorData {
		// use ccUnits for backend normalization to "Tera"
		p_old := ccunit.NewPrefix(collectorUnit[metricName].Prefix)
		p_new := ccunit.NewPrefix("T")
		convFunc := ccunit.GetPrefixPrefixFactor(p_old, p_new)
		u_new := schema.Unit{Prefix: p_new.Prefix(), Base: collectorUnit[metricName].Base}

		roundedData := make([]schema.Float, 0)
		for _, v_old := range data {
			v_new := math.Round(convFunc(float64(v_old)).(float64)*100.0) / 100.0
			roundedData = append(roundedData, schema.Float(v_new))
		}

		cm := model.ClusterMetricWithName{
			Name:     metricName,
			Unit:     &u_new,
			Timestep: collectorTimestep[metricName],
			Data:     roundedData,
		}

		clusterMetrics.Metrics = append(clusterMetrics.Metrics, &cm)
	}

	return &clusterMetrics, nil
}

// NumberOfNodes is the resolver for the numberOfNodes field.
func (r *subClusterResolver) NumberOfNodes(ctx context.Context, obj *schema.SubCluster) (int, error) {
	nodeList, err := archive.ParseNodeList(obj.Nodes)
	if err != nil {
		return 0, err
	}
	return nodeList.NodeCount(), nil
}

// Cluster returns generated.ClusterResolver implementation.
func (r *Resolver) Cluster() generated.ClusterResolver { return &clusterResolver{r} }

// Job returns generated.JobResolver implementation.
func (r *Resolver) Job() generated.JobResolver { return &jobResolver{r} }

// MetricValue returns generated.MetricValueResolver implementation.
func (r *Resolver) MetricValue() generated.MetricValueResolver { return &metricValueResolver{r} }

// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }

// Node returns generated.NodeResolver implementation.
func (r *Resolver) Node() generated.NodeResolver { return &nodeResolver{r} }

// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }

// SubCluster returns generated.SubClusterResolver implementation.
func (r *Resolver) SubCluster() generated.SubClusterResolver { return &subClusterResolver{r} }

type (
	clusterResolver     struct{ *Resolver }
	jobResolver         struct{ *Resolver }
	metricValueResolver struct{ *Resolver }
	mutationResolver    struct{ *Resolver }
	nodeResolver        struct{ *Resolver }
	queryResolver       struct{ *Resolver }
	subClusterResolver  struct{ *Resolver }
)


Let me read the remaining files needed.