UNPKG

freshroute-server

Version:

Local development server for FreshRoute extension with API mocking and extended functionality

1,082 lines (1,079 loc) 38.2 kB
"use strict"; let isMonitoring = false; let groups = []; let environmentVariables = []; function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } async function runVisualDebugger() { const urlInput = document.getElementById("debugUrlInput"); const resultsContainer = document.getElementById("debuggerResults"); const showOnlyMatching = document.getElementById("showOnlyMatchingRules").checked; const testUrl2 = urlInput.value.trim(); if (!testUrl2) { alert("Please enter a URL to debug"); return; } try { new URL(testUrl2); } catch (error) { resultsContainer.innerHTML = ` <div class="failure-reason"> <strong>❌ Invalid URL Format</strong><br> Please enter a valid URL (e.g., https://example.com/path) </div> `; return; } resultsContainer.innerHTML = ` <div class="debugger-placeholder"> <div class="placeholder-icon">⏳</div> <h3>Analyzing Rules...</h3> <p>Running step-by-step pattern matching analysis</p> </div> `; setTimeout(() => { try { const debugResults = analyzeUrlAgainstRules(testUrl2, showOnlyMatching); renderDebugResults(debugResults, testUrl2); } catch (error) { console.error("Debug analysis error:", error); resultsContainer.innerHTML = ` <div class="failure-reason"> <strong>❌ Analysis Error</strong><br> ${error.message} </div> `; } }, 500); } function clearDebugger() { document.getElementById("debugUrlInput").value = ""; document.getElementById("debuggerResults").innerHTML = ` <div class="debugger-placeholder"> <div class="placeholder-icon">🎯</div> <h3>Ready for Visual Debugging</h3> <p>Enter a URL above and click "Debug Rules" to see step-by-step pattern matching</p> <ul class="feature-list"> <li>🔍 Step-by-step rule evaluation</li> <li>🎨 Visual regex pattern matching</li> <li>🔄 Variable substitution tracking</li> <li>📝 Capture group visualization</li> <li>❌ Match failure explanations</li> </ul> </div> `; } document.addEventListener("DOMContentLoaded", async () => { console.log("FreshRoute Dashboard initialized"); await loadData(); await loadDashboardLogs(); setupEventListeners(); updateMonitoringStatus(); chrome.runtime.onMessage.addListener(handleRuleActivity); }); async function loadData() { try { const [localData, syncData] = await Promise.all([ chrome.storage.local.get(["groups"]), chrome.storage.sync.get(["environmentVariables"]) ]); groups = localData.groups || []; environmentVariables = syncData.environmentVariables || []; console.log("Dashboard loaded data:", { groups: groups.length, environmentVariables: environmentVariables.length }); if (groups.length > 0) { let totalRules = 0; let enabledRules = 0; groups.forEach((group, i) => { const groupRules = (group.rules || []).length; const groupEnabledRules = (group.rules || []).filter((rule) => rule.enabled).length; totalRules += groupRules; enabledRules += groupEnabledRules; console.log(`Group ${i}: "${group.name}" - ${groupRules} rules (${groupEnabledRules} enabled), group enabled: ${group.enabled !== false}`); }); console.log(`Total: ${totalRules} rules, ${enabledRules} enabled across ${groups.length} groups`); } } catch (error) { console.error("Error loading data:", error); } } function setupEventListeners() { document.getElementById("startMonitoringBtn").addEventListener("click", startMonitoring); document.getElementById("pauseMonitoringBtn").addEventListener("click", pauseMonitoring); document.getElementById("clearLogsBtn").addEventListener("click", clearLogs); document.getElementById("testUrlBtn").addEventListener("click", testUrl); document.getElementById("openNetworkRecorderBtn").addEventListener("click", openNetworkRecorder); document.getElementById("runDebuggerBtn").addEventListener("click", runVisualDebugger); document.getElementById("clearDebuggerBtn").addEventListener("click", clearDebugger); document.querySelectorAll(".tab-btn").forEach((btn) => { btn.addEventListener("click", () => switchTab(btn.dataset.tab)); }); document.getElementById("testUrlInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { testUrl(); } }); document.getElementById("debugUrlInput").addEventListener("keydown", (e) => { if (e.key === "Enter") { runVisualDebugger(); } }); } function switchTab(tabId) { document.querySelectorAll(".tab-btn").forEach((btn) => btn.classList.remove("active")); document.querySelectorAll(".tab-content").forEach((content) => content.classList.remove("active")); document.querySelector(`[data-tab="${tabId}"]`).classList.add("active"); document.getElementById(tabId).classList.add("active"); } function openNetworkRecorder() { window.location.href = chrome.runtime.getURL("network-recorder.html"); } function startMonitoring() { if (isMonitoring) return; isMonitoring = true; updateMonitoringStatus(); addLogEntry("success", "Monitoring started - Listening for rule activity..."); document.getElementById("startMonitoringBtn").disabled = true; document.getElementById("pauseMonitoringBtn").disabled = false; } function pauseMonitoring() { if (!isMonitoring) return; isMonitoring = false; updateMonitoringStatus(); addLogEntry("error", "Monitoring paused"); document.getElementById("startMonitoringBtn").disabled = false; document.getElementById("pauseMonitoringBtn").disabled = true; } function handleRuleActivity(message, sender, sendResponse) { if (message.type === "ruleApplied") { const { rule, originalUrl, newUrl, responseTime } = message; const logType = rule.type === "url" || rule.type === "url_rewrite" ? "redirect" : "header"; const logMessage = rule.type === "url" || rule.type === "url_rewrite" ? `${originalUrl}${newUrl}` : `Headers modified for ${originalUrl}`; addLogEntry(logType, logMessage, rule.name || "Unnamed Rule"); } } function updateMonitoringStatus() { const statusElement = document.getElementById("monitoringStatus"); const statusText = statusElement.querySelector("span"); if (isMonitoring) { statusElement.className = "monitoring-status active"; statusText.textContent = "Monitoring Active"; } else { statusElement.className = "monitoring-status inactive"; statusText.textContent = "Monitoring Stopped"; } } async function testUrl() { const urlInput = document.getElementById("testUrlInput"); const testResultDiv = document.getElementById("testResult"); const testUrl2 = urlInput.value.trim(); if (!testUrl2) { testResultDiv.innerHTML = '<div class="test-result no-match">Please enter a URL to test</div>'; return; } testResultDiv.innerHTML = ""; try { const url = new URL(testUrl2); const matchedRules = []; const substitutedVars = {}; environmentVariables.forEach((envVar) => { substitutedVars[envVar.name] = envVar.value; }); groups.forEach((group, groupIndex) => { if (group.enabled === false) return; (group.rules || []).forEach((rule, ruleIndex) => { if (!rule.enabled) return; if (rule.type === "url_rewrite") { let pattern = rule.sourceUrl; let targetUrl = rule.targetUrl; const matchingType = rule.matchingType || "regex"; Object.keys(substitutedVars).forEach((varName) => { const regex = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); pattern = pattern.replace(regex, substitutedVars[varName]); targetUrl = targetUrl.replace(regex, substitutedVars[varName]); }); try { const regexPattern = convertMatchingTypeToRegex(pattern, matchingType); const regex = new RegExp(regexPattern); if (regex.test(testUrl2)) { let newUrl; if (matchingType === "startsWith") { const match = testUrl2.match(regex); if (match && match.length > 1) { newUrl = targetUrl + match[1]; } else { newUrl = targetUrl; } } else if (matchingType === "wildcard" || matchingType === "regex") { newUrl = testUrl2.replace(regex, targetUrl); } else { newUrl = targetUrl; } matchedRules.push({ type: "URL Redirect", groupName: group.name, ruleName: rule.name || `URL Rule ${ruleIndex + 1}`, matchingType: getMatchingTypeDisplayName(matchingType), pattern, regexPattern, action: `Redirect to: ${newUrl}`, originalUrl: testUrl2, newUrl }); } } catch (error) { console.error("Invalid pattern for rule:", rule.name, error); } } else if (rule.type === "modify_headers") { let pattern = rule.urlPattern; Object.keys(substitutedVars).forEach((varName) => { const regex = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); pattern = pattern.replace(regex, substitutedVars[varName]); }); try { const regex = new RegExp(pattern); if (regex.test(testUrl2)) { const headerActions = []; (rule.headers || []).forEach((header) => { let value = header.value; Object.keys(substitutedVars).forEach((varName) => { const varRegex = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); value = value.replace(varRegex, substitutedVars[varName]); }); headerActions.push(`${header.name}: ${value}`); }); matchedRules.push({ type: "Header Modification", groupName: group.name, ruleName: rule.name || `Header Rule ${ruleIndex + 1}`, pattern, action: `Headers: ${headerActions.join(", ")}` }); } } catch (error) { console.error("Invalid regex pattern:", pattern, error); } } }); }); if (matchedRules.length > 0) { let resultHtml = '<div class="test-result match">'; resultHtml += `<strong>✅ ${matchedRules.length} rule(s) matched:</strong><br><br>`; matchedRules.forEach((rule, index) => { resultHtml += `<strong>${index + 1}. ${rule.type}</strong><br>`; resultHtml += `Group: ${rule.groupName}<br>`; resultHtml += `Rule: ${rule.ruleName}<br>`; if (rule.matchingType) { resultHtml += `Type: ${rule.matchingType}<br>`; } resultHtml += `Pattern: <code>${rule.pattern}</code><br>`; if (rule.regexPattern && rule.regexPattern !== rule.pattern) { resultHtml += `Regex: <code>${rule.regexPattern}</code><br>`; } resultHtml += `Action: ${rule.action}<br>`; if (rule.newUrl) { resultHtml += `Result URL: <code>${rule.newUrl}</code><br>`; } if (index < matchedRules.length - 1) { resultHtml += "<br>---<br><br>"; } }); resultHtml += "</div>"; testResultDiv.innerHTML = resultHtml; addLogEntry("success", `URL test: ${matchedRules.length} rules matched for ${testUrl2}`); } else { testResultDiv.innerHTML = `<div class="test-result no-match"><strong>❌ No rules matched</strong><br>The URL doesn't match any enabled rule patterns</div>`; addLogEntry("error", `URL test: No rules matched for ${testUrl2}`); } } catch (error) { testResultDiv.innerHTML = '<div class="test-result no-match"><strong>❌ Invalid URL</strong><br>Please enter a valid URL</div>'; console.error("URL test error:", error); } } function addLogEntry(type, message, ruleName = null) { const logContainer = document.getElementById("liveLogContainer"); const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString(); if (logContainer.children.length === 1 && logContainer.children[0].textContent.includes("No activity yet")) { logContainer.innerHTML = ""; } const logEntry = document.createElement("div"); logEntry.className = `log-entry ${type}`; let logHtml = `<span class="log-timestamp">[${timestamp}]</span>`; logHtml += `<span class="log-url">${message}</span>`; if (ruleName) { logHtml += ` <span class="log-rule">(${ruleName})</span>`; } logEntry.innerHTML = logHtml; logContainer.insertBefore(logEntry, logContainer.firstChild); while (logContainer.children.length > 100) { logContainer.removeChild(logContainer.lastChild); } saveDashboardLogs(); } function clearLogs() { const logContainer = document.getElementById("liveLogContainer"); logContainer.innerHTML = '<div class="log-entry"><span class="log-timestamp">[Logs cleared]</span><span class="log-url">Log history cleared</span></div>'; chrome.storage.local.set({ dashboardLogs: [] }); } async function saveDashboardLogs() { const logContainer = document.getElementById("liveLogContainer"); const logs = Array.from(logContainer.children).slice(0, 50).map((entry) => entry.innerHTML); await chrome.storage.local.set({ dashboardLogs: logs }); } async function loadDashboardLogs() { try { const { dashboardLogs } = await chrome.storage.local.get(["dashboardLogs"]); if (dashboardLogs && dashboardLogs.length > 0) { const logContainer = document.getElementById("liveLogContainer"); logContainer.innerHTML = ""; dashboardLogs.forEach((logHtml) => { const logEntry = document.createElement("div"); logEntry.className = "log-entry"; logEntry.innerHTML = logHtml; logContainer.appendChild(logEntry); }); } } catch (error) { console.error("Error loading dashboard logs:", error); } } window.FreshRouteDashboard = { testUrl, startMonitoring, pauseMonitoring, clearLogs, addLogEntry }; function convertWildcardToRegex(wildcardPattern) { let regexPattern = wildcardPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&"); regexPattern = regexPattern.replace(/\*/g, "([^/]*)"); if (regexPattern.includes("/") && regexPattern.match(/\([^/]*\)/g)) { regexPattern = regexPattern.replace(/\/\([^/]*\)$/g, "/(.*)"); } return regexPattern; } function convertMatchingTypeToRegex(sourceUrl, matchingType) { switch (matchingType) { case "contains": return `.*${escapeRegexChars(sourceUrl)}.*`; case "equals": return `^${escapeRegexChars(sourceUrl)}$`; case "startsWith": return `^${escapeRegexChars(sourceUrl)}.*`; case "endsWith": return `.*${escapeRegexChars(sourceUrl)}$`; case "wildcard": return convertWildcardToRegex(sourceUrl); case "regex": default: return sourceUrl; } } function escapeRegexChars(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function getMatchingTypeDisplayName(matchingType) { switch (matchingType) { case "contains": return "Contains"; case "equals": return "Equals"; case "startsWith": return "Starts With"; case "endsWith": return "Ends With"; case "wildcard": return "Wildcard"; case "regex": return "Regex"; default: return "Unknown"; } } function analyzeUrlAgainstRules(testUrl2, showOnlyMatching = false) { const results = { url: testUrl2, totalRules: 0, matchingRules: 0, groups: [] }; const substitutedVars = {}; environmentVariables.forEach((envVar) => { substitutedVars[envVar.name] = envVar.value; }); groups.forEach((group, groupIndex) => { const groupResult = { name: group.name, enabled: group.enabled !== false, rules: [] }; if (!groupResult.enabled) { groupResult.disabledReason = "Group is disabled"; } (group.rules || []).forEach((rule, ruleIndex) => { results.totalRules++; const ruleResult = analyzeRuleMatch(rule, testUrl2, substitutedVars, groupResult.enabled); groupResult.rules.push(ruleResult); if (ruleResult.matches) { results.matchingRules++; } }); if (groupResult.rules.length > 0) { if (!showOnlyMatching || groupResult.rules.some((r) => r.matches)) { results.groups.push(groupResult); } } }); return results; } function analyzeRuleMatch(rule, testUrl2, substitutedVars, groupEnabled) { const result = { name: rule.name || "Unnamed Rule", type: rule.type, enabled: rule.enabled, groupEnabled, matches: false, steps: [], finalResult: null, error: null }; if (!groupEnabled) { result.steps.push({ type: "failure", title: "❌ Group Disabled", content: "This rule's group is disabled, so the rule will not be evaluated.", details: null }); return result; } if (!rule.enabled) { result.steps.push({ type: "failure", title: "❌ Rule Disabled", content: "This individual rule is disabled, so it will not be evaluated.", details: null }); return result; } result.steps.push({ type: "success", title: "✅ Rule Status", content: "Rule is enabled and ready for evaluation.", details: null }); try { if (rule.type === "url_rewrite") { return analyzeUrlRewriteRule(rule, testUrl2, substitutedVars, result); } else if (rule.type === "modify_headers") { return analyzeHeaderRule(rule, testUrl2, substitutedVars, result); } else { result.error = `Unknown rule type: ${rule.type}`; result.steps.push({ type: "failure", title: "❌ Unknown Rule Type", content: `Rule type "${rule.type}" is not recognized.`, details: null }); } } catch (error) { result.error = error.message; result.steps.push({ type: "failure", title: "❌ Analysis Error", content: `Error analyzing rule: ${error.message}`, details: null }); } return result; } function analyzeUrlRewriteRule(rule, testUrl2, substitutedVars, result) { const originalPattern = rule.sourceUrl; const originalTarget = rule.targetUrl; const matchingType = rule.matchingType || "regex"; let substitutedPattern = originalPattern; let substitutedTarget = originalTarget; const variableReplacements = []; Object.keys(substitutedVars).forEach((varName) => { const regex2 = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); const patternMatches = originalPattern.match(regex2); const targetMatches = originalTarget.match(regex2); if (patternMatches || targetMatches) { variableReplacements.push({ variable: varName, value: substitutedVars[varName], patternCount: patternMatches ? patternMatches.length : 0, targetCount: targetMatches ? targetMatches.length : 0 }); substitutedPattern = substitutedPattern.replace(regex2, substitutedVars[varName]); substitutedTarget = substitutedTarget.replace(regex2, substitutedVars[varName]); } }); if (variableReplacements.length > 0) { result.steps.push({ type: "info", title: "🔄 Variable Substitution", content: "Environment variables have been substituted in the pattern.", details: { type: "variables", originalPattern, substitutedPattern, originalTarget, substitutedTarget, replacements: variableReplacements } }); } else { result.steps.push({ type: "info", title: "🔄 Variable Substitution", content: "No environment variables found in this rule.", details: null }); } result.steps.push({ type: "info", title: "🎯 Matching Type", content: `Using ${getMatchingTypeDisplayName(matchingType)} matching type.`, details: { type: "matching-type", matchingType, originalPattern: substitutedPattern } }); let regexPattern; try { regexPattern = convertMatchingTypeToRegex(substitutedPattern, matchingType); if (regexPattern !== substitutedPattern) { result.steps.push({ type: "info", title: "🔄 Pattern Conversion", content: `${getMatchingTypeDisplayName(matchingType)} pattern converted to regex.`, details: { type: "pattern-conversion", originalPattern: substitutedPattern, regexPattern, matchingType } }); } } catch (error) { result.steps.push({ type: "failure", title: "❌ Pattern Conversion Error", content: `Error converting ${matchingType} pattern: ${error.message}`, details: { type: "error", pattern: substitutedPattern, error: error.message } }); result.error = `Pattern conversion error: ${error.message}`; return result; } let regex; try { regex = new RegExp(regexPattern); result.steps.push({ type: "success", title: "✅ Pattern Validation", content: "Final regex pattern compiled successfully.", details: { type: "pattern", pattern: regexPattern } }); } catch (error) { result.steps.push({ type: "failure", title: "❌ Invalid Pattern", content: `Final regex pattern is invalid: ${error.message}`, details: { type: "error", pattern: regexPattern, error: error.message } }); result.error = `Invalid regex: ${error.message}`; return result; } const match = testUrl2.match(regex); if (match) { result.matches = true; const matchDetails = { type: "match", fullMatch: match[0], captureGroups: match.slice(1), urlBreakdown: createUrlBreakdown(testUrl2, match) }; result.steps.push({ type: "success", title: "✅ Pattern Match", content: "URL matches the regex pattern!", details: matchDetails }); try { let targetUrl = substitutedTarget; match.forEach((group, index) => { if (index > 0) { const placeholder = `$${index}`; targetUrl = targetUrl.replace(new RegExp("\\" + placeholder, "g"), group || ""); } }); result.finalResult = { type: "redirect", originalUrl: testUrl2, targetUrl }; result.steps.push({ type: "success", title: "🎯 Target URL Generation", content: `Generated target URL: ${targetUrl}`, details: { type: "target", originalTarget: substitutedTarget, finalTarget: targetUrl, captureGroups: match.slice(1) } }); } catch (error) { result.steps.push({ type: "failure", title: "❌ Target Generation Error", content: `Error generating target URL: ${error.message}`, details: null }); } } else { result.steps.push({ type: "failure", title: "❌ No Pattern Match", content: "URL does not match the regex pattern.", details: { type: "no-match", pattern: substitutedPattern, url: testUrl2, reason: analyzeMatchFailure(testUrl2, substitutedPattern) } }); } return result; } function analyzeHeaderRule(rule, testUrl2, substitutedVars, result) { const originalPattern = rule.urlPattern; let substitutedPattern = originalPattern; const variableReplacements = []; Object.keys(substitutedVars).forEach((varName) => { const regex2 = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); const matches = originalPattern.match(regex2); if (matches) { variableReplacements.push({ variable: varName, value: substitutedVars[varName], count: matches.length }); substitutedPattern = substitutedPattern.replace(regex2, substitutedVars[varName]); } }); if (variableReplacements.length > 0) { result.steps.push({ type: "info", title: "🔄 Variable Substitution", content: "Environment variables have been substituted in the URL pattern.", details: { type: "variables", originalPattern, substitutedPattern, replacements: variableReplacements } }); } else { result.steps.push({ type: "info", title: "🔄 Variable Substitution", content: "No environment variables found in this rule.", details: null }); } let regex; try { regex = new RegExp(substitutedPattern); result.steps.push({ type: "success", title: "✅ Pattern Validation", content: "URL pattern compiled successfully.", details: { type: "pattern", pattern: substitutedPattern } }); } catch (error) { result.steps.push({ type: "failure", title: "❌ Invalid Pattern", content: `URL pattern is invalid: ${error.message}`, details: { type: "error", pattern: substitutedPattern, error: error.message } }); result.error = `Invalid regex: ${error.message}`; return result; } const match = regex.test(testUrl2); if (match) { result.matches = true; result.steps.push({ type: "success", title: "✅ Pattern Match", content: "URL matches the pattern! Headers will be modified.", details: { type: "match", pattern: substitutedPattern } }); const processedHeaders = []; rule.headers.forEach((header) => { let headerValue = header.value; Object.keys(substitutedVars).forEach((varName) => { const varRegex = new RegExp(`\\{\\{${varName}\\}\\}`, "g"); headerValue = headerValue.replace(varRegex, substitutedVars[varName]); }); processedHeaders.push({ name: header.name, operation: header.operation, target: header.target, originalValue: header.value, processedValue: headerValue }); }); result.finalResult = { type: "headers", headers: processedHeaders }; result.steps.push({ type: "success", title: "📝 Header Processing", content: `${processedHeaders.length} header(s) will be modified.`, details: { type: "headers", headers: processedHeaders } }); } else { result.steps.push({ type: "failure", title: "❌ No Pattern Match", content: "URL does not match the pattern.", details: { type: "no-match", pattern: substitutedPattern, url: testUrl2, reason: analyzeMatchFailure(testUrl2, substitutedPattern) } }); } return result; } function createUrlBreakdown(url, match, pattern) { const fullMatch = match[0]; const beforeMatch = url.substring(0, url.indexOf(fullMatch)); const afterMatch = url.substring(url.indexOf(fullMatch) + fullMatch.length); return { before: beforeMatch, matched: fullMatch, after: afterMatch, captureGroups: match.slice(1) }; } function analyzeMatchFailure(url, pattern) { var _a, _b; const reasons = []; if (pattern.includes("https://") && url.startsWith("http://")) { reasons.push("Pattern expects HTTPS but URL uses HTTP"); } if (pattern.includes("http://") && url.startsWith("https://")) { reasons.push("Pattern expects HTTP but URL uses HTTPS"); } const patternDomain = (_a = pattern.match(/\/\/([^\/]+)/)) == null ? void 0 : _a[1]; const urlDomain = (_b = url.match(/\/\/([^\/]+)/)) == null ? void 0 : _b[1]; if (patternDomain && urlDomain && !patternDomain.includes(".*") && patternDomain !== urlDomain) { reasons.push(`Domain mismatch: pattern expects "${patternDomain}" but URL has "${urlDomain}"`); } if (!pattern.startsWith("^") && !pattern.endsWith("$")) { reasons.push("Pattern is not anchored - consider adding ^ at start and $ at end"); } if (pattern.includes("*") && !pattern.includes("(.*") && !pattern.includes("([^/]*)")) { reasons.push("Pattern contains unescaped asterisks - use wildcard matching type instead of regex"); } if (reasons.length === 0) { reasons.push("Pattern structure does not match URL format"); } return reasons; } function renderDebugResults(results, testUrl2) { const container = document.getElementById("debuggerResults"); if (results.groups.length === 0) { container.innerHTML = ` <div class="debugger-placeholder"> <div class="placeholder-icon">🔍</div> <h3>No Rules to Debug</h3> <p>No active rules found or all rules filtered out.</p> <p>Try unchecking "Show only matching rules" to see all rules.</p> </div> `; return; } let html = ` <div class="debug-summary"> <h3>🎯 Debug Analysis for: <code>${escapeHtml(testUrl2)}</code></h3> <div class="debug-stats"> <span class="stat">📊 ${results.totalRules} total rules</span> <span class="stat ${results.matchingRules > 0 ? "success" : "failure"}"> ${results.matchingRules > 0 ? "✅" : "❌"} ${results.matchingRules} matching </span> </div> </div> `; results.groups.forEach((group) => { const hasMatches = group.rules.some((rule) => rule.matches); html += ` <div class="rule-debug-card ${hasMatches ? "matching" : "non-matching"}"> <div class="rule-debug-header" data-group="${group.name}"> <div class="rule-debug-title"> <span class="rule-debug-expand">▼</span> <div class="rule-info"> <div class="rule-name">📁 ${escapeHtml(group.name)}</div> <div class="group-summary">${group.rules.length} rules in group</div> </div> <div class="match-status ${hasMatches ? "success" : "failure"}"> ${hasMatches ? "✅ Has matches" : "❌ No matches"} </div> </div> </div> <div class="rule-debug-content expanded"> ${group.rules.map((rule) => renderRuleDebug(rule)).join("")} </div> </div> `; }); container.innerHTML = html; container.querySelectorAll(".rule-debug-header").forEach((header) => { header.addEventListener("click", toggleRuleDebugCard); }); } function renderRuleDebug(rule) { const typeClass = rule.type.replace("_", "-"); const statusClass = rule.matches ? "success" : "failure"; let html = ` <div class="rule-debug-item"> <div class="rule-debug-summary"> <div class="rule-info"> <div class="rule-name">${escapeHtml(rule.name)}</div> <span class="rule-type-badge ${typeClass}">${rule.type.replace("_", " ")}</span> </div> <div class="match-status ${statusClass}"> ${rule.matches ? "✅ Match" : "❌ No match"} </div> </div> <div class="rule-steps"> ${rule.steps.map((step) => renderDebugStep(step)).join("")} </div> ${rule.finalResult ? renderFinalResult(rule.finalResult) : ""} </div> `; return html; } function renderDebugStep(step) { let detailsHtml = ""; if (step.details) { switch (step.details.type) { case "variables": detailsHtml = renderVariableDetails(step.details); break; case "match": detailsHtml = renderMatchDetails(step.details); break; case "no-match": detailsHtml = renderNoMatchDetails(step.details); break; case "target": detailsHtml = renderTargetDetails(step.details); break; case "headers": detailsHtml = renderHeaderDetails(step.details); break; case "pattern": detailsHtml = `<div class="step-content"><code>${escapeHtml(step.details.pattern)}</code></div>`; break; case "matching-type": detailsHtml = renderMatchingTypeDetails(step.details); break; case "pattern-conversion": detailsHtml = renderPatternConversionDetails(step.details); break; } } return ` <div class="debug-step ${step.type}"> <div class="step-title">${step.title}</div> <div class="step-content">${step.content}</div> ${detailsHtml} </div> `; } function renderVariableDetails(details) { if (!details.replacements || details.replacements.length === 0) { return ""; } let html = '<div class="variable-substitutions">'; html += "<strong>Variable Substitutions:</strong><br>"; details.replacements.forEach((replacement) => { html += ` <div class="variable-sub"> <span class="var-name">{{${replacement.variable}}}</span> → <span class="var-value">"${escapeHtml(replacement.value)}"</span> ${replacement.patternCount ? `(${replacement.patternCount} in pattern)` : ""} ${replacement.targetCount ? `(${replacement.targetCount} in target)` : ""} </div> `; }); html += "</div>"; if (details.originalPattern !== details.substitutedPattern) { html += ` <div class="pattern-comparison"> <div class="pattern-before"> <strong>Before:</strong><br> <code>${escapeHtml(details.originalPattern)}</code> </div> <div class="pattern-after"> <strong>After:</strong><br> <code>${escapeHtml(details.substitutedPattern)}</code> </div> </div> `; } return html; } function renderMatchDetails(details) { let html = '<div class="regex-match-visual">'; if (details.urlBreakdown) { html += "<strong>URL Breakdown:</strong><br>"; html += '<div class="url-parts">'; if (details.urlBreakdown.before) { html += `<span class="url-part unmatched">${escapeHtml(details.urlBreakdown.before)}</span>`; } html += `<span class="url-part matched">${escapeHtml(details.urlBreakdown.matched)}</span>`; if (details.urlBreakdown.after) { html += `<span class="url-part unmatched">${escapeHtml(details.urlBreakdown.after)}</span>`; } html += "</div>"; } if (details.captureGroups && details.captureGroups.length > 0) { html += '<div class="capture-groups">'; html += "<strong>Capture Groups:</strong><br>"; details.captureGroups.forEach((group, index) => { html += `<span class="capture-group">$${index + 1}: ${escapeHtml(group || "(empty)")}</span>`; }); html += "</div>"; } html += "</div>"; return html; } function renderNoMatchDetails(details) { let html = '<div class="failure-reason">'; html += "<strong>Why it didn't match:</strong><br>"; html += `<code>Pattern: ${escapeHtml(details.pattern)}</code><br>`; html += `<code>URL: ${escapeHtml(details.url)}</code><br><br>`; if (details.reason && details.reason.length > 0) { html += "<strong>Possible issues:</strong><ul>"; details.reason.forEach((reason) => { html += `<li>${escapeHtml(reason)}</li>`; }); html += "</ul>"; } html += "</div>"; return html; } function renderTargetDetails(details) { let html = '<div class="target-preview">'; html += "<strong>Target URL Generation:</strong><br>"; html += `<code>Template: ${escapeHtml(details.originalTarget)}</code><br>`; html += `<code>Result: ${escapeHtml(details.finalTarget)}</code><br>`; if (details.captureGroups && details.captureGroups.length > 0) { html += "<br><strong>Substitutions:</strong><br>"; details.captureGroups.forEach((group, index) => { html += `<code>$${index + 1} = "${escapeHtml(group || "(empty)")}"</code><br>`; }); } html += "</div>"; return html; } function renderHeaderDetails(details) { let html = '<div class="target-preview">'; html += "<strong>Header Modifications:</strong><br>"; details.headers.forEach((header) => { html += ` <div style="margin: 5px 0; padding: 5px; background: rgba(255,255,255,0.7); border-radius: 4px;"> <strong>${escapeHtml(header.name)}</strong> (${header.operation} ${header.target})<br> ${header.originalValue !== header.processedValue ? `<code>Original: ${escapeHtml(header.originalValue)}</code><br>` : ""} <code>Value: ${escapeHtml(header.processedValue)}</code> </div> `; }); html += "</div>"; return html; } function renderFinalResult(result) { if (result.type === "redirect") { return ` <div class="target-preview"> <strong>🎯 Final Result:</strong><br> <strong>Redirect:</strong> <code>${escapeHtml(result.originalUrl)}</code><br> <strong>To:</strong> <code>${escapeHtml(result.targetUrl)}</code> </div> `; } else if (result.type === "headers") { return ` <div class="target-preview"> <strong>🎯 Final Result:</strong><br> <strong>Headers modified for:</strong> <code>${escapeHtml(result.url || "matching URLs")}</code> </div> `; } return ""; } function toggleRuleDebugCard(event) { const header = event.currentTarget; const content = header.nextElementSibling; const expandIcon = header.querySelector(".rule-debug-expand"); if (content.classList.contains("expanded")) { content.classList.remove("expanded"); expandIcon.textContent = "▶"; } else { content.classList.add("expanded"); expandIcon.textContent = "▼"; } } function renderMatchingTypeDetails(details) { let html = '<div class="matching-type-details">'; html += "<strong>Matching Type Details:</strong><br>"; html += `<code>Matching Type: ${escapeHtml(details.matchingType)}</code><br>`; html += `<code>Original Pattern: ${escapeHtml(details.originalPattern)}</code><br>`; html += "</div>"; return html; } function renderPatternConversionDetails(details) { let html = '<div class="pattern-conversion-details">'; html += "<strong>Pattern Conversion Details:</strong><br>"; html += `<code>Original Pattern: ${escapeHtml(details.originalPattern)}</code><br>`; html += `<code>Regex Pattern: ${escapeHtml(details.regexPattern)}</code><br>`; html += `<code>Matching Type: ${escapeHtml(details.matchingType)}</code><br>`; html += "</div>"; return html; }