Implement the following plan:

# Plan: Add RRDTool-style Average Consolidation Function to Resampler

## Context

The current downsampler in `cc-lib/v2/resampler` offers two algorithms:
- **LTTB** (LargestTriangleThreeBucket): Perceptually-aware — picks points that preserve visual shape (peaks/valleys). Used at all call sites.
- **SimpleResampler**: Decimation — picks every nth point. Fast but lossy.

Neither produces scientifically accurate averages over time intervals. RRDTool's **AVERAGE Consolidation Function (CF)** divides data into fixed-size buckets and computes the arithmetic mean of all points in each bucket. This is the standard approach for monitoring data where the true mean per interval matters more than visual fidelity (e.g., for statistics, SLA reporting, capacity planning).

The goal: add an `AverageResampler` to cc-lib, then expose it as a per-query option in cc-backend's GraphQL API so the frontend/caller can choose the algorithm.

---

## Part 1: cc-lib changes (`/Users/jan/prg/CC/cc-lib/`)

### 1a. Add `AverageResampler` to `resampler/resampler.go`

New exported function with the same signature as existing resamplers:

```go
func AverageResampler(data []schema.Float, oldFrequency int64, newFrequency int64) ([]schema.Float, int64, error)
```

**Algorithm (RRDTool AVERAGE CF):**
1. Call `validateFrequency()` for early-exit checks (reuses existing validation).
2. Compute `step = newFrequency / oldFrequency` (points per bucket).
3. For each bucket of `step` consecutive points, compute arithmetic mean skipping NaN values. If all values in a bucket are NaN, output NaN for that bucket.
4. Return averaged data, `newFrequency`, nil.

**NaN handling**: Skip NaN values and average only valid points (consistent with RRDTool behavior and `AddStats()` in `pkg/metricstore/api.go`). This differs from LTTB which propagates NaN if *any* bucket point is NaN.

### 1b. Add `ResamplerFunc` type and lookup

Add a function type and string-based lookup to make algorithm selection easy for consumers:

```go
// 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)
```

### 1c. Add tests to `resampler/resampler_test.go`

Following the existing test patterns (table-driven, same helper functions):
- Basic averaging: `[1,2,3,4,5,6]` step=2 → `[1.5, 3.5, 5.5]`
- NaN skipping within buckets
- All-NaN bucket → NaN output
- Early-exit conditions (same set as SimpleResampler/LTTB tests)
- Frequency validation errors
- Benchmark: `BenchmarkAverageResampler`

---

## Part 2: cc-backend changes (`/Users/jan/tmp/cc-backend/`)

### 2a. Add `replace` directive to `go.mod`

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

### 2b. Add GraphQL enum and query parameter

In `api/schema.graphqls`, add enum and parameter to both queries that accept `resolution`:

```graphql
enum ResampleAlgo {
  LTTB
  AVERAGE
  SIMPLE
}
```

Add `resampleAlgo: ResampleAlgo` parameter to:
- `jobMetrics(id: ID!, metrics: [String!], scopes: [MetricScope!], resolution: Int, resampleAlgo: ResampleAlgo): [JobMetricWithName!]!`
- `nodeMetricsList(..., resolution: Int, resampleAlgo: ResampleAlgo): NodesResultList!`

Then run `make graphql` to regenerate.

### 2c. Update resolvers to pass algorithm through

**`internal/graph/schema.resolvers.go`**:

`JobMetrics` resolver (line ~501): accept new `resampleAlgo *model.ResampleAlgo` parameter (auto-generated by gqlgen), convert to string, pass to `LoadData`.

`NodeMetricsList` resolver (line ~875): same treatment, pass to `LoadNodeListData`.

### 2d. Thread algorithm through data loading

**`internal/metricdispatch/dataLoader.go`**:

- `LoadData()` (line 85): add `resampleAlgo string` parameter. Use `resampler.GetResampler(resampleAlgo)` to get the function, call it instead of hardcoded `resampler.LargestTriangleThreeBucket` at line 145.
- `LoadNodeListData()` (line 409): add `resampleAlgo string` parameter, pass through to `InternalMetricStore.LoadNodeListData`.

**`pkg/metricstore/query.go`**:

- `InternalMetricStore.LoadNodeListData()` (line 616): add `resampleAlgo string` to signature, include in `APIQueryRequest` or pass to `MemoryStore.Read`.

**`pkg/metricstore/metricstore.go`**:

- `MemoryStore.Read()` (line ~682): add `resampleAlgo string` parameter. At line 740, use `resampler.GetResampler(resampleAlgo)` instead of hardcoded LTTB.

### 2e. Update cache key

**`internal/metricdispatch/dataLoader.go`**: The `cacheKey()` function must include the algorithm name so different algorithms don't serve stale cached results.

---

## Files to modify

| File | Repo | Change |
|------|------|--------|
| `resampler/resampler.go` | cc-lib | Add `AverageResampler`, `ResamplerFunc`, `GetResampler` |
| `resampler/resampler_test.go` | cc-lib | Add tests + benchmark |
| `go.mod` | cc-backend | Add `replace` directive |
| `api/schema.graphqls` | cc-backend | Add `ResampleAlgo` enum, update query params |
| `internal/graph/generated/` | cc-backend | Regenerated (`make graphql`) |
| `internal/graph/schema.resolvers.go` | cc-backend | Pass algorithm to data loaders |
| `internal/metricdispatch/dataLoader.go` | cc-backend | Thread algorithm to resampler calls, update cache key |
| `pkg/metricstore/query.go` | cc-backend | Thread algorithm through `LoadNodeListData` |
| `pkg/metricstore/metricstore.go` | cc-backend | Thread algorithm through `Read()`, use `GetResampler` |

## Verification

1. `cd /Users/jan/prg/CC/cc-lib && go test ./resampler/...` — new + existing tests pass
2. `cd /Users/jan/tmp/cc-backend && make graphql` — GraphQL code regenerates cleanly
3. `make` — full build succeeds
4. `make test` — all existing tests pass
5. Manual: GraphQL playground query with `resampleAlgo: AVERAGE` vs default LTTB — average produces smoother curves without artificial peak preservation


If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /Users/jan/.claude/projects/-Users-jan-tmp-cc-backend/190ae749-6a19-47a7-82da-dba5a0e7402d.jsonl