worker-service 0.2.0

Worker Service - A worker administration microservice that interoperates with the worker-matcher crate
{% extends "layout.html.tera" %}

{% block title %}Review queue — {{ entity_plural | capitalize }} — {{ app_display }}{% endblock title %}

{% block content %}
<nav class="breadcrumb-nav" aria-label="Breadcrumb">
  <ol class="breadcrumb-list" aria-label="Breadcrumb trail">
    <li class="breadcrumb-list-item"><a href="/">Home</a></li>
    <li class="breadcrumb-list-item"><a href="/{{ entity_plural }}">{{ entity_plural | capitalize }}</a></li>
    <li class="breadcrumb-list-item" aria-current="page">Review queue</li>
  </ol>
</nav>

<section aria-label="Duplicate review queue">
  <h2>Duplicate review queue</h2>
  <p>
    <span class="hint" aria-label="Queue summary">
      {{ stats.pending }} pending &middot;
      {{ stats.confirmed }} confirmed &middot;
      {{ stats.rejected }} rejected &middot;
      {{ stats.auto_merged }} auto-merged
    </span>
  </p>

  <p>
    <a class="button"
       href="/{{ entity_plural }}/review-queue/kanban"
       aria-label="Switch to kanban board view">Kanban view</a>
    <a class="button"
       href="/{{ entity_plural }}/deduplicate"
       aria-label="Run a batch deduplication scan">Run dedup scan</a>
  </p>

  <form class="form"
        aria-label="Filter review queue"
        hx-get="/{{ entity_plural }}/review-queue"
        hx-target="#review-queue-tbody"
        hx-select="#review-queue-tbody"
        hx-swap="outerHTML"
        hx-trigger="change">
    <div class="field" aria-label="Status filter field">
      <label class="label" for="status-filter">Status</label>
      <select class="select"
              id="status-filter"
              name="status"
              aria-label="Filter by review status">
        <option class="option" value="Pending" {% if filter_status == "Pending" %}selected{% endif %}>Pending</option>
        <option class="option" value="Confirmed" {% if filter_status == "Confirmed" %}selected{% endif %}>Confirmed</option>
        <option class="option" value="Rejected" {% if filter_status == "Rejected" %}selected{% endif %}>Rejected</option>
        <option class="option" value="AutoMerged" {% if filter_status == "AutoMerged" %}selected{% endif %}>Auto-merged</option>
      </select>
    </div>

    <div class="field" aria-label="Quality filter field">
      <label class="label" for="quality-filter">Match quality</label>
      <select class="select"
              id="quality-filter"
              name="quality"
              aria-label="Filter by match quality">
        <option class="option" value="">Any</option>
        <option class="option" value="definite" {% if filter_quality == "definite" %}selected{% endif %}>Definite</option>
        <option class="option" value="probable" {% if filter_quality == "probable" %}selected{% endif %}>Probable</option>
        <option class="option" value="possible" {% if filter_quality == "possible" %}selected{% endif %}>Possible</option>
        <option class="option" value="unlikely" {% if filter_quality == "unlikely" %}selected{% endif %}>Unlikely</option>
      </select>
    </div>
  </form>

  {% if items | length == 0 %}
    <div class="alert" role="alert" aria-label="Empty review queue" data-type="info">
      No review items match the current filter.
    </div>
  {% else %}
    <table class="data-table" aria-label="Review queue items">
      <thead class="data-table-head">
        <tr class="data-table-row">
          <th class="data-table-th" scope="col">Candidate A</th>
          <th class="data-table-th" scope="col">Candidate B</th>
          <th class="data-table-th" scope="col">Score</th>
          <th class="data-table-th" scope="col">Quality</th>
          <th class="data-table-th" scope="col">Detected</th>
          <th class="data-table-th" scope="col">Actions</th>
        </tr>
      </thead>
      <tbody class="data-table-body" id="review-queue-tbody">
        {% for item in items %}
          <tr class="data-table-row">
            <td class="data-table-td">
              <a href="/{{ entity_plural }}/{{ item.a_id }}">{{ item.a_label }}</a>
            </td>
            <td class="data-table-td">
              <a href="/{{ entity_plural }}/{{ item.b_id }}">{{ item.b_label }}</a>
            </td>
            <td class="data-table-td">
              <meter class="meter"
                     aria-label="Match score {{ item.score_pct }} percent"
                     min="0"
                     max="100"
                     low="50"
                     high="85"
                     optimum="100"
                     value="{{ item.score_pct }}">{{ item.score_pct }}%</meter>
              <div><strong>{{ item.score }}</strong></div>
              {% if item.breakdown %}
                <details class="details" aria-label="Score breakdown">
                  <summary>Breakdown</summary>
                  <ol class="summary-list" aria-label="Per-component scores">
                    {% for k, v in item.breakdown %}
                      <li class="summary-list-item">
                        <dl>
                          <dt>{{ k }}</dt>
                          <dd>{{ v }}</dd>
                        </dl>
                      </li>
                    {% endfor %}
                  </ol>
                </details>
              {% endif %}
            </td>
            <td class="data-table-td">
              {% if item.quality == "definite" %}
                <span class="badge" data-type="success" aria-label="Definite match">Definite</span>
              {% elif item.quality == "probable" %}
                <span class="badge" data-type="warning" aria-label="Probable match">Probable</span>
              {% elif item.quality == "possible" %}
                <span class="badge" data-type="info" aria-label="Possible match">Possible</span>
              {% else %}
                <span class="badge" data-type="error" aria-label="Unlikely match">Unlikely</span>
              {% endif %}
            </td>
            <td class="data-table-td">
              <span class="hint" aria-label="Detection method">{{ item.detection_method }}</span>
              <br>
              <time datetime="{{ item.created_at }}">{{ item.created_at }}</time>
            </td>
            <td class="data-table-td">
              <a class="button"
                 href="/{{ entity_plural }}/compare?a={{ item.a_id }}&b={{ item.b_id }}&review_id={{ item.id }}"
                 aria-label="Open side-by-side comparison">Compare</a>
              <button class="button"
                      type="button"
                      aria-label="Merge candidates"
                      onclick="document.getElementById('merge-confirm-{{ item.id }}').showModal()">
                Merge
              </button>
              <button class="action-bar-button"
                      type="button"
                      aria-label="Reject as not a duplicate"
                      hx-post="/api/{{ entity_plural }}/review-queue/{{ item.id }}/reject"
                      hx-target="closest tr"
                      hx-swap="outerHTML"
                      onclick="lily.toast('Marked as not a duplicate', 'info')">
                Reject
              </button>

              <dialog id="merge-confirm-{{ item.id }}"
                      class="alert-dialog"
                      role="alertdialog"
                      aria-modal="true"
                      aria-labelledby="merge-confirm-{{ item.id }}-title"
                      aria-describedby="merge-confirm-{{ item.id }}-body">
                <h2 id="merge-confirm-{{ item.id }}-title">Merge these {{ entity_plural }}?</h2>
                <p id="merge-confirm-{{ item.id }}-body">
                  This will merge <strong>{{ item.b_label }}</strong> into
                  <strong>{{ item.a_label }}</strong> as the surviving record.
                  Match score: <strong>{{ item.score }}</strong> ({{ item.quality }}).
                  This action is reversible from the audit log.
                </p>
                <form method="dialog" class="form" aria-label="Confirm merge">
                  <button class="button" type="submit" value="cancel" aria-label="Cancel merge">Cancel</button>
                  <button class="button"
                          type="submit"
                          value="confirm"
                          aria-label="Confirm merge"
                          hx-post="/api/{{ entity_plural }}/merge"
                          hx-vals='{"main_id": "{{ item.a_id }}", "duplicate_id": "{{ item.b_id }}", "review_id": "{{ item.id }}"}'
                          hx-target="closest tr"
                          hx-swap="outerHTML"
                          onclick="lily.toast('Merge completed', 'success')">Merge</button>
                </form>
              </dialog>
            </td>
          </tr>
        {% endfor %}
      </tbody>
    </table>

    {% if pagination and pagination.total_pages > 1 %}
      <nav class="pagination-nav" aria-label="Review queue pagination">
        <ol class="pagination-list" aria-label="Page links">
          {% if pagination.has_prev %}
            <li class="pagination-list-item">
              <a class="pagination-link"
                 href="/{{ entity_plural }}/review-queue?page={{ pagination.page - 1 }}"
                 aria-label="Previous page">Previous</a>
            </li>
          {% endif %}
          {% for p in pagination.pages %}
            <li class="pagination-list-item">
              {% if p == pagination.page %}
                <span aria-current="page" aria-label="Page {{ p }}, current">{{ p }}</span>
              {% else %}
                <a class="pagination-link"
                   href="/{{ entity_plural }}/review-queue?page={{ p }}"
                   aria-label="Page {{ p }}">{{ p }}</a>
              {% endif %}
            </li>
          {% endfor %}
          {% if pagination.has_next %}
            <li class="pagination-list-item">
              <a class="pagination-link"
                 href="/{{ entity_plural }}/review-queue?page={{ pagination.page + 1 }}"
                 aria-label="Next page">Next</a>
            </li>
          {% endif %}
        </ol>
      </nav>
    {% endif %}
  {% endif %}
</section>
{% endblock content %}