aboutsummaryrefslogtreecommitdiff
path: root/webmentions_ssg
diff options
context:
space:
mode:
authorDennis Fink2026-08-17 20:06:22 +0200
committerDennis Fink2026-08-17 20:06:22 +0200
commitd4a03623544aac0ce911d8471ff4b033bc3255e1 (patch)
tree0d707db5a9aed7442c28e0ac0de8898aab54c090 /webmentions_ssg
parent85a73c811a3de333138978d8d450533caa668dc5 (diff)
downloadwebmentions-ssg-d4a03623544aac0ce911d8471ff4b033bc3255e1.tar.gz
webmentions-ssg-d4a03623544aac0ce911d8471ff4b033bc3255e1.zip
feat(sender): add manual source rescan action
Add an authenticated action to queue a source rescan from the sent Webmentions overview. Extract the reusable confirmation modal and its JavaScript so received and sent admin actions share the same implementation.
Diffstat (limited to '')
-rw-r--r--webmentions_ssg/static/js/confirmation-modal.js26
-rw-r--r--webmentions_ssg/tasks/scanner.py9
-rw-r--r--webmentions_ssg/templates/includes/confirmation_modal.html27
-rw-r--r--webmentions_ssg/templates/received.html57
-rw-r--r--webmentions_ssg/templates/sent.html19
-rw-r--r--webmentions_ssg/views.py18
6 files changed, 97 insertions, 59 deletions
diff --git a/webmentions_ssg/static/js/confirmation-modal.js b/webmentions_ssg/static/js/confirmation-modal.js
new file mode 100644
index 0000000..e278790
--- /dev/null
+++ b/webmentions_ssg/static/js/confirmation-modal.js
@@ -0,0 +1,26 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const confirmationModal = document.getElementById("confirmation-modal");
+
+ if (confirmationModal === null) {
+ return;
+ }
+
+ confirmationModal.addEventListener("show.bs.modal", (event) => {
+ const button = event.relatedTarget;
+
+ const form = confirmationModal.querySelector("#confirmation-modal-form");
+ const title = confirmationModal.querySelector("#confirmation-modal-title");
+ const message = confirmationModal.querySelector(
+ "#confirmation-modal-message",
+ );
+ const submit = form.querySelector("#submit");
+
+ form.action = button.dataset.action;
+
+ title.textContent = button.dataset.title;
+ message.textContent = button.dataset.message;
+
+ submit.value = button.dataset.confirmLabel;
+ submit.className = `btn ${button.dataset.confirmClass}`;
+ });
+});
diff --git a/webmentions_ssg/tasks/scanner.py b/webmentions_ssg/tasks/scanner.py
index f80e5f4..ff75697 100644
--- a/webmentions_ssg/tasks/scanner.py
+++ b/webmentions_ssg/tasks/scanner.py
@@ -1,6 +1,6 @@
from collections.abc import Iterator
from dataclasses import dataclass
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from fnmatch import fnmatchcase
from itertools import chain
from pathlib import Path
@@ -327,7 +327,7 @@ def scan_sources() -> None:
seen_paths: set[str] = set()
scanned_count = 0
- scan_time = datetime.now(timezone.utc)
+ scan_time = datetime.now(UTC)
for index_path in root.rglob("index.html"):
relative_path = index_path.relative_to(root)
@@ -395,3 +395,8 @@ def scan_sources() -> None:
scanned_count,
pending_count,
)
+
+
+@huey.task()
+def manual_scan_sources() -> None:
+ return scan_sources()
diff --git a/webmentions_ssg/templates/includes/confirmation_modal.html b/webmentions_ssg/templates/includes/confirmation_modal.html
new file mode 100644
index 0000000..ff9bd06
--- /dev/null
+++ b/webmentions_ssg/templates/includes/confirmation_modal.html
@@ -0,0 +1,27 @@
+<div class="modal fade"
+ id="confirmation-modal"
+ tabindex="-1"
+ aria-labelledby="confirmation-modal-title"
+ aria-hidden="true">
+ <div class="modal-dialog modal-dialog-centered">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h1 class="modal-title fs-5" id="confirmation-modal-title"></h1>
+ <button type="button"
+ class="btn-close"
+ data-bs-dismiss="modal"
+ aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p id="confirmation-modal-message" class="mb-0"></p>
+ </div>
+ <div class="modal-footer">
+ {{ render_form(action_form,
+ id="confirmation-modal-form",
+ button_style="primary",
+ form_group_classes="mb-0") }}
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webmentions_ssg/templates/received.html b/webmentions_ssg/templates/received.html
index a44b706..525e811 100644
--- a/webmentions_ssg/templates/received.html
+++ b/webmentions_ssg/templates/received.html
@@ -84,65 +84,12 @@
{% if webmentions.pages > 1 %}
<div class="mt-3">{{ render_pagination(webmentions) }}</div>
{% endif %}
- <div class="modal fade"
- id="confirmation-modal"
- tabindex="-1"
- aria-labelledby="confirmation-modal-title"
- aria-hidden="true">
- <div class="modal-dialog modal-dialog-centered">
- <div class="modal-content">
- <div class="modal-header">
- <h1 class="modal-title fs-5" id="confirmation-modal-title"></h1>
- <button type="button"
- class="btn-close"
- data-bs-dismiss="modal"
- aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <p id="confirmation-modal-message" class="mb-0"></p>
- </div>
- <div class="modal-footer">
- {{ render_form(action_form,
- id="confirmation-modal-form",
- button_style="primary",
- form_group_classes="mb-0") }}
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
- </div>
- </div>
- </div>
- </div>
+ {% include "includes/confirmation_modal.html" %}
{% else %}
<p class="text-body-secondary">No Webmentions have been received yet.</p>
{% endif %}
{% endblock %}
{% block scripts %}
{{ super() }}
- <script>
- const confirmationModal = document.getElementById(
- "confirmation-modal"
- );
-
- confirmationModal.addEventListener("show.bs.modal", (event) => {
- const button = event.relatedTarget;
-
- const form = confirmationModal.querySelector(
- "#confirmation-modal-form"
- );
- const title = confirmationModal.querySelector(
- "#confirmation-modal-title"
- );
- const message = confirmationModal.querySelector(
- "#confirmation-modal-message"
- );
- const submit = form.querySelector("#submit");
-
- form.action = button.dataset.action;
-
- title.textContent = button.dataset.title;
- message.textContent = button.dataset.message;
-
- submit.value = button.dataset.confirmLabel;
- submit.className = `btn ${button.dataset.confirmClass}`;
- });
- </script>
+ <script src="{{ url_for('static', filename='js/confirmation-modal.js') }}"></script>
{% endblock %}
diff --git a/webmentions_ssg/templates/sent.html b/webmentions_ssg/templates/sent.html
index 4993bef..bc9e021 100644
--- a/webmentions_ssg/templates/sent.html
+++ b/webmentions_ssg/templates/sent.html
@@ -1,9 +1,21 @@
{% extends "base.html" %}
+{% from "bootstrap5/form.html" import render_form %}
{% from "bootstrap5/pagination.html" import render_pagination %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h1 class="mb-0">Sources</h1>
- <span class="text-body-secondary">{{ sources.total }} sources</span>
+ <div class="d-flex flex-column justify-content-between">
+ <span class="text-body-secondary mb-1">{{ sources.total }} sources</span>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ data-bs-toggle="modal"
+ data-bs-target="#confirmation-modal"
+ data-action="{{ url_for("root.rescan_sent_sources") }}"
+ data-title="Rescan sources?"
+ data-message="All sources will be scanned again for changes and webmentions."
+ data-confirm-label="Rescan"
+ data-confirm-class="btn-primary">Rescan</button>
+ </div>
</div>
{% if sources.items %}
<div class="table-responsive">
@@ -60,7 +72,12 @@
{% if sources.pages > 1 %}
<div class="mt-3">{{ render_pagination(sources) }}</div>
{% endif %}
+ {% include "includes/confirmation_modal.html" %}
{% else %}
<p class="text-body-secondary">No Webmention sources have been discovered yet.</p>
{% endif %}
{% endblock %}
+{% block scripts %}
+ {{ super() }}
+ <script src="{{ url_for('static', filename='js/confirmation-modal.js') }}"></script>
+{% endblock %}
diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py
index eeea765..13e5086 100644
--- a/webmentions_ssg/views.py
+++ b/webmentions_ssg/views.py
@@ -22,6 +22,7 @@ from . import CSRF, forms
from . import DATABASE as db
from .models import ReceivedWebmention, Source, User
from .tasks.receiver import verify_webmention
+from .tasks.scanner import manual_scan_sources
root_page = Blueprint("root", __name__)
@@ -138,7 +139,22 @@ def sent() -> ResponseReturnValue:
.order_by(Source.last_seen_at.desc()),
per_page=25,
)
- return render_template("sent.html", title="Sent Webmentions", sources=sources)
+ return render_template(
+ "sent.html",
+ title="Sent Webmentions",
+ sources=sources,
+ action_form=forms.AdminActionForm(),
+ )
+
+
+@root_page.post("/sent/rescan")
+@login_required
+def rescan_sent_sources():
+ form = forms.AdminActionForm()
+ if form.validate_on_submit():
+ manual_scan_sources()
+ flash("Source rescan queued.", "success")
+ return redirect(url_for("root.sent"))
@root_page.route("/sent/<uuid:identifier>")