баг репорт и графифай
This commit is contained in:
407
resume/index.html
Normal file
407
resume/index.html
Normal file
@@ -0,0 +1,407 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Зуев Егор — Junior DevOps / Infrastructure Engineer</title>
|
||||
<meta name="description"
|
||||
content="Резюме Зуева Егора Алексеевича: Junior DevOps / Infrastructure Engineer. K3s, Caddy, Ansible, PostgreSQL, OpenTelemetry, SigNoz, Homelab.">
|
||||
<meta name="keywords"
|
||||
content="DevOps, Junior DevOps, Зуев Егор, Kubernetes, K3s, Ansible, Caddy, PostgreSQL, OpenTelemetry, SigNoz, Proxmox, Homelab">
|
||||
|
||||
<meta property="og:title" content="Зуев Егор — Junior DevOps / Infrastructure Engineer">
|
||||
<meta property="og:description"
|
||||
content="Инженерное резюме: мультитенантная инфраструктура стартапа «Умное расписание», CI/CD, IaC, Observability и Homelab.">
|
||||
<meta property="og:type" content="profile">
|
||||
<meta property="og:url" content="https://magistr.zuev.company/resume/">
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="topbar" aria-label="Навигация по резюме">
|
||||
<a class="brand" href="#profile" aria-label="К началу резюме">
|
||||
<span class="brand-mark">EZ</span>
|
||||
<span>DevOps</span>
|
||||
</a>
|
||||
<div class="nav-links">
|
||||
<a class="nav-link" href="#profile">Профиль</a>
|
||||
<a class="nav-link" href="#startup">Стартап</a>
|
||||
<a class="nav-link" href="#homelab">Homelab</a>
|
||||
<a class="nav-link" href="#stack">Стек</a>
|
||||
<a class="nav-link" href="#incidents">Инциденты</a>
|
||||
<a class="nav-link" href="#education">Образование</a>
|
||||
</div>
|
||||
<div class="topbar-actions">
|
||||
<button class="icon-button" id="btnPrint" type="button" aria-label="Сохранить в PDF">
|
||||
<span>PDF</span>
|
||||
</button>
|
||||
<a class="primary-action" href="https://t.me/ZuevEA" target="_blank" rel="noopener noreferrer">Связаться</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<section class="hero section-shell" id="profile">
|
||||
<div class="hero-copy reveal">
|
||||
<div class="status-row">
|
||||
<span class="status-pill">Ищу предложения</span>
|
||||
<span>Junior DevOps / Infrastructure Engineer</span>
|
||||
<span>100 000 ₽ на руки</span>
|
||||
</div>
|
||||
<h1>Зуев Егор Алексеевич</h1>
|
||||
<p class="lead">
|
||||
DevOps-инженер с базой в информационной безопасности: проектирую инфраструктуру для
|
||||
мультитенантных веб-сервисов, автоматизирую развертывание и делаю наблюдаемость полезной
|
||||
для поиска проблем, а не просто красивой витриной метрик.
|
||||
</p>
|
||||
<div class="contact-grid" aria-label="Контактная информация">
|
||||
<button class="contact-chip" type="button" data-copy="+7 (910) 740-05-55">
|
||||
<span class="chip-label">Телефон</span>
|
||||
<span>+7 (910) 740-05-55</span>
|
||||
</button>
|
||||
<button class="contact-chip" type="button" data-copy="ez@e-zuev.ru">
|
||||
<span class="chip-label">Email</span>
|
||||
<span>ez@e-zuev.ru</span>
|
||||
</button>
|
||||
<a class="contact-chip" href="https://t.me/ZuevEA" target="_blank" rel="noopener noreferrer">
|
||||
<span class="chip-label">Telegram</span>
|
||||
<span>@ZuevEA</span>
|
||||
</a>
|
||||
<span class="contact-chip">
|
||||
<span class="chip-label">Локация</span>
|
||||
<span>Курск / удаленно</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="architecture-board reveal" aria-label="Карта инженерного вклада">
|
||||
<span class="board-kicker">Инженерный контур</span>
|
||||
<h2 class="board-title">От кода до работающего сервиса</h2>
|
||||
<p class="board-lead">
|
||||
Закрываю практические задачи DevOps: собираю приложение в контейнеры, доставляю в Kubernetes,
|
||||
изолирую данные клиентов и настраиваю наблюдаемость для быстрого разбора проблем.
|
||||
</p>
|
||||
|
||||
<div class="delivery-lane" aria-label="Цепочка работы">
|
||||
<div class="lane-step">
|
||||
<span>01</span>
|
||||
<strong>Сборка</strong>
|
||||
<p>Docker-образы backend и frontend, приватный registry, Gitea Actions.</p>
|
||||
</div>
|
||||
<div class="lane-step">
|
||||
<span>02</span>
|
||||
<strong>Доставка</strong>
|
||||
<p>K3s, Traefik, Caddy, rolling update и проверка rollout-статуса.</p>
|
||||
</div>
|
||||
<div class="lane-step">
|
||||
<span>03</span>
|
||||
<strong>Данные</strong>
|
||||
<p>Отдельная PostgreSQL БД для каждого вуза, HikariCP, Flyway, backup и изоляция доступа.</p>
|
||||
</div>
|
||||
<div class="lane-step">
|
||||
<span>04</span>
|
||||
<strong>Контроль</strong>
|
||||
<p>OpenTelemetry, SigNoz, tenant.id в логах и трассировках.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="board-proof" aria-label="Что это дает проекту">
|
||||
<div>
|
||||
<b>Безопаснее</b>
|
||||
<span>секреты в Vault, RBAC и принцип наименьших привилегий</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Понятнее</b>
|
||||
<span>инциденты привязаны к конкретному вузу и его БД</span>
|
||||
</div>
|
||||
<div>
|
||||
<b>Быстрее</b>
|
||||
<span>новый вуз подключается без ручной сборки окружения</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="evidence-band">
|
||||
<div class="section-shell evidence-grid">
|
||||
<article class="evidence-card reveal">
|
||||
<span class="eyebrow">Инфраструктура</span>
|
||||
<h2>Мультитенантность без общей базы</h2>
|
||||
<p>Выбран и реализован подход Database-per-Tenant: отдельная PostgreSQL БД для каждого вуза,
|
||||
изоляция сетевого доступа и независимое обслуживание.</p>
|
||||
</article>
|
||||
<article class="evidence-card reveal">
|
||||
<span class="eyebrow">Автоматизация</span>
|
||||
<h2>IaC и безопасный провижининг</h2>
|
||||
<p>Ansible-роли для подготовки хостов, SSH hardening, firewall, PostgreSQL, backup и мониторинга.
|
||||
Секреты вынесены в Ansible Vault.</p>
|
||||
</article>
|
||||
<article class="evidence-card reveal">
|
||||
<span class="eyebrow">Observability</span>
|
||||
<h2>Трассировка с контекстом тенанта</h2>
|
||||
<p>OpenTelemetry + SigNoz + MDC позволяют фильтровать логи и трейсы по `tenant.id`, чтобы быстрее
|
||||
понимать, какой вуз затронут проблемой.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-shell case-section" id="startup">
|
||||
<div class="section-heading reveal">
|
||||
<span class="eyebrow">Проектный опыт</span>
|
||||
<h2>Стартап «Умное расписание»</h2>
|
||||
<p>Система управления университетским расписанием с мультитенантной архитектурой: отдельные базы
|
||||
данных для вузов, Kubernetes-контур, автоматизация инфраструктуры и сквозная наблюдаемость.</p>
|
||||
</div>
|
||||
|
||||
<div class="case-layout">
|
||||
<aside class="case-summary reveal">
|
||||
<div>
|
||||
<span class="summary-label">Роль</span>
|
||||
<strong>DevOps / Infrastructure Engineer</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="summary-label">Период</span>
|
||||
<strong>Декабрь 2025 — настоящее время</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="summary-label">Контур</span>
|
||||
<strong>K3s, Proxmox VE, Caddy, PostgreSQL, SigNoz</strong>
|
||||
</div>
|
||||
<a class="text-link" href="https://magistr.zuev.company" target="_blank"
|
||||
rel="noopener noreferrer">magistr.zuev.company</a>
|
||||
</aside>
|
||||
|
||||
<div class="work-streams">
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">01</div>
|
||||
<h3>Kubernetes и маршрутизация</h3>
|
||||
<p>Развернул прикладной контур в K3s: namespace `magistr`, frontend на Apache httpd, backend
|
||||
Spring Boot, Traefik Ingress и внешний Caddy как TLS-терминатор и балансировщик между
|
||||
нодами.</p>
|
||||
<ul>
|
||||
<li>Frontend в 2 репликах для rolling update.</li>
|
||||
<li>Backend с OpenTelemetry Java Agent.</li>
|
||||
<li>Caddy проксирует `/*`, `/api/*` и OTLP-трафик.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">02</div>
|
||||
<h3>Динамические тенанты</h3>
|
||||
<p>Спроектировал добавление новых университетов без правки исходного кода: `tenants-config`
|
||||
хранится в Kubernetes ConfigMap, backend обновляет его через Kubernetes API.</p>
|
||||
<ul>
|
||||
<li>`initContainer` копирует read-only ConfigMap в writable `emptyDir`.</li>
|
||||
<li>RBAC ограничен операциями `get` и `patch` для конкретной ConfigMap.</li>
|
||||
<li>`TenantConfigWatcher` сверяет конфигурацию каждые 30 секунд.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">03</div>
|
||||
<h3>Отказоустойчивость старта</h3>
|
||||
<p>Устранил риск каскадного падения backend при недоступности одной из БД: приложение стартует
|
||||
без CrashLoopBackOff и продолжает подключаться к проблемной БД в фоне.</p>
|
||||
<ul>
|
||||
<li>H2 in-memory fallback, если нет настроенных тенантов.</li>
|
||||
<li>`setInitializationFailTimeout(-1)` для HikariCP.</li>
|
||||
<li>Явный PostgreSQL dialect для Hibernate.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">04</div>
|
||||
<h3>Ansible и безопасность</h3>
|
||||
<p>Собрал модульный плейбук для подготовки серверов БД: базовый hardening, Docker, firewall,
|
||||
PostgreSQL, backup и OpenTelemetry Collector.</p>
|
||||
<ul>
|
||||
<li>SSH на порту 2222, отключение root/password login.</li>
|
||||
<li>UFW/firewalld с доступом к PostgreSQL только с IP нод K3s.</li>
|
||||
<li>Секреты PostgreSQL зашифрованы через Ansible Vault.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">05</div>
|
||||
<h3>CI/CD и приватный registry</h3>
|
||||
<p>Настроил Gitea Actions: сборка backend/frontend Docker-образов, публикация в приватный
|
||||
registry и деплой через `kubectl rollout` на self-hosted runner.</p>
|
||||
<ul>
|
||||
<li>Docker Buildx для сборки образов.</li>
|
||||
<li>Kubernetes secret `gitea-registry` для pull-доступа.</li>
|
||||
<li>Отдельный LXC-контейнер под runner.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="work-card reveal">
|
||||
<div class="work-card-marker">06</div>
|
||||
<h3>Наблюдаемость</h3>
|
||||
<p>Внедрил OpenTelemetry и SigNoz с детализацией по тенантам: логи, трейсы, JVM/HTTP-метрики и
|
||||
PostgreSQL-метрики с хостов БД.</p>
|
||||
<ul>
|
||||
<li>`tenant.id` попадает в MDC и атрибуты span.</li>
|
||||
<li>SigNoz + ClickHouse вынесены в отдельный LXC-контейнер.</li>
|
||||
<li>OTel Collector собирает PostgreSQL и host-метрики.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-shell homelab-section" id="homelab">
|
||||
<div class="section-heading reveal">
|
||||
<span class="eyebrow">Практика вне стартапа</span>
|
||||
<h2>Homelab и самостоятельная инженерная база</h2>
|
||||
<p>Отдельный контур для экспериментов с Linux, виртуализацией, сетями, мониторингом и автоматизацией.
|
||||
Не смешиваю его с проектным опытом, но использую как полигон для навыков.</p>
|
||||
</div>
|
||||
|
||||
<div class="homelab-grid">
|
||||
<article class="lab-card reveal">
|
||||
<h3>Виртуализация и хранение</h3>
|
||||
<p>Proxmox VE, LXC/KVM, Debian/Ubuntu Server, ZFS-пулы, snapshots и отдельные изолированные
|
||||
окружения для сервисов.</p>
|
||||
</article>
|
||||
<article class="lab-card reveal">
|
||||
<h3>Self-hosted разработка</h3>
|
||||
<p>Gitea и Forgejo для репозиториев, webhooks, тестов CI-сценариев и безопасной отработки
|
||||
automation-паттернов.</p>
|
||||
</article>
|
||||
<article class="lab-card reveal">
|
||||
<h3>Linux и эксплуатация</h3>
|
||||
<p>CachyOS/Arch на рабочей машине, systemd, journalctl, bash-скрипты, pacman, разбор проблем через
|
||||
системные логи.</p>
|
||||
</article>
|
||||
<article class="lab-card reveal">
|
||||
<h3>Мониторинг железа</h3>
|
||||
<p>Prometheus, Node Exporter и Grafana для CPU, дисков, температур, сетевой активности и базовых
|
||||
алертов по состоянию сервера.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stack-band" id="stack">
|
||||
<div class="section-shell">
|
||||
<div class="section-heading reveal">
|
||||
<span class="eyebrow">Стек по зонам ответственности</span>
|
||||
<h2>Инструменты, которые уже применял руками</h2>
|
||||
</div>
|
||||
|
||||
<div class="stack-matrix">
|
||||
<article class="stack-column reveal">
|
||||
<h3>Runtime и оркестрация</h3>
|
||||
<div class="tag-list">
|
||||
<span>Docker</span>
|
||||
<span>Docker Compose</span>
|
||||
<span>Kubernetes</span>
|
||||
<span>K3s</span>
|
||||
<span>Traefik</span>
|
||||
<span>Caddy</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stack-column reveal">
|
||||
<h3>IaC и доставка</h3>
|
||||
<div class="tag-list">
|
||||
<span>Ansible</span>
|
||||
<span>Ansible Vault</span>
|
||||
<span>Gitea Actions</span>
|
||||
<span>Forgejo</span>
|
||||
<span>Git</span>
|
||||
<span>Bash</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stack-column reveal">
|
||||
<h3>Данные и resiliency</h3>
|
||||
<div class="tag-list">
|
||||
<span>PostgreSQL</span>
|
||||
<span>HikariCP</span>
|
||||
<span>Flyway</span>
|
||||
<span>H2 fallback</span>
|
||||
<span>ZFS</span>
|
||||
<span>Backup</span>
|
||||
</div>
|
||||
</article>
|
||||
<article class="stack-column reveal">
|
||||
<h3>Observability и безопасность</h3>
|
||||
<div class="tag-list">
|
||||
<span>OpenTelemetry</span>
|
||||
<span>SigNoz</span>
|
||||
<span>ClickHouse</span>
|
||||
<span>Prometheus</span>
|
||||
<span>Grafana</span>
|
||||
<span>UFW/firewalld</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-shell incidents-section" id="incidents">
|
||||
<div class="section-heading reveal">
|
||||
<span class="eyebrow">Runbook-мышление</span>
|
||||
<h2>Как подхожу к инцидентам</h2>
|
||||
<p>Короткие сценарии показывают порядок действий: сначала локализация и границы ущерба, затем
|
||||
восстановление, потом предотвращение повтора.</p>
|
||||
</div>
|
||||
|
||||
<div class="incident-layout reveal">
|
||||
<div class="incident-tabs" role="tablist" aria-label="Сценарии инцидентов">
|
||||
<button class="incident-tab active" type="button" data-incident="postgres">PostgreSQL CPU 100%</button>
|
||||
<button class="incident-tab" type="button" data-incident="tls">TLS / Caddy 502</button>
|
||||
<button class="incident-tab" type="button" data-incident="runner">Gitea Runner завис</button>
|
||||
</div>
|
||||
<article class="incident-panel" id="incidentPanel" aria-live="polite"></article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section-shell education-section" id="education">
|
||||
<div class="section-heading reveal">
|
||||
<span class="eyebrow">Образование</span>
|
||||
<h2>Информационная безопасность как основа DevOps-подхода</h2>
|
||||
</div>
|
||||
|
||||
<div class="education-grid">
|
||||
<article class="education-card reveal">
|
||||
<span class="summary-label">2024 — 2026</span>
|
||||
<h3>10.04.01 Информационная безопасность</h3>
|
||||
<p class="education-school">Юго-Западный государственный университет</p>
|
||||
<p>Магистратура, факультет цифровых технологий. Фокус на защите инфраструктуры, изоляции сред и
|
||||
безопасной эксплуатации информационных систем.</p>
|
||||
</article>
|
||||
<article class="education-card reveal">
|
||||
<span class="summary-label">2020 — 2024</span>
|
||||
<h3>10.03.01 Информационная безопасность</h3>
|
||||
<p class="education-school">Юго-Западный государственный университет</p>
|
||||
<p>Бакалавриат, факультет фундаментальной и прикладной информатики. База по сетям, Linux,
|
||||
безопасности приложений и администрированию.</p>
|
||||
</article>
|
||||
<article class="education-card reveal">
|
||||
<span class="summary-label">Рабочий фокус</span>
|
||||
<h3>DevSecOps</h3>
|
||||
<p>Принцип наименьших привилегий, изоляция сред, контроль секретов, сетевой периметр и
|
||||
наблюдаемость как часть безопасности.</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="section-shell footer-grid">
|
||||
<div>
|
||||
<strong>Зуев Егор Алексеевич</strong>
|
||||
<span>Junior DevOps / Infrastructure Engineer</span>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<a href="mailto:ez@e-zuev.ru">ez@e-zuev.ru</a>
|
||||
<a href="https://t.me/ZuevEA" target="_blank" rel="noopener noreferrer">@ZuevEA</a>
|
||||
<span>Курск / удаленно</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite">Скопировано</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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);
|
||||
}
|
||||
1251
resume/style.css
Normal file
1251
resume/style.css
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user