// Socket.IO client-side integration for changedetection.io $(document).ready(function () { function reapplyTableStripes() { $('.watch-table tbody tr').each(function(index) { $(this).removeClass('pure-table-odd pure-table-even'); $(this).addClass(index % 2 === 0 ? 'pure-table-odd' : 'pure-table-even'); }); } function bindSocketHandlerButtonsEvents(socket) { $('.ajax-op').on('click.socketHandlerNamespace', function (e) { e.preventDefault(); const op = $(this).data('op'); const uuid = $(this).closest('tr').data('watch-uuid'); console.log(`Socket.IO: Sending watch operation '${op}' for UUID ${uuid}`); // Emit the operation via Socket.IO socket.emit('watch_operation', { 'op': op, 'uuid': uuid }); return false; }); // Only the actual operation buttons carry name="op"; this excludes UI-only // buttons in the bar such as "Invert" (client-side selection toggle). $('#checkbox-operations button[name="op"]').on('click.socketHandlerNamespace', function (e) { e.preventDefault(); const $button = $(this); const op = $button.val(); // The cross-page selection store (watch-overview.js) is the source of // truth when present — it includes rows selected on other pages, not // just the visible checked boxes. Fall back to the DOM if absent. const watchSel = window.cdioWatchSelection; const checkedUuids = watchSel ? watchSel.all() : $('input[name="uuids"]:checked').map(function () { return this.value.trim(); }).get(); // Check if this button requires confirmation console.log('Button clicked, op:', op, 'requires-confirm:', $button.is('[data-requires-confirm]')); if ($button.is('[data-requires-confirm]')) { console.log('Showing modal confirmation for operation:', op); const config = { type: $button.data('confirm-type') || 'danger', title: $button.data('confirm-title') || 'Confirm Action', message: $button.data('confirm-message') || '
Are you sure you want to proceed?
', confirmText: $button.data('confirm-button') || 'Confirm', cancelText: $button.data('cancel-button') || 'Cancel', onConfirm: function() { console.log(`Socket.IO: Sending watch operation '${op}' for UUIDs:`, checkedUuids); socket.emit('checkbox-operation', { op: op, uuids: checkedUuids, extra_data: $('#op_extradata').val() }); // Keep the rows selected after the operation so further // actions can be applied to the same selection — except // delete, where the rows (and their UUIDs) are now gone. if (op === 'delete' && watchSel) { watchSel.clear(); if (watchSel.refreshUI) watchSel.refreshUI(); } } }; ModalDialog.confirm(config); } else { console.log(`Socket.IO: Sending watch operation '${op}' for UUIDs:`, checkedUuids); socket.emit('checkbox-operation', { op: op, uuids: checkedUuids, extra_data: $('#op_extradata').val() }); // Keep the rows selected after the operation so further actions // can be applied to the same selection. } return false; }); } // Cache DOM elements for performance. // queue-size-int is a class (not an id) because the count is rendered in // BOTH the desktop sidebar and the hamburger drawer — keep them in sync. const queueBubble = document.getElementById('queue-bubble'); const queueSizeNodes = document.querySelectorAll('.queue-size-int'); // "Checking now" count - rendered in both the desktop sidebar and the hamburger drawer. const checkingNowNodes = document.querySelectorAll('.checking-now-int'); // 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: ['websocket', 'polling'], reconnectionDelay: 3000, reconnectionAttempts: 25 }); // Expose the socket so other pages can attach their own listeners // (e.g. the queue page reacts to watch_update / queue_size events). window.cdioSocket = socket; document.dispatchEvent(new CustomEvent('cdio:socket-ready', { detail: { socket: socket } })); // Connection status logging socket.on('connect', function () { $('#realtime-conn-error').hide(); console.log('Socket.IO connected with path:', socketio_url); console.log('Socket transport:', socket.io.engine.transport.name); bindSocketHandlerButtonsEvents(socket); }); socket.on('connect_error', function(error) { console.error('Socket.IO connection error:', error); }); socket.on('connect_timeout', function() { console.error('Socket.IO connection timeout'); }); socket.on('error', function(error) { console.error('Socket.IO error:', error); }); socket.on('disconnect', function (reason) { console.log('Socket.IO disconnected, reason:', reason); $('.ajax-op').off('.socketHandlerNamespace'); $('#realtime-conn-error').show(); }); // Tell the server we're leaving cleanly so it can release the connection // immediately rather than waiting for a timeout. // Note: this only fires for voluntary closes (tab/window close, navigation away). // Hard kills, crashes and network drops will still timeout normally on the server. window.addEventListener('beforeunload', function () { socket.disconnect(); }); socket.on('queue_size', function (data) { console.log(`${data.event_timestamp} - Queue size update: ${data.q_length}`); const formattedCount = parseInt(data.q_length).toLocaleString() || 'None'; queueSizeNodes.forEach(node => { node.textContent = formattedCount; }); document.body.classList.toggle('has-queue', parseInt(data.q_length) > 0); // Update queue bubble in action sidebar //if (queueBubble) { if (0) { const count = parseInt(data.q_length) || 0; const oldCount = parseInt(queueBubble.getAttribute('data-count')) || 0; if (count > 0) { // Format number according to browser locale const formatter = new Intl.NumberFormat(navigator.language); queueBubble.textContent = formatter.format(count); queueBubble.setAttribute('data-count', count); queueBubble.classList.add('visible'); // Add large-number class for numbers > 999 if (count > 999) { queueBubble.classList.add('large-number'); } else { queueBubble.classList.remove('large-number'); } // Pulse animation if count changed if (count !== oldCount) { queueBubble.classList.remove('pulse'); // Force reflow to restart animation void queueBubble.offsetWidth; queueBubble.classList.add('pulse'); } } else { // Hide bubble when queue is empty queueBubble.classList.remove('visible', 'pulse', 'large-number'); queueBubble.setAttribute('data-count', '0'); } } }) socket.on('checking_now', function (data) { console.log(`${data.event_timestamp} - Checking now update: ${data.count}`); const formattedCount = parseInt(data.count).toLocaleString() || 'None'; checkingNowNodes.forEach(node => { node.textContent = formattedCount; }); document.body.classList.toggle('is-checking-now', parseInt(data.count) > 0); }) // Listen for operation results socket.on('operation_result', function (data) { if (data.success) { console.log(`Socket.IO: Operation '${data.operation}' completed successfully for UUID ${data.uuid}`); } else { console.error(`Socket.IO: Operation failed: ${data.error}`); alert(i18nT('requestProblem', 'There was a problem processing the request: %(error)s').split('%(error)s').join(data.error)); } }); // Server-driven feedback for bulk checkbox operations (real count / error from the server) socket.on('toast', function (data) { if (data && data.message && window.Toast) { const fn = window.Toast[data.type] || window.Toast.info; fn(data.message); } }); socket.on('watch_small_status_comment', function (data) { console.log(`Socket.IO: Operation watch_small_status_comment'${data.uuid}' status ${data.status}`); $('tr[data-watch-uuid="' + data.uuid + '"] td.last-checked .status-text').html(" ").text(data.status); }); socket.on('notification_event', function (data) { console.log(`Stub handler for notification_event ${data.watch_uuid}`) }); socket.on('watch_deleted', function (data) { $('tr[data-watch-uuid="' + data.uuid + '"] td').fadeOut(500, function () { $(this).closest('tr').remove(); reapplyTableStripes(); }); }); // So that the favicon is only updated when the server has written the scraped favicon to disk. socket.on('watch_bumped_favicon', function (watch) { const $watchRow = $(`tr[data-watch-uuid="${watch.uuid}"]`); if ($watchRow.length) { $watchRow.addClass('has-favicon'); // Because the event could be emitted from a process that is outside the app context, url_for() might not work. // Lets use url_for at template generation time to give us a PLACEHOLDER instead let favicon_url = favicon_baseURL.replace('/PLACEHOLDER', `/${watch.uuid}?cache=${watch.event_timestamp}`); console.log(`Setting favicon for UUID - ${watch.uuid} - ${favicon_url}`); $('img.favicon', $watchRow).attr('src', favicon_url); } }) socket.on('general_stats_update', function (general_stats) { $('#watch-table-wrapper').toggleClass("has-unread-changes", general_stats.unread_changes_count !==0) $('#watch-table-wrapper').toggleClass("has-error", general_stats.count_errors !== 0) // NB: the watch-list status .seg counts (unread/errors/deals) are // server-rendered and SCOPED to the current tag/processor/search, so we // deliberately don't overwrite them here with these GLOBAL totals — doing // so would show e.g. the whole-list unread count on a filtered view. // The left-rail "Page Watches" unread badge IS global — keep it live. const unreadText = new Intl.NumberFormat(navigator.language).format(general_stats.unread_changes_count); $('.js-unread-count').text(unreadText).toggle(general_stats.unread_changes_count !== 0); }); socket.on('watch_update', function (data) { const watch = data.watch; // Updating watch table rows const $watchRow = $('tr[data-watch-uuid="' + watch.uuid + '"]'); console.log('Found watch row elements:', $watchRow.length); if ($watchRow.length) { $($watchRow).toggleClass('checking-now', watch.checking_now); $($watchRow).toggleClass('queued', watch.queued); $($watchRow).toggleClass('unviewed', watch.unviewed); $($watchRow).toggleClass('has-error', watch.has_error); $($watchRow).toggleClass('has-favicon', watch.has_favicon); $($watchRow).toggleClass('notification_muted', watch.notification_muted); $($watchRow).toggleClass('paused', watch.paused); $($watchRow).toggleClass('single-history', watch.history_n === 1); $($watchRow).toggleClass('multiple-history', watch.history_n >= 2); $('td.title-col .error-text', $watchRow).html(watch.error_text) $('td.last-changed', $watchRow).text(watch.last_changed_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); $('td.last-checked', $watchRow).data('eta_complete', watch.last_checked + watch.fetch_time); console.log('Updated UI for watch:', watch.uuid); } $('body').toggleClass('checking-now', watch.checking_now && window.location.href.includes(watch.uuid)); }); } catch (e) { // If Socket.IO fails to initialize, just log it and continue console.log('Socket.IO initialization error:', e); } } });