Files
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

223 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Сайт-визитка Амана Ибраша
Личный сайт для закрепа в профилях соцсетей: человек заходит, видит, кто это, и может написать. Дальше есть разделы про архитектуру, проекты и хоумлаб.
Чистый 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, его можно править руками.
```bash
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`, поэтому телефонам идут лёгкие. Рецепт:
```bash
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`)
```bash
./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)
```bash
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`):
```bash
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. Проверка перед выкладкой
```bash
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, песочница, паркур).