Add mass update functionality for feed URLs with preview
All checks were successful
Test and build Docker image / docker (push) Successful in 24s

This commit is contained in:
Joakim Hellsén 2026-03-16 04:48:34 +01:00
commit 955b94456d
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
6 changed files with 952 additions and 26 deletions

View file

@ -0,0 +1,73 @@
{% if preview_rows %}
<p class="small text-muted mb-1">
{{ preview_change_count }} feed URL{{ 's' if preview_change_count != 1 else '' }} ready to update.
</p>
<div class="small text-muted mb-2 d-flex flex-wrap gap-2">
<span class="badge bg-secondary">Total: {{ preview_summary.total }}</span>
<span class="badge bg-info text-dark">Matched: {{ preview_summary.matched }}</span>
<span class="badge bg-success">Will update: {{ preview_summary.will_update }}</span>
<span class="badge bg-warning text-dark">Conflicts: {{ preview_summary.conflicts }}</span>
<span class="badge bg-warning">Force overwrite: {{ preview_summary.force_overwrite }}</span>
<span class="badge bg-warning text-dark">Force ignore errors: {{ preview_summary.force_ignore_errors }}</span>
<span class="badge bg-danger">Resolve errors: {{ preview_summary.resolve_errors }}</span>
<span class="badge bg-secondary">No change: {{ preview_summary.no_change }}</span>
<span class="badge bg-secondary">No match: {{ preview_summary.no_match }}</span>
</div>
<form action="/bulk_change_feed_urls" method="post" class="mb-2">
<input type="hidden" name="webhook_url" value="{{ webhook_url }}" />
<input type="hidden" name="replace_from" value="{{ replace_from }}" />
<input type="hidden" name="replace_to" value="{{ replace_to }}" />
<input type="hidden"
name="resolve_urls"
value="{{ 'true' if resolve_urls else 'false' }}" />
<input type="hidden"
name="force_update"
value="{{ 'true' if force_update else 'false' }}" />
<button type="submit"
class="btn btn-warning w-100"
{% if preview_change_count == 0 %}disabled{% endif %}
onclick="return confirm('Apply these feed URL updates?');">Apply mass update</button>
</form>
<div class="table-responsive mt-2">
<table class="table table-sm table-dark table-striped align-middle mb-0">
<thead>
<tr>
<th scope="col">Old URL</th>
<th scope="col">New URL</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
{% for row in preview_rows %}
<tr>
<td>
<code>{{ row.old_url }}</code>
</td>
<td>
<code>{{ row.resolved_url if resolve_urls else row.candidate_url }}</code>
</td>
<td>
{% if not row.has_match %}
<span class="badge bg-secondary">No match</span>
{% elif row.will_force_ignore_errors %}
<span class="badge bg-warning text-dark">Will force update (ignore resolve error)</span>
{% elif row.resolution_error %}
<span class="badge bg-danger">{{ row.resolution_error }}</span>
{% elif row.will_force_overwrite %}
<span class="badge bg-warning">Will force overwrite</span>
{% elif row.target_exists %}
<span class="badge bg-warning text-dark">Conflict: target URL exists</span>
{% elif row.will_change %}
<span class="badge bg-success">Will update</span>
{% else %}
<span class="badge bg-secondary">No change</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% elif replace_from %}
<p class="small text-muted mb-0">No preview rows found for that replacement pattern.</p>
{% endif %}

View file

@ -1,13 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description"
content="Stay updated with the latest news and events with our easy-to-use RSS bot. Never miss a message or announcement again with real-time notifications directly to your Discord server." />
content="Stay updated with the latest news and events with our easy-to-use RSS bot. Never miss a message or announcement again with real-time notifications directly to your Discord server." />
<meta name="keywords"
content="discord, rss, bot, notifications, announcements, updates, real-time, server, messages, news, events, feed." />
content="discord, rss, bot, notifications, announcements, updates, real-time, server, messages, news, events, feed." />
<link href="/static/bootstrap.min.css" rel="stylesheet" />
<link href="/static/styles.css" rel="stylesheet" />
<link rel="icon" href="/static/favicon.ico" type="image/x-icon" />
@ -18,19 +17,20 @@
{% block head %}
{% endblock head %}
</head>
<body class="text-white-50">
{% include "nav.html" %}
<div class="p-2 mb-2">
<div class="container-fluid">
<div class="d-grid p-2">
{% if messages %}
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<pre>{{ messages }}</pre>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<pre>{{ messages }}</pre>
<button type="button"
class="btn-close"
data-bs-dismiss="alert"
aria-label="Close"></button>
</div>
{% endif %}
{% block content %}
{% endblock content %}
<footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top">
@ -41,18 +41,20 @@
<ul class="nav col-md-4 justify-content-end">
<li class="nav-item">
<a href="https://github.com/TheLovinator1/discord-rss-bot/issues"
class="nav-link px-2 text-muted">Report an issue</a>
class="nav-link px-2 text-muted">Report an issue</a>
</li>
<li class="nav-item">
<a href="https://github.com/TheLovinator1/discord-rss-bot/issues"
class="nav-link px-2 text-muted">Send feedback</a>
class="nav-link px-2 text-muted">Send feedback</a>
</li>
</ul>
</footer>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
integrity="sha384-/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u/6OCvVKyz1W+idaz"
crossorigin="anonymous"></script>
<script src="/static/bootstrap.min.js" defer></script>
</body>
</html>

View file

@ -32,11 +32,10 @@
{% for hook_from_context in webhooks %}
<div class="p-2 mb-3 border border-dark">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h5 mb-0">
<a class="text-muted"
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">{{ hook_from_context.name }}</a>
</h2>
<a class="text-muted"
<h2 class="h5 mb-0">{{ hook_from_context.name }}</h2>
<a class="text-muted fs-6 btn btn-outline-light btn-sm ms-auto me-2"
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">Settings</a>
<a class="text-muted fs-6 btn btn-outline-light btn-sm"
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">View Latest Entries</a>
</div>
<!-- Group feeds by domain within each webhook -->

View file

@ -3,6 +3,7 @@
| {{ webhook_name }}
{% endblock title %}
{% block content %}
{% if message %}<div class="alert alert-info" role="alert">{{ message }}</div>{% endif %}
<div class="card mb-3 border border-dark p-3 text-light">
<div class="d-flex flex-column flex-md-row justify-content-between gap-3">
<div>
@ -61,6 +62,57 @@
Delete Webhook
</button>
</form>
<hr class="border-secondary my-3" />
<h3 class="h6">Mass update feed URLs</h3>
<p class="text-muted small mb-2">Replace part of feed URLs for all feeds attached to this webhook.</p>
<form action="/webhook_entries"
method="get"
class="row g-2 mb-2"
hx-get="/webhook_entries_mass_update_preview"
hx-target="#mass-update-preview"
hx-swap="innerHTML">
<input type="hidden" name="webhook_url" value="{{ webhook_url|encode_url }}" />
<div class="col-12">
<label for="replace_from" class="form-label small">Replace this</label>
<input type="text"
name="replace_from"
id="replace_from"
class="form-control border text-muted bg-dark"
value="{{ replace_from }}"
placeholder="https://old-domain.example" />
</div>
<div class="col-12">
<label for="replace_to" class="form-label small">With this</label>
<input type="text"
name="replace_to"
id="replace_to"
class="form-control border text-muted bg-dark"
value="{{ replace_to }}"
placeholder="https://new-domain.example" />
</div>
<div class="col-12 form-check ms-1">
<input class="form-check-input"
type="checkbox"
value="true"
id="resolve_urls"
name="resolve_urls"
{% if resolve_urls %}checked{% endif %} />
<label class="form-check-label small" for="resolve_urls">Resolve final URL with redirects (uses httpx)</label>
</div>
<div class="col-12 form-check ms-1">
<input class="form-check-input"
type="checkbox"
value="true"
id="force_update"
name="force_update"
{% if force_update %}checked{% endif %} />
<label class="form-check-label small" for="force_update">Force update (overwrite conflicting target feed URLs)</label>
</div>
<div class="col-12">
<button type="submit" class="btn btn-outline-warning w-100">Preview changes</button>
</div>
</form>
<div id="mass-update-preview">{% include "_webhook_mass_update_preview.html" %}</div>
</div>
</div>
<div class="col-lg-7">
@ -77,6 +129,7 @@
{{ feed.url }}
{% endif %}
</a>
{% if feed.title %}<span class="text-muted">- {{ feed.url }}</span>{% endif %}
{% if not feed.updates_enabled %}<span class="text-warning">Disabled</span>{% endif %}
{% if feed.last_exception %}<span class="text-danger">({{ feed.last_exception.value_str }})</span>{% endif %}
</li>