freshroute-server
Version:
Local development server for FreshRoute extension with API mocking and extended functionality
1,258 lines (1,257 loc) β’ 72.8 kB
JavaScript
"use strict";
(function() {
let notificationsEnabled = true;
let extensionEnabled = true;
let compactNotifications = true;
let activeNotifications = [];
let notificationCounter = 0;
let isNotificationAreaHovered = false;
let currentNotificationIndex = 0;
let floatingToggle = null;
let isDragging = false;
let dragOffset = { x: 0, y: 0 };
let autoOffTimer = null;
let timerInterval = null;
let lastActivityTimestamp = null;
const INACTIVITY_TIMEOUT = 30 * 60 * 1e3;
let quickMenu = null;
let isQuickMenuOpen = false;
let ruleGroups = [];
function waitForBody(callback) {
if (document.body) {
callback();
return;
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", callback);
} else {
const observer = new MutationObserver((mutations, obs) => {
if (document.body) {
obs.disconnect();
callback();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
if (document.body) {
callback();
} else {
console.error("π¨ Failed to find document.body after timeout");
}
}, 5e3);
}
}
function exposeAllowedDomainsGlobally() {
try {
if (typeof getAllowedDomains === "function") {
window.FRESHROUTE_ALLOWED_DOMAINS = getAllowedDomains();
} else if (typeof ALLOWED_DOMAINS !== "undefined" && Array.isArray(ALLOWED_DOMAINS)) {
window.FRESHROUTE_ALLOWED_DOMAINS = [...ALLOWED_DOMAINS];
} else if (typeof window !== "undefined" && window.DomainValidator && typeof window.DomainValidator.getAllowedDomains === "function") {
window.FRESHROUTE_ALLOWED_DOMAINS = window.DomainValidator.getAllowedDomains();
} else {
window.FRESHROUTE_ALLOWED_DOMAINS = ["localhost", "127.0.0.1"];
console.warn("β οΈ Could not access allowed domains from allowed-domains.js, exposing minimal fallback");
}
console.log("π Exposed allowed domains globally:", window.FRESHROUTE_ALLOWED_DOMAINS);
} catch (error) {
console.error("β Error exposing allowed domains globally:", error);
window.FRESHROUTE_ALLOWED_DOMAINS = ["localhost", "127.0.0.1"];
}
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
loadSettings();
exposeAllowedDomainsGlobally();
});
} else {
loadSettings();
exposeAllowedDomainsGlobally();
}
async function loadSettings() {
try {
const settings = await safeStorageGet([
"notificationsEnabled",
"extensionEnabled",
"compactNotifications",
"floatingTogglePosition",
"floatingToggleVisible",
"lastActivityTimestamp"
]);
const { groups } = await safeLocalStorageGet(["groups"]);
ruleGroups = groups || [];
notificationsEnabled = settings.notificationsEnabled !== false;
extensionEnabled = settings.extensionEnabled !== false;
compactNotifications = settings.compactNotifications !== false;
lastActivityTimestamp = settings.lastActivityTimestamp;
await checkInactivityStatus();
createFloatingToggle(settings.floatingTogglePosition, settings.floatingToggleVisible);
} catch (error) {
console.log("Content script: Error loading settings");
createFloatingToggle();
}
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "updateNotificationSettings") {
notificationsEnabled = message.enabled;
} else if (message.action === "updateCompactNotificationSettings") {
compactNotifications = message.enabled;
if (activeNotifications.length > 0) {
if (compactNotifications) {
showCompactNotifications(false);
} else {
showAllNotifications();
}
}
} else if (message.action === "updateExtensionSettings") {
extensionEnabled = message.enabled;
updateFloatingToggleState();
if (!extensionEnabled) {
clearAllNotifications();
clearInactivityTimer();
} else {
startInactivityTimer();
}
} else if (message.action === "ruleApplied") {
updateLastActivity();
if (notificationsEnabled && extensionEnabled) {
showNotification(message);
}
}
});
window.addEventListener("message", (event) => {
var _a, _b;
if (event.source !== window) return;
if (event.data.type === "FRESHROUTE_STATUS_REQUEST") {
const manifest = safeGetManifest();
const responseData = {
type: "FRESHROUTE_STATUS_REQUEST_RESPONSE",
data: {
version: manifest.version,
enabled: extensionEnabled,
notificationsEnabled,
compactNotifications,
activeNotifications: activeNotifications.length,
extensionId: ((_a = chrome.runtime) == null ? void 0 : _a.id) || "unknown",
timestamp: Date.now(),
lastActivityTimestamp,
inactivityTimeoutMinutes: Math.round(INACTIVITY_TIMEOUT / 1e3 / 60)
}
};
window.postMessage(responseData, "*");
console.log("π± Extension status requested by setup page", responseData);
} else if (event.data.type === "FRESHROUTE_CONNECTION_TEST") {
const manifest = safeGetManifest();
window.postMessage({
type: "FRESHROUTE_CONNECTION_TEST_RESPONSE",
data: {
requestTime: event.data.timestamp,
responseTime: Date.now(),
activeRules: activeNotifications.length,
// Use notifications as proxy for activity
extensionId: ((_b = chrome.runtime) == null ? void 0 : _b.id) || "unknown",
version: manifest.version,
enabled: extensionEnabled
}
}, "*");
console.log("π± Connection test requested by setup page");
} else if (event.data.type === "FRESHROUTE_PING") {
const manifest = safeGetManifest();
window.postMessage({
type: "FRESHROUTE_PONG",
data: {
timestamp: Date.now(),
version: manifest.version
}
}, "*");
console.log("π± Legacy ping received from setup page");
} else if (event.data.type === "FRESHROUTE_GET_ALLOWED_DOMAINS") {
let allowedDomains = [];
try {
if (typeof getAllowedDomains === "function") {
allowedDomains = getAllowedDomains();
} else if (typeof ALLOWED_DOMAINS !== "undefined" && Array.isArray(ALLOWED_DOMAINS)) {
allowedDomains = [...ALLOWED_DOMAINS];
} else if (typeof window !== "undefined" && window.DomainValidator && typeof window.DomainValidator.getAllowedDomains === "function") {
allowedDomains = window.DomainValidator.getAllowedDomains();
} else {
allowedDomains = ["localhost", "127.0.0.1"];
console.warn("β οΈ Could not access allowed domains from allowed-domains.js, using minimal fallback");
}
} catch (error) {
console.error("β Error accessing allowed domains:", error);
allowedDomains = ["localhost", "127.0.0.1"];
}
window.postMessage({
type: "FRESHROUTE_GET_ALLOWED_DOMAINS_RESPONSE",
data: {
allowedDomains,
source: "extension",
timestamp: Date.now()
}
}, "*");
console.log("π± Allowed domains requested by setup page:", allowedDomains);
}
});
function showNotification(data) {
var _a, _b, _c, _d, _e;
const notificationId = `freshroute-notification-${++notificationCounter}`;
let bgColor, icon, title, details;
switch (data.ruleType) {
case "delay_request":
bgColor = "#FF9800";
icon = "β±οΈ";
title = "Request Delayed";
details = {
summary: `${data.ruleName} β’ ${data.delayMs}ms`,
expanded: `
<div class="notification-source"><strong>π Full URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div>
<div class="notification-rule"><strong>π Rule:</strong> ${data.ruleName}</div>
<div class="notification-detail"><strong>β±οΈ Delay Details:</strong><br>
β’ Delayed by ${data.delayMs}ms (${(data.delayMs / 1e3).toFixed(1)} seconds)<br>
β’ Request will proceed after delay<br>
β’ Type: Artificial network slowdown simulation
</div>
<div class="notification-timestamp"><strong>π Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div>
`
};
break;
case "modify_headers":
case "header_modification":
bgColor = "#2196F3";
icon = "π";
title = "Headers Modified";
let headerDetails = "";
if (data.headers && Array.isArray(data.headers)) {
headerDetails = data.headers.map((h) => `β’ ${h.operation.toUpperCase()}: ${h.name}${h.value ? ` = "${h.value}"` : ""}`).join("<br>");
} else {
headerDetails = "β’ Headers modified for this request<br>β’ Check browser dev tools Network tab for details";
}
details = {
summary: `${data.ruleName} β’ Headers updated`,
expanded: `
<div class="notification-source"><strong>π Full URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div>
<div class="notification-rule"><strong>π Rule:</strong> ${data.ruleName}</div>
<div class="notification-detail"><strong>π Header Modifications:</strong><br>
${headerDetails}
</div>
<div class="notification-technical"><strong>π§ Technical Info:</strong><br>
β’ Target: ${data.target || "Request"} headers<br>
β’ Method: declarativeNetRequest API<br>
β’ Scope: This request only
</div>
<div class="notification-timestamp"><strong>π Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div>
`
};
break;
case "mock_response":
bgColor = "#9C27B0";
icon = "π";
title = "Mock Response";
details = {
summary: `${data.ruleName} β’ ${((_a = data.responseType) == null ? void 0 : _a.toUpperCase()) || "JSON"} (${data.statusCode || 200})`,
expanded: `
<div class="notification-source"><strong>π Intercepted URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div>
<div class="notification-rule"><strong>π Rule:</strong> ${data.ruleName}</div>
<div class="notification-detail"><strong>π Mock Response Details:</strong><br>
β’ Method: ${data.method || "ANY"}<br>
β’ Response Type: ${(data.responseType || "json").toUpperCase()}<br>
β’ Status Code: ${data.statusCode || 200}<br>
β’ Mock data returned instead of real API call
</div>
<div class="notification-technical"><strong>π§ Technical Info:</strong><br>
β’ Type: API Response Mocking<br>
β’ Method: Fetch/XHR interception<br>
β’ Scope: This request only
</div>
<div class="notification-timestamp"><strong>π Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div>
`
};
break;
case "url_rewrite":
default:
bgColor = "#4CAF50";
icon = "π";
title = "URL Rewritten";
const hasRedirect = data.redirectUrl && data.redirectUrl !== data.url;
let rewriteDetails = "";
if (hasRedirect) {
const sourceUrl = data.url;
const targetUrl = data.redirectUrl;
let transformationInfo = "";
if (data.captureGroups) {
transformationInfo = `<br><strong>π― Capture Groups:</strong><br>${data.captureGroups.map((group, i) => `β’ $${i + 1}: "${group}"`).join("<br>")}`;
} else {
const sourceDomain = ((_b = sourceUrl.match(/\/\/([^\/]+)/)) == null ? void 0 : _b[1]) || "unknown";
const targetDomain = ((_c = targetUrl.match(/\/\/([^\/]+)/)) == null ? void 0 : _c[1]) || "unknown";
if (sourceDomain !== targetDomain) {
transformationInfo = `<br><strong>π Domain Change:</strong><br>β’ From: ${sourceDomain}<br>β’ To: ${targetDomain}`;
}
const sourcePort = (_d = sourceUrl.match(/:(\d+)\//)) == null ? void 0 : _d[1];
const targetPort = (_e = targetUrl.match(/:(\d+)\//)) == null ? void 0 : _e[1];
if (sourcePort && targetPort && sourcePort !== targetPort) {
transformationInfo += `<br><strong>π Port Change:</strong> ${sourcePort} β ${targetPort}`;
}
}
rewriteDetails = `
<div class="notification-source"><strong>π Original URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${sourceUrl}</code></div>
<div class="notification-target"><strong>π― Redirected To:</strong><br><code style="font-size: 9px; word-break: break-all;">${targetUrl}</code></div>
<div class="notification-rule"><strong>π Rule:</strong> ${data.ruleName}</div>
<div class="notification-detail"><strong>π Transformation Details:</strong>${transformationInfo}</div>
<div class="notification-technical"><strong>π§ Technical Info:</strong><br>
β’ Type: URL Redirect (${data.method || "declarativeNetRequest"})<br>
β’ Status: ${data.statusCode || "Active"}<br>
β’ Scope: Navigation request
</div>
`;
} else {
rewriteDetails = `
<div class="notification-source"><strong>π Matched URL:</strong><br><code style="font-size: 9px; word-break: break-all;">${data.url}</code></div>
<div class="notification-rule"><strong>π Rule:</strong> ${data.ruleName}</div>
<div class="notification-detail"><strong>β
Pattern Matched:</strong><br>
β’ Rule pattern successfully matched this URL<br>
β’ No redirect configured (pattern-only rule)<br>
β’ Rule is active and monitoring
</div>
`;
}
details = {
summary: `${data.ruleName} β’ ${hasRedirect ? "Redirected" : "Matched"}`,
expanded: `
${rewriteDetails}
<div class="notification-timestamp"><strong>π Applied:</strong> ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}</div>
`
};
break;
}
const notificationData = {
id: notificationId,
timestamp: Date.now(),
bgColor,
icon,
title,
details,
autoRemoveTimer: null,
remainingTime: 5e3,
isPaused: false
};
activeNotifications.push(notificationData);
if (compactNotifications) {
let preserveExpandedState = false;
if (activeNotifications.length > 1) {
const currentNotification = activeNotifications[currentNotificationIndex];
preserveExpandedState = currentNotification && currentNotification.expanded;
}
currentNotificationIndex = activeNotifications.length - 1;
if (preserveExpandedState) {
activeNotifications[currentNotificationIndex].expanded = true;
}
showCompactNotifications(true);
} else {
showStandardNotification(notificationData);
}
console.log(`π± Notification shown: ${title} (${activeNotifications.length} active, compact: ${compactNotifications}${compactNotifications ? ", no auto-removal" : ", auto-remove: 5s"})`);
}
function calculateNotificationPosition() {
let totalHeight = 20;
activeNotifications.forEach((notification) => {
const notificationDiv = notification.element.querySelector(".freshroute-notification");
if (notificationDiv) {
const isExpanded = notificationDiv.getAttribute("data-expanded") === "true";
totalHeight += isExpanded ? 75 : 42;
}
});
return totalHeight;
}
function toggleNotification(notificationId) {
const notificationData = activeNotifications.find((n) => n.id === notificationId);
if (!notificationData) return;
const notificationDiv = notificationData.element.querySelector(".freshroute-notification");
const expandedContent = notificationData.element.querySelector(".expanded-content");
const expandButton = notificationData.element.querySelector(".notification-expand");
if (!notificationDiv || !expandedContent || !expandButton) return;
const isExpanded = notificationDiv.getAttribute("data-expanded") === "true";
if (isExpanded) {
notificationDiv.setAttribute("data-expanded", "false");
notificationDiv.classList.add("collapsed");
notificationDiv.style.height = "32px";
expandedContent.style.opacity = "0";
expandedContent.style.maxHeight = "0";
expandedContent.style.paddingTop = "0";
expandButton.textContent = "βΌ";
console.log(`π± Notification collapsed: ${notificationId}`);
} else {
notificationDiv.setAttribute("data-expanded", "true");
notificationDiv.classList.remove("collapsed");
notificationDiv.style.height = "65px";
expandedContent.style.opacity = "1";
expandedContent.style.maxHeight = "45px";
expandedContent.style.paddingTop = "6px";
expandButton.textContent = "β²";
console.log(`π± Notification expanded: ${notificationId}`);
}
setTimeout(() => {
repositionNotifications();
}, 100);
}
function removeNotification(notificationId) {
const index = activeNotifications.findIndex((n) => n.id === notificationId);
if (index !== -1) {
const notification = activeNotifications[index];
console.log(`π± Removing notification: ${notificationId} (${activeNotifications.length - 1} will remain)`);
if (notification.autoRemoveTimer) {
clearTimeout(notification.autoRemoveTimer);
notification.autoRemoveTimer = null;
}
if (compactNotifications) {
if (currentNotificationIndex === index) {
if (activeNotifications.length > 1) {
if (currentNotificationIndex >= activeNotifications.length - 1) {
currentNotificationIndex = Math.max(0, activeNotifications.length - 2);
}
}
} else if (currentNotificationIndex > index) {
currentNotificationIndex--;
}
activeNotifications.splice(index, 1);
if (activeNotifications.length > 0) {
showCompactNotifications(false);
} else {
clearCompactNotification();
currentNotificationIndex = 0;
}
} else {
if (notification.element) {
const notificationDiv = notification.element.querySelector(".freshroute-notification");
if (notificationDiv) {
notificationDiv.style.transform = "translateX(100%)";
notificationDiv.style.opacity = "0";
}
}
activeNotifications.splice(index, 1);
repositionNotifications();
setTimeout(() => {
if (notification.element && notification.element.parentNode) {
notification.element.remove();
console.log(`π± Notification DOM removed: ${notificationId}`);
}
}, 300);
}
} else {
console.log(`π± Notification not found: ${notificationId}`);
}
}
function repositionNotifications() {
let currentTop = 20;
activeNotifications.forEach((notification, index) => {
const notificationDiv = notification.element.querySelector(".freshroute-notification");
if (notificationDiv) {
notificationDiv.style.top = `${currentTop}px`;
const isExpanded = notificationDiv.getAttribute("data-expanded") === "true";
currentTop += isExpanded ? 75 : 42;
}
});
console.log(`π± Repositioned ${activeNotifications.length} notifications with variable heights`);
}
function clearAllNotifications() {
activeNotifications.forEach((notification) => {
if (notification.autoRemoveTimer) {
clearTimeout(notification.autoRemoveTimer);
notification.autoRemoveTimer = null;
}
if (notification.element && notification.element.parentNode) {
notification.element.remove();
}
});
clearCompactNotification();
activeNotifications = [];
currentNotificationIndex = 0;
console.log("π± All notifications cleared (timers stopped)");
}
window.FreshRouteNotifications = {
clear: clearAllNotifications,
count: () => activeNotifications.length,
list: () => activeNotifications.map((n) => ({ id: n.id, timestamp: n.timestamp })),
mode: () => compactNotifications ? "compact" : "standard",
currentIndex: () => compactNotifications ? currentNotificationIndex : -1,
clearAll: () => {
console.log(`ποΈ Manually clearing all ${activeNotifications.length} notifications`);
clearAllNotifications();
},
test: (type = "url_rewrite") => {
const testData = {
"url_rewrite": {
action: "ruleApplied",
ruleName: "Development Environment Redirect",
ruleType: "url_rewrite",
url: "https://production-api.example.com/v1/users/12345/profile",
redirectUrl: "https://localhost:3000/api/v1/users/12345/profile"
},
"modify_headers": {
action: "ruleApplied",
ruleName: "API Authentication Headers",
ruleType: "modify_headers",
url: "https://api.freshservice.com/api/v2/tickets?per_page=100&filter=status:open"
},
"delay_request": {
action: "ruleApplied",
ruleName: "Slow Network Simulation",
ruleType: "delay_request",
url: "https://api.github.com/repos/octocat/Hello-World/issues",
delayMs: 2500
}
};
showNotification(testData[type] || testData.url_rewrite);
},
testMultiple: (count = 5) => {
const types = ["url_rewrite", "modify_headers", "delay_request"];
for (let i = 0; i < count; i++) {
setTimeout(() => {
const type = types[i % types.length];
const testData = {
ruleName: `Test Rule ${i + 1}`,
ruleType: type,
url: `https://example${i + 1}.com/api/test`,
redirectUrl: type === "url_rewrite" ? `https://localhost:300${i}/api/test` : void 0,
delayMs: type === "delay_request" ? 1e3 + i * 500 : void 0
};
showNotification(testData);
}, i * 300);
}
},
toggleMode: () => {
compactNotifications = !compactNotifications;
console.log(`π± Switched to ${compactNotifications ? "compact" : "standard"} mode`);
if (activeNotifications.length > 0) {
if (compactNotifications) {
showCompactNotifications(false);
} else {
showAllNotifications();
}
}
}
};
window.addEventListener("beforeunload", () => {
clearAllNotifications();
clearInactivityTimer();
saveFloatingTogglePosition();
});
async function safeStorageSet(data) {
try {
if (chrome && chrome.storage && chrome.storage.sync) {
await chrome.storage.sync.set(data);
return true;
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - storage operation skipped");
return false;
}
console.error("Storage error:", error);
return false;
}
return false;
}
function safeSendMessage(message) {
try {
if (chrome && chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage(message);
return true;
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - message sending skipped");
return false;
}
console.error("Message sending error:", error);
return false;
}
return false;
}
async function safeLocalStorageSet(data) {
try {
if (chrome && chrome.storage && chrome.storage.local) {
await chrome.storage.local.set(data);
return true;
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - local storage operation skipped");
return false;
}
console.error("Local storage error:", error);
return false;
}
return false;
}
async function safeStorageGet(keys) {
try {
if (chrome && chrome.storage && chrome.storage.sync) {
return await chrome.storage.sync.get(keys);
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - storage get operation skipped");
return {};
}
console.error("Storage get error:", error);
return {};
}
return {};
}
async function safeLocalStorageGet(keys) {
try {
if (chrome && chrome.storage && chrome.storage.local) {
return await chrome.storage.local.get(keys);
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - local storage get operation skipped");
return {};
}
console.error("Local storage get error:", error);
return {};
}
return {};
}
function safeGetManifest() {
try {
if (chrome && chrome.runtime && chrome.runtime.getManifest) {
return chrome.runtime.getManifest();
}
} catch (error) {
if (error.message.includes("Extension context invalidated")) {
console.log("π Extension context invalidated - manifest access skipped");
return { version: "unknown" };
}
console.error("Manifest access error:", error);
return { version: "unknown" };
}
return { version: "unknown" };
}
function updateLastActivity() {
const now = Date.now();
lastActivityTimestamp = now;
safeStorageSet({ lastActivityTimestamp: now });
startInactivityTimer();
console.log("π― Rule activity recorded - inactivity timer reset");
}
async function checkInactivityStatus() {
if (!extensionEnabled || !lastActivityTimestamp) {
if (extensionEnabled) {
startInactivityTimer();
}
return;
}
const now = Date.now();
const timeSinceLastActivity = now - lastActivityTimestamp;
if (timeSinceLastActivity >= INACTIVITY_TIMEOUT) {
await turnOffExtension();
console.log("π Extension auto-disabled after 30 minutes of inactivity");
} else {
const remainingTime = INACTIVITY_TIMEOUT - timeSinceLastActivity;
startInactivityTimer(remainingTime);
}
}
function startInactivityTimer(customDuration = null) {
if (!extensionEnabled) return;
clearInactivityTimer();
const duration = customDuration || INACTIVITY_TIMEOUT;
autoOffTimer = setTimeout(async () => {
await turnOffExtension();
console.log("π Extension auto-disabled after 30 minutes of inactivity");
}, duration);
startTimerDisplay();
console.log(`β° Inactivity timer started: ${Math.round(duration / 1e3 / 60)} minutes until auto-off`);
}
function startTimerDisplay() {
clearInterval(timerInterval);
timerInterval = setInterval(() => {
if (!extensionEnabled || !lastActivityTimestamp) {
clearInterval(timerInterval);
return;
}
updateFloatingToggleTimer();
}, 1e3);
}
function clearInactivityTimer() {
if (autoOffTimer) {
clearTimeout(autoOffTimer);
autoOffTimer = null;
}
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
console.log("β° Inactivity timer cleared");
}
async function turnOffExtension() {
extensionEnabled = false;
await safeStorageSet({ extensionEnabled: false });
clearInactivityTimer();
updateFloatingToggleState();
clearAllNotifications();
safeSendMessage({
action: "updateExtensionEnabled",
enabled: false
});
console.log("π΄ Extension turned off automatically due to inactivity");
}
function formatTimeRemaining() {
if (!extensionEnabled || !lastActivityTimestamp) return "";
const now = Date.now();
const timeSinceLastActivity = now - lastActivityTimestamp;
const timeRemaining = INACTIVITY_TIMEOUT - timeSinceLastActivity;
if (timeRemaining <= 0) return "0m";
const minutes = Math.floor(timeRemaining / (1e3 * 60));
const seconds = Math.floor(timeRemaining % (1e3 * 60) / 1e3);
if (minutes > 0) {
return `${minutes}m`;
} else {
return `${seconds}s`;
}
}
function createFloatingToggle(savedPosition = null, isVisible = true) {
if (floatingToggle) {
floatingToggle.remove();
}
if (window.location.href.includes("chrome://") || window.location.href.includes("chrome-extension://") || window.location.href.includes("moz-extension://")) {
return;
}
floatingToggle = document.createElement("div");
floatingToggle.id = "freshroute-floating-toggle";
floatingToggle.className = "freshroute-floating-toggle";
floatingToggle.setAttribute("data-freshroute-extension", "active");
const timeRemaining = formatTimeRemaining();
const toggleWidth = extensionEnabled && timeRemaining ? 130 : 80;
const defaultPosition = {
x: window.innerWidth - toggleWidth - 20,
// 20px padding from right edge
y: 20
// 20px padding from top edge
};
const position = savedPosition || defaultPosition;
const timerDisplay = extensionEnabled && timeRemaining ? ` β’ ${timeRemaining}` : "";
floatingToggle.innerHTML = `
<div class="floating-toggle-content" style="
position: fixed;
top: ${position.y}px;
left: ${position.x}px;
width: ${extensionEnabled && timeRemaining ? "130px" : "80px"};
height: 32px;
background: ${extensionEnabled ? "#4CAF50" : "#f44336"};
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
z-index: 999999;
transition: all 0.3s ease;
user-select: none;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
opacity: ${isVisible !== false ? "0.8" : "0.3"};
padding: 0 8px;
">
<div class="toggle-icon" style="
color: white;
font-size: 12px;
font-weight: bold;
pointer-events: none;
margin-left: 4px;
">${extensionEnabled ? "π" : "β"}</div>
<div class="toggle-text" style="
color: white;
font-size: 8px;
font-weight: 600;
margin-left: 3px;
pointer-events: none;
white-space: nowrap;
">${extensionEnabled ? "ON" : "OFF"}${timerDisplay}</div>
</div>
`;
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
floatingToggle.addEventListener("mouseenter", () => {
if (!isDragging) {
toggleContent.style.opacity = "1";
toggleContent.style.transform = "scale(1.05)";
toggleContent.title = "Left-click: Toggle ON/OFF\nRight-click: Quick Menu\nDouble-click: Hide/Show\nDrag: Move position\n\nAuto-off: After 30min of inactivity";
}
});
floatingToggle.addEventListener("mouseleave", () => {
if (!isDragging) {
toggleContent.style.opacity = isVisible !== false ? "0.8" : "0.3";
toggleContent.style.transform = "scale(1)";
}
});
addDragFunctionality(floatingToggle);
floatingToggle.addEventListener("click", (e) => {
if (!isDragging) {
toggleExtension();
}
});
floatingToggle.addEventListener("contextmenu", (e) => {
e.preventDefault();
if (!isDragging) {
toggleQuickMenu();
}
});
floatingToggle.addEventListener("dblclick", (e) => {
e.preventDefault();
toggleFloatingToggleVisibility();
});
if (document.body) {
document.body.appendChild(floatingToggle);
if (extensionEnabled) {
startTimerDisplay();
}
console.log("ποΈ Floating toggle created");
} else {
console.log("β οΈ DOM not ready, waiting for body...");
waitForBody(() => {
if (document.body && floatingToggle) {
document.body.appendChild(floatingToggle);
if (extensionEnabled) {
startTimerDisplay();
}
console.log("ποΈ Floating toggle created (after DOM ready)");
}
});
}
}
function addDragFunctionality(element) {
const toggleContent = element.querySelector(".floating-toggle-content");
element.addEventListener("mousedown", (e) => {
isDragging = false;
const rect = toggleContent.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
toggleContent.style.cursor = "grabbing";
toggleContent.style.transition = "none";
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", handleDragEnd);
e.preventDefault();
});
}
function handleDrag(e) {
if (!floatingToggle) return;
isDragging = true;
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
let newX = e.clientX - dragOffset.x;
let newY = e.clientY - dragOffset.y;
const toggleWidth = toggleContent.offsetWidth;
const maxX = window.innerWidth - toggleWidth;
const maxY = window.innerHeight - 32;
newX = Math.max(0, Math.min(newX, maxX));
newY = Math.max(0, Math.min(newY, maxY));
toggleContent.style.left = newX + "px";
toggleContent.style.top = newY + "px";
}
function handleDragEnd(e) {
const toggleContent = floatingToggle == null ? void 0 : floatingToggle.querySelector(".floating-toggle-content");
if (toggleContent) {
toggleContent.style.cursor = "grab";
toggleContent.style.transition = "all 0.3s ease";
}
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", handleDragEnd);
saveFloatingTogglePosition();
setTimeout(() => {
isDragging = false;
}, 100);
}
function toggleExtension() {
extensionEnabled = !extensionEnabled;
updateFloatingToggleState();
safeStorageSet({ extensionEnabled });
if (extensionEnabled) {
updateLastActivity();
} else {
clearInactivityTimer();
clearAllNotifications();
}
safeSendMessage({
action: "updateExtensionEnabled",
enabled: extensionEnabled
});
console.log(`ποΈ Extension ${extensionEnabled ? "enabled" : "disabled"} via floating toggle`);
}
function updateFloatingToggleState() {
if (!floatingToggle) return;
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
const toggleIcon = floatingToggle.querySelector(".toggle-icon");
const toggleText = floatingToggle.querySelector(".toggle-text");
if (toggleContent && toggleIcon && toggleText) {
const timeRemaining = formatTimeRemaining();
const timerDisplay = extensionEnabled && timeRemaining ? ` β’ ${timeRemaining}` : "";
let backgroundColor = extensionEnabled ? "#4CAF50" : "#f44336";
if (isQuickMenuOpen) {
backgroundColor = "#667eea";
}
toggleContent.style.background = backgroundColor;
toggleContent.style.width = extensionEnabled && timeRemaining ? "130px" : "80px";
toggleIcon.textContent = extensionEnabled ? "π" : "β";
toggleText.innerHTML = `${extensionEnabled ? "ON" : "OFF"}${timerDisplay}`;
}
}
function updateFloatingToggleTimer() {
if (!floatingToggle || !extensionEnabled) return;
const toggleText = floatingToggle.querySelector(".toggle-text");
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
if (toggleText && toggleContent) {
const timeRemaining = formatTimeRemaining();
const timerDisplay = timeRemaining ? ` β’ ${timeRemaining}` : "";
toggleContent.style.width = timeRemaining ? "130px" : "80px";
toggleText.innerHTML = `ON${timerDisplay}`;
}
}
function toggleFloatingToggleVisibility() {
if (!floatingToggle) return;
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
const currentOpacity = parseFloat(toggleContent.style.opacity);
const isVisible = currentOpacity > 0.5;
const newOpacity = isVisible ? "0.3" : "0.8";
toggleContent.style.opacity = newOpacity;
safeStorageSet({ floatingToggleVisible: !isVisible });
console.log(`ποΈ Floating toggle ${isVisible ? "hidden" : "shown"}`);
}
function saveFloatingTogglePosition() {
if (!floatingToggle) return;
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
if (toggleContent) {
const rect = toggleContent.getBoundingClientRect();
const position = {
x: rect.left,
y: rect.top
};
safeStorageSet({ floatingTogglePosition: position });
}
}
function toggleQuickMenu() {
if (isQuickMenuOpen) {
closeQuickMenu();
} else {
openQuickMenu();
}
}
function openQuickMenu() {
if (!floatingToggle || isQuickMenuOpen) return;
closeQuickMenu();
const toggleContent = floatingToggle.querySelector(".floating-toggle-content");
const toggleRect = toggleContent.getBoundingClientRect();
quickMenu = document.createElement("div");
quickMenu.id = "freshroute-quick-menu";
quickMenu.className = "freshroute-quick-menu";
const menuTop = toggleRect.bottom + 8;
const menuLeft = toggleRect.left;
quickMenu.innerHTML = `
<div class="quick-menu-content" style="
position: fixed;
top: ${menuTop}px;
left: ${menuLeft}px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
z-index: 1000000;
min-width: 280px;
max-width: 320px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
border: 1px solid rgba(0,0,0,0.1);
overflow: hidden;
transform: scale(0.95) translateY(-10px);
opacity: 0;
transition: all 0.2s ease;
">
<div class="menu-header" style="
padding: 16px 20px 12px;
border-bottom: 1px solid rgba(0,0,0,0.08);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
">
<div style="font-size: 14px; font-weight: 600; margin-bottom: 4px;">
π FreshRoute Quick Menu
</div>
<div style="font-size: 11px; opacity: 0.9;">
Right-click to toggle groups β’ Click outside to close
</div>
</div>
<div class="menu-body" style="
max-height: 300px;
overflow-y: auto;
">
${generateGroupsList()}
</div>
</div>
`;
if (document.body) {
document.body.appendChild(quickMenu);
} else {
waitForBody(() => {
if (document.body && quickMenu) {
document.body.appendChild(quickMenu);
}
});
}
setTimeout(() => {
const content = quickMenu.querySelector(".quick-menu-content");
if (content) {
content.style.transform = "scale(1) translateY(0)";
content.style.opacity = "1";
}
}, 10);
addQuickMenuListeners();
isQuickMenuOpen = true;
updateFloatingToggleState();
console.log("ποΈ Quick menu opened");
}
function generateGroupsList() {
if (!ruleGroups || ruleGroups.length === 0) {
return `
<div class="no-groups" style="
padding: 32px 20px;
text-align: center;
color: #666;
">
<div style="font-size: 24px; margin-bottom: 8px;">π</div>
<div style="font-size: 13px; font-weight: 500; margin-bottom: 4px;">No Rule Groups</div>
<div style="font-size: 11px; color: #999;">Create groups in the main settings</div>
</div>
`;
}
return ruleGroups.map((group) => {
const isEnabled = group.enabled !== false;
const ruleCount = group.rules ? group.rules.length : 0;
const activeRules = group.rules ? group.rules.filter((r) => r.enabled !== false).length : 0;
return `
<div class="group-item" data-group-id="${group.id}" style="
padding: 14px 20px;
border-bottom: 1px solid rgba(0,0,0,0.05);
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
">
<div class="group-info" style="flex: 1; min-width: 0;">
<div class="group-name" style="
font-size: 13px;
font-weight: 500;
color: ${isEnabled ? "#333" : "#999"};
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
">${group.name || "Unnamed Group"}</div>
<div class="group-stats" style="
font-size: 10px;
color: #666;
">${activeRules}/${ruleCount} rules active</div>
</div>
<div class="group-toggle" style="
width: 36px;
height: 20px;
border-radius: 10px;
background: ${isEnabled ? "#4CAF50" : "#ddd"};
position: relative;
transition: background 0.3s ease;
margin-left: 12px;
flex-shrink: 0;
">
<div style="
width: 16px;
height: 16px;
border-radius: 8px;
background: white;
position: absolute;
top: 2px;
left: ${isEnabled ? "18px" : "2px"};
transition: left 0.3s ease;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
"></div>
</div>
</div>
`;
}).join("");
}
function addQuickMenuListeners() {
if (!quickMenu) return;
const groupItems = quickMenu.querySelectorAll(".group-item");
groupItems.forEach((item) => {
item.addEventListener("click", (e) => {
e.stopPropagation();
const groupId = item.dataset.groupId;
toggleRuleGroup(groupId);
});
item.addEventListener("mouseenter", () => {
item.style.background = "rgba(102, 126, 234, 0.05)";
});
item.addEventListener("mouseleave", () => {
item.style.background = "transparent";
});
});
document.addEventListener("click", handleOutsideClick);
document.addEventListener("contextmenu", handleOutsideClick);
}
function handleOutsideClick(e) {
if (quickMenu && !quickMenu.contains(e.target) && !floatingToggle.contains(e.target)) {
closeQuickMenu();
}
}
async function toggleRuleGroup(groupId) {
if (!groupId) return;
const groupIndex = ruleGroups.findIndex((g) => g.id === groupId);
if (groupIndex === -1) return;
ruleGroups[groupIndex].enabled = !ruleGroups[groupIndex].enabled;
await safeLocalStorageSet({ groups: ruleGroups });
safeSendMessage({ action: "updateRules" });
updateQuickMenuDisplay();
console.log(`ποΈ Group "${ruleGroups[groupIndex].name}" ${ruleGroups[groupIndex].enabled ? "enabled" : "disabled"}`);
}
function updateQuickMenuDisplay() {
if (!quickMenu) return;
const menuBody = quickMenu.querySelector(".menu-body");
if (menuBody) {
menuBody.innerHTML = generateGroupsList();
addQuickMenuListeners();
}
}
function closeQuickMenu() {
if (!quickMenu || !isQuickMenuOpen) return;
document.removeEventListener("click", handleOutsideClick);
document.removeEventListener("contextmenu", handleOutsideClick);
const content = quickMenu.querySelector(".quick-menu-content");
if (content) {
content.style.transform = "scale(0.95) translateY(-10px)";
content.style.opacity = "0";
}
setTimeout(() => {
if (quickMenu && quickMenu.parentNode) {
quickMenu.remove();
}
quickMenu = null;
isQuickMenuOpen = false;
updateFloatingToggleState();
}, 200);
console.log("ποΈ Quick menu closed");
}
function injectResponseHeaderDebugger() {
window.debugExtensionHeaders = function() {
console.log("π οΈ Extension CORS Handler Active");
console.log("π Current document headers:", document.location.href);
fetch(document.location.href, { method: "HEAD" }).then((response) => {
console.log("π¦ Response headers:");
const extensionHeaders = [];
const allHeaders = [];
for (const [key, value] of response.headers.entries()) {
allHeaders.push(` ${key}: ${value}`);
if (key.toLowerCase().startsWith("x-extension") || key.toLowerCase().startsWith("x-debug") || key.toLowerCase() === "cache-control") {
extensionHeaders.push(` ${key}: ${value}`);
}
}
console.log("All headers:");
allHeaders.forEach((header) => console.log(header));
if (extensionHeaders.length > 0) {
console.log("π― Extension-modified headers:");
extensionHeaders.forEach((header) => console.log(header));
} else {
console.log("β οΈ No extension-modified headers found");
}
return { allHeaders, extensionHeaders };
}).catch((e) => {
console.log("β Could not fetch headers:", e.message);
return { error: e.message };
});
};
window.testExtensionHeaders = async function(url) {
const testUrl = url || document.location.href;
console.log(`π§ͺ Testing headers for: ${testUrl}`);
try {
const response = await fetch(testUrl, {
method: "HEAD",
mode: "cors",
credentials: "include"
});
const result = {
url: response.url,
status: response.status,
extensionHeaders: {},
allHeaders: {}
};
for (const [key, value] of response.headers.entries()) {
result.allHeaders[key] = value;
if (key.toLowerCase().startsWith("x-") || key.toLowerCase() === "cache-control") {
result.extensionHeaders[key] = value;
}
}
console.table(result.extensionHeaders);
return result;
} catch (error) {
console.error("β Header test failed:", error);
return { error: error.message };
}
};
console.log("π Debug functions available:");
console.log(" - debugExtensionHeaders() - Detailed header analysis");
console.log(" - testExtensionHeaders(url) - Test specific URL headers");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", injectResponseHeaderDebugger);
} else {
injectResponseHeaderDebugger();
}
console.log("π URL Rewriter Content Script loaded");
function startNotificationTimer(notificationId) {
const notificationData = activeNotifications.find((n) => n.id === notificationId);
if (!notificationData) return;
console.log(`β±οΈ Starting timer for notification: ${notificationId} (${notificationData.remainingTime}ms)`);
notificationData.autoRemoveTimer = setTimeout(() => {
removeNotification(notificationId);
}, notificationData.remainingTime);
notificationData.startTime = Date.now();
notificationData.isPaused = false;
}
function pauseAllNotifications() {
if (isNotificationAreaHovered) return;
isNotificationAreaHovered = true;
console.log(`βΈοΈ Pausing ALL notifications (${activeNotifications.length} active)`);
activeNotifications.forEach((notificationData) => {
if (notificationData.isPaused) return;
if (notificationData.autoRemoveTimer) {
clearTimeout(notificationData.autoRemoveTimer);
notificationData.autoRemoveTimer = null;
}
const elapsed = Date.now() - notificationData.startTime;
notificationData.remainingTime = Math.max(0, notificationData.remainingTime - elapsed);
notificationData.isPaused = true;
const notificationDiv = notificationData.element.querySelector(".freshroute-notification");
if (notificationDiv) {
notificationDiv.style.boxShadow = "0 3px 12px rgba(0,0,0,0.3), 0 0 0 2px rgba(255,255,255,0.3)";
}
});
}
function resumeAllNotifications() {
if (!isNotificationAreaHovered) return;
setTimeout(() => {
const anyHovered = activeNotifications.some((notificationData) => {
const notificationDiv = notificationData.element.querySelector(".freshroute-notification");
return notificationDiv && notificationDiv.matches(":hover");
});
if (anyHovered) return;
isNotificationAreaHovered = false;
console.log(`βΆοΈ Resuming ALL notifications (${activeNotifications.length} active)`);
activeNotifications.forEach((notificationData) => {
if (!notificationData.isPaused) return;
const notificationDiv = notificationData.element.querySelector(".freshroute-notification");
if (notificationDiv) {
notificationDiv.style.boxShadow = "0 3px 12px rgba(0,0,0,0.2)";
}
if (notificationData.remainingTime > 0) {
notificationData.autoRemoveTimer = setTimeout(() => {
removeNotification(notificationData.id);
}, notificationData.remainingTime);
notificationData.startTime = Date.now();
notificationData.isPaused = false;