UI - Improvements to live preview of Filters text

"Ignore text" is now "Remove text", it works the same but it removes the text instead of ignoring it, which is the same thing, but makes the code simpler
pull/2679/head
dgtlmoon 3 months ago committed by GitHub
parent dad9760832
commit 00458b95c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1158,8 +1158,6 @@ def changedetection_app(config=None, datastore_o=None):
@login_optionally_required @login_optionally_required
def preview_page(uuid): def preview_page(uuid):
content = [] content = []
ignored_line_numbers = []
trigger_line_numbers = []
versions = [] versions = []
timestamp = None timestamp = None
@ -1176,11 +1174,10 @@ def changedetection_app(config=None, datastore_o=None):
system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver' system_uses_webdriver = datastore.data['settings']['application']['fetch_backend'] == 'html_webdriver'
extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')] extra_stylesheets = [url_for('static_content', group='styles', filename='diff.css')]
is_html_webdriver = False is_html_webdriver = False
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'): if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
is_html_webdriver = True is_html_webdriver = True
triggered_line_numbers = []
if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()): if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
flash("Preview unavailable - No fetch/check completed or triggers not reached", "error") flash("Preview unavailable - No fetch/check completed or triggers not reached", "error")
else: else:
@ -1193,31 +1190,12 @@ def changedetection_app(config=None, datastore_o=None):
try: try:
versions = list(watch.history.keys()) versions = list(watch.history.keys())
tmp = watch.get_history_snapshot(timestamp).splitlines() content = watch.get_history_snapshot(timestamp)
# Get what needs to be highlighted triggered_line_numbers = html_tools.strip_ignore_text(content=content,
ignore_rules = watch.get('ignore_text', []) + datastore.data['settings']['application']['global_ignore_text'] wordlist=watch['trigger_text'],
mode='line numbers'
# .readlines will keep the \n, but we will parse it here again, in the future tidy this up )
ignored_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=ignore_rules,
mode='line numbers'
)
trigger_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=watch['trigger_text'],
mode='line numbers'
)
# Prepare the classes and lines used in the template
i=0
for l in tmp:
classes=[]
i+=1
if i in ignored_line_numbers:
classes.append('ignored')
if i in trigger_line_numbers:
classes.append('triggered')
content.append({'line': l, 'classes': ' '.join(classes)})
except Exception as e: except Exception as e:
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''}) content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})
@ -1228,8 +1206,7 @@ def changedetection_app(config=None, datastore_o=None):
history_n=watch.history_n, history_n=watch.history_n,
extra_stylesheets=extra_stylesheets, extra_stylesheets=extra_stylesheets,
extra_title=f" - Diff - {watch.label} @ {timestamp}", extra_title=f" - Diff - {watch.label} @ {timestamp}",
ignored_line_numbers=ignored_line_numbers, triggered_line_numbers=triggered_line_numbers,
triggered_line_numbers=trigger_line_numbers,
current_diff_url=watch['url'], current_diff_url=watch['url'],
screenshot=watch.get_screenshot(), screenshot=watch.get_screenshot(),
watch=watch, watch=watch,
@ -1400,9 +1377,11 @@ def changedetection_app(config=None, datastore_o=None):
# Return a 500 error # Return a 500 error
abort(500) abort(500)
# Ajax callback
@app.route("/edit/<string:uuid>/preview-rendered", methods=['POST']) @app.route("/edit/<string:uuid>/preview-rendered", methods=['POST'])
@login_optionally_required @login_optionally_required
def watch_get_preview_rendered(uuid): def watch_get_preview_rendered(uuid):
from flask import jsonify
'''For when viewing the "preview" of the rendered text from inside of Edit''' '''For when viewing the "preview" of the rendered text from inside of Edit'''
now = time.time() now = time.time()
import brotli import brotli
@ -1434,7 +1413,7 @@ def changedetection_app(config=None, datastore_o=None):
update_handler.fetcher.content = decompressed_data update_handler.fetcher.content = decompressed_data
update_handler.fetcher.headers['content-type'] = tmp_watch.get('content-type') update_handler.fetcher.headers['content-type'] = tmp_watch.get('content-type')
try: try:
changed_detected, update_obj, contents, text_after_filter = update_handler.run_changedetection( changed_detected, update_obj, text_after_filter = update_handler.run_changedetection(
watch=tmp_watch, watch=tmp_watch,
skip_when_checksum_same=False, skip_when_checksum_same=False,
) )
@ -1448,8 +1427,32 @@ def changedetection_app(config=None, datastore_o=None):
if not text_after_filter.strip(): if not text_after_filter.strip():
text_after_filter = 'Empty content' text_after_filter = 'Empty content'
logger.trace(f"Parsed in {time.time()-now:.3f}s") # because run_changedetection always returns bytes due to saving the snapshots etc
return text_after_filter.strip() text_after_filter = text_after_filter.decode('utf-8') if isinstance(text_after_filter, bytes) else text_after_filter
do_anchor = datastore.data["settings"]["application"].get("render_anchor_tag_content", False)
trigger_line_numbers = []
try:
text_before_filter = html_tools.html_to_text(html_content=decompressed_data,
render_anchor_tag_content=do_anchor)
trigger_line_numbers = html_tools.strip_ignore_text(content=text_after_filter,
wordlist=tmp_watch['trigger_text'],
mode='line numbers'
)
except Exception as e:
text_before_filter = f"Error: {str(e)}"
logger.trace(f"Parsed in {time.time() - now:.3f}s")
return jsonify(
{
'after_filter': text_after_filter,
'before_filter': text_before_filter.decode('utf-8') if isinstance(text_before_filter, bytes) else text_before_filter,
'trigger_line_numbers': trigger_line_numbers
}
)
@app.route("/form/add/quickwatch", methods=['POST']) @app.route("/form/add/quickwatch", methods=['POST'])

@ -475,7 +475,7 @@ class processor_text_json_diff_form(commonSettingsForm):
title = StringField('Title', default='') title = StringField('Title', default='')
ignore_text = StringListField('Ignore text', [ValidateListRegex()]) ignore_text = StringListField('Remove lines containing', [ValidateListRegex()])
headers = StringDictKeyValue('Request headers') headers = StringDictKeyValue('Request headers')
body = TextAreaField('Request body', [validators.Optional()]) body = TextAreaField('Request body', [validators.Optional()])
method = SelectField('Request method', choices=valid_method, default=default_method) method = SelectField('Request method', choices=valid_method, default=default_method)

@ -1,16 +1,14 @@
from abc import abstractmethod from abc import abstractmethod
from changedetectionio.content_fetchers.base import Fetcher from changedetectionio.content_fetchers.base import Fetcher
from changedetectionio.strtobool import strtobool from changedetectionio.strtobool import strtobool
from copy import deepcopy from copy import deepcopy
from loguru import logger from loguru import logger
import hashlib import hashlib
import os
import re
import importlib import importlib
import pkgutil
import inspect import inspect
import os
import pkgutil
import re
class difference_detection_processor(): class difference_detection_processor():
@ -157,12 +155,12 @@ class difference_detection_processor():
# After init, call run_changedetection() which will do the actual change-detection # After init, call run_changedetection() which will do the actual change-detection
@abstractmethod @abstractmethod
def run_changedetection(self, watch, skip_when_checksum_same=True): def run_changedetection(self, watch, skip_when_checksum_same: bool = True):
update_obj = {'last_notification_error': False, 'last_error': False} update_obj = {'last_notification_error': False, 'last_error': False}
some_data = 'xxxxx' some_data = 'xxxxx'
update_obj["previous_md5"] = hashlib.md5(some_data.encode('utf-8')).hexdigest() update_obj["previous_md5"] = hashlib.md5(some_data.encode('utf-8')).hexdigest()
changed_detected = False changed_detected = False
return changed_detected, update_obj, ''.encode('utf-8'), b'' return changed_detected, update_obj, ''.encode('utf-8')
def find_sub_packages(package_name): def find_sub_packages(package_name):

@ -298,4 +298,4 @@ class perform_site_check(difference_detection_processor):
# Always record the new checksum # Always record the new checksum
update_obj["previous_md5"] = fetched_md5 update_obj["previous_md5"] = fetched_md5
return changed_detected, update_obj, snapshot_content.encode('utf-8').strip(), b'' return changed_detected, update_obj, snapshot_content.encode('utf-8').strip()

@ -202,7 +202,6 @@ class perform_site_check(difference_detection_processor):
render_anchor_tag_content=do_anchor, render_anchor_tag_content=do_anchor,
is_rss=is_rss) # 1874 activate the <title workaround hack is_rss=is_rss) # 1874 activate the <title workaround hack
if watch.get('trim_text_whitespace'): if watch.get('trim_text_whitespace'):
stripped_text_from_html = '\n'.join(line.strip() for line in stripped_text_from_html.replace("\n\n", "\n").splitlines()) stripped_text_from_html = '\n'.join(line.strip() for line in stripped_text_from_html.replace("\n\n", "\n").splitlines())
@ -215,8 +214,8 @@ class perform_site_check(difference_detection_processor):
stripped_text_from_html = stripped_text_from_html.replace("\n\n", "\n") stripped_text_from_html = stripped_text_from_html.replace("\n\n", "\n")
stripped_text_from_html = '\n'.join(sorted(stripped_text_from_html.splitlines(), key=lambda x: x.lower())) stripped_text_from_html = '\n'.join(sorted(stripped_text_from_html.splitlines(), key=lambda x: x.lower()))
# Re #340 - return the content before the 'ignore text' was applied # Re #340 - return the content before the 'ignore text' was applied
# Also used to calculate/show what was removed
text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8') text_content_before_ignored_filter = stripped_text_from_html.encode('utf-8')
# @todo whitespace coming from missing rtrim()? # @todo whitespace coming from missing rtrim()?
@ -241,8 +240,8 @@ class perform_site_check(difference_detection_processor):
if not rendered_diff and stripped_text_from_html: if not rendered_diff and stripped_text_from_html:
# We had some content, but no differences were found # We had some content, but no differences were found
# Store our new file as the MD5 so it will trigger in the future # Store our new file as the MD5 so it will trigger in the future
c = hashlib.md5(text_content_before_ignored_filter.translate(None, b'\r\n\t ')).hexdigest() c = hashlib.md5(stripped_text_from_html.encode('utf-8').translate(None, b'\r\n\t ')).hexdigest()
return False, {'previous_md5': c}, stripped_text_from_html.encode('utf-8'), stripped_text_from_html.encode('utf-8') return False, {'previous_md5': c}, stripped_text_from_html.encode('utf-8')
else: else:
stripped_text_from_html = rendered_diff stripped_text_from_html = rendered_diff
@ -365,4 +364,5 @@ class perform_site_check(difference_detection_processor):
if not watch.get('previous_md5'): if not watch.get('previous_md5'):
watch['previous_md5'] = fetched_md5 watch['previous_md5'] = fetched_md5
return changed_detected, update_obj, text_content_before_ignored_filter, stripped_text_from_html # stripped_text_from_html - Everything after filters and NO 'ignored' content
return changed_detected, update_obj, stripped_text_from_html

@ -0,0 +1,120 @@
(function($) {
/*
$('#code-block').highlightLines([
{
'color': '#dd0000',
'lines': [10, 12]
},
{
'color': '#ee0000',
'lines': [15, 18]
}
]);
});
*/
$.fn.highlightLines = function(configurations) {
return this.each(function() {
const $pre = $(this);
const textContent = $pre.text();
const lines = textContent.split(/\r?\n/); // Handles both \n and \r\n line endings
// Build a map of line numbers to styles
const lineStyles = {};
configurations.forEach(config => {
const { color, lines: lineNumbers } = config;
lineNumbers.forEach(lineNumber => {
lineStyles[lineNumber] = color;
});
});
// Function to escape HTML characters
function escapeHtml(text) {
return text.replace(/[&<>"'`=\/]/g, function(s) {
return "&#" + s.charCodeAt(0) + ";";
});
}
// Process each line
const processedLines = lines.map((line, index) => {
const lineNumber = index + 1; // Line numbers start at 1
const escapedLine = escapeHtml(line);
const color = lineStyles[lineNumber];
if (color) {
// Wrap the line in a span with inline style
return `<span style="background-color: ${color}">${escapedLine}</span>`;
} else {
return escapedLine;
}
});
// Join the lines back together
const newContent = processedLines.join('\n');
// Set the new content as HTML
$pre.html(newContent);
});
};
$.fn.miniTabs = function(tabsConfig, options) {
const settings = {
tabClass: 'minitab',
tabsContainerClass: 'minitabs',
activeClass: 'active',
...(options || {})
};
return this.each(function() {
const $wrapper = $(this);
const $contents = $wrapper.find('div[id]').hide();
const $tabsContainer = $('<div>', { class: settings.tabsContainerClass }).prependTo($wrapper);
// Generate tabs
Object.entries(tabsConfig).forEach(([tabTitle, contentSelector], index) => {
const $content = $wrapper.find(contentSelector);
if (index === 0) $content.show(); // Show first content by default
$('<a>', {
class: `${settings.tabClass}${index === 0 ? ` ${settings.activeClass}` : ''}`,
text: tabTitle,
'data-target': contentSelector
}).appendTo($tabsContainer);
});
// Tab click event
$tabsContainer.on('click', `.${settings.tabClass}`, function(e) {
e.preventDefault();
const $tab = $(this);
const target = $tab.data('target');
// Update active tab
$tabsContainer.find(`.${settings.tabClass}`).removeClass(settings.activeClass);
$tab.addClass(settings.activeClass);
// Show/hide content
$contents.hide();
$wrapper.find(target).show();
});
});
};
// Object to store ongoing requests by namespace
const requests = {};
$.abortiveSingularAjax = function(options) {
const namespace = options.namespace || 'default';
// Abort the current request in this namespace if it's still ongoing
if (requests[namespace]) {
requests[namespace].abort();
}
// Start a new AJAX request and store its reference in the correct namespace
requests[namespace] = $.ajax(options);
// Return the current request in case it's needed
return requests[namespace];
};
})(jQuery);

@ -1,53 +1,63 @@
function redirect_to_version(version) { function redirectToVersion(version) {
var currentUrl = window.location.href; var currentUrl = window.location.href.split('?')[0]; // Base URL without query parameters
var baseUrl = currentUrl.split('?')[0]; // Base URL without query parameters
var anchor = ''; var anchor = '';
// Check if there is an anchor // Check if there is an anchor
if (baseUrl.indexOf('#') !== -1) { if (currentUrl.indexOf('#') !== -1) {
anchor = baseUrl.substring(baseUrl.indexOf('#')); anchor = currentUrl.substring(currentUrl.indexOf('#'));
baseUrl = baseUrl.substring(0, baseUrl.indexOf('#')); currentUrl = currentUrl.substring(0, currentUrl.indexOf('#'));
} }
window.location.href = baseUrl + '?version=' + version + anchor;
window.location.href = currentUrl + '?version=' + version + anchor;
} }
document.addEventListener('keydown', function (event) { function setupDateWidget() {
var selectElement = document.getElementById('preview-version'); $(document).on('keydown', function (event) {
if (selectElement) { var $selectElement = $('#preview-version');
var selectedOption = selectElement.querySelector('option:checked'); var $selectedOption = $selectElement.find('option:selected');
if (selectedOption) {
if (event.key === 'ArrowLeft') { if ($selectedOption.length) {
if (selectedOption.previousElementSibling) { if (event.key === 'ArrowLeft' && $selectedOption.prev().length) {
redirect_to_version(selectedOption.previousElementSibling.value); redirectToVersion($selectedOption.prev().val());
} } else if (event.key === 'ArrowRight' && $selectedOption.next().length) {
} else if (event.key === 'ArrowRight') { redirectToVersion($selectedOption.next().val());
if (selectedOption.nextElementSibling) {
redirect_to_version(selectedOption.nextElementSibling.value);
}
} }
} }
} });
});
$('#preview-version').on('change', function () {
redirectToVersion($(this).val());
});
document.getElementById('preview-version').addEventListener('change', function () { var $selectedOption = $('#preview-version option:selected');
redirect_to_version(this.value);
}); if ($selectedOption.length) {
var $prevOption = $selectedOption.prev();
var $nextOption = $selectedOption.next();
var selectElement = document.getElementById('preview-version'); if ($prevOption.length) {
if (selectElement) { $('#btn-previous').attr('href', '?version=' + $prevOption.val());
var selectedOption = selectElement.querySelector('option:checked');
if (selectedOption) {
if (selectedOption.previousElementSibling) {
document.getElementById('btn-previous').href = "?version=" + selectedOption.previousElementSibling.value;
} else { } else {
document.getElementById('btn-previous').remove() $('#btn-previous').remove();
} }
if (selectedOption.nextElementSibling) {
document.getElementById('btn-next').href = "?version=" + selectedOption.nextElementSibling.value; if ($nextOption.length) {
$('#btn-next').attr('href', '?version=' + $nextOption.val());
} else { } else {
document.getElementById('btn-next').remove() $('#btn-next').remove();
} }
} }
} }
$(document).ready(function () {
if ($('#preview-version').length) {
setupDateWidget();
}
$('#diff-col > pre').highlightLines([
{
'color': '#ee0000',
'lines': triggered_line_numbers
}
]);
});

@ -12,25 +12,6 @@ function toggleOpacity(checkboxSelector, fieldSelector, inverted) {
checkbox.addEventListener('change', updateOpacity); checkbox.addEventListener('change', updateOpacity);
} }
(function($) {
// Object to store ongoing requests by namespace
const requests = {};
$.abortiveSingularAjax = function(options) {
const namespace = options.namespace || 'default';
// Abort the current request in this namespace if it's still ongoing
if (requests[namespace]) {
requests[namespace].abort();
}
// Start a new AJAX request and store its reference in the correct namespace
requests[namespace] = $.ajax(options);
// Return the current request in case it's needed
return requests[namespace];
};
})(jQuery);
function request_textpreview_update() { function request_textpreview_update() {
if (!$('body').hasClass('preview-text-enabled')) { if (!$('body').hasClass('preview-text-enabled')) {
@ -51,7 +32,19 @@ function request_textpreview_update() {
data: data, data: data,
namespace: 'watchEdit' namespace: 'watchEdit'
}).done(function (data) { }).done(function (data) {
$('#filters-and-triggers #text-preview-inner').text(data); $('#filters-and-triggers #text-preview-before-inner').text(data['before_filter']);
$('#filters-and-triggers #text-preview-inner')
.text(data['after_filter'])
.highlightLines([
{
'color': '#ee0000',
'lines': data['trigger_line_numbers']
}
]);
}).fail(function (error) { }).fail(function (error) {
if (error.statusText === 'abort') { if (error.statusText === 'abort') {
console.log('Request was aborted due to a new request being fired.'); console.log('Request was aborted due to a new request being fired.');
@ -78,6 +71,7 @@ $(document).ready(function () {
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0); const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
$("#text-preview-inner").css('max-height', (vh-300)+"px"); $("#text-preview-inner").css('max-height', (vh-300)+"px");
$("#text-preview-before-inner").css('max-height', (vh-300)+"px");
// Realtime preview of 'Filters & Text' setup // Realtime preview of 'Filters & Text' setup
var debounced_request_textpreview_update = request_textpreview_update.debounce(100); var debounced_request_textpreview_update = request_textpreview_update.debounce(100);
@ -92,6 +86,9 @@ $(document).ready(function () {
$('input:visible')[method]('keyup blur change', debounced_request_textpreview_update); $('input:visible')[method]('keyup blur change', debounced_request_textpreview_update);
$("#filters-and-triggers-tab")[method]('click', debounced_request_textpreview_update); $("#filters-and-triggers-tab")[method]('click', debounced_request_textpreview_update);
}); });
$('.minitabs-wrapper').miniTabs({
"Content after filters": "#text-preview-inner",
"Content raw/before filters": "#text-preview-before-inner"
});
}); });

@ -0,0 +1,37 @@
.minitabs-wrapper {
width: 100%;
> div[id] {
padding: 20px;
border: 1px solid #ccc;
border-top: none;
}
.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:hover {
background-color: #ddd;
}
.minitab.active {
background-color: #fff;
font-weight: bold;
}
}

@ -1,3 +1,5 @@
@import "minitabs";
body.preview-text-enabled { body.preview-text-enabled {
#filters-and-triggers > div { #filters-and-triggers > div {
display: flex; /* Establishes Flexbox layout */ display: flex; /* Establishes Flexbox layout */
@ -19,18 +21,22 @@ body.preview-text-enabled {
#text-preview { #text-preview {
position: sticky; position: sticky;
top: 25px; padding-top: 1rem;
display: block !important; display: block !important;
} }
#activate-text-preview {
background-color: var(--color-grey-500);
}
/* actual preview area */ /* actual preview area */
#text-preview-inner { .monospace-preview {
background: var(--color-grey-900); background: var(--color-grey-900);
border: 1px solid var(--color-grey-600); border: 1px solid var(--color-grey-600);
padding: 1rem; padding: 1rem;
color: #333; color: var(--color-grey-100);
font-family: "Courier New", Courier, monospace; /* Sets the font to a monospace type */ font-family: "Courier New", Courier, monospace; /* Sets the font to a monospace type */
font-size: 12px; font-size: 70%;
overflow-x: scroll; overflow-x: scroll;
white-space: pre-wrap; /* Preserves whitespace and line breaks like <pre> */ white-space: pre-wrap; /* Preserves whitespace and line breaks like <pre> */
overflow-wrap: break-word; /* Allows long words to break and wrap to the next line */ overflow-wrap: break-word; /* Allows long words to break and wrap to the next line */
@ -40,6 +46,6 @@ body.preview-text-enabled {
#activate-text-preview { #activate-text-preview {
right: 0; right: 0;
position: absolute; position: absolute;
z-index: 0; z-index: 3;
box-shadow: 1px 1px 4px var(--color-shadow-jump); box-shadow: 1px 1px 4px var(--color-shadow-jump);
} }

@ -428,6 +428,32 @@ html[data-darkmode="true"] #toggle-light-mode .icon-dark {
fill: #ff0000 !important; fill: #ff0000 !important;
transition: all ease 0.3s !important; } transition: all ease 0.3s !important; }
.minitabs-wrapper {
width: 100%; }
.minitabs-wrapper > div[id] {
padding: 20px;
border: 1px solid #ccc;
border-top: none; }
.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 0.3s; }
.minitabs-wrapper .minitab:hover {
background-color: #ddd; }
.minitabs-wrapper .minitab.active {
background-color: #fff;
font-weight: bold; }
body.preview-text-enabled { body.preview-text-enabled {
/* layout of the page */ /* layout of the page */
/* actual preview area */ } /* actual preview area */ }
@ -447,16 +473,18 @@ body.preview-text-enabled {
display: none; } display: none; }
body.preview-text-enabled #text-preview { body.preview-text-enabled #text-preview {
position: sticky; position: sticky;
top: 25px; padding-top: 1rem;
display: block !important; } display: block !important; }
body.preview-text-enabled #text-preview-inner { body.preview-text-enabled #activate-text-preview {
background-color: var(--color-grey-500); }
body.preview-text-enabled .monospace-preview {
background: var(--color-grey-900); background: var(--color-grey-900);
border: 1px solid var(--color-grey-600); border: 1px solid var(--color-grey-600);
padding: 1rem; padding: 1rem;
color: #333; color: var(--color-grey-100);
font-family: "Courier New", Courier, monospace; font-family: "Courier New", Courier, monospace;
/* Sets the font to a monospace type */ /* Sets the font to a monospace type */
font-size: 12px; font-size: 70%;
overflow-x: scroll; overflow-x: scroll;
white-space: pre-wrap; white-space: pre-wrap;
/* Preserves whitespace and line breaks like <pre> */ /* Preserves whitespace and line breaks like <pre> */
@ -466,7 +494,7 @@ body.preview-text-enabled {
#activate-text-preview { #activate-text-preview {
right: 0; right: 0;
position: absolute; position: absolute;
z-index: 0; z-index: 3;
box-shadow: 1px 1px 4px var(--color-shadow-jump); } box-shadow: 1px 1px 4px var(--color-shadow-jump); }
body { body {

@ -24,7 +24,7 @@
const watch_visual_selector_data_url="{{url_for('static_content', group='visual_selector_data', 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'] }}"; const default_system_fetch_backend="{{ settings_application['fetch_backend'] }}";
</script> </script>
<script src="{{url_for('static_content', group='js', filename='plugins.js')}}" defer></script>
<script src="{{url_for('static_content', group='js', filename='watch-settings.js')}}" defer></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='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='notifications.js')}}" defer></script>
@ -371,10 +371,10 @@ nav
") }} ") }}
<span class="pure-form-message-inline"> <span class="pure-form-message-inline">
<ul> <ul>
<li>Matching text will be <strong>removed</strong> from the text snapshot</li>
<li>Each line processed separately, any line matching will be ignored (removed before creating the checksum)</li> <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>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>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> </ul>
</span> </span>
@ -422,14 +422,21 @@ Unavailable") }}
<script> <script>
const preview_text_edit_filters_url="{{url_for('watch_get_preview_rendered', uuid=uuid)}}"; const preview_text_edit_filters_url="{{url_for('watch_get_preview_rendered', uuid=uuid)}}";
</script> </script>
<span><strong>Preview of the text that is used for changedetection after all filters run.</strong></span><br> <br>
{#<div id="text-preview-controls"><span id="text-preview-refresh" class="pure-button button-xsmall">Refresh</span></div>#} {#<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> <div class="minitabs-wrapper">
</p> <div id="text-preview-inner" class="monospace-preview">
<p>Loading...</p>
</div>
<div id="text-preview-before-inner" style="display: none;" class="monospace-preview">
<p>Loading...</p>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{# rendered sub Template #} {# rendered sub Template #}
{% if extra_form_content %} {% if extra_form_content %}

@ -3,11 +3,13 @@
{% block content %} {% block content %}
<script> <script>
const screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid)}}"; const screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid)}}";
const triggered_line_numbers = {{ triggered_line_numbers|tojson }};
{% if last_error_screenshot %} {% if last_error_screenshot %}
const error_screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}"; const error_screenshot_url = "{{url_for('static_content', group='screenshot', filename=uuid, error_screenshot=1) }}";
{% endif %} {% endif %}
const highlight_submit_ignore_url = "{{url_for('highlight_submit_ignore_url', uuid=uuid)}}"; const highlight_submit_ignore_url = "{{url_for('highlight_submit_ignore_url', uuid=uuid)}}";
</script> </script>
<script src="{{url_for('static_content', group='js', filename='plugins.js')}}"></script>
<script src="{{ url_for('static_content', group='js', filename='diff-overview.js') }}" defer></script> <script src="{{ url_for('static_content', group='js', filename='diff-overview.js') }}" defer></script>
<script src="{{ url_for('static_content', group='js', filename='preview.js') }}" defer></script> <script src="{{ url_for('static_content', group='js', filename='preview.js') }}" defer></script>
<script src="{{ url_for('static_content', group='js', filename='tabs.js') }}" defer></script> <script src="{{ url_for('static_content', group='js', filename='tabs.js') }}" defer></script>
@ -67,16 +69,15 @@
<div class="tab-pane-inner" id="text"> <div class="tab-pane-inner" id="text">
<div class="snapshot-age">{{ current_version|format_timestamp_timeago }}</div> <div class="snapshot-age">{{ current_version|format_timestamp_timeago }}</div>
<span class="ignored">Grey lines are ignored</span> <span class="triggered">Blue lines are triggers</span>
<span class="tip"><strong>Pro-tip</strong>: Highlight text to add to ignore filters</span> <span class="tip"><strong>Pro-tip</strong>: Highlight text to add to ignore filters</span>
<table> <table>
<tbody> <tbody>
<tr> <tr>
<td id="diff-col" class="highlightable-filter"> <td id="diff-col" class="highlightable-filter">
{% for row in content %} <pre style="border-left: 2px solid #ddd;">
<div class="{{ row.classes }}">{{ row.line }}</div> {{ content }}
{% endfor %} </pre>
</td> </td>
</tr> </tr>
</tbody> </tbody>

@ -172,11 +172,11 @@ nav
<span class="pure-form-message-inline">Note: This is applied globally in addition to the per-watch rules.</span><br> <span class="pure-form-message-inline">Note: This is applied globally in addition to the per-watch rules.</span><br>
<span class="pure-form-message-inline"> <span class="pure-form-message-inline">
<ul> <ul>
<li>Matching text will be <strong>removed</strong> from the text snapshot</li>
<li>Note: This is applied globally in addition to the per-watch rules.</li> <li>Note: This is applied globally in addition to the per-watch rules.</li>
<li>Each line processed separately, any line matching will be ignored (removed before creating the checksum)</li> <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>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>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> </ul>
</span> </span>
</fieldset> </fieldset>

@ -44,7 +44,7 @@ def test_select_custom(client, live_server, measure_memory_usage):
follow_redirects=True follow_redirects=True
) )
# We should see something via proxy # We should see something via proxy
assert b'<div class=""> - 0.' in res.data assert b' - 0.' in res.data
# #
# Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default # Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default

@ -39,9 +39,8 @@ def test_setup(client, live_server, measure_memory_usage):
live_server_setup(live_server) live_server_setup(live_server)
def test_check_removed_line_contains_trigger(client, live_server, measure_memory_usage): def test_check_removed_line_contains_trigger(client, live_server, measure_memory_usage):
#live_server_setup(live_server)
# Give the endpoint time to spin up # Give the endpoint time to spin up
time.sleep(1)
set_original() set_original()
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
@ -152,7 +151,9 @@ def test_check_add_line_contains_trigger(client, live_server, measure_memory_usa
# A line thats not the trigger should not trigger anything # A line thats not the trigger should not trigger anything
res = client.get(url_for("form_watch_checknow"), follow_redirects=True) res = client.get(url_for("form_watch_checknow"), follow_redirects=True)
assert b'1 watches queued for rechecking.' in res.data assert b'1 watches queued for rechecking.' in res.data
wait_for_all_checks(client) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' not in res.data assert b'unviewed' not in res.data

@ -115,9 +115,9 @@ def test_check_filter_multiline(client, live_server, measure_memory_usage):
# Plaintext that doesnt look like a regex should match also # Plaintext that doesnt look like a regex should match also
assert b'and this should be' in res.data assert b'and this should be' in res.data
assert b'<div class="">Something' in res.data assert b'Something' in res.data
assert b'<div class="">across 6 billion multiple' in res.data assert b'across 6 billion multiple' in res.data
assert b'<div class="">lines' in res.data assert b'lines' in res.data
# but the last one, which also says 'lines' shouldnt be here (non-greedy match checking) # but the last one, which also says 'lines' shouldnt be here (non-greedy match checking)
assert b'aaand something lines' not in res.data assert b'aaand something lines' not in res.data
@ -183,20 +183,19 @@ def test_check_filter_and_regex_extract(client, live_server, measure_memory_usag
follow_redirects=True follow_redirects=True
) )
# Class will be blank for now because the frontend didnt apply the diff assert b'1000 online' in res.data
assert b'<div class="">1000 online' in res.data
# All regex matching should be here # All regex matching should be here
assert b'<div class="">2000 online' in res.data assert b'2000 online' in res.data
# Both regexs should be here # Both regexs should be here
assert b'<div class="">80 guests' in res.data assert b'80 guests' in res.data
# Regex with flag handling should be here # Regex with flag handling should be here
assert b'<div class="">SomeCase insensitive 3456' in res.data assert b'SomeCase insensitive 3456' in res.data
# Singular group from /somecase insensitive (345\d)/i # Singular group from /somecase insensitive (345\d)/i
assert b'<div class="">3456' in res.data assert b'3456' in res.data
# Regex with multiline flag handling should be here # Regex with multiline flag handling should be here

@ -23,7 +23,7 @@ def set_original_ignore_response():
f.write(test_return_data) f.write(test_return_data)
def test_highlight_ignore(client, live_server, measure_memory_usage): def test_ignore(client, live_server, measure_memory_usage):
live_server_setup(live_server) live_server_setup(live_server)
set_original_ignore_response() set_original_ignore_response()
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
@ -51,9 +51,9 @@ def test_highlight_ignore(client, live_server, measure_memory_usage):
# Should return a link # Should return a link
assert b'href' in res.data assert b'href' in res.data
# And it should register in the preview page # It should not be in the preview anymore
res = client.get(url_for("preview_page", uuid=uuid)) res = client.get(url_for("preview_page", uuid=uuid))
assert b'<div class="ignored">oh yeah 456' in res.data assert b'<div class="ignored">oh yeah 456' not in res.data
# Should be in base.html # Should be in base.html
assert b'csrftoken' in res.data assert b'csrftoken' in res.data

@ -79,14 +79,14 @@ def set_modified_ignore_response():
f.write(test_return_data) f.write(test_return_data)
# Ignore text now just removes it entirely, is a LOT more simpler code this way
def test_check_ignore_text_functionality(client, live_server, measure_memory_usage): def test_check_ignore_text_functionality(client, live_server, measure_memory_usage):
# Use a mix of case in ZzZ to prove it works case-insensitive. # Use a mix of case in ZzZ to prove it works case-insensitive.
ignore_text = "XXXXX\r\nYYYYY\r\nzZzZZ\r\nnew ignore stuff" ignore_text = "XXXXX\r\nYYYYY\r\nzZzZZ\r\nnew ignore stuff"
set_original_ignore_response() set_original_ignore_response()
# Give the endpoint time to spin up
time.sleep(1)
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
@ -151,12 +151,10 @@ def test_check_ignore_text_functionality(client, live_server, measure_memory_usa
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
# Check the preview/highlighter, we should be able to see what we ignored, but it should be highlighted
# We only introduce the "modified" content that includes what we ignore so we can prove the newest version also displays
# at /preview
res = client.get(url_for("preview_page", uuid="first")) res = client.get(url_for("preview_page", uuid="first"))
# We should be able to see what we ignored
assert b'<div class="ignored">new ignore stuff' in res.data # Should no longer be in the preview
assert b'new ignore stuff' not in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

@ -499,7 +499,7 @@ def test_correct_header_detect(client, live_server, measure_memory_usage):
) )
assert b'&#34;hello&#34;: 123,' in res.data assert b'&#34;hello&#34;: 123,' in res.data
assert b'&#34;world&#34;: 123</div>' in res.data assert b'&#34;world&#34;: 123' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

@ -2,7 +2,7 @@
import time import time
from flask import url_for from flask import url_for
from . util import live_server_setup from .util import live_server_setup, wait_for_all_checks
def set_original_ignore_response(): def set_original_ignore_response():
@ -59,12 +59,9 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
live_server_setup(live_server) live_server_setup(live_server)
sleep_time_for_fetch_thread = 3
trigger_text = "Add to cart" trigger_text = "Add to cart"
set_original_ignore_response() set_original_ignore_response()
# Give the endpoint time to spin up
time.sleep(1)
# Add our URL to the import page # Add our URL to the import page
test_url = url_for('test_endpoint', _external=True) test_url = url_for('test_endpoint', _external=True)
@ -89,14 +86,14 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
) )
assert b"Updated watch." in res.data assert b"Updated watch." in res.data
wait_for_all_checks(client)
# Check it saved # Check it saved
res = client.get( res = client.get(
url_for("edit_page", uuid="first"), url_for("edit_page", uuid="first"),
) )
assert bytes(trigger_text.encode('utf-8')) in res.data assert bytes(trigger_text.encode('utf-8')) in res.data
# Give the thread time to pick it up
time.sleep(sleep_time_for_fetch_thread)
# so that we set the state to 'unviewed' after all the edits # so that we set the state to 'unviewed' after all the edits
client.get(url_for("diff_history_page", uuid="first")) client.get(url_for("diff_history_page", uuid="first"))
@ -104,8 +101,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up wait_for_all_checks(client)
time.sleep(sleep_time_for_fetch_thread)
# It should report nothing found (no new 'unviewed' class) # It should report nothing found (no new 'unviewed' class)
res = client.get(url_for("index")) res = client.get(url_for("index"))
@ -117,19 +113,17 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
# Trigger a check # Trigger a check
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("form_watch_checknow"), follow_redirects=True)
# Give the thread time to pick it up wait_for_all_checks(client)
time.sleep(sleep_time_for_fetch_thread)
# It should report nothing found (no new 'unviewed' class) # It should report nothing found (no new 'unviewed' class)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' not in res.data assert b'unviewed' not in res.data
# Now set the content which contains the trigger text # Now set the content which contains the trigger text
time.sleep(sleep_time_for_fetch_thread)
set_modified_with_trigger_text_response() set_modified_with_trigger_text_response()
client.get(url_for("form_watch_checknow"), follow_redirects=True) client.get(url_for("form_watch_checknow"), follow_redirects=True)
time.sleep(sleep_time_for_fetch_thread) wait_for_all_checks(client)
res = client.get(url_for("index")) res = client.get(url_for("index"))
assert b'unviewed' in res.data assert b'unviewed' in res.data
@ -142,4 +136,7 @@ def test_trigger_functionality(client, live_server, measure_memory_usage):
res = client.get(url_for("preview_page", uuid="first")) res = client.get(url_for("preview_page", uuid="first"))
# We should be able to see what we triggered on # We should be able to see what we triggered on
assert b'<div class="triggered">Add to cart' in res.data # The JS highlighter should tell us which lines (also used in the live-preview)
assert b'const triggered_line_numbers = [6]' in res.data
assert b'Add to cart' in res.data

@ -161,8 +161,8 @@ def test_check_xpath_text_function_utf8(client, live_server, measure_memory_usag
follow_redirects=True follow_redirects=True
) )
assert b'<div class="">Stock Alert (UK): RPi CM4' in res.data assert b'Stock Alert (UK): RPi CM4' in res.data
assert b'<div class="">Stock Alert (UK): Big monitor' in res.data assert b'Stock Alert (UK): Big monitor' in res.data
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True) res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
assert b'Deleted' in res.data assert b'Deleted' in res.data

@ -278,7 +278,7 @@ class update_worker(threading.Thread):
update_handler.call_browser() update_handler.call_browser()
changed_detected, update_obj, contents, content_after_filters = update_handler.run_changedetection( changed_detected, update_obj, contents = update_handler.run_changedetection(
watch=watch, watch=watch,
skip_when_checksum_same=skip_when_same_checksum, skip_when_checksum_same=skip_when_same_checksum,
) )

Loading…
Cancel
Save