Merge branch 'realtime-ui' of github.com:dgtlmoon/changedetection.io into realtime-ui
This commit is contained in:
@@ -16,8 +16,9 @@ import eventlet.wsgi
|
||||
import getopt
|
||||
import platform
|
||||
import signal
|
||||
import socket
|
||||
import socket # Make sure socket is imported at the module level
|
||||
import sys
|
||||
from flask import request
|
||||
|
||||
from changedetectionio import store
|
||||
from changedetectionio.flask_app import changedetection_app
|
||||
@@ -147,22 +148,16 @@ def main():
|
||||
# Get the Flask app
|
||||
app = changedetection_app(app_config, datastore)
|
||||
|
||||
# Now initialize Socket.IO after the app is fully set up
|
||||
# Initialize Socket.IO integrated with the main Flask app
|
||||
try:
|
||||
from changedetectionio.realtime.socket_server import ChangeDetectionSocketIO
|
||||
from changedetectionio.flask_app import socketio_server
|
||||
import threading
|
||||
from changedetectionio.realtime.socket_server import init_socketio
|
||||
|
||||
# Create the Socket.IO server
|
||||
socketio_server = ChangeDetectionSocketIO(app, datastore)
|
||||
|
||||
# Run the Socket.IO server in a separate thread on port 5005
|
||||
socket_thread = threading.Thread(target=socketio_server.run,
|
||||
kwargs={'host': host, 'port': 5005})
|
||||
socket_thread.daemon = True
|
||||
socket_thread.start()
|
||||
|
||||
logger.info("Socket.IO server initialized successfully on port 5005")
|
||||
# Initialize Socket.IO with the main Flask app
|
||||
# This will be used later when we run the app with socketio.run()
|
||||
socketio = init_socketio(app, datastore)
|
||||
app.config['SOCKETIO'] = socketio
|
||||
|
||||
logger.info("Socket.IO server initialized successfully (integrated with main app)")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to initialize Socket.IO server: {str(e)}")
|
||||
|
||||
@@ -191,6 +186,9 @@ def main():
|
||||
|
||||
@app.context_processor
|
||||
def inject_version():
|
||||
# Socket.IO is now integrated with the main app
|
||||
# The client will automatically connect to the Socket.IO endpoint on the same host/port
|
||||
|
||||
return dict(right_sticky="v{}".format(datastore.data['version_tag']),
|
||||
new_version_available=app.config['NEW_VERSION_AVAILABLE'],
|
||||
has_password=datastore.data['settings']['application']['password'] != False
|
||||
@@ -227,7 +225,20 @@ def main():
|
||||
server_side=True), app)
|
||||
|
||||
else:
|
||||
# We'll integrate the Socket.IO server with the WSGI server
|
||||
# The Socket.IO server is already attached to the Flask app
|
||||
eventlet.wsgi.server(eventlet.listen((host, int(port)), s_type), app)
|
||||
# Run the app with Socket.IO's integrated server
|
||||
if 'SOCKETIO' in app.config:
|
||||
# When using eventlet or threading, we need to make sure the host is valid
|
||||
# Use '0.0.0.0' for all interfaces if host is empty or invalid
|
||||
try:
|
||||
socket_host = host if host else '0.0.0.0'
|
||||
logger.info(f"Starting integrated Socket.IO server on http://{socket_host}:{port}")
|
||||
app.config['SOCKETIO'].run(app, host=socket_host, port=int(port), debug=False, use_reloader=False, allow_unsafe_werkzeug=True)
|
||||
except socket.gaierror:
|
||||
# If the hostname is invalid, fall back to '0.0.0.0'
|
||||
logger.warning(f"Invalid hostname '{host}', falling back to '0.0.0.0'")
|
||||
app.config['SOCKETIO'].run(app, host='0.0.0.0', port=int(port), debug=False, use_reloader=False, allow_unsafe_werkzeug=True)
|
||||
else:
|
||||
# Fallback to eventlet if Socket.IO initialization failed
|
||||
logger.info(f"Starting standard Flask server on http://{host}:{port}")
|
||||
eventlet.wsgi.server(eventlet.listen((host, int(port)), s_type), app)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ from flask import Blueprint, request, redirect, url_for, flash, render_template,
|
||||
from loguru import logger
|
||||
from functools import wraps
|
||||
|
||||
from changedetectionio.blueprint.ui.ajax import constuct_ui_ajax_blueprint
|
||||
from changedetectionio.store import ChangeDetectionStore
|
||||
from changedetectionio.blueprint.ui.edit import construct_blueprint as construct_edit_blueprint
|
||||
from changedetectionio.blueprint.ui.notification import construct_blueprint as construct_notification_blueprint
|
||||
@@ -22,7 +23,10 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, running_updat
|
||||
# Register the views blueprint
|
||||
views_blueprint = construct_views_blueprint(datastore, update_q, queuedWatchMetaData, watch_check_completed)
|
||||
ui_blueprint.register_blueprint(views_blueprint)
|
||||
|
||||
|
||||
ui_ajax_blueprint = constuct_ui_ajax_blueprint(datastore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed)
|
||||
ui_blueprint.register_blueprint(ui_ajax_blueprint)
|
||||
|
||||
# Import the login decorator
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
|
||||
|
||||
35
changedetectionio/blueprint/ui/ajax.py
Normal file
35
changedetectionio/blueprint/ui/ajax.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import time
|
||||
|
||||
from blinker import signal
|
||||
from flask import Blueprint, request, redirect, url_for, flash, render_template, session
|
||||
|
||||
|
||||
from changedetectionio.store import ChangeDetectionStore
|
||||
|
||||
def constuct_ui_ajax_blueprint(datastore: ChangeDetectionStore, update_q, running_update_threads, queuedWatchMetaData, watch_check_completed):
|
||||
ui_ajax_blueprint = Blueprint('ajax', __name__, template_folder="templates", url_prefix='/ajax')
|
||||
|
||||
# Import the login decorator
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
|
||||
@ui_ajax_blueprint.route("/toggle", methods=['POST'])
|
||||
@login_optionally_required
|
||||
def ajax_toggler():
|
||||
op = request.values.get('op')
|
||||
uuid = request.values.get('uuid')
|
||||
if op and datastore.data['watching'].get(uuid):
|
||||
if op == 'pause':
|
||||
datastore.data['watching'][uuid].toggle_pause()
|
||||
elif op == 'mute':
|
||||
datastore.data['watching'][uuid].toggle_mute()
|
||||
elif op == 'recheck':
|
||||
update_q.put(queuedWatchMetaData.PrioritizedItem(priority=1, item={'uuid': uuid}))
|
||||
|
||||
watch_check_completed = signal('watch_check_completed')
|
||||
if watch_check_completed:
|
||||
watch_check_completed.send(watch_uuid=uuid)
|
||||
|
||||
return 'OK'
|
||||
|
||||
|
||||
return ui_ajax_blueprint
|
||||
@@ -72,31 +72,33 @@ def construct_blueprint(datastore: ChangeDetectionStore, update_q, queuedWatchMe
|
||||
per_page=datastore.data['settings']['application'].get('pager_size', 50), css_framework="semantic")
|
||||
|
||||
sorted_tags = sorted(datastore.data['settings']['application'].get('tags').items(), key=lambda x: x[1]['title'])
|
||||
|
||||
output = render_template(
|
||||
"watch-overview.html",
|
||||
active_tag=active_tag,
|
||||
active_tag_uuid=active_tag_uuid,
|
||||
app_rss_token=datastore.data['settings']['application'].get('rss_access_token'),
|
||||
datastore=datastore,
|
||||
errored_count=errored_count,
|
||||
form=form,
|
||||
guid=datastore.data['app_guid'],
|
||||
has_proxies=datastore.proxy_list,
|
||||
has_unviewed=datastore.has_unviewed,
|
||||
hosted_sticky=os.getenv("SALTED_PASS", False) == False,
|
||||
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(),
|
||||
sort_attribute=request.args.get('sort') if request.args.get('sort') else request.cookies.get('sort'),
|
||||
sort_order=request.args.get('order') if request.args.get('order') else request.cookies.get('order'),
|
||||
system_default_fetcher=datastore.data['settings']['application'].get('fetch_backend'),
|
||||
tags=sorted_tags,
|
||||
watches=sorted_watches
|
||||
)
|
||||
active_tag=active_tag,
|
||||
active_tag_uuid=active_tag_uuid,
|
||||
app_rss_token=datastore.data['settings']['application'].get('rss_access_token'),
|
||||
ajax_toggle_url=url_for('ui.ajax.ajax_toggler'),
|
||||
datastore=datastore,
|
||||
errored_count=errored_count,
|
||||
form=form,
|
||||
guid=datastore.data['app_guid'],
|
||||
has_proxies=datastore.proxy_list,
|
||||
has_unviewed=datastore.has_unviewed,
|
||||
hosted_sticky=os.getenv("SALTED_PASS", False) == False,
|
||||
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(),
|
||||
sort_attribute=request.args.get('sort') if request.args.get('sort') else request.cookies.get('sort'),
|
||||
sort_order=request.args.get('order') if request.args.get('order') else request.cookies.get('order'),
|
||||
system_default_fetcher=datastore.data['settings']['application'].get('fetch_backend'),
|
||||
tags=sorted_tags,
|
||||
watches=sorted_watches
|
||||
)
|
||||
|
||||
if session.get('share-link'):
|
||||
del(session['share-link'])
|
||||
del (session['share-link'])
|
||||
|
||||
resp = make_response(output)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='watch-overview.js')}}" defer></script>
|
||||
<script>let nowtimeserver={{ now_time_server }};</script>
|
||||
<script>let ajax_toggle_url="{{ ajax_toggle_url }}";</script>
|
||||
|
||||
<style>
|
||||
.checking-now .last-checked {
|
||||
@@ -111,16 +112,15 @@
|
||||
{% if watch.has_restock_info %} has-restock-info {% if watch['restock']['in_stock'] %}in-stock{% else %}not-in-stock{% endif %} {% else %}no-restock-info{% endif %}
|
||||
{% if watch.uuid in queued_uuids %}queued{% endif %}
|
||||
{% if checking_now %}checking-now{% endif %}
|
||||
{% if watch.notification_muted %}notification_muted{% endif %}
|
||||
">
|
||||
<td class="inline checkbox-uuid" ><input name="uuids" type="checkbox" value="{{ watch.uuid}} " > <span>{{ loop.index+pagination.skip }}</span></td>
|
||||
<td class="inline watch-controls">
|
||||
{% if not watch.paused %}
|
||||
<a class="state-off" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause" ></a>
|
||||
{% else %}
|
||||
<a class="state-on" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
|
||||
{% endif %}
|
||||
{% set mute_label = 'UnMute notification' if watch.notification_muted else 'Mute notification' %}
|
||||
<a class="link-mute state-{{'on' if watch.notification_muted else 'off'}}" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="{{ mute_label }}" title="{{ mute_label }}" class="icon icon-mute" ></a>
|
||||
<a class="ajax-op state-off pause-toggle" data-op="pause" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause checks" title="Pause checks" class="icon icon-pause" ></a>
|
||||
<a class="ajax-op state-on pause-toggle" data-op="pause" style="display: none" href="{{url_for('watchlist.index', op='pause', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="UnPause checks" title="UnPause checks" class="icon icon-unpause" ></a>
|
||||
|
||||
<a class="ajax-op state-off mute-toggle" data-op="mute" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="Mute notification" title="Mute notification" class="icon icon-mute" ></a>
|
||||
<a class="ajax-op state-on mute-toggle" data-op="mute" style="display: none" href="{{url_for('watchlist.index', op='mute', uuid=watch.uuid, tag=active_tag_uuid)}}"><img src="{{url_for('static_content', group='images', filename='bell-off.svg')}}" alt="UnMute notification" title="UnMute notification" class="icon icon-mute" ></a>
|
||||
</td>
|
||||
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
||||
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
||||
@@ -207,7 +207,7 @@
|
||||
<td>
|
||||
<a href="" class="already-in-queue-button recheck pure-button pure-button-primary" style="display: none;" disabled="disabled">Queued</a>
|
||||
|
||||
<a href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" class="recheck pure-button pure-button-primary">Recheck</a>
|
||||
<a href="{{ url_for('ui.form_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" data-op='recheck' class="ajax-op recheck pure-button pure-button-primary">Recheck</a>
|
||||
<a href="{{ url_for('ui.ui_edit.edit_page', uuid=watch.uuid, tag=active_tag_uuid)}}#general" class="pure-button pure-button-primary">Edit</a>
|
||||
|
||||
{% if watch.history_n >= 2 %}
|
||||
|
||||
@@ -125,6 +125,18 @@ def get_darkmode_state():
|
||||
def get_css_version():
|
||||
return __version__
|
||||
|
||||
@app.template_global()
|
||||
def get_socketio_path():
|
||||
"""Generate the correct Socket.IO path prefix for the client"""
|
||||
# If behind a proxy with a sub-path, we need to respect that path
|
||||
prefix = ""
|
||||
if os.getenv('USE_X_SETTINGS') and 'X-Forwarded-Prefix' in request.headers:
|
||||
prefix = request.headers['X-Forwarded-Prefix']
|
||||
|
||||
# Socket.IO will be available at {prefix}/socket.io/
|
||||
return prefix
|
||||
|
||||
|
||||
@app.template_filter('format_number_locale')
|
||||
def _jinja2_filter_format_number_locale(value: float) -> str:
|
||||
"Formats for example 4000.10 to the local locale default of 4,000.10"
|
||||
|
||||
@@ -4,6 +4,7 @@ from flask_socketio import SocketIO
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
from loguru import logger
|
||||
import blinker
|
||||
|
||||
@@ -12,116 +13,115 @@ from changedetectionio.flask_app import _jinja2_filter_datetime, watch_check_com
|
||||
|
||||
class SignalHandler:
|
||||
"""A standalone class to receive signals"""
|
||||
def __init__(self, socketio_instance):
|
||||
def __init__(self, socketio_instance, datastore):
|
||||
self.socketio_instance = socketio_instance
|
||||
|
||||
# Get signal from app config
|
||||
app_signal = socketio_instance.main_app.config.get('WATCH_CHECK_COMPLETED_SIGNAL')
|
||||
if app_signal:
|
||||
app_signal.connect(self.handle_signal, weak=False)
|
||||
logger.info("SignalHandler: Connected to signal from app config")
|
||||
else:
|
||||
# Fallback if not in app config
|
||||
from changedetectionio.flask_app import watch_check_completed as wcc
|
||||
wcc.connect(self.handle_signal, weak=False)
|
||||
logger.info("SignalHandler: Connected to signal from direct import")
|
||||
|
||||
self.datastore = datastore
|
||||
|
||||
# Connect to the watch_check_completed signal
|
||||
from changedetectionio.flask_app import watch_check_completed as wcc
|
||||
wcc.connect(self.handle_signal, weak=False)
|
||||
logger.info("SignalHandler: Connected to signal from direct import")
|
||||
|
||||
def handle_signal(self, *args, **kwargs):
|
||||
logger.info(f"SignalHandler: Signal received with {len(args)} args and {len(kwargs)} kwargs")
|
||||
# Safely extract the watch UUID from kwargs
|
||||
watch_uuid = kwargs.get('watch_uuid')
|
||||
if watch_uuid:
|
||||
# Get the datastore from the socket instance
|
||||
datastore = self.socketio_instance.datastore
|
||||
# Get the watch object from the datastore
|
||||
watch = datastore.data['watching'].get(watch_uuid)
|
||||
watch = self.datastore.data['watching'].get(watch_uuid)
|
||||
if watch:
|
||||
# Forward to the socket instance with the watch parameter
|
||||
self.socketio_instance.handle_watch_update(watch=watch)
|
||||
# Forward to handle_watch_update with the watch parameter
|
||||
handle_watch_update(self.socketio_instance, watch=watch, datastore=self.datastore)
|
||||
logger.info(f"Signal handler processed watch UUID {watch_uuid}")
|
||||
else:
|
||||
logger.warning(f"Watch UUID {watch_uuid} not found in datastore")
|
||||
|
||||
class ChangeDetectionSocketIO:
|
||||
def __init__(self, app, datastore):
|
||||
self.main_app = app
|
||||
self.datastore = datastore
|
||||
|
||||
# Use threading mode instead of eventlet
|
||||
self.socketio = SocketIO(self.main_app,
|
||||
async_mode='threading',
|
||||
cors_allowed_origins="*",
|
||||
logger=False,
|
||||
engineio_logger=False)
|
||||
|
||||
# Set up event handlers
|
||||
self.socketio.on_event('connect', self.handle_connect)
|
||||
self.socketio.on_event('disconnect', self.handle_disconnect)
|
||||
|
||||
# Don't patch the update_watch method - this was causing issues
|
||||
# Just start a background thread to periodically emit watch status
|
||||
self.thread = None
|
||||
self.thread_lock = threading.Lock()
|
||||
|
||||
# Create a dedicated signal handler
|
||||
self.signal_handler = SignalHandler(self)
|
||||
def handle_watch_update(socketio, **kwargs):
|
||||
"""Handle watch update signal from blinker"""
|
||||
try:
|
||||
watch = kwargs.get('watch')
|
||||
datastore = kwargs.get('datastore')
|
||||
|
||||
def handle_connect(self):
|
||||
# Emit the watch update to all connected clients
|
||||
from changedetectionio.flask_app import running_update_threads, update_q
|
||||
|
||||
# Get list of watches that are currently running
|
||||
running_uuids = []
|
||||
for t in running_update_threads:
|
||||
if hasattr(t, 'current_uuid') and t.current_uuid:
|
||||
running_uuids.append(t.current_uuid)
|
||||
|
||||
# Get list of watches in the queue
|
||||
queue_list = []
|
||||
for q_item in update_q.queue:
|
||||
if hasattr(q_item, 'item') and 'uuid' in q_item.item:
|
||||
queue_list.append(q_item.item['uuid'])
|
||||
|
||||
# Create a simplified watch data object to send to clients
|
||||
watch_data = {
|
||||
'checking_now': True if watch.get('uuid') in running_uuids else False,
|
||||
'fetch_time': watch.get('fetch_time'),
|
||||
'has_error': watch.get('last_error') or watch.get('last_notification_error'),
|
||||
'last_changed': watch.get('last_changed'),
|
||||
'last_checked': watch.get('last_checked'),
|
||||
'last_checked_text': _jinja2_filter_datetime(watch),
|
||||
'last_changed_text': timeago.format(int(watch['last_changed']), time.time()) if watch.history_n >=2 and int(watch.get('last_changed',0)) >0 else 'Not yet',
|
||||
'queued': True if watch.get('uuid') in queue_list else False,
|
||||
'paused': True if watch.get('paused') else False,
|
||||
'notification_muted': True if watch.get('notification_muted') else False,
|
||||
'unviewed': watch.has_unviewed,
|
||||
'uuid': watch.get('uuid'),
|
||||
}
|
||||
socketio.emit("watch_update", watch_data)
|
||||
logger.debug(f"Socket.IO: Emitted update for watch {watch.get('uuid')}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")
|
||||
|
||||
|
||||
def init_socketio(app, datastore):
|
||||
"""Initialize SocketIO with the main Flask app"""
|
||||
# Use threading mode only - eventlet monkey patching causes issues
|
||||
# when patching after other modules have been imported
|
||||
async_mode = 'threading'
|
||||
logger.info("Using threading mode for Socket.IO (long-polling transport)")
|
||||
|
||||
socketio = SocketIO(app,
|
||||
async_mode=async_mode, # Use eventlet if available, otherwise threading
|
||||
cors_allowed_origins="*",
|
||||
logger=True,
|
||||
engineio_logger=True)
|
||||
|
||||
# Set up event handlers
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
"""Handle client connection"""
|
||||
logger.info("Socket.IO: Client connected")
|
||||
|
||||
from changedetectionio.auth_decorator import login_optionally_required
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
|
||||
def handle_disconnect(self):
|
||||
# Access datastore from socketio
|
||||
datastore = socketio.datastore
|
||||
|
||||
# Check if authentication is required and user is not authenticated
|
||||
has_password_enabled = datastore.data['settings']['application'].get('password') or os.getenv("SALTED_PASS", False)
|
||||
if has_password_enabled and not current_user.is_authenticated:
|
||||
logger.warning("Socket.IO: Rejecting unauthenticated connection")
|
||||
return False # Reject the connection
|
||||
|
||||
logger.info("Socket.IO: Client connected")
|
||||
|
||||
@socketio.on('disconnect')
|
||||
def handle_disconnect():
|
||||
"""Handle client disconnection"""
|
||||
logger.info("Socket.IO: Client disconnected")
|
||||
|
||||
def handle_watch_update(self, **kwargs):
|
||||
"""Handle watch update signal from blinker"""
|
||||
try:
|
||||
watch = kwargs.get('watch')
|
||||
# Emit the watch update to all connected clients
|
||||
with self.main_app.app_context():
|
||||
from changedetectionio.flask_app import running_update_threads, update_q
|
||||
|
||||
# Get list of watches that are currently running
|
||||
running_uuids = []
|
||||
for t in running_update_threads:
|
||||
if hasattr(t, 'current_uuid') and t.current_uuid:
|
||||
running_uuids.append(t.current_uuid)
|
||||
# Create a dedicated signal handler that will receive signals and emit them to clients
|
||||
signal_handler = SignalHandler(socketio, datastore)
|
||||
|
||||
# Get list of watches in the queue
|
||||
queue_list = []
|
||||
for q_item in update_q.queue:
|
||||
if hasattr(q_item, 'item') and 'uuid' in q_item.item:
|
||||
queue_list.append(q_item.item['uuid'])
|
||||
# Store the datastore reference on the socketio object for later use
|
||||
socketio.datastore = datastore
|
||||
|
||||
|
||||
# Create a simplified watch data object to send to clients
|
||||
watch_data = {
|
||||
'checking_now': True if watch.get('uuid') in running_uuids else False,
|
||||
'fetch_time': watch.get('fetch_time'),
|
||||
'has_error': watch.get('last_error') or watch.get('last_notification_error'),
|
||||
'last_changed': watch.get('last_changed'),
|
||||
'last_checked': watch.get('last_checked'),
|
||||
'last_checked_text': _jinja2_filter_datetime(watch),
|
||||
'last_changed_text': timeago.format(int(watch['last_changed']), time.time()) if watch.history_n >=2 and int(watch.get('last_changed',0)) >0 else 'Not yet',
|
||||
'queued': True if watch.get('uuid') in queue_list else False,
|
||||
'unviewed': watch.has_unviewed,
|
||||
'uuid': watch.get('uuid'),
|
||||
}
|
||||
self.socketio.emit("watch_update", watch_data)
|
||||
logger.debug(f"Socket.IO: Emitted update for watch {watch.get('uuid')}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Socket.IO error in handle_watch_update: {str(e)}")
|
||||
|
||||
|
||||
def run(self, host='0.0.0.0', port=5005):
|
||||
"""Run the Socket.IO server on a separate port"""
|
||||
# Start the background task when the server starts
|
||||
#self.start_background_task()
|
||||
|
||||
# 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.main_app, host=host, port=port, debug=False, use_reloader=False, allow_unsafe_werkzeug=True)
|
||||
logger.info("Socket.IO initialized and attached to main Flask app")
|
||||
return socketio
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
// Socket.IO client-side integration for changedetection.io
|
||||
// @todo only bind ajax if the socket server attached success.
|
||||
|
||||
$(document).ready(function () {
|
||||
// Try to create the socket connection to port 5005 - if it fails, the site will still work normally
|
||||
try {
|
||||
// Connect to the dedicated Socket.IO server on port 5005
|
||||
const socket = io('http://127.0.0.1:5005');
|
||||
$('.ajax-op').click(function (e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: ajax_toggle_url,
|
||||
data: {'op': $(this).data('op'), 'uuid': $(this).closest('tr').data('watch-uuid')},
|
||||
statusCode: {
|
||||
400: function () {
|
||||
// More than likely the CSRF token was lost when the server restarted
|
||||
alert("There was a problem processing the request, please reload the page.");
|
||||
}
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// Only try to connect if authentication isn't required or user is authenticated
|
||||
// The 'is_authenticated' variable will be set in the template
|
||||
if (typeof is_authenticated !== 'undefined' ? is_authenticated : true) {
|
||||
// Try to create the socket connection to the SocketIO server - if it fails, the site will still work normally
|
||||
try {
|
||||
// Connect to Socket.IO on the same host/port, with path from template
|
||||
const socket = io({
|
||||
path: socketio_url, // This will be the path prefix like "/app/socket.io" from the template
|
||||
transports: ['polling', 'websocket'], // Try WebSocket but fall back to polling
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionAttempts: 5
|
||||
});
|
||||
|
||||
// Connection status logging
|
||||
socket.on('connect', function () {
|
||||
@@ -15,32 +41,19 @@ $(document).ready(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_update', function (watch) {
|
||||
console.log(`Watch update ${watch.uuid}`);
|
||||
|
||||
|
||||
const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]');
|
||||
if ($watchRow.length) {
|
||||
$($watchRow).toggleClass('checking-now', watch.checking_now);
|
||||
$($watchRow).toggleClass('queued', watch.queued);
|
||||
$($watchRow).toggleClass('unviewed', watch.unviewed);
|
||||
$($watchRow).toggleClass('error', watch.has_error);
|
||||
$($watchRow).toggleClass('notification_muted', watch.notification_muted);
|
||||
$($watchRow).toggleClass('paused', watch.paused);
|
||||
|
||||
$('td.last-changed', $watchRow).text(watch.last_checked_text)
|
||||
$('td.last-checked .innertext', $watchRow).text(watch.last_checked_text)
|
||||
$('td.last-checked', $watchRow).data('timestamp', watch.last_checked).data('fetchduration', watch.fetch_time);
|
||||
@@ -53,4 +66,5 @@ $(document).ready(function () {
|
||||
// If Socket.IO fails to initialize, just log it and continue
|
||||
console.log('Socket.IO initialization error:', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -68,5 +68,25 @@
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
tr.paused {
|
||||
a.pause-toggle {
|
||||
&.state-on {
|
||||
display: inline !important;
|
||||
}
|
||||
&.state-off {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
tr.notification_muted {
|
||||
a.mute-toggle {
|
||||
&.state-on {
|
||||
display: inline !important;
|
||||
}
|
||||
&.state-off {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,14 @@ body.preview-text-enabled {
|
||||
display: none !important; }
|
||||
.watch-table tr.queued a.already-in-queue-button {
|
||||
display: inline-block !important; }
|
||||
.watch-table tr.paused a.pause-toggle.state-on {
|
||||
display: inline !important; }
|
||||
.watch-table tr.paused a.pause-toggle.state-off {
|
||||
display: none !important; }
|
||||
.watch-table tr.notification_muted a.mute-toggle.state-on {
|
||||
display: inline !important; }
|
||||
.watch-table tr.notification_muted a.mute-toggle.state-off {
|
||||
display: none !important; }
|
||||
|
||||
ul#conditions_match_logic {
|
||||
list-style: none; }
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<script>
|
||||
const csrftoken="{{ csrf_token() }}";
|
||||
const socketio_url="{{ get_socketio_path() }}/socket.io";
|
||||
const is_authenticated = {% if current_user.is_authenticated or not has_password %}true{% else %}false{% endif %};
|
||||
</script>
|
||||
<script src="{{url_for('static_content', group='js', filename='jquery-3.6.0.min.js')}}"></script>
|
||||
<script src="{{url_for('static_content', group='js', filename='csrf.js')}}" defer></script>
|
||||
|
||||
Reference in New Issue
Block a user