Files
changedetection.io/changedetectionio/blueprint/ui/templates/_browser_config_fields.html
T
dgtlmoonandClaude Opus 5 ba2280ba81 External CDP Browser engine - replaces the extra_browser_* hackery
An "extra browser" was a name + a ws(s):// endpoint in settings.requests.extra_browsers,
selected by a watch as the magic string 'extra_browser_<name>'. That string resolved to
html_webdriver plus a custom connection URL, which meant the protocol the endpoint was
spoken to came from env vars rather than from the entry: CDP over a WebSocket with
PLAYWRIGHT_DRIVER_URL set, CDP via pyppeteer with FAST_PUPPETEER_CHROME_FETCHER, and the
W3C WebDriver protocol over HTTP on a Selenium-only install - where a wss:// URL cannot
work at all. The form only ever accepted ws:// / wss://, so the feature was silently
broken on exactly the installs that could not honour it.

So it becomes an engine, html_external_cdp, which pins the protocol: a subclass of the
Playwright fetcher that takes its endpoint from the watch's browser config
(FetcherConfig.connection_url) instead of the environment. It is base-only
(ready_to_use=False) because an endpoint is required, so each endpoint is one browser
config ("variation") on the Browsers page - which is what the old settings list was.

update_36 migrates each extra_browsers row to such a variation, keyed by the SAME
'extra_browser_<name>' string watches already hold, so no watch, group override, API value
or global default needs rewriting; the legacy selector simply becomes a real browser-config
id. A row whose endpoint the model rejects is logged and skipped rather than taking the
update chain, and with it startup, down.

Knock-on cleanups, all of which delete a special case rather than add one:

 - The proxy opt-out for custom endpoints is now Fetcher.ignores_proxy_setting, asked of
   the engine, instead of a string-prefix test in call_browser().
 - A live browser-steps / visual-selector session asks the engine where to connect
   (Fetcher.browser_steps_connection_url, overridden by html_external_cdp) and refuses an
   engine whose supports_browser_steps is False, instead of reading the env var itself and
   silently stepping a browser the watch does not check with. That refusal is real: on a
   Selenium install html_webdriver cannot drive a live session.
 - is_valid_browser_selector() answers "may a watch store this in fetch_backend?" in one
   place; the API (create/update/import), the quick-add form validator and the bulk "set
   browser" operation each had their own copy, which is how they came to disagree about
   whether a browser-config id was acceptable.
 - api-spec.yaml's fetch_backend pattern enumerated extra_browser_* while rejecting
   browser-config ids and every engine newer than html_webdriver. Valid values are
   per-install, so the schema now bounds the string and the handlers do the real check.
 - html_external_cdp registers unconditionally (unlike html_playwright_builtin): migrated
   configs name it, so it must resolve even without the playwright library, or those
   watches would quietly fetch with the plain HTTP client. The library is imported lazily
   inside run(), and an unavailable engine now warns instead of falling back silently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:39:16 +02:00

139 lines
6.1 KiB
HTML

{# Browser config form fields for the add/edit page. Expects `form`, `applicable` (the set of
FetcherConfig fields this base engine can honour - FetcherConfig.applicable_fields()) and
`base_label` (read-only base engine name). The base engine is fixed by the page (chosen via
the "Add variation" link, or the existing config's base), so it's shown read-only, not as a
picker.
Gating on `applicable` rather than on capability flags directly is deliberate: it is the same
set the save path allows (FetcherConfig.from_submitted), so a field can never be rendered but
unsaveable, or hidden but written from its widget default. #}
{% from '_helpers.html' import render_field, render_checkbox_field %}
<fieldset>
<div class="pure-control-group">
<label>{{ _('Based on browser') }}</label>
<div><strong>{{ base_label }}</strong></div>
</div>
<div class="pure-control-group">
{{ render_field(form.label) }}
<span class="pure-form-message-inline">{{ _('A friendly name, e.g. “Mobile” or “German desktop”.') }}</span>
</div>
{% if 'browser_type' in applicable %}
<div class="pure-control-group">
{{ render_field(form.browser_type) }}
<span class="pure-form-message-inline">{{ _('Which browser engine to launch locally.') }}</span>
</div>
{% endif %}
{% if 'delete_created_files' in applicable %}
<div class="pure-control-group">{{ render_checkbox_field(form.delete_created_files) }}</div>
{% endif %}
</fieldset>
{% if 'viewport_width' in applicable %}
<fieldset>
<legend>{{ _('Screen size') }}</legend>
<div class="pure-control-group">
<div class="viewport-presets">
<button type="button" class="pure-button button-small" data-w="1920" data-h="1080">{{ _('Desktop') }}</button>
<button type="button" class="pure-button button-small" data-w="1366" data-h="768">{{ _('Laptop') }}</button>
<button type="button" class="pure-button button-small" data-w="768" data-h="1024">{{ _('Tablet') }}</button>
<button type="button" class="pure-button button-small" data-w="390" data-h="844">{{ _('Mobile') }}</button>
<button type="button" class="pure-button button-small" data-w="" data-h="">{{ _('Clear') }}</button>
</div>
</div>
<div class="pure-control-group">{{ render_field(form.viewport_width) }}</div>
<div class="pure-control-group">{{ render_field(form.viewport_height) }}</div>
</fieldset>
<fieldset>
<legend>{{ _('Language & timezone') }}</legend>
<div class="pure-control-group">
{{ render_field(form.locale) }}
<datalist id="locale-datalist">
{% for l in locale_choices or [] %}<option value="{{ l }}"></option>{% endfor %}
</datalist>
<span class="pure-form-message-inline">{{ _('Start typing to search - e.g. <code>de-DE</code>, <code>en-GB</code>.')|safe }}</span>
</div>
<div class="pure-control-group">
{{ render_field(form.timezone_id) }}
<datalist id="timezone-datalist">
{% for tz in timezone_choices or [] %}<option value="{{ tz }}"></option>{% endfor %}
</datalist>
<span class="pure-form-message-inline">{{ _('Start typing to search - e.g. <code>Europe/Berlin</code>.')|safe }}</span>
</div>
</fieldset>
{% endif %}
{% if 'connection_url' in applicable %}
<fieldset>
<legend>{{ _('Browser endpoint') }}</legend>
<div class="pure-control-group">
{{ render_field(form.connection_url) }}
<span class="pure-form-message-inline">
{{ _('Address of the external browser to connect to, e.g. a Bright Data or Oxylabs Scraping Browser.') }}
<a href="https://changedetection.io/tutorial/using-bright-datas-scraping-browser-pass-captchas-and-other-protection-when-monitoring">{{ _('More instructions and examples here') }}</a>
</span>
</div>
</fieldset>
{% endif %}
{% if 'timeout' in applicable or 'user_agent' in applicable %}
<fieldset>
<legend>{{ _('HTTP request options') }}</legend>
{% if 'timeout' in applicable %}
<div class="pure-control-group">
{{ render_field(form.timeout) }}
<span class="pure-form-message-inline">{{ _('Maximum seconds to wait for the response (plain HTTP client only).') }}</span>
</div>
{% endif %}
{% if 'user_agent' in applicable %}
<div class="pure-control-group">
{{ render_field(form.user_agent) }}
<span class="pure-form-message-inline">{{ _('Sent as the User-Agent header for requests using this browser.') }}</span>
</div>
{% endif %}
</fieldset>
{% endif %}
{% if 'block_resource_types' in applicable %}
<fieldset>
<legend>{{ _('Skip downloads (save bandwidth)') }}</legend>
<div class="pure-control-group">
<label>{{ form.block_resource_types.label.text }}</label>
<div class="browser-inline-checkboxes">
{% for opt in form.block_resource_types %}
<span class="checkbox">{{ opt() }} <label for="{{ opt.id }}">{{ opt.label.text }}</label></span>
{% endfor %}
</div>
</div>
<div class="pure-control-group">
{{ render_field(form.block_url_patterns) }}
<span class="pure-form-message-inline">{{ _('One address pattern per line, e.g. <code>*.ttf</code>.')|safe }}</span>
</div>
</fieldset>
{% endif %}
{% if 'screenshot_format' in applicable %}
<fieldset>
<div class="pure-control-group">{{ render_field(form.screenshot_format) }}</div>
</fieldset>
{% endif %}
<script>
(function () {
var w = document.getElementById('{{ form.viewport_width.id }}');
var h = document.getElementById('{{ form.viewport_height.id }}');
document.querySelectorAll('.viewport-presets button').forEach(function (btn) {
btn.addEventListener('click', function () {
if (w) w.value = this.getAttribute('data-w');
if (h) h.value = this.getAttribute('data-h');
});
});
})();
</script>
<style>
.viewport-presets { display:flex; flex-wrap:wrap; gap:0.4em; }
.browser-inline-checkboxes { display:flex; flex-wrap:wrap; gap:0.4em 1.2em; }
.browser-inline-checkboxes .checkbox { white-space:nowrap; }
</style>