mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-09 11:06:47 +00:00
Compare commits
5 Commits
update-cry
...
0.49.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
108cdf84a5 | ||
|
|
8c6f6f1578 | ||
|
|
df4ffaaff8 | ||
|
|
d522c65e50 | ||
|
|
c3b2a8b019 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
162
changedetectionio/gc_cleanup.py
Normal file
162
changedetectionio/gc_cleanup.py
Normal 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"
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
@import "parts/_love";
|
||||
@import "parts/preview_text_filter";
|
||||
@import "parts/_edit";
|
||||
@import "parts/_conditions_table";
|
||||
|
||||
body {
|
||||
color: var(--color-text);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user