баг репорт и графифай

This commit is contained in:
Zuev
2026-07-04 15:47:12 +03:00
parent 9e6e0038c6
commit 96e055e13f
255 changed files with 110846 additions and 22 deletions

199
resume/script.js Normal file
View File

@@ -0,0 +1,199 @@
// Интерактивная логика инженерного резюме Егора Зуева.
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 = `
<h3>${escapeHtml(runbook.title)}</h3>
<div class="incident-steps">
${runbook.steps.map((step) => `
<div class="incident-step">
<strong>${escapeHtml(step.label)}</strong>
<p>${formatRunbookText(step.text)}</p>
</div>
`).join('')}
</div>
`;
};
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
tabs.forEach((item) => item.classList.remove('active'));
tab.classList.add('active');
renderIncident(tab.dataset.incident);
});
});
renderIncident(document.querySelector('.incident-tab.active')?.dataset.incident || 'postgres');
}
function formatRunbookText(text) {
return escapeHtml(text).replace(/`([^`]+)`/g, '<code>$1</code>');
}
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
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);
}