diff --git a/changedetectionio/blueprint/add_watch_ui/__init__.py b/changedetectionio/blueprint/add_watch_ui/__init__.py index dddeac1f..7d502ee4 100644 --- a/changedetectionio/blueprint/add_watch_ui/__init__.py +++ b/changedetectionio/blueprint/add_watch_ui/__init__.py @@ -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)) diff --git a/changedetectionio/blueprint/add_watch_ui/browser_config.py b/changedetectionio/blueprint/add_watch_ui/browser_config.py index 2b36a60e..ad9fe23f 100644 --- a/changedetectionio/blueprint/add_watch_ui/browser_config.py +++ b/changedetectionio/blueprint/add_watch_ui/browser_config.py @@ -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. diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 8cc0961b..93937850 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -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(): diff --git a/changedetectionio/static/styles/scss/parts/_watch_table.scss b/changedetectionio/static/styles/scss/parts/_watch_table.scss index 91d089c4..3e9492cd 100644 --- a/changedetectionio/static/styles/scss/parts/_watch_table.scss +++ b/changedetectionio/static/styles/scss/parts/_watch_table.scss @@ -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 diff --git a/changedetectionio/templates/sidebar-nav.html b/changedetectionio/templates/sidebar-nav.html index 2c0b0fb6..0baac75c 100644 --- a/changedetectionio/templates/sidebar-nav.html +++ b/changedetectionio/templates/sidebar-nav.html @@ -18,6 +18,9 @@ {%- endif -%} + {# 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 -%}
  • {{ _('Add a page watch') }}
  • + {%- endif -%}
  • 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 diff --git a/changedetectionio/tests/test_i18n.py b/changedetectionio/tests/test_i18n.py index 82db5f7c..c23bf57c 100644 --- a/changedetectionio/tests/test_i18n.py +++ b/changedetectionio/tests/test_i18n.py @@ -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) diff --git a/changedetectionio/translations/cs/LC_MESSAGES/messages.po b/changedetectionio/translations/cs/LC_MESSAGES/messages.po index 887641e8..9091de16 100644 --- a/changedetectionio/translations/cs/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/cs/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/de/LC_MESSAGES/messages.po b/changedetectionio/translations/de/LC_MESSAGES/messages.po index 56a9545c..db1286c5 100644 --- a/changedetectionio/translations/de/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/de/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po b/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po index 765f37c4..b6c73fe8 100644 --- a/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/en_GB/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/en_US/LC_MESSAGES/messages.po b/changedetectionio/translations/en_US/LC_MESSAGES/messages.po index a351a08d..0a674a6f 100644 --- a/changedetectionio/translations/en_US/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/en_US/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/es/LC_MESSAGES/messages.po b/changedetectionio/translations/es/LC_MESSAGES/messages.po index 67ede8d3..c6b0db8a 100644 --- a/changedetectionio/translations/es/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/es/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/fr/LC_MESSAGES/messages.po b/changedetectionio/translations/fr/LC_MESSAGES/messages.po index 1a63ee42..5923fc49 100644 --- a/changedetectionio/translations/fr/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/fr/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/it/LC_MESSAGES/messages.po b/changedetectionio/translations/it/LC_MESSAGES/messages.po index 5a53193f..ae18711e 100644 --- a/changedetectionio/translations/it/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/it/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/ja/LC_MESSAGES/messages.po b/changedetectionio/translations/ja/LC_MESSAGES/messages.po index 91c2d692..96adf958 100644 --- a/changedetectionio/translations/ja/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/ja/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/ko/LC_MESSAGES/messages.po b/changedetectionio/translations/ko/LC_MESSAGES/messages.po index e7b61fe1..831d0750 100644 --- a/changedetectionio/translations/ko/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/ko/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/messages.pot b/changedetectionio/translations/messages.pot index d4f4ce33..340fad0c 100644 --- a/changedetectionio/translations/messages.pot +++ b/changedetectionio/translations/messages.pot @@ -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 \n" "Language-Team: LANGUAGE \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)" diff --git a/changedetectionio/translations/pl/LC_MESSAGES/messages.po b/changedetectionio/translations/pl/LC_MESSAGES/messages.po index 9ff0b57f..452f591b 100644 --- a/changedetectionio/translations/pl/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/pl/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/pt_BR/LC_MESSAGES/messages.po b/changedetectionio/translations/pt_BR/LC_MESSAGES/messages.po index 14d06e21..161d22b3 100644 --- a/changedetectionio/translations/pt_BR/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/pt_BR/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/ru/LC_MESSAGES/messages.po b/changedetectionio/translations/ru/LC_MESSAGES/messages.po index 878b3efe..923ddfb9 100644 --- a/changedetectionio/translations/ru/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/ru/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/tr/LC_MESSAGES/messages.po b/changedetectionio/translations/tr/LC_MESSAGES/messages.po index 64e40de5..d69da53f 100644 --- a/changedetectionio/translations/tr/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/tr/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/uk/LC_MESSAGES/messages.po b/changedetectionio/translations/uk/LC_MESSAGES/messages.po index 4b4e5a0d..feeeea74 100644 --- a/changedetectionio/translations/uk/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/uk/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/zh/LC_MESSAGES/messages.po b/changedetectionio/translations/zh/LC_MESSAGES/messages.po index 99422560..85afc827 100644 --- a/changedetectionio/translations/zh/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/zh/LC_MESSAGES/messages.po @@ -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)" diff --git a/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po b/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po index b9356d82..a64bb70e 100644 --- a/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po +++ b/changedetectionio/translations/zh_Hant_TW/LC_MESSAGES/messages.po @@ -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)"