freshroute-server
Version:
Local development server for FreshRoute extension with API mocking and extended functionality
1,002 lines (963 loc) • 38.6 kB
JavaScript
;
class NetworkRecorder {
constructor() {
this.isRecording = false;
this.capturedRequests = [];
this.selectedRequests = /* @__PURE__ */ new Set();
this.recordingStartTime = null;
this.recordingTimer = null;
this.groups = [];
this.initializeElements();
this.bindEvents();
this.loadGroups();
}
initializeElements() {
this.startBtn = document.getElementById("startRecording");
this.stopBtn = document.getElementById("stopRecording");
this.clearBtn = document.getElementById("clearRecording");
this.closeBtn = document.getElementById("closeWindow");
this.recorderStatus = document.getElementById("recorderStatus");
this.statusText = document.getElementById("statusText");
this.recordingDuration = document.getElementById("recordingDuration");
this.requestCount = document.getElementById("requestCount");
this.methodFilters = document.querySelectorAll('.method-filters input[type="checkbox"]');
this.statusFilters = document.querySelectorAll('.status-filters input[type="checkbox"]');
this.typeFilters = document.querySelectorAll('.type-filters input[type="checkbox"]');
this.urlFilter = document.getElementById("urlFilter");
this.requestsList = document.getElementById("requestsList");
this.emptyState = document.getElementById("emptyState");
this.selectAllBtn = document.getElementById("selectAll");
this.selectNoneBtn = document.getElementById("selectNone");
this.invertSelectionBtn = document.getElementById("invertSelection");
this.convertSection = document.getElementById("convertSection");
this.targetGroup = document.getElementById("targetGroup");
this.newGroupName = document.getElementById("newGroupName");
this.includeHeaders = document.getElementById("includeHeaders");
this.autoGenerateNames = document.getElementById("autoGenerateNames");
this.convertToMocksBtn = document.getElementById("convertToMocks");
this.selectedCount = document.getElementById("selectedCount");
this.requestModal = document.getElementById("requestModal");
this.modalBody = document.getElementById("modalBody");
this.closeModalBtn = document.getElementById("closeModal");
}
bindEvents() {
this.startBtn.addEventListener("click", () => this.startRecording());
this.stopBtn.addEventListener("click", () => this.stopRecording());
this.clearBtn.addEventListener("click", () => this.clearRecording());
this.closeBtn.addEventListener("click", () => {
window.location.href = chrome.runtime.getURL("options.html");
});
const backBtn = document.getElementById("backToOptions");
if (backBtn) {
backBtn.addEventListener("click", () => {
window.location.href = chrome.runtime.getURL("options.html");
});
}
this.selectAllBtn.addEventListener("click", () => this.selectAll());
this.selectNoneBtn.addEventListener("click", () => this.selectNone());
this.invertSelectionBtn.addEventListener("click", () => this.invertSelection());
this.methodFilters.forEach((filter) => {
filter.addEventListener("change", () => this.applyFilters());
});
this.statusFilters.forEach((filter) => {
filter.addEventListener("change", () => this.applyFilters());
});
this.typeFilters.forEach((filter) => {
filter.addEventListener("change", () => this.applyFilters());
});
this.urlFilter.addEventListener("input", () => this.applyFilters());
this.targetGroup.addEventListener("change", () => {
const isNewGroup = this.targetGroup.value === "";
this.newGroupName.style.display = isNewGroup ? "block" : "none";
});
this.convertToMocksBtn.addEventListener("click", () => this.convertToMocks());
this.closeModalBtn.addEventListener("click", () => this.closeModal());
this.requestModal.addEventListener("click", (e) => {
if (e.target === this.requestModal) this.closeModal();
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "networkRequest" && this.isRecording) {
this.addCapturedRequest(message.data);
} else if (message.type === "networkResponse" && this.isRecording) {
this.updateRequestWithResponse(message.data);
} else if (message.type === "networkResponseBodyUpdate" && this.isRecording) {
this.updateRequestWithResponseBody(message.data);
} else if (message.type === "networkRequestUpdated" && this.isRecording) {
console.log("📥 Network recorder received networkRequestUpdated message for:", message.request.url);
this.updateRequestWithUpdatedData(message.request);
} else if (message.type === "networkRequestCaptured" && this.isRecording) {
this.addCapturedRequest(message.request);
} else if (message.type === "debuggerDetached" && this.isRecording) {
this.handleDebuggerDetached(message.reason);
} else if (message.type === "recordingTabSwitched" && this.isRecording) {
this.handleTabSwitched(message.tabId, message.url);
}
});
}
async loadGroups() {
try {
const result = await chrome.storage.local.get(["groups"]);
this.groups = result.groups || [];
this.populateGroupSelect();
} catch (error) {
console.error("Error loading groups:", error);
}
}
populateGroupSelect() {
while (this.targetGroup.children.length > 1) {
this.targetGroup.removeChild(this.targetGroup.lastChild);
}
this.groups.forEach((group) => {
const option = document.createElement("option");
option.value = group.id;
option.textContent = group.name;
this.targetGroup.appendChild(option);
});
}
startRecording() {
this.isRecording = true;
this.recordingStartTime = Date.now();
this.startBtn.style.display = "none";
this.stopBtn.style.display = "inline-flex";
this.clearBtn.style.display = "inline-flex";
this.recorderStatus.textContent = "Recording";
this.recorderStatus.className = "recorder-status recording";
this.statusText.textContent = "Recording network requests...";
this.recordingTimer = setInterval(() => {
this.updateRecordingDuration();
}, 1e3);
chrome.runtime.sendMessage({
type: "startNetworkRecording"
});
this.updateRecordingDuration();
}
stopRecording() {
this.isRecording = false;
this.startBtn.style.display = "inline-flex";
this.stopBtn.style.display = "none";
this.recorderStatus.textContent = "Stopped";
this.recorderStatus.className = "recorder-status stopped";
this.statusText.textContent = "Recording stopped";
if (this.recordingTimer) {
clearInterval(this.recordingTimer);
this.recordingTimer = null;
}
chrome.runtime.sendMessage({
type: "stopNetworkRecording"
});
if (this.capturedRequests.length > 0) {
this.convertSection.style.display = "block";
}
}
clearRecording() {
this.capturedRequests = [];
this.selectedRequests.clear();
this.renderRequestsList();
this.updateSelectedCount();
this.convertSection.style.display = "none";
if (!this.isRecording) {
this.recorderStatus.textContent = "Ready";
this.recorderStatus.className = "recorder-status ready";
this.statusText.textContent = "Ready to record";
this.recordingDuration.textContent = "00:00";
this.clearBtn.style.display = "none";
}
}
updateRecordingDuration() {
if (!this.recordingStartTime) return;
const elapsed = Date.now() - this.recordingStartTime;
const minutes = Math.floor(elapsed / 6e4);
const seconds = Math.floor(elapsed % 6e4 / 1e3);
this.recordingDuration.textContent = `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
}
addCapturedRequest(requestData) {
const request = {
...requestData,
id: requestData.requestId || Date.now() + Math.random(),
timestamp: Date.now(),
status: null,
// Will be updated when response arrives
responseHeaders: null,
statusLine: null
};
this.capturedRequests.push(request);
this.requestCount.textContent = this.capturedRequests.length;
this.renderRequestsList();
}
updateRequestWithResponse(responseData) {
const request = this.capturedRequests.find((r) => r.requestId === responseData.requestId);
if (request) {
request.status = responseData.status;
request.responseHeaders = responseData.responseHeaders;
request.statusLine = responseData.statusLine;
this.renderRequestsList();
}
}
updateRequestWithResponseBody(responseBodyData) {
console.log("🔄 Network recorder received response body update for:", responseBodyData.url);
const request = this.capturedRequests.find((r) => r.url === responseBodyData.url);
if (request) {
console.log("✅ Found matching request, updating response body");
request.responseBody = responseBodyData.responseBody;
this.renderRequestsList();
} else {
console.log("❌ No matching request found for URL:", responseBodyData.url);
console.log("Available requests:", this.capturedRequests.map((r) => r.url));
}
}
updateRequestWithUpdatedData(updatedRequest) {
const request = this.capturedRequests.find(
(r) => r.url === updatedRequest.url && r.method === updatedRequest.method
);
if (request) {
console.log("✅ Found matching request, updating with CDP data:", updatedRequest.url);
if (updatedRequest.status) request.status = updatedRequest.status;
if (updatedRequest.statusLine) request.statusLine = updatedRequest.statusLine;
if (updatedRequest.responseHeaders) request.responseHeaders = updatedRequest.responseHeaders;
if (updatedRequest.requestHeaders) {
request.requestHeaders = updatedRequest.requestHeaders;
console.log("📋 Updated request headers, count:", Object.keys(updatedRequest.requestHeaders).length);
}
if (updatedRequest.requestBody) {
request.requestBody = updatedRequest.requestBody;
console.log("📤 Updated request body, type:", typeof updatedRequest.requestBody);
}
if (updatedRequest.responseBody) {
request.responseBody = updatedRequest.responseBody;
console.log("📥 Updated response body, length:", updatedRequest.responseBody.length);
}
this.renderRequestsList();
} else {
console.log("❌ No matching request found for CDP update:", updatedRequest.url);
console.log("Available requests:", this.capturedRequests.map((r) => `${r.method} ${r.url}`));
}
}
handleDebuggerDetached(reason) {
console.log("🔌 Debugger detached:", reason);
if (reason === "User cancelled debugging notification") {
const notification = document.createElement("div");
notification.className = "notification warning";
notification.innerHTML = `
<span>⚠️ Debugger was detached - response bodies will not be captured</span>
<button onclick="this.parentElement.remove()">×</button>
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 5e3);
}
}
handleTabSwitched(tabId, url) {
console.log("🔄 Recording switched to tab:", tabId, url);
const notification = document.createElement("div");
notification.className = "notification info";
notification.innerHTML = `
<span>📍 Recording switched to: ${url}</span>
<button onclick="this.parentElement.remove()">×</button>
`;
document.body.appendChild(notification);
setTimeout(() => {
if (notification.parentElement) {
notification.remove();
}
}, 3e3);
}
renderRequestsList() {
const filteredRequests = this.getFilteredRequests();
if (filteredRequests.length === 0) {
this.requestsList.innerHTML = `
<div class="empty-state">
<div class="empty-icon">📡</div>
<div class="empty-title">${this.capturedRequests.length === 0 ? "No requests captured yet" : "No requests match filters"}</div>
<div class="empty-subtitle">${this.capturedRequests.length === 0 ? 'Click "Start Recording" to begin capturing network requests' : "Try adjusting your filters"}</div>
</div>
`;
return;
}
this.requestsList.innerHTML = filteredRequests.map(
(request) => this.createRequestItemHTML(request)
).join("");
this.bindRequestItemEvents();
}
createRequestItemHTML(request) {
const isSelected = this.selectedRequests.has(request.id);
const statusClass = request.status ? this.getStatusClass(request.status) : "status-pending";
const methodClass = `method-${request.method}`;
const time = new Date(request.timestamp).toLocaleTimeString();
const statusText = request.status || "Pending";
let urlObj;
try {
urlObj = new URL(request.url);
} catch {
urlObj = { hostname: "unknown", pathname: request.url, search: "" };
}
const displayUrl = request.url.length > 80 ? request.url.substring(0, 80) + "..." : request.url;
const hasResponseData = request.responseBody || request.responseHeaders;
const hasRequestData = request.requestBody || request.requestHeaders;
return `
<div class="request-item ${isSelected ? "selected" : ""}" data-request-id="${request.id}" title="Double-click to view details">
<input type="checkbox" class="request-checkbox" ${isSelected ? "checked" : ""}>
<div class="request-details">
<div class="request-first-line">
<span class="request-method ${methodClass}">${request.method}</span>
<span class="request-url" title="${request.url}">${displayUrl}</span>
<div class="request-indicators">
${hasRequestData ? '<span class="data-indicator request-data" title="Has request data">📤</span>' : ""}
${hasResponseData ? '<span class="data-indicator response-data" title="Has response data">📥</span>' : ""}
<span class="request-status ${statusClass}">${statusText}</span>
</div>
</div>
<div class="request-second-line">
<div class="request-meta">
<span class="request-domain" title="Domain">${urlObj.hostname}</span>
<span class="request-type type-${this.getTypeClass(request.type)}">${this.getTypeDisplayName(request.type)}</span>
</div>
<div class="request-actions">
<span class="request-time">${time}</span>
<span class="view-details-hint">👁️ Double-click for details</span>
</div>
</div>
</div>
</div>
`;
}
bindRequestItemEvents() {
const requestItems = this.requestsList.querySelectorAll(".request-item");
requestItems.forEach((item) => {
const checkbox = item.querySelector(".request-checkbox");
const requestId = item.dataset.requestId;
checkbox.addEventListener("change", (e) => {
e.stopPropagation();
this.toggleRequestSelection(requestId, checkbox.checked);
});
item.addEventListener("click", (e) => {
if (e.target === checkbox) return;
const isSelected = this.selectedRequests.has(requestId);
this.toggleRequestSelection(requestId, !isSelected);
checkbox.checked = !isSelected;
});
item.addEventListener("dblclick", () => {
this.showRequestDetails(requestId);
});
});
}
toggleRequestSelection(requestId, selected) {
if (selected) {
this.selectedRequests.add(requestId);
} else {
this.selectedRequests.delete(requestId);
}
this.updateRequestItemSelection(requestId, selected);
this.updateSelectedCount();
}
updateRequestItemSelection(requestId, selected) {
const item = this.requestsList.querySelector(`[data-request-id="${requestId}"]`);
if (item) {
item.classList.toggle("selected", selected);
}
}
updateSelectedCount() {
const count = this.selectedRequests.size;
this.selectedCount.textContent = `${count} request${count !== 1 ? "s" : ""} selected`;
}
selectAll() {
const filteredRequests = this.getFilteredRequests();
filteredRequests.forEach((request) => {
this.selectedRequests.add(request.id);
});
this.renderRequestsList();
this.updateSelectedCount();
}
selectNone() {
this.selectedRequests.clear();
this.renderRequestsList();
this.updateSelectedCount();
}
invertSelection() {
const filteredRequests = this.getFilteredRequests();
const newSelection = /* @__PURE__ */ new Set();
filteredRequests.forEach((request) => {
if (!this.selectedRequests.has(request.id)) {
newSelection.add(request.id);
}
});
this.selectedRequests = newSelection;
this.renderRequestsList();
this.updateSelectedCount();
}
getFilteredRequests() {
return this.capturedRequests.filter((request) => {
const methodChecked = Array.from(this.methodFilters).some((filter) => filter.checked && filter.value === request.method);
if (!methodChecked) return false;
const statusCategory = request.status ? this.getStatusCategory(request.status) : "2xx";
const statusChecked = Array.from(this.statusFilters).some((filter) => filter.checked && filter.value === statusCategory);
if (!statusChecked) return false;
const requestType = request.type || "other";
const typeChecked = Array.from(this.typeFilters).some((filter) => filter.checked && filter.value === requestType);
if (!typeChecked) return false;
const urlFilterValue = this.urlFilter.value.toLowerCase();
if (urlFilterValue && !request.url.toLowerCase().includes(urlFilterValue)) {
return false;
}
return true;
});
}
getStatusCategory(status) {
if (status >= 200 && status < 300) return "2xx";
if (status >= 300 && status < 400) return "3xx";
if (status >= 400 && status < 500) return "4xx";
if (status >= 500) return "5xx";
return "2xx";
}
getStatusClass(status) {
const category = this.getStatusCategory(status);
return `status-${category}`;
}
getTypeClass(type) {
const typeMap = {
"xmlhttprequest": "xhr",
"fetch": "fetch",
"script": "script",
"stylesheet": "css",
"image": "image",
"font": "font",
"document": "document"
};
return typeMap[type] || "other";
}
getTypeDisplayName(type) {
const typeMap = {
"xmlhttprequest": "XHR",
"fetch": "Fetch",
"script": "JS",
"stylesheet": "CSS",
"image": "IMG",
"font": "Font",
"document": "Doc"
};
return typeMap[type] || (type || "Other").toUpperCase();
}
applyFilters() {
this.renderRequestsList();
}
showRequestDetails(requestId) {
const request = this.capturedRequests.find((r) => r.id == requestId);
if (!request) return;
let urlObj;
try {
urlObj = new URL(request.url);
} catch {
urlObj = { hostname: "unknown", pathname: request.url, search: "" };
}
const timestamp = request.timestamp ? new Date(request.timestamp).toLocaleString() : "Unknown";
const formatHeaders = (headers) => {
if (!headers) return "No headers";
if (Array.isArray(headers)) {
return headers.map((h) => `${h.name}: ${h.value}`).join("\n");
}
if (typeof headers === "object") {
return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
}
return JSON.stringify(headers, null, 2);
};
const formatBody = (body) => {
if (!body) return "No body";
if (typeof body === "string") {
try {
const parsed = JSON.parse(body);
return JSON.stringify(parsed, null, 2);
} catch {
return body;
}
}
if (body && typeof body === "object" && body.formData) {
const formDataEntries = [];
for (const [key, values] of Object.entries(body.formData)) {
values.forEach((value) => {
formDataEntries.push(`${key}=${value}`);
});
}
return formDataEntries.join("\\n");
}
if (body && typeof body === "object" && body.raw) {
const rawParts = body.raw.map((part) => {
if (part.bytes) {
try {
return new TextDecoder().decode(new Uint8Array(part.bytes));
} catch {
return "[Binary data]";
}
}
return part.file || "[File data]";
});
return rawParts.join("\\n");
}
return JSON.stringify(body, null, 2);
};
this.modalBody.innerHTML = `
<div class="request-detail-tabs">
<button class="detail-tab active" data-tab="general">General</button>
<button class="detail-tab" data-tab="headers">Headers</button>
<button class="detail-tab" data-tab="body">Request Body</button>
<button class="detail-tab" data-tab="response">Response</button>
</div>
<div class="detail-tab-content active" id="general-tab">
<div class="request-detail-section">
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">URL:</span>
<span class="detail-value url-value" title="${request.url}">${request.url}</span>
</div>
<div class="detail-item">
<span class="detail-label">Domain:</span>
<span class="detail-value">${urlObj.hostname}</span>
</div>
<div class="detail-item">
<span class="detail-label">Path:</span>
<span class="detail-value">${urlObj.pathname}${urlObj.search}</span>
</div>
<div class="detail-item">
<span class="detail-label">Method:</span>
<span class="detail-value method-badge method-${request.method}">${request.method}</span>
</div>
<div class="detail-item">
<span class="detail-label">Status:</span>
<span class="detail-value status-badge ${this.getStatusClass(request.status)}">${request.status || "Pending"}</span>
</div>
<div class="detail-item">
<span class="detail-label">Type:</span>
<span class="detail-value type-badge type-${this.getTypeClass(request.type)}">${this.getTypeDisplayName(request.type)}</span>
</div>
<div class="detail-item">
<span class="detail-label">Timestamp:</span>
<span class="detail-value">${timestamp}</span>
</div>
<div class="detail-item">
<span class="detail-label">Request ID:</span>
<span class="detail-value">${request.requestId || "N/A"}</span>
</div>
</div>
</div>
</div>
<div class="detail-tab-content" id="headers-tab">
<div class="request-detail-section">
<h4>Request Headers</h4>
<div class="headers-container">
<button class="copy-btn" onclick="copyToClipboard(this)" data-copy-text="${formatHeaders(request.requestHeaders).replace(/"/g, """)}">Copy</button>
<pre class="headers-content">${formatHeaders(request.requestHeaders)}</pre>
</div>
</div>
<div class="request-detail-section">
<h4>Response Headers</h4>
<div class="headers-container">
<button class="copy-btn" onclick="copyToClipboard(this)" data-copy-text="${formatHeaders(request.responseHeaders).replace(/"/g, """)}">Copy</button>
<pre class="headers-content">${formatHeaders(request.responseHeaders)}</pre>
</div>
</div>
</div>
<div class="detail-tab-content" id="body-tab">
<div class="request-detail-section">
<h4>Request Body</h4>
<div class="body-container">
<button class="copy-btn" onclick="copyToClipboard(this)" data-copy-text="${formatBody(request.requestBody).replace(/"/g, """)}">Copy</button>
<pre class="body-content">${formatBody(request.requestBody)}</pre>
</div>
</div>
</div>
<div class="detail-tab-content" id="response-tab">
<div class="request-detail-section">
<h4>Response Body</h4>
<div class="body-container">
<button class="copy-btn" onclick="copyToClipboard(this)" data-copy-text="${formatBody(request.responseBody).replace(/"/g, """)}">Copy</button>
<pre class="body-content">${formatBody(request.responseBody)}</pre>
</div>
</div>
${request.statusLine ? `
<div class="request-detail-section">
<h4>Status Line</h4>
<div class="status-line">${request.statusLine}</div>
</div>
` : ""}
</div>
`;
this.setupModalTabs();
this.requestModal.style.display = "flex";
}
setupModalTabs() {
const tabs = this.modalBody.querySelectorAll(".detail-tab");
const contents = this.modalBody.querySelectorAll(".detail-tab-content");
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
tabs.forEach((t) => t.classList.remove("active"));
contents.forEach((c) => c.classList.remove("active"));
tab.classList.add("active");
const targetTab = tab.dataset.tab;
const targetContent = this.modalBody.querySelector(`#${targetTab}-tab`);
if (targetContent) {
targetContent.classList.add("active");
}
});
});
}
closeModal() {
this.requestModal.style.display = "none";
}
async convertToMocks() {
const selectedRequestIds = Array.from(this.selectedRequests);
if (selectedRequestIds.length === 0) {
alert("Please select at least one request to convert.");
return;
}
const selectedRequests = this.capturedRequests.filter(
(r) => selectedRequestIds.includes(r.id)
);
let targetGroupId = this.targetGroup.value;
let targetGroupName = "";
if (!targetGroupId) {
targetGroupName = this.newGroupName.value.trim();
if (!targetGroupName) {
const now = /* @__PURE__ */ new Date();
const dateStr = now.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit"
});
const domains = [...new Set(selectedRequests.map((req) => {
try {
return new URL(req.url).hostname;
} catch {
return "unknown";
}
}))];
const domainInfo = domains.length === 1 ? domains[0] : `${domains.length} domains`;
targetGroupName = `Network Recording ${dateStr} - ${domainInfo} (${selectedRequestIds.length} requests)`;
}
targetGroupId = "group_" + Date.now();
const newGroup = {
id: targetGroupId,
name: targetGroupName,
rules: []
};
this.groups.push(newGroup);
await chrome.storage.local.set({ groups: this.groups });
this.populateGroupSelect();
} else {
const group = this.groups.find((g) => g.id === targetGroupId);
targetGroupName = group ? group.name : "Unknown Group";
}
const mockRules = selectedRequests.map((request) => this.convertRequestToMockRule(request));
try {
for (const mockRule of mockRules) {
await this.saveMockRuleToGroup(mockRule, targetGroupId);
}
alert(`Successfully converted ${mockRules.length} request(s) to mock rules in group "${targetGroupName}".`);
this.selectNone();
} catch (error) {
console.error("Error converting to mock rules:", error);
alert("Error converting requests to mock rules. Please try again.");
}
}
convertRequestToMockRule(request) {
const autoGenerate = this.autoGenerateNames.checked;
const includeHeaders = this.includeHeaders.checked;
let ruleName = "";
if (autoGenerate) {
const urlPath = new URL(request.url).pathname.split("/").pop() || "api";
ruleName = `${request.method} ${urlPath} (${request.status})`;
} else {
ruleName = `Mock Rule ${Date.now()}`;
}
const mockRule = {
id: Date.now().toString(),
type: "mock_response",
name: ruleName,
enabled: true,
method: request.method,
matchingType: "equals",
// Use equals match for recorded URLs
urlPattern: request.url,
responseType: "json",
// Default to JSON, can be changed later
responseBody: request.responseBody || "",
statusCode: request.status,
delay: 0,
description: `Auto-generated from network recording at ${(/* @__PURE__ */ new Date()).toLocaleString()}`
};
if (includeHeaders && request.responseHeaders) {
mockRule.responseHeaders = request.responseHeaders;
}
return mockRule;
}
async saveMockRuleToGroup(mockRule, groupId) {
const result = await chrome.storage.local.get(["groups"]);
const groups = result.groups || [];
const groupIndex = groups.findIndex((g) => g.id === groupId);
if (groupIndex === -1) {
throw new Error("Group not found");
}
groups[groupIndex].rules.push(mockRule);
await chrome.storage.local.set({ groups });
this.groups = groups;
}
}
document.addEventListener("DOMContentLoaded", () => {
new NetworkRecorder();
});
const additionalCSS = `
/* Modal tab styles */
.request-detail-tabs {
display: flex;
border-bottom: 2px solid #e1e5e9;
margin-bottom: 20px;
gap: 0;
}
.detail-tab {
background: none;
border: none;
padding: 12px 20px;
cursor: pointer;
font-weight: 500;
color: #666;
border-bottom: 3px solid transparent;
transition: all 0.2s ease;
font-size: 14px;
}
.detail-tab:hover {
background: #f8f9fa;
color: #333;
}
.detail-tab.active {
color: #007bff;
border-bottom-color: #007bff;
background: #f8f9fa;
}
/* Tab content */
.detail-tab-content {
display: none;
animation: fadeIn 0.2s ease-in;
}
.detail-tab-content.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* Request detail sections */
.request-detail-section {
margin-bottom: 25px;
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #e9ecef;
}
.request-detail-section h4 {
margin: 0 0 15px 0;
color: #495057;
font-size: 16px;
font-weight: 600;
border-bottom: 2px solid #dee2e6;
padding-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.request-detail-section h4::before {
content: '';
width: 4px;
height: 18px;
background: #007bff;
border-radius: 2px;
}
/* Detail grid */
.detail-grid {
display: grid;
gap: 12px;
}
.detail-item {
display: grid;
grid-template-columns: 140px 1fr;
gap: 12px;
align-items: start;
padding: 8px 0;
border-bottom: 1px solid #e9ecef;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-label {
font-weight: 600;
color: #495057;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.detail-value {
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 13px;
line-height: 1.4;
color: #212529;
background: #fff;
padding: 6px 10px;
border-radius: 4px;
border: 1px solid #dee2e6;
word-break: break-all;
}
.url-value {
font-size: 12px;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
transition: all 0.2s ease;
}
.url-value:hover {
white-space: normal;
word-break: break-all;
background: #e3f2fd;
border-color: #2196f3;
}
/* Status and method badges */
.method-badge, .status-badge, .type-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
border: none;
}
.method-GET { background: #28a745; color: white; }
.method-POST { background: #ffc107; color: #212529; }
.method-PUT { background: #17a2b8; color: white; }
.method-DELETE { background: #dc3545; color: white; }
.method-PATCH { background: #6f42c1; color: white; }
.method-OPTIONS { background: #6c757d; color: white; }
.status-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status-redirect { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
.status-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.status-pending { background: #e2e3e5; color: #383d41; border: 1px solid #d6d8db; }
/* Content containers */
.headers-container, .body-container {
position: relative;
}
.headers-content, .body-content {
background: #ffffff;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
color: #212529;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.1);
}
.headers-content:empty::before, .body-content:empty::before {
content: 'No data available';
color: #6c757d;
font-style: italic;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Copy buttons for content */
.copy-btn {
position: absolute;
top: 8px;
right: 8px;
background: #007bff;
color: white;
border: none;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
}
.headers-container:hover .copy-btn,
.body-container:hover .copy-btn {
opacity: 1;
}
.copy-btn:hover {
background: #0056b3;
}
.copy-btn.copied {
background: #28a745;
}
/* Enhanced scrollbars */
.headers-content::-webkit-scrollbar, .body-content::-webkit-scrollbar {
width: 8px;
}
.headers-content::-webkit-scrollbar-track, .body-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.headers-content::-webkit-scrollbar-thumb, .body-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.headers-content::-webkit-scrollbar-thumb:hover, .body-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Status line styling */
.status-line {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 12px;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 13px;
color: #495057;
font-weight: 500;
}
/* JSON syntax highlighting */
.json-key { color: #d73a49; }
.json-value { color: #032f62; }
.json-string { color: #22863a; }
.json-number { color: #e36209; }
.json-boolean { color: #005cc5; }
.json-null { color: #6f42c1; }
`;
const style = document.createElement("style");
style.textContent = additionalCSS;
document.head.appendChild(style);
window.copyToClipboard = function(button) {
const text = button.getAttribute("data-copy-text");
if (!text) return;
const cleanText = text.replace(/"/g, '"').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
navigator.clipboard.writeText(cleanText).then(() => {
const originalText = button.textContent;
button.textContent = "Copied!";
button.classList.add("copied");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove("copied");
}, 2e3);
}).catch((err) => {
console.error("Failed to copy: ", err);
const textArea = document.createElement("textarea");
textArea.value = cleanText;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
button.textContent = "Copied!";
button.classList.add("copied");
setTimeout(() => {
button.textContent = "Copy";
button.classList.remove("copied");
}, 2e3);
} catch (err2) {
console.error("Fallback copy failed: ", err2);
}
document.body.removeChild(textArea);
});
};