Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 01:23:32 +04:00
1 parent 45d7512d2b
commit d452824c62
314 files changed
+44394 -121

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# Barev Club — образы для продакшена.
# app — Node: API + воркер + боты (один процесс) и лендинг Next.js (тот же образ)
# web — nginx: статика Mini App и админки + прокси /trpc и /files на API
FROM node:22-alpine AS deps
RUN corepack enable
WORKDIR /repo
COPY . .
RUN pnpm install --frozen-lockfile
FROM deps AS build
# адреса, которые зашиваются в клиентский код при сборке
ARG LANDING_URL
ARG API_PUBLIC_URL
ARG CLIENT_BOT_USERNAME=barevclub_bot
ENV VITE_LANDING_URL=$LANDING_URL NEXT_PUBLIC_API_URL=$API_PUBLIC_URL CLIENT_BOT_USERNAME=$CLIENT_BOT_USERNAME NEXT_TELEMETRY_DISABLED=1
RUN pnpm --filter @barev/miniapp-client build \
&& pnpm --filter @barev/miniapp-pro build \
&& pnpm --filter @barev/admin build \
&& pnpm --filter @barev/landing build
FROM nginx:alpine AS web
COPY --from=build /repo/apps/miniapp-client/dist /srv/client
COPY --from=build /repo/apps/miniapp-pro/dist /srv/pro
COPY --from=build /repo/apps/admin/dist /srv/admin
COPY infra/prod/nginx.conf /etc/nginx/conf.d/default.conf
FROM node:22-alpine AS app
RUN corepack enable
WORKDIR /repo
ENV NODE_ENV=production TZ=Asia/Yerevan
COPY --from=deps /repo /repo
# лендинг: standalone-сборка со своими node_modules
COPY --from=build /repo/apps/landing/.next/standalone /landing
COPY --from=build /repo/apps/landing/.next/static /landing/apps/landing/.next/static
EXPOSE 4000 3000
# при старте: миграции и синхронизация каталога, затем API + воркер + боты
CMD ["sh", "-c", "cd packages/db && ./node_modules/.bin/prisma migrate deploy && ./node_modules/.bin/tsx src/scripts/sync-catalog.ts && cd /repo && exec apps/runtime/node_modules/.bin/tsx apps/runtime/index.ts"]
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Деплой на VPS. Образы собираются на самом сервере (нативный amd64; под эмуляцией на Mac pnpm падает),
# после сборки кеш чистится — сервер общий и диск небольшой.
# infra/prod/deploy.sh — синхронизировать код, собрать, перезапустить
# ONLY=app infra/prod/deploy.sh — собрать только образ app (API/воркер/боты/лендинг)
set -euo pipefail
cd "$(dirname "$0")/../.."
HOST=${DEPLOY_HOST:-root@69.62.120.231}
KEY=${DEPLOY_KEY:-$HOME/.ssh/vps_claude}
DIR=/opt/barev
DOMAIN=${BAREV_DOMAIN:-barevclub.com}
BOT=${CLIENT_BOT_USERNAME:-barevclub_bot}
r() { ssh -i "$KEY" "$HOST" "$@"; }
# один деплой за раз: два параллельных запуска ломают пересоздание контейнеров (конфликт имён → 502)
LOCK=$DIR/.deploy-lock
if ! r "mkdir -p $DIR && mkdir $LOCK 2>/dev/null"; then
echo "✗ на сервере уже идёт деплой (есть $LOCK). Дождитесь его окончания."
echo " Если прошлый деплой оборвался, снимите блокировку: ssh -i $KEY $HOST rmdir $LOCK"
exit 1
fi
trap 'r "rmdir $LOCK" 2>/dev/null || true' EXIT
echo "→ синхронизация кода"
r "mkdir -p $DIR/src"
rsync -az --delete -e "ssh -i $KEY" \
--exclude node_modules --exclude .next --exclude dist --exclude .turbo --exclude '*.tsbuildinfo' \
--exclude .git --exclude .env --exclude docs/design --exclude i18n-review --exclude '/*.pdf' \
./ "$HOST:$DIR/src/"
scp -q -i "$KEY" infra/prod/docker-compose.yml "$HOST:$DIR/docker-compose.yml"
# секреты генерируются один раз и дальше не перезаписываются
if ! r "test -f $DIR/.env"; then
echo "→ создаю $DIR/.env с новыми секретами"
rnd() { openssl rand -hex 24; }
DBP=$(rnd); S3K=$(rnd); ADMINP=$(openssl rand -base64 12 | tr -d '/+=')
sed -e "s|^DB_PASSWORD=.*|DB_PASSWORD=$DBP|" \
-e "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://barev:$DBP@barev-db:5432/barev|" \
-e "s|^ADMIN_SESSION_SECRET=.*|ADMIN_SESSION_SECRET=$(rnd)|" \
-e "s|^SEED_OWNER_PASSWORD=.*|SEED_OWNER_PASSWORD=$ADMINP|" \
-e "s|^S3_SECRET_KEY=.*|S3_SECRET_KEY=$S3K|" \
-e "s|^CLIENT_BOT_TOKEN=.*|CLIENT_BOT_TOKEN=$(grep ^CLIENT_BOT_TOKEN= .env | cut -d= -f2-)|" \
-e "s|^PRO_BOT_TOKEN=.*|PRO_BOT_TOKEN=$(grep ^PRO_BOT_TOKEN= .env | cut -d= -f2-)|" \
infra/prod/env.example | r "umask 077 && cat > $DIR/.env"
sed "s|barev-secret|$S3K|" infra/prod/seaweed-s3.json.template | r "cat > $DIR/seaweed-s3.json"
# ключ шифрования файлов в S3 (SSE-S3)
printf '[s3.sse]\nkek = "%s"\n' "$(openssl rand -hex 32)" | r "cat > $DIR/security.toml && chmod 644 $DIR/security.toml"
echo " пароль владельца админки: $ADMINP"
fi
for target in ${ONLY:-web app}; do
echo "→ сборка barev-$target"
r "cd $DIR/src && nice -n 10 docker build --network host -f infra/prod/Dockerfile --target $target \
--build-arg LANDING_URL=https://$DOMAIN --build-arg API_PUBLIC_URL=https://api.$DOMAIN \
--build-arg CLIENT_BOT_USERNAME=$BOT -t barev-$target:latest . > /tmp/barev-build-$target.log 2>&1 \
|| (tail -40 /tmp/barev-build-$target.log; exit 1)"
done
echo "→ запуск"
r "cd $DIR && docker compose up -d --remove-orphans && docker builder prune -af >/dev/null && docker image prune -f >/dev/null"
r "cd $DIR && docker compose ps --format '{{.Name}}\t{{.Status}}' && df -h / | tail -1"
+105
View File
@@ -0,0 +1,105 @@
# Barev Club — продакшен на общем VPS.
# Наружу смотрит только barev-web (в сети proxy Nginx Proxy Manager), остальное — во внутренней сети.
name: barev
x-env: &env
env_file: .env
services:
barev-db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: barev
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: barev
TZ: Asia/Yerevan
volumes: [db:/var/lib/postgresql/data]
networks: [internal]
mem_limit: 256m
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U barev']
interval: 10s
retries: 10
barev-redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --save 60 1 --maxmemory 64mb --maxmemory-policy noeviction
volumes: [redis:/data]
networks: [internal]
mem_limit: 96m
# S3-совместимое хранилище для фотоотчётов и сканов (приватный бакет)
barev-s3:
image: chrislusf/seaweedfs:latest
restart: unless-stopped
command: server -dir=/data -s3 -s3.port=9000 -s3.config=/etc/seaweed/s3.json -master.volumeSizeLimitMB=64 -volume.max=10
volumes:
- s3:/data
- ./seaweed-s3.json:/etc/seaweed/s3.json:ro
# ключ шифрования SSE-S3 (s3.sse.kek): файлы шифруются при записи
- ./security.toml:/etc/seaweedfs/security.toml:ro
networks: [internal]
mem_limit: 192m
# API + воркер + оба бота
barev-app:
image: barev-app:latest
<<: *env
restart: unless-stopped
depends_on:
barev-db: { condition: service_healthy }
barev-redis: { condition: service_started }
barev-s3: { condition: service_started }
networks: [internal]
mem_limit: 512m
barev-landing:
image: barev-app:latest
<<: *env
restart: unless-stopped
working_dir: /landing
command: ['node', 'apps/landing/server.js']
environment:
PORT: '3000'
HOSTNAME: 0.0.0.0
networks: [internal]
mem_limit: 192m
barev-web:
image: barev-web:latest
restart: unless-stopped
depends_on: [barev-app, barev-landing]
networks: [internal, proxy]
mem_limit: 64m
# ежедневный бэкап БД, хранится 14 дней
barev-backup:
image: postgres:16-alpine
restart: unless-stopped
environment:
PGPASSWORD: ${DB_PASSWORD}
volumes: [backups:/backups]
networks: [internal]
mem_limit: 64m
entrypoint:
- sh
- -c
- |
while true; do
pg_dump -h barev-db -U barev -Fc barev > /backups/barev-$$(date +%F).dump && echo "backup ok"
find /backups -name '*.dump' -mtime +14 -delete
sleep 86400
done
networks:
internal: {}
proxy:
external: true
volumes:
db: {}
redis: {}
s3: {}
backups: {}
+45
View File
@@ -0,0 +1,45 @@
# Продакшен-окружение (на сервере: /opt/barev/.env). Секреты генерирует infra/prod/deploy.sh при первом деплое.
NODE_ENV=production
TZ=Asia/Yerevan
DB_PASSWORD=__generated__
DATABASE_URL=postgresql://barev:__generated__@barev-db:5432/barev
REDIS_URL=redis://barev-redis:6379
CLIENT_BOT_TOKEN=
PRO_BOT_TOKEN=
CLIENT_BOT_USERNAME=barevclub_bot
PRO_BOT_USERNAME=barevworker_bot
TG_INIT_DATA_MAX_AGE=86400
LANDING_DOMAIN=barevclub.com
LANDING_URL=https://barevclub.com
CLIENT_MINIAPP_URL=https://app.barevclub.com
PRO_MINIAPP_URL=https://pro.barevclub.com
ADMIN_URL=https://admin.barevclub.com
# серверный адрес API для лендинга (внутри docker-сети)
API_URL=http://barev-app:4000
API_PORT=4000
ADMIN_SESSION_SECRET=__generated__
SEED_OWNER_EMAIL=owner@barev.local
SEED_OWNER_PASSWORD=__generated__
S3_ENDPOINT=http://barev-s3:9000
S3_REGION=us-east-1
S3_BUCKET=barev-private
S3_ACCESS_KEY=barev
S3_SECRET_KEY=__generated__
SENTRY_DSN=
# Лендинг: контакты в подвале и в JSON-LD (пусто — не показываются)
CONTACT_EMAIL=
# Подтверждение сайта в Search Console / Яндекс Вебмастере / Bing (только код из meta-тега; можно вместо этого подтвердить через DNS)
GOOGLE_SITE_VERIFICATION=
YANDEX_VERIFICATION=
BING_SITE_VERIFICATION=
# Страница /ru/apartments: квартиры AREV из CRM (crm.yandp.tech) и заявки на просмотр.
# Ключ — companies.settings.barevClub.key организации AREV в CRM. Пусто — страница показывает «список недоступен».
CRM_URL=https://crm.yandp.tech
CRM_BAREV_KEY=
+85
View File
@@ -0,0 +1,85 @@
# Шлюз Barev: Nginx Proxy Manager (TLS) → этот контейнер → статика / API / лендинг.
# Хост определяется по первому сегменту домена: app.*, pro.*, admin.*, api.*, иначе — лендинг.
map $host $barev_site {
~^app\. client;
~^pro\. pro;
~^admin\. admin;
~^api\. api;
default landing;
}
server {
listen 80;
server_name _;
client_max_body_size 12m;
server_tokens off;
# www.barevclub.com → barevclub.com: один адрес для SEO и CORS (API разрешает только основной домен)
if ($host ~* ^www\.(.+)$) {
return 301 https://$1$request_uri;
}
# NPM терминирует TLS и передаёт схему в X-Forwarded-Proto: http → https для всех хостов Barev.
# ACME-проверки Let's Encrypt NPM обрабатывает сам, сюда они не доходят.
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
# API доступен с любого хоста Barev — Mini App и админка ходят в него с того же origin
location /trpc/ {
proxy_pass http://barev-app:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location /files/ {
proxy_pass http://barev-app:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /client-log {
proxy_pass http://barev-app:4000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location = /health {
proxy_pass http://barev-app:4000;
}
location / {
if ($barev_site = landing) {
proxy_pass http://barev-landing:3000;
}
if ($barev_site = api) {
return 404;
}
root /srv/$barev_site;
index index.html;
try_files $uri $uri/ /index.html;
}
# index.html Mini App и админки не кешируем: иначе Telegram держит старую страницу со ссылкой на старый бандл
location = /index.html {
if ($barev_site = landing) {
proxy_pass http://barev-landing:3000;
}
root /srv/$barev_site;
add_header Cache-Control "no-cache";
}
# статика с хешами в имени — кешируем надолго
location /assets/ {
root /srv/$barev_site;
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"identities": [
{
"name": "barev",
"credentials": [{ "accessKey": "barev", "secretKey": "barev-secret" }],
"actions": ["Admin", "Read", "Write", "List", "Tagging"]
}
]
}