- Add swipe-to-reveal in mobile chat list (right=pin, left=mute/leave) - Pin/mute API endpoints with is_pinned DB column migration - Show pin icon on pinned chats, mute icon on muted chats - Pinned chats sort to top of list - Fix push notifications sent to message sender (filter uid) - Remove debug console.log from WS handler - Fix chat rename not updating in sidebar without reload - Fix emoji picker z-index and positioning on mobile (z-50, bottom-full) - Add last seen time in chat header for private chats Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JaniChat
Корпоративный мессенджер в браузере. Telegram-подобный интерфейс, PWA, push-уведомления. Пользователей создаёт администратор — свободной регистрации нет.
Домены:
https://janichat.ru— фронтенд (React PWA)https://api.janichat.ru— бэкенд API + WebSocket
Стек
| Слой | Технологии |
|---|---|
| Frontend | React 18, TypeScript, Vite, Tailwind CSS, Zustand |
| Backend | Fastify, TypeScript, Node.js 20 |
| БД | PostgreSQL (контейнер hotelsync-postgres) |
| Realtime | WebSocket (@fastify/websocket) |
| Push | Web Push API (VAPID) |
| Деплой | Docker Compose, nginx (hotelsync-nginx) |
Архитектура
janichat.ru api.janichat.ru
│ │
hotelsync-nginx (nginx)────┤
│ │
janichat-frontend janichat-api
(nginx:80) (fastify:3000)
│
hotelsync-postgres
(база: janichat)
Оба контейнера находятся в сети hotelsync_hotelsync-net — общей сети с основным проектом HotelSync.
Запуск и деплой
Деплой на сервер
ssh root@92.63.177.212 "cd /opt/janichat && bash deploy.sh"
Скрипт /opt/janichat/deploy.sh:
git pull origin mainиз/opt/janichat/appdocker compose build --no-cachedocker compose up -ddocker exec hotelsync-nginx nginx -s reload— обновляет IP контейнеров в nginx (важно!)
Локальная разработка
# Backend
cd backend
npm install
cp .env.example .env # заполнить DATABASE_URL, JWT_SECRET, VAPID ключи
npm run dev
# Frontend
cd frontend
npm install
npm run dev
Переменные окружения (сервер: /opt/janichat/.env)
DATABASE_URL=postgresql://hotelsync:PASSWORD@postgres:5432/janichat
JWT_SECRET=...
VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
VAPID_EMAIL=mailto@example.com
ALLOWED_ORIGINS=https://janichat.ru
Frontend build args (Dockerfile):
VITE_API_URL=https://api.janichat.ru
VITE_WS_URL=wss://api.janichat.ru
База данных
База janichat в контейнере hotelsync-postgres. Схема создаётся автоматически при старте бэкенда (initDB()).
Таблицы
| Таблица | Описание |
|---|---|
users |
Пользователи: login, display_name, phone, position, avatar, is_admin |
chats |
Чаты: private / group / channel |
chat_members |
Участники чатов: роль, права (can_send_messages, can_add_members…) |
messages |
Сообщения: text / image / file / system |
attachments |
Файлы, прикреплённые к сообщениям |
push_subscriptions |
Подписки Web Push |
Роли участников
| Роль | Права |
|---|---|
owner |
Всё, включая удаление чата |
admin |
Управление участниками, смена ролей |
member |
Чтение и отправка (если разрешено) |
Миграции
Выполняются автоматически при старте (initDB). Новые колонки добавляются через ALTER TABLE ... ADD COLUMN IF NOT EXISTS.
API
Базовый URL: https://api.janichat.ru
Аутентификация: Authorization: Bearer <JWT>
JWT выдаётся при логине, действителен 30 дней.
Auth
| Метод | Путь | Описание |
|---|---|---|
| POST | /api/auth/login |
Вход (по phone или username + password) |
| GET | /api/auth/me |
Текущий пользователь |
| PUT | /api/auth/me |
Обновить профиль (displayName, bio, phone, position, password) |
| PUT | /api/auth/avatar |
Загрузить аватар (multipart) |
| DELETE | /api/auth/avatar |
Удалить аватар |
Users
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/users |
Все пользователи (кроме себя) |
| GET | /api/users/search?q= |
Поиск по имени, логину, телефону |
| GET | /api/users/:id |
Профиль пользователя |
Chats
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/chats |
Мои чаты |
| POST | /api/chats/private/:userId |
Создать/открыть личный чат |
| POST | /api/chats |
Создать группу или канал |
| GET | /api/chats/:id |
Чат с участниками |
| PUT | /api/chats/:id |
Обновить название/описание |
| DELETE | /api/chats/:id |
Удалить (owner или admin системы) |
| PUT | /api/chats/:id/avatar |
Загрузить аватар чата |
| DELETE | /api/chats/:id/avatar |
Удалить аватар чата |
| POST | /api/chats/:id/members |
Добавить участника |
| DELETE | /api/chats/:id/members/:memberId |
Удалить участника |
| PUT | /api/chats/:id/members/:memberId |
Изменить роль / права |
| DELETE | /api/chats/:id/leave |
Покинуть чат |
Messages
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/messages/chat/:chatId |
История (50 последних, курсор: ?before=<id>) |
| POST | /api/messages/chat/:chatId |
Отправить текстовое сообщение (HTTP fallback) |
| POST | /api/messages/chat/:chatId/upload |
Загрузить файл/изображение |
Push
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/push/key |
Публичный VAPID ключ |
| POST | /api/push/subscribe |
Подписаться на push |
| DELETE | /api/push/unsubscribe |
Отписаться |
Admin (только is_admin)
| Метод | Путь | Описание |
|---|---|---|
| GET | /api/admin/users |
Все пользователи |
| POST | /api/admin/users |
Создать пользователя |
| PUT | /api/admin/users/:id |
Обновить пользователя (displayName, phone, position, isAdmin, isActive, password) |
| DELETE | /api/admin/users/:id |
Удалить пользователя |
| GET | /api/admin/chats |
Все чаты системы |
| DELETE | /api/admin/chats/:id |
Удалить чат |
| GET | /api/admin/stats |
Статистика (users, chats, messages) |
WebSocket
Подключение: wss://api.janichat.ru/ws?token=<JWT>
Клиент → Сервер
// Отправить сообщение
{ "type": "send_message", "payload": { "chatId": "uuid", "content": "текст", "type": "text", "replyToId": null } }
// Индикатор набора
{ "type": "typing", "payload": { "chatId": "uuid", "typing": true } }
// Прочитать сообщения
{ "type": "read_messages", "payload": { "chatId": "uuid" } }
// Удалить сообщение
{ "type": "delete_message", "payload": { "messageId": "uuid" } }
// Редактировать сообщение
{ "type": "edit_message", "payload": { "messageId": "uuid", "content": "новый текст" } }
Сервер → Клиент
{ "type": "new_message", "payload": { /* Message объект */ } }
{ "type": "message_edited", "payload": { /* Message объект */ } }
{ "type": "message_deleted", "payload": { "messageId": "uuid", "chatId": "uuid" } }
{ "type": "user_online", "payload": { "userId": "uuid", "online": true } }
{ "type": "typing", "payload": { "chatId": "uuid", "userId": "uuid", "typing": true } }
{ "type": "messages_read", "payload": { "chatId": "uuid", "userId": "uuid" } }
Фронтенд
Структура
src/
├── api/
│ ├── client.ts # Axios с JWT из localStorage
│ └── ws.ts # WsClient (WebSocket + автореконнект)
├── components/
│ ├── Avatar.tsx # Аватар (фото или инициалы)
│ ├── ChatHeader.tsx # Шапка чата
│ ├── ChatInfoPanel.tsx # Боковая панель: участники, аватар чата
│ ├── ChatListItem.tsx # Элемент списка чатов
│ ├── MessageInput.tsx # Поле ввода (WS + HTTP fallback)
│ ├── MessageItem.tsx # Пузырь сообщения
│ ├── MessageList.tsx # Список сообщений + пагинация
│ ├── NewChatModal.tsx # Создание чата/группы/канала
│ ├── ProfileModal.tsx # Профиль пользователя + выход
│ └── admin/
│ └── AdminPanel.tsx # Панель администратора
├── pages/
│ ├── Login.tsx # Страница входа (по телефону)
│ └── MainLayout.tsx # Основной layout: сайдбар + чат
├── store/
│ └── index.ts # Zustand store
├── hooks/
│ └── usePushNotifications.ts
├── types.ts # TypeScript типы
└── App.tsx # Роутинг, инициализация WS, восстановление сессии
Управление состоянием (Zustand)
Главный store (store/index.ts) хранит:
user— текущий пользователь (инициализируется из localStorage)chats— список чатовactiveChat,fullChat— открытый чатmessages— сообщения по chatIdonlineUsers— множество userId онлайн-пользователейconnected— статус WebSocket
Сессия
Токен и данные пользователя хранятся в localStorage (jc_token, jc_user). Zustand инициализируется сразу из localStorage — при перезагрузке страницы сессия сохраняется без запроса к серверу.
PWA
Приложение устанавливается на мобильные устройства через "Добавить на главный экран".
public/manifest.json— название, иконки, цвет темыpublic/sw.js— Service Worker: кэш, обработка push-уведомлений, открытие чата по клику на уведомление- Иконки:
icon-192.png,icon-512.png,apple-touch-icon.png(алмаз на синем фоне)
Права доступа
Типы чатов
| Тип | Кто пишет |
|---|---|
private |
Оба участника |
group |
Все участники (можно ограничить через can_send_messages) |
channel |
Только owner и admin |
Создание пользователей
Только администратор (is_admin = true) через панель администратора. Свободная регистрация отсутствует.
Учётные записи по умолчанию
При первом запуске (пустая БД) создаётся:
| Логин | Пароль | Роль |
|---|---|---|
admin |
admin123 |
Администратор |
Смените пароль после первого входа!
Сервер
| Параметр | Значение |
|---|---|
| IP | 92.63.177.212 |
| ОС | Ubuntu 24.04 LTS |
| SSH | ssh root@92.63.177.212 (ключ ~/.ssh/id_ed25519) |
| Проект | /opt/janichat/ |
| Git-репо (сервер) | /opt/janichat/app/ |
| Загрузки | /uploads/ (внутри контейнера janichat-api) |
Полезные команды
# Логи API
docker logs janichat-api --tail=50 -f
# Логи фронтенда
docker logs janichat-frontend --tail=20
# Перезапустить только API
docker restart janichat-api
# Войти в контейнер API
docker exec -it janichat-api sh
# Перезагрузить nginx (после смены IP контейнеров)
docker exec hotelsync-nginx nginx -s reload