Compare commits

..

5 Commits

Author SHA1 Message Date
dgtlmoon
108cdf84a5 0.49.11
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-04-03 18:54:20 +02:00
dgtlmoon
8c6f6f1578 0.49.10 2025-04-03 11:17:14 +02:00
dgtlmoon
df4ffaaff8 Update README.md
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
2025-04-03 08:55:59 +02:00
dgtlmoon
d522c65e50 UI - "Conditions" section, making the Conditions setup table work better on mobile/responsive
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
2025-04-02 18:06:39 +02:00
dgtlmoon
c3b2a8b019 Adding a GC memory cleanup (releases cached libxml memory and others) (#3079)
Some checks failed
Build and push containers / metadata (push) Has been cancelled
Build and push containers / build-push-containers (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Build distribution 📦 (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Test the built 📦 package works basically. (push) Has been cancelled
Publish Python 🐍distribution 📦 to PyPI and TestPyPI / Publish Python 🐍 distribution 📦 to PyPI (push) Has been cancelled
ChangeDetection.io App Test / lint-code (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-10 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-11 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-12 (push) Has been cancelled
ChangeDetection.io App Test / test-application-3-13 (push) Has been cancelled
ChangeDetection.io Container Build Test / test-container-build (push) Has been cancelled
2025-04-01 15:48:14 +02:00
13 changed files with 448 additions and 46 deletions

View File

@@ -89,7 +89,7 @@ _Need an actual Chrome runner with Javascript support? We support fetching via W
#### Key Features
- Lots of trigger filters, such as "Trigger on text", "Remove text by selector", "Ignore text", "Extract text", also using regular-expressions!
- Target elements with xPath(1.0) and CSS Selectors, Easily monitor complex JSON with JSONPath or jq
- Target elements with xPath 1 and xPath 2, CSS Selectors, Easily monitor complex JSON with JSONPath or jq
- Switch between fast non-JS and Chrome JS based "fetchers"
- Track changes in PDF files (Monitor text changed in the PDF, Also monitor PDF filesize and checksums)
- Easily specify how often a site should be checked

View File

@@ -2,7 +2,7 @@
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
__version__ = '0.49.9'
__version__ = '0.49.11'
from changedetectionio.strtobool import strtobool
from json.decoder import JSONDecodeError
@@ -11,6 +11,7 @@ os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
import eventlet
import eventlet.wsgi
import getopt
import platform
import signal
import socket
import sys
@@ -144,6 +145,19 @@ def main():
signal.signal(signal.SIGTERM, sigshutdown_handler)
signal.signal(signal.SIGINT, sigshutdown_handler)
# Custom signal handler for memory cleanup
def sigusr_clean_handler(_signo, _stack_frame):
from changedetectionio.gc_cleanup import memory_cleanup
logger.info('SIGUSR1 received: Running memory cleanup')
return memory_cleanup(app)
# Register the SIGUSR1 signal handler
# Only register the signal handler if running on Linux
if platform.system() == "Linux":
signal.signal(signal.SIGUSR1, sigusr_clean_handler)
else:
logger.info("SIGUSR1 handler only registered on Linux, skipped.")
# Go into cleanup mode
if do_cleanup:

View File

@@ -8,7 +8,7 @@ from . import default_plugin
# List of all supported JSON Logic operators
operator_choices = [
(None, "Choose one"),
(None, "Choose one - Operator"),
(">", "Greater Than"),
("<", "Less Than"),
(">=", "Greater Than or Equal To"),
@@ -21,7 +21,7 @@ operator_choices = [
# Fields available in the rules
field_choices = [
(None, "Choose one"),
(None, "Choose one - Field"),
]
# The data we will feed the JSON Rules to see if it passes the test/conditions or not

View File

@@ -19,7 +19,7 @@ class ConditionFormRow(Form):
validators=[validators.Optional()]
)
value = StringField("Value", validators=[validators.Optional()])
value = StringField("Value", validators=[validators.Optional()], render_kw={"placeholder": "A value"})
def validate(self, extra_validators=None):
# First, run the default validators

View File

@@ -447,6 +447,16 @@ def changedetection_app(config=None, datastore_o=None):
import changedetectionio.blueprint.watchlist as watchlist
app.register_blueprint(watchlist.construct_blueprint(datastore=datastore, update_q=update_q, queuedWatchMetaData=queuedWatchMetaData), url_prefix='')
# Memory cleanup endpoint
@app.route('/gc-cleanup', methods=['GET'])
@login_optionally_required
def gc_cleanup():
from changedetectionio.gc_cleanup import memory_cleanup
from flask import jsonify
result = memory_cleanup(app)
return jsonify({"status": "success", "message": "Memory cleanup completed", "result": result})
# @todo handle ctrl break
ticker_thread = threading.Thread(target=ticker_thread_check_time_launch_checks).start()

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env python3
import ctypes
import gc
import re
import psutil
import sys
import threading
import importlib
from loguru import logger
def memory_cleanup(app=None):
"""
Perform comprehensive memory cleanup operations and log memory usage
at each step with nicely formatted numbers.
Args:
app: Optional Flask app instance for clearing Flask-specific caches
Returns:
str: Status message
"""
# Get current process
process = psutil.Process()
# Log initial memory usage with nicely formatted numbers
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"Memory cleanup started - Current memory usage: {current_memory:,.2f} MB")
# 1. Standard garbage collection - force full collection on all generations
gc.collect(0) # Collect youngest generation
gc.collect(1) # Collect middle generation
gc.collect(2) # Collect oldest generation
# Run full collection again to ensure maximum cleanup
gc.collect()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After full gc.collect() - Memory usage: {current_memory:,.2f} MB")
# 3. Call libc's malloc_trim to release memory back to the OS
libc = ctypes.CDLL("libc.so.6")
libc.malloc_trim(0)
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After malloc_trim(0) - Memory usage: {current_memory:,.2f} MB")
# 4. Clear Python's regex cache
re.purge()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After re.purge() - Memory usage: {current_memory:,.2f} MB")
# 5. Reset thread-local storage
# Create a new thread local object to encourage cleanup of old ones
threading.local()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After threading.local() - Memory usage: {current_memory:,.2f} MB")
# 6. Clear sys.intern cache if Python version supports it
try:
sys.intern.clear()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After sys.intern.clear() - Memory usage: {current_memory:,.2f} MB")
except (AttributeError, TypeError):
logger.debug("sys.intern.clear() not supported in this Python version")
# 7. Clear XML/lxml caches if available
try:
# Check if lxml.etree is in use
lxml_etree = sys.modules.get('lxml.etree')
if lxml_etree:
# Clear module-level caches
if hasattr(lxml_etree, 'clear_error_log'):
lxml_etree.clear_error_log()
# Check for _ErrorLog and _RotatingErrorLog objects and clear them
for obj in gc.get_objects():
if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
class_name = obj.__class__.__name__
if class_name in ('_ErrorLog', '_RotatingErrorLog', '_DomainErrorLog') and hasattr(obj, 'clear'):
try:
obj.clear()
except (AttributeError, TypeError):
pass
# Clear Element objects which can hold references to documents
elif class_name in ('_Element', 'ElementBase') and hasattr(obj, 'clear'):
try:
obj.clear()
except (AttributeError, TypeError):
pass
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After lxml.etree cleanup - Memory usage: {current_memory:,.2f} MB")
# Check if lxml.html is in use
lxml_html = sys.modules.get('lxml.html')
if lxml_html:
# Clear HTML-specific element types
for obj in gc.get_objects():
if hasattr(obj, '__class__') and hasattr(obj.__class__, '__name__'):
class_name = obj.__class__.__name__
if class_name in ('HtmlElement', 'FormElement', 'InputElement',
'SelectElement', 'TextareaElement', 'CheckboxGroup',
'RadioGroup', 'MultipleSelectOptions', 'FieldsDict') and hasattr(obj, 'clear'):
try:
obj.clear()
except (AttributeError, TypeError):
pass
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After lxml.html cleanup - Memory usage: {current_memory:,.2f} MB")
except (ImportError, AttributeError):
logger.debug("lxml cleanup not applicable")
# 8. Clear JSON parser caches if applicable
try:
# Check if json module is being used and try to clear its cache
json_module = sys.modules.get('json')
if json_module and hasattr(json_module, '_default_encoder'):
json_module._default_encoder.markers.clear()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After JSON parser cleanup - Memory usage: {current_memory:,.2f} MB")
except (AttributeError, KeyError):
logger.debug("JSON cleanup not applicable")
# 9. Force Python's memory allocator to release unused memory
try:
if hasattr(sys, 'pypy_version_info'):
# PyPy has different memory management
gc.collect()
else:
# CPython - try to release unused memory
ctypes.pythonapi.PyGC_Collect()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After PyGC_Collect - Memory usage: {current_memory:,.2f} MB")
except (AttributeError, TypeError):
logger.debug("PyGC_Collect not supported")
# 10. Clear Flask-specific caches if applicable
if app:
try:
# Clear Flask caches if they exist
for key in list(app.config.get('_cache', {}).keys()):
app.config['_cache'].pop(key, None)
# Clear Jinja2 template cache if available
if hasattr(app, 'jinja_env') and hasattr(app.jinja_env, 'cache'):
app.jinja_env.cache.clear()
current_memory = process.memory_info().rss / 1024 / 1024
logger.debug(f"After Flask cache clear - Memory usage: {current_memory:,.2f} MB")
except (AttributeError, KeyError):
logger.debug("No Flask cache to clear")
# Final garbage collection pass
gc.collect()
libc.malloc_trim(0)
# Log final memory usage
final_memory = process.memory_info().rss / 1024 / 1024
logger.info(f"Memory cleanup completed - Final memory usage: {final_memory:,.2f} MB")
return "cleaned"

View File

@@ -8,7 +8,7 @@ $(document).ready(function () {
$(".addRuleRow").on("click", function(e) {
e.preventDefault();
let currentRow = $(this).closest("tr");
let currentRow = $(this).closest(".fieldlist-row");
// Clone without events
let newRow = currentRow.clone(false);
@@ -29,8 +29,8 @@ $(document).ready(function () {
e.preventDefault();
// Only remove if there's more than one row
if ($("#rulesTable tbody tr").length > 1) {
$(this).closest("tr").remove();
if ($("#rulesTable .fieldlist-row").length > 1) {
$(this).closest(".fieldlist-row").remove();
reindexRules();
}
});
@@ -39,7 +39,7 @@ $(document).ready(function () {
$(".verifyRuleRow").on("click", function(e) {
e.preventDefault();
let row = $(this).closest("tr");
let row = $(this).closest(".fieldlist-row");
let field = row.find("select[name$='field']").val();
let operator = row.find("select[name$='operator']").val();
let value = row.find("input[name$='value']").val();
@@ -128,7 +128,7 @@ $(document).ready(function () {
$(".addRuleRow, .removeRuleRow, .verifyRuleRow").off("click");
// Reindex all form elements
$("#rulesTable tbody tr").each(function(index) {
$("#rulesTable .fieldlist-row").each(function(index) {
$(this).find("select, input").each(function() {
let oldName = $(this).attr("name");
let oldId = $(this).attr("id");

View File

@@ -0,0 +1,135 @@
/* Styles for the flexbox-based table replacement for conditions */
.fieldlist_formfields {
width: 100%;
background-color: var(--color-background, #fff);
border-radius: 4px;
border: 1px solid var(--color-border-table-cell, #cbcbcb);
/* Header row */
.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-header-cell {
flex: 1;
padding: 0.5em 1em;
text-align: left;
&:last-child {
flex: 0 0 120px; /* Fixed width for actions column */
}
}
/* Body rows */
.fieldlist-body {
display: flex;
flex-direction: column;
}
.fieldlist-row {
display: flex;
border-bottom: 1px solid var(--color-border-table-cell, #cbcbcb);
&:last-child {
border-bottom: none;
}
&:nth-child(2n-1) {
background-color: var(--color-table-stripe, #f2f2f2);
}
&.error-row {
background-color: var(--color-error-input, #ffdddd);
}
}
.fieldlist-cell {
flex: 1;
padding: 0.5em 1em;
display: flex;
flex-direction: column;
justify-content: center;
/* Make inputs take up full width of their cell */
input, select {
width: 100%;
}
&.fieldlist-actions {
flex: 0 0 120px; /* Fixed width for actions column */
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
}
/* Error styling */
ul.errors {
margin-top: 0.5em;
margin-bottom: 0;
padding: 0.5em;
background-color: var(--color-error-background-snapshot-age, #ffdddd);
border-radius: 4px;
list-style-position: inside;
}
/* Responsive styles */
@media only screen and (max-width: 760px) {
.fieldlist-header, .fieldlist-row {
flex-direction: column;
}
.fieldlist-header-cell {
display: none;
}
.fieldlist-row {
padding: 0.5em 0;
border-bottom: 2px solid var(--color-border-table-cell, #cbcbcb);
}
.fieldlist-cell {
padding: 0.25em 0.5em;
&.fieldlist-actions {
flex: 1;
justify-content: flex-start;
padding-top: 0.5em;
}
}
/* Add some spacing between fields on mobile */
.fieldlist-cell:not(:last-child) {
margin-bottom: 0.5em;
}
/* Label each cell on mobile view */
.fieldlist-cell::before {
content: attr(data-label);
font-weight: bold;
margin-bottom: 0.25em;
}
}
}
/* Button styling */
.fieldlist_formfields {
.addRuleRow, .removeRuleRow, .verifyRuleRow {
cursor: pointer;
border: none;
padding: 4px 8px;
border-radius: 3px;
font-weight: bold;
background-color: #aaa;
color: var(--color-foreground-text, #fff);
&:hover {
background-color: #999;
}
}
}

View File

@@ -14,6 +14,7 @@
@import "parts/_love";
@import "parts/preview_text_filter";
@import "parts/_edit";
@import "parts/_conditions_table";
body {
color: var(--color-text);

View File

@@ -530,6 +530,99 @@ ul#conditions_match_logic {
ul#conditions_match_logic li {
padding-right: 1em; }
/* Styles for the flexbox-based table replacement for conditions */
.fieldlist_formfields {
width: 100%;
background-color: var(--color-background, #fff);
border-radius: 4px;
border: 1px solid var(--color-border-table-cell, #cbcbcb);
/* Header row */
/* Body rows */
/* Error styling */
/* Responsive styles */ }
.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: 0.5em 1em;
text-align: left; }
.fieldlist_formfields .fieldlist-header-cell:last-child {
flex: 0 0 120px;
/* Fixed width for actions column */ }
.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: 0.5em 1em;
display: flex;
flex-direction: column;
justify-content: center;
/* Make inputs take up full width of their cell */ }
.fieldlist_formfields .fieldlist-cell input, .fieldlist_formfields .fieldlist-cell select {
width: 100%; }
.fieldlist_formfields .fieldlist-cell.fieldlist-actions {
flex: 0 0 120px;
/* Fixed width for actions column */
display: flex;
flex-direction: row;
align-items: center;
gap: 4px; }
.fieldlist_formfields ul.errors {
margin-top: 0.5em;
margin-bottom: 0;
padding: 0.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 {
/* Add some spacing between fields on mobile */
/* Label each cell on mobile view */ }
.fieldlist_formfields .fieldlist-header, .fieldlist_formfields .fieldlist-row {
flex-direction: column; }
.fieldlist_formfields .fieldlist-header-cell {
display: none; }
.fieldlist_formfields .fieldlist-row {
padding: 0.5em 0;
border-bottom: 2px solid var(--color-border-table-cell, #cbcbcb); }
.fieldlist_formfields .fieldlist-cell {
padding: 0.25em 0.5em; }
.fieldlist_formfields .fieldlist-cell.fieldlist-actions {
flex: 1;
justify-content: flex-start;
padding-top: 0.5em; }
.fieldlist_formfields .fieldlist-cell:not(:last-child) {
margin-bottom: 0.5em; }
.fieldlist_formfields .fieldlist-cell::before {
content: attr(data-label);
font-weight: bold;
margin-bottom: 0.25em; } }
/* Button styling */
.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; }
body {
color: var(--color-text);
background: var(--color-background-page);

View File

@@ -61,21 +61,20 @@
{{ field(**kwargs)|safe }}
{% endmacro %}
{% macro render_fieldlist_of_formfields_as_table(fieldlist, table_id="rulesTable") %}
<table class="fieldlist_formfields pure-table" id="{{ table_id }}">
<thead>
<tr>
{% for subfield in fieldlist[0] %}
<th>{{ subfield.label }}</th>
{% endfor %}
<th>Actions</th>
</tr>
</thead>
<tbody>
{% macro render_conditions_fieldlist_of_formfields_as_table(fieldlist, table_id="rulesTable") %}
<div class="fieldlist_formfields" id="{{ table_id }}">
<div class="fieldlist-header">
{% for subfield in fieldlist[0] %}
<div class="fieldlist-header-cell">{{ subfield.label }}</div>
{% endfor %}
<div class="fieldlist-header-cell">Actions</div>
</div>
<div class="fieldlist-body">
{% for form_row in fieldlist %}
<tr {% if form_row.errors %} class="error-row" {% endif %}>
<div class="fieldlist-row {% if form_row.errors %}error-row{% endif %}">
{% for subfield in form_row %}
<td>
<div class="fieldlist-cell">
{{ subfield()|safe }}
{% if subfield.errors %}
<ul class="errors">
@@ -84,17 +83,17 @@
{% endfor %}
</ul>
{% endif %}
</td>
</div>
{% endfor %}
<td>
<button type="button" class="addRuleRow">+</button>
<button type="button" class="removeRuleRow">-</button>
<div class="fieldlist-cell fieldlist-actions">
<button type="button" class="addRuleRow" title="Add a row/rule after">+</button>
<button type="button" class="removeRuleRow" title="Remove this row/rule">-</button>
<button type="button" class="verifyRuleRow" title="Verify this rule against current snapshot"></button>
</td>
</tr>
</div>
</div>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endmacro %}

View File

@@ -1,6 +1,6 @@
{% extends 'base.html' %}
{% block content %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, playwright_warning, only_webdriver_type_watches_warning, render_fieldlist_of_formfields_as_table %}
{% from '_helpers.html' import render_field, render_checkbox_field, render_button, render_time_schedule_form, playwright_warning, only_webdriver_type_watches_warning, render_conditions_fieldlist_of_formfields_as_table %}
{% 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>
@@ -289,21 +289,9 @@ Math: {{ 1 + 1 }}") }}
<script>
const verify_condition_rule_url="{{url_for('conditions.verify_condition_single_rule', watch_uuid=uuid)}}";
</script>
<style>
.verifyRuleRow {
background-color: #4caf50;
color: white;
border: none;
cursor: pointer;
font-weight: bold;
}
.verifyRuleRow:hover {
background-color: #45a049;
}
</style>
<div class="pure-control-group">
{{ render_field(form.conditions_match_logic) }}
{{ render_fieldlist_of_formfields_as_table(form.conditions) }}
{{ render_conditions_fieldlist_of_formfields_as_table(form.conditions) }}
<div class="pure-form-message-inline">
<p id="verify-state-text">Use the verify (✓) button to test if a condition passes against the current snapshot.</p>

View File

@@ -39,7 +39,7 @@ apprise==1.9.2
paho-mqtt!=2.0.*
# Requires extra wheel for rPi
cryptography~=43.0.1
cryptography~=42.0.8
# Used for CSS filtering
beautifulsoup4