cadre 0.5.4

Cadre is a simple, self-hosted, high-performance remote configuration service.
Documentation
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <script src="https://unpkg.com/lodash@4.17.21/lodash.js"></script>

    <link
      href="https://unpkg.com/jsoneditor@9.7.4/dist/jsoneditor.min.css"
      rel="stylesheet"
    />
    <script src="https://unpkg.com/jsoneditor@9.7.4/dist/jsoneditor.min.js"></script>

    <style>
      * {
        box-sizing: border-box;
      }

      body {
        max-width: 800px;
        margin: 0 auto;
        padding: 36px 12px;
        color: #3f3f3f;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
          Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
          "Segoe UI Symbol";
        font-size: 18px;
        line-height: 1.5;
      }

      h1 {
        margin: 0 0 24px;
        color: black;
        font-weight: 600;
      }

      h1 small {
        white-space: nowrap;
        font-size: 0.65em;
        color: #a0a0a0;
      }

      a {
        color: #1e1eff;
      }

      pre {
        background: #efefef;
        padding: 12px 16px;
        overflow-x: auto;
      }

      code {
        font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
          monospace;
        font-size: 0.9em;
      }

      button {
        appearance: none;
        border: 0;
        border-radius: 8px;
        padding: 10px 16px;
        background: #c838ff;
        font-weight: 600;
        color: white;
        cursor: pointer;
      }

      button:hover {
        background: #b134e3;
      }

      button:active {
        background: #a31ed7;
      }

      button:disabled {
        background: #e092ff;
        cursor: default;
      }
    </style>

    <title>cadre</title>
  </head>

  <body>
    <h1>cadre <small>configure apps</small></h1>

    <p>
      <em>This is the cadre web interface.</em> To use, edit the JSON
      configuration below. Templates are stored in S3 immediately after saving.
      Secrets are parsed and populated at request time.
    </p>

    <pre><code><span style="color: #7b0091">curl</span> <span id="base-url">$BASE_URL</span>/c/<span style="font-style: italic; color: rgb(179, 80, 0)">$ENVIRONMENT</span></code></pre>

    <p>
      Note that <em>cadre</em> may cache configuration templates and secrets in
      memory for up to a minute after a given request.
    </p>

    <form id="secret-form" style="margin: 20px 0">
      Secret:
      <input id="secret-input" type="text" value="" />
    </form>

    <form id="environment-form" style="margin: 20px 0">
      Environment:
      <input id="environment-input" type="text" value="default" />
    </form>

    <div style="margin: 20px 0; display: flex; align-items: center">
      <button id="submit" disabled>Save Changes</button>
      <div id="indicator" style="margin-left: 10px; font-size: 24px"></div>
      <div
        id="changes"
        style="
          margin-left: 10px;
          font-size: 0.8em;
          font-style: italic;
          display: none;
        "
      >
        (pending changes)
      </div>
    </div>

    <div id="jsoneditor" style="height: 480px"></div>

    <script type="module">
      document.getElementById("base-url").innerText = window.location.origin;

      let submitting = false;
      let clearIndicator = 0;
      const environmentForm = document.getElementById("environment-form");
      const environmentInput = document.getElementById("environment-input");
      const secretForm = document.getElementById("secret-form");
      const secretInput = document.getElementById("secret-input");
      const submit = document.getElementById("submit");
      const indicator = document.getElementById("indicator");
      const changes = document.getElementById("changes");

      // Pass URL parameter secret to form value.
      const queryString = window.location.search;
      const urlParams = new URLSearchParams(queryString);
      secretInput.value = urlParams.get("secret");

      let editor = null;
      let environment = environmentInput.value;
      let secret = secretInput.value;
      let currentValue = {};
      let editorValue = {};

      async function updateEnv() {
        try {
          const resp = await fetch(`/t/${environment}`, {
            headers: { "X-Cadre-Secret": secret },
          });
          let value;
          if (resp.ok) {
            value = await resp.json();
          } else {
            value = {};
          }
          currentValue = editorValue = value;
          editor?.set(value);
        } catch (error) {
          alert(error.toString());
        }
      }

      await updateEnv();
      environmentForm.addEventListener("submit", (event) => {
        event.preventDefault();
        environment = environmentInput.value;
        updateEnv();
      });
      secretForm.addEventListener("submit", (event) => {
        event.preventDefault();
        secret = secretInput.value;
        updateEnv();
      });

      function refresh() {
        submit.disabled = submitting;
        changes.style.display = _.isEqual(currentValue, editorValue)
          ? "none"
          : "block";
      }

      const container = document.getElementById("jsoneditor");
      const options = {
        mode: "code",
        onValidate: (value) => {
          editorValue = value;
          refresh();
        },
      };
      editor = new JSONEditor(container, options, editorValue);

      submit.addEventListener("click", async () => {
        submitting = true;
        clearTimeout(clearIndicator);
        indicator.innerText = "🔵";
        submit.disabled = true;
        try {
          const newValue = editorValue;
          const resp = await fetch(`/t/${environment}`, {
            method: "PUT",
            body: JSON.stringify(newValue),
            headers: {
              "Content-Type": "application/json",
              "X-Cadre-Secret": secret,
            },
          });
          if (resp.status === 200) {
            currentValue = newValue;
            indicator.innerText = "";
          } else {
            alert("Request error: " + resp.status);
            indicator.innerText = "";
          }
          clearIndicator = setTimeout(() => (indicator.innerText = ""), 2500);
        } finally {
          submitting = false;
          refresh();
        }
      });
    </script>
  </body>
</html>