docs(diagrams): add interactive architecture map HTML

- Interactive component map with visual connections
- Click to see component details and data flows
- Search functionality for quick component lookup
- Connection highlighting when selecting components
- Responsive design with color-coded layers
- Component information panel with ports, protocols, flows

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
igor04091968
2026-05-05 01:51:25 +03:00
co-authored by Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent d8320799e6
commit 7a89f33e3b
+918
View File
@@ -0,0 +1,918 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ActivityWatch-Russian - Architecture Map</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.legend {
background: rgba(255,255,255,0.95);
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.legend h2 {
margin-bottom: 15px;
color: #333;
}
.legend-items {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 30px;
height: 30px;
border-radius: 5px;
border: 2px solid #333;
}
.architecture-map {
background: rgba(255,255,255,0.95);
border-radius: 15px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
position: relative;
min-height: 800px;
}
.layer {
margin-bottom: 40px;
position: relative;
}
.layer-title {
font-size: 1.3em;
font-weight: bold;
color: #333;
margin-bottom: 15px;
padding: 10px;
background: #f0f0f0;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.components-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.component {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.3s ease;
position: relative;
border: 3px solid transparent;
}
.component:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(0,0,0,0.2);
}
.component.selected {
border-color: #667eea;
background: #f0f4ff;
}
.component.windows-client {
border-top: 4px solid #ff6b6b;
}
.component.linux-server {
border-top: 4px solid #4ecdc4;
}
.component.integration {
border-top: 4px solid #ffe66d;
}
.component.monitoring {
border-top: 4px solid #95e1d3;
}
.component.external {
border-top: 4px solid #dda0dd;
}
.component h3 {
color: #333;
margin-bottom: 10px;
font-size: 1.1em;
}
.component-description {
color: #666;
font-size: 0.9em;
margin-bottom: 10px;
}
.component-tech {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.tech-badge {
background: #f0f0f0;
padding: 3px 8px;
border-radius: 12px;
font-size: 0.75em;
color: #555;
}
.connections {
margin-top: 20px;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
display: none;
}
.connections.active {
display: block;
}
.connections h4 {
margin-bottom: 10px;
color: #333;
}
.connection-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
margin-bottom: 5px;
background: white;
border-radius: 5px;
border-left: 3px solid #667eea;
}
.connection-protocol {
background: #667eea;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75em;
}
.connection-direction {
color: #999;
font-size: 1.2em;
}
.info-panel {
position: fixed;
right: 20px;
top: 20px;
width: 350px;
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
max-height: 80vh;
overflow-y: auto;
display: none;
z-index: 1000;
}
.info-panel.active {
display: block;
}
.info-panel h3 {
color: #667eea;
margin-bottom: 15px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.info-panel-close {
position: absolute;
top: 10px;
right: 10px;
background: #ff6b6b;
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 1.2em;
}
.info-section {
margin-bottom: 15px;
}
.info-section h4 {
color: #333;
margin-bottom: 8px;
font-size: 0.95em;
}
.info-section p, .info-section ul {
color: #666;
font-size: 0.85em;
line-height: 1.5;
}
.info-section ul {
padding-left: 20px;
}
.flow-diagram {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
margin-top: 15px;
}
.flow-step {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.flow-arrow {
color: #667eea;
font-size: 1.5em;
}
.search-box {
background: white;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.search-box input {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
.search-box input:focus {
outline: none;
border-color: #667eea;
}
.component.hidden {
display: none;
}
@media (max-width: 768px) {
.components-grid {
grid-template-columns: 1fr;
}
.info-panel {
width: 90%;
right: 5%;
}
}
</style>
</head>
<body>
<div class="container">
<h1>ActivityWatch-Russian - Architecture Map</h1>
<div class="search-box">
<input type="text" id="searchInput" placeholder="🔍 Поиск компонентов...">
</div>
<div class="legend">
<h2>Легенда</h2>
<div class="legend-items">
<div class="legend-item">
<div class="legend-color" style="background: #ff6b6b;"></div>
<span>Windows Clients</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4ecdc4;"></div>
<span>Linux Server</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #ffe66d;"></div>
<span>Integration Layer</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #95e1d3;"></div>
<span>Monitoring Stack</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #dda0dd;"></div>
<span>External Systems</span>
</div>
</div>
</div>
<div class="architecture-map">
<!-- Windows Clients Layer -->
<div class="layer">
<div class="layer-title">🖥️ Windows Clients - Сбор данных</div>
<div class="components-grid">
<div class="component windows-client" data-id="dlp-endpoint" data-search="dlp endpoint collector clipboard print usb">
<h3>DLP Endpoint Collector</h3>
<p class="component-description">Мониторинг clipboard, печати и USB для обнаружения утечек данных</p>
<div class="component-tech">
<span class="tech-badge">PowerShell</span>
<span class="tech-badge">Windows API</span>
</div>
</div>
<div class="component windows-client" data-id="browser-domains" data-search="browser domains collector web monitoring">
<h3>Browser Domains Collector</h3>
<p class="component-description">Отслеживание посещаемых доменов с категоризацией сайтов</p>
<div class="component-tech">
<span class="tech-badge">PowerShell</span>
<span class="tech-badge">Window API</span>
</div>
</div>
<div class="component windows-client" data-id="email-outbound" data-search="email outbound collector outlook smtp">
<h3>Email Outbound Collector</h3>
<p class="component-description">Мониторинг исходящей почты через Outlook и SMTP</p>
<div class="component-tech">
<span class="tech-badge">PowerShell</span>
<span class="tech-badge">Outlook API</span>
</div>
</div>
<div class="component windows-client" data-id="worktime-session" data-search="worktime session collector time tracking">
<h3>Worktime Session Collector</h3>
<p class="component-description">Отслеживание рабочих сессий и учет времени</p>
<div class="component-tech">
<span class="tech-badge">PowerShell</span>
<span class="tech-badge">Windows API</span>
</div>
</div>
</div>
</div>
<!-- Linux Server Layer -->
<div class="layer">
<div class="layer-title">🐧 Linux Server - Хранение и обработка</div>
<div class="components-grid">
<div class="component linux-server" data-id="aw-server" data-search="activitywatch server rust api">
<h3>ActivityWatch Server</h3>
<p class="component-description">Основной сервер для приема, хранения и обработки событий</p>
<div class="component-tech">
<span class="tech-badge">Rust</span>
<span class="tech-badge">HTTP API</span>
</div>
</div>
<div class="component linux-server" data-id="postgresql" data-search="postgresql database storage">
<h3>PostgreSQL Database</h3>
<p class="component-description">Основное хранилище событий, метаданных и конфигураций</p>
<div class="component-tech">
<span class="tech-badge">PostgreSQL</span>
<span class="tech-badge">SQL</span>
</div>
</div>
<div class="component linux-server" data-id="webui" data-search="webui russian localization patches">
<h3>WebUI with RU Patches</h3>
<p class="component-description">Веб-интерфейс с русской локализацией и DLP компонентами</p>
<div class="component-tech">
<span class="tech-badge">JavaScript</span>
<span class="tech-badge">Vue.js</span>
</div>
</div>
</div>
</div>
<!-- Integration Layer -->
<div class="layer">
<div class="layer-title">🔗 Integration Layer - Интеграции</div>
<div class="components-grid">
<div class="component integration" data-id="pfsense-poller" data-search="pfsense poller firewall logs">
<h3>pfSense Poller</h3>
<p class="component-description">Сбор логов с pfSense firewall и отправка в ActivityWatch</p>
<div class="component-tech">
<span class="tech-badge">Python</span>
<span class="tech-badge">HTTP API</span>
</div>
</div>
<div class="component integration" data-id="dlp-aggregation" data-search="dlp aggregation scripts postgresql">
<h3>DLP Aggregation Scripts</h3>
<p class="component-description">Агрегация DLP событий из ActivityWatch в PostgreSQL</p>
<div class="component-tech">
<span class="tech-badge">Python</span>
<span class="tech-badge">psycopg2</span>
</div>
</div>
<div class="component integration" data-id="prometheus-exporter" data-search="prometheus exporter metrics">
<h3>Prometheus Exporter</h3>
<p class="component-description">Экспорт метрик ActivityWatch в формате Prometheus</p>
<div class="component-tech">
<span class="tech-badge">Python</span>
<span class="tech-badge">Prometheus</span>
</div>
</div>
</div>
</div>
<!-- Monitoring Stack -->
<div class="layer">
<div class="layer-title">📊 Monitoring Stack - Мониторинг</div>
<div class="components-grid">
<div class="component monitoring" data-id="prometheus" data-search="prometheus metrics monitoring">
<h3>Prometheus</h3>
<p class="component-description">Сбор и хранение метрик для мониторинга системы</p>
<div class="component-tech">
<span class="tech-badge">Prometheus</span>
<span class="tech-badge">TSDB</span>
</div>
</div>
<div class="component monitoring" data-id="grafana" data-search="grafana dashboards visualization">
<h3>Grafana</h3>
<p class="component-description">Визуализация метрик и построение дашбордов</p>
<div class="component-tech">
<span class="tech-badge">Grafana</span>
<span class="tech-badge">Dashboards</span>
</div>
</div>
<div class="component monitoring" data-id="sql-exporter" data-search="sql exporter postgresql grafana">
<h3>SQL Exporter</h3>
<p class="component-description">Прямые SQL запросы к PostgreSQL для Grafana</p>
<div class="component-tech">
<span class="tech-badge">Python</span>
<span class="tech-badge">SQL</span>
</div>
</div>
</div>
</div>
<!-- External Systems -->
<div class="layer">
<div class="layer-title">🌐 External Systems - Внешние системы</div>
<div class="components-grid">
<div class="component external" data-id="pfsense" data-search="pfsense firewall network">
<h3>pfSense Firewall</h3>
<p class="component-description">Брандмауэр для мониторинга сетевой активности</p>
<div class="component-tech">
<span class="tech-badge">pfSense</span>
<span class="tech-badge">Firewall</span>
</div>
</div>
<div class="component external" data-id="domain-controller" data-search="domain controller active directory gpo">
<h3>Domain Controller</h3>
<p class="component-description">Развертывание коллекторов через GPO и управление пользователями</p>
<div class="component-tech">
<span class="tech-badge">AD</span>
<span class="tech-badge">GPO</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="info-panel" id="infoPanel">
<button class="info-panel-close" onclick="closeInfoPanel()">×</button>
<h3 id="infoPanelTitle">Component Info</h3>
<div id="infoPanelContent"></div>
</div>
<script>
// Component data with connections
const componentsData = {
'dlp-endpoint': {
title: 'DLP Endpoint Collector',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '→', description: 'Отправка DLP событий' }
],
flows: [
'Пользователь копирует данные',
'DLP Collector перехватывает событие',
'Проверка по DLP правилам',
'При нарушении → скриншот + инцидент',
'Отправка в ActivityWatch Server'
],
ports: ['HTTP:5600'],
details: 'Мониторинг clipboard, печати и USB устройств для обнаружения утечек конфиденциальных данных. Поддерживает блокировку нарушений и создание скриншотов.'
},
'browser-domains': {
title: 'Browser Domains Collector',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '→', description: 'Отправка браузерных событий' }
],
flows: [
'Пользователь открывает сайт',
'Collector определяет домен',
'Категоризация сайта',
'Проверка DLP правил',
'Отправка события в ActivityWatch'
],
ports: ['HTTP:5600'],
details: 'Отслеживание посещаемых веб-сайтов с автоматической категоризацией и проверкой по DLP правилам для запрещенных доменов.'
},
'email-outbound': {
title: 'Email Outbound Collector',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '→', description: 'Отправка email событий' }
],
flows: [
'Пользователь отправляет email',
'Collector перехватывает событие',
'Проверка по email правилам',
'При нарушении → блокировка',
'Логирование в ActivityWatch'
],
ports: ['HTTP:5600'],
details: 'Мониторинг исходящей почты через Outlook и SMTP соединения для предотвращения утечек через email.'
},
'worktime-session': {
title: 'Worktime Session Collector',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '→', description: 'Отправка сессий' }
],
flows: [
'Определение начала работы',
'Отслеживание активности',
'Определение перерывов',
'Агрегация по дням',
'Отправка в ActivityWatch'
],
ports: ['HTTP:5600'],
details: 'Учет рабочего времени сотрудников с определением начала/конца сессий и перерывов.'
},
'aw-server': {
title: 'ActivityWatch Server',
connections: [
{ target: 'postgresql', protocol: 'TCP', direction: '→', description: 'Хранение данных' },
{ target: 'webui', protocol: 'HTTP', direction: '→', description: 'Отдача интерфейса' },
{ target: 'prometheus-exporter', protocol: 'HTTP API', direction: '←', description: 'Метрики' }
],
flows: [
'Прием событий от коллекторов',
'Валидация и нормализация',
'Сохранение в PostgreSQL',
'Предоставление API',
'Отдача WebUI'
],
ports: ['HTTP:5600', 'WebSocket:5666'],
details: 'Основной сервер ActivityWatch на Rust, принимающий события от коллекторов и предоставляющий API и WebUI.'
},
'postgresql': {
title: 'PostgreSQL Database',
connections: [
{ target: 'dlp-aggregation', protocol: 'TCP', direction: '↔', description: 'Агрегация событий' },
{ target: 'grafana', protocol: 'TCP', direction: '→', description: 'Прямые запросы' },
{ target: 'sql-exporter', protocol: 'TCP', direction: '→', description: 'SQL запросы' }
],
flows: [
'Хранение событий активности',
'Хранение DLP инцидентов',
'Хранение метаданных',
'Агрегация данных',
'Аналитические запросы'
],
ports: ['TCP:5432'],
details: 'Основная база данных для хранения всех событий, инцидентов и метаданных системы.'
},
'webui': {
title: 'WebUI with RU Patches',
connections: [
{ target: 'aw-server', protocol: 'HTTP', direction: '←', description: 'API запросы' }
],
flows: [
'Загрузка WebUI',
'Применение RU патчей',
'Инъекция DLP компонентов',
'Перевод интерфейса',
'Отображение данных'
],
ports: ['HTTP:5600'],
details: 'Веб-интерфейс с русской локализацией, DLP центром и компонентами для анализа активности.'
},
'pfsense-poller': {
title: 'pfSense Poller',
connections: [
{ target: 'pfsense', protocol: 'HTTPS', direction: '←', description: 'Сбор логов' },
{ target: 'aw-server', protocol: 'HTTP API', direction: '→', description: 'Отправка событий' }
],
flows: [
'Подключение к pfSense API',
'Сбор логов firewall',
'Нормализация данных',
'Отправка в ActivityWatch',
'Периодический опрос'
],
ports: ['HTTPS:443'],
details: 'Python скрипт для сбора логов с pfSense firewall и отправки сетевых событий в ActivityWatch.'
},
'dlp-aggregation': {
title: 'DLP Aggregation Scripts',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '←', description: 'Чтение событий' },
{ target: 'postgresql', protocol: 'TCP', direction: '→', description: 'Запись агрегации' }
],
flows: [
'Чтение событий из AW',
'Парсинг и нормализация',
'Агрегация по периодам',
'Запись в PostgreSQL',
'Генерация отчетов'
],
ports: ['TCP:5432'],
details: 'Python скрипты для агрегации DLP событий из ActivityWatch buckets в PostgreSQL для аналитики.'
},
'prometheus-exporter': {
title: 'Prometheus Exporter',
connections: [
{ target: 'aw-server', protocol: 'HTTP API', direction: '←', description: 'Сбор метрик' },
{ target: 'prometheus', protocol: 'HTTP', direction: '→', description: 'Экспорт метрик' }
],
flows: [
'Запрос к AW API',
'Сбор метрик buckets',
'Подсчет событий',
'Форматирование Prometheus',
'Отдача на /metrics'
],
ports: ['HTTP:9398'],
details: 'HTTP сервер для экспорта метрик ActivityWatch в формате Prometheus для мониторинга.'
},
'prometheus': {
title: 'Prometheus',
connections: [
{ target: 'prometheus-exporter', protocol: 'HTTP', direction: '←', description: 'Scraping' }
],
flows: [
'Периодический scrape',
'Сбор метрик',
'Хранение в TSDB',
'Evaluation правил',
'Alerting'
],
ports: ['HTTP:9090'],
details: 'Система мониторинга для сбора и хранения метрик с оценкой правил и алертингом.'
},
'grafana': {
title: 'Grafana',
connections: [
{ target: 'prometheus', protocol: 'HTTP', direction: '←', description: 'Запросы метрик' },
{ target: 'postgresql', protocol: 'TCP', direction: '←', description: 'SQL запросы' }
],
flows: [
'Запрос к Prometheus',
'Запрос к PostgreSQL',
'Визуализация данных',
'Построение графиков',
'Дашборды'
],
ports: ['HTTP:3000'],
details: 'Платформа визуализации для построения дашбордов на основе метрик Prometheus и данных PostgreSQL.'
},
'sql-exporter': {
title: 'SQL Exporter',
connections: [
{ target: 'postgresql', protocol: 'TCP', direction: '←', description: 'SQL запросы' },
{ target: 'grafana', protocol: 'HTTP', direction: '→', description: 'Данные для дашбордов' }
],
flows: [
'SQL запросы к PostgreSQL',
'Агрегация данных',
'Форматирование',
'Экспорт в Grafana',
'Периодическое обновление'
],
ports: ['HTTP:9398'],
details: 'Экспорт данных из PostgreSQL через SQL запросы для использования в Grafana дашбордах.'
},
'pfsense': {
title: 'pfSense Firewall',
connections: [
{ target: 'pfsense-poller', protocol: 'HTTPS', direction: '→', description: 'API доступ' }
],
flows: [
'Обработка трафика',
'Логирование соединений',
'Предоставление API',
'Мониторинг безопасности'
],
ports: ['HTTPS:443', 'SSH:22'],
details: 'Брандмауэр для мониторинга сетевой активности и защиты периметра.'
},
'domain-controller': {
title: 'Domain Controller',
connections: [
{ target: 'dlp-endpoint', protocol: 'GPO', direction: '→', description: 'Развертывание' },
{ target: 'browser-domains', protocol: 'GPO', direction: '→', description: 'Развертывание' },
{ target: 'email-outbound', protocol: 'GPO', direction: '→', description: 'Развертывание' }
],
flows: [
'Создание GPO',
'Развертывание коллекторов',
'Управление пользователями',
'Мониторинг статуса'
],
ports: ['LDAP:389', 'SMB:445'],
details: 'Active Directory для автоматического развертывания коллекторов через Group Policy Objects.'
}
};
// DOM elements
const components = document.querySelectorAll('.component');
const infoPanel = document.getElementById('infoPanel');
const infoPanelTitle = document.getElementById('infoPanelTitle');
const infoPanelContent = document.getElementById('infoPanelContent');
const searchInput = document.getElementById('searchInput');
// Component click handler
components.forEach(component => {
component.addEventListener('click', () => {
const componentId = component.dataset.id;
const data = componentsData[componentId];
// Remove selected class from all components
components.forEach(c => c.classList.remove('selected'));
// Add selected class to clicked component
component.classList.add('selected');
// Show info panel
showInfoPanel(componentId, data);
// Highlight connected components
highlightConnections(componentId, data.connections);
});
});
function showInfoPanel(componentId, data) {
infoPanelTitle.textContent = data.title;
let content = `
<div class="info-section">
<h4>📝 Описание</h4>
<p>${data.details}</p>
</div>
<div class="info-section">
<h4>🔗 Связи</h4>
`;
data.connections.forEach(conn => {
content += `
<div class="connection-item">
<span class="connection-protocol">${conn.protocol}</span>
<span class="connection-direction">${conn.direction}</span>
<span>${conn.description}</span>
</div>
`;
});
content += `</div>`;
if (data.flows && data.flows.length > 0) {
content += `
<div class="info-section">
<h4>⚡ Поток данных</h4>
<div class="flow-diagram">
`;
data.flows.forEach((flow, index) => {
content += `
<div class="flow-step">
<span>${index + 1}.</span>
<span>${flow}</span>
${index < data.flows.length - 1 ? '<span class="flow-arrow">↓</span>' : ''}
</div>
`;
});
content += `</div></div>`;
}
if (data.ports && data.ports.length > 0) {
content += `
<div class="info-section">
<h4>🌐 Порты</h4>
<ul>
`;
data.ports.forEach(port => {
content += `<li>${port}</li>`;
});
content += `</ul></div>`;
}
infoPanelContent.innerHTML = content;
infoPanel.classList.add('active');
}
function closeInfoPanel() {
infoPanel.classList.remove('active');
components.forEach(c => {
c.classList.remove('selected');
c.style.opacity = '1';
});
}
function highlightConnections(componentId, connections) {
// Reset all components
components.forEach(c => {
c.style.opacity = '0.3';
});
// Highlight current component
const currentComponent = document.querySelector(`[data-id="${componentId}"]`);
if (currentComponent) {
currentComponent.style.opacity = '1';
}
// Highlight connected components
connections.forEach(conn => {
const targetComponent = document.querySelector(`[data-id="${conn.target}"]`);
if (targetComponent) {
targetComponent.style.opacity = '1';
}
});
}
// Search functionality
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase();
components.forEach(component => {
const searchData = component.dataset.search.toLowerCase();
const title = component.querySelector('h3').textContent.toLowerCase();
const description = component.querySelector('.component-description').textContent.toLowerCase();
if (searchData.includes(searchTerm) ||
title.includes(searchTerm) ||
description.includes(searchTerm)) {
component.classList.remove('hidden');
} else {
component.classList.add('hidden');
}
});
});
// Close info panel on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeInfoPanel();
}
});
// Close info panel when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.component') && !e.target.closest('.info-panel')) {
closeInfoPanel();
}
});
</script>
</body>
</html>