Add redirect support, live candidate list, per-article save count, and error pages

- Detect redirect targets (e.g. "handling stolen goods" → "Possession of
  stolen goods") and use piped links [[target|title]] in edits; exclude
  articles already linking to the redirect target from candidates
- Remove candidates from the list in real time as they are checked and
  found invalid, with live count update in the summary
- Track and display per-article save count in the stats line
- Rename "Find Link" to "Missing Link" throughout
- Show redirect target in the article heading
- Report save errors to the user via error page instead of crashing
- Filter self-links using case-insensitive first-letter comparison

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-05-11 13:53:35 +01:00
parent 0239b83555
commit c9b4e2face
3 changed files with 64 additions and 17 deletions

View file

@ -18,11 +18,17 @@
<div class="d-flex flex-wrap align-items-baseline gap-3 mb-1">
<h1 class="h4 mb-0">Find links to "{{ title }}"</h1>
<a href="https://en.wikipedia.org/wiki/{{ title }}" target="_blank" class="text-muted small">{{ title }} ↗</a>
{% if redirect_to %}
<span class="text-muted small">→ redirects to <a href="https://en.wikipedia.org/wiki/{{ redirect_to }}" target="_blank">{{ redirect_to }} ↗</a></span>
{% endif %}
</div>
<div class="d-flex gap-3 mb-3 text-muted small">
<span>{{ total }} mentions total</span>
<span>{{ with_link }} already linked ({{ "{:.0%}".format(with_link / total) }})</span>
{% if saves_this_session %}
<span class="text-success">{{ saves_this_session }} added this session</span>
{% endif %}
</div>
<div id="search-progress" class="my-4">
@ -57,11 +63,11 @@
</div>
{% if hits %}
<details class="border rounded p-3 mt-2">
<summary class="text-muted small" style="cursor:pointer">{{ hits | length }} candidates</summary>
<ol class="mt-3 mb-0 small">
<details id="candidates-section" class="border rounded p-3 mt-2">
<summary class="text-muted small" style="cursor:pointer"><span id="candidates-count">{{ hits | length }}</span> candidates</summary>
<ol class="mt-3 mb-0 small" id="candidates-list">
{% for hit in hits %}
<li class="mb-1">
<li class="mb-1" data-title="{{ hit.title }}">
<a href="{{ url_for("article_page", url_title=url_title, title=hit.title) }}">{{ hit.title }}</a>
</li>
{% endfor %}
@ -76,6 +82,7 @@
(function () {
const hits = {{ hits | map(attribute='title') | list | tojson }};
const linkTo = {{ title | tojson }};
const redirectTo = {{ redirect_to | tojson }};
const apiUrl = {{ url_for('api_valid_hit') | tojson }};
const pageUrl = new URL(window.location.href);
@ -83,6 +90,16 @@
const elStatus = document.getElementById('search-status');
const elResult = document.getElementById('result');
const elAllDone = document.getElementById('all-done');
const elList = document.getElementById('candidates-list');
function removeCandidate(title) {
if (!elList) return;
const li = elList.querySelector(`li[data-title="${CSS.escape(title)}"]`);
if (!li) return;
li.remove();
const elCount = document.getElementById('candidates-count');
if (elCount) elCount.textContent = elList.children.length;
}
async function search() {
for (const hitTitle of hits) {
@ -91,6 +108,7 @@
let data;
try {
const params = new URLSearchParams({ link_to: linkTo, link_from: hitTitle });
if (redirectTo) params.append('redirect_to', redirectTo);
const resp = await fetch(apiUrl + '?' + params);
if (!resp.ok) continue;
data = await resp.json();
@ -98,7 +116,7 @@
continue;
}
if (!data.valid) continue;
if (!data.valid) { removeCandidate(hitTitle); continue; }
elProgress.hidden = true;
@ -123,6 +141,8 @@
}
elProgress.hidden = true;
const elCandidates = document.getElementById('candidates-section');
if (elCandidates) elCandidates.hidden = true;
elAllDone.hidden = false;
}