mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-06-09 10:21:05 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb57423125 | |||
| 613d14428e | |||
| e51d8880bc | |||
| 82795fe883 | |||
| 0ad730a6c7 |
@@ -7,7 +7,7 @@ import threading
|
||||
from flask import request
|
||||
from . import auth
|
||||
|
||||
from . import validate_openapi_request
|
||||
from . import validate_openapi_request, strip_internal_api_fields
|
||||
|
||||
|
||||
class Tag(Resource):
|
||||
@@ -85,7 +85,8 @@ class Tag(Resource):
|
||||
# Create clean tag dict without Watch-specific fields
|
||||
clean_tag = {k: v for k, v in tag.items() if k not in watch_only_fields}
|
||||
|
||||
return clean_tag
|
||||
# Never expose `__`-prefixed transient/internal fields
|
||||
return strip_internal_api_fields(clean_tag)
|
||||
|
||||
@auth.check_token
|
||||
@validate_openapi_request('deleteTag')
|
||||
@@ -113,8 +114,9 @@ class Tag(Resource):
|
||||
if not tag:
|
||||
abort(404, message='No tag exists with the UUID of {}'.format(uuid))
|
||||
|
||||
# Make a mutable copy of request.json for modification
|
||||
json_data = dict(request.json)
|
||||
# Make a mutable copy of request.json for modification.
|
||||
# Silently discard `__`-prefixed transient/internal keys (not part of the public schema).
|
||||
json_data = strip_internal_api_fields(dict(request.json))
|
||||
|
||||
# Validate notification_urls if provided
|
||||
if 'notification_urls' in json_data:
|
||||
@@ -162,7 +164,8 @@ class Tag(Resource):
|
||||
def post(self):
|
||||
"""Create a single tag/group."""
|
||||
|
||||
json_data = request.get_json()
|
||||
# Silently discard `__`-prefixed transient/internal keys (not part of the public schema).
|
||||
json_data = strip_internal_api_fields(request.get_json())
|
||||
title = json_data.get("title",'').strip()
|
||||
|
||||
# Validate that only valid fields are provided
|
||||
|
||||
@@ -12,7 +12,7 @@ from flask_restful import abort, Resource
|
||||
from loguru import logger
|
||||
import copy
|
||||
|
||||
from . import validate_openapi_request, get_readonly_watch_fields
|
||||
from . import validate_openapi_request, get_readonly_watch_fields, strip_internal_api_fields
|
||||
from ..notification import valid_notification_formats
|
||||
from ..notification.handler import newline_re
|
||||
|
||||
@@ -126,7 +126,8 @@ class Watch(Resource):
|
||||
watch['processor_config_restock_diff'] = restock_config
|
||||
watch['processor_config_restock_diff_source'] = restock_source
|
||||
|
||||
return watch
|
||||
# Never expose `__`-prefixed transient/internal fields (e.g. __check_status)
|
||||
return strip_internal_api_fields(watch)
|
||||
|
||||
@auth.check_token
|
||||
@validate_openapi_request('deleteWatch')
|
||||
@@ -187,8 +188,10 @@ class Watch(Resource):
|
||||
# Handle processor-config-* fields separately (save to JSON, not datastore)
|
||||
from changedetectionio import processors
|
||||
|
||||
# Make a mutable copy of request.json for modification
|
||||
json_data = dict(request.json)
|
||||
# Make a mutable copy of request.json for modification.
|
||||
# Silently discard `__`-prefixed transient/internal keys — they are not part of the
|
||||
# public schema and must never be writable (e.g. clients that round-trip GET → PUT).
|
||||
json_data = strip_internal_api_fields(dict(request.json))
|
||||
|
||||
# Extract and remove processor config fields from json_data
|
||||
processor_config_data = processors.extract_processor_config_from_form_data(json_data)
|
||||
@@ -443,7 +446,8 @@ class CreateWatch(Resource):
|
||||
def post(self):
|
||||
"""Create a single watch."""
|
||||
|
||||
json_data = request.get_json()
|
||||
# Silently discard `__`-prefixed transient/internal keys (not part of the public schema).
|
||||
json_data = strip_internal_api_fields(request.get_json())
|
||||
url = json_data['url'].strip()
|
||||
|
||||
if not is_safe_valid_url(url):
|
||||
|
||||
@@ -133,6 +133,43 @@ def get_tag_schema_properties():
|
||||
"""
|
||||
return _resolve_schema_properties('Tag')
|
||||
|
||||
def strip_private_keys(data):
|
||||
"""
|
||||
Remove `__`-prefixed keys from a watch/tag dict at the API boundary.
|
||||
|
||||
These are transient in-memory fields (e.g. `__check_status` set by the worker to
|
||||
surface "Fetching page..." in the UI) and are not part of the public OpenAPI
|
||||
contract. They must never appear in GET responses (otherwise a client that
|
||||
round-trips GET → PUT trips the unknown-field validator), and must be silently
|
||||
discarded from incoming PUT/POST payloads.
|
||||
|
||||
Returns a new dict; the input is not mutated.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
return {k: v for k, v in data.items() if not (isinstance(k, str) and k.startswith('__'))}
|
||||
|
||||
|
||||
def strip_internal_api_fields(data):
|
||||
"""
|
||||
Strip both `__`-prefixed keys AND system-managed fields that aren't in the public
|
||||
OpenAPI spec (skip-cache hashes, LLM runtime state, processor-set status, etc.).
|
||||
|
||||
Use this at every public API boundary so GET responses and PUT/POST payloads agree
|
||||
on what's part of the contract. The set of system-managed fields lives in
|
||||
model/schema_utils.py:SYSTEM_MANAGED_NON_SPEC_FIELDS — extend it there, not here.
|
||||
|
||||
Returns a new dict; the input is not mutated.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
from changedetectionio.model.schema_utils import SYSTEM_MANAGED_NON_SPEC_FIELDS
|
||||
return {
|
||||
k: v for k, v in data.items()
|
||||
if not (isinstance(k, str) and (k.startswith('__') or k in SYSTEM_MANAGED_NON_SPEC_FIELDS))
|
||||
}
|
||||
|
||||
|
||||
def validate_openapi_request(operation_id):
|
||||
"""Decorator to validate incoming requests against OpenAPI spec."""
|
||||
def decorator(f):
|
||||
|
||||
@@ -343,28 +343,14 @@ class watch_base(dict):
|
||||
return
|
||||
|
||||
# Import from shared schema utilities (no circular dependency)
|
||||
from .schema_utils import get_readonly_watch_fields
|
||||
readonly_fields = get_readonly_watch_fields()
|
||||
from .schema_utils import get_readonly_watch_fields, SYSTEM_MANAGED_NON_SPEC_FIELDS
|
||||
|
||||
# Additional system-managed fields not in OpenAPI spec (yet)
|
||||
# These are set by processors/workers and should not trigger edited flag
|
||||
additional_system_fields = {
|
||||
'last_check_status', # Set by processors
|
||||
'last_filter_config_hash', # Set by text_json_diff processor, internal skip-cache
|
||||
'restock', # Set by restock processor
|
||||
'last_viewed', # Set by mark_all_viewed endpoint
|
||||
# LLM runtime fields written back by worker/evaluator
|
||||
'_llm_result',
|
||||
'_llm_intent',
|
||||
'_llm_change_summary',
|
||||
'llm_prefilter',
|
||||
'llm_evaluation_cache',
|
||||
'llm_last_tokens_used',
|
||||
'llm_tokens_used_cumulative',
|
||||
}
|
||||
|
||||
# Only mark as edited if this is a user-writable field
|
||||
if key not in readonly_fields and key not in additional_system_fields:
|
||||
# `last_viewed` is set internally by mark_all_viewed and shouldn't flag the watch as
|
||||
# edited, but is not in SYSTEM_MANAGED_NON_SPEC_FIELDS because it IS user-writable via
|
||||
# the UpdateWatch schema (the API path).
|
||||
if (key not in get_readonly_watch_fields()
|
||||
and key != 'last_viewed'
|
||||
and key not in SYSTEM_MANAGED_NON_SPEC_FIELDS):
|
||||
self.__watch_was_edited = True
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
|
||||
@@ -8,6 +8,35 @@ Shared by both the model layer and API layer to avoid circular dependencies.
|
||||
import functools
|
||||
|
||||
|
||||
# Watch fields written by workers/processors that are NOT part of the public OpenAPI spec.
|
||||
#
|
||||
# These fields exist on a watch dict at runtime but are internal implementation details
|
||||
# (skip-cache hashes, last-check status strings, LLM runtime state, etc.). Used by:
|
||||
# - model/__init__.py: don't trigger the "edited" flag when these are written internally
|
||||
# - api/Watch.py: strip from GET responses and silently discard from PUT/POST inputs
|
||||
# so that a GET → PUT round trip doesn't trip the unknown-field validator
|
||||
#
|
||||
# `last_viewed` is intentionally NOT included: it's set internally by mark_all_viewed BUT
|
||||
# is also explicitly writable via the UpdateWatch schema (see api/Watch.py valid_fields).
|
||||
SYSTEM_MANAGED_NON_SPEC_FIELDS = frozenset({
|
||||
'last_check_status', # Set by processors
|
||||
'last_filter_config_hash', # text_json_diff internal skip-cache
|
||||
'restock', # Set by restock processor
|
||||
'_llm_result', # LLM runtime — populated by evaluator
|
||||
'_llm_intent',
|
||||
'_llm_change_summary',
|
||||
'llm_prefilter',
|
||||
'llm_evaluation_cache',
|
||||
'llm_last_tokens_used',
|
||||
'llm_tokens_used_cumulative',
|
||||
})
|
||||
|
||||
|
||||
def get_system_managed_non_spec_fields():
|
||||
"""Return the set of internal fields not in the public OpenAPI spec."""
|
||||
return SYSTEM_MANAGED_NON_SPEC_FIELDS
|
||||
|
||||
|
||||
@functools.cache
|
||||
def get_openapi_schema_dict():
|
||||
"""
|
||||
|
||||
@@ -495,16 +495,17 @@ class perform_site_check(difference_detection_processor):
|
||||
# Start with content reference, avoid copy until modification
|
||||
html_content = content
|
||||
|
||||
# Apply include filters (CSS, XPath, JSON)
|
||||
# Except for plaintext (incase they tried to confuse the system, it will HTML escape
|
||||
#if not stream_content_type.is_plaintext:
|
||||
if filter_config.has_include_filters:
|
||||
html_content = content_processor.apply_include_filters(content, stream_content_type)
|
||||
|
||||
# Apply subtractive selectors
|
||||
# Apply subtractive selectors first so include filters operate on already-cleaned content.
|
||||
# Otherwise a subtractive selector that relies on ancestor context (e.g. ".main .ads")
|
||||
# cannot match after the include filter has extracted the inner element and stripped
|
||||
# the parent wrapper.
|
||||
if filter_config.has_subtractive_selectors:
|
||||
html_content = content_processor.apply_subtractive_selectors(html_content)
|
||||
|
||||
# Apply include filters (CSS, XPath, JSON)
|
||||
if filter_config.has_include_filters:
|
||||
html_content = content_processor.apply_include_filters(html_content, stream_content_type)
|
||||
|
||||
# === TEXT EXTRACTION ===
|
||||
if watch.is_source_type_url:
|
||||
# For source URLs, keep raw content
|
||||
@@ -550,30 +551,43 @@ class perform_site_check(difference_detection_processor):
|
||||
|
||||
update_obj["last_check_status"] = self.fetcher.get_last_status_code()
|
||||
|
||||
# Snapshot an ignore-applied stream BEFORE extract operations so line-level
|
||||
# ignore patterns still match original content (#4138). Otherwise an extract_text
|
||||
# regex like /(\d+\.\d+\.\d+)/ would transform "v.1.2.1" into "1.2.1" and the
|
||||
# ignore_text pattern "v" would no longer match — meaning changes to ignored
|
||||
# lines would incorrectly affect the checksum.
|
||||
text_for_checksuming = None
|
||||
if filter_config.ignore_text:
|
||||
text_for_checksuming = html_tools.strip_ignore_text(stripped_text, filter_config.ignore_text)
|
||||
|
||||
# === LINE FILTER (plain-text substring) ===
|
||||
if filter_config.extract_lines_containing:
|
||||
stripped_text = transformer.extract_lines_containing(stripped_text, filter_config.extract_lines_containing)
|
||||
if text_for_checksuming is not None:
|
||||
text_for_checksuming = transformer.extract_lines_containing(text_for_checksuming, filter_config.extract_lines_containing)
|
||||
|
||||
# === REGEX EXTRACTION ===
|
||||
if filter_config.extract_text:
|
||||
extracted = transformer.extract_by_regex(stripped_text, filter_config.extract_text)
|
||||
stripped_text = extracted
|
||||
stripped_text = transformer.extract_by_regex(stripped_text, filter_config.extract_text)
|
||||
if text_for_checksuming is not None:
|
||||
text_for_checksuming = transformer.extract_by_regex(text_for_checksuming, filter_config.extract_text)
|
||||
|
||||
# === MORE TEXT TRANSFORMATIONS ===
|
||||
if watch.get('remove_duplicate_lines'):
|
||||
stripped_text = transformer.remove_duplicate_lines(stripped_text)
|
||||
if text_for_checksuming is not None:
|
||||
text_for_checksuming = transformer.remove_duplicate_lines(text_for_checksuming)
|
||||
|
||||
if watch.get('sort_text_alphabetically'):
|
||||
stripped_text = transformer.sort_alphabetically(stripped_text)
|
||||
if text_for_checksuming is not None:
|
||||
text_for_checksuming = transformer.sort_alphabetically(text_for_checksuming)
|
||||
|
||||
# === CHECKSUM CALCULATION ===
|
||||
text_for_checksuming = stripped_text
|
||||
|
||||
# Apply ignore_text for checksum calculation
|
||||
if filter_config.ignore_text:
|
||||
text_for_checksuming = html_tools.strip_ignore_text(stripped_text, filter_config.ignore_text)
|
||||
|
||||
# Optionally remove ignored lines from output
|
||||
if text_for_checksuming is None:
|
||||
text_for_checksuming = stripped_text
|
||||
else:
|
||||
# Optionally remove ignored lines from displayed output too
|
||||
strip_ignored_lines = watch.get('strip_ignored_lines')
|
||||
if strip_ignored_lines is None:
|
||||
strip_ignored_lines = self.datastore.data['settings']['application'].get('strip_ignored_lines')
|
||||
|
||||
@@ -406,6 +406,106 @@ def test_roundtrip_API(client, live_server, measure_memory_usage, datastore_path
|
||||
"extract_lines_containing should be persisted and returned via API"
|
||||
|
||||
|
||||
def test_api_strips_internal_fields(client, live_server, measure_memory_usage, datastore_path):
|
||||
"""
|
||||
Internal/transient fields must never cross the API boundary in either direction:
|
||||
1. `__`-prefixed keys (e.g. `__check_status` set by the worker for UI status)
|
||||
2. System-managed fields not in the OpenAPI spec (see SYSTEM_MANAGED_NON_SPEC_FIELDS):
|
||||
`last_check_status`, `last_filter_config_hash`, `_llm_*`, `llm_*`, etc.
|
||||
|
||||
GET responses must strip them. PUT/POST payloads must silently discard them.
|
||||
Without this, a client that round-trips GET → PUT trips the unknown-field validator.
|
||||
"""
|
||||
from changedetectionio.model.schema_utils import SYSTEM_MANAGED_NON_SPEC_FIELDS
|
||||
|
||||
api_key = live_server.app.config['DATASTORE'].data['settings']['application'].get('api_access_token')
|
||||
datastore = live_server.app.config['DATASTORE']
|
||||
|
||||
set_original_response(datastore_path=datastore_path)
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
|
||||
# Create
|
||||
res = client.post(
|
||||
url_for("createwatch"),
|
||||
data=json.dumps({"url": test_url}),
|
||||
headers={'content-type': 'application/json', 'x-api-key': api_key},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert res.status_code == 201
|
||||
watch_uuid = res.json.get('uuid')
|
||||
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Force both a transient __-prefixed and a system-managed field onto the watch,
|
||||
# simulating worker/processor-set state.
|
||||
watch_obj = datastore.data['watching'][watch_uuid]
|
||||
watch_obj['__check_status'] = 'Fetching page..'
|
||||
watch_obj['last_check_status'] = 200
|
||||
watch_obj['_llm_result'] = {'summary': 'cached llm output'}
|
||||
watch_obj['last_filter_config_hash'] = 'abc123'
|
||||
|
||||
# --- GET must strip all internal fields ---
|
||||
res = client.get(
|
||||
url_for("watch", uuid=watch_uuid),
|
||||
headers={'x-api-key': api_key},
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert not any(k.startswith('__') for k in res.json.keys()), \
|
||||
f"No __-prefixed field should leak into API responses; got keys: {list(res.json.keys())}"
|
||||
leaked_system_fields = SYSTEM_MANAGED_NON_SPEC_FIELDS & set(res.json.keys())
|
||||
assert not leaked_system_fields, \
|
||||
f"System-managed non-spec fields must not appear in GET response; leaked: {leaked_system_fields}"
|
||||
|
||||
# --- PUT must accept (and silently drop) those same internal fields ---
|
||||
# This is the key round-trip property: a client should be able to PUT back what it just GET'd.
|
||||
# Use the actual GET response as the payload (the realistic round-trip case).
|
||||
payload = dict(res.json)
|
||||
payload['__check_status'] = 'attacker-supplied value' # not in the GET, but a client could add it
|
||||
payload['last_check_status'] = 999 # ditto
|
||||
payload['_llm_result'] = 'attacker overwrite'
|
||||
res = client.put(
|
||||
url_for("watch", uuid=watch_uuid),
|
||||
headers={'x-api-key': api_key, 'content-type': 'application/json'},
|
||||
data=json.dumps(payload),
|
||||
)
|
||||
assert res.status_code == 200, \
|
||||
f"PUT round-tripping GET response plus internal fields should succeed (got {res.status_code}: {res.data!r})"
|
||||
|
||||
# Internal fields must not have been overwritten by the PUT
|
||||
assert watch_obj.get('__check_status') == 'Fetching page..', \
|
||||
"PUT must not overwrite __-prefixed fields"
|
||||
assert watch_obj.get('_llm_result') == {'summary': 'cached llm output'}, \
|
||||
"PUT must not overwrite system-managed non-spec fields"
|
||||
|
||||
# --- POST must also silently discard internal fields ---
|
||||
# Use unique sentinel values so we can distinguish "POST persisted my value" from
|
||||
# "the worker concurrently re-set the field while processing the new watch".
|
||||
attacker_check_status = 'attacker-sentinel-__check_status-9f7c'
|
||||
attacker_llm_result = 'attacker-sentinel-_llm_result-9f7c'
|
||||
res = client.post(
|
||||
url_for("createwatch"),
|
||||
data=json.dumps({
|
||||
"url": test_url + "?2",
|
||||
"__check_status": attacker_check_status,
|
||||
"_llm_result": attacker_llm_result,
|
||||
}),
|
||||
headers={'content-type': 'application/json', 'x-api-key': api_key},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert res.status_code == 201, \
|
||||
f"POST with internal fields should succeed (got {res.status_code}: {res.data!r})"
|
||||
new_uuid = res.json.get('uuid')
|
||||
new_watch = datastore.data['watching'][new_uuid]
|
||||
# If POST had persisted the attacker payload these specific sentinel values would remain.
|
||||
# The worker may legitimately re-set __check_status with its own status string, that's fine.
|
||||
assert new_watch.get('__check_status') != attacker_check_status, \
|
||||
"POST must not persist __-prefixed fields from input"
|
||||
assert new_watch.get('_llm_result') != attacker_llm_result, \
|
||||
"POST must not persist system-managed fields from input"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
def test_access_denied(client, live_server, measure_memory_usage, datastore_path):
|
||||
# `config_api_token_enabled` Should be On by default
|
||||
res = client.get(
|
||||
|
||||
@@ -251,3 +251,41 @@ body > table > tr:nth-child(3) > td:nth-child(3)""",
|
||||
# First column should exist
|
||||
assert b"Emil" in res.data
|
||||
|
||||
|
||||
# Re PR #978: subtractive_selectors must run BEFORE include_filters so that selectors
|
||||
# relying on ancestor context (e.g. ".main .ad") can still match. If include runs first,
|
||||
# the ancestor wrapper is stripped and the subtractive selector matches nothing.
|
||||
def test_subtractive_selectors_applied_before_include_filters(client, live_server, measure_memory_usage, datastore_path):
|
||||
page_html = """<html><body>
|
||||
<div class="main">
|
||||
<p class="keep">first kept paragraph</p>
|
||||
<p class="advertisement">noisy advertisement text</p>
|
||||
<p class="keep">second kept paragraph</p>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(page_html)
|
||||
|
||||
test_url = url_for("test_endpoint", _external=True)
|
||||
client.application.config.get('DATASTORE').add_watch(
|
||||
url=test_url,
|
||||
extras={
|
||||
# Include filter strips the .main wrapper from the output
|
||||
"include_filters": [".main p"],
|
||||
# Subtractive selector depends on the .main ancestor — only effective if it runs first
|
||||
"subtractive_selectors": [".main .advertisement"],
|
||||
},
|
||||
)
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(
|
||||
url_for("ui.ui_preview.preview_page", uuid="first"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert b"first kept paragraph" in res.data
|
||||
assert b"second kept paragraph" in res.data
|
||||
# The bug: ad survives if include filter runs first
|
||||
assert b"noisy advertisement text" not in res.data
|
||||
|
||||
@@ -559,3 +559,78 @@ def test_extract_lines_containing_with_include_filters_css(client, live_server,
|
||||
assert b'forecast' not in res.data
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
|
||||
# Re issue #4138: ignore_text must take effect BEFORE extract_text regex, otherwise the
|
||||
# regex transforms line content (e.g. "v.1.2.1" -> "1.2.1") and ignore_text patterns
|
||||
# like "v"/"rc" can no longer match — causing changes to ignored lines to incorrectly
|
||||
# trigger change-detection.
|
||||
def test_ignore_text_applied_before_extract_text_regex(client, live_server, measure_memory_usage, datastore_path):
|
||||
initial_data = """<html><body>
|
||||
<p>0.8.9</p>
|
||||
<p>v.1.2.1</p>
|
||||
<p>rc-1.0.0</p>
|
||||
</body></html>"""
|
||||
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(initial_data)
|
||||
|
||||
test_url = url_for('test_endpoint', _external=True)
|
||||
uuid = client.application.config.get('DATASTORE').add_watch(url=test_url, extras={'paused': True})
|
||||
|
||||
res = client.post(
|
||||
url_for("ui.ui_edit.edit_page", uuid=uuid, unpause_on_save=1),
|
||||
data={
|
||||
'ignore_text': 'v\r\nrc',
|
||||
'extract_text': r'/(\d+\.\d+\.\d+)/',
|
||||
"url": test_url,
|
||||
"tags": "",
|
||||
"headers": "",
|
||||
'fetch_backend': "html_requests",
|
||||
"time_between_check_use_default": "y",
|
||||
},
|
||||
follow_redirects=True
|
||||
)
|
||||
assert b"unpaused" in res.data
|
||||
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
# Bump only the IGNORED lines — these should not move the checksum
|
||||
changed_data = """<html><body>
|
||||
<p>0.8.9</p>
|
||||
<p>v.1.3.0</p>
|
||||
<p>rc-2.0.0</p>
|
||||
</body></html>"""
|
||||
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(changed_data)
|
||||
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'has-unread-changes' not in res.data, \
|
||||
"Changing only ignored lines should not trigger a change even when extract_text regex is set"
|
||||
|
||||
client.get(url_for("ui.mark_all_viewed"), follow_redirects=True)
|
||||
time.sleep(1)
|
||||
|
||||
# Now bump the non-ignored line — this SHOULD trigger
|
||||
triggered_data = """<html><body>
|
||||
<p>0.9.0</p>
|
||||
<p>v.1.3.0</p>
|
||||
<p>rc-2.0.0</p>
|
||||
</body></html>"""
|
||||
|
||||
with open(os.path.join(datastore_path, "endpoint-content.txt"), "w") as f:
|
||||
f.write(triggered_data)
|
||||
|
||||
client.get(url_for("ui.form_watch_checknow"), follow_redirects=True)
|
||||
wait_for_all_checks(client)
|
||||
|
||||
res = client.get(url_for("watchlist.index"))
|
||||
assert b'has-unread-changes' in res.data, \
|
||||
"Changing a non-ignored line should still trigger a change"
|
||||
|
||||
delete_all_watches(client)
|
||||
|
||||
Binary file not shown.
@@ -77,7 +77,7 @@ msgstr "Soubor musí být .zip soubor zálohy!"
|
||||
#: changedetectionio/blueprint/backups/restore.py
|
||||
#, python-format
|
||||
msgid "Backup file is too large (max %(mb)s MB)"
|
||||
msgstr ""
|
||||
msgstr "Záložní soubor moc velký (max %(mb)s MB)"
|
||||
|
||||
#: changedetectionio/blueprint/backups/restore.py
|
||||
msgid "Invalid or corrupted zip file"
|
||||
@@ -136,7 +136,7 @@ msgstr "Pozn.: Nepřepíše hlavní nastavení aplikaci, pouze sledování a sku
|
||||
#: changedetectionio/blueprint/backups/templates/backup_restore.html
|
||||
#, python-format
|
||||
msgid "Max upload size: %(upload)s MB, Max decompressed size: %(decomp)s MB"
|
||||
msgstr ""
|
||||
msgstr "Max. velikost nahrání: %(upload)s MB, Max. velikost k rozbalení: %(decomp)s MB"
|
||||
|
||||
#: changedetectionio/blueprint/backups/templates/backup_restore.html
|
||||
msgid "Include all groups found in backup?"
|
||||
@@ -210,7 +210,7 @@ msgstr ".XLSX a Wachete"
|
||||
|
||||
#: changedetectionio/blueprint/imports/templates/import.html
|
||||
msgid "Backup Restore"
|
||||
msgstr ""
|
||||
msgstr "Obnova zálohy"
|
||||
|
||||
#: changedetectionio/blueprint/imports/templates/import.html
|
||||
msgid "Restoring changedetection.io backups is in the"
|
||||
@@ -362,12 +362,12 @@ msgstr "Všechna oznámení odtlumena."
|
||||
|
||||
#: changedetectionio/blueprint/settings/llm.py
|
||||
msgid "AI / LLM configuration removed."
|
||||
msgstr ""
|
||||
msgstr "AI / LLM konfigurace odstraněna."
|
||||
|
||||
#: changedetectionio/blueprint/settings/llm.py
|
||||
#, python-brace-format
|
||||
msgid "AI summary cache cleared ({} file(s) removed)."
|
||||
msgstr ""
|
||||
msgstr "AI cache souhrnů vyčištěna ({}s soubor(ů) odstraněno)."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/notification-log.html
|
||||
msgid "Notification debug log"
|
||||
@@ -405,7 +405,7 @@ msgstr "CAPTCHA a proxy"
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/blueprint/tags/templates/edit-tag.html
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "AI / LLM"
|
||||
msgstr ""
|
||||
msgstr "AI / LLM"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Info"
|
||||
@@ -433,15 +433,15 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Set to empty to disable / no limit"
|
||||
msgstr ""
|
||||
msgstr "Nastavit prázdnou hodnotu pro vypnutí / bez limitu"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Password protection for your changedetection.io application."
|
||||
msgstr ""
|
||||
msgstr "Chránit heslem tuto changedetection.io applikaci"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Password is locked."
|
||||
msgstr ""
|
||||
msgstr "Heslo je uzamčeno."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Allow access to the watch change history page when password is enabled (Good for sharing the diff page)"
|
||||
@@ -449,7 +449,7 @@ msgstr "Povolit přístup na stránku historie změn monitoru, když je povoleno
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "When a request returns no content, or the HTML does not contain any text, is this considered a change?"
|
||||
msgstr ""
|
||||
msgstr "Pokud požadavek vrátí prázdný obsah, nebo pokud HTML neobsahuje žádný text, má být označeno jako změna?"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Choose a default proxy for all watches"
|
||||
@@ -457,7 +457,7 @@ msgstr "Vyberte výchozí proxy pro všechna sledování"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Base URL used for the"
|
||||
msgstr ""
|
||||
msgstr "Základní URL použita pro"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "token in notification links."
|
||||
@@ -465,7 +465,7 @@ msgstr "token v odkazech oznámení."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Default value is the system environment variable"
|
||||
msgstr ""
|
||||
msgstr "Výchozí hodnota je systémová proměnná prostředí"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/_common_fields.html
|
||||
msgid "read more here"
|
||||
@@ -485,7 +485,7 @@ msgstr ""
|
||||
msgid ""
|
||||
"If you're having trouble waiting for the page to be fully rendered (text missing etc), try increasing the 'wait' time"
|
||||
" here."
|
||||
msgstr ""
|
||||
msgstr "Pokud máte potíže při čekání na plné vykreslení stránky (chybějící text atp.), zkuste navýšit čas 'prodlevy' zde."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/blueprint/ui/templates/edit.html
|
||||
msgid "This will wait <i>n</i> seconds before extracting the text."
|
||||
@@ -493,7 +493,7 @@ msgstr "Toto počká <i>n</i> sekund před extrahováním textu."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Number of concurrent workers to process watches. More workers = faster processing but higher memory usage."
|
||||
msgstr ""
|
||||
msgstr "Počet souběžných pracovních procesů sledování. Více procesů = rychlejší zpracování, ale vyšší spotřeba paměti."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Currently running:"
|
||||
@@ -513,27 +513,27 @@ msgstr "aktivně zpracovává"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Example - 3 seconds random jitter could trigger up to 3 seconds earlier or up to 3 seconds later"
|
||||
msgstr ""
|
||||
msgstr "Příklad - 3 sekundový náhodný rozptyl může spustit o 3 sekundy dříve nebo až 3 sekundy později"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "For regular plain requests (not chrome based), maximum number of seconds until timeout, 1-999."
|
||||
msgstr ""
|
||||
msgstr "Pro běžné základní požadavky (bez použití chrome), maximální počet sekund do vypršení, 1-999."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Applied to all requests."
|
||||
msgstr ""
|
||||
msgstr "Nastaveno pro všechny požadavky."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Note: Simply changing the User-Agent often does not defeat anti-robot technologies, it's important to consider"
|
||||
msgstr ""
|
||||
msgstr "Pozn.: Pouhá změna hodnoty User-Agent často neobejde technologie zamezující přístup robotů, je třeba vzít v potaz"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "all of the ways that the browser is detected"
|
||||
msgstr ""
|
||||
msgstr "všechny možnosti jak lze prohlížeč rozpoznat."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Connect using Bright Data proxies, find out more here."
|
||||
msgstr ""
|
||||
msgstr "Připojit pomocí Bright Data proxy, více se lze dozvědět zde."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/blueprint/ui/templates/diff.html
|
||||
#: changedetectionio/blueprint/ui/templates/edit.html changedetectionio/templates/_common_fields.html
|
||||
@@ -542,7 +542,7 @@ msgstr "Tip:"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Ignore whitespace, tabs and new-lines/line-feeds when considering if a change was detected."
|
||||
msgstr ""
|
||||
msgstr "Ignorovat mezery, tabulátory a nové řádky/odřádkování, při odhadu zda došlo ke změně."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Note:"
|
||||
@@ -550,31 +550,31 @@ msgstr "Poznámka:"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Changing this will change the status of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
msgstr "Při změně této hodnoty se změní stav existujících sledování a to pravděpodobně spustí upozornění atp."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Render anchor tag content, default disabled, when enabled renders links as"
|
||||
msgstr ""
|
||||
msgstr "Vykreslit obsah kotvícího tagu, výchozí vypnuto, při zapnutí vykresluje odkazu jako"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Changing this could affect the content of your existing watches, possibly trigger alerts etc."
|
||||
msgstr ""
|
||||
msgstr "Při změně této hodnoty se nejspíše změní stav existujících sledování a to nejspíše spustí upozornění atp."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
msgid "Remove HTML element(s) by CSS and XPath selectors before text conversion."
|
||||
msgstr ""
|
||||
msgstr "Odstranit HTML element(y) pomocí CSS a XPath značek před konverzí textu."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
msgid "Don't paste HTML here, use only CSS and XPath selectors"
|
||||
msgstr ""
|
||||
msgstr "Nevkládat HTML, ale pouze CSS a XPath značky"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/include_subtract.html
|
||||
msgid "Add multiple elements, CSS or XPath selectors per line to ignore multiple parts of the HTML."
|
||||
msgstr ""
|
||||
msgstr "Přidat vícero elementů, CSS nebo XPath značky vždy na novou řádku, aby bylo postupně ignorováno více částí HTML."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Note: This is applied globally in addition to the per-watch rules."
|
||||
msgstr ""
|
||||
msgstr "Pozn.: Toto je aplikováno globálně dodatečně k pravidlům nastaveným pro jednotlivá sledování."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Matching text will be ignored in the text snapshot (you can still see it but it wont trigger a change)"
|
||||
@@ -582,47 +582,47 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Each line processed separately, any line matching will be ignored (removed before creating the checksum)"
|
||||
msgstr ""
|
||||
msgstr "Každá řádka zpracována samostatně, odpovídající řádky budou ignorovány (odstraněny před založením kontrolního součtu)"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Regular Expression support, wrap the entire line in forward slash"
|
||||
msgstr ""
|
||||
msgstr "Podpora regulárních výrazů, ohraničit celé řádky lomítkem"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/templates/edit/text-options.html
|
||||
msgid "Changing this will affect the comparison checksum which may trigger an alert"
|
||||
msgstr ""
|
||||
msgstr "Změna této hodnoty ovlivní porovnávací kontrolní součet, což může spustit upozornění"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Remove any text that appears in the \"Ignore text\" from the output (otherwise its just ignored for change-detection)"
|
||||
msgstr ""
|
||||
msgstr "Odstranit všechen text z výstupu zadaný pod \"Ignorovat text\" (jinak bude ignorováno pouze pro detekci změn)"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "API Access"
|
||||
msgstr ""
|
||||
msgstr "API Přístup"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Drive your changedetection.io via API, More about"
|
||||
msgstr ""
|
||||
msgstr "Ovládejte svou changedetection.io pomocí API, Více o"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "API access and examples here"
|
||||
msgstr "Přístup k API a příklady zde"
|
||||
msgstr "přístupu k API a příklady zde"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Restrict API access limit by using"
|
||||
msgstr ""
|
||||
msgstr "Omezit API přístupový limit použitím"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "header - required for the Chrome Extension to work"
|
||||
msgstr ""
|
||||
msgstr "hlavičky - vyžadováno pro správné fungování Chrome rozšíření"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "copy"
|
||||
msgstr ""
|
||||
msgstr "kopírovat"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Regenerate API key"
|
||||
msgstr ""
|
||||
msgstr "Obnovit API klíč"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Chrome Extension"
|
||||
@@ -630,43 +630,43 @@ msgstr "Rozšíření pro Chrome"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Easily add any web-page to your changedetection.io installation from within Chrome."
|
||||
msgstr ""
|
||||
msgstr "Přidávejte jakékoliv webové stránky do své changedetection.io instalace přímo z prohlížeče Chrome."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Step 1"
|
||||
msgstr ""
|
||||
msgstr "Krok 1"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Install the extension,"
|
||||
msgstr ""
|
||||
msgstr "Nainstalovat rozšíření,"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Step 2"
|
||||
msgstr ""
|
||||
msgstr "Krok 2"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Navigate to this page,"
|
||||
msgstr ""
|
||||
msgstr "Navigovat na tuto stránku,"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Step 3"
|
||||
msgstr ""
|
||||
msgstr "Krok 3"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Open the extension from the toolbar and click"
|
||||
msgstr ""
|
||||
msgstr "Otevřít rozšíření z lišty a kliknout"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Sync API Access"
|
||||
msgstr ""
|
||||
msgstr "Synchronizovat API přístup"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Try our new Chrome Extension!"
|
||||
msgstr ""
|
||||
msgstr "Ozkoušet naše nové Chrom rozšíření"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Chrome store icon"
|
||||
msgstr ""
|
||||
msgstr "ikona obchodu Chrome"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Chrome Webstore"
|
||||
@@ -674,15 +674,15 @@ msgstr "Chrome Webstore"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Maximum number of history snapshots to include in the watch specific RSS feed."
|
||||
msgstr ""
|
||||
msgstr "Maximální počet snímků historie přiřazených ke sledování specifického RSS zdroje."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "For watching other RSS feeds - When watching RSS/Atom feeds, convert them into clean text for better change detection."
|
||||
msgstr ""
|
||||
msgstr "Sledování dalších RSS zdrojů - Při sledování RSS/Atom zdrojů, převádět na obyčejný text pro lepší sledování změn."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Does your reader support HTML? Set it here"
|
||||
msgstr ""
|
||||
msgstr "Máte čtečku podporující HTML? Nastavit zde"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "'System default' for the same template for all items, or re-use your \"Notification Body\" as the template."
|
||||
@@ -690,23 +690,23 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Ensure the settings below are correct, they are used to manage the time schedule for checking your web page watches."
|
||||
msgstr ""
|
||||
msgstr "Ujistěte se, že nastavení níže je správně, je použito pro časové rozestupy kontrol sledování webových stránek."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "UTC Time & Date from Server:"
|
||||
msgstr ""
|
||||
msgstr "UTC Čas a Datum Serveru:"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Local Time & Date in Browser:"
|
||||
msgstr ""
|
||||
msgstr "Místní Čas a Datum prohlížeče:"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Enable this setting to open the diff page in a new tab. If disabled, the diff page will open in the current tab."
|
||||
msgstr ""
|
||||
msgstr "Po povolení tohoto nastavení bude stránka rozdílů otevřena v novém tabu. Při vypnutí bude použit aktuální tab."
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Realtime UI Updates Enabled - (Restart required if this is changed)"
|
||||
msgstr ""
|
||||
msgstr "Povolit aktualizace UI v reálném čase - (změna vyžaduje restart)"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html
|
||||
msgid "Enable or Disable Favicons next to the watch list"
|
||||
@@ -2345,31 +2345,31 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than"
|
||||
msgstr ""
|
||||
msgstr "Větší než"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than"
|
||||
msgstr ""
|
||||
msgstr "Menší než"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Greater Than or Equal To"
|
||||
msgstr ""
|
||||
msgstr "Větší než nebo shodný s"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Less Than or Equal To"
|
||||
msgstr ""
|
||||
msgstr "Menší než nebo shodný s"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Equals"
|
||||
msgstr ""
|
||||
msgstr "Shoduje se s"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Not Equals"
|
||||
msgstr ""
|
||||
msgstr "Neshoduje se"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Contains"
|
||||
msgstr ""
|
||||
msgstr "Obsahuje"
|
||||
|
||||
#: changedetectionio/conditions/__init__.py
|
||||
msgid "Choose one - Field"
|
||||
@@ -2811,12 +2811,12 @@ msgstr "Použít globální nastavení pro čas mezi kontrolou a plánovačem."
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "AI Change Intent"
|
||||
msgstr ""
|
||||
msgstr "AI záměr změny"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html changedetectionio/blueprint/ui/templates/diff.html
|
||||
#: changedetectionio/forms.py changedetectionio/templates/edit/include_llm_intent.html
|
||||
msgid "AI Change Summary"
|
||||
msgstr ""
|
||||
msgstr "AI souhrn změny"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "CSS/JSONPath/JQ/XPath Filters"
|
||||
@@ -2828,7 +2828,7 @@ msgstr "Odstranit prvky"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Extract lines containing"
|
||||
msgstr ""
|
||||
msgstr "Extrahovat řádky obsahující"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Extract text"
|
||||
@@ -3055,7 +3055,7 @@ msgstr "Základní URL pro upozornění"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Not set"
|
||||
msgstr ""
|
||||
msgstr "Nenastaveno"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Treat empty pages as a change?"
|
||||
@@ -3063,7 +3063,7 @@ msgstr "Považovat prázdné stránky za změnu?"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Ignore Text"
|
||||
msgstr "Text chyby"
|
||||
msgstr "Ignorovat text"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Ignore whitespace"
|
||||
@@ -3071,7 +3071,7 @@ msgstr "Ignorujte mezery"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Screenshot: Minimum Change Percentage"
|
||||
msgstr ""
|
||||
msgstr "Screenshot: minimální procento změny"
|
||||
|
||||
#: changedetectionio/forms.py changedetectionio/processors/image_ssim_diff/forms.py
|
||||
msgid "Must be between 0 and 100"
|
||||
@@ -3135,7 +3135,7 @@ msgstr "Kolikrát může filtr chybět před odesláním upozornění"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Model"
|
||||
msgstr ""
|
||||
msgstr "Model"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings.html changedetectionio/forms.py
|
||||
msgid "API Key"
|
||||
@@ -3163,7 +3163,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Monthly token budget"
|
||||
msgstr ""
|
||||
msgstr "Měsíční rozpočet tokenů"
|
||||
|
||||
#: changedetectionio/blueprint/settings/templates/settings_llm_tab.html changedetectionio/forms.py
|
||||
msgid "Max input characters"
|
||||
@@ -3179,7 +3179,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "AI thinking budget (tokens)"
|
||||
msgstr ""
|
||||
msgstr "AI pracovní rozpočet (tokeny)"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Off (no thinking)"
|
||||
@@ -3191,7 +3191,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "When monthly token budget is reached"
|
||||
msgstr ""
|
||||
msgstr "Při dosažení měsíčního rozpočtu tokenů"
|
||||
|
||||
#: changedetectionio/forms.py
|
||||
msgid "Skip AI summarisation only (watch still checks)"
|
||||
@@ -3277,7 +3277,7 @@ msgstr "Porovnání snímků obrazovky"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/preview.py
|
||||
msgid "Preview unavailable - No snapshots captured yet"
|
||||
msgstr ""
|
||||
msgstr "Náhled nedostupný - Zatím nebyly pořízeny žádné snapshoty"
|
||||
|
||||
#: changedetectionio/processors/image_ssim_diff/processor.py
|
||||
msgid "Visual / Image screenshot change detection"
|
||||
@@ -3779,7 +3779,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "A new version is available"
|
||||
msgstr ""
|
||||
msgstr "Je dostupná nová verze"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Search, or Use Alt+S Key"
|
||||
@@ -3787,7 +3787,7 @@ msgstr "Vyhledejte nebo použijte klávesu Alt+S"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Share this link:"
|
||||
msgstr ""
|
||||
msgstr "Sdílet tento odkaz:"
|
||||
|
||||
#: changedetectionio/templates/base.html
|
||||
msgid "Real-time updates offline"
|
||||
@@ -3840,7 +3840,7 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/templates/edit/include_llm_intent.html
|
||||
msgid "AI"
|
||||
msgstr ""
|
||||
msgstr "AI"
|
||||
|
||||
#: changedetectionio/templates/edit/include_llm_intent.html
|
||||
msgid ""
|
||||
@@ -4104,23 +4104,23 @@ msgstr "IMPORTOVAT"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Resume automatic scheduling"
|
||||
msgstr ""
|
||||
msgstr "Pokračovat s automatickým naplánováním"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Pause auto-queue scheduling of watches"
|
||||
msgstr ""
|
||||
msgstr "Pozastavit automatické řazení plánovaných sledovaní"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Scheduling is paused - click to resume"
|
||||
msgstr ""
|
||||
msgstr "Naplánování je pozastaveno - klikněte pro opětovné spuštění"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Unmute notifications"
|
||||
msgstr "Odtlumit oznámení"
|
||||
msgstr "Opět povolit oznámení"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Notifications are muted - click to unmute"
|
||||
msgstr "Oznámení jsou ztlumena - klikněte pro odtlumení"
|
||||
msgstr "Oznámení jsou ztlumena - klikněte pro opětovné povolení"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "EDIT"
|
||||
@@ -4136,11 +4136,11 @@ msgstr ""
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Toggle AI Mode"
|
||||
msgstr ""
|
||||
msgstr "Přepnout AI Mód"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Toggle AI mode"
|
||||
msgstr ""
|
||||
msgstr "Přepnout AI mód"
|
||||
|
||||
#: changedetectionio/templates/menu.html
|
||||
msgid "Toggle Light/Dark Mode"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: changedetection.io 0.55.3\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2026-05-12 17:39+0200\n"
|
||||
"POT-Creation-Date: 2026-05-15 12:51+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
-20
@@ -1,27 +1,24 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Install additional packages from EXTRA_PACKAGES env var
|
||||
# Uses a marker file to avoid reinstalling on every container restart
|
||||
INSTALLED_MARKER="/datastore/.extra_packages_installed"
|
||||
CURRENT_PACKAGES="$EXTRA_PACKAGES"
|
||||
|
||||
# Install additional Python packages from the EXTRA_PACKAGES env var.
|
||||
#
|
||||
# Why no marker / skip-cache:
|
||||
# A previous version of this script wrote a marker file to
|
||||
# /datastore/.extra_packages_installed and skipped pip when it was present.
|
||||
# That marker lived on the persistent /datastore volume, but the pip-installed
|
||||
# packages live in the container's writable layer — so after a `docker compose
|
||||
# down && up` (or any container recreation) the packages were gone while the
|
||||
# marker remained, and the script wrongly believed everything was installed.
|
||||
# See: https://github.com/dgtlmoon/changedetection.io/issues/4140
|
||||
#
|
||||
# Running pip on every start is correct by construction: when the requirements
|
||||
# are already satisfied, pip is a fast no-op ("Requirement already satisfied"),
|
||||
# adding ~1s per package. That's a small price for not lying about the install
|
||||
# state — and pip's own resolver is the authoritative check, not a flat file.
|
||||
if [ -n "$EXTRA_PACKAGES" ]; then
|
||||
# Check if we need to install/update packages
|
||||
if [ ! -f "$INSTALLED_MARKER" ] || [ "$(cat $INSTALLED_MARKER 2>/dev/null)" != "$CURRENT_PACKAGES" ]; then
|
||||
echo "Installing extra packages: $EXTRA_PACKAGES"
|
||||
pip3 install --no-cache-dir $EXTRA_PACKAGES
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "$CURRENT_PACKAGES" > "$INSTALLED_MARKER"
|
||||
echo "Extra packages installed successfully"
|
||||
else
|
||||
echo "ERROR: Failed to install extra packages"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Extra packages already installed: $EXTRA_PACKAGES"
|
||||
fi
|
||||
echo "Ensuring extra packages installed: $EXTRA_PACKAGES"
|
||||
pip3 install --no-cache-dir $EXTRA_PACKAGES
|
||||
fi
|
||||
|
||||
# Execute the main command
|
||||
|
||||
Reference in New Issue
Block a user