// content.js - Elly Website Blocker Content Script // This script runs on all pages to monitor and block access // // NOTE: No ES module `import` here — content scripts must run as classic scripts on some // Chromium/Opera builds even when manifest declares "type": "module", which throws // "Cannot use import statement outside a module" on arbitrary sites. Keep in sync with // lib/searchEngineHelpers.js (same logic as background). /** * Detects common search-engine results pages so they can be allowed while destinations stay blocked. * Duplicated from lib/searchEngineHelpers.js — update both if this changes. * @param {string} url * @returns {boolean} */ function isSearchEngineResultsPage(url) { if (!url || typeof url !== 'string') { return false; } try { const u = new URL(url); if (u.protocol !== 'http:' && u.protocol !== 'https:') { return false; } const host = u.hostname.replace(/^www\./, '').toLowerCase(); const path = u.pathname.toLowerCase(); const hasQuery = Boolean(u.search && u.search.length > 1); if (host === 'google.com' || host.endsWith('.google.com')) { return path.startsWith('/search'); } if (host.endsWith('bing.com')) { return path.startsWith('/search'); } if (host === 'search.yahoo.com' || (host.endsWith('.yahoo.com') && path.includes('/search'))) { return true; } if (host === 'duckduckgo.com') { return path === '/' || path === '/lite' || path.startsWith('/?') || hasQuery; } if (host === 'ecosia.org') { return path.startsWith('/search'); } if (host === 'search.brave.com') { return path.startsWith('/search') || hasQuery; } if (host === 'yandex.com' || host.endsWith('.yandex.com')) { return path.startsWith('/search'); } if (host === 'startpage.com' || host === 'www.startpage.com') { return path.startsWith('/search') || path.startsWith('/sp/search'); } if (host === 'ask.com') { return path.startsWith('/web'); } if (host === 'baidu.com') { return path.startsWith('/s') || u.search.includes('wd='); } if (host === 'vrilsrc.com' || host === 'www.vrilsrc.com') { return path === '/' || path.startsWith('/search') || hasQuery; } return false; } catch { return false; } } // ============================================ // GLOBAL STATE & INITIALIZATION // ============================================ let cachedSettings = { blockedSites: [], limitedSites: {}, schedules: [], globalSettings: {}, whitelist: [] }; let settingsLoaded = false; let heartbeatInterval = null; let isExtensionActive = true; // ============================================ // SAFE CONSOLE UTILITIES (MUST BE FIRST) // ============================================ // Safe console methods that won't throw errors const safeConsole = { log: (...args) => { try { console.log(...args); } catch (e) { // Do nothing - console might not be available } }, error: (...args) => { try { // Filter out extension context errors const firstArg = args[0]; if (firstArg && typeof firstArg === 'string' && (firstArg.includes('Extension context invalidated') || firstArg.includes('Could not establish connection'))) { return; // Suppress } console.error(...args); } catch (e) { // Do nothing } }, warn: (...args) => { try { console.warn(...args); } catch (e) { // Do nothing } } }; // Replace the global console with safe versions (function () { const originalConsole = { ...console }; console.log = safeConsole.log; console.error = safeConsole.error; console.warn = safeConsole.warn; })(); // ============================================ // SAFE EXTENSION UTILITIES // ============================================ function isExtensionContextValid() { if (typeof chrome === 'undefined' || !chrome.runtime) { return false; } try { // Accessing chrome.runtime.id can throw if context is invalidated return !!chrome.runtime.id; } catch (error) { return false; } } function safeExtractDomain(url) { try { return extractDomain(url); } catch (error) { safeConsole.log('Error extracting domain'); return 'unknown'; } } function safeIsUrlMatch(url, pattern) { try { return isUrlMatch(url, pattern); } catch (error) { safeConsole.log('Error matching URL'); return false; } } function safeStorageGet(keys, callback) { if (!isExtensionContextValid()) { safeConsole.log('Extension context invalid, returning empty settings'); isExtensionActive = false; callback({}); return; } try { chrome.storage.sync.get(keys, (result) => { if (chrome.runtime.lastError) { safeConsole.log('Storage error'); isExtensionActive = false; callback({}); return; } callback(result || {}); }); } catch (error) { safeConsole.log('Storage access failed'); isExtensionActive = false; callback({}); } } function safeSendMessage(message, callback = () => { }) { // First check if extension is valid if (!isExtensionContextValid()) { safeConsole.log('Cannot send message - extension context invalid'); isExtensionActive = false; disableExtensionFeatures(); if (callback) callback(null); return false; } try { // Send message with proper error handling chrome.runtime.sendMessage(message, (response) => { // Check for errors in the callback if (chrome.runtime.lastError) { const error = chrome.runtime.lastError.message || ''; if (error.includes('Extension context invalidated') || error.includes('Could not establish connection')) { safeConsole.log('Extension context invalidated in callback'); isExtensionActive = false; clearAllIntervals(); disableExtensionFeatures(); } if (callback) callback(null); return; } if (callback) callback(response); }); return true; } catch (error) { // Catch synchronous errors safeConsole.log('Sync error in safeSendMessage'); if (error.message && ( error.message.includes('Extension context invalidated') || error.message.includes('Could not establish connection') || error.message.includes('Extension') || error.message.includes('context'))) { safeConsole.log('Extension context invalidated (sync catch)'); isExtensionActive = false; clearAllIntervals(); disableExtensionFeatures(); } if (callback) callback(null); return false; } } // ============================================ // SETTINGS MANAGEMENT // ============================================ function loadSettings() { return new Promise((resolve) => { safeStorageGet(['blockedSites', 'limitedSites', 'schedules', 'settings', 'whitelist'], (result) => { cachedSettings = { blockedSites: result.blockedSites || [], limitedSites: result.limitedSites || {}, schedules: result.schedules || [], globalSettings: result.settings || {}, whitelist: result.whitelist || [] }; settingsLoaded = true; safeConsole.log('Elly settings loaded'); resolve(cachedSettings); }); }); } // ============================================ // BLOCK REASON LOGIC (NO CHROME APIS) // ============================================ /** * Resolves limitedSites entry when the key was stored as full hostname (e.g. www.) or bare domain. * @param {string} url * @param {Record} limitedSites * @returns {null | { limitMinutes: number }} */ function getLimitedSiteEntryForUrl(url, limitedSites) { if (!limitedSites || typeof limitedSites !== 'object') { return null; } try { const domain = safeExtractDomain(url); let host = ''; try { host = new URL(url).hostname.toLowerCase(); } catch (e) { host = ''; } const bare = host.replace(/^www\./i, ''); if (limitedSites[domain]) { return limitedSites[domain]; } if (host && limitedSites[host]) { return limitedSites[host]; } if (bare && limitedSites[bare]) { return limitedSites[bare]; } for (const key of Object.keys(limitedSites)) { const k = String(key).toLowerCase(); if (k === host || k === bare || k === domain) { return limitedSites[key]; } } } catch (e) { safeConsole.log('getLimitedSiteEntryForUrl'); } return null; } function updateBlockReasonWithSettings(url, settings) { if (!url || !settings) { return 'Elly says: Access restricted'; } try { const domain = safeExtractDomain(url); // Check whitelist first - using safeIsUrlMatch if (settings.whitelist?.some(pattern => safeIsUrlMatch(url, pattern))) { return ''; // Not blocked - whitelisted } // Check blocked sites - using safeIsUrlMatch if (settings.blockedSites?.some(pattern => safeIsUrlMatch(url, pattern))) { return 'Elly says: Site is in your blocklist'; } // Time limits must run before the search-engine SERP exception (otherwise SERP pages look "unblocked"). const limitEntry = getLimitedSiteEntryForUrl(url, settings.limitedSites); if (limitEntry && typeof limitEntry.limitMinutes === 'number') { return `Elly says: Daily time limit reached (${limitEntry.limitMinutes} minutes)`; } // Check schedules const now = new Date(); const currentDay = now.getDay(); const currentTime = now.getHours() * 60 + now.getMinutes(); for (const schedule of settings.schedules || []) { if (!schedule?.enabled) continue; const scheduleDays = schedule.days || [0, 1, 2, 3, 4, 5, 6]; if (!scheduleDays.includes(currentDay)) continue; const startMinutes = timeToMinutes(schedule.startTime); const endMinutes = timeToMinutes(schedule.endTime); if (currentTime >= startMinutes && currentTime <= endMinutes) { if (schedule.type === 'block') { const allowed = schedule.allowedSites || []; if (!allowed.some(pattern => safeIsUrlMatch(url, pattern))) { return `Elly says: Blocked by schedule (${schedule.name || 'Schedule'})`; } } else if (schedule.type === 'allow') { const blocked = schedule.blockedSites || []; if (blocked.some(pattern => safeIsUrlMatch(url, pattern))) { return `Elly says: Blocked by schedule (${schedule.name || 'Schedule'})`; } } } } if (settings.globalSettings?.allowSearchEngineResults !== false && isSearchEngineResultsPage(url)) { return ''; } return ''; // Not blocked } catch (error) { // Use safe console to avoid throwing more errors safeConsole.log('Error checking access'); return 'Elly says: Error checking access'; } } function isUrlMatch(url, pattern) { if (!url || !pattern) return false; try { const urlStr = String(url || ''); const patternStr = String(pattern || '').trim(); if (!patternStr) { return false; } const lowerUrl = urlStr.toLowerCase(); const lowerPat = patternStr.toLowerCase(); if ( lowerPat.startsWith('chrome://') || lowerPat.startsWith('opera://') || lowerPat.startsWith('edge://') || lowerPat.startsWith('about:') ) { return lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat); } if (patternStr.startsWith('*.')) { const domain = patternStr.slice(2).toLowerCase().replace(/\/.*$/, ''); try { const host = new URL(urlStr).hostname.toLowerCase(); const bareHost = host.replace(/^www\./, ''); const bareDomain = domain.replace(/^www\./, ''); return bareHost === bareDomain || bareHost.endsWith(`.${bareDomain}`); } catch { return false; } } if (patternStr.includes('://') || patternStr.includes('/')) { if (lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat)) { return true; } } try { const host = new URL(urlStr).hostname.toLowerCase(); if (!host) { return false; } let domainPart = lowerPat; if (domainPart.includes('://')) { try { domainPart = new URL(patternStr).hostname.toLowerCase(); } catch { domainPart = lowerPat.replace(/^https?:\/\//, '').split('/')[0]; } } else if (domainPart.includes('/')) { domainPart = domainPart.split('/')[0]; } domainPart = domainPart.replace(/^\*\./, ''); const bareHost = host.replace(/^www\./, ''); const bareDomain = domainPart.replace(/^www\./, ''); return bareHost === bareDomain || bareHost.endsWith(`.${bareDomain}`); } catch { return false; } } catch (error) { safeConsole.log('Error in isUrlMatch'); return false; } } function extractDomain(url) { try { const urlObj = new URL(url); return urlObj.hostname.replace('www.', ''); } catch { return url; } } function timeToMinutes(timeStr) { try { const [hours, minutes] = (timeStr || '00:00').split(':').map(Number); return hours * 60 + minutes; } catch { return 0; } } // ============================================ // HEARTBEAT & MONITORING // ============================================ function startHeartbeat() { if (heartbeatInterval) { clearInterval(heartbeatInterval); } // Initial check if (!isExtensionContextValid()) { safeConsole.log('Extension not valid, stopping heartbeat'); isExtensionActive = false; disableExtensionFeatures(); return; } heartbeatInterval = setInterval(() => { // Check extension state before sending if (!isExtensionActive || !isExtensionContextValid()) { safeConsole.log('Extension inactive, clearing heartbeat'); clearInterval(heartbeatInterval); heartbeatInterval = null; disableExtensionFeatures(); return; } try { // Get current URL safely const currentUrl = window.location.href; const currentDomain = safeExtractDomain(currentUrl); // Use safeSendMessage instead of direct chrome.runtime.sendMessage const sent = safeSendMessage({ action: 'heartbeat', url: currentUrl, timestamp: Date.now(), domain: currentDomain }); // If message failed to send, extension is likely invalidated if (!sent) { safeConsole.log('Heartbeat failed to send'); clearInterval(heartbeatInterval); heartbeatInterval = null; isExtensionActive = false; disableExtensionFeatures(); } } catch (error) { safeConsole.log('Error in heartbeat'); clearInterval(heartbeatInterval); heartbeatInterval = null; isExtensionActive = false; disableExtensionFeatures(); } }, 30000); // 30 seconds } function clearAllIntervals() { if (heartbeatInterval) { clearInterval(heartbeatInterval); heartbeatInterval = null; } } function disableExtensionFeatures() { safeConsole.log('Disabling Elly extension features'); clearAllIntervals(); // Remove any UI elements const badge = document.querySelector('[data-elly-badge]'); if (badge) badge.remove(); // Clear any notifications const notifications = document.querySelectorAll('.elly-notification'); notifications.forEach(notification => notification.remove()); isExtensionActive = false; } // ============================================ // BLOCK PAGE HANDLING // ============================================ /** @type {ReturnType | null} */ let blockPageCountdownIntervalId = null; /** * Display-only countdown for the block page. Does not navigate away (that caused redirect loops with * time limits / SERP handling and blocked PIN entry when the interval kept firing after 0). * @param {number} seconds * @returns {ReturnType | null} */ function startCountdownTimer(seconds = 900) { if (blockPageCountdownIntervalId != null) { clearInterval(blockPageCountdownIntervalId); blockPageCountdownIntervalId = null; } let remaining = seconds; function updateTimer() { const s = Math.max(0, remaining); const minutes = Math.floor(s / 60); const rem = s % 60; const timerElement = document.getElementById('countdownTimer'); if (timerElement) { timerElement.textContent = `${minutes.toString().padStart(2, '0')}:${rem.toString().padStart(2, '0')}`; } if (remaining <= 0) { if (blockPageCountdownIntervalId != null) { clearInterval(blockPageCountdownIntervalId); blockPageCountdownIntervalId = null; } return; } remaining--; } blockPageCountdownIntervalId = setInterval(updateTimer, 1000); updateTimer(); return blockPageCountdownIntervalId; } function getOriginalUrlFromStorage() { return sessionStorage.getItem('ellyOriginalUrl') || null; } // ============================================ // PIN INPUT HANDLING (for block page) // ============================================ function setupPinInput() { const el = document.getElementById('overridePinEntry'); if (!el) { return; } el.addEventListener('input', (e) => { e.target.value = e.target.value.replace(/\D/g, '').slice(0, 12); }); el.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); submitPin(); } }); } /** * Resolves the current tab id for the block page (query param or chrome.tabs.getCurrent). * @returns {Promise} */ async function resolveTabId() { const params = new URLSearchParams(window.location.search); const fromQuery = params.get('tabId'); if (fromQuery) { const n = parseInt(fromQuery, 10); if (Number.isFinite(n)) { return n; } } try { const tab = await chrome.tabs.getCurrent(); return tab?.id ?? null; } catch (error) { safeConsole.log('resolveTabId failed'); return null; } } /** * Scrolls the Parent PIN section into view and focuses the field. * Focus must run synchronously inside the click handler — deferring with * queueMicrotask / setTimeout drops user activation and browsers ignore focus(). * @returns {void} */ function focusPinSection() { const section = document.getElementById('ellyPinSection'); const inp = /** @type {HTMLInputElement|null} */ (document.getElementById('overridePinEntry')); const strict = !!cachedSettings.globalSettings?.strictMode; if (strict || (section && section.hidden) || (inp && inp.closest('[hidden]'))) { void showEllyContentAlertModal( 'Strict Mode is on — temporary PIN overrides are disabled. A parent must remove this site from the Blocklist in Elly Settings.', { title: 'Elly says' } ); return; } if (!section || !inp) { void showEllyContentAlertModal( 'PIN field is not available on this page. Reload the block page and try again.', { title: 'Elly says' } ); return; } section.hidden = false; section.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Synchronous focus while still inside the user-gesture stack. inp.focus({ preventScroll: true }); try { inp.select(); } catch { /* ignore — some engines reject select() on password inputs */ } section.classList.remove('elly-pin-flash'); // Force reflow so the flash animation restarts on repeated clicks. void section.offsetWidth; section.classList.add('elly-pin-flash'); window.setTimeout(() => section.classList.remove('elly-pin-flash'), 1200); } function clearPinField() { const inp = document.getElementById('overridePinEntry'); if (inp) { inp.value = ''; } } /** * Tabler-style modal (replaces window.alert) for block page / content-script UI. * @param {string} message * @param {{ title?: string }} [opts] * @returns {Promise} */ function showEllyContentAlertModal(message, opts = {}) { const title = opts.title || 'Elly'; return new Promise((resolve) => { const existing = document.getElementById('elly-content-alert-overlay'); if (existing) { existing.remove(); } const overlay = document.createElement('div'); overlay.id = 'elly-content-alert-overlay'; overlay.className = 'elly-content-alert-overlay'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-labelledby', 'elly-content-alert-heading'); const card = document.createElement('div'); card.className = 'elly-content-alert-card'; const h3 = document.createElement('h3'); h3.id = 'elly-content-alert-heading'; h3.className = 'elly-content-alert-title'; const icon = document.createElement('i'); icon.className = 'ti ti-info-circle'; h3.appendChild(icon); h3.appendChild(document.createTextNode(` ${title}`)); const p = document.createElement('p'); p.className = 'elly-content-alert-msg'; p.textContent = message; const ok = document.createElement('button'); ok.type = 'button'; ok.className = 'elly-content-alert-ok'; ok.textContent = 'OK'; const finish = () => { ok.removeEventListener('click', onOk); overlay.removeEventListener('click', onBackdrop); document.removeEventListener('keydown', onKey); overlay.remove(); resolve(); }; const onOk = () => finish(); const onBackdrop = (e) => { if (e.target === overlay) { finish(); } }; const onKey = (e) => { if (e.key === 'Escape') { e.preventDefault(); finish(); } }; ok.addEventListener('click', onOk); overlay.addEventListener('click', onBackdrop); document.addEventListener('keydown', onKey); card.appendChild(h3); card.appendChild(p); card.appendChild(ok); overlay.appendChild(card); document.body.appendChild(overlay); ok.focus(); }); } /** * Loads blocked URL from storage, sets sessionStorage for redirect, and fills #reasonText. * @returns {Promise} */ async function hydrateBlockPageUi() { const reasonEl = document.getElementById('reasonText'); if (!reasonEl) { return; } const tabId = await resolveTabId(); if (!tabId || !isExtensionContextValid()) { reasonEl.textContent = 'Elly says: Access restricted'; return; } const storageData = await new Promise((resolve) => { chrome.storage.local.get(['originalUrls'], (result) => { if (chrome.runtime.lastError) { safeConsole.log('hydrateBlockPageUi storage error'); resolve({}); return; } resolve(result || {}); }); }); const originalUrls = storageData.originalUrls || {}; const originalUrl = originalUrls[tabId] ?? originalUrls[String(tabId)]; if (originalUrl) { try { sessionStorage.setItem('ellyOriginalUrl', originalUrl); } catch (error) { safeConsole.log('sessionStorage set failed'); } } const reason = updateBlockReasonWithSettings(originalUrl || '', cachedSettings); const text = reason && String(reason).trim() !== '' ? reason : 'Elly says: Access restricted'; reasonEl.textContent = text; } async function submitPin() { const raw = document.getElementById('overridePinEntry')?.value?.trim() || ''; if (!/^\d{8,12}$/.test(raw)) { await showEllyContentAlertModal('PIN must be 8 to 12 digits', { title: 'Elly says' }); return; } const pin = raw; if (!isExtensionContextValid()) { await showEllyContentAlertModal('Extension reloaded - please refresh the page', { title: 'Elly says' }); return; } const tabId = await resolveTabId(); if (!tabId) { await showEllyContentAlertModal('Could not determine tab — reload the block page', { title: 'Elly says' }); return; } try { // Create a promise wrapper with error handling const response = await new Promise((resolve) => { const sent = safeSendMessage({ action: 'overrideBlock', pin: pin, tabId: tabId }, (response) => { if (response === null) { // Error case handled by safeSendMessage resolve({ success: false, error: 'Extension context invalid' }); } else { resolve(response || { success: false }); } }); // If message couldn't be sent at all if (!sent) { resolve({ success: false, error: 'Cannot communicate with extension' }); } }); if (response?.success) { clearPinField(); let originalUrl = getOriginalUrlFromStorage(); if (!originalUrl) { const data = await new Promise((resolve) => { chrome.storage.local.get(['originalUrls'], (result) => { if (chrome.runtime.lastError) { resolve({}); return; } resolve(result || {}); }); }); const ou = data.originalUrls || {}; originalUrl = ou[tabId] ?? ou[String(tabId)] ?? null; if (originalUrl) { try { sessionStorage.setItem('ellyOriginalUrl', originalUrl); } catch (error) { safeConsole.log('sessionStorage set failed after override'); } } } if (originalUrl) { window.location.href = originalUrl; } else { await showEllyContentAlertModal( 'Override active for 15 minutes. Open the site from history or bookmarks.', { title: 'Elly says' } ); } } else { await showEllyContentAlertModal(response?.error || 'Incorrect PIN', { title: 'Elly says' }); clearPinInputs(); } } catch (error) { safeConsole.log('PIN submission error'); await showEllyContentAlertModal('Error processing PIN - please refresh', { title: 'Elly says' }); } } function clearPinInputs() { const el = document.getElementById('overridePinEntry'); if (el) { el.value = ''; el.focus(); } } // ============================================ // NOTIFICATIONS & UI // ============================================ function showFocusModeNotification() { if (!isExtensionActive) return; showNotification( '🎯 Focus Mode Active', 'Distracting sites are temporarily blocked', 'Stay productive!', 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' ); } function showTimeLimitWarning(domain, remainingMinutes) { if (!isExtensionActive) return; showNotification( '⏰ Time Limit Warning', `You have ${remainingMinutes} minutes remaining on ${domain}`, 'Consider taking a break soon', 'linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%)', 'pulseWarning' ); } function showNotification(title, message, subtext, gradient, animation = 'slideIn') { if (!isExtensionActive) return; const notification = document.createElement('div'); notification.className = 'elly-notification'; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${gradient}; color: white; padding: 15px 20px; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); z-index: 10000; font-family: Arial, sans-serif; max-width: 300px; animation: ${animation} 0.5s ease-out; `; notification.innerHTML = `
${title}
${message}
${subtext}
`; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.5s ease-in'; setTimeout(() => notification.remove(), 500); }, 5000); } function addNotificationStyles() { const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } @keyframes pulseWarning { 0%, 100% { box-shadow: 0 10px 30px rgba(255, 126, 95, 0.5); } 50% { box-shadow: 0 10px 40px rgba(255, 126, 95, 0.8); } } `; if (document.head) { document.head.appendChild(style); } else if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { if (document.head) document.head.appendChild(style); }); } } // ============================================ // PAGE MONITORING // ============================================ function monitorPageChanges() { let lastUrl = window.location.href; const urlCheckInterval = setInterval(() => { if (!isExtensionActive) { clearInterval(urlCheckInterval); return; } if (window.location.href !== lastUrl) { lastUrl = window.location.href; checkCurrentPage(); } }, 1000); // Monitor link clicks document.addEventListener('click', (e) => { if (!isExtensionActive) return; if (e.target.tagName === 'A' && e.target.href) { const reason = updateBlockReasonWithSettings(e.target.href, cachedSettings); if (reason) { e.preventDefault(); showWarning(reason, e.target.href); } } }, true); } function checkCurrentPage() { if (!settingsLoaded || !isExtensionActive) return; const reason = updateBlockReasonWithSettings(window.location.href, cachedSettings); if (reason) { safeConsole.log('Current page should be blocked:', reason); // Could redirect to block page here } } function showWarning(reason, url) { if (!isExtensionActive) return; const warning = document.createElement('div'); warning.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #ef4444, #dc2626); color: white; padding: 16px 24px; border-radius: 12px; font-weight: 600; font-size: 14px; z-index: 100000; box-shadow: 0 10px 40px rgba(239, 68, 68, 0.4); max-width: 400px; text-align: center; animation: slideIn 0.3s ease; `; warning.innerHTML = `
${reason}
${url}
`; document.body.appendChild(warning); setTimeout(() => { if (warning.parentElement) { warning.style.opacity = '0'; warning.style.transition = 'opacity 0.3s'; setTimeout(() => warning.remove(), 300); } }, 5000); } // ============================================ // MAIN INITIALIZATION // ============================================ function initializeContentScript() { safeConsole.log('Initializing Elly content script...'); // Initialize extension state isExtensionActive = isExtensionContextValid(); if (!isExtensionActive) { safeConsole.log('Elly: Extension not available or reloaded'); return; } // Check if we're on the block page or regular page if (window.location.href.includes('block.html')) { initializeBlockPage(); } else { initializeRegularPage(); } } /** * Wires block page controls. Inline onclick is blocked by extension-page CSP (script-src 'self'). * Call before awaiting storage so Focus PIN / Go Back work even if settings load is slow. * @returns {void} */ function wireBlockPageControls() { const goBack = () => { if (window.history.length > 1) { window.history.back(); } else { window.close(); } }; const btnOverride = document.getElementById('ellyBtnOverride'); const btnGoBack = document.getElementById('ellyBtnGoBack'); const btnSubmit = document.getElementById('ellyPinSubmit'); const btnCancel = document.getElementById('ellyPinCancel'); if (btnOverride && !btnOverride.dataset.ellyWired) { btnOverride.dataset.ellyWired = '1'; btnOverride.addEventListener('click', (e) => { e.preventDefault(); focusPinSection(); }); } if (btnGoBack && !btnGoBack.dataset.ellyWired) { btnGoBack.dataset.ellyWired = '1'; btnGoBack.addEventListener('click', (e) => { e.preventDefault(); goBack(); }); } if (btnSubmit && !btnSubmit.dataset.ellyWired) { btnSubmit.dataset.ellyWired = '1'; btnSubmit.addEventListener('click', (e) => { e.preventDefault(); void submitPin(); }); } if (btnCancel && !btnCancel.dataset.ellyWired) { btnCancel.dataset.ellyWired = '1'; btnCancel.addEventListener('click', (e) => { e.preventDefault(); clearPinField(); }); } } function initializeBlockPage() { // Wire immediately — do not wait on storage (Focus PIN must respond on first click). wireBlockPageControls(); setupPinInput(); addNotificationStyles(); loadSettings().then(async () => { await hydrateBlockPageUi(); applyStrictModeBlockPageUi(); startCountdownTimer(); }); } /** * Strict Mode disables temporary 15-minute overrides on the block page. */ function applyStrictModeBlockPageUi() { const strict = !!cachedSettings.globalSettings?.strictMode; const pinSection = document.getElementById('ellyPinSection'); const btnOverride = document.getElementById('ellyBtnOverride'); if (pinSection) { pinSection.hidden = strict; } if (btnOverride) { btnOverride.hidden = strict; } if (strict) { let note = document.getElementById('ellyStrictModeNote'); if (!note) { note = document.createElement('p'); note.id = 'ellyStrictModeNote'; note.className = 'pin-hint'; note.style.marginTop = '12px'; note.textContent = 'Strict Mode is on — temporary overrides are disabled. A parent must remove this site from the Blocklist (PIN required).'; const controls = document.querySelector('.controls'); if (controls && controls.parentNode) { controls.parentNode.insertBefore(note, controls); } else { document.querySelector('.container')?.appendChild(note); } } note.hidden = false; } else { const note = document.getElementById('ellyStrictModeNote'); if (note) { note.hidden = true; } } } function initializeRegularPage() { // Load settings and start monitoring loadSettings().then(() => { safeConsole.log('Elly content script initialized'); // Start monitoring startHeartbeat(); monitorPageChanges(); checkCurrentPage(); // Add notification styles addNotificationStyles(); // Add Elly badge if enabled if (cachedSettings.globalSettings?.showBadge !== false) { addEllyBadge(); } }).catch(error => { safeConsole.log('Failed to load settings'); }); } function addEllyBadge() { if (!isExtensionActive) return; // Remove existing badge const existingBadge = document.querySelector('[data-elly-badge]'); if (existingBadge) existingBadge.remove(); const badge = document.createElement('div'); badge.setAttribute('data-elly-badge', 'true'); badge.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: linear-gradient(135deg, #6366f1, #8b5cf6); color: white; padding: 8px 16px; border-radius: 20px; font-size: 12px; font-weight: 600; z-index: 99999; box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3); display: flex; align-items: center; gap: 6px; cursor: pointer; opacity: 0.9; transition: opacity 0.3s; `; badge.innerHTML = `👁Elly Active`; badge.addEventListener('click', () => { if (isExtensionContextValid()) { const sent = safeSendMessage({ action: 'openDashboard' }); if (!sent) { badge.innerHTML = `⚠️Extension Reloaded`; badge.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)'; } } else { badge.innerHTML = `⚠️Extension Reloaded`; badge.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)'; } }); badge.addEventListener('mouseenter', () => badge.style.opacity = '1'); badge.addEventListener('mouseleave', () => badge.style.opacity = '0.9'); document.body.appendChild(badge); } // ============================================ // ERROR HANDLING & STARTUP // ============================================ // Check extension context immediately if (!isExtensionContextValid()) { safeConsole.log('Elly: Extension context invalidated - content script will not run'); safeConsole.log('Note: This is normal when reloading the extension in developer mode'); } else { // Start initialization if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeContentScript); } else { initializeContentScript(); } } // Cleanup on page unload window.addEventListener('beforeunload', () => { clearAllIntervals(); }); // Log startup try { console.log('%c🔒 Elly Content Script Loaded', 'color: #6366f1; font-weight: bold; font-size: 14px;'); } catch (e) { // Ignore console errors }