Инфраструктура для сайта
Задача была простой: настроить всю инфраструктуру для друга, чтобы он смог просто писать запросы нейросетям, — а далее магия пайплайнов)

Автоматизированный конвейер разработки
Опыт настройки и архитектура CI/CD
Инфраструктуру настраивал впервые в таком масштабе, поэтому решил попробовать стратегию Blue/Green, чтобы при деплое не падал прод — она плавно переключает поток пользователей.
Для этого проекта был поднят self-hosted GitLab. Причин, почему я решил его развернуть, было несколько:
- Хотел сам пощупать инструмент изнутри и расширить функционал.
- У друга никак не получалось зарегистрироваться на официальном GitLab ))) Тут я просто создал ему аккаунт, и проблема решилась.
Конфигурация раннеров (GitLab Runners)
Мне было интересно протестировать работу раннеров на разных машинах, поэтому разделил их логически:
- Универсальный раннер (тип
docker): Когда я изучал Ansible, то выделил под него отдельную виртуалку (думал, что это какой-то огромный сервис). Виртуалка осталась, и я решил разместить туда раннер для непрерывной интеграции кода. Там сейчас происходят проверки и сборка. - Прод-раннер (тип
shell): Этот раннер крутится прямо на виртуалке с сайтом. На продакшене я просто установил Docker Engine.
Как устроен процесс CI/CD
Сначала выполняются базовые этапы:
test— проверяет код линтером (с правом на ошибку благодаряallow_failure).build— собирает проект в папкуdist/.
Вся основная магия происходит на этапе деплоя, где три джобы последовательно передают друг другу эстафету через needs и файл deploy.env:
deploy— определяет, какой цвет сейчас активен (blue или green), поднимает в Docker новую версию приложения на соседнем порту и проверяет её «здоровье» черезcurl.switch_proxy— если контейнер ожил, эта джоба внутри Docker-контейнера с Ansible декодирует SSH-ключ из Base64 и переключает трафик внешнего прокси-сервера на новый порт.delete_old_color— напоследок выдерживает паузу в 15 секунд, чтобы старый контейнер успел обработать долетающие запросы, после чего глушит его и чистит дисковый кэш Docker.
Итог: Получилась отличная отказоустойчивая система, которая не уронит прод, даже если новая сборка окажется битой.



Интеграция с ИИ
+- так выглядела версия без пайплана при помощи cloude code - запрос клоду - коммит - ручной деплой на сервере по ssh

CI/CD
stages:
- test
- build
- deploy
default:
image: node:20-alpine
tags:
- cringe
cache:
key:
files:
- package-lock.json
paths:
- .npm/
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
test:
stage: test
script:
- npm ci
- npm run lint
allow_failure: true ## Добавил чтобы если что пайплайн не упал / нейронки любят добавлять странные пробелы ковычки / для текущего стека не критично / пока не знаю как правильно сделать
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/ ### будем использовать его в стейдже build ###
expire_in: 1 day
deploy:
stage: deploy
tags:
- shell
artifacts:
reports:
dotenv: deploy.env ## Нужен из-за того что у меня несколько ранеров на разных аиртуалках - надо как-то переносить переменные внутри нашел вот такой вот параметр) вынес наверх потому-что мой IDE постоянно ругается на него если он внизу ###
### Копируем переменные из GitLab в .env ###
script:
- echo "POSTGRES_USER=$POSTGRES_USER" > .env
- echo "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" >> .env
- echo "POSTGRES_DB=$POSTGRES_DB" >> .env
- echo "JWT_SECRET=$JWT_SECRET" >> .env
- echo "ADMIN_PASSWORD=$ADMIN_PASSWORD" >> .env
- | ### Проверяем какой цвет сейчас активев и записываем в переменные порт, текщий цвет, свободный цвет ###
if docker ps --format '{{.Names}}' | grep -q "web_blue"; then
CURRENT_COLOR="blue"
NEXT_COLOR="green"
NEXT_PORT="81"
else
CURRENT_COLOR="green"
NEXT_COLOR="blue"
NEXT_PORT="80"
fi
- docker compose up -d --build api_${NEXT_COLOR} web_${NEXT_COLOR}
- | ### проверка что бэкенд реально ожил + попутно записываем переменные для джоба переключения прокси и выключения старых цветов ###
for i in $(seq 1 30); do
if curl -sf http://localhost:${NEXT_PORT}/api/health > /dev/null; then
echo "API healthy"
echo "TARGET_PORT=${NEXT_PORT}" > deploy.env
echo "OLD_COLOR=${CURRENT_COLOR}" >> deploy.env
exit 0
fi
sleep 1
done
echo "API упал при старте!" && exit 1
switch_proxy:
stage: deploy
image: willhallonline/ansible:2.18-alpine-3.18 ### выбрал так-как тут есть ssh встроенный ) ###
tags:
- cringe
needs:
- deploy
script: ### возможно есть решение более правильное - была проблема с переменными из гитлаба я не мог просто взять и записать приватный ключи в нее из-за специфики гитлаба - решение просто закодировать файл в base64!) а уже на ранере раскодировать)###
- mkdir -p ~/.ssh
- echo "$PROXY_SSH_KEY" | base64 -d > ~/.ssh/id_deploy
- chmod 600 ~/.ssh/id_deploy
- ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i ansible/hosts.ini ansible/switch_proxy.yml --private-key=~/.ssh/id_deploy --extra-vars "target_port=${TARGET_PORT}"
delete_old_color:
stage: deploy
tags:
- shell
needs:
- switch_proxy
- deploy ### чтобы забрать артефакты/переменные из deploy.env
script:
- sleep 15
- echo "Выключаем старый стек ${OLD_COLOR}..."
- docker compose stop api_${OLD_COLOR} web_${OLD_COLOR}
- docker image prune -f
Если интересно что там нейронка кодит то вот )
egnirC — портфолио
3D-художник: персонажи и синематики. Сайт в стиле «рваной бумаги»: чертёжная синька, тёплый крафт, ransom-note типографика, скотч, штампы, рукописные пометки.
Стек
- React 18 — компоненты
- Vite — дев-сервер + сборка
- Чистый CSS + SVG — никаких UI-библиотек
- Google Fonts: Anton, Special Elite, Caveat, Playfair Display, Courier Prime
- Анимации через CSS +
IntersectionObserver(хукuseReveal)
Запуск
npm install
npm run dev # http://localhost:5173
npm run build # production build → dist/
npm run preview # превью собранной версии
Структура
.
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.jsx # точка входа
├── App.jsx # компоновка секций
├── hooks/
│ └── useReveal.js # IntersectionObserver → CSS-класс .in
├── components/
│ ├── Nav.jsx # липкая «скотч-лента» с подсветкой активной секции
│ ├── Hero.jsx # ransom-title + SVG character sheet + спецификация
│ ├── Reels.jsx # 1 hero + 3 small, кликом меняется hero
│ ├── Process.jsx # 5 стадий пайплайна со стрелками
│ ├── Clients.jsx # бегущая строка визиток
│ └── About.jsx # blurb + контакты
└── styles/
├── paper.css # дизайн-токены + текстуры + ransom + tape + stamp
└── app.css # секции, hero, reels, анимации, респонсив
Дизайн-система
Палитра
| токен | hex | назначение |
|---|---|---|
--blueprint | #1e436b | чертёжный фон, hero |
--kraft | #c9a876 | тёплый крафт, акценты |
--paper | #f4ead5 | основная бумага |
--ink | #181513 | текст, тёмные секции |
--stamp-red | #c63d2f | штампы, акцент, hover |
--highlighter | #ffe066 | стикеры, маркер |
Шрифты (ransom-note mix)
Anton— плакатные заголовкиSpecial Elite— печатная машинка, меткиCaveat— рукописные пометкиPlayfair Display— серьёзный курсив-серифCourier Prime— моноширинный текст
Эффекты
- Рваные края —
.torn-allчерезclip-path: polygon(...) - Скотч-полоски —
.tape.tape-tl/tr/bl/br/top - Штамп —
.stamp(двойной бордер, поворот, шум) - Складки бумаги —
.fold-h/v(градиент-линия) - Текстуры — SVG-noise через data-URI
Подмена контента
- Реальные кадры: замени
.placeholder-blueprint/.placeholder-imgна<img>или<video>вReels.jsx,Process.jsx,Hero.jsx. - Имя / описание: правь массивы констант в начале каждого компонента (
REELS,STAGES,CLIENTS,CONTACTS,TAGS). - Цвета / шрифты: токены в
:rootфайлаsrc/styles/paper.css.
Что ещё можно прикрутить
framer-motionилиgsapдля более сложных анимаций бумаги- Реальный плеер видео (Vimeo embed /
<video>) - Тёмная/светлая тема через переключение
data-theme - Lazy-loading и optimization картинок (Vite уже умеет)
- Деплой на Vercel / Netlify в один клик
DOCS — egnirC portfolio
Техническая документация по коду. Обзорное описание и дизайн-систему см. в README.md.
Запуск
npm install
npm run dev # vite dev-сервер, http://localhost:5173 (открывается сам)
npm run build # production-сборка → dist/
npm run preview # локальный просмотр собранного dist/
Требования: Node 18+ (Vite 5). Зависимости — только react / react-dom; всё
остальное — devDependencies (vite, @vitejs/plugin-react).
Поток приложения
index.html → src/main.jsx → src/App.jsx
│
├─ <SplashScreen> (egnirC Editor loader, до loading=false)
├─ <site-stage> (контентные секции: Nav/Hero/Reels/…)
├─ <render-btn> (большая синяя кнопка → запускает crash-sequence)
├─ desktop-overlay (обои + ярлыки, после "крэша")
└─ диалоги (err-dialog / crash-reporter / app-windows / program-windows)
src/main.jsx импортирует CSS-файлы в порядке: base.css → reactions.css → splash.css → crash.css → programs.css.
State-машина App.jsx
loading=true ─┐
│ splash done
▼
step="idle" ← (Render-button clicked) ─┐
│ │
│ startCrash() │ goIdle() (X на err-dialog)
▼ │
step="error" ──(1s)──► step="freeze" ─┘
│ │
│ onOk() / 5.6s timeout │ onOk()
▼ ▼
step="crash" (UE crash reporter)
│
│ onClose() (X / Close Without Sending)
▼
step="desktop" (обои + ярлыки + интерактивные окна)
│
│ UE icon double-click
▼
loading=true → splash → step="idle"
body.classList — источник истины для CSS-каскада: crashing / crashing-dark /
desktop-mode / loading. Transitions на .site-stage и .freeze-overlay
включены только при body.crashing — выход из крэша щёлкает мгновенно
(см. crash.css снап-закрытие).
Сайт: контентные секции (src/components/)
Все компоненты — функциональные, без пропсов; контент задаётся константами в начале файла. Чтобы поменять данные сайта — правьте эти массивы.
| Файл | Что рендерит | Константа |
|---|---|---|
Nav.jsx | Липкая навигация-«скотч». IntersectionObserver подсвечивает активную секцию. | SECTIONS |
Hero.jsx | Первый экран: ransom-заголовок, <Row>-спека, SVG CharacterSheet. | inline в JSX |
Reels.jsx | Шоурил: одна крупная карточка + стопка мелких. Клик меняет лидер. | REELS |
Reactions.jsx | Видео-реакции блогеров + репосты TG. | VIDEOS, TG_POSTS |
Process.jsx | 5 стадий пайплайна с SVG-стрелками. | STAGES |
Clients.jsx | Бесконечная бегущая строка (marquee). | CLIENTS |
About.jsx | Карточки «теги» и «контакты». Внутри якорь #contact. | TAGS, CONTACTS |
Идентификаторы секций для Nav: top (Hero), reels, reactions, process,
clients, about, contact.
Эффекты и оверлеи (src/components/)
| Файл | Что делает |
|---|---|
SplashScreen.jsx | UE5-style loader. Pre-roll прогресса с моковой timeline (TIMELINE массив с pct/frames/status), closing state триггерит fade-out. |
InkCursor.jsx | Кастомный карандаш-курсор + canvas со следом. Глобально body { cursor: none } (только pointer:fine). Скрывается на body.desktop-mode и body.crashing-dark. |
Watermelon.jsx | Easter egg в углу — арбуз с tag-стикером, при клике взрыв частиц (melon-juice/melon-seed). |
Effects.jsx | Композиция: FilmGrain (SVG noise overlay), Vignette, ScrollTape, HeroParallax, Tilt (на data-tilt), Magnetic (магнитная кнопка), Confetti, TornDivider (рваная бумага между секциями), HeroStamp. |
ProgramWindows.jsx | Окна-ошибки 9 программ в пайплайне (см. ниже). |
Hooks (src/hooks/)
-
useReveal(options?)→{ ref, className }.IntersectionObserver-trigger для.reveal inанимации. Дефолтthreshold: 0.15,rootMargin: "0px 0px -50px 0px". Срабатывает один раз. -
useDraggable()→{ elemRef, onMouseDown, dragStyle, reset }. Делает любое окно перетаскиваемым за указанную «шапку». При первом драге запоминает смещение, кламп поwindow.innerWidth/Height. Игнорирует mousedown на интерактивных контролах (button, a, input, textarea, .tb-btn, .x).reset()сбрасывает позицию в null — окно возвращается к CSS-дефолту по центру.
Desktop / crash-flow (вся логика в App.jsx)
Состояния:
loading— splash-экранstep—idle | error | freeze | crash | desktopiconCells—{id: {col, row}}, позиции ярлыков на гриде (persist вlocalStorageключegnirc_icon_cells_v2, с миграцией со старогоv1)selectedIcons: Set<id>— мульти-выделениеdragPos: {iconIds, leadId, dx, dy} | null— состояние мульти-драгаmarquee— Win-style drag-rectangle для выделенияbinItems: [{id, at}]— корзина (persistegnirc_bin_v1)notepadText— содержимое блокнота (persistegnirc_notepad_v2)deniedDrive— открыт ли диалог «Отказано в доступе» (для какого диска)openApp—pc | notepad | bin | <PROGRAM_ID> | nullshineActive— бежит ли блик по кнопке Render (флагegnirc_render_seen_v1)
Грид: GRID_W=92, GRID_H=96. Иконки лежат в клетках col*92, row*96.
Layout-effect на первом заходе расставляет недостающие иконки по
DEFAULT_POSITIONS (UE в центр; pc/notepad/bin — col 0; программы — col 1-3).
Multi-drag: клик-и-тяга по иконке в мульти-выделении тащит всю группу одной дельтой (lead-иконка + остальные). При дропе считается snap-delta для lead, клампится так чтобы вся группа осталась в границах экрана (по максимуму свободного места у самой крайней иконки), потом применяется ко всем. Дроп на корзину (если её нет в группе) уносит туда всю группу.
Программы в пайплайне (ProgramWindows.jsx)
Каждый ярлык открывает тематическое окно-ошибку. PROGRAM_IDS = ["iclone", "cc4", "comfyui", "aftereffects", "photoshop", "blender", "cascadeur", "davinci", "premiere"]. Один общий appDrag на все 9 окон (одновременно
открыто только одно).
| ID | Окно |
|---|---|
iclone | green-error диалог: BoneMapping mismatch (UE_Mannequin → CC4_Base, 12 костей вместо 67). |
cc4 | green-error диалог: UV-острова face_03 и head_back_02 пересекаются. |
comfyui | err-dialog с тёмно-фиолетовым стек-трейсом + физичные SVG-провода, свисающие с низа (4 шт, verlet). |
photoshop | err-dialog: scratch disk full, автосохранёнка 34 мин назад. |
premiere | err-dialog: .prproj из новой версии + audio sync drift +137 frames + 3/18 clips offline. |
aftereffects | Render Queue окно, ETA растёт каждые 2с: 4ч → 8ч → 1д → 3д → Time elapsed: ∞. Полоса замирает (stalled класс с мигом). |
blender | CSS 3D-куб (transform-style: preserve-3d, 6 граней), вращается над горизонтом, статус "Verts: 8 · Faces: 6 · Tris: 12". |
cascadeur | ASCII-стикмен 9 кадров (450мс/кадр) пытается встать и падает, пунктирный «пол», tooltip: physics: optional. |
davinci | Красный экран MEDIA OFFLINE с диагональной штриховкой, мигающий LED. |
Зелёная тема (green-error класс) — для iClone и CC4: Reallusion-pipeline
green palette в titlebar/body/buttons/.ico индикаторе.
ComfyUI провода: SVG поверх вьюпорта (z=13499, под .program-dialog
z=13500). 4 цепи по 18 точек, SEG_LEN=11px, gravity 0.55, damping 0.985, distance-constraints x3 прохода для стабильности. Анкеры читаются
getBoundingClientRect() каждый кадр → при драге диалога провода качаются с
инерцией. Imperative SVG setAttribute (без React state), чтобы не было
60 re-render'ов/сек. Цвета под реальные ComfyUI link-типы (gold/MODEL,
pink/LATENT, cyan/IMAGE, green/CLIP).
Стили (src/styles/)
| Файл | Что внутри |
|---|---|
base.css | Дизайн-токены (:root), бумажные текстуры, типографика, контентные секции, ink-cursor, film grain, vignette. |
reactions.css | Стили секции Reactions (видео-карточки, TG-репосты). |
splash.css | UE5-style splash-окно: progress bar, кадры, fade-out. |
crash.css | Render-кнопка (с блик-анимацией), err-dialog, crash-reporter, freeze-overlay, site-stage scale, desktop-overlay, ярлыки рабочего стола, marquee, app-windows (pc/notepad/bin Win95-стиль), drop-target highlight. |
programs.css | Окна программ из ProgramWindows.jsx: green-error тема, AE Render Queue, Blender 3D куб, Cascadeur стикмен, DaVinci красный, ComfyUI провода SVG. |
Z-index стек (важно для оверлеев):
20000 send-tip (всплывашка на crash-кнопках)
14100 err-dialog.denied-dialog (отказано в доступе поверх PC)
14000 splash-overlay
13500 app-window (pc/notepad/bin) + program-dialog
13499 comfy-wires-svg (под диалогом ComfyUI, "за" нижним краем)
13000 desktop-overlay (интерактивный, step=desktop)
12500 crash-reporter (UE crash)
12000 err-dialog базовый (OOM, во время step=error/freeze)
11500 freeze-overlay (whitewash / dim+blur)
11401 app-window.bg (фоновый рендер окон во время крэша)
11400 desktop-overlay.backdrop (обои+иконки во время крэша)
9000 film-grain
7100 render-btn
localStorage / sessionStorage ключи
| Ключ | Где | Содержимое |
|---|---|---|
egnirc_icon_cells_v2 | App.jsx | {id: {col, row}} позиции ярлыков. Миграция со старого egnirc_icon_cell_v1 (только ue). |
egnirc_bin_v1 | App.jsx | [{id, at}] объекты в корзине. |
egnirc_notepad_v2 | App.jsx | Текст «секретов» в Блокноте. Bump с v1 чтобы старые пустые значения сменились дефолтом. |
egnirc_render_seen_v1 | App.jsx | Флаг что юзер уже нажимал Render → блик больше не показывается. |
Активы (src/assets/)
wallpaper.jpg,splash.png,ue-icon.png— фон и UE-иконки.programs/— 9 PNG-иконок программ в пайплайне (256×256, бренд-иконки).- Системные иконки (
binIcon,binFullIcon,pcIcon,notepadIcon) — inline SVG data-URI вassets.js(Win95-стиль).
Прочие директории
preview.html— большой автономный HTML-снимок (~700 КБ) для быстрого предпросмотра без сборки. Не используется в продакшене.dist/,node_modules/— игнорируются git.
Changelog (recent sessions)
2026-05-22 — финальная пачка
- Реальные PNG-иконки программ заменили placeholder-SVG. Добавлен
Premiere Pro (9-я программа, col 3 row 0). [
7ee1171] - ComfyUI: физичные провода (verlet, gravity, damping, distance
constraints) свисают с низа диалога, качаются при драге. [
f32dc39] - Зелёная тема
.green-errorдля iClone 8 и Character Creator 4 (Reallusion-pipeline palette). [f32dc39] - Fix:
.program-dialogz-index12000 → 13500 — диалоги были скрыты подdesktop-overlay. [f32dc39] - 8 программ в пайплайне (
ProgramWindows.jsx): iClone/CC4/ComfyUI/ AfterEffects/Photoshop/Blender/Cascadeur/DaVinci с тематическими окнами (3D куб, ASCII-стикмен, растущий ETA, и т.д.). [d63b742] - Multi-drag иконок с сохранением относительных позиций —
dragPosстал{iconIds, leadId, dx, dy}. Дельта клампится по всей группе. [465e233] - Кнопка Render увеличена + дашед-виньетка через
outline+ блик (skewXprjбегает раз в 5с до первого нажатия, флаг в localStorage). [0f63c95] - Fix: курсор на err-dialog (
cursor: defaultдля desktop-mode) + multi-select marquee (selectedIcon → Set). [e11243b] - Revert Мой компьютер на Win95-стиль (откат Win11-редизайна
146bc60). [
02f2106] - Ошибка «Отказано в доступе» при клике на диск; Блокнот
переименован в «секреты» с дефолтным текстом; снап-закрытие
крэша (transitions только при
body.crashing). [95a5207]