Инфраструктура для сайтаCHANNEL: OPEN
HOMELAB·ci-cd

Инфраструктура для сайта

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

GitLabDockerProxmoxDebianNginxNode.jsAnsibleAi
Инфраструктура для сайта
Открыть сайт друга)Он кстати оч крутой 3d артист

Автоматизированный конвейер разработки

Опыт настройки и архитектура CI/CD

Инфраструктуру настраивал впервые в таком масштабе, поэтому решил попробовать стратегию Blue/Green, чтобы при деплое не падал прод — она плавно переключает поток пользователей.

Для этого проекта был поднят self-hosted GitLab. Причин, почему я решил его развернуть, было несколько:

  1. Хотел сам пощупать инструмент изнутри и расширить функционал.
  2. У друга никак не получалось зарегистрироваться на официальном GitLab ))) Тут я просто создал ему аккаунт, и проблема решилась.

Конфигурация раннеров (GitLab Runners)

Мне было интересно протестировать работу раннеров на разных машинах, поэтому разделил их логически:

  • Универсальный раннер (тип docker): Когда я изучал Ansible, то выделил под него отдельную виртуалку (думал, что это какой-то огромный сервис). Виртуалка осталась, и я решил разместить туда раннер для непрерывной интеграции кода. Там сейчас происходят проверки и сборка.
  • Прод-раннер (тип shell): Этот раннер крутится прямо на виртуалке с сайтом. На продакшене я просто установил Docker Engine.

Как устроен процесс CI/CD

Сначала выполняются базовые этапы:

  • test — проверяет код линтером (с правом на ошибку благодаря allow_failure).
  • build — собирает проект в папку dist/.

Вся основная магия происходит на этапе деплоя, где три джобы последовательно передают друг другу эстафету через needs и файл deploy.env:

  1. deploy — определяет, какой цвет сейчас активен (blue или green), поднимает в Docker новую версию приложения на соседнем порту и проверяет её «здоровье» через curl.
  2. switch_proxy — если контейнер ожил, эта джоба внутри Docker-контейнера с Ansible декодирует SSH-ключ из Base64 и переключает трафик внешнего прокси-сервера на новый порт.
  3. delete_old_color — напоследок выдерживает паузу в 15 секунд, чтобы старый контейнер успел обработать долетающие запросы, после чего глушит его и чистит дисковый кэш Docker.

Итог: Получилась отличная отказоустойчивая система, которая не уронит прод, даже если новая сборка окажется битой.

GitLab/ProxMox
GitLab/ProxMox

Интеграция с ИИ

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

Тут мое окружение для тестов (tmux)
Тут мое окружение для тестов (tmux)

CI/CD

yml
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.jsx5 стадий пайплайна с SVG-стрелками.STAGES
Clients.jsxБесконечная бегущая строка (marquee).CLIENTS
About.jsxКарточки «теги» и «контакты». Внутри якорь #contact.TAGS, CONTACTS

Идентификаторы секций для Nav: top (Hero), reels, reactions, process, clients, about, contact.


Эффекты и оверлеи (src/components/)

ФайлЧто делает
SplashScreen.jsxUE5-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.jsxEaster 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-экран
  • stepidle | error | freeze | crash | desktop
  • iconCells{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}] — корзина (persist egnirc_bin_v1)
  • notepadText — содержимое блокнота (persist egnirc_notepad_v2)
  • deniedDrive — открыт ли диалог «Отказано в доступе» (для какого диска)
  • openApppc | notepad | bin | <PROGRAM_ID> | null
  • shineActive — бежит ли блик по кнопке 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Окно
iclonegreen-error диалог: BoneMapping mismatch (UE_MannequinCC4_Base, 12 костей вместо 67).
cc4green-error диалог: UV-острова face_03 и head_back_02 пересекаются.
comfyuierr-dialog с тёмно-фиолетовым стек-трейсом + физичные SVG-провода, свисающие с низа (4 шт, verlet).
photoshoperr-dialog: scratch disk full, автосохранёнка 34 мин назад.
premiereerr-dialog: .prproj из новой версии + audio sync drift +137 frames + 3/18 clips offline.
aftereffectsRender Queue окно, ETA растёт каждые 2с: 4ч → 8ч → 1д → 3д → Time elapsed: ∞. Полоса замирает (stalled класс с мигом).
blenderCSS 3D-куб (transform-style: preserve-3d, 6 граней), вращается над горизонтом, статус "Verts: 8 · Faces: 6 · Tris: 12".
cascadeurASCII-стикмен 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.cssUE5-style splash-окно: progress bar, кадры, fade-out.
crash.cssRender-кнопка (с блик-анимацией), 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_v2App.jsx{id: {col, row}} позиции ярлыков. Миграция со старого egnirc_icon_cell_v1 (только ue).
egnirc_bin_v1App.jsx[{id, at}] объекты в корзине.
egnirc_notepad_v2App.jsxТекст «секретов» в Блокноте. Bump с v1 чтобы старые пустые значения сменились дефолтом.
egnirc_render_seen_v1App.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-dialog z-index 12000 → 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 + блик (skewX prj бегает раз в 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]

◀ ALL PROJECTS