Add watch UI should show which browser(s) are available and set default browser (#4334)

This commit is contained in:
dgtlmoon
2026-08-25 20:15:59 +02:00
committed by GitHub
parent 22ab668f83
commit bb2e6adece
29 changed files with 917 additions and 81 deletions
@@ -3,6 +3,7 @@ from loguru import logger
from changedetectionio import forms
from changedetectionio.auth_decorator import login_optionally_required
from . import browser_config
from changedetectionio.store import ChangeDetectionStore
from changedetectionio.validate_url import is_fetch_url_allowed
@@ -19,11 +20,17 @@ def construct_blueprint(datastore: ChangeDetectionStore):
form = forms.quickWatchForm(None)
llm_configured = bool(_get_llm_config(datastore))
# Start on the browser the live preview would actually use
form.fetch_backend.data = browser_config.default_visual_browser(datastore)
return render_template(
"add-watch-ui.html",
form=form,
llm_configured=llm_configured,
llm_intent_watch_placeholder=LLM_INTENT_WATCH_PLACEHOLDER,
# Listed but not selectable (the system default when it can't render a preview)
unusable_browsers=browser_config.unusable_values(datastore),
system_default_browser=browser_config.system_default_description(datastore),
)
@add_watch_ui_blueprint.route("/snapshot", methods=['GET'])
@@ -61,15 +68,27 @@ def construct_blueprint(datastore: ChangeDetectionStore):
logger.warning(f"Add-watch snapshot: refused '{url}' - {reason}")
return make_response(reason, 400)
# Use whatever fetcher the application is configured to use by default
# (e.g. CloakBrowser, Playwright/sockpuppet) so the preview matches real checks.
fetcher_name = datastore.data['settings']['application'].get('fetch_backend', 'html_requests')
logger.debug(f"Add-watch snapshot: fetching '{url}' using system default fetcher '{fetcher_name}'")
# Which browser to preview with. The page posts the one picked in its browser list;
# with nothing asked for we fall back to whatever it would have preselected.
# Either way it has to be able to render a preview - the plain HTTP client
# produces no screenshot and no element data, so previewing with it is pointless
# (and it used to be the silent default here, see the system-default bug).
fetcher_name = (request.args.get('fetch_backend') or '').strip() or browser_config.default_visual_browser(datastore)
if not fetcher_name or not browser_config.is_visual_capable(fetcher_name, datastore):
logger.warning(f"Add-watch snapshot: refused browser '{fetcher_name}' for '{url}'")
return make_response('No interactive browser available that can render a live preview '
'(needs screenshots + element data)', 400)
# acquire_browser_for_fetcher() looks the name up as a fetcher class, so 'system'
# has to be collapsed to the real backend first or a fetcher that launches its own
# browser would be skipped in favour of the CDP endpoint.
resolved_fetcher = browser_config.resolve_backend(fetcher_name, datastore)
logger.debug(f"Add-watch snapshot: fetching '{url}' using '{fetcher_name}' (resolved: '{resolved_fetcher}')")
async def _fetch_snapshot():
keepalive_ms = 30 * 1000
browser, playwright_context = await acquire_browser_for_fetcher(
fetcher_name, proxy=None, keepalive_ms=keepalive_ms
resolved_fetcher, proxy=None, keepalive_ms=keepalive_ms
)
stepper = browsersteps_live_ui(playwright_browser=browser, proxy=None, start_url=url)
@@ -119,6 +138,13 @@ def construct_blueprint(datastore: ChangeDetectionStore):
with open(os.path.join(temp_dir, "preload-fetch.json"), 'w', encoding='utf-8') as f:
json.dump({"content": html, "status_code": 200,
"headers": {"content-type": "text/html"}}, f)
# This directory becomes the new watch's data_dir on submit, so the watch's own
# settings file is where the previewing browser belongs: record it here and the
# saved watch checks with the browser that actually rendered this snapshot,
# instead of falling back to a system default that may not even do screenshots.
# Read back by make_temporary_watch_active_watch(); commit() then rewrites it in full.
with open(os.path.join(temp_dir, "watch.json"), 'w', encoding='utf-8') as f:
json.dump({"fetch_backend": fetcher_name}, f)
except Exception as e:
logger.error(f"Add-watch snapshot: could not park temporary data for {url}: {e}")
temp_uuid = None
@@ -0,0 +1,130 @@
"""Which content fetchers can drive the Add-Watch live preview / visual selector.
Single source of truth for "can this browser render a live preview": the Add-Watch
browser list, the /snapshot endpoint and the submit-time form validator all resolve
through here, so the UI can never offer - and the server can never accept - a fetcher
that is unable to produce what the visual selector needs.
The preview is rendered by browsersteps_live_ui() (see blueprint/browser_steps), so a
usable browser needs `supports_browser_steps` *as well as* screenshots and xpath
element data. That third flag is what rules out Selenium/WebDriver: it can screenshot
during a normal check, but it cannot drive the interactive session the preview needs
(acquire_browser_for_fetcher() would quietly connect to PLAYWRIGHT_DRIVER_URL instead,
which is not the browser the user picked). It also rules out settings.requests
extra_browsers, which are WebDriver connection URLs, so they are not offered here.
"""
from loguru import logger
SYSTEM_DEFAULT = 'system'
# Every flag a fetcher must set to be usable for the Add-Watch live preview.
REQUIRED_CAPABILITIES = (
'supports_browser_steps',
'supports_screenshots',
'supports_xpath_element_data',
)
def is_visual_capable(fetch_backend, datastore):
"""True when this backend can render the Add-Watch live preview.
An unknown name - including anything a client made up - resolves to no fetcher
class and so to all-False capabilities, so it can never pass. That is what makes
the posted value safe without any string filtering of our own.
Reads the flags off the class the same way Watch.fetcher_supports_screenshots does,
rather than via pluggy_interface.get_fetcher_capabilities(): that logs every lookup
at INFO, and this runs for the whole fetcher list on any page carrying the quick-add
form. Plugin fetchers are registered as module attributes, so they resolve here too.
"""
from changedetectionio import content_fetchers
from changedetectionio.content_fetchers.base import FetcherCapabilities
name = fetch_backend or SYSTEM_DEFAULT
if name == SYSTEM_DEFAULT:
name = datastore.data['settings']['application'].get('fetch_backend') or 'html_requests'
caps = FetcherCapabilities.from_fetcher(getattr(content_fetchers, name, None))
missing = [flag for flag in REQUIRED_CAPABILITIES if not getattr(caps, flag, False)]
logger.debug(f"Add-watch browser '{fetch_backend or SYSTEM_DEFAULT}' -> '{name}': "
f"{', '.join(f'{k}={v}' for k, v in caps.model_dump().items())} - "
f"{'usable for live preview' if not missing else 'not offered, missing ' + ', '.join(missing)}")
return not missing
def resolve_backend(fetch_backend, datastore):
"""The concrete fetcher name behind a choice, so 'system' can be acted on.
Needed because acquire_browser_for_fetcher() looks the name up as a class: handing
it 'system' would skip a fetcher that launches its own browser (CloakBrowser) and
fall through to the CDP endpoint instead.
"""
from changedetectionio import content_fetchers
_fetcher_class, resolved, _custom_url = content_fetchers.resolve_content_fetcher(
{'fetch_backend': fetch_backend or SYSTEM_DEFAULT}, datastore)
return resolved
def list_visual_browser_choices(datastore):
"""(value, label) for every fetcher that can drive the visual selector.
is_visual_capable() logs each candidate's capabilities as it goes, so a user
wondering why their browser isn't in the list can see the missing flag at debug
level.
"""
from changedetectionio import content_fetchers
choices = [(name, str(description)) for name, description in content_fetchers.available_fetchers()
if is_visual_capable(name, datastore)]
logger.debug(f"Add-watch browsers offered for the live preview: "
f"{[name for name, _label in choices] or 'none'}")
return choices
def default_visual_browser(datastore):
"""Which browser the Add-Watch page should start on.
'system' when the global default happens to be capable (so the new watch keeps
following the system setting), otherwise the first capable browser, and None when
there is nothing usable at all.
"""
if is_visual_capable(SYSTEM_DEFAULT, datastore):
return SYSTEM_DEFAULT
choices = list_visual_browser_choices(datastore)
return choices[0][0] if choices else None
def system_default_description(datastore):
"""Label for whatever backend 'system' currently points at."""
from changedetectionio import content_fetchers
system_backend = datastore.data['settings']['application'].get('fetch_backend') or 'html_requests'
return str(dict(content_fetchers.available_fetchers()).get(system_backend, system_backend))
def radio_choices(datastore):
"""(value, label) for the Add-Watch browser radio list, system default first.
'System settings default' is always listed - with the reason in its own label when
the global default cannot render a preview, because greying it out explains why it
is unavailable where silently dropping it does not. Which entries are unusable is
reported separately by unusable_values(); WTForms cannot carry per-option render_kw
through a RadioField, so the disabled attribute is applied when rendering.
"""
from flask_babel import gettext
described = system_default_description(datastore)
if is_visual_capable(SYSTEM_DEFAULT, datastore):
system_label = gettext('System settings default (%(browser)s)', browser=described)
else:
system_label = gettext('System settings default (%(browser)s - no live preview)', browser=described)
return [(SYSTEM_DEFAULT, system_label)] + list_visual_browser_choices(datastore)
def unusable_values(datastore):
"""Values that are listed for explanation only and must render disabled."""
return set() if is_visual_capable(SYSTEM_DEFAULT, datastore) else {SYSTEM_DEFAULT}
@@ -14,6 +14,12 @@ $(document).ready(() => {
const $includeFilters = $('#include_filters');
const $temporaryUuid = $('#temporary_uuid');
// When the LLM intent box isn't available (LLM_FEATURES_DISABLED / not configured) the
// template renders no "Select by element" checkbox - element selection is the only thing
// this page can do, so it is on from the start and can't be turned off.
const selectionAlwaysOn = $byElement.length === 0;
const selectionEnabled = () => selectionAlwaysOn || $byElement.is(':checked');
const vs = window.initVisualSelector({
$canvas: $('#selector-canvas'),
$includeFilters: $includeFilters,
@@ -22,7 +28,7 @@ $(document).ready(() => {
$fetchingNotice: $('#add-watch-spinner .fetching-update-notice'),
$wrapper: $wrapper,
$clearButton: $clear,
enableSelection: false, // off until the user opts into "Select by element"
enableSelection: selectionAlwaysOn, // otherwise off until the user opts into "Select by element"
processorIsImage: false,
// The snapshot comes from the live browser-steps capture, so scale X by the page
// CSS width (browser_width) like browser-steps.js - handles device-scale-factor != 1.
@@ -36,8 +42,8 @@ $(document).ready(() => {
$error.toggle(which === 'error');
const ready = which === 'ready';
$wrapper.toggle(ready);
$xpathRow.toggle(ready && $byElement.is(':checked'));
$clear.toggle(ready && $byElement.is(':checked'));
$xpathRow.toggle(ready && selectionEnabled());
$clear.toggle(ready && selectionEnabled());
}
function fetchSnapshot() {
@@ -53,7 +59,9 @@ $(document).ready(() => {
$.ajax({
url: add_watch_snapshot_url,
data: {url: url},
// Preview with the browser picked in the list - that same browser is what
// gets saved on the watch, so what you see here is what it will check with.
data: {url: url, fetch_backend: $('input[name="fetch_backend"]:checked').val() || ''},
dataType: 'json',
}).done((data) => {
showState('ready');
@@ -54,10 +54,30 @@
{{ render_simple_field(form.processor) }}
</div>
<div class="add-watch-option-group" id="quick-watch-fetch-backend">
{# Rendered by hand rather than with render_field(): the system-default entry
is listed for explanation and has to render disabled, and WTForms can't
carry a per-option disabled attribute through a RadioField's choices. #}
<span class="label"><label>{{ form.fetch_backend.label.text }}</label></span>
<ul>
{%- for browser in form.fetch_backend -%}
{%- set unusable = browser.data in unusable_browsers -%}
<li{% if unusable %} class="unusable" title="{{ _('%(browser)s cannot render a live preview, choose a browser below', browser=system_default_browser) }}"{% endif %}>
{{ browser(disabled=true) if unusable else browser() }}{{ browser.label }}
</li>
{%- endfor -%}
</ul>
</div>
<div class="add-watch-option-group" id="by-element-toggle-group">
{%- if llm_configured -%}
<label class="pure-checkbox" for="by-element-toggle">
<input type="checkbox" id="by-element-toggle"> {{ _('Select by element') }}
</label>
{%- endif -%}
{# No LLM intent available? Then narrowing by element is the only thing this
page can do - selection is always on, so nothing is labelled or offered
here, just the hint on how to use the preview. #}
<span class="pure-form-message-inline">{{ _('Hover & click the preview to watch just one part of the page.') }}</span>
<a id="clear-selector" class="pure-button button-secondary button-xsmall" style="display: none;">{{ _('Clear selection') }}</a>
</div>
+7
View File
@@ -91,6 +91,13 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
processor = request.form.get('processor', processors.get_default_processor())
llm_intent = request.form.get('llm_intent', '').strip()
extras = {'paused': add_paused, 'processor': processor}
# Browser picked on the Add Watch page (validated against the live-preview capable
# list by the form). Absent from the watch-list quick-add, which leaves the new
# watch on the system default as before. A snapshot that recorded its own browser
# overrides this inside make_temporary_watch_active_watch().
if form.fetch_backend.data:
extras['fetch_backend'] = form.fetch_backend.data
if llm_intent:
extras['llm_intent'] = llm_intent
+63
View File
@@ -487,6 +487,39 @@ class ValidateContentFetcherIsReady(object):
# raise ValidationError(message % (field.data, e))
class ValidateKnownContentFetcher(object):
"""The posted fetch_backend has to name a fetcher this install actually has.
Deliberately *not* a live-preview capability check. This validator sits on the
shared quick-add form, whose POST endpoint is also how the watch list (and tests,
and scripts) add a watch with any legal backend - 'html_requests' included. Which
browsers the Add-Watch page *offers* is a rendering decision (see the add_watch_ui
blueprint's browser_config), and whether one can render a live preview is enforced
where that matters, in /snapshot.
Optional: no value posted means "leave it on the system default", as before.
"""
def __init__(self, message=None):
self.message = message
def __call__(self, form, field):
from flask import current_app
from changedetectionio import content_fetchers
if not field.data:
return
allowed = {'system'} | {name for name, _description in content_fetchers.available_fetchers()}
datastore = current_app.config.get('DATASTORE')
if datastore:
allowed |= {value for value, _label in datastore.extra_browsers}
if field.data not in allowed:
logger.warning(f"Rejected unknown fetch_backend {field.data!r} - known: {sorted(allowed)}")
raise ValidationError(self.message or gettext("Unknown fetch method."))
class ValidateNotificationBodyAndTitleWhenURLisSet(object):
"""
Validates that they entered something in both notification title+body when the URL is set
@@ -784,11 +817,41 @@ class ValidateStartsWithRegex(object):
if not self.pattern.match(stripped):
raise ValidationError(self.message or _l("Invalid value."))
def visual_browser_choices():
"""Browsers that can render the Add-Watch live preview, as RadioField choices.
Lazy import (the add_watch_ui blueprint imports this module) and empty outside an
app context, because WTForms evaluates a choices callable on field construction.
"""
from flask import current_app, has_app_context
from changedetectionio.blueprint.add_watch_ui import browser_config
if not has_app_context():
return []
datastore = current_app.config.get('DATASTORE')
return browser_config.radio_choices(datastore) if datastore else []
class quickWatchForm(Form):
url = StringField('URL', validators=[validateURL()])
tags = StringTagUUID(_l('Group tag'), validators=[validators.Optional()])
watch_submit_button = SubmitField(_l('Watch'), render_kw={"class": "pure-button pure-button-primary"})
processor = RadioField(_l('Processor'), choices=lambda: processors.available_processors(), default=processors.get_default_processor)
# Only the Add-Watch page renders this; the watch-list quick-add posts nothing, which
# leaves the new watch on 'system' exactly as before.
#
# A radio list rather than a dropdown: fetcher descriptions run long (they include the
# driver URL) and a wrapping label reads fine in a narrow pane, where a <select> would
# either overflow or need truncating.
#
# choices is only what the Add-Watch page *offers* (browsers that can render a live
# preview), so validate_choice has to stay off: this same endpoint legitimately receives
# any installed backend from the watch-list quick-add, and pre_validate() would reject
# e.g. 'html_requests' for not being in the offered list.
fetch_backend = RadioField(_l('Browser'),
choices=visual_browser_choices,
validate_choice=False,
validators=[ValidateKnownContentFetcher()])
edit_and_watch_submit_button = SubmitField(_l('Edit > Watch'), render_kw={"class": "pure-button pure-button-primary"})
@@ -119,13 +119,17 @@ window.initVisualSelector = function (opts) {
c = $selectorCanvasElem[0];
xctx = c.getContext("2d");
ctx = c.getContext("2d");
// Drop any handlers left over from a previous load BEFORE (re)building the
// selector - applyElementData() runs synchronously for inline xpathData and
// binds the element handlers itself, so unbinding afterwards would silently
// kill hover/click selection on the add-watch snapshot path.
$selectorCanvasElem.off("mousemove mousedown mouseleave");
if (source.xpathData) {
// Inline data (add-watch snapshot) - no extra round trip needed
applyElementData(source.xpathData);
} else {
fetchData();
}
$selectorCanvasElem.off("mousemove mousedown");
});
// data: URIs must be used verbatim; real URLs get a cache-buster
@@ -169,6 +169,54 @@ body {
}
}
// Browser picker: fetcher descriptions are long (they carry the driver URL), so this is
// a radio list whose labels wrap inside the narrow pane instead of a <select> that would
// overflow it. The system-default entry is listed even when it can't render a preview.
#quick-watch-fetch-backend {
ul {
margin: 0.35rem 0 0 0;
padding: 0;
list-style: none;
}
li {
display: flex;
align-items: flex-start;
gap: 0.5em;
padding: 0.15rem 0;
input[type="radio"] {
flex: 0 0 auto;
margin-top: 0.2em;
}
label {
// Wrap rather than push the pane wider
display: block;
min-width: 0;
overflow-wrap: anywhere;
font-size: 0.85rem;
line-height: 1.35;
}
}
li.unusable {
opacity: 0.55;
cursor: not-allowed;
label {
cursor: not-allowed;
}
}
.pure-form-message-inline {
display: block;
margin-top: 0.35rem;
font-size: 0.8rem;
opacity: 0.8;
}
}
#by-element-toggle-group {
.pure-form-message-inline {
display: block;
File diff suppressed because one or more lines are too long
+18 -1
View File
@@ -35,7 +35,8 @@ except ImportError:
from ..processors import get_custom_watch_obj_for_processor, find_processors
# Import the base class and helpers
from .file_saving_datastore import FileSavingDataStore, load_all_watches, load_all_tags, save_json_atomic
from .file_saving_datastore import (FileSavingDataStore, load_all_watches, load_all_tags, load_watch_from_file,
save_json_atomic)
from .updates import DatastoreUpdatesMixin
# Because the server will run as a daemon and wont know the URL for notification links when firing off a notification
@@ -852,10 +853,26 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore):
last-screenshot.png + elements.deflate in final on-disk format) is renamed into
place as the new watch's data_dir - no re-fetch, no copy. If the temp_uuid is
missing/expired/invalid we fall back to a normal add_watch() so the UI still works.
Settings the snapshot recorded for itself in its own watch.json (currently just
fetch_backend - the browser that rendered the preview) win over the posted form,
because they describe what actually fetched. add_watch() -> commit() rewrites that
file in full once the directory has been promoted.
"""
seed_dir = self.get_temporary_watch_dir(temp_uuid)
if not (seed_dir and os.path.isdir(seed_dir)):
seed_dir = None
extras = dict(extras or {})
seed_watch_json = os.path.join(seed_dir, "watch.json") if seed_dir else None
# isfile() first - a snapshot parked before this file existed is normal, not an error
if seed_watch_json and os.path.isfile(seed_watch_json):
seed_watch = load_watch_from_file(seed_watch_json, temp_uuid, self.rehydrate_entity)
if seed_watch and seed_watch.get('fetch_backend'):
extras['fetch_backend'] = seed_watch.get('fetch_backend')
logger.debug(f"Promoting temporary watch {temp_uuid} with its recorded "
f"fetch_backend '{extras['fetch_backend']}'")
return self.add_watch(url=url, tag=tag, extras=extras, seed_data_dir=seed_dir)
def cleanup_temporary_watches(self, ttl_seconds=3600):
@@ -48,6 +48,16 @@ def test_llm_features_disabled_hides_ui(client, live_server, monkeypatch):
assert b'name="llm_intent"' not in res.data
assert b'name="llm_change_summary"' not in res.data
# 4. Add-watch page - with no "what matters" intent box there is nothing to choose
# between, so element selection is always on and the opt-in checkbox is not rendered.
res = client.get(url_for('add_watch_ui.add_watch_ui_index'))
assert res.status_code == 200
_llm_markers_absent(res.data, where='add-watch-ui')
assert b'name="llm_intent"' not in res.data
assert b'id="by-element-toggle"' not in res.data
assert b'for="by-element-toggle"' not in res.data # no label either - it isn't a choice
assert b'id="by-element-toggle-group"' in res.data
def test_llm_features_enabled_by_default(client, live_server, monkeypatch):
"""When LLM_FEATURES_DISABLED is unset, the AI / LLM surfaces are still rendered."""
@@ -60,3 +70,11 @@ def test_llm_features_enabled_by_default(client, live_server, monkeypatch):
assert res.status_code == 200
# The AI / LLM settings tab anchor should be present when not disabled
assert b'href="#ai"' in res.data
# 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')
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
assert b'id="by-element-toggle"' in res.data
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""The Add-Watch page's browser picker.
The live preview needs a browser that can render screenshots + xpath element data, and
whatever rendered the preview is what the saved watch must check with - otherwise you
visually pick an element with Chrome and the watch then re-checks it with the plain HTTP
client. These tests cover the offered list, the server-side gate, and that the browser
which rendered a parked snapshot wins over the posted form.
"""
import json
import os
from flask import url_for
def _datastore(client):
return client.application.config.get('DATASTORE')
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
# html_webdriver is only preview-capable when it resolves to playwright/puppeteer, so
# pretend it does (a WEBDRIVER_URL-only deployment resolves it to selenium, which can't).
monkeypatch.setattr(browser_config, 'is_visual_capable',
lambda name, datastore: name == 'html_webdriver')
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 plain HTTP client can't render a preview, so it is not an option
assert b'value="html_requests"' not in res.data
# The system default resolves to html_requests here, so it is listed but disabled
assert b'value="system"' in res.data
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
monkeypatch.setattr(browser_config, 'is_visual_capable', lambda name, datastore: False)
# Nothing capable, and no explicit browser asked for -> nothing to preview with
res = client.get(url_for('add_watch_ui.add_watch_ui_snapshot', url='https://example.com'))
assert res.status_code == 400
assert b'No interactive browser' in res.data
# Explicitly asking for a browser that can't preview is refused just the same
res = client.get(url_for('add_watch_ui.add_watch_ui_snapshot', url='https://example.com',
fetch_backend='html_requests'))
assert res.status_code == 400
# A made-up name never resolves to a capable fetcher either (real capability lookup here)
monkeypatch.undo()
res = client.get(url_for('add_watch_ui.add_watch_ui_snapshot', url='https://example.com',
fetch_backend='../../etc/passwd'))
assert res.status_code == 400
res = client.get(url_for('add_watch_ui.add_watch_ui_snapshot', url='https://example.com',
fetch_backend='os'))
assert res.status_code == 400
def test_submit_rejects_unknown_fetcher(client, live_server, measure_memory_usage, datastore_path):
"""A posted browser is checked server side, so a doctored form can't pin a junk fetcher."""
datastore = _datastore(client)
test_url = url_for('test_endpoint', _external=True)
for bad in ('os', 'html_requests\n', '{{7*7}}', '../../etc/passwd', 'html_nope'):
before = len(datastore.data['watching'])
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "fetch_backend": bad},
follow_redirects=True
)
assert res.status_code == 200
assert b"Watch added" not in res.data
assert len(datastore.data['watching']) == before, f"{bad!r} should not have created a watch"
def test_submit_still_accepts_any_installed_fetcher(client, live_server, measure_memory_usage, datastore_path):
"""This endpoint is shared with the watch-list quick-add, which may legitimately name a
backend that can't render a live preview (restock tests add watches with html_requests) -
only *offering* it on the Add-Watch page is restricted, not adding a watch with it."""
datastore = _datastore(client)
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "processor": "restock_diff", "fetch_backend": "html_requests"},
follow_redirects=True
)
assert b"Watch added" in res.data
uuid = next(iter(datastore.data['watching']))
assert datastore.data['watching'][uuid].get('fetch_backend') == 'html_requests'
def test_submit_saves_chosen_browser(client, live_server, measure_memory_usage, datastore_path):
"""The picked browser lands on the watch instead of leaving it on 'system'."""
datastore = _datastore(client)
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "fetch_backend": "html_webdriver"},
follow_redirects=True
)
assert b"Watch added" in res.data
uuid = next(iter(datastore.data['watching']))
assert datastore.data['watching'][uuid].get('fetch_backend') == 'html_webdriver'
def test_parked_snapshot_browser_wins_over_form(client, live_server, measure_memory_usage, datastore_path):
"""The browser that actually rendered the snapshot is the one the watch keeps.
/snapshot records it in the temporary watch's own watch.json; promoting the snapshot
must prefer that over whatever the form posted, since the parked screenshot and
element data came from it.
"""
datastore = _datastore(client)
test_url = url_for('test_endpoint', _external=True)
temp_uuid = '11111111-2222-3333-4444-555555555555'
temp_dir = datastore.get_temporary_watch_dir(temp_uuid)
os.makedirs(temp_dir, exist_ok=True)
with open(os.path.join(temp_dir, "watch.json"), 'w') as f:
json.dump({"fetch_backend": "html_webdriver"}, f)
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url, "fetch_backend": "html_requests", "temporary_uuid": temp_uuid},
follow_redirects=True
)
assert b"Watch added" in res.data
uuid = next(iter(datastore.data['watching']))
assert datastore.data['watching'][uuid].get('fetch_backend') == 'html_webdriver'
# The snapshot dir was promoted into the watch, not left behind
assert not os.path.isdir(temp_dir)
def test_watchlist_quick_add_is_unaffected(client, live_server, measure_memory_usage, datastore_path):
"""The watch-list quick-add posts no browser at all - that still means 'system'."""
datastore = _datastore(client)
test_url = url_for('test_endpoint', _external=True)
res = client.post(
url_for("ui.ui_views.form_quick_watch_add"),
data={"url": test_url},
follow_redirects=True
)
assert b"Watch added" in res.data
uuid = next(iter(datastore.data['watching']))
assert datastore.data['watching'][uuid].get('fetch_backend') in (None, '', 'system')
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Přidejte nové sledování zjišťování změn webové stránky"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "V současné době:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Vyberte podle prvku"
@@ -2604,10 +2619,6 @@ msgstr "Štítek"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3002,6 +3013,10 @@ msgstr "Minuty"
msgid "Seconds"
msgstr "Sekundy"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Při použití adresy URL oznámení je vyžadováno tělo a název oznámení"
@@ -3052,6 +3067,10 @@ msgstr "Monitorovat"
msgid "Processor"
msgstr "Procesor"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Upravit > Monitorovat"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Fügen Sie eine neue Überwachung zur Erkennung von Webseitenänderungen hinzu"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "Momentan:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Nach Element auswählen"
@@ -2649,10 +2664,6 @@ msgstr "Tag"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3051,6 +3062,10 @@ msgstr "Minuten"
msgid "Seconds"
msgstr "Sekunden"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Benachrichtigungstext und Titel sind erforderlich, wenn eine Benachrichtigungs-URL verwendet wird"
@@ -3101,6 +3116,10 @@ msgstr "Überwachen"
msgid "Processor"
msgstr "Prozessor"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Bearbeiten"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr ""
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr ""
@@ -2598,10 +2613,6 @@ msgstr ""
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -2994,6 +3005,10 @@ msgstr ""
msgid "Seconds"
msgstr ""
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr ""
@@ -3044,6 +3059,10 @@ msgstr ""
msgid "Processor"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr ""
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr ""
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr ""
@@ -2598,10 +2613,6 @@ msgstr ""
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -2994,6 +3005,10 @@ msgstr ""
msgid "Seconds"
msgstr ""
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr ""
@@ -3044,6 +3059,10 @@ msgstr ""
msgid "Processor"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr ""
@@ -14,6 +14,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Agregar un nuevo monitor de detección de cambios en páginas web"
@@ -39,6 +49,11 @@ msgstr ""
msgid "Currently:"
msgstr "Actualmente:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Seleccionar por elemento"
@@ -2665,10 +2680,6 @@ msgstr "Etiqueta"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3067,6 +3078,10 @@ msgstr "Minutos"
msgid "Seconds"
msgstr "Segundos"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Se requiere el cuerpo y el título de la notificación cuando se utiliza una URL de notificación"
@@ -3117,6 +3132,10 @@ msgstr "Monitor"
msgid "Processor"
msgstr "Procesador"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Editar > Ver"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Ajouter une nouvelle surveillance de détection de changement de page Web"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "Actuellement:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Sélectionner par élément"
@@ -2609,10 +2624,6 @@ msgstr "Étiqueter"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3007,6 +3018,10 @@ msgstr "Minutes"
msgid "Seconds"
msgstr "secondes"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Le corps et le titre de la notification sont requis lorsqu'une URL de notification est utilisée"
@@ -3057,6 +3072,10 @@ msgstr "Moniteur"
msgid "Processor"
msgstr "Processeur"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Modifier > Surveiller"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Aggiungi un nuovo monitoraggio modifiche pagina web"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr ""
@@ -2600,10 +2615,6 @@ msgstr "Tag"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -2996,6 +3007,10 @@ msgstr "Minuti"
msgid "Seconds"
msgstr "Secondi"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Corpo e titolo notifica sono richiesti quando si usa un URL di notifica"
@@ -3046,6 +3061,10 @@ msgstr "Monitora"
msgid "Processor"
msgstr "Processore"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Modifica > Monitora"
@@ -19,6 +19,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "新しいウェブページ変更検知ウォッチを追加"
@@ -44,6 +54,11 @@ msgstr ""
msgid "Currently:"
msgstr "現在:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "要素で選択"
@@ -2617,10 +2632,6 @@ msgstr "タグ"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3013,6 +3024,10 @@ msgstr "分"
msgid "Seconds"
msgstr "秒"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "通知URLを使用する場合は通知本文とタイトルが必要です"
@@ -3063,6 +3078,10 @@ msgstr "ウォッチ"
msgid "Processor"
msgstr "プロセッサー"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "編集 > ウォッチ"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "새 웹페이지 변경 감지 모니터링 추가"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "현재:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "요소별로 선택"
@@ -2608,10 +2623,6 @@ msgstr "태그"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3004,6 +3015,10 @@ msgstr "분"
msgid "Seconds"
msgstr "초"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "알림 URL을 사용하는 경우 알림 본문 및 제목이 필요합니다."
@@ -3054,6 +3069,10 @@ msgstr "모니터링"
msgid "Processor"
msgstr "프로세서"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "편집 > 모니터링"
+24 -5
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-24 21:15+0200\n"
"POT-Creation-Date: 2026-08-25 14:13+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,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr ""
@@ -42,6 +52,11 @@ msgstr ""
msgid "Currently:"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr ""
@@ -2597,10 +2612,6 @@ msgstr ""
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -2993,6 +3004,10 @@ msgstr ""
msgid "Seconds"
msgstr ""
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr ""
@@ -3043,6 +3058,10 @@ msgstr ""
msgid "Processor"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr ""
@@ -14,6 +14,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Dodaj obserwację wykrywającą zmiany na nowej stronie internetowej"
@@ -39,6 +49,11 @@ msgstr "Pobieranie zrzutu ekranu i informacji o elemencie…"
msgid "Currently:"
msgstr "Obecnie:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Wybierz według elementu"
@@ -2753,10 +2768,6 @@ msgstr "Etykieta"
msgid "Set browser / fetch method for selected"
msgstr "Ustaw przeglądarkę / metodę pobierania dla zaznaczonych obserwacji"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr "Przeglądarka"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr "Zastosuj"
@@ -3153,6 +3164,10 @@ msgstr "Minut"
msgid "Seconds"
msgstr "Sekundy"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "W przypadku korzystania z adresu URL powiadomienia należy podać treść i tytuł powiadomienia"
@@ -3203,6 +3218,10 @@ msgstr "Obserwacja"
msgid "Processor"
msgstr "Procesor"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr "Przeglądarka"
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Edycja > Obserwuj"
@@ -19,6 +19,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Adicionar um novo monitoramento de detecção de mudança de página"
@@ -44,6 +54,11 @@ msgstr ""
msgid "Currently:"
msgstr "Atualmente:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Selecionar por elemento"
@@ -2646,10 +2661,6 @@ msgstr "Tag"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3044,6 +3055,10 @@ msgstr "Minutos"
msgid "Seconds"
msgstr "Segundos"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Corpo e Título da Notificação são obrigatórios quando uma URL de Notificação é usada"
@@ -3094,6 +3109,10 @@ msgstr "Monitoramento"
msgid "Processor"
msgstr "Processador"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Editar > Monitoramento"
@@ -17,6 +17,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Добавить новый контроль обнаружения изменений веб-страницы"
@@ -42,6 +52,11 @@ msgstr ""
msgid "Currently:"
msgstr "В настоящее время:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Выбрать по элементу"
@@ -2712,10 +2727,6 @@ msgstr "Ярлык"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3112,6 +3123,10 @@ msgstr "Минуты"
msgid "Seconds"
msgstr "Секунды"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Текст и заголовок уведомления требуются при использовании URL-адреса уведомления."
@@ -3162,6 +3177,10 @@ msgstr "Смотреть"
msgid "Processor"
msgstr "Процессор"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Редактировать > Смотреть"
@@ -19,6 +19,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Yeni bir web sayfası değişiklik tespiti izleyicisi ekle"
@@ -44,6 +54,11 @@ msgstr ""
msgid "Currently:"
msgstr "Şu anda:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Öğeye göre seç"
@@ -2651,10 +2666,6 @@ msgstr "Etiket"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3047,6 +3058,10 @@ msgstr "Dakika"
msgid "Seconds"
msgstr "Saniye"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Bir Bildirim URL'si kullanıldığında Bildirim Gövdesi ve Başlığı gereklidir"
@@ -3097,6 +3112,10 @@ msgstr "İzleyici"
msgid "Processor"
msgstr "İşlemci"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Düzenle > İzleyici"
@@ -17,6 +17,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "Додати нове завдання відстеження змін веб-сторінки"
@@ -42,6 +52,11 @@ msgstr ""
msgid "Currently:"
msgstr "В даний час:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "Вибір за елементом"
@@ -2630,10 +2645,6 @@ msgstr "Тег"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3026,6 +3037,10 @@ msgstr "Хвилини"
msgid "Seconds"
msgstr "Секунди"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "Тіло та заголовок сповіщення обов'язкові, якщо використовується URL сповіщення"
@@ -3076,6 +3091,10 @@ msgstr "Завдання"
msgid "Processor"
msgstr "Процесор"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "Редагувати > Завдання"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "新增网页变更监控"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "当前:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "按元素选择"
@@ -2604,10 +2619,6 @@ msgstr "标签"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3000,6 +3011,10 @@ msgstr "分钟"
msgid "Seconds"
msgstr "秒"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "使用通知 URL 时,必须填写通知正文和标题"
@@ -3050,6 +3065,10 @@ msgstr "监控项"
msgid "Processor"
msgstr "处理器"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "编辑 > 监控项"
@@ -18,6 +18,16 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.18.0\n"
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/browser_config.py
#, python-format
msgid "System settings default (%(browser)s - no live preview)"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
msgid "Add a new web page change detection watch"
msgstr "新增網頁變更檢測任務"
@@ -43,6 +53,11 @@ msgstr ""
msgid "Currently:"
msgstr "目前:"
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html
#, python-format
msgid "%(browser)s cannot render a live preview, choose a browser below"
msgstr ""
#: changedetectionio/blueprint/add_watch_ui/templates/add-watch-ui.html changedetectionio/blueprint/ui/templates/edit.html
msgid "Select by element"
msgstr "按元素選擇"
@@ -2604,10 +2619,6 @@ msgstr "標籤"
msgid "Set browser / fetch method for selected"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Browser"
msgstr ""
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html
msgid "Apply"
msgstr ""
@@ -3000,6 +3011,10 @@ msgstr "分鐘"
msgid "Seconds"
msgstr "秒"
#: changedetectionio/forms.py
msgid "Unknown fetch method."
msgstr ""
#: changedetectionio/forms.py
msgid "Notification Body and Title is required when a Notification URL is used"
msgstr "使用通知 URL 時,必須填寫通知內容與標題"
@@ -3050,6 +3065,10 @@ msgstr "監測任務"
msgid "Processor"
msgstr "處理器"
#: changedetectionio/blueprint/watchlist/templates/watch-overview.html changedetectionio/forms.py
msgid "Browser"
msgstr ""
#: changedetectionio/forms.py
msgid "Edit > Watch"
msgstr "編輯 > 監測任務"