From ae5d202661759560af63b7ded55e1a4ec09310c0 Mon Sep 17 00:00:00 2001 From: Jan Eitzinger Date: Thu, 23 Oct 2025 15:14:28 +0200 Subject: [PATCH 01/19] Remove S3Backend stub --- pkg/archive/s3Backend.go | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 pkg/archive/s3Backend.go diff --git a/pkg/archive/s3Backend.go b/pkg/archive/s3Backend.go deleted file mode 100644 index 6f0632e..0000000 --- a/pkg/archive/s3Backend.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. -// All rights reserved. This file is part of cc-backend. -// Use of this source code is governed by a MIT-style -// license that can be found in the LICENSE file. - -package archive - -type S3ArchiveConfig struct { - Path string `json:"filePath"` -} - -type S3Archive struct { - path string -} From 2287f4493aae730faea9ddb47969e927d3d30498 Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Tue, 28 Oct 2025 13:14:33 +0100 Subject: [PATCH 02/19] Reapply "Fix wrong memorystore nats schema" This reverts commit ea7660ddb3ed0be0d89b9ea4d05201029ecf5296. --- internal/memorystore/configSchema.go | 64 +++++++++++++++------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/internal/memorystore/configSchema.go b/internal/memorystore/configSchema.go index 133ba58..2616edc 100644 --- a/internal/memorystore/configSchema.go +++ b/internal/memorystore/configSchema.go @@ -51,35 +51,41 @@ const configSchema = `{ }, "nats": { "description": "Configuration for accepting published data through NATS.", - "type": "object", - "properties": { - "address": { - "description": "Address of the NATS server.", - "type": "string" - }, - "username": { - "description": "Optional: If configured with username/password method.", - "type": "string" - }, - "password": { - "description": "Optional: If configured with username/password method.", - "type": "string" - }, - "creds-file-path": { - "description": "Optional: If configured with Credential File method. Path to your NATS cred file.", - "type": "string" - }, - "subscriptions": { - "description": "Array of various subscriptions. Allows to subscibe to different subjects and publishers.", - "type": "object", - "properties": { - "subscribe-to": { - "description": "Channel name", - "type": "string" - }, - "cluster-tag": { - "description": "Optional: Allow lines without a cluster tag, use this as default", - "type": "string" + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { + "description": "Address of the NATS server.", + "type": "string" + }, + "username": { + "description": "Optional: If configured with username/password method.", + "type": "string" + }, + "password": { + "description": "Optional: If configured with username/password method.", + "type": "string" + }, + "creds-file-path": { + "description": "Optional: If configured with Credential File method. Path to your NATS cred file.", + "type": "string" + }, + "subscriptions": { + "description": "Array of various subscriptions. Allows to subscibe to different subjects and publishers.", + "type": "array", + "items": { + "type": "object", + "properties": { + "subscribe-to": { + "description": "Channel name", + "type": "string" + }, + "cluster-tag": { + "description": "Optional: Allow lines without a cluster tag, use this as default", + "type": "string" + } + } } } } From e49e5a04741900c91d167987bef5db524b624f48 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 5 Nov 2025 18:17:29 +0100 Subject: [PATCH 03/19] finalize timed node state backend code, concat functions --- api/schema.graphqls | 7 +- internal/graph/generated/generated.go | 137 ++++++++------------------ internal/graph/model/models_gen.go | 7 +- internal/graph/schema.resolvers.go | 40 ++++---- internal/repository/node.go | 46 +++++---- 5 files changed, 96 insertions(+), 141 deletions(-) diff --git a/api/schema.graphqls b/api/schema.graphqls index 4ee573c..410bdd5 100644 --- a/api/schema.graphqls +++ b/api/schema.graphqls @@ -28,9 +28,8 @@ type NodeStates { type NodeStatesTimed { state: String! - type: String! - count: Int! - time: Int! + counts: [Int!]! + times: [Int!]! } type Job { @@ -317,7 +316,7 @@ type Query { node(id: ID!): Node nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList! nodeStates(filter: [NodeFilter!]): [NodeStates!]! - nodeStatesTimed(filter: [NodeFilter!]): [NodeStatesTimed!]! + nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]! job(id: ID!): Job jobMetrics( diff --git a/internal/graph/generated/generated.go b/internal/graph/generated/generated.go index e1baf4c..e5f59e4 100644 --- a/internal/graph/generated/generated.go +++ b/internal/graph/generated/generated.go @@ -302,10 +302,9 @@ type ComplexityRoot struct { } NodeStatesTimed struct { - Count func(childComplexity int) int - State func(childComplexity int) int - Time func(childComplexity int) int - Type func(childComplexity int) int + Counts func(childComplexity int) int + State func(childComplexity int) int + Times func(childComplexity int) int } NodesResultList struct { @@ -332,7 +331,7 @@ type ComplexityRoot struct { NodeMetrics func(childComplexity int, cluster string, nodes []string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time) int NodeMetricsList func(childComplexity int, cluster string, subCluster string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int) int NodeStates func(childComplexity int, filter []*model.NodeFilter) int - NodeStatesTimed func(childComplexity int, filter []*model.NodeFilter) int + NodeStatesTimed func(childComplexity int, filter []*model.NodeFilter, typeArg string) int Nodes func(childComplexity int, filter []*model.NodeFilter, order *model.OrderByInput) int RooflineHeatmap func(childComplexity int, filter []*model.JobFilter, rows int, cols int, minX float64, minY float64, maxX float64, maxY float64) int ScopedJobStats func(childComplexity int, id string, metrics []string, scopes []schema.MetricScope) int @@ -473,7 +472,7 @@ type QueryResolver interface { Node(ctx context.Context, id string) (*schema.Node, error) Nodes(ctx context.Context, filter []*model.NodeFilter, order *model.OrderByInput) (*model.NodeStateResultList, error) NodeStates(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStates, error) - NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStatesTimed, error) + NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter, typeArg string) ([]*model.NodeStatesTimed, error) Job(ctx context.Context, id string) (*schema.Job, error) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int) ([]*model.JobMetricWithName, error) JobStats(ctx context.Context, id string, metrics []string) ([]*model.NamedStats, error) @@ -1617,12 +1616,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.NodeStates.State(childComplexity), true - case "NodeStatesTimed.count": - if e.complexity.NodeStatesTimed.Count == nil { + case "NodeStatesTimed.counts": + if e.complexity.NodeStatesTimed.Counts == nil { break } - return e.complexity.NodeStatesTimed.Count(childComplexity), true + return e.complexity.NodeStatesTimed.Counts(childComplexity), true case "NodeStatesTimed.state": if e.complexity.NodeStatesTimed.State == nil { @@ -1631,19 +1630,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.NodeStatesTimed.State(childComplexity), true - case "NodeStatesTimed.time": - if e.complexity.NodeStatesTimed.Time == nil { + case "NodeStatesTimed.times": + if e.complexity.NodeStatesTimed.Times == nil { break } - return e.complexity.NodeStatesTimed.Time(childComplexity), true - - case "NodeStatesTimed.type": - if e.complexity.NodeStatesTimed.Type == nil { - break - } - - return e.complexity.NodeStatesTimed.Type(childComplexity), true + return e.complexity.NodeStatesTimed.Times(childComplexity), true case "NodesResultList.count": if e.complexity.NodesResultList.Count == nil { @@ -1855,7 +1847,7 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return 0, false } - return e.complexity.Query.NodeStatesTimed(childComplexity, args["filter"].([]*model.NodeFilter)), true + return e.complexity.Query.NodeStatesTimed(childComplexity, args["filter"].([]*model.NodeFilter), args["type"].(string)), true case "Query.nodes": if e.complexity.Query.Nodes == nil { @@ -2441,9 +2433,8 @@ type NodeStates { type NodeStatesTimed { state: String! - type: String! - count: Int! - time: Int! + counts: [Int!]! + times: [Int!]! } type Job { @@ -2730,7 +2721,7 @@ type Query { node(id: ID!): Node nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList! nodeStates(filter: [NodeFilter!]): [NodeStates!]! - nodeStatesTimed(filter: [NodeFilter!]): [NodeStatesTimed!]! + nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]! job(id: ID!): Job jobMetrics( @@ -3315,6 +3306,11 @@ func (ec *executionContext) field_Query_nodeStatesTimed_args(ctx context.Context return nil, err } args["filter"] = arg0 + arg1, err := graphql.ProcessArgField(ctx, rawArgs, "type", ec.unmarshalNString2string) + if err != nil { + return nil, err + } + args["type"] = arg1 return args, nil } @@ -10630,8 +10626,8 @@ func (ec *executionContext) fieldContext_NodeStatesTimed_state(_ context.Context return fc, nil } -func (ec *executionContext) _NodeStatesTimed_type(ctx context.Context, field graphql.CollectedField, obj *model.NodeStatesTimed) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_NodeStatesTimed_type(ctx, field) +func (ec *executionContext) _NodeStatesTimed_counts(ctx context.Context, field graphql.CollectedField, obj *model.NodeStatesTimed) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NodeStatesTimed_counts(ctx, field) if err != nil { return graphql.Null } @@ -10644,7 +10640,7 @@ func (ec *executionContext) _NodeStatesTimed_type(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Type, nil + return obj.Counts, nil }) if err != nil { ec.Error(ctx, err) @@ -10656,56 +10652,12 @@ func (ec *executionContext) _NodeStatesTimed_type(ctx context.Context, field gra } return graphql.Null } - res := resTmp.(string) + res := resTmp.([]int) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalNInt2ᚕintᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_NodeStatesTimed_type(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "NodeStatesTimed", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type String does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _NodeStatesTimed_count(ctx context.Context, field graphql.CollectedField, obj *model.NodeStatesTimed) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_NodeStatesTimed_count(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { - ctx = rctx // use context from middleware stack in children - return obj.Count, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(int) - fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_NodeStatesTimed_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_NodeStatesTimed_counts(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "NodeStatesTimed", Field: field, @@ -10718,8 +10670,8 @@ func (ec *executionContext) fieldContext_NodeStatesTimed_count(_ context.Context return fc, nil } -func (ec *executionContext) _NodeStatesTimed_time(ctx context.Context, field graphql.CollectedField, obj *model.NodeStatesTimed) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_NodeStatesTimed_time(ctx, field) +func (ec *executionContext) _NodeStatesTimed_times(ctx context.Context, field graphql.CollectedField, obj *model.NodeStatesTimed) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_NodeStatesTimed_times(ctx, field) if err != nil { return graphql.Null } @@ -10732,7 +10684,7 @@ func (ec *executionContext) _NodeStatesTimed_time(ctx context.Context, field gra }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return obj.Time, nil + return obj.Times, nil }) if err != nil { ec.Error(ctx, err) @@ -10744,12 +10696,12 @@ func (ec *executionContext) _NodeStatesTimed_time(ctx context.Context, field gra } return graphql.Null } - res := resTmp.(int) + res := resTmp.([]int) fc.Result = res - return ec.marshalNInt2int(ctx, field.Selections, res) + return ec.marshalNInt2ᚕintᚄ(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_NodeStatesTimed_time(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_NodeStatesTimed_times(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "NodeStatesTimed", Field: field, @@ -11514,7 +11466,7 @@ func (ec *executionContext) _Query_nodeStatesTimed(ctx context.Context, field gr }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().NodeStatesTimed(rctx, fc.Args["filter"].([]*model.NodeFilter)) + return ec.resolvers.Query().NodeStatesTimed(rctx, fc.Args["filter"].([]*model.NodeFilter), fc.Args["type"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -11541,12 +11493,10 @@ func (ec *executionContext) fieldContext_Query_nodeStatesTimed(ctx context.Conte switch field.Name { case "state": return ec.fieldContext_NodeStatesTimed_state(ctx, field) - case "type": - return ec.fieldContext_NodeStatesTimed_type(ctx, field) - case "count": - return ec.fieldContext_NodeStatesTimed_count(ctx, field) - case "time": - return ec.fieldContext_NodeStatesTimed_time(ctx, field) + case "counts": + return ec.fieldContext_NodeStatesTimed_counts(ctx, field) + case "times": + return ec.fieldContext_NodeStatesTimed_times(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type NodeStatesTimed", field.Name) }, @@ -19555,18 +19505,13 @@ func (ec *executionContext) _NodeStatesTimed(ctx context.Context, sel ast.Select if out.Values[i] == graphql.Null { out.Invalids++ } - case "type": - out.Values[i] = ec._NodeStatesTimed_type(ctx, field, obj) + case "counts": + out.Values[i] = ec._NodeStatesTimed_counts(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } - case "count": - out.Values[i] = ec._NodeStatesTimed_count(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - case "time": - out.Values[i] = ec._NodeStatesTimed_time(ctx, field, obj) + case "times": + out.Values[i] = ec._NodeStatesTimed_times(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index 7b64464..cd9bc87 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -196,10 +196,9 @@ type NodeStates struct { } type NodeStatesTimed struct { - State string `json:"state"` - Type string `json:"type"` - Count int `json:"count"` - Time int `json:"time"` + State string `json:"state"` + Counts []int `json:"counts"` + Times []int `json:"times"` } type NodesResultList struct { diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 15bc6df..9f91397 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -387,13 +387,13 @@ func (r *queryResolver) Nodes(ctx context.Context, filter []*model.NodeFilter, o func (r *queryResolver) NodeStates(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStates, error) { repo := repository.GetNodeRepository() - stateCounts, serr := repo.CountNodeStates(ctx, filter) + 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.CountHealthStates(ctx, filter) + healthCounts, herr := repo.CountStates(ctx, filter, "health_state") if herr != nil { cclog.Warnf("Error while counting healthStates: %s", herr.Error()) return nil, herr @@ -406,26 +406,28 @@ func (r *queryResolver) NodeStates(ctx context.Context, filter []*model.NodeFilt } // NodeStatesTimed is the resolver for the nodeStatesTimed field. -func (r *queryResolver) NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter) ([]*model.NodeStatesTimed, error) { - panic(fmt.Errorf("not implemented: NodeStatesTimed - NodeStatesTimed")) - // repo := repository.GetNodeRepository() +func (r *queryResolver) NodeStatesTimed(ctx context.Context, filter []*model.NodeFilter, typeArg string) ([]*model.NodeStatesTimed, error) { + repo := repository.GetNodeRepository() - // stateCounts, serr := repo.CountNodeStates(ctx, filter) - // if serr != nil { - // cclog.Warnf("Error while counting nodeStates: %s", serr.Error()) - // return nil, serr - // } + 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 + } - // healthCounts, herr := repo.CountHealthStates(ctx, filter) - // if herr != nil { - // cclog.Warnf("Error while counting healthStates: %s", herr.Error()) - // return nil, herr - // } + 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 + } - // allCounts := make([]*model.NodeStates, 0) - // allCounts = append(stateCounts, healthCounts...) - - // return allCounts, nil + return nil, errors.New("Unknown Node State Query Type") } // Job is the resolver for the job field. diff --git a/internal/repository/node.go b/internal/repository/node.go index c3152f4..3115b9d 100644 --- a/internal/repository/node.go +++ b/internal/repository/node.go @@ -357,8 +357,8 @@ func (r *NodeRepository) ListNodes(cluster string) ([]*schema.Node, error) { return nodeList, nil } -func (r *NodeRepository) CountNodeStates(ctx context.Context, filters []*model.NodeFilter) ([]*model.NodeStates, error) { - query, qerr := AccessCheck(ctx, sq.Select("hostname", "node_state", "MAX(time_stamp) as time").From("node")) +func (r *NodeRepository) CountStates(ctx context.Context, filters []*model.NodeFilter, column string) ([]*model.NodeStates, error) { + query, qerr := AccessCheck(ctx, sq.Select("hostname", column, "MAX(time_stamp) as time").From("node")) if qerr != nil { return nil, qerr } @@ -395,16 +395,16 @@ func (r *NodeRepository) CountNodeStates(ctx context.Context, filters []*model.N stateMap := map[string]int{} for rows.Next() { - var hostname, node_state string + var hostname, state string var timestamp int64 - if err := rows.Scan(&hostname, &node_state, ×tamp); err != nil { + if err := rows.Scan(&hostname, &state, ×tamp); err != nil { rows.Close() cclog.Warn("Error while scanning rows (NodeStates)") return nil, err } - stateMap[node_state] += 1 + stateMap[state] += 1 } nodes := make([]*model.NodeStates, 0) @@ -416,8 +416,8 @@ func (r *NodeRepository) CountNodeStates(ctx context.Context, filters []*model.N return nodes, nil } -func (r *NodeRepository) CountHealthStates(ctx context.Context, filters []*model.NodeFilter) ([]*model.NodeStates, error) { - query, qerr := AccessCheck(ctx, sq.Select("hostname", "health_state", "MAX(time_stamp) as time").From("node")) +func (r *NodeRepository) CountStatesTimed(ctx context.Context, filters []*model.NodeFilter, column string) ([]*model.NodeStatesTimed, error) { + query, qerr := AccessCheck(ctx, sq.Select(column, "time_stamp", "count(*) as count").From("node")) // "cluster"? if qerr != nil { return nil, qerr } @@ -425,6 +425,11 @@ func (r *NodeRepository) CountHealthStates(ctx context.Context, filters []*model query = query.Join("node_state ON node_state.node_id = node.id") for _, f := range filters { + // Required + if f.TimeStart != nil { + query = query.Where("time_stamp > ?", f.TimeStart) + } + // Optional if f.Hostname != nil { query = buildStringCondition("hostname", f.Hostname, query) } @@ -443,7 +448,7 @@ func (r *NodeRepository) CountHealthStates(ctx context.Context, filters []*model } // Add Group and Order - query = query.GroupBy("hostname").OrderBy("hostname DESC") + query = query.GroupBy(column + ", time_stamp").OrderBy("time_stamp ASC") rows, err := query.RunWith(r.stmtCache).Query() if err != nil { @@ -452,27 +457,32 @@ func (r *NodeRepository) CountHealthStates(ctx context.Context, filters []*model return nil, err } - stateMap := map[string]int{} + rawData := make(map[string][][]int) for rows.Next() { - var hostname, health_state string - var timestamp int64 + var state string + var time, count int - if err := rows.Scan(&hostname, &health_state, ×tamp); err != nil { + if err := rows.Scan(&state, &time, &count); err != nil { rows.Close() cclog.Warn("Error while scanning rows (NodeStates)") return nil, err } - stateMap[health_state] += 1 + if rawData[state] == nil { + rawData[state] = [][]int{make([]int, 0), make([]int, 0)} + } + + rawData[state][0] = append(rawData[state][0], time) + rawData[state][1] = append(rawData[state][1], count) } - nodes := make([]*model.NodeStates, 0) - for state, counts := range stateMap { - node := model.NodeStates{State: state, Count: counts} - nodes = append(nodes, &node) + timedStates := make([]*model.NodeStatesTimed, 0) + for state, data := range rawData { + entry := model.NodeStatesTimed{State: state, Times: data[0], Counts: data[1]} + timedStates = append(timedStates, &entry) } - return nodes, nil + return timedStates, nil } func AccessCheck(ctx context.Context, query sq.SelectBuilder) (sq.SelectBuilder, error) { From ecad52c18d4c661088a2009a4114dd1c51f5527d Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 6 Nov 2025 11:15:11 +0100 Subject: [PATCH 04/19] fix: fix defautl time range select values --- web/frontend/src/generic/select/TimeSelection.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/frontend/src/generic/select/TimeSelection.svelte b/web/frontend/src/generic/select/TimeSelection.svelte index 6595099..7d3c0c8 100644 --- a/web/frontend/src/generic/select/TimeSelection.svelte +++ b/web/frontend/src/generic/select/TimeSelection.svelte @@ -36,7 +36,7 @@ /* Const Init */ const defaultTo = new Date(Date.now()); - const defaultFrom = new Date(defaultTo.setHours(defaultTo.getHours() - 4)); + const defaultFrom = new Date(new Date(Date.now()).setHours(defaultTo.getHours() - 4)); /* State Init */ let timeType = $state("range"); From a6c43e6f2facae92547738338ff95d2f0cd62d3b Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Tue, 11 Nov 2025 17:03:59 +0100 Subject: [PATCH 05/19] finalize timed node state frontend code for status view --- web/frontend/src/generic/plots/Stacked.svelte | 510 +++++------------- web/frontend/src/generic/units.js | 5 +- web/frontend/src/status/StatusDash.svelte | 84 +-- 3 files changed, 199 insertions(+), 400 deletions(-) diff --git a/web/frontend/src/generic/plots/Stacked.svelte b/web/frontend/src/generic/plots/Stacked.svelte index cb8ffd0..deac815 100644 --- a/web/frontend/src/generic/plots/Stacked.svelte +++ b/web/frontend/src/generic/plots/Stacked.svelte @@ -1,34 +1,25 @@ -{#if data && data[0].length > 0} +{#if data && collectData[0].length > 0}
{:else} Cannot render plot: No series data returned for {metric?metric:'job resources'}Cannot render plot: No series data returned for {stateType} State Stacked Chart {/if} diff --git a/web/frontend/src/generic/units.js b/web/frontend/src/generic/units.js index dbf220a..1737b97 100644 --- a/web/frontend/src/generic/units.js +++ b/web/frontend/src/generic/units.js @@ -51,12 +51,13 @@ export function formatDurationTime(t, forNode = false) { } } -export function formatUnixTime(t) { +export function formatUnixTime(t, withDate = false) { if (t !== null) { if (isNaN(t)) { return t; } else { - return new Date(t * 1000).toLocaleString() + if (withDate) return new Date(t * 1000).toLocaleString(); + else return new Date(t * 1000).toLocaleTimeString(); } } } diff --git a/web/frontend/src/status/StatusDash.svelte b/web/frontend/src/status/StatusDash.svelte index 758c563..e3c0a45 100644 --- a/web/frontend/src/status/StatusDash.svelte +++ b/web/frontend/src/status/StatusDash.svelte @@ -25,10 +25,12 @@ import { init, } from "../generic/utils.js"; - import { scaleNumbers, formatDurationTime } from "../generic/units.js"; + import { formatDurationTime } from "../generic/units.js"; import Refresher from "../generic/helper/Refresher.svelte"; + import TimeSelection from "../generic/select/TimeSelection.svelte"; import Roofline from "../generic/plots/Roofline.svelte"; import Pie, { colors } from "../generic/plots/Pie.svelte"; + import Stacked from "../generic/plots/Stacked.svelte"; /* Svelte 5 Props */ let { @@ -44,10 +46,12 @@ /* State Init */ let cluster = $state(presetCluster); let pieWidth = $state(0); - let stackedWidth = $state(0); + let stackedWidth1 = $state(0); + let stackedWidth2 = $state(0); let plotWidths = $state([]); let from = $state(new Date(Date.now() - 5 * 60 * 1000)); let to = $state(new Date(Date.now())); + let stackedFrom = $state(Math.floor(Date.now() / 1000) - 14400); // Bar Gauges let allocatedNodes = $state({}); let allocatedAccs = $state({}); @@ -80,28 +84,38 @@ })); const refinedStateData = $derived.by(() => { - return $nodesStateCounts?.data?.nodeStates.filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state)) + return $nodesStateCounts?.data?.nodeStates. + filter((e) => ['allocated', 'reserved', 'idle', 'mixed','down', 'unknown'].includes(e.state)). + sort((a, b) => b.count - a.count) }); const refinedHealthData = $derived.by(() => { - return $nodesStateCounts?.data?.nodeStates.filter((e) => ['full', 'partial', 'failed'].includes(e.state)) + return $nodesStateCounts?.data?.nodeStates. + filter((e) => ['full', 'partial', 'failed'].includes(e.state)). + sort((a, b) => b.count - a.count) }); - // NodeStates for Stacked charts - const nodesStateTimes = $derived(queryStore({ + // States for Stacked charts + const statesTimed = $derived(queryStore({ client: client, query: gql` - query ($filter: [NodeFilter!]) { - nodeStatesTimed(filter: $filter) { + query ($filter: [NodeFilter!], $typeNode: String!, $typeHealth: String!) { + nodeStates: nodeStatesTimed(filter: $filter, type: $typeNode) { state - type - count - time + counts + times + } + healthStates: nodeStatesTimed(filter: $filter, type: $typeHealth) { + state + counts + times } } `, variables: { - filter: { cluster: { eq: cluster }, timeStart: Date.now() - (24 * 3600 * 1000)} // Add Selector for Timeframe (4h, 12h, 24h)? + filter: { cluster: { eq: cluster }, timeStart: 1760097059}, // stackedFrom + typeNode: "node", + typeHealth: "health" }, })); @@ -378,12 +392,19 @@ - + + + { + stackedFrom = Math.floor(newFrom.getTime() / 1000); + }} + /> + { - console.log('Trigger Refresh StatusTab') from = new Date(Date.now() - 5 * 60 * 1000); to = new Date(Date.now()); }} @@ -394,43 +415,40 @@
- {#if $initq.data && $nodesStateCounts.data} @@ -449,7 +465,7 @@
{#key refinedStateData}

- {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States + Current {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node States

{#key refinedHealthData}

- {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node Health + Current {cluster.charAt(0).toUpperCase() + cluster.slice(1)} Node Health

Date: Wed, 12 Nov 2025 12:46:00 +0100 Subject: [PATCH 06/19] fix leftover dev variable --- web/frontend/src/status/StatusDash.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/frontend/src/status/StatusDash.svelte b/web/frontend/src/status/StatusDash.svelte index e3c0a45..08cf93a 100644 --- a/web/frontend/src/status/StatusDash.svelte +++ b/web/frontend/src/status/StatusDash.svelte @@ -113,7 +113,7 @@ } `, variables: { - filter: { cluster: { eq: cluster }, timeStart: 1760097059}, // stackedFrom + filter: { cluster: { eq: cluster }, timeStart: stackedFrom}, typeNode: "node", typeHealth: "health" }, From 7f740455fe35180a665cbb92f5c3c8ea175a6b22 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 12 Nov 2025 13:09:31 +0100 Subject: [PATCH 07/19] fix old gql field name --- web/frontend/src/generic/plots/Roofline.svelte | 14 +++++++------- web/frontend/src/status/StatusDash.svelte | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/web/frontend/src/generic/plots/Roofline.svelte b/web/frontend/src/generic/plots/Roofline.svelte index 4f6dcb9..79ece22 100644 --- a/web/frontend/src/generic/plots/Roofline.svelte +++ b/web/frontend/src/generic/plots/Roofline.svelte @@ -276,13 +276,13 @@ // Nodes: Color based on Idle vs. Allocated } else if (nodesData) { // console.log('In Plot Handler NodesData', nodesData) - if (nodesData[i]?.nodeState == "idle") { + if (nodesData[i]?.schedulerState == "idle") { //u.ctx.strokeStyle = "rgb(0, 0, 255)"; u.ctx.fillStyle = "rgba(0, 0, 255, 0.5)"; - } else if (nodesData[i]?.nodeState == "allocated") { + } else if (nodesData[i]?.schedulerState == "allocated") { //u.ctx.strokeStyle = "rgb(0, 255, 0)"; u.ctx.fillStyle = "rgba(0, 255, 0, 0.5)"; - } else if (nodesData[i]?.nodeState == "notindb") { + } else if (nodesData[i]?.schedulerState == "notindb") { //u.ctx.strokeStyle = "rgb(0, 0, 0)"; u.ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; } else { // Fallback: All other DEFINED states @@ -436,11 +436,11 @@ tooltip.style.borderColor = getRGB(u.data[2][i]); // Nodes: Color based on Idle vs. Allocated } else if (nodesData) { - if (nodesData[i]?.nodeState == "idle") { + if (nodesData[i]?.schedulerState == "idle") { tooltip.style.borderColor = "rgb(0, 0, 255)"; - } else if (nodesData[i]?.nodeState == "allocated") { + } else if (nodesData[i]?.schedulerState == "allocated") { tooltip.style.borderColor = "rgb(0, 255, 0)"; - } else if (nodesData[i]?.nodeState == "notindb") { // Missing from DB table + } else if (nodesData[i]?.schedulerState == "notindb") { // Missing from DB table tooltip.style.borderColor = "rgb(0, 0, 0)"; } else { // Fallback: All other DEFINED states tooltip.style.borderColor = "rgb(255, 0, 0)"; @@ -459,7 +459,7 @@ } else if (nodesData && useColors) { tooltip.textContent = ( // Tooltip Content as String for Node - `Host: ${getLegendData(u, i).nodeName}\nState: ${getLegendData(u, i).nodeState}\nJobs: ${getLegendData(u, i).numJobs}` + `Host: ${getLegendData(u, i).nodeName}\nState: ${getLegendData(u, i).schedulerState}\nJobs: ${getLegendData(u, i).numJobs}` ); } else if (nodesData && !useColors) { tooltip.textContent = ( diff --git a/web/frontend/src/status/StatusDash.svelte b/web/frontend/src/status/StatusDash.svelte index 08cf93a..9916709 100644 --- a/web/frontend/src/status/StatusDash.svelte +++ b/web/frontend/src/status/StatusDash.svelte @@ -191,7 +191,7 @@ hostname cluster subCluster - nodeState + schedulerState } } # totalNodes includes multiples if shared jobs @@ -362,7 +362,7 @@ for (let j = 0; j < subClusterData.length; j++) { const nodeName = subClusterData[j]?.host ? subClusterData[j].host : "unknown" const nodeMatch = $statusQuery?.data?.nodes?.items?.find((n) => n.hostname == nodeName && n.subCluster == subClusterData[j].subCluster); - const nodeState = nodeMatch?.nodeState ? nodeMatch.nodeState : "notindb" + const schedulerState = nodeMatch?.schedulerState ? nodeMatch.schedulerState : "notindb" let numJobs = 0 if ($statusQuery?.data) { @@ -370,7 +370,7 @@ numJobs = nodeJobs?.length ? nodeJobs.length : 0 } - result.push({nodeName: nodeName, nodeState: nodeState, numJobs: numJobs}) + result.push({nodeName: nodeName, schedulerState: schedulerState, numJobs: numJobs}) }; }; return result From c2c63d2f67e747a00712b44ebc7d16c7b7aa6e3f Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 12 Nov 2025 13:38:58 +0100 Subject: [PATCH 08/19] fix backend node queries - wrong table name - wrong scan field count: timestamp catch for log --- internal/repository/node.go | 39 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/internal/repository/node.go b/internal/repository/node.go index 3115b9d..02ccf20 100644 --- a/internal/repository/node.go +++ b/internal/repository/node.go @@ -116,16 +116,17 @@ func (r *NodeRepository) FetchMetadata(hostname string, cluster string) (map[str func (r *NodeRepository) GetNode(hostname string, cluster string, withMeta bool) (*schema.Node, error) { node := &schema.Node{} + var timestamp int if err := sq.Select("node.hostname", "node.cluster", "node.subcluster", "node_state.node_state", "node_state.health_state", "MAX(node_state.time_stamp) as time"). From("node_state"). - Join("node ON nodes_state.node_id = node.id"). + Join("node ON node_state.node_id = node.id"). Where("node.hostname = ?", hostname). Where("node.cluster = ?", cluster). GroupBy("node_state.node_id"). RunWith(r.DB). - QueryRow().Scan(&node.Hostname, &node.Cluster, &node.SubCluster, &node.NodeState, &node.HealthState); err != nil { - cclog.Warnf("Error while querying node '%s' from database: %v", hostname, err) + QueryRow().Scan(&node.Hostname, &node.Cluster, &node.SubCluster, &node.NodeState, &node.HealthState, ×tamp); err != nil { + cclog.Warnf("Error while querying node '%s' at time '%d' from database: %v", hostname, timestamp, err) return nil, err } @@ -144,15 +145,16 @@ func (r *NodeRepository) GetNode(hostname string, cluster string, withMeta bool) func (r *NodeRepository) GetNodeById(id int64, withMeta bool) (*schema.Node, error) { node := &schema.Node{} + var timestamp int if err := sq.Select("node.hostname", "node.cluster", "node.subcluster", "node_state.node_state", "node_state.health_state", "MAX(node_state.time_stamp) as time"). From("node_state"). - Join("node ON nodes_state.node_id = node.id"). + Join("node ON node_state.node_id = node.id"). Where("node.id = ?", id). GroupBy("node_state.node_id"). RunWith(r.DB). - QueryRow().Scan(&node.Hostname, &node.Cluster, &node.SubCluster, &node.NodeState, &node.HealthState); err != nil { - cclog.Warnf("Error while querying node ID '%d' from database: %v", id, err) + QueryRow().Scan(&node.Hostname, &node.Cluster, &node.SubCluster, &node.NodeState, &node.HealthState, ×tamp); err != nil { + cclog.Warnf("Error while querying node ID '%d' at time '%d' from database: %v", id, timestamp, err) return nil, err } @@ -278,7 +280,7 @@ func (r *NodeRepository) QueryNodes( sq.Select("node.hostname", "node.cluster", "node.subcluster", "node_state.node_state", "node_state.health_state", "MAX(node_state.time_stamp) as time"). From("node"). - Join("node_state ON nodes_state.node_id = node.id")) + Join("node_state ON node_state.node_id = node.id")) if qerr != nil { return nil, qerr } @@ -314,11 +316,11 @@ func (r *NodeRepository) QueryNodes( nodes := make([]*schema.Node, 0, 50) for rows.Next() { node := schema.Node{} - + var timestamp int if err := rows.Scan(&node.Hostname, &node.Cluster, &node.SubCluster, - &node.NodeState, &node.HealthState); err != nil { + &node.NodeState, &node.HealthState, ×tamp); err != nil { rows.Close() - cclog.Warn("Error while scanning rows (Nodes)") + cclog.Warnf("Error while scanning rows (QueryNodes) at time '%d'", timestamp) return nil, err } nodes = append(nodes, &node) @@ -345,9 +347,10 @@ func (r *NodeRepository) ListNodes(cluster string) ([]*schema.Node, error) { defer rows.Close() for rows.Next() { node := &schema.Node{} + var timestamp int if err := rows.Scan(&node.Hostname, &node.Cluster, - &node.SubCluster, &node.NodeState, &node.HealthState); err != nil { - cclog.Warn("Error while scanning node list") + &node.SubCluster, &node.NodeState, &node.HealthState, ×tamp); err != nil { + cclog.Warnf("Error while scanning node list (ListNodes) at time '%d'", timestamp) return nil, err } @@ -396,11 +399,11 @@ func (r *NodeRepository) CountStates(ctx context.Context, filters []*model.NodeF stateMap := map[string]int{} for rows.Next() { var hostname, state string - var timestamp int64 + var timestamp int if err := rows.Scan(&hostname, &state, ×tamp); err != nil { rows.Close() - cclog.Warn("Error while scanning rows (NodeStates)") + cclog.Warnf("Error while scanning rows (CountStates) at time '%d'", timestamp) return nil, err } @@ -460,11 +463,11 @@ func (r *NodeRepository) CountStatesTimed(ctx context.Context, filters []*model. rawData := make(map[string][][]int) for rows.Next() { var state string - var time, count int + var timestamp, count int - if err := rows.Scan(&state, &time, &count); err != nil { + if err := rows.Scan(&state, ×tamp, &count); err != nil { rows.Close() - cclog.Warn("Error while scanning rows (NodeStates)") + cclog.Warnf("Error while scanning rows (CountStatesTimed) at time '%d'", timestamp) return nil, err } @@ -472,7 +475,7 @@ func (r *NodeRepository) CountStatesTimed(ctx context.Context, filters []*model. rawData[state] = [][]int{make([]int, 0), make([]int, 0)} } - rawData[state][0] = append(rawData[state][0], time) + rawData[state][0] = append(rawData[state][0], timestamp) rawData[state][1] = append(rawData[state][1], count) } From fb278182d37a5cccdf24e66a6b10532575f033c0 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 12 Nov 2025 13:50:09 +0100 Subject: [PATCH 09/19] add schedulerState resolver --- internal/graph/schema.resolvers.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go index 9f91397..debe54a 100644 --- a/internal/graph/schema.resolvers.go +++ b/internal/graph/schema.resolvers.go @@ -312,7 +312,11 @@ func (r *nodeResolver) ID(ctx context.Context, obj *schema.Node) (string, error) // SchedulerState is the resolver for the schedulerState field. func (r *nodeResolver) SchedulerState(ctx context.Context, obj *schema.Node) (schema.SchedulerState, error) { - panic(fmt.Errorf("not implemented: SchedulerState - schedulerState")) + if obj.NodeState != "" { + return obj.NodeState, nil + } else { + return "", fmt.Errorf("No SchedulerState (NodeState) on Object") + } } // HealthState is the resolver for the healthState field. From f56783a4390247460336c37d03bcbdeea77d5b71 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 12 Nov 2025 16:44:49 +0100 Subject: [PATCH 10/19] add plot cursor sync to nodelist rows --- .../src/generic/plots/Comparogram.svelte | 16 ++++++++++------ web/frontend/src/generic/plots/MetricPlot.svelte | 10 ++++++++++ .../src/systems/nodelist/NodeListRow.svelte | 6 ++++++ 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/web/frontend/src/generic/plots/Comparogram.svelte b/web/frontend/src/generic/plots/Comparogram.svelte index 2a37417..96c06f8 100644 --- a/web/frontend/src/generic/plots/Comparogram.svelte +++ b/web/frontend/src/generic/plots/Comparogram.svelte @@ -39,7 +39,7 @@ yunit = "", title = "", forResources = false, - plotSync, + plotSync = null, } = $props(); /* Const Init */ @@ -204,11 +204,7 @@ live: true, }, cursor: { - drag: { x: true, y: true }, - sync: { - key: plotSync.key, - scales: ["x", null], - } + drag: { x: true, y: true } } }; @@ -293,6 +289,14 @@ if (!uplot) { opts.width = ren_width; opts.height = ren_height; + + if (plotSync) { + opts.cursor.sync = { + key: plotSync.key, + scales: ["x", null], + } + } + uplot = new uPlot(opts, data, plotWrapper); // Data is uplot formatted [[X][Ymin][Yavg][Ymax]] plotSync.sub(uplot) } else { diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index 2f70c96..1073c68 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -48,6 +48,7 @@ zoomState = null, thresholdState = null, extendedLegendData = null, + plotSync = null, onZoom } = $props(); @@ -576,6 +577,14 @@ if (!uplot) { opts.width = ren_width; opts.height = ren_height; + + if (plotSync) { + opts.cursor.sync = { + key: plotSync.key, + scales: ["x", null], + } + } + if (zoomState && metricConfig?.aggregation == "avg") { opts.scales = {...zoomState} } else if (zoomState && metricConfig?.aggregation == "sum") { @@ -584,6 +593,7 @@ if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} }; } // else: reset scaling to default } + uplot = new uPlot(opts, plotData, plotWrapper); } else { uplot.setSize({ width: ren_width, height: ren_height }); diff --git a/web/frontend/src/systems/nodelist/NodeListRow.svelte b/web/frontend/src/systems/nodelist/NodeListRow.svelte index a9111f6..cef70cc 100644 --- a/web/frontend/src/systems/nodelist/NodeListRow.svelte +++ b/web/frontend/src/systems/nodelist/NodeListRow.svelte @@ -13,6 +13,7 @@ gql, getContextClient, } from "@urql/svelte"; + import uPlot from "uplot"; import { Card, CardBody, Spinner } from "@sveltestrap/sveltestrap"; import { maxScope, checkMetricDisabled, scramble, scrambleNames } from "../../generic/utils.js"; import MetricPlot from "../../generic/plots/MetricPlot.svelte"; @@ -25,6 +26,9 @@ selectedMetrics, } = $props(); + /* Var Init*/ + let plotSync = uPlot.sync(`nodeMetricStack-${nodeData.host}`); + /* Const Init */ const client = getContextClient(); const paging = { itemsPerPage: 50, page: 1 }; @@ -159,6 +163,7 @@ statisticsSeries={metricData.data?.metric.statisticsSeries} useStatsSeries={!!metricData.data?.metric.statisticsSeries} height={175} + {plotSync} forNode />
@@ -172,6 +177,7 @@ series={metricData.data.metric.series} height={175} {extendedLegendData} + {plotSync} forNode /> {/key} From 404be5f317691368e555f0195b0e01e2e6a0b4a1 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Wed, 12 Nov 2025 17:20:50 +0100 Subject: [PATCH 11/19] add optional legend flip to plots --- web/frontend/src/Node.root.svelte | 1 + web/frontend/src/User.root.svelte | 1 + web/frontend/src/generic/plots/Comparogram.svelte | 11 +++++++---- web/frontend/src/generic/plots/Histogram.svelte | 9 +++++++-- web/frontend/src/generic/plots/MetricPlot.svelte | 12 ++++++++---- web/frontend/src/generic/plots/Stacked.svelte | 9 ++++++--- web/frontend/src/job/Metric.svelte | 2 ++ web/frontend/src/status/StatisticsDash.svelte | 1 + web/frontend/src/status/UsageDash.svelte | 3 +++ web/frontend/src/systems/NodeOverview.svelte | 1 + 10 files changed, 37 insertions(+), 13 deletions(-) diff --git a/web/frontend/src/Node.root.svelte b/web/frontend/src/Node.root.svelte index 5c37ad1..85ea2f0 100644 --- a/web/frontend/src/Node.root.svelte +++ b/web/frontend/src/Node.root.svelte @@ -217,6 +217,7 @@ cluster={clusters.find((c) => c.name == cluster)} subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster} series={item.metric.series} + enableFlip forNode /> {:else if item.disabled === true && item.metric} diff --git a/web/frontend/src/User.root.svelte b/web/frontend/src/User.root.svelte index f435846..c5c70a0 100644 --- a/web/frontend/src/User.root.svelte +++ b/web/frontend/src/User.root.svelte @@ -348,6 +348,7 @@ ylabel="Number of Jobs" yunit="Jobs" usesBins + enableFlip /> {/snippet} diff --git a/web/frontend/src/generic/plots/Comparogram.svelte b/web/frontend/src/generic/plots/Comparogram.svelte index 96c06f8..b391a1f 100644 --- a/web/frontend/src/generic/plots/Comparogram.svelte +++ b/web/frontend/src/generic/plots/Comparogram.svelte @@ -271,9 +271,12 @@ function update(u) { const { left, top } = u.cursor; - const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; - legendEl.style.transform = - "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; + const internalWidth = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; + if (left < (width/2)) { + legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; + } else { + legendEl.style.transform = "translate(" + (left - internalWidth - 15) + "px, " + (top + 15) + "px)"; + } } return { @@ -289,7 +292,7 @@ if (!uplot) { opts.width = ren_width; opts.height = ren_height; - + if (plotSync) { opts.cursor.sync = { key: plotSync.key, diff --git a/web/frontend/src/generic/plots/Histogram.svelte b/web/frontend/src/generic/plots/Histogram.svelte index adbb849..a0af6c2 100644 --- a/web/frontend/src/generic/plots/Histogram.svelte +++ b/web/frontend/src/generic/plots/Histogram.svelte @@ -34,6 +34,7 @@ xtime = false, ylabel = "", yunit = "", + enableFlip = false, } = $props(); /* Const Init */ @@ -117,8 +118,12 @@ function update(u) { const { left, top } = u.cursor; - legendEl.style.transform = - "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; + const internalWidth = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; + if (enableFlip && (left < (width/2))) { + legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; + } else { + legendEl.style.transform = "translate(" + (left - internalWidth - 15) + "px, " + (top + 15) + "px)"; + } } return { diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index 1073c68..d9dda8e 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -49,6 +49,7 @@ thresholdState = null, extendedLegendData = null, plotSync = null, + enableFlip = false, onZoom } = $props(); @@ -396,9 +397,12 @@ function update(u) { const { left, top } = u.cursor; - const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; - legendEl.style.transform = - "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; + const internalWidth = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; + if (enableFlip && (left < (width/2))) { + legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; + } else { + legendEl.style.transform = "translate(" + (left - internalWidth - 15) + "px, " + (top + 15) + "px)"; + } } if (dataSize <= 12 || useStatsSeries) { @@ -593,7 +597,7 @@ if ((thresholdState === thresholds?.normal)) { opts.scales = {...zoomState} }; } // else: reset scaling to default } - + uplot = new uPlot(opts, plotData, plotWrapper); } else { uplot.setSize({ width: ren_width, height: ren_height }); diff --git a/web/frontend/src/generic/plots/Stacked.svelte b/web/frontend/src/generic/plots/Stacked.svelte index deac815..4c532db 100644 --- a/web/frontend/src/generic/plots/Stacked.svelte +++ b/web/frontend/src/generic/plots/Stacked.svelte @@ -271,9 +271,12 @@ function update(u) { const { left, top } = u.cursor; - const width = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; - legendEl.style.transform = - "translate(" + (left - width - 15) + "px, " + (top + 15) + "px)"; + const internalWidth = u?.over?.querySelector(".u-legend")?.offsetWidth ? u.over.querySelector(".u-legend").offsetWidth : 0; + if (left < (width/2)) { + legendEl.style.transform = "translate(" + (left + 15) + "px, " + (top + 15) + "px)"; + } else { + legendEl.style.transform = "translate(" + (left - internalWidth - 15) + "px, " + (top + 15) + "px)"; + } } return { diff --git a/web/frontend/src/job/Metric.svelte b/web/frontend/src/job/Metric.svelte index 431b7b2..886b483 100644 --- a/web/frontend/src/job/Metric.svelte +++ b/web/frontend/src/job/Metric.svelte @@ -185,6 +185,7 @@ {isShared} {zoomState} {thresholdState} + enableFlip /> {:else if statsSeries[selectedScopeIndex] != null && patternMatches} {/if} {/key} diff --git a/web/frontend/src/status/StatisticsDash.svelte b/web/frontend/src/status/StatisticsDash.svelte index fb2161b..81489a6 100644 --- a/web/frontend/src/status/StatisticsDash.svelte +++ b/web/frontend/src/status/StatisticsDash.svelte @@ -132,6 +132,7 @@ ylabel="Number of Jobs" yunit="Jobs" usesBins + enableFlip /> {/snippet} diff --git a/web/frontend/src/status/UsageDash.svelte b/web/frontend/src/status/UsageDash.svelte index 3b39e55..a64b7d2 100644 --- a/web/frontend/src/status/UsageDash.svelte +++ b/web/frontend/src/status/UsageDash.svelte @@ -255,6 +255,7 @@ height="275" usesBins xtime + enableFlip /> {/key} @@ -359,6 +360,7 @@ ylabel="Number of Jobs" yunit="Jobs" height="275" + enableFlip /> @@ -462,6 +464,7 @@ ylabel="Number of Jobs" yunit="Jobs" height="275" + enableFlip /> diff --git a/web/frontend/src/systems/NodeOverview.svelte b/web/frontend/src/systems/NodeOverview.svelte index 366f705..5051374 100644 --- a/web/frontend/src/systems/NodeOverview.svelte +++ b/web/frontend/src/systems/NodeOverview.svelte @@ -149,6 +149,7 @@ {cluster} subCluster={item.subCluster} forNode + enableFlip /> {/key} {:else if item.disabled === null} From 2152ced97ac9c7aa5b23b13661296d13d665a378 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 13 Nov 2025 11:18:40 +0100 Subject: [PATCH 12/19] improve metricplot threshold handling - simplified and adaptive thresholds for shared jobs --- .../src/generic/plots/MetricPlot.svelte | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/web/frontend/src/generic/plots/MetricPlot.svelte b/web/frontend/src/generic/plots/MetricPlot.svelte index d9dda8e..cea9709 100644 --- a/web/frontend/src/generic/plots/MetricPlot.svelte +++ b/web/frontend/src/generic/plots/MetricPlot.svelte @@ -13,7 +13,7 @@ - `statisticsSeries [GraphQL.StatisticsSeries]?`: Min/Max/Median representation of metric data [Default: null] - `cluster String?`: Cluster name of the parent job / data [Default: ""] - `subCluster String`: Name of the subCluster of the parent job - - `isShared Bool?`: If this job used shared resources; will adapt threshold indicators accordingly [Default: false] + - `isShared Bool?`: If this job used shared resources; for additional legend display [Default: false] - `forNode Bool?`: If this plot is used for node data display; will render x-axis as negative time with $now as maximum [Default: false] - `numhwthreads Number?`: Number of job HWThreads [Default: 0] - `numaccs Number?`: Number of job Accelerators [Default: 0] @@ -85,7 +85,6 @@ subClusterTopology, metricConfig, scope, - isShared, numhwthreads, numaccs )); @@ -279,7 +278,6 @@ subClusterTopology, metricConfig, scope, - isShared, numhwthreads, numaccs ) { @@ -295,32 +293,35 @@ scope = statParts[0] } - if ( - (scope == "node" && isShared == false) || - metricConfig?.aggregation == "avg" - ) { - return { - normal: metricConfig.normal, - caution: metricConfig.caution, - alert: metricConfig.alert, - peak: metricConfig.peak, - }; + if (metricConfig?.aggregation == "avg") { + // Return as Configured + return { + normal: metricConfig.normal, + caution: metricConfig.caution, + alert: metricConfig.alert, + peak: metricConfig.peak, + }; } if (metricConfig?.aggregation == "sum") { + // Scale Thresholds + let fraction; + if (numaccs > 0) fraction = subClusterTopology.accelerators.length / numaccs; + else if (numhwthreads > 0) fraction = subClusterTopology.core.length / numhwthreads; + else fraction = 1; // Fallback + let divisor; - if (isShared == true) { // Shared - if (numaccs > 0) divisor = subClusterTopology.accelerators.length / numaccs; - else if (numhwthreads > 0) divisor = subClusterTopology.core.length / numhwthreads; - } - else if (scope == 'node') divisor = 1; // Use as configured for nodes - else if (scope == 'socket') divisor = subClusterTopology.socket.length; - else if (scope == "memoryDomain") divisor = subClusterTopology.memoryDomain.length; + // Exclusive: Fraction = 1; Shared: Fraction > 1 + if (scope == 'node') divisor = fraction; + // Cap divisor at number of available sockets or domains + else if (scope == 'socket') divisor = (fraction < subClusterTopology.socket.length) ? subClusterTopology.socket.length : fraction; + else if (scope == "memoryDomain") divisor = (fraction < subClusterTopology.memoryDomain.length) ? subClusterTopology.socket.length : fraction; + // Use Maximum Division for Smallest Scopes else if (scope == "core") divisor = subClusterTopology.core.length; else if (scope == "hwthread") divisor = subClusterTopology.core.length; // alt. name for core else if (scope == "accelerator") divisor = subClusterTopology.accelerators.length; else { - console.log('Unknown scope, return default aggregation thresholds ', scope) + console.log('Unknown scope, return default aggregation thresholds for sum', scope) divisor = 1; } From 9fe342a7e99a29c5d73bfa2c052d276868f5b450 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 13 Nov 2025 13:40:31 +0100 Subject: [PATCH 13/19] fix metricSelect error if cluster filter is active --- web/frontend/src/generic/select/MetricSelection.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/frontend/src/generic/select/MetricSelection.svelte b/web/frontend/src/generic/select/MetricSelection.svelte index ba5a289..8bcaefc 100644 --- a/web/frontend/src/generic/select/MetricSelection.svelte +++ b/web/frontend/src/generic/select/MetricSelection.svelte @@ -98,7 +98,12 @@ if (!cluster) { return avail.map((av) => av.cluster).join(', ') } else { - return avail.find((av) => av.cluster === cluster).subClusters.join(', ') + const subAvail = avail.find((av) => av.cluster === cluster)?.subClusters + if (subAvail) { + return subAvail.join(', ') + } else { + return `Not available for ${cluster}` + } } } From 3b533938a6703b75983edf7dc78817e17130d8c3 Mon Sep 17 00:00:00 2001 From: Christoph Kluge Date: Thu, 13 Nov 2025 17:27:41 +0100 Subject: [PATCH 14/19] review status view components, make node states refreshable --- web/frontend/src/Status.root.svelte | 63 +++++++++++------ .../src/generic/helper/Refresher.svelte | 4 +- web/frontend/src/status/StatisticsDash.svelte | 25 +++---- web/frontend/src/status/StatusDash.svelte | 68 ++++++++----------- web/frontend/src/status/UsageDash.svelte | 2 - 5 files changed, 80 insertions(+), 82 deletions(-) diff --git a/web/frontend/src/Status.root.svelte b/web/frontend/src/Status.root.svelte index d5ae0f7..3d9002a 100644 --- a/web/frontend/src/Status.root.svelte +++ b/web/frontend/src/Status.root.svelte @@ -9,13 +9,17 @@ import { getContext } from "svelte" + import { + init, + } from "./generic/utils.js"; import { Row, Col, Card, CardBody, TabContent, - TabPane + TabPane, + Spinner } from "@sveltestrap/sveltestrap"; import StatusDash from "./status/StatusDash.svelte"; @@ -28,8 +32,8 @@ } = $props(); /*Const Init */ + const { query: initq } = init(); const useCbColors = getContext("cc-config")?.plotConfiguration_colorblindMode || false - @@ -40,24 +44,39 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file +{#if $initq.fetching} + + + + + +{:else if $initq.error} + + + {$initq.error.message} + + +{:else} + + + + + + + + + + + + + + + + + + + + + +{/if} diff --git a/web/frontend/src/generic/helper/Refresher.svelte b/web/frontend/src/generic/helper/Refresher.svelte index bfa58dd..7f568bf 100644 --- a/web/frontend/src/generic/helper/Refresher.svelte +++ b/web/frontend/src/generic/helper/Refresher.svelte @@ -27,7 +27,7 @@ function refreshIntervalChanged() { if (refreshIntervalId != null) clearInterval(refreshIntervalId); if (refreshInterval == null) return; - refreshIntervalId = setInterval(() => onRefresh(), refreshInterval); + refreshIntervalId = setInterval(() => onRefresh(refreshInterval), refreshInterval); } /* Svelte 5 onMount */ @@ -51,7 +51,7 @@