mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-08-25 07:37:14 +00:00
UI - front end improvement for mark all viewed (less IO)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import time
|
||||
import threading
|
||||
from blinker import signal
|
||||
from flask import Blueprint, request, redirect, url_for, flash, render_template, session, current_app
|
||||
from flask_babel import gettext
|
||||
from loguru import logger
|
||||
@@ -237,32 +238,32 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, worker_pool,
|
||||
list_filters = wl_filters.list_filters_from_args(datastore, request.args)
|
||||
now = int(time.time())
|
||||
|
||||
# Mark watches as viewed - use background thread only for large watch counts
|
||||
def mark_viewed_impl():
|
||||
"""Mark watches as viewed - can run synchronously or in background thread."""
|
||||
marked_count = 0
|
||||
try:
|
||||
for watch_uuid, watch in datastore.data['watching'].items():
|
||||
if not wl_filters.watch_matches_filters(datastore, watch, list_filters):
|
||||
continue
|
||||
# Runs SYNCHRONOUSLY, and must stay that way. Re #4021: this used to hand the work to a
|
||||
# background thread and redirect immediately, so the watch list re-rendered from a
|
||||
# datastore that was still being marked and showed rows as unviewed until a manual
|
||||
# refresh. The realtime events that would have corrected it were emitted while the
|
||||
# browser was mid-navigation with no socket connected, so they went nowhere.
|
||||
# It is cheap enough to do inline: the per-watch signal is suppressed below (that was
|
||||
# the actual cost, not the disk write, which measures ~0.05ms per watch).
|
||||
marked_count = 0
|
||||
try:
|
||||
for watch_uuid, watch in datastore.data['watching'].items():
|
||||
if not wl_filters.watch_matches_filters(datastore, watch, list_filters):
|
||||
continue
|
||||
|
||||
datastore.set_last_viewed(watch_uuid, now)
|
||||
marked_count += 1
|
||||
datastore.set_last_viewed(watch_uuid, now, send_signal=False)
|
||||
marked_count += 1
|
||||
|
||||
logger.info(f"Marking complete: {marked_count} watches marked as viewed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking as viewed: {e}")
|
||||
logger.info(f"Marking complete: {marked_count} watches marked as viewed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error marking as viewed: {e}")
|
||||
|
||||
# For small watch counts (< 10), run synchronously to avoid race conditions in tests
|
||||
# For larger counts, use background thread to avoid blocking the UI
|
||||
watch_count = len(datastore.data['watching'])
|
||||
if watch_count < 10:
|
||||
# Run synchronously for small watch counts
|
||||
mark_viewed_impl()
|
||||
else:
|
||||
# Start background thread for large watch counts
|
||||
thread = threading.Thread(target=mark_viewed_impl, daemon=True)
|
||||
thread.start()
|
||||
# One summary event instead of one per watch, so other open tabs refresh their counters.
|
||||
# This page doesn't need it - the redirect below re-renders it from the marked datastore.
|
||||
if marked_count:
|
||||
general_stats_update = signal('general_stats_update')
|
||||
if general_stats_update:
|
||||
general_stats_update.send()
|
||||
|
||||
return redirect(url_for('watchlist.index', **wl_filters.filter_query_args(request.args)))
|
||||
|
||||
|
||||
@@ -40,6 +40,30 @@ class SignalHandler:
|
||||
notification_event_signal.connect(self.handle_notification_event, weak=False)
|
||||
logger.info("SignalHandler: Connected to notification_event signal")
|
||||
|
||||
# One-shot stats refresh for bulk operations that deliberately skip the per-watch
|
||||
# signal (see set_last_viewed(send_signal=False)) — n signals would mean n full
|
||||
# rescans of every watch plus 3n broadcasts.
|
||||
general_stats_signal = signal('general_stats_update')
|
||||
general_stats_signal.connect(self.handle_general_stats_update, weak=False)
|
||||
|
||||
|
||||
def handle_general_stats_update(self, *args, **kwargs):
|
||||
"""Emit the global counters once, without touching any individual row.
|
||||
|
||||
Sent after a bulk operation. The tab that triggered it is usually reloading anyway;
|
||||
this is for OTHER open tabs so their unread/error counters don't sit stale.
|
||||
Note their ROWS still keep a stale 'unviewed' class until the row resync lands —
|
||||
tracked separately, this only fixes the counters.
|
||||
"""
|
||||
try:
|
||||
errored_count = sum(1 for w in self.datastore.data['watching'].values() if w.get('last_error'))
|
||||
self.socketio_instance.emit("general_stats_update", {
|
||||
'count_errors': errored_count,
|
||||
'unread_changes_count': self.datastore.unread_changes_count,
|
||||
})
|
||||
logger.trace("Socket.IO: Emitted one-shot general_stats_update")
|
||||
except Exception as e:
|
||||
logger.error(f"Socket.IO error in handle_general_stats_update: {str(e)}")
|
||||
|
||||
def handle_watch_small_status_update(self, *args, **kwargs):
|
||||
"""Small simple status update, for example 'Connecting...'"""
|
||||
|
||||
@@ -455,14 +455,21 @@ class ChangeDetectionStore(DatastoreUpdatesMixin, FileSavingDataStore):
|
||||
# Watch Management Methods
|
||||
# ============================================================================
|
||||
|
||||
def set_last_viewed(self, uuid, timestamp):
|
||||
def set_last_viewed(self, uuid, timestamp, send_signal=True):
|
||||
logger.debug(f"Setting watch UUID: {uuid} last viewed to {int(timestamp)}")
|
||||
self.data['watching'][uuid].update({'last_viewed': int(timestamp)})
|
||||
self.data['watching'][uuid].commit()
|
||||
|
||||
watch_check_update = signal('watch_check_update')
|
||||
if watch_check_update:
|
||||
watch_check_update.send(watch_uuid=uuid)
|
||||
# Bulk callers (mark-all-viewed) pass send_signal=False and emit one summary event
|
||||
# afterwards instead. Each signal fans out to handle_watch_update(), which rescans
|
||||
# EVERY watch twice (errored_count + unread_changes_count), takes the queue and
|
||||
# worker-pool locks, and broadcasts 3 socket events — so signalling per watch makes
|
||||
# a bulk mark O(n^2) with 3n emits, and floods every connected browser with n
|
||||
# row updates it will immediately re-render anyway.
|
||||
if send_signal:
|
||||
watch_check_update = signal('watch_check_update')
|
||||
if watch_check_update:
|
||||
watch_check_update.send(watch_uuid=uuid)
|
||||
|
||||
def remove_password(self):
|
||||
self.__data['settings']['application']['password'] = False
|
||||
|
||||
Reference in New Issue
Block a user