баг репорт и графифай
This commit is contained in:
199
resume/script.js
Normal file
199
resume/script.js
Normal 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('&', '&')
|
||||
.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);
|
||||
}
|
||||
Reference in New Issue
Block a user