Performance - (watched page lister) open link single eval (#4405)

* Watchlist/Notifications - Resolve 'Link to Open' once per row, don't leak 'DISABLED' into notification tokens

Follow-up to #4290.

`watch.open_link` was read three times per watch list row (two hrefs plus the
new title attribute). Each read re-runs `_resolve_link()`, which for a Jinja2
templated URL builds a fresh ImmutableSandboxedEnvironment - the cost the
comment in validate_url.py already warns about. Hoisted to a single
`{% set open_link = watch.open_link %}` alongside the other per-row lookups.

`watch_open_url` was set from `watch.open_link`, which returns the string
'DISABLED' when the URL fails validation, so the default RSS body template
(`RSS_TEMPLATE_HTML_DEFAULT`) rendered `<a href="DISABLED">` for those watches.
Now falls back to the raw URL, matching the neighbouring `watch_url` token.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Rebuild template

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dgtlmoon
2026-09-09 16:19:34 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent c104179ab5
commit 821b538ae6
2 changed files with 8 additions and 5 deletions
@@ -11,6 +11,9 @@
{%- set checking_now = is_checking_now(watch) -%}
{%- set history_n = watch.history_n -%}
{%- set favicon = watch.get_favicon_filename() -%}
{# Resolved once per row: each read re-validates the URL and, for a Jinja2 templated
URL, builds a fresh sandboxed environment - see validate_url.py #}
{%- set open_link = watch.open_link -%}
{%- set error_texts = watch.compile_error_texts(has_proxies=has_proxies) -%}
{%- set system_use_url_watchlist = datastore.data['settings']['application']['ui'].get('use_page_title_in_list') -%}
{# Class settings mirrored in changedetectionio/static/js/realtime.js for the frontend #}
@@ -48,7 +51,7 @@
<div class="grid-wrapper">
{% if 'favicons_enabled' not in ui_settings or ui_settings['favicons_enabled'] %}
<div class="favicon">
<a target="_blank" rel="noopener" href="{{ watch.open_link }}">
<a target="_blank" rel="noopener" href="{{ open_link }}">
{# Intersection Observer lazy loading: store real URL in data-src, load only when visible in viewport #}
<img alt="Favicon thumbnail"
class="favicon lazy-favicon"
@@ -74,7 +77,7 @@
{% else %}
{{ watch.get('title') or watch.link }}
{% endif %}
<a class="external" target="_blank" rel="noopener" href="{{ watch.open_link }}" title="{{ watch.open_link }}">&nbsp;</a>
<a class="external" target="_blank" rel="noopener" href="{{ open_link }}" title="{{ open_link }}">&nbsp;</a>
</span>
{%- for watch_tag_uuid, watch_tag in datastore.get_all_tags_for_watch(watch['uuid']).items() -%}
+3 -3
View File
@@ -351,7 +351,7 @@ def set_basic_notification_vars(current_snapshot, prev_snapshot, watch, triggere
'triggered_text': triggered_text,
'uuid': watch.get('uuid') if watch else None,
'watch_url': watch.get('url') if watch else None,
'watch_open_url': watch.open_link if watch else None,
'watch_open_url': (watch.open_link_override or watch.get('url')) if watch else None,
'watch_uuid': watch.get('uuid') if watch else None,
'watch_mime_type': watch.get('content-type')
}
@@ -513,7 +513,7 @@ Thanks - Your omniscient changedetection.io installation.
if 'notification_urls' in n_object:
n_object.update({
'watch_url': watch['url'],
'watch_open_url': watch.open_link,
'watch_open_url': watch.open_link_override or watch['url'],
'uuid': watch_uuid,
'screenshot': None
})
@@ -563,7 +563,7 @@ Thanks - Your omniscient changedetection.io installation.
if 'notification_urls' in n_object:
n_object.update({
'watch_url': watch['url'],
'watch_open_url': watch.open_link,
'watch_open_url': watch.open_link_override or watch['url'],
'uuid': watch_uuid
})
self.notification_q.put(n_object)