extension/shared/app.js
// Add to state object (line ~8-27)
advancedFilterOpen: false,
filters: {
methods: [], // Replace methodFilter
statusCodes: [], // Replace statusFilter
mockEnabled: null,
blocked: null,
hasNotes: null
},
sortBy: 'timestamp',
sortOrder: 'desc'
initFilterState() helper function
function initFilterState() {
return {
methods: [],
statusCodes: [],
requestBodyContains: '',
responseBodyContains: '',
requestHeader: { name: '', value: '' },
responseHeader: { name: '', value: '' },
timestampRange: { from: null, to: null },
mockEnabled: null,
blocked: null,
hasNotes: null
};
}
filteredInteractions() function (line ~1606-1618)
methodFilter with filters.methods[] arraystatusFilter with filters.statusCodes[] arraymockEnabled, blocked, hasNotes filtersmatchesStatusFilter(status, filterArray) helper
function matchesStatusFilter(status, filters) {
if (!filters || filters.length === 0) return true;
for (const f of filters) {
if (f === '0' && status === 0) return true;
if (f.endsWith('xx')) {
const bucket = Math.floor(status / 100);
if (String(bucket) === f.charAt(0)) return true;
} else if (String(status) === f) {
return true;
}
}
return false;
}
renderAdvancedFilterPanel() function
renderToolbar() function (line ~559-577)
renderFilterChips() function
function renderFilterChips() {
const chips = [];
state.filters.methods.forEach(m => {
chips.push(`<span class="ek-filter-chip" data-action="remove-filter" data-type="method" data-value="${m}">× method:${m}</span>`);
});
// ... repeat for other filter types
return chips.length ? `<div class="ek-filter-chips">${chips.join('')}</div>` : '';
}
else if (action === 'toggle-advanced-filters') {
state.advancedFilterOpen = !state.advancedFilterOpen;
render();
}
else if (action === 'filter-method-multi') {
const method = el.getAttribute('data-method');
const checked = el.checked;
if (checked) {
if (!state.filters.methods.includes(method)) {
state.filters.methods.push(method);
}
} else {
state.filters.methods = state.filters.methods.filter(m => m !== method);
}
softRenderList();
}
else if (action === 'remove-filter') {
const type = el.getAttribute('data-type');
const value = el.getAttribute('data-value');
if (type === 'method') {
state.filters.methods = state.filters.methods.filter(m => m !== value);
}
// ... handle other filter types
softRenderList();
}
else if (action === 'clear-filters') {
state.filters = initFilterState();
state.search = '';
render();
}
extension/shared/style.css
.ek-advanced-filters {
background: var(--bg-secondary);
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.ek-filter-section {
margin-bottom: 16px;
}
.ek-filter-label {
font-size: 12px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 8px;
}
.ek-filter-chip {
display: inline-block;
background: var(--accent);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
margin-right: 6px;
cursor: pointer;
}
.ek-filter-chip:hover {
background: var(--accent-hover);
}
searchBodyContent(body, query) helper
function searchBodyContent(body, query) {
if (!query) return true;
const q = query.toLowerCase();
// Handle JSON bodies
if (typeof body === 'object') {
const str = JSON.stringify(body).toLowerCase();
return str.includes(q);
}
// Handle string bodies
if (typeof body === 'string') {
return body.toLowerCase().includes(q);
}
return false;
}
searchHeaders(headers, nameQuery, valueQuery) helper
function searchHeaders(headers, nameQuery, valueQuery) {
if (!nameQuery && !valueQuery) return true;
if (!headers || typeof headers !== 'object') return false;
const nq = nameQuery.toLowerCase();
const vq = valueQuery.toLowerCase();
for (const [name, value] of Object.entries(headers)) {
const nameMatch = !nq || name.toLowerCase().includes(nq);
const valueMatch = !vq || String(value).toLowerCase().includes(vq);
if (nameMatch && valueMatch) return true;
}
return false;
}
filteredInteractions() to include body/header search
// Add after method/status filters
// Request body search
if (state.filters.requestBodyContains) {
results = results.filter(i =>
searchBodyContent(i.requestBody, state.filters.requestBodyContains)
);
}
// Response body search
if (state.filters.responseBodyContains) {
results = results.filter(i =>
searchBodyContent(i.responseBody, state.filters.responseBodyContains)
);
}
// Request header search
if (state.filters.requestHeader.name || state.filters.requestHeader.value) {
results = results.filter(i =>
searchHeaders(
i.requestHeaders,
state.filters.requestHeader.name,
state.filters.requestHeader.value
)
);
}
// Response header search
if (state.filters.responseHeader.name || state.filters.responseHeader.value) {
results = results.filter(i =>
searchHeaders(
i.responseHeaders,
state.filters.responseHeader.name,
state.filters.responseHeader.value
)
);
}
<div class="ek-filter-section">
<label class="ek-filter-label">Search Body Content</label>
<input class="ek-input"
placeholder="Request contains..."
value="${escapeHtml(state.filters.requestBodyContains)}"
data-action="filter-request-body"
data-testid="filter-request-body"/>
<input class="ek-input"
placeholder="Response contains..."
value="${escapeHtml(state.filters.responseBodyContains)}"
data-action="filter-response-body"
data-testid="filter-response-body"/>
</div>
<div class="ek-filter-section">
<label class="ek-filter-label">Search Headers</label>
<div class="ek-row-inline">
<input class="ek-input" placeholder="Request header name"
value="${escapeHtml(state.filters.requestHeader.name)}"
data-action="filter-req-header-name" style="flex:1"/>
<input class="ek-input" placeholder="value"
value="${escapeHtml(state.filters.requestHeader.value)}"
data-action="filter-req-header-value" style="flex:1"/>
</div>
<div class="ek-row-inline">
<input class="ek-input" placeholder="Response header name"
value="${escapeHtml(state.filters.responseHeader.name)}"
data-action="filter-res-header-name" style="flex:1"/>
<input class="ek-input" placeholder="value"
value="${escapeHtml(state.filters.responseHeader.value)}"
data-action="filter-res-header-value" style="flex:1"/>
</div>
</div>
else if (action === 'filter-request-body') {
let t;
el.addEventListener('input', (e) => {
state.filters.requestBodyContains = e.target.value;
clearTimeout(t);
t = setTimeout(() => softRenderList(), 300); // Longer debounce for body search
});
}
// Repeat for other body/header inputs
async function filteredInteractions() {
const hasBodySearch = state.filters.requestBodyContains ||
state.filters.responseBodyContains;
const isLargeDataset = state.interactions.length > 500;
if (hasBodySearch && isLargeDataset) {
showLoadingOverlay('Searching body content...');
}
// Run filtering logic...
hideLoadingOverlay();
return results;
}
sortBy, sortOrdersortInteractions(interactions, sortBy, sortOrder) function
function sortInteractions(interactions, sortBy, sortOrder) {
const sorted = [...interactions];
const comparators = {
timestamp: (a, b) => a.timestamp - b.timestamp,
url: (a, b) => a.url.localeCompare(b.url),
method: (a, b) => a.method.localeCompare(b.method),
status: (a, b) => (a.responseStatus || 0) - (b.responseStatus || 0),
duration: (a, b) => (a.durationMs || 0) - (b.durationMs || 0)
};
sorted.sort(comparators[sortBy] || comparators.timestamp);
if (sortOrder === 'desc') {
sorted.reverse();
}
return sorted;
}
filteredInteractions() to apply sort
function filteredInteractions() {
let results = state.interactions.filter(/* ... filtering logic ... */);
// Apply sort at the end
results = sortInteractions(results, state.sortBy, state.sortOrder);
return results;
}
renderSortableListHeader() function
function renderSortableListHeader() {
const cols = [
{ key: 'method', label: 'Method' },
{ key: 'url', label: 'URL', flex: 2 },
{ key: 'status', label: 'Status' },
{ key: 'duration', label: 'Time' },
{ key: 'timestamp', label: 'Recorded' }
];
return `
<div class="ek-list-header">
${cols.map(col => {
const active = state.sortBy === col.key;
const arrow = !active ? '' : state.sortOrder === 'asc' ? ' ↑' : ' ↓';
return `
<div class="ek-col ${active ? 'active' : ''}"
style="flex:${col.flex || 1}"
data-action="sort-by"
data-column="${col.key}"
data-testid="sort-${col.key}">
${col.label}${arrow}
</div>
`;
}).join('')}
</div>
`;
}
function renderInteractionRow(i) {
return `
<div class="ek-row" data-action="select" data-id="${i.id}">
<div class="ek-col">${i.method}</div>
<div class="ek-col ek-url" style="flex:2">${prettyUrl(i.url).path}</div>
<div class="ek-col">${i.responseStatus || '—'}</div>
<div class="ek-col">${i.durationMs ? i.durationMs + 'ms' : '—'}</div>
<div class="ek-col">${formatTimestamp(i.timestamp)}</div>
</div>
`;
}
else if (action === 'sort-by') {
const column = el.getAttribute('data-column');
if (state.sortBy === column) {
// Toggle order if clicking same column
state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc';
} else {
// New column: default to descending (most common use case)
state.sortBy = column;
state.sortOrder = 'desc';
}
render();
}
.ek-list-header {
display: flex;
gap: 8px;
padding: 8px 12px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
font-size: 11px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
}
.ek-list-header .ek-col {
cursor: pointer;
user-select: none;
}
.ek-list-header .ek-col:hover {
color: var(--text-primary);
}
.ek-list-header .ek-col.active {
color: var(--accent);
}
.ek-row {
display: flex;
gap: 8px;
padding: 8px 12px;
align-items: center;
}
.ek-col {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
DB_VERSION to 2 in extension/shared/store.jsonupgradeneeded handler
req.onupgradeneeded = (event) => {
const db = req.result;
const oldVersion = event.oldVersion;
// V1 → V2 migration
if (oldVersion < 2) {
if (db.objectStoreNames.contains(STORE_INTERACTIONS)) {
const tx = event.target.transaction;
const store = tx.objectStore(STORE_INTERACTIONS);
// Add new indexes
if (!store.indexNames.contains('method')) {
store.createIndex('method', 'method', { unique: false });
}
if (!store.indexNames.contains('responseStatus')) {
store.createIndex('responseStatus', 'responseStatus', { unique: false });
}
if (!store.indexNames.contains('mockEnabled')) {
store.createIndex('mockEnabled', 'mockEnabled', { unique: false });
}
}
}
};
export async function getInteractionsByMethod(method) {
const store = await tx(STORE_INTERACTIONS);
const idx = store.index('method');
return req2promise(idx.getAll(method));
}
export async function getInteractionsByStatus(status) {
const store = await tx(STORE_INTERACTIONS);
const idx = store.index('responseStatus');
return req2promise(idx.getAll(status));
}
matchesStatusFilter() with various inputssearchBodyContent() with JSON and string bodiessearchHeaders() with name/value combinationssortInteractions() for each sort key