waddling-errors 0.7.3

Structured, secure-by-default diagnostic codes for distributed systems with no_std and role-based documentation
Documentation
/* ============================================
   SEARCH & FILTERING
   Search input handling and error filtering
   ============================================ */

function initSearch() {
    if (!DOM.searchInput) return;
    
    // Search input with debounce
    DOM.searchInput.addEventListener('input', debounce((e) => {
        AppState.searchQuery = e.target.value.trim();
        filterErrors();
        renderResults();
        updateSidebar();
    }, 200));
    
    // Clear button - clears ALL filters (search, category, tag, severity)
    if (DOM.searchClear) {
        DOM.searchClear.addEventListener('click', () => {
            clearFilters();
            DOM.searchInput.focus();
        });
    }
    
    // Keyboard shortcut (Ctrl/Cmd + K)
    document.addEventListener('keydown', (e) => {
        if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
            e.preventDefault();
            DOM.searchInput.focus();
            DOM.searchInput.select();
        }
        // Escape to clear
        if (e.key === 'Escape' && document.activeElement === DOM.searchInput) {
            DOM.searchInput.blur();
        }
    });
}

function initSeverityFilters() {
    if (!DOM.severityBar) return;
    
    // WDP Part 10 severity character codes
    const SEVERITY_CHARS = {
        error: 'E', blocked: 'B', critical: 'C', warning: 'W',
        help: 'H', success: 'S', completed: 'K', info: 'I', trace: 'T'
    };
    
    // Initialize all severities as active
    AppState.activeSeverities = new Set(SEVERITY_ORDER);
    
    // Count errors by severity
    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]++;
    });
    
    // Build compact severity filter buttons (v3 style with count badges)
    DOM.severityBar.innerHTML = '<span class="filter-label">SEVERITY</span>';
    
    SEVERITY_ORDER.forEach(severity => {
        const count = counts[severity];
        if (count === 0) return; // Skip severities with no errors
        
        // Get severity character code from WDP mapping
        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);
        
        // Create compact button with count badge
        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)) {
        // Don't allow deselecting all
        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);
    
    // Check if query looks like a hash (5 alphanumeric characters)
    let resolvedQuery = query;
    if (query && /^[a-z0-9]{5}$/i.test(query)) {
        // Try to resolve hash to error code
        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();
            // Show visual feedback that we resolved the hash
            showToast('Hash Resolved', `${query}  ${matchedCode}`, 'success');
        } else {
            console.log('[Search] No hash match for:', query);
        }
    }
    
    AppState.filteredErrors = AppState.errors.filter(error => {
        // Visibility filter
        if (!isErrorVisible(error)) return false;
        
        // Severity filter
        const severity = (error.severity || 'info').toLowerCase();
        if (!AppState.activeSeverities.has(severity)) return false;
        
        // Category filter - checks component, primary, or sequence based on activeCategoryType
        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') {
                // activeCategory for sequences is the numeric string key (e.g., "26")
                if (String(error.sequence) !== AppState.activeCategory) return false;
            }
        }
        
        // Tag filter
        if (AppState.activeTag) {
            const tags = error.tags || [];
            if (!tags.includes(AppState.activeTag)) return false;
        }
        
        // Search query - support hash lookup, wildcard pattern matching, and text search
        if (resolvedQuery) {
            // Check if it's a wildcard pattern like *.*.*.* or E.*.*.* or E.Auth.Token.EXHAUSTED
            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 {
                // Regular text search (includes resolved hash codes)
                // Also include sequence name for named sequence search (e.g., "EXHAUSTED")
                const seqNum = error.sequence;
                const seqMeta = AppState.sequences?.[seqNum];
                const seqName = seqMeta?.name || '';
                
                const searchFields = [
                    error.code,
                    error.hash, // Include hash in search
                    error.message,
                    error.description,
                    error.component, // Include component name
                    error.primary,   // Include primary name
                    seqName,         // Include sequence name (e.g., "EXHAUSTED", "NOT_FOUND")
                    ...(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');
    
    // Update viewing counter
    updateViewingCounter();
}

function matchWildcardPattern(code, pattern, error) {
    if (!code) return false;
    
    console.log('[Wildcard] Matching code:', code, 'against pattern:', pattern);
    
    // Split both by dots
    const codeParts = code.split('.');
    const patternParts = pattern.split('.');
    
    // Must have same number of parts for exact wildcard matching
    if (codeParts.length !== patternParts.length && !pattern.includes('*')) {
        // Fallback to substring match
        console.log('[Wildcard] Length mismatch, using substring match');
        return code.toLowerCase().includes(pattern.toLowerCase());
    }
    
    // Check each part (index 0=severity, 1=component, 2=primary, 3=sequence)
    for (let i = 0; i < patternParts.length; i++) {
        const pp = patternParts[i];
        const cp = codeParts[i] || '';
        
        if (pp === '*') continue; // Wildcard matches anything
        
        // Special handling for sequence part (last part)
        if (i === 3) {
            // Get sequence metadata for named matching
            const seqNum = parseInt(cp, 10);
            const seqMeta = AppState.sequences?.[seqNum];
            const seqName = seqMeta?.name || '';
            
            // Check if pattern is numeric
            if (/^\d+$/.test(pp)) {
                // Pattern is numeric - compare as integers (handles 1 vs 001)
                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 {
                // Pattern is a name (e.g., "EXHAUSTED") - compare against sequence name
                console.log('[Wildcard] Comparing sequence name:', pp, 'vs', seqName);
                if (pp.toLowerCase() !== seqName.toLowerCase()) {
                    console.log('[Wildcard] Sequence name mismatch');
                    return false;
                }
            }
        } else {
            // Case-insensitive string comparison for other parts
            // Also check against error.component/error.primary for preserved casing
            let matches = pp.toLowerCase() === cp.toLowerCase();
            
            // For component (index 1), also check error.component
            if (i === 1 && error?.component) {
                matches = matches || pp.toLowerCase() === error.component.toLowerCase();
            }
            // For primary (index 2), also check error.primary
            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 = '*.*.*.*';
    
    // Reset severity pills
    document.querySelectorAll('.severity-pill').forEach(pill => {
        pill.classList.add('active');
        pill.setAttribute('aria-pressed', 'true');
    });
    
    filterErrors();
    renderResults();
    updateSidebar();
    renderActiveFilters();
}

/**
 * Renders active filter chips below the search bar
 * Only shows tag filter - component filter is already visible in sidebar
 */
function renderActiveFilters() {
    if (!DOM.activeFilters) return;
    
    const chips = [];
    
    // Note: Component chip removed - sidebar already highlights selected component
    // This avoids redundant UI while still providing clear feedback
    
    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('');
}

/**
 * Initialize filter chip removal event delegation
 * Sets up CSP-compliant click handlers for filter chips
 */
function initFilterChips() {
    if (!DOM.activeFilters) return;
    
    // Event delegation for filter chip removal (CSP-compliant, no inline handlers)
    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();
    });
}