{% extends 'base.html' %} {% block content %} {% from '_helpers.html' import render_field, render_checkbox_field, render_button %} {% from '_common_fields.html' import render_common_settings_form %} <script src="{{url_for('static_content', group='js', filename='tabs.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='vis.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='global-settings.js')}}" defer></script> <script> const browser_steps_available_screenshots=JSON.parse('{{ watch.get_browsersteps_available_screenshots|tojson }}'); const browser_steps_config=JSON.parse('{{ browser_steps_config|tojson }}'); <!-- Should be _external so that firefox and others load it more reliably --> const browser_steps_fetch_screenshot_image_url="{{url_for('browser_steps.browser_steps_fetch_screenshot_image', uuid=uuid, _external=True)}}"; const browser_steps_last_error_step={{ watch.browser_steps_last_error_step|tojson }}; const browser_steps_start_url="{{url_for('browser_steps.browsersteps_start_session', uuid=uuid)}}"; const browser_steps_sync_url="{{url_for('browser_steps.browsersteps_ui_update', uuid=uuid)}}"; {% if emailprefix %} const email_notification_prefix=JSON.parse('{{ emailprefix|tojson }}'); {% endif %} const notification_base_url="{{url_for('ajax_callback_send_notification_test', 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)}}"; const screenshot_url="{{url_for('static_content', group='screenshot', filename=uuid)}}"; const watch_visual_selector_data_url="{{url_for('static_content', group='visual_selector_data', filename=uuid)}}"; const default_system_fetch_backend="{{ settings_application['fetch_backend'] }}"; </script> <script src="{{url_for('static_content', group='js', filename='watch-settings.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='limit.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='notifications.js')}}" defer></script> <script src="{{url_for('static_content', group='js', filename='visual-selector.js')}}" defer></script> {% if playwright_enabled %} <script src="{{url_for('static_content', group='js', filename='browser-steps.js')}}" defer></script> {% endif %} {% set has_tag_filters_extra="WARNING: Watch has tag/groups set with special filters\n" if has_special_tag_options else '' %} <script src="{{url_for('static_content', group='js', filename='recheck-proxy.js')}}" defer></script> <div class="edit-form monospaced-textarea"> <div class="tabs collapsable"> <ul> <li class="tab" id=""><a href="#general">General</a></li> <li class="tab"><a href="#request">Request</a></li> {% if extra_tab_content %} <li class="tab"><a href="#extras_tab">{{ extra_tab_content }}</a></li> {% endif %} {% if playwright_enabled %} <li class="tab"><a id="browsersteps-tab" href="#browser-steps">Browser Steps</a></li> {% endif %} {% if watch['processor'] == 'text_json_diff' %} <li class="tab"><a id="visualselector-tab" href="#visualselector">Visual Filter Selector</a></li> <li class="tab"><a href="#filters-and-triggers">Filters & Triggers</a></li> {% endif %} <li class="tab"><a href="#notifications">Notifications</a></li> <li class="tab"><a href="#stats">Stats</a></li> </ul> </div> <div class="box-wrap inner"> <form class="pure-form pure-form-stacked" action="{{ url_for('edit_page', uuid=uuid, next = request.args.get('next'), unpause_on_save = request.args.get('unpause_on_save')) }}" method="POST"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <div class="tab-pane-inner" id="general"> <fieldset> <div class="pure-control-group"> {{ render_field(form.url, placeholder="https://...", required=true, class="m-d") }} <span class="pure-form-message-inline">Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a></span><br> <span class="pure-form-message-inline">You can use variables in the URL, perfect for inserting the current date and other logic, <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Handling-variables-in-the-watched-URL">help and examples here</a></span><br> </div> <div class="pure-control-group inline-radio"> {{ render_field(form.processor) }} </div> <div class="pure-control-group"> {{ render_field(form.title, class="m-d") }} </div> <div class="pure-control-group"> {{ render_field(form.tags) }} <span class="pure-form-message-inline">Organisational tag/group name used in the main listing page</span> </div> <div class="pure-control-group time-between-check border-fieldset"> {{ render_field(form.time_between_check, class="time-check-widget") }} {{ render_checkbox_field(form.time_between_check_use_default, class="use-default-timecheck") }} </div> <div class="pure-control-group"> {{ render_checkbox_field(form.extract_title_as_title) }} </div> <div class="pure-control-group"> {{ render_checkbox_field(form.filter_failure_notification_send) }} <span class="pure-form-message-inline"> Sends a notification when the filter can no longer be seen on the page, good for knowing when the page changed and your filter will not work anymore. </span> </div> </fieldset> </div> <div class="tab-pane-inner" id="request"> <div class="pure-control-group inline-radio"> {{ render_field(form.fetch_backend, class="fetch-backend") }} <span class="pure-form-message-inline"> <p>Use the <strong>Basic</strong> method (default) where your watched site doesn't need Javascript to render.</p> <p>The <strong>Chrome/Javascript</strong> method requires a network connection to a running WebDriver+Chrome server, set by the ENV var 'WEBDRIVER_URL'. </p> Tip: <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Proxy-configuration#brightdata-proxy-support">Connect using Bright Data and Oxylabs Proxies, find out more here.</a> </span> </div> {% if form.proxy %} <div class="pure-control-group inline-radio"> <div>{{ form.proxy.label }} <a href="" id="check-all-proxies" class="pure-button button-secondary button-xsmall" >Check/Scan all</a></div> <div>{{ form.proxy(class="fetch-backend-proxy") }}</div> <span class="pure-form-message-inline"> Choose a proxy for this watch </span> </div> {% endif %} <!-- webdriver always --> <fieldset data-visible-for="fetch_backend=html_webdriver" style="display: none;"> <div class="pure-control-group"> {{ render_field(form.webdriver_delay) }} <div class="pure-form-message-inline"> <strong>If you're having trouble waiting for the page to be fully rendered (text missing etc), try increasing the 'wait' time here.</strong> <br> This will wait <i>n</i> seconds before extracting the text. {% if using_global_webdriver_wait %} <br><strong>Using the current global default settings</strong> {% endif %} </div> </div> <div class="pure-control-group"> <a class="pure-button button-secondary button-xsmall show-advanced">Show advanced options</a> </div> <div class="advanced-options" style="display: none;"> {{ render_field(form.webdriver_js_execute_code) }} <div class="pure-form-message-inline"> Run this code before performing change detection, handy for filling in fields and other actions <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Run-JavaScript-before-change-detection">More help and examples here</a> </div> </div> </fieldset> <!-- html requests always --> <fieldset data-visible-for="fetch_backend=html_requests"> <div class="pure-control-group"> <a class="pure-button button-secondary button-xsmall show-advanced">Show advanced options</a> </div> <div class="advanced-options" style="display: none;"> <div class="pure-control-group" id="request-method"> {{ render_field(form.method) }} </div> <div id="request-body"> {{ render_field(form.body, rows=5, placeholder="Example { \"name\":\"John\", \"age\":30, \"car\":null }") }} </div> </div> </fieldset> <!-- hmm --> <div class="pure-control-group advanced-options" style="display: none;"> {{ render_field(form.headers, rows=5, placeholder="Example Cookie: foobar User-Agent: wonderbra 1.0") }} <div class="pure-form-message-inline"> {% if has_extra_headers_file %} <strong>Alert! Extra headers file found and will be added to this watch!</strong> {% else %} Headers can be also read from a file in your data-directory <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Adding-headers-from-an-external-file">Read more here</a> {% endif %} <br> (Not supported by Selenium browser) </div> </div> <fieldset data-visible-for="fetch_backend=html_requests fetch_backend=html_webdriver" > <div class="pure-control-group inline-radio advanced-options" style="display: none;"> {{ render_checkbox_field(form.ignore_status_codes) }} </div> </fieldset> </div> {% if playwright_enabled %} <div class="tab-pane-inner" id="browser-steps"> <img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality"> <fieldset> <div class="pure-control-group"> <!-- Too hard right now, better to just send the events to the fetcher for now and leave it in the final screenshot and/or report an error <a id="play-steps" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Play steps ▶</a> --> <!--- Do this later --> <div class="checkbox" style="display: none;"> <input type=checkbox id="include_text_elements" > <label for="include_text_elements">Turn on text finder</label> </div> <div id="loading-status-text" style="display: none;">Please wait, first browser step can take a little time to load..<div class="spinner"></div></div> <div class="flex-wrapper" > <div id="browser-steps-ui" class="noselect" style="width: 100%; background-color: #eee; border-radius: 5px;"> <div class="noselect" id="browsersteps-selector-wrapper" style="width: 100%"> <span class="loader" > <span id="browsersteps-click-start"> <h2 >Click here to Start</h2> <svg style="height: 3.5rem;" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"/><g id="play_x5F_alt"><path d="M16,0C7.164,0,0,7.164,0,16s7.164,16,16,16s16-7.164,16-16S24.836,0,16,0z M10,24V8l16.008,8L10,24z" style="fill: var(--color-grey-400);"/></g></svg><br> Please allow 10-15 seconds for the browser to connect.<br> </span> <div class="spinner" style="display: none;"></div> </span> <img class="noselect" id="browsersteps-img" src="" style="max-width: 100%; width: 100%;" > <canvas class="noselect" id="browsersteps-selector-canvas" style="max-width: 100%; width: 100%;"></canvas> </div> </div> <div id="browser-steps-fieldlist" style="padding-left: 1em; width: 350px; font-size: 80%;" > <span id="browser-seconds-remaining">Loading</span> <span style="font-size: 80%;"> (<a target=_new href="https://github.com/dgtlmoon/changedetection.io/pull/478/files#diff-1a79d924d1840c485238e66772391268a89c95b781d69091384cf1ea1ac146c9R4">?</a>) </span> {{ render_field(form.browser_steps) }} </div> </div> </div> </fieldset> </div> {% endif %} <div class="tab-pane-inner" id="notifications"> <fieldset> <div class="pure-control-group inline-radio"> {{ render_checkbox_field(form.notification_muted) }} </div> {% if is_html_webdriver %} <div class="pure-control-group inline-radio"> {{ render_checkbox_field(form.notification_screenshot) }} <span class="pure-form-message-inline"> <strong>Use with caution!</strong> This will easily fill up your email storage quota or flood other storages. </span> </div> {% endif %} <div class="field-group" id="notification-field-group"> {% if has_default_notification_urls %} <div class="inline-warning"> <img class="inline-warning-icon" src="{{url_for('static_content', group='images', filename='notice.svg')}}" alt="Look out!" title="Lookout!" > There are <a href="{{ url_for('settings_page')}}#notifications">system-wide notification URLs enabled</a>, this form will override notification settings for this watch only ‐ an empty Notification URL list here will still send notifications. </div> {% endif %} <a href="#notifications" id="notification-setting-reset-to-default" class="pure-button button-xsmall" style="right: 20px; top: 20px; position: absolute; background-color: #5f42dd; border-radius: 4px; font-size: 70%; color: #fff">Use system defaults</a> {{ render_common_settings_form(form, emailprefix, settings_application, extra_notification_token_placeholder_info) }} </div> </fieldset> </div> {% if watch['processor'] == 'text_json_diff' %} <div class="tab-pane-inner" id="filters-and-triggers"> <span id="activate-text-preview" class="pure-button pure-button-primary button-xsmall">Activate preview</span> <div> <div id="edit-text-filter"> <div class="pure-control-group" id="pro-tips"> <strong>Pro-tips:</strong><br> <ul> <li> Use the preview page to see your filters and triggers highlighted. </li> <li> Some sites use JavaScript to create the content, for this you should <a href="https://github.com/dgtlmoon/changedetection.io/wiki/Fetching-pages-with-WebDriver">use the Chrome/WebDriver Fetcher</a> </li> </ul> </div> <div class="pure-control-group"> {% set field = render_field(form.include_filters, rows=5, placeholder=has_tag_filters_extra+"#example xpath://body/div/span[contains(@class, 'example-class')]", class="m-d") %} {{ field }} {% if '/text()' in field %} <span class="pure-form-message-inline"><strong>Note!: //text() function does not work where the <element> contains <![CDATA[]]></strong></span><br> {% endif %} <span class="pure-form-message-inline">One CSS, xPath, JSON Path/JQ selector per line, <i>any</i> rules that matches will be used.<br> <p><div data-target="#advanced-help-selectors" class="toggle-show pure-button button-tag button-xsmall">Show advanced help and tips</div><br></p> <ul id="advanced-help-selectors" style="display: none;"> <li>CSS - Limit text to this CSS rule, only text matching this CSS rule is included.</li> <li>JSON - Limit text to this JSON rule, using either <a href="https://pypi.org/project/jsonpath-ng/" target="new">JSONPath</a> or <a href="https://stedolan.github.io/jq/" target="new">jq</a> (if installed). <ul> <li>JSONPath: Prefix with <code>json:</code>, use <code>json:$</code> to force re-formatting if required, <a href="https://jsonpath.com/" target="new">test your JSONPath here</a>.</li> {% if jq_support %} <li>jq: Prefix with <code>jq:</code> and <a href="https://jqplay.org/" target="new">test your jq here</a>. Using <a href="https://stedolan.github.io/jq/" target="new">jq</a> allows for complex filtering and processing of JSON data with built-in functions, regex, filtering, and more. See examples and documentation <a href="https://stedolan.github.io/jq/manual/" target="new">here</a>. Prefix <code>jqraw:</code> outputs the results as text instead of a JSON list.</li> {% else %} <li>jq support not installed</li> {% endif %} </ul> </li> <li>XPath - Limit text to this XPath rule, simply start with a forward-slash. To specify XPath to be used explicitly or the XPath rule starts with an XPath function: Prefix with <code>xpath:</code> <ul> <li>Example: <code>//*[contains(@class, 'sametext')]</code> or <code>xpath:count(//*[contains(@class, 'sametext')])</code>, <a href="http://xpather.com/" target="new">test your XPath here</a></li> <li>Example: Get all titles from an RSS feed <code>//title/text()</code></li> <li>To use XPath1.0: Prefix with <code>xpath1:</code></li> </ul> </li> <li> Please be sure that you thoroughly understand how to write CSS, JSONPath, XPath{% if jq_support %}, or jq selector{%endif%} rules before filing an issue on GitHub! <a href="https://github.com/dgtlmoon/changedetection.io/wiki/CSS-Selector-help">here for more CSS selector help</a>.<br> </li> </ul> </span> </div> <fieldset class="pure-control-group"> {{ render_field(form.subtractive_selectors, rows=5, placeholder=has_tag_filters_extra+"header footer nav .stockticker") }} <span class="pure-form-message-inline"> <ul> <li> Remove HTML element(s) by CSS selector before text conversion. </li> <li> Don't paste HTML here, use only CSS selectors </li> <li> Add multiple elements or CSS selectors per line to ignore multiple parts of the HTML. </li> </ul> </span> </fieldset> <div class="text-filtering border-fieldset"> <fieldset class="pure-group" id="text-filtering-type-options"> <h3>Text filtering</h3> Limit trigger/ignore/block/extract to;<br> {{ render_checkbox_field(form.filter_text_added) }} {{ render_checkbox_field(form.filter_text_replaced) }} {{ render_checkbox_field(form.filter_text_removed) }} <span class="pure-form-message-inline">Note: Depending on the length and similarity of the text on each line, the algorithm may consider an <strong>addition</strong> instead of <strong>replacement</strong> for example.</span> <span class="pure-form-message-inline">So it's always better to select <strong>Added</strong>+<strong>Replaced</strong> when you're interested in new content.</span><br> <span class="pure-form-message-inline">When content is merely moved in a list, it will also trigger an <strong>addition</strong>, consider enabling <code><strong>Only trigger when unique lines appear</strong></code></span> </fieldset> <fieldset class="pure-control-group"> {{ render_checkbox_field(form.sort_text_alphabetically) }} <span class="pure-form-message-inline">Helps reduce changes detected caused by sites shuffling lines around, combine with <i>check unique lines</i> below.</span> </fieldset> <fieldset class="pure-control-group"> {{ render_checkbox_field(form.trim_text_whitespace) }} <span class="pure-form-message-inline">Remove any whitespace before and after each line of text</span> </fieldset> <fieldset class="pure-control-group"> {{ render_checkbox_field(form.check_unique_lines) }} <span class="pure-form-message-inline">Good for websites that just move the content around, and you want to know when NEW content is added, compares new lines against all history for this watch.</span> </fieldset> <fieldset> <div class="pure-control-group"> {{ render_field(form.trigger_text, rows=5, placeholder="Some text to wait for in a line /some.regex\d{2}/ for case-INsensitive regex ") }} <span class="pure-form-message-inline"> <ul> <li>Text to wait for before triggering a change/notification, all text and regex are tested <i>case-insensitive</i>.</li> <li>Trigger text is processed from the result-text that comes out of any CSS/JSON Filters for this watch</li> <li>Each line is processed separately (think of each line as "OR")</li> <li>Note: Wrap in forward slash / to use regex example: <code>/foo\d/</code></li> </ul> </span> </div> </fieldset> <fieldset class="pure-group"> {{ render_field(form.ignore_text, rows=5, placeholder="Some text to ignore in a line /some.regex\d{2}/ for case-INsensitive regex ") }} <span class="pure-form-message-inline"> <ul> <li>Each line processed separately, any line matching will be ignored (removed before creating the checksum)</li> <li>Regular Expression support, wrap the entire line in forward slash <code>/regex/</code></li> <li>Changing this will affect the comparison checksum which may trigger an alert</li> <li>Use the preview/show current tab to see ignores</li> </ul> </span> </fieldset> <fieldset> <div class="pure-control-group"> {{ render_field(form.text_should_not_be_present, rows=5, placeholder="For example: Out of stock Sold out Not in stock Unavailable") }} <span class="pure-form-message-inline"> <ul> <li>Block change-detection while this text is on the page, all text and regex are tested <i>case-insensitive</i>, good for waiting for when a product is available again</li> <li>Block text is processed from the result-text that comes out of any CSS/JSON Filters for this watch</li> <li>All lines here must not exist (think of each line as "OR")</li> <li>Note: Wrap in forward slash / to use regex example: <code>/foo\d/</code></li> </ul> </span> </div> </fieldset> <fieldset> <div class="pure-control-group"> {{ render_field(form.extract_text, rows=5, placeholder="\d+ online") }} <span class="pure-form-message-inline"> <ul> <li>Extracts text in the final output (line by line) after other filters using regular expressions or string match; <ul> <li>Regular expression ‐ example <code>/reports.+?2022/i</code></li> <li>Don't forget to consider the white-space at the start of a line <code>/.+?reports.+?2022/i</code></li> <li>Use <code>//(?aiLmsux))</code> type flags (more <a href="https://docs.python.org/3/library/re.html#index-15">information here</a>)<br></li> <li>Keyword example ‐ example <code>Out of stock</code></li> <li>Use groups to extract just that text ‐ example <code>/reports.+?(\d+)/i</code> returns a list of years only</li> <li>Example - match lines containing a keyword <code>/.*icecream.*/</code></li> </ul> </li> <li>One line per regular-expression/string match</li> </ul> </span> </div> </fieldset> </div> </div> <div id="text-preview" style="display: none;" > <script> const preview_text_edit_filters_url="{{url_for('watch_get_preview_rendered', uuid=uuid)}}"; </script> <span><strong>Preview of the text that is used for changedetection after all filters run.</strong></span><br> {#<div id="text-preview-controls"><span id="text-preview-refresh" class="pure-button button-xsmall">Refresh</span></div>#} <p> <div id="text-preview-inner"></div> </p> </div> </div> </div> {% endif %} {# rendered sub Template #} {% if extra_form_content %} <div class="tab-pane-inner" id="extras_tab"> {{ extra_form_content|safe }} </div> {% endif %} {% if watch['processor'] == 'text_json_diff' %} <div class="tab-pane-inner visual-selector-ui" id="visualselector"> <img class="beta-logo" src="{{url_for('static_content', group='images', filename='beta-logo.png')}}" alt="New beta functionality"> <fieldset> <div class="pure-control-group"> {% if visualselector_enabled %} <span class="pure-form-message-inline" id="visual-selector-heading"> The Visual Selector tool lets you select the <i>text</i> elements that will be used for the change detection. It automatically fills-in the filters in the "CSS/JSONPath/JQ/XPath Filters" box of the <a href="#filters-and-triggers">Filters & Triggers</a> tab. Use <strong>Shift+Click</strong> to select multiple items. </span> <div id="selector-header"> <a id="clear-selector" class="pure-button button-secondary button-xsmall" style="font-size: 70%">Clear selection</a> <i class="fetching-update-notice" style="font-size: 80%;">One moment, fetching screenshot and element information..</i> </div> <div id="selector-wrapper" style="display: none"> <!-- request the screenshot and get the element offset info ready --> <!-- use img src ready load to know everything is ready to map out --> <!-- @todo: maybe something interesting like a field to select 'elements that contain text... and their parents n' --> <img id="selector-background" > <canvas id="selector-canvas"></canvas> </div> <div id="selector-current-xpath" style="overflow-x: hidden"><strong>Currently:</strong> <span class="text">Loading...</span></div> {% else %} <span class="pure-form-message-inline"> <p>Sorry, this functionality only works with Playwright/Chrome enabled watches.</p> <p>Enable the Playwright Chrome fetcher, or alternatively try our <a href="https://lemonade.changedetection.io/start">very affordable subscription based service</a>.</p> <p>This is because Selenium/WebDriver can not extract full page screenshots reliably.</p> </span> {% endif %} </div> </fieldset> </div> {% endif %} <div class="tab-pane-inner" id="stats"> <div class="pure-control-group"> <style> #stats-table tr > td:first-child { font-weight: bold; } </style> <table class="pure-table" id="stats-table"> <tbody> <tr> <td>Check count</td> <td>{{ "{:,}".format( watch.check_count) }}</td> </tr> <tr> <td>Consecutive filter failures</td> <td>{{ "{:,}".format( watch.consecutive_filter_failures) }}</td> </tr> <tr> <td>History length</td> <td>{{ "{:,}".format(watch.history|length) }}</td> </tr> <tr> <td>Last fetch duration</td> <td>{{ watch.fetch_time }}s</td> </tr> <tr> <td>Notification alert count</td> <td>{{ watch.notification_alert_count }}</td> </tr> <tr> <td>Server type reply</td> <td>{{ watch.get('remote_server_reply') }}</td> </tr> </tbody> </table> {% if watch.history_n %} <p> <a href="{{url_for('watch_get_latest_html', uuid=uuid)}}" class="pure-button button-small">Download latest HTML snapshot</a> </p> {% endif %} </div> </div> <div id="actions"> <div class="pure-control-group"> {{ render_button(form.save_button) }} <a href="{{url_for('form_delete', uuid=uuid)}}" class="pure-button button-small button-error ">Delete</a> <a href="{{url_for('clear_watch_history', uuid=uuid)}}" class="pure-button button-small button-error ">Clear History</a> <a href="{{url_for('form_clone', uuid=uuid)}}" class="pure-button button-small ">Create Copy</a> </div> </div> </form> </div> </div> {% endblock %}