// Интерактивная логика инженерного резюме Егора Зуева. const incidentRunbooks = { postgres: { title: 'PostgreSQL: CPU 100% у одного тенанта', steps: [ { label: 'Локализация', text: 'Проверяю SigNoz по `tenant.id`, затем на хосте БД смотрю `pg_stat_activity`, активные транзакции, блокировки и системную нагрузку.' }, { label: 'Восстановление', text: 'Ограничиваю тяжелый запрос через `pg_cancel_backend`, проверяю пул HikariCP и возвращаю сервис в нормальный режим без затрагивания других тенантов.' }, { label: 'Профилактика', text: 'Добавляю индекс или правлю запрос, включаю slow query logs и настраиваю алерт по CPU, активным сессиям и длительным транзакциям.' } ] }, tls: { title: 'Caddy: TLS или 502 на новом домене', steps: [ { label: 'Локализация', text: 'Проверяю DNS A/CNAME, логи Caddy через `journalctl`, доступность upstream-нод K3s и состояние Traefik Ingress.' }, { label: 'Восстановление', text: 'Перезагружаю конфигурацию Caddy, проверяю rate limit ACME, при необходимости переключаю issuer и временно вывожу tenant из автопровижининга.' }, { label: 'Профилактика', text: 'Добавляю предварительную проверку DNS перед созданием тенанта и мониторинг срока действия сертификатов через blackbox-проверки.' } ] }, runner: { title: 'Gitea Actions: self-hosted runner завис', steps: [ { label: 'Локализация', text: 'Смотрю `systemctl status`, `docker ps`, `docker logs`, свободное место через `df -h` и очередь job в Gitea Actions.' }, { label: 'Восстановление', text: 'Очищаю зависшие контейнеры и кэш Docker, перезапускаю runner, затем вручную контролирую `kubectl rollout status` для backend/frontend.' }, { label: 'Профилактика', text: 'Выношу runner на отдельный ZFS dataset с квотой, добавляю регулярную очистку слоев и алерты по свободному месту.' } ] } }; document.addEventListener('DOMContentLoaded', () => { initPrintButton(); initCopyButtons(); initReveal(); initNavigationHighlight(); initIncidentTabs(); }); function initPrintButton() { const printButton = document.getElementById('btnPrint'); if (!printButton) return; printButton.addEventListener('click', () => { window.print(); }); } function initCopyButtons() { document.querySelectorAll('[data-copy]').forEach((element) => { element.addEventListener('click', async () => { const value = element.getAttribute('data-copy'); if (!value) return; try { await navigator.clipboard.writeText(value); showToast('Скопировано: ' + value); } catch (error) { console.error('Ошибка копирования:', error); showToast('Не удалось скопировать'); } }); }); } function initReveal() { const revealElements = document.querySelectorAll('.reveal'); if (!revealElements.length) return; if (!('IntersectionObserver' in window)) { revealElements.forEach((element) => element.classList.add('active')); return; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('active'); observer.unobserve(entry.target); } }); }, { threshold: 0.08, rootMargin: '0px 0px -48px 0px' }); revealElements.forEach((element) => observer.observe(element)); } function initNavigationHighlight() { const links = Array.from(document.querySelectorAll('.nav-link')); const sections = links .map((link) => document.querySelector(link.getAttribute('href'))) .filter(Boolean); if (!links.length || !sections.length) return; const setActiveLink = () => { const currentSection = sections.reduce((current, section) => { const sectionTop = section.getBoundingClientRect().top; if (sectionTop <= 120) { return section; } return current; }, sections[0]); links.forEach((link) => { const isActive = link.getAttribute('href') === '#' + currentSection.id; link.classList.toggle('active', isActive); }); }; setActiveLink(); window.addEventListener('scroll', setActiveLink, { passive: true }); } function initIncidentTabs() { const tabs = document.querySelectorAll('.incident-tab'); const panel = document.getElementById('incidentPanel'); if (!tabs.length || !panel) return; const renderIncident = (incidentId) => { const runbook = incidentRunbooks[incidentId]; if (!runbook) return; panel.innerHTML = `
${formatRunbookText(step.text)}
$1');
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
function showToast(message) {
const toast = document.getElementById('toast');
if (!toast) return;
toast.textContent = message;
toast.classList.add('show');
window.clearTimeout(window.resumeToastTimer);
window.resumeToastTimer = window.setTimeout(() => {
toast.classList.remove('show');
}, 2400);
}