Files
topsysops-app/README.md
T
ogrechkoandClaude Sonnet 5 130c5382cd Initial commit: TopSysOps site + admin panel
Multi-page public site (Next.js), Node/Express backend with Postgres,
and Vite admin/account app, restructured from a single-page layout
into dedicated service, promotions, cases, about, FAQ, and blog pages
for SEO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
2026-08-25 23:12:16 +03:00

391 lines
27 KiB
Markdown
Raw 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.
# TopSysOps - сайт + админ-панель
Продакшн-версия сайта top-sysops.ru: публичная часть с серверным рендерингом на
Next.js (Главная, Акции, Услуги, Кейсы, FAQ, Блог с рич-текст редактором, Отзывы,
Контакты), личный кабинет для клиентов (переписка с командой, ИИ-чат, профиль) и
админ-панель (дашборд посещаемости, модерация отзывов, редактор прайс-листа,
редактор акций, редактор кейсов и FAQ, редактор блога, переписка с клиентами,
ИИ-ассистент для сотрудников, виджет чата поддержки на сайте). SEO: sitemap.xml,
robots.txt и разметка schema.org генерируются самим Next.js.
## Из чего состоит
```
├── docker-compose.yml # четыре сервиса: db, backend, nextjs, web
├── .env.example # шаблон настроек, скопировать в .env
├── backend/ # Node.js/Express API + PostgreSQL
├── public-site/ # Next.js (SSR) - публичный сайт и блог
└── admin-app/ # React (Vite) - админка и личный кабинет + Caddy
```
- **db** - PostgreSQL. Хранит услуги/цены, акции, отзывы, статьи блога,
статистику визитов, клиентов и учётные записи администраторов.
- **backend** - REST API. При первом запуске сам накатывает миграции и создаёт
администратора и стартовый прайс-лист/акции (те же позиции, что были на старом сайте).
- **nextjs** - публичный сайт и блог, отдаются с сервера (SSR) - это и есть
ответ на "нужен SEO": краулер получает готовый HTML с разметкой schema.org,
а не пустую страницу, которую дорисовывает JavaScript. Не имеет отдельного
порта наружу - доступен только через Caddy.
- **web** - Caddy: собирает и раздаёт статику `admin-app` (админка и личный
кабинет - им SEO не нужен, поэтому остались обычным SPA), проксирует
`/api/*` на backend, всё остальное - на `nextjs`. Caddy же **сам получает и
продлевает HTTPS-сертификат** Let's Encrypt - вручную ничего настраивать не
нужно, только домен в `.env`.
Почему не переписали админку и кабинет на Next.js тоже: SEO им не нужно
вообще (они закрыты в `robots.txt` и требуют авторизации), а полный переезд
уже готовой и работающей админки ради нулевой выгоды - это только риск что-то
сломать. SSR применён именно там, где от него есть польза - на страницах,
которые должны индексироваться.
## Требования
- Сервер на Ubuntu 24.04 (2 ГБ RAM хватит с запасом).
- Домен, у которого A-запись указывает на IP сервера - нужен для автоматического HTTPS.
Если домена пока нет, можно временно развернуть по IP без HTTPS (см. `.env.example`).
- Открытые порты 80 и 443.
## 1. Установка Docker на Ubuntu 24
```bash
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# чтобы не писать sudo перед каждой командой docker (перелогиньтесь после этого)
sudo usermod -aG docker $USER
```
Проверка: `docker compose version`.
## 2. Загрузка проекта на сервер
Скопируйте всю папку проекта на сервер, например через `scp` или `git`:
```bash
scp -r topsysops-app your-user@your-server-ip:~/topsysops-app
# или, если проект в git-репозитории:
git clone <ваш-репозиторий> topsysops-app
```
## 3. Настройка .env
```bash
cd topsysops-app
cp .env.example .env
nano .env
```
Обязательно замените:
- `POSTGRES_PASSWORD` - пароль базы данных;
- `JWT_SECRET` - длинная случайная строка (`openssl rand -hex 32`);
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` - логин и пароль первого администратора;
- `DOMAIN` - ваш домен (например `top-sysops.ru`) или `:80`, если тестируете
по IP без HTTPS.
Файл `.env` содержит секреты - он уже добавлен в `.gitignore`, не публикуйте его.
## 4. Запуск
```bash
docker compose up -d --build
```
Первый запуск соберёт образы (нужен интернет на сервере), поднимет базу,
применит миграции, создаст администратора и стартовый прайс-лист/акции, затем
запустит сайт. Проверить статус и логи:
```bash
docker compose ps
docker compose logs -f backend
```
Дождитесь строки `[start] backend listening on port 4000`, затем откройте
сайт в браузере по вашему домену (или `http://IP-сервера`, если `DOMAIN=:80`).
Если используете реальный домен, Caddy автоматически выпустит сертификат при
первом обращении по HTTPS - на это может уйти несколько секунд.
## Если TLS у вас на Nginx Proxy Manager
Именно этот сценарий - в `.env.example` он уже настроен по умолчанию:
`DOMAIN=http://top-sysops.ru`, `HTTP_PORT=7778`, порт 443 у контейнера
закомментирован в `docker-compose.yml`. Со стороны NPM:
1. **Hosts -> Proxy Hosts -> Add Proxy Host**.
2. **Domain Names**: ваш домен (`top-sysops.ru`).
3. **Scheme**: `http`, **Forward Hostname/IP**: IP или hostname сервера, где
запущен docker compose (`127.0.0.1`, если NPM на той же машине; иначе
реальный IP), **Forward Port**: `7778` (или то, что указали в `HTTP_PORT`).
4. Вкладка **SSL**: выберите/запросите сертификат Let's Encrypt, включите
**Force SSL** - весь HTTPS полностью на стороне NPM, контейнеру об этом
вообще думать не нужно.
5. Save. Проверьте, что `docker compose ps` показывает `web` со проброшенным
портом `7778->80` - именно на него должен смотреть Forward Port в NPM.
Если после этого видите "too many redirects" - смотрите раздел
"Устранение неполадок" ниже, обычно это означает, что `DOMAIN` в `.env`
задан без схемы `http://`.
## 5. Первый вход в админку
Откройте `https://ваш-домен/admin`, войдите под `ADMIN_USERNAME` / `ADMIN_PASSWORD`
из `.env`, затем сразу зайдите в **Настройки -> Сменить пароль** и задайте
собственный пароль - значение из `.env` дальше не используется системой, но
лучше не оставлять его действующим.
Из админки доступно:
- **Дашборд** - визиты (всего/сегодня/график за 7 дней), количество отзывов;
- **Отзывы** - одобрить / отклонить / удалить;
- **Клиенты** - список зарегистрированных клиентов и переписка с ними (ответ на
сообщения из личного кабинета, метка «ждёт ответа» для непрочитанных);
- **Прайс-лист** - добавление и редактирование категорий и позиций услуг,
скрытие позиции с сайта без удаления (иконка глаза);
- **Акции** - то же самое для акций;
- **Кейсы** - карточки портфолио (название, тег, статус с цветным индикатором,
описание), с возможностью скрыть без удаления;
- **FAQ** - вопросы и ответы для сайта;
- **Блог** - статьи (заголовок, слаг, краткое описание, текст в WYSIWYG-редакторе
с форматированием - заголовки, списки, ссылки, цитаты - публикация) -
попадают на по-настоящему серверно отрендеренную страницу `/blog/<slug>` и в
sitemap.xml сразу после сохранения;
- **Контакты** - адрес, телефон, email, режим работы, ссылка на карту;
- **ИИ-ассистент** - внутренний чат для сотрудников на базе OpenRouter (см. ниже);
- **Настройки** - смена пароля и управление администраторами (добавить нового
сотрудника с логином/паролем, удалить лишнего - кроме себя и последнего
оставшегося администратора).
Изменения в прайсе, акциях, кейсах и FAQ появляются на сайте в течение минуты:
Next.js кэширует данные с публичного сайта на 60 секунд (`revalidate: 60`), это
плата за серверный рендеринг - страницы отдаются мгновенно, а не после похода в
базу на каждый заход. В самой админке изменения видны сразу же, без задержки.
## Обновление прайс-листа на уже развёрнутом сайте
Стартовый прайс-лист (в `seed.js`) применяется только к пустой базе - то есть
только при самом первом запуске. Для уже развёрнутого сайта обновление новой
линейки услуг оформлено как обычные миграции
(`backend/migrations/006_update_services_2026_07.sql` и
`007_landing_tier.sql`) - они применяются **автоматически** при следующем
запуске бэкенда, ничего вручную запускать не нужно:
```bash
docker compose up -d --build
```
Система миграций (`backend/src/migrate.js`) гарантирует, что каждый файл
выполнится ровно один раз - если уже разворачивали сайт раньше и обновляете
код сейчас, эти миграции просто окажутся среди тех, что применятся при
следующем перезапуске `backend`, наравне с остальными.
Что делают эти миграции: убирают старую категорию "Разработка сайтов"
(от 5 000 ₽) и добавляют:
- **Разработка под ключ** - Лендинг (от 20 000 ₽, простая посадочная без
бота/админки), MVP «Быстрый старт» (от 120 000 ₽, с формой-ботом и простой
админкой), MVP Pro (от 450 000 ₽, с личным кабинетом и ИИ-ассистентом - как
этот сайт), продакшн-разработка (от 1 200 000 ₽, индивидуально),
Discovery-воркшоп (от 30 000 ₽), сопровождение проекта (от 40 000 ₽/мес);
- **White-label и партнёрство** - white-label «Кабинет + ИИ-ассистент»
(от 500 000 ₽), IT-мониторинг сайта (от 10 000 ₽/мес), реферальная и
партнёрская программы.
Цифры сверены с рынком Санкт-Петербурга (лендинги студийного уровня -
19 000-65 000 ₽, MVP с личным кабинетом у студий - от 260 000 ₽ и выше).
Дальше все эти позиции редактируются как обычно - через админку («Прайс-лист»).
Если после `docker compose up -d --build` старая категория всё равно видна -
проверьте `docker compose logs backend | grep migrate`: там должны быть строки
`applying 006_update_services_2026_07.sql` и `applying 007_landing_tier.sql`.
Если их нет - значит контейнер `backend` не пересобрался/не перезапустился с
новым кодом (проверьте `docker compose ps`, при необходимости
`docker compose up -d --build backend` отдельно).
## SEO
- **robots.txt** и **sitemap.xml** - генерируются самим Next.js (`public-site/app/robots.js`
и `app/sitemap.js`), закрывают `/admin` и `/account` от индексации, доступны
по адресам `https://ваш-домен/robots.txt` и `/sitemap.xml`. Sitemap включает
главную, `/blog` и все опубликованные статьи - обновляется автоматически.
- **schema.org (JSON-LD)** - рендерится прямо на сервере (это заслуга SSR: разметка
оказывается в HTML, который получает краулер, а не дорисовывается JS уже в
браузере). На сайте автоматически публикуется `LocalBusiness` (адрес, телефон,
email из раздела «Контакты», рейтинг из одобренных отзывов), `FAQPage` (из
раздела FAQ) и каталог услуг (`hasOfferCatalog`, из прайс-листа) - помогает
Google/Яндексу показывать расширенные сниппеты (рейтинг со звёздами,
раскрывающиеся вопросы в выдаче). На страницах статей блога - разметка `Article`.
- **Блог с рич-текст редактором** - основной канал органического трафика для
B2B IT-услуг. Пишите статьи через админку («Блог», обычный WYSIWYG-редактор:
заголовки, списки, ссылки, цитаты) - каждая становится отдельной,
по-настоящему серверно отрендеренной страницей на `/blog/<slug>` и сразу
появляется в sitemap.xml.
- **Серверный рендеринг (SSR)** - публичный сайт и блог теперь на Next.js:
краулер и превью в соцсетях получают готовый HTML сразу, без ожидания
JavaScript. Админка и личный кабинет остались SPA на Vite - им это не нужно,
они и так закрыты в `robots.txt` и требуют входа.
- Что дальше сделать руками (не автоматизируется из кода): зарегистрировать сайт
в Яндекс.Вебмастере и Google Search Console (там же отправить sitemap.xml
вручную первый раз), завести профиль в Яндекс.Картах/Google Business с тем же
адресом и телефоном, что в разделе «Контакты» (важно для локального SEO по
запросам вида «IT-аутсорсинг Санкт-Петербург»).
## Чат поддержки на сайте (TopTicket / Chatwoot)
Виджет чата для посетителей сайта настраивается в `.env` и встраивается на
этапе сборки Next.js-приложения (значения переменных `NEXT_PUBLIC_*` попадают
в клиентский бандл, поэтому смена провайдера требует пересборки контейнера
`nextjs`, а не `web`).
По умолчанию уже включён TopTicket (ваша собственная разработка, help.top-sysops.ru)
с реальными данными. Выберите вариант в `.env`:
```bash
# TopTicket (по умолчанию) - embed-сниппет вашей разработки:
# <script src="https://help.top-sysops.ru/widget.js" data-key="..." async>
CHAT_PROVIDER=topticket
TOPTICKET_WIDGET_URL=https://help.top-sysops.ru/widget.js
TOPTICKET_WIDGET_KEY=EBpDS4JqT7a-_MzZ
# либо Chatwoot
CHAT_PROVIDER=chatwoot
CHATWOOT_BASE_URL=https://chat.lamlaba.ru
CHATWOOT_WEBSITE_TOKEN=ваш-website-token # Inboxes -> ваш инбокс -> Configuration
# либо совсем без чата
CHAT_PROVIDER=none
```
После изменения `.env`:
```bash
docker compose up -d --build nextjs
```
Виджет подключается только на публичных страницах сайта, в админке не показывается.
## ИИ-ассистент в админке (OpenRouter)
Это отдельный, второй чат - не для посетителей сайта, а внутренний инструмент
для сотрудников: помогает набросать ответ на отзыв, посчитать что-то по
прайсу и т.д. Доступен только тем, кто вошёл в админку.
Работает через [OpenRouter](https://openrouter.ai/) - агрегатор моделей разных
провайдеров под одним API. Ключ используется только на бэкенде и никогда не
попадает в браузер:
```bash
OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
OPENROUTER_MODEL=openai/gpt-4o-mini # любая модель, доступная в вашем аккаунте
```
После заполнения:
```bash
docker compose up -d backend
```
Если `OPENROUTER_API_KEY` не задан, раздел «ИИ-ассистент» в админке просто
покажет предупреждение и не будет пытаться обращаться к API - остальной сайт
при этом продолжает работать как обычно.
## Контакты
Адрес, телефон, email, режим работы и ссылка на карту редактируются в админке
(раздел «Контакты», `/admin/contact`) — как и прайс-лист, акции, кейсы и FAQ,
без пересборки. Пустое поле просто не показывается на сайте (например, если
не хотите публиковать телефон).
## Личный кабинет клиентов
Отдельная система входа для посетителей сайта - не путать с админкой. Регистрация
и вход доступны по адресам `/account/register` и `/account`, на главной странице
есть заметная плашка-приглашение с этими же ссылками.
После входа клиенту доступны:
- **Обзор** - приветствие и текущие акции;
- **Написать нам** - личная переписка с командой TopSysOps. Сообщения клиента
видны в админке (раздел «Клиенты»), сотрудник отвечает прямо оттуда, ответ
сразу появляется в кабинете клиента;
- **ИИ-ассистент** - второй, отдельный чат, отвечает на вопросы об услугах и
ценах через тот же OpenRouter, что и ассистент в админке (см. выше), но с
собственным системным промптом и более строгим лимитом запросов;
- **Профиль** - изменение имени/компании/телефона, смена пароля.
Регистрация клиентов и вход в админку - две независимые системы: у них разные
таблицы в базе (`customers` и `admin_users`) и разные сессионные cookie
(`ts_customer_session` и `ts_admin_session`), так что сотрудник может быть
одновременно залогинен и в кабинет, и в админку в одном браузере без конфликтов.
Дополнительная защита от спама/перебора: на регистрацию, вход, отправку
сообщений и обращения к ИИ-ассистенту в кабинете клиента тоже действует
`express-rate-limit`, как и в остальном API.
## Резервное копирование базы данных
```bash
docker compose exec db pg_dump -U topsysops topsysops > backup-$(date +%F).sql
```
Восстановление из бэкапа:
```bash
cat backup-2026-07-25.sql | docker compose exec -T db psql -U topsysops topsysops
```
Рекомендуется добавить это в cron, например раз в сутки.
## Обновление после изменений в коде
```bash
git pull # если используете git
docker compose up -d --build
```
Данные в PostgreSQL хранятся в именованном томе `db_data` и не теряются при
пересборке контейнеров.
## Устранение неполадок
- `docker compose logs -f backend` - если сайт не открывается, чаще всего
проблема видна тут (например, не заполнен `JWT_SECRET`).
- `docker compose logs -f nextjs` - если не открывается главная страница или
блог (например, не может достучаться до бэкенда - тогда секции будут просто
пустыми, а в логе будет `[serverApi] failed to fetch ...`).
- `docker compose logs -f web` - если не выдаётся HTTPS-сертификат, здесь будут
сообщения Caddy (обычно это неверный DNS: домен ещё не указывает на сервер).
- Если меняли `DOMAIN` в `.env`, перезапустите `web`: `docker compose up -d web`.
- **"Too many redirects" / бесконечный редирект** - значит перед контейнером
стоит внешний реверс-прокси со своим TLS-сертификатом (например, Nginx Proxy
Manager - см. раздел выше), а Caddy внутри контейнера всё равно пытается сам
сделать редирект http-&gt;https. Исправляется
так: в `.env` поставьте `DOMAIN=http://ваш-домен` (со схемой `http://`) и
перезапустите `web` (`docker compose up -d web`) - это отключит
автоматический HTTPS/редирект в Caddy, он будет просто отдавать http, а
шифрование останется на внешнем nginx. Также проверьте, что внешний nginx
передаёт заголовки `Host`, `X-Forwarded-Proto` и `X-Real-IP`/`X-Forwarded-For`.
- Полный сброс базы (например, во время тестов): `docker compose down -v`
удалит и данные - используйте только осознанно.
## Безопасность на будущее
- Не открывайте порт 5432 (PostgreSQL) наружу - в текущей конфигурации он и так
не проброшен на хост, доступен только другим контейнерам.
- Ограничьте доступ по SSH и настройте `ufw` (`ufw allow 80,443,OpenSSH`, затем
`ufw enable`). Если используете сценарий с внешним nginx и внутренним портом
вроде 7778 (`HTTP_PORT=7778`), этот порт наружу открывать не нужно - трафик
между nginx и контейнером идёт локально, через него наружу открыты только
80/443 самого nginx.
- `express-rate-limit` уже ограничивает количество попыток входа в админку и
количество отзывов с одного IP - это защита от перебора паролей и спама, но
не полноценный WAF; при росте нагрузки рассмотрите Cloudflare или аналог.