diff --git a/changedetectionio/blueprint/watchlist/__init__.py b/changedetectionio/blueprint/watchlist/__init__.py index 300eeebd..173c50ea 100644 --- a/changedetectionio/blueprint/watchlist/__init__.py +++ b/changedetectionio/blueprint/watchlist/__init__.py @@ -84,7 +84,7 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe has_proxies=datastore.proxy_list, has_unviewed=datastore.has_unviewed, hosted_sticky=os.getenv("SALTED_PASS", False) == False, - now_time_server=time.time(), + now_time_server=round(time.time()), pagination=pagination, queued_uuids=[q_uuid.item['uuid'] for q_uuid in update_q.queue], search_q=request.args.get('q', '').strip(), diff --git a/changedetectionio/blueprint/watchlist/templates/watch-overview.html b/changedetectionio/blueprint/watchlist/templates/watch-overview.html index 91510221..f552f2ad 100644 --- a/changedetectionio/blueprint/watchlist/templates/watch-overview.html +++ b/changedetectionio/blueprint/watchlist/templates/watch-overview.html @@ -102,7 +102,7 @@ {% set is_unviewed = watch.newest_history_key| int > watch.last_viewed and watch.history_n>=2 %} {% set checking_now = is_checking_now(watch) %} - {{ loop.index+pagination.skip }} @@ -144,7 +143,7 @@ Try other proxies/location  {% endif %} Try adding external proxies/locations - + {% endif %} {% if 'empty result or contain only an image' in watch.last_error %} more help here. @@ -192,25 +191,40 @@ {% endif %} {#last_checked becomes fetch-start-time#} - {% if checking_now %} - Checking now + + + + {{watch|format_last_checked_time|safe}} + + + + {% if watch.history_n >=2 and watch.last_changed >0 %} + {{watch.last_changed|format_timestamp_timeago}} {% else %} - {{watch|format_last_checked_time|safe}} + Not yet {% endif %} - - {% if watch.history_n >=2 and watch.last_changed >0 %}{{watch|format_last_checked_time|safe}}{% else %}Not yet{% endif %} - - Recheck - - Edit + {% if watch.uuid in queued_uuids %}Queued{% else %}Recheck{% endif %} + Edit - {% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %} - {% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %} + {% if watch.history_n >= 2 %} - History - + {% set open_diff_in_new_tab = datastore.data['settings']['application']['ui'].get('open_diff_in_new_tab') %} + {% set target_attr = ' target="' ~ watch.uuid ~ '"' if open_diff_in_new_tab else '' %} + + {% if is_unviewed %} + History + {% else %} + History + {% endif %} + + {% else %} + {% if watch.history_n == 1 or (watch.history_n ==0 and watch.error_text_ctime )%} + Preview + {% endif %} + {% endif %} {% endfor %} @@ -239,4 +253,4 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/changedetectionio/realtime/socket_server.py b/changedetectionio/realtime/socket_server.py index 71408542..d9802d70 100644 --- a/changedetectionio/realtime/socket_server.py +++ b/changedetectionio/realtime/socket_server.py @@ -5,16 +5,16 @@ import json import time from loguru import logger +from changedetectionio.flask_app import _jinja2_filter_datetime + + class ChangeDetectionSocketIO: def __init__(self, app, datastore): self.main_app = app self.datastore = datastore - - # Create a separate app for Socket.IO - self.app = Flask(__name__) - + # Use threading mode instead of eventlet - self.socketio = SocketIO(self.app, + self.socketio = SocketIO(self.main_app, async_mode='threading', cors_allowed_origins="*", logger=False, @@ -28,23 +28,7 @@ class ChangeDetectionSocketIO: # Just start a background thread to periodically emit watch status self.thread = None self.thread_lock = threading.Lock() - - # Set up a simple index route for the Socket.IO app - @self.app.route('/') - def index(): - return """ - - - ChangeDetection.io Socket.IO Server - - -

ChangeDetection.io Socket.IO Server

-

This is the Socket.IO server for ChangeDetection.io real-time updates.

-

Socket.IO endpoint is available at: /socket.io/

- - - """ - + def start_background_task(self): """Start the background task if it's not already running""" with self.thread_lock: @@ -85,22 +69,17 @@ class ChangeDetectionSocketIO: for thread in threads_snapshot: if hasattr(thread, 'current_uuid') and thread.current_uuid: currently_checking.append(thread.current_uuid) - + self.socketio.emit("checking_now", list(currently_checking)) + # Send all watch data periodically for uuid, watch in self.datastore.data['watching'].items(): # Simplified watch data to avoid sending everything simplified_data = { 'uuid': uuid, - 'url': watch.get('url', ''), - 'title': watch.get('title', ''), - 'last_checked': int(watch.get('last_checked', 0)), - 'last_changed': int(watch.get('newest_history_key', 0)), - 'history_n': watch.history_n if hasattr(watch, 'history_n') else 0, - 'unviewed_history': int(watch.get('newest_history_key', 0)) > int(watch.get('last_viewed', 0)) and watch.history_n >=2, - 'paused': watch.get('paused', False), - 'checking': uuid in currently_checking + 'last_checked': _jinja2_filter_datetime(watch), +# 'history_n': watch.history_n if hasattr(watch, 'history_n') else 0, } - watches_data.append(simplified_data) + #watches_data.append(simplified_data) # Emit all watch data periodically self.socketio.emit('watch_data', watches_data) @@ -123,4 +102,4 @@ class ChangeDetectionSocketIO: # Run the Socket.IO server # Use 0.0.0.0 to listen on all interfaces logger.info(f"Starting Socket.IO server on http://{host}:{port}") - self.socketio.run(self.app, host=host, port=port, debug=False, use_reloader=False, allow_unsafe_werkzeug=True) \ No newline at end of file + self.socketio.run(self.main_app, host=host, port=port, debug=False, use_reloader=False, allow_unsafe_werkzeug=True) \ No newline at end of file diff --git a/changedetectionio/static/js/edit-columns-resizer.js b/changedetectionio/static/js/edit-columns-resizer.js deleted file mode 100644 index 7ca8ceb3..00000000 --- a/changedetectionio/static/js/edit-columns-resizer.js +++ /dev/null @@ -1,108 +0,0 @@ -$(document).ready(function() { - // Global variables for resize functionality - let isResizing = false; - let initialX, initialLeftWidth; - - // Setup document-wide mouse move and mouse up handlers - $(document).on('mousemove', handleMouseMove); - $(document).on('mouseup', function() { - isResizing = false; - }); - - // Handle mouse move for resizing - function handleMouseMove(e) { - if (!isResizing) return; - - const $container = $('#filters-and-triggers > div'); - const containerWidth = $container.width(); - const diffX = e.clientX - initialX; - const newLeftWidth = ((initialLeftWidth + diffX) / containerWidth) * 100; - - // Limit the minimum width percentage - if (newLeftWidth > 20 && newLeftWidth < 80) { - $('#edit-text-filter').css('flex', `0 0 ${newLeftWidth}%`); - $('#text-preview').css('flex', `0 0 ${100 - newLeftWidth}%`); - } - } - - // Function to create and setup the resizer - function setupResizer() { - // Only proceed if text-preview is visible - if (!$('#text-preview').is(':visible')) return; - - // Don't add another resizer if one already exists - if ($('#column-resizer').length > 0) return; - - // Create resizer element - const $resizer = $('
', { - class: 'column-resizer', - id: 'column-resizer' - }); - - // Insert before the text preview div - const $container = $('#filters-and-triggers > div'); - if ($container.length) { - $resizer.insertBefore('#text-preview'); - - // Setup mousedown handler for the resizer - $resizer.on('mousedown', function(e) { - isResizing = true; - initialX = e.clientX; - initialLeftWidth = $('#edit-text-filter').width(); - - // Prevent text selection during resize - e.preventDefault(); - }); - } - } - - // Setup resizer when preview is activated - $('#activate-text-preview').on('click', function() { - // Give it a small delay to ensure the preview is visible - setTimeout(setupResizer, 100); - }); - - // Also setup resizer when the filters-and-triggers tab is clicked - $('#filters-and-triggers-tab a').on('click', function() { - // Give it a small delay to ensure everything is loaded - setTimeout(setupResizer, 100); - }); - - // Run the setupResizer function when the page is fully loaded - // to ensure it's added if the text-preview is already visible - setTimeout(setupResizer, 500); - - // Handle window resize events - $(window).on('resize', function() { - // Make sure the resizer is added if text-preview is visible - if ($('#text-preview').is(':visible')) { - setupResizer(); - } - }); - - // Keep checking if the resizer needs to be added - // This ensures it's restored if something removes it - setInterval(function() { - if ($('#text-preview').is(':visible')) { - setupResizer(); - } - }, 500); - - // Add a MutationObserver to watch for DOM changes - // This will help restore the resizer if it gets removed - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (mutation.type === 'childList' && - $('#text-preview').is(':visible') && - $('#column-resizer').length === 0) { - setupResizer(); - } - }); - }); - - // Start observing the container for DOM changes - observer.observe(document.getElementById('filters-and-triggers'), { - childList: true, - subtree: true - }); -}); \ No newline at end of file diff --git a/changedetectionio/static/js/socket.js b/changedetectionio/static/js/socket.js index 199d1ae6..201a9b7c 100644 --- a/changedetectionio/static/js/socket.js +++ b/changedetectionio/static/js/socket.js @@ -14,13 +14,26 @@ $(document).ready(function() { socket.on('disconnect', function() { console.log('Socket.IO disconnected'); }); - + + socket.on('checking_now', function(uuid_list) { + console.log("Got checking now update"); + // Remove 'checking-now' class where it should no longer be + $('.watch-table tbody tr.checking-now').each(function() { + if (!uuid_list.includes($(this).data('watch-uuid'))) { + $(this).removeClass('checking-now'); + } + }); + + // Add the class on the rows where it should be + uuid_list.forEach(function(uuid) { + $('.watch-table tbody tr[data-watch-uuid="' + uuid + '"]').addClass('checking-now'); + }); + }); + // Listen for periodically emitted watch data socket.on('watch_data', function(watches) { - console.log('Received watch data updates'); - - // First, remove checking-now class from all rows - $('.checking-now').removeClass('checking-now'); +/* console.log('Received watch data updates'); + // Update all watches with their current data watches.forEach(function(watch) { @@ -28,12 +41,13 @@ $(document).ready(function() { if ($watchRow.length) { updateWatchRow($watchRow, watch); } - }); + });*/ }); // Function to update a watch row with new data function updateWatchRow($row, data) { // Update the last-checked time + return; const $lastChecked = $row.find('.last-checked'); if ($lastChecked.length) { // Update data-timestamp attribute @@ -63,60 +77,13 @@ $(document).ready(function() { } } } - - // Update the last-changed time - const $lastChanged = $row.find('.last-changed'); - if ($lastChanged.length && data.last_changed) { - // Update data-timestamp attribute - $lastChanged.attr('data-timestamp', data.last_changed); - - // Only update the text if we have history - if (data.history_n >= 2 && data.last_changed > 0) { - let $timeagoSpan = $lastChanged.find('.timeago'); - - // If there's no timeago span yet, create one - if (!$timeagoSpan.length) { - $lastChanged.html(''); - $timeagoSpan = $lastChanged.find('.timeago'); - } - - // Format as timeago - if (typeof timeago !== 'undefined') { - $timeagoSpan.text(timeago.format(data.last_changed * 1000)); - } else { - // Simple fallback if timeago isn't available - const date = new Date(data.last_changed * 1000); - $timeagoSpan.text(date.toLocaleString()); - } - } else { - $lastChanged.text('Not yet'); - } - } + // Toggle the unviewed class based on viewed status - $row.toggleClass('unviewed', data.unviewed_history === false); +// $row.toggleClass('unviewed', data.unviewed_history === false); // If the watch is currently being checked - $row.toggleClass('checking-now', data.checking === true); - - // If a change was detected and not viewed, add highlight effect - if (data.history_n > 0 && data.viewed === false) { - // Don't add the highlight effect too often - if (!$row.hasClass('socket-highlight')) { - $row.addClass('socket-highlight'); - setTimeout(function() { - $row.removeClass('socket-highlight'); - }, 2000); - - console.log('New change detected for:', data.title || data.url); - } - - // Update any change count indicators if present - const $changeCount = $row.find('.change-count'); - if ($changeCount.length) { - $changeCount.text(data.history_n); - } - } + // $row.toggleClass('checking-now', data.checking === true); } } catch (e) { // If Socket.IO fails to initialize, just log it and continue diff --git a/changedetectionio/static/js/watch-overview.js b/changedetectionio/static/js/watch-overview.js index e3bc3359..1d537e43 100644 --- a/changedetectionio/static/js/watch-overview.js +++ b/changedetectionio/static/js/watch-overview.js @@ -68,7 +68,7 @@ $(function () { if (eta_complete + 2 > nowtimeserver && fetch_duration > 3) { const remaining_seconds = Math.abs(eta_complete) - nowtimeserver - 1; - let r = (1.0 - (remaining_seconds / fetch_duration)) * 100; + let r = Math.round((1.0 - (remaining_seconds / fetch_duration)) * 100); if (r < 10) { r = 10; } @@ -76,7 +76,6 @@ $(function () { r = 100; } $(this).css('background-size', `${r}% 100%`); - //$(this).text(`${r}% remain ${remaining_seconds}`); } else { $(this).css('background-size', `100% 100%`); } diff --git a/changedetectionio/static/styles/scss/parts/_socket.scss b/changedetectionio/static/styles/scss/parts/_socket.scss index b6b472d8..8eff565d 100644 --- a/changedetectionio/static/styles/scss/parts/_socket.scss +++ b/changedetectionio/static/styles/scss/parts/_socket.scss @@ -1,30 +1 @@ // Styles for Socket.IO real-time updates - -@keyframes socket-highlight-flash { - 0% { - background-color: rgba(var(--color-change-highlight-rgb), 0); - } - 50% { - background-color: rgba(var(--color-change-highlight-rgb), 0.4); - } - 100% { - background-color: rgba(var(--color-change-highlight-rgb), 0); - } -} - -.socket-highlight { - animation: socket-highlight-flash 2s ease-in-out; -} - -// Animation for the checking-now state -@keyframes checking-progress { - 0% { background-size: 0% 100%; } - 100% { background-size: 100% 100%; } -} - -tr.checking-now .last-checked { - background-image: linear-gradient(to right, rgba(0, 120, 255, 0.2) 0%, rgba(0, 120, 255, 0.1) 100%); - background-size: 100% 100%; - background-repeat: no-repeat; - animation: checking-progress 10s linear; -} \ No newline at end of file diff --git a/changedetectionio/static/styles/scss/parts/_watch_table.scss b/changedetectionio/static/styles/scss/parts/_watch_table.scss index 9ea0d875..8386ae98 100644 --- a/changedetectionio/static/styles/scss/parts/_watch_table.scss +++ b/changedetectionio/static/styles/scss/parts/_watch_table.scss @@ -1,3 +1,4 @@ + /* table related */ .watch-table { width: 100%; @@ -7,33 +8,15 @@ &.unviewed { font-weight: bold; } - &.has-history { - a.preview { - display: none !important; - } - &.history { - display: inline-block !important; - } - } - &.error { color: var(--color-watch-table-error); } - &.queued { - a.queue { - display: inline-block !important; - } - a.recheck { - display: none !important; - } - } color: var(--color-watch-table-row-text); } td { white-space: nowrap; - &.title-col { word-break: break-all; white-space: normal; @@ -64,120 +47,15 @@ content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); margin: 0 3px 0 5px; } + + + /* Row with 'checking-now' */ + tr.checking-now { + td.last-checked { + .spinner, .spinner-text { + display: inline-block !important; + } + } + } } - -@media only screen and (max-width: 760px), -(min-device-width: 768px) and (max-device-width: 800px) { - /* -Max width before this PARTICULAR table gets nasty -This query will take effect for any screen smaller than 760px -and also iPads specifically. -*/ - .watch-table { - /* make headings work on mobile */ - thead { - display: block; - - tr { - th { - display: inline-block; - // Hide the "Last" text for smaller screens - @media (max-width: 768px) { - .hide-on-mobile { - display: none; - } - } - } - } - - .empty-cell { - display: none; - } - } - - /* Force table to not be like tables anymore */ - tbody { - td, - tr { - display: block; - } - } - - tbody { - tr { - display: flex; - flex-wrap: wrap; - - // The third child of each row will take up the remaining space - // This is useful for the URL column, which should expand to fill the remaining space - :nth-child(3) { - flex-grow: 1; - } - - // The last three children (from the end) of each row will take up the full width - // This is useful for the "Last Checked", "Last Changed", and the action buttons columns, which should each take up the full width - :nth-last-child(-n+3) { - flex-basis: 100%; - } - } - } - - .last-checked { - > span { - vertical-align: middle; - } - } - - .last-checked::before { - color: var(--color-last-checked); - content: "Last Checked "; - } - - .last-changed::before { - color: var(--color-last-checked); - content: "Last Changed "; - } - - /* Force table to not be like tables anymore */ - td.inline { - display: inline-block; - } - - .pure-table td, - .pure-table th { - border: none; - } - - td { - /* Behave like a "row" */ - border: none; - border-bottom: 1px solid var(--color-border-watch-table-cell); - vertical-align: middle; - - &:before { - /* Top/left values mimic padding */ - top: 6px; - left: 6px; - width: 45%; - padding-right: 10px; - white-space: nowrap; - } - } - - &.pure-table-striped { - tr { - background-color: var(--color-table-background); - } - - tr:nth-child(2n-1) { - background-color: var(--color-table-stripe); - } - - tr:nth-child(2n-1) td { - background-color: inherit; - } - } - - } -} \ No newline at end of file diff --git a/changedetectionio/static/styles/scss/styles.scss b/changedetectionio/static/styles/scss/styles.scss index 21075079..71d44039 100644 --- a/changedetectionio/static/styles/scss/styles.scss +++ b/changedetectionio/static/styles/scss/styles.scss @@ -13,6 +13,7 @@ @import "parts/_menu"; @import "parts/_love"; @import "parts/preview_text_filter"; +@import "parts/_watch_table"; @import "parts/_edit"; @import "parts/_conditions_table"; @@ -169,56 +170,6 @@ code { color: var(--color-text); } -/* table related */ -.watch-table { - width: 100%; - font-size: 80%; - - tr { - &.unviewed { - font-weight: bold; - } - &.error { - color: var(--color-watch-table-error); - } - color: var(--color-watch-table-row-text); - } - - - td { - white-space: nowrap; - &.title-col { - word-break: break-all; - white-space: normal; - } - } - - - th { - white-space: nowrap; - - a { - font-weight: normal; - - &.active { - font-weight: bolder; - } - - &.inactive { - .arrow { - display: none; - } - } - } - } - - .title-col a[target="_blank"]::after, - .current-diff-url::after { - content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); - margin: 0 3px 0 5px; - } -} - .inline-tag { white-space: nowrap; border-radius: 5px; diff --git a/changedetectionio/static/styles/styles.css b/changedetectionio/static/styles/styles.css index 28cb0ec2..14dbd72e 100644 --- a/changedetectionio/static/styles/styles.css +++ b/changedetectionio/static/styles/styles.css @@ -523,6 +523,37 @@ body.preview-text-enabled { z-index: 3; box-shadow: 1px 1px 4px var(--color-shadow-jump); } +/* table related */ +.watch-table { + width: 100%; + font-size: 80%; + /* Row with 'checking-now' */ } + .watch-table tr { + color: var(--color-watch-table-row-text); } + .watch-table tr.unviewed { + font-weight: bold; } + .watch-table tr.error { + color: var(--color-watch-table-error); } + .watch-table td { + white-space: nowrap; } + .watch-table td.title-col { + word-break: break-all; + white-space: normal; } + .watch-table th { + white-space: nowrap; } + .watch-table th a { + font-weight: normal; } + .watch-table th a.active { + font-weight: bolder; } + .watch-table th a.inactive .arrow { + display: none; } + .watch-table .title-col a[target="_blank"]::after, + .watch-table .current-diff-url::after { + content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); + margin: 0 3px 0 5px; } + .watch-table tr.checking-now td.last-checked .spinner, .watch-table tr.checking-now td.last-checked .spinner-text { + display: inline-block !important; } + ul#conditions_match_logic { list-style: none; } ul#conditions_match_logic input, ul#conditions_match_logic label, ul#conditions_match_logic li { @@ -735,34 +766,6 @@ code { background: var(--color-background-code); color: var(--color-text); } -/* table related */ -.watch-table { - width: 100%; - font-size: 80%; } - .watch-table tr { - color: var(--color-watch-table-row-text); } - .watch-table tr.unviewed { - font-weight: bold; } - .watch-table tr.error { - color: var(--color-watch-table-error); } - .watch-table td { - white-space: nowrap; } - .watch-table td.title-col { - word-break: break-all; - white-space: normal; } - .watch-table th { - white-space: nowrap; } - .watch-table th a { - font-weight: normal; } - .watch-table th a.active { - font-weight: bolder; } - .watch-table th a.inactive .arrow { - display: none; } - .watch-table .title-col a[target="_blank"]::after, - .watch-table .current-diff-url::after { - content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); - margin: 0 3px 0 5px; } - .inline-tag, .watch-tag-list, .tracking-ldjson-price-data, .restock-label { white-space: nowrap; border-radius: 5px;