aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--webmentions_ssg/templates/includes/confirmation_modal.html2
-rw-r--r--webmentions_ssg/templates/received.html2
-rw-r--r--webmentions_ssg/templates/sent.html4
-rw-r--r--webmentions_ssg/templates/sent_source.html27
-rw-r--r--webmentions_ssg/views.py58
5 files changed, 79 insertions, 14 deletions
diff --git a/webmentions_ssg/templates/includes/confirmation_modal.html b/webmentions_ssg/templates/includes/confirmation_modal.html
index 5c0a5a1..e6a4c10 100644
--- a/webmentions_ssg/templates/includes/confirmation_modal.html
+++ b/webmentions_ssg/templates/includes/confirmation_modal.html
@@ -3,7 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
SPDX-License-Identifier: BSD-3-Clause
-->
-
+{% from "bootstrap5/form.html" import render_form %}
<div class="modal fade"
id="confirmation-modal"
tabindex="-1"
diff --git a/webmentions_ssg/templates/received.html b/webmentions_ssg/templates/received.html
index 1c5a216..7cfdce8 100644
--- a/webmentions_ssg/templates/received.html
+++ b/webmentions_ssg/templates/received.html
@@ -3,9 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
SPDX-License-Identifier: BSD-3-Clause
-->
-
{% 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">
diff --git a/webmentions_ssg/templates/sent.html b/webmentions_ssg/templates/sent.html
index 1d311a0..4ca75ce 100644
--- a/webmentions_ssg/templates/sent.html
+++ b/webmentions_ssg/templates/sent.html
@@ -3,9 +3,7 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
SPDX-License-Identifier: BSD-3-Clause
-->
-
{% 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">
@@ -50,7 +48,7 @@ SPDX-License-Identifier: BSD-3-Clause
{% endif %}
<tr class="{{ row_class }}">
<td class="text-nowrap">
- <a href="{{ url_for('root.sent_source', identifier=source.uuid) }}">{{ source.path }}</a>
+ <a href="{{ url_for('root.sent_source', source_identifier=source.uuid) }}">{{ source.path }}</a>
</td>
<td class="text-break">
<a href="{{ source.url }}">{{ source.url }}</a>
diff --git a/webmentions_ssg/templates/sent_source.html b/webmentions_ssg/templates/sent_source.html
index 6bb2ac5..77376b5 100644
--- a/webmentions_ssg/templates/sent_source.html
+++ b/webmentions_ssg/templates/sent_source.html
@@ -3,7 +3,6 @@ SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>
SPDX-License-Identifier: BSD-3-Clause
-->
-
{% extends "base.html" %}
{% block content %}
<div class="mb-3">
@@ -85,11 +84,15 @@ SPDX-License-Identifier: BSD-3-Clause
<th scope="col">Status URL</th>
<th scope="col">Last attempted at</th>
<th scope="col">Last sent at</th>
+ <th scope="col">Actions</th>
</tr>
</thead>
<tbody>
{% for webmention in webmentions %}
- {% if webmention.status.value == "sent" %}
+ {% if webmention.status is none %}
+ {% set row_class = "table-info" %}
+ {% set badge_class = "text-bg-info" %}
+ {% elif webmention.status.value == "sent" %}
{% set row_class = "table-success" %}
{% set badge_class = "text-bg-success" %}
{% elif webmention.status.value == "unsupported" %}
@@ -107,7 +110,9 @@ SPDX-License-Identifier: BSD-3-Clause
<a href="{{ webmention.target }}">{{ webmention.target }}</a>
</td>
<td>
- {% if webmention.status is not none %}
+ {% if webmention.status is none %}
+ <span class="badge {{ badge_class }}">Pending</span>
+ {% else %}
<span class="badge {{ badge_class }}">{{ webmention.status.value.title() }}</span>
{% endif %}
</td>
@@ -171,12 +176,28 @@ SPDX-License-Identifier: BSD-3-Clause
—
{% endif %}
</td>
+ <td>
+ <button type="button"
+ class="btn btn-sm btn-primary"
+ data-bs-toggle="modal"
+ data-bs-target="#confirmation-modal"
+ data-action="{{ url_for("root.resend_sent_webmention", source_identifier=source.uuid, webmention_identifier=webmention.uuid) }}"
+ data-title="Resend Webmention?"
+ data-message="This Webmention will be queued again for delivery to {{ webmention.target }}."
+ data-confirm-label="Resend"
+ data-confirm-class="btn-primary">Resend</button>
+ </td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
+ {% include "includes/confirmation_modal.html" %}
{% else %}
<p class="text-body-secondary">No Webmentions have been discovered for this source.</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 3430cea..2235735 100644
--- a/webmentions_ssg/views.py
+++ b/webmentions_ssg/views.py
@@ -24,9 +24,10 @@ from sqlalchemy.orm import selectinload
from . import CSRF, forms
from . import DATABASE as db
-from .models import ReceivedWebmention, Source, User
+from .models import ReceivedWebmention, SentWebmention, Source, User
from .tasks.receiver import verify_webmention
from .tasks.scanner import manual_scan_sources
+from .tasks.sender import send_webmention
root_page = Blueprint("root", __name__)
@@ -209,19 +210,19 @@ def rescan_sent_sources() -> ResponseReturnValue:
return redirect(url_for("root.sent"))
-@root_page.route("/sent/<uuid:identifier>")
+@root_page.route("/sent/<uuid:source_identifier>")
@login_required
-def sent_source(identifier: uuid.UUID) -> ResponseReturnValue:
+def sent_source(source_identifier: uuid.UUID) -> ResponseReturnValue:
"""
Display the sent Webmentions associated with a source.
- :param identifier: Identifier of the source to display.
+ :param source_identifier: Identifier of the source to display.
:return: Rendered source details page.
"""
source = db.session.scalar(
sa.select(Source)
.options(selectinload(Source.sent_webmentions))
- .where(Source.uuid == identifier)
+ .where(Source.uuid == source_identifier)
)
if source is None:
return abort(404)
@@ -234,9 +235,56 @@ def sent_source(identifier: uuid.UUID) -> ResponseReturnValue:
title="Sent Webmentions",
source=source,
webmentions=webmentions,
+ action_form=forms.AdminActionForm(),
)
+@root_page.post("/sent/<uuid:source_identifier>/<uuid:webmention_identifier>/resend")
+@login_required
+def resend_sent_webmention(
+ source_identifier: uuid.UUID, webmention_identifier: uuid.UUID
+) -> ResponseReturnValue:
+ """
+ Queue an outgoing Webmention to be sent again.
+
+ The current desired source revision is reopened for processing so the
+ sender task does not treat the Webmention as already completed. Historical
+ successful-send fields are preserved, while result fields from the previous
+ attempt are cleared.
+
+ :param source_identifier: Identifier of the source owning the Webmention.
+ :param webmention_identifier: Identifier of the sent Webmention to resend.
+ :return: Redirect response to the source details view.
+ """
+ form = forms.AdminActionForm()
+
+ if not form.validate_on_submit():
+ return abort(400)
+
+ webmention = db.session.get(SentWebmention, webmention_identifier)
+
+ if webmention is None or webmention.source_id != source_identifier:
+ return abort(404)
+
+ # Reopen the existing desired revision instead of inventing a new source
+ # revision. send_webmention() otherwise exits when this revision has already
+ # been processed.
+ webmention.processed_revision = None
+ webmention.status = None
+ webmention.failure_reason = None
+ webmention.endpoint = None
+ webmention.response_status = None
+ webmention.status_url = None
+
+ db.session.commit()
+
+ send_webmention(webmention_identifier)
+
+ flash(f"Webmention {webmention.uuid} queued for resend.", "success")
+
+ return redirect(url_for("root.sent_source", identifier=source_identifier))
+
+
@root_page.post("/endpoint")
@CSRF.exempt
def endpoint() -> ResponseReturnValue: