WIP
This commit is contained in:
@@ -4,11 +4,14 @@
|
|||||||
|
|
||||||
__version__ = '0.49.15'
|
__version__ = '0.49.15'
|
||||||
|
|
||||||
from changedetectionio.strtobool import strtobool
|
# Set environment variables before importing other modules
|
||||||
from json.decoder import JSONDecodeError
|
|
||||||
import os
|
import os
|
||||||
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
||||||
|
# Import eventlet for WSGI server - no monkey patching to avoid conflicts
|
||||||
import eventlet
|
import eventlet
|
||||||
|
|
||||||
|
from changedetectionio.strtobool import strtobool
|
||||||
|
from json.decoder import JSONDecodeError
|
||||||
import eventlet.wsgi
|
import eventlet.wsgi
|
||||||
import getopt
|
import getopt
|
||||||
import platform
|
import platform
|
||||||
@@ -141,7 +144,27 @@ def main():
|
|||||||
logger.critical(str(e))
|
logger.critical(str(e))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Get the Flask app
|
||||||
app = changedetection_app(app_config, datastore)
|
app = changedetection_app(app_config, datastore)
|
||||||
|
|
||||||
|
# Now initialize Socket.IO after the app is fully set up
|
||||||
|
try:
|
||||||
|
from changedetectionio.realtime.socket_server import ChangeDetectionSocketIO
|
||||||
|
from changedetectionio.flask_app import socketio_server
|
||||||
|
import threading
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to initialize Socket.IO server: {str(e)}")
|
||||||
|
|
||||||
signal.signal(signal.SIGTERM, sigshutdown_handler)
|
signal.signal(signal.SIGTERM, sigshutdown_handler)
|
||||||
signal.signal(signal.SIGINT, sigshutdown_handler)
|
signal.signal(signal.SIGINT, sigshutdown_handler)
|
||||||
@@ -204,5 +227,7 @@ def main():
|
|||||||
server_side=True), app)
|
server_side=True), app)
|
||||||
|
|
||||||
else:
|
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)
|
eventlet.wsgi.server(eventlet.listen((host, int(port)), s_type), app)
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
|
|
||||||
{% set is_unviewed = watch.newest_history_key| int > watch.last_viewed and watch.history_n>=2 %}
|
{% set is_unviewed = watch.newest_history_key| int > watch.last_viewed and watch.history_n>=2 %}
|
||||||
{% set checking_now = is_checking_now(watch) %}
|
{% set checking_now = is_checking_now(watch) %}
|
||||||
<tr id="{{ watch.uuid }}"
|
<tr id="{{ watch.uuid }}" data-watch-uuid="{{ watch.uuid }}"
|
||||||
class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} processor-{{ watch['processor'] }}
|
class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} processor-{{ watch['processor'] }}
|
||||||
{% if watch.last_error is defined and watch.last_error != False %}error{% endif %}
|
{% if watch.last_error is defined and watch.last_error != False %}error{% endif %}
|
||||||
{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %}
|
{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from flask_restful import abort, Api
|
|||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_wtf import CSRFProtect
|
from flask_wtf import CSRFProtect
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
import eventlet
|
||||||
|
|
||||||
from changedetectionio import __version__
|
from changedetectionio import __version__
|
||||||
from changedetectionio import queuedWatchMetaData
|
from changedetectionio import queuedWatchMetaData
|
||||||
@@ -54,6 +55,9 @@ app = Flask(__name__,
|
|||||||
static_folder="static",
|
static_folder="static",
|
||||||
template_folder="templates")
|
template_folder="templates")
|
||||||
|
|
||||||
|
# Will be initialized in changedetection_app
|
||||||
|
socketio_server = None
|
||||||
|
|
||||||
# Enable CORS, especially useful for the Chrome extension to operate from anywhere
|
# Enable CORS, especially useful for the Chrome extension to operate from anywhere
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
@@ -215,7 +219,7 @@ class User(flask_login.UserMixin):
|
|||||||
def changedetection_app(config=None, datastore_o=None):
|
def changedetection_app(config=None, datastore_o=None):
|
||||||
logger.trace("TRACE log is enabled")
|
logger.trace("TRACE log is enabled")
|
||||||
|
|
||||||
global datastore
|
global datastore, socketio_server
|
||||||
datastore = datastore_o
|
datastore = datastore_o
|
||||||
|
|
||||||
# so far just for read-only via tests, but this will be moved eventually to be the main source
|
# so far just for read-only via tests, but this will be moved eventually to be the main source
|
||||||
@@ -467,6 +471,8 @@ def changedetection_app(config=None, datastore_o=None):
|
|||||||
if not os.getenv("GITHUB_REF", False) and not strtobool(os.getenv('DISABLE_VERSION_CHECK', 'no')):
|
if not os.getenv("GITHUB_REF", False) and not strtobool(os.getenv('DISABLE_VERSION_CHECK', 'no')):
|
||||||
threading.Thread(target=check_for_new_version).start()
|
threading.Thread(target=check_for_new_version).start()
|
||||||
|
|
||||||
|
# Return the Flask app - the Socket.IO will be attached to it but initialized separately
|
||||||
|
# This avoids circular dependencies
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
changedetectionio/realtime/__init__.py
Normal file
3
changedetectionio/realtime/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Socket.IO realtime updates module for changedetection.io
|
||||||
|
"""
|
||||||
126
changedetectionio/realtime/socket_server.py
Normal file
126
changedetectionio/realtime/socket_server.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
from flask import Flask
|
||||||
|
from flask_socketio import SocketIO
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
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,
|
||||||
|
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()
|
||||||
|
|
||||||
|
# Set up a simple index route for the Socket.IO app
|
||||||
|
@self.app.route('/')
|
||||||
|
def index():
|
||||||
|
return """
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>ChangeDetection.io Socket.IO Server</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>ChangeDetection.io Socket.IO Server</h1>
|
||||||
|
<p>This is the Socket.IO server for ChangeDetection.io real-time updates.</p>
|
||||||
|
<p>Socket.IO endpoint is available at: <code>/socket.io/</code></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def start_background_task(self):
|
||||||
|
"""Start the background task if it's not already running"""
|
||||||
|
with self.thread_lock:
|
||||||
|
if self.thread is None:
|
||||||
|
self.thread = threading.Thread(target=self.background_task)
|
||||||
|
self.thread.daemon = True
|
||||||
|
self.thread.start()
|
||||||
|
logger.info("Socket.IO: Started background task thread")
|
||||||
|
|
||||||
|
def handle_connect(self):
|
||||||
|
"""Handle client connection"""
|
||||||
|
logger.info("Socket.IO: Client connected")
|
||||||
|
|
||||||
|
# Start the background task when the first client connects
|
||||||
|
self.start_background_task()
|
||||||
|
|
||||||
|
def handle_disconnect(self):
|
||||||
|
"""Handle client disconnection"""
|
||||||
|
logger.info("Socket.IO: Client disconnected")
|
||||||
|
|
||||||
|
def background_task(self):
|
||||||
|
"""Background task that emits watch status periodically"""
|
||||||
|
check_interval = 3 # seconds between updates
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self.main_app.app_context():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Collect all watch data
|
||||||
|
watches_data = []
|
||||||
|
|
||||||
|
# Get list of watches that are currently running
|
||||||
|
from changedetectionio.flask_app import running_update_threads
|
||||||
|
currently_checking = []
|
||||||
|
|
||||||
|
# Make a copy to avoid issues if the list changes
|
||||||
|
threads_snapshot = list(running_update_threads)
|
||||||
|
for thread in threads_snapshot:
|
||||||
|
if hasattr(thread, 'current_uuid') and thread.current_uuid:
|
||||||
|
currently_checking.append(thread.current_uuid)
|
||||||
|
|
||||||
|
# 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': watch.get('last_checked', 0),
|
||||||
|
'last_changed': watch.get('newest_history_key', 0),
|
||||||
|
'history_n': watch.history_n if hasattr(watch, 'history_n') else 0,
|
||||||
|
'viewed': watch.get('viewed', True),
|
||||||
|
'paused': watch.get('paused', False),
|
||||||
|
'checking': uuid in currently_checking
|
||||||
|
}
|
||||||
|
watches_data.append(simplified_data)
|
||||||
|
|
||||||
|
# Emit all watch data periodically
|
||||||
|
self.socketio.emit('watch_data', watches_data)
|
||||||
|
logger.debug(f"Socket.IO: Emitted watch data for {len(watches_data)} watches")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Socket.IO error in background task: {str(e)}")
|
||||||
|
|
||||||
|
# Wait before next update
|
||||||
|
time.sleep(check_interval)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Socket.IO background task failed: {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.app, host=host, port=port, debug=False, use_reloader=False, allow_unsafe_werkzeug=True)
|
||||||
108
changedetectionio/static/js/edit-columns-resizer.js
Normal file
108
changedetectionio/static/js/edit-columns-resizer.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
$(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 = $('<div>', {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
78
changedetectionio/static/js/socket.js
Normal file
78
changedetectionio/static/js/socket.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Socket.IO client-side integration for changedetection.io
|
||||||
|
|
||||||
|
$(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');
|
||||||
|
|
||||||
|
// Connection status logging
|
||||||
|
socket.on('connect', function() {
|
||||||
|
console.log('Socket.IO connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('disconnect', function() {
|
||||||
|
console.log('Socket.IO disconnected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
|
||||||
|
// Update all watches with their current data
|
||||||
|
watches.forEach(function(watch) {
|
||||||
|
const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]');
|
||||||
|
if ($watchRow.length) {
|
||||||
|
updateWatchRow($watchRow, watch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to update a watch row with new data
|
||||||
|
function updateWatchRow($row, data) {
|
||||||
|
// Update the last-checked time
|
||||||
|
const $lastChecked = $row.find('.last-checked');
|
||||||
|
if ($lastChecked.length && data.last_checked) {
|
||||||
|
// Format as timeago if we have the timeago library available
|
||||||
|
if (typeof timeago !== 'undefined') {
|
||||||
|
$lastChecked.text(timeago.format(data.last_checked, Date.now()/1000));
|
||||||
|
} else {
|
||||||
|
// Simple fallback if timeago isn't available
|
||||||
|
const date = new Date(data.last_checked * 1000);
|
||||||
|
$lastChecked.text(date.toLocaleString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle the unviewed class based on viewed status
|
||||||
|
$row.toggleClass('unviewed', data.viewed === 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If Socket.IO fails to initialize, just log it and continue
|
||||||
|
console.log('Socket.IO initialization error:', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
30
changedetectionio/static/styles/scss/parts/_socket.scss
Normal file
30
changedetectionio/static/styles/scss/parts/_socket.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -101,6 +101,7 @@
|
|||||||
|
|
||||||
--color-watch-table-error: var(--color-dark-red);
|
--color-watch-table-error: var(--color-dark-red);
|
||||||
--color-watch-table-row-text: var(--color-grey-100);
|
--color-watch-table-row-text: var(--color-grey-100);
|
||||||
|
--color-change-highlight-rgb: 255, 215, 0; /* Gold color for socket.io highlight */
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-darkmode="true"] {
|
html[data-darkmode="true"] {
|
||||||
|
|||||||
@@ -1070,6 +1070,7 @@ ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "parts/_visualselector";
|
@import "parts/_visualselector";
|
||||||
|
@import "parts/_socket";
|
||||||
|
|
||||||
#webdriver_delay {
|
#webdriver_delay {
|
||||||
width: 5em;
|
width: 5em;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
</script>
|
</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='jquery-3.6.0.min.js')}}"></script>
|
||||||
<script src="{{url_for('static_content', group='js', filename='csrf.js')}}" defer></script>
|
<script src="{{url_for('static_content', group='js', filename='csrf.js')}}" defer></script>
|
||||||
|
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js" integrity="sha384-c79GN5VsunZvi+Q/WObgk2in0CbZsHnjEqvFxC5DxHn9lTfNce2WW6h2pH6u/kF+" crossorigin="anonymous"></script>
|
||||||
|
<script src="{{url_for('static_content', group='js', filename='socket.js')}}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="">
|
<body class="">
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ flask_restful
|
|||||||
flask_cors # For the Chrome extension to operate
|
flask_cors # For the Chrome extension to operate
|
||||||
flask_wtf~=1.2
|
flask_wtf~=1.2
|
||||||
flask~=2.3
|
flask~=2.3
|
||||||
|
flask-socketio>=5.5.1
|
||||||
|
python-socketio>=5.13.0
|
||||||
|
python-engineio>=4.12.0
|
||||||
inscriptis~=2.2
|
inscriptis~=2.2
|
||||||
pytz
|
pytz
|
||||||
timeago~=1.0
|
timeago~=1.0
|
||||||
|
|||||||
Reference in New Issue
Block a user