The file /Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/Systems.root.svelte has been updated successfully. (file state is current in your context — no need to Read it back)

  const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
  const displayNodeOverview = $derived((displayType === 'OVERVIEW'));

<!--
  @component Main cluster node status view component; renders overview or list depending on type

  Properties:
  - `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
  - `cluster String`: The cluster to show status information for [Default: null]
  - `subCluster String`: The subCluster to show status information for [Default: null]
  - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
  - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
-->

<script>
  import { getContext } from "svelte";
  import {
    Row,
    Col,
    Card,
    Input,
    InputGroup,
    InputGroupText,
    Icon,
    Button,
  } from "@sveltestrap/sveltestrap";
  import {
    gql,
    getContextClient,
    mutationStore,
  } from "@urql/svelte";

  import { init } from "./generic/utils.js";
  import NodeOverview from "./systems/NodeOverview.svelte";
  import NodeList from "./systems/NodeList.svelte";
  import MetricSelection from "./generic/select/MetricSelection.svelte";
  import TimeSelection from "./generic/select/TimeSelection.svelte";
  import Refresher from "./generic/helper/Refresher.svelte";

  /*
    Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
    Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
  */

  /* Scelte 5 Props */
  let {
    displayType,
    cluster = null,
    subCluster = null,
    presetFrom = null,
    presetTo = null,
  } = $props();

  /* Const Init */
  const { query: initq } = init();
  const client = getContextClient();
  const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
  const nowDate = new Date(Date.now());

  /* Var Init */
  let timeoutId = null;

  /* State Init */
  let hostnameFilter = $state("");
  let nodeStateFilter = $state("all");
  let pendingHostnameFilter = $state("");
  let isMetricsSelectionOpen = $state(false);

  /* Derived Init Return */
  const thisInit = $derived($initq?.data ? true : false);

  /* Derived States */
  const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
  const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
  const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
  const resampleResolutions = $derived(resampleConfig ? [...resampleConfig.resolutions] : []);
  const resampleDefault = $derived(resampleConfig ? Math.max(...resampleConfig.resolutions) : 0);
  const displayNodeOverview = $derived((displayType === 'OVERVIEW'));

  const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
  const systemUnits = $derived.by(() => {
    const pendingUnits = {};
    if (thisInit && systemMetrics.length > 0) {
      for (let sm of systemMetrics) {
        pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
      };
    }
    return {...pendingUnits};
  });

  let selectedResolution = $derived(resampleDefault);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));
  let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));

  let selectedMetric = $derived.by(() => {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length !== 0) return systemMetrics[0].name
    }
    return ""
  });

  let selectedMetrics = $derived.by(() => {
    let configKey = `nodeList_selectedMetrics`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
    }
    return []
  });

  /* Effects */
  $effect(() => {
    if (displayNodeOverview) {
      updateOverviewMetric(selectedMetric)
    }
  });

  /* Functions */
  // Wait after input for some time to prevent too many requests
  function updateHostnameFilter() {
    if (timeoutId != null) clearTimeout(timeoutId);
    timeoutId = setTimeout(function () {
      hostnameFilter = pendingHostnameFilter;
    }, 500);
  };

  function updateOverviewMetric(newMetric) {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    updateConfigurationMutation({
      name: configKey,
      value: JSON.stringify(newMetric),
    }).subscribe((res) => {
      if (res.fetching === false && res.error) {
        throw res.error;
      }
    });
  };

  const updateConfigurationMutation = ({ name, value }) => {
    return mutationStore({
      client: client,
      query: gql`
        mutation ($name: String!, $value: String!) {
          updateConfiguration(name: $name, value: $value)
        }
      `,
      variables: { name, value },
    });
  };

</script>

<!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
  {#if thisInit}
    <!-- List Metric Select Col-->
    {#if !displayNodeOverview}
      <Col>
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText class="text-capitalize">Metrics</InputGroupText>
          <Button
            outline
            color="primary"
            onclick={() => (isMetricsSelectionOpen = true)}
          >
            {selectedMetrics.length} selected
          </Button>
        </InputGroup>
      </Col>
      {#if resampleConfig}
        <Col>
          <InputGroup>
            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
            <InputGroupText>Resolution</InputGroupText>
            <Input type="select" bind:value={selectedResolution}>
              {#each resampleResolutions as res}
                <option value={res}
                  >{res} sec</option
                >
              {/each}
            </Input>
          </InputGroup>
        </Col>
      {/if}
    {/if}
    <!-- Node Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="hdd" /></InputGroupText>
        <InputGroupText>Node(s)</InputGroupText>
        <Input
          placeholder="Filter hostname ..."
          type="text"
          bind:value={pendingHostnameFilter}
          oninput={updateHostnameFilter}
        />
      </InputGroup>
    </Col>
    <!-- State Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
        <InputGroupText>State</InputGroupText>
        <Input type="select" bind:value={nodeStateFilter}>
          {#each stateOptions as so}
            <option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
          {/each}
        </Input>
      </InputGroup>
    </Col>
    <!-- Range Col-->
    <Col>
      <TimeSelection
        presetFrom={from}
        presetTo={to}
        applyTime={(newFrom, newTo) => {
          from = newFrom;
          to = newTo;
        }}
      />
    </Col>
    <!-- Overview Metric Col-->
    {#if displayNodeOverview}
      <Col class="mt-2 mt-lg-0">
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText>Metric</InputGroupText>
          <Input type="select" bind:value={selectedMetric}>
            {#each systemMetrics as metric (metric.name)}
              <option value={metric.name}
                >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
              >
            {:else}
              <option disabled>No available options</option>
            {/each}
          </Input>
        </InputGroup>
      </Col>
    {/if}
  {/if}
  <!-- Refresh Col-->
  <Col class="mt-2 mt-lg-0">
    <Refresher
      onRefresh={() => {
        const diff = Date.now() - to;
        from = new Date(from.getTime() + diff);
        to = new Date(to.getTime() + diff);
      }}
    />
  </Col>
</Row>

<!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
  <Row>
    <Col>
      <Card body color="danger">Unknown displayList type! </Card>
    </Col>
  </Row>
{:else}
  {#if displayNodeOverview}
    <!-- ROW2-1: Node Overview (Grid Included)-->
    <NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {nodeStateFilter}/>
  {:else}
    <!-- ROW2-2: Node List (Grid Included)-->
    <NodeList pendingSelectedMetrics={selectedMetrics} {cluster} {subCluster}
       {selectedResolution} {hostnameFilter} {nodeStateFilter} {from} {to} {systemUnits}/>
  {/if}
{/if}

{#if !displayNodeOverview}
  <MetricSelection
    bind:isOpen={isMetricsSelectionOpen}
    presetMetrics={selectedMetrics}
    {cluster}
    {subCluster}
    {globalMetrics}
    configName="nodeList_selectedMetrics"
    applyMetrics={(newMetrics) =>
      selectedMetrics = [...newMetrics]
    }
  />
{/if}

-  const resampleResolutions = $derived(resampleConfig ? [...resampleConfig.resolutions] : []);


With the current branch the frontend does not show any jobs in the job list. There is a comsole error: Uncaught TypeError: t.resampleConfig.resolutions is not iterable (cannot read property undefined)…


Edit

  let selectedResolution = $derived(resampleDefault);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));

  // null lets the backend resolve the resolution from the configured resample policy.
  let selectedResolution = $state(null);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));


  // null lets the backend resolve the resolution from the configured resample policy.
  let selectedResolution = $state(null);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));

<!--
  @component Main cluster node status view component; renders overview or list depending on type

  Properties:
  - `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
  - `cluster String`: The cluster to show status information for [Default: null]
  - `subCluster String`: The subCluster to show status information for [Default: null]
  - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
  - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
-->

<script>
  import { getContext } from "svelte";
  import {
    Row,
    Col,
    Card,
    Input,
    InputGroup,
    InputGroupText,
    Icon,
    Button,
  } from "@sveltestrap/sveltestrap";
  import {
    gql,
    getContextClient,
    mutationStore,
  } from "@urql/svelte";

  import { init } from "./generic/utils.js";
  import NodeOverview from "./systems/NodeOverview.svelte";
  import NodeList from "./systems/NodeList.svelte";
  import MetricSelection from "./generic/select/MetricSelection.svelte";
  import TimeSelection from "./generic/select/TimeSelection.svelte";
  import Refresher from "./generic/helper/Refresher.svelte";

  /*
    Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
    Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
  */

  /* Scelte 5 Props */
  let {
    displayType,
    cluster = null,
    subCluster = null,
    presetFrom = null,
    presetTo = null,
  } = $props();

  /* Const Init */
  const { query: initq } = init();
  const client = getContextClient();
  const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
  const nowDate = new Date(Date.now());

  /* Var Init */
  let timeoutId = null;

  /* State Init */
  let hostnameFilter = $state("");
  let nodeStateFilter = $state("all");
  let pendingHostnameFilter = $state("");
  let isMetricsSelectionOpen = $state(false);

  /* Derived Init Return */
  const thisInit = $derived($initq?.data ? true : false);

  /* Derived States */
  const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
  const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
  const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
  const displayNodeOverview = $derived((displayType === 'OVERVIEW'));

  const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
  const systemUnits = $derived.by(() => {
    const pendingUnits = {};
    if (thisInit && systemMetrics.length > 0) {
      for (let sm of systemMetrics) {
        pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
      };
    }
    return {...pendingUnits};
  });

  let selectedResolution = $derived(resampleDefault);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));
  let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));

  let selectedMetric = $derived.by(() => {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length !== 0) return systemMetrics[0].name
    }
    return ""
  });

  let selectedMetrics = $derived.by(() => {
    let configKey = `nodeList_selectedMetrics`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
    }
    return []
  });

  /* Effects */
  $effect(() => {
    if (displayNodeOverview) {
      updateOverviewMetric(selectedMetric)
    }
  });

  /* Functions */
  // Wait after input for some time to prevent too many requests
  function updateHostnameFilter() {
    if (timeoutId != null) clearTimeout(timeoutId);
    timeoutId = setTimeout(function () {
      hostnameFilter = pendingHostnameFilter;
    }, 500);
  };

  function updateOverviewMetric(newMetric) {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    updateConfigurationMutation({
      name: configKey,
      value: JSON.stringify(newMetric),
    }).subscribe((res) => {
      if (res.fetching === false && res.error) {
        throw res.error;
      }
    });
  };

  const updateConfigurationMutation = ({ name, value }) => {
    return mutationStore({
      client: client,
      query: gql`
        mutation ($name: String!, $value: String!) {
          updateConfiguration(name: $name, value: $value)
        }
      `,
      variables: { name, value },
    });
  };

</script>

<!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
  {#if thisInit}
    <!-- List Metric Select Col-->
    {#if !displayNodeOverview}
      <Col>
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText class="text-capitalize">Metrics</InputGroupText>
          <Button
            outline
            color="primary"
            onclick={() => (isMetricsSelectionOpen = true)}
          >
            {selectedMetrics.length} selected
          </Button>
        </InputGroup>
      </Col>
      {#if resampleConfig}
        <Col>
          <InputGroup>
            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
            <InputGroupText>Resolution</InputGroupText>
            <Input type="select" bind:value={selectedResolution}>
              {#each resampleResolutions as res}
                <option value={res}
                  >{res} sec</option
                >
              {/each}
            </Input>
          </InputGroup>
        </Col>
      {/if}
    {/if}
    <!-- Node Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="hdd" /></InputGroupText>
        <InputGroupText>Node(s)</InputGroupText>
        <Input
          placeholder="Filter hostname ..."
          type="text"
          bind:value={pendingHostnameFilter}
          oninput={updateHostnameFilter}
        />
      </InputGroup>
    </Col>
    <!-- State Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
        <InputGroupText>State</InputGroupText>
        <Input type="select" bind:value={nodeStateFilter}>
          {#each stateOptions as so}
            <option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
          {/each}
        </Input>
      </InputGroup>
    </Col>
    <!-- Range Col-->
    <Col>
      <TimeSelection
        presetFrom={from}
        presetTo={to}
        applyTime={(newFrom, newTo) => {
          from = newFrom;
          to = newTo;
        }}
      />
    </Col>
    <!-- Overview Metric Col-->
    {#if displayNodeOverview}
      <Col class="mt-2 mt-lg-0">
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText>Metric</InputGroupText>
          <Input type="select" bind:value={selectedMetric}>
            {#each systemMetrics as metric (metric.name)}
              <option value={metric.name}
                >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
              >
            {:else}
              <option disabled>No available options</option>
            {/each}
          </Input>
        </InputGroup>
      </Col>
    {/if}
  {/if}
  <!-- Refresh Col-->
  <Col class="mt-2 mt-lg-0">
    <Refresher
      onRefresh={() => {
        const diff = Date.now() - to;
        from = new Date(from.getTime() + diff);
        to = new Date(to.getTime() + diff);
      }}
    />
  </Col>
</Row>

<!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
  <Row>
    <Col>
      <Card body color="danger">Unknown displayList type! </Card>
    </Col>
  </Row>
{:else}
  {#if displayNodeOverview}
    <!-- ROW2-1: Node Overview (Grid Included)-->
    <NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {nodeStateFilter}/>
  {:else}
    <!-- ROW2-2: Node List (Grid Included)-->
    <NodeList pendingSelectedMetrics={selectedMetrics} {cluster} {subCluster}
       {selectedResolution} {hostnameFilter} {nodeStateFilter} {from} {to} {systemUnits}/>
  {/if}
{/if}

{#if !displayNodeOverview}
  <MetricSelection
    bind:isOpen={isMetricsSelectionOpen}
    presetMetrics={selectedMetrics}
    {cluster}
    {subCluster}
    {globalMetrics}
    configName="nodeList_selectedMetrics"
    applyMetrics={(newMetrics) =>
      selectedMetrics = [...newMetrics]
    }
  />
{/if}

-  let selectedResolution = $derived(resampleDefault);

+  // null lets the backend resolve the resolution from the configured resample policy.

+  let selectedResolution = $state(null);


Now remove the resolution-selector dropdown and fix the column count:


<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">

<Row cols={{ xs: 2, lg: 5 }} class="mb-3">


<!--
  @component Main cluster node status view component; renders overview or list depending on type

  Properties:
  - `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
  - `cluster String`: The cluster to show status information for [Default: null]
  - `subCluster String`: The subCluster to show status information for [Default: null]
  - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
  - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
-->

<script>
  import { getContext } from "svelte";
  import {
    Row,
    Col,
    Card,
    Input,
    InputGroup,
    InputGroupText,
    Icon,
    Button,
  } from "@sveltestrap/sveltestrap";
  import {
    gql,
    getContextClient,
    mutationStore,
  } from "@urql/svelte";

  import { init } from "./generic/utils.js";
  import NodeOverview from "./systems/NodeOverview.svelte";
  import NodeList from "./systems/NodeList.svelte";
  import MetricSelection from "./generic/select/MetricSelection.svelte";
  import TimeSelection from "./generic/select/TimeSelection.svelte";
  import Refresher from "./generic/helper/Refresher.svelte";

  /*
    Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
    Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
  */

  /* Scelte 5 Props */
  let {
    displayType,
    cluster = null,
    subCluster = null,
    presetFrom = null,
    presetTo = null,
  } = $props();

  /* Const Init */
  const { query: initq } = init();
  const client = getContextClient();
  const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
  const nowDate = new Date(Date.now());

  /* Var Init */
  let timeoutId = null;

  /* State Init */
  let hostnameFilter = $state("");
  let nodeStateFilter = $state("all");
  let pendingHostnameFilter = $state("");
  let isMetricsSelectionOpen = $state(false);

  /* Derived Init Return */
  const thisInit = $derived($initq?.data ? true : false);

  /* Derived States */
  const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
  const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
  const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
  const displayNodeOverview = $derived((displayType === 'OVERVIEW'));

  const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
  const systemUnits = $derived.by(() => {
    const pendingUnits = {};
    if (thisInit && systemMetrics.length > 0) {
      for (let sm of systemMetrics) {
        pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
      };
    }
    return {...pendingUnits};
  });

  // null lets the backend resolve the resolution from the configured resample policy.
  let selectedResolution = $state(null);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));
  let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));

  let selectedMetric = $derived.by(() => {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length !== 0) return systemMetrics[0].name
    }
    return ""
  });

  let selectedMetrics = $derived.by(() => {
    let configKey = `nodeList_selectedMetrics`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
    }
    return []
  });

  /* Effects */
  $effect(() => {
    if (displayNodeOverview) {
      updateOverviewMetric(selectedMetric)
    }
  });

  /* Functions */
  // Wait after input for some time to prevent too many requests
  function updateHostnameFilter() {
    if (timeoutId != null) clearTimeout(timeoutId);
    timeoutId = setTimeout(function () {
      hostnameFilter = pendingHostnameFilter;
    }, 500);
  };

  function updateOverviewMetric(newMetric) {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    updateConfigurationMutation({
      name: configKey,
      value: JSON.stringify(newMetric),
    }).subscribe((res) => {
      if (res.fetching === false && res.error) {
        throw res.error;
      }
    });
  };

  const updateConfigurationMutation = ({ name, value }) => {
    return mutationStore({
      client: client,
      query: gql`
        mutation ($name: String!, $value: String!) {
          updateConfiguration(name: $name, value: $value)
        }
      `,
      variables: { name, value },
    });
  };

</script>

<!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">
  {#if thisInit}
    <!-- List Metric Select Col-->
    {#if !displayNodeOverview}
      <Col>
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText class="text-capitalize">Metrics</InputGroupText>
          <Button
            outline
            color="primary"
            onclick={() => (isMetricsSelectionOpen = true)}
          >
            {selectedMetrics.length} selected
          </Button>
        </InputGroup>
      </Col>
      {#if resampleConfig}
        <Col>
          <InputGroup>
            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
            <InputGroupText>Resolution</InputGroupText>
            <Input type="select" bind:value={selectedResolution}>
              {#each resampleResolutions as res}
                <option value={res}
                  >{res} sec</option
                >
              {/each}
            </Input>
          </InputGroup>
        </Col>
      {/if}
    {/if}
    <!-- Node Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="hdd" /></InputGroupText>
        <InputGroupText>Node(s)</InputGroupText>
        <Input
          placeholder="Filter hostname ..."
          type="text"
          bind:value={pendingHostnameFilter}
          oninput={updateHostnameFilter}
        />
      </InputGroup>
    </Col>
    <!-- State Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
        <InputGroupText>State</InputGroupText>
        <Input type="select" bind:value={nodeStateFilter}>
          {#each stateOptions as so}
            <option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
          {/each}
        </Input>
      </InputGroup>
    </Col>
    <!-- Range Col-->
    <Col>
      <TimeSelection
        presetFrom={from}
        presetTo={to}
        applyTime={(newFrom, newTo) => {
          from = newFrom;
          to = newTo;
        }}
      />
    </Col>
    <!-- Overview Metric Col-->
    {#if displayNodeOverview}
      <Col class="mt-2 mt-lg-0">
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText>Metric</InputGroupText>
          <Input type="select" bind:value={selectedMetric}>
            {#each systemMetrics as metric (metric.name)}
              <option value={metric.name}
                >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
              >
            {:else}
              <option disabled>No available options</option>
            {/each}
          </Input>
        </InputGroup>
      </Col>
    {/if}
  {/if}
  <!-- Refresh Col-->
  <Col class="mt-2 mt-lg-0">
    <Refresher
      onRefresh={() => {
        const diff = Date.now() - to;
        from = new Date(from.getTime() + diff);
        to = new Date(to.getTime() + diff);
      }}
    />
  </Col>
</Row>

<!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
  <Row>
    <Col>
      <Card body color="danger">Unknown displayList type! </Card>
    </Col>
  </Row>
{:else}
  {#if displayNodeOverview}
    <!-- ROW2-1: Node Overview (Grid Included)-->
    <NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {nodeStateFilter}/>
  {:else}
    <!-- ROW2-2: Node List (Grid Included)-->
    <NodeList pendingSelectedMetrics={selectedMetrics} {cluster} {subCluster}
       {selectedResolution} {hostnameFilter} {nodeStateFilter} {from} {to} {systemUnits}/>
  {/if}
{/if}

{#if !displayNodeOverview}
  <MetricSelection
    bind:isOpen={isMetricsSelectionOpen}
    presetMetrics={selectedMetrics}
    {cluster}
    {subCluster}
    {globalMetrics}
    configName="nodeList_selectedMetrics"
    applyMetrics={(newMetrics) =>
      selectedMetrics = [...newMetrics]
    }
  />
{/if}

-<Row cols={{ xs: 2, lg: !displayNodeOverview ? (resampleConfig ? 6 : 5) : 5 }} class="mb-3">

+<Row cols={{ xs: 2, lg: 5 }} class="mb-3">


      {#if resampleConfig}
        <Col>
          <InputGroup>
            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
            <InputGroupText>Resolution</InputGroupText>
            <Input type="select" bind:value={selectedResolution}>
              {#each resampleResolutions as res}
                <option value={res}
                  >{res} sec</option
                >
              {/each}
            </Input>
          </InputGroup>
        </Col>
      {/if}
    {/if}


<!--
  @component Main cluster node status view component; renders overview or list depending on type

  Properties:
  - `displayType String?`: The type of node display ['OVERVIEW' || 'LIST']
  - `cluster String`: The cluster to show status information for [Default: null]
  - `subCluster String`: The subCluster to show status information for [Default: null]
  - `presetFrom Date?`: Custom Time Range selection 'from' [Default: null]
  - `presetTo Date?`: Custom Time Range selection 'to' [Default: null]
-->

<script>
  import { getContext } from "svelte";
  import {
    Row,
    Col,
    Card,
    Input,
    InputGroup,
    InputGroupText,
    Icon,
    Button,
  } from "@sveltestrap/sveltestrap";
  import {
    gql,
    getContextClient,
    mutationStore,
  } from "@urql/svelte";

  import { init } from "./generic/utils.js";
  import NodeOverview from "./systems/NodeOverview.svelte";
  import NodeList from "./systems/NodeList.svelte";
  import MetricSelection from "./generic/select/MetricSelection.svelte";
  import TimeSelection from "./generic/select/TimeSelection.svelte";
  import Refresher from "./generic/helper/Refresher.svelte";

  /*
    Note 1: "Sorting" as use-case ignored for now, probably default to alphanumerical on hostnames of cluster (handled in frontend at the moment)
    Note 2: Add Idle State Filter (== No allocated Jobs) [Frontend?] : Cannot be handled by CCMS, requires secondary job query and refiltering of visible nodes
  */

  /* Scelte 5 Props */
  let {
    displayType,
    cluster = null,
    subCluster = null,
    presetFrom = null,
    presetTo = null,
  } = $props();

  /* Const Init */
  const { query: initq } = init();
  const client = getContextClient();
  const stateOptions = ['all', 'allocated', 'idle', 'reserved', 'mixed', 'down', 'unknown', 'notindb'];
  const nowDate = new Date(Date.now());

  /* Var Init */
  let timeoutId = null;

  /* State Init */
  let hostnameFilter = $state("");
  let nodeStateFilter = $state("all");
  let pendingHostnameFilter = $state("");
  let isMetricsSelectionOpen = $state(false);

  /* Derived Init Return */
  const thisInit = $derived($initq?.data ? true : false);

  /* Derived States */
  const ccconfig = $derived(thisInit ? getContext("cc-config") : null);
  const globalMetrics = $derived(thisInit ? getContext("globalMetrics") : null);
  const resampleConfig = $derived(thisInit ? getContext("resampling") : null);
  const displayNodeOverview = $derived((displayType === 'OVERVIEW'));

  const systemMetrics = $derived(globalMetrics ? [...globalMetrics.filter((gm) => gm?.availability.find((av) => av.cluster == cluster))] : []);
  const systemUnits = $derived.by(() => {
    const pendingUnits = {};
    if (thisInit && systemMetrics.length > 0) {
      for (let sm of systemMetrics) {
        pendingUnits[sm.name] = (sm?.unit?.prefix ? sm.unit.prefix : "") + (sm?.unit?.base ? sm.unit.base : "")
      };
    }
    return {...pendingUnits};
  });

  // null lets the backend resolve the resolution from the configured resample policy.
  let selectedResolution = $state(null);
  let to = $derived(presetTo ? presetTo : new Date(Date.now()));
  let from = $derived(presetFrom ? presetFrom : new Date(nowDate.setHours(nowDate.getHours() - 4)));

  let selectedMetric = $derived.by(() => {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length !== 0) return systemMetrics[0].name
    }
    return ""
  });

  let selectedMetrics = $derived.by(() => {
    let configKey = `nodeList_selectedMetrics`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    if (thisInit) {
      if (ccconfig[configKey]) return ccconfig[configKey]
      else if (systemMetrics.length >= 3) return [systemMetrics[0].name, systemMetrics[1].name, systemMetrics[2].name]
    }
    return []
  });

  /* Effects */
  $effect(() => {
    if (displayNodeOverview) {
      updateOverviewMetric(selectedMetric)
    }
  });

  /* Functions */
  // Wait after input for some time to prevent too many requests
  function updateHostnameFilter() {
    if (timeoutId != null) clearTimeout(timeoutId);
    timeoutId = setTimeout(function () {
      hostnameFilter = pendingHostnameFilter;
    }, 500);
  };

  function updateOverviewMetric(newMetric) {
    let configKey = `nodeOverview_selectedMetric`;
    if (cluster) configKey += `:${cluster}`;
    if (subCluster) configKey += `:${subCluster}`;

    updateConfigurationMutation({
      name: configKey,
      value: JSON.stringify(newMetric),
    }).subscribe((res) => {
      if (res.fetching === false && res.error) {
        throw res.error;
      }
    });
  };

  const updateConfigurationMutation = ({ name, value }) => {
    return mutationStore({
      client: client,
      query: gql`
        mutation ($name: String!, $value: String!) {
          updateConfiguration(name: $name, value: $value)
        }
      `,
      variables: { name, value },
    });
  };

</script>

<!-- ROW1: Tools-->
<Row cols={{ xs: 2, lg: 5 }} class="mb-3">
  {#if thisInit}
    <!-- List Metric Select Col-->
    {#if !displayNodeOverview}
      <Col>
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText class="text-capitalize">Metrics</InputGroupText>
          <Button
            outline
            color="primary"
            onclick={() => (isMetricsSelectionOpen = true)}
          >
            {selectedMetrics.length} selected
          </Button>
        </InputGroup>
      </Col>
      {#if resampleConfig}
        <Col>
          <InputGroup>
            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>
            <InputGroupText>Resolution</InputGroupText>
            <Input type="select" bind:value={selectedResolution}>
              {#each resampleResolutions as res}
                <option value={res}
                  >{res} sec</option
                >
              {/each}
            </Input>
          </InputGroup>
        </Col>
      {/if}
    {/if}
    <!-- Node Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="hdd" /></InputGroupText>
        <InputGroupText>Node(s)</InputGroupText>
        <Input
          placeholder="Filter hostname ..."
          type="text"
          bind:value={pendingHostnameFilter}
          oninput={updateHostnameFilter}
        />
      </InputGroup>
    </Col>
    <!-- State Col-->
    <Col class="mt-2 mt-lg-0">
      <InputGroup>
        <InputGroupText><Icon name="clipboard2-pulse" /></InputGroupText>
        <InputGroupText>State</InputGroupText>
        <Input type="select" bind:value={nodeStateFilter}>
          {#each stateOptions as so}
            <option value={so}>{so.charAt(0).toUpperCase() + so.slice(1)}</option>
          {/each}
        </Input>
      </InputGroup>
    </Col>
    <!-- Range Col-->
    <Col>
      <TimeSelection
        presetFrom={from}
        presetTo={to}
        applyTime={(newFrom, newTo) => {
          from = newFrom;
          to = newTo;
        }}
      />
    </Col>
    <!-- Overview Metric Col-->
    {#if displayNodeOverview}
      <Col class="mt-2 mt-lg-0">
        <InputGroup>
          <InputGroupText><Icon name="graph-up" /></InputGroupText>
          <InputGroupText>Metric</InputGroupText>
          <Input type="select" bind:value={selectedMetric}>
            {#each systemMetrics as metric (metric.name)}
              <option value={metric.name}
                >{metric.name} {systemUnits[metric.name] ? "("+systemUnits[metric.name]+")" : ""}</option
              >
            {:else}
              <option disabled>No available options</option>
            {/each}
          </Input>
        </InputGroup>
      </Col>
    {/if}
  {/if}
  <!-- Refresh Col-->
  <Col class="mt-2 mt-lg-0">
    <Refresher
      onRefresh={() => {
        const diff = Date.now() - to;
        from = new Date(from.getTime() + diff);
        to = new Date(to.getTime() + diff);
      }}
    />
  </Col>
</Row>

<!-- ROW2: Content-->
{#if displayType !== "OVERVIEW" && displayType !== "LIST"}
  <Row>
    <Col>
      <Card body color="danger">Unknown displayList type! </Card>
    </Col>
  </Row>
{:else}
  {#if displayNodeOverview}
    <!-- ROW2-1: Node Overview (Grid Included)-->
    <NodeOverview {cluster} {ccconfig} {selectedMetric} {globalMetrics} {from} {to} {hostnameFilter} {nodeStateFilter}/>
  {:else}
    <!-- ROW2-2: Node List (Grid Included)-->
    <NodeList pendingSelectedMetrics={selectedMetrics} {cluster} {subCluster}
       {selectedResolution} {hostnameFilter} {nodeStateFilter} {from} {to} {systemUnits}/>
  {/if}
{/if}

{#if !displayNodeOverview}
  <MetricSelection
    bind:isOpen={isMetricsSelectionOpen}
    presetMetrics={selectedMetrics}
    {cluster}
    {subCluster}
    {globalMetrics}
    configName="nodeList_selectedMetrics"
    applyMetrics={(newMetrics) =>
      selectedMetrics = [...newMetrics]
    }
  />
{/if}

-            <InputGroupText><Icon name="plus-slash-minus" /></InputGroupText>

-            <InputGroupText>Resolution</InputGroupText>

-            <Input type="select" bind:value={selectedResolution}>

-              {#each resampleResolutions as res}


**4. config/admin/Options.svelte** — update the admin display, since `trigger`/`resolutions` no longer exist (only `targetPoints` is provided):


<CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>

Triggered at {resampleConfig.trigger} datapoints.

Configured resolutions: {resampleConfig.resolutions}

<CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>

Resampling is enabled.

Target data points per plot: {resampleConfig.targetPoints}


The file /Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/config/admin/Options.svelte has been updated successfully. (file state is current in your context — no need to Read it back)

<CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>

Resampling is enabled.

Target data points per plot: {resampleConfig.targetPoints}

<Col>
  <Card class="h-100">
    <CardBody>
      <CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
      <input
        type="checkbox"
        id="scramble-names-checkbox"
        style="margin-right: 1em;"
        onclick={() => handleScramble()}
        bind:checked={scrambled}
      />
      Active?
    </CardBody>
  </Card>
</Col>

{#if clusterNames?.length > 0}
  <Col>
    <Card class="h-100">
      <CardBody>
        <CardTitle class="mb-3">Public Dashboard Links</CardTitle>
        <Row>
        {#each clusterNames as cn}
          <Col>
            <Button color="info" class="mb-2 mb-xl-0" href={`/monitoring/dashboard/${cn}`} target="_blank">
              <Icon name="clipboard-pulse" class="mr-2"/>
              {cn.charAt(0).toUpperCase() + cn.slice(1)} Public Dashboard
            </Button>
          </Col>
        {/each}
        </Row>
      </CardBody>
    </Card>
  </Col>
{/if}

{#if resampleConfig}
  <Col>
    <Card class="h-100">
      <CardBody>
        <CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>

Triggered at {resampleConfig.trigger} datapoints.

Configured resolutions: {resampleConfig.resolutions}

      </CardBody>
    </Card>
  </Col>
{/if}

-
Triggered at {resampleConfig.trigger} datapoints.

-
Configured resolutions: {resampleConfig.resolutions}

+
Target data points per plot: {resampleConfig.targetPoints}


Let me verify no remaining `.resolutions`/`.trigger` references remain in the frontend source:


Bash

cd /Users/jan/prg/ClusterCockpit/cc-backend; grep -rn "resampleConfig.resolutions\|resampleConfig\.trigger\|resampleResolutions\|resampleDefault\|\.resolutions\b" web/frontend/src


web/frontend/src/generic/plots/MetricPlot.svelte:76:  const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : (resampleConfig?.targetPoints ? Math.floor(resampleConfig.targetPoints / 4) : null));
web/frontend/src/generic/plots/MetricPlot.svelte:77:  const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
web/frontend/src/generic/plots/MetricPlot.svelte:78:  const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
web/frontend/src/generic/plots/MetricPlot.svelte:520:                if (resampleTargetPoints && !resampleResolutions) {
web/frontend/src/generic/plots/MetricPlot.svelte:526:                } else if (resampleResolutions) {
web/frontend/src/generic/plots/MetricPlot.svelte:529:                  newRes = resampleResolutions.reduce(function(prev, curr) {


All remaining references use safe optional-chaining (`?.resolutions ?`) with a `targetPoints` fallback, so they won't crash. Now let me build the frontend to confirm it compiles.


cd /Users/jan/prg/ClusterCockpit/cc-backend/web/frontend && npm run build 2>&1 | tail -30


(!) Circular dependencies
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/runtime.js
node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dom/hydration.js -> node_modules/svelte/src/internal/client/dom/operations.js
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dev/equality.js -> node_modules/svelte/src/internal/client/proxy.js -> node_modules/svelte/src/internal/client/runtime.js
...and 38 more
created public/build/status.js in 3.8s

src/dashpublic.entrypoint.js → public/build/dashpublic.js...
(!) Circular dependencies
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/runtime.js
node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dom/hydration.js -> node_modules/svelte/src/internal/client/dom/operations.js
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dev/equality.js -> node_modules/svelte/src/internal/client/proxy.js -> node_modules/svelte/src/internal/client/runtime.js
...and 38 more
created public/build/dashpublic.js in 3.3s

src/config.entrypoint.js → public/build/config.js...
(!) Circular dependencies
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/runtime.js
node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dom/hydration.js -> node_modules/svelte/src/internal/client/dom/operations.js
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dev/equality.js -> node_modules/svelte/src/internal/client/proxy.js -> node_modules/svelte/src/internal/client/runtime.js
...and 38 more
created public/build/config.js in 2.5s

src/logs.entrypoint.js → public/build/logs.js...
(!) Circular dependencies
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/runtime.js
node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dom/hydration.js -> node_modules/svelte/src/internal/client/dom/operations.js
node_modules/svelte/src/internal/client/runtime.js -> node_modules/svelte/src/internal/client/reactivity/effects.js -> node_modules/svelte/src/internal/client/dom/operations.js -> node_modules/svelte/src/internal/client/dev/equality.js -> node_modules/svelte/src/internal/client/proxy.js -> node_modules/svelte/src/internal/client/runtime.js
...and 36 more
created public/build/logs.js in 1.2s


Build succeeded (only Svelte's internal circular-dependency warnings, no errors). Let me confirm `resampleConfig` is still legitimately used in `JobListRow.svelte` so I didn't leave a dead prop causing confusion:


cd /Users/jan/prg/ClusterCockpit/cc-backend; grep -n "resampleConfig" web/frontend/src/generic/joblist/JobListRow.svelte web/frontend/src/generic/JobList.svelte


web/frontend/src/generic/joblist/JobListRow.svelte:16:  - `resampleConfig [Obj]`: Includes the backend supplied resampling info
web/frontend/src/generic/joblist/JobListRow.svelte:41:    resampleConfig
web/frontend/src/generic/JobList.svelte:107:  const resampleConfig = $derived(initialized ? getContext("resampling") : null);
web/frontend/src/generic/JobList.svelte:294:            <JobListRow {triggerMetricRefresh} {job} {metrics} {plotWidth} {showFootprint} {globalMetrics} {clusterInfos} {resampleConfig}


The fix is complete and the frontend builds cleanly.

## Summary

The crash came from a **frontend/backend config-shape mismatch** introduced on this branch. The backend's `ResampleConfig` was migrated from the old array-based shape `{trigger, resolutions[]}` to the new policy-based shape `{default-policy, default-algo, targetPoints}` (`internal/config/config.go:120`, served per-user via `resamplingForUser` in `internal/routerConfig/routes.go:600`, which sets only `targetPoints`).

Several Svelte components still did `Math.max(...resampleConfig.resolutions)`. With `resolutions` now `undefined`, the spread threw `TypeError: ...resolutions is not iterable`, aborting the whole job-list render — hence no jobs displayed.

`MetricPlot.svelte` had already been migrated (it derives everything from `targetPoints` with safe optional chaining), but four components hadn't. Under the new model the backend computes the resolution from the resample policy whenever the GraphQL query sends `resolution: null`, and `MetricPlot` computes explicit resolutions from `targetPoints` on zoom — so the legacy "pick the max configured resolution" logic is obsolete.

Changes:
- **`generic/joblist/JobListRow.svelte`** — removed `resampleDefault`; `selectedResolution` now defaults to `$state(null)` (backend resolves from policy; zoom overrides).
- **`job/Metric.svelte`** — same fix.
- **`Systems.root.svelte`** — removed `resampleResolutions`/`resampleDefault`, defaulted `selectedResolution` to `null`, and removed the now-defunct manual resolution dropdown (no fixed resolution list exists anymore), adjusting the toolbar column count.
- **`config/admin/Options.svelte`** — admin info card now shows "Target data points per plot: {targetPoints}" instead of the removed `trigger`/`resolutions` fields.

All remaining `resolutions`/`trigger` references (in `MetricPlot.svelte`) are guarded with `?.` and fall back to the `targetPoints` path, so they're safe.

Note: `npm run build` writes generated bundles into `web/frontend/public/build/`. Those are part of your working tree now — include them (or rebuild via `make frontend`) when you commit.


Goal: fix the job list not rendering on this branch due to a resample config mismatch. Done: updated four Svelte components to the new policy-based shape and rebuilt the frontend successfully. Next: commit the changes including the rebuilt bundles. (disable recaps in /config)


I do not see a difference in the frontend for long jobs with spiky metric data. It seems the average resampling has no effect. I tried to change resampling by config file and from the web frontend. Can you check why there is no effect. I tried for job db id: 409


I'll investigate why average resampling has no visible effect. Let me explore the full resampling application path in parallel.


Agent

Trace archive resampling apply path

Prompt:
In the repo /Users/jan/prg/ClusterCockpit/cc-backend, I need to understand exactly how resampling is APPLIED to metric data when serving a job's metrics, especially for archived jobs.

Read and report in detail (file paths + line numbers + code snippets):

1. `internal/metricdispatch/dataLoader.go` — the full `LoadData` function. Show how it decides between fetching from metric store (running jobs) vs loading from archive. Show the exact block where it calls the resampler for archived jobs: how `resampleFn`, `resolution`, `resampleAlgo` are used, and what condition gates whether resampling happens at all. Is resampling applied ONLY for archived jobs and NOT for data coming from the metric store/running jobs? Quote the relevant `if` conditions.

2. Whether there is any guard like `if resolution > 0` or `if resolution != timestep` or a frequency/timestep comparison that would SKIP resampling. Show me the exact conditions.

3. How `LoadData` is reached from the GraphQL `JobMetrics` resolver in `internal/graph/schema.resolvers.go` (already know it computes resolution from policy). I want to know: for an ARCHIVED job, does the archive return statisticsSeries or raw series data? Does the resampling apply to `series[].data` only, or also statisticsSeries?

4. Look at the archive loading: `pkg/archive` — when a job is archived, is the metric data already downsampled/stored at a fixed resolution? Find where archived metric data is read (e.g. `LoadMetrics`, `GetMetricData`). Does the archive store data at native timestep or pre-aggregated?

5. Check `go.mod` for the cc-lib version, and try to locate the resampler source — search Go module cache for `cc-lib` resampler package (paths like `~/go/pkg/mod/github.com/!cluster!cockpit/cc-lib*/resampler/`). Report the AVERAGE algorithm implementation if found, and the `GetResampler` function and `SimpleResampler`/`Avg` functions.

Be concrete and quote code.


Trace frontend resolution send + plot render

Prompt:
In the repo /Users/jan/prg/ClusterCockpit/cc-backend, I'm debugging why resampled metric data shows no visible difference in the frontend for long jobs.

Investigate and report (file paths + line numbers + code snippets):

1. In `web/frontend/src/job/Metric.svelte` and `web/frontend/src/generic/joblist/JobListRow.svelte`: what value of `resolution` (`selectedResolution`) is sent in the `jobMetrics` GraphQL query on initial load? (It was recently changed.) Trace the variable.

2. In `web/frontend/src/generic/plots/MetricPlot.svelte`: how does the plot decide whether to render raw `series[].data` vs the `statisticsSeries` (min/mean/median/max bands)? Find the logic that picks between plotting individual series lines and plotting a statistics band. For a long job with many nodes/series, which is shown by default? Search for "statisticsSeries", "useStatsSeries", "data", "renderData", "plotSeries".

3. Does the plot render `statisticsSeries` (which is computed independently of resampling) instead of the resampled `series.data`? If the plot shows statisticsSeries by default for long/multi-series jobs, then resampling of series.data would have no visible effect. Confirm or deny this with code.

4. How is `statisticsSeries` produced for archived jobs — is it affected by the `resolution`/`resampleAlgo` parameters at all? Search backend `internal/metricdispatch` and `pkg/archive` and graphql resolvers for statisticsSeries generation.

5. Report the GraphQL schema definition of `JobMetric` / `Series` / `StatsSeries` in `api/schema.graphqls`.

Be concrete and quote code with file:line.


Perfect! Now I have a clear picture. Let me summarize my findings in a comprehensive report.

## Investigation Report: Resampled Metric Data Invisibility for Long Jobs

### 1. **Resolution (`selectedResolution`) on Initial Load**

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/job/Metric.svelte`**
- **Line 82:** `let selectedResolution = $state(null);`
- **Current behavior (after change):** On initial load, `selectedResolution` is set to `null`
- **Previous behavior (commit 0069c86e):** Was set to `let selectedResolution = $state(resampleDefault);` where `resampleDefault = resampleConfig ? Math.max(...resampleConfig.resolutions) : 0;`

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/generic/joblist/JobListRow.svelte`**
- **Line 99:** `let selectedResolution = $state(null);`
- **Same change:** Now initializes to `null` instead of a default resolution value

**Backend Resolution:**
- **File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/graph/schema.resolvers.go`** (lines 508-520)
  - When `resolution` is `null`, the backend applies a fallback policy (lines 509-520):
    1. First checks user's resample policy preference (line 510)
    2. If not set, checks global default policy (line 514)
    3. If still null, defaults to 0 (lines 517-519)
  - **Code snippet:**
    ```go
    if resolution == nil {
        resolution = resolveResolutionFromPolicy(ctx, int64(job.Duration), job.Cluster, metrics)
    }
    if resolution == nil {
        if config.Keys.EnableResampling != nil {
            resolution = resolveResolutionFromDefaultPolicy(int64(job.Duration), job.Cluster, metrics)
        }
        if resolution == nil {
            defaultRes := 0
            resolution = &defaultRes
        }
    }
    ```

---

### 2. **Plot Decision Logic: Raw Series vs StatisticsSeries**

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/generic/plots/MetricPlot.svelte`**

**Key Variable (Line 80):**
```javascript
const useStatsSeries = $derived(!!statisticsSeries); // Display Stats Series By Default if Exists
```

**Decision Logic:**

1. **Lines 86-92** - Determine which data to measure for plot dimensions:
   ```javascript
   const longestSeries = $derived.by(() => {
     if (useStatsSeries) {
       return usesMeanStatsSeries ? statisticsSeries?.mean?.length : statisticsSeries?.median?.length;
     } else {
       return series.reduce((n, series) => Math.max(n, series.data.length), 0);
     }
   });
   ```

2. **Lines 124-154** - Plot data construction (raw vs stats):
   ```javascript
   const plotData = $derived.by(() => {
     let pendingData = [new Array(longestSeries)];
     // ... timestamp construction ...
     // Y
     if (useStatsSeries) {
       pendingData.push(statisticsSeries.min);
       pendingData.push(statisticsSeries.max);
       if (usesMeanStatsSeries) {
         pendingData.push(statisticsSeries.mean);
       } else {
         pendingData.push(statisticsSeries.median);
       }
     } else {
       for (let i = 0; i < series.length; i++) {
         pendingData.push(series[i].data);  // Raw series data
       };
     };
     return pendingData;
   })
   ```

3. **Lines 155-244** - Plot series construction:
   ```javascript
   const plotSeries = $derived.by(() => {
     let pendingSeries = [{label: "Runtime", ...}];
     if (useStatsSeries) {
       // Lines 166-183: Push min/max/(mean or median) as 3 series
       pendingSeries.push({label: "min", ...});
       pendingSeries.push({label: "max", ...});
       pendingSeries.push({label: usesMeanStatsSeries ? "mean" : "median", ...});
     } else {
       // Lines 186-241: Push individual series for each node
       for (let i = 0; i < series?.length; i++) {
         pendingSeries.push({label: hostname/Acc/scope, ...});
       }
     };
     return pendingSeries;
   })
   ```

**For Long Multi-node Jobs (Default Behavior):**
- If `statisticsSeries` is present → `useStatsSeries = true` → **Shows min/mean(or median)/max bands, NOT individual series**
- Individual node lines are **completely ignored** when stats series exist

---

### 3. **Does Plot Show StatisticsSeries Instead of Resampled series.data?**

**YES - This is the root cause of invisibility.**

**Evidence:**

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go`** (lines 198-210):

```go
// Generate statistics series for jobs with many nodes to enable min/median/max graphs
// instead of overwhelming the UI with individual node lines. Note that newly calculated
// statistics use min/median/max, while archived statistics may use min/mean/max.
const maxSeriesSize int = 8
for _, scopes := range jd {
    for _, jm := range scopes {
        if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
            continue
        }

        jm.AddStatisticsSeries()
    }
}
```

**Key Problem:** For jobs with **≥8 nodes/series**, `AddStatisticsSeries()` is automatically called to create statistics bands. Once `statisticsSeries` exists, the frontend **always prefers it over individual series** (line 80: `const useStatsSeries = $derived(!!statisticsSeries);`).

When the plot renders:
- **plotData (lines 124-154):** Uses `statisticsSeries.min/max/mean/median` - NOT the resampled `series[i].data`
- **plotSeries (lines 155-244):** Renders 3 statistical lines, ignoring individual node lines

**Result:** Resampling of `series.data` in the backend is **completely invisible** because the plot never renders `series.data` when `statisticsSeries` exists.

---

### 4. **StatisticsSeries Generation for Archived Jobs**

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go`** (lines 86-227)

**For Archived Jobs (lines 130-190):**

```go
} else {
    var jdTemp schema.JobData
    jdTemp, err = archive.GetHandle().LoadJobData(job)
    if err != nil {
        return err, 0, 0
    }

    jd = deepCopy(jdTemp)

    // Resample archived data to reduce data points to the requested resolution
    resampleFn, rfErr := resampler.GetResampler(resampleAlgo)
    if rfErr != nil {
        return rfErr, 0, 0
    }
    for _, v := range jd {
        for _, v_ := range v {
            timestep := int64(0)
            for i := 0; i < len(v_.Series); i += 1 {
                v_.Series[i].Data, timestep, err = resampleFn(v_.Series[i].Data,
                    int64(v_.Timestep), int64(resolution))  // ← resolution affects only series.data
                if err != nil {
                    return err, 0, 0
                }
            }
            v_.Timestep = int(timestep)
        }
    }
    // ... filtering ...
    size = jd.Size()
}
```

**Critical Finding (lines 198-210):**

```go
// Generate statistics series for jobs with many nodes to enable min/median/max graphs
const maxSeriesSize int = 8
for _, scopes := range jd {
    for _, jm := range scopes {
        if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
            continue
        }

        jm.AddStatisticsSeries()  // ← Called AFTER resampling, but generated from already-loaded archive data
    }
}
```

**ANSWER:** `StatisticsSeries` is **NOT computed from the resolution parameter**. It is:
1. Generated from the original archive data (if `jm.Series.length >= 8`)
2. Generated **AFTER** resampling, but from pre-aggregated statistics stored in the archive
3. Independent of the `resolution` parameter entirely

The `resolution` parameter only affects `series[i].Data` resampling, which is then ignored by the frontend when `statisticsSeries` exists.

---

### 5. **GraphQL Schema Definitions**

**File: `/Users/jan/prg/ClusterCockpit/cc-backend/api/schema.graphqls`** (lines 175-194)

```graphql
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!]!
}
```

**GraphQL Query (Metric.svelte:48-49, JobListRow.svelte:47-48):**
```graphql
query ($dbid: ID!, $selectedMetrics: [String!]!, $selectedScopes: [MetricScope!]!, $selectedResolution: Int) {
  singleUpdate: jobMetrics(id: $dbid, metrics: $selectedMetrics, scopes: $selectedScopes, resolution: $selectedResolution) {
    ...
```

---

## Summary

The resampled metric data shows **no visible difference** for long jobs because:

1. **Recent Change:** `selectedResolution` now initializes to `null` instead of a default value (commit 0069c86e: policy-based resampling)
2. **Backend:** Resampling of `series[i].data` is applied when `resolution != 0` or a policy resolves to a non-zero value
3. **Frontend Display Decision:** Line 80 of MetricPlot.svelte: `const useStatsSeries = !!statisticsSeries`
4. **Critical Issue:** Once `statisticsSeries` exists (auto-generated for jobs with ≥8 nodes), the plot **always renders** min/mean/median/max bands and **never renders** individual `series[i].data`
5. **Root Cause:** `StatisticsSeries` is **not resampled**—it's pre-computed from archive statistics, independent of the `resolution` parameter
6. **Result:** Even if `series.data` is perfectly resampled, **it's never displayed** because the plot defaults to showing statistics bands

Prompt:
In the repo /Users/jan/prg/ClusterCockpit/cc-backend, I'm debugging why resampled metric data shows no visible difference in the frontend for long jobs.

Investigate and report (file paths + line numbers + code snippets):

1. In `web/frontend/src/job/Metric.svelte` and `web/frontend/src/generic/joblist/JobListRow.svelte`: what value of `resolution` (`selectedResolution`) is sent in the `jobMetrics` GraphQL query on initial load? (It was recently changed.) Trace the variable.

2. In `web/frontend/src/generic/plots/MetricPlot.svelte`: how does the plot decide whether to render raw `series[].data` vs the `statisticsSeries` (min/mean/median/max bands)? Find the logic that picks between plotting individual series lines and plotting a statistics band. For a long job with many nodes/series, which is shown by default? Search for "statisticsSeries", "useStatsSeries", "data", "renderData", "plotSeries".

3. Does the plot render `statisticsSeries` (which is computed independently of resampling) instead of the resampled `series.data`? If the plot shows statisticsSeries by default for long/multi-series jobs, then resampling of series.data would have no visible effect. Confirm or deny this with code.

4. How is `statisticsSeries` produced for archived jobs — is it affected by the `resolution`/`resampleAlgo` parameters at all? Search backend `internal/metricdispatch` and `pkg/archive` and graphql resolvers for statisticsSeries generation.

5. Report the GraphQL schema definition of `JobMetric` / `Series` / `StatsSeries` in `api/schema.graphqls`.

Be concrete and quote code with file:line.


I do not see a difference in the frontend for long jobs with spiky metric data. It seems the average resampling has no effect. I tried to change resampling by config file and from the web frontend. Ca…


Perfect! Now I have all the information I need. Let me create a comprehensive report:

## Detailed Analysis: Resampling Application in cc-backend

Based on my thorough investigation of the codebase, here's exactly how resampling is applied to metric data when serving job metrics, especially for archived jobs:

### 1. **Full LoadData Function Flow** (`internal/metricdispatch/dataLoader.go:86-236`)

The `LoadData` function **ONLY applies resampling to ARCHIVED jobs**, not running jobs. Here's the exact logic:

**Lines 97-129: Running Jobs Path**
```go
if job.State == schema.JobStateRunning ||
    job.MonitoringStatus == schema.MonitoringStatusRunningOrArchiving {
    // ... loads from metric store
    jd, err = ms.LoadData(job, metrics, scopes, ctx, resolution)
    // NO RESAMPLING HERE - resolution passed through to metric store handler
}
```

**Lines 130-158: Archived Jobs Path (RESAMPLING APPLIED HERE)**
```go
} else {
    var jdTemp schema.JobData
    jdTemp, err = archive.GetHandle().LoadJobData(job)  // Load from archive

    jd = deepCopy(jdTemp)  // Deep copy to avoid cache corruption

    // ===== RESAMPLING BLOCK =====
    // Lines 141-158: ALWAYS applied to archived data
    resampleFn, rfErr := resampler.GetResampler(resampleAlgo)  // Line 143
    if rfErr != nil {
        return rfErr, 0, 0
    }
    for _, v := range jd {
        for _, v_ := range v {
            timestep := int64(0)
            for i := 0; i < len(v_.Series); i += 1 {
                // Lines 151: Applied to Series[].Data ONLY
                v_.Series[i].Data, timestep, err = resampleFn(
                    v_.Series[i].Data,           // source data from each series
                    int64(v_.Timestep),          // old frequency (seconds)
                    int64(resolution))           // new target frequency (seconds)
                if err != nil {
                    return err, 0, 0
                }
            }
            v_.Timestep = int(timestep)  // Update timestep to new resolution
        }
    }
}
```

**Key Observations:**
- **Gate condition**: Resampling happens ONLY when `job.State != schema.JobStateRunning` AND `job.MonitoringStatus != schema.MonitoringStatusRunningOrArchiving`
- **What is resampled**: ONLY `v_.Series[i].Data` (individual time-series data), NOT `StatisticsSeries`
- **When it's applied**: ALWAYS for archived jobs, with NO guards on resolution value

### 2. **Guard Conditions and Frequency Checks**

There is **NO explicit guard** like `if resolution > 0` or `if resolution != timestep` in the LoadData function. However, the guard is **IMPLICIT in the resampler itself**.

Looking at `/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-lib/v2@v2.12.0/resampler/resampler.go:127-147`:

```go
func validateFrequency(lenData int, oldFrequency, newFrequency int64) (int, int, error) {
    // Line 129: Returns -1 (no resampling) if newFrequency <= oldFrequency
    if oldFrequency == 0 || newFrequency == 0 || newFrequency <= oldFrequency {
        return -1, 0, nil
    }

    // ... more validation ...

    // Lines 142-143: Returns -1 (no resampling) if:
    // 1. newDataLength == 0
    // 2. lenData < MinimumRequiredPoints (default 1000)
    // 3. newDataLength >= lenData
    if (newDataLength == 0) || (lenData < MinimumRequiredPoints) || (newDataLength >= lenData) {
        return -1, 0, nil
    }
}
```

When `validateFrequency` returns `-1`, each resampler function (LTTB, Simple, Average) **returns the original data unchanged** (lines 201-202 in LTTB, lines 115-116 in SimpleResampler, lines 320-321 in AverageResampler):

```go
if newDataLength == -1 {
    return data, oldFrequency, nil  // NO resampling performed
}
```

**So the implicit guards are:**
1. **If resolution ≤ v_.Timestep**: No resampling (newFrequency ≤ oldFrequency)
2. **If series has < 1000 points**: No resampling (MinimumRequiredPoints default)
3. **If resulting downsampled size >= original size**: No resampling

### 3. **GraphQL to LoadData Flow** (`internal/graph/schema.resolvers.go:501-542`)

The `JobMetrics` resolver computes `resolution` BEFORE calling LoadData:

```go
func (r *queryResolver) JobMetrics(ctx context.Context, id string, metrics []string,
    scopes []schema.MetricScope, resolution *int, resampleAlgo *model.ResampleAlgo) {

    // Lines 509-520: Compute resolution from policy
    if resolution == nil {
        resolution = resolveResolutionFromPolicy(ctx, int64(job.Duration), job.Cluster, metrics)
    }
    if resolution == nil {
        if config.Keys.EnableResampling != nil {
            resolution = resolveResolutionFromDefaultPolicy(int64(job.Duration), job.Cluster, metrics)
        }
        if resolution == nil {
            defaultRes := 0
            resolution = &defaultRes  // Falls back to 0
        }
    }

    algoName := resolveResampleAlgo(ctx, resampleAlgo)  // Line 522

    // Line 524: Calls LoadData with computed resolution
    data, err := metricdispatch.LoadData(job, metrics, scopes, ctx, *resolution, algoName)
}
```

### 4. **Archive Data Format and Resampling Target**

**From** `pkg/archive/fsBackend.go:448-458`:
```go
func (fsa *FsArchive) LoadJobData(job *schema.Job) (schema.JobData, error) {
    filename := getPath(job, fsa.path, "data.json.gz")  // or data.json
    return loadJobData(filename, isCompressed)
}
```

The archive stores **native/original time-series data** (not pre-aggregated). The data structure returned is:
- **schema.JobData**: `map[metricName]map[scope]*schema.JobMetric`
- **schema.JobMetric** contains:
  - `Series []Series` - array of individual time-series (one per node)
  - `StatisticsSeries *StatsSeries` - optional aggregated stats (min/mean/median/max over time)
  - `Timestep int` - sampling interval in seconds

**Resampling applies ONLY to:**
- `Series[].Data` (the raw time-series points) — lines 151 in dataLoader.go
- **NOT to StatisticsSeries** (aggregated data is left unchanged)

### 5. **Resampler Source and AVERAGE Implementation**

**Version: cc-lib v2.12.0** (from go.mod line 12)

**File**: `/Users/jan/go/pkg/mod/github.com/!cluster!cockpit/cc-lib/v2@v2.12.0/resampler/resampler.go`

**GetResampler function (lines 284-297):**
```go
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)
    }
}
```

**AVERAGE Resampler Implementation (lines 315-344):**
```go
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  // No resampling
    }

    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  // All values in bucket were NaN
        } else {
            newData[i] = schema.Float(sum / float64(count))  // Arithmetic mean
        }
    }

    return newData, newFrequency, nil
}
```

**Average algorithm**: Divides data into fixed-size buckets based on step size and computes arithmetic mean of valid (non-NaN) points in each bucket. If all points in a bucket are NaN, result is NaN.

### 6. **Resolution Computation Policy** (`internal/metricdispatch/resamplepolicy.go`)

The resolution is computed from user policy **BEFORE** passing to LoadData:

```go
func ComputeResolution(duration int64, frequency int64, targetPoints int) int {
    if frequency <= 0 || targetPoints <= 0 || duration <= 0 {
        return 0  // Returns 0 if can't compute
    }

    totalPoints := duration / frequency
    if totalPoints <= int64(targetPoints) {
        return 0  // No resampling needed if already below target
    }

    targetRes := math.Ceil(float64(duration) / float64(targetPoints))
    // Round up to nearest multiple of frequency
    resolution := int(math.Ceil(targetRes/float64(frequency))) * int(frequency)

    return resolution
}
```

Policies: Low=200 points, Medium=500 points, High=1000 points.

---

### Summary Table

| Aspect | Details |
|--------|---------|
| **When Applied** | ONLY for archived jobs (job.State != Running) |
| **What's Resampled** | Series[].Data only (individual time-series) |
| **What's NOT Resampled** | StatisticsSeries, Timestep is recalculated |
| **Guard: No explicit if** | Yes - guard is implicit in resampler functions |
| **Guard: resolution > 0** | Not in LoadData; implicit in validateFrequency (checks newFrequency ≤ oldFrequency) |
| **Running Jobs** | Resolution param passed to metric store, NOT resampled in LoadData |
| **AVERAGE Algorithm** | Bucketing + arithmetic mean of valid (non-NaN) values per bucket |
| **Algorithms Available** | LTTB (default), Average, Simple |

Prompt:
In the repo /Users/jan/prg/ClusterCockpit/cc-backend, I need to understand exactly how resampling is APPLIED to metric data when serving a job's metrics, especially for archived jobs.

Read and report in detail (file paths + line numbers + code snippets):

1. `internal/metricdispatch/dataLoader.go` — the full `LoadData` function. Show how it decides between fetching from metric store (running jobs) vs loading from archive. Show the exact block where it calls the resampler for archived jobs: how `resampleFn`, `resolution`, `resampleAlgo` are used, and what condition gates whether resampling happens at all. Is resampling applied ONLY for archived jobs and NOT for data coming from the metric store/running jobs? Quote the relevant `if` conditions.

2. Whether there is any guard like `if resolution > 0` or `if resolution != timestep` or a frequency/timestep comparison that would SKIP resampling. Show me the exact conditions.

3. How `LoadData` is reached from the GraphQL `JobMetrics` resolver in `internal/graph/schema.resolvers.go` (already know it computes resolution from policy). I want to know: for an ARCHIVED job, does the archive return statisticsSeries or raw series data? Does the resampling apply to `series[].data` only, or also statisticsSeries?

4. Look at the archive loading: `pkg/archive` — when a job is archived, is the metric data already downsampled/stored at a fixed resolution? Find where archived metric data is read (e.g. `LoadMetrics`, `GetMetricData`). Does the archive store data at native timestep or pre-aggregated?

5. Check `go.mod` for the cc-lib version, and try to locate the resampler source — search Go module cache for `cc-lib` resampler package (paths like `~/go/pkg/mod/github.com/!cluster!cockpit/cc-lib*/resampler/`). Report the AVERAGE algorithm implementation if found, and the `GetResampler` function and `SimpleResampler`/`Avg` functions.

Be concrete and quote code.


Both agents converged on a strong hypothesis: the plot renders `statisticsSeries` (min/mean/max bands) rather than the resampled `series.data`, and statisticsSeries handling may bypass resampling. Let me verify the critical details directly — the exact ordering in `dataLoader.go`, the user's actual config, and job 409's archived data.


86	func LoadData(job *schema.Job,
87		metrics []string,
88		scopes []schema.MetricScope,
89		ctx context.Context,
90		resolution int,
91		resampleAlgo string,
92	) (schema.JobData, error) {
93		data := cache.Get(cacheKey(job, metrics, scopes, resolution, resampleAlgo), func() (_ any, ttl time.Duration, size int) {
94			var jd schema.JobData
95			var err error
96
97			if job.State == schema.JobStateRunning ||
98				job.MonitoringStatus == schema.MonitoringStatusRunningOrArchiving {
99
100				ms, err := GetMetricDataRepo(job.Cluster, job.SubCluster)
101				if err != nil {
102					cclog.Errorf("failed to access metricDataRepo for cluster %s-%s: %s",
103						job.Cluster, job.SubCluster, err.Error())
104					return err, 0, 0
105				}
106
107				if scopes == nil {
108					scopes = append(scopes, schema.MetricScopeNode)
109				}
110
111				if metrics == nil {
112					cluster := archive.GetCluster(job.Cluster)
113					for _, mc := range cluster.MetricConfig {
114						metrics = append(metrics, mc.Name)
115					}
116				}
117
118				jd, err = ms.LoadData(job, metrics, scopes, ctx, resolution)
119				if err != nil {
120					if len(jd) != 0 {
121						cclog.Warnf("partial error loading metrics from store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
122							job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
123					} else {
124						cclog.Warnf("failed to load job data from metric store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
125							job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
126						return err, 0, 0
127					}
128				}
129				size = jd.Size()
130			} else {
131				var jdTemp schema.JobData
132				jdTemp, err = archive.GetHandle().LoadJobData(job)
133				if err != nil {
134					cclog.Warnf("failed to load job data from archive for job %d (user: %s, project: %s, cluster: %s-%s): %s",
135						job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
136					return err, 0, 0
137				}
138
139				jd = deepCopy(jdTemp)
140
141				// Resample archived data to reduce data points to the requested resolution,
142				// improving transfer performance and client-side rendering.
143				resampleFn, rfErr := resampler.GetResampler(resampleAlgo)
144				if rfErr != nil {
145					return rfErr, 0, 0
146				}
147				for _, v := range jd {
148					for _, v_ := range v {
149						timestep := int64(0)
150						for i := 0; i < len(v_.Series); i += 1 {
151							v_.Series[i].Data, timestep, err = resampleFn(v_.Series[i].Data, int64(v_.Timestep), int64(resolution))
152							if err != nil {
153								return err, 0, 0
154							}
155						}
156						v_.Timestep = int(timestep)
157					}
158				}
159
160				// Filter job data to only include requested metrics and scopes, avoiding unnecessary data transfer.
161				if metrics != nil || scopes != nil {
162					if metrics == nil {
163						metrics = make([]string, 0, len(jd))
164						for k := range jd {
165							metrics = append(metrics, k)
166						}
167					}
168
169					res := schema.JobData{}
170					for _, metric := range metrics {
171						if perscope, ok := jd[metric]; ok {
172							if len(perscope) > 1 {
173								subset := make(map[schema.MetricScope]*schema.JobMetric)
174								for _, scope := range scopes {
175									if jm, ok := perscope[scope]; ok {
176										subset[scope] = jm
177									}
178								}
179
180								if len(subset) > 0 {
181									perscope = subset
182								}
183							}
184
185							res[metric] = perscope
186						}
187					}
188					jd = res
189				}
190				size = jd.Size()
191			}
192
193			ttl = 5 * time.Hour
194			if job.State == schema.JobStateRunning {
195				ttl = 2 * time.Minute
196			}
197
198			// Generate statistics series for jobs with many nodes to enable min/median/max graphs
199			// instead of overwhelming the UI with individual node lines. Note that newly calculated
200			// statistics use min/median/max, while archived statistics may use min/mean/max.
201			const maxSeriesSize int = 8
202			for _, scopes := range jd {
203				for _, jm := range scopes {
204					if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
205						continue
206					}
207
208					jm.AddStatisticsSeries()
209				}
210			}
211
212			nodeScopeRequested := false
213			for _, scope := range scopes {
214				if scope == schema.MetricScopeNode {
215					nodeScopeRequested = true

func LoadData(job *schema.Job,
	metrics []string,
	scopes []schema.MetricScope,
	ctx context.Context,
	resolution int,
	resampleAlgo string,
) (schema.JobData, error) {
	data := cache.Get(cacheKey(job, metrics, scopes, resolution, resampleAlgo), func() (_ any, ttl time.Duration, size int) {
		var jd schema.JobData
		var err error

		if job.State == schema.JobStateRunning ||
			job.MonitoringStatus == schema.MonitoringStatusRunningOrArchiving {

			ms, err := GetMetricDataRepo(job.Cluster, job.SubCluster)
			if err != nil {
				cclog.Errorf("failed to access metricDataRepo for cluster %s-%s: %s",
					job.Cluster, job.SubCluster, err.Error())
				return err, 0, 0
			}

			if scopes == nil {
				scopes = append(scopes, schema.MetricScopeNode)
			}

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

			jd, err = ms.LoadData(job, metrics, scopes, ctx, resolution)
			if err != nil {
				if len(jd) != 0 {
					cclog.Warnf("partial error loading metrics from store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
						job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
				} else {
					cclog.Warnf("failed to load job data from metric store for job %d (user: %s, project: %s, cluster: %s-%s): %s",
						job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
					return err, 0, 0
				}
			}
			size = jd.Size()
		} else {
			var jdTemp schema.JobData
			jdTemp, err = archive.GetHandle().LoadJobData(job)
			if err != nil {
				cclog.Warnf("failed to load job data from archive for job %d (user: %s, project: %s, cluster: %s-%s): %s",
					job.JobID, job.User, job.Project, job.Cluster, job.SubCluster, err.Error())
				return err, 0, 0
			}

			jd = deepCopy(jdTemp)

			// Resample archived data to reduce data points to the requested resolution,
			// improving transfer performance and client-side rendering.
			resampleFn, rfErr := resampler.GetResampler(resampleAlgo)
			if rfErr != nil {
				return rfErr, 0, 0
			}
			for _, v := range jd {
				for _, v_ := range v {
					timestep := int64(0)
					for i := 0; i < len(v_.Series); i += 1 {
						v_.Series[i].Data, timestep, err = resampleFn(v_.Series[i].Data, int64(v_.Timestep), int64(resolution))
						if err != nil {
							return err, 0, 0
						}
					}
					v_.Timestep = int(timestep)
				}
			}

			// Filter job data to only include requested metrics and scopes, avoiding unnecessary data transfer.
			if metrics != nil || scopes != nil {
				if metrics == nil {
					metrics = make([]string, 0, len(jd))
					for k := range jd {
						metrics = append(metrics, k)
					}
				}

				res := schema.JobData{}
				for _, metric := range metrics {
					if perscope, ok := jd[metric]; ok {
						if len(perscope) > 1 {
							subset := make(map[schema.MetricScope]*schema.JobMetric)
							for _, scope := range scopes {
								if jm, ok := perscope[scope]; ok {
									subset[scope] = jm
								}
							}

							if len(subset) > 0 {
								perscope = subset
							}
						}

						res[metric] = perscope
					}
				}
				jd = res
			}
			size = jd.Size()
		}

		ttl = 5 * time.Hour
		if job.State == schema.JobStateRunning {
			ttl = 2 * time.Minute
		}

		// Generate statistics series for jobs with many nodes to enable min/median/max graphs
		// instead of overwhelming the UI with individual node lines. Note that newly calculated
		// statistics use min/median/max, while archived statistics may use min/mean/max.
		const maxSeriesSize int = 8
		for _, scopes := range jd {
			for _, jm := range scopes {
				if jm.StatisticsSeries != nil || len(jm.Series) < maxSeriesSize {
					continue
				}

				jm.AddStatisticsSeries()
			}
		}

		nodeScopeRequested := false
		for _, scope := range scopes {
			if scope == schema.MetricScopeNode {
				nodeScopeRequested = true


1	{
2	  "main": {
3	    "addr": "127.0.0.1:8080"
4	  },
5	  "cron": {
6	    "commit-job-worker": "1m",
7	    "duration-worker": "3m",
8	    "footprint-worker": "5m"
9	  },
10	  "auth": {
11	    "jwts": {
12	      "max-age": "2000h"
13	    }
14	  },
15	  "metric-store-external": [
16	    {
17	      "scope": "fritz",
18	      "url": "http://0.0.0.0:8082",
19	      "token": "REDACTED"
20	    }
21	  ],
22	  "resampling": {
23	    "default-policy": "low",
24	    "default-algo": "average"
25	  },
26	  "metric-store": {
27	    "retention-in-memory": "24h",
28	    "memory-cap": 100
29	  }
30	}
31


The config confirms resampling is enabled (`policy: low` = 200 points, `algo: average`). Now let me determine job 409's actual path — whether it's archived, how many nodes, and crucially whether its archived data already contains a `statisticsSeries`. Let me query the DB and inspect the archive.


Bash

cd /Users/jan/prg/ClusterCockpit/cc-backend; ls var/ 2>/dev/null; echo "---DB QUERY---"; sqlite3 var/job.db "SELECT id, job_id, cluster, subcluster, start_time, num_nodes, job_state, monitoring_status, duration FROM job WHERE id=409;" 2>&1 | head


checkpoints
job-archive
job.db
job.db-shm
job.db-wal
---DB QUERY---
409|398864|fritz|main|1675955241|1|completed|3|56182


Job 409: jobID=398864, cluster=fritz, **numNodes=1**, completed, monitoring_status=3 (archived), duration ~15.6h. A single fritz node has many cores, so at **core scope** there will be ≥8 series → `AddStatisticsSeries` territory. The decisive question is whether the archive **already stores** a `statisticsSeries` (so `AddStatisticsSeries` is skipped and full-resolution stats are shown). Let me inspect the archive file.


cd /Users/jan/prg/ClusterCockpit/cc-backend; D=job-archive/fritz/398/864/1675955241; ls -la "$D" 2>&1; echo "=== meta scopes/keys ==="; for f in "$D"/data.json "$D"/data.json.gz; do [ -e "$f" ] && echo "FOUND: $f"; done


Error: Exit code 1
"job-archive/fritz/398/864/1675955241": No such file or directory (os error 2)
=== meta scopes/keys ===


cd /Users/jan/prg/ClusterCockpit/cc-backend; find job-archive/fritz -path '*398864*' -prune -o -name '*.json*' -print 2>/dev/null | head; echo "=== search by startTime ==="; find job-archive -type d -name '1675955241' 2>/dev/null | head; echo "=== fritz top layout ==="; ls job-archive/fritz | head


=== search by startTime ===
=== fritz top layout ===
"job-archive/fritz": No such file or directory (os error 2)


cd /Users/jan/prg/ClusterCockpit/cc-backend; echo "=== job-archive top ==="; ls job-archive 2>&1 | head; echo "=== find 1675955241 anywhere ==="; find . -type d -name '1675955241' 2>/dev/null | head; echo "=== archive config ==="; grep -rn "job-archive\|\"archive\"\|path" config.json configs/*.json 2>/dev/null | grep -i archive | head


=== job-archive top ===
"job-archive": No such file or directory (os error 2)
=== find 1675955241 anywhere ===
./var/job-archive/fritz/398/864/1675955241
=== archive config ===
configs/config.json:14:      "target-path": "./var/nodestate-archive"
configs/config.json:46:  "archive": {
configs/config.json:87:      "mode": "archive",


The archive is under `var/`. Let me inspect job 409's archived data for a stored `statisticsSeries`, scopes, and series counts.


cd /Users/jan/prg/ClusterCockpit/cc-backend; D=var/job-archive/fritz/398/864/1675955241; ls -la "$D"; echo "=== which data file ==="; ls "$D"/data.json* 2>&1


.rw-r--r--@ 502k jan  9 Sep  2025 data.json.gz
.rw-r--r--@ 2.6k jan  9 Sep  2025 meta.json
=== which data file ===
var/job-archive/fritz/398/864/1675955241/data.json.gz


cd /Users/jan/prg/ClusterCockpit/cc-backend; D=var/job-archive/fritz/398/864/1675955241; echo "=== top-level metrics ==="; gzcat "$D"/data.json.gz | python3 -c '
import json,sys
d=json.load(sys.stdin)
for metric,scopes in d.items():
    for scope,jm in scopes.items():
        ns=len(jm.get("series",[]) or [])
        has_stats = jm.get("statisticsSeries") is not None
        ts=jm.get("timestep")
        dlen=len(jm["series"][0]["data"]) if ns else 0
        print(f"{metric:20s} scope={scope:12s} series={ns:3d} timestep={ts:4} dataLen={dlen:5d} statisticsSeries={has_stats}")
' 2>&1 | head -60


=== top-level metrics ===
clock                scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
clock                scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
cpu_load             scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
cpu_power            scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
cpu_power            scope=socket       series=  2 timestep=  60 dataLen=  937 statisticsSeries=False
cpu_user             scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
cpu_user             scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
flops_any            scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
flops_any            scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
flops_dp             scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
flops_dp             scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
flops_sp             scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
flops_sp             scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
ib_recv              scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
ib_recv_pkts         scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
ib_xmit              scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
ib_xmit_pkts         scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
ipc                  scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
ipc                  scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
mem_bw               scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
mem_bw               scope=socket       series=  2 timestep=  60 dataLen=  937 statisticsSeries=False
mem_power            scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
mem_power            scope=socket       series=  2 timestep=  60 dataLen=  937 statisticsSeries=False
mem_used             scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
nfs4_read            scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
nfs4_total           scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
nfs4_write           scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False
vectorization_ratio  scope=core         series= 72 timestep=  60 dataLen=  937 statisticsSeries=False
vectorization_ratio  scope=node         series=  1 timestep=  60 dataLen=  937 statisticsSeries=False


Key finding: job 409's archive has **no stored `statisticsSeries`** (all `False`), data is 937 points at 60s timestep, with 72-series core-scope metrics. So `AddStatisticsSeries` runs *after* resampling — meaning resampling *should* be visible. Since the user sees no effect at all, resampling likely isn't being applied. Let me verify two prime suspects: (1) whether the `resampling` config is even parsed (nesting level / `EnableResampling` nil), and (2) the cc-lib `validateFrequency`/`MinimumRequiredPoints` gate.


1	// Copyright (C) NHR@FAU, University Erlangen-Nuremberg.
2	// All rights reserved. This file is part of cc-backend.
3	// Use of this source code is governed by a MIT-style
4	// license that can be found in the LICENSE file.
5
6	// Package config implements the program configuration data structures, validation and parsing
7	package config
8
9	import (
10		"bytes"
11		"encoding/json"
12		"time"
13
14		cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
15		"github.com/ClusterCockpit/cc-lib/v2/resampler"
16	)
17
18	type ProgramConfig struct {
19		// Address where the http (or https) server will listen on (for example: 'localhost:80').
20		Addr string `json:"addr"`
21
22		// Addresses from which secured admin API endpoints can be reached, can be wildcard "*"
23		APIAllowedIPs []string `json:"api-allowed-ips"`
24
25		APISubjects *NATSConfig `json:"api-subjects"`
26
27		// Drop root permissions once the config was read and the port was taken.
28		User  string `json:"user"`
29		Group string `json:"group"`
30
31		// Disable authentication (for everything: API, Web-UI, ...)
32		DisableAuthentication bool `json:"disable-authentication"`
33
34		// If `embed-static-files` is true (default), the frontend files are directly
35		// embeded into the go binary and expected to be in web/frontend. Only if
36		// it is false the files in `static-files` are served instead.
37		EmbedStaticFiles bool   `json:"embed-static-files"`
38		StaticFiles      string `json:"static-files"`
39
40		// Path to SQLite database file
41		DB string `json:"db"`
42
43		EnableJobTaggers bool `json:"enable-job-taggers"`
44
45		// Validate json input against schema
46		Validate bool `json:"validate"`
47
48		// If 0 or empty, the session does not expire!
49		SessionMaxAge string `json:"session-max-age"`
50
51		// If both those options are not empty, use HTTPS using those certificates.
52		HTTPSCertFile string `json:"https-cert-file"`
53		HTTPSKeyFile  string `json:"https-key-file"`
54
55		// If not the empty string and `addr` does not end in ":80",
56		// redirect every request incoming at port 80 to that url.
57		RedirectHTTPTo string `json:"redirect-http-to"`
58
59		// Where to store MachineState files
60		MachineStateDir string `json:"machine-state-dir"`
61
62		// If not zero, automatically mark jobs as stopped running X seconds longer than their walltime.
63		StopJobsExceedingWalltime int `json:"stop-jobs-exceeding-walltime"`
64
65		// Defines time X in seconds in which jobs are considered to be "short" and will be filtered in specific views.
66		ShortRunningJobsDuration int `json:"short-running-jobs-duration"`
67
68		// Energy Mix CO2 Emission Constant [g/kWh]
69		// If entered, displays estimated CO2 emission for job based on jobs totalEnergy
70		EmissionConstant int `json:"emission-constant"`
71
72		// If exists, will enable dynamic zoom in frontend metric plots using the configured values
73		EnableResampling *ResampleConfig `json:"resampling"`
74
75		// Systemd unit name for log viewer (default: "clustercockpit")
76		SystemdUnit string `json:"systemd-unit"`
77
78		// Node state retention configuration
79		NodeStateRetention *NodeStateRetention `json:"nodestate-retention"`
80

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

// Package config implements the program configuration data structures, validation and parsing
package config

import (
	"bytes"
	"encoding/json"
	"time"

	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
	"github.com/ClusterCockpit/cc-lib/v2/resampler"
)

type ProgramConfig struct {
	// Address where the http (or https) server will listen on (for example: 'localhost:80').
	Addr string `json:"addr"`

	// Addresses from which secured admin API endpoints can be reached, can be wildcard "*"
	APIAllowedIPs []string `json:"api-allowed-ips"`

	APISubjects *NATSConfig `json:"api-subjects"`

	// Drop root permissions once the config was read and the port was taken.
	User  string `json:"user"`
	Group string `json:"group"`

	// Disable authentication (for everything: API, Web-UI, ...)
	DisableAuthentication bool `json:"disable-authentication"`

	// If `embed-static-files` is true (default), the frontend files are directly
	// embeded into the go binary and expected to be in web/frontend. Only if
	// it is false the files in `static-files` are served instead.
	EmbedStaticFiles bool   `json:"embed-static-files"`
	StaticFiles      string `json:"static-files"`

	// Path to SQLite database file
	DB string `json:"db"`

	EnableJobTaggers bool `json:"enable-job-taggers"`

	// Validate json input against schema
	Validate bool `json:"validate"`

	// If 0 or empty, the session does not expire!
	SessionMaxAge string `json:"session-max-age"`

	// If both those options are not empty, use HTTPS using those certificates.
	HTTPSCertFile string `json:"https-cert-file"`
	HTTPSKeyFile  string `json:"https-key-file"`

	// If not the empty string and `addr` does not end in ":80",
	// redirect every request incoming at port 80 to that url.
	RedirectHTTPTo string `json:"redirect-http-to"`

	// Where to store MachineState files
	MachineStateDir string `json:"machine-state-dir"`

	// If not zero, automatically mark jobs as stopped running X seconds longer than their walltime.
	StopJobsExceedingWalltime int `json:"stop-jobs-exceeding-walltime"`

	// Defines time X in seconds in which jobs are considered to be "short" and will be filtered in specific views.
	ShortRunningJobsDuration int `json:"short-running-jobs-duration"`

	// Energy Mix CO2 Emission Constant [g/kWh]
	// If entered, displays estimated CO2 emission for job based on jobs totalEnergy
	EmissionConstant int `json:"emission-constant"`

	// If exists, will enable dynamic zoom in frontend metric plots using the configured values
	EnableResampling *ResampleConfig `json:"resampling"`

	// Systemd unit name for log viewer (default: "clustercockpit")
	SystemdUnit string `json:"systemd-unit"`

	// Node state retention configuration
	NodeStateRetention *NodeStateRetention `json:"nodestate-retention"`


cd /Users/jan/prg/ClusterCockpit/cc-backend; echo "=== how config.Init is called ==="; grep -rn "config.Init\|InitConfig\|EnableResampling\|MarshalJSON\|configSchema\|RawMessage" cmd/cc-backend/*.go internal/config/config.go | head -30


=== how config.Init is called ===
cmd/cc-backend/init.go:75:	if err := archive.Init(json.RawMessage(archiveCfg)); err != nil {
internal/config/config.go:73:	EnableResampling *ResampleConfig `json:"resampling"`
internal/config/config.go:166:func Init(mainConfig json.RawMessage) {
internal/config/config.go:167:	Validate(configSchema, mainConfig)
internal/config/config.go:174:	if Keys.EnableResampling != nil {
internal/config/config.go:175:		policy := Keys.EnableResampling.DefaultPolicy
cmd/cc-backend/main.go:99:	config.Init(cfg)
cmd/cc-backend/main.go:305:		archiveCfg = json.RawMessage(defaultArchiveConfig)


`EnableResampling` lives in `ProgramConfig` (json `"resampling"`), and `config.Init` parses the **"main"** section (since `addr` maps to `main.addr`). But the user's config.json has `"resampling"` at the **top level**, as a sibling of `"main"` — not inside it. Let me confirm how the config file is split into sections.