commit 4ee119a960de9823a92fbda6ad0d56d23fce2999 Author: Kirill Date: Wed Oct 7 16:28:14 2026 +0300 feat(all): 1-st site version diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..33dbd03 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git/ +.github/ +.forgejo/ +.idea/ +.venv/ +shell.nix +README.md diff --git a/.gitea/workflows/demo.yaml b/.gitea/workflows/demo.yaml new file mode 100644 index 0000000..e5ae7c9 --- /dev/null +++ b/.gitea/workflows/demo.yaml @@ -0,0 +1,95 @@ +name: Build, Test and Push Flask App + +on: + push: + branches: [ main, master ] + +jobs: + docker-build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Docker CLI + run: | + apt-get update + apt-get install -y docker.io + docker --version + + - name: Configure Docker CLI for insecure registry + run: | + mkdir -p ~/.docker + echo '{"insecure-registries" : ["git.rustprogersteam.ru"]}' > ~/.docker/config.json + cat ~/.docker/config.json + + - name: 'Docker Tags' + id: tags + uses: https://github.com/cssnr/docker-tags-action@v2 + with: + images: 'git.rustprogersteam.ru/${{ vars.DOCKER_USERNAME }}/myresume' + + - name: Login to Docker Registry + uses: docker/login-action@v3 + with: + registry: git.rustprogersteam.ru + username: ${{ vars.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + git.rustprogersteam.ru/${{ vars.DOCKER_USERNAME }}/myresume/resume:latest + git.rustprogersteam.ru/${{ vars.DOCKER_USERNAME }}/myresume/resume:${{ github.sha }} + labels: ${{ steps.tags.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + deploy: + needs: docker-build-and-push + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Ansible + run: | + apt-get update + apt-get install -y ansible openssh-client + + - name: Set up SSH key + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + mkdir -p ~/.ssh + printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keygen -y -f ~/.ssh/deploy_key >/dev/null + + - name: Trust server host key + run: | + ssh-keyscan -H "${{ secrets.SERVER_IP }}" >> ~/.ssh/known_hosts + + - name: Test SSH + run: | + ssh \ + -i ~/.ssh/deploy_key \ + -o StrictHostKeyChecking=yes \ + "${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}" \ + 'echo SSH works' + + - name: Run Ansible playbook + run: | + ansible-playbook \ + -i "${{ secrets.SERVER_IP }}," \ + ansible/restart_compose.yml \ + --user "${{ secrets.SERVER_USER }}" \ + --private-key ~/.ssh/deploy_key diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..657f931 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir -r dependencies.txt + +ENV FLASK_ENV=production +ENV PYTHONUNBUFFERED=1 + +CMD ["gunicorn", "app:app", "-b", "0.0.0.0:80", "--workers", "1"] diff --git a/ansible/restart_compose.yml b/ansible/restart_compose.yml new file mode 100644 index 0000000..6264467 --- /dev/null +++ b/ansible/restart_compose.yml @@ -0,0 +1,15 @@ +- name: Restart Docker Compose services + hosts: all + # become: yes # <-- больше не нужно, т.к. пользователь уже имеет права Docker + tasks: + - name: docker compose down + ansible.builtin.shell: + cmd: docker compose down + chdir: /etc/compose/resume + - name: prune system + ansible.builtin.shell: + cmd: docker system prune -a -f + - name: docker compose up -d + ansible.builtin.shell: + cmd: docker compose up -d + chdir: /etc/compose/resume diff --git a/app.py b/app.py new file mode 100644 index 0000000..011eef1 --- /dev/null +++ b/app.py @@ -0,0 +1,12 @@ +from flask import Flask, render_template +import os + +app = Flask(__name__) + +@app.route("/") +def index(): + return render_template('main.html') + +@app.route("/author") +def author(): + return render_template('author.html') diff --git a/dependencies.txt b/dependencies.txt new file mode 100644 index 0000000..4eee046 --- /dev/null +++ b/dependencies.txt @@ -0,0 +1,2 @@ +flask==3.1.2 +gunicorn diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..b531701 --- /dev/null +++ b/shell.nix @@ -0,0 +1,21 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + python3 + ruff + pyright + ]; + + shellHook = '' + if [ ! -d ".venv" ]; then + python -m venv .venv + fi + + source .venv/bin/activate + + if [ -f "dependencies.txt" ]; then + pip install -r dependencies.txt + fi + ''; +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..7745706 --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,809 @@ +:root { + --bg: #07080d; + --bg-soft: #0c0e16; + --card: rgba(255, 255, 255, 0.035); + --card-border: rgba(255, 255, 255, 0.08); + --text: #e8ecf5; + --muted: #97a0b5; + --accent: #5eead4; + --accent-2: #7c8cff; + --accent-3: #f472b6; + --radius: 18px; + --max: 1140px; + --font: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif; + --mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; } + +body { + font-family: var(--font); + background: var(--bg); + color: var(--text); + line-height: 1.65; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; +} + +a { color: inherit; text-decoration: none; } + +::selection { background: rgba(94, 234, 212, 0.25); } + +/* ---------- фон ---------- */ +.bg-glow { + position: fixed; + inset: 0; + z-index: -2; + background: + radial-gradient(600px 400px at 15% 0%, rgba(124, 140, 255, 0.18), transparent 65%), + radial-gradient(700px 500px at 85% 10%, rgba(94, 234, 212, 0.14), transparent 65%), + radial-gradient(800px 600px at 50% 110%, rgba(244, 114, 182, 0.10), transparent 70%); + animation: drift 24s ease-in-out infinite alternate; +} + +@keyframes drift { + from { transform: translate3d(-2%, -1%, 0) scale(1); } + to { transform: translate3d(2%, 2%, 0) scale(1.06); } +} + +.grid-overlay { + position: fixed; + inset: 0; + z-index: -1; + background-image: + linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,0.035) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: radial-gradient(ellipse at 50% 30%, #000 30%, transparent 85%); + -webkit-mask-image: radial-gradient(ellipse at 50% 30%, #000 30%, transparent 85%); +} + +/* ---------- хедер ---------- */ +.site-header { + position: sticky; + top: 0; + z-index: 50; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + background: rgba(7, 8, 13, 0.72); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + transition: box-shadow .3s ease; +} + +.site-header.scrolled { box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); } + +.nav { + max-width: var(--max); + margin: 0 auto; + padding: 16px 24px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} + +.logo { + display: inline-flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 1.05rem; + letter-spacing: -0.02em; +} + +.logo-mark { + font-family: var(--mono); + font-size: .8rem; + padding: 5px 9px; + border-radius: 8px; + background: linear-gradient(135deg, rgba(94, 234, 212, .2), rgba(124, 140, 255, .2)); + border: 1px solid rgba(94, 234, 212, .35); + color: var(--accent); +} + +.accent { color: var(--accent); } + +.nav-links { + display: flex; + align-items: center; + gap: 6px; + list-style: none; +} + +.nav-links a { + display: block; + padding: 8px 14px; + border-radius: 10px; + font-size: .92rem; + color: var(--muted); + transition: color .2s, background .2s; +} + +.nav-links a:hover { color: var(--text); background: rgba(255, 255, 255, 0.06); } + +.nav-cta { + color: var(--accent) !important; + border: 1px solid rgba(94, 234, 212, .35); + background: rgba(94, 234, 212, .07); +} + +.nav-cta:hover { background: rgba(94, 234, 212, .14) !important; } + +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: 0; + cursor: pointer; + padding: 6px; +} + +.nav-toggle span { + width: 22px; + height: 2px; + background: var(--text); + border-radius: 2px; + transition: transform .25s, opacity .25s; +} + +.nav-toggle.open span:nth-child(1) { transform: translateY(7px) rotate(45deg); } +.nav-toggle.open span:nth-child(2) { opacity: 0; } +.nav-toggle.open span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); } + +/* ---------- hero ---------- */ +.hero { + max-width: var(--max); + margin: 0 auto; + padding: clamp(70px, 12vw, 130px) 24px clamp(50px, 8vw, 90px); + text-align: center; +} + +.hero-badge { + display: inline-flex; + align-items: center; + gap: 9px; + font-family: var(--mono); + font-size: .78rem; + letter-spacing: .04em; + padding: 7px 15px; + border-radius: 999px; + background: rgba(255, 255, 255, .04); + border: 1px solid rgba(255, 255, 255, .1); + color: var(--muted); + margin-bottom: 26px; +} + +.pulse { + width: 8px; + height: 8px; + border-radius: 50%; + background: #34d399; + box-shadow: 0 0 0 0 rgba(52, 211, 153, .7); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 70% { box-shadow: 0 0 0 9px rgba(52, 211, 153, 0); } + 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0); } +} + +.hero-title { + font-size: clamp(2.3rem, 6.5vw, 4.3rem); + line-height: 1.1; + letter-spacing: -0.035em; + font-weight: 700; + margin-bottom: 22px; +} + +.gradient-text { + background: linear-gradient(100deg, var(--accent) 0%, var(--accent-2) 45%, var(--accent-3) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.hero-subtitle { + max-width: 660px; + margin: 0 auto 34px; + color: var(--muted); + font-size: clamp(1rem, 2vw, 1.12rem); +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 14px; + justify-content: center; + margin-bottom: 56px; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 13px 26px; + border-radius: 12px; + font-size: .95rem; + font-weight: 600; + transition: transform .2s, box-shadow .2s, background .2s, border-color .2s; +} + +.btn:active { transform: translateY(1px); } + +.btn-primary { + background: linear-gradient(135deg, var(--accent), #2dd4bf); + color: #04120f; + box-shadow: 0 8px 26px rgba(94, 234, 212, .22); +} + +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 14px 34px rgba(94, 234, 212, .32); } + +.btn-ghost { + border: 1px solid rgba(255, 255, 255, .14); + color: var(--text); + background: rgba(255, 255, 255, .03); +} + +.btn-ghost:hover { background: rgba(255, 255, 255, .08); border-color: rgba(255, 255, 255, .25); } + +.hero-stats { + display: flex; + justify-content: center; + flex-wrap: wrap; + gap: clamp(26px, 6vw, 70px); +} + +.stat { display: flex; flex-direction: column; gap: 2px; } + +.stat-num { + font-family: var(--mono); + font-size: clamp(1.7rem, 4vw, 2.4rem); + font-weight: 600; + background: linear-gradient(180deg, #fff, var(--accent)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +.stat-label { font-size: .8rem; color: var(--muted); letter-spacing: .03em; } + +/* ---------- секции ---------- */ +.section { + max-width: var(--max); + margin: 0 auto; + padding: clamp(50px, 8vw, 90px) 24px; +} + +.section-head { max-width: 700px; margin-bottom: 46px; } + +.section-tag { + display: inline-block; + font-family: var(--mono); + font-size: .78rem; + color: var(--accent); + letter-spacing: .08em; + margin-bottom: 12px; +} + +.section-title { + font-size: clamp(1.7rem, 4vw, 2.5rem); + letter-spacing: -0.03em; + line-height: 1.2; + margin-bottom: 14px; +} + +.section-desc { color: var(--muted); } + +/* ---------- карточки ---------- */ +.cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); + gap: 20px; +} + +.card { + position: relative; + display: flex; + flex-direction: column; + gap: 14px; + padding: 26px; + border-radius: var(--radius); + background: var(--card); + border: 1px solid var(--card-border); + overflow: hidden; + transition: transform .3s ease, border-color .3s ease, background .3s ease; +} + +.card::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(140deg, rgba(94, 234, 212, .10), transparent 45%); + opacity: 0; + transition: opacity .35s ease; + pointer-events: none; +} + +.card:hover { + transform: translateY(-6px); + border-color: rgba(94, 234, 212, .35); + background: rgba(255, 255, 255, .055); +} + +.card:hover::before { opacity: 1; } + +.card-icon { + font-size: 1.9rem; + width: 56px; + height: 56px; + display: grid; + place-items: center; + border-radius: 14px; + background: rgba(255, 255, 255, .05); + border: 1px solid rgba(255, 255, 255, .08); + padding: 10px; + object-fit: contain; + flex: none; +} + +.card-body h3 { + font-size: 1.22rem; + letter-spacing: -0.02em; + margin-bottom: 8px; +} + +.card-body p { font-size: .93rem; color: var(--muted); } + +.card-badge { + align-self: flex-start; + margin-top: auto; + font-family: var(--mono); + font-size: .72rem; + letter-spacing: .05em; + text-transform: uppercase; + padding: 5px 11px; + border-radius: 999px; + background: rgba(124, 140, 255, .1); + border: 1px solid rgba(124, 140, 255, .25); + color: #a9b3ff; +} + +.card-featured { + background: linear-gradient(160deg, rgba(94, 234, 212, .08), rgba(255, 255, 255, .03)); + border-color: rgba(94, 234, 212, .25); +} + +.inline-link { + color: var(--accent); + border-bottom: 1px dashed rgba(94, 234, 212, .5); + font-weight: 600; + transition: border-color .2s, color .2s; +} + +.inline-link:hover { color: #fff; border-bottom-color: #fff; } + +/* ---------- о сервере ---------- */ +.about { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 40px; + align-items: center; + padding: clamp(28px, 5vw, 48px); + border-radius: 24px; + background: rgba(255, 255, 255, .03); + border: 1px solid var(--card-border); +} + +.about-text p { color: var(--muted); margin-bottom: 16px; font-size: .97rem; } +.about-btn { margin-top: 8px; } + +img.card-icon { border-radius: 12px; } + +/* ---------- характеристики сервера ---------- */ +.about-sub { + font-size: 1.02rem; + font-weight: 600; + letter-spacing: -0.01em; + margin: 24px 0 6px; + color: var(--text); +} + +.spec-list { margin: 0; } + +.spec-row { + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: 6px 16px; + padding: 10px 0; + border-bottom: 1px dashed rgba(255, 255, 255, .08); +} + +.spec-row:last-child { border-bottom: 0; } + +.spec-row dt { + font-family: var(--mono); + font-size: .74rem; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--muted); +} + +.spec-row dd { + margin: 0; + text-align: right; + font-size: .96rem; + font-weight: 600; + color: var(--text); +} + +.about-terminal { + border-radius: 16px; + overflow: hidden; + background: #0a0d14; + border: 1px solid rgba(255, 255, 255, .1); + box-shadow: 0 24px 60px rgba(0, 0, 0, .55); +} + +.terminal-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 11px 14px; + background: rgba(255, 255, 255, .04); + border-bottom: 1px solid rgba(255, 255, 255, .07); +} + +.dot { width: 11px; height: 11px; border-radius: 50%; } +.dot.red { background: #ff5f57; } +.dot.yellow { background: #febc2e; } +.dot.green { background: #28c840; } + +.terminal-title { + margin-left: 8px; + font-family: var(--mono); + font-size: .76rem; + color: var(--muted); +} + +.terminal-body { + padding: 20px; + font-family: var(--mono); + font-size: .85rem; + line-height: 1.85; + min-height: 260px; + color: #c9d4e8; + white-space: pre-wrap; + word-break: break-word; +} + +.terminal-body .prompt { color: var(--accent); } +.terminal-body .dim { color: #6b7691; } +.terminal-body .ok { color: #4ade80; } +.terminal-body .cursor { + display: inline-block; + width: 8px; + height: 1.05em; + background: var(--accent); + vertical-align: text-bottom; + animation: blink 1s steps(1) infinite; +} + +@keyframes blink { 50% { opacity: 0; } } + +/* ---------- футер ---------- */ +.site-footer { + margin-top: clamp(40px, 7vw, 80px); + border-top: 1px solid rgba(255, 255, 255, .07); + background: rgba(255, 255, 255, .02); + backdrop-filter: blur(10px); +} + +.footer-inner { + max-width: var(--max); + margin: 0 auto; + padding: 26px 24px; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 18px; + font-size: .87rem; + color: var(--muted); +} + +.footer-left { justify-self: start; } +.footer-center { justify-self: center; text-align: center; } +.footer-right { justify-self: end; text-align: right; } + +.footer-badge { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 7px 14px; + border-radius: 999px; + background: rgba(255, 255, 255, .04); + border: 1px solid rgba(255, 255, 255, .09); +} + +.footer-center a, +.footer-right a { + color: var(--muted); + transition: color .2s; + border-bottom: 1px solid transparent; +} + +.footer-center a:hover, +.footer-right a:hover { + color: var(--accent); + border-bottom-color: rgba(94, 234, 212, .5); +} + +/* ---------- страница об авторе ---------- */ +.card-icon svg { width: 24px; height: 24px; color: var(--accent); } + +.author-hero { + max-width: 880px; + margin: 0 auto; + padding: clamp(56px, 10vw, 110px) 24px clamp(24px, 4vw, 44px); + text-align: center; +} + +.author-avatar { + display: block; + width: clamp(140px, 24vw, 190px); + height: auto; + aspect-ratio: 1; + margin: 0 auto 26px; + border-radius: 50%; + object-fit: cover; + border: 3px solid rgba(94, 234, 212, .45); + box-shadow: 0 0 0 10px rgba(94, 234, 212, .07), 0 26px 60px rgba(0, 0, 0, .55); +} + +.author-name { + font-size: clamp(1.9rem, 5vw, 3rem); + letter-spacing: -0.03em; + line-height: 1.15; + margin-bottom: 10px; +} + +.author-role { + font-family: var(--mono); + font-size: .82rem; + letter-spacing: .16em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 22px; +} + +.author-bio { + max-width: 640px; + margin: 0 auto; + color: var(--muted); + font-size: 1.02rem; +} + +.contact-groups { + display: flex; + flex-direction: column; + gap: 26px; + margin-top: 34px; +} + +.contact-group h3 { + font-family: var(--mono); + font-size: .74rem; + font-weight: 600; + letter-spacing: .16em; + text-transform: uppercase; + color: var(--muted); + text-align: center; + margin-bottom: 14px; +} + +.contact-buttons { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 0; +} + +.contact-btn { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 11px 18px; + border-radius: 12px; + background: rgba(255, 255, 255, .04); + border: 1px solid rgba(255, 255, 255, .1); + font-size: .92rem; + font-weight: 600; + transition: transform .2s ease, border-color .2s ease, background .2s ease; +} + +.contact-btn img { width: 18px; height: 18px; opacity: .9; } + +.contact-btn .handle { + font-family: var(--mono); + font-size: .8rem; + font-weight: 400; + color: var(--muted); +} + +.contact-btn:hover { + transform: translateY(-3px); + border-color: rgba(94, 234, 212, .4); + background: rgba(94, 234, 212, .08); +} + +.author-cards .card-wide { grid-column: span 2; } + +.card-body .list { + list-style: none; + display: flex; + flex-direction: column; + gap: 9px; + font-size: .93rem; + color: var(--muted); +} + +.card-body .list li::before { + content: "\203A"; + color: var(--accent); + margin-right: 9px; + font-weight: 700; +} + +.card-body strong { color: var(--text); } + +.chips { display: flex; flex-wrap: wrap; gap: 8px; } + +.chip { + font-family: var(--mono); + font-size: .76rem; + padding: 6px 12px; + border-radius: 999px; + background: rgba(124, 140, 255, .1); + border: 1px solid rgba(124, 140, 255, .25); + color: #b6beff; +} + +/* ---------- кнопка друга ---------- */ +.friend-cta { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 6px 24px clamp(40px, 7vw, 72px); + text-align: center; +} + +.friend-note { + font-family: var(--mono); + font-size: .78rem; + letter-spacing: .1em; + text-transform: uppercase; + color: var(--muted); +} + +.friend-btn { + display: inline-flex; + align-items: center; + gap: 14px; + padding: 16px 28px; + border-radius: 16px; + font-weight: 700; + font-size: 1.02rem; + background: linear-gradient(135deg, rgba(244, 114, 182, .14), rgba(124, 140, 255, .14)); + border: 1px solid rgba(244, 114, 182, .35); + transition: transform .2s ease, box-shadow .2s ease; +} + +.friend-btn:hover { + transform: translateY(-3px); + box-shadow: 0 16px 40px rgba(244, 114, 182, .22); +} + +/* ---------- нижняя строка футера / nix webring ---------- */ +.footer-bottom { + max-width: var(--max); + margin: 0 auto; + padding: 0 24px 26px; + text-align: center; +} + +.webring { + display: inline-flex; + justify-content: center; + gap: 6px; +} + +.webring a { + display: inline-block; + min-width: 42px; + font-family: var(--mono); + font-size: .82rem; + letter-spacing: .06em; + color: var(--muted); + padding: 8px 16px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, .1); + background: rgba(255, 255, 255, .03); + transition: color .2s ease, border-color .2s ease, background .2s ease; +} + +.webring a:hover { + color: var(--accent); + border-color: rgba(94, 234, 212, .4); + background: rgba(94, 234, 212, .08); +} + +.webring .webring-main { font-weight: 600; letter-spacing: .1em; } + +/* ---------- появление ---------- */ +[data-reveal] { + opacity: 0; + transform: translateY(26px); + transition: opacity .7s cubic-bezier(.22, 1, .36, 1), transform .7s cubic-bezier(.22, 1, .36, 1); +} + +[data-reveal].visible { opacity: 1; transform: none; } + +/* ---------- адаптив ---------- */ +@media (max-width: 900px) { + .about { grid-template-columns: 1fr; } + .author-cards .card-wide { grid-column: auto; } +} + +@media (max-width: 760px) { + .nav-toggle { display: flex; } + + .nav-links { + position: absolute; + top: 100%; + left: 0; + right: 0; + flex-direction: column; + align-items: stretch; + gap: 4px; + padding: 14px 20px 22px; + background: rgba(9, 10, 16, .97); + border-bottom: 1px solid rgba(255, 255, 255, .08); + transform: translateY(-14px); + opacity: 0; + pointer-events: none; + transition: opacity .25s, transform .25s; + } + + .nav-links.open { + opacity: 1; + transform: none; + pointer-events: auto; + } + + .nav-links a { padding: 12px 14px; text-align: center; } + + .footer-inner { + grid-template-columns: 1fr; + text-align: center; + gap: 14px; + } + + .footer-left, .footer-center, .footer-right { + justify-self: center; + text-align: center; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .01ms !important; + transition-duration: .01ms !important; + } + html { scroll-behavior: auto; } +} diff --git a/static/images/4get.png b/static/images/4get.png new file mode 100644 index 0000000..3029493 Binary files /dev/null and b/static/images/4get.png differ diff --git a/static/images/copyparty.svg b/static/images/copyparty.svg new file mode 100644 index 0000000..2c4f0d0 --- /dev/null +++ b/static/images/copyparty.svg @@ -0,0 +1,210 @@ + + + copyparty_logo + + + + + + + + + + + + image/svg+xml + + copyparty_logo + github.com/9001/copyparty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/images/discord.svg b/static/images/discord.svg new file mode 100644 index 0000000..be45274 --- /dev/null +++ b/static/images/discord.svg @@ -0,0 +1 @@ +Discord \ No newline at end of file diff --git a/static/images/forgejo.svg b/static/images/forgejo.svg new file mode 100644 index 0000000..9734645 --- /dev/null +++ b/static/images/forgejo.svg @@ -0,0 +1 @@ +Forgejo \ No newline at end of file diff --git a/static/images/gitAva.jpeg b/static/images/gitAva.jpeg new file mode 100644 index 0000000..38aacbb Binary files /dev/null and b/static/images/gitAva.jpeg differ diff --git a/static/images/gitea.svg b/static/images/gitea.svg new file mode 100644 index 0000000..071e514 --- /dev/null +++ b/static/images/gitea.svg @@ -0,0 +1 @@ +Gitea \ No newline at end of file diff --git a/static/images/github.svg b/static/images/github.svg new file mode 100644 index 0000000..775adcc --- /dev/null +++ b/static/images/github.svg @@ -0,0 +1 @@ +GitHub \ No newline at end of file diff --git a/static/images/grafana.svg b/static/images/grafana.svg new file mode 100644 index 0000000..c99695a --- /dev/null +++ b/static/images/grafana.svg @@ -0,0 +1 @@ +Grafana \ No newline at end of file diff --git a/static/images/matrix.svg b/static/images/matrix.svg new file mode 100644 index 0000000..034f98c --- /dev/null +++ b/static/images/matrix.svg @@ -0,0 +1 @@ +Matrix \ No newline at end of file diff --git a/static/images/sharkey.png b/static/images/sharkey.png new file mode 100644 index 0000000..1498542 Binary files /dev/null and b/static/images/sharkey.png differ diff --git a/static/images/steam.svg b/static/images/steam.svg new file mode 100644 index 0000000..bd55b41 --- /dev/null +++ b/static/images/steam.svg @@ -0,0 +1 @@ +Steam \ No newline at end of file diff --git a/static/images/telegram.svg b/static/images/telegram.svg new file mode 100644 index 0000000..aaac6e1 --- /dev/null +++ b/static/images/telegram.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..376a8dc --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,153 @@ +(() => { + "use strict"; + + /* ---------- мобильное меню ---------- */ + const toggle = document.getElementById("navToggle"); + const links = document.getElementById("navLinks"); + + if (toggle && links) { + toggle.addEventListener("click", () => { + const open = links.classList.toggle("open"); + toggle.classList.toggle("open", open); + toggle.setAttribute("aria-expanded", String(open)); + }); + + links.querySelectorAll("a").forEach((a) => + a.addEventListener("click", () => { + links.classList.remove("open"); + toggle.classList.remove("open"); + toggle.setAttribute("aria-expanded", "false"); + }) + ); + } + + /* ---------- тень хедера при скролле ---------- */ + const header = document.querySelector(".site-header"); + const onScroll = () => header && header.classList.toggle("scrolled", window.scrollY > 8); + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + + /* ---------- появление блоков ---------- */ + const reveal = document.querySelectorAll("[data-reveal]"); + if ("IntersectionObserver" in window) { + const io = new IntersectionObserver( + (entries) => { + entries.forEach((entry, i) => { + if (entry.isIntersecting) { + const el = entry.target; + setTimeout(() => el.classList.add("visible"), i * 70); + io.unobserve(el); + } + }); + }, + { threshold: 0.12, rootMargin: "0px 0px -40px 0px" } + ); + reveal.forEach((el) => io.observe(el)); + } else { + reveal.forEach((el) => el.classList.add("visible")); + } + + /* ---------- счётчики ---------- */ + const counters = document.querySelectorAll("[data-count]"); + const animateCount = (el) => { + const target = parseInt(el.dataset.count, 10); + const duration = 1400; + const start = performance.now(); + const step = (now) => { + const p = Math.min((now - start) / duration, 1); + const eased = 1 - Math.pow(1 - p, 3); + el.textContent = Math.round(target * eased).toString(); + if (p < 1) requestAnimationFrame(step); + }; + requestAnimationFrame(step); + }; + + if ("IntersectionObserver" in window) { + const co = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + animateCount(entry.target); + co.unobserve(entry.target); + } + }); + }, + { threshold: 0.5 } + ); + counters.forEach((el) => co.observe(el)); + } else { + counters.forEach((el) => (el.textContent = el.dataset.count)); + } + + /* ---------- печатающийся терминал ---------- */ + const typed = document.getElementById("typed"); + if (typed) { + const lines = [ + { t: '$ docker ps --format "table {{.Names}}\\t{{.Status}}"', d: 22 }, + { t: "\n", d: 0 }, + { t: 'NAMES STATUS\n', d: 14 }, + { t: "gitea Up 14 months\n", d: 14 }, + { t: "matrix Up 14 months\n", d: 14 }, + { t: "sharkey Up 14 months\n", d: 14 }, + { t: "copyparty Up 14 months\n", d: 14 }, + { t: "grafana Up 14 months\n", d: 14 }, + { t: "4get Up 14 months\n", d: 14 }, + { t: '$ uptime', d: 22 }, + { t: '\n all systems nominal', d: 14 }, + { t: '$ ', d: 22 }, + ]; + + // токенизируем: HTML-теги — атомарные, текст — посимвольно + const tokenize = (str) => str.match(/<[^>]+>|./gs) || []; + + let lineIndex = 0; + let tokens = []; + let pos = 0; + let html = ""; + + const render = () => { + typed.innerHTML = html + ''; + }; + + const typeNext = () => { + if (lineIndex >= lines.length) { + render(); + return; + } + + if (pos === 0) tokens = tokenize(lines[lineIndex].t); + + if (pos < tokens.length) { + html += tokens[pos++]; + render(); + const step = tokens[pos - 1].startsWith("<") + ? 0 + : lines[lineIndex].d + Math.random() * 35; + setTimeout(typeNext, step); + } else { + lineIndex++; + pos = 0; + setTimeout(typeNext, 190); + } + }; + + const startTyping = () => setTimeout(typeNext, 500); + + if ("IntersectionObserver" in window) { + const to = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + startTyping(); + to.disconnect(); + } + }); + }, + { threshold: 0.3 } + ); + to.observe(typed); + } else { + startTyping(); + } + } +})(); diff --git a/templates/author.html b/templates/author.html new file mode 100644 index 0000000..699455d --- /dev/null +++ b/templates/author.html @@ -0,0 +1,233 @@ + + + + + + + Black Raven + + + + + + + + + + + + + +
+
+ Аватар Black Raven + +

Black Raven

+

SRE / DevOps инженер

+ +

+ Привет! Собираю инфраструктуру как код, автоматизирую рутину + и держу собственный сервер — именно на нём живёт всё, + что вы видите на главной странице. Здесь коротко о том, + кто я и где меня можно найти. +

+ + +
+ +
+
+
+
+ +
+
+

Образование

+

2023–2027, ННГУ им. Лобачевского, Нижний Новгород — + программная инженерия.

+
+
+ +
+
+ +
+
+

Достижения

+
    +
  • 2025 — хакатон от ИТ-холдинга Т1, 3-е место + сертификат
  • +
  • 2026 — ИТ-марафон «Кейс Лаб», направление + «Облачные сервисы» + сертификат
  • +
+
+
+ +
+
+ +
+
+

Языки

+
    +
  • Английский — B2
  • +
  • Французский — A1?
  • +
+
+
+ +
+
+ +
+
+

Обо мне

+

Ответственный и пунктуальный, люблю своё дело, + умею работать в команде и открыт к обмену опытом.

+
+
+ +
+
+ +
+
+

Навыки

+
+ Linux + Docker / Compose + Ansible + Terraform + Git + GitHub Actions + Bash + Python + Kubernetes + Argo CD + Vim + сеть и безопасность +
+
+
+ +
+
+ +
+
+

Проекты

+
    +
  • Terraform — инфраструктура как код: + final_assessment
  • +
  • Argo CD — GitOps-доставка в Kubernetes: + lab4
  • +
  • Ansible + Docker — конфигурация серверов и контейнеризация: + MyResume
  • +
+
+
+
+
+ +
+

Также рекомендую ознакомиться

+ + risdeveau.lair.moe + +
+
+ + + + + + diff --git a/templates/main.html b/templates/main.html new file mode 100644 index 0000000..1afbbbc --- /dev/null +++ b/templates/main.html @@ -0,0 +1,220 @@ + + + + + + + srcoder.moe — мой сервер + + + + + + + + + + + + + +
+
+

+ Привет, это srcoder.moe +

+ +

+ Самостоятельно развернутый и поддерживаемый мной домашний сервер. + Хостинг сервисов для разработки, общения, хранения файлов и мониторинга — + всё крутится у меня, а не у чужого провайдера. +

+ + + +
+
0сервисов
+
0часа аптайма / 7 дней
+
0год запуска
+
+
+ +
+
+ +

Что развёрнуто на сервере

+

Шесть сервисов, закрывающих все базовые потребности — от кода до поиска.

+
+ +
+
+ +
+

Gitea

+

Лёгкая самохостируемая платформа для хранения git-репозиториев: + Issues, Pull Requests, вики, CI и права доступа — весь процесс + разработки живёт на моём домене, без зависимости от внешних хостингов.

+
+ git / разработка +
+ +
+ +
+

Matrix

+

Открытый децентрализованный протокол обмена сообщениями в реальном времени — + по сути независимый мессенджер с сквозным шифрованием, комнатами и звонками. + Свои данные сервер хранит сам, а общение идёт напрямую между участниками.

+
+ мессенджер +
+ +
+ +
+

Sharkey

+

Современный форк федерированной соцсети Misskey: микроблоги, реакции, + сторис и ленты. Через протокол ActivityPub аккаунты общаются с другими + инстансами Fediverse — то есть это моя собственная соцсеть с мостом + в Mastodon и соседние сервера.

+
+ соцсеть +
+ +
+ +
+

Copyparty

+

Самописный файловый сервер с веб-интерфейсом: загрузка, шаринг, + инстант-загрузчики, интегрированный файловый браузер и даже + возможность стримить звук. Простое и быстрое хранилище + для любых файлов — от бэкапов до публичных дистрибутивов.

+
+ файлы +
+ +
+ +
+

Grafana

+

Система мониторинга и визуализации: собирает метрики сервера — + нагрузку CPU, память, диск, трафик и здоровье сервисов — + и рисует их живыми дашбордами. Благодаря ей я вижу, что происходит + с машиной, ещё до того, как что-то сломается.

+
+ мониторинг +
+ + +
+
+ +
+
+
+ + +

+ Хостер моего сервера — Play2Go: + он предоставляет хорошие тарифы на виртуальные серверы по доступной цене + и современное оборудование. Из минусов — иногда случаются проблемы с сетью, + но в целом для моих задач подходит хорошо. +

+ +

Характеристики сервера

+
+
+
Процессор
+
2 vCPU AMD Ryzen 9 5950X
+
+
+
Оперативная память
+
4 GB DDR4
+
+
+
Хранилище
+
120 GB NVMe SSD
+
+
+
Скорость сети
+
До 1 Gbit/s
+
+
+
Защита от DDoS
+
Мощная защита от атак L3–L4
+
+
+ + Страница об авторе → +
+ + +
+ +
+
+ + + + + +