cratery 1.11.1

Cratery -- a private cargo registry
<!DOCTYPE html>
<html lang="en" class="dark">

<head>
  <meta charset="UTF-8">
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/png" href="/webapp/favicon.png">
  <title>
    Cratery -- Documentation generation jobs
  </title>
  <script src="https://cdn.tailwindcss.com"></script>
</head>

<header style="position: sticky; top: 0;">
  <nav class="bg-white border-gray-200 px-4 lg:px-6 py-2.5 dark:bg-gray-800">
      <div class="flex flex-wrap justify-between items-center mx-auto max-w-screen-xl">
          <a href="/webapp/index.html" class="flex items-center">
              <picture>
                  <source srcset="./logo-white.svg" media="(prefers-color-scheme: dark)" />
                  <source srcset="./logo-black.svg" media="(prefers-color-scheme: light)" />
                  <img src="./logo-white.svg" class="mr-3 h-6 sm:h-9" style="min-width: 200px;" alt="Cratery Logo" />
              </picture>
          </a>
          <div class="flex items-center lg:order-2">
            <a id="link-admin" href="/webapp/admin.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Admin</a>
            <a id="link-account" href="/webapp/account.html" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">My Account</a>
            <a onclick="doLogout()" style="cursor: pointer;" class="text-gray-800 dark:text-white hover:bg-gray-50 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-4 lg:px-5 py-2 lg:py-2.5 mr-2 dark:hover:bg-gray-700 focus:outline-none dark:focus:ring-gray-800">Logout</a>
          </div>
      </div>
  </nav>
</header>
<body onload="doPageLoad()" class="bg-white dark:bg-gray-800">
  <section class="bg-white dark:bg-gray-900">
    <div class="p-2 flex flex-row flex-wrap">
      <a href="/webapp/admin.html" class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" style="display: inline-block;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
        </svg>
       Back to admin
      </a>
    </div>
    <div class="py-4 lg:py-4 px-4 mx-auto flex flew-row">
      <div class="flex-none w-1/4">
        <h2 class="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">Workers</h2>
        <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
          <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
            <tr>
              <th scope="col" class="px-6 py-3">
                  Node
              </th>
            </tr>
          </thead>
          <tbody id="workers">
          </tbody>
      </table>
      </div>
      <div class="flex-1 ml-1 w-3/4">
        <h2 class="mb-4 text-4xl tracking-tight font-extrabold text-center text-gray-900 dark:text-white">Documentation generation jobs</h2>
        <div class="relative overflow-x-auto space-y-8">
          <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
              <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
                <tr>
                  <th scope="col" class="px-6 py-3">
                      Crate / Version
                  </th>
                  <th scope="col" class="px-6 py-3">
                      Status
                  </th>
                  <th scope="col" class="px-6 py-3">
                      Updates
                  </th>
                  <th scope="col" class="px-6 py-3">
                      Trigger
                  </th>
                  <th scope="col" class="px-6 py-3">
                      Actions
                  </th>
                </tr>
              </thead>
              <tbody id="jobs">
              </tbody>
          </table>
        </div>
      </div>
    </div>
  </section>
  <div id="modal-log" tabindex="-1" class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 w-full md:inset-0 h-modal md:h-full" style="display: none;">
    <div class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-51 w-full md:inset-0 h-modal md:h-full" style="background-color: black; opacity: 0.75;"></div>
    <div class="relative" style="margin: auto; margin-top: 10%; width: 800px;">
        <div class="relative p-4 bg-white rounded-lg shadow dark:bg-gray-800 md:p-8">
            <div class="mb-4 text-sm font-light text-gray-500 dark:text-gray-400">
              <h3 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">Log</h3>
            </div>
            <form class="mb-3 space-y-8">
              <textarea id="modal-log-value" rows="10" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"></textarea>
            </form>
            <div class="justify-between items-center pt-0 space-y-4 sm:flex sm:space-y-0">
              <div class="items-center space-y-4 sm:space-x-4 sm:flex sm:space-y-0">
                <button id="modal-log-close" type="button"  class="py-2 px-4 w-full text-sm font-medium text-gray-500 bg-white rounded-lg border border-gray-200 sm:w-auto hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-primary-300 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">Close</button>
              </div>
            </div>
        </div>
    </div>
  </div>
</body>
<footer class="p-4 bg-white md:p-8 lg:p-10 dark:bg-gray-800">
  <div class="mx-auto max-w-screen-xl text-center">
      <span class="text-sm text-gray-500 sm:text-center dark:text-gray-400">Version <span id="version"></span>, Copyright © <span id="year"></span> <a href="https://cenotelie.fr/" target="_blank" class="hover:underline">Cénotélie</a>. All Rights Reserved.</span>
  </div>
</footer>

<link href="/webapp/index.css" rel="stylesheet" />
<script src="/webapp/api.js"></script>
<script src="/webapp/index.js"></script>
<script src="/webapp/stream.js"></script>
<script>
    function doPageLoad() {
      onPageLoad().then((_user) => {
        apiGetDocGenJobs().then(jobs => {
          const jobsEl = document.getElementById("jobs");
          for (const job of jobs) {
            jobsEl.appendChild(renderJob(job));
          }
          // listen for job updates
          const sourceForJobs = new StreamEventSource("/api/v1/admin/jobs/docgen/updates", null);
          sourceForJobs.map(message => JSON.parse(message.data)).then((event) => onJobEvent(event)).catch(() => {});
          // get workers
          apiGetWorkers().then((workers) => {
            const table = document.getElementById("workers");
            for (const worker of workers) {
              table.appendChild(renderWorker(worker));
            }
          });
          // listen for worker updates
          const sourceForWorkers = new StreamEventSource("/api/v1/admin/workers/updates", null);
          sourceForWorkers.map(message => JSON.parse(message.data)).then((event) => onWorkerEvent(event)).catch(() => {});
        });
      });
    }

    function renderWorker(worker) {
      const detailsState = {hidden : true};
      const detailsEl = document.createElement("div");
      detailsEl.appendChild(document.createTextNode(`id: ${worker.descriptor.identifier}`));
      detailsEl.appendChild(document.createElement("br"));
      detailsEl.appendChild(document.createTextNode(`host: ${worker.descriptor.toolchainHost}`));
      detailsEl.appendChild(document.createElement("br"));
      detailsEl.appendChild(document.createTextNode(`versions: ${worker.descriptor.toolchainVersionStable}, ${worker.descriptor.toolchainVersionNightly	}`));
      detailsEl.style.display = "none";

      const buttonMinus = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>';
      const buttonPlus = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>';
      const button = document.createElement("span");
      button.style.cursor = "pointer";
      button.style.display = "inline-block";
      button.innerHTML = buttonPlus;
      button.onclick = () => {
        detailsState.hidden = !detailsState.hidden;
        if (detailsState.hidden) {
          detailsEl.style.display = "none";
          button.innerHTML = buttonPlus;
        } else {
          detailsEl.style.display = "unset";
          button.innerHTML = buttonMinus;
        }
      };

      const title = document.createElement("span");
      title.style.display = "inline-block";
      title.style.marginLeft = "5px";
      title.appendChild(document.createTextNode(worker.descriptor.name));

      const statusWrapper = document.createElement("span");
      statusWrapper.id = `worker-state-${worker.descriptor.identifier}`;
      statusWrapper.className = "mx-1";
      statusWrapper.appendChild(renderWorkerStatus(worker.state));
    
      const row = document.createElement("tr");
      row.id = `worker-${worker.descriptor.identifier}`;
      const cell1 = document.createElement("td");
      cell1.setAttribute("scope", "row");
      cell1.className = "px-6 py-4";
      cell1.title = `worker id: ${worker.descriptor.identifier}`;
      cell1.appendChild(button);
      cell1.appendChild(title);
      cell1.appendChild(statusWrapper);
      cell1.appendChild(document.createElement("br"));
      cell1.appendChild(detailsEl);

      row.appendChild(cell1);
      return row;
    }

    function renderWorkerStatus(state) {
      const color = state === "Available" ? "green" : "blue";
      const statusEl = document.createElement("span");
      statusEl.className = `bg-${color}-100 text-${color}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-${color}-900 dark:text-${color}-300`;
      statusEl.style.display = "inline-block";
      statusEl.appendChild(document.createTextNode(state === "Available" ? "Available" : "Working ..."));
      return statusEl;
    }

    function renderJob(job) {
      const linkEl = document.createElement("a");
      linkEl.className = "font-medium text-blue-600 dark:text-blue-500 hover:underline";
      linkEl.setAttribute("href", `/crates/${job.package}/${job.version}`);
      linkEl.appendChild(document.createTextNode(`${job.package} ${job.version}`));

      const row = document.createElement("tr");
      const cell1 = document.createElement("td");
      cell1.setAttribute("scope", "row");
      cell1.className = "px-6 py-4";
      cell1.appendChild(linkEl);
      cell1.appendChild(document.createElement("br"));
      cell1.appendChild(document.createTextNode(`(${job.target})`));
      const cell2 = document.createElement("td");
      cell2.setAttribute("id", `job-cell-status-${job.id}`);
      cell2.className = "px-6 py-4";
      cell2.appendChild(renderJobStatus(job.id, job.state));
      const cell3 = document.createElement("td");
      cell3.setAttribute("id", `job-cell-updates-${job.id}`);
      cell3.className = "px-6 py-4";
      const cell4 = document.createElement("td");
      cell4.className = "px-6 py-4";
      const cell5 = document.createElement("td");
      cell5.className = "px-6 py-4";

      cell3.appendChild(renderJobUpdate(job.queuedOn, "queued"));
      if (job.state === "Working" || job.state === "Success" || job.state === "Failure") {
        cell3.appendChild(renderJobUpdate(job.startedOn, "started work"));
      }
      if (job.state === "Success") {
        cell3.appendChild(renderJobUpdate(job.finishedOn, "finished on success"));
      }
      if (job.state === "Failure") {
        cell3.appendChild(renderJobUpdate(job.finishedOn, "finished on failure"));
      }

      cell4.appendChild(renderTrigger(job.trigger));

      if (job.state === "Failure") {
        const refreshEl = document.createElement("span");
        refreshEl.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">\
            <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />\
          </svg>';
        refreshEl.title = "Retry";
        refreshEl.style.cursor = "pointer";
        refreshEl.style.display = "inline-block";
        refreshEl.onclick = () => {
          apiRegenCrateDoc(job.package, job.version).then(() => {
            window.location.reload();
          });
        };
        cell5.appendChild(refreshEl);
      }

      row.appendChild(cell1);
      row.appendChild(cell2);
      row.appendChild(cell3);
      row.appendChild(cell4);
      row.appendChild(cell5);
      return row;
    }

    function renderJobStatus(jobId, jobState) {
      const color = getJobStatusColor(jobState);
      const statusEl = document.createElement("span");
      statusEl.className = `bg-${color}-100 text-${color}-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-${color}-900 dark:text-${color}-300`;
      statusEl.style.display = "inline-block";
      statusEl.appendChild(document.createTextNode(getJobStatusText(jobState)));
      if (jobState === "Failure" || jobState === "Success") {
        statusEl.style.cursor = "pointer";
        statusEl.onclick = () => {
          apiGetDocGenJobLog(jobId).then((log) => {
            document.getElementById('modal-log-value').value = log;
          }).catch((error) => {
            document.getElementById('modal-log-value').value = JSON.stringify(error);
          });
          const closeEl = document.getElementById('modal-log-close');
          const modalEl = document.getElementById('modal-log');
          closeEl.addEventListener('click', () => {
            modalEl.style.display = "none";
          });
          modalEl.style.display = "unset";
        };
      }
      return statusEl;
    }

    function renderJobUpdate(timestamp, text) {
      const element = document.createElement("span");
      element.className = "text-base text-gray-900 dark:text-white";
      element.style.display = "block";
      element.appendChild(document.createTextNode(`${serializeDateTime(timestamp)}: ${text}`));
      return element;
    }

    function renderTrigger(trigger) {
      if (trigger === "MissingOnLaunch") {
        const element = document.createElement("span");
        element.className = "text-base text-gray-900 dark:text-white";
        element.appendChild(document.createTextNode("doc missing on launch"));
        return element;
      }
      const keys = Object.getOwnPropertyNames(trigger);
      if (keys.includes("Upload") || keys.includes("NewTarget") || keys.includes("Manual")) {
        const wrapper = document.createElement("div");
        wrapper.className = "flex items-center me-4";
        const element = document.createElement("p");
        element.className = "text-base text-gray-900 dark:text-white";
        element.appendChild(document.createTextNode(keys.includes("Upload") ? "on upload by " : keys.includes("NewTarget") ? "new target added by " : "on request from "));
        wrapper.appendChild(element);
        wrapper.innerHTML += '<p class="ml-4 text-base text-gray-900 dark:text-white">\
                  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">\
                    <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />\
                  </svg>\
                </p>';
        const user = document.createElement("p");
        user.className = "text-base text-gray-900 dark:text-white";
        user.appendChild(document.createTextNode(trigger[keys.includes("Upload") ? "Upload" : keys.includes("NewTarget") ? "NewTarget" : "Manual"].by.name));
        wrapper.appendChild(user);
        return wrapper;
      }
      return null;
    }

    function onWorkerEvent(event) {
      const keys = Object.getOwnPropertyNames(event);
      if (keys.includes("WorkerConnected")) {
        const table = document.getElementById("workers");
        table.appendChild(renderWorker(event.WorkerConnected));
      } else if (keys.includes("WorkerRemoved")) {
        const row = document.getElementById(`worker-${event.WorkerRemoved.worker_id}`);
        row.remove();
      } else if (keys.includes("WorkerStartedJob")) {
        const statusWrapper = document.getElementById(`worker-state-${event.WorkerStartedJob.worker_id}`);
        removeAllChildren(statusWrapper);
        statusWrapper.appendChild(renderWorkerStatus("InUse"));
      } else if (keys.includes("WorkerAvailable")) {
        const statusWrapper = document.getElementById(`worker-state-${event.WorkerAvailable.worker_id}`);
        removeAllChildren(statusWrapper);
        statusWrapper.appendChild(renderWorkerStatus("Available"));
      }
    }

    function onJobEvent(event) {
      const keys = Object.getOwnPropertyNames(event);
      if (keys.includes("Queued")) {
        const jobsEl = document.getElementById("jobs");
        jobsEl.insertBefore(renderJob(event.Queued), jobsEl.firstElementChild);
      } else if (keys.includes("Update")) {
        // update an existing row
        const cell2 = document.getElementById(`job-cell-status-${event.Update.jobId}`);
        removeAllChildren(cell2);
        cell2.appendChild(renderJobStatus(event.Update.jobId, event.Update.state));
        const cell3 = document.getElementById(`job-cell-updates-${event.Update.jobId}`);
        if (event.Update.state === "Working") {
          cell3.appendChild(renderJobUpdate(event.Update.lastUpdate, "started work"));
        }
        if (event.Update.state === "Success") {
          cell3.appendChild(renderJobUpdate(event.Update.lastUpdate, "finished on success"));
        }
        if (event.Update.state === "Failure") {
          cell3.appendChild(renderJobUpdate(event.Update.lastUpdate, "finished on failure"));
        }
      }
    }
</script>
</html>