Add PlotRenderOptions also to AdminsSettings


     1→<!--
     2→  @component User settings wrapper
     3→
     4→  Properties:
     5→  - `username String!`: Empty string if auth. is disabled, otherwise the username as string
     6→  - `isApi Bool!`: Is currently logged in user api authority
     7→-->
     8→
     9→<script>
    10→  import { getContext } from "svelte";
    11→  import UserOptions from "./user/UserOptions.svelte";
    12→  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
    13→  import PlotColorScheme from "./user/PlotColorScheme.svelte";
    14→
    15→  /* Svelte 5 Props */
    16→  let {
    17→    username,
    18→    isApi
    19→  } = $props();
    20→
    21→  /* Const Init */
    22→  const ccconfig = getContext("cc-config");
    23→
    24→  /* State Init */
    25→  let message = $state({ msg: "", target: "", color: "#d63384" });
    26→  let displayMessage = $state(false);
    27→  let cbmode = $state(ccconfig?.plotConfiguration_colorblindMode || false);
    28→
    29→  /* Functions */
    30→  async function handleSettingSubmit(event, setting) {
    31→    event.preventDefault();
    32→
    33→    const selector = setting.selector
    34→    const target = setting.target
    35→    let form = document.querySelector(selector);
    36→    let formData = new FormData(form);
    37→    try {
    38→      const res = await fetch(form.action, { method: "POST", body: formData });
    39→      if (res.ok) {
    40→        let text = await res.text();
    41→        if (formData.get("key") === "plotConfiguration_colorblindMode") {
    42→          cbmode = JSON.parse(formData.get("value"));
    43→        }
    44→        popMessage(text, target, "#048109");
    45→      } else {
    46→        let text = await res.text();
    47→        throw new Error("Response Code " + res.status + "-> " + text);
    48→      }
    49→    } catch (err) {
    50→      popMessage(err, target, "#d63384");
    51→    }
    52→
    53→    return false;
    54→  }
    55→
    56→  function popMessage(response, restarget, rescolor) {
    57→    message = { msg: response, target: restarget, color: rescolor };
    58→    displayMessage = true;
    59→    setTimeout(function () {
    60→      displayMessage = false;
    61→    }, 3500);
    62→  }
    63→</script>
    64→
    65→<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
    66→<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
    67→<PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
    68→

<!--
  @component User settings wrapper

  Properties:
  - `username String!`: Empty string if auth. is disabled, otherwise the username as string
  - `isApi Bool!`: Is currently logged in user api authority
-->

<script>
  import { getContext } from "svelte";
  import UserOptions from "./user/UserOptions.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
  import PlotColorScheme from "./user/PlotColorScheme.svelte";

  /* Svelte 5 Props */
  let {
    username,
    isApi
  } = $props();

  /* Const Init */
  const ccconfig = getContext("cc-config");

  /* State Init */
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);
  let cbmode = $state(ccconfig?.plotConfiguration_colorblindMode || false);

  /* Functions */
  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        if (formData.get("key") === "plotConfiguration_colorblindMode") {
          cbmode = JSON.parse(formData.get("value"));
        }
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }
</script>

<UserOptions config={ccconfig} {username} {isApi} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
<PlotColorScheme config={ccconfig} bind:cbmode bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>


     1→<!--
     2→  @component Admin settings wrapper
     3→
     4→  Properties:
     5→  - `ncontent String`: The homepage notice content
     6→  - `clusterNames [String]`: The available clusternames
     7→-->
     8→
     9→<script>
    10→  import { Row, Col } from "@sveltestrap/sveltestrap";
    11→  import { onMount, getContext } from "svelte";
    12→  import EditRole from "./admin/EditRole.svelte";
    13→  import EditProject from "./admin/EditProject.svelte";
    14→  import AddUser from "./admin/AddUser.svelte";
    15→  import ShowUsers from "./admin/ShowUsers.svelte";
    16→  import Options from "./admin/Options.svelte";
    17→  import NoticeEdit from "./admin/NoticeEdit.svelte";
    18→  import RunTaggers from "./admin/RunTaggers.svelte";
    19→
    20→  /* Svelte 5 Props */
    21→  let {
    22→    ncontent,
    23→    clusterNames
    24→  } = $props();
    25→
    26→  /* Const Init*/
    27→  const ccconfig = getContext("cc-config");
    28→
    29→  /* State Init */
    30→  let users = $state([]);
    31→  let roles = $state([]);
    32→
    33→  /* Functions */
    34→  function getUserList() {
    35→    fetch("/config/users/?via-ldap=false&not-just-user=true")
    36→      .then((res) => res.json())
    37→      .then((usersRaw) => {
    38→        users = usersRaw;
    39→      });
    40→  }
    41→
    42→  function getValidRoles() {
    43→    fetch("/config/roles/")
    44→      .then((res) => res.json())
    45→      .then((rolesRaw) => {
    46→        roles = rolesRaw;
    47→      });
    48→  }
    49→
    50→  function initAdmin() {
    51→    getUserList();
    52→    getValidRoles();
    53→  }
    54→
    55→  /* on Mount */
    56→  onMount(() => initAdmin());
    57→</script>
    58→
    59→<Row cols={2} class="p-2 g-2">
    60→  <Col class="mb-1">
    61→    <AddUser {roles} reloadUser={() => getUserList()} />
    62→  </Col>
    63→  <Col class="mb-1">
    64→    <ShowUsers reloadUser={() => getUserList()} bind:users />
    65→  </Col>
    66→  <Col>
    67→    <EditRole {roles} reloadUser={() => getUserList()} />
    68→  </Col>
    69→  <Col>
    70→    <EditProject reloadUser={() => getUserList()} />
    71→  </Col>
    72→  <Options config={ccconfig} {clusterNames}/>
    73→  <NoticeEdit {ncontent}/>
    74→  <RunTaggers />
    75→</Row>
    76→

<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>


Now let me check the PlotRenderOptions component to understand its props.


1→
    10→
    11→
    33→
    34→<Row cols={3} class="p-2 g-2">
    35→
    36→  <Col>
    37→    <Card class="h-100">
    38→      <form
    39→        id="line-width-form"
    40→        method="post"
    41→        action="/frontend/configuration/"
    42→        class="card-body"
    43→        onsubmit={(e) => updateSetting(e, {
    44→          selector: "#line-width-form",
    45→          target: "lw",
    46→        })}
    47→      >
    48→
    49→        <CardTitle
    50→          style="margin-bottom: 1em; display: flex; align-items: center;"
    51→        >
    52→
Line Width

    53→
    54→          {#if displayMessage && message.target == "lw"}
    55→

    56→
    57→                Update: {message.msg}
    58→
    59→

    60→          {/if}
    61→        </CardTitle>
    62→        <input type="hidden" name="key" value="plotConfiguration_lineWidth" />
    63→

    64→          Line Width
    65→          <input
    66→            type="number"
    67→            class="form-control"
    68→            id="lwvalue"
    69→            name="value"
    70→            aria-describedby="lineWidthHelp"
    71→            value={config.plotConfiguration_lineWidth}
    72→            min="1"
    73→          />
    74→

    75→            Width of the lines in the timeseries plots.
    76→

    77→

    78→        <Button color="primary" type="submit">Submit</Button>
    79→      </form>
    80→    </Card>
    81→  </Col>
    82→
    83→
    84→  <Col>
    85→    <Card class="h-100">
    86→      <form
    87→        id="plots-per-row-form"
    88→        method="post"
    89→        action="/frontend/configuration/"
    90→        class="card-body"
    91→        onsubmit={(e) => updateSetting(e, {
    92→          selector: "#plots-per-row-form",
    93→          target: "ppr",
    94→        })}
    95→      >
    96→
    97→        <CardTitle
    98→          style="margin-bottom: 1em; display: flex; align-items: center;"
    99→        >
   100→
Plots per Row

   101→          {#if displayMessage && message.target == "ppr"}
   102→

   103→
   104→                Update: {message.msg}
   105→
   106→

   107→          {/if}
   108→        </CardTitle>
   109→        <input type="hidden" name="key" value="plotConfiguration_plotsPerRow" />
   110→

   111→          Plots per Row
   112→          <input
   113→            type="number"
   114→            class="form-control"
   115→            id="pprvalue"
   116→            name="value"
   117→            aria-describedby="plotsperrowHelp"
   118→            value={config.plotConfiguration_plotsPerRow}
   119→            min="1"
   120→          />
   121→

   122→            How many plots to show next to each other on pages such as
   123→            /monitoring/job/, /monitoring/system/...
   124→

   125→

   126→        <Button color="primary" type="submit">Submit</Button>
   127→      </form>
   128→    </Card>
   129→  </Col>
   130→
   131→
   132→  <Col class="d-flex justify-content-between">
   133→    <Card class="h-100" style="width: 49%;">
   134→      <form
   135→        id="backgrounds-form"
   136→        method="post"
   137→        action="/frontend/configuration/"
   138→        class="card-body"
   139→        onsubmit={(e) => updateSetting(e, {
   140→          selector: "#backgrounds-form",
   141→          target: "bg",
   142→        })}
   143→      >
   144→
   145→        <CardTitle
   146→          style="margin-bottom: 1em; display: flex; align-items: center;"
   147→        >
   148→
Colored Backgrounds

   149→          {#if displayMessage && message.target == "bg"}
   150→

   151→
   152→                Update: {message.msg}
   153→
   154→

   155→          {/if}
   156→        </CardTitle>
   157→        <input type="hidden" name="key" value="plotConfiguration_colorBackground" />
   158→

   159→

   160→            {#if config.plotConfiguration_colorBackground}
   161→              <input type="radio" id="colb-true-checked" name="value" value="true" checked />
   162→            {:else}
   163→              <input type="radio" id="colb-true" name="value" value="true" />
   164→            {/if}
   165→            Yes
   166→

   167→

   168→            {#if config.plotConfiguration_colorBackground}
   169→              <input type="radio" id="colb-false" name="value" value="false" />
   170→            {:else}
   171→              <input type="radio" id="colb-false-checked" name="value" value="false" checked />
   172→            {/if}
   173→            No
   174→

   175→

   176→        <Button color="primary" type="submit">Submit</Button>
   177→      </form>
   178→    </Card>
   179→    <Card class="h-100" style="width: 49%;">
   180→      <form
   181→        id="colorblindmode-form"
   182→        method="post"
   183→        action="/frontend/configuration/"
   184→        class="card-body"
   185→        onsubmit={(e) => updateSetting(e, {
   186→          selector: "#colorblindmode-form",
   187→          target: "cbm",
   188→        })}
   189→      >
   190→
   191→        <CardTitle
   192→          style="margin-bottom: 1em; display: flex; align-items: center;"
   193→        >
   194→
Color Blind Mode

   195→          {#if displayMessage && message.target == "cbm"}
   196→

   197→
   198→                Update: {message.msg}
   199→
   200→

   201→          {/if}
   202→        </CardTitle>
   203→        <input type="hidden" name="key" value="plotConfiguration_colorblindMode" />
   204→

   205→

   206→            {#if config?.plotConfiguration_colorblindMode}
   207→              <input type="radio" id="cbm-true-checked" name="value" value="true" checked />
   208→            {:else}
   209→              <input type="radio" id="cbm-true" name="value" value="true" />
   210→            {/if}
   211→            Yes
   212→

   213→

   214→            {#if config?.plotConfiguration_colorblindMode}
   215→              <input type="radio" id="cbm-false" name="value" value="false" />
   216→            {:else}
   217→              <input type="radio" id="cbm-false-checked" name="value" value="false" checked />
   218→            {/if}
   219→            No
   220→

   221→

   222→        <Button color="primary" type="submit">Submit</Button>
   223→      </form>
   224→    </Card>
   225→  </Col>
   226→
   227→  {#if resamplingEnabled}
   228→
   229→  <Col>
   230→    <Card class="h-100">
   231→      <form
   232→        id="resample-policy-form"
   233→        method="post"
   234→        action="/frontend/configuration/"
   235→        class="card-body"
   236→        onsubmit={(e) => updateSetting(e, {
   237→          selector: "#resample-policy-form",
   238→          target: "rsp",
   239→        })}
   240→      >
   241→        <CardTitle
   242→          style="margin-bottom: 1em; display: flex; align-items: center;"
   243→        >
   244→
Resample Policy

   245→          {#if displayMessage && message.target == "rsp"}
   246→

   247→
   248→                Update: {message.msg}
   249→
   250→

   251→          {/if}
   252→        </CardTitle>
   253→        <input type="hidden" name="key" value="plotConfiguration_resamplePolicy" />
   254→

   255→          {#each [["", "Default"], ["low", "Low"], ["medium", "Medium"], ["high", "High"]] as [val, label]}
   256→

   257→              <input type="radio" id="rsp-{val || 'default'}" name="value" value={JSON.stringify(val)}
   258→                checked={(!config.plotConfiguration_resamplePolicy && val === "") || config.plotConfiguration_resamplePolicy === val} />
   259→              {label}
   260→

   261→          {/each}
   262→

   263→            Controls how many data points are shown in metric plots. Low = fast overview (~200 points), Medium = balanced (~500), High = maximum detail (~1000).
   264→

   265→

   266→        <Button color="primary" type="submit">Submit</Button>
   267→      </form>
   268→    </Card>
   269→  </Col>
   270→
   271→
   272→  <Col>
   273→    <Card class="h-100">
   274→      <form
   275→        id="resample-algo-form"
   276→        method="post"
   277→        action="/frontend/configuration/"
   278→        class="card-body"
   279→        onsubmit={(e) => updateSetting(e, {
   280→          selector: "#resample-algo-form",
   281→          target: "rsa",
   282→        })}
   283→      >
   284→        <CardTitle
   285→          style="margin-bottom: 1em; display: flex; align-items: center;"
   286→        >
   287→
Resample Algorithm

   288→          {#if displayMessage && message.target == "rsa"}
   289→

   290→
   291→                Update: {message.msg}
   292→
   293→

   294→          {/if}
   295→        </CardTitle>
   296→        <input type="hidden" name="key" value="plotConfiguration_resampleAlgo" />
   297→

   298→          {#each [["", "Default"], ["lttb", "LTTB"], ["average", "Average"], ["simple", "Simple"]] as [val, label]}
   299→

   300→              <input type="radio" id="rsa-{val || 'default'}" name="value" value={JSON.stringify(val)}
   301→                checked={(!config.plotConfiguration_resampleAlgo && val === "") || config.plotConfiguration_resampleAlgo === val} />
   302→              {label}
   303→

   304→          {/each}
   305→

   306→            Algorithm used when downsampling time-series data. LTTB preserves visual shape, Average smooths data, Simple picks every Nth point.
   307→

   308→

   309→        <Button color="primary" type="submit">Submit</Button>
   310→      </form>
   311→    </Card>
   312→  </Col>
   313→  {/if}
   314→</Row>

<Row cols={3} class="p-2 g-2">

  <Col>
    <Card class="h-100">
      <form
        id="line-width-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#line-width-form",
          target: "lw",
        })}
      >

        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Line Width

          {#if displayMessage && message.target == "lw"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_lineWidth" />

          Line Width
          <input
            type="number"
            class="form-control"
            id="lwvalue"
            name="value"
            aria-describedby="lineWidthHelp"
            value={config.plotConfiguration_lineWidth}
            min="1"
          />

            Width of the lines in the timeseries plots.

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
  </Col>

  <Col>
    <Card class="h-100">
      <form
        id="plots-per-row-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#plots-per-row-form",
          target: "ppr",
        })}
      >

        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Plots per Row

          {#if displayMessage && message.target == "ppr"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_plotsPerRow" />

          Plots per Row
          <input
            type="number"
            class="form-control"
            id="pprvalue"
            name="value"
            aria-describedby="plotsperrowHelp"
            value={config.plotConfiguration_plotsPerRow}
            min="1"
          />

            How many plots to show next to each other on pages such as
            /monitoring/job/, /monitoring/system/...

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
  </Col>

  <Col class="d-flex justify-content-between">
    <Card class="h-100" style="width: 49%;">
      <form
        id="backgrounds-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#backgrounds-form",
          target: "bg",
        })}
      >

        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Colored Backgrounds

          {#if displayMessage && message.target == "bg"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_colorBackground" />

            {#if config.plotConfiguration_colorBackground}
              <input type="radio" id="colb-true-checked" name="value" value="true" checked />
            {:else}
              <input type="radio" id="colb-true" name="value" value="true" />
            {/if}
            Yes

            {#if config.plotConfiguration_colorBackground}
              <input type="radio" id="colb-false" name="value" value="false" />
            {:else}
              <input type="radio" id="colb-false-checked" name="value" value="false" checked />
            {/if}
            No

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
    <Card class="h-100" style="width: 49%;">
      <form
        id="colorblindmode-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#colorblindmode-form",
          target: "cbm",
        })}
      >

        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Color Blind Mode

          {#if displayMessage && message.target == "cbm"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_colorblindMode" />

            {#if config?.plotConfiguration_colorblindMode}
              <input type="radio" id="cbm-true-checked" name="value" value="true" checked />
            {:else}
              <input type="radio" id="cbm-true" name="value" value="true" />
            {/if}
            Yes

            {#if config?.plotConfiguration_colorblindMode}
              <input type="radio" id="cbm-false" name="value" value="false" />
            {:else}
              <input type="radio" id="cbm-false-checked" name="value" value="false" checked />
            {/if}
            No

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
  </Col>

  {#if resamplingEnabled}

  <Col>
    <Card class="h-100">
      <form
        id="resample-policy-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#resample-policy-form",
          target: "rsp",
        })}
      >
        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Resample Policy

          {#if displayMessage && message.target == "rsp"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_resamplePolicy" />

          {#each [["", "Default"], ["low", "Low"], ["medium", "Medium"], ["high", "High"]] as [val, label]}

              <input type="radio" id="rsp-{val || 'default'}" name="value" value={JSON.stringify(val)}
                checked={(!config.plotConfiguration_resamplePolicy && val === "") || config.plotConfiguration_resamplePolicy === val} />
              {label}

          {/each}

            Controls how many data points are shown in metric plots. Low = fast overview (~200 points), Medium = balanced (~500), High = maximum detail (~1000).

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
  </Col>

  <Col>
    <Card class="h-100">
      <form
        id="resample-algo-form"
        method="post"
        action="/frontend/configuration/"
        class="card-body"
        onsubmit={(e) => updateSetting(e, {
          selector: "#resample-algo-form",
          target: "rsa",
        })}
      >
        <CardTitle
          style="margin-bottom: 1em; display: flex; align-items: center;"
        >

Resample Algorithm

          {#if displayMessage && message.target == "rsa"}

                Update: {message.msg}

          {/if}
        </CardTitle>
        <input type="hidden" name="key" value="plotConfiguration_resampleAlgo" />

          {#each [["", "Default"], ["lttb", "LTTB"], ["average", "Average"], ["simple", "Simple"]] as [val, label]}

              <input type="radio" id="rsa-{val || 'default'}" name="value" value={JSON.stringify(val)}
                checked={(!config.plotConfiguration_resampleAlgo && val === "") || config.plotConfiguration_resampleAlgo === val} />
              {label}

          {/each}

            Algorithm used when downsampling time-series data. LTTB preserves visual shape, Average smooths data, Simple picks every Nth point.

        <Button color="primary" type="submit">Submit</Button>
      </form>
    </Card>
  </Col>
  {/if}
</Row>


Now I need to understand how admin settings handle form submissions. Let me check the Options component.


1→
     7→
     8→
    38→
    39→<Col>
    40→  <Card class="h-100">
    41→    <CardBody>
    42→      <CardTitle class="mb-3">Scramble Names / Presentation Mode</CardTitle>
    43→      <input
    44→        type="checkbox"
    45→        id="scramble-names-checkbox"
    46→        style="margin-right: 1em;"
    47→        onclick={() => handleScramble()}
    48→        bind:checked={scrambled}
    49→      />
    50→      Active?
    51→    </CardBody>
    52→  </Card>
    53→</Col>
    54→
    55→{#if clusterNames?.length > 0}
    56→  <Col>
    57→    <Card class="h-100">
    58→      <CardBody>
    59→        <CardTitle class="mb-3">Public Dashboard Links</CardTitle>
    60→        <Row>
    61→        {#each clusterNames as cn}
    62→          <Col>
    63→            <Button color="info" class="mb-2 mb-xl-0" href={`/monitoring/dashboard/${cn}`} target="_blank">
    64→              <Icon name="clipboard-pulse" class="mr-2"/>
    65→              {cn.charAt(0).toUpperCase() + cn.slice(1)} Public Dashboard
    66→            </Button>
    67→          </Col>
    68→        {/each}
    69→        </Row>
    70→      </CardBody>
    71→    </Card>
    72→  </Col>
    73→{/if}
    74→
    75→{#if resampleConfig}
    76→  <Col>
    77→    <Card class="h-100">
    78→      <CardBody>
    79→        <CardTitle class="mb-3">Metric Plot Resampling Info</CardTitle>
    80→
Triggered at {resampleConfig.trigger} datapoints.

    81→
Configured resolutions: {resampleConfig.resolutions}

    82→      </CardBody>
    83→    </Card>
    84→  </Col>
    85→{/if}
    86→

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


I see the AdminSettings page uses a `<Row cols={2}>` layout with `<Col>` elements. The PlotRenderOptions component renders its own `<Row cols={3}>` wrapper. I need to add it to the AdminSettings page with the same `handleSettingSubmit` pattern used in UserSettings.


Edit

  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);

  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);


The file /Users/jan/prg/ClusterCockpit/cc-backend/web/frontend/src/config/AdminSettings.svelte has been updated successfully.

  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);

<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>

+  import { fade } from "svelte/transition";

+  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

+  let message = $state({ msg: "", target: "", color: "#d63384" });


Now add the `handleSettingSubmit` and `popMessage` functions, and the PlotRenderOptions component to the template.


  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  /* on Mount */
  onMount(() => initAdmin());

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }

  /* on Mount */
  onMount(() => initAdmin());


  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }

  /* on Mount */
  onMount(() => initAdmin());

<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>

+  async function handleSettingSubmit(event, setting) {

+    let form = document.querySelector(selector);

+      const res = await fetch(form.action, { method: "POST", body: formData });

+        popMessage(text, target, "#048109");

+        throw new Error("Response Code " + res.status + "-> " + text);

+      popMessage(err, target, "#d63384");

+  function popMessage(response, restarget, rescolor) {

+    message = { msg: response, target: restarget, color: rescolor };


Now add `PlotRenderOptions` to the template:


  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>

  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>


  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>

<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>

+<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>


Let me verify the final file looks correct.


     1→<!--
     2→  @component Admin settings wrapper
     3→
     4→  Properties:
     5→  - `ncontent String`: The homepage notice content
     6→  - `clusterNames [String]`: The available clusternames
     7→-->
     8→
     9→<script>
    10→  import { Row, Col } from "@sveltestrap/sveltestrap";
    11→  import { onMount, getContext } from "svelte";
    12→  import { fade } from "svelte/transition";
    13→  import EditRole from "./admin/EditRole.svelte";
    14→  import EditProject from "./admin/EditProject.svelte";
    15→  import AddUser from "./admin/AddUser.svelte";
    16→  import ShowUsers from "./admin/ShowUsers.svelte";
    17→  import Options from "./admin/Options.svelte";
    18→  import NoticeEdit from "./admin/NoticeEdit.svelte";
    19→  import RunTaggers from "./admin/RunTaggers.svelte";
    20→  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";
    21→
    22→  /* Svelte 5 Props */
    23→  let {
    24→    ncontent,
    25→    clusterNames
    26→  } = $props();
    27→
    28→  /* Const Init*/
    29→  const ccconfig = getContext("cc-config");
    30→
    31→  /* State Init */
    32→  let users = $state([]);
    33→  let roles = $state([]);
    34→  let message = $state({ msg: "", target: "", color: "#d63384" });
    35→  let displayMessage = $state(false);
    36→
    37→  /* Functions */
    38→  function getUserList() {
    39→    fetch("/config/users/?via-ldap=false&not-just-user=true")
    40→      .then((res) => res.json())
    41→      .then((usersRaw) => {
    42→        users = usersRaw;
    43→      });
    44→  }
    45→
    46→  function getValidRoles() {
    47→    fetch("/config/roles/")
    48→      .then((res) => res.json())
    49→      .then((rolesRaw) => {
    50→        roles = rolesRaw;
    51→      });
    52→  }
    53→
    54→  function initAdmin() {
    55→    getUserList();
    56→    getValidRoles();
    57→  }
    58→
    59→  async function handleSettingSubmit(event, setting) {
    60→    event.preventDefault();
    61→
    62→    const selector = setting.selector
    63→    const target = setting.target
    64→    let form = document.querySelector(selector);
    65→    let formData = new FormData(form);
    66→    try {
    67→      const res = await fetch(form.action, { method: "POST", body: formData });
    68→      if (res.ok) {
    69→        let text = await res.text();
    70→        popMessage(text, target, "#048109");
    71→      } else {
    72→        let text = await res.text();
    73→        throw new Error("Response Code " + res.status + "-> " + text);
    74→      }
    75→    } catch (err) {
    76→      popMessage(err, target, "#d63384");
    77→    }
    78→
    79→    return false;
    80→  }
    81→
    82→  function popMessage(response, restarget, rescolor) {
    83→    message = { msg: response, target: restarget, color: rescolor };
    84→    displayMessage = true;
    85→    setTimeout(function () {
    86→      displayMessage = false;
    87→    }, 3500);
    88→  }
    89→
    90→  /* on Mount */
    91→  onMount(() => initAdmin());
    92→</script>
    93→
    94→<Row cols={2} class="p-2 g-2">
    95→  <Col class="mb-1">
    96→    <AddUser {roles} reloadUser={() => getUserList()} />
    97→  </Col>
    98→  <Col class="mb-1">
    99→    <ShowUsers reloadUser={() => getUserList()} bind:users />
   100→  </Col>
   101→  <Col>
   102→    <EditRole {roles} reloadUser={() => getUserList()} />
   103→  </Col>
   104→  <Col>
   105→    <EditProject reloadUser={() => getUserList()} />
   106→  </Col>
   107→  <Options config={ccconfig} {clusterNames}/>
   108→  <NoticeEdit {ncontent}/>
   109→  <RunTaggers />
   110→</Row>
   111→<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>
   112→

<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>


Done. Added `PlotRenderOptions` to `AdminSettings.svelte` with:

- Imported the component and `fade` transition
- Added `message` and `displayMessage` state variables
- Added `handleSettingSubmit` and `popMessage` functions (same pattern as `UserSettings.svelte`)
- Rendered `PlotRenderOptions` below the existing admin `<Row>`, so admins can configure line width, plots per row, colored backgrounds, color blind mode, and resampling options


The render options are not show here yet for admins: http://localhost:8080/config . Debug and fix it


Let me trace how the admin settings page is served and what data it receives.


Found 1 file
web/frontend/src/Config.root.svelte


Found 12 files
web/frontend/src/status.entrypoint.js
web/frontend/src/generic/utils.js
web/frontend/src/config.entrypoint.js
web/frontend/src/dashpublic.entrypoint.js
web/frontend/src/tags.entrypoint.js
web/frontend/src/user.entrypoint.js
web/frontend/src/systems.entrypoint.js
web/frontend/src/node.entrypoint.js
web/frontend/src/jobs.entrypoint.js
web/frontend/src/list.entrypoint.js
web/frontend/src/job.entrypoint.js
web/frontend/src/analysis.entrypoint.js


     1→import { mount } from 'svelte';
     2→import {} from './header.entrypoint.js'
     3→import Config from './Config.root.svelte'
     4→
     5→mount(Config, {
     6→    target: document.getElementById('svelte-app'),
     7→    props: {
     8→        isAdmin: isAdmin,
     9→        isSupport: isSupport,
    10→        isApi: isApi,
    11→        username: username,
    12→        ncontent: ncontent,
    13→        clusterNames: hClusterNames
    14→    },
    15→    context: new Map([
    16→            ['cc-config', clusterCockpitConfig],
    17→            ['resampling', resampleConfig]
    18→    ])
    19→})
    20→

import { mount } from 'svelte';
import {} from './header.entrypoint.js'
import Config from './Config.root.svelte'

mount(Config, {
    target: document.getElementById('svelte-app'),
    props: {
        isAdmin: isAdmin,
        isSupport: isSupport,
        isApi: isApi,
        username: username,
        ncontent: ncontent,
        clusterNames: hClusterNames
    },
    context: new Map([
            ['cc-config', clusterCockpitConfig],
            ['resampling', resampleConfig]
    ])
})


     1→<!--
     2→  @component Main Config Option Component, Wrapper for admin and user sub-components
     3→
     4→  Properties:
     5→  - `ìsAdmin Bool!`: Is currently logged in user admin authority
     6→  - `isSupport Bool!`: Is currently logged in user support authority
     7→  - `isApi Bool!`: Is currently logged in user api authority
     8→  - `username String!`: Empty string if auth. is disabled, otherwise the username as string
     9→  - `ncontent String!`: The currently displayed message on the homescreen
    10→  - `clusterNames [String]`: The available clusternames
    11→-->
    12→
    13→<script>
    14→  import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
    15→  import UserSettings from "./config/UserSettings.svelte";
    16→  import SupportSettings from "./config/SupportSettings.svelte";
    17→  import AdminSettings from "./config/AdminSettings.svelte";
    18→
    19→  /* Svelte 5 Props */
    20→  let {
    21→    isAdmin,
    22→    isSupport,
    23→    isApi,
    24→    username,
    25→    ncontent,
    26→    clusterNames
    27→  } = $props();
    28→</script>
    29→
    30→{#if isAdmin}
    31→  <Card style="margin-bottom: 1.5rem;">
    32→    <CardHeader>
    33→      <CardTitle class="mb-1">Admin Options</CardTitle>
    34→    </CardHeader>
    35→    <AdminSettings {ncontent} {clusterNames}/>
    36→  </Card>
    37→{/if}
    38→
    39→{#if isSupport || isAdmin}
    40→  <Card style="margin-bottom: 1.5rem;">
    41→    <CardHeader>
    42→      <CardTitle class="mb-1">Support Options</CardTitle>
    43→    </CardHeader>
    44→    <SupportSettings/>
    45→  </Card>
    46→{/if}
    47→
    48→<Card>
    49→  <CardHeader>
    50→    <CardTitle class="mb-1">User Options</CardTitle>
    51→  </CardHeader>
    52→  <UserSettings {username} {isApi}/>
    53→</Card>
    54→

<!--
  @component Main Config Option Component, Wrapper for admin and user sub-components

  Properties:
  - `ìsAdmin Bool!`: Is currently logged in user admin authority
  - `isSupport Bool!`: Is currently logged in user support authority
  - `isApi Bool!`: Is currently logged in user api authority
  - `username String!`: Empty string if auth. is disabled, otherwise the username as string
  - `ncontent String!`: The currently displayed message on the homescreen
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Card, CardHeader, CardTitle } from "@sveltestrap/sveltestrap";
  import UserSettings from "./config/UserSettings.svelte";
  import SupportSettings from "./config/SupportSettings.svelte";
  import AdminSettings from "./config/AdminSettings.svelte";

  /* Svelte 5 Props */
  let {
    isAdmin,
    isSupport,
    isApi,
    username,
    ncontent,
    clusterNames
  } = $props();
</script>

{#if isAdmin}
  <Card style="margin-bottom: 1.5rem;">
    <CardHeader>
      <CardTitle class="mb-1">Admin Options</CardTitle>
    </CardHeader>
    <AdminSettings {ncontent} {clusterNames}/>
  </Card>
{/if}

{#if isSupport || isAdmin}
  <Card style="margin-bottom: 1.5rem;">
    <CardHeader>
      <CardTitle class="mb-1">Support Options</CardTitle>
    </CardHeader>
    <SupportSettings/>
  </Card>
{/if}

<Card>
  <CardHeader>
    <CardTitle class="mb-1">User Options</CardTitle>
  </CardHeader>
  <UserSettings {username} {isApi}/>
</Card>


The context `cc-config` and `resampling` are set at the entrypoint level, so they should be available to AdminSettings via `getContext`. Let me check how the `clusterCockpitConfig` and `resampleConfig` variables are provided in the template.


web/templates/config.tmpl:15:        const clusterCockpitConfig = {{ .Config }};
web/templates/config.tmpl:16:        const resampleConfig = {{ .Resampling }};
web/templates/monitoring/taglist.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/analysis.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/node.tmpl:11:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/status.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/jobs.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/jobs.tmpl:15:        const resampleConfig = {{ .Resampling }};
web/templates/monitoring/dashboard.tmpl:11:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/systems.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/systems.tmpl:13:        const resampleConfig = {{ .Resampling }};
web/templates/monitoring/user.tmpl:12:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/user.tmpl:13:        const resampleConfig = {{ .Resampling }};
web/templates/monitoring/job.tmpl:13:        const clusterCockpitConfig = {{ .Config }};
web/templates/monitoring/job.tmpl:17:        const resampleConfig = {{ .Resampling }};
web/templates/monitoring/list.tmpl:12:        const clusterCockpitConfig = {{ .Config }};


Good — the config template provides both `clusterCockpitConfig` and `resampleConfig`. The context is set in the entrypoint. So the PlotRenderOptions component should have access to everything it needs.

Let me check if the issue is that `PlotRenderOptions` is rendered outside the `<Row>` and perhaps the `<Card>` wrapper — maybe it's just not visually within the admin card. Let me look more carefully at the structure.

Looking at the Config.root.svelte, AdminSettings is rendered inside a `<Card>`. The `PlotRenderOptions` component renders its own `<Row cols={3}>`. The component is placed after the closing `</Row>` tag inside AdminSettings, which should still be inside the Card wrapper from Config.root.svelte.

Let me check if the issue is with the `/frontend/configuration/` endpoint — whether it works for admin users or if there's a different endpoint.


No matches found


web/frontend/src/config/support/SupportOptions.svelte:70:          action="/frontend/configuration/"
web/frontend/src/config/user/UserOptions.svelte:87:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:41:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:89:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:137:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:183:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:234:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotRenderOptions.svelte:277:        action="/frontend/configuration/"
web/frontend/src/config/user/PlotColorScheme.svelte:331:        action="/frontend/configuration/"


The endpoint exists and is used by other components. Let me check the Go router for this route.


internal/api/node.go:28:// metricListToNames converts a map of metric configurations to a list of metric names
internal/api/api_test.go:155:	// Load and check main configuration
internal/api/api_test.go:159:		cclog.Abort("Main configuration must be present")
internal/api/api_test.go:176:		cclog.Warn("Authentication disabled due to missing configuration")
internal/api/nats_test.go:141:	// Load and check main configuration
internal/api/nats_test.go:145:		cclog.Abort("Main configuration must be present")
internal/api/nats_test.go:162:		cclog.Warn("Authentication disabled due to missing configuration")
internal/api/rest.go:143:// MountConfigAPIRoutes registers configuration and user management endpoints.
internal/api/rest.go:163:// These routes support JWT generation and user configuration updates with session authentication.
internal/api/rest.go:169:		r.Post("/configuration/", api.updateConfiguration)
internal/api/rest.go:384:// @summary     Update user configuration
internal/api/rest.go:386:// @description Updates a user's configuration key-value pair.
internal/api/rest.go:394:// @router      /configuration/ [post]
internal/api/rest.go:400:		handleError(fmt.Errorf("updating configuration failed: %w", err), http.StatusInternalServerError, rw)
internal/api/docs.go:1221:        "/configuration/": {
internal/api/docs.go:1223:                "description": "Updates a user's configuration key-value pair.",
internal/api/docs.go:1233:                "summary": "Update user configuration",
internal/api/docs.go:2129:                    "description": "Cluster-wide metric configurations",
internal/api/docs.go:2604:                "configuration": {
internal/api/docs.go:2605:                    "description": "Optional configuration identifier",
internal/api/docs.go:2739:                    "description": "Subcluster-specific metric configurations",
internal/api/docs.go:2787:                    "description": "Energy measurement configuration",


The route is at `/frontend/configuration/` — let me check the router mount path.


   143→// MountConfigAPIRoutes registers configuration and user management endpoints.
   144→// These routes use session-based authentication and require admin privileges.
   145→// Routes use full paths (including /config prefix) to avoid conflicting with
   146→// the /config page route when registered via Group instead of Route.
   147→func (api *RestAPI) MountConfigAPIRoutes(r chi.Router) {
   148→	// Settings Frontend Uses SessionAuth
   149→	if api.Authentication != nil {
   150→		r.Get("/config/roles/", api.getRoles)
   151→		r.Post("/config/users/", api.createUser)
   152→		r.Put("/config/users/", api.createUser)
   153→		r.Get("/config/users/", api.getUsers)
   154→		r.Delete("/config/users/", api.deleteUser)
   155→		r.Post("/config/user/{id}", api.updateUser)
   156→		r.Post("/config/notice/", api.editNotice)
   157→		r.Get("/config/taggers/", api.getTaggers)
   158→		r.Post("/config/taggers/run/", api.runTagger)
   159→	}
   160→}
   161→
   162→// MountFrontendAPIRoutes registers frontend-specific API endpoints.
   163→// These routes support JWT generation and user configuration updates with session authentication.
   164→func (api *RestAPI) MountFrontendAPIRoutes(r chi.Router) {
   165→	r.Get("/logs/", api.getJournalLog)
   166→	// Settings Frontend Uses SessionAuth
   167→	if api.Authentication != nil {
   168→		r.Get("/jwt/", api.getJWT)
   169→		r.Post("/configuration/", api.updateConfiguration)
   170→	}
   171→}
   172→
   173→// ErrorResponse model
   174→type ErrorResponse struct {
   175→	// Statustext of Errorcode
   176→	Status string `json:"status"`
   177→	Error  string `json:"error"` // Error Message

// MountConfigAPIRoutes registers configuration and user management endpoints.
// These routes use session-based authentication and require admin privileges.
// Routes use full paths (including /config prefix) to avoid conflicting with
// the /config page route when registered via Group instead of Route.
func (api *RestAPI) MountConfigAPIRoutes(r chi.Router) {
	// Settings Frontend Uses SessionAuth
	if api.Authentication != nil {
		r.Get("/config/roles/", api.getRoles)
		r.Post("/config/users/", api.createUser)
		r.Put("/config/users/", api.createUser)
		r.Get("/config/users/", api.getUsers)
		r.Delete("/config/users/", api.deleteUser)
		r.Post("/config/user/{id}", api.updateUser)
		r.Post("/config/notice/", api.editNotice)
		r.Get("/config/taggers/", api.getTaggers)
		r.Post("/config/taggers/run/", api.runTagger)
	}
}

// MountFrontendAPIRoutes registers frontend-specific API endpoints.
// These routes support JWT generation and user configuration updates with session authentication.
func (api *RestAPI) MountFrontendAPIRoutes(r chi.Router) {
	r.Get("/logs/", api.getJournalLog)
	// Settings Frontend Uses SessionAuth
	if api.Authentication != nil {
		r.Get("/jwt/", api.getJWT)
		r.Post("/configuration/", api.updateConfiguration)
	}
}

// ErrorResponse model
type ErrorResponse struct {
	// Statustext of Errorcode
	Status string `json:"status"`
	Error  string `json:"error"` // Error Message


OK so the endpoint exists and should be accessible. The issue is probably something else. Let me actually look at the browser to understand what's happening. Let me check if there's a build step needed.


Bash

ls web/frontend/public/build/ 2>/dev/null | head -5

Check if build output exists


analysis.css
analysis.js
config.css
config.js
dashpublic.css


The frontend needs to be rebuilt. But first, let me also check if there's a `fade` import issue — `fade` was imported but never used directly in the AdminSettings template (it's used inside PlotRenderOptions which imports its own).


Edit

  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";

import { onMount, getContext } from "svelte";


<!--
  @component Admin settings wrapper

  Properties:
  - `ncontent String`: The homepage notice content
  - `clusterNames [String]`: The available clusternames
-->

<script>
  import { Row, Col } from "@sveltestrap/sveltestrap";
  import { onMount, getContext } from "svelte";
  import { fade } from "svelte/transition";
  import EditRole from "./admin/EditRole.svelte";
  import EditProject from "./admin/EditProject.svelte";
  import AddUser from "./admin/AddUser.svelte";
  import ShowUsers from "./admin/ShowUsers.svelte";
  import Options from "./admin/Options.svelte";
  import NoticeEdit from "./admin/NoticeEdit.svelte";
  import RunTaggers from "./admin/RunTaggers.svelte";
  import PlotRenderOptions from "./user/PlotRenderOptions.svelte";

  /* Svelte 5 Props */
  let {
    ncontent,
    clusterNames
  } = $props();

  /* Const Init*/
  const ccconfig = getContext("cc-config");

  /* State Init */
  let users = $state([]);
  let roles = $state([]);
  let message = $state({ msg: "", target: "", color: "#d63384" });
  let displayMessage = $state(false);

  /* Functions */
  function getUserList() {
    fetch("/config/users/?via-ldap=false&not-just-user=true")
      .then((res) => res.json())
      .then((usersRaw) => {
        users = usersRaw;
      });
  }

  function getValidRoles() {
    fetch("/config/roles/")
      .then((res) => res.json())
      .then((rolesRaw) => {
        roles = rolesRaw;
      });
  }

  function initAdmin() {
    getUserList();
    getValidRoles();
  }

  async function handleSettingSubmit(event, setting) {
    event.preventDefault();

    const selector = setting.selector
    const target = setting.target
    let form = document.querySelector(selector);
    let formData = new FormData(form);
    try {
      const res = await fetch(form.action, { method: "POST", body: formData });
      if (res.ok) {
        let text = await res.text();
        popMessage(text, target, "#048109");
      } else {
        let text = await res.text();
        throw new Error("Response Code " + res.status + "-> " + text);
      }
    } catch (err) {
      popMessage(err, target, "#d63384");
    }

    return false;
  }

  function popMessage(response, restarget, rescolor) {
    message = { msg: response, target: restarget, color: rescolor };
    displayMessage = true;
    setTimeout(function () {
      displayMessage = false;
    }, 3500);
  }

  /* on Mount */
  onMount(() => initAdmin());
</script>

<Row cols={2} class="p-2 g-2">
  <Col class="mb-1">
    <AddUser {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col class="mb-1">
    <ShowUsers reloadUser={() => getUserList()} bind:users />
  </Col>
  <Col>
    <EditRole {roles} reloadUser={() => getUserList()} />
  </Col>
  <Col>
    <EditProject reloadUser={() => getUserList()} />
  </Col>
  <Options config={ccconfig} {clusterNames}/>
  <NoticeEdit {ncontent}/>
  <RunTaggers />
</Row>
<PlotRenderOptions config={ccconfig} bind:message bind:displayMessage updateSetting={(e, newSetting) => handleSettingSubmit(e, newSetting)}/>

-  import { fade } from "svelte/transition";


Now let me rebuild the frontend.


Bash

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

Build the frontend


ok (no errors)

> cc-frontend@1.5.2 build
> rollup -c

src/header.entrypoint.js → public/build/header.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/header.js in 1.6s

src/jobs.entrypoint.js → public/build/jobs.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/jobs.js in 5.4s

src/user.entrypoint.js → public/build/user.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/user.js in 5.5s

src/list.entrypoint.js → public/build/list.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/list.js in 4s

src/tags.entrypoint.js → public/build/taglist.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/taglist.js in 2.5s

src/job.entrypoint.js → public/build/job.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/job.js in 5s

src/systems.entrypoint.js → public/build/systems.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/systems.js in 3.3s

src/node.entrypoint.js → public/build/node.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/node.js in 2.9s

src/analysis.entrypoint.js → public/build/analysis.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/analysis.js in 5.1s

src/status.entrypoint.js → public/build/status.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/status.js in 4.3s

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.5s

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.7s

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.3s


Frontend built successfully. Reload the `/config` page in your browser — the PlotRenderOptions (line width, plots per row, colored backgrounds, color blind mode, and resampling settings) should now appear in the Admin Options section.


Add resample-config option to the current config


     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→  "metric-store": {
    23→    "retention-in-memory": "24h",
    24→    "memory-cap": 100
    25→  }
    26→}
    27→


Let me check how the resample config is defined.


/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/api.go:62:	ResampleAlgo string     `json:"resample-algo,omitempty"`
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/metricstore.go:36:	"github.com/ClusterCockpit/cc-lib/v2/resampler"
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/metricstore.go:682:func (m *MemoryStore) Read(selector util.Selector, metric string, from, to, resolution int64, resampleAlgo string) ([]schema.Float, int64, int64, int64, error) {
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/metricstore.go:740:	resampleFn, rfErr := resampler.GetResampler(resampleAlgo)
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/metricstore.go:744:	data, resolution, err = resampleFn(data, minfo.Frequency, resolution)
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/query.go:624:	resampleAlgo string,
/Users/jan/prg/ClusterCockpit/cc-backend/pkg/metricstore/query.go:646:		ResampleAlgo: resampleAlgo,
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:49:	"github.com/ClusterCockpit/cc-lib/v2/resampler"
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:65:	resampleAlgo string,
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:68:		*job.ID, job.State, metrics, scopes, resolution, resampleAlgo)
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:75:// For completed archived jobs, data is loaded from the job archive and resampled if needed.
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:91:	resampleAlgo string,
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:93:	data := cache.Get(cacheKey(job, metrics, scopes, resolution, resampleAlgo), func() (_ any, ttl time.Duration, size int) {
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:143:			resampleFn, rfErr := resampler.GetResampler(resampleAlgo)
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:151:						v_.Series[i].Data, timestep, err = resampleFn(v_.Series[i].Data, int64(v_.Timestep), int64(resolution))
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:423:	resampleAlgo string,
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/dataLoader.go:438:	data, err := ms.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, resolution, from, to, ctx, resampleAlgo)
/Users/jan/prg/ClusterCockpit/cc-backend/web/web.go:75:	ResampleAlgo    string   `json:"resample-algo"`
/Users/jan/prg/ClusterCockpit/cc-backend/web/web.go:76:	ResamplePolicy  string   `json:"resample-policy"`
/Users/jan/prg/ClusterCockpit/cc-backend/web/web.go:149:	UIDefaultsMap["plotConfiguration_resampleAlgo"] = UIDefaults.PlotConfiguration.ResampleAlgo
/Users/jan/prg/ClusterCockpit/cc-backend/web/web.go:150:	UIDefaultsMap["plotConfiguration_resamplePolicy"] = UIDefaults.PlotConfiguration.ResamplePolicy
/Users/jan/prg/ClusterCockpit/cc-backend/web/web.go:271:	Resampling    *config.ResampleConfig // If not nil, defines resampling trigger and resolutions
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricstoreclient/cc-metric-store.go:620:	resampleAlgo string,
/Users/jan/prg/ClusterCockpit/cc-backend/internal/metricdispatch/metricdata.go:55:		resampleAlgo string) (map[string]schema.JobData, error)
/Users/jan/prg/ClusterCockpit/cc-backend/internal/config/config.go:15:	"github.com/ClusterCockpit/cc-lib/v2/resampler"
/Users/jan/prg/ClusterCockpit/cc-backend/internal/config/config.go:73:	EnableResampling *ResampleConfig `json:"resampling"`
/Users/jan/prg/ClusterCockpit/cc-backend/internal/config/config.go:108:type ResampleConfig struct {
/Users/jan/prg/ClusterCockpit/cc-backend/internal/config/config.go:160:	if Keys.EnableResampling != nil && Keys.EnableResampling.MinimumPoints > 0 {
/Users/jan/prg/ClusterCockpit/cc-backend/internal/config/config.go:161:		resampler.SetMinimumRequiredPoints(Keys.EnableResampling.MinimumPoints)
/Users/jan/prg/ClusterCockpit/cc-backend/internal/routerConfig/routes.go:505:				Resampling: resampling,

[Showing results with pagination = limit: 30, offset: 0]


   105→	MaxFileSizeMB      int    `json:"max-file-size-mb"`
   106→}
   107→
   108→type ResampleConfig struct {
   109→	// Minimum number of points to trigger resampling of data
   110→	MinimumPoints int `json:"minimum-points"`
   111→	// Array of resampling target resolutions, in seconds; Example: [600,300,60]
   112→	Resolutions []int `json:"resolutions"`
   113→	// Trigger next zoom level at less than this many visible datapoints
   114→	Trigger int `json:"trigger"`
   115→	// Policy-derived target point count (set dynamically from user preference, not from config.json)
   116→	TargetPoints int `json:"targetPoints,omitempty"`
   117→}
   118→
   119→type NATSConfig struct {
   120→	SubjectJobEvent  string `json:"subject-job-event"`
   121→	SubjectNodeState string `json:"subject-node-state"`
   122→	JobConcurrency   int    `json:"job-concurrency"`
   123→	NodeConcurrency  int    `json:"node-concurrency"`
   124→}

	MaxFileSizeMB      int    `json:"max-file-size-mb"`
}

type ResampleConfig struct {
	// Minimum number of points to trigger resampling of data
	MinimumPoints int `json:"minimum-points"`
	// Array of resampling target resolutions, in seconds; Example: [600,300,60]
	Resolutions []int `json:"resolutions"`
	// Trigger next zoom level at less than this many visible datapoints
	Trigger int `json:"trigger"`
	// Policy-derived target point count (set dynamically from user preference, not from config.json)
	TargetPoints int `json:"targetPoints,omitempty"`
}

type NATSConfig struct {
	SubjectJobEvent  string `json:"subject-job-event"`
	SubjectNodeState string `json:"subject-node-state"`
	JobConcurrency   int    `json:"job-concurrency"`
	NodeConcurrency  int    `json:"node-concurrency"`
}


Edit

  "metric-store": {
    "retention-in-memory": "24h",
    "memory-cap": 100
  }

  "resampling": {
    "minimum-points": 0,
    "resolutions": [600, 300, 60],
    "trigger": 300
  },
  "metric-store": {
    "retention-in-memory": "24h",
    "memory-cap": 100
  }


The file /Users/jan/prg/ClusterCockpit/cc-backend/config.json has been updated successfully.

  "resampling": {
    "minimum-points": 0,
    "resolutions": [600, 300, 60],
    "trigger": 300
  },
  "metric-store": {
    "retention-in-memory": "24h",
    "memory-cap": 100
  }

"token": "REDACTED"
}
],
+  "resampling": {
+    "minimum-points": 0,
+    "resolutions": [600, 300, 60],
+    "trigger": 300
+  },
"metric-store": {
"retention-in-memory": "24h",
"memory-cap": 100


Added the `resampling` config with:
- `resolutions`: `[600, 300, 60]` (10min, 5min, 1min targets)
- `trigger`: `300` (zoom threshold for next resolution level)
- `minimum-points`: `0` (disabled, uses default)

Restart the backend for the config to take effect — the resampling options should then appear on the admin settings page.


What about the policy based approach? Investigate and also replace the file based  configuration with the resample method and policy as done in the API already.


Agent

Explore policy-based resampling

Prompt:
Explore the policy-based resampling approach in this codebase. I need to understand:

1. Look at the recent git commits, especially "Replace explicit resampling config with policy based approach" (commit 0069c86e) and "Introduce average resampler support" (c0d2d65f). Run `git show 0069c86e` and `git show c0d2d65f` to see the full diffs.

2. Look at `internal/config/config.go` - specifically the `ResampleConfig` struct and how `EnableResampling` is used. How does the config.json `"resampling"` key map to the Go struct?

3. Look at `web/web.go` - how is the resampling config passed to the frontend? What does the `Resampling` field in page data look like?

4. Look at `internal/routerConfig/routes.go` - how is the resampling config used when rendering pages?

5. Look at the GraphQL schema (`api/schema.graphqls`) and resolvers (`internal/graph/schema.resolvers.go`) for any resolution/resample parameters in queries like `jobMetrics` or similar.

6. Look at how resolution is computed from policy - search for "policy" or "targetPoints" or "TargetPoints" in the codebase.

7. Check `web/frontend/src/config/user/PlotRenderOptions.svelte` for the resample policy and algorithm UI options.

Be very thorough - I need to understand the full flow from config.json -> Go config -> frontend context -> GraphQL query -> metric loading.


Explore the policy-based resampling approach in this codebase. I need to understand:

1. Look at the recent git commits, especially "Replace explicit resampling config with policy based approach" (commit 0069c86e) and "Introduce average resampler support" (c0d2d65f). Run `git show 0069c86e` and `git show c0d2d65f` to see the full diffs.

2. Look at `internal/config/config.go` - specifically the `ResampleConfig` struct and how `EnableResampling` is used. How does the config.json `"resampling"` key map to the Go struct?

3. Look at `web/web.go` - how is the resampling config passed to the frontend? What does the `Resampling` field in page data look like?

4. Look at `internal/routerConfig/routes.go` - how is the resampling config used when rendering pages?

5. Look at the GraphQL schema (`api/schema.graphqls`) and resolvers (`internal/graph/schema.resolvers.go`) for any resolution/resample parameters in queries like `jobMetrics` or similar.

6. Look at how resolution is computed from policy - search for "policy" or "targetPoints" or "TargetPoints" in the codebase.

7. Check `web/frontend/src/config/user/PlotRenderOptions.svelte` for the resample policy and algorithm UI options.

Be very thorough - I need to understand the full flow from config.json -> Go config -> frontend context -> GraphQL query -> metric loading.

Prompt:
Explore the policy-based resampling approach in this codebase. I need to understand:

1. Look at the recent git commits, especially "Replace explicit resampling config with policy based approach" (commit 0069c86e) and "Introduce average resampler support" (c0d2d65f). Run `git show 0069c86e` and `git show c0d2d65f` to see the full diffs.

2. Look at `internal/config/config.go` - specifically the `ResampleConfig` struct and how `EnableResampling` is used. How does the config.json `"resampling"` key map to the Go struct?

3. Look at `web/web.go` - how is the resampling config passed to the frontend? What does the `Resampling` field in page data look like?

4. Look at `internal/routerConfig/routes.go` - how is the resampling config used when rendering pages?

5. Look at the GraphQL schema (`api/schema.graphqls`) and resolvers (`internal/graph/schema.resolvers.go`) for any resolution/resample parameters in queries like `jobMetrics` or similar.

6. Look at how resolution is computed from policy - search for "policy" or "targetPoints" or "TargetPoints" in the codebase.

7. Check `web/frontend/src/config/user/PlotRenderOptions.svelte` for the resample policy and algorithm UI options.

Be very thorough - I need to understand the full flow from config.json -> Go config -> frontend context -> GraphQL query -> metric loading.


Bash

cd /Users/jan/prg/ClusterCockpit/cc-backend && git show 0069c86e --stat


commit 0069c86e81fa407eb3573ff557ac5f127e17bd66
Author: Jan Eitzinger <jan@moebiusband.org>
Date:   Fri Mar 20 05:34:12 2026 +0100

    Replace explicit resampling config with policy based approach

    Entire-Checkpoint: f69e38210bb1

 internal/config/config.go                          |   2 +
 internal/graph/resample.go                         | 113 +++++++++++++++++++++
 internal/graph/schema.resolvers.go                 |  39 +++----
 internal/metricdispatch/resamplepolicy.go          |  49 +++++++++
 internal/metricdispatch/resamplepolicy_test.go     |  68 +++++++++++++
 internal/routerConfig/routes.go                    |  34 ++++++-
 .../src/config/user/PlotRenderOptions.svelte       |  92 +++++++++++++++++
 web/frontend/src/generic/plots/MetricPlot.svelte   |  30 +++---
 web/web.go                                         |   4 +
 9 files changed, 400 insertions(+), 31 deletions(-)


cd /Users/jan/prg/ClusterCockpit/cc-backend && git show 0069c86e


commit 0069c86e81fa407eb3573ff557ac5f127e17bd66
Author: Jan Eitzinger <jan@moebiusband.org>
Date:   Fri Mar 20 05:34:12 2026 +0100

    Replace explicit resampling config with policy based approach

    Entire-Checkpoint: f69e38210bb1

internal/config/config.go
  @@ -112,6 +112,8 @@
  +	// Policy-derived target point count (set dynamically from user preference, not from config.json)
  +	TargetPoints int `json:"targetPoints,omitempty"`
   }

   type NATSConfig struct {
  +2 -0

internal/graph/resample.go
  @@ -0,0 +1,113 @@
  +// 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 graph
  +
  +import (
  +	"context"
  +	"strings"
  +
  ... (truncated)
  +113 -0

internal/graph/schema.resolvers.go
  @@ -499,26 +499,27 @@
  -	if resolution == nil { // Load from Config
  +	job, err := r.Query().Job(ctx, id)
  +	if err != nil {
  +		cclog.Warn("Error while querying job for metrics")
  +		return nil, err
  +	}
  +
  +	// Resolve resolution: explicit param > user policy > global config > 0
  +	if resolution == nil {
  +		resolution = resolveResolutionFromPolicy(ctx, int64(job.Duration), job.Cluster, metrics)
  ... (truncated)
  @@ -878,11 +879,16 @@
  -	if resolution == nil { // Load from Config
  +	// Resolve resolution: explicit param > user policy > global config > 0
  +	duration := int64(to.Sub(from).Seconds())
  +	if resolution == nil {
  +		resolution = resolveResolutionFromPolicy(ctx, duration, cluster, metrics)
  +	}
  +	if resolution == nil {
   		if config.Keys.EnableResampling != nil {
   			defaultRes := slices.Max(config.Keys.EnableResampling.Resolutions)
   			resolution = &defaultRes
  ... (truncated)
  @@ -906,10 +912,7 @@
  -	algoName := ""
  -	if resampleAlgo != nil {
  -		algoName = strings.ToLower(resampleAlgo.String())
  -	}
  +	algoName := resolveResampleAlgo(ctx, resampleAlgo)

   	// data -> map hostname:jobdata
   	data, err := metricdispatch.LoadNodeListData(cluster, subCluster, nodes, metrics, scopes, *resolution, from, to, ctx, algoName)
  +21 -18

internal/metricdispatch/resamplepolicy.go
  @@ -0,0 +1,49 @@
  +// 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 metricdispatch
  +
  +import "math"
  +
  +type ResamplePolicy string
  +
  ... (truncated)
  +49 -0

internal/metricdispatch/resamplepolicy_test.go
  @@ -0,0 +1,68 @@
  +// 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 metricdispatch
  +
  +import "testing"
  +
  +func TestTargetPointsForPolicy(t *testing.T) {
  +	tests := []struct {
  ... (truncated)
  +68 -0

internal/routerConfig/routes.go
  @@ -15,6 +15,7 @@
  +	"github.com/ClusterCockpit/cc-backend/internal/metricdispatch"
   	"github.com/ClusterCockpit/cc-backend/internal/repository"
   	"github.com/ClusterCockpit/cc-backend/web"
   	cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
  @@ -493,13 +494,15 @@
  +			resampling := resamplingForUser(conf)
  +
   			page := web.Page{
   				Title:      title,
   				User:       *user,
   				Roles:      availableRoles,
   				Build:      buildInfo,
   				Config:     conf,
  -				Resampling: config.Keys.EnableResampling,
  +				Resampling: resampling,
  @@ -586,3 +589,32 @@
  +
  +// resamplingForUser returns a ResampleConfig that incorporates the user's
  +// resample policy preference. If the user has a policy set, it creates a
  +// policy-derived config with targetPoints and trigger. Otherwise falls back
  +// to the global config.
  +func resamplingForUser(conf map[string]any) *config.ResampleConfig {
  +	globalCfg := config.Keys.EnableResampling
  +
  +	policyVal, ok := conf["plotConfiguration_resamplePolicy"]
  +	if !ok {
  ... (truncated)
  +33 -1

web/frontend/src/config/user/PlotRenderOptions.svelte
  @@ -16,6 +16,7 @@
  +  import { getContext } from "svelte";
     import { fade } from "svelte/transition";

     /* Svelte 5 Props */
  @@ -25,6 +26,9 @@
  +
  +  const resampleConfig = getContext("resampling");
  +  const resamplingEnabled = !!resampleConfig;
   </script>

   <Row cols={3} class="p-2 g-2">
  @@ -219,4 +223,92 @@
  +
  +  {#if resamplingEnabled}
  +  <!-- RESAMPLE POLICY -->
  +  <Col>
  +    <Card class="h-100">
  +      <form
  +        id="resample-policy-form"
  +        method="post"
  +        action="/frontend/configuration/"
  +        class="card-body"
  ... (truncated)
  +92 -0

web/frontend/src/generic/plots/MetricPlot.svelte
  @@ -73,9 +73,10 @@
  -  const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : null);
  +  const resampleTrigger = $derived(resampleConfig?.trigger ? Number(resampleConfig.trigger) : (resampleConfig?.targetPoints ? Math.floor(resampleConfig.targetPoints / 4) : null));
     const resampleResolutions = $derived(resampleConfig?.resolutions ? [...resampleConfig.resolutions] : null);
     const resampleMinimum = $derived(resampleConfig?.resolutions ? Math.min(...resampleConfig.resolutions) : null);
  +  const resampleTargetPoints = $derived(resampleConfig?.targetPoints ? Number(resampleConfig.targetPoints) : null);
     const useStatsSeries = $derived(!!statisticsSeries); // Display Stats Series By Default if Exists
     const thresholds = $derived(findJobAggregationThresholds(
       subClusterTopology,
  @@ -515,24 +516,29 @@
  -                /* Get closest zoom level; prevents multiple iterative zoom requests for big zoom-steps (e.g. 600 -> 300 -> 120 -> 60) */
  -                // Which resolution to theoretically request to achieve 30 or more visible data points:
  -                const target = (numX * timestep) / resampleTrigger
  -                // Which configured resolution actually matches the closest to theoretical target:
  -                const closest = resampleResolutions.reduce(function(prev, curr) {
  -                  return (Math.abs(curr - target) < Math.abs(prev - target) ? curr : prev);
  -                });
  +                let newRes;
  +                if (resampleTargetPoints && !resampleResolutions) {
  +                  // Policy-based: compute resolution dynamically from visible window
  ... (truncated)
  +18 -12

web/web.go
  @@ -72,6 +72,8 @@
  +	ResampleAlgo    string   `json:"resample-algo"`
  +	ResamplePolicy  string   `json:"resample-policy"`
   }

   var UIDefaults = WebConfig{
  @@ -144,6 +146,8 @@
  +	UIDefaultsMap["plotConfiguration_resampleAlgo"] = UIDefaults.PlotConfiguration.ResampleAlgo
  +	UIDefaultsMap["plotConfiguration_resamplePolicy"] = UIDefaults.PlotConfiguration.ResamplePolicy

   	for _, c := range UIDefaults.MetricConfig.Clusters {
   		if c.JobListMetrics != nil {
  +4 -0


cd /Users/jan/prg/ClusterCockpit/cc-backend && git show c0d2d65f