Work on link display logic for add-watch-ui

This commit is contained in:
dgtlmoon
2026-08-26 11:13:08 +02:00
parent f16c6f6c6b
commit 09f3700a0a
25 changed files with 157 additions and 18 deletions
@@ -1,4 +1,5 @@
from flask import Blueprint, render_template, request, jsonify, make_response
from flask import Blueprint, render_template, request, jsonify, make_response, flash, redirect, url_for
from flask_babel import gettext
from loguru import logger
from changedetectionio import forms
@@ -17,6 +18,14 @@ def construct_blueprint(datastore: ChangeDetectionStore):
from changedetectionio.llm.evaluator import get_llm_config as _get_llm_config
from changedetectionio.llm.ui_strings import LLM_INTENT_WATCH_PLACEHOLDER
# Same gate that hides the sidebar link (sidebar-nav.html): with no browser that can
# render a preview there is nothing for the visual selector to work on, so bounce
# direct navigation rather than serving a page whose only outcome is an error.
if not browser_config.has_visual_browser(datastore):
flash(gettext("Adding a watch with a browser needs an interactive browser "
"(screenshots + element data) - none is configured."), 'error')
return redirect(url_for('watchlist.index'))
form = forms.quickWatchForm(None)
llm_configured = bool(_get_llm_config(datastore))
@@ -83,6 +83,15 @@ def list_visual_browser_choices(datastore):
return choices
def has_visual_browser(datastore):
"""True when at least one installed browser can render a live preview.
Gates the whole Add-Watch page (sidebar link + the route itself): without one there
is nothing for the visual selector to work on, so the page can only fail.
"""
return bool(list_visual_browser_choices(datastore))
def default_visual_browser(datastore):
"""Which browser the Add-Watch page should start on.
+8
View File
@@ -611,6 +611,14 @@ def changedetection_app(config=None, datastore_o=None):
from changedetectionio.llm.evaluator import is_llm_features_disabled
return dict(llm_features_disabled=is_llm_features_disabled())
@app.context_processor
def inject_has_visual_browser():
# Whether any installed content fetcher can render the Add-Watch live preview -
# sidebar-nav.html hides the Add-Watch link without one. Same capability lookup the
# page's browser picker and /snapshot use, so they can't disagree.
from changedetectionio.blueprint.add_watch_ui import browser_config
return dict(has_visual_browser=browser_config.has_visual_browser(datastore))
# Set up a request hook to check authentication for all routes
@app.before_request
def check_authentication():
@@ -387,8 +387,8 @@ body.watch-selection-active #checkbox-operations {
transparent
);
outline-offset: 1px;
border-radius: 4px;
}
border-radius: var(--common-round-border);
}
// Reserved for future use
@@ -18,6 +18,9 @@
</a>
{%- endif -%}
</li>
{# Add-Watch needs a browser that can render a live preview (see the add_watch_ui
blueprint's browser_config) - without one the page can only fail, so don't offer it #}
{%- if has_visual_browser -%}
<li class="action-sidebar-li">
<a href="{{ url_for('add_watch_ui.add_watch_ui_index') }}"
class="action-sidebar-item {% if ep.startswith('add_watch_ui.') %}active{% endif %}"
@@ -37,6 +40,7 @@
<span class="action-label">{{ _('Add a page watch') }}</span>
</a>
</li>
{%- endif -%}
<li class="action-sidebar-li">
<a href="{{ url_for('watchlist.index') }}"
class="action-sidebar-item {% if ep == 'watchlist.index' or ep.startswith('ui.ui_views') or ep.startswith('ui.ui_edit') %}active{% endif %}"
@@ -22,6 +22,13 @@ def _llm_markers_absent(body: bytes, where: str = ''):
def test_llm_features_disabled_hides_ui(client, live_server, monkeypatch):
monkeypatch.setenv('LLM_FEATURES_DISABLED', 'true')
# The Add-Watch page (checked at the end) is gated on having a browser that can render a
# live preview, which a plain test container doesn't - pretend one is installed so this
# test stays about LLM surfaces, not about which fetchers happen to be available.
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
# Sanity: helper reports the env var is in effect
from changedetectionio.llm.evaluator import is_llm_features_disabled, get_llm_config
assert is_llm_features_disabled() is True
@@ -74,6 +81,9 @@ def test_llm_features_enabled_by_default(client, live_server, monkeypatch):
# With an LLM configured, the Add-watch page offers both ways of narrowing a watch,
# so "Select by element" goes back to being an opt-in checkbox.
monkeypatch.setenv('LLM_MODEL', 'gemini/gemini-2.5-flash')
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
assert res.status_code == 200
assert b'name="llm_intent"' in res.data
@@ -17,6 +17,43 @@ def _datastore(client):
return client.application.config.get('DATASTORE')
def test_hidden_without_a_live_preview_browser(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""No capable browser -> the sidebar link is hidden and the route bounces.
One seam is patched deliberately: has_visual_browser() (page + sidebar gate) and
default_visual_browser() both resolve through list_visual_browser_choices(), so the
gate can't disagree with what the picker would have offered.
"""
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'list_visual_browser_choices', lambda datastore: [])
# Sidebar link is not rendered
res = client.get(url_for('watchlist.index'))
assert url_for('add_watch_ui.add_watch_ui_index').encode() not in res.data
# Direct navigation is bounced back to the watch list
res = client.get(url_for('add_watch_ui.add_watch_ui_index'), follow_redirects=True)
assert b'name="fetch_backend"' not in res.data
assert b'needs an interactive browser' in res.data
def test_shown_when_a_live_preview_browser_exists(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""A capable browser -> the sidebar link is offered and the page serves its picker."""
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
res = client.get(url_for('watchlist.index'))
assert url_for('add_watch_ui.add_watch_ui_index').encode() in res.data
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
assert res.status_code == 200
assert b'name="fetch_backend"' in res.data
assert b'value="html_webdriver"' in res.data
# The system default resolves to html_requests (no preview), so a real browser is preselected
assert b'checked' in res.data
def test_browser_picker_lists_only_live_preview_capable(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""Capable browsers are offered; the plain HTTP client never is."""
from changedetectionio.blueprint.add_watch_ui import browser_config
@@ -37,20 +74,6 @@ def test_browser_picker_lists_only_live_preview_capable(client, live_server, mea
assert b'disabled' in res.data
def test_browser_picker_disables_incapable_system_default(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""With nothing capable at all, 'system' is still shown (disabled) rather than vanishing."""
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'is_visual_capable', lambda name, datastore: False)
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
assert res.status_code == 200
assert b'name="fetch_backend"' in res.data
assert b'value="system"' in res.data
assert b'disabled' in res.data
# ...and nothing else is offered
assert b'value="html_' not in res.data
def test_snapshot_refuses_browser_that_cannot_preview(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""/snapshot won't spend a fetch on a browser that produces no screenshot."""
from changedetectionio.blueprint.add_watch_ui import browser_config
+9 -1
View File
@@ -68,7 +68,7 @@ def test_zh_Hant_TW_timeago_integration():
assert '天前' in result_3d, f"Expected '天前' in '{result_3d}'"
def test_language_switching(client, live_server, measure_memory_usage, datastore_path):
def test_language_switching(client, live_server, measure_memory_usage, datastore_path, monkeypatch):
"""
Test that the language switching functionality works correctly.
@@ -77,6 +77,14 @@ def test_language_switching(client, live_server, measure_memory_usage, datastore
3. Switch back to English and verify English text appears
"""
# The Add-Watch page is only served when a browser that can render a live preview is
# installed (it is the page this test reads translated processor labels off). A plain
# test container has none - without PLAYWRIGHT_DRIVER_URL html_webdriver is Selenium,
# which cannot - so pretend one exists rather than assert against a redirect.
from changedetectionio.blueprint.add_watch_ui import browser_config
monkeypatch.setattr(browser_config, 'list_visual_browser_choices',
lambda datastore: [('html_webdriver', 'WebDriver Chrome/Javascript')])
# Establish session cookie
client.get(url_for("add_watch_ui.add_watch_ui_index"), follow_redirects=True)
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -14,6 +14,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -19,6 +19,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
+5 -1
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: changedetection.io 0.55.8\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2026-08-25 14:13+0200\n"
"POT-Creation-Date: 2026-08-25 17:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -17,6 +17,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -14,6 +14,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -19,6 +19,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -17,6 +17,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -19,6 +19,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -17,6 +17,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
@@ -18,6 +18,10 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/__init__.py
msgid "Adding a watch with a browser needs an interactive browser (screenshots + element data) - none is configured."
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"