From c596cf6c148051ff1181030826d7994a86cd66cd Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Fri, 17 Jul 2026 15:29:24 +0200 Subject: [PATCH] WIP --- .../blueprint/browser_steps/__init__.py | 5 +- .../blueprint/settings/__init__.py | 25 +-- .../settings/templates/settings.html | 14 +- .../blueprint/ui/browser_config.py | 7 +- .../ui/templates/browsers-overview.html | 68 +++++--- .../content_fetchers/__init__.py | 44 ++---- changedetectionio/forms.py | 6 +- changedetectionio/model/Watch.py | 148 ++++++++++++------ changedetectionio/model/browser_config.py | 139 +++++++--------- changedetectionio/store/__init__.py | 12 +- changedetectionio/store/updates.py | 40 +++++ .../test_custom_browser_url.py | 9 +- .../tests/fetchers/test_content.py | 9 +- .../tests/restock/test_restock.py | 9 +- .../tests/test_access_control.py | 5 +- .../tests/test_browser_config.py | 112 +++++++++++++ 16 files changed, 449 insertions(+), 203 deletions(-) diff --git a/changedetectionio/blueprint/browser_steps/__init__.py b/changedetectionio/blueprint/browser_steps/__init__.py index 58be9a95..b9e2a307 100644 --- a/changedetectionio/blueprint/browser_steps/__init__.py +++ b/changedetectionio/blueprint/browser_steps/__init__.py @@ -266,9 +266,8 @@ def construct_blueprint(datastore: ChangeDetectionStore): # Resolve the fetcher backend for this watch so we can ask it to launch its own browser # if it supports that (e.g. CloakBrowser, which runs locally rather than via CDP) watch = datastore.data['watching'][watch_uuid] - fetcher_name = watch.get_fetch_backend or 'system' - if fetcher_name == 'system': - fetcher_name = datastore.data['settings']['application'].get('fetch_backend', 'html_requests') + # get_fetch_backend is fully resolved (group override / watch / 'system' -> global default). + fetcher_name = watch.get_fetch_backend browser, playwright_context = await acquire_browser_for_fetcher(fetcher_name, proxy=proxy, keepalive_ms=keepalive_ms) diff --git a/changedetectionio/blueprint/settings/__init__.py b/changedetectionio/blueprint/settings/__init__.py index 164436a6..9a1c76a4 100644 --- a/changedetectionio/blueprint/settings/__init__.py +++ b/changedetectionio/blueprint/settings/__init__.py @@ -66,17 +66,10 @@ def construct_blueprint(datastore: ChangeDetectionStore): extra_notification_tokens=datastore.get_unique_notification_tokens_available() ) - # The global default fetch method is now the "Default browser": the always-present - # built-in engines + the user's saved browsers (no 'system' - this IS the system - # default). Everything else in the app resolves 'system' back to this value. - from changedetectionio.model.browser_config import list_builtin_browsers - default_browser_choices = [(b['id'], b['label']) for b in list_builtin_browsers()] - default_browser_choices += [(cid, e.get('label') or cid) - for cid, e in datastore.browser_config_store.all().items()] - form.application.form.fetch_backend.choices = default_browser_choices - form.application.form.fetch_backend.label.text = gettext('Default browser') - # Accept legacy raw-engine values / ids not in the (dynamic) choice list. - form.application.form.fetch_backend.validate_choice = False + # The global "Default browser" was migrated to the /browsers tab (its per-row radio + # writes settings.application.fetch_backend). Drop the inherited fetch_backend field so a + # settings save never renders, validates, or clobbers the default - /browsers owns it now. + del form.application.form.fetch_backend # Remove the last option 'System default' form.application.form.notification_format.choices.pop() @@ -260,8 +253,18 @@ def construct_blueprint(datastore: ChangeDetectionStore): # Cost display: only when user configured their own key (not hosted/operator-managed) llm_show_costs = not llm_env_configured + # Read-only label for the global "Default browser" (managed on the /browsers tab). + from changedetectionio.model.browser_config import list_builtin_browsers + _default_id = datastore.get_default_backend() + _default_entry = datastore.browser_config_store.get(_default_id) + if _default_entry: + default_browser_label = _default_entry.get('label') or _default_id + else: + default_browser_label = dict((b['id'], b['label']) for b in list_builtin_browsers()).get(_default_id, _default_id) + output = render_template("settings.html", active_plugins=active_plugins, + default_browser_label=default_browser_label, api_key=datastore.data['settings']['application'].get('api_access_token'), llm_config=llm_config, llm_env_configured=llm_env_configured, diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html index 1d5ffa63..5aadb105 100644 --- a/changedetectionio/blueprint/settings/templates/settings.html +++ b/changedetectionio/blueprint/settings/templates/settings.html @@ -100,14 +100,18 @@
-
- {{ render_field(form.application.form.fetch_backend, class="fetch-backend") }} +
+ +
+ {{ default_browser_label }} + {{ _('Manage browsers & set default')|safe }} +
-

{{ _('Use the Basic method (default) where your watched sites don\'t need Javascript to render.')|safe }}

-

{{ _('The Chrome/Javascript method requires a network connection to a running WebDriver+Chrome server, set by the ENV var \'WEBDRIVER_URL\'.')|safe }}

+

{{ _('Every watch uses this browser unless it (or its group) picks another one.')|safe }}

+

{{ _('Add screen sizes, languages, timezones and choose the default on the Browsers page.', url=url_for('ui.browser_config.browsers_overview'))|safe }}

-
+
{{ _('If you\'re having trouble waiting for the page to be fully rendered (text missing etc), try increasing the \'wait\' time here.') }}
diff --git a/changedetectionio/blueprint/ui/browser_config.py b/changedetectionio/blueprint/ui/browser_config.py index 8e4e2c65..edba309e 100644 --- a/changedetectionio/blueprint/ui/browser_config.py +++ b/changedetectionio/blueprint/ui/browser_config.py @@ -208,12 +208,15 @@ def construct_blueprint(datastore: ChangeDetectionStore): @browser_config_blueprint.route("/browsers/set-default/", methods=['POST']) @login_optionally_required def browser_config_set_default(config_id): - # "Default" is the global system fetch_backend - the single source of truth that a - # watch/group set to 'system' resolves to. This is settable from here or from Settings. + # "Default browser" is the global settings.application.fetch_backend - the single source + # of truth a watch/group set to 'system' resolves to. This /browsers tab is now the only + # place it's set (the Settings page shows it read-only). Only usable (ready-to-use) + # built-in engines or saved browser configs may be the default. from changedetectionio.model.browser_config import list_builtin_browsers builtins = {b['id'] for b in list_builtin_browsers()} if config_id in builtins or datastore.browser_config_store.get(config_id): datastore.data['settings']['application']['fetch_backend'] = config_id + logger.debug(f"Default browser (settings.application.fetch_backend) set to '{config_id}'") flash(gettext("Default browser set")) else: flash(gettext("Browser config not found"), 'error') diff --git a/changedetectionio/blueprint/ui/templates/browsers-overview.html b/changedetectionio/blueprint/ui/templates/browsers-overview.html index b2d8608f..9d79c039 100644 --- a/changedetectionio/blueprint/ui/templates/browsers-overview.html +++ b/changedetectionio/blueprint/ui/templates/browsers-overview.html @@ -8,16 +8,27 @@

{{ _('Save a browser once (its screen size, language, timezone…) and reuse it on any watch or group. Add a variation from one of the available browsers below.') }}

+

+ {{ _('The Default browser (chosen with the radio button below) is used by every watch unless the watch — or its group — picks another one. This is the same value shown on the Settings → Fetching page.', url=url_for('settings.settings_page'))|safe }} +

{# ---- User-created browser variations ---- #}
{{ _('Your browsers') }}
{% if browser_configs %} - - +
{{ _('Name') }}{{ _('Setup') }}
+ {% for cid, entry in browser_configs.items() %} {% set bc = entry.get('browser_config') or {} %} - + + -
{{ _('Default') }}{{ _('Name') }}{{ _('Setup') }}
+
+ + +
+
{{ entry.get('label') }} {{ entry.get('base_fetcher') }} @@ -30,14 +41,6 @@ {% if bc.get('timezone_id') %}{{ bc.timezone_id }}{% endif %} {% if bc.get('block_resource_types') %}{{ _('no') }} {{ bc.block_resource_types|join(', ') }}{% endif %} - {% if cid != default_browser_id %} -
- - -
- {% endif %} -
{{ _('Edit') }}
{{ _('Available browsers') }} {% if base_fetchers %} - - +
{{ _('Browser') }}{{ _('Capabilities') }}
+ {% for f in base_fetchers %} {% set fbc = f.browser_config or {} %} - + + @@ -102,6 +110,21 @@ + +
{{ _('Default') }}{{ _('Browser') }}{{ _('Capabilities') }}
+ {# Only ready-to-use engines can be the default (base-only engines aren't usable directly) #} + {% if f.ready_to_use %} + + + + + {% endif %} + {{ f.description }} {% if not f.ready_to_use %}{{ _('needs setup') }}{% endif %} @@ -74,14 +88,8 @@ {% endfor %} - {# ready-to-use engines are usable directly: can be edited + made default #} + {# ready-to-use engines are usable directly: they can be edited #} {% if f.ready_to_use %} - {% if f.name != default_browser_id %} -
- - -
- {% endif %} {{ _('Edit') }} {% endif %}