// Constants for favicon management const FAVICON_STATES = { INITIAL: 'initial', CUSTOM: 'custom', EMOJI: 'emoji', COLOR: 'color', RESTORING: 'restoring' }; const FAVICON_PRIORITIES = { RESTORE: 3, USER_ACTION: 2, ANIMATION: 1, DEFAULT: 0 }; // Constants for state management const STATE_KEYS = { SEQUENCE: 'tabtitle_sequence_state', FAVICON: 'tabtitle_favicon_state', USER_PREFS: 'tabtitle_user_prefs', VERSION: 'tabtitle_state_version' }; const CURRENT_STATE_VERSION = '1.0'; // State Manager for persistence class StateManager { constructor() { this.version = CURRENT_STATE_VERSION; this.initialized = false; this.storageAvailable = this._checkStorageAvailability(); } _checkStorageAvailability() { try { const storage = window.localStorage; const testKey = '__storage_test__'; storage.setItem(testKey, testKey); storage.removeItem(testKey); return true; } catch (e) { return false; } } initialize() { if (!this.storageAvailable) { console.warn('LocalStorage is not available. State persistence will be disabled.'); return; } // Check version and handle migrations if needed const savedVersion = localStorage.getItem(STATE_KEYS.VERSION); if (savedVersion !== this.version) { this._handleVersionMigration(savedVersion); } this.initialized = true; } _handleVersionMigration(oldVersion) { // Handle state migrations between versions // For now, just clear old state and set new version this.clearAll(); localStorage.setItem(STATE_KEYS.VERSION, this.version); } saveSequenceState(state) { if (!this.initialized || !this.storageAvailable) return; try { const serializedState = JSON.stringify({ currentIndex: state.currentIndex, isRunning: state.isRunning, startTime: state.startTime, currentItemStartTime: state.currentItemStartTime, delayElapsed: state.delayElapsed, remainingDelay: state.remainingDelay, sequenceMetadata: state.sequenceMetadata, timestamp: Date.now() }); localStorage.setItem(STATE_KEYS.SEQUENCE, serializedState); } catch (error) { console.warn('Failed to save sequence state:', error); } } loadSequenceState() { if (!this.initialized || !this.storageAvailable) return null; try { const savedState = localStorage.getItem(STATE_KEYS.SEQUENCE); if (!savedState) return null; const state = JSON.parse(savedState); // Only return state if it's less than 30 minutes old if (Date.now() - state.timestamp < 30 * 60 * 1000) { return { ...state, sequenceMetadata: state.sequenceMetadata || { groupId: null, type: null, totalItems: 0, completedItems: 0, lastCompletedTimestamp: null } }; } // Clear old state localStorage.removeItem(STATE_KEYS.SEQUENCE); return null; } catch (error) { console.warn('Failed to load sequence state:', error); return null; } } saveFaviconState(state) { if (!this.initialized || !this.storageAvailable) return; try { const serializedState = JSON.stringify({ currentFavicon: state.currentFavicon, isUpdating: state.isUpdating, original: state.original, customActive: state.customActive, // Add customActive flag to saved state timestamp: Date.now() }); localStorage.setItem(STATE_KEYS.FAVICON, serializedState); } catch (error) { console.warn('Failed to save favicon state:', error); } } loadFaviconState() { if (!this.initialized || !this.storageAvailable) return null; try { const savedState = localStorage.getItem(STATE_KEYS.FAVICON); if (!savedState) return null; const state = JSON.parse(savedState); // Only return state if it's less than 30 minutes old if (Date.now() - state.timestamp < 30 * 60 * 1000) { return state; } // Clear old state localStorage.removeItem(STATE_KEYS.FAVICON); return null; } catch (error) { console.warn('Failed to load favicon state:', error); return null; } } saveUserPreferences(prefs) { if (!this.initialized || !this.storageAvailable) return; try { localStorage.setItem(STATE_KEYS.USER_PREFS, JSON.stringify(prefs)); } catch (error) { console.warn('Failed to save user preferences:', error); } } loadUserPreferences() { if (!this.initialized || !this.storageAvailable) return null; try { const savedPrefs = localStorage.getItem(STATE_KEYS.USER_PREFS); return savedPrefs ? JSON.parse(savedPrefs) : null; } catch (error) { console.warn('Failed to load user preferences:', error); return null; } } clearAll() { if (!this.storageAvailable) return; Object.values(STATE_KEYS).forEach(key => { localStorage.removeItem(key); }); } } // Sequence types and manager class SequenceManager { constructor() { this.currentSequence = []; this.currentIndex = 0; this.state = { isRunning: false, startTime: 0, currentItemStartTime: 0, delayElapsed: 0, remainingDelay: 0, sequenceMetadata: { groupId: null, type: null, totalItems: 0, completedItems: 0, lastCompletedTimestamp: null, currentCycle: 0 } }; this.timeouts = { delay: null, duration: null }; this.defaultDelay = 0; this.retryAttempts = 0; this.maxRetries = 3; this.debug = true; this.lastError = null; } _log(message, data = null) { if (this.debug) { console.log(`[SequenceManager] ${message}`, data || ''); } } start(sequence, metadata = {}) { this._log('SequenceManager.start called with sequence:', sequence); this._log('SequenceManager.start called with metadata:', metadata); this.stop(); this.currentSequence = sequence; this.currentIndex = 0; this.lastError = null; this.state = { ...this.state, isRunning: true, startTime: Date.now(), currentItemStartTime: Date.now(), delayElapsed: 0, remainingDelay: 0, sequenceMetadata: { groupId: metadata.groupId || null, type: this._determineSequenceType(sequence), totalItems: sequence.length, completedItems: 0, lastCompletedTimestamp: null, currentCycle: 0 } }; this.retryAttempts = 0; if (this.currentSequence.length > 0) { this.startCurrentItem(); } } _determineSequenceType(sequence) { if (!sequence || sequence.length === 0) return null; const types = new Set(sequence.map(item => item.type)); if (types.size === 1) { return Array.from(types)[0]; // 'message' or 'favicon' } return 'mixed'; } stop() { this.state.isRunning = false; this.clearTimeouts(); this.retryAttempts = 0; } pause() { if (this.state.isRunning) { this.state.isRunning = false; this.state.delayElapsed = Date.now() - this.state.currentItemStartTime; this.state.remainingDelay = this._calculateRemainingDelay(); this.clearTimeouts(); } } resume() { if (!this.state.isRunning && this.currentSequence.length > 0) { this.state.isRunning = true; this.state.currentItemStartTime = Date.now() - this.state.delayElapsed; // Calculate remaining delay based on when we paused const remainingDelay = this._calculateRemainingDelay(); this.startCurrentItem(remainingDelay); } } _calculateRemainingDelay() { const currentItem = this.getCurrentItem(); if (!currentItem) return 0; const itemDelay = currentItem.delay || this.defaultDelay; const elapsedTime = Date.now() - this.state.currentItemStartTime; return Math.max(0, itemDelay - elapsedTime); } clearTimeouts() { Object.values(this.timeouts).forEach(timeout => { if (timeout) { clearTimeout(timeout); } }); this.timeouts = { delay: null, duration: null }; } startCurrentItem(remainingDelay = 0) { const item = this.getCurrentItem(); if (!item) return; const delay = remainingDelay > 0 ? remainingDelay : (item.delay || this.defaultDelay); if (delay > 0) { this.timeouts.delay = setTimeout(() => { this.executeItem(item); }, delay); } else { this.executeItem(item); } } executeItem(item) { if (document.hidden) { this._log('SequenceManager.executeItem called with item:', item); if (!this.state.isRunning) { this._log('SequenceManager.executeItem: WARNING - state is NOT running, but executing.'); } else { this._log('SequenceManager.executeItem: state is running, proceeding.'); } this._log('Executing sequence item', { item, currentIndex: this.currentIndex, timestamp: new Date(), cycle: this.state.sequenceMetadata.currentCycle }); } else { // Optional: console.log(`[SequenceManager] Executing item ${this.currentIndex} (tab visible)`); } if (!this.state.isRunning && !document.hidden) { this._log('SequenceManager.executeItem: State became not running while item was pending (likely tab became visible). Halting execution for this item.'); return; } try { this.clearTimeouts(); if (item.duration > 0) { this.timeouts.duration = setTimeout(() => { if (document.hidden) this._log('Item duration completed', { item, duration: item.duration }); this.moveToNextItem(); }, item.duration); } this.state.sequenceMetadata.completedItems++; if (this.onItemStart) { if (document.hidden) this._log('SequenceManager.executeItem is calling onItemStart for item:', item); this.onItemStart(item); } this.retryAttempts = 0; this.lastError = null; } catch (error) { this.lastError = error; this._log('Error executing item', error); // Error logs should remain unconditional this._handleExecutionError(item); } } _handleExecutionError(item) { if (this.retryAttempts < this.maxRetries) { this.retryAttempts++; this._log(`Retrying sequence item execution`, { attempt: this.retryAttempts, maxRetries: this.maxRetries, item, error: this.lastError }); // Store current state before retry const currentState = { ...this.state }; setTimeout(() => { // Restore state before retrying this.state = currentState; this.executeItem(item); }, 1000); } else { this._log('Max retry attempts reached', { item, error: this.lastError }); this.moveToNextItem(); } } moveToNextItem() { if (!this.state.isRunning) return; if (document.hidden) { this._log('Moving to next item', { currentIndex: this.currentIndex, nextIndex: (this.currentIndex + 1) % this.currentSequence.length }); } this.currentIndex = (this.currentIndex + 1) % this.currentSequence.length; this.state.currentItemStartTime = Date.now(); this.state.delayElapsed = 0; this.clearTimeouts(); if (this.currentIndex === 0) { // Completed one full cycle this.state.sequenceMetadata.currentCycle++; this.state.sequenceMetadata.lastCompletedTimestamp = Date.now(); if (this.onSequenceComplete) { this.onSequenceComplete(this.state.sequenceMetadata); } } this.startCurrentItem(); } getCurrentItem() { return this.currentSequence[this.currentIndex]; } setItemStartHandler(handler) { this.onItemStart = handler; } setSequenceCompleteHandler(handler) { this.onSequenceComplete = handler; } getState() { return { ...this.state, currentIndex: this.currentIndex, currentSequence: this.currentSequence, sequenceMetadata: this.state.sequenceMetadata }; } setState(state) { if (!state) return; this._log('Setting state', state); this.state = { isRunning: state.isRunning || false, startTime: state.startTime || 0, currentItemStartTime: state.currentItemStartTime || 0, delayElapsed: state.delayElapsed || 0, remainingDelay: state.remainingDelay || 0, sequenceMetadata: state.sequenceMetadata || { groupId: null, type: null, totalItems: 0, completedItems: 0, lastCompletedTimestamp: null } }; this.currentIndex = state.currentIndex || 0; if (state.currentSequence) { this.currentSequence = state.currentSequence; } } } class FaviconManager { constructor() { this.state = { isUpdating: false, // Internal flag to prevent observer feedback loops currentHref: null, // What the script has set the href to originalHref: null, // The href of the icon found on page load, or null customActive: false // Is the script actively managing the favicon? }; this.managedElement = null; // The element being managed this.hadOriginalLinkTag = false; // Was a tag present initially? this.observer = null; this.initialized = false; this.debug = true; } _log(message, data = null) { if (this.debug) { console.log(`[FaviconManager] ${message}`, data ? JSON.stringify(data, null, 2) : ''); } } initialize() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { this._log('DOMContentLoaded event fired'); setTimeout(() => this._init(), 100); }); } else { this._log('Document already loaded'); setTimeout(() => this._init(), 100); } window.addEventListener('load', () => { this._log('Window load event fired'); setTimeout(() => { // Delay to catch favicons added by other scripts on load const finalExistingElement = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]'); if (finalExistingElement && finalExistingElement.href) { if (!this.state.customActive) { // Only update original if script hasn't taken over this._log('Favicon link found/confirmed after window.load.', { href: finalExistingElement.href }); this.state.originalHref = finalExistingElement.href; // If script hasn't set a custom one yet, reflect this loaded one. this.state.currentHref = finalExistingElement.href; this.managedElement = finalExistingElement; this.hadOriginalLinkTag = true; } } else { // No favicon element found even after window.load if (!this.hadOriginalLinkTag && !this.state.customActive) { // and we didn't find one earlier and script is not active this._log('No favicon tag found after window.load, originalHref remains null.'); // this.state.originalHref is already null or was set to null in _init } } this._log('Favicon state after load event processed', { originalHref: this.state.originalHref, hadOriginalLinkTag: this.hadOriginalLinkTag, currentHref: this.state.currentHref }); }, 500); // Increased delay }); } _init() { this._log('Initializing FaviconManager'); const existingElement = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]'); if (existingElement && existingElement.href) { this._log('Found existing favicon element on init', { href: existingElement.href }); this.hadOriginalLinkTag = true; this.state.originalHref = existingElement.href; this.state.currentHref = existingElement.href; // Initially, current is original this.managedElement = existingElement; // Manage the existing element } else { this._log('No existing favicon tag found on init.'); this.hadOriginalLinkTag = false; this.state.originalHref = null; this.state.currentHref = null; this.managedElement = null; // No element to manage yet } this._log('Initial favicon state stored', { originalHref: this.state.originalHref, hadOriginalLinkTag: this.hadOriginalLinkTag }); this.observer = new MutationObserver((mutations) => { if (this.state.isUpdating || this.state.customActive || document.hidden) return; mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === 'href' && mutation.target.hasAttribute('rel') && (mutation.target.getAttribute('rel') === 'icon' || mutation.target.getAttribute('rel') === 'shortcut icon')) { const newHref = mutation.target.href; // Check if the href is not empty and actually changed from what we know as original if (newHref && newHref !== this.state.originalHref) { this._log('External update to a favicon link detected while script inactive and tab visible. Updating originalHref.', { newHref }); this.state.originalHref = newHref; this.state.currentHref = newHref; // Reflect this as current this.managedElement = mutation.target; // Consider this the element to manage now this.hadOriginalLinkTag = true; // If an icon appeared/changed, we now consider it as having an original tag. } } // If a link tag is ADDED and we didn't have one previously recorded: if (mutation.type === 'childList' && !this.hadOriginalLinkTag) { mutation.addedNodes.forEach(node => { if (node.nodeName === 'LINK' && (node.getAttribute('rel') === 'icon' || node.getAttribute('rel') === 'shortcut icon') && node.href) { this._log('Favicon link added externally, adopting as original.', { href: node.href }); this.state.originalHref = node.href; this.state.currentHref = node.href; this.managedElement = node; this.hadOriginalLinkTag = true; } }); } }); }); this.observer.observe(document.head, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); this.initialized = true; this._log('FaviconManager initialized'); } setFavicon(url) { if (!this.initialized || !url) { this._log('Cannot set favicon - not initialized or no URL provided', { initialized: this.initialized, url }); return; } this._log('Setting favicon', { url, previousHref: this.state.currentHref }); this.state.isUpdating = true; if (!this.managedElement) { // If no original tag was found and we haven't created one yet. this._log('No managed favicon element, creating a new one.'); this.managedElement = document.createElement('link'); this.managedElement.rel = 'icon'; document.head.appendChild(this.managedElement); // this.hadOriginalLinkTag remains false if it was initially false. // this.state.originalHref remains null if it was initially null. } this.managedElement.href = url; this.state.currentHref = url; this.state.customActive = true; // Script is now actively controlling the favicon. this._log('Favicon href updated on managed element.'); setTimeout(() => { this.state.isUpdating = false; this._log('Reset updating flag'); }, 100); } restore() { if (!this.initialized) { this._log('Cannot restore favicon - not initialized.'); return; } this._log('Attempting to restore favicon.', { originalHref: this.state.originalHref, hadOriginalLinkTag: this.hadOriginalLinkTag }); this.state.customActive = false; // Signal script is no longer managing favicon early this.state.isUpdating = true; if (this.hadOriginalLinkTag && this.state.originalHref) { // An original favicon tag was present and we have its href. if (!this.managedElement) { // This implies the original (or our managed one) was removed from DOM by external script. // Try to find it again or re-create a link element for the original. let existingOriginal = null; const favicons = document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]'); for (let i = 0; i < favicons.length; i++) { if (favicons[i].href === this.state.originalHref) { existingOriginal = favicons[i]; break; } } if (existingOriginal) { this.managedElement = existingOriginal; this._log('Found existing original element again.'); } else { this._log('Original link tag was present, but managedElement is null and original not found. Re-creating link for original favicon.'); this.managedElement = document.createElement('link'); this.managedElement.rel = 'icon'; document.head.appendChild(this.managedElement); } } this.managedElement.href = this.state.originalHref; this.state.currentHref = this.state.originalHref; this._log('Restored to original favicon href.'); } else { // No original tag was present (this.hadOriginalLinkTag is false), // or originalHref was null/empty. // Remove the script-managed element if it exists and we likely created it. if (this.managedElement && !this.hadOriginalLinkTag) { this._log('No original tag, removing script-managed favicon element.'); if (this.managedElement.parentElement) { this.managedElement.remove(); } this.managedElement = null; } else if (this.managedElement && this.hadOriginalLinkTag && !this.state.originalHref) { // Original tag existed but href was null/empty. If we are managing it, clear its href or remove if script-created. // For simplicity, if originalHref is effectively none, and we have a managedElement, remove it if we think we made it. // Or, if it was the original empty-href tag, what to do? For now, if originalHref is falsey, we clear out. this._log('Original tag had null/empty href, or originalHref is null. Removing current managed element if it exists.'); if (this.managedElement.parentElement) { this.managedElement.remove(); } this.managedElement = null; } else { this._log('No original tag and no script-managed element to remove, or original tag is being kept as is.'); } this.state.currentHref = null; // No specific favicon href after restore in this case. } this.state.isUpdating = false; this._log('Favicon restoration process complete.', { currentHref: this.state.currentHref, managedElement: !!this.managedElement }); } } class TabTitle { constructor(apiKey, domainId) { console.log("TabTitle CONSTRUCTOR CALLED - Checkpoint 1"); // Unconditional log if (!apiKey) { console.error("TabTitle: API key is required"); return; } if (!domainId) { console.error("TabTitle: Domain ID is required"); return; } this.apiKey = apiKey; this.domainId = domainId; this.sessionId = typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : this._generateSimpleUUID(); this.config = {}; this.messages = []; this.isPageVisible = !document.hidden; this.originalTitle = document.title; this.veryFirstOriginalTitle = document.title; this.currentMessageTimeout = null; this.sequences = []; this.lastState = null; this.faviconRules = []; this.currentFaviconRule = null; this.faviconInterval = null; this.faviconTimeout = null; this.inactiveAt = null; this.debug = true; this.apiBaseUrl = 'http://tabtitle.io'; // Initialize managers this.stateManager = new StateManager(); this.stateManager.initialize(); this.faviconManager = new FaviconManager(); this.faviconManager.initialize(); this.sequenceManager = new SequenceManager(); this.sequenceManager.setItemStartHandler(this._handleSequenceItem.bind(this)); this.sequenceManager.setSequenceCompleteHandler(this._handleSequenceComplete.bind(this)); // Restore saved states this._restoreSavedStates(); // Set up title observer before initializing const titleObserver = new MutationObserver(() => { if (document.title !== this.originalTitle && !this.currentMessageTimeout) { this.originalTitle = document.title; } }); titleObserver.observe(document.querySelector('title') || document.head, { subtree: true, characterData: true, childList: true }); this.originalFavicon = this._getFaviconUrl(); document.addEventListener( "visibilitychange", this._handleVisibilityChange.bind(this) ); // Set up periodic state saving setInterval(() => { this._saveCurrentStates(); }, 5000); // Save states every 5 seconds this._fetchMessages(); this._fetchFaviconRules(); setInterval(() => { this._fetchMessages(); this._fetchFaviconRules(); }, 5 * 60 * 1000); this._log('TabTitle Initialized with API Key:', apiKey, 'Domain ID:', this.domainId); } _log(message, ...args) { if (this.debug) { console.log(`[TabTitle] ${message}`, ...args); } } _generateSimpleUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } _setupEventListeners() { document.addEventListener('visibilitychange', this._handleVisibilityChange.bind(this)); window.addEventListener('beforeunload', this._saveCurrentStates.bind(this)); } _restoreSavedStates() { // Restore sequence state const sequenceState = this.stateManager.loadSequenceState(); if (sequenceState && this.sequenceManager) { this.sequenceManager.setState(sequenceState); if (sequenceState.isRunning && !this.isPageVisible) { // Resume sequence if it was running this.sequenceManager.resume(); } } // Restore favicon state const faviconState = this.stateManager.loadFaviconState(); if (faviconState && this.faviconManager) { this.faviconManager.state = faviconState; if (faviconState.currentFavicon && !this.isPageVisible) { this.faviconManager.setFavicon(faviconState.currentFavicon); } } // Restore user preferences const userPrefs = this.stateManager.loadUserPreferences(); if (userPrefs) { // Apply user preferences (animation speeds, delays, etc.) this._applyUserPreferences(userPrefs); } } _saveCurrentStates() { if (this.sequenceManager) { this.stateManager.saveSequenceState(this.sequenceManager.getState()); } if (this.faviconManager) { this.stateManager.saveFaviconState(this.faviconManager.state); } } _applyUserPreferences(prefs) { if (prefs.defaultDelay) { this.sequenceManager.defaultDelay = prefs.defaultDelay; } // Add more preference applications as needed } _handleVisibilityChange() { this._log('Visibility changed. New document.hidden state:', document.hidden, 'Previous this.isPageVisible:', this.isPageVisible); const newVisibility = !document.hidden; this.isPageVisible = newVisibility; // Update the internal state first if (!this.isPageVisible) { // Tab became hidden (document.hidden is true) this._log('[TabTitle] Handling HIDDEN state because this.isPageVisible is now false.'); this.inactiveAt = Date.now(); this.sendAnalyticsEvent('TAB_INACTIVE'); this.sequenceManager.pause(); this._saveCurrentStates(); this._applyFaviconRules(); this._startApplicableSequence(); // Fallback to standalone messages if no sequence is now running if (!this.sequenceManager.state.isRunning) { this._startTitleChange(); } } else { // Tab became visible this._log('[TabTitle] Handling VISIBLE state because this.isPageVisible is now true.'); if (this.inactiveAt) { const timeAwayMs = Date.now() - this.inactiveAt; this.sendAnalyticsEvent('TAB_REENGAGED', { timeAwayMs }); this.inactiveAt = null; } this.sequenceManager.resume(); this._log(`[TabTitle] VISIBLE: Restoring title to veryFirstOriginalTitle: "${this.veryFirstOriginalTitle}". Current document.title: "${document.title}"`); document.title = this.veryFirstOriginalTitle; // Use the snapshot here this._log(`[TabTitle] VISIBLE: Title after attempting restore: "${document.title}"`); if (this.currentMessageTimeout) { clearTimeout(this.currentMessageTimeout); this.currentMessageTimeout = null; } this._stopFaviconChange(); } } _applyFaviconConfig(rule, source = 'rule') { console.log('Applying favicon config:', rule, 'Source:', source); if (!rule || !rule.favicon_config) { console.warn('Invalid favicon config:', rule); return; } const config = rule.favicon_config; if (!config.type) { console.warn('Missing favicon type:', config); return; } try { if (config.type === 'custom' && config.customIconUrl) { console.log('Setting custom favicon:', config.customIconUrl); this.faviconManager.setFavicon(config.customIconUrl); } else if (config.type === 'emoji' && config.emoji) { console.log('Creating emoji favicon:', config.emoji); const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32; const ctx = canvas.getContext('2d'); if (!ctx) { console.error('Failed to get canvas context'); return; } // Clear canvas with transparency instead of white background ctx.clearRect(0, 0, 32, 32); // Draw emoji ctx.font = '28px Arial'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; try { ctx.fillText(config.emoji, 16, 16); const faviconUrl = canvas.toDataURL('image/png'); console.log('Generated emoji favicon URL:', faviconUrl.substring(0, 100) + '...'); this.faviconManager.setFavicon(faviconUrl); } catch (err) { console.error('Error drawing emoji:', err); } } else if (config.type === 'color' && config.colors && config.colors.length > 0) { console.log('Setting up color favicon with colors:', config.colors); let colorIndex = 0; // Set initial color this._setColorFavicon(config.colors[0]); // Clear any existing interval if (this.faviconInterval) { clearInterval(this.faviconInterval); } // Set up color animation if we have multiple colors and timing if (config.colors.length > 1 && config.timing) { console.log('Starting color animation with timing:', config.timing); this.faviconInterval = setInterval(() => { colorIndex = (colorIndex + 1) % config.colors.length; this._setColorFavicon(config.colors[colorIndex]); }, config.timing); } } else { console.warn('Unsupported favicon type or missing required properties:', config); } } catch (err) { console.error('Error applying favicon config:', err); } if (source === 'rule' && rule.id) { this.sendAnalyticsEvent('FAVICON_RULE_APPLIED', { ruleId: rule.id, faviconType: config.type, delayTime: rule.delay_time || 0 // Assuming delay_time is in seconds }); } } _setColorFavicon(color) { console.log('Setting color favicon:', color); const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32; const ctx = canvas.getContext('2d'); if (!ctx) { console.error('Failed to get canvas context for color favicon'); return; } ctx.fillStyle = color; ctx.fillRect(0, 0, 32, 32); try { const faviconUrl = canvas.toDataURL('image/png'); console.log('Generated color favicon URL:', faviconUrl.substring(0, 100) + '...'); this.faviconManager.setFavicon(faviconUrl); } catch (err) { console.error('Error creating color favicon:', err); } } _getFaviconUrl() { const favicon = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]'); return favicon ? favicon.href : null; } _setFavicon(url) { if (!url) return; this.faviconManager.setFavicon(url, { delay: 0, cacheBust: true }); } _restoreOriginalFavicon() { if (this.faviconManager.state.original) { this.faviconManager.restore(); } } _createColorFavicon(color) { const canvas = document.createElement('canvas'); canvas.width = 32; canvas.height = 32; const ctx = canvas.getContext('2d'); ctx.fillStyle = color; ctx.fillRect(0, 0, 32, 32); return canvas.toDataURL(); } async _fetchFaviconRules() { if (!this.apiKey) { this._log("FaviconRules: API Key not set, skipping fetch."); return; } this._log("Fetching favicon rules..."); try { const response = await fetch(`${this.apiBaseUrl}/api/favicon-rules`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); if (!response.ok) { let errorText = 'Unknown error'; try { errorText = await response.text(); } catch (e) { /* ignore */ } this._log(`Error fetching favicon rules. Status: ${response.status}, Text: ${errorText}`); throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); if (!data || !data.rules) { this._log('Favicon rules data is not in expected format:', data); this.faviconRules = []; return; } this.faviconRules = data.rules || []; // Corrected from data.faviconRules to data.rules this._log(`Fetched favicon rules count: ${this.faviconRules.length}`); // Send analytics event for favicon rules loaded this.sendAnalyticsEvent('CONFIG_FAVICON_RULES_LOADED', { ruleCount: this.faviconRules.length }); // If page is hidden, apply rules if (document.hidden) { this._applyFaviconRules(); } } catch (error) { console.error("TabTitle: Failed to fetch favicon rules:", error); this.faviconRules = []; // Ensure it's an array on error } } _checkFaviconConditions(rule) { const currentPath = window.location.pathname; let conditionsMet = true; let reason = ''; this._log(`Checking favicon rule: ${rule.name || rule.id}, path: ${rule.path}, schedule:`, rule.schedule, `against current path: ${currentPath}`); // this._log(`Checking favicon rule: ${rule.name || rule.id}, path: ${rule.path}, schedule: ${JSON.stringify(rule.schedule)} against current path: ${currentPath}`); // Check path first, independent of schedule if (rule.path && rule.path !== '/') { if (!currentPath.startsWith(rule.path)) { reason = `Path condition failed. Rule path: ${rule.path}, current path: ${currentPath}`; return false; } } // If no schedule, we've already checked the path if (!rule.schedule) { this._log(`Favicon rule ${rule.name || rule.id} matched (no schedule, path check passed or not applicable).`); return true; } const now = new Date(); // Check time range conditions if (rule.schedule.timeRanges?.length > 0) { const currentTime = now.getHours() * 60 + now.getMinutes(); const matchesTime = rule.schedule.timeRanges.some((range) => { const [start, end] = range.split("-"); const [startHour, startMinute] = start.split(":").map(Number); const [endHour, endMinute] = end.split(":").map(Number); const startTime = startHour * 60 + startMinute; const endTime = endHour * 60 + endMinute; return currentTime >= startTime && currentTime <= endTime; }); if (!matchesTime) return false; } // Check days of week conditions if (rule.schedule.daysOfWeek?.length > 0) { if (!rule.schedule.daysOfWeek.includes(now.getDay())) { reason = `Day of week condition failed. Rule days of week: ${rule.schedule.daysOfWeek.join(', ')}, current day: ${now.getDay()}`; return false; } } // Check date range conditions if (rule.schedule.dateRanges?.length > 0) { const currentDate = now.toISOString().split("T")[0]; const matchesDate = rule.schedule.dateRanges.some((range) => { const [start, end] = range.split("__"); return currentDate >= start && currentDate <= end; }); if (!matchesDate) return false; } this._log(`Favicon rule ${rule.name || rule.id} matched all conditions.`); return true; } _applyFaviconRules() { this._log('[TabTitle] Attempting to apply favicon rules.'); if (this.isPageVisible) { this._log('[TabTitle] Page is visible, skipping applyFaviconRules.'); return; } // Clear any existing animation if (this.faviconInterval) { clearInterval(this.faviconInterval); this.faviconInterval = null; } if (this.faviconTimeout) { clearTimeout(this.faviconTimeout); this.faviconTimeout = null; } // Find ALL matching rules and sort by priority const matchingRules = this.faviconRules .filter(rule => this._checkFaviconConditions(rule)) .sort((a, b) => b.priority - a.priority); this._log(`After filtering, ${matchingRules.length} favicon rules match.`); if (matchingRules.length === 0) { // Restore original favicon if no rules match if (this.faviconManager.state.original) { this._setFavicon(this.faviconManager.state.original.url); } return; } // Get the highest priority const highestPriority = matchingRules[0].priority; // Get all rules with the highest priority const highestPriorityRules = matchingRules.filter(rule => rule.priority === highestPriority); if (highestPriorityRules.length === 1) { // If only one rule, apply it normally this._applySingleFaviconRule(highestPriorityRules[0], this.faviconManager.state); } else { // If multiple rules with same priority, cycle between them this._cycleMultipleFaviconRules(highestPriorityRules, this.faviconManager.state); } } _applySingleFaviconRule(rule, state) { if (!rule || !rule.favicon_config) return; // this._log('[TabTitle] Applying single favicon rule:', rule); this._applyFaviconConfig(rule, 'rule'); state.lastAppliedRuleIndex = state.rules.indexOf(rule); state.lastAppliedTime = Date.now(); // If there's a timing for this specific rule (e.g. for temporary favicons in a sequence) // or if it's a cycle and has a duration, set a timeout to restore or move to the next. // This part might need integration with how sequence step durations are handled if a favicon rule is part of a sequence step. const ruleDuration = rule.favicon_config.timing || (state.isCycling ? state.cycleDuration : null); if (ruleDuration && ruleDuration > 0) { this._log(`Rule ${rule.name} has duration ${ruleDuration}. Scheduling restore/next.`); state.currentRuleTimeout = setTimeout(() => { if (state.isCycling) { this._log(`Cycling from rule ${rule.name}.`); this._cycleMultipleFaviconRules(state.rules, state); // Move to next or loop } else { this._log(`Duration for rule ${rule.name} ended. Restoring original.`); this.faviconManager.restore(); } }, ruleDuration); } } _cycleMultipleFaviconRules(rules, state) { let currentIndex = 0; let lastExecutionTime = 0; const scheduleNextRule = (isFirst = false) => { const rule = rules[currentIndex]; const nextIndex = (currentIndex + 1) % rules.length; // For first iteration, apply the initial delay if it exists const delay = isFirst && rule.delay_time ? rule.delay_time * 1000 : 0; // Calculate actual timing based on last execution const now = Date.now(); const timeSinceLastExecution = lastExecutionTime ? now - lastExecutionTime : 0; const adjustedDelay = Math.max(0, delay - timeSinceLastExecution); // Schedule the current rule this.faviconTimeout = setTimeout(() => { if (!this.isPageVisible) { if (document.hidden) { this._log(`Cycling to favicon rule: ${rule.name || rule.id}`, { ruleConfig: rule.favicon_config, delay: adjustedDelay }); } this._applyFaviconConfig(rule, 'rule'); lastExecutionTime = Date.now(); // Schedule the next rule after the current rule's timing const timing = rule.favicon_config.timing || 1000; currentIndex = nextIndex; // Clear existing timeout before creating new one if (this.faviconTimeout) { clearTimeout(this.faviconTimeout); } // Schedule next rule with its timing (not delay, as delays only apply to first iteration) this.faviconTimeout = setTimeout(() => { scheduleNextRule(false); }, timing); } }, adjustedDelay); }; // Clear any existing timeouts and intervals if (this.faviconInterval) { clearInterval(this.faviconInterval); this.faviconInterval = null; } if (this.faviconTimeout) { clearTimeout(this.faviconTimeout); this.faviconTimeout = null; } // Start the sequence scheduleNextRule(true); } _startFaviconChange() { if (this.isPageVisible) return; console.log('Starting favicon change'); // Clear any existing animations first if (this.faviconInterval) { clearInterval(this.faviconInterval); this.faviconInterval = null; } if (this.faviconTimeout) { clearTimeout(this.faviconTimeout); this.faviconTimeout = null; } // Find matching rules const matchingRules = this.faviconRules .filter(rule => this._checkFaviconConditions(rule)) .sort((a, b) => b.priority - a.priority); if (matchingRules.length === 0) return; // Get highest priority rules const highestPriority = matchingRules[0].priority; const highestPriorityRules = matchingRules.filter(rule => rule.priority === highestPriority); // Set customActive flag before applying rules this.faviconManager.state.customActive = true; // Apply rules if (highestPriorityRules.length === 1) { this._applySingleFaviconRule(highestPriorityRules[0], this.faviconManager.state); } else { this._cycleMultipleFaviconRules(highestPriorityRules, this.faviconManager.state); } } _stopFaviconChange() { // Clear any animations if (this.faviconInterval) { clearInterval(this.faviconInterval); this.faviconInterval = null; } if (this.faviconTimeout) { clearTimeout(this.faviconTimeout); this.faviconTimeout = null; } // Update elapsed time if a delay was active if (this.faviconManager.state.isDelayActive && this.faviconManager.state.startTime) { this.faviconManager.state.delayElapsed = Date.now() - this.faviconManager.state.startTime; } // Always restore the original favicon when stopping this._restoreOriginalFavicon(); } async _fetchMessages() { try { const response = await fetch(`${this.apiBaseUrl}/api/messages`, { headers: { 'Authorization': `Bearer ${this.apiKey}` } }); if (!response.ok) { let errorText = 'Unknown error'; try { errorText = await response.text(); } catch (e) { /* ignore */ } this._log(`Error fetching messages. Status: ${response.status}, Text: ${errorText}`); throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); // Ensure data and data.messages are valid before proceeding if (!data || !Array.isArray(data.messages)) { this._log('Messages data is not in expected format or messages array is missing:', data); this.messages = []; this.sequences = []; // Optionally send an analytics event for failed/malformed config load this.sendAnalyticsEvent('CONFIG_MESSAGES_LOADED', { messageCount: 0, sequenceCount: 0, standaloneMessageCount: 0, error: 'Malformed data' }); return; } this._log(`Fetched messages count: ${data.messages.length}`); // Process messages and sequences const processedData = this._processSequenceData(data.messages); this.messages = processedData.standaloneMessages || []; this.sequences = processedData.groupedSequences || []; this._log(`Processed standalone messages count: ${this.messages.length}`); this._log(`Processed sequences for SequenceManager count: ${this.sequences.length}`); // Send analytics event for messages loaded this.sendAnalyticsEvent('CONFIG_MESSAGES_LOADED', { messageCount: data.messages.length, // Total raw messages fetched sequenceCount: this.sequences.length, standaloneMessageCount: this.messages.length }); // Only start animations if page is hidden if (document.hidden) { this._startTitleChange(); // For standalone messages this._startApplicableSequence(); // For sequences } } catch (error) { console.error("TabTitle: Failed to fetch messages:", error); this.messages = []; this.sequences = []; // Optionally send an analytics event for fetch error this.sendAnalyticsEvent('CONFIG_MESSAGES_LOADED', { messageCount: 0, sequenceCount: 0, standaloneMessageCount: 0, error: 'Fetch error' }); } } _processSequenceData(allMessages) { if (!Array.isArray(allMessages)) { this._log('Error: _processSequenceData expected an array, got:', allMessages); return { standaloneMessages: [], groupedSequences: [] }; } const standaloneMessages = []; const sequenceMap = new Map(); allMessages.forEach(msg => { if (msg.sequence_id) { if (!sequenceMap.has(msg.sequence_id)) { // Initialize sequence metadata. // This assumes sequence-level properties like path or conditions // would be present on each message belonging to the sequence, // or would need to be fetched separately if sequences are distinct entities. // For now, metadata is minimal. sequenceMap.set(msg.sequence_id, { metadata: { groupId: msg.sequence_id, // TODO: Determine how sequence-wide conditions, path, priority, etc. are defined. // For now, these will be derived from the first message or assumed to be handled by item conditions. // path: msg.conditions?.paths?.[0], // Example: take path from first message // conditions: msg.conditions // Example: take conditions from first message }, items: [] }); } // Structure items as expected by SequenceManager.start() and _handleSequenceItem sequenceMap.get(msg.sequence_id).items.push({ id: msg.id, // Message ID type: 'message', // All sequence items are 'message' type for now content: { message: { content: msg.content } }, // Nested structure for _handleSequenceItem delay: (msg.delay_time || 0) * 1000, // SM expects milliseconds duration: (msg.duration || 0) * 1000, // SM expects milliseconds originalMessage: msg // Keep original message for access to all its properties (like sequence_order) }); } else { standaloneMessages.push(msg); } }); // Sort items within each sequence by sequence_order sequenceMap.forEach(seq => { seq.items.sort((a, b) => (a.originalMessage.sequence_order || 0) - (b.originalMessage.sequence_order || 0)); }); const groupedSequences = Array.from(sequenceMap.values()); this._log('Processed standalone messages:', standaloneMessages.length); this._log('Processed grouped sequences:', groupedSequences.length, groupedSequences.map(s => ({id: s.metadata.groupId, items: s.items.length }) )); return { standaloneMessages, groupedSequences }; } _checkSequenceConditions(sequence) { if (!sequence.metadata.conditions && !sequence.metadata.path) { return true; } const currentPath = window.location.pathname; // Check path first if (sequence.metadata.path && !currentPath.startsWith(sequence.metadata.path)) { return false; } // If no conditions, path check was sufficient if (!sequence.metadata.conditions) { return true; } return this._checkConditions({ conditions: sequence.metadata.conditions }); } _stopTitleChange() { document.title = this.veryFirstOriginalTitle; if (this.currentMessageTimeout) { clearTimeout(this.currentMessageTimeout); this.currentMessageTimeout = null; } } _checkConditions(message) { if (!message.conditions) return true; const now = new Date(); const currentPath = window.location.pathname; const messageIdStr = message.id || 'N/A'; const timeStr = now.toLocaleTimeString(); const dayStr = now.getDay(); const dateStr = now.toISOString().split('T')[0]; const logMsg = `Checking conditions for message ID ${messageIdStr} against path: ${currentPath}, time: ${timeStr}, day: ${dayStr}, date: ${dateStr}`; this._log(logMsg); this._log('Message conditions:', message.conditions); // this._log('Message conditions:', JSON.stringify(message.conditions)); // Check path conditions if (message.conditions.paths?.length > 0) { const matchesPath = message.conditions.paths.some( (path) => currentPath === path || currentPath.startsWith(path) ); if (!matchesPath) return false; } // Check time range conditions if (message.conditions.timeRanges?.length > 0) { const currentTime = now.getHours() * 60 + now.getMinutes(); const matchesTime = message.conditions.timeRanges.some((range) => { const [start, end] = range.split("-"); const [startHour, startMinute] = start.split(":").map(Number); const [endHour, endMinute] = end.split(":").map(Number); const startTime = startHour * 60 + startMinute; const endTime = endHour * 60 + endMinute; return currentTime >= startTime && currentTime <= endTime; }); if (!matchesTime) return false; } // Check days of week conditions if (message.conditions.daysOfWeek?.length > 0) { if (!message.conditions.daysOfWeek.includes(now.getDay())) { return false; } } // Check date range conditions if (message.conditions.dateRanges?.length > 0) { const currentDate = now.toISOString().split("T")[0]; const matchesDate = message.conditions.dateRanges.some((range) => { const [start, end] = range.split("__"); return currentDate >= start && currentDate <= end; }); if (!matchesDate) return false; } this._log(`Message ID ${message.id || 'N/A'} matched all conditions.`); return true; } _startTitleChange() { if (!document.hidden) { this._log('Page is visible, not starting title change'); this._stopTitleChange(); return; } // If we have an active sequence, don't start regular messages if (this.sequenceManager.state.isRunning) { this._log('Sequence is running, not starting standalone title messages'); return; } if (this.currentMessageTimeout) { this._log('Title change already in progress for a standalone message.'); return; } this._log('Attempting to start title change for standalone messages'); const validMessages = this.messages .filter((msg) => this._checkConditions(msg)) .sort((a,b) => (b.priority || 0) - (a.priority || 0)); if (validMessages.length > 0) { const messageToDisplay = { content: validMessages[0].content, duration: validMessages[0].duration || 0, delay: validMessages[0].delay || 0 }; this._displayMessageDirectly(messageToDisplay); } else { this._log('No valid standalone messages to display.'); } } _displayMessageDirectly(messageConfig, sourceInfo = null) { this._log(`DM_ENTRY: document.hidden is ${document.hidden}, isPageVisible is ${this.isPageVisible}. Source:`, sourceInfo, `Current title: "${document.title}"`, `Msg to display: "${messageConfig.content}"`); // Outer check: Should we even attempt to display? if (this.isPageVisible && (!sourceInfo || sourceInfo.source !== 'sequence_visible_override')) { this._log('DM_OUTER_IF_TRUE: Page is visible and not forced. Not displaying message.', { content: messageConfig.content }); this._stopTitleChange(); // Restore original title return; } this._log('DM_OUTER_IF_FALSE: Proceeding (page hidden or display forced). ', { content: messageConfig.content }); if (this.currentMessageTimeout) { clearTimeout(this.currentMessageTimeout); this.currentMessageTimeout = null; } const displayLogic = () => { this._log(`DM_DISPLAYLOGIC_ENTRY: document.hidden is ${document.hidden}, isPageVisible is ${this.isPageVisible}.`, { content: messageConfig.content }); // Inner check: Re-validate if page is still hidden or forced at the moment of actual display if (this.isPageVisible && (!sourceInfo || sourceInfo.source !== 'sequence_visible_override')) { this._log("DM_INNER_IF_TRUE: Page became visible (or was visible) and not forced from within displayLogic. Not displaying.", { content: messageConfig.content }); // Only restore original title if it was a standalone message timer that got interrupted by visibility // For sequences, the main visibility handler (_handleVisibilityChange) handles title restoration. if (sourceInfo && sourceInfo.type === 'standalone_message_timer' && this.veryFirstOriginalTitle !== null) { document.title = this.veryFirstOriginalTitle; this._log("DM_INNER_IF_TRUE: Restored very first original title (standalone message interrupted by visibility)."); } return; } this._log(`DM_INNER_IF_FALSE: Setting title to: "${messageConfig.content}". Current document.title: "${document.title}"`); document.title = messageConfig.content; this.currentMessage = messageConfig.content; this._log(`DM_INNER_IF_FALSE: Title after setting: "${document.title}"`); // Send analytics event for message displayed if (sourceInfo && sourceInfo.type === 'sequence_message') { this.sendAnalyticsEvent('SEQUENCE_MESSAGE_DISPLAYED', { sequenceId: sourceInfo.sequenceId, messageId: sourceInfo.messageId, messageContent: typeof messageConfig.content === 'string' ? messageConfig.content.substring(0, 50) : 'N/A' }); } else if (sourceInfo && sourceInfo.type === 'standalone_message') { // Potentially a different event for standalone messages if needed } if (messageConfig.duration && messageConfig.duration > 0) { this._log(`DM_SETTING_DURATION_TIMEOUT: Message will display for ${messageConfig.duration}ms.`, { content: messageConfig.content }); if (this.messageClearTimer) { clearTimeout(this.messageClearTimer); } this.messageClearTimer = setTimeout(() => { // Only restore if current title is still the one we set AND the page is still hidden (or was hidden when this timer was set) // If page became visible, _handleVisibilityChange would have already restored the title. if (document.title === messageConfig.content && !this.isPageVisible) { this._log(`DM_DURATION_ENDED_HIDDEN: Restoring very first original title after ${messageConfig.duration}ms timeout as page is still hidden.`, { content: messageConfig.content }); document.title = this.veryFirstOriginalTitle; } else if (document.title === messageConfig.content && this.isPageVisible) { this._log(`DM_DURATION_ENDED_VISIBLE: Message duration ended but page is now visible. Title should have been restored by visibility handler.`); } this.messageClearTimer = null; this.currentMessage = null; }, messageConfig.duration); } }; if (messageConfig.delay && messageConfig.delay > 0) { this._log('DM_DELAYING_MESSAGE_DISPLAY:', { content: messageConfig.content, delay: messageConfig.delay }); this.currentMessageTimeout = setTimeout(displayLogic, messageConfig.delay); } else { this._log('DM_CALLING_DISPLAYLOGIC_DIRECTLY:', { content: messageConfig.content }); displayLogic(); } } _handleSequenceItem(item) { this._log('TabTitle._handleSequenceItem received item from SequenceManager:', item); // this._log('TabTitle._handleSequenceItem received item from SequenceManager:', JSON.stringify(item, null, 2)); if (!item || !item.type || !item.content) { this._log('TabTitle._handleSequenceItem: Received invalid or incomplete item structure.', item); return; } const currentSequenceMetadata = this.sequenceManager.getState().sequenceMetadata; if (item.type === 'message' && item.content.message) { const messageConfig = { content: item.content.message.content, duration: item.duration || 0 }; this._log('TabTitle._handleSequenceItem calling _displayMessageDirectly for message:', messageConfig); this._displayMessageDirectly( { ...messageConfig, delay: 0 }, { type: 'sequence_message', sequenceId: currentSequenceMetadata?.groupId, messageId: item.id } ); } else if (item.type === 'favicon' && item.content.favicon) { // This branch is currently not expected to be hit if favicons are not part of SequenceManager sequences this._log('TabTitle._handleSequenceItem received a favicon item. This is unexpected for current design.'); const faviconConfig = { type: item.content.favicon.type, colors: item.content.favicon.type === 'color' ? [item.content.favicon.value] : undefined, emoji: item.content.favicon.type === 'emoji' ? item.content.favicon.value : undefined, customIconUrl: item.content.favicon.type === 'custom' ? item.content.favicon.value : undefined, timing: item.content.favicon.timing || 1000 }; this._applyFaviconConfig({ id: item.id, favicon_config: faviconConfig, delay_time: item.delay / 1000 }, 'sequence_favicon'); } } _handleSequenceComplete(metadata) { console.log('Sequence completed:', metadata); if (this.isPageVisible) { this._stopTitleChange(); this._stopFaviconChange(); } } _removeFavicon() { try { const existingFavicons = document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]'); existingFavicons.forEach(favicon => { if (favicon && favicon.parentNode) { try { favicon.parentNode.removeChild(favicon); } catch (e) { console.warn('Error removing favicon:', e); } } }); } catch (e) { console.warn('Error in _removeFavicon:', e); } } async sendAnalyticsEvent(eventType, details = {}) { if (!this.apiKey) { this._log('Analytics: API Key not set, skipping event.'); return; } const payload = { domainApiKey: this.apiKey, eventType: eventType, path: window.location.pathname + window.location.search, sessionId: this.sessionId, ...details }; this._log('Sending analytics event:', payload); try { const response = await fetch(`${this.apiBaseUrl}/api/v1/analytics/event`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ message: 'Failed to parse error response' })); this._log('Analytics: Error sending event:', response.status, errorData); } else { const successData = await response.json().catch(() => ({ message: 'Failed to parse success response'})); this._log('Analytics: Event sent successfully:', successData); } } catch (error) { this._log('Analytics: Network or other error sending event:', error); } } _startApplicableSequence() { this._log('[TabTitle] Attempting to start applicable sequence.'); if (this.sequenceManager.state.isRunning) { this._log('SequenceManager is already running. Not starting another sequence.'); return; } const validSequences = this.sequences.filter(seq => this._checkSequenceConditions(seq)); this._log(`Found ${validSequences.length} valid sequences to potentially start.`); if (validSequences.length > 0) { // For now, just start the first valid sequence found. // TODO: Implement priority if multiple sequences can be valid. const sequenceToStart = validSequences[0]; this._log('Attempting to start sequence with Group ID:', sequenceToStart.metadata.groupId, 'Item count:', sequenceToStart.items.length); // this._log('Attempting to start sequence with Group ID:', sequenceToStart.metadata.groupId, 'Details:', JSON.stringify(sequenceToStart, null, 2)); // Send analytics event for sequence started this.sendAnalyticsEvent('SEQUENCE_STARTED', { sequenceId: sequenceToStart.metadata.groupId, itemCount: sequenceToStart.items.length, // sequenceType: 'messageSequence' // Or derive from items if needed }); this.sequenceManager.start(sequenceToStart.items, sequenceToStart.metadata); } else { this._log('No valid sequences found to start.'); } } } window.TabTitle = TabTitle;