function initSearch() {
if (!DOM.searchInput) return;
DOM.searchInput.addEventListener('input', debounce((e) => {
AppState.searchQuery = e.target.value.trim();
filterErrors();
renderResults();
updateSidebar();
}, 200));
if (DOM.searchClear) {
DOM.searchClear.addEventListener('click', () => {
clearFilters();
DOM.searchInput.focus();
});
}
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
DOM.searchInput.focus();
DOM.searchInput.select();
}
if (e.key === 'Escape' && document.activeElement === DOM.searchInput) {
DOM.searchInput.blur();
}
});
}
function initSeverityFilters() {
if (!DOM.severityBar) return;
const SEVERITY_CHARS = {
error: 'E', blocked: 'B', critical: 'C', warning: 'W',
help: 'H', success: 'S', completed: 'K', info: 'I', trace: 'T'
};
AppState.activeSeverities = new Set(SEVERITY_ORDER);
const counts = {};
SEVERITY_ORDER.forEach(sev => counts[sev] = 0);
AppState.errors.forEach(err => {
const sev = (err.severity || 'info').toLowerCase();
if (counts.hasOwnProperty(sev)) counts[sev]++;
});
DOM.severityBar.innerHTML = '<span class="filter-label">SEVERITY</span>';
SEVERITY_ORDER.forEach(severity => {
const count = counts[severity];
if (count === 0) return;
const sevChar = SEVERITY_CHARS[severity] || severity.charAt(0).toUpperCase();
const sevData = AppState.severities?.[sevChar] || {};
const emoji = sevData.emoji || SEVERITY_ICONS[severity] || '';
const name = sevData.name || capitalize(severity);
const btn = createElement('button', `severity-filter-item severity-${severity} active`, {
'data-severity': severity,
'aria-pressed': 'true',
'title': `${name}: ${count} error${count !== 1 ? 's' : ''}`
});
btn.innerHTML = `
<span class="sev-emoji">${emoji}</span>
<span class="sev-char">${sevChar}</span>
<span class="sev-count">${count}</span>
`;
btn.addEventListener('click', () => toggleSeverity(severity, btn));
DOM.severityBar.appendChild(btn);
});
}
function toggleSeverity(severity, btn) {
if (AppState.activeSeverities.has(severity)) {
if (AppState.activeSeverities.size === 1) {
showToast('Cannot Remove', 'At least one severity must be selected', 'warning');
return;
}
AppState.activeSeverities.delete(severity);
btn.classList.remove('active');
btn.setAttribute('aria-pressed', 'false');
} else {
AppState.activeSeverities.add(severity);
btn.classList.add('active');
btn.setAttribute('aria-pressed', 'true');
}
filterErrors();
renderResults();
updateSidebar();
}
function filterErrors() {
const query = AppState.searchQuery.toLowerCase();
console.log('[Search] Query:', query);
let resolvedQuery = query;
if (query && /^[a-z0-9]{5}$/i.test(query)) {
const hashLookup = AppState.hashLookup || {};
const matchedCode = hashLookup[query] || hashLookup[query.toLowerCase()] || hashLookup[query.toUpperCase()];
if (matchedCode) {
console.log('[Search] Hash lookup:', query, '→', matchedCode);
resolvedQuery = matchedCode.toLowerCase();
showToast('Hash Resolved', `${query} → ${matchedCode}`, 'success');
} else {
console.log('[Search] No hash match for:', query);
}
}
AppState.filteredErrors = AppState.errors.filter(error => {
if (!isErrorVisible(error)) return false;
const severity = (error.severity || 'info').toLowerCase();
if (!AppState.activeSeverities.has(severity)) return false;
if (AppState.activeCategory) {
const categoryType = AppState.activeCategoryType || 'components';
if (categoryType === 'components') {
const component = error.component || getCategoryFromCode(error.code);
if (component !== AppState.activeCategory) return false;
} else if (categoryType === 'primaries') {
if (error.primary !== AppState.activeCategory) return false;
} else if (categoryType === 'sequences') {
if (String(error.sequence) !== AppState.activeCategory) return false;
}
}
if (AppState.activeTag) {
const tags = error.tags || [];
if (!tags.includes(AppState.activeTag)) return false;
}
if (resolvedQuery) {
if (resolvedQuery.includes('*') || (resolvedQuery.includes('.') && !resolvedQuery.includes(' '))) {
console.log('[Search] Testing wildcard pattern:', resolvedQuery, 'against', error.code);
if (!matchWildcardPattern(error.code, resolvedQuery, error)) return false;
} else {
const seqNum = error.sequence;
const seqMeta = AppState.sequences?.[seqNum];
const seqName = seqMeta?.name || '';
const searchFields = [
error.code,
error.hash, error.message,
error.description,
error.component, error.primary, seqName, ...(error.tags || []),
...(error.hints || []).map(h => typeof h === 'string' ? h : h.message || '')
].filter(Boolean).join(' ').toLowerCase();
console.log('[Search] Testing text search:', resolvedQuery, 'in fields');
if (!searchFields.includes(resolvedQuery)) return false;
}
}
return true;
});
console.log('[Search] Filtered to', AppState.filteredErrors.length, 'errors');
updateViewingCounter();
}
function matchWildcardPattern(code, pattern, error) {
if (!code) return false;
console.log('[Wildcard] Matching code:', code, 'against pattern:', pattern);
const codeParts = code.split('.');
const patternParts = pattern.split('.');
if (codeParts.length !== patternParts.length && !pattern.includes('*')) {
console.log('[Wildcard] Length mismatch, using substring match');
return code.toLowerCase().includes(pattern.toLowerCase());
}
for (let i = 0; i < patternParts.length; i++) {
const pp = patternParts[i];
const cp = codeParts[i] || '';
if (pp === '*') continue;
if (i === 3) {
const seqNum = parseInt(cp, 10);
const seqMeta = AppState.sequences?.[seqNum];
const seqName = seqMeta?.name || '';
if (/^\d+$/.test(pp)) {
const patternNum = parseInt(pp, 10);
console.log('[Wildcard] Comparing sequence numbers:', patternNum, 'vs', seqNum);
if (patternNum !== seqNum) {
console.log('[Wildcard] Sequence number mismatch');
return false;
}
} else {
console.log('[Wildcard] Comparing sequence name:', pp, 'vs', seqName);
if (pp.toLowerCase() !== seqName.toLowerCase()) {
console.log('[Wildcard] Sequence name mismatch');
return false;
}
}
} else {
let matches = pp.toLowerCase() === cp.toLowerCase();
if (i === 1 && error?.component) {
matches = matches || pp.toLowerCase() === error.component.toLowerCase();
}
if (i === 2 && error?.primary) {
matches = matches || pp.toLowerCase() === error.primary.toLowerCase();
}
if (!matches) {
console.log('[Wildcard] Part mismatch at index', i, ':', pp, 'vs', cp);
return false;
}
}
}
console.log('[Wildcard] Match successful!');
return true;
}
function setCategory(category) {
AppState.activeCategory = category;
filterErrors();
renderResults();
updateSidebar();
renderActiveFilters();
}
function setTag(tag) {
AppState.activeTag = tag;
filterErrors();
renderResults();
updateSidebar();
renderActiveFilters();
}
function clearCategory() {
AppState.activeCategory = null;
AppState.activeCategoryType = null;
filterErrors();
renderResults();
updateSidebar();
renderActiveFilters();
}
function clearTag() {
AppState.activeTag = null;
filterErrors();
renderResults();
updateSidebar();
renderActiveFilters();
}
function clearFilters() {
AppState.searchQuery = '';
AppState.activeCategory = null;
AppState.activeCategoryType = null;
AppState.activeTag = null;
AppState.activeSeverities = new Set(SEVERITY_ORDER);
if (DOM.searchInput) DOM.searchInput.value = '*.*.*.*';
document.querySelectorAll('.severity-pill').forEach(pill => {
pill.classList.add('active');
pill.setAttribute('aria-pressed', 'true');
});
filterErrors();
renderResults();
updateSidebar();
renderActiveFilters();
}
function renderActiveFilters() {
if (!DOM.activeFilters) return;
const chips = [];
if (AppState.activeTag) {
chips.push(`
<span class="filter-chip tag" role="status">
<span class="filter-chip-label">Tag:</span>
<span class="filter-chip-value">${escapeHtml(AppState.activeTag)}</span>
<button type="button" class="filter-chip-remove" data-remove-filter="tag" title="Remove filter" aria-label="Remove tag filter">×</button>
</span>
`);
}
DOM.activeFilters.innerHTML = chips.join('');
}
function initFilterChips() {
if (!DOM.activeFilters) return;
DOM.activeFilters.addEventListener('click', (e) => {
if (!(e.target instanceof Element)) return;
const btn = e.target.closest('[data-remove-filter]');
if (!btn) return;
const kind = btn.getAttribute('data-remove-filter');
if (kind === 'category') clearCategory();
if (kind === 'tag') clearTag();
});
}