Синхронизация между пользователями
Изменения, сделанные одним сотрудником, сразу видны у остальных: задачу сдвинули на доске — у коллег доска, список задач, открытая карточка и таймер в шапке обновляются без перезагрузки страницы. То же для карточки проекта, бюджета, файлов, команды, документов, логистики, табелей, платежей и командировок.
Как устроено
Сервер. Middleware
internal/realtimeстоит на группе/api/v1. После каждого успешного изменяющего запроса (POST, PUT, PATCH, DELETE с ответом 2xx) он по шаблону маршрута (c.FullPath()) определяет, что изменилось, и рассылает событие. Сервисы модулей о рассылке не знают; новый маршрут попадает в неё правилом вinternal/realtime/rules.go(таблица правил — в тестеrealtime_test.go).Доставка. По WebSocket уведомлений
/ws/notifications— всем подключённым сотрудникам, событиеentity_changed:json{"type": "entity_changed", "timestamp": "…", "metadata": {"entity": "task", "action": "updated", "id": 26, "project_id": 6, "part": "", "work": true, "actor_id": 7}}В событии только тип, id и проект — никаких данных. Получатель перечитывает данные через API со своими правами, поэтому событие о чужом проекте ничего не раскрывает, кроме того, что объект с таким id изменился.
Фронтенд.
useNotificationsпередаётentity_changedна локальную шину событий (relayServerEntityChange,useRealtimeEvents). На неё подписаны экраны:onTaskChanged,onProjectChanged,onRealtimeEntity. Изменения в своей вкладке объявляет сам фронт сразу после ответа API; серверное эхо приходит в пределах той же задержки (120–150 мс), и перезагрузки сливаются в одну.
Что рассылается
| Маршруты | Сущность | Кто обновляется |
|---|---|---|
/tasks… (кроме чата задачи) | task (work — старт, пауза, завершение, статус) | доска и обзор проекта, список задач, главная, профиль, открытая карточка, таймер в шапке |
/projects/:id, /status, /specs… | project, part card / specs | карточка проекта: статус, ворота, деньги |
/projects/:id/budget/settings, /finance/cost-items…, …/estimate/apply | project, part budget / estimate | вкладка бюджета, деньги в обзоре; смета из ТЗ — ещё задача согласования |
/projects/:id/files, /files…, /folders/:id/files, файлы задачи | project, part files | вкладка «Файлы» |
/project-team…, /team-requests…, /team-request-items… | project, part team | команда и заявки на людей |
/projects/:id/documents… | project, part documents | документы проекта |
/documents/:id… (статус, черновик счёта: правка, выпуск, удаление) | project, parts documents и files | документы и файлы проекта |
/estimates/:id…, /estimate-sections/:id, /estimate-lines/:id | project, part estimate (+ card при смене статуса) | смета |
клиент открыл ссылку или решил (/public/estimates/:token…) | project, part estimate — из сервиса смет, без пользователя | смета |
/logistics…, /equipment-requests…, оборудование проекта | logistics_operation, logistics_equipment_request (refresh) | логистика, оборудование проекта, бюджет |
/timesheets…, /timesheet-corrections…, /time-entries… | timesheet (refresh) | табели, отметки «часы не запишутся» в задачах |
/payments… | payment | платежи и остатки счетов |
/trips… | trip | командировки |
/absences… (кроме POST /absences/check), /calendar… | absence (refresh) | «Табель → Отсутствия», виджет «Кто отсутствует», расписание на главной, метки «в отпуске» у имени |
Чат и уведомления рассылают свои события сами. Справочники, настройки и права обновляются при открытии страницы.
В сети / не в сети
Точка у аватара сотрудника: зелёная — в сети, серая — нет (internal/presence).
- В сети — у сотрудника открыта RMS (соединение
/ws/notificationsили/ws/chat) и он что-то делает: клиент на клики, клавиши, прокрутку и касания шлёт по соединению уведомлений{"type":"activity"}— не чаще раза в минуту.heartbeatтолько держит соединение и в сети не продлевает. - Не в сети — 30 минут без активности (проверка раз в минуту), хотя вкладка открыта; первое же действие возвращает в сеть. Закрыл все вкладки — сразу не в сети.
- Статус пишется в
users.presence_status(его же отдают справочник и контакты чата); при старте API всех сбрасывают в «не в сети» — соединений ещё нет. - Начальное состояние —
GET /api/v1/directory/presence?user_ids=…(до 500), изменения — событиеpresence_changedвсем подключённым:metadata.user_id,metadata.status(online/offline).
Ограничения
- Хаб WebSocket живёт в процессе API: изменения из фонового воркера (
notifications-worker) по нему не рассылаются. Учёт «в сети» тоже в памяти этого процесса — при нескольких экземплярах API его нужно вынести в общее хранилище. - Пока соединение разорвано, события теряются; после переподключения экраны не перечитываются сами — достаточно открыть раздел заново.
- Событие получает каждый подключённый сотрудник; при росте числа пользователей получателей можно сузить до участников проекта.