freshroute-server
Version:
Local development server for FreshRoute extension with API mocking and extended functionality
1,173 lines (1,169 loc) • 130 kB
JavaScript
"use strict";
let groups = [];
let editingGroupIndex = null;
let editingRuleIndex = null;
let editingRuleType = null;
let environmentVariables = [];
let editingEnvironmentVariableIndex = null;
window.groups = groups;
window.editingGroupIndex = editingGroupIndex;
window.editingRuleIndex = editingRuleIndex;
window.editingRuleType = editingRuleType;
let apiMockManager = null;
const PRESET_TEMPLATES = {
freshservice: {
name: "Freshservice",
description: "Complete setup for Freshservice development environment with URL redirection and authentication headers",
icon: "icons/freshservice.png",
variables: [
{
key: "sourceDomain",
label: "Source Domain",
placeholder: "e.g., localhost.freshservice-dev.com",
required: true,
type: "text",
defaultValue: "localhost.freshservice-dev.com"
},
{
key: "targetDomain",
label: "Target Domain",
placeholder: "e.g., infinity-share.freshinfinitysquad.com",
required: true,
type: "text",
defaultValue: "infinity-share.freshinfinitysquad.com"
},
{
key: "cookieValue",
label: "Cookie Value",
placeholder: "session=abc123...",
required: true,
type: "text",
hasCookieFetch: true
},
{
key: "csrfToken",
label: "X-CSRF-TOKEN",
placeholder: "csrf-token-value",
required: false,
type: "text",
description: "⚠️ Important: If not filled, some API calls related to create/update operations like PATCH, POST, PUT may fail due to CSRF protection."
}
],
generateRules: function(variables) {
const rules = [];
const cleanSourceDomain = extractHostname(variables.sourceDomain);
const cleanTargetDomain = extractHostname(variables.targetDomain);
if (cleanSourceDomain && cleanTargetDomain) {
rules.push({
id: "freshservice-api-redirect",
name: "Freshservice Main API Redirect",
type: "url_rewrite",
sourceUrl: `^http://${escapeRegex(cleanSourceDomain)}:3000/(api|support/v1|support/v2|support/employee_offboarding|lookup_choices)(.*)`,
targetUrl: `https://${cleanTargetDomain}/$1$2`,
enabled: true
});
rules.push({
id: "freshservice-microservices-redirect",
name: "Freshservice Microservices Redirect",
type: "url_rewrite",
sourceUrl: `^http://${escapeRegex(cleanSourceDomain)}:(8080|4000|8000)/(.*)$`,
targetUrl: `https://${cleanTargetDomain}/$2`,
enabled: true
});
rules.push({
id: "freshservice-microservices-redirect-extendd",
name: "Freshservice Microservices Redirect (Extend)",
type: "url_rewrite",
sourceUrl: `^http://${escapeRegex(cleanTargetDomain)}:(8080|4000|8000|3000)/(.*)$`,
targetUrl: `https://${cleanTargetDomain}/$2`,
enabled: true
});
}
if (cleanTargetDomain && (variables.cookieValue || variables.csrfToken)) {
const headers = [];
if (variables.cookieValue) {
headers.push({
name: "Cookie",
value: variables.cookieValue,
operation: "set",
target: "request"
});
}
if (variables.csrfToken) {
headers.push({
name: "X-CSRF-TOKEN",
value: variables.csrfToken,
operation: "set",
target: "request"
});
}
headers.push(
{
name: "X-Extension-Modified",
value: "true",
operation: "set",
target: "response"
},
{
name: "X-Debug-Info",
value: "Freshservice-Extension-Active",
operation: "set",
target: "response"
},
{
name: "Cache-Control",
value: "no-cache, no-store, must-revalidate",
operation: "set",
target: "response"
}
);
if (headers.length > 0) {
rules.push({
id: "freshservice-headers",
name: "Freshservice Authentication Headers",
type: "modify_headers",
urlPattern: `^https://${escapeRegex(cleanTargetDomain)}/.*`,
headers,
enabled: true
});
}
}
return rules;
}
},
freshdesk: {
name: "Freshdesk",
description: "Complete setup for Freshdesk development environment with URL redirection and authentication headers",
icon: "icons/freshdesk.png",
variables: [],
disabled: true,
generateRules: function(variables) {
const rules = [];
return rules;
}
},
freshsales: {
name: "Freshsales",
description: "Complete setup for Freshsales development environment with URL redirection and authentication headers",
icon: "icons/freshsales.png",
variables: [],
disabled: true,
generateRules: function(variables) {
const rules = [];
return rules;
}
},
freshmarketer: {
name: "Freshmarketer",
description: "Complete setup for Freshmarketer development environment with URL redirection and authentication headers",
icon: "icons/freshmarketer.png",
variables: [],
disabled: true,
generateRules: function(variables) {
const rules = [];
return rules;
}
}
// Additional presets can be added here
};
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function extractHostname(urlOrDomain) {
if (!urlOrDomain) return "";
let cleanDomain = urlOrDomain.trim();
if (cleanDomain.startsWith("http://") || cleanDomain.startsWith("https://")) {
try {
const url = new URL(cleanDomain);
return url.host;
} catch (e) {
cleanDomain = cleanDomain.replace(/^https?:\/\//, "");
}
}
cleanDomain = cleanDomain.split("/")[0];
cleanDomain = cleanDomain.split("?")[0];
cleanDomain = cleanDomain.split("#")[0];
return cleanDomain;
}
async function fetchAndFillCookies(inputKey, targetDomain, formContainer) {
let button = null;
let originalText = "🍪 Fetch Cookies";
try {
const inputElement = formContainer.querySelector(`[data-key="${inputKey}"]`);
if (!inputElement) {
throw new Error(`Input with key "${inputKey}" not found`);
}
button = inputElement.parentElement.querySelector(".fetch-cookies-btn");
if (!button) {
throw new Error("Fetch cookies button not found");
}
originalText = button.textContent;
button.textContent = "🔄 Fetching...";
button.disabled = true;
let cleanDomain = targetDomain.replace(/^https?:\/\//, "").split("/")[0];
const cookies = await chrome.cookies.getAll({
domain: cleanDomain
});
const subdomainCookies = await chrome.cookies.getAll({
domain: "." + cleanDomain
});
const allCookies = [...cookies, ...subdomainCookies];
const uniqueCookies = allCookies.filter(
(cookie, index, self) => index === self.findIndex((c) => c.name === cookie.name && c.domain === cookie.domain)
);
console.log(`🍪 Found ${uniqueCookies.length} cookies for domain: ${cleanDomain}`);
if (uniqueCookies.length === 0) {
alert(`No cookies found for domain: ${cleanDomain}
Make sure you have visited the target site recently and are logged in.`);
return;
}
const cookieString = uniqueCookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
inputElement.value = cookieString;
const cookieCount = uniqueCookies.length;
const cookieNames = uniqueCookies.map((c) => c.name).join(", ");
const message = `✅ Successfully fetched ${cookieCount} cookies from ${cleanDomain}:
Cookie names: ${cookieNames}
Total cookie string length: ${cookieString.length} characters
The cookies have been filled into the Cookie Value field.`;
alert(message);
console.log("🍪 Cookie details:", uniqueCookies.map((c) => ({
name: c.name,
domain: c.domain,
path: c.path,
secure: c.secure,
httpOnly: c.httpOnly,
sameSite: c.sameSite
})));
} catch (error) {
console.error("❌ Error fetching cookies:", error);
let errorMessage = "Failed to fetch cookies. ";
if (error.message.includes("chrome.cookies")) {
errorMessage += "The extension may not have cookie permissions. Please check the manifest.json file.";
} else if (error.message.includes("Invalid domain")) {
errorMessage += "Please enter a valid domain name (e.g., example.com).";
} else {
errorMessage += `Error: ${error.message}`;
}
alert(errorMessage);
} finally {
if (button) {
button.textContent = originalText;
button.disabled = false;
}
}
}
document.addEventListener("DOMContentLoaded", async () => {
try {
await new Promise((resolve) => setTimeout(resolve, 100));
if (!window.DomainValidator) {
console.error("❌ DomainValidator not found! Domain restrictions will not work.");
window.DomainValidator = {
validateTargetUrl: function(url) {
return {
allowed: false,
reason: "Domain validation system failed to load. Please reload the extension.",
suggestions: ["Reload the extension", "Check browser console for errors"]
};
},
getAllowedDomains: function() {
return [];
}
};
} else {
console.log("✅ DomainValidator loaded successfully");
console.log("Available DomainValidator methods:", Object.keys(window.DomainValidator));
}
await loadGroupsAndRules();
await loadEnvironmentVariables();
setupEventListeners();
setupSettingsListeners();
console.log("🔍 Debug - Available functions:");
console.log("typeof renderGroups:", typeof renderGroups);
console.log("typeof saveGroups:", typeof saveGroups);
console.log("typeof window.renderGroups:", typeof window.renderGroups);
console.log("typeof window.saveGroups:", typeof window.saveGroups);
setTimeout(() => {
try {
console.log("🔍 Debug - In setTimeout - Available functions:");
console.log("typeof renderGroups:", typeof renderGroups);
console.log("typeof window.renderGroups:", typeof window.renderGroups);
if (typeof renderGroups === "function") {
renderGroups();
} else {
console.error("renderGroups is not a function, type:", typeof renderGroups);
if (typeof window.renderGroups === "function") {
window.renderGroups();
} else {
console.error("window.renderGroups is also not a function");
const allFunctions = Object.getOwnPropertyNames(window).filter((name) => typeof window[name] === "function");
console.log("Available functions in window:", allFunctions.filter((name) => name.includes("render")));
}
}
} catch (error) {
console.error("Error calling renderGroups:", error);
}
}, 100);
renderEnvironmentVariables();
loadSettings();
initializeApiMockManager();
} catch (error) {
console.error("Error in DOMContentLoaded:", error);
}
});
function setupEventListeners() {
document.getElementById("closeUrlModal").addEventListener("click", () => closeUrlRuleModal());
document.getElementById("saveUrlRuleBtn").addEventListener("click", () => saveUrlRule());
document.getElementById("cancelUrlRuleBtn").addEventListener("click", () => closeUrlRuleModal());
document.getElementById("closeHeaderModal").addEventListener("click", () => closeHeaderRuleModal());
document.getElementById("saveHeaderRuleBtn").addEventListener("click", () => saveHeaderRule());
document.getElementById("cancelHeaderRuleBtn").addEventListener("click", () => closeHeaderRuleModal());
document.getElementById("addHeaderFieldBtn").addEventListener("click", () => addHeaderField());
document.getElementById("addGroupBtn").addEventListener("click", () => openGroupModal());
document.getElementById("closeGroupModal").addEventListener("click", () => closeGroupModal());
document.getElementById("saveGroupBtn").addEventListener("click", () => saveGroup());
document.getElementById("cancelGroupBtn").addEventListener("click", () => closeGroupModal());
document.getElementById("addPresetGroupBtn").addEventListener("click", () => openPresetModal());
document.getElementById("closePresetModal").addEventListener("click", () => closePresetModal());
document.getElementById("createPresetGroupBtn").addEventListener("click", () => createPresetGroup());
document.getElementById("cancelPresetBtn").addEventListener("click", () => closePresetModal());
document.getElementById("addEnvironmentVariableBtn").addEventListener("click", () => openEnvironmentVariableModal());
document.getElementById("closeEnvironmentVariableModal").addEventListener("click", () => closeEnvironmentVariableModal());
document.getElementById("saveEnvironmentVariableBtn").addEventListener("click", () => saveEnvironmentVariable());
document.getElementById("cancelEnvironmentVariableBtn").addEventListener("click", () => closeEnvironmentVariableModal());
document.getElementById("importBtn").addEventListener("click", () => document.getElementById("importFile").click());
document.getElementById("importFile").addEventListener("change", (e) => importRules(e));
document.getElementById("testSourceUrlBtn").addEventListener("click", () => testRegexPattern("sourceUrl", "sourceUrlTest", "sourceUrlResult", "targetUrl", "targetUrlPreview"));
document.getElementById("testHeaderUrlBtn").addEventListener("click", () => testHeaderRegexPattern("headerUrlPattern", "headerUrlTest", "headerUrlResult"));
document.getElementById("toggleSourceUrlTest").addEventListener("click", () => toggleTestSection("sourceUrlTestSection", "toggleSourceUrlTest", "targetUrlExamples"));
document.getElementById("toggleHeaderUrlTest").addEventListener("click", () => toggleTestSection("headerUrlTestSection", "toggleHeaderUrlTest"));
document.getElementById("sourceUrl").addEventListener("input", () => {
clearRegexResult("sourceUrlResult");
clearRegexResult("targetUrlPreview");
});
document.getElementById("headerUrlPattern").addEventListener("input", () => {
clearRegexResult("headerUrlResult");
validateHeaderUrlPatternInput();
});
document.getElementById("targetUrl").addEventListener("input", () => {
const testUrl = document.getElementById("sourceUrlTest").value.trim();
const sourcePattern = document.getElementById("sourceUrl").value.trim();
if (testUrl && sourcePattern) {
testRegexPattern("sourceUrl", "sourceUrlTest", "sourceUrlResult", "targetUrl", "targetUrlPreview");
}
validateTargetUrlDomainInput();
});
document.getElementById("matchingType").addEventListener("change", () => {
updateMatchingTypeUI();
});
document.getElementById("headerMatchingType").addEventListener("change", () => {
updateHeaderMatchingTypeUI();
});
document.addEventListener("click", (e) => {
if (e.target.classList.contains("btn-example")) {
const targetField = e.target.dataset.target;
const pattern = e.target.dataset.pattern;
insertExamplePattern(targetField, pattern);
}
});
setupModalCloseHandling();
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeUrlRuleModal();
closeHeaderRuleModal();
closeGroupModal();
closePresetModal();
closeEnvironmentVariableModal();
}
});
document.addEventListener("click", (event) => {
document.querySelectorAll(".add-rule-dropdown-content.show").forEach((dropdown) => {
if (!dropdown.closest(".add-rule-dropdown").contains(event.target)) {
dropdown.classList.remove("show");
}
});
});
document.querySelectorAll(".tab-button").forEach((button) => {
button.addEventListener("click", (e) => {
const targetTab = e.currentTarget.dataset.tab;
switchTab(targetTab);
});
});
document.getElementById("rulesTab").addEventListener("click", () => {
showRulesView();
updateTabNavigation("rules");
});
document.getElementById("settingsTab").addEventListener("click", () => {
showSettingsView();
updateTabNavigation("settings");
});
document.getElementById("backToRulesBtn").addEventListener("click", () => {
showRulesView();
});
document.getElementById("dashboardBtn").addEventListener("click", () => {
window.location.href = chrome.runtime.getURL("dashboard.html");
});
document.getElementById("networkRecorderBtn").addEventListener("click", () => {
window.location.href = chrome.runtime.getURL("network-recorder.html");
});
setupApiMockEventListeners();
}
function setupApiMockEventListeners() {
const addMockRuleBtn = document.getElementById("addMockRuleBtn");
if (addMockRuleBtn) {
addMockRuleBtn.addEventListener("click", () => openApiMockModal());
}
const refreshApiMockFilesBtn = document.getElementById("refreshApiMockFilesBtn");
if (refreshApiMockFilesBtn) {
refreshApiMockFilesBtn.addEventListener("click", () => refreshApiMockFiles());
}
document.addEventListener("click", (e) => {
if (e.target.classList.contains("mock-rule-edit")) {
const ruleId = e.target.dataset.ruleId;
if (ruleId) {
editApiMockRule(ruleId);
}
}
if (e.target.classList.contains("mock-rule-delete")) {
const ruleId = e.target.dataset.ruleId;
if (ruleId) {
deleteApiMockRule(ruleId);
}
}
if (e.target.classList.contains("notification-close")) {
const notification = e.target.closest(".notification");
if (notification) {
notification.remove();
}
}
});
document.addEventListener("change", (e) => {
if (e.target.classList.contains("mock-rule-toggle")) {
const ruleId = e.target.dataset.ruleId;
const enabled = e.target.checked;
if (ruleId) {
toggleApiMockRule(ruleId, enabled);
}
}
});
}
function setupSettingsListeners() {
const notificationsToggle = document.getElementById("notificationsEnabledToggle");
if (notificationsToggle) {
notificationsToggle.addEventListener("change", async (e) => {
await chrome.storage.sync.set({ notificationsEnabled: e.target.checked });
console.log("Notifications setting updated:", e.target.checked);
});
}
const compactNotificationsToggle = document.getElementById("compactNotificationsToggle");
if (compactNotificationsToggle) {
compactNotificationsToggle.addEventListener("change", async (e) => {
await chrome.storage.sync.set({ compactNotifications: e.target.checked });
console.log("Compact notifications setting updated:", e.target.checked);
});
}
const exportEnvironmentVariablesToggle = document.getElementById("exportEnvironmentVariablesToggle");
if (exportEnvironmentVariablesToggle) {
exportEnvironmentVariablesToggle.addEventListener("change", async (e) => {
await chrome.storage.sync.set({ exportEnvironmentVariables: e.target.checked });
console.log("Export environment variables setting updated:", e.target.checked);
});
}
const refreshApiMockFilesBtn = document.getElementById("refreshApiMockFilesBtn");
if (refreshApiMockFilesBtn) {
refreshApiMockFilesBtn.addEventListener("click", () => {
if (apiMockManager) {
showNotification("Refreshing FreshRoute server files...", "info");
apiMockManager.refreshFiles();
}
});
}
const manageFilesMainBtn = document.getElementById("manageFilesMainBtn");
if (manageFilesMainBtn) {
manageFilesMainBtn.addEventListener("click", () => {
if (apiMockManager) {
apiMockManager.openFileManagementModal();
}
});
}
const saveMockServerSettingsBtn = document.getElementById("saveMockServerSettingsBtn");
if (saveMockServerSettingsBtn) {
saveMockServerSettingsBtn.addEventListener("click", saveMockServerSettings);
}
}
async function loadSettings() {
try {
const {
notificationsEnabled,
compactNotifications,
exportEnvironmentVariables,
mockServerHttpUrl,
mockServerWsUrl
} = await chrome.storage.sync.get([
"notificationsEnabled",
"compactNotifications",
"exportEnvironmentVariables",
"mockServerHttpUrl",
"mockServerWsUrl"
]);
const notificationsToggle = document.getElementById("notificationsEnabledToggle");
const compactNotificationsToggle = document.getElementById("compactNotificationsToggle");
const exportEnvVariablesToggle = document.getElementById("exportEnvironmentVariablesToggle");
const mockServerHttpUrlInput = document.getElementById("mockServerHttpUrl");
const mockServerWsUrlInput = document.getElementById("mockServerWsUrl");
if (notificationsToggle) {
notificationsToggle.checked = notificationsEnabled !== false;
}
if (compactNotificationsToggle) {
compactNotificationsToggle.checked = compactNotifications !== false;
}
if (exportEnvVariablesToggle) {
exportEnvVariablesToggle.checked = exportEnvironmentVariables === true;
}
if (mockServerHttpUrlInput) {
mockServerHttpUrlInput.value = mockServerHttpUrl || "http://localhost:3001";
}
if (mockServerWsUrlInput) {
mockServerWsUrlInput.value = mockServerWsUrl || "ws://localhost:3002";
}
} catch (error) {
console.error("Error loading settings:", error);
}
}
async function saveMockServerSettings() {
var _a, _b;
try {
const mockServerHttpUrlInput = document.getElementById("mockServerHttpUrl");
const mockServerWsUrlInput = document.getElementById("mockServerWsUrl");
const httpUrl = ((_a = mockServerHttpUrlInput == null ? void 0 : mockServerHttpUrlInput.value) == null ? void 0 : _a.trim()) || "http://localhost:3001";
const wsUrl = ((_b = mockServerWsUrlInput == null ? void 0 : mockServerWsUrlInput.value) == null ? void 0 : _b.trim()) || "ws://localhost:3002";
try {
new URL(httpUrl);
new URL(wsUrl);
} catch (error) {
showNotification("Please enter valid URLs", "error");
return;
}
await chrome.storage.sync.set({
mockServerHttpUrl: httpUrl,
mockServerWsUrl: wsUrl
});
if (apiMockManager) {
console.log("🔄 Updating API Mock Manager with new URLs:", { httpUrl, wsUrl });
await apiMockManager.updateServerUrls(httpUrl, wsUrl);
} else {
console.warn("⚠️ API Mock Manager not available during settings save");
}
showNotification("FreshRoute server settings saved successfully", "success");
console.log("FreshRoute server settings saved:", { httpUrl, wsUrl });
} catch (error) {
console.error("Error saving FreshRoute server settings:", error);
showNotification("Error saving FreshRoute server settings", "error");
}
}
async function loadGroupsAndRules() {
try {
let result = await chrome.storage.local.get(["groups", "rules"]);
if (!result.groups && !result.rules) {
result = await chrome.storage.sync.get(["groups", "rules"]);
if (result.groups || result.rules) {
console.log("Migrating data from sync to local storage...");
await chrome.storage.local.set(result);
await chrome.storage.sync.remove(["groups", "rules"]);
}
}
if (result.groups) {
groups = result.groups;
window.groups = groups;
} else {
groups = await migrateFromOldFormat(result.rules || []);
window.groups = groups;
await saveGroups();
}
} catch (error) {
console.error("Error loading groups and rules:", error);
groups = [];
window.groups = groups;
}
}
async function loadEnvironmentVariables() {
try {
const result = await chrome.storage.sync.get(["environmentVariables"]);
environmentVariables = result.environmentVariables || [];
} catch (error) {
console.error("Error loading environment variables:", error);
environmentVariables = [];
}
}
async function saveEnvironmentVariables() {
try {
await chrome.storage.sync.set({ environmentVariables });
console.log("💾 Environment variables saved to storage");
setTimeout(() => {
console.log("🚀 Triggering rule update...");
chrome.runtime.sendMessage({ action: "updateRules" }, (response) => {
if (chrome.runtime.lastError) {
console.error("❌ Error sending update message:", chrome.runtime.lastError);
} else if (response && response.success) {
console.log("✅ Rules updated successfully after environment variable change");
} else {
console.warn("⚠️ Rule update may have failed:", response == null ? void 0 : response.error);
}
});
}, 50);
} catch (error) {
console.error("Error saving environment variables:", error);
alert("Error saving environment variables. Please try again.");
}
}
function renderEnvironmentVariables() {
const container = document.getElementById("environmentVariables");
if (environmentVariables.length === 0) {
container.innerHTML = `
<div class="env-empty-state">
<h3>📋 No Environment Variables</h3>
<p>Create reusable variables to avoid repetition in your rules.</p>
<p>Variables can be used in URL patterns, target URLs, and header values.</p>
<p>Click "Add Variable" to create your first environment variable.</p>
</div>
`;
return;
}
container.innerHTML = `
<div class="env-variables-container">
${environmentVariables.map((variable, index) => `
<div class="env-variable-card">
<div class="env-variable-info">
<div class="env-variable-name">${escapeHtml(variable.name)}</div>
<div class="env-variable-value">Value: ${escapeHtml(variable.value)}</div>
${variable.description ? `<div class="env-variable-description">${escapeHtml(variable.description)}</div>` : ""}
<div class="env-variable-usage">Usage: {{${escapeHtml(variable.name)}}}</div>
</div>
<div class="env-variable-actions">
<button class="btn btn-secondary btn-small env-variable-edit" data-index="${index}">Edit</button>
<button class="btn btn-danger btn-small env-variable-delete" data-index="${index}">Delete</button>
</div>
</div>
`).join("")}
</div>
`;
addEnvironmentVariableListeners();
}
function addEnvironmentVariableListeners() {
document.querySelectorAll(".env-variable-edit").forEach((button) => {
button.addEventListener("click", (e) => {
const index = parseInt(e.target.dataset.index);
editEnvironmentVariable(index);
});
});
document.querySelectorAll(".env-variable-delete").forEach((button) => {
button.addEventListener("click", (e) => {
const index = parseInt(e.target.dataset.index);
deleteEnvironmentVariable(index);
});
});
}
async function migrateFromOldFormat(oldRules) {
if (oldRules.length === 0) return [];
const unifiedGroup = {
id: "default-mixed-" + Date.now(),
name: "Default Rules",
expanded: true,
enabled: true,
rules: oldRules
};
return [unifiedGroup];
}
async function saveGroups() {
try {
await chrome.storage.local.set({ groups });
chrome.runtime.sendMessage({ action: "updateRules" });
if (apiMockManager) {
apiMockManager.onRulesChanged();
}
console.log("Groups saved successfully");
} catch (error) {
console.error("Error saving groups:", error);
}
}
function renderGroups() {
console.log("🎨 Starting renderGroups with groups:", groups);
const container = document.getElementById("allGroups");
if (groups.length === 0) {
console.log("🎨 No groups to render, showing empty state");
container.innerHTML = `
<div class="empty-state">
<p>No rule groups configured.</p>
<p>Click "Add Group" to create your first group.</p>
</div>
`;
return;
}
console.log("🎨 Rendering", groups.length, "groups");
container.innerHTML = groups.map((group, groupIndex) => {
const enabledRules = group.rules.filter((rule) => rule.enabled).length;
const groupEnabled = group.enabled !== false;
const urlRules = group.rules.filter((rule) => rule.type === "url_rewrite").length;
const headerRules = group.rules.filter((rule) => rule.type === "modify_headers").length;
const mockRules = group.rules.filter((rule) => rule.type === "mock_response").length;
let typeIndicator = "";
const totalTypes = (urlRules > 0 ? 1 : 0) + (headerRules > 0 ? 1 : 0) + (mockRules > 0 ? 1 : 0);
if (totalTypes > 1) {
const types = [];
if (urlRules > 0) types.push(`${urlRules} URL`);
if (headerRules > 0) types.push(`${headerRules} Headers`);
if (mockRules > 0) types.push(`${mockRules} Mock`);
typeIndicator = `<span class="mixed-group-indicator">Mixed (${types.join(", ")})</span>`;
} else if (urlRules > 0) {
typeIndicator = `<span class="rule-type-indicator url-rewrite">URL Rewrite</span>`;
} else if (headerRules > 0) {
typeIndicator = `<span class="rule-type-indicator header-modify">Headers</span>`;
} else if (mockRules > 0) {
typeIndicator = `<span class="rule-type-indicator mock-response">Mock Response</span>`;
}
return `
<div class="group-container">
<div class="group-header ${group.expanded === false ? "collapsed" : ""}" data-group-index="${groupIndex}">
<div class="group-left">
<span class="group-expand ${group.expanded === false ? "collapsed" : ""}" data-group-index="${groupIndex}">▼</span>
<span class="group-name" title="${escapeHtml(group.name)}">${escapeHtml(group.name)}</span>
<span class="group-count">${group.rules.length} rules (${enabledRules} enabled)</span>
${typeIndicator}
</div>
<div class="group-toggle">
<label class="toggle-switch">
<input type="checkbox" ${groupEnabled ? "checked" : ""}
data-group-index="${groupIndex}" class="group-toggle-switch">
<span class="slider"></span>
</label>
</div>
<div class="group-actions">
<div class="add-rule-dropdown">
<button class="btn btn-success btn-small add-rule-toggle" data-group-index="${groupIndex}">Add Rule ▼</button>
<div class="add-rule-dropdown-content" data-group-index="${groupIndex}">
<button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="url_rewrite">🔗 URL Rewrite Rule</button>
<button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="modify_headers">📝 Header Modification Rule</button>
<button class="add-rule-option" data-group-index="${groupIndex}" data-rule-type="mock_response">🎭 Mock Response Rule</button>
</div>
</div>
<button class="btn export-btn btn-small group-export" data-group-index="${groupIndex}">Export</button>
<button class="btn btn-secondary btn-small group-edit" data-group-index="${groupIndex}">Edit</button>
<button class="btn btn-danger btn-small group-delete" data-group-index="${groupIndex}">Delete</button>
</div>
</div>
<div class="group-content" style="display: ${group.expanded === false ? "none" : "block"}">
${group.rules.length > 0 ? renderRulesInGroup(group.rules, groupIndex) : '<div class="empty-group">No rules in this group. Click "Add Rule" to add the first rule.</div>'}
</div>
</div>
`;
}).join("");
addGroupControlListeners();
}
function renderRulesInGroup(rules, groupIndex) {
console.log("🎨 renderRulesInGroup called for group", groupIndex, "with rules:", rules);
return rules.map((rule, ruleIndex) => {
console.log("🎨 Processing rule", ruleIndex, "of type", rule.type, ":", rule);
let ruleTypeIndicator;
if (rule.type === "url_rewrite") {
ruleTypeIndicator = '<span class="rule-type-indicator url-rewrite">URL</span>';
} else if (rule.type === "modify_headers") {
ruleTypeIndicator = '<span class="rule-type-indicator header-modify">Header</span>';
} else if (rule.type === "mock_response") {
ruleTypeIndicator = '<span class="rule-type-indicator mock-response">Mock</span>';
}
const isFirstRule = ruleIndex === 0;
const isLastRule = ruleIndex === rules.length - 1;
if (rule.type === "url_rewrite") {
return `
<div class="rule-card ${rule.enabled ? "enabled" : "disabled"}">
<div class="rule-header">
<div class="rule-left">
<div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div>
</div>
<div class="rule-controls">
<label class="toggle-switch">
<input type="checkbox" ${rule.enabled ? "checked" : ""}
data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle">
<span class="slider"></span>
</label>
<div class="rule-reorder-controls">
<button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isFirstRule ? "disabled" : ""} title="Move Up">▲</button>
<button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isLastRule ? "disabled" : ""} title="Move Down">▼</button>
</div>
<button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button>
<button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="url">Edit</button>
<button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button>
</div>
</div>
<div class="rule-details">
<div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div>
<div><strong>Source:</strong> <code>${escapeHtml(rule.sourceUrl)}</code></div>
<div><strong>Target:</strong> <code>${escapeHtml(rule.targetUrl)}</code></div>
</div>
</div>
`;
} else if (rule.type === "modify_headers") {
return `
<div class="rule-card ${rule.enabled ? "enabled" : "disabled"}">
<div class="rule-header">
<div class="rule-left">
<div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div>
</div>
<div class="rule-controls">
<label class="toggle-switch">
<input type="checkbox" ${rule.enabled ? "checked" : ""}
data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle">
<span class="slider"></span>
</label>
<div class="rule-reorder-controls">
<button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isFirstRule ? "disabled" : ""} title="Move Up">▲</button>
<button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isLastRule ? "disabled" : ""} title="Move Down">▼</button>
</div>
<button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button>
<button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="header">Edit</button>
<button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button>
</div>
</div>
<div class="rule-details">
<div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div>
<div><strong>URL Pattern:</strong> <code>${escapeHtml(rule.urlPattern)}</code></div>
<div><strong>Headers:</strong></div>
<ul>
${rule.headers.map((header) => `
<li><strong>${escapeHtml(header.name)}</strong> (${header.operation})
${header.target} ${header.value ? `→ ${escapeHtml(header.value)}` : ""}</li>
`).join("")}
</ul>
</div>
</div>
`;
} else if (rule.type === "mock_response") {
return `
<div class="rule-card ${rule.enabled ? "enabled" : "disabled"}">
<div class="rule-header">
<div class="rule-left">
<div class="rule-title">${escapeHtml(rule.name || "Unnamed Rule")} ${ruleTypeIndicator}</div>
</div>
<div class="rule-controls">
<label class="toggle-switch">
<input type="checkbox" ${rule.enabled ? "checked" : ""}
data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" class="rule-toggle">
<span class="slider"></span>
</label>
<div class="rule-reorder-controls">
<button class="btn btn-reorder rule-move-up" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isFirstRule ? "disabled" : ""} title="Move Up">▲</button>
<button class="btn btn-reorder rule-move-down" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}"
${isLastRule ? "disabled" : ""} title="Move Down">▼</button>
</div>
<button class="btn btn-duplicate btn-small rule-duplicate" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Duplicate</button>
<button class="btn btn-secondary btn-small rule-edit" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}" data-rule-type="mock">Edit</button>
<button class="btn btn-danger btn-small rule-delete" data-group-index="${groupIndex}" data-rule-index="${ruleIndex}">Delete</button>
</div>
</div>
<div class="rule-details">
<div><strong>Method:</strong> <span class="method-badge method-${(rule.method || "any").toLowerCase()}">${rule.method || "ANY"}</span></div>
<div><strong>Type:</strong> ${getMatchingTypeDisplayName(rule.matchingType || "regex")}</div>
<div><strong>URL Pattern:</strong> <code>${escapeHtml(rule.urlPattern || "")}</code></div>
<div><strong>Response:</strong> ${(rule.responseType || "json").toUpperCase()} (${rule.statusCode || 200})</div>
${rule.delay > 0 ? `<div><strong>Delay:</strong> ${rule.delay}ms</div>` : ""}
${rule.description ? `<div><strong>Description:</strong> ${escapeHtml(rule.description)}</div>` : ""}
</div>
</div>
`;
}
console.warn("⚠️ Unknown rule type:", rule.type, rule);
return `
<div class="rule-card ${rule.enabled ? "enabled" : "disabled"}">
<div class="rule-header">
<div class="rule-left">
<div class="rule-title">${escapeHtml(rule.name || "Unknown Rule")} <span class="rule-type-indicator">Unknown</span></div>
</div>
</div>
<div class="rule-details">
<div><strong>Type:</strong> ${rule.type || "Unknown"}</div>
<div><strong>Data:</strong> <pre>${JSON.stringify(rule, null, 2)}</pre></div>
</div>
</div>
`;
}).join("");
}
function addGroupControlListeners() {
document.querySelectorAll(".group-expand").forEach((arrow) => {
arrow.addEventListener("click", (e) => {
e.stopPropagation();
const groupIndex = parseInt(arrow.dataset.groupIndex);
toggleGroup(groupIndex);
});
});
document.querySelectorAll(".group-header").forEach((header) => {
header.addEventListener("click", (e) => {
if (e.target === header || e.target.classList.contains("group-name")) {
const groupIndex = parseInt(header.dataset.groupIndex);
toggleGroup(groupIndex);
}
});
});
document.querySelectorAll(".group-toggle-switch").forEach((toggle) => {
toggle.addEventListener("change", (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
toggleGroupEnabled(groupIndex);
});
});
document.querySelectorAll(".add-rule-toggle").forEach((button) => {
button.addEventListener("click", (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
toggleAddRuleDropdown(groupIndex);
});
});
document.querySelectorAll(".add-rule-option").forEach((option) => {
option.addEventListener("click", (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleType = e.target.dataset.ruleType;
addRuleToGroup(groupIndex, ruleType);
const dropdown = e.target.closest(".add-rule-dropdown-content");
dropdown.classList.remove("show");
});
});
document.querySelectorAll(".group-export").forEach((button) => {
button.addEventListener("click", async (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
await exportGroup(groupIndex);
});
});
document.querySelectorAll(".group-edit").forEach((button) => {
button.addEventListener("click", (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
editGroup(groupIndex);
});
});
document.querySelectorAll(".group-delete").forEach((button) => {
button.addEventListener("click", (e) => {
e.stopPropagation();
const groupIndex = parseInt(e.target.dataset.groupIndex);
deleteGroup(groupIndex);
});
});
document.querySelectorAll(".rule-toggle").forEach((toggle) => {
toggle.addEventListener("change", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
toggleRule(groupIndex, ruleIndex);
});
});
document.querySelectorAll(".rule-edit").forEach((button) => {
button.addEventListener("click", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
const type = e.target.dataset.ruleType;
if (type === "url") {
editUrlRule(groupIndex, ruleIndex);
} else if (type === "header") {
editHeaderRule(groupIndex, ruleIndex);
} else if (type === "mock") {
openMockRuleModal(groupIndex, ruleIndex);
}
});
});
document.querySelectorAll(".rule-delete").forEach((button) => {
button.addEventListener("click", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
deleteRule(groupIndex, ruleIndex);
});
});
document.querySelectorAll(".rule-duplicate").forEach((button) => {
button.addEventListener("click", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
duplicateRule(groupIndex, ruleIndex);
});
});
document.querySelectorAll(".rule-move-up").forEach((button) => {
button.addEventListener("click", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
moveRuleUp(groupIndex, ruleIndex);
});
});
document.querySelectorAll(".rule-move-down").forEach((button) => {
button.addEventListener("click", (e) => {
const groupIndex = parseInt(e.target.dataset.groupIndex);
const ruleIndex = parseInt(e.target.dataset.ruleIndex);
moveRuleDown(groupIndex, ruleIndex);
});
});
}
function toggleGroup(groupIndex) {
groups[groupIndex].expanded = groups[groupIndex].expanded !== false ? false : true;
saveGroups();
renderGroups();
}
function toggleGroupEnabled(groupIndex) {
groups[groupIndex].enabled = !groups[groupIndex].enabled;
if (!groups[groupIndex].enabled) {
groups[groupIndex].rules.forEach((rule) => {
rule.wasEnabled = rule.enabled;
rule.enabled = false;
});
} else {
groups[groupIndex].rules.forEach((rule) => {
rule.enabled = rule.wasEnabled !== void 0 ? rule.wasEnabled : true;
delete rule.wasEnabled;
});
}
saveGroups();
renderGroups();
}
function toggleAddRuleDropdown(groupIndex) {
document.querySelectorAll(".add-rule-dropdown-content.show").forEach((dropdown2) => {
dropdown2.classList.remove("show");
});
const dropdown = document.querySelector(`.add-rule-dropdown-content[data-group-index="${groupIndex}"]`);
if (dropdown) {
dropdown.classList.toggle("show");
}
}
function addRuleToGroup(groupIndex, ruleType) {
groups[groupIndex];
if (ruleType === "url_rewrite") {
openUrlRuleModal(groupIndex);
} else if (ruleType === "modify_headers") {
openHeaderRuleModal(groupIndex);
} else if (ruleType === "mock_response") {
openMockRuleModal(groupIndex);
}
}
async function exportGroup(groupIndex) {
const group = groups[groupIndex];
const { exportEnvironmentVariables } = await chrome.storage.sync.get(["exportEnvironmentVariables"]);
const exportData = {
version: "1.0",
exportDate: (/* @__PURE__ */ new Date()).toISOString(),
groups: [group]
};
if (exportEnvironmentVariables === true && environmentVariables.length > 0) {
exportData.environmentVariables = environmentVariables;
}
const fileName = `${group.name.replace(/[^a-z0-9]/gi, "_").toLowerCase()}_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.json`;
downloadJSON(exportData, fileName);
}
async function toggleRule(groupIndex, ruleIndex) {
groups[groupIndex].rules[ruleIndex].enabled = !groups[groupIndex].rules[ruleIndex].enabled;
await saveGroups();
renderGroups();
}
async function deleteRule(groupIndex, ruleIndex) {
if (confirm("Are you sure you want to delete this rule?")) {
groups[groupIndex].rules.splice(ruleIndex, 1);
await saveGroups();
renderGroups();
}
}
function openGroupModal(groupIndex = null) {
editingGroupIndex = groupIndex;
const modal = document.getElementById("groupModal");
const nameInput = document.getElementById("groupName");
if (groupIndex !== null) {
const group = groups[groupIndex];
nameInput.value = group.name || "";
} else {
nameInput.value = "";
}
modal.style.display = "block";
nameInput.focus();
}
function closeGroupModal() {
document.getElementById("groupModal").style.display = "none";
editingGroupIndex = null;
}
function editGroup(index) {
openGroupModal(index);
}
async function deleteGroup(index) {
if (confirm("Are you sure you want to delete this group and all its rules?")) {
groups.splice(index, 1);
await saveGroups();
renderGroups();
}
}
async function saveGroup() {
const name = document.getElementById("groupName").value.trim();
if (!name) {
alert("Please enter a group name.");
return;
}
const group = {
id: editingGroupIndex !== null ? groups[editingGroupIndex].id : Date.now().toString(),
name,
expanded: editingGroupIndex !== null ? groups[editingGroupIndex].expanded : true,
enabled: editingGroupIndex !== null ? groups[editingGroupIndex].enabled : true,
rules: editingGroupIndex !== null ? groups[editingGroupIndex].rules : []
};
if (editingGroupIndex !== null) {
groups[editingGroupIndex] = group;
} else {
groups.push(group);
}
try {
await saveGroups();
renderGroups();
} catch (error) {
console.error("Error saving group:", error);
alert("Error saving group: " + error.message);
}
closeGroupModal();
}
function openUrlRuleModal(groupIndex = null, ruleIndex = null) {
editingRuleIndex = ruleIndex;
editingRuleType = "url_rewrite";
editingGroupIndex = groupIndex;
const modal = document.getElementById("urlRuleModal");
const matchingTypeSelect = document.getElementById("matchingType");
const sourceInput = document.getElementById("sourceUrl");
const targetInput = document.getElementById("targetUrl");
if (groupIndex !== null && ruleIndex !== null) {
const rule = groups[groupIndex].rules[ruleIndex];
matchingTypeSelect.value = rule.matchingType || "regex";
sourceInput.value = rule.sourceUrl || "";
targetInput.value = rule.targetUrl || "";
if (rule.targetUrl) {
setTimeout(() => validateTargetUrlDomainInput(), 100);
}
} else {
matchingTypeSelect.value = "regex";
sourceInput.value = "";
targetInput.value = "";
}
resetTestSection("sourceUrlTestSection", "toggleSourceUrlTest");
resetTestSection("targetUrlExamples");
clearRegexResult("sourceUrlResult");
clearRegexResult("targetUrlPreview");
updateMatchingTypeUI();
addVariableSuggestions(sourceInput);
addVariableSuggestions(targetInput);
modal.style.display = "block";
sourceInput.focus();
}
function closeUrlRuleModal() {
document.getElementById("urlRuleModal").style.display = "none";
editingRuleIndex = null;
editingRuleType = null;
editingGroupIndex = null;
}
function editUrlRule(groupIndex, ruleIndex) {
openUrlRuleModal(groupIndex, ruleIndex);
}
async function saveUrlRule() {
const matchingType = document.getElementById("matchingType").value;
const sourceUrl = document.getElementById("sourceUrl").value.trim();
const targetUrl = document.getElementById("targetUrl").value.trim();
if (!sourceUrl || !targetUrl)