// 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<string, unknown>} 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<typeof setInterval> | 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<typeof setInterval> | 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<number|null>}
*/
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<void>}
*/
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<void>}
*/
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 = `
<div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
<div style="font-size: 12px; opacity: 0.9;">${message}</div>
<div style="margin-top: 10px; font-size: 11px; opacity: 0.7;">${subtext}</div>
`;
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 = `
<div style="margin-bottom: 8px;"><strong>${reason}</strong></div>
<div style="font-size: 12px; opacity: 0.9; margin-bottom: 12px;">${url}</div>
<button style="
background: white;
color: #dc2626;
border: none;
padding: 6px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
font-size: 12px;
" onclick="this.parentElement.remove()">OK</button>
`;
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 = `<span style="font-size: 14px;">👁</span><span>Elly Active</span>`;
badge.addEventListener('click', () => {
if (isExtensionContextValid()) {
const sent = safeSendMessage({ action: 'openDashboard' });
if (!sent) {
badge.innerHTML = `<span style="font-size: 14px;">⚠️</span><span>Extension Reloaded</span>`;
badge.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
}
} else {
badge.innerHTML = `<span style="font-size: 14px;">⚠️</span><span>Extension Reloaded</span>`;
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
}