aibrashandClaude Sonnet 5.5 6ddece7017
deploy / deploy (push) Successful in 4s
Кнопка «Помахать» с общим счётчиком, домен, шапка
- Главная: тихая кнопка «Помахать» (web/js/wave.js), общий счётчик для всех посетителей
- server/server.mjs: мини-API (/api/waves, /api/wave), счётчик хранится вне репозитория
  (/var/lib/vizitka-site/waves.json), лимит раз в секунду с IP, проверка Origin
- deploy: systemd-сервис vizitka-api, проксирование /api в nginx, setup-server.sh ставит nodejs
- CI: после git pull перезапускает API
- Домен https://me.ibrash.duckdns.org вместо заглушки, исправлен пробел в шапке
- README и заметки обновлены

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 16:55:30 +05:00

Сайт-визитка Амана Ибраша

Личный сайт для закрепа в профилях соцсетей: человек заходит, видит, кто это, и может написать. Дальше есть разделы про архитектуру, проекты и хоумлаб. Чистый HTML + CSS + JS, без сборщиков и фреймворков, без бэкенда. Статика раздаётся nginx.

  • Репозиторий: git@git.ibrash.duckdns.org:aibrash/vizitka-site.git (Gitea 1.27), ветка main
  • Сервер: root@192.168.10.112 (Debian 12, имя хоста visitka-site): клон репозитория в /opt/vizitka-site, nginx отдаёт его папку web/, пока по HTTP внутри сети
  • Домен: https://me.ibrash.duckdns.org (HTTPS терминирует отдельный прокси на openresty перед сервером). Он прописан в OG-тегах, sitemap.xml и robots.txt; как сменить: раздел 5

1. Стилистика сайта

Если добавляешь страницу или блок, держись этих правил: сайт должен остаться одним целым.

Настроение

Светлый, воздушный, дружелюбный минимализм. Не «портфолио архитектора» и не «хакерский терминал» (оба варианта пробовали и от них отказались). Это визитка парня, который живёт «весело и всерьёз»: тренировки, работа, фикс, учёба. Картинок мало, но хорошие.

Палитра (токены в web/css/style.css, блок :root)

Токен Цвет Для чего
--bg #f4f6fb фон страницы
--card #ffffff карточки, плитки, подложки
--ink #121a2b основной текст и заголовки
--muted #55627a второстепенный текст (контраст 5,5:1 на фоне)
--line #e2e8f3 рамки и разделители
--accent #3556ff кнопки, ссылки, активные пункты, цифры
--accent-soft #e8edff фон чипов и иконок
--pop #ffd84d «солнечный» жёлтый: аватар и пятно на фоне
--good #1fb865 статус «на связи»

Фон: два размытых цветных пятна (.blob--a жёлтое, .blob--b голубое), больше ничего. Тёмной темы нет, color-scheme: light.

Шрифт

Только Onest (variable, wght 100..900), локально: web/fonts/onest-{cyrillic,latin}-wght-normal.woff2. Заголовки 700 и 800 с отрицательным letter-spacing (-.02em и меньше), текст 400. Никаких внешних шрифтов и CDN.

Форма

Всё скруглённое: картинки и плитки 22 px, крупные карточки 24 до 28 px, ряды ссылок 18 px, чипы и кнопки капсулой (99px). Одна общая мягкая тень --shadow. Рамок почти нет, глубину даёт тень.

Раскладка

  • Mobile first. Проверять на ширине 360 px (и 320): горизонтального скролла быть не должно.
  • Главная: на телефоне одна колонка, на ПК две (слева «обо мне», справа карточка «Давай общаться»).
  • Внутренние страницы: контейнер .page--wide (до 62 rem), текст ограничен 44 rem ради читаемости.
  • Тапаемые элементы не меньше 44 px.

Анимации

Должны быть красивыми, но дешёвыми для слабого телефона: только transform и opacity.

  • Главная: одна «сцена» входа (аватар «выпрыгивает», слова строки о себе появляются по очереди, теги появляются и потом чуть парят, фон дрейфует, «волна» у приветствия).
  • Внутренние страницы: появление блоков по мере прокрутки (animation-timeline: view(), в браузерах без поддержки просто статика), картинки «оседают» (scale 1,12 → 1), числа в карточках проекта «набегают» от нуля (web/js/count.js), карточки и картинки приподнимаются или плавно увеличиваются при наведении, блик на кнопках ссылок, переходы между страницами (@view-transition).
  • Первый экран страницы (обложка проекта, шапка) не анимируется появлением: иначе просядет LCP.
  • Обязательно уважать prefers-reduced-motion: reduce (в CSS всё выключается одним правилом).

Тон текстов

Живой, лёгкий, без пафоса и канцелярита. Русский язык. К посетителю на «ты» («Хочешь пообщаться? Пиши»), о себе от первого лица. Короткие предложения, лёгкая самоирония («Скучать некогда»), заголовки с заглавной только первого слова. Эмодзи только в тегах и фактах на главной. Не выдумывать факты о владельце (см. раздел 3): если чего-то не знаешь, пиши нейтрально или помечай TODO.

Требования к качеству

  • Lighthouse mobile: performance 90+ на каждой странице (сейчас от 93 до 100), доступность, практики и SEO по 100.
  • Строгий CSP: никаких inline-скриптов и inline-стилей, никаких внешних ресурсов. Стили элементам из JS задаются через element.style.setProperty (это разрешено).
  • Картинки: width и height обязательны (нет сдвигов), loading="lazy" для всего ниже первого экрана, у обложки fetchpriority="high".

2. Как устроен сайт

web/                  то, что уходит на сервер (nginx root)
  index.html          главная (body[data-hide]: скрыта, пока не загрузится config.json)
  config.json         ВСЁ содержимое главной и меню: имя, факты, теги, статус, ссылки, фото, список страниц
  css/style.css       единственный файл стилей
  js/boot.js          в <head>: флаг .js, запрос config.json, window.siteReady
  js/card.js          заполняет главную из config.json
  js/nav.js           верхнее меню из config.pages (скрывает draft), подсветка текущего раздела
  js/count.js         числа «набегают» при появлении
  js/wave.js          кнопка «Помахать» со счётчиком на главной (говорит с /api)
  arch/               список проектов + arch/<проект>/ (три страницы)
  projects/           список + projects/amans-world/ + projects/homelab/
  training/ work/ fixed/   ЧЕРНОВИКИ будущих страниц (draft, noindex), в меню не видны
  img/                me.webp, arch/ (рендеры и чертежи), proj/ (скриншоты игры, иллюстрация хоумлаба)
  og/                 превью для соцсетей 1200x630 (png/jpg)
  fonts/              woff2 Onest и лицензия
  404.html, favicon.svg, robots.txt, sitemap.xml
templates/page.html   шаблон новой страницы
server/               server.mjs: мини-API счётчика «Помахать» (Node, без зависимостей)
tools/                gen_arch.py, gen_projects.py: генераторы страниц архитектуры и проектов
deploy/               nginx-site.conf, vizitka-api.service, setup-server.sh (разовая настройка сервера)
.gitea/workflows/deploy.yml   CI

Главная

Содержимое берётся из web/config.json (имя, photo, status, tagline, facts, tags, cta, email, socials). Если photo пустое, показываются инициалы. До загрузки конфига страница скрыта (html.js:not(.ready) body[data-hide] .page), чтобы вёрстка не прыгала; без JS всё видно сразу. Хоумлаб на главной не упоминается намеренно (так просил владелец).

Кнопка «Помахать» (интерактив на главной)

Тихая «призрачная» кнопка внизу карточки «Давай общаться»: прозрачная, тонкая рамка, цвет --muted, не конкурирует с ссылками. При клике ладонь машет (@keyframes wave, как в приветствии), число плавно докручивается, всплывает маленькое «+1». Счётчик общий для всех посетителей.

  • Фронтенд: web/js/wave.js и блок .wave-row в index.html. Если API недоступно (например, при просмотре через python3 -m http.server), блок прячется (visibility:hidden, место зарезервировано, чтобы вёрстка не прыгала).
  • Бэкенд: server/server.mjs, GET /api/waves и POST /api/wave. Лимит: не чаще раза в секунду с одного IP и не больше 300 в минуту на всех; POST принимается только с разрешённых сайтов (ALLOWED_ORIGINS); тело запроса игнорируется, от клиента данные не берутся.
  • Счётчик не сбрасывается: хранится в /var/lib/vizitka-site/waves.json (создаётся systemd, вне репозитория), пишется атомарно, перед каждой записью предыдущее значение копируется в waves.json.bak, при остановке сервиса значение сохраняется. git pull, пуши и перезапуски сервера его не трогают. Не клади файл счётчика внутрь репозитория.
  • Сервис vizitka-api (systemd, DynamicUser, песочница, MemoryMax=96M), настройки в /etc/vizitka-api.env (создаются один раз и потом не перезаписываются), nginx проксирует /api/ на 127.0.0.1:8787 и передаёт IP посетителя (X-Client-IP).
  • Локальная проверка: DATA_FILE=/tmp/waves.json STATIC_DIR=web PORT=8791 node server/server.mjs и открыть http://localhost:8791.
  • Править текст кнопки и числа: прямо в web/index.html и web/js/wave.js.

Меню и страницы

Меню рисует js/nav.js из config.json → pages. На главной оно появляется, только если есть опубликованные страницы. Внутренние страницы всегда показывают имя и пункты. На узких экранах фамилия в шапке прячется (чтобы влезли все пункты).

Новая страница:

  1. Скопируй templates/page.html в web/<имя>/index.html, поменяй title, description, og:*, текст.
  2. В web/config.json в pages добавь { "label": "Название", "url": "/<имя>/" }.
  3. Добавь адрес в web/sitemap.xml.

Черновики (training, work, fixed): в pages у них "draft": true, в самой странице есть <meta name="robots" content="noindex">. Чтобы опубликовать: убери draft, удали noindex, добавь в sitemap. Страницу можно не вносить в меню (так сделан хоумлаб: он открывается из карточки на /projects/).

Страницы архитектуры и проектов

Они сгенерированы скриптами, но результат обычный HTML, его можно править руками.

python3 tools/gen_arch.py       # /arch/ и /arch/<проект>/
python3 tools/gen_projects.py   # /projects/, /projects/amans-world/, /projects/homelab/

Тексты, подписи и списки картинок лежат прямо в скриптах (CASES в gen_arch.py, секции в gen_projects.py). Скрипты идемпотентны: после запуска git status не должен показывать изменений, если ничего не правил. Дописывают недостающие адреса в sitemap.xml, остальное не трогают.

Картинки

В web/img/arch/ и web/img/proj/ каждая картинка в трёх размерах: имя.webp (1200 до 1600 px по ширине), имя-md.webp (960), имя-sm.webp (640). Страницы отдают их через srcset, поэтому телефонам идут лёгкие. Рецепт:

magick src.png -strip -resize 1400x1400\> -quality 78 имя.webp
magick src.png -strip -resize 960x960\>   -quality 76 имя-md.webp
magick src.png -strip -resize 640x640\>   -quality 74 имя-sm.webp

Для чертежей (много мелкого текста) качество 80. Фото-подобные рендеры сжимаются хорошо (от 150 до 230 КБ при 1200 px).


3. Что известно о владельце (источник правды для текстов)

Известно и можно использовать: Аман Ибраш, 18 лет, студент KazGASA в Алматы (архитектура), работает системным администратором и растёт в сторону DevOps, любит тренировки, фикс (фиксед гир) и работу. Дома хоумлаб (домашний сервер, на нём Nextcloud; адреса через DuckDNS; браузерная игра Aman's World; в планах переезд этого сайта на сервер и CI). Сам пользуется Fedora Linux (на своём компьютере, про ОС сервера ничего не известно, на сервере сейчас Debian 12). Контакты, соцсети и e-mail лежат в web/config.json.

Не известно, не выдумывать: годы обучения, места работы, технологический стек DevOps, цифры тренировок и велосипеда, детали игры сверх написанного.

Нельзя публиковать: внутренние адреса и IP, имена хостов, данные работодателя, адрес и содержимое закрытого Nextcloud, имена заказчиков из папок с проектами. Страница хоумлаба называет только сервисы.

Откуда материалы

  • Архитектурные проекты из Nextcloud владельца: готовые рендеры «Тихого двора» лежат в …/2026-09_ИЖД_Аман_Ибраш/03_Out/02_Рендеры/, схемы и аксонометрии в …/02_Work/05_Presentation/img/; павильон и «Лофт» вырезаны из PDF-альбомов в 03_Out/01_PDF/. Ссылка-доступ к Nextcloud не хранится в репозитории.
  • Это студенческие работы. Про то, как сделаны рендеры дома, на сайте ничего не утверждается.
  • Скриншоты игры: заставка и хаб сняты автоматически, два кадра гонок прислал владелец.

4. Игра Aman's World (что известно)

3D-игра на Godot 4 (веб-экспорт, около 40 МБ при первом запуске), адрес https://grid.ibrash.duckdns.org/. Работает только на ПК, можно играть клавиатурой и мышью или геймпадом (вроде Xbox). Неоновый хаб с 5 порталами (Sandbox, Backrooms, ещё один, Race, Race 2), ник до 16 символов, над гоночными порталами таблицы рекордов. Sector 3 Run (Race): 8 чекпоинтов; Ascend Only (Race 2): 13 чекпоинтов; перед стартом «R: готов (0/1)». По звукам и файлам: бег по стенам, прыжок от стены, рывок, скольжение, двойной прыжок, подтягивание на уступ, ускорители. Детали геймплея я только выводил из интерфейса и ассетов, владелец формулировки не проверял целиком.


5. Деплой и CI

Схема (простая)

push в main → Gitea Actions (.gitea/workflows/deploy.yml) заходит по ssh на root@192.168.10.112, делает git pull в /opt/vizitka-site и перезапускает API счётчика (systemctl restart vizitka-api) → nginx раздаёт /opt/vizitka-site/web. Для статики перезапуск не нужен; сборки нет: что лежит в web/, то и на сайте.

Репозиторий публичный, поэтому сервер клонирует и обновляет его по HTTPS без ключей и паролей: https://git.ibrash.duckdns.org/aibrash/vizitka-site.git. Если репозиторий станет закрытым, на сервер придётся добавить deploy-ключ (в Gitea: репозиторий → Настройки → Deploy keys) или токен.

Что настроено на сервере (сделано, deploy/setup-server.sh)

./deploy/setup-server.sh root@192.168.10.112   # ставит nginx, git и nodejs, клонирует репозиторий в /opt/vizitka-site, включает сервис vizitka-api и подключает deploy/nginx-site.conf

Скрипт безопасно запускать повторно: если клон уже есть, он делает git pull, обновляет systemd-юнит и заново применяет конфиг nginx (/etc/vizitka-api.env не перезаписывается, счётчик не трогается) (там root /opt/vizitka-site/web, сжатие, кэш, 404, строгий CSP, скрытые файлы закрыты). Если нужно поменять конфиг nginx: правь deploy/nginx-site.conf и снова запусти скрипт. Если /opt/vizitka-site уже был не репозиторием, скрипт откладывает его в /opt/vizitka-site.old-<время>, а не удаляет (одна такая папка там сейчас лежит, это старая выкладка через rsync, её можно удалить).

Что нужно для работы CI

  1. Секрет DEPLOY_SSH_KEY в Gitea (репозиторий → Настройки → Actions → Секреты): приватный ssh-ключ, у которого есть доступ к root@192.168.10.112. Владелец положил туда свой ключ (он уже добавлен). Ключ с правами root: безопаснее отдельный ключ только для CI (ssh-keygen -t ed25519 -N "" -C gitea-ci, публичную часть добавить в /root/.ssh/authorized_keys с опциями no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty).
  2. Runner act_runner с меткой ubuntu-latest, который видит 192.168.10.112 по ssh (порт 22), то есть стоит в той же сети. Раннеру нужен только ssh-клиент (workflow доустановит openssh-client, если его нет), actions/checkout не используется. Есть ли у владельца runner, на момент написания неизвестно: без него workflow просто ждёт в очереди.
  3. Отпечаток ключа сервера закреплён прямо в workflow (known_hosts). Если сервер переустановят, обновить: ssh-keyscan -t ed25519 192.168.10.112 и подставить строку в deploy.yml.

Быстро выложить вручную (без CI)

ssh root@192.168.10.112 'cd /opt/vizitka-site && git pull --ff-only'

Проверка, что сайт жив: curl -I http://192.168.10.112/ (ожидается 200). Откат: ssh root@192.168.10.112 'cd /opt/vizitka-site && git reset --hard <коммит>'.

HTTPS и домен (позже)

Сейчас сервер отдаёт HTTP только внутри сети. Для публичного доступа лучше поставить перед ним обратный прокси (Caddy или nginx на отдельном хосте с сертификатом) или Cloudflare Tunnel, а сам сервер с сайтом не выставлять наружу напрямую рядом с остальными домашними устройствами. Домен me.ibrash.duckdns.org уже подставлен в OG-теги, sitemap.xml, robots.txt и в tools/. Если домен сменится, замени один раз и закоммить (и поправь ORIGINS/ALLOWED_ORIGINS для API: /etc/vizitka-api.env, потом systemctl restart vizitka-api):

grep -rlF 'https://me.ibrash.duckdns.org' web tools | xargs sed -i 's|https://me.ibrash.duckdns.org|https://НОВЫЙ.ДОМЕН|g'

Реальный IP посетителя nginx берёт из X-Forwarded-For (set_real_ip_from для частных сетей в deploy/nginx-site.conf): если передний прокси поменяется и не будет присылать этот заголовок, лимит «раз в секунду» станет общим на всех, проверь по access.log.


6. Проверка перед выкладкой

python3 -m http.server 8787 --directory web      # локальный просмотр: http://localhost:8787
  • Страницы открываются, в консоли нет ошибок (в том числе нарушений CSP), нет горизонтального скролла на 360 px, нет битых картинок.
  • Lighthouse mobile (через Chromium от Playwright: CHROME_PATH=…/chrome npx lighthouse URL --only-categories=performance,accessibility,best-practices,seo) на каждой странице: 90+.
  • После tools/gen_*.py git status чистый.
  • Быстрый осмотр после выкладки: curl -I http://192.168.10.112/ и открыть сайт на телефоне (выкладка идёт через git pull, значит, на сервер попадает только закоммиченное и запушенное).

7. Заметки для следующих сессий (в том числе для ИИ-ассистента)

Предпочтения владельца. Минимализм (его слова: «верни минимализм, мне нравилось»), светлый и приятный дизайн, упор на мобильную версию, «красивые анимации», только реально хорошие фото. Просил упрощать, когда становилось сложно. Хочет, чтобы всё лишнее удаляли, но без потерь для нужного: однажды удалил папки со старыми версиями сам, а потом спрашивал «куда всё делось». Перед удалением чего-либо в проекте лучше спросить. Коммитить и пушить только по прямой просьбе.

Окружение.

  • Оболочка zsh: незакавыченный глоб (--include=*.html) падает с «no matches found», берите в кавычки.
  • pkill -f "шаблон" убивает собственную оболочку, если шаблон есть в самой команде. Останавливайте процессы по порту (fuser -k 8787/tcp) или по PID.
  • cp и ls в интерактивном окружении могут быть алиасами (запрос на перезапись, цветной вывод): используйте command cp -f, command ls.
  • Рабочий каталог сбрасывается после каждой команды, пишите абсолютные пути.
  • Система может блокировать действия вроде добавления ключей в authorized_keys на сервере. Это нормально: подготовьте всё остальное и скажите владельцу, что сделать руками.
  • Headless Chromium из Playwright по умолчанию светлый, поэтому для проверок подходит; Lighthouse можно запускать с его бинарником.

Ловушки в вёрстке.

  • Вложенные display: grid раздувают колонку по содержимому: ставьте grid-template-columns: minmax(0, 1fr), иначе карточка выходит за край на телефоне.
  • Анимацию появления по прокрутке нельзя вешать на первый кадр страницы (LCP вырастает в разы): есть исключения для .gallery--one, .shot--lead первого проекта.
  • Страницы с данными из JSON прячутся до загрузки только на главной (body[data-hide]); на внутренних страницах has-nav проставлен прямо в HTML, чтобы меню не двигало вёрстку.
  • @view-transition и animation-timeline: view() работают не везде; везде есть запасной вариант без анимации.

Что осталось сделать (на момент написания).

  1. Убедиться, что в Gitea есть runner и CI срабатывает на пуш (секрет DEPLOY_SSH_KEY уже добавлен). Удалить с сервера /opt/vizitka-site.old-*.
  2. Проверить, что кнопка «Помахать» в проде получает реальные IP посетителей (смотреть /var/log/nginx/access.log) и что счётчик переживает перезапуск (systemctl restart vizitka-api).
  3. Реальные ники (в config.json у Telegram и Instagram стоят короткие ники и их handle могут быть временными), проверка текстов про игру и про хоумлаб.
  4. Страницы Тренировки, Работа, Фикс (черновики уже лежат в web/).
  5. Фото сервера для хоумлаба вместо иллюстрации, больше кадров игры (Backrooms, песочница, паркур).
S
Description
No description provided
Readme
4.9 MiB
Languages
HTML 45%
Python 21.4%
CSS 19.4%
JavaScript 12.1%
Shell 2.1%