{% extends "base.html" %}{% block content %}
<script>
  const { baseurl, scheduleNextRefresh, recordResponseTime, startAutorefresh, stopAutorefresh } = initAutorefresh(updateJobsData, "{{ protocol }}", "{{ prefix }}");

  function escapeHtml(s) {
    if (s == null) return '';
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }

  function shortId(id) {
    return id && id.length > 8 ? id.substring(0, 8) : id;
  }

  function timeAgo(dateStr) {
    var date = new Date(dateStr);
    var now = new Date();
    var seconds = Math.floor((now - date) / 1000);
    if (seconds < 60) return seconds + "s ago";
    var minutes = Math.floor(seconds / 60);
    if (minutes < 60) return minutes + "m ago";
    var hours = Math.floor(minutes / 60);
    if (hours < 24) return hours + "h ago";
    var days = Math.floor(hours / 24);
    if (days < 30) return days + "d ago";
    return date.toLocaleString();
  }

  function showNotification(message) {
    var notification = document.createElement("div");
    notification.className = "notification";
    notification.textContent = message;
    document.body.appendChild(notification);
    setTimeout(function () { document.body.removeChild(notification); }, 3000);
  }

  function showErrorNotification(message) {
    var notification = document.createElement("div");
    notification.className = "error-notification";
    notification.textContent = message;
    document.body.appendChild(notification);
    setTimeout(function () { document.body.removeChild(notification); }, 3000);
  }

  let page = 1;
  let perPage = parseInt(localStorage.getItem('jobs_per_page')) || {{ per_page if per_page is defined else 10 }};
  let totalPages = {{ total_pages }};
  let totalJobs = {{ total }};

  let sortCol = 'created_at';
  let sortDir = 'desc';

  function sortJobs(jobs) {
    return jobs.slice().sort(function (a, b) {
      var valA, valB;
      if (sortCol === 'name') {
        valA = (a.job.name || '').toLowerCase();
        valB = (b.job.name || '').toLowerCase();
        if (valA < valB) return sortDir === 'asc' ? -1 : 1;
        if (valA > valB) return sortDir === 'asc' ? 1 : -1;
        return 0;
      } else {
        valA = a.job.created_at ? new Date(a.job.created_at).getTime() : 0;
        valB = b.job.created_at ? new Date(b.job.created_at).getTime() : 0;
        return sortDir === 'asc' ? valA - valB : valB - valA;
      }
    });
  }

  function setSort(col) {
    if (sortCol === col) {
      sortDir = sortDir === 'asc' ? 'desc' : 'asc';
    } else {
      sortCol = col;
      sortDir = col === 'created_at' ? 'desc' : 'asc';
    }
    renderSortIndicators();
    updateJobsData();
  }

  function renderSortIndicators() {
    ['name', 'created_at'].forEach(function (col) {
      var el = document.getElementById('sort-' + col);
      if (!el) return;
      var th = el.closest('th');
      if (sortCol === col) {
        el.textContent = sortDir === 'asc' ? ' ↑' : ' ↓';
        th && th.classList.add('th-sorted');
      } else {
        el.textContent = ' ↕';
        th && th.classList.remove('th-sorted');
      }
    });
  }

  function goToPage(p) {
    if (p < 1 || p > totalPages) return;
    page = p;
    updateJobsData();
  }

  function changePerPage(newPerPage) {
    perPage = newPerPage;
    localStorage.setItem('jobs_per_page', newPerPage);
    page = 1;
    updateJobsData();
  }

  function updateJobsData() {
    var selectedState = document.getElementById("state-filter").value;
    var selectedQueue = document.getElementById("queue-name-filter").value;
    var safePage = (Number.isFinite(page) && page > 0) ? page : 1;
    var safePerPage = (Number.isFinite(perPage) && perPage > 0) ? perPage : 10;
    page = safePage;
    perPage = safePerPage;
    var t0 = performance.now();
    fetch(baseurl + "/jobs/json?state=" + selectedState + "&queue_name=" + selectedQueue + "&page=" + safePage + "&per_page=" + safePerPage + "&sort_by=" + sortCol + "&sort_dir=" + sortDir)
      .then(function (r) {
        if (!r.ok) throw new Error("HTTP " + r.status);
        return r.json();
      })
      .then(function (response) {
        recordResponseTime(performance.now() - t0);
        totalPages = response.total_pages;
        totalJobs = response.total;
        page = response.page;
        perPage = response.per_page;
        updateJobsTable(response.data);
        renderPagination();
        scheduleNextRefresh();
      })
      .catch(function (error) {
        recordResponseTime(performance.now() - t0);
        console.error("Error fetching jobs data: ", error);
        showErrorNotification("Error fetching jobs data");
        scheduleNextRefresh();
      });
  }

  function renderPagination() {
    var container = document.getElementById("pagination");
    container.innerHTML = "";

    // Page navigation (only when more than 1 page)
    if (totalPages > 1) {
      // Previous button
      var prevBtn = document.createElement("button");
      prevBtn.className = "btn-pagination" + (page <= 1 ? " opacity-40 cursor-not-allowed" : "");
      prevBtn.disabled = page <= 1;
      prevBtn.title = "Previous page";
      prevBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>';
      prevBtn.addEventListener("click", function () { goToPage(page - 1); });
      container.appendChild(prevBtn);

      // Page numbers
      var pages = getPageNumbers(page, totalPages);
      pages.forEach(function (p) {
        if (p === "...") {
          var ellipsis = document.createElement("span");
          ellipsis.className = "text-sm text-gray-400 dark:text-gray-500 px-0.5 select-none";
          ellipsis.textContent = "...";
          container.appendChild(ellipsis);
        } else {
          var btn = document.createElement("button");
          btn.textContent = p;
          if (p === page) {
            btn.className = "inline-flex items-center justify-center min-w-9 h-9 px-2 rounded-md text-sm font-medium bg-blue-600 text-white dark:bg-blue-500 cursor-default";
          } else {
            btn.className = "inline-flex items-center justify-center min-w-9 h-9 px-2 rounded-md text-sm font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 cursor-pointer border-none transition-colors duration-150";
            btn.addEventListener("click", (function (pageNum) {
              return function () { goToPage(pageNum); };
            })(p));
          }
          container.appendChild(btn);
        }
      });

      // Next button
      var nextBtn = document.createElement("button");
      nextBtn.className = "btn-pagination" + (page >= totalPages ? " opacity-40 cursor-not-allowed" : "");
      nextBtn.disabled = page >= totalPages;
      nextBtn.title = "Next page";
      nextBtn.innerHTML = '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>';
      nextBtn.addEventListener("click", function () { goToPage(page + 1); });
      container.appendChild(nextBtn);
    }

    // Per-page selector (always visible)
    var perPageWrapper = document.createElement("span");
    perPageWrapper.className = (totalPages > 1 ? "ml-6 " : "") + "flex items-center gap-2";
    var perPageLabel = document.createElement("span");
    perPageLabel.className = "text-xs text-gray-500 dark:text-gray-400";
    perPageLabel.textContent = "Show";
    perPageWrapper.appendChild(perPageLabel);
    var perPageSelect = document.createElement("select");
    perPageSelect.className = "text-xs px-2.5 py-1.5 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 cursor-pointer";
    [10, 50, 100].forEach(function (val) {
      var opt = document.createElement("option");
      opt.value = val;
      opt.textContent = val;
      if (val === perPage) opt.selected = true;
      perPageSelect.appendChild(opt);
    });
    perPageSelect.addEventListener("change", function () { changePerPage(Number(this.value)); });
    perPageWrapper.appendChild(perPageSelect);
    container.appendChild(perPageWrapper);

    // Total count (always visible)
    var info = document.createElement("span");
    info.className = "text-xs text-gray-500 dark:text-gray-400 ml-4";
    info.textContent = totalJobs + " job" + (totalJobs !== 1 ? "s" : "");
    container.appendChild(info);
  }

  function getPageNumbers(current, total) {
    if (total <= 7) {
      var arr = [];
      for (var i = 1; i <= total; i++) arr.push(i);
      return arr;
    }
    var pages = [];
    pages.push(1);
    if (current > 3) pages.push("...");
    var start = Math.max(2, current - 1);
    var end = Math.min(total - 1, current + 1);
    for (var i = start; i <= end; i++) pages.push(i);
    if (current < total - 2) pages.push("...");
    pages.push(total);
    return pages;
  }

  var svgView = '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>';
  var svgRequeue = '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M1 4v6h6M23 20v-6h-6"/><path stroke-linecap="round" stroke-linejoin="round" d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15"/></svg>';
  var svgDelete = '<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14"/></svg>';

  function updateJobsTable(data) {
    var tableBody = document.querySelector("#jobs tbody");
    tableBody.innerHTML = "";

    var isAdmin = "{{ access }}" === "admin";
    var states = ["scheduled", "queued", "started", "failed", "deferred", "finished", "canceled", "stopped"];

    var allRows = [];
    data.forEach(function (queue) {
      states.forEach(function (state) {
        if (!queue[state]) return;
        queue[state].forEach(function (job) {
          allRows.push({ queueName: queue.queue_name, state: state, job: job });
        });
      });
    });
    if (sortCol === 'name') allRows = sortJobs(allRows);

    allRows.forEach(function (row) {
      var job = row.job;
      var state = row.state;
      var safeId = escapeHtml(job.id);
      var safeName = escapeHtml(job.name);
      var safeQueue = escapeHtml(row.queueName);
      var fullDate = new Date(job.created_at).toLocaleString();
      var relDate = timeAgo(job.created_at);
      var endedHtml = '';
      if (job.ended_at) {
        var endedFull = new Date(job.ended_at).toLocaleString();
        var endedLabel = (state === "failed") ? "Failed" : "Ended";
        endedHtml = '<div class="text-xs text-gray-400 dark:text-gray-500" title="' + endedFull + '">' + endedLabel + ' ' + timeAgo(job.ended_at) + '</div>';
      }

      var viewBtn = '<a class="btn-view" href="' + baseurl + '/job/' + encodeURIComponent(job.id) + '" title="View details">' + svgView + '</a>';
      var requeueBtn = (isAdmin && state === "failed")
        ? '<button class="btn-requeue" data-requeue-id="' + safeId + '" title="Requeue">' + svgRequeue + '</button>'
        : '<button class="btn-skeleton" disabled title="Requeue (failed jobs only)">' + svgRequeue + '</button>';
      var deleteBtn = isAdmin
        ? '<button class="btn-delete" data-delete-id="' + safeId + '" title="Delete">' + svgDelete + '</button>'
        : '<button class="btn-skeleton" disabled title="Delete (admin only)">' + svgDelete + '</button>';

      var safeShortId = escapeHtml(shortId(job.id));
      tableBody.innerHTML +=
        '<tr class="tr-stripe">' +
        '<td class="td-cell max-w-[24rem] min-w-0">' +
        '<div class="flex min-w-0 items-center gap-2">' +
        '<span class="min-w-0 flex-1 truncate block" title="' + safeName + '">' + safeName + '</span>' +
        '<span class="shrink-0 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap" title="' + safeId + '">' + safeShortId + '</span>' +
        '</div></td>' +
        '<td class="td-cell"><span class="badge-' + state + '">' + state + '</span></td>' +
        '<td class="td-cell">' + safeQueue + '</td>' +
        '<td class="td-cell whitespace-nowrap"><div title="' + fullDate + '">' + relDate + '</div>' + endedHtml + '</td>' +
        '<td class="td-cell"><div class="flex items-center gap-1">' + viewBtn + requeueBtn + deleteBtn + '</div></td>' +
        '</tr>';
    });

    tableBody.querySelectorAll('[data-delete-id]').forEach(function (btn) {
      btn.addEventListener('click', function () { deleteJob(this.getAttribute('data-delete-id'), this); });
    });
    tableBody.querySelectorAll('[data-requeue-id]').forEach(function (btn) {
      btn.addEventListener('click', function () { requeueJob(this.getAttribute('data-requeue-id'), this); });
    });
  }

  function setBtnLoading(btn) {
    btn.disabled = true;
    btn.classList.add('opacity-50', 'cursor-not-allowed');
    btn.dataset.originalHtml = btn.innerHTML;
    btn.innerHTML = '<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>';
  }

  function resetBtn(btn) {
    btn.disabled = false;
    btn.classList.remove('opacity-50', 'cursor-not-allowed');
    btn.innerHTML = btn.dataset.originalHtml;
  }

  function getCsrfToken() {
    var meta = document.querySelector('meta[name="csrf-token"]');
    return meta ? meta.getAttribute('content') : '';
  }

  function deleteJob(job_id, btn) {
    if (!confirm("Delete job " + shortId(job_id) + "?")) return;
    setBtnLoading(btn);
    fetch(baseurl + "/job/" + job_id, { method: "DELETE", headers: {"X-CSRF-Token": getCsrfToken()} })
      .then(function (r) {
        if (!r.ok) { showErrorNotification("Error deleting job"); resetBtn(btn); }
        else { updateJobsData(); }
      })
      .catch(function (error) {
        console.error("Error deleting job: ", error);
        showErrorNotification("Error deleting job");
        resetBtn(btn);
      });
  }

  function cleanupStaleJobs() {
    var selectedQueue = document.getElementById("queue-name-filter").value;
    var queueLabel = selectedQueue === "all" ? "all queues" : "queue '" + selectedQueue + "'";
    if (!confirm("Remove stale job IDs from " + queueLabel + "?\n\nThis deletes registry entries whose job data has already been evicted from Redis. The operation cannot be undone.")) return;
    var btn = document.getElementById("cleanup-btn");
    setBtnLoading(btn);
    fetch(baseurl + "/queues/" + encodeURIComponent(selectedQueue) + "/cleanup-stale", {
      method: "POST",
      headers: {"X-CSRF-Token": getCsrfToken()}
    })
    .then(function(r) {
      if (!r.ok) throw new Error("HTTP " + r.status);
      return r.json();
    })
    .then(function(result) {
      resetBtn(btn);
      showNotification("Removed " + result.removed + " stale job ID" + (result.removed !== 1 ? "s" : ""));
      updateJobsData();
    })
    .catch(function(error) {
      console.error("Error cleaning up stale jobs:", error);
      showErrorNotification("Error cleaning up stale jobs");
      resetBtn(btn);
    });
  }

  function requeueJob(job_id, btn) {
    if (!confirm("Requeue job " + shortId(job_id) + "?")) return;
    setBtnLoading(btn);
    fetch(baseurl + "/job/" + job_id + "/requeue", { method: "POST", headers: {"X-CSRF-Token": getCsrfToken()} })
      .then(function (r) {
        if (!r.ok) { showErrorNotification("Error requeueing job"); resetBtn(btn); }
        else { updateJobsData(); }
      })
      .catch(function (error) {
        console.error("Error requeueing job: ", error);
        showErrorNotification("Error requeueing job");
        resetBtn(btn);
      });
  }

  document.addEventListener("DOMContentLoaded", function () {
    if (window.autoRefreshEnabled) { startAutorefresh(); }

    var urlParams = new URLSearchParams(window.location.search);
    var defaultState = urlParams.get("state") || "all";
    document.getElementById("state-filter").value = defaultState;
    var defaultQueue = urlParams.get("queue_name") || "all";
    document.getElementById("queue-name-filter").value = defaultQueue;

    document.getElementById("state-filter").addEventListener("change", function () { page = 1; updateJobsData(); });
    document.getElementById("queue-name-filter").addEventListener("change", function () { page = 1; updateJobsData(); });

    renderSortIndicators();
    renderPagination();
    updateJobsData();
  });

  document.addEventListener("autorefresh-changed", function (e) {
    if (e.detail.enabled) {
      startAutorefresh();
      showNotification("Autorefresh enabled");
    } else {
      stopAutorefresh();
      showNotification("Autorefresh disabled");
    }
  });
</script>

<div class="max-w-[90%] mx-auto mt-6 px-4">
  <div class="flex items-center justify-between gap-4 flex-wrap">
    <p class="text-sm text-gray-600 dark:text-gray-400">
      Filter jobs by state and queue. Hover over timestamps for the full date, over the job name for the full name, or over the short ID for the full job id.
    </p>
    {% if access == "admin" %}
    <button id="cleanup-btn" onclick="cleanupStaleJobs()" class="btn-export shrink-0" title="Remove job IDs from registries whose backing data has been evicted from Redis">
      Clean up stale jobs
    </button>
    {% endif %}
  </div>
  <table id="jobs" class="data-table table-fixed w-full">
    <thead>
      <tr>
        <th class="th-cell cursor-pointer select-none" onclick="setSort('name')" title="Sort by name">
          Name<span id="sort-name" class="text-gray-400 dark:text-gray-500 text-xs"> ↕</span>
        </th>
        <th class="th-cell">
          <div class="flex flex-col gap-1">
            <span>Status</span>
            <select id="state-filter" class="text-xs font-normal normal-case tracking-normal px-1.5 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 cursor-pointer">
              <option value="all">All</option>
              <option value="scheduled">Scheduled</option>
              <option value="queued">Queued</option>
              <option value="started">Started</option>
              <option value="failed">Failed</option>
              <option value="deferred">Deferred</option>
              <option value="finished">Finished</option>
              <option value="canceled">Canceled</option>
              <option value="stopped">Stopped</option>
            </select>
          </div>
        </th>
        <th class="th-cell">
          <div class="flex flex-col gap-1">
            <span>Queue</span>
            <select id="queue-name-filter" class="text-xs font-normal normal-case tracking-normal px-1.5 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 cursor-pointer">
              <option value="all">All queues</option>
              {% for queue in job_data %}
              <option value="{{ queue.queue_name }}">{{ queue.queue_name }}</option>
              {% endfor %}
            </select>
          </div>
        </th>
        <th class="th-cell cursor-pointer select-none" onclick="setSort('created_at')" title="Sort by date">
          Created<span id="sort-created_at" class="text-gray-400 dark:text-gray-500 text-xs"> ↓</span>
        </th>
        <th class="th-cell">Actions</th>
      </tr>
    </thead>
    <tbody>
      {% for queue in job_data %} {% for state in ["queued", "started",
      "failed", "deferred", "scheduled", "finished", "canceled", "stopped"] %} {% for job in queue[state] %}
      <tr class="tr-stripe">
        <td class="td-cell max-w-[24rem] min-w-0">
          <div class="flex min-w-0 items-center gap-2">
            <span class="min-w-0 flex-1 truncate block" title="{{ job.name | e }}">{{ job.name }}</span>
            <span class="shrink-0 text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap" title="{{ job.id | e }}">{{ job.id[:8] }}</span>
          </div>
        </td>
        <td class="td-cell"><span class="badge-{{ state }}">{{ state }}</span></td>
        <td class="td-cell">{{ queue.queue_name }}</td>
        <td class="td-cell whitespace-nowrap">
          <div>{{ job.created_at }}</div>
          {% if job.ended_at %}
          <div class="text-xs text-gray-400 dark:text-gray-500">{{ "Failed" if state == "failed" else "Ended" }} {{ job.ended_at }}</div>
          {% endif %}
        </td>
        <td class="td-cell">
          <div class="flex items-center gap-1">
            <a class="btn-view" href="#" data-job-id="{{ job.id }}" title="View details">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
            </a>
            {% if access == "admin" and state == "failed" %}
            <button class="btn-requeue" data-requeue-ssr="{{ job.id }}" title="Requeue">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M1 4v6h6M23 20v-6h-6"/><path stroke-linecap="round" stroke-linejoin="round" d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15"/></svg>
            </button>
            {% else %}
            <button class="btn-skeleton" disabled title="Requeue (failed jobs only)">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M1 4v6h6M23 20v-6h-6"/><path stroke-linecap="round" stroke-linejoin="round" d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15"/></svg>
            </button>
            {% endif %}
            {% if access == "admin" %}
            <button class="btn-delete" data-delete-ssr="{{ job.id }}" title="Delete">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14"/></svg>
            </button>
            {% else %}
            <button class="btn-skeleton" disabled title="Delete (admin only)">
              <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14"/></svg>
            </button>
            {% endif %}
          </div>
        </td>
      </tr>
      {% endfor %} {% endfor %} {% endfor %}
    </tbody>
  </table>
  <div id="pagination" class="flex justify-center mt-6 mb-4 gap-2 items-center py-3"></div>
</div>

<script>
  document.addEventListener("DOMContentLoaded", function () {
    // Wire up SSR view links
    document.querySelectorAll("a[data-job-id]").forEach(function (link) {
      var jobId = link.getAttribute("data-job-id");
      link.href = baseurl + "/job/" + jobId;
    });
    // Wire up SSR action buttons with confirmations
    document.querySelectorAll("[data-delete-ssr]").forEach(function (btn) {
      btn.addEventListener("click", function () { deleteJob(this.getAttribute("data-delete-ssr"), this); });
    });
    document.querySelectorAll("[data-requeue-ssr]").forEach(function (btn) {
      btn.addEventListener("click", function () { requeueJob(this.getAttribute("data-requeue-ssr"), this); });
    });
  });
</script>
{% endblock %}
