diff --git a/changedetectionio/async_update_worker.py b/changedetectionio/async_update_worker.py index a607ca09bc0..ed945d9844d 100644 --- a/changedetectionio/async_update_worker.py +++ b/changedetectionio/async_update_worker.py @@ -91,7 +91,7 @@ async def async_update_worker(worker_id, q, notification_q, app, datastore): try: processor_module = importlib.import_module(processor_module_name) except ModuleNotFoundError as e: - print(f"Processor module '{processor}' not found.") + logger.error(f"Processor module '{processor}' not found.") raise e update_handler = processor_module.perform_site_check(datastore=datastore, diff --git a/changedetectionio/blueprint/settings/templates/settings.html b/changedetectionio/blueprint/settings/templates/settings.html index b38c3d4f273..986908abea1 100644 --- a/changedetectionio/blueprint/settings/templates/settings.html +++ b/changedetectionio/blueprint/settings/templates/settings.html @@ -5,6 +5,7 @@ {% from '_common_fields.html' import render_common_settings_form %} @@ -19,6 +21,8 @@ + +
diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index ce4bb716aa4..22ebe348c58 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -106,14 +106,14 @@ def _handle_operations(op, uuids, datastore, worker_handler, update_q, queuedWat for uuid in uuids: watch_check_update.send(watch_uuid=uuid) -def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_handler, queuedWatchMetaData, watch_check_update): +def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_handler, queuedWatchMetaData, watch_check_update, notification_q): ui_blueprint = Blueprint('ui', __name__, template_folder="templates") # Register the edit blueprint edit_blueprint = construct_edit_blueprint(datastore, update_q, queuedWatchMetaData) ui_blueprint.register_blueprint(edit_blueprint) - # Register the notification blueprint + # Register the notification blueprint - mostly used for sending test notification_blueprint = construct_notification_blueprint(datastore) ui_blueprint.register_blueprint(notification_blueprint) diff --git a/changedetectionio/blueprint/ui/notification.py b/changedetectionio/blueprint/ui/notification.py index f20fb52715b..dfcc875d576 100644 --- a/changedetectionio/blueprint/ui/notification.py +++ b/changedetectionio/blueprint/ui/notification.py @@ -1,50 +1,85 @@ -from flask import Blueprint, request, make_response +from flask import Blueprint, request, make_response, jsonify import random from loguru import logger +from changedetectionio.notification.handler import process_notification from changedetectionio.store import ChangeDetectionStore from changedetectionio.auth_decorator import login_optionally_required def construct_blueprint(datastore: ChangeDetectionStore): notification_blueprint = Blueprint('ui_notification', __name__, template_folder="../ui/templates") - + + + @notification_blueprint.route("/notification/render-preview/", methods=['POST']) + @notification_blueprint.route("/notification/render-preview", methods=['POST']) + @notification_blueprint.route("/notification/render-preview/", methods=['POST']) + @login_optionally_required + def ajax_callback_test_render_preview(watch_uuid=None): + return ajax_callback_send_notification_test(watch_uuid=watch_uuid, send_as_null_test=True) + # AJAX endpoint for sending a test @notification_blueprint.route("/notification/send-test/", methods=['POST']) @notification_blueprint.route("/notification/send-test", methods=['POST']) @notification_blueprint.route("/notification/send-test/", methods=['POST']) @login_optionally_required - def ajax_callback_send_notification_test(watch_uuid=None): + def ajax_callback_send_notification_test(watch_uuid=None, send_as_null_test=False): # Watch_uuid could be unset in the case it`s used in tag editor, global settings import apprise - from changedetectionio.notification.handler import process_notification + from urllib.parse import urlparse from changedetectionio.notification.apprise_plugin.assets import apprise_asset - from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler + # Necessary so that we import our custom handlers + from changedetectionio.notification.apprise_plugin.custom_handlers import apprise_http_custom_handler, apprise_null_custom_handler apobj = apprise.Apprise(asset=apprise_asset) + sent_obj = {} is_global_settings_form = request.args.get('mode', '') == 'global-settings' is_group_settings_form = request.args.get('mode', '') == 'group-settings' # Use an existing random one on the global/main settings form - if not watch_uuid and (is_global_settings_form or is_group_settings_form) \ - and datastore.data.get('watching'): - logger.debug(f"Send test notification - Choosing random Watch {watch_uuid}") + if not watch_uuid and is_global_settings_form and datastore.data.get('watching'): watch_uuid = random.choice(list(datastore.data['watching'].keys())) + logger.debug(f"Send test notification - Chose random watch UUID: {watch_uuid}") + + if is_group_settings_form and datastore.data.get('watching'): + logger.debug(f"Send test notification - Choosing random Watch from group {watch_uuid}") + matching_watches = [uuid for uuid, watch in datastore.data['watching'].items() if watch.get('tags') and watch_uuid in watch['tags']] + if matching_watches: + watch_uuid = random.choice(matching_watches) + else: + # Just fallback to any + watch_uuid = random.choice(list(datastore.data['watching'].keys())) if not watch_uuid: return make_response("Error: You must have atleast one watch configured for 'test notification' to work", 400) watch = datastore.data['watching'].get(watch_uuid) - notification_urls = None + notification_urls = [] - if request.form.get('notification_urls'): - notification_urls = request.form['notification_urls'].strip().splitlines() + if send_as_null_test: + test_schema = '' + try: + if request.form.get('notification_urls') and '://' in request.form.get('notification_urls'): + first_test_notification_url = request.form['notification_urls'].strip().splitlines()[0] + test_schema = urlparse(first_test_notification_url).scheme.lower().strip() + except Exception as e: + logger.error(f"Error trying to get a test schema based on the first notification_url {str(e)}") + + notification_urls = [ + # Null lets us do the whole chain of the same code without any extra repeated code + f'null://null-test-just-to-render-everything-on-the-same-codepath-and-get-preview?test_schema={test_schema}' + ] + + else: + if request.form.get('notification_urls'): + notification_urls += request.form['notification_urls'].strip().splitlines() if not notification_urls: logger.debug("Test notification - Trying by group/tag in the edit form if available") + # @todo this logic is not clear, omegaconf? # On an edit page, we should also fire off to the tags if they have notifications if request.form.get('tags') and request.form['tags'].strip(): for k in request.form['tags'].split(','): @@ -58,23 +93,28 @@ def ajax_callback_send_notification_test(watch_uuid=None): notification_urls = datastore.data['settings']['application']['notification_urls'] if not notification_urls: - return 'Error: No Notification URLs set/found' + return make_response("Error: No Notification URLs set/found.", 400) for n_url in notification_urls: if len(n_url.strip()): if not apobj.add(n_url): - return f'Error: {n_url} is not a valid AppRise URL.' + return make_response(f'Error: {n_url} is not a valid AppRise URL.', 400) try: # use the same as when it is triggered, but then override it with the form test values n_object = { 'watch_url': request.form.get('window_url', "https://changedetection.io"), - 'notification_urls': notification_urls + 'notification_urls': notification_urls, + 'uuid': watch_uuid # Ensure uuid is present so diff rendering works } # Only use if present, if not set in n_object it should use the default system value - if 'notification_format' in request.form and request.form['notification_format'].strip(): - n_object['notification_format'] = request.form.get('notification_format', '').strip() + notif_format = request.form.get('notification_format', '').strip() + # Use it if provided and not "System default", otherwise fall back + if notif_format and notif_format != 'System default': + n_object['notification_format'] = notif_format + else: + n_object['notification_format'] = datastore.data['settings']['application'].get('notification_format') if 'notification_title' in request.form and request.form['notification_title'].strip(): n_object['notification_title'] = request.form.get('notification_title', '').strip() @@ -92,7 +132,13 @@ def ajax_callback_send_notification_test(watch_uuid=None): n_object['as_async'] = False n_object.update(watch.extra_notification_token_values()) - sent_obj = process_notification(n_object, datastore) + + # This uses the same processor that the queue runner uses + # @todo - Split the notification URLs so we know which one worked, maybe highlight them in green in the UI + result = process_notification(n_object, datastore) + if result: + sent_obj['result'] = result[0] + sent_obj['status'] = 'OK - Sent test notifications' except Exception as e: e_str = str(e) @@ -100,9 +146,9 @@ def ajax_callback_send_notification_test(watch_uuid=None): e_str = e_str.replace( "DEBUG - .CustomNotifyPluginWrapper'>", '') - return make_response(e_str, 400) - return 'OK - Sent test notifications' + # it will be a list of things reached, for this purpose just the first is good so we can see the body that was sent + return make_response(sent_obj, 200) return notification_blueprint \ No newline at end of file diff --git a/changedetectionio/blueprint/ui/templates/edit.html b/changedetectionio/blueprint/ui/templates/edit.html index f6e7f6a086d..5bbed4f6e10 100644 --- a/changedetectionio/blueprint/ui/templates/edit.html +++ b/changedetectionio/blueprint/ui/templates/edit.html @@ -21,6 +21,7 @@ const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}'); {% endif %} const notification_base_url="{{url_for('ui.ui_notification.ajax_callback_send_notification_test', watch_uuid=uuid)}}"; + const notification_test_render_preview_url="{{url_for('ui.ui_notification.ajax_callback_test_render_preview', watch_uuid=uuid)}}"; const playwright_enabled={% if playwright_enabled %}true{% else %}false{% endif %}; const recheck_proxy_start_url="{{url_for('check_proxies.start_check', uuid=uuid)}}"; const proxy_recheck_status_url="{{url_for('check_proxies.get_recheck_status', uuid=uuid)}}"; @@ -356,12 +357,12 @@

Text filtering


{#
Refresh
#} -
+
-
+

Loading...

- diff --git a/changedetectionio/conditions/pluggy_interface.py b/changedetectionio/conditions/pluggy_interface.py index f67b9f51781..b7b6eb9d3c6 100644 --- a/changedetectionio/conditions/pluggy_interface.py +++ b/changedetectionio/conditions/pluggy_interface.py @@ -1,7 +1,7 @@ import pluggy import os import importlib -import sys +from loguru import logger from . import default_plugin # ✅ Ensure that the namespace in HookspecMarker matches PluginManager @@ -65,7 +65,7 @@ def load_plugins_from_directory(): # Register the plugin with pluggy plugin_manager.register(module, module_name) except (ImportError, AttributeError) as e: - print(f"Error loading plugin {module_name}: {e}") + logger.critical(f"Error loading plugin {module_name}: {e}") # Load plugins from the plugins directory load_plugins_from_directory() diff --git a/changedetectionio/flask_app.py b/changedetectionio/flask_app.py index 6e52d4ed919..ebf1181e853 100644 --- a/changedetectionio/flask_app.py +++ b/changedetectionio/flask_app.py @@ -519,7 +519,7 @@ def static_content(group, filename): # watchlist UI buttons etc import changedetectionio.blueprint.ui as ui - app.register_blueprint(ui.construct_blueprint(datastore, update_q, worker_handler, queuedWatchMetaData, watch_check_update)) + app.register_blueprint(ui.construct_blueprint(datastore, update_q, worker_handler, queuedWatchMetaData, watch_check_update, notification_q)) import changedetectionio.blueprint.watchlist as watchlist app.register_blueprint(watchlist.construct_blueprint(datastore=datastore, update_q=update_q, queuedWatchMetaData=queuedWatchMetaData), url_prefix='') diff --git a/changedetectionio/model/Watch.py b/changedetectionio/model/Watch.py index aeae26d8ba9..ff518a618db 100644 --- a/changedetectionio/model/Watch.py +++ b/changedetectionio/model/Watch.py @@ -642,8 +642,10 @@ def extra_notification_token_values(self): def extra_notification_token_placeholder_info(self): # Used for providing extra tokens - # return [('widget', "Get widget amounts")] - return [] + values = [] + values.append(('watch_html_link', "Link to URL as ")) + values.append(('watch_url_raw', "Raw URL/link before any jinja2 macro")) + return values def extract_regex_from_all_history(self, regex): diff --git a/changedetectionio/notification/__init__.py b/changedetectionio/notification/__init__.py index 0129f2440e8..87fa400c155 100644 --- a/changedetectionio/notification/__init__.py +++ b/changedetectionio/notification/__init__.py @@ -2,8 +2,8 @@ ult_notification_format_for_watch = 'System default' default_notification_format = 'HTML Color' -default_notification_body = '{{watch_url}} had a change.\n---\n{{diff}}\n---\n' -default_notification_title = 'ChangeDetection.io Notification - {{watch_url}}' +default_notification_body = '{{watch_title}} had a change.\n---\n{{diff}}\n---\n' +default_notification_title = 'ChangeDetection.io Notification - {{watch_title}}' # The values (markdown etc) are from apprise NotifyFormat, # But to avoid importing the whole heavy module just use the same strings here. diff --git a/changedetectionio/notification/apprise_plugin/custom_handlers.py b/changedetectionio/notification/apprise_plugin/custom_handlers.py index 1fd28b41616..a3293407e4a 100644 --- a/changedetectionio/notification/apprise_plugin/custom_handlers.py +++ b/changedetectionio/notification/apprise_plugin/custom_handlers.py @@ -19,6 +19,11 @@ def notify_supported_methods(func): return func +def notify_null_method(func): + func = notify(on="null")(func) + return func + + def _get_auth(parsed_url: dict) -> str | tuple[str, str]: user: str | None = parsed_url.get("user") password: str | None = parsed_url.get("password") @@ -110,3 +115,21 @@ def apprise_http_custom_handler( except Exception as e: logger.error(f"Unexpected error occurred while sending custom notification to {url}: {e}") return False + + +@notify_null_method +def apprise_null_custom_handler( + body: str, + title: str, + notify_type: str, + meta: dict, + *args, + **kwargs, +) -> bool: + url: str = meta.get("url") + schema: str = meta.get("schema") + method: str = re.sub(r"s$", "", schema).upper() + logger.info(f"Processed 'null' notification") + + return True + diff --git a/changedetectionio/notification/handler.py b/changedetectionio/notification/handler.py index c1b3c125320..3baa873dc20 100644 --- a/changedetectionio/notification/handler.py +++ b/changedetectionio/notification/handler.py @@ -1,31 +1,167 @@ +import os import time import apprise from loguru import logger from .apprise_plugin.assets import apprise_asset, APPRISE_AVATAR_URL +from changedetectionio.safe_jinja import render as jinja_render +from urllib.parse import urlparse + +def _populate_notification_tokens(n_object, datastore): + """ + Populate notification tokens (diff, current_snapshot, etc.) if not already present. + This ensures both queued notifications and test notifications have the same data. + """ + from changedetectionio import diff + from changedetectionio.notification import default_notification_format_for_watch + from markupsafe import escape + + watch_uuid = n_object.get('uuid') + if not watch_uuid: + return + + watch = datastore.data['watching'].get(watch_uuid) + if not watch: + return + + dates = [] + trigger_text = '' + watch_html_link = '' + + if watch: + watch_history = watch.history + dates = list(watch_history.keys()) + trigger_text = watch.get('trigger_text', []) + + # Add text that was triggered + if len(dates): + snapshot_contents = watch.get_history_snapshot(dates[-1]) + + if n_object.get('notification_format').lower().startswith('html'): + snapshot_contents = str(escape(snapshot_contents)) + + else: + snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." + + # If we ended up here with "System default" + if n_object.get('notification_format') == default_notification_format_for_watch: + n_object['notification_format'] = datastore.data['settings']['application'].get('notification_format') + + html_colour_enable = False + line_feed_sep = "\n" + + # HTML needs linebreak, but MarkDown and Text can use a linefeed + if n_object.get('notification_format').lower().startswith('html'): + line_feed_sep = "
" + # Snapshot will be plaintext on the disk, convert to some kind of HTML + snapshot_contents = snapshot_contents.replace('\n', line_feed_sep) + if n_object.get('notification_format') == 'HTML Color': + html_colour_enable = True + + triggered_text = '' + if len(trigger_text): + from changedetectionio import html_tools + triggered_text = html_tools.get_triggered_text(content=snapshot_contents, trigger_text=trigger_text) + if triggered_text: + triggered_text = line_feed_sep.join(triggered_text) + + # Could be called as a 'test notification' with only 1 snapshot available + prev_snapshot = "Example text: example test\nExample text: change detection is cool\nExample text: some more examples\n" + current_snapshot = "Example text: example test\nExample text: More than 1 watch change needs to exist to build a nice preview!" + + if len(dates) > 1: + prev_snapshot = watch.get_history_snapshot(dates[-2]) + current_snapshot = watch.get_history_snapshot(dates[-1]) + if n_object.get('notification_format').lower().startswith('html'): + prev_snapshot = str(escape(prev_snapshot)) + current_snapshot = str(escape(current_snapshot)) + + + if watch: + v = {'url': watch.get('url'), 'label': watch.label} + watch_html_link = jinja_render(template_str='
{{ url | e }}', **v) + + + n_object.update({ + 'current_snapshot': snapshot_contents, + 'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), + 'diff_added': diff.render_diff(prev_snapshot, current_snapshot, include_removed=False, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), + 'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), + 'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True), + 'diff_removed': diff.render_diff(prev_snapshot, current_snapshot, include_added=False, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), + 'screenshot': watch.get_screenshot() if watch and watch.get('notification_screenshot') else None, + 'triggered_text': triggered_text, + 'watch_html_link': watch_html_link, + 'watch_url': watch.link, + 'watch_url_raw': watch.get('url'), + }) + + if watch: + n_object.update(watch.extra_notification_token_values()) + +def scan_notification_file_templates(url, datastore, n_body, notification_parameters): + import glob + from urllib.parse import urlparse, parse_qs + + try: + scheme = urlparse(url).scheme.lower().strip() + + # schema could be overriden dynamically + if scheme == 'null' and 'test_schema=' in url: + scheme = parse_qs(urlparse(url).query).get("test_schema", [None])[0] + + logger.debug(f"Looking for '{scheme}' notification wrapper templates...") + + # Try exact match first, then wildcard matches + candidates = [ + os.path.join(datastore.datastore_path, f"notification-wrapper-{scheme}.html"), + *[f for f in glob.glob(os.path.join(datastore.datastore_path, "notification-wrapper-*--.html")) + if scheme.startswith(os.path.basename(f).replace("notification-wrapper-", "").replace("--.html", ""))] + ] + + for tpl_name in candidates: + if os.path.isfile(tpl_name): + template_params = notification_parameters.copy() + template_params['notification_body'] = n_body + + with open(tpl_name, 'r', encoding='utf-8') as f: + logger.info(f"Using HTML notification template wrapper from '{tpl_name}'") + return jinja_render(template_str=f.read(), **template_params) + + except Exception as e: + logger.warning(f"Failed to load notification template: {e}") + + return None def process_notification(n_object, datastore): - from changedetectionio.safe_jinja import render as jinja_render from . import default_notification_format_for_watch, default_notification_format, valid_notification_formats # be sure its registered - from .apprise_plugin.custom_handlers import apprise_http_custom_handler + from .apprise_plugin.custom_handlers import apprise_http_custom_handler, apprise_null_custom_handler + + n_body = '' + n_title = '' now = time.time() if n_object.get('notification_timestamp'): logger.trace(f"Time since queued {now-n_object['notification_timestamp']:.3f}s") - # Insert variables into the notification content - notification_parameters = create_notification_parameters(n_object, datastore) n_format = valid_notification_formats.get( n_object.get('notification_format', default_notification_format), valid_notification_formats[default_notification_format], ) - # If we arrived with 'System default' then look it up if n_format == default_notification_format_for_watch and datastore.data['settings']['application'].get('notification_format') != default_notification_format_for_watch: # Initially text or whatever n_format = datastore.data['settings']['application'].get('notification_format', valid_notification_formats[default_notification_format]) + # Ensure diff rendering is done if not already present (for test notifications) + if not n_object.get('diff') and n_object.get('uuid'): + _populate_notification_tokens(n_object, datastore) + + # Insert variables into the notification content + notification_parameters = create_notification_parameters(n_object, datastore) + + logger.trace(f"Complete notification body including Jinja and placeholders calculated in {time.time() - now:.2f}s") # https://github.com/caronc/apprise/wiki/Development_LogCapture @@ -44,22 +180,27 @@ def process_notification(n_object, datastore): with apprise.LogCapture(level=apprise.logging.DEBUG) as logs: for url in n_object['notification_urls']: + # Commented out is OK + if url.startswith('#') or not url or not url.strip(): + logger.trace(f"Skipping notification URL - '{url}'") + continue # Get the notification body from datastore n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters) + # hmm unsure about this, but why not if n_object.get('notification_format', '').startswith('HTML'): n_body = n_body.replace("\n", '
') n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters) - url = url.strip() - if url.startswith('#'): - logger.trace(f"Skipping commented out notification URL - {url}") - continue + n_body_from_file_template = scan_notification_file_templates(url=url, + datastore=datastore, + n_body=n_body, + notification_parameters=notification_parameters) + if n_body_from_file_template: + n_body = n_body_from_file_template + - if not url: - logger.warning(f"Process Notification: skipping empty notification URL.") - continue logger.info(f">> Process Notification: AppRise notifying {url}") url = jinja_render(template_str=url, **notification_parameters) @@ -104,7 +245,7 @@ def process_notification(n_object, datastore): # Apprise will default to HTML, so we need to override it # So that whats' generated in n_body is in line with what is going to be sent. # https://github.com/caronc/apprise/issues/633#issuecomment-1191449321 - if not 'format=' in url and (n_format == 'Text' or n_format == 'Markdown'): + if not 'format=' in url and (n_format.lower() == 'text' or n_format.lower() == 'markdown'): prefix = '?' if not '?' in url else '&' # Apprise format is lowercase text https://github.com/caronc/apprise/issues/633 n_format = n_format.lower() @@ -118,14 +259,15 @@ def process_notification(n_object, datastore): 'url': url, 'body_format': n_format}) - # Blast off the notifications tht are set in .add() - apobj.notify( - title=n_title, - body=n_body, - body_format=n_format, - # False is not an option for AppRise, must be type None - attach=n_object.get('screenshot', None) - ) + if n_object.get('notification_urls'): + # Blast off the notifications tht are set in .add() + apobj.notify( + title=n_title, + body=n_body, + body_format=n_format, + # False is not an option for AppRise, must be type None + attach=n_object.get('screenshot', None) + ) # Returns empty string if nothing found, multi-line string otherwise diff --git a/changedetectionio/notification/notification-wrapper-HTML-mail--.html b/changedetectionio/notification/notification-wrapper-HTML-mail--.html new file mode 100644 index 00000000000..b8f01bdaa15 --- /dev/null +++ b/changedetectionio/notification/notification-wrapper-HTML-mail--.html @@ -0,0 +1,15 @@ +{# Copy this to your data-store directory if you wish to enable it for HTML style notifications, applies to all as a wrapper :) #} + + +Hello,
+ +

A change was detected on your web page watch for

{{ watch_html_link }}.

+ +[ view history ] [ pause checks ] [ mute notifications ] + +
+ {{ notification_body }} +
+ + + \ No newline at end of file diff --git a/changedetectionio/notification/readme.md b/changedetectionio/notification/readme.md new file mode 100644 index 00000000000..1729d8bfd66 --- /dev/null +++ b/changedetectionio/notification/readme.md @@ -0,0 +1,17 @@ +## Notification syntax + +All notifications use the https://github.com/caronc/apprise syntax, there are some custom ones such as `posts` etc for general web-services usability. + + +## Template file notification wrappers + +You can by default wrap all notifications by creating a `notification-wrapper-HTML-schema.html` in your datastore directory. + +For example + +You can use "`--`" in the filename where the _schema_ is to symbolize a wildcard. For example `notification-wrapper-HTML-mail--.html` would +apply to `mail://` `mailto://` etc etc + +See is `notification-wrapper-HTML-mail--.html` which applies to `mail://`, `mailto://foobar..` etc notifications + + diff --git a/changedetectionio/notification_service.py b/changedetectionio/notification_service.py index 5f3136b8112..9044eb1b9b7 100644 --- a/changedetectionio/notification_service.py +++ b/changedetectionio/notification_service.py @@ -22,70 +22,14 @@ def __init__(self, datastore, notification_q): def queue_notification_for_watch(self, n_object, watch): """ - Queue a notification for a watch with full diff rendering and template variables + Queue a notification for a watch. Diff rendering and template variables will be + handled by process_notification() to ensure consistency with test notifications. """ - from changedetectionio import diff - from changedetectionio.notification import default_notification_format_for_watch - - dates = [] - trigger_text = '' - now = time.time() - if watch: - watch_history = watch.history - dates = list(watch_history.keys()) - trigger_text = watch.get('trigger_text', []) - - # Add text that was triggered - if len(dates): - snapshot_contents = watch.get_history_snapshot(dates[-1]) - else: - snapshot_contents = "No snapshot/history available, the watch should fetch atleast once." - - # If we ended up here with "System default" - if n_object.get('notification_format') == default_notification_format_for_watch: - n_object['notification_format'] = self.datastore.data['settings']['application'].get('notification_format') - - html_colour_enable = False - # HTML needs linebreak, but MarkDown and Text can use a linefeed - if n_object.get('notification_format') == 'HTML': - line_feed_sep = "
" - # Snapshot will be plaintext on the disk, convert to some kind of HTML - snapshot_contents = snapshot_contents.replace('\n', line_feed_sep) - elif n_object.get('notification_format') == 'HTML Color': - line_feed_sep = "
" - # Snapshot will be plaintext on the disk, convert to some kind of HTML - snapshot_contents = snapshot_contents.replace('\n', line_feed_sep) - html_colour_enable = True - else: - line_feed_sep = "\n" - - triggered_text = '' - if len(trigger_text): - from . import html_tools - triggered_text = html_tools.get_triggered_text(content=snapshot_contents, trigger_text=trigger_text) - if triggered_text: - triggered_text = line_feed_sep.join(triggered_text) - - # Could be called as a 'test notification' with only 1 snapshot available - prev_snapshot = "Example text: example test\nExample text: change detection is cool\nExample text: some more examples\n" - current_snapshot = "Example text: example test\nExample text: change detection is fantastic\nExample text: even more examples\nExample text: a lot more examples" - - if len(dates) > 1: - prev_snapshot = watch.get_history_snapshot(dates[-2]) - current_snapshot = watch.get_history_snapshot(dates[-1]) - + # Add basic metadata for the notification n_object.update({ - 'current_snapshot': snapshot_contents, - 'diff': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), - 'diff_added': diff.render_diff(prev_snapshot, current_snapshot, include_removed=False, line_feed_sep=line_feed_sep), - 'diff_full': diff.render_diff(prev_snapshot, current_snapshot, include_equal=True, line_feed_sep=line_feed_sep, html_colour=html_colour_enable), - 'diff_patch': diff.render_diff(prev_snapshot, current_snapshot, line_feed_sep=line_feed_sep, patch_format=True), - 'diff_removed': diff.render_diff(prev_snapshot, current_snapshot, include_added=False, line_feed_sep=line_feed_sep), 'notification_timestamp': now, - 'screenshot': watch.get_screenshot() if watch and watch.get('notification_screenshot') else None, - 'triggered_text': triggered_text, 'uuid': watch.get('uuid') if watch else None, 'watch_url': watch.get('url') if watch else None, }) @@ -93,7 +37,6 @@ def queue_notification_for_watch(self, n_object, watch): if watch: n_object.update(watch.extra_notification_token_values()) - logger.trace(f"Main rendered notification placeholders (diff_added etc) calculated in {time.time()-now:.3f}s") logger.debug("Queued notification for sending") self.notification_q.put(n_object) diff --git a/changedetectionio/pluggy_interface.py b/changedetectionio/pluggy_interface.py index fe2f7182eed..099511b64bf 100644 --- a/changedetectionio/pluggy_interface.py +++ b/changedetectionio/pluggy_interface.py @@ -1,7 +1,7 @@ import pluggy import os import importlib -import sys +from loguru import logger # Global plugin namespace for changedetection.io PLUGIN_NAMESPACE = "changedetectionio" @@ -57,7 +57,7 @@ def load_plugins_from_directories(): # Register the plugin with pluggy plugin_manager.register(module, module_name) except (ImportError, AttributeError) as e: - print(f"Error loading plugin {module_name}: {e}") + logger.critical(f"Error loading plugin {module_name}: {e}") # Load plugins load_plugins_from_directories() diff --git a/changedetectionio/safe_jinja.py b/changedetectionio/safe_jinja.py index 9a29a81572c..ac6aa067c14 100644 --- a/changedetectionio/safe_jinja.py +++ b/changedetectionio/safe_jinja.py @@ -18,7 +18,7 @@ def render(template_str, **args: t.Any) -> str: return output[:JINJA2_MAX_RETURN_PAYLOAD_SIZE] def render_fully_escaped(content): - env = jinja2.sandbox.ImmutableSandboxedEnvironment(autoescape=True) + env = jinja2.sandbox.ImmutableSandboxedEnvironment(autoescape=True, extensions=['jinja2_time.TimeExtension']) template = env.from_string("{{ some_html|e }}") return template.render(some_html=content) diff --git a/changedetectionio/static/js/notifications.js b/changedetectionio/static/js/notifications.js index fa5326ebb10..de1b78cc2fe 100644 --- a/changedetectionio/static/js/notifications.js +++ b/changedetectionio/static/js/notifications.js @@ -1,5 +1,18 @@ $(document).ready(function () { + // Could be from 'watch' or system settings or other + function getNotificationData() { + data = { + notification_body: $('textarea.notification-body').val(), + notification_format: $('select.notification-format').val(), + notification_title: $('input.notification-title').val(), + notification_urls: $('textarea.notification-urls').val(), + tags: $('#tags').val(), + window_url: window.location.href, + } + return data + } + $('#add-email-helper').click(function (e) { e.preventDefault(); email = prompt("Destination email"); @@ -10,17 +23,82 @@ $(document).ready(function () { } }); + $('#notifications-minitabs').miniTabs({ + "Customise": "#notification-setup", + "Preview": "#notification-preview", + }); + + $(document).on('click', '[data-target="#notification-preview"]', function (e) { + var data = getNotificationData(); + $('#notification-iframe-html-preview').contents().find('body').html('Loading...'); + $.ajax({ + type: "POST", + url: notification_test_render_preview_url, + data: data, + statusCode: { + 400: function (data) { + $('#notification-test-log').show().toggleClass('error', true); + $("#notification-test-log>span").text(data.responseText); + }, + } + }).done(function (data) { + $('#notification-test-log').toggleClass('error', false); + setPreview(data['result']); + }) + + }); + + function setPreview(data) { + const iframe = document.getElementById("notification-iframe-html-preview"); + const isDark = document.documentElement.getAttribute('data-darkmode') === 'true'; + + // this should come back in the data objk + const isTextFormat = $('select.notification-format').val() === 'Text'; + + $('#notification-preview-title-text').text(data['title']); + $('#notification-div-text-preview').text(data['body']); + return; + iframe.srcdoc = ` + + + + + ${data['body']} + `; + } + + $('#send-test-notification').click(function (e) { e.preventDefault(); - data = { - notification_body: $('#notification_body').val(), - notification_format: $('#notification_format').val(), - notification_title: $('#notification_title').val(), - notification_urls: $('.notification-urls').val(), - tags: $('#tags').val(), - window_url: window.location.href, - } + var data = getNotificationData(); $('.notifications-wrapper .spinner').fadeIn(); $('#notification-test-log').show(); @@ -30,11 +108,14 @@ $(document).ready(function () { data: data, statusCode: { 400: function (data) { + $("#notification-test-log").toggleClass('error', true); $("#notification-test-log>span").text(data.responseText); }, } }).done(function (data) { - $("#notification-test-log>span").text(data); + $("#notification-test-log").toggleClass('error', false); + $("#notification-test-log>span").text(data['status']); + }).fail(function (jqXHR, textStatus, errorThrown) { // Handle connection refused or other errors if (textStatus === "error" && errorThrown === "") { @@ -42,11 +123,13 @@ $(document).ready(function () { $("#notification-test-log>span").text("Error: Connection refused or server is unreachable."); } else { console.error("Error:", textStatus, errorThrown); - $("#notification-test-log>span").text("An error occurred: " + textStatus); + $("#notification-test-log>span").text("An error occurred: " + errorThrown); } }).always(function () { $('.notifications-wrapper .spinner').hide(); }) }); + + }); diff --git a/changedetectionio/static/js/tabs.js b/changedetectionio/static/js/tabs.js index 99caafba1bc..82447bf55f2 100644 --- a/changedetectionio/static/js/tabs.js +++ b/changedetectionio/static/js/tabs.js @@ -1,11 +1,11 @@ // Rewrite this is a plugin.. is all this JS really 'worth it?' window.addEventListener('hashchange', function () { - var tabs = document.getElementsByClassName('active'); - while (tabs[0]) { - tabs[0].classList.remove('active'); + var tabs = document.querySelectorAll('.tabs .active'); + tabs.forEach(function (tab) { + tab.classList.remove('active'); document.body.classList.remove('full-width'); - } + }); set_active_tab(); }, false); diff --git a/changedetectionio/static/js/watch-settings.js b/changedetectionio/static/js/watch-settings.js index 4d805340139..3366be4df99 100644 --- a/changedetectionio/static/js/watch-settings.js +++ b/changedetectionio/static/js/watch-settings.js @@ -74,7 +74,7 @@ $(document).ready(function () { $('#filters-and-triggers input')[method]('change', request_textpreview_update.throttle(1000)); $("#filters-and-triggers-tab")[method]('click', request_textpreview_update.throttle(1000)); }); - $('.minitabs-wrapper').miniTabs({ + $('#filter-preview-minitabs').miniTabs({ "Content after filters": "#text-preview-inner", "Content raw/before filters": "#text-preview-before-inner" }); diff --git a/changedetectionio/static/styles/scss/parts/_darkmode.scss b/changedetectionio/static/styles/scss/parts/_darkmode.scss index 6a80ecf6970..4c3a84489d0 100644 --- a/changedetectionio/static/styles/scss/parts/_darkmode.scss +++ b/changedetectionio/static/styles/scss/parts/_darkmode.scss @@ -18,8 +18,15 @@ html[data-darkmode="true"] { display: block; } } + + .minitabs-content { + > div { + background-color: rgb(249 249 249 / 13%) !important; + } + } } + diff --git a/changedetectionio/static/styles/scss/parts/_minitabs.scss b/changedetectionio/static/styles/scss/parts/_minitabs.scss index a2dc2d49391..7abdf3bdd85 100644 --- a/changedetectionio/static/styles/scss/parts/_minitabs.scss +++ b/changedetectionio/static/styles/scss/parts/_minitabs.scss @@ -1,6 +1,13 @@ .minitabs-wrapper { width: 100%; + .tab-contents-monospace-preview { + font-family: "Courier New", Courier, monospace; /* Sets the font to a monospace type */ + font-size: 70%; + word-break: break-word; + white-space: pre-wrap; /* Preserves whitespace and line breaks like
 */
+  }
+
   > div[id] {
     padding: 20px;
     border: 1px solid #ccc;
@@ -10,38 +17,45 @@
   .minitabs-content {
     width: 100%;
     display: flex;
+
     > div {
       flex: 1 1 auto;
       min-width: 0;
-      overflow: scroll;
+      padding: 1rem;
+      border: 1px solid #ddd;
+      background-color: #eee;
     }
   }
 
   .minitabs {
     display: flex;
     border-bottom: 1px solid #ccc;
-  }
 
-  .minitab {
-    flex: 1;
-    text-align: center;
-    padding: 12px 0;
-    text-decoration: none;
-    color: #333;
-    background-color: #f1f1f1;
-    border: 1px solid #ccc;
-    border-bottom: none;
-    cursor: pointer;
-    transition: background-color 0.3s;
-  }
+    .minitab {
+      flex: 1;
+      text-align: center;
+      padding: 12px 0;
+      text-decoration: none;
+      color: #333;
+      background-color: #f1f1f1;
+      border: 1px solid #ccc;
+      border-bottom: none;
+      cursor: pointer;
+      transition: background-color 0.3s;
+      border-top-left-radius: 5px;
+      border-top-right-radius: 5px;
+      opacity: 0.45;
+      &:hover {
+        background-color: #ddd;
+      }
 
-  .minitab:hover {
-    background-color: #ddd;
+      &.active {
+        background-color: #eee;
+        font-weight: bold;
+        opacity: 1.0;
+      }
+    }
   }
+}
 
-  .minitab.active {
-    background-color: #fff;
-    font-weight: bold;
-  }
 
-}
diff --git a/changedetectionio/static/styles/scss/parts/_notification.scss b/changedetectionio/static/styles/scss/parts/_notification.scss
new file mode 100644
index 00000000000..844f175ebce
--- /dev/null
+++ b/changedetectionio/static/styles/scss/parts/_notification.scss
@@ -0,0 +1,12 @@
+#notification-preview {
+  resize: both;
+  overflow: hidden;
+}
+
+#notification-iframe-html-preview {
+  width: 100%;
+  height: 100%;
+  border: 0;
+  display: block;
+  overflow: auto;
+}
diff --git a/changedetectionio/static/styles/scss/parts/_preview_text_filter.scss b/changedetectionio/static/styles/scss/parts/_preview_text_filter.scss
index bc10c3cd031..6df1eff2543 100644
--- a/changedetectionio/static/styles/scss/parts/_preview_text_filter.scss
+++ b/changedetectionio/static/styles/scss/parts/_preview_text_filter.scss
@@ -1,5 +1,3 @@
-@use "minitabs";
-
 body.preview-text-enabled {
 
   @media (min-width: 800px) {
@@ -31,19 +29,7 @@ body.preview-text-enabled {
   }
 
   #activate-text-preview {
-      background-color: var(--color-grey-500);
-  }
-
-  /* actual preview area */
-  .monospace-preview {
-    background: var(--color-background-input);
-    border: 1px solid var(--color-grey-600);
-    padding: 1rem;
-    color: var(--color-text-input);
-    font-family: "Courier New", Courier, monospace; /* Sets the font to a monospace type */
-    font-size: 70%;
-    word-break: break-word;
-    white-space: pre-wrap; /* Preserves whitespace and line breaks like 
 */
+    background-color: var(--color-grey-500);
   }
 }
 
@@ -53,3 +39,11 @@ body.preview-text-enabled {
   z-index: 3;
   box-shadow: 1px 1px 4px var(--color-shadow-jump);
 }
+
+#filter-preview-minitabs {
+  .minitabs-content {
+    > div {
+      overflow: scroll;
+    }
+  }
+}
\ No newline at end of file
diff --git a/changedetectionio/static/styles/scss/styles.scss b/changedetectionio/static/styles/scss/styles.scss
index fdd2d0f6ffc..c837f4e05c0 100644
--- a/changedetectionio/static/styles/scss/styles.scss
+++ b/changedetectionio/static/styles/scss/styles.scss
@@ -20,6 +20,8 @@
 @use "parts/lister_extra";
 @use "parts/socket";
 @use "parts/visualselector";
+@use "parts/_minitabs";
+@use "parts/_notification";
 @use "parts/widgets";
 
 body {
@@ -335,6 +337,11 @@ a.pure-button-selected {
     overflow-wrap: break-word;
     max-width: 100%;
     box-sizing: border-box;
+    &.error {
+      > span {
+        color: var(--color-error) !important;
+      }
+    }
   }
 }
 
@@ -344,11 +351,6 @@ label {
  }  
 }
 
-#notification-customisation {
-  border: 1px solid var(--color-border-notification);
-  padding: 0.5rem;
-  border-radius: 5px;
-}
 
 #notification-error-log {
   border: 1px solid var(--color-border-notification);
diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css
index 2ddc03712dc..1089f8c7e4e 100644
--- a/changedetectionio/static/styles/styles.css
+++ b/changedetectionio/static/styles/styles.css
@@ -1 +1 @@
-:root{--color-white: #fff;--color-grey-50: #111;--color-grey-100: #262626;--color-grey-200: #333;--color-grey-300: #444;--color-grey-325: #555;--color-grey-350: #565d64;--color-grey-400: #666;--color-grey-500: #777;--color-grey-600: #999;--color-grey-700: #cbcbcb;--color-grey-750: #ddd;--color-grey-800: #e0e0e0;--color-grey-850: #eee;--color-grey-900: #f2f2f2;--color-black: #000;--color-dark-red: #a00;--color-light-red: #dd0000;--color-background-page: var(--color-grey-100);--color-background-gradient-first: #5ad8f7;--color-background-gradient-second: #2f50af;--color-background-gradient-third: #9150bf;--color-background: var(--color-white);--color-text: var(--color-grey-200);--color-link: #1b98f8;--color-menu-accent: #ed5900;--color-background-code: var(--color-grey-850);--color-error: var(--color-dark-red);--color-error-input: #ffebeb;--color-error-list: var(--color-light-red);--color-table-background: var(--color-background);--color-table-stripe: var(--color-grey-900);--color-text-tab: var(--color-white);--color-background-tab: rgba(255, 255, 255, 0.2);--color-background-tab-hover: rgba(255, 255, 255, 0.5);--color-text-tab-active: #222;--color-api-key: #0078e7;--color-background-button-primary: #0078e7;--color-background-button-green: #42dd53;--color-background-button-red: #dd4242;--color-background-button-success: rgb(28, 184, 65);--color-background-button-error: rgb(202, 60, 60);--color-text-button-error: var(--color-white);--color-background-button-warning: rgb(202, 60, 60);--color-text-button-warning: var(--color-white);--color-background-button-secondary: rgb(66, 184, 221);--color-background-button-cancel: rgb(200, 200, 200);--color-text-button: var(--color-white);--color-background-button-tag: rgb(99, 99, 99);--color-background-snapshot-age: #dfdfdf;--color-error-text-snapshot-age: var(--color-white);--color-error-background-snapshot-age: #ff0000;--color-background-button-tag-active: #9c9c9c;--color-text-messages: var(--color-white);--color-background-messages-message: rgba(255, 255, 255, .2);--color-background-messages-error: rgba(255, 1, 1, .5);--color-background-messages-notice: rgba(255, 255, 255, .5);--color-border-notification: #ccc;--color-background-checkbox-operations: rgba(0, 0, 0, 0.05);--color-warning: #ff3300;--color-border-warning: var(--color-warning);--color-text-legend: var(--color-white);--color-link-new-version: #e07171;--color-last-checked: #bbb;--color-text-footer: #444;--color-border-watch-table-cell: #eee;--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);--color-background-new-watch-form: rgba(0, 0, 0, 0.05);--color-background-new-watch-input: var(--color-white);--color-background-new-watch-input-transparent: rgba(255, 255, 255, 0.1);--color-text-new-watch-input: var(--color-text);--color-border-input: var(--color-grey-500);--color-shadow-input: var(--color-grey-400);--color-background-input: var(--color-white);--color-text-input: var(--color-text);--color-text-input-description: var(--color-grey-500);--color-text-input-placeholder: var(--color-grey-600);--color-background-table-thead: var(--color-grey-800);--color-border-table-cell: var(--color-grey-700);--color-text-menu-heading: var(--color-grey-350);--color-text-menu-link: var(--color-grey-500);--color-background-menu-link-hover: var(--color-grey-850);--color-text-menu-link-hover: var(--color-grey-300);--color-shadow-jump: var(--color-grey-500);--color-icon-github: var(--color-black);--color-icon-github-hover: var(--color-grey-300);--color-watch-table-error: var(--color-dark-red);--color-watch-table-row-text: var(--color-grey-100)}html[data-darkmode=true]{--color-link: #59bdfb;--color-text: var(--color-white);--color-background-gradient-first: #3f90a5;--color-background-gradient-second: #1e316c;--color-background-gradient-third: #4d2c64;--color-background-new-watch-input: var(--color-grey-100);--color-background-new-watch-input-transparent: var(--color-grey-100);--color-text-new-watch-input: var(--color-text);--color-background-table-thead: var(--color-grey-200);--color-table-background: var(--color-grey-300);--color-table-stripe: var(--color-grey-325);--color-background: var(--color-grey-300);--color-text-menu-heading: var(--color-grey-850);--color-text-menu-link: var(--color-grey-800);--color-border-table-cell: var(--color-grey-400);--color-text-tab-active: var(--color-text);--color-border-input: var(--color-grey-400);--color-shadow-input: var(--color-grey-50);--color-background-input: var(--color-grey-350);--color-text-input-description: var(--color-grey-600);--color-text-input-placeholder: var(--color-grey-600);--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);--color-background-code: var(--color-grey-200);--color-background-tab: rgba(0, 0, 0, 0.2);--color-background-tab-hover: rgba(0, 0, 0, 0.5);--color-background-snapshot-age: var(--color-grey-200);--color-shadow-jump: var(--color-grey-200);--color-icon-github: var(--color-white);--color-icon-github-hover: var(--color-grey-700);--color-watch-table-error: var(--color-light-red);--color-watch-table-row-text: var(--color-grey-800)}html[data-darkmode=true] .icon-spread{filter:hue-rotate(-10deg) brightness(1.5)}html[data-darkmode=true] .watch-table .title-col a[target=_blank]::after,html[data-darkmode=true] .watch-table .current-diff-url::after{filter:invert(0.5) hue-rotate(10deg) brightness(2)}html[data-darkmode=true] .watch-table .status-browsersteps{filter:invert(0.5) hue-rotate(10deg) brightness(1.5)}html[data-darkmode=true] .watch-table .watch-controls .state-off img{opacity:.3}html[data-darkmode=true] .watch-table .watch-controls .state-on img{opacity:1}html[data-darkmode=true] .watch-table .unviewed{color:#fff}html[data-darkmode=true] .watch-table .unviewed.error{color:var(--color-watch-table-error)}.arrow{border:solid #1b98f8;border-width:0 2px 2px 0;display:inline-block;padding:3px}.arrow.right{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}.arrow.left{transform:rotate(135deg);-webkit-transform:rotate(135deg)}.arrow.up,.arrow.asc{transform:rotate(-135deg);-webkit-transform:rotate(-135deg)}.arrow.down,.arrow.desc{transform:rotate(45deg);-webkit-transform:rotate(45deg)}#browser_steps th{display:none}#browser_steps li{list-style:decimal;padding:5px}#browser_steps li.browser-step-with-error{background-color:#ffd6d6;border-radius:4px}#browser_steps li:not(:first-child):hover{opacity:1}#browser_steps li .control{padding-left:5px;padding-right:5px}#browser_steps li .control a{font-size:70%}#browser_steps li.empty{padding:0px;opacity:.35}#browser_steps li.empty .control{display:none}#browser_steps li:hover{background:#eee}#browser_steps li>label{display:none}@media only screen and (min-width: 760px){#browser-steps .flex-wrapper{display:flex;flex-flow:row;height:70vh;font-size:80%}#browser-steps .flex-wrapper #browser-steps-ui{flex-grow:1;flex-shrink:1;flex-basis:0;background-color:#eee;border-radius:5px}#browser-steps-fieldlist{flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:400px;padding-left:1rem;overflow-y:scroll}#browsersteps-selector-wrapper{height:100% !important}}#browsersteps-selector-wrapper{width:100%;overflow-y:scroll;position:relative;height:80vh}#browsersteps-selector-wrapper>img{position:absolute;max-width:100%}#browsersteps-selector-wrapper>canvas{position:relative;max-width:100%}#browsersteps-selector-wrapper>canvas:hover{cursor:pointer}#browsersteps-selector-wrapper .loader{position:absolute;left:50%;top:50%;transform:translate(-50%, -50%);z-index:100;max-width:350px;text-align:center}#browsersteps-selector-wrapper .spinner,#browsersteps-selector-wrapper .spinner:after{width:80px;height:80px;font-size:3px}#browsersteps-selector-wrapper #browsersteps-click-start{color:var(--color-grey-400)}#browsersteps-selector-wrapper #browsersteps-click-start:hover{cursor:pointer}ul#requests-extra_proxies{list-style:none}ul#requests-extra_proxies li>label{display:none}ul#requests-extra_proxies table tr{display:table-row}ul#requests-extra_proxies table tr input[type=text]{width:100%}@media only screen and (min-width: 1024px){ul#requests-extra_proxies table tr{display:inline}}#request label[for=proxy]{display:inline-block}body.proxy-check-active #request .proxy-check-details{font-size:80%;color:#555;display:block;padding-left:2em;max-width:500px}body.proxy-check-active #request .proxy-timing{font-size:80%;padding-left:1rem;color:var(--color-link)}#recommended-proxy{display:grid;gap:2rem;padding-bottom:1em}@media(min-width: 991px){#recommended-proxy{grid-template-columns:repeat(2, 1fr)}}#recommended-proxy>div{border:1px #aaa solid;border-radius:4px;padding:1em}#extra-proxies-setting{border:1px solid var(--color-grey-800);border-radius:4px;margin:1em;padding:1em}ul#requests-extra_browsers{list-style:none}ul#requests-extra_browsers li>label{display:none}ul#requests-extra_browsers table tr{display:table-row}ul#requests-extra_browsers table tr input[type=text]{width:100%}@media only screen and (min-width: 1280px){ul#requests-extra_browsers table tr{display:inline}ul#requests-extra_browsers table tr input[type=text]{width:100%}}#extra-browsers-setting{border:1px solid var(--color-grey-800);border-radius:4px;margin:1em;padding:1em}.pagination-page-info{color:#fff;font-size:.85rem;text-transform:capitalize}.pagination.menu>*{display:inline-block}.pagination.menu li{display:inline-block}.pagination.menu a{padding:.65rem;margin:3px;border:none;background:#444;border-radius:2px;color:var(--color-text-button)}.pagination.menu a.disabled{display:none}.pagination.menu a.active{font-weight:bold;background:#888}.pagination.menu a:hover{background:#999}.spinner,.spinner:after{border-radius:50%;width:10px;height:10px}.spinner{margin:0px auto;font-size:3px;vertical-align:middle;display:inline-block;text-indent:-9999em;border-top:1.1em solid rgba(38,104,237,.2);border-right:1.1em solid rgba(38,104,237,.2);border-bottom:1.1em solid rgba(38,104,237,.2);border-left:1.1em solid #2668ed;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-animation:load8 1.1s infinite linear;animation:load8 1.1s infinite linear}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}#toggle-light-mode .icon-dark{display:none}html[data-darkmode=true] #toggle-light-mode .icon-light{display:none}html[data-darkmode=true] #toggle-light-mode .icon-dark{display:block}.pure-menu-link{padding:.5rem 1em;line-height:1.2rem}.pure-menu-item svg{height:1.2rem}.pure-menu-item *{vertical-align:middle}.pure-menu-item .github-link{height:1.8rem;display:block}.pure-menu-item .github-link svg{height:100%}.pure-menu-item .bi-heart:hover{cursor:pointer}#overlay{opacity:.95;position:fixed;width:350px;max-width:100%;height:100%;top:0;right:-350px;background-color:var(--color-table-stripe);z-index:2;transform:translateX(0);transition:transform .5s ease}#overlay.visible{transform:translateX(-100%)}#overlay .content{font-size:.875rem;padding:1rem;margin-top:5rem;max-width:400px;color:var(--color-watch-table-row-text)}#heartpath{transition:all ease .3s !important}#heartpath:hover{fill:red !important;transition:all ease .3s !important}.minitabs-wrapper{width:100%}.minitabs-wrapper>div[id]{padding:20px;border:1px solid #ccc;border-top:none}.minitabs-wrapper .minitabs-content{width:100%;display:flex}.minitabs-wrapper .minitabs-content>div{flex:1 1 auto;min-width:0;overflow:scroll}.minitabs-wrapper .minitabs{display:flex;border-bottom:1px solid #ccc}.minitabs-wrapper .minitab{flex:1;text-align:center;padding:12px 0;text-decoration:none;color:#333;background-color:#f1f1f1;border:1px solid #ccc;border-bottom:none;cursor:pointer;transition:background-color .3s}.minitabs-wrapper .minitab:hover{background-color:#ddd}.minitabs-wrapper .minitab.active{background-color:#fff;font-weight:bold}@media(min-width: 800px){body.preview-text-enabled #filters-and-triggers>div{display:flex;gap:20px;position:relative}}body.preview-text-enabled #edit-text-filter,body.preview-text-enabled #text-preview{flex:1;align-self:flex-start}body.preview-text-enabled #edit-text-filter #pro-tips{display:none}body.preview-text-enabled #text-preview{position:sticky;top:20px;padding-top:1rem;padding-bottom:1rem;display:block !important}body.preview-text-enabled #activate-text-preview{background-color:var(--color-grey-500)}body.preview-text-enabled .monospace-preview{background:var(--color-background-input);border:1px solid var(--color-grey-600);padding:1rem;color:var(--color-text-input);font-family:"Courier New",Courier,monospace;font-size:70%;word-break:break-word;white-space:pre-wrap}#activate-text-preview{right:0;position:absolute;z-index:3;box-shadow:1px 1px 4px var(--color-shadow-jump)}.watch-table{width:100%;font-size:80%}.watch-table tr{color:var(--color-watch-table-row-text)}.watch-table tr.unviewed{font-weight:bold}.watch-table td{white-space:nowrap}.watch-table td.title-col{word-break:break-all;white-space:normal}.watch-table td a.external::after{content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);margin:0 3px 0 5px}.watch-table th{white-space:nowrap}.watch-table th a{font-weight:normal}.watch-table th a.active{font-weight:bolder}.watch-table th a.inactive .arrow{display:none}.watch-table tr.checking-now td:first-child{position:relative}.watch-table tr.checking-now td:first-child::before{content:"";position:absolute;top:0;bottom:0;left:0;width:3px;background-color:#293eff}.watch-table tr.checking-now td.last-checked .spinner-wrapper{display:inline-block !important}.watch-table tr.checking-now td.last-checked .innertext{display:none !important}.watch-table tr.queued a.recheck{display:none !important}.watch-table tr.queued a.already-in-queue-button{display:inline-block !important}.watch-table tr.paused a.pause-toggle.state-on{display:inline !important}.watch-table tr.paused a.pause-toggle.state-off{display:none !important}.watch-table tr.notification_muted a.mute-toggle.state-on{display:inline !important}.watch-table tr.notification_muted a.mute-toggle.state-off{display:none !important}.watch-table tr.has-error{color:var(--color-watch-table-error)}.watch-table tr.has-error .error-text{display:block !important}.watch-table tr.single-history a.preview-link{display:inline-block !important}.watch-table tr.multiple-history a.history-link{display:inline-block !important}#watch-table-wrapper #post-list-buttons{text-align:right;padding:0px;margin:0px}#watch-table-wrapper #post-list-buttons li{display:inline-block}#watch-table-wrapper #post-list-buttons a{border-top-left-radius:initial;border-top-right-radius:initial;border-bottom-left-radius:5px;border-bottom-right-radius:5px}#watch-table-wrapper.has-error #post-list-buttons #post-list-with-errors{display:inline-block !important}#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-unread,#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-mark-views,#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-unread{display:inline-block !important}@media(max-width: 767px){.watch-table thead{display:block}.watch-table thead tr th{display:inline-block}}@media(max-width: 767px)and (max-width: 768px){.watch-table thead tr th .hide-on-mobile{display:none}}@media(max-width: 767px){.watch-table thead .empty-cell{display:none}.watch-table .last-checked{margin-left:calc(20px + .5rem)}.watch-table .last-checked>span{vertical-align:middle}.watch-table .last-changed{margin-left:calc(20px + .5rem)}.watch-table .last-checked::before{color:var(--color-text);content:"Last Checked "}.watch-table .last-changed::before{color:var(--color-text);content:"Last Changed "}.watch-table td.inline{display:inline-block}.watch-table .pure-table td,.watch-table .pure-table th{border:none}.watch-table td{border:none;border-bottom:1px solid var(--color-border-watch-table-cell);vertical-align:middle}.watch-table td:before{top:6px;left:6px;width:45%;padding-right:10px;white-space:nowrap}.watch-table.pure-table-striped tr{background-color:var(--color-table-background)}.watch-table.pure-table-striped tr:nth-child(2n-1){background-color:var(--color-table-stripe)}.watch-table.pure-table-striped tr:nth-child(2n-1) td{background-color:inherit}}@media(max-width: 767px){.watch-table tbody tr{padding-bottom:10px;padding-top:10px;display:grid;grid-template-columns:20px 1fr 100px;grid-template-rows:auto auto auto auto;gap:.5rem}.watch-table tbody tr .counter-i{display:none}.watch-table tbody tr td.checkbox-uuid{display:grid;place-items:center}.watch-table tbody tr>td{border-bottom:none}.watch-table tbody tr>td.title-col{grid-column:1/-1;grid-row:1}.watch-table tbody tr>td.title-col .watch-title{font-size:.92rem}.watch-table tbody tr>td.title-col .link-spread{display:none}.watch-table tbody tr>td.last-checked{grid-column:1/-1;grid-row:2}.watch-table tbody tr>td.last-changed{grid-column:1/-1;grid-row:3}.watch-table tbody tr>td.checkbox-uuid{grid-column:1;grid-row:4}.watch-table tbody tr>td.buttons{grid-column:2;grid-row:4;display:flex;align-items:center;justify-content:flex-start}.watch-table tbody tr>td.watch-controls{grid-column:3;grid-row:4;display:grid;place-items:center}.watch-table tbody tr>td.watch-controls a img{padding:10px}.pure-table td{padding:3px !important}}ul#conditions_match_logic{list-style:none}ul#conditions_match_logic input,ul#conditions_match_logic label,ul#conditions_match_logic li{display:inline-block}ul#conditions_match_logic li{padding-right:1em}.fieldlist_formfields{width:100%;background-color:var(--color-background, #fff);border-radius:4px;border:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-header{display:flex;background-color:var(--color-background-table-thead, #e0e0e0);font-weight:bold;border-bottom:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-header-cell{flex:1;padding:.5em 1em;text-align:left}.fieldlist_formfields .fieldlist-header-cell:last-child{flex:0 0 120px}.fieldlist_formfields .fieldlist-body{display:flex;flex-direction:column}.fieldlist_formfields .fieldlist-row{display:flex;border-bottom:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-row:last-child{border-bottom:none}.fieldlist_formfields .fieldlist-row:nth-child(2n-1){background-color:var(--color-table-stripe, #f2f2f2)}.fieldlist_formfields .fieldlist-row.error-row{background-color:var(--color-error-input, #ffdddd)}.fieldlist_formfields .fieldlist-cell{flex:1;padding:.5em 1em;display:flex;flex-direction:column;justify-content:center}.fieldlist_formfields .fieldlist-cell input,.fieldlist_formfields .fieldlist-cell select{width:100%}.fieldlist_formfields .fieldlist-cell.fieldlist-actions{flex:0 0 120px;display:flex;flex-direction:row;align-items:center;gap:4px}.fieldlist_formfields ul.errors{margin-top:.5em;margin-bottom:0;padding:.5em;background-color:var(--color-error-background-snapshot-age, #ffdddd);border-radius:4px;list-style-position:inside}@media only screen and (max-width: 760px){.fieldlist_formfields .fieldlist-header,.fieldlist_formfields .fieldlist-row{flex-direction:column}.fieldlist_formfields .fieldlist-header-cell{display:none}.fieldlist_formfields .fieldlist-row{padding:.5em 0;border-bottom:2px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-cell{padding:.25em .5em}.fieldlist_formfields .fieldlist-cell.fieldlist-actions{flex:1;justify-content:flex-start;padding-top:.5em}.fieldlist_formfields .fieldlist-cell:not(:last-child){margin-bottom:.5em}.fieldlist_formfields .fieldlist-cell::before{content:attr(data-label);font-weight:bold;margin-bottom:.25em}}.fieldlist_formfields .addRuleRow,.fieldlist_formfields .removeRuleRow,.fieldlist_formfields .verifyRuleRow{cursor:pointer;border:none;padding:4px 8px;border-radius:3px;font-weight:bold;background-color:#aaa;color:var(--color-foreground-text, #fff)}.fieldlist_formfields .addRuleRow:hover,.fieldlist_formfields .removeRuleRow:hover,.fieldlist_formfields .verifyRuleRow:hover{background-color:#999}.watch-table.favicon-not-enabled tr .favicon{display:none}.watch-table tr td.inline.title-col .flex-wrapper{display:flex;align-items:center;gap:4px}.watch-table td,.watch-table th{vertical-align:middle}.watch-table tr.has-favicon.unviewed img.favicon{opacity:1 !important}.watch-table .status-icons{white-space:nowrap;display:flex;align-items:center;gap:4px}.watch-table .status-icons>*{vertical-align:middle}.title-col{padding:10px}.title-wrapper{display:flex;align-items:center;gap:10px}.title-col-inner{display:inline-block;vertical-align:middle}.watch-table img.favicon{vertical-align:middle;max-width:25px;max-height:25px;height:25px;padding-right:4px}body.checking-now #checking-now-fixed-tab{display:block !important}#checking-now-fixed-tab{background:#ccc;border-radius:5px;bottom:0;color:var(--color-text);display:none;font-size:.8rem;left:0;padding:5px;position:fixed}#selector-wrapper{height:100%;text-align:center;max-height:70vh;overflow-y:scroll;position:relative}#selector-wrapper>img{position:absolute;z-index:4;max-width:100%}#selector-wrapper>canvas{position:relative;z-index:5;max-width:100%}#selector-wrapper>canvas:hover{cursor:pointer}#selector-current-xpath{font-size:80%}.ternary-radio-group{display:flex;gap:0;border:1px solid var(--color-grey-750);border-radius:4px;overflow:hidden;width:fit-content;background:var(--color-background)}.ternary-radio-group .ternary-radio-option{position:relative;cursor:pointer;margin:0;display:flex;align-items:center}.ternary-radio-group .ternary-radio-option input[type=radio]{position:absolute;opacity:0;width:0;height:0}.ternary-radio-group .ternary-radio-option .ternary-radio-label{padding:8px 16px;background:var(--color-grey-900);border:none;border-right:1px solid var(--color-grey-750);font-size:13px;font-weight:500;color:var(--color-text);transition:all .2s ease;cursor:pointer;display:block;text-align:center}.ternary-radio-group .ternary-radio-option:last-child .ternary-radio-label{border-right:none}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label{background:var(--color-link);color:var(--color-text-button);font-weight:600}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label.ternary-default{background:var(--color-grey-600);color:var(--color-text-button)}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover{background:#1a7bc4}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover.ternary-default{background:var(--color-grey-500)}.ternary-radio-group .ternary-radio-option:hover .ternary-radio-label{background:var(--color-grey-800)}@media(max-width: 480px){.ternary-radio-group{width:100%}.ternary-radio-group .ternary-radio-label{flex:1;min-width:auto}}input[type=radio].pure-radio:checked+label,input[type=radio].pure-radio:checked{background:var(--color-link);color:var(--color-text-button)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option .ternary-radio-label{background:var(--color-grey-350)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option:hover .ternary-radio-label{background:var(--color-grey-400)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label{background:var(--color-link);color:var(--color-text-button)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label.ternary-default{background:var(--color-grey-600)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover{background:#1a7bc4}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover.ternary-default{background:var(--color-grey-500)}body{color:var(--color-text);background:var(--color-background-page);font-family:Helvetica Neue,Helvetica,Lucida Grande,Arial,Ubuntu,Cantarell,Fira Sans,sans-serif}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.status-icon{display:inline-block;height:1rem;vertical-align:middle}.pure-table-even{background:var(--color-background)}a{text-decoration:none;color:var(--color-link)}a.github-link{color:var(--color-icon-github);margin:0 1rem 0 .5rem}a.github-link svg{fill:currentColor}a.github-link:hover{color:var(--color-icon-github-hover)}#search-q{opacity:0;-webkit-transition:all .9s ease;-moz-transition:all .9s ease;transition:all .9s ease;width:0;display:none}#search-q.expanded{width:auto;display:inline-block;opacity:1}#search-result-info{color:#fff}button.toggle-button{vertical-align:middle;background:rgba(0,0,0,0);border:none;cursor:pointer;color:var(--color-icon-github)}button.toggle-button:hover{color:var(--color-icon-github-hover)}button.toggle-button svg{fill:currentColor}button.toggle-button .icon-light{display:block}.pure-menu-horizontal{background:var(--color-background);padding:5px;display:flex;justify-content:space-between;align-items:center}#pure-menu-horizontal-spinner{height:3px;background:linear-gradient(-75deg, #ff6000, #ff8f00, #ffdd00, #ed0000);background-size:400% 400%;width:100%;animation:gradient 200s ease infinite}body.spinner-active #pure-menu-horizontal-spinner{animation:gradient 1s ease infinite}@keyframes gradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}.pure-menu-heading{color:var(--color-text-menu-heading)}.pure-menu-link{color:var(--color-text-menu-link)}.pure-menu-link:hover{background-color:var(--color-background-menu-link-hover);color:var(--color-text-menu-link-hover)}.tab-pane-inner{scroll-margin-top:200px}section.content{padding-top:100px;padding-bottom:1em;flex-direction:column;display:flex;align-items:center;justify-content:center}code{background:var(--color-background-code);color:var(--color-text)}.inline-tag,.restock-label,.tracking-ldjson-price-data,.watch-tag-list{white-space:nowrap;border-radius:5px;padding:2px 5px;margin-right:4px}.watch-tag-list{color:var(--color-white);background:var(--color-text-watch-tag-list)}@media(min-width: 768px){.box{margin:0 1em !important}}.box{max-width:100%;margin:0 .3em;flex-direction:column;display:flex;justify-content:center}body:after{content:"";background:linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%)}body:after,body:before{display:block;height:650px;position:absolute;top:0;left:0;width:100%;z-index:-1}body::after{opacity:.91}body::before{content:""}body:after,body:before{-webkit-clip-path:polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%);clip-path:polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%)}.button-small{font-size:85%}.button-xsmall{font-size:70%}.fetch-error{padding-top:1em;font-size:80%;max-width:400px;display:block}.pure-button-primary,a.pure-button-primary,.pure-button-selected,a.pure-button-selected{background-color:var(--color-background-button-primary)}.button-secondary{color:var(--color-text-button);border-radius:4px;text-shadow:0 1px 1px rgba(0,0,0,.2)}.button-success{background:var(--color-background-button-success)}.button-tag{background:var(--color-background-button-tag);color:var(--color-text-button);font-size:65%;border-bottom-left-radius:initial;border-bottom-right-radius:initial;margin-right:4px}.button-tag.active{background:var(--color-background-button-tag-active);font-weight:bold}.button-error{background:var(--color-background-button-error);color:var(--color-text-button-error)}.button-warning{background:var(--color-background-button-warning);color:var(--color-text-button-warning)}.button-secondary{background:var(--color-background-button-secondary)}.button-cancel{background:var(--color-background-button-cancel)}.messages li{list-style:none;padding:1em;border-radius:10px;color:var(--color-text-messages);font-weight:bold}.messages li.message{background:var(--color-background-messages-message)}.messages li.error{background:var(--color-background-messages-error)}.messages li.notice{background:var(--color-background-messages-notice)}.messages.with-share-link>*:hover{cursor:pointer}.notifications-wrapper{padding-top:.5rem}.notifications-wrapper #notification-test-log{padding-top:1rem;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;max-width:100%;box-sizing:border-box}label:hover{cursor:pointer}#notification-customisation{border:1px solid var(--color-border-notification);padding:.5rem;border-radius:5px}#notification-error-log{border:1px solid var(--color-border-notification);padding:1rem;border-radius:5px;overflow-wrap:break-word}#token-table.pure-table td,#token-table.pure-table th{font-size:80%}.pure-form input[type=text].transparent-field{background-color:var(--color-background-new-watch-input-transparent) !important;color:var(--color-white) !important;border:1px solid hsla(0,0%,100%,.2) !important;box-shadow:none !important;-webkit-box-shadow:none !important}.pure-form input[type=text].transparent-field::placeholder{opacity:.5;color:hsla(0,0%,100%,.7);font-weight:lighter}#new-watch-form{background:var(--color-background-new-watch-form);padding:1em;border-radius:10px;margin-bottom:1em;max-width:100%}#new-watch-form #url::placeholder{font-weight:bold}#new-watch-form input{display:inline-block;margin-bottom:5px}#new-watch-form input:not(.pure-button){background-color:var(--color-background-new-watch-input);color:var(--color-text-new-watch-input)}#new-watch-form .label{display:none}#new-watch-form legend{color:var(--color-text-legend);font-weight:bold}@media only screen and (min-width: 760px){#new-watch-form #watch-add-wrapper-zone{display:flex;gap:.3rem;flex-direction:row;min-width:70vw}}#new-watch-form #watch-add-wrapper-zone>span{flex-grow:0}#new-watch-form #watch-add-wrapper-zone>span input{width:100%;padding-right:1em}#new-watch-form #watch-add-wrapper-zone>span:first-child{flex-grow:1}@media only screen and (max-width: 760px){#new-watch-form #watch-add-wrapper-zone #url{width:100%}}#new-watch-form #watch-group-tag{font-size:.9rem;padding:.3rem;display:flex;align-items:center;gap:.5rem;color:var(--color-white)}#new-watch-form #watch-group-tag label,#new-watch-form #watch-group-tag input{margin:0}#new-watch-form #watch-group-tag input{flex:1}#diff-col{padding-left:40px}#diff-jump{position:fixed;left:0px;top:120px;background:var(--color-background);padding:10px;border-top-right-radius:5px;border-bottom-right-radius:5px;box-shadow:1px 1px 4px var(--color-shadow-jump)}#diff-jump a{color:var(--color-link);cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-o-user-select:none}footer{padding:10px;background:var(--color-background);color:var(--color-text-footer);text-align:center}#feed-icon{vertical-align:middle}.sticky-tab{position:absolute;top:60px;font-size:65%;background:var(--color-background);padding:10px}.sticky-tab#left-sticky{left:0;position:fixed;border-top-right-radius:5px;border-bottom-right-radius:5px;box-shadow:1px 1px 4px var(--color-shadow-jump)}.sticky-tab#right-sticky{right:0px}.sticky-tab#hosted-sticky{right:0px;top:100px;font-weight:bold}#new-version-text a{color:var(--color-link-new-version)}.watch-controls{color:#f8321b}.watch-controls .state-on img{opacity:.8}.watch-controls img{opacity:.2}.watch-controls img:hover{transition:opacity .3s;opacity:.8}.monospaced-textarea textarea{width:100%;font-family:monospace;white-space:pre;overflow-wrap:normal;overflow-x:auto}.pure-form fieldset{padding-top:0px}.pure-form fieldset ul{padding-bottom:0px;margin-bottom:0px}.pure-form .pure-control-group,.pure-form .pure-group,.pure-form .pure-controls{padding-bottom:1em}.pure-form .pure-control-group div,.pure-form .pure-group div,.pure-form .pure-controls div{margin:0px}.pure-form .pure-control-group .checkbox>*,.pure-form .pure-group .checkbox>*,.pure-form .pure-controls .checkbox>*{display:inline;vertical-align:middle}.pure-form .pure-control-group .checkbox>label,.pure-form .pure-group .checkbox>label,.pure-form .pure-controls .checkbox>label{padding-left:5px}.pure-form .pure-control-group legend,.pure-form .pure-group legend,.pure-form .pure-controls legend{color:var(--color-text-legend)}.pure-form .error input{background-color:var(--color-error-input)}.pure-form ul.errors{padding:.5em .6em;border:1px solid var(--color-error-list);border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form ul.errors li{margin-left:1em;color:var(--color-error-list)}.pure-form label{font-weight:bold}.pure-form textarea{width:100%}.pure-form .inline-radio ul{margin:0px;list-style:none}.pure-form .inline-radio ul li{display:flex;align-items:center;gap:1em}@media only screen and (max-width: 760px),(min-device-width: 768px)and (max-device-width: 1024px){.edit-form{padding:.5em;margin:0}#nav-menu{overflow-x:scroll}}@media only screen and (max-width: 760px),(min-device-width: 768px)and (max-device-width: 800px){div.sticky-tab#hosted-sticky{top:60px;left:0px;right:auto}section.content{padding-top:110px}div.tabs.collapsable ul li{display:block;border-radius:0px;margin-right:0px}input[type=text]{width:100%}}.pure-table{border-color:var(--color-border-table-cell)}.pure-table thead{background-color:var(--color-background-table-thead);color:var(--color-text);border-bottom:1px solid var(--color-background-table-thead)}.pure-table td,.pure-table th{border-left-color:var(--color-border-table-cell)}.pure-table-striped tr:nth-child(2n-1) td{background-color:var(--color-table-stripe)}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{border:var(--color-border-input);box-shadow:inset 0 1px 3px var(--color-shadow-input);background-color:var(--color-background-input);color:var(--color-text-input)}.pure-form input[type=color]:active,.pure-form input[type=date]:active,.pure-form input[type=datetime-local]:active,.pure-form input[type=datetime]:active,.pure-form input[type=email]:active,.pure-form input[type=month]:active,.pure-form input[type=number]:active,.pure-form input[type=password]:active,.pure-form input[type=search]:active,.pure-form input[type=tel]:active,.pure-form input[type=text]:active,.pure-form input[type=time]:active,.pure-form input[type=url]:active,.pure-form input[type=week]:active,.pure-form select:active,.pure-form textarea:active{background-color:var(--color-background-input)}input::placeholder,textarea::placeholder{color:var(--color-text-input-placeholder)}.m-d{min-width:100%}@media only screen and (min-width: 761px){.m-d{min-width:80%}}.tabs ul{margin:0px;padding:0px;display:block}.tabs ul li{margin-right:3px;display:inline-block;color:var(--color-text-tab);border-top-left-radius:5px;border-top-right-radius:5px;background-color:var(--color-background-tab)}.tabs ul li:not(.active):hover{background-color:var(--color-background-tab-hover)}.tabs ul li.active,.tabs ul li :target{background-color:var(--color-background)}.tabs ul li.active a,.tabs ul li :target a{color:var(--color-text-tab-active);font-weight:bold}.tabs ul li a{display:block;padding:.8em;color:var(--color-text-tab)}.pure-form-stacked>div:first-child{display:block}.login-form .inner{background:var(--color-background);padding:20px;border-radius:5px}.tab-pane-inner{padding:0px}.tab-pane-inner:not(:target){display:none}.tab-pane-inner:target{display:block}.beta-logo{height:50px;right:-3px;top:-3px;position:absolute}#selector-header{padding-bottom:1em}body.full-width .edit-form{width:95%}.edit-form{min-width:70%;max-width:95%}.edit-form .box-wrap{position:relative}.edit-form .inner{background:var(--color-background);padding:20px}.edit-form #actions{display:block;background:var(--color-background)}.edit-form #actions .pure-control-group{display:flex;gap:.625em;flex-wrap:wrap}.edit-form .pure-form-message-inline{padding-left:0;color:var(--color-text-input-description)}.edit-form .pure-form-message-inline code{font-size:.875em}.border-fieldset{border:1px solid #ccc;padding:1rem;border-radius:5px;margin-bottom:1rem}.border-fieldset h3{margin-top:0}.border-fieldset fieldset:last-of-type{padding-bottom:0}.border-fieldset fieldset:last-of-type .pure-control-group{padding-bottom:0}ul{padding-left:1em;padding-top:0px;margin-top:4px}.time-check-widget tr{display:inline}.time-check-widget tr input[type=number]{width:5em}@media only screen and (max-width: 760px){.time-check-widget tbody{display:grid;grid-template-columns:auto 1fr auto 1fr;gap:.625em .3125em;align-items:center}.time-check-widget tr{display:contents}.time-check-widget tr th{text-align:right;padding-right:5px}.time-check-widget tr input[type=number]{width:100%;max-width:5em}}#webdriver_delay{width:5em}#api-key:hover{cursor:pointer}#api-key-copy{color:var(--color-api-key)}.button-green{background-color:var(--color-background-button-green)}.button-red{background-color:var(--color-background-button-red)}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.snapshot-age{padding:4px;margin:.5rem 0;background-color:var(--color-background-snapshot-age);border-radius:3px;font-weight:bold;margin-bottom:4px}.snapshot-age.error{background-color:var(--color-error-background-snapshot-age);color:var(--color-error-text-snapshot-age)}#checkbox-operations{background:var(--color-background-checkbox-operations);padding:1em;border-radius:10px;margin-bottom:1em;display:none}#checkbox-operations button{margin-bottom:3px;margin-top:3px;display:inline-flex;align-items:center}.checkbox-uuid>*{vertical-align:middle}.inline-warning{border:1px solid var(--color-border-warning);padding:.5rem;border-radius:5px;color:var(--color-warning)}.inline-warning>span{display:inline-block;vertical-align:middle}.inline-warning img.inline-warning-icon{display:inline;height:26px;vertical-align:middle}.tracking-ldjson-price-data{background-color:var(--color-background-button-green);color:#000;opacity:.6}.ldjson-price-track-offer{font-weight:bold;font-style:italic}.ldjson-price-track-offer a.pure-button{border-radius:3px;padding:3px;background-color:var(--color-background-button-green)}.price-follow-tag-icon{display:inline-block;height:.8rem;vertical-align:middle}#quick-watch-processor-type ul#processor{color:#fff;padding-left:0px}#quick-watch-processor-type ul#processor li{list-style:none;font-size:.9rem;display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.5rem;margin-bottom:.5rem}#quick-watch-processor-type label,#quick-watch-processor-type input{padding:0;margin:0}.restock-label.in-stock{background-color:var(--color-background-button-green);color:#fff}.restock-label.not-in-stock{background-color:var(--color-background-button-cancel);color:#777}.restock-label.error{background-color:var(--color-background-button-error);color:#fff;opacity:.7}.restock-label svg{vertical-align:middle}#chrome-extension-link{padding:9px;border:1px solid var(--color-grey-800);border-radius:10px;vertical-align:middle}#chrome-extension-link img{height:21px;padding:2px;vertical-align:middle}#realtime-conn-error{position:fixed;bottom:0;left:0;background:var(--color-warning);padding:10px;font-size:.8rem;color:#fff;opacity:.8}
+:root{--color-white: #fff;--color-grey-50: #111;--color-grey-100: #262626;--color-grey-200: #333;--color-grey-300: #444;--color-grey-325: #555;--color-grey-350: #565d64;--color-grey-400: #666;--color-grey-500: #777;--color-grey-600: #999;--color-grey-700: #cbcbcb;--color-grey-750: #ddd;--color-grey-800: #e0e0e0;--color-grey-850: #eee;--color-grey-900: #f2f2f2;--color-black: #000;--color-dark-red: #a00;--color-light-red: #dd0000;--color-background-page: var(--color-grey-100);--color-background-gradient-first: #5ad8f7;--color-background-gradient-second: #2f50af;--color-background-gradient-third: #9150bf;--color-background: var(--color-white);--color-text: var(--color-grey-200);--color-link: #1b98f8;--color-menu-accent: #ed5900;--color-background-code: var(--color-grey-850);--color-error: var(--color-dark-red);--color-error-input: #ffebeb;--color-error-list: var(--color-light-red);--color-table-background: var(--color-background);--color-table-stripe: var(--color-grey-900);--color-text-tab: var(--color-white);--color-background-tab: rgba(255, 255, 255, 0.2);--color-background-tab-hover: rgba(255, 255, 255, 0.5);--color-text-tab-active: #222;--color-api-key: #0078e7;--color-background-button-primary: #0078e7;--color-background-button-green: #42dd53;--color-background-button-red: #dd4242;--color-background-button-success: rgb(28, 184, 65);--color-background-button-error: rgb(202, 60, 60);--color-text-button-error: var(--color-white);--color-background-button-warning: rgb(202, 60, 60);--color-text-button-warning: var(--color-white);--color-background-button-secondary: rgb(66, 184, 221);--color-background-button-cancel: rgb(200, 200, 200);--color-text-button: var(--color-white);--color-background-button-tag: rgb(99, 99, 99);--color-background-snapshot-age: #dfdfdf;--color-error-text-snapshot-age: var(--color-white);--color-error-background-snapshot-age: #ff0000;--color-background-button-tag-active: #9c9c9c;--color-text-messages: var(--color-white);--color-background-messages-message: rgba(255, 255, 255, .2);--color-background-messages-error: rgba(255, 1, 1, .5);--color-background-messages-notice: rgba(255, 255, 255, .5);--color-border-notification: #ccc;--color-background-checkbox-operations: rgba(0, 0, 0, 0.05);--color-warning: #ff3300;--color-border-warning: var(--color-warning);--color-text-legend: var(--color-white);--color-link-new-version: #e07171;--color-last-checked: #bbb;--color-text-footer: #444;--color-border-watch-table-cell: #eee;--color-text-watch-tag-list: rgba(231, 0, 105, 0.4);--color-background-new-watch-form: rgba(0, 0, 0, 0.05);--color-background-new-watch-input: var(--color-white);--color-background-new-watch-input-transparent: rgba(255, 255, 255, 0.1);--color-text-new-watch-input: var(--color-text);--color-border-input: var(--color-grey-500);--color-shadow-input: var(--color-grey-400);--color-background-input: var(--color-white);--color-text-input: var(--color-text);--color-text-input-description: var(--color-grey-500);--color-text-input-placeholder: var(--color-grey-600);--color-background-table-thead: var(--color-grey-800);--color-border-table-cell: var(--color-grey-700);--color-text-menu-heading: var(--color-grey-350);--color-text-menu-link: var(--color-grey-500);--color-background-menu-link-hover: var(--color-grey-850);--color-text-menu-link-hover: var(--color-grey-300);--color-shadow-jump: var(--color-grey-500);--color-icon-github: var(--color-black);--color-icon-github-hover: var(--color-grey-300);--color-watch-table-error: var(--color-dark-red);--color-watch-table-row-text: var(--color-grey-100)}html[data-darkmode=true]{--color-link: #59bdfb;--color-text: var(--color-white);--color-background-gradient-first: #3f90a5;--color-background-gradient-second: #1e316c;--color-background-gradient-third: #4d2c64;--color-background-new-watch-input: var(--color-grey-100);--color-background-new-watch-input-transparent: var(--color-grey-100);--color-text-new-watch-input: var(--color-text);--color-background-table-thead: var(--color-grey-200);--color-table-background: var(--color-grey-300);--color-table-stripe: var(--color-grey-325);--color-background: var(--color-grey-300);--color-text-menu-heading: var(--color-grey-850);--color-text-menu-link: var(--color-grey-800);--color-border-table-cell: var(--color-grey-400);--color-text-tab-active: var(--color-text);--color-border-input: var(--color-grey-400);--color-shadow-input: var(--color-grey-50);--color-background-input: var(--color-grey-350);--color-text-input-description: var(--color-grey-600);--color-text-input-placeholder: var(--color-grey-600);--color-text-watch-tag-list: rgba(250, 62, 146, 0.4);--color-background-code: var(--color-grey-200);--color-background-tab: rgba(0, 0, 0, 0.2);--color-background-tab-hover: rgba(0, 0, 0, 0.5);--color-background-snapshot-age: var(--color-grey-200);--color-shadow-jump: var(--color-grey-200);--color-icon-github: var(--color-white);--color-icon-github-hover: var(--color-grey-700);--color-watch-table-error: var(--color-light-red);--color-watch-table-row-text: var(--color-grey-800)}html[data-darkmode=true] .icon-spread{filter:hue-rotate(-10deg) brightness(1.5)}html[data-darkmode=true] .watch-table .title-col a[target=_blank]::after,html[data-darkmode=true] .watch-table .current-diff-url::after{filter:invert(0.5) hue-rotate(10deg) brightness(2)}html[data-darkmode=true] .watch-table .status-browsersteps{filter:invert(0.5) hue-rotate(10deg) brightness(1.5)}html[data-darkmode=true] .watch-table .watch-controls .state-off img{opacity:.3}html[data-darkmode=true] .watch-table .watch-controls .state-on img{opacity:1}html[data-darkmode=true] .watch-table .unviewed{color:#fff}html[data-darkmode=true] .watch-table .unviewed.error{color:var(--color-watch-table-error)}.arrow{border:solid #1b98f8;border-width:0 2px 2px 0;display:inline-block;padding:3px}.arrow.right{transform:rotate(-45deg);-webkit-transform:rotate(-45deg)}.arrow.left{transform:rotate(135deg);-webkit-transform:rotate(135deg)}.arrow.up,.arrow.asc{transform:rotate(-135deg);-webkit-transform:rotate(-135deg)}.arrow.down,.arrow.desc{transform:rotate(45deg);-webkit-transform:rotate(45deg)}#browser_steps th{display:none}#browser_steps li{list-style:decimal;padding:5px}#browser_steps li.browser-step-with-error{background-color:#ffd6d6;border-radius:4px}#browser_steps li:not(:first-child):hover{opacity:1}#browser_steps li .control{padding-left:5px;padding-right:5px}#browser_steps li .control a{font-size:70%}#browser_steps li.empty{padding:0px;opacity:.35}#browser_steps li.empty .control{display:none}#browser_steps li:hover{background:#eee}#browser_steps li>label{display:none}@media only screen and (min-width: 760px){#browser-steps .flex-wrapper{display:flex;flex-flow:row;height:70vh;font-size:80%}#browser-steps .flex-wrapper #browser-steps-ui{flex-grow:1;flex-shrink:1;flex-basis:0;background-color:#eee;border-radius:5px}#browser-steps-fieldlist{flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:400px;padding-left:1rem;overflow-y:scroll}#browsersteps-selector-wrapper{height:100% !important}}#browsersteps-selector-wrapper{width:100%;overflow-y:scroll;position:relative;height:80vh}#browsersteps-selector-wrapper>img{position:absolute;max-width:100%}#browsersteps-selector-wrapper>canvas{position:relative;max-width:100%}#browsersteps-selector-wrapper>canvas:hover{cursor:pointer}#browsersteps-selector-wrapper .loader{position:absolute;left:50%;top:50%;transform:translate(-50%, -50%);z-index:100;max-width:350px;text-align:center}#browsersteps-selector-wrapper .spinner,#browsersteps-selector-wrapper .spinner:after{width:80px;height:80px;font-size:3px}#browsersteps-selector-wrapper #browsersteps-click-start{color:var(--color-grey-400)}#browsersteps-selector-wrapper #browsersteps-click-start:hover{cursor:pointer}ul#requests-extra_proxies{list-style:none}ul#requests-extra_proxies li>label{display:none}ul#requests-extra_proxies table tr{display:table-row}ul#requests-extra_proxies table tr input[type=text]{width:100%}@media only screen and (min-width: 1024px){ul#requests-extra_proxies table tr{display:inline}}#request label[for=proxy]{display:inline-block}body.proxy-check-active #request .proxy-check-details{font-size:80%;color:#555;display:block;padding-left:2em;max-width:500px}body.proxy-check-active #request .proxy-timing{font-size:80%;padding-left:1rem;color:var(--color-link)}#recommended-proxy{display:grid;gap:2rem;padding-bottom:1em}@media(min-width: 991px){#recommended-proxy{grid-template-columns:repeat(2, 1fr)}}#recommended-proxy>div{border:1px #aaa solid;border-radius:4px;padding:1em}#extra-proxies-setting{border:1px solid var(--color-grey-800);border-radius:4px;margin:1em;padding:1em}ul#requests-extra_browsers{list-style:none}ul#requests-extra_browsers li>label{display:none}ul#requests-extra_browsers table tr{display:table-row}ul#requests-extra_browsers table tr input[type=text]{width:100%}@media only screen and (min-width: 1280px){ul#requests-extra_browsers table tr{display:inline}ul#requests-extra_browsers table tr input[type=text]{width:100%}}#extra-browsers-setting{border:1px solid var(--color-grey-800);border-radius:4px;margin:1em;padding:1em}.pagination-page-info{color:#fff;font-size:.85rem;text-transform:capitalize}.pagination.menu>*{display:inline-block}.pagination.menu li{display:inline-block}.pagination.menu a{padding:.65rem;margin:3px;border:none;background:#444;border-radius:2px;color:var(--color-text-button)}.pagination.menu a.disabled{display:none}.pagination.menu a.active{font-weight:bold;background:#888}.pagination.menu a:hover{background:#999}.spinner,.spinner:after{border-radius:50%;width:10px;height:10px}.spinner{margin:0px auto;font-size:3px;vertical-align:middle;display:inline-block;text-indent:-9999em;border-top:1.1em solid rgba(38,104,237,.2);border-right:1.1em solid rgba(38,104,237,.2);border-bottom:1.1em solid rgba(38,104,237,.2);border-left:1.1em solid #2668ed;-webkit-transform:translateZ(0);-ms-transform:translateZ(0);transform:translateZ(0);-webkit-animation:load8 1.1s infinite linear;animation:load8 1.1s infinite linear}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}#toggle-light-mode .icon-dark{display:none}html[data-darkmode=true] #toggle-light-mode .icon-light{display:none}html[data-darkmode=true] #toggle-light-mode .icon-dark{display:block}html[data-darkmode=true] .minitabs-content>div{background-color:rgba(249,249,249,.13) !important}.pure-menu-link{padding:.5rem 1em;line-height:1.2rem}.pure-menu-item svg{height:1.2rem}.pure-menu-item *{vertical-align:middle}.pure-menu-item .github-link{height:1.8rem;display:block}.pure-menu-item .github-link svg{height:100%}.pure-menu-item .bi-heart:hover{cursor:pointer}#overlay{opacity:.95;position:fixed;width:350px;max-width:100%;height:100%;top:0;right:-350px;background-color:var(--color-table-stripe);z-index:2;transform:translateX(0);transition:transform .5s ease}#overlay.visible{transform:translateX(-100%)}#overlay .content{font-size:.875rem;padding:1rem;margin-top:5rem;max-width:400px;color:var(--color-watch-table-row-text)}#heartpath{transition:all ease .3s !important}#heartpath:hover{fill:red !important;transition:all ease .3s !important}@media(min-width: 800px){body.preview-text-enabled #filters-and-triggers>div{display:flex;gap:20px;position:relative}}body.preview-text-enabled #edit-text-filter,body.preview-text-enabled #text-preview{flex:1;align-self:flex-start}body.preview-text-enabled #edit-text-filter #pro-tips{display:none}body.preview-text-enabled #text-preview{position:sticky;top:20px;padding-top:1rem;padding-bottom:1rem;display:block !important}body.preview-text-enabled #activate-text-preview{background-color:var(--color-grey-500)}#activate-text-preview{right:0;position:absolute;z-index:3;box-shadow:1px 1px 4px var(--color-shadow-jump)}#filter-preview-minitabs .minitabs-content>div{overflow:scroll}.watch-table{width:100%;font-size:80%}.watch-table tr{color:var(--color-watch-table-row-text)}.watch-table tr.unviewed{font-weight:bold}.watch-table td{white-space:nowrap}.watch-table td.title-col{word-break:break-all;white-space:normal}.watch-table td a.external::after{content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==);margin:0 3px 0 5px}.watch-table th{white-space:nowrap}.watch-table th a{font-weight:normal}.watch-table th a.active{font-weight:bolder}.watch-table th a.inactive .arrow{display:none}.watch-table tr.checking-now td:first-child{position:relative}.watch-table tr.checking-now td:first-child::before{content:"";position:absolute;top:0;bottom:0;left:0;width:3px;background-color:#293eff}.watch-table tr.checking-now td.last-checked .spinner-wrapper{display:inline-block !important}.watch-table tr.checking-now td.last-checked .innertext{display:none !important}.watch-table tr.queued a.recheck{display:none !important}.watch-table tr.queued a.already-in-queue-button{display:inline-block !important}.watch-table tr.paused a.pause-toggle.state-on{display:inline !important}.watch-table tr.paused a.pause-toggle.state-off{display:none !important}.watch-table tr.notification_muted a.mute-toggle.state-on{display:inline !important}.watch-table tr.notification_muted a.mute-toggle.state-off{display:none !important}.watch-table tr.has-error{color:var(--color-watch-table-error)}.watch-table tr.has-error .error-text{display:block !important}.watch-table tr.single-history a.preview-link{display:inline-block !important}.watch-table tr.multiple-history a.history-link{display:inline-block !important}#watch-table-wrapper #post-list-buttons{text-align:right;padding:0px;margin:0px}#watch-table-wrapper #post-list-buttons li{display:inline-block}#watch-table-wrapper #post-list-buttons a{border-top-left-radius:initial;border-top-right-radius:initial;border-bottom-left-radius:5px;border-bottom-right-radius:5px}#watch-table-wrapper.has-error #post-list-buttons #post-list-with-errors{display:inline-block !important}#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-unread,#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-mark-views,#watch-table-wrapper.has-unread-changes #post-list-buttons #post-list-unread{display:inline-block !important}@media(max-width: 767px){.watch-table thead{display:block}.watch-table thead tr th{display:inline-block}}@media(max-width: 767px)and (max-width: 768px){.watch-table thead tr th .hide-on-mobile{display:none}}@media(max-width: 767px){.watch-table thead .empty-cell{display:none}.watch-table .last-checked{margin-left:calc(20px + .5rem)}.watch-table .last-checked>span{vertical-align:middle}.watch-table .last-changed{margin-left:calc(20px + .5rem)}.watch-table .last-checked::before{color:var(--color-text);content:"Last Checked "}.watch-table .last-changed::before{color:var(--color-text);content:"Last Changed "}.watch-table td.inline{display:inline-block}.watch-table .pure-table td,.watch-table .pure-table th{border:none}.watch-table td{border:none;border-bottom:1px solid var(--color-border-watch-table-cell);vertical-align:middle}.watch-table td:before{top:6px;left:6px;width:45%;padding-right:10px;white-space:nowrap}.watch-table.pure-table-striped tr{background-color:var(--color-table-background)}.watch-table.pure-table-striped tr:nth-child(2n-1){background-color:var(--color-table-stripe)}.watch-table.pure-table-striped tr:nth-child(2n-1) td{background-color:inherit}}@media(max-width: 767px){.watch-table tbody tr{padding-bottom:10px;padding-top:10px;display:grid;grid-template-columns:20px 1fr 100px;grid-template-rows:auto auto auto auto;gap:.5rem}.watch-table tbody tr .counter-i{display:none}.watch-table tbody tr td.checkbox-uuid{display:grid;place-items:center}.watch-table tbody tr>td{border-bottom:none}.watch-table tbody tr>td.title-col{grid-column:1/-1;grid-row:1}.watch-table tbody tr>td.title-col .watch-title{font-size:.92rem}.watch-table tbody tr>td.title-col .link-spread{display:none}.watch-table tbody tr>td.last-checked{grid-column:1/-1;grid-row:2}.watch-table tbody tr>td.last-changed{grid-column:1/-1;grid-row:3}.watch-table tbody tr>td.checkbox-uuid{grid-column:1;grid-row:4}.watch-table tbody tr>td.buttons{grid-column:2;grid-row:4;display:flex;align-items:center;justify-content:flex-start}.watch-table tbody tr>td.watch-controls{grid-column:3;grid-row:4;display:grid;place-items:center}.watch-table tbody tr>td.watch-controls a img{padding:10px}.pure-table td{padding:3px !important}}ul#conditions_match_logic{list-style:none}ul#conditions_match_logic input,ul#conditions_match_logic label,ul#conditions_match_logic li{display:inline-block}ul#conditions_match_logic li{padding-right:1em}.fieldlist_formfields{width:100%;background-color:var(--color-background, #fff);border-radius:4px;border:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-header{display:flex;background-color:var(--color-background-table-thead, #e0e0e0);font-weight:bold;border-bottom:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-header-cell{flex:1;padding:.5em 1em;text-align:left}.fieldlist_formfields .fieldlist-header-cell:last-child{flex:0 0 120px}.fieldlist_formfields .fieldlist-body{display:flex;flex-direction:column}.fieldlist_formfields .fieldlist-row{display:flex;border-bottom:1px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-row:last-child{border-bottom:none}.fieldlist_formfields .fieldlist-row:nth-child(2n-1){background-color:var(--color-table-stripe, #f2f2f2)}.fieldlist_formfields .fieldlist-row.error-row{background-color:var(--color-error-input, #ffdddd)}.fieldlist_formfields .fieldlist-cell{flex:1;padding:.5em 1em;display:flex;flex-direction:column;justify-content:center}.fieldlist_formfields .fieldlist-cell input,.fieldlist_formfields .fieldlist-cell select{width:100%}.fieldlist_formfields .fieldlist-cell.fieldlist-actions{flex:0 0 120px;display:flex;flex-direction:row;align-items:center;gap:4px}.fieldlist_formfields ul.errors{margin-top:.5em;margin-bottom:0;padding:.5em;background-color:var(--color-error-background-snapshot-age, #ffdddd);border-radius:4px;list-style-position:inside}@media only screen and (max-width: 760px){.fieldlist_formfields .fieldlist-header,.fieldlist_formfields .fieldlist-row{flex-direction:column}.fieldlist_formfields .fieldlist-header-cell{display:none}.fieldlist_formfields .fieldlist-row{padding:.5em 0;border-bottom:2px solid var(--color-border-table-cell, #cbcbcb)}.fieldlist_formfields .fieldlist-cell{padding:.25em .5em}.fieldlist_formfields .fieldlist-cell.fieldlist-actions{flex:1;justify-content:flex-start;padding-top:.5em}.fieldlist_formfields .fieldlist-cell:not(:last-child){margin-bottom:.5em}.fieldlist_formfields .fieldlist-cell::before{content:attr(data-label);font-weight:bold;margin-bottom:.25em}}.fieldlist_formfields .addRuleRow,.fieldlist_formfields .removeRuleRow,.fieldlist_formfields .verifyRuleRow{cursor:pointer;border:none;padding:4px 8px;border-radius:3px;font-weight:bold;background-color:#aaa;color:var(--color-foreground-text, #fff)}.fieldlist_formfields .addRuleRow:hover,.fieldlist_formfields .removeRuleRow:hover,.fieldlist_formfields .verifyRuleRow:hover{background-color:#999}.watch-table.favicon-not-enabled tr .favicon{display:none}.watch-table tr td.inline.title-col .flex-wrapper{display:flex;align-items:center;gap:4px}.watch-table td,.watch-table th{vertical-align:middle}.watch-table tr.has-favicon.unviewed img.favicon{opacity:1 !important}.watch-table .status-icons{white-space:nowrap;display:flex;align-items:center;gap:4px}.watch-table .status-icons>*{vertical-align:middle}.title-col{padding:10px}.title-wrapper{display:flex;align-items:center;gap:10px}.title-col-inner{display:inline-block;vertical-align:middle}.watch-table img.favicon{vertical-align:middle;max-width:25px;max-height:25px;height:25px;padding-right:4px}body.checking-now #checking-now-fixed-tab{display:block !important}#checking-now-fixed-tab{background:#ccc;border-radius:5px;bottom:0;color:var(--color-text);display:none;font-size:.8rem;left:0;padding:5px;position:fixed}#selector-wrapper{height:100%;text-align:center;max-height:70vh;overflow-y:scroll;position:relative}#selector-wrapper>img{position:absolute;z-index:4;max-width:100%}#selector-wrapper>canvas{position:relative;z-index:5;max-width:100%}#selector-wrapper>canvas:hover{cursor:pointer}#selector-current-xpath{font-size:80%}.minitabs-wrapper{width:100%}.minitabs-wrapper .tab-contents-monospace-preview{font-family:"Courier New",Courier,monospace;font-size:70%;word-break:break-word;white-space:pre-wrap}.minitabs-wrapper>div[id]{padding:20px;border:1px solid #ccc;border-top:none}.minitabs-wrapper .minitabs-content{width:100%;display:flex}.minitabs-wrapper .minitabs-content>div{flex:1 1 auto;min-width:0;padding:1rem;border:1px solid #ddd;background-color:#eee}.minitabs-wrapper .minitabs{display:flex;border-bottom:1px solid #ccc}.minitabs-wrapper .minitabs .minitab{flex:1;text-align:center;padding:12px 0;text-decoration:none;color:#333;background-color:#f1f1f1;border:1px solid #ccc;border-bottom:none;cursor:pointer;transition:background-color .3s;border-top-left-radius:5px;border-top-right-radius:5px;opacity:.45}.minitabs-wrapper .minitabs .minitab:hover{background-color:#ddd}.minitabs-wrapper .minitabs .minitab.active{background-color:#eee;font-weight:bold;opacity:1}#notification-preview{resize:both;overflow:hidden}#notification-iframe-html-preview{width:100%;height:100%;border:0;display:block;overflow:auto}.ternary-radio-group{display:flex;gap:0;border:1px solid var(--color-grey-750);border-radius:4px;overflow:hidden;width:fit-content;background:var(--color-background)}.ternary-radio-group .ternary-radio-option{position:relative;cursor:pointer;margin:0;display:flex;align-items:center}.ternary-radio-group .ternary-radio-option input[type=radio]{position:absolute;opacity:0;width:0;height:0}.ternary-radio-group .ternary-radio-option .ternary-radio-label{padding:8px 16px;background:var(--color-grey-900);border:none;border-right:1px solid var(--color-grey-750);font-size:13px;font-weight:500;color:var(--color-text);transition:all .2s ease;cursor:pointer;display:block;text-align:center}.ternary-radio-group .ternary-radio-option:last-child .ternary-radio-label{border-right:none}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label{background:var(--color-link);color:var(--color-text-button);font-weight:600}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label.ternary-default{background:var(--color-grey-600);color:var(--color-text-button)}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover{background:#1a7bc4}.ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover.ternary-default{background:var(--color-grey-500)}.ternary-radio-group .ternary-radio-option:hover .ternary-radio-label{background:var(--color-grey-800)}@media(max-width: 480px){.ternary-radio-group{width:100%}.ternary-radio-group .ternary-radio-label{flex:1;min-width:auto}}input[type=radio].pure-radio:checked+label,input[type=radio].pure-radio:checked{background:var(--color-link);color:var(--color-text-button)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option .ternary-radio-label{background:var(--color-grey-350)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option:hover .ternary-radio-label{background:var(--color-grey-400)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label{background:var(--color-link);color:var(--color-text-button)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label.ternary-default{background:var(--color-grey-600)}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover{background:#1a7bc4}html[data-darkmode=true] .ternary-radio-group .ternary-radio-option input:checked+.ternary-radio-label:hover.ternary-default{background:var(--color-grey-500)}body{color:var(--color-text);background:var(--color-background-page);font-family:Helvetica Neue,Helvetica,Lucida Grande,Arial,Ubuntu,Cantarell,Fira Sans,sans-serif}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}.status-icon{display:inline-block;height:1rem;vertical-align:middle}.pure-table-even{background:var(--color-background)}a{text-decoration:none;color:var(--color-link)}a.github-link{color:var(--color-icon-github);margin:0 1rem 0 .5rem}a.github-link svg{fill:currentColor}a.github-link:hover{color:var(--color-icon-github-hover)}#search-q{opacity:0;-webkit-transition:all .9s ease;-moz-transition:all .9s ease;transition:all .9s ease;width:0;display:none}#search-q.expanded{width:auto;display:inline-block;opacity:1}#search-result-info{color:#fff}button.toggle-button{vertical-align:middle;background:rgba(0,0,0,0);border:none;cursor:pointer;color:var(--color-icon-github)}button.toggle-button:hover{color:var(--color-icon-github-hover)}button.toggle-button svg{fill:currentColor}button.toggle-button .icon-light{display:block}.pure-menu-horizontal{background:var(--color-background);padding:5px;display:flex;justify-content:space-between;align-items:center}#pure-menu-horizontal-spinner{height:3px;background:linear-gradient(-75deg, #ff6000, #ff8f00, #ffdd00, #ed0000);background-size:400% 400%;width:100%;animation:gradient 200s ease infinite}body.spinner-active #pure-menu-horizontal-spinner{animation:gradient 1s ease infinite}@keyframes gradient{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}.pure-menu-heading{color:var(--color-text-menu-heading)}.pure-menu-link{color:var(--color-text-menu-link)}.pure-menu-link:hover{background-color:var(--color-background-menu-link-hover);color:var(--color-text-menu-link-hover)}.tab-pane-inner{scroll-margin-top:200px}section.content{padding-top:100px;padding-bottom:1em;flex-direction:column;display:flex;align-items:center;justify-content:center}code{background:var(--color-background-code);color:var(--color-text)}.inline-tag,.restock-label,.tracking-ldjson-price-data,.watch-tag-list{white-space:nowrap;border-radius:5px;padding:2px 5px;margin-right:4px}.watch-tag-list{color:var(--color-white);background:var(--color-text-watch-tag-list)}@media(min-width: 768px){.box{margin:0 1em !important}}.box{max-width:100%;margin:0 .3em;flex-direction:column;display:flex;justify-content:center}body:after{content:"";background:linear-gradient(130deg, var(--color-background-gradient-first), var(--color-background-gradient-second) 41.07%, var(--color-background-gradient-third) 84.05%)}body:after,body:before{display:block;height:650px;position:absolute;top:0;left:0;width:100%;z-index:-1}body::after{opacity:.91}body::before{content:""}body:after,body:before{-webkit-clip-path:polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%);clip-path:polygon(100% 0, 0 0, 0 77.5%, 1% 77.4%, 2% 77.1%, 3% 76.6%, 4% 75.9%, 5% 75.05%, 6% 74.05%, 7% 72.95%, 8% 71.75%, 9% 70.55%, 10% 69.3%, 11% 68.05%, 12% 66.9%, 13% 65.8%, 14% 64.8%, 15% 64%, 16% 63.35%, 17% 62.85%, 18% 62.6%, 19% 62.5%, 20% 62.65%, 21% 63%, 22% 63.5%, 23% 64.2%, 24% 65.1%, 25% 66.1%, 26% 67.2%, 27% 68.4%, 28% 69.65%, 29% 70.9%, 30% 72.15%, 31% 73.3%, 32% 74.35%, 33% 75.3%, 34% 76.1%, 35% 76.75%, 36% 77.2%, 37% 77.45%, 38% 77.5%, 39% 77.3%, 40% 76.95%, 41% 76.4%, 42% 75.65%, 43% 74.75%, 44% 73.75%, 45% 72.6%, 46% 71.4%, 47% 70.15%, 48% 68.9%, 49% 67.7%, 50% 66.55%, 51% 65.5%, 52% 64.55%, 53% 63.75%, 54% 63.15%, 55% 62.75%, 56% 62.55%, 57% 62.5%, 58% 62.7%, 59% 63.1%, 60% 63.7%, 61% 64.45%, 62% 65.4%, 63% 66.45%, 64% 67.6%, 65% 68.8%, 66% 70.05%, 67% 71.3%, 68% 72.5%, 69% 73.6%, 70% 74.65%, 71% 75.55%, 72% 76.35%, 73% 76.9%, 74% 77.3%, 75% 77.5%, 76% 77.45%, 77% 77.25%, 78% 76.8%, 79% 76.2%, 80% 75.4%, 81% 74.45%, 82% 73.4%, 83% 72.25%, 84% 71.05%, 85% 69.8%, 86% 68.55%, 87% 67.35%, 88% 66.2%, 89% 65.2%, 90% 64.3%, 91% 63.55%, 92% 63%, 93% 62.65%, 94% 62.5%, 95% 62.55%, 96% 62.8%, 97% 63.3%, 98% 63.9%, 99% 64.75%, 100% 65.7%)}.button-small{font-size:85%}.button-xsmall{font-size:70%}.fetch-error{padding-top:1em;font-size:80%;max-width:400px;display:block}.pure-button-primary,a.pure-button-primary,.pure-button-selected,a.pure-button-selected{background-color:var(--color-background-button-primary)}.button-secondary{color:var(--color-text-button);border-radius:4px;text-shadow:0 1px 1px rgba(0,0,0,.2)}.button-success{background:var(--color-background-button-success)}.button-tag{background:var(--color-background-button-tag);color:var(--color-text-button);font-size:65%;border-bottom-left-radius:initial;border-bottom-right-radius:initial;margin-right:4px}.button-tag.active{background:var(--color-background-button-tag-active);font-weight:bold}.button-error{background:var(--color-background-button-error);color:var(--color-text-button-error)}.button-warning{background:var(--color-background-button-warning);color:var(--color-text-button-warning)}.button-secondary{background:var(--color-background-button-secondary)}.button-cancel{background:var(--color-background-button-cancel)}.messages li{list-style:none;padding:1em;border-radius:10px;color:var(--color-text-messages);font-weight:bold}.messages li.message{background:var(--color-background-messages-message)}.messages li.error{background:var(--color-background-messages-error)}.messages li.notice{background:var(--color-background-messages-notice)}.messages.with-share-link>*:hover{cursor:pointer}.notifications-wrapper{padding-top:.5rem}.notifications-wrapper #notification-test-log{padding-top:1rem;white-space:pre-wrap;word-break:break-word;overflow-wrap:break-word;max-width:100%;box-sizing:border-box}.notifications-wrapper #notification-test-log.error>span{color:var(--color-error) !important}label:hover{cursor:pointer}#notification-error-log{border:1px solid var(--color-border-notification);padding:1rem;border-radius:5px;overflow-wrap:break-word}#token-table.pure-table td,#token-table.pure-table th{font-size:80%}.pure-form input[type=text].transparent-field{background-color:var(--color-background-new-watch-input-transparent) !important;color:var(--color-white) !important;border:1px solid hsla(0,0%,100%,.2) !important;box-shadow:none !important;-webkit-box-shadow:none !important}.pure-form input[type=text].transparent-field::placeholder{opacity:.5;color:hsla(0,0%,100%,.7);font-weight:lighter}#new-watch-form{background:var(--color-background-new-watch-form);padding:1em;border-radius:10px;margin-bottom:1em;max-width:100%}#new-watch-form #url::placeholder{font-weight:bold}#new-watch-form input{display:inline-block;margin-bottom:5px}#new-watch-form input:not(.pure-button){background-color:var(--color-background-new-watch-input);color:var(--color-text-new-watch-input)}#new-watch-form .label{display:none}#new-watch-form legend{color:var(--color-text-legend);font-weight:bold}@media only screen and (min-width: 760px){#new-watch-form #watch-add-wrapper-zone{display:flex;gap:.3rem;flex-direction:row;min-width:70vw}}#new-watch-form #watch-add-wrapper-zone>span{flex-grow:0}#new-watch-form #watch-add-wrapper-zone>span input{width:100%;padding-right:1em}#new-watch-form #watch-add-wrapper-zone>span:first-child{flex-grow:1}@media only screen and (max-width: 760px){#new-watch-form #watch-add-wrapper-zone #url{width:100%}}#new-watch-form #watch-group-tag{font-size:.9rem;padding:.3rem;display:flex;align-items:center;gap:.5rem;color:var(--color-white)}#new-watch-form #watch-group-tag label,#new-watch-form #watch-group-tag input{margin:0}#new-watch-form #watch-group-tag input{flex:1}#diff-col{padding-left:40px}#diff-jump{position:fixed;left:0px;top:120px;background:var(--color-background);padding:10px;border-top-right-radius:5px;border-bottom-right-radius:5px;box-shadow:1px 1px 4px var(--color-shadow-jump)}#diff-jump a{color:var(--color-link);cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;-o-user-select:none}footer{padding:10px;background:var(--color-background);color:var(--color-text-footer);text-align:center}#feed-icon{vertical-align:middle}.sticky-tab{position:absolute;top:60px;font-size:65%;background:var(--color-background);padding:10px}.sticky-tab#left-sticky{left:0;position:fixed;border-top-right-radius:5px;border-bottom-right-radius:5px;box-shadow:1px 1px 4px var(--color-shadow-jump)}.sticky-tab#right-sticky{right:0px}.sticky-tab#hosted-sticky{right:0px;top:100px;font-weight:bold}#new-version-text a{color:var(--color-link-new-version)}.watch-controls{color:#f8321b}.watch-controls .state-on img{opacity:.8}.watch-controls img{opacity:.2}.watch-controls img:hover{transition:opacity .3s;opacity:.8}.monospaced-textarea textarea{width:100%;font-family:monospace;white-space:pre;overflow-wrap:normal;overflow-x:auto}.pure-form fieldset{padding-top:0px}.pure-form fieldset ul{padding-bottom:0px;margin-bottom:0px}.pure-form .pure-control-group,.pure-form .pure-group,.pure-form .pure-controls{padding-bottom:1em}.pure-form .pure-control-group div,.pure-form .pure-group div,.pure-form .pure-controls div{margin:0px}.pure-form .pure-control-group .checkbox>*,.pure-form .pure-group .checkbox>*,.pure-form .pure-controls .checkbox>*{display:inline;vertical-align:middle}.pure-form .pure-control-group .checkbox>label,.pure-form .pure-group .checkbox>label,.pure-form .pure-controls .checkbox>label{padding-left:5px}.pure-form .pure-control-group legend,.pure-form .pure-group legend,.pure-form .pure-controls legend{color:var(--color-text-legend)}.pure-form .error input{background-color:var(--color-error-input)}.pure-form ul.errors{padding:.5em .6em;border:1px solid var(--color-error-list);border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form ul.errors li{margin-left:1em;color:var(--color-error-list)}.pure-form label{font-weight:bold}.pure-form textarea{width:100%}.pure-form .inline-radio ul{margin:0px;list-style:none}.pure-form .inline-radio ul li{display:flex;align-items:center;gap:1em}@media only screen and (max-width: 760px),(min-device-width: 768px)and (max-device-width: 1024px){.edit-form{padding:.5em;margin:0}#nav-menu{overflow-x:scroll}}@media only screen and (max-width: 760px),(min-device-width: 768px)and (max-device-width: 800px){div.sticky-tab#hosted-sticky{top:60px;left:0px;right:auto}section.content{padding-top:110px}div.tabs.collapsable ul li{display:block;border-radius:0px;margin-right:0px}input[type=text]{width:100%}}.pure-table{border-color:var(--color-border-table-cell)}.pure-table thead{background-color:var(--color-background-table-thead);color:var(--color-text);border-bottom:1px solid var(--color-background-table-thead)}.pure-table td,.pure-table th{border-left-color:var(--color-border-table-cell)}.pure-table-striped tr:nth-child(2n-1) td{background-color:var(--color-table-stripe)}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{border:var(--color-border-input);box-shadow:inset 0 1px 3px var(--color-shadow-input);background-color:var(--color-background-input);color:var(--color-text-input)}.pure-form input[type=color]:active,.pure-form input[type=date]:active,.pure-form input[type=datetime-local]:active,.pure-form input[type=datetime]:active,.pure-form input[type=email]:active,.pure-form input[type=month]:active,.pure-form input[type=number]:active,.pure-form input[type=password]:active,.pure-form input[type=search]:active,.pure-form input[type=tel]:active,.pure-form input[type=text]:active,.pure-form input[type=time]:active,.pure-form input[type=url]:active,.pure-form input[type=week]:active,.pure-form select:active,.pure-form textarea:active{background-color:var(--color-background-input)}input::placeholder,textarea::placeholder{color:var(--color-text-input-placeholder)}.m-d{min-width:100%}@media only screen and (min-width: 761px){.m-d{min-width:80%}}.tabs ul{margin:0px;padding:0px;display:block}.tabs ul li{margin-right:3px;display:inline-block;color:var(--color-text-tab);border-top-left-radius:5px;border-top-right-radius:5px;background-color:var(--color-background-tab)}.tabs ul li:not(.active):hover{background-color:var(--color-background-tab-hover)}.tabs ul li.active,.tabs ul li :target{background-color:var(--color-background)}.tabs ul li.active a,.tabs ul li :target a{color:var(--color-text-tab-active);font-weight:bold}.tabs ul li a{display:block;padding:.8em;color:var(--color-text-tab)}.pure-form-stacked>div:first-child{display:block}.login-form .inner{background:var(--color-background);padding:20px;border-radius:5px}.tab-pane-inner{padding:0px}.tab-pane-inner:not(:target){display:none}.tab-pane-inner:target{display:block}.beta-logo{height:50px;right:-3px;top:-3px;position:absolute}#selector-header{padding-bottom:1em}body.full-width .edit-form{width:95%}.edit-form{min-width:70%;max-width:95%}.edit-form .box-wrap{position:relative}.edit-form .inner{background:var(--color-background);padding:20px}.edit-form #actions{display:block;background:var(--color-background)}.edit-form #actions .pure-control-group{display:flex;gap:.625em;flex-wrap:wrap}.edit-form .pure-form-message-inline{padding-left:0;color:var(--color-text-input-description)}.edit-form .pure-form-message-inline code{font-size:.875em}.border-fieldset{border:1px solid #ccc;padding:1rem;border-radius:5px;margin-bottom:1rem}.border-fieldset h3{margin-top:0}.border-fieldset fieldset:last-of-type{padding-bottom:0}.border-fieldset fieldset:last-of-type .pure-control-group{padding-bottom:0}ul{padding-left:1em;padding-top:0px;margin-top:4px}.time-check-widget tr{display:inline}.time-check-widget tr input[type=number]{width:5em}@media only screen and (max-width: 760px){.time-check-widget tbody{display:grid;grid-template-columns:auto 1fr auto 1fr;gap:.625em .3125em;align-items:center}.time-check-widget tr{display:contents}.time-check-widget tr th{text-align:right;padding-right:5px}.time-check-widget tr input[type=number]{width:100%;max-width:5em}}#webdriver_delay{width:5em}#api-key:hover{cursor:pointer}#api-key-copy{color:var(--color-api-key)}.button-green{background-color:var(--color-background-button-green)}.button-red{background-color:var(--color-background-button-red)}.noselect{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.snapshot-age{padding:4px;margin:.5rem 0;background-color:var(--color-background-snapshot-age);border-radius:3px;font-weight:bold;margin-bottom:4px}.snapshot-age.error{background-color:var(--color-error-background-snapshot-age);color:var(--color-error-text-snapshot-age)}#checkbox-operations{background:var(--color-background-checkbox-operations);padding:1em;border-radius:10px;margin-bottom:1em;display:none}#checkbox-operations button{margin-bottom:3px;margin-top:3px;display:inline-flex;align-items:center}.checkbox-uuid>*{vertical-align:middle}.inline-warning{border:1px solid var(--color-border-warning);padding:.5rem;border-radius:5px;color:var(--color-warning)}.inline-warning>span{display:inline-block;vertical-align:middle}.inline-warning img.inline-warning-icon{display:inline;height:26px;vertical-align:middle}.tracking-ldjson-price-data{background-color:var(--color-background-button-green);color:#000;opacity:.6}.ldjson-price-track-offer{font-weight:bold;font-style:italic}.ldjson-price-track-offer a.pure-button{border-radius:3px;padding:3px;background-color:var(--color-background-button-green)}.price-follow-tag-icon{display:inline-block;height:.8rem;vertical-align:middle}#quick-watch-processor-type ul#processor{color:#fff;padding-left:0px}#quick-watch-processor-type ul#processor li{list-style:none;font-size:.9rem;display:grid;grid-template-columns:auto 1fr;align-items:center;gap:.5rem;margin-bottom:.5rem}#quick-watch-processor-type label,#quick-watch-processor-type input{padding:0;margin:0}.restock-label.in-stock{background-color:var(--color-background-button-green);color:#fff}.restock-label.not-in-stock{background-color:var(--color-background-button-cancel);color:#777}.restock-label.error{background-color:var(--color-background-button-error);color:#fff;opacity:.7}.restock-label svg{vertical-align:middle}#chrome-extension-link{padding:9px;border:1px solid var(--color-grey-800);border-radius:10px;vertical-align:middle}#chrome-extension-link img{height:21px;padding:2px;vertical-align:middle}#realtime-conn-error{position:fixed;bottom:0;left:0;background:var(--color-warning);padding:10px;font-size:.8rem;color:#fff;opacity:.8}
diff --git a/changedetectionio/templates/_common_fields.html b/changedetectionio/templates/_common_fields.html
index f6452675a48..0b485698a3c 100644
--- a/changedetectionio/templates/_common_fields.html
+++ b/changedetectionio/templates/_common_fields.html
@@ -24,121 +24,153 @@
                               
                             
-
-
- {{ render_field(form.notification_title, class="m-d notification-title", placeholder=settings_application['notification_title']) }} - Title for all notifications -
-
- {{ render_field(form.notification_body , rows=5, class="notification-body", placeholder=settings_application['notification_body']) }} - Body for all notifications ‐ You can use Jinja2 templating in the notification title, body and URL, and tokens from below. - - -
-
-
Show token/placeholders
-
- +
-
- {{ render_field(form.notification_format , class="notification-format") }} - Format for all notifications -
-{% endmacro %} +{% endmacro %} \ No newline at end of file diff --git a/changedetectionio/templates/_helpers.html b/changedetectionio/templates/_helpers.html index 3ab14758b88..e957ebe13f5 100644 --- a/changedetectionio/templates/_helpers.html +++ b/changedetectionio/templates/_helpers.html @@ -27,8 +27,15 @@ {% endmacro %} {% macro render_checkbox_field(field) %} -
+
{{ field(**kwargs)|safe }} {{ field.label }} + {% if field.top_errors %} +
    + {% for error in field.top_errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} {% if field.errors %}
    {% for error in field.errors %} @@ -43,9 +50,16 @@ {% if BooleanField %} {% set _ = field.__setattr__('boolean_mode', true) %} {% endif %} -
    +
    {{ field.label }}
    {{ field(**kwargs)|safe }}
    + {% if field.top_errors %} +
      + {% for error in field.top_errors %} +
    • {{ error }}
    • + {% endfor %} +
    + {% endif %} {% if field.errors %}
      {% for error in field.errors %} @@ -58,8 +72,15 @@ {% macro render_simple_field(field) %} - {{ field.label }} - {{ field(**kwargs)|safe }} + {{ field.label }} + {{ field(**kwargs)|safe }} + {% if field.top_errors %} +
        + {% for error in field.top_errors %} +
      • {{ error }}
      • + {% endfor %} +
      + {% endif %} {% if field.errors %}
        {% for error in field.errors %} @@ -74,8 +95,15 @@ {% macro render_nolabel_field(field) %} {{ field(**kwargs)|safe }} - {% if field.errors %} + {% if field.top_errors or field.errors %} + {% if field.top_errors %} +
          + {% for error in field.top_errors %} +
        • {{ error }}
        • + {% endfor %} +
        + {% endif %} {% if field.errors %}
          {% for error in field.errors %} diff --git a/changedetectionio/tests/smtp/test_notification_smtp.py b/changedetectionio/tests/smtp/test_notification_smtp.py index d89ea7950b9..a20cee6c63d 100644 --- a/changedetectionio/tests/smtp/test_notification_smtp.py +++ b/changedetectionio/tests/smtp/test_notification_smtp.py @@ -1,17 +1,17 @@ -import json import os import time -import re from flask import url_for from changedetectionio.tests.util import set_original_response, set_modified_response, set_more_modified_response, live_server_setup, \ wait_for_all_checks, \ set_longer_modified_response -from changedetectionio.tests.util import extract_UUID_from_client import logging -import base64 # NOTE - RELIES ON mailserver as hostname running, see github build recipes -smtp_test_server = 'mailserver' +# Should be hostname (never IP), looks for our test mailserver that repeats the content +# python3 changedetectionio/tests/smtp/smtp-test-server.py & +# mailserver=localhost pytest tests/smtp/test_notification_smtp.py::test_check_notification_email_formats_default_HTML +smtp_test_server = os.getenv('mailserver', 'mailserver') + from changedetectionio.notification import ( default_notification_body, @@ -20,7 +20,35 @@ valid_notification_formats, ) +from email import policy +from email.parser import BytesParser, Parser + +def parse_mime(raw): + """Return (EmailMessage, dict[str, list[str]] bodies by content-type).""" + if isinstance(raw, (bytes, bytearray)): + msg = BytesParser(policy=policy.default).parsebytes(raw) + else: + msg = Parser(policy=policy.default).parsestr(raw) + + parts_by_type = {} + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_maintype() == "multipart": + continue + ctype = part.get_content_type() # e.g. "text/plain" + text = part.get_content() # decoded str + parts_by_type.setdefault(ctype, []).append(text) + else: + parts_by_type.setdefault(msg.get_content_type(), []).append(msg.get_content()) + return msg, parts_by_type + +def one_or_join(parts_dict, ctype): + """Join multiple parts of the same type (rare but possible).""" + return "\n".join(parts_dict.get(ctype, [])) + +def norm_newlines(s: str) -> str: + return s.replace("\r\n", "\n").replace("\r", "\n") def get_last_message_from_smtp_server(): import socket @@ -77,37 +105,36 @@ def test_check_notification_email_formats_default_HTML(client, live_server, meas time.sleep(3) - msg = get_last_message_from_smtp_server() - assert len(msg) >= 1 + raw = get_last_message_from_smtp_server() + assert raw # not empty + + msg, bodies = parse_mime(raw) + + plain = norm_newlines(one_or_join(bodies, "text/plain")) + html = norm_newlines(one_or_join(bodies, "text/html")) + + # Now assert against the decoded bodies + assert "(added) So let's see what happens.\n" in plain # plaintext uses a literal apostrophe + assert "(added) So let's see what happens.
          " in html # html uses ' and
          + + # You can also check counts, boundaries, etc. + assert html.count("So let's see what happens.") == 3 + assert "modified head title had a change." in plain + assert "modified head title had a change.
          " in html + + - # The email should have two bodies, and the text/html part should be
          - assert 'Content-Type: text/plain' in msg - assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n - assert 'Content-Type: text/html' in msg - assert '(added) So let\'s see what happens.
          ' in msg # the html part res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) assert b'Deleted' in res.data def test_check_notification_email_formats_default_Text_override_HTML(client, live_server, measure_memory_usage): - ## live_server_setup(live_server) # Setup on conftest per function # HTML problems? see this # https://github.com/caronc/apprise/issues/633 - set_original_response() notification_url = f'mailto://changedetection@{smtp_test_server}:11025/?to=fff@home.com' - notification_body = f""" - - - My Webpage - - -

          Test

          - {default_notification_body} - - -""" + notification_body = f"""{default_notification_body}""" ##################### # Set this up for when we remove the notification from the watch, it should fallback with these details @@ -116,11 +143,12 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv data={"application-notification_urls": notification_url, "application-notification_title": "fallback-title " + default_notification_title, "application-notification_body": notification_body, - "application-notification_format": 'Text', + "application-notification_format": 'Text', # handler.py should be sure to add &format=text to override default html from apprise "requests-time_between_check-minutes": 180, 'application-fetch_backend': "html_requests"}, follow_redirects=True ) + assert b"Settings updated." in res.data # Add a watch and trigger a HTTP POST @@ -140,18 +168,26 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv wait_for_all_checks(client) time.sleep(3) - msg = get_last_message_from_smtp_server() - assert len(msg) >= 1 - # with open('/tmp/m.txt', 'w') as f: - # f.write(msg) + raw = get_last_message_from_smtp_server() + assert raw + + msg, bodies = parse_mime(raw) + + plain = norm_newlines(one_or_join(bodies, "text/plain")) + html = norm_newlines(one_or_join(bodies, "text/html")) + assert not html # should be no HTML here + + # Expect ONLY text/plain body + assert "text/plain" in bodies + assert "text/html" not in bodies - # The email should not have two bodies, should be TEXT only + # Assert on decoded plaintext (literal apostrophe, not ') + # Should be NO markup when in text mode + assert "(added) So let's see what happens.\n" in plain - assert 'Content-Type: text/plain' in msg - assert '(added) So let\'s see what happens.\r\n' in msg # The plaintext part with \r\n + # ---------- Flip to HTML format, then expect multipart with both ---------- set_original_response() - # Now override as HTML format res = client.post( url_for("ui.ui_edit.edit_page", uuid="first"), data={ @@ -161,23 +197,28 @@ def test_check_notification_email_formats_default_Text_override_HTML(client, liv "time_between_check_use_default": "y"}, follow_redirects=True ) + assert b"Updated watch." in res.data wait_for_all_checks(client) time.sleep(3) - msg = get_last_message_from_smtp_server() - assert len(msg) >= 1 - # The email should have two bodies, and the text/html part should be
          - assert 'Content-Type: text/plain' in msg - assert '(removed) So let\'s see what happens.\r\n' in msg # The plaintext part with \n - assert 'Content-Type: text/html' in msg - assert '(removed) So let\'s see what happens.
          ' in msg # the html part + raw = get_last_message_from_smtp_server() + assert raw - # https://github.com/dgtlmoon/changedetection.io/issues/2103 - assert '

          Test

          ' in msg - assert '<' not in msg - assert 'Content-Type: text/html' in msg + msg, bodies = parse_mime(raw) + plain = norm_newlines(one_or_join(bodies, "text/plain")) + html = norm_newlines(one_or_join(bodies, "text/html")) - res = client.get(url_for("ui.form_delete", uuid="all"), follow_redirects=True) - assert b'Deleted' in res.data + # Expect both text/plain and text/html bodies now + assert "text/plain" in bodies + assert "text/html" in bodies + + # Plaintext reflects the removal line (literal apostrophe) + assert "(removed) So let's see what happens.\n" in plain + assert "(removed) So let's see what happens.
          " in html + + # Optional: ensure we got multipart/alternative (typical for dual bodies) + if msg.is_multipart(): + # most senders do "multipart/alternative" for text/plain + text/html + assert msg.get_content_subtype() in ("alternative", "mixed", "related") diff --git a/changedetectionio/tests/test_jinja2.py b/changedetectionio/tests/test_jinja2.py index ed60826ba03..e5a807141d8 100644 --- a/changedetectionio/tests/test_jinja2.py +++ b/changedetectionio/tests/test_jinja2.py @@ -3,6 +3,7 @@ import time from flask import url_for from .util import live_server_setup, wait_for_all_checks +from ..notification import default_notification_title, default_notification_body, default_notification_format # def test_setup(client, live_server, measure_memory_usage): @@ -10,7 +11,6 @@ # If there was only a change in the whitespacing, then we shouldnt have a change detected def test_jinja2_in_url_query(client, live_server, measure_memory_usage): - # Add our URL to the import page test_url = url_for('test_return_query', _external=True) @@ -56,3 +56,20 @@ def test_jinja2_security_url_query(client, live_server, measure_memory_usage): assert b'is invalid and cannot be used' in res.data # Some of the spewed output from the subclasses assert b'dict_values' not in res.data + + +def test_jinja2_notification(client, live_server, measure_memory_usage): + + res = client.post( + url_for("settings.settings_page"), + data={"application-notification_urls": "posts://127.0.0.1", + "application-notification_title": "on the {% now 'America/New_York', '%Y-%m-%d' %}", + "application-notification_body": "on the {% now 'America/New_York', '%Y-%m-%d' %}", + "application-notification_format": default_notification_format, + "requests-time_between_check-minutes": 180, + 'application-fetch_backend': "html_requests"}, + follow_redirects=True + ) + + assert b"Settings updated." in res.data + assert b"Settings updated." in res.data diff --git a/changedetectionio/tests/test_notification.py b/changedetectionio/tests/test_notification.py index c7ac1bdf150..e8c91da1d1b 100644 --- a/changedetectionio/tests/test_notification.py +++ b/changedetectionio/tests/test_notification.py @@ -291,6 +291,20 @@ def test_notification_custom_endpoint_and_jinja2(client, live_server, measure_me # test_endpoint - that sends the contents of a file # test_notification_endpoint - that takes a POST and writes it to file (test-datastore/notification.txt) + # Drop in a custom wrapping template + with open("test-datastore/notification-wrapper.html", "w" ) as f: + f.write(""" + + + A change was detected at {{watch_html_link}} + template_params = notification_parameters.copy() + template_params['notification_body'] = n_body + template_params['notification_url_current'] = url + n_body = jinja_render(template_str=notification_template, **template_params) + + """) + + # CUSTOM JSON BODY CHECK for POST:// set_original_response() # https://github.com/caronc/apprise/wiki/Notify_Custom_JSON#header-manipulation