The file /Users/jan/prg/CC/cc-lib/resampler/resampler.go has been updated successfully.

// ResamplerFunc is the signature shared by all resampler algorithms.
type ResamplerFunc func(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error)

// GetResampler returns the resampler function for the given name.
// Valid names: "lttb" (default), "average", "simple". Empty string returns LTTB.
func GetResampler(name string) (ResamplerFunc, error) {
	switch name {
	case "", "lttb":
		return LargestTriangleThreeBucket, nil
	case "average":
		return AverageResampler, nil
	case "simple":
		return SimpleResampler, nil
	default:
		return nil, fmt.Errorf("unknown resampler algorithm: %s", name)
	}
}

// AverageResampler performs RRDTool-style average consolidation by dividing data into
// fixed-size buckets and computing the arithmetic mean of all valid points in each bucket.
//
// This produces scientifically accurate averages over time intervals, unlike LTTB which
// preserves visual shape, or SimpleResampler which decimates. NaN values are skipped;
// if all values in a bucket are NaN, the output for that bucket is NaN.
//
// Parameters:
//   - data: input time-series data points
//   - oldFrequency: original sampling frequency
//   - newFrequency: target sampling frequency (must be a multiple of oldFrequency)
//
// Returns:
//   - Averaged data slice
//   - Actual frequency used
//   - Error if newFrequency is not a multiple of oldFrequency
func AverageResampler(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error) {
	newDataLength, step, err := validateFrequency(len(data), oldFrequency, newFrequency)
	if err != nil {
		return nil, 0, err
	}
	if newDataLength == -1 {
		return data, oldFrequency, nil
	}

	newData := make([]schema.Float, newDataLength)
	for i := range newDataLength {
		sum := 0.0
		count := 0
		base := i * step
		for j := range step {
			v := float64(data[base+j])
			if !math.IsNaN(v) {
				sum += v
				count++
			}
		}
		if count == 0 {
			newData[i] = schema.NaN
		} else {
			newData[i] = schema.Float(sum / float64(count))
		}
	}

	return newData, newFrequency, nil
}

func SetMinimumRequiredPoints(setVal int) {
	MinimumRequiredPoints = setVal
}

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-lib.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

// Package resampler provides time-series data downsampling algorithms.
//
// This package implements two downsampling strategies for reducing the number
// of data points in time-series data while preserving important characteristics:
//
//   - SimpleResampler: A fast, straightforward algorithm that selects every nth point
//   - LargestTriangleThreeBucket (LTTB): A perceptually-aware algorithm that preserves
//     visual characteristics by selecting points that maximize the area of triangles
//     formed with neighboring points
//
// Both algorithms are designed to work with schema.Float data and handle NaN values
// appropriately. They require that the new sampling frequency is a multiple of the
// old frequency.
//
// References:
//   - LTTB Algorithm: https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf
//   - Implementation adapted from: https://github.com/haoel/downsampling
package resampler

import (
	"fmt"
	"math"

	"github.com/ClusterCockpit/cc-lib/v2/schema"
)

// MinimumRequiredPoints is the default number of points required to trigger resampling.
// If the data has fewer points, the original time series will be returned without resampling.
var MinimumRequiredPoints int = 1000

// calculateTriangleArea computes the area of a triangle defined by three points.
//
// The area is calculated using the cross product formula:
// Area = 0.5 * |((paX - pcX) * (pbY - paY)) - ((paX - pbX) * (pcY - paY))|
//
// This is used by the LTTB algorithm to determine which points preserve the
// most visual information when downsampling.
func calculateTriangleArea(paX, paY, pbX, pbY, pcX, pcY schema.Float) float64 {
	area := ((paX-pcX)*(pbY-paY) - (paX-pbX)*(pcY-paY)) * 0.5
	return math.Abs(float64(area))
}

// calculateAverageDataPoint computes the average point from a slice of data points.
//
// Parameters:
//   - points: slice of Y values to average
//   - xStart: starting X coordinate for the points
//
// Returns:
//   - avgX: average X coordinate
//   - avgY: average Y value, or NaN if any input point is NaN
//
// This function is used by LTTB to find the centroid of points in a bucket,
// which is then used to calculate triangle areas for point selection.
func calculateAverageDataPoint(points []schema.Float, xStart int64) (avgX schema.Float, avgY schema.Float) {
	hasNaN := false
	for _, point := range points {
		avgX += schema.Float(xStart)
		avgY += point
		xStart++
		if math.IsNaN(float64(point)) {
			hasNaN = true
		}
	}

	l := schema.Float(len(points))
	avgX /= l
	avgY /= l

	if hasNaN {
		return avgX, schema.NaN
	}
	return avgX, avgY
}

// SimpleResampler performs simple downsampling by selecting every nth point.
//
// This is the fastest downsampling method but may miss important features in the data.
// It works by calculating a step size (newFrequency / oldFrequency) and selecting
// every step-th point from the original data.
//
// Parameters:
//   - data: input time-series data points
//   - oldFrequency: original sampling frequency (points per time unit)
//   - newFrequency: target sampling frequency (must be a multiple of oldFrequency)
//
// Returns:
//   - Downsampled data slice
//   - Actual frequency used (may be oldFrequency if downsampling wasn't performed)
//   - Error if newFrequency is not a multiple of oldFrequency
//
// The function returns the original data unchanged if:
//   - Either frequency is 0
//   - newFrequency <= oldFrequency (no downsampling needed)
//   - The resulting data would have fewer than 1 point
//   - The original data has fewer than 100 points
//   - The downsampled data would have the same or more points than the original
//
// Example:
//
//	data := []schema.Float{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}
//	downsampled, freq, err := SimpleResampler(data, 1, 2)
//	// Returns: [1.0, 3.0, 5.0], 2, nil
func SimpleResampler(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error) {
	// checks if the frequencies are valid or not.
	newDataLength, step, err := validateFrequency(len(data), oldFrequency, newFrequency)
	if err != nil {
		return nil, 0, err
	}
	if newDataLength == -1 {
		return data, oldFrequency, nil
	}

	newData := make([]schema.Float, newDataLength)
	for i := range newDataLength {
		newData[i] = data[i*step]
	}

	return newData, newFrequency, nil
}

func validateFrequency(lenData int, oldFrequency, newFrequency int64) (int, int, error) {
	// Validate inputs and check if downsampling is needed
	if oldFrequency == 0 || newFrequency == 0 || newFrequency <= oldFrequency {
		return -1, 0, nil
	}

	// Ensure new frequency is a multiple of old frequency
	if newFrequency%oldFrequency != 0 {
		return -1, 0, fmt.Errorf("new sampling frequency (%d) must be a multiple of old frequency (%d)", newFrequency, oldFrequency)
	}

	step := int(newFrequency / oldFrequency)
	newDataLength := lenData / step

	// Don't downsample if result would be trivial or counterproductive
	if (newDataLength == 0) || (lenData < MinimumRequiredPoints) || (newDataLength >= lenData) {
		return -1, 0, nil
	}

	return newDataLength, step, nil
}

// LargestTriangleThreeBucket (LTTB) performs perceptually-aware downsampling.
//
// LTTB is a downsampling algorithm that preserves the visual characteristics of
// time-series data by selecting points that form the largest triangles with their
// neighbors. This ensures that important peaks, valleys, and trends are retained
// even when significantly reducing the number of points.
//
// Algorithm Overview:
//  1. The data is divided into buckets (except first and last points which are always kept)
//  2. For each bucket, the algorithm selects the point that forms the largest triangle
//     with the previous selected point and the average of the next bucket
//  3. This maximizes the visual area and preserves important features
//
// Time Complexity: O(n) where n is the number of input points
// Space Complexity: O(m) where m is the number of output points
//
// Parameters:
//   - data: input time-series data points
//   - oldFrequency: original sampling frequency (points per time unit)
//   - newFrequency: target sampling frequency (must be a multiple of oldFrequency)
//
// Returns:
//   - Downsampled data slice
//   - Actual frequency used (may be oldFrequency if downsampling wasn't performed)
//   - Error if newFrequency is not a multiple of oldFrequency
//
// The function returns the original data unchanged if:
//   - Either frequency is 0
//   - newFrequency <= oldFrequency (no downsampling needed)
//   - The resulting data would have fewer than 1 point
//   - The original data has fewer than 100 points
//   - The downsampled data would have the same or more points than the original
//
// NaN Handling:
// The algorithm properly handles NaN values and preserves them in the output when
// they represent the maximum area point in a bucket.
//
// Example:
//
//	data := []schema.Float{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0}
//	downsampled, freq, err := LargestTriangleThreeBucket(data, 1, 2)
//	// Returns downsampled data preserving visual characteristics
//
// References:
//   - Original paper: https://skemman.is/bitstream/1946/15343/3/SS_MSthesis.pdf
//   - Adapted from: https://github.com/haoel/downsampling/blob/master/core/lttb.go
func LargestTriangleThreeBucket(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error) {
	// checks if the frequencies are valid or not.
	newDataLength, _, err := validateFrequency(len(data), oldFrequency, newFrequency)
	if err != nil {
		return nil, 0, err
	}
	if newDataLength == -1 {
		return data, oldFrequency, nil
	}

	newData := make([]schema.Float, 0, newDataLength)

	// Bucket size. Leave room for start and end data points
	bucketSize := float64(len(data)-2) / float64(newDataLength-2)

	// Always add the first point
	newData = append(newData, data[0])

	// Bucket pointers:
	// - bucketLow: current bucket's start
	// - bucketMiddle: current bucket's end (also next bucket's start)
	// - bucketHigh: next bucket's end
	bucketLow := 1
	bucketMiddle := int(math.Floor(bucketSize)) + 1

	var prevMaxAreaPoint int

	// Process each bucket (excluding first and last points)
	for i := 0; i < newDataLength-2; i++ {
		bucketHigh := int(math.Floor(float64(i+2)*bucketSize)) + 1
		if bucketHigh >= len(data)-1 {
			bucketHigh = len(data) - 2
		}

		// Calculate average point for next bucket (point c in triangle)
		avgPointX, avgPointY := calculateAverageDataPoint(data[bucketMiddle:bucketHigh+1], int64(bucketMiddle))

		// Get the range for current bucket
		currBucketStart := bucketLow
		currBucketEnd := bucketMiddle

		// Point a (previously selected point)
		pointX := prevMaxAreaPoint
		pointY := data[prevMaxAreaPoint]

		maxArea := -1.0
		var maxAreaPoint int
		flag_ := 0

		// Find the point in current bucket that forms the largest triangle
		for ; currBucketStart < currBucketEnd; currBucketStart++ {
			area := calculateTriangleArea(
				schema.Float(pointX), pointY,
				avgPointX, avgPointY,
				schema.Float(currBucketStart), data[currBucketStart],
			)

			if area > maxArea || math.IsNaN(area) {
				maxArea = area
				maxAreaPoint = currBucketStart
			}
			// if math.IsNaN(float64(avgPointY)) {
			// 	flag_ = 1
			// }
		}

		// Add the point with maximum area from this bucket
		if flag_ == 1 {
			newData = append(newData, schema.NaN) // Pick this point from the bucket
		} else {
			newData = append(newData, data[maxAreaPoint]) // Pick this point from the bucket
		}

		prevMaxAreaPoint = maxAreaPoint

		// Move to the next bucket
		bucketLow = bucketMiddle
		bucketMiddle = bucketHigh
	}

	// Always add the last point
	newData = append(newData, data[len(data)-1])

	return newData, newFrequency, nil
}

func SetMinimumRequiredPoints(setVal int) {
	MinimumRequiredPoints = setVal
}

+// ResamplerFunc is the signature shared by all resampler algorithms.

+type ResamplerFunc func(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error)

+// GetResampler returns the resampler function for the given name.

+// Valid names: "lttb" (default), "average", "simple". Empty string returns LTTB.

+func GetResampler(name string) (ResamplerFunc, error) {

+    return LargestTriangleThreeBucket, nil

+    return nil, fmt.Errorf("unknown resampler algorithm: %s", name)

+// AverageResampler performs RRDTool-style average consolidation by dividing data into

+// fixed-size buckets and computing the arithmetic mean of all valid points in each bucket.

+// This produces scientifically accurate averages over time intervals, unlike LTTB which

+// preserves visual shape, or SimpleResampler which decimates. NaN values are skipped;

+// if all values in a bucket are NaN, the output for that bucket is NaN.

+//   - data: input time-series data points

+//   - oldFrequency: original sampling frequency

+//   - newFrequency: target sampling frequency (must be a multiple of oldFrequency)

+//   - Error if newFrequency is not a multiple of oldFrequency

+func AverageResampler(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error) {

+  newDataLength, step, err := validateFrequency(len(data), oldFrequency, newFrequency)

+  newData := make([]schema.Float, newDataLength)

+      newData[i] = schema.Float(sum / float64(count))


Now add tests to the test file:


Edit

// Benchmark tests

func BenchmarkSimpleResampler(b *testing.B) {

// TestAverageResampler tests the AverageResampler function
func TestAverageResampler(t *testing.T) {
	SetMinimumRequiredPoints(100)
	defer SetMinimumRequiredPoints(1000)

	tests := []struct {
		name         string
		data         []schema.Float
		oldFrequency int64
		newFrequency int64
		expectedLen  int
		expectedFreq int64
		expectError  bool
		checkValues  bool
		expectedData []schema.Float
	}{
		{
			name:         "Basic averaging step=2",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  100,
			expectedFreq: 2,
		},
		{
			name:         "No downsampling needed (new <= old)",
			data:         makeTestData(200, 1.0),
			oldFrequency: 2,
			newFrequency: 1,
			expectedLen:  200,
			expectedFreq: 2,
		},
		{
			name:         "Zero old frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 0,
			newFrequency: 2,
			expectedLen:  200,
			expectedFreq: 0,
		},
		{
			name:         "Zero new frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 0,
			expectedLen:  200,
			expectedFreq: 1,
		},
		{
			name:         "Non-multiple frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 3,
			newFrequency: 7,
			expectError:  true,
		},
		{
			name:         "Small dataset (< 100 points)",
			data:         makeTestData(50, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  50,
			expectedFreq: 1,
		},
		{
			name:         "Large downsampling factor",
			data:         makeTestData(1000, 1.0),
			oldFrequency: 1,
			newFrequency: 10,
			expectedLen:  100,
			expectedFreq: 10,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, freq, err := AverageResampler(tt.data, tt.oldFrequency, tt.newFrequency)

			if tt.expectError {
				if err == nil {
					t.Errorf("AverageResampler() expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Errorf("AverageResampler() unexpected error: %v", err)
				return
			}

			if len(result) != tt.expectedLen {
				t.Errorf("AverageResampler() result length = %v, want %v", len(result), tt.expectedLen)
			}

			if freq != tt.expectedFreq {
				t.Errorf("AverageResampler() frequency = %v, want %v", freq, tt.expectedFreq)
			}

			if tt.checkValues && tt.expectedData != nil {
				for i := range result {
					if math.Abs(float64(result[i]-tt.expectedData[i])) > 1e-10 {
						t.Errorf("AverageResampler() result[%d] = %v, want %v", i, result[i], tt.expectedData[i])
					}
				}
			}
		})
	}
}

// TestAverageResamplerValues tests exact output values of AverageResampler
func TestAverageResamplerValues(t *testing.T) {
	SetMinimumRequiredPoints(3)
	defer SetMinimumRequiredPoints(1000)

	// [1,2,3,4,5,6] step=2 -> [1.5, 3.5, 5.5]
	data := []schema.Float{1, 2, 3, 4, 5, 6}
	result, freq, err := AverageResampler(data, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if freq != 2 {
		t.Errorf("frequency = %v, want 2", freq)
	}
	expected := []schema.Float{1.5, 3.5, 5.5}
	if len(result) != len(expected) {
		t.Fatalf("result length = %v, want %v", len(result), len(expected))
	}
	for i := range expected {
		if math.Abs(float64(result[i]-expected[i])) > 1e-10 {
			t.Errorf("result[%d] = %v, want %v", i, result[i], expected[i])
		}
	}
}

// TestAverageResamplerNaN tests NaN handling in AverageResampler
func TestAverageResamplerNaN(t *testing.T) {
	SetMinimumRequiredPoints(3)
	defer SetMinimumRequiredPoints(1000)

	// NaN skipping: [1, NaN, 3, 4] step=2 -> [1.0, 3.5]
	data := []schema.Float{1, schema.NaN, 3, 4}
	result, _, err := AverageResampler(data, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(result) != 2 {
		t.Fatalf("result length = %v, want 2", len(result))
	}
	if math.Abs(float64(result[0]-1.0)) > 1e-10 {
		t.Errorf("result[0] = %v, want 1.0", result[0])
	}
	if math.Abs(float64(result[1]-3.5)) > 1e-10 {
		t.Errorf("result[1] = %v, want 3.5", result[1])
	}

	// All-NaN bucket: [NaN, NaN, 3, 4] step=2 -> [NaN, 3.5]
	data2 := []schema.Float{schema.NaN, schema.NaN, 3, 4}
	result2, _, err := AverageResampler(data2, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !math.IsNaN(float64(result2[0])) {
		t.Errorf("result[0] = %v, want NaN", result2[0])
	}
	if math.Abs(float64(result2[1]-3.5)) > 1e-10 {
		t.Errorf("result[1] = %v, want 3.5", result2[1])
	}
}

// TestGetResampler tests the GetResampler lookup function
func TestGetResampler(t *testing.T) {
	tests := []struct {
		name      string
		expectErr bool
	}{
		{"", false},
		{"lttb", false},
		{"average", false},
		{"simple", false},
		{"unknown", true},
	}

	for _, tt := range tests {
		t.Run("name="+tt.name, func(t *testing.T) {
			fn, err := GetResampler(tt.name)
			if tt.expectErr {
				if err == nil {
					t.Error("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if fn == nil {
				t.Error("expected non-nil function")
			}
		})
	}
}

// Benchmark tests

func BenchmarkAverageResampler(b *testing.B) {
	data := makeTestData(10000, 1.0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = AverageResampler(data, 1, 10)
	}
}

func BenchmarkSimpleResampler(b *testing.B) {


The file /Users/jan/prg/CC/cc-lib/resampler/resampler_test.go has been updated successfully.

// TestAverageResampler tests the AverageResampler function
func TestAverageResampler(t *testing.T) {
	SetMinimumRequiredPoints(100)
	defer SetMinimumRequiredPoints(1000)

	tests := []struct {
		name         string
		data         []schema.Float
		oldFrequency int64
		newFrequency int64
		expectedLen  int
		expectedFreq int64
		expectError  bool
		checkValues  bool
		expectedData []schema.Float
	}{
		{
			name:         "Basic averaging step=2",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  100,
			expectedFreq: 2,
		},
		{
			name:         "No downsampling needed (new <= old)",
			data:         makeTestData(200, 1.0),
			oldFrequency: 2,
			newFrequency: 1,
			expectedLen:  200,
			expectedFreq: 2,
		},
		{
			name:         "Zero old frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 0,
			newFrequency: 2,
			expectedLen:  200,
			expectedFreq: 0,
		},
		{
			name:         "Zero new frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 0,
			expectedLen:  200,
			expectedFreq: 1,
		},
		{
			name:         "Non-multiple frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 3,
			newFrequency: 7,
			expectError:  true,
		},
		{
			name:         "Small dataset (< 100 points)",
			data:         makeTestData(50, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  50,
			expectedFreq: 1,
		},
		{
			name:         "Large downsampling factor",
			data:         makeTestData(1000, 1.0),
			oldFrequency: 1,
			newFrequency: 10,
			expectedLen:  100,
			expectedFreq: 10,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, freq, err := AverageResampler(tt.data, tt.oldFrequency, tt.newFrequency)

			if tt.expectError {
				if err == nil {
					t.Errorf("AverageResampler() expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Errorf("AverageResampler() unexpected error: %v", err)
				return
			}

			if len(result) != tt.expectedLen {
				t.Errorf("AverageResampler() result length = %v, want %v", len(result), tt.expectedLen)
			}

			if freq != tt.expectedFreq {
				t.Errorf("AverageResampler() frequency = %v, want %v", freq, tt.expectedFreq)
			}

			if tt.checkValues && tt.expectedData != nil {
				for i := range result {
					if math.Abs(float64(result[i]-tt.expectedData[i])) > 1e-10 {
						t.Errorf("AverageResampler() result[%d] = %v, want %v", i, result[i], tt.expectedData[i])
					}
				}
			}
		})
	}
}

// TestAverageResamplerValues tests exact output values of AverageResampler
func TestAverageResamplerValues(t *testing.T) {
	SetMinimumRequiredPoints(3)
	defer SetMinimumRequiredPoints(1000)

	// [1,2,3,4,5,6] step=2 -> [1.5, 3.5, 5.5]
	data := []schema.Float{1, 2, 3, 4, 5, 6}
	result, freq, err := AverageResampler(data, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if freq != 2 {
		t.Errorf("frequency = %v, want 2", freq)
	}
	expected := []schema.Float{1.5, 3.5, 5.5}
	if len(result) != len(expected) {
		t.Fatalf("result length = %v, want %v", len(result), len(expected))
	}
	for i := range expected {
		if math.Abs(float64(result[i]-expected[i])) > 1e-10 {
			t.Errorf("result[%d] = %v, want %v", i, result[i], expected[i])
		}
	}
}

// TestAverageResamplerNaN tests NaN handling in AverageResampler
func TestAverageResamplerNaN(t *testing.T) {
	SetMinimumRequiredPoints(3)
	defer SetMinimumRequiredPoints(1000)

	// NaN skipping: [1, NaN, 3, 4] step=2 -> [1.0, 3.5]
	data := []schema.Float{1, schema.NaN, 3, 4}
	result, _, err := AverageResampler(data, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(result) != 2 {
		t.Fatalf("result length = %v, want 2", len(result))
	}
	if math.Abs(float64(result[0]-1.0)) > 1e-10 {
		t.Errorf("result[0] = %v, want 1.0", result[0])
	}
	if math.Abs(float64(result[1]-3.5)) > 1e-10 {
		t.Errorf("result[1] = %v, want 3.5", result[1])
	}

	// All-NaN bucket: [NaN, NaN, 3, 4] step=2 -> [NaN, 3.5]
	data2 := []schema.Float{schema.NaN, schema.NaN, 3, 4}
	result2, _, err := AverageResampler(data2, 1, 2)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !math.IsNaN(float64(result2[0])) {
		t.Errorf("result[0] = %v, want NaN", result2[0])
	}
	if math.Abs(float64(result2[1]-3.5)) > 1e-10 {
		t.Errorf("result[1] = %v, want 3.5", result2[1])
	}
}

// TestGetResampler tests the GetResampler lookup function
func TestGetResampler(t *testing.T) {
	tests := []struct {
		name      string
		expectErr bool
	}{
		{"", false},
		{"lttb", false},
		{"average", false},
		{"simple", false},
		{"unknown", true},
	}

	for _, tt := range tests {
		t.Run("name="+tt.name, func(t *testing.T) {
			fn, err := GetResampler(tt.name)
			if tt.expectErr {
				if err == nil {
					t.Error("expected error, got nil")
				}
				return
			}
			if err != nil {
				t.Errorf("unexpected error: %v", err)
			}
			if fn == nil {
				t.Error("expected non-nil function")
			}
		})
	}
}

// Benchmark tests

func BenchmarkAverageResampler(b *testing.B) {
	data := makeTestData(10000, 1.0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = AverageResampler(data, 1, 10)
	}
}

func BenchmarkSimpleResampler(b *testing.B) {

// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
// All rights reserved. This file is part of cc-lib.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package resampler

import (
	"math"
	"testing"

	"github.com/ClusterCockpit/cc-lib/v2/schema"
)

// TestCalculateTriangleArea tests the triangle area calculation helper function
func TestCalculateTriangleArea(t *testing.T) {
	tests := []struct {
		name     string
		paX, paY schema.Float
		pbX, pbY schema.Float
		pcX, pcY schema.Float
		expected float64
	}{
		{
			name: "Right triangle",
			paX:  0, paY: 0,
			pbX: 0, pbY: 3,
			pcX: 4, pcY: 0,
			expected: 6.0,
		},
		{
			name: "Collinear points (zero area)",
			paX:  0, paY: 0,
			pbX: 1, pbY: 1,
			pcX: 2, pcY: 2,
			expected: 0.0,
		},
		{
			name: "Negative coordinates",
			paX:  -1, paY: -1,
			pbX: -1, pbY: 2,
			pcX: 3, pcY: -1,
			expected: 6.0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := calculateTriangleArea(tt.paX, tt.paY, tt.pbX, tt.pbY, tt.pcX, tt.pcY)
			if math.Abs(result-tt.expected) > 1e-10 {
				t.Errorf("calculateTriangleArea() = %v, want %v", result, tt.expected)
			}
		})
	}
}

// TestCalculateAverageDataPoint tests the average point calculation helper function
func TestCalculateAverageDataPoint(t *testing.T) {
	tests := []struct {
		name      string
		points    []schema.Float
		xStart    int64
		expectedX schema.Float
		expectedY schema.Float
		expectNaN bool
	}{
		{
			name:      "Simple average",
			points:    []schema.Float{1.0, 2.0, 3.0},
			xStart:    0,
			expectedX: 1.0,
			expectedY: 2.0,
			expectNaN: false,
		},
		{
			name:      "Single point",
			points:    []schema.Float{5.0},
			xStart:    10,
			expectedX: 10.0,
			expectedY: 5.0,
			expectNaN: false,
		},
		{
			name:      "With NaN value",
			points:    []schema.Float{1.0, schema.NaN, 3.0},
			xStart:    0,
			expectedX: 1.0,
			expectNaN: true,
		},
		{
			name:      "All NaN values",
			points:    []schema.Float{schema.NaN, schema.NaN},
			xStart:    0,
			expectedX: 0.5,
			expectNaN: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			avgX, avgY := calculateAverageDataPoint(tt.points, tt.xStart)
			if math.Abs(float64(avgX-tt.expectedX)) > 1e-10 {
				t.Errorf("calculateAverageDataPoint() avgX = %v, want %v", avgX, tt.expectedX)
			}
			if tt.expectNaN {
				if !math.IsNaN(float64(avgY)) {
					t.Errorf("calculateAverageDataPoint() avgY should be NaN, got %v", avgY)
				}
			} else {
				if math.Abs(float64(avgY-tt.expectedY)) > 1e-10 {
					t.Errorf("calculateAverageDataPoint() avgY = %v, want %v", avgY, tt.expectedY)
				}
			}
		})
	}
}

// TestSimpleResampler tests the SimpleResampler function
func TestSimpleResampler(t *testing.T) {
	// Set minimum required points to 100 for testing
	SetMinimumRequiredPoints(100)
	defer SetMinimumRequiredPoints(1000) // Restore default after test

	tests := []struct {
		name         string
		data         []schema.Float
		oldFrequency int64
		newFrequency int64
		expectedLen  int
		expectedFreq int64
		expectError  bool
		checkValues  bool
		expectedData []schema.Float
	}{
		{
			name:         "Normal downsampling",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  100,
			expectedFreq: 2,
			expectError:  false,
		},
		{
			name:         "No downsampling needed (new <= old)",
			data:         makeTestData(200, 1.0),
			oldFrequency: 2,
			newFrequency: 1,
			expectedLen:  200,
			expectedFreq: 2,
			expectError:  false,
		},
		{
			name:         "Zero old frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 0,
			newFrequency: 2,
			expectedLen:  200,
			expectedFreq: 0,
			expectError:  false,
		},
		{
			name:         "Zero new frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 0,
			expectedLen:  200,
			expectedFreq: 1,
			expectError:  false,
		},
		{
			name:         "Non-multiple frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 3,
			newFrequency: 7,
			expectError:  true,
		},
		{
			name:         "Small dataset (< 100 points)",
			data:         makeTestData(50, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  50,
			expectedFreq: 1,
			expectError:  false,
		},
		{
			name:         "Exact values check",
			data:         []schema.Float{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  10,
			expectedFreq: 1,
			expectError:  false,
			checkValues:  false, // Too small, won't downsample
		},
		{
			name:         "Large downsampling factor",
			data:         makeTestData(1000, 1.0),
			oldFrequency: 1,
			newFrequency: 10,
			expectedLen:  100,
			expectedFreq: 10,
			expectError:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, freq, err := SimpleResampler(tt.data, tt.oldFrequency, tt.newFrequency)

			if tt.expectError {
				if err == nil {
					t.Errorf("SimpleResampler() expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Errorf("SimpleResampler() unexpected error: %v", err)
				return
			}

			if len(result) != tt.expectedLen {
				t.Errorf("SimpleResampler() result length = %v, want %v", len(result), tt.expectedLen)
			}

			if freq != tt.expectedFreq {
				t.Errorf("SimpleResampler() frequency = %v, want %v", freq, tt.expectedFreq)
			}

			if tt.checkValues && tt.expectedData != nil {
				for i := range result {
					if result[i] != tt.expectedData[i] {
						t.Errorf("SimpleResampler() result[%d] = %v, want %v", i, result[i], tt.expectedData[i])
					}
				}
			}
		})
	}
}

// TestLargestTriangleThreeBucket tests the LTTB algorithm
func TestLargestTriangleThreeBucket(t *testing.T) {
	// Set minimum required points to 100 for testing
	SetMinimumRequiredPoints(100)
	defer SetMinimumRequiredPoints(1000) // Restore default after test

	tests := []struct {
		name         string
		data         []schema.Float
		oldFrequency int64
		newFrequency int64
		expectedLen  int
		expectedFreq int64
		expectError  bool
	}{
		{
			name:         "Normal downsampling",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  100,
			expectedFreq: 2,
			expectError:  false,
		},
		{
			name:         "Sine wave pattern",
			data:         makeSineWave(500, 10),
			oldFrequency: 1,
			newFrequency: 5,
			expectedLen:  100,
			expectedFreq: 5,
			expectError:  false,
		},
		{
			name:         "No downsampling needed (new <= old)",
			data:         makeTestData(200, 1.0),
			oldFrequency: 2,
			newFrequency: 1,
			expectedLen:  200,
			expectedFreq: 2,
			expectError:  false,
		},
		{
			name:         "Zero old frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 0,
			newFrequency: 2,
			expectedLen:  200,
			expectedFreq: 0,
			expectError:  false,
		},
		{
			name:         "Zero new frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 1,
			newFrequency: 0,
			expectedLen:  200,
			expectedFreq: 1,
			expectError:  false,
		},
		{
			name:         "Non-multiple frequency",
			data:         makeTestData(200, 1.0),
			oldFrequency: 3,
			newFrequency: 7,
			expectError:  true,
		},
		{
			name:         "Small dataset (< 100 points)",
			data:         makeTestData(50, 1.0),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  50,
			expectedFreq: 1,
			expectError:  false,
		},
		{
			name:         "With NaN values",
			data:         makeTestDataWithNaN(200),
			oldFrequency: 1,
			newFrequency: 2,
			expectedLen:  100,
			expectedFreq: 2,
			expectError:  false,
		},
		{
			name:         "Large downsampling factor",
			data:         makeTestData(1000, 1.0),
			oldFrequency: 1,
			newFrequency: 10,
			expectedLen:  100,
			expectedFreq: 10,
			expectError:  false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result, freq, err := LargestTriangleThreeBucket(tt.data, tt.oldFrequency, tt.newFrequency)

			if tt.expectError {
				if err == nil {
					t.Errorf("LargestTriangleThreeBucket() expected error, got nil")
				}
				return
			}

			if err != nil {
				t.Errorf("LargestTriangleThreeBucket() unexpected error: %v", err)
				return
			}

			if len(result) != tt.expectedLen {
				t.Errorf("LargestTriangleThreeBucket() result length = %v, want %v", len(result), tt.expectedLen)
			}

			if freq != tt.expectedFreq {
				t.Errorf("LargestTriangleThreeBucket() frequency = %v, want %v", freq, tt.expectedFreq)
			}

			// Verify first and last points are preserved
			if len(result) > 0 && len(tt.data) > 0 {
				// Handle NaN comparison specially since NaN != NaN
				if math.IsNaN(float64(tt.data[0])) {
					if !math.IsNaN(float64(result[0])) {
						t.Errorf("LargestTriangleThreeBucket() first point not preserved: got %v, want NaN", result[0])
					}
				} else if result[0] != tt.data[0] {
					t.Errorf("LargestTriangleThreeBucket() first point not preserved: got %v, want %v", result[0], tt.data[0])
				}

				if math.IsNaN(float64(tt.data[len(tt.data)-1])) {
					if !math.IsNaN(float64(result[len(result)-1])) {
						t.Errorf("LargestTriangleThreeBucket() last point not preserved: got %v, want NaN", result[len(result)-1])
					}
				} else if result[len(result)-1] != tt.data[len(tt.data)-1] {
					t.Errorf("LargestTriangleThreeBucket() last point not preserved: got %v, want %v", result[len(result)-1], tt.data[len(tt.data)-1])
				}
			}
		})
	}
}

// TestLTTBPreservesFeatures tests that LTTB preserves important features better than simple resampling
func TestLTTBPreservesFeatures(t *testing.T) {
	// Set minimum required points to 100 for testing
	SetMinimumRequiredPoints(100)
	defer SetMinimumRequiredPoints(1000) // Restore default after test

	// Create a dataset with a spike
	data := make([]schema.Float, 200)
	for i := range data {
		if i == 100 {
			data[i] = 100.0 // Spike
		} else {
			data[i] = 1.0
		}
	}

	lttbResult, _, _ := LargestTriangleThreeBucket(data, 1, 2)

	// LTTB should be more likely to preserve the spike
	lttbHasSpike := false

	for _, v := range lttbResult {
		if v > 50.0 {
			lttbHasSpike = true
			break
		}
	}

	// This test is probabilistic, but LTTB should generally preserve the spike
	if !lttbHasSpike {
		t.Log("Warning: LTTB did not preserve spike (may happen occasionally)")
	}
}

// Helper functions for test data generation

// makeTestData creates a simple linear test dataset
func makeTestData(size int, increment schema.Float) []schema.Float {
	data := make([]schema.Float, size)
	for i := range data {
		data[i] = schema.Float(i) * increment
	}
	return data
}

// makeSineWave creates a sine wave test dataset
func makeSineWave(size int, periods float64) []schema.Float {
	data := make([]schema.Float, size)
	for i := range data {
		angle := 2 * math.Pi * periods * float64(i) / float64(size)
		data[i] = schema.Float(math.Sin(angle))
	}
	return data
}

// makeTestDataWithNaN creates test data with some NaN values
func makeTestDataWithNaN(size int) []schema.Float {
	data := make([]schema.Float, size)
	for i := range data {
		if i%20 == 0 {
			data[i] = schema.NaN
		} else {
			data[i] = schema.Float(i)
		}
	}
	return data
}

// Benchmark tests

func BenchmarkSimpleResampler(b *testing.B) {
	data := makeTestData(10000, 1.0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = SimpleResampler(data, 1, 10)
	}
}

func BenchmarkLargestTriangleThreeBucket(b *testing.B) {
	data := makeTestData(10000, 1.0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = LargestTriangleThreeBucket(data, 1, 10)
	}
}

func BenchmarkLTTBLargeDataset(b *testing.B) {
	data := makeTestData(100000, 1.0)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		_, _, _ = LargestTriangleThreeBucket(data, 1, 100)
	}
}

+// TestAverageResampler tests the AverageResampler function

+func TestAverageResampler(t *testing.T) {

+      name:         "Basic averaging step=2",

+      data:         makeTestData(200, 1.0),

+      name:         "No downsampling needed (new <= old)",

+      name:         "Zero old frequency",

+      name:         "Zero new frequency",

+      name:         "Non-multiple frequency",

+      name:         "Small dataset (< 100 points)",

+      data:         makeTestData(50, 1.0),

+      name:         "Large downsampling factor",

+      data:         makeTestData(1000, 1.0),

+    t.Run(tt.name, func(t *testing.T) {

+      result, freq, err := AverageResampler(tt.data, tt.oldFrequency, tt.newFrequency)

+          t.Errorf("AverageResampler() expected error, got nil")

+        t.Errorf("AverageResampler() unexpected error: %v", err)

+      if len(result) != tt.expectedLen {

+        t.Errorf("AverageResampler() result length = %v, want %v", len(result), tt.expectedLen)

+        t.Errorf("AverageResampler() frequency = %v, want %v", freq, tt.expectedFreq)

+      if tt.checkValues && tt.expectedData != nil {

+          if math.Abs(float64(result[i]-tt.expectedData[i])) > 1e-10 {

+            t.Errorf("AverageResampler() result[%d] = %v, want %v", i, result[i], tt.expectedData[i])

+// TestAverageResamplerValues tests exact output values of AverageResampler

+func TestAverageResamplerValues(t *testing.T) {

+  result, freq, err := AverageResampler(data, 1, 2)

+    t.Fatalf("unexpected error: %v", err)

+    t.Errorf("frequency = %v, want 2", freq)

+  expected := []schema.Float{1.5, 3.5, 5.5}

+    t.Fatalf("result length = %v, want %v", len(result), len(expected))

+    if math.Abs(float64(result[i]-expected[i])) > 1e-10 {

+      t.Errorf("result[%d] = %v, want %v", i, result[i], expected[i])

+// TestAverageResamplerNaN tests NaN handling in AverageResampler

+func TestAverageResamplerNaN(t *testing.T) {

+  data := []schema.Float{1, schema.NaN, 3, 4}

+  result, _, err := AverageResampler(data, 1, 2)

+    t.Fatalf("result length = %v, want 2", len(result))

+  if math.Abs(float64(result[0]-1.0)) > 1e-10 {

+    t.Errorf("result[0] = %v, want 1.0", result[0])

+  if math.Abs(float64(result[1]-3.5)) > 1e-10 {

+    t.Errorf("result[1] = %v, want 3.5", result[1])

+  // All-NaN bucket: [NaN, NaN, 3, 4] step=2 -> [NaN, 3.5]

+  data2 := []schema.Float{schema.NaN, schema.NaN, 3, 4}

+  result2, _, err := AverageResampler(data2, 1, 2)

+    t.Fatalf("unexpected error: %v", err)

+  if !math.IsNaN(float64(result2[0])) {

+    t.Errorf("result[0] = %v, want NaN", result2[0])

+    t.Errorf("result[1] = %v, want 3.5", result2[1])

+// TestGetResampler tests the GetResampler lookup function

+    t.Run("name="+tt.name, func(t *testing.T) {

+          t.Error("expected error, got nil")

+        t.Errorf("unexpected error: %v", err)

+        t.Error("expected non-nil function")

+func BenchmarkAverageResampler(b *testing.B) {

+    _, _, _ = AverageResampler(data, 1, 10)


Now run the cc-lib tests:


Bash

cd /Users/jan/prg/CC/cc-lib && go test ./resampler/... -v -count=1 2>&1 | tail -60

Run resampler tests in cc-lib


    --- PASS: TestSimpleResampler/Non-multiple_frequency (0.00s)
    --- PASS: TestSimpleResampler/Small_dataset_(<_100_points) (0.00s)
    --- PASS: TestSimpleResampler/Exact_values_check (0.00s)
    --- PASS: TestSimpleResampler/Large_downsampling_factor (0.00s)
=== RUN   TestLargestTriangleThreeBucket
=== RUN   TestLargestTriangleThreeBucket/Normal_downsampling
=== RUN   TestLargestTriangleThreeBucket/Sine_wave_pattern
=== RUN   TestLargestTriangleThreeBucket/No_downsampling_needed_(new_<=_old)
=== RUN   TestLargestTriangleThreeBucket/Zero_old_frequency
=== RUN   TestLargestTriangleThreeBucket/Zero_new_frequency
=== RUN   TestLargestTriangleThreeBucket/Non-multiple_frequency
=== RUN   TestLargestTriangleThreeBucket/Small_dataset_(<_100_points)
=== RUN   TestLargestTriangleThreeBucket/With_NaN_values
=== RUN   TestLargestTriangleThreeBucket/Large_downsampling_factor
--- PASS: TestLargestTriangleThreeBucket (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Normal_downsampling (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Sine_wave_pattern (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/No_downsampling_needed_(new_<=_old) (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Zero_old_frequency (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Zero_new_frequency (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Non-multiple_frequency (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Small_dataset_(<_100_points) (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/With_NaN_values (0.00s)
    --- PASS: TestLargestTriangleThreeBucket/Large_downsampling_factor (0.00s)
=== RUN   TestLTTBPreservesFeatures
--- PASS: TestLTTBPreservesFeatures (0.00s)
=== RUN   TestAverageResampler
=== RUN   TestAverageResampler/Basic_averaging_step=2
=== RUN   TestAverageResampler/No_downsampling_needed_(new_<=_old)
=== RUN   TestAverageResampler/Zero_old_frequency
=== RUN   TestAverageResampler/Zero_new_frequency
=== RUN   TestAverageResampler/Non-multiple_frequency
=== RUN   TestAverageResampler/Small_dataset_(<_100_points)
=== RUN   TestAverageResampler/Large_downsampling_factor
--- PASS: TestAverageResampler (0.00s)
    --- PASS: TestAverageResampler/Basic_averaging_step=2 (0.00s)
    --- PASS: TestAverageResampler/No_downsampling_needed_(new_<=_old) (0.00s)
    --- PASS: TestAverageResampler/Zero_old_frequency (0.00s)
    --- PASS: TestAverageResampler/Zero_new_frequency (0.00s)
    --- PASS: TestAverageResampler/Non-multiple_frequency (0.00s)
    --- PASS: TestAverageResampler/Small_dataset_(<_100_points) (0.00s)
    --- PASS: TestAverageResampler/Large_downsampling_factor (0.00s)
=== RUN   TestAverageResamplerValues
--- PASS: TestAverageResamplerValues (0.00s)
=== RUN   TestAverageResamplerNaN
--- PASS: TestAverageResamplerNaN (0.00s)
=== RUN   TestGetResampler
=== RUN   TestGetResampler/name=
=== RUN   TestGetResampler/name=lttb
=== RUN   TestGetResampler/name=average
=== RUN   TestGetResampler/name=simple
=== RUN   TestGetResampler/name=unknown
--- PASS: TestGetResampler (0.00s)
    --- PASS: TestGetResampler/name= (0.00s)
    --- PASS: TestGetResampler/name=lttb (0.00s)
    --- PASS: TestGetResampler/name=average (0.00s)
    --- PASS: TestGetResampler/name=simple (0.00s)
    --- PASS: TestGetResampler/name=unknown (0.00s)
PASS
ok  	github.com/ClusterCockpit/cc-lib/v2/resampler	0.421s
Shell cwd was reset to /Users/jan/tmp/cc-backend


All cc-lib tests pass. Now Part 2: cc-backend changes.

## Part 2a: Add replace directive to go.mod


Edit

require (
	github.com/99designs/gqlgen v0.17.88
	github.com/ClusterCockpit/cc-lib/v2 v2.9.1

replace github.com/ClusterCockpit/cc-lib/v2 => ../../prg/CC/cc-lib

require (
	github.com/99designs/gqlgen v0.17.88
	github.com/ClusterCockpit/cc-lib/v2 v2.9.1


The file /Users/jan/tmp/cc-backend/go.mod has been updated successfully.

replace github.com/ClusterCockpit/cc-lib/v2 => ../../prg/CC/cc-lib

require (
	github.com/99designs/gqlgen v0.17.88
	github.com/ClusterCockpit/cc-lib/v2 v2.9.1

module github.com/ClusterCockpit/cc-backend

go 1.25.0

tool (
	github.com/99designs/gqlgen
	github.com/swaggo/swag/cmd/swag
)

require (
	github.com/99designs/gqlgen v0.17.88
	github.com/ClusterCockpit/cc-lib/v2 v2.9.1
	github.com/ClusterCockpit/cc-line-protocol/v2 v2.4.0
	github.com/Masterminds/squirrel v1.5.4
	github.com/aws/aws-sdk-go-v2 v1.41.3
	github.com/aws/aws-sdk-go-v2/config v1.32.11
	github.com/aws/aws-sdk-go-v2/credentials v1.19.11
	github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0
	github.com/coreos/go-oidc/v3 v3.17.0
	github.com/expr-lang/expr v1.17.8
	github.com/go-chi/chi/v5 v5.2.5
	github.com/go-chi/cors v1.2.2
	github.com/go-co-op/gocron/v2 v2.19.1
	github.com/go-ldap/ldap/v3 v3.4.12
	github.com/golang-jwt/jwt/v5 v5.3.1
	github.com/golang-migrate/migrate/v4 v4.19.1
	github.com/google/gops v0.3.29
	github.com/gorilla/sessions v1.4.0
	github.com/jmoiron/sqlx v1.4.0
	github.com/joho/godotenv v1.5.1
	github.com/mattn/go-sqlite3 v1.14.34
	github.com/parquet-go/parquet-go v0.29.0
	github.com/qustavo/sqlhooks/v2 v2.1.0
	github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
	github.com/stretchr/testify v1.11.1
	github.com/swaggo/http-swagger v1.3.4
	github.com/swaggo/swag v1.16.6
	github.com/vektah/gqlparser/v2 v2.5.32
	golang.org/x/crypto v0.49.0
	golang.org/x/oauth2 v0.36.0
	golang.org/x/sync v0.20.0
	golang.org/x/time v0.15.0
)

require (
	github.com/Azure/go-ntlmssp v0.1.0 // indirect
	github.com/KyleBanks/depth v1.2.1 // indirect
	github.com/agnivade/levenshtein v1.2.1 // indirect
	github.com/andybalholm/brotli v1.2.0 // indirect
	github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
	github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
	github.com/aws/smithy-go v1.24.2 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/fsnotify/fsnotify v1.9.0 // indirect
	github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
	github.com/go-jose/go-jose/v4 v4.1.3 // indirect
	github.com/go-openapi/jsonpointer v0.22.5 // indirect
	github.com/go-openapi/jsonreference v0.21.5 // indirect
	github.com/go-openapi/spec v0.22.4 // indirect
	github.com/go-openapi/swag/conv v0.25.5 // indirect
	github.com/go-openapi/swag/jsonname v0.25.5 // indirect
	github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
	github.com/go-openapi/swag/loading v0.25.5 // indirect
	github.com/go-openapi/swag/stringutils v0.25.5 // indirect
	github.com/go-openapi/swag/typeutils v0.25.5 // indirect
	github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
	github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
	github.com/goccy/go-yaml v1.19.2 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/gorilla/securecookie v1.1.2 // indirect
	github.com/gorilla/websocket v1.5.3 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect
	github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
	github.com/jonboulle/clockwork v0.5.0 // indirect
	github.com/klauspost/compress v1.18.4 // indirect
	github.com/kr/pretty v0.3.1 // indirect
	github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
	github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
	github.com/nats-io/nats.go v1.49.0 // indirect
	github.com/nats-io/nkeys v0.4.15 // indirect
	github.com/nats-io/nuid v1.0.1 // indirect
	github.com/oapi-codegen/runtime v1.2.0 // indirect
	github.com/parquet-go/bitpack v1.0.0 // indirect
	github.com/parquet-go/jsonlite v1.4.0 // indirect
	github.com/pierrec/lz4/v4 v4.1.26 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/robfig/cron/v3 v3.0.1 // indirect
	github.com/rogpeppe/go-internal v1.10.0 // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/sosodev/duration v1.4.0 // indirect
	github.com/stmcginnis/gofish v0.21.4 // indirect
	github.com/stretchr/objx v0.5.2 // indirect
	github.com/swaggo/files v1.0.1 // indirect
	github.com/twpayne/go-geom v1.6.1 // indirect
	github.com/urfave/cli/v2 v2.27.7 // indirect
	github.com/urfave/cli/v3 v3.7.0 // indirect
	github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
	go.yaml.in/yaml/v2 v2.4.3 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/mod v0.34.0 // indirect
	golang.org/x/net v0.52.0 // indirect
	golang.org/x/sys v0.42.0 // indirect
	golang.org/x/text v0.35.0 // indirect
	golang.org/x/tools v0.43.0 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	sigs.k8s.io/yaml v1.6.0 // indirect
)

+replace github.com/ClusterCockpit/cc-lib/v2 => ../../prg/CC/cc-lib


## Part 2b: Add GraphQL enum and update query parameters


enum ResampleAlgo {
  LTTB
  AVERAGE
  SIMPLE
}

enum Aggregate {


The file /Users/jan/tmp/cc-backend/api/schema.graphqls has been updated successfully.

enum ResampleAlgo {
  LTTB
  AVERAGE
  SIMPLE
}

enum Aggregate {

scalar Time
scalar Any

scalar NullableFloat
scalar MetricScope
scalar JobState
scalar SchedulerState
scalar MonitoringState

type Node {
  id: ID!
  hostname: String!
  cluster: String!
  subCluster: String!
  jobsRunning: Int!
  cpusAllocated: Int
  memoryAllocated: Int
  gpusAllocated: Int
  schedulerState: SchedulerState!
  healthState: MonitoringState!
  metaData: Any
  healthData: Any
}

type NodeStates {
  state: String!
  count: Int!
}

type NodeStatesTimed {
  state: String!
  counts: [Int!]!
  times: [Int!]!
}

type Job {
  id: ID!
  jobId: Int!
  user: String!
  project: String!
  cluster: String!
  subCluster: String!
  startTime: Time!
  duration: Int!
  walltime: Int!
  numNodes: Int!
  numHWThreads: Int!
  numAcc: Int!
  energy: Float!
  SMT: Int!
  shared: String!
  partition: String!
  arrayJobId: Int!
  monitoringStatus: Int!
  state: JobState!
  tags: [Tag!]!
  resources: [Resource!]!
  concurrentJobs: JobLinkResultList
  footprint: [FootprintValue]
  energyFootprint: [EnergyFootprintValue]
  metaData: Any
  userData: User
}

type JobLink {
  id: ID!
  jobId: Int!
}

type Cluster {
  name: String!
  partitions: [String!]! # Slurm partitions
  subClusters: [SubCluster!]! # Hardware partitions/subclusters
}

type SubCluster {
  name: String!
  nodes: String!
  numberOfNodes: Int!
  processorType: String!
  socketsPerNode: Int!
  coresPerSocket: Int!
  threadsPerCore: Int!
  flopRateScalar: MetricValue!
  flopRateSimd: MetricValue!
  memoryBandwidth: MetricValue!
  topology: Topology!
  metricConfig: [MetricConfig!]!
  footprint: [String!]!
}

type FootprintValue {
  name: String!
  stat: String!
  value: Float!
}

type EnergyFootprintValue {
  hardware: String!
  metric: String!
  value: Float!
}

type MetricValue {
  name: String
  unit: Unit!
  value: Float!
}

type Topology {
  node: [Int!]
  socket: [[Int!]!]
  memoryDomain: [[Int!]!]
  die: [[Int!]!]
  core: [[Int!]!]
  accelerators: [Accelerator!]
}

type Accelerator {
  id: String!
  type: String!
  model: String!
}

type SubClusterConfig {
  name: String!
  peak: Float
  normal: Float
  caution: Float
  alert: Float
  remove: Boolean
}

type MetricConfig {
  name: String!
  unit: Unit!
  scope: MetricScope!
  aggregation: String!
  timestep: Int!
  peak: Float!
  normal: Float
  caution: Float!
  alert: Float!
  lowerIsBetter: Boolean
  subClusters: [SubClusterConfig!]!
}

type Tag {
  id: ID!
  type: String!
  name: String!
  scope: String!
}

type Resource {
  hostname: String!
  hwthreads: [Int!]
  accelerators: [String!]
  configuration: String
}

type JobMetricWithName {
  name: String!
  scope: MetricScope!
  metric: JobMetric!
}

type ClusterMetricWithName {
  name: String!
  unit: Unit
  timestep: Int!
  data: [NullableFloat!]!
}

type JobMetric {
  unit: Unit
  timestep: Int!
  series: [Series!]
  statisticsSeries: StatsSeries
}

type Series {
  hostname: String!
  id: String
  statistics: MetricStatistics
  data: [NullableFloat!]!
}

type StatsSeries {
  mean: [NullableFloat!]!
  median: [NullableFloat!]!
  min: [NullableFloat!]!
  max: [NullableFloat!]!
}

type NamedStatsWithScope {
  name: String!
  scope: MetricScope!
  stats: [ScopedStats!]!
}

type ScopedStats {
  hostname: String!
  id: String
  data: MetricStatistics!
}

type JobStats {
  id: Int!
  jobId: String!
  startTime: Int!
  duration: Int!
  cluster: String!
  subCluster: String!
  numNodes: Int!
  numHWThreads: Int
  numAccelerators: Int
  stats: [NamedStats!]!
}

type NamedStats {
  name: String!
  data: MetricStatistics!
}

type Unit {
  base: String!
  prefix: String
}

type MetricStatistics {
  avg: Float!
  min: Float!
  max: Float!
}

type MetricFootprints {
  metric: String!
  data: [NullableFloat!]!
}

type Footprints {
  timeWeights: TimeWeights!
  metrics: [MetricFootprints!]!
}

type TimeWeights {
  nodeHours: [NullableFloat!]!
  accHours: [NullableFloat!]!
  coreHours: [NullableFloat!]!
}

enum Aggregate {
  USER
  PROJECT
  CLUSTER
  SUBCLUSTER
}
enum SortByAggregate {
  TOTALWALLTIME
  TOTALJOBS
  TOTALUSERS
  TOTALNODES
  TOTALNODEHOURS
  TOTALCORES
  TOTALCOREHOURS
  TOTALACCS
  TOTALACCHOURS
}

type NodeMetrics {
  host: String!
  nodeState: String!
  metricHealth: String!
  subCluster: String!
  metrics: [JobMetricWithName!]!
}

type ClusterMetrics {
  nodeCount: Int!
  metrics: [ClusterMetricWithName!]!
}

type NodesResultList {
  items: [NodeMetrics!]!
  offset: Int
  limit: Int
  count: Int
  totalNodes: Int
  hasNextPage: Boolean
}

type ClusterSupport {
  cluster: String!
  subClusters: [String!]!
}

type GlobalMetricListItem {
  name: String!
  unit: Unit!
  scope: MetricScope!
  footprint: String
  availability: [ClusterSupport!]!
}

type Count {
  name: String!
  count: Int!
}

type User {
  username: String!
  name: String!
  email: String!
}

input MetricStatItem {
  metricName: String!
  range: FloatRange!
}

type Query {
  clusters: [Cluster!]! # List of all clusters
  tags: [Tag!]! # List of all tags
  globalMetrics: [GlobalMetricListItem!]!

  user(username: String!): User
  allocatedNodes(cluster: String!): [Count!]!

  ## Node Queries New
  node(id: ID!): Node
  nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodesWithMeta(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodeStates(filter: [NodeFilter!]): [NodeStates!]!
  nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]!

  job(id: ID!): Job
  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
  ): [JobMetricWithName!]!

  jobStats(id: ID!, metrics: [String!]): [NamedStats!]!

  scopedJobStats(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
  ): [NamedStatsWithScope!]!

  jobs(
    filter: [JobFilter!]
    page: PageRequest
    order: OrderByInput
  ): JobResultList!

  jobsStatistics(
    filter: [JobFilter!]
    metrics: [String!]
    page: PageRequest
    sortBy: SortByAggregate
    groupBy: Aggregate
    numDurationBins: String
    numMetricBins: Int
  ): [JobsStatistics!]!

  jobsMetricStats(filter: [JobFilter!], metrics: [String!]): [JobStats!]!
  jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints

  rooflineHeatmap(
    filter: [JobFilter!]!
    rows: Int!
    cols: Int!
    minX: Float!
    minY: Float!
    maxX: Float!
    maxY: Float!
  ): [[Float!]!]!

  nodeMetrics(
    cluster: String!
    nodes: [String!]
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
  ): [NodeMetrics!]!

  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
  ): NodesResultList!

  clusterMetrics(
    cluster: String!
    metrics: [String!]
    from: Time!
    to: Time!
  ): ClusterMetrics!
}

type Mutation {
  createTag(type: String!, name: String!, scope: String!): Tag!
  deleteTag(id: ID!): ID!
  addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagFromList(tagIds: [ID!]!): [Int!]!

  updateConfiguration(name: String!, value: String!): String
}

type IntRangeOutput {
  from: Int!
  to: Int!
}
type TimeRangeOutput {
  range: String
  from: Time!
  to: Time!
}

input NodeFilter {
  hostname: StringInput
  cluster: StringInput
  subCluster: StringInput
  schedulerState: SchedulerState
  healthState: MonitoringState
  timeStart: Int
}

input JobFilter {
  tags: [ID!]
  dbId: [ID!]
  jobId: StringInput
  arrayJobId: Int
  user: StringInput
  project: StringInput
  jobName: StringInput
  cluster: StringInput
  subCluster: StringInput
  partition: StringInput
  duration: IntRange
  energy: FloatRange

  numNodes: IntRange
  numAccelerators: IntRange
  numHWThreads: IntRange

  startTime: TimeRange
  state: [JobState!]
  metricStats: [MetricStatItem!]
  shared: String
  schedule: String
  node: StringInput
}

input OrderByInput {
  field: String!
  type: String!
  order: SortDirectionEnum! = ASC
}

enum SortDirectionEnum {
  DESC
  ASC
}

input StringInput {
  eq: String
  neq: String
  contains: String
  startsWith: String
  endsWith: String
  in: [String!]
}

input IntRange {
  from: Int!
  to: Int!
}
input TimeRange {
  range: String
  from: Time
  to: Time
}

input FloatRange {
  from: Float!
  to: Float!
}

type NodeStateResultList {
  items: [Node!]!
  count: Int
}

type JobResultList {
  items: [Job!]!
  offset: Int
  limit: Int
  count: Int
  hasNextPage: Boolean
}

type JobLinkResultList {
  listQuery: String
  items: [JobLink!]!
  count: Int
}

type HistoPoint {
  count: Int!
  value: Int!
}

type MetricHistoPoints {
  metric: String!
  unit: String!
  stat: String
  data: [MetricHistoPoint!]
}

type MetricHistoPoint {
  bin: Int
  count: Int!
  min: Int
  max: Int
}

type JobsStatistics {
  id: ID! # If `groupBy` was used, ID of the user/project/cluster/subcluster
  name: String! # if User-Statistics: Given Name of Account (ID) Owner
  totalUsers: Int! # if *not* User-Statistics: Number of active users (based on running jobs)
  totalJobs: Int! # Number of jobs
  runningJobs: Int! # Number of running jobs
  shortJobs: Int! # Number of jobs with a duration of less than config'd ShortRunningJobsDuration
  totalWalltime: Int! # Sum of the duration of all matched jobs in hours
  totalNodes: Int! # Sum of the nodes of all matched jobs
  totalNodeHours: Int! # Sum of the node hours of all matched jobs
  totalCores: Int! # Sum of the cores of all matched jobs
  totalCoreHours: Int! # Sum of the core hours of all matched jobs
  totalAccs: Int! # Sum of the accs of all matched jobs
  totalAccHours: Int! # Sum of the gpu hours of all matched jobs
  histDuration: [HistoPoint!]! # value: hour, count: number of jobs with a rounded duration of value
  histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes
  histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores
  histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs
  histMetrics: [MetricHistoPoints!]! # metric: metricname, data array of histopoints: value: metric average bin, count: number of jobs with that metric average
}

input PageRequest {
  itemsPerPage: Int!
  page: Int!
}

coreHours: [NullableFloat!]!
}
+enum ResampleAlgo {
+  LTTB
+  AVERAGE
+  SIMPLE
+}
+
enum Aggregate {
USER
PROJECT


  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
  ): [JobMetricWithName!]!

  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
    resampleAlgo: ResampleAlgo
  ): [JobMetricWithName!]!


  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
    resampleAlgo: ResampleAlgo
  ): [JobMetricWithName!]!

scalar Time
scalar Any

scalar NullableFloat
scalar MetricScope
scalar JobState
scalar SchedulerState
scalar MonitoringState

type Node {
  id: ID!
  hostname: String!
  cluster: String!
  subCluster: String!
  jobsRunning: Int!
  cpusAllocated: Int
  memoryAllocated: Int
  gpusAllocated: Int
  schedulerState: SchedulerState!
  healthState: MonitoringState!
  metaData: Any
  healthData: Any
}

type NodeStates {
  state: String!
  count: Int!
}

type NodeStatesTimed {
  state: String!
  counts: [Int!]!
  times: [Int!]!
}

type Job {
  id: ID!
  jobId: Int!
  user: String!
  project: String!
  cluster: String!
  subCluster: String!
  startTime: Time!
  duration: Int!
  walltime: Int!
  numNodes: Int!
  numHWThreads: Int!
  numAcc: Int!
  energy: Float!
  SMT: Int!
  shared: String!
  partition: String!
  arrayJobId: Int!
  monitoringStatus: Int!
  state: JobState!
  tags: [Tag!]!
  resources: [Resource!]!
  concurrentJobs: JobLinkResultList
  footprint: [FootprintValue]
  energyFootprint: [EnergyFootprintValue]
  metaData: Any
  userData: User
}

type JobLink {
  id: ID!
  jobId: Int!
}

type Cluster {
  name: String!
  partitions: [String!]! # Slurm partitions
  subClusters: [SubCluster!]! # Hardware partitions/subclusters
}

type SubCluster {
  name: String!
  nodes: String!
  numberOfNodes: Int!
  processorType: String!
  socketsPerNode: Int!
  coresPerSocket: Int!
  threadsPerCore: Int!
  flopRateScalar: MetricValue!
  flopRateSimd: MetricValue!
  memoryBandwidth: MetricValue!
  topology: Topology!
  metricConfig: [MetricConfig!]!
  footprint: [String!]!
}

type FootprintValue {
  name: String!
  stat: String!
  value: Float!
}

type EnergyFootprintValue {
  hardware: String!
  metric: String!
  value: Float!
}

type MetricValue {
  name: String
  unit: Unit!
  value: Float!
}

type Topology {
  node: [Int!]
  socket: [[Int!]!]
  memoryDomain: [[Int!]!]
  die: [[Int!]!]
  core: [[Int!]!]
  accelerators: [Accelerator!]
}

type Accelerator {
  id: String!
  type: String!
  model: String!
}

type SubClusterConfig {
  name: String!
  peak: Float
  normal: Float
  caution: Float
  alert: Float
  remove: Boolean
}

type MetricConfig {
  name: String!
  unit: Unit!
  scope: MetricScope!
  aggregation: String!
  timestep: Int!
  peak: Float!
  normal: Float
  caution: Float!
  alert: Float!
  lowerIsBetter: Boolean
  subClusters: [SubClusterConfig!]!
}

type Tag {
  id: ID!
  type: String!
  name: String!
  scope: String!
}

type Resource {
  hostname: String!
  hwthreads: [Int!]
  accelerators: [String!]
  configuration: String
}

type JobMetricWithName {
  name: String!
  scope: MetricScope!
  metric: JobMetric!
}

type ClusterMetricWithName {
  name: String!
  unit: Unit
  timestep: Int!
  data: [NullableFloat!]!
}

type JobMetric {
  unit: Unit
  timestep: Int!
  series: [Series!]
  statisticsSeries: StatsSeries
}

type Series {
  hostname: String!
  id: String
  statistics: MetricStatistics
  data: [NullableFloat!]!
}

type StatsSeries {
  mean: [NullableFloat!]!
  median: [NullableFloat!]!
  min: [NullableFloat!]!
  max: [NullableFloat!]!
}

type NamedStatsWithScope {
  name: String!
  scope: MetricScope!
  stats: [ScopedStats!]!
}

type ScopedStats {
  hostname: String!
  id: String
  data: MetricStatistics!
}

type JobStats {
  id: Int!
  jobId: String!
  startTime: Int!
  duration: Int!
  cluster: String!
  subCluster: String!
  numNodes: Int!
  numHWThreads: Int
  numAccelerators: Int
  stats: [NamedStats!]!
}

type NamedStats {
  name: String!
  data: MetricStatistics!
}

type Unit {
  base: String!
  prefix: String
}

type MetricStatistics {
  avg: Float!
  min: Float!
  max: Float!
}

type MetricFootprints {
  metric: String!
  data: [NullableFloat!]!
}

type Footprints {
  timeWeights: TimeWeights!
  metrics: [MetricFootprints!]!
}

type TimeWeights {
  nodeHours: [NullableFloat!]!
  accHours: [NullableFloat!]!
  coreHours: [NullableFloat!]!
}

enum ResampleAlgo {
  LTTB
  AVERAGE
  SIMPLE
}

enum Aggregate {
  USER
  PROJECT
  CLUSTER
  SUBCLUSTER
}
enum SortByAggregate {
  TOTALWALLTIME
  TOTALJOBS
  TOTALUSERS
  TOTALNODES
  TOTALNODEHOURS
  TOTALCORES
  TOTALCOREHOURS
  TOTALACCS
  TOTALACCHOURS
}

type NodeMetrics {
  host: String!
  nodeState: String!
  metricHealth: String!
  subCluster: String!
  metrics: [JobMetricWithName!]!
}

type ClusterMetrics {
  nodeCount: Int!
  metrics: [ClusterMetricWithName!]!
}

type NodesResultList {
  items: [NodeMetrics!]!
  offset: Int
  limit: Int
  count: Int
  totalNodes: Int
  hasNextPage: Boolean
}

type ClusterSupport {
  cluster: String!
  subClusters: [String!]!
}

type GlobalMetricListItem {
  name: String!
  unit: Unit!
  scope: MetricScope!
  footprint: String
  availability: [ClusterSupport!]!
}

type Count {
  name: String!
  count: Int!
}

type User {
  username: String!
  name: String!
  email: String!
}

input MetricStatItem {
  metricName: String!
  range: FloatRange!
}

type Query {
  clusters: [Cluster!]! # List of all clusters
  tags: [Tag!]! # List of all tags
  globalMetrics: [GlobalMetricListItem!]!

  user(username: String!): User
  allocatedNodes(cluster: String!): [Count!]!

  ## Node Queries New
  node(id: ID!): Node
  nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodesWithMeta(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodeStates(filter: [NodeFilter!]): [NodeStates!]!
  nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]!

  job(id: ID!): Job
  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
  ): [JobMetricWithName!]!

  jobStats(id: ID!, metrics: [String!]): [NamedStats!]!

  scopedJobStats(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
  ): [NamedStatsWithScope!]!

  jobs(
    filter: [JobFilter!]
    page: PageRequest
    order: OrderByInput
  ): JobResultList!

  jobsStatistics(
    filter: [JobFilter!]
    metrics: [String!]
    page: PageRequest
    sortBy: SortByAggregate
    groupBy: Aggregate
    numDurationBins: String
    numMetricBins: Int
  ): [JobsStatistics!]!

  jobsMetricStats(filter: [JobFilter!], metrics: [String!]): [JobStats!]!
  jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints

  rooflineHeatmap(
    filter: [JobFilter!]!
    rows: Int!
    cols: Int!
    minX: Float!
    minY: Float!
    maxX: Float!
    maxY: Float!
  ): [[Float!]!]!

  nodeMetrics(
    cluster: String!
    nodes: [String!]
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
  ): [NodeMetrics!]!

  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
  ): NodesResultList!

  clusterMetrics(
    cluster: String!
    metrics: [String!]
    from: Time!
    to: Time!
  ): ClusterMetrics!
}

type Mutation {
  createTag(type: String!, name: String!, scope: String!): Tag!
  deleteTag(id: ID!): ID!
  addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagFromList(tagIds: [ID!]!): [Int!]!

  updateConfiguration(name: String!, value: String!): String
}

type IntRangeOutput {
  from: Int!
  to: Int!
}
type TimeRangeOutput {
  range: String
  from: Time!
  to: Time!
}

input NodeFilter {
  hostname: StringInput
  cluster: StringInput
  subCluster: StringInput
  schedulerState: SchedulerState
  healthState: MonitoringState
  timeStart: Int
}

input JobFilter {
  tags: [ID!]
  dbId: [ID!]
  jobId: StringInput
  arrayJobId: Int
  user: StringInput
  project: StringInput
  jobName: StringInput
  cluster: StringInput
  subCluster: StringInput
  partition: StringInput
  duration: IntRange
  energy: FloatRange

  numNodes: IntRange
  numAccelerators: IntRange
  numHWThreads: IntRange

  startTime: TimeRange
  state: [JobState!]
  metricStats: [MetricStatItem!]
  shared: String
  schedule: String
  node: StringInput
}

input OrderByInput {
  field: String!
  type: String!
  order: SortDirectionEnum! = ASC
}

enum SortDirectionEnum {
  DESC
  ASC
}

input StringInput {
  eq: String
  neq: String
  contains: String
  startsWith: String
  endsWith: String
  in: [String!]
}

input IntRange {
  from: Int!
  to: Int!
}
input TimeRange {
  range: String
  from: Time
  to: Time
}

input FloatRange {
  from: Float!
  to: Float!
}

type NodeStateResultList {
  items: [Node!]!
  count: Int
}

type JobResultList {
  items: [Job!]!
  offset: Int
  limit: Int
  count: Int
  hasNextPage: Boolean
}

type JobLinkResultList {
  listQuery: String
  items: [JobLink!]!
  count: Int
}

type HistoPoint {
  count: Int!
  value: Int!
}

type MetricHistoPoints {
  metric: String!
  unit: String!
  stat: String
  data: [MetricHistoPoint!]
}

type MetricHistoPoint {
  bin: Int
  count: Int!
  min: Int
  max: Int
}

type JobsStatistics {
  id: ID! # If `groupBy` was used, ID of the user/project/cluster/subcluster
  name: String! # if User-Statistics: Given Name of Account (ID) Owner
  totalUsers: Int! # if *not* User-Statistics: Number of active users (based on running jobs)
  totalJobs: Int! # Number of jobs
  runningJobs: Int! # Number of running jobs
  shortJobs: Int! # Number of jobs with a duration of less than config'd ShortRunningJobsDuration
  totalWalltime: Int! # Sum of the duration of all matched jobs in hours
  totalNodes: Int! # Sum of the nodes of all matched jobs
  totalNodeHours: Int! # Sum of the node hours of all matched jobs
  totalCores: Int! # Sum of the cores of all matched jobs
  totalCoreHours: Int! # Sum of the core hours of all matched jobs
  totalAccs: Int! # Sum of the accs of all matched jobs
  totalAccHours: Int! # Sum of the gpu hours of all matched jobs
  histDuration: [HistoPoint!]! # value: hour, count: number of jobs with a rounded duration of value
  histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes
  histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores
  histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs
  histMetrics: [MetricHistoPoints!]! # metric: metricname, data array of histopoints: value: metric average bin, count: number of jobs with that metric average
}

input PageRequest {
  itemsPerPage: Int!
  page: Int!
}


  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
  ): NodesResultList!

  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
    resampleAlgo: ResampleAlgo
  ): NodesResultList!


  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
    resampleAlgo: ResampleAlgo
  ): NodesResultList!

scalar Time
scalar Any

scalar NullableFloat
scalar MetricScope
scalar JobState
scalar SchedulerState
scalar MonitoringState

type Node {
  id: ID!
  hostname: String!
  cluster: String!
  subCluster: String!
  jobsRunning: Int!
  cpusAllocated: Int
  memoryAllocated: Int
  gpusAllocated: Int
  schedulerState: SchedulerState!
  healthState: MonitoringState!
  metaData: Any
  healthData: Any
}

type NodeStates {
  state: String!
  count: Int!
}

type NodeStatesTimed {
  state: String!
  counts: [Int!]!
  times: [Int!]!
}

type Job {
  id: ID!
  jobId: Int!
  user: String!
  project: String!
  cluster: String!
  subCluster: String!
  startTime: Time!
  duration: Int!
  walltime: Int!
  numNodes: Int!
  numHWThreads: Int!
  numAcc: Int!
  energy: Float!
  SMT: Int!
  shared: String!
  partition: String!
  arrayJobId: Int!
  monitoringStatus: Int!
  state: JobState!
  tags: [Tag!]!
  resources: [Resource!]!
  concurrentJobs: JobLinkResultList
  footprint: [FootprintValue]
  energyFootprint: [EnergyFootprintValue]
  metaData: Any
  userData: User
}

type JobLink {
  id: ID!
  jobId: Int!
}

type Cluster {
  name: String!
  partitions: [String!]! # Slurm partitions
  subClusters: [SubCluster!]! # Hardware partitions/subclusters
}

type SubCluster {
  name: String!
  nodes: String!
  numberOfNodes: Int!
  processorType: String!
  socketsPerNode: Int!
  coresPerSocket: Int!
  threadsPerCore: Int!
  flopRateScalar: MetricValue!
  flopRateSimd: MetricValue!
  memoryBandwidth: MetricValue!
  topology: Topology!
  metricConfig: [MetricConfig!]!
  footprint: [String!]!
}

type FootprintValue {
  name: String!
  stat: String!
  value: Float!
}

type EnergyFootprintValue {
  hardware: String!
  metric: String!
  value: Float!
}

type MetricValue {
  name: String
  unit: Unit!
  value: Float!
}

type Topology {
  node: [Int!]
  socket: [[Int!]!]
  memoryDomain: [[Int!]!]
  die: [[Int!]!]
  core: [[Int!]!]
  accelerators: [Accelerator!]
}

type Accelerator {
  id: String!
  type: String!
  model: String!
}

type SubClusterConfig {
  name: String!
  peak: Float
  normal: Float
  caution: Float
  alert: Float
  remove: Boolean
}

type MetricConfig {
  name: String!
  unit: Unit!
  scope: MetricScope!
  aggregation: String!
  timestep: Int!
  peak: Float!
  normal: Float
  caution: Float!
  alert: Float!
  lowerIsBetter: Boolean
  subClusters: [SubClusterConfig!]!
}

type Tag {
  id: ID!
  type: String!
  name: String!
  scope: String!
}

type Resource {
  hostname: String!
  hwthreads: [Int!]
  accelerators: [String!]
  configuration: String
}

type JobMetricWithName {
  name: String!
  scope: MetricScope!
  metric: JobMetric!
}

type ClusterMetricWithName {
  name: String!
  unit: Unit
  timestep: Int!
  data: [NullableFloat!]!
}

type JobMetric {
  unit: Unit
  timestep: Int!
  series: [Series!]
  statisticsSeries: StatsSeries
}

type Series {
  hostname: String!
  id: String
  statistics: MetricStatistics
  data: [NullableFloat!]!
}

type StatsSeries {
  mean: [NullableFloat!]!
  median: [NullableFloat!]!
  min: [NullableFloat!]!
  max: [NullableFloat!]!
}

type NamedStatsWithScope {
  name: String!
  scope: MetricScope!
  stats: [ScopedStats!]!
}

type ScopedStats {
  hostname: String!
  id: String
  data: MetricStatistics!
}

type JobStats {
  id: Int!
  jobId: String!
  startTime: Int!
  duration: Int!
  cluster: String!
  subCluster: String!
  numNodes: Int!
  numHWThreads: Int
  numAccelerators: Int
  stats: [NamedStats!]!
}

type NamedStats {
  name: String!
  data: MetricStatistics!
}

type Unit {
  base: String!
  prefix: String
}

type MetricStatistics {
  avg: Float!
  min: Float!
  max: Float!
}

type MetricFootprints {
  metric: String!
  data: [NullableFloat!]!
}

type Footprints {
  timeWeights: TimeWeights!
  metrics: [MetricFootprints!]!
}

type TimeWeights {
  nodeHours: [NullableFloat!]!
  accHours: [NullableFloat!]!
  coreHours: [NullableFloat!]!
}

enum ResampleAlgo {
  LTTB
  AVERAGE
  SIMPLE
}

enum Aggregate {
  USER
  PROJECT
  CLUSTER
  SUBCLUSTER
}
enum SortByAggregate {
  TOTALWALLTIME
  TOTALJOBS
  TOTALUSERS
  TOTALNODES
  TOTALNODEHOURS
  TOTALCORES
  TOTALCOREHOURS
  TOTALACCS
  TOTALACCHOURS
}

type NodeMetrics {
  host: String!
  nodeState: String!
  metricHealth: String!
  subCluster: String!
  metrics: [JobMetricWithName!]!
}

type ClusterMetrics {
  nodeCount: Int!
  metrics: [ClusterMetricWithName!]!
}

type NodesResultList {
  items: [NodeMetrics!]!
  offset: Int
  limit: Int
  count: Int
  totalNodes: Int
  hasNextPage: Boolean
}

type ClusterSupport {
  cluster: String!
  subClusters: [String!]!
}

type GlobalMetricListItem {
  name: String!
  unit: Unit!
  scope: MetricScope!
  footprint: String
  availability: [ClusterSupport!]!
}

type Count {
  name: String!
  count: Int!
}

type User {
  username: String!
  name: String!
  email: String!
}

input MetricStatItem {
  metricName: String!
  range: FloatRange!
}

type Query {
  clusters: [Cluster!]! # List of all clusters
  tags: [Tag!]! # List of all tags
  globalMetrics: [GlobalMetricListItem!]!

  user(username: String!): User
  allocatedNodes(cluster: String!): [Count!]!

  ## Node Queries New
  node(id: ID!): Node
  nodes(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodesWithMeta(filter: [NodeFilter!], order: OrderByInput): NodeStateResultList!
  nodeStates(filter: [NodeFilter!]): [NodeStates!]!
  nodeStatesTimed(filter: [NodeFilter!], type: String!): [NodeStatesTimed!]!

  job(id: ID!): Job
  jobMetrics(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
    resolution: Int
    resampleAlgo: ResampleAlgo
  ): [JobMetricWithName!]!

  jobStats(id: ID!, metrics: [String!]): [NamedStats!]!

  scopedJobStats(
    id: ID!
    metrics: [String!]
    scopes: [MetricScope!]
  ): [NamedStatsWithScope!]!

  jobs(
    filter: [JobFilter!]
    page: PageRequest
    order: OrderByInput
  ): JobResultList!

  jobsStatistics(
    filter: [JobFilter!]
    metrics: [String!]
    page: PageRequest
    sortBy: SortByAggregate
    groupBy: Aggregate
    numDurationBins: String
    numMetricBins: Int
  ): [JobsStatistics!]!

  jobsMetricStats(filter: [JobFilter!], metrics: [String!]): [JobStats!]!
  jobsFootprints(filter: [JobFilter!], metrics: [String!]!): Footprints

  rooflineHeatmap(
    filter: [JobFilter!]!
    rows: Int!
    cols: Int!
    minX: Float!
    minY: Float!
    maxX: Float!
    maxY: Float!
  ): [[Float!]!]!

  nodeMetrics(
    cluster: String!
    nodes: [String!]
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
  ): [NodeMetrics!]!

  nodeMetricsList(
    cluster: String!
    subCluster: String!
    stateFilter: String!
    nodeFilter: String!
    scopes: [MetricScope!]
    metrics: [String!]
    from: Time!
    to: Time!
    page: PageRequest
    resolution: Int
  ): NodesResultList!

  clusterMetrics(
    cluster: String!
    metrics: [String!]
    from: Time!
    to: Time!
  ): ClusterMetrics!
}

type Mutation {
  createTag(type: String!, name: String!, scope: String!): Tag!
  deleteTag(id: ID!): ID!
  addTagsToJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagsFromJob(job: ID!, tagIds: [ID!]!): [Tag!]!
  removeTagFromList(tagIds: [ID!]!): [Int!]!

  updateConfiguration(name: String!, value: String!): String
}

type IntRangeOutput {
  from: Int!
  to: Int!
}
type TimeRangeOutput {
  range: String
  from: Time!
  to: Time!
}

input NodeFilter {
  hostname: StringInput
  cluster: StringInput
  subCluster: StringInput
  schedulerState: SchedulerState
  healthState: MonitoringState
  timeStart: Int
}

input JobFilter {
  tags: [ID!]
  dbId: [ID!]
  jobId: StringInput
  arrayJobId: Int
  user: StringInput
  project: StringInput
  jobName: StringInput
  cluster: StringInput
  subCluster: StringInput
  partition: StringInput
  duration: IntRange
  energy: FloatRange

  numNodes: IntRange
  numAccelerators: IntRange
  numHWThreads: IntRange

  startTime: TimeRange
  state: [JobState!]
  metricStats: [MetricStatItem!]
  shared: String
  schedule: String
  node: StringInput
}

input OrderByInput {
  field: String!
  type: String!
  order: SortDirectionEnum! = ASC
}

enum SortDirectionEnum {
  DESC
  ASC
}

input StringInput {
  eq: String
  neq: String
  contains: String
  startsWith: String
  endsWith: String
  in: [String!]
}

input IntRange {
  from: Int!
  to: Int!
}
input TimeRange {
  range: String
  from: Time
  to: Time
}

input FloatRange {
  from: Float!
  to: Float!
}

type NodeStateResultList {
  items: [Node!]!
  count: Int
}

type JobResultList {
  items: [Job!]!
  offset: Int
  limit: Int
  count: Int
  hasNextPage: Boolean
}

type JobLinkResultList {
  listQuery: String
  items: [JobLink!]!
  count: Int
}

type HistoPoint {
  count: Int!
  value: Int!
}

type MetricHistoPoints {
  metric: String!
  unit: String!
  stat: String
  data: [MetricHistoPoint!]
}

type MetricHistoPoint {
  bin: Int
  count: Int!
  min: Int
  max: Int
}

type JobsStatistics {
  id: ID! # If `groupBy` was used, ID of the user/project/cluster/subcluster
  name: String! # if User-Statistics: Given Name of Account (ID) Owner
  totalUsers: Int! # if *not* User-Statistics: Number of active users (based on running jobs)
  totalJobs: Int! # Number of jobs
  runningJobs: Int! # Number of running jobs
  shortJobs: Int! # Number of jobs with a duration of less than config'd ShortRunningJobsDuration
  totalWalltime: Int! # Sum of the duration of all matched jobs in hours
  totalNodes: Int! # Sum of the nodes of all matched jobs
  totalNodeHours: Int! # Sum of the node hours of all matched jobs
  totalCores: Int! # Sum of the cores of all matched jobs
  totalCoreHours: Int! # Sum of the core hours of all matched jobs
  totalAccs: Int! # Sum of the accs of all matched jobs
  totalAccHours: Int! # Sum of the gpu hours of all matched jobs
  histDuration: [HistoPoint!]! # value: hour, count: number of jobs with a rounded duration of value
  histNumNodes: [HistoPoint!]! # value: number of nodes, count: number of jobs with that number of nodes
  histNumCores: [HistoPoint!]! # value: number of cores, count: number of jobs with that number of cores
  histNumAccs: [HistoPoint!]! # value: number of accs, count: number of jobs with that number of accs
  histMetrics: [MetricHistoPoints!]! # metric: metricname, data array of histopoints: value: metric average bin, count: number of jobs with that metric average
}

input PageRequest {
  itemsPerPage: Int!
  page: Int!
}

to: Time!
page: PageRequest
resolution: Int
+    resampleAlgo: ResampleAlgo
): NodesResultList!
clusterMetrics(


## Part 2b: Regenerate GraphQL code


Bash

Regenerate GraphQL code


===>  GENERATE graphql


Good, `make graphql` succeeded. Now let me check what the generated code looks like for the new parameter:


internal/graph/schema.resolvers.go:501:func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*model.JobMetricWithName, error) {
internal/graph/schema.resolvers.go:875: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, resampleAlgo *model.ResampleAlgo) (*model.NodesResultList, error) {
internal/graph/model/models_gen.go:331:type ResampleAlgo string
internal/graph/model/models_gen.go:334:	ResampleAlgoLttb    ResampleAlgo = "LTTB"
internal/graph/model/models_gen.go:335:	ResampleAlgoAverage ResampleAlgo = "AVERAGE"
internal/graph/model/models_gen.go:336:	ResampleAlgoSimple  ResampleAlgo = "SIMPLE"
internal/graph/model/models_gen.go:339:var AllResampleAlgo = []ResampleAlgo{
internal/graph/model/models_gen.go:340:	ResampleAlgoLttb,
internal/graph/model/models_gen.go:341:	ResampleAlgoAverage,
internal/graph/model/models_gen.go:342:	ResampleAlgoSimple,
internal/graph/model/models_gen.go:345:func (e ResampleAlgo) IsValid() bool {
internal/graph/model/models_gen.go:347:	case ResampleAlgoLttb, ResampleAlgoAverage, ResampleAlgoSimple:
internal/graph/model/models_gen.go:353:func (e ResampleAlgo) String() string {
internal/graph/model/models_gen.go:357:func (e *ResampleAlgo) UnmarshalGQL(v any) error {
internal/graph/model/models_gen.go:363:	*e = ResampleAlgo(str)
internal/graph/model/models_gen.go:365:		return fmt.Errorf("%s is not a valid ResampleAlgo", str)
internal/graph/model/models_gen.go:370:func (e ResampleAlgo) MarshalGQL(w io.Writer) {
internal/graph/model/models_gen.go:374:func (e *ResampleAlgo) UnmarshalJSON(b []byte) error {
internal/graph/model/models_gen.go:382:func (e ResampleAlgo) MarshalJSON() ([]byte, error) {
internal/graph/generated/generated.go:329:		JobMetrics      func(childComplexity int, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) int
internal/graph/generated/generated.go:337:		NodeMetricsList func(childComplexity int, cluster string, subCluster string, stateFilter string, nodeFilter string, scopes []schema.MetricScope, metrics []string, from time.Time, to time.Time, page *model.PageRequest, resolution *int, resampleAlgo *model.ResampleAlgo) int
internal/graph/generated/generated.go:485:	JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*model.JobMetricWithName, error)
internal/graph/generated/generated.go:494:	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, resampleAlgo *model.ResampleAlgo) (*model.NodesResultList, error)
internal/graph/generated/generated.go:1668:		return e.ComplexityRoot.Query.JobMetrics(childComplexity, args["id"].(string), args["metrics"].([]string), args["scopes"].([]schema.MetricScope), args["resolution"].(*int), args["resampleAlgo"].(*model.ResampleAlgo)), true
internal/graph/generated/generated.go:1756:		return e.ComplexityRoot.Query.NodeMetricsList(childComplexity, args["cluster"].(string), args["subCluster"].(string), args["stateFilter"].(string), args["nodeFilter"].(string), args["scopes"].([]schema.MetricScope), args["metrics"].([]string), args["from"].(time.Time), args["to"].(time.Time), args["page"].(*model.PageRequest), args["resolution"].(*int), args["resampleAlgo"].(*model.ResampleAlgo)), true
internal/graph/generated/generated.go:2527:enum ResampleAlgo {
internal/graph/generated/generated.go:2623:    resampleAlgo: ResampleAlgo
internal/graph/generated/generated.go:2683:    resampleAlgo: ResampleAlgo
internal/graph/generated/generated.go:3017:	arg4, err := graphql.ProcessArgField(ctx, rawArgs, "resampleAlgo", ec.unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo)
internal/graph/generated/generated.go:3199:	arg10, err := graphql.ProcessArgField(ctx, rawArgs, "resampleAlgo", ec.unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo)
internal/graph/generated/generated.go:9457:			return ec.Resolvers.Query().JobMetrics(ctx, fc.Args["id"].(string), fc.Args["metrics"].([]string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["resolution"].(*int), fc.Args["resampleAlgo"].(*model.ResampleAlgo))
internal/graph/generated/generated.go:9938:			return ec.Resolvers.Query().NodeMetricsList(ctx, fc.Args["cluster"].(string), fc.Args["subCluster"].(string), fc.Args["stateFilter"].(string), fc.Args["nodeFilter"].(string), fc.Args["scopes"].([]schema.MetricScope), fc.Args["metrics"].([]string), fc.Args["from"].(time.Time), fc.Args["to"].(time.Time), fc.Args["page"].(*model.PageRequest), fc.Args["resolution"].(*int), fc.Args["resampleAlgo"].(*model.ResampleAlgo))
internal/graph/generated/generated.go:19693:func (ec *executionContext) unmarshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo(ctx context.Context, v any) (*model.ResampleAlgo, error) {
internal/graph/generated/generated.go:19697:	var res = new(model.ResampleAlgo)
internal/graph/generated/generated.go:19702:func (ec *executionContext) marshalOResampleAlgo2ᚖgithubᚗcomᚋClusterCockpitᚋccᚑbackendᚋinternalᚋgraphᚋmodelᚐResampleAlgo(ctx context.Context, sel ast.SelectionSet, v *model.ResampleAlgo) graphql.Marshaler {


The code generation worked. The resolvers already have the new `resampleAlgo *model.ResampleAlgo` parameter. Now I need to:

1. Update the resolvers to pass the algorithm through
2. Update `LoadData` and `LoadNodeListData` in dataLoader.go
3. Update `MemoryStore.Read` in metricstore.go
4. Update `LoadNodeListData` in query.go
5. Update cache key

Let me create a helper to convert the enum to string, then thread it through. Let me read the current resolver code more carefully:


   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, resampleAlgo *model.ResampleAlgo) ([]*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

// JobMetrics is the resolver for the jobMetrics field.
func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string, scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) ([]*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


Now let me update all the files. I'll convert the enum to a lowercase string for the resampler lookup.

## Part 2c: Update resolvers


Edit

	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{}

	algoName := ""
	if resampleAlgo != nil {
		algoName = strings.ToLower(resampleAlgo.String())
	}

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

	res := []*model.JobMetricWithName{}