From 30e84f1030245db08dd04cd1b6f1738aa6f32cb1 Mon Sep 17 00:00:00 2001 From: dgtlmoon Date: Tue, 3 Jun 2025 14:54:13 +0200 Subject: [PATCH] UI - Real time - checkbox operations now realtime without reload --- changedetectionio/blueprint/ui/__init__.py | 197 +++++++++++--------- changedetectionio/realtime/socket_server.py | 37 +++- changedetectionio/static/js/realtime.js | 18 ++ changedetectionio/store.py | 3 + 4 files changed, 167 insertions(+), 88 deletions(-) diff --git a/changedetectionio/blueprint/ui/__init__.py b/changedetectionio/blueprint/ui/__init__.py index 9ed40554..52488684 100644 --- a/changedetectionio/blueprint/ui/__init__.py +++ b/changedetectionio/blueprint/ui/__init__.py @@ -7,6 +7,105 @@ from changedetectionio.blueprint.ui.edit import construct_blueprint as construct from changedetectionio.blueprint.ui.notification import construct_blueprint as construct_notification_blueprint from changedetectionio.blueprint.ui.views import construct_blueprint as construct_views_blueprint +def _handle_operations(op, uuids, datastore, worker_handler, update_q, queuedWatchMetaData, watch_check_update, extra_data=None, emit_flash=True): + from flask import request, flash + + if op == 'delete': + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.delete(uuid) + if emit_flash: + flash(f"{len(uuids)} watches deleted") + + elif op == 'pause': + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid]['paused'] = True + if emit_flash: + flash(f"{len(uuids)} watches paused") + + elif op == 'unpause': + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid.strip()]['paused'] = False + if emit_flash: + flash(f"{len(uuids)} watches unpaused") + + elif (op == 'mark-viewed'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.set_last_viewed(uuid, int(time.time())) + if emit_flash: + flash(f"{len(uuids)} watches updated") + + elif (op == 'mute'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid]['notification_muted'] = True + if emit_flash: + flash(f"{len(uuids)} watches muted") + + elif (op == 'unmute'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid]['notification_muted'] = False + if emit_flash: + flash(f"{len(uuids)} watches un-muted") + + elif (op == 'recheck'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + # Recheck and require a full reprocessing + worker_handler.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid})) + if emit_flash: + flash(f"{len(uuids)} watches queued for rechecking") + + elif (op == 'clear-errors'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid]["last_error"] = False + if emit_flash: + flash(f"{len(uuids)} watches errors cleared") + + elif (op == 'clear-history'): + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.clear_watch_history(uuid) + if emit_flash: + flash(f"{len(uuids)} watches cleared/reset.") + + elif (op == 'notification-default'): + from changedetectionio.notification import ( + default_notification_format_for_watch + ) + for uuid in uuids: + if datastore.data['watching'].get(uuid): + datastore.data['watching'][uuid]['notification_title'] = None + datastore.data['watching'][uuid]['notification_body'] = None + datastore.data['watching'][uuid]['notification_urls'] = [] + datastore.data['watching'][uuid]['notification_format'] = default_notification_format_for_watch + if emit_flash: + flash(f"{len(uuids)} watches set to use default notification settings") + + elif (op == 'assign-tag'): + op_extradata = extra_data + if op_extradata: + tag_uuid = datastore.add_tag(title=op_extradata) + if op_extradata and tag_uuid: + for uuid in uuids: + if datastore.data['watching'].get(uuid): + # Bug in old versions caused by bad edit page/tag handler + if isinstance(datastore.data['watching'][uuid]['tags'], str): + datastore.data['watching'][uuid]['tags'] = [] + + datastore.data['watching'][uuid]['tags'].append(tag_uuid) + if emit_flash: + flash(f"{len(uuids)} watches were tagged") + + if uuids: + for uuid in uuids: + watch_check_update.send(watch_uuid=uuid) + def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_handler, queuedWatchMetaData, watch_check_update): ui_blueprint = Blueprint('ui', __name__, template_folder="templates") @@ -149,93 +248,17 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_handle def form_watch_list_checkbox_operations(): op = request.form['op'] uuids = [u.strip() for u in request.form.getlist('uuids') if u] - - if (op == 'delete'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.delete(uuid) - flash("{} watches deleted".format(len(uuids))) - - elif (op == 'pause'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid]['paused'] = True - flash("{} watches paused".format(len(uuids))) - - elif (op == 'unpause'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid.strip()]['paused'] = False - flash("{} watches unpaused".format(len(uuids))) - - elif (op == 'mark-viewed'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.set_last_viewed(uuid, int(time.time())) - flash("{} watches updated".format(len(uuids))) - - elif (op == 'mute'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid]['notification_muted'] = True - flash("{} watches muted".format(len(uuids))) - - elif (op == 'unmute'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid]['notification_muted'] = False - flash("{} watches un-muted".format(len(uuids))) - - elif (op == 'recheck'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - # Recheck and require a full reprocessing - worker_handler.queue_item_async_safe(update_q, queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid})) - flash("{} watches queued for rechecking".format(len(uuids))) - - elif (op == 'clear-errors'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid]["last_error"] = False - flash(f"{len(uuids)} watches errors cleared") - - elif (op == 'clear-history'): - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.clear_watch_history(uuid) - flash("{} watches cleared/reset.".format(len(uuids))) - - elif (op == 'notification-default'): - from changedetectionio.notification import ( - default_notification_format_for_watch - ) - for uuid in uuids: - if datastore.data['watching'].get(uuid): - datastore.data['watching'][uuid]['notification_title'] = None - datastore.data['watching'][uuid]['notification_body'] = None - datastore.data['watching'][uuid]['notification_urls'] = [] - datastore.data['watching'][uuid]['notification_format'] = default_notification_format_for_watch - flash("{} watches set to use default notification settings".format(len(uuids))) - - elif (op == 'assign-tag'): - op_extradata = request.form.get('op_extradata', '').strip() - if op_extradata: - tag_uuid = datastore.add_tag(title=op_extradata) - if op_extradata and tag_uuid: - for uuid in uuids: - if datastore.data['watching'].get(uuid): - # Bug in old versions caused by bad edit page/tag handler - if isinstance(datastore.data['watching'][uuid]['tags'], str): - datastore.data['watching'][uuid]['tags'] = [] - - datastore.data['watching'][uuid]['tags'].append(tag_uuid) - - flash(f"{len(uuids)} watches were tagged") - - if uuids: - for uuid in uuids: -# with app.app_context(): - watch_check_update.send(watch_uuid=uuid) + extra_data = request.form.get('op_extradata', '').strip() + _handle_operations( + datastore=datastore, + extra_data=extra_data, + queuedWatchMetaData=queuedWatchMetaData, + uuids=uuids, + worker_handler=worker_handler, + update_q=update_q, + watch_check_update=watch_check_update, + op=op, + ) return redirect(url_for('watchlist.index')) diff --git a/changedetectionio/realtime/socket_server.py b/changedetectionio/realtime/socket_server.py index 86396a1f..af12c256 100644 --- a/changedetectionio/realtime/socket_server.py +++ b/changedetectionio/realtime/socket_server.py @@ -26,6 +26,9 @@ class SignalHandler: queue_length_signal.connect(self.handle_queue_length, weak=False) # logger.info("SignalHandler: Connected to queue_length signal") + watch_delete_signal = signal('watch_deleted') + watch_delete_signal.connect(self.handle_deleted_signal, weak=False) + # Create and start the queue update thread using standard threading import threading self.polling_emitter_thread = threading.Thread( @@ -61,6 +64,16 @@ class SignalHandler: else: logger.warning(f"Watch UUID {watch_uuid} not found in datastore") + def handle_deleted_signal(self, *args, **kwargs): + watch_uuid = kwargs.get('watch_uuid') + if watch_uuid: + # Emit the queue size to all connected clients + self.socketio_instance.emit("watch_deleted", { + "uuid": watch_uuid, + "event_timestamp": time.time() + }) + logger.debug(f"Watch UUID {watch_uuid} was deleted") + def handle_queue_length(self, *args, **kwargs): """Handle queue_length signal and emit to all clients""" try: @@ -167,7 +180,6 @@ def handle_watch_update(socketio, **kwargs): if hasattr(q_item, 'item') and 'uuid' in q_item.item: queue_list.append(q_item.item['uuid']) - error_texts = "" # Get the error texts from the watch error_texts = watch.compile_error_texts() # Create a simplified watch data object to send to clients @@ -259,6 +271,29 @@ def init_socketio(app, datastore): # Set up event handlers logger.info("Socket.IO: Registering connect event handler") + @socketio.on('checkbox-operation') + def event_checkbox_operations(data): + from changedetectionio.blueprint.ui import _handle_operations + from changedetectionio import queuedWatchMetaData + from changedetectionio import worker_handler + from changedetectionio.flask_app import update_q, watch_check_update + logger.trace(f"Got checkbox operations event: {data}") + + datastore = socketio.datastore + + _handle_operations( + op=data.get('op'), + uuids=data.get('uuids'), + datastore=datastore, + extra_data=data.get('extra_data'), + worker_handler=worker_handler, + update_q=update_q, + queuedWatchMetaData=queuedWatchMetaData, + watch_check_update=watch_check_update, + emit_flash=False + ) + + @socketio.on('connect') def handle_connect(): """Handle client connection""" diff --git a/changedetectionio/static/js/realtime.js b/changedetectionio/static/js/realtime.js index 9534db74..28d98fe4 100644 --- a/changedetectionio/static/js/realtime.js +++ b/changedetectionio/static/js/realtime.js @@ -18,6 +18,24 @@ $(document).ready(function () { return false; }); + + + $('#checkbox-operations button').on('click.socketHandlerNamespace', function (e) { + e.preventDefault(); + const op = $(this).val(); + const checkedUuids = $('input[name="uuids"]:checked').map(function () { + return this.value.trim(); + }).get(); + console.log(`Socket.IO: Sending watch operation '${op}' for UUIDs:`, checkedUuids); + socket.emit('checkbox-operation', { + op: op, + uuids: checkedUuids, + extra_data: $('#op_extradata').val() // Set by the alert() handler + }); + $('input[name="uuids"]:checked').prop('checked', false); + return false; + }); + } diff --git a/changedetectionio/store.py b/changedetectionio/store.py index ed5b4dca..29cf3401 100644 --- a/changedetectionio/store.py +++ b/changedetectionio/store.py @@ -253,6 +253,9 @@ class ChangeDetectionStore: del self.data['watching'][uuid] self.needs_write_urgent = True + watch_delete_signal = signal('watch_deleted') + if watch_delete_signal: + watch_delete_signal.send(watch_uuid=uuid) # Clone a watch by UUID def clone(self, uuid):