Add live preview to blacklist and whitelist
All checks were successful
Test and build Docker image / docker (push) Successful in 1m58s

This commit is contained in:
Joakim Hellsén 2026-04-27 18:27:05 +02:00
commit 6a3bba5b69
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
15 changed files with 1539 additions and 370 deletions

View file

@ -0,0 +1,86 @@
<div class="d-flex flex-column gap-4 filter-preview">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start gap-3">
<div>
<h3 class="h5 mb-1">Live preview</h3>
<p class="text-muted mb-0">Latest {{ preview_limit }} entries from {{ feed.title or feed.url }}</p>
</div>
<div class="d-flex flex-wrap gap-2">
<span class="badge bg-secondary">{{ preview_summary.total }} checked</span>
<span class="badge bg-success">{{ preview_summary.sent }} sent</span>
<span class="badge bg-danger">{{ preview_summary.skipped }} skipped</span>
<span class="badge bg-warning text-dark">{{ preview_summary.blacklist_matches }} blacklist match{{ 'es' if preview_summary.blacklist_matches != 1 else '' }}</span>
<span class="badge bg-info text-dark">{{ preview_summary.whitelist_matches }} whitelist match{{ 'es' if preview_summary.whitelist_matches != 1 else '' }}</span>
</div>
</div>
<p class="text-muted small mb-0">{{ preview_helper_text }}</p>
<section>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<h4 class="h6 text-uppercase text-muted mb-0">Decision list</h4>
<span class="text-muted small">Updates as you type. Saving is still manual.</span>
</div>
{% if preview_rows %}
<div class="d-flex flex-column gap-2 filter-preview__list">
{% for row in preview_rows %}
<article class="p-3 border border-dark rounded-0 filter-preview__item">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-start gap-3 mb-2">
<div class="filter-preview__content">
<h5 class="h6 mb-1">
{% if row.entry.link %}
<a class="text-muted text-decoration-none filter-preview__link"
href="{{ row.entry.link }}">{{ row.entry.title or row.entry.id }}</a>
{% else %}
<span class="text-light">{{ row.entry.title or row.entry.id }}</span>
{% endif %}
</h5>
<p class="text-muted small mb-0">
{% if row.entry.author %}By {{ row.entry.author }} |{% endif %}
{{ row.published_label }}
</p>
</div>
<span class="badge bg-{{ row.status_class }} filter-preview__status">{{ row.status_label }}</span>
</div>
<p class="mb-2">{{ row.decision.reason }}</p>
<div class="d-flex flex-wrap gap-2 align-items-center small">
{% if row.decision.blacklist_match %}
<span class="badge bg-danger">{{ row.decision.blacklist_match.description }}</span>
<span class="filter-preview__pattern">{{ row.decision.blacklist_match.pattern }}</span>
{% endif %}
{% if row.decision.whitelist_match %}
<span class="badge bg-success">{{ row.decision.whitelist_match.description }}</span>
<span class="filter-preview__pattern">{{ row.decision.whitelist_match.pattern }}</span>
{% endif %}
</div>
<div class="filter-preview__field-table mt-2">
{% for field in row.field_rows %}
<section class="filter-preview__field-row">
<div class="filter-preview__field-name">{{ field.label }}</div>
<div class="filter-preview__field-value">{{ field.value_html|safe }}</div>
<div class="filter-preview__field-badges">
{% for badge in field.badges %}<span class="badge bg-{{ badge.class }}">{{ badge.label }}</span>{% endfor %}
</div>
</section>
{% endfor %}
</div>
</article>
{% endfor %}
</div>
{% else %}
<div class="p-3 border border-dark rounded-0">
<p class="text-muted mb-0">No entries are available yet for this feed, so there is nothing to preview.</p>
</div>
{% endif %}
</section>
<section>
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
<h4 class="h6 text-uppercase text-muted mb-0">Rendered entries</h4>
<span class="text-muted small">Uses the same entry rendering as the feed page.</span>
</div>
{% if preview_html %}
<div class="filter-preview__rendered">{{ preview_html|safe }}</div>
{% else %}
<div class="p-3 border border-dark rounded-0">
<p class="text-muted mb-0">Rendered preview will appear here when entries are available.</p>
</div>
{% endif %}
</section>
</div>

View file

@ -1,98 +1,126 @@
{% extends "base.html" %}
{% block title %}
| Blacklist
| Blacklist
{% endblock title %}
{% block content %}
<div class="p-2 border border-dark">
<form action="/blacklist" method="post">
<!-- Feed URL -->
<div class="row pb-2">
<div class="col-sm-12">
<div class="form-text">
<ul class="list-inline">
<li>
Comma separated list of words to blacklist. If a word is found in the
corresponding blacklists, the feed will not be sent.
</li>
<li>Whitelist always takes precedence over blacklist. Leave empty to disable.</li>
<li>Words are case-insensitive. No spaces should be used before or after the comma.</li>
<li>
Correct:
<code>
primogem,events,gameplay preview,special program
</code>
</li>
<li>
Wrong:
<code>
primogem, events, gameplay preview, special program
</code>
</li>
</ul>
</div>
<label for="blacklist_title" class="col-sm-6 col-form-label">Blacklist - Title</label>
<input name="blacklist_title" type="text" class="form-control bg-dark border-dark text-muted"
id="blacklist_title" value="{%- if blacklist_title -%}{{ blacklist_title }}{%- endif -%}" />
<label for="blacklist_summary" class="col-sm-6 col-form-label">Blacklist - Summary</label>
<input name="blacklist_summary" type="text" class="form-control bg-dark border-dark text-muted"
id="blacklist_summary" value="{%- if blacklist_summary -%}{{ blacklist_summary }}{%- endif -%}" />
<label for="blacklist_content" class="col-sm-6 col-form-label">Blacklist - Content</label>
<input name="blacklist_content" type="text" class="form-control bg-dark border-dark text-muted"
id="blacklist_content" value="{%- if blacklist_content -%}{{ blacklist_content }}{%- endif -%}" />
<label for="blacklist_author" class="col-sm-6 col-form-label">Blacklist - Author</label>
<input name="blacklist_author" type="text" class="form-control bg-dark border-dark text-muted"
id="blacklist_author" value="{%- if blacklist_author -%}{{ blacklist_author }}{%- endif -%}" />
<div class="mt-4">
<div class="form-text">
<ul class="list-inline">
<li>
Regular expression patterns for advanced filtering. Each pattern should be on a new
line.
</li>
<li>Patterns are case-insensitive.</li>
<li>
Examples:
<code>
<pre>
^New Release:.*
\b(update|version|patch)\s+\d+\.\d+
.*\[(important|notice)\].*
</pre>
</code>
</li>
</ul>
<div class="row g-3 filter-page">
<div class="col-lg-5">
<section class="card border border-dark shadow-sm text-light rounded-0 filter-page__sidebar">
<div class="card-body p-3 p-md-4">
<div class="mb-4">
<h2 class="h4 mb-2">Blacklist Rules</h2>
<p class="text-muted mb-3">
Build block rules on the left and watch the latest feed entries update on the right before you save.
</p>
<div class="p-3 border border-dark rounded-0 small text-muted">
<p class="mb-2">
Use comma-separated terms or snippets for quick blocking. Use regex when the pattern is more specific.
</p>
<p class="mb-2">
Plain text matching is case-insensitive and partial, so <code>orld</code> matches <code>World of Warcraft</code>.
</p>
<p class="mb-2">Whitelist matches still win. If an entry matches both, the preview keeps it as sent.</p>
<p class="mb-0">Keep the left side for editing and the right side for checking what gets removed.</p>
</div>
</div>
<label for="regex_blacklist_title" class="col-sm-6 col-form-label">Regex Blacklist - Title</label>
<textarea name="regex_blacklist_title" class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_title"
rows="3">{%- if regex_blacklist_title -%}{{ regex_blacklist_title }}{%- endif -%}</textarea>
<label for="regex_blacklist_summary" class="col-sm-6 col-form-label">Regex Blacklist -
Summary</label>
<textarea name="regex_blacklist_summary" class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_summary"
rows="3">{%- if regex_blacklist_summary -%}{{ regex_blacklist_summary }}{%- endif -%}</textarea>
<label for="regex_blacklist_content" class="col-sm-6 col-form-label">Regex Blacklist -
Content</label>
<textarea name="regex_blacklist_content" class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_content"
rows="3">{%- if regex_blacklist_content -%}{{ regex_blacklist_content }}{%- endif -%}</textarea>
<label for="regex_blacklist_author" class="col-sm-6 col-form-label">Regex Blacklist - Author</label>
<textarea name="regex_blacklist_author" class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_author"
rows="3">{%- if regex_blacklist_author -%}{{ regex_blacklist_author }}{%- endif -%}</textarea>
<form action="/blacklist"
method="post"
class="row g-3"
hx-get="/blacklist_preview"
hx-target="#filter-preview"
hx-swap="innerHTML"
hx-trigger="input delay:400ms, change delay:200ms">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<div class="col-12">
<h3 class="h6 text-uppercase text-muted mb-3">Word Rules</h3>
<div class="p-3 border border-dark rounded-0 form-text mb-3">
<p class="mb-2">Comma separated terms or snippets. Spaces around commas are ignored.</p>
<p class="mb-0">
Example:
<code>primogem,events,orld,special program</code>
</p>
</div>
<label for="blacklist_title" class="form-label">Block if title contains</label>
<input name="blacklist_title"
type="text"
class="form-control bg-dark border-dark text-muted"
id="blacklist_title"
value="{{ blacklist_title }}" />
</div>
<div class="col-12">
<label for="blacklist_summary" class="form-label">Block if summary contains</label>
<input name="blacklist_summary"
type="text"
class="form-control bg-dark border-dark text-muted"
id="blacklist_summary"
value="{{ blacklist_summary }}" />
</div>
<div class="col-12">
<label for="blacklist_content" class="form-label">Block if content contains</label>
<input name="blacklist_content"
type="text"
class="form-control bg-dark border-dark text-muted"
id="blacklist_content"
value="{{ blacklist_content }}" />
</div>
<div class="col-12">
<label for="blacklist_author" class="form-label">Block if author contains</label>
<input name="blacklist_author"
type="text"
class="form-control bg-dark border-dark text-muted"
id="blacklist_author"
value="{{ blacklist_author }}" />
</div>
<div class="col-12 pt-2">
<h3 class="h6 text-uppercase text-muted mb-3">Regex Rules</h3>
<div class="p-3 border border-dark rounded-0 form-text mb-3">
<p class="mb-2">One pattern per line. Matching is case-insensitive.</p>
<pre class="mb-0 filter-page__example">^New Release:.*
\b(update|version|patch)\s+\d+\.\d+
.*\[(important|notice)\].*</pre>
</div>
<label for="regex_blacklist_title" class="form-label">Block if title matches regex</label>
<textarea name="regex_blacklist_title"
class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_title"
rows="3">{{ regex_blacklist_title }}</textarea>
</div>
<div class="col-12">
<label for="regex_blacklist_summary" class="form-label">Block if summary matches regex</label>
<textarea name="regex_blacklist_summary"
class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_summary"
rows="3">{{ regex_blacklist_summary }}</textarea>
</div>
<div class="col-12">
<label for="regex_blacklist_content" class="form-label">Block if content matches regex</label>
<textarea name="regex_blacklist_content"
class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_content"
rows="3">{{ regex_blacklist_content }}</textarea>
</div>
<div class="col-12">
<label for="regex_blacklist_author" class="form-label">Block if author matches regex</label>
<textarea name="regex_blacklist_author"
class="form-control bg-dark border-dark text-muted"
id="regex_blacklist_author"
rows="3">{{ regex_blacklist_author }}</textarea>
</div>
<div class="col-12 d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark btn-sm" type="submit">Update blacklist</button>
<a class="btn btn-outline-light btn-sm"
href="/feed?feed_url={{ feed.url|encode_url }}">Back to feed</a>
</div>
</form>
</div>
</div>
</section>
</div>
<!-- Add a hidden feed_url field to the form -->
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<!-- Submit button -->
<div class="d-md-flex">
<button class="btn btn-dark btn-sm">Update blacklist</button>
<div class="col-lg-7">
<section class="card border border-dark shadow-sm text-light rounded-0 h-100">
<div class="card-body p-3 p-md-4">
<div id="filter-preview">{% include "_filter_preview.html" %}</div>
</div>
</section>
</div>
</form>
</div>
</div>
{% endblock content %}

View file

@ -1,98 +1,124 @@
{% extends "base.html" %}
{% block title %}
| Whitelist
| Whitelist
{% endblock title %}
{% block content %}
<div class="p-2 border border-dark">
<form action="/whitelist" method="post">
<!-- Feed URL -->
<div class="row pb-2">
<div class="col-sm-12">
<div class="form-text">
<ul class="list-inline">
<li>
Comma separated list of words to whitelist. Only send message to
Discord if one of these words are present in the corresponding fields.
</li>
<li>Whitelist always takes precedence over blacklist. Leave empty to disable.</li>
<li>Words are case-insensitive. No spaces should be used before or after the comma.</li>
<li>
Correct:
<code>
primogem,events,gameplay preview,special program
</code>
</li>
<li>
Wrong:
<code>
primogem, events, gameplay preview, special program
</code>
</li>
</ul>
</div>
<label for="whitelist_title" class="col-sm-6 col-form-label">Whitelist - Title</label>
<input name="whitelist_title" type="text" class="form-control bg-dark border-dark text-muted"
id="whitelist_title" value="{%- if whitelist_title -%}{{ whitelist_title }} {%- endif -%}" />
<label for="whitelist_summary" class="col-sm-6 col-form-label">Whitelist - Summary</label>
<input name="whitelist_summary" type="text" class="form-control bg-dark border-dark text-muted"
id="whitelist_summary" value="{%- if whitelist_summary -%}{{ whitelist_summary }}{%- endif -%}" />
<label for="whitelist_content" class="col-sm-6 col-form-label">Whitelist - Content</label>
<input name="whitelist_content" type="text" class="form-control bg-dark border-dark text-muted"
id="whitelist_content" value="{%- if whitelist_content -%}{{ whitelist_content }}{%- endif -%}" />
<label for="whitelist_author" class="col-sm-6 col-form-label">Whitelist - Author</label>
<input name="whitelist_author" type="text" class="form-control bg-dark border-dark text-muted"
id="whitelist_author" value="{%- if whitelist_author -%} {{ whitelist_author }} {%- endif -%}" />
<div class="mt-4">
<div class="form-text">
<ul class="list-inline">
<li>
Regular expression patterns for advanced filtering. Each pattern should be on a new
line.
</li>
<li>Patterns are case-insensitive.</li>
<li>
Examples:
<code>
<pre>
^New Release:.*
\b(update|version|patch)\s+\d+\.\d+
.*\[(important|notice)\].*
</pre>
</code>
</li>
</ul>
<div class="row g-3 filter-page">
<div class="col-lg-5">
<section class="card border border-dark shadow-sm text-light rounded-0 filter-page__sidebar">
<div class="card-body p-3 p-md-4">
<div class="mb-4">
<h2 class="h4 mb-2">Whitelist Rules</h2>
<p class="text-muted mb-3">
Shape what is allowed through, and use the live pane to see which entries are the only ones that will still be sent.
</p>
<div class="p-3 border border-dark rounded-0 small text-muted">
<p class="mb-2">Whitelist rules are restrictive. If any whitelist rule exists, entries must match it to be sent.</p>
<p class="mb-2">
Plain text matching is case-insensitive and partial, so <code>orld</code> matches <code>World of Warcraft</code>.
</p>
<p class="mb-2">When an entry matches both lists, whitelist still wins and the preview shows it as sent.</p>
<p class="mb-0">Saved blacklist rules remain active while you preview whitelist edits.</p>
</div>
</div>
<label for="regex_whitelist_title" class="col-sm-6 col-form-label">Regex Whitelist - Title</label>
<textarea name="regex_whitelist_title" class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_title"
rows="3">{%- if regex_whitelist_title -%}{{ regex_whitelist_title }}{%- endif -%}</textarea>
<label for="regex_whitelist_summary" class="col-sm-6 col-form-label">Regex Whitelist -
Summary</label>
<textarea name="regex_whitelist_summary" class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_summary"
rows="3">{%- if regex_whitelist_summary -%}{{ regex_whitelist_summary }}{%- endif -%}</textarea>
<label for="regex_whitelist_content" class="col-sm-6 col-form-label">Regex Whitelist -
Content</label>
<textarea name="regex_whitelist_content" class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_content"
rows="3">{%- if regex_whitelist_content -%}{{ regex_whitelist_content }}{%- endif -%}</textarea>
<label for="regex_whitelist_author" class="col-sm-6 col-form-label">Regex Whitelist - Author</label>
<textarea name="regex_whitelist_author" class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_author"
rows="3">{%- if regex_whitelist_author -%}{{ regex_whitelist_author }}{%- endif -%}</textarea>
<form action="/whitelist"
method="post"
class="row g-3"
hx-get="/whitelist_preview"
hx-target="#filter-preview"
hx-swap="innerHTML"
hx-trigger="input delay:400ms, change delay:200ms">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<div class="col-12">
<h3 class="h6 text-uppercase text-muted mb-3">Word Rules</h3>
<div class="p-3 border border-dark rounded-0 form-text mb-3">
<p class="mb-2">Comma separated terms or snippets. Spaces around commas are ignored.</p>
<p class="mb-0">
Example:
<code>primogem,events,orld,special program</code>
</p>
</div>
<label for="whitelist_title" class="form-label">Allow if title contains</label>
<input name="whitelist_title"
type="text"
class="form-control bg-dark border-dark text-muted"
id="whitelist_title"
value="{{ whitelist_title }}" />
</div>
<div class="col-12">
<label for="whitelist_summary" class="form-label">Allow if summary contains</label>
<input name="whitelist_summary"
type="text"
class="form-control bg-dark border-dark text-muted"
id="whitelist_summary"
value="{{ whitelist_summary }}" />
</div>
<div class="col-12">
<label for="whitelist_content" class="form-label">Allow if content contains</label>
<input name="whitelist_content"
type="text"
class="form-control bg-dark border-dark text-muted"
id="whitelist_content"
value="{{ whitelist_content }}" />
</div>
<div class="col-12">
<label for="whitelist_author" class="form-label">Allow if author contains</label>
<input name="whitelist_author"
type="text"
class="form-control bg-dark border-dark text-muted"
id="whitelist_author"
value="{{ whitelist_author }}" />
</div>
<div class="col-12 pt-2">
<h3 class="h6 text-uppercase text-muted mb-3">Regex Rules</h3>
<div class="p-3 border border-dark rounded-0 form-text mb-3">
<p class="mb-2">One pattern per line. Matching is case-insensitive.</p>
<pre class="mb-0 filter-page__example">^New Release:.*
\b(update|version|patch)\s+\d+\.\d+
.*\[(important|notice)\].*</pre>
</div>
<label for="regex_whitelist_title" class="form-label">Allow if title matches regex</label>
<textarea name="regex_whitelist_title"
class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_title"
rows="3">{{ regex_whitelist_title }}</textarea>
</div>
<div class="col-12">
<label for="regex_whitelist_summary" class="form-label">Allow if summary matches regex</label>
<textarea name="regex_whitelist_summary"
class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_summary"
rows="3">{{ regex_whitelist_summary }}</textarea>
</div>
<div class="col-12">
<label for="regex_whitelist_content" class="form-label">Allow if content matches regex</label>
<textarea name="regex_whitelist_content"
class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_content"
rows="3">{{ regex_whitelist_content }}</textarea>
</div>
<div class="col-12">
<label for="regex_whitelist_author" class="form-label">Allow if author matches regex</label>
<textarea name="regex_whitelist_author"
class="form-control bg-dark border-dark text-muted"
id="regex_whitelist_author"
rows="3">{{ regex_whitelist_author }}</textarea>
</div>
<div class="col-12 d-flex flex-wrap gap-2 pt-2">
<button class="btn btn-dark btn-sm" type="submit">Update whitelist</button>
<a class="btn btn-outline-light btn-sm"
href="/feed?feed_url={{ feed.url|encode_url }}">Back to feed</a>
</div>
</form>
</div>
</div>
</section>
</div>
<!-- Add a hidden feed_url field to the form -->
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<!-- Submit button -->
<div class="d-md-flex">
<button class="btn btn-dark btn-sm">Update whitelist</button>
<div class="col-lg-7">
<section class="card border border-dark shadow-sm text-light rounded-0 h-100">
<div class="card-body p-3 p-md-4">
<div id="filter-preview">{% include "_filter_preview.html" %}</div>
</div>
</section>
</div>
</form>
</div>
</div>
{% endblock content %}