PRODUCT-AI.md: L0 умность в ячейках (Time Arc v1-v4 shipped); L1 подсказки ghosts kind=hint (Front#75); L2 голос-кнопка mark press-to-talk -> ActionPlan с подтверждением пишущих действий + палитра fallback (Front#76). STT/intent через Back-прокси /v1/ai/*; anti-scope: чат, Composer, AI-home, авто-booking, AiRouter в Front. Ссылки: FrontSpec §3.3 + новый §7.3, README. Refs EventHub/EventHubSpec#22
39 KiB
EventHub Client UI — Техническое задание
Клиентское Web SPA для end-user: владельцы календарей и участники. Администраторы работают в EventHubFrontAdmin. Контракт API — EventHubBackSpec.md и маршруты EventHubBack (порт HTTP 8080, WS 8081).
1. Роли и режимы
Один пользовательский аккаунт; UI разделяется по контексту (не по JWT-роли админки):
| Режим | Описание | Ключевые возможности |
|---|---|---|
| Участник | Поиск и запись на события коммерческих календарей | Поиск, просмотр, запись (если booking_open), отмена, отзывы, жалобы, тикеты |
| Владелец | Управление своими календарями | CRUD календарей/событий/специалистов, заявки confirm/decline, подписка |
| Специалист | Confirm заявок на «свои» события | Те же экраны календаря; confirm/decline только где specialist_id = текущий user |
Deep-link на календарь, владельцем которого является текущий пользователь, показывает owner-действия (редактирование, заявки).
Права на операции проверяет бэкенд; фронт скрывает недоступные действия по owner_id / ответу API.
2. Аутентификация и авторизация
Эндпоинты:
POST /v1/register— регистрация (email,password); статус пользователяpendingдо верификации.POST /v1/verify— подтверждение email{ token }; после успеха у пользователя есть дефолтный personal-календарь.POST /v1/login— вход; тело:email,password,client_type=web,device_name(метка браузера/ОС). Ответ{ token, refresh_token, session_id, user }.POST /v1/refresh— ротация по{ refresh_token }; ответ включаетsession_id.POST /v1/logout—{ refresh_token }до очистки storage.GET /v1/sessions,DELETE /v1/sessions/:id,POST /v1/sessions/revoke-others— активные сессии в профиле (устройство, «текущая», отзыв / выйти на других).GET /v1/user/me— профиль текущего пользователя.
Реализация:
- JWT +
session_idвlocalStorage(ключи, отличные от Admin UI). - Axios:
Authorization: Bearer <access>; при 401 — single-flight refresh; при неудаче — редирект на/login. - При старте приложения —
GET /v1/user/meпри наличии токена. - Logout —
POST /v1/logout, затем clear storage. - WebSocket:
ws://…:8081/ws?token=<access_jwt>; subscribe/unsubscribe поcalendar_id.
3. Технологический стек
- Язык: TypeScript
- Сборщик: Vite
- UI: React 19 + Tailwind CSS 4 + shadcn/Radix
- Типографика: Syne только для логотипа CalenTIQ (латиница); UI, заголовки, даты и время — Manrope (кириллица)
- Состояние: Zustand (auth + mood + lens + calendar context), TanStack React Query v5 (сервер)
- HTTP: Axios с интерсепторами
- Формы: React Hook Form + Zod
- Дата/время: dayjs (ru locale)
- i18n: react-i18next (ru, en)
- Роутинг: React Router 7
- WebSocket: нативный API с переподключением
- Контейнеризация: Docker, Nginx (прокси
/v1→ user API:8080,/ws→:8081)
3.1. UX / IA (calendar workspace, mobile-first)
- Главная сущность — календарь. Центр UI — виджет с видами месяц (default) / неделя / день.
- Главные вкладки: Календарь (
/,/c/:id), Найти (/search), Записи (/bookings), Ещё (/more). - Записи (
/bookings) — grouped inbox: «К подтверждению» (owner/specialist pending + Confirm/Decline) и «Мои записи» (participant); пустые группы скрывать. Confirm на карточке события остаётся. Past/expired: бейджexpired(«истекла»), Cancel скрыт; empty-state с CTA Discover / календари. Время на list-карточках — вторичное к title (.eh-book-list-time). - В Ещё: язык (
LanguageSwitcher); пункт входящих specialist invites скрыт приpending=0; приpending > 0— badge со счётчиком (deep-link/invitesработает). Mood в Ещё нет — chip в AppShell рядом с BrandWordmark (Front#40; ранее Front#39 в chrome workspace). - Mood + Lens (Front#39/#40 + Front#46): mood chip (FLOW/MOMENTUM/CONTROL → sheet picker) —
в AppShell рядом с логотипом (не в toolbar Month/Week/Day). Lens — инфо-табло
под строкой AppShell (logo/mood/nav) на calendar workspace: сегменты Обзор / Сегодня /
Заявки + метрики/chips (не рядом с ViewMode). Soft-default по mood без auto-pin при
клике на вкладку; при смене mood —
DEFAULT_LENS_BY_MOOD. Workspace chrome: (1) селектор; (2) период + вид Месяц/Неделя/День; (3) owner actions — для commercial только «+ Новое событие» («Заполнить расписание» — во вкладке Студия). Без flip / CREATOR. Нет lens на не-calendar routes. Прошлый месяц — React-grid (не HTML-iframe); lens overview на истории — минимум (ARCHIVE.md, Front#73). - Week view (Front#40): одна строка day-headers (без дубля заголовков
WeekDayColumn); горизонтальный скролл через.eh-cal-week-scroll(mobile ~390). - Контекст виджета: свой календарь (селектор) или browse чужого (после поиска). Чужой
personal— только просмотр; чужойcommercialсbooking_open=true— запись; приbooking_open=false— просмотр + «Запись временно недоступна». Бейдж «чужой» не показывать; CTA «Мой календарь» +ChevronLeft→/default. - Единственный personal (Back: title
Default, UI «По умолчанию»): overlay confirmed записей на чужие commercial + specialist duty (Front#35/#43) — только на нём, не на студиях. Create calendar UI — только тип Студия (commercial). Селектор: «По умолчанию»- студии.
- Guest на чужом commercial (D9 модель B): roster карточек активных специалистов →
тап → сетка только со слотами
event.specialist_id= выбранный; «Все слоты студии» снимает фильтр. Это не chip-filter на общей сетке. Book как обычно — безspecialistв body заявки (слот уже привязан к спецу на событии). В режиме «Все» — тот же studio-агрегат слотов, что у owner «Студия» (см. ниже); фильтр D9 без второго UI режима. - Owner commercial workspace — режимы Студия / Мастер:
- Студия — агрегат слотов по
start_time+duration: одна ячейка с заливкойbooked/total, labelHH:mm · K/N свободно; клик → popover мастеров со статусами (занятость поbooking_occupancy/ active bookings, BackSpec §6 events). - Мастер — фильтр сетки по
specialist_id; sticky roster как guest D9 B (owner тоже использует roster-фильтр, не только guest). - Месяц + Студия: density markers в ячейках дня, не список чипов слотов.
- Personal: без studio-агрегата и без вкладки «Команда» / Team.
- ★
0.0на free-слотах не показывать.
- Студия — агрегат слотов по
- Agenda / rail desktop (commercial owner,
≥ 1024px): вкладки Расписание | Команда | Студия в общем.eh-owner-rail-panelфиксированной высоты (вкладки не прыгают). Команда: invite свёрнут за «Пригласить». Студия = hub: about + «Заполнить расписание» + «Редактировать» (+ link/calendars); Delete только на/calendars. Phone и tablet: segmented День | Команда | Студия под grid (не боковой rail — иначе колонки недели сжимаются). Карточка события: sheet< 768px, боковая панель≥ 768px. Списки —ScrollRegion(полоса скрыта, стрелки по краям при overflow); dialogs — thin scrollbar on hover. - Agenda empty: personal «Нет событий»; commercial «Свободных окон нет». UI-тип commercial — «Студия» (не «коммерческий»).
- Выбор события открывает карточку действий: mobile — bottom sheet; desktop — боковая панель.
- На browse
/c/:id— CTA «Мой календарь» (+ иконка) →/default; tab Calendar неaria-current. Follow на чужом commercial без изменений. - Список отслеживаемых:
/following(из «Ещё»). - Открытие события —
navigatepush (неreplace), чтобы Back возвращал к календарю. - Owner: WS subscribe на активный календарь;
PUTсобытия из карточки;PUTкалендаря на/calendars. - Поиск (
/search): query/filters в URL params (восстановление при возврате); chip type фильтрует discovery tops без ухода из Popular; в строке результата — id snippet иcalendar_titleдля event. - CRUD своих календарей —
/calendars(из «Ещё»). Legacy/discover,/calendars/:id→ redirects. - На phone и tablet (
< 1024px, Tailwindlg) — одна primary nav: bottom tab bar (+ safe-area). На desktop (≥ 1024px) — одна строка chrome: logo + mood chip | nav tabs | nickname + logout (не два ряда header+nav, не bottom+top одновременно). Сырой email в chrome не показывать — primary identity =nickname(fallback без@). Mobile: logo mark-only + mood chip icon-only (Front#45); desktop — полный wordmark + label. Navaria-currentсинхронизирован сuseLocation(без remountOutletпо pathname). - Mood themes — см. §3.2.
3.2. Настроения (mood themes) / режимы CalenTIQ
Публичный бренд UI: CalenTIQ (см. BRANDING.md). Внутренние ключи API/preferences без изменений. Три mood только — без CREATOR, без flip календаря.
Пользователь переключает визуальный режим; атрибут html[data-mood]. Для гостя и при отсутствии preferences.mood — дефолт calm (в профиль при логине не пишется). После входа смена mood сохраняется в preferences.mood через PATCH /v1/user/me.
| UI-лейбл | Идентификатор (API) | Характер |
|---|---|---|
| FLOW | calm |
sage/mist, мягкий градиент (default) |
| MOMENTUM | energetic |
тёмный ink + coral #FF6A3D |
| CONTROL | business |
charcoal + steel #324A5F, более жёсткие радиусы |
Где mood:
- Auth (
/login,/register,/verify, forgot/reset) — compactMoodSwitcher(+ language). - AppShell — primary control: mood chip рядом с BrandWordmark → dialog/sheet
(не в
/more, не в calendar toolbar Month/Week/Day). Desktop: logo + mood | nav | nickname + logout. Front#45: mobile (< sm) — полный BrandWordmark CalenTIQ + mood chip icon-only (aria-label= FLOW/MOMENTUM/CONTROL); сsm+— полный label chip без truncate. Sheet: §C glyph (sprout/bolt/briefcase) + label + tagline. Time Arc appicon — только favicon, не в switcher.
Lens (отдельно от mood и от view month/week/day): overview / today / bookings
(UI: Обзор / Сегодня / Заявки). Инфо-табло под AppShell на calendar routes (не в
toolbar ViewMode). Persist: session preference + soft LS; клик по вкладке не ставит
pin навсегда; при смене mood — soft-default FLOW→overview, MOMENTUM→today,
CONTROL→bookings. Контент: free gaps / события сегодня / upcoming bookings (Front-derive).
Язык UI (ru/en): до логина — navigator.language; после входа — поле language профиля (PATCH /v1/user/me). Если в профиле язык пуст — при логине записывается текущий (браузерный/выбранный на экране входа). Язык также в /more и в форме /profile.
3.3. Time Arc (живая сетка, Front#50)
На commercial week/day в режиме Студия клиент считает предложенный час без LLM и без Back: свободные слоты студии ∩ занятость пользователя (personal + confirmed bookings + mirror shared-personal) × mood (FLOW — воздух и середина дня, MOMENTUM — ближайший, CONTROL — доля свободных мест). Одна ячейка — data-time-arc; коллизии — data-collided. Тап — обычный Book.
Владелец или share write|admin (заместитель): черновики слотов из прошлой ISO-недели в дырах текущей; «Создать неделю» —
preview, затем существующий POST events. После создания черновик снимается сразу
(без F5), в том числе если время в форме сдвинули (прошлый час → +1 день / правка start).
Фраза с часом уже в прошлом («11» в 14:00) ставит черновик на следующий день, тот же час.
v2 (Front#51): отказ от дуги (тап другой свободной ячейки / свайп недели) пишет штраф часу и мастеру в localStorage (eh.timeArc.skips) — дуга переезжает. ≥2 confirmed записей к одному specialist_id бустят его слоты. Месяц: тепло data-time-arc-heat на днях со скоренным часом. Personal: черновик «тот же час +7д» после прошедшего confirmed визита; тап открывает студию.
v3 (дуга между календарями): /following сортирует студии по ближайшему Time Arc
(start_time); баннер «Ближайший час» — min среди подписок, без дубля when
на той же строке списка. /search: баннер nearest — другая студия, чем первая
карточка выдачи (если совпадает — следующий hint); Discover остаётся списком
с data-time-arc-when на остальных calendar-строках. Тап открывает неделю студии
с курсором на этом дне. Черновики владельца по активной студии. Без LLM / Back AI.
v4 (Front#53): pending occupancy тянет demand-черновики на тот же час в свободные дни текущей ISO-недели (data-time-arc-demand). Long-press пустого часа week/day у владельца commercial — локальный разбор фразы → phrase-черновик (data-time-arc-phrase); клик и long-press месяца по-прежнему открывают create. Специалист на чужой студии: дуга на своём будущем pending-слоте. Без LLM / AiRouter.
Share / заместитель (Back#73, согласование Front#49): GET /v1/calendars включает shared;
busy Time Arc += события personal с mirror_to_default; ghost/demand/phrase у share
write|admin; Following не смешивать с share. Confirmed foreign bookings overlay на
default personal — только role=owner ids в ownSet.
Канон ИИ клиента (подсказки / голос, Future — PRODUCT-AI.md, см. §7.3): умность — в ячейках; опциональные hint-ghosts и голос-кнопка mark → ActionPlan → подтверждение. Не чат; AiRouter вне продукта.
4. Маршруты
Публичные:
/login,/register,/verify,/forgot-password,/reset-password/search,/discover— каталог без сессии/c/:calendarId,/c/:calendarId/e/:eventId— read-only неделя commercial-студии;calendarId— UUID илиshort_name(гость: active commercial). запись / follow / «мой календарь» →/login?next=
Защищённые (ProtectedRoute):
/default— resolve единственного personal →/c/:id(post-login и tab «Календарь»)/default/e/:eventId— то же + карточка события/— alias →/default/bookings— grouped inbox: (A) к подтверждению (owner/specialist pending) + (B) мои записи участника; deep-link в/c/.../e/.../calendars— управление своими календарями (create только Студия; personal delete UI скрыт)/more— вторичное меню + language switcher (без mood); пункт specialist invites скрыт, если входящих pending = 0; при pending > 0 — badge со счётчиком (deep-link/invites?token=работает)/invites— inbox входящих specialist invites (не смешивать с/bookings)/following— отслеживаемые чужие календари (follow; не платная Подписка)/reviews,/subscription,/tickets,/profile
Redirects: /discover → /search; /calendars/:id → /c/:id; /calendars/:id/events/:eventId → /c/:id/e/:eventId.
Состояние вида: Zustand calendarContextStore (view, cursorDate, last own calendarId в localStorage); lens — lensStore + localStorage (см. §3.2).
5. Модули
5.1. Auth
Регистрация, верификация, вход, восстановление сессии.
5.2. Профиль
GET /v1/user/me + форма на /profile. Редактируемые через PATCH /v1/user/me: nickname, phone, timezone, avatar_url, language; смена пароля — current_password + password. Upload аватара: POST /v1/user/me/avatar (multipart file) → обновляет avatar_url (Front#69). Read-only в UI: id, email, role, status. Mood — chip в AppShell рядом с BrandWordmark (+ compact на auth); persist preferences.mood через PATCH /v1/user/me (не в /more, не в calendar toolbar). Язык при первом визите — из браузера; при первом логине без language — пишется в профиль. Mood по умолчанию calm, при логине не сидится. Lens — только localStorage (не профиль).
5.3. Поиск (участник)
GET /v1/search — фильтры (type, q, tags, даты, lat/lon/radius, sort, order, пагинация). Без q/дат/tags/geo/sort — топы календарей и событий; иначе — поиск. В списке бейдж типа — calendar|event (не personal/commercial). Даты, теги, geo и сортировка свёрнуты по умолчанию. Переход к /c/:id или /c/:id/e/:eventId.
В результатах calendar (и event, если Back отдаёт) может быть image_url — Discover показывает
cover (.eh-discover-media--photo); иначе mood wash + initials (.eh-discover-media--fallback,
не fake photo-hero). Битый image_url → fallback. Cover upload владельца: POST /v1/calendars/:id/cover (Front#69); URL также можно задать через PUT image_url.
Гео (волна 2, GEO.md, Front#74): основной путь — «рядом со мной» (GPS) или
центр по адресу (suggest), не ручные lat/lon. Радиус опционален (пресеты 1/5/10/25/50 км
и «без ограничения»). В выдаче — distance_km. Список, не map-first.
Вид карты (Back#77): у результатов segment-переключатель «Список | Карта»
(?view=map в URL, переживает перезагрузку). Данные — тот же GET /v1/search
(useSearch); в режиме карты пагинация списка и блок «ближайшее» скрыты.
Маркеры группируются по точке (~1 м): пин календаря с бейджем числа событий в группе,
событие с отличной точкой — отдельная малая точка; клик по маркеру → попап со списком
карточек группы (те же href /c/:id и /c/:cid/e/:eid, что в списке). Результаты без
координат не рисуются — счётчик «Без адреса: N». При активном гео-фильтре — точка центра
и круг радиуса. Тайлы OpenFreeMap (MapLibre GL, без ключа); компонент карты — lazy-chunk.
5.4. Календари
- Список своих:
GET /v1/calendars - CRUD:
POST/PUT/DELETE /v1/calendars,GET /v1/calendars/:id(following,booking_open) - Follow чужого:
POST/DELETE /v1/calendars/:id/follow, списокGET /v1/user/following(UI: «Отслеживать» в about, страница/following; не путать с/subscription) - Просмотр коммерческого чужого календаря по id; при
booking_open=false— баннер «Запись временно недоступна», кнопка записи скрыта - Прошедший месяц/неделя/день: тот же React-виджет по
GET …/events?from&to(сервер отдаёт snapshot/файл); бейдж «Архив». HTMLGET …/viewнет (ARCHIVE.md, Front#73). Текущий и будущие месяцы — без изменения, только hot. - Создание / апгрейд
commercialбез уже active sub/trial →402→/subscription(trial — явныйstart_trial, не auto при create); после оплаты — возврат к созданию/редактированию - Владелец commercial: команда — owner-строка specialist (создаётся бэком при create
commercial) + остальные через invite (не сырой
user_id): typeaheadGET /v1/users/lookupи/или email →POST …/specialist-invites; список + исходящие pending; deactivate/remove принятых (не owner). Выборspecialist_idв форме события (толькоactive). Баннер «подписка истекла / истекает» приbooking_open=false.SpecialistsPanel(owner): во вкладке Команда rail (desktop) / segmented (mobile) — не внутри About; owner всегда первым, бейдж «Владелец», галочка «Я специалист» ↔PUT statusactive|inactive; edit name/specialization; Delete для owner скрыт. Mobile density — имя/теги сверху, actions снизу full-width (invite/deactivate не ломать); desktop — вертикальный rail + scroll. Режимы сетки Студия (агрегатstart_time+duration, заливка booked/total, labelHH:mm · K/N свободно, popover мастеров) и Мастер (фильтрspecialist_id+ sticky roster как D9 B) — см. §3.1. Месяц в Студии — density markers. Occupancy ячеек/popover — из APIbooking_occupancy("free"|"pending"|"confirmed", BackSpec events); ★0.0на free не показывать. Personal — без агрегата / без вкладки Команда. - Guest browse commercial: roster активных → фильтр расписания по
specialist_id(D9 B; см. §3.1); в «Все» — studio-агрегат как у owner; owner panel (invite/CRUD) не смешивать с guest roster. - Invitee: inbox входящих
GET /v1/user/specialist-invitesна/invites— принять / отклонить; deep-link из email → accept поtokenпосле логина. В More пункт «Приглашения» показывается только приpending > 0с badge счётчика (не путать с booking-заявками на/bookings).
5.5. События
- Список:
GET /v1/calendars/:calendar_id/events— для commercial в JSON события опциональноbooking_occupancy("free"|"pending"|"confirmed"; приоритет confirmed > pending > free; см. BackSpec §6). Studio-агрегат / popover мастеров опираются на это поле (+ capacity/bookings при необходимости). - CRUD владельца:
POST/PUT/DELETE(в т.ч. опциональныйspecialist_id). Create-форма: capacity = 1 (илиsettings.default_capacityстудии);specialist_idподставляется из текущего roster-фильтра сетки; recurrence — из org defaults календаря. WeekFill: горизонт по умолчанию 1 неделя; «Создать расписание» disabled, пока нет именованных слотов и выбранного специалиста; превью считает, сколько событий будет создано; пустой apply — один error toast, не success. - Детали:
GET /v1/events/:id(отображение специалиста, если задан; тот жеbooking_occupancy) - Вхождения:
GET /v1/events/:id/occurrences - Отмена вхождения:
DELETE /v1/events/:id/occurrences/:start_time
5.6. Запись (bookings)
- Участник:
POST /v1/events/:id/bookingsтолько приbooking_open; иначе UI-сообщение / ответ403;GET /v1/user/bookings,DELETE /v1/bookings/:id,GET /v1/bookings/:id. Тело book без specialist (назначение — наevent.specialist_id). - Владелец и specialist:
- на карточке события:
GET /v1/events/:id/bookings+ Confirm/Decline (обязательно) - inbox
/bookingsгруппа «К подтверждению»:GET /v1/user/booking-requests(actionable pending, где user — owner календаря илиevent.specialist_id; past-pending Back отдаёт какexpiredи не включает в inbox); те жеPUT /v1/bookings/:id{ action: confirm | decline }(наexpired→409)
- на карточке события:
- UI
/bookings: три группы (пустые скрывать): К подтверждению (booking-requests) + Записи студии (GET /v1/user/studio-bookings, confirmed) + Мои записи (upcoming/past participant).specialist_inviteсюда не попадает. Empty state: title + hint + CTA Discover / «Мои календари». Время слота на list-карточках — soft (secondary to title). - Confirmed overlay на personal (Front-only, Front#35): confirmed из
GET /v1/user/bookingsна чужих commercial — догрузить event/calendar и нарисовать на своём default personal grid; titleсобытие · студия · специалист; клик →/c/:foreignCalId/e/:eventId. Back API не менялся. - Specialist duty overlay на personal (Front-only, Front#43): на своём personal —
слоты commercial, где
event.specialist_id= текущий user. Gate: accepted invites (GET /v1/user/specialist-invites) → uniquecalendar_id→ active specialist (GET /v1/calendars/:id/specialists) → list events viewport. Titleсобытие · студия; клик →/c/:commercialId/e/:eventId. Back expand virtuals безspecialist_id— Front мержит masters + expand (mergeExpandedWithMasters) перед фильтром. Legacy specialist без invite-записи — вне MVP. Не зеркалить чужой roster на personal owner. - Статусы:
pending|confirmed|cancelled|expired; confirmation:auto|manual| timeout. Past+pending /expiredв «Мои записи» и наEventActionCard— бейдж «истекла», Cancel скрыт. На past-событии Book скрыт. Defensive: если API ещё отдал pastpending— UI мапит вexpired. - Pending резервирует capacity (как на бэке)
5.7. Отзывы
GET/POST /v1/reviews, GET/PUT/DELETE /v1/reviews/:id, GET /v1/user/reviews. Цели: event | calendar.
Голос: PUT /v1/reviews/:id/vote { "value": "like"|"dislike" }, DELETE /v1/reviews/:id/vote; в ответах — my_vote.
UI: без сессии скрыты форма отзыва, жалоба (календарь/событие/отзыв) и кнопки like/dislike
(счётчики лайков в списке отзывов остаются). С сессией: форма отзыва скрыта без
confirmed booking; подсказка gated + жалоба остаются (жалоба не требует booking).
5.8. Жалобы
POST /v1/reports — target_type: event | calendar | review. Жалоба не требует booking.
В client UI жалоба доступна только залогиненному пользователю.
5.9. Подписка
GET /v1/subscription, POST /v1/subscription (start_trial | activate + plan + опционально payment_info).
Планы и цены (как Back plan_price/1, minor units → ₽): monthly 999, quarterly 2499, biannual 4499, annual 7999.
UI: карточки сравнения, локализованный «Бесплатно» без подписки, trial, демо-оплата (шлюз-заглушка).
Пилот (Spec#18 A): без боевого PSP; commercial через start_trial и/или admin activate.
B2C оплаты услуги нет. Create/upgrade commercial без уже active sub/trial → 402 → /subscription
(сначала явный start_trial или activate; auto-start trial при create нет,
Back#61 / BackSpec §2.1.2, §2.9). После renew commercial-календари владельца снова
с booking_open=true без смены type (restricted → full).
5.10. Тикеты
- ErrorBoundary / необработанные ошибки →
POST /v1/ticketsсsource=frontend - Форма «Сообщить о баге» →
source=manual - Список:
GET /v1/tickets, деталиGET /v1/tickets/:id
5.11. Realtime
Подписка WS на открытые календари; сообщения calendar_update, event_update, booking_update → инвалидация React Query.
5.12. Версии сборки
Как в Admin UI / Back (EventHubSpec/WORKFLOW.md §6):
- Product
MAJOR.MINORизEventHubSpec/VERSION(CI:scripts/resolve-product-version.sh; локальныйVERSION— fallback). - Bake:
VITE_APP_VERSION,VITE_APP_BUILD,VITE_GIT_SHA,VITE_BUILT_AT. - UI: экран «Ещё» —
UI {version}.{build} (sha); API —GET /health→API ….
6. Структура проекта
src/
├── api/ — Axios client + *Api.ts
├── hooks/ — React Query hooks
├── store/ — Zustand (auth)
├── pages/ — auth, discover, calendars, events, bookings, reviews, subscription, tickets, profile
├── layouts/ — consumer shell (не admin Control Center)
├── components/ui — shadcn primitives
├── i18n/ — locales ru/en
├── schemas/ — Zod
└── types/
Репозиторий: EventHubFront.
6.1. Стенды и балансировщик
| Stand | URL | Swarm |
|---|---|---|
| IFT | https://ui.ift.eventhub.local |
alias client-ui |
| stage | https://ui.stage.eventhub.local |
alias client-ui |
| dev | https://ui.dev.eventhub.local |
alias client-ui |
Образ registry: git.sabilin.com/eventhub/eventhub-front. Traefik host-based (без path prefix). Nginx в контейнере: / SPA, /v1/ → eventhub:8080, /ws → eventhub:8081.
CI: lint → build → Playwright mock (до push) → push image → deploy IFT → e2e-ift (live API) → stage → e2e-stage (live API) (зеркало FrontAdmin). Деплой зависит от EventHubDevOps#7 (deploy-service.sh … client).
E2E: e2e/TESTIDS.md. Моки (npm run test:e2e, project=mock) — только локально / до деплоя. После деплоя на IFT/stage — SMOKE_USER_* + npm run test:e2e:ift|stage против реального API (без Playwright route-моков).
7. Ограничения MVP / Фаза 2 (Spec#8)
Не в текущем контракте реализации (кроме specialist_invite — см. §5.4 / BackSpec §2.1.2). Эпик: #8
Pilot / ops (фаза 2): SMTP / transactional email; backup; secrets / certs / alerts;
legal stubs; позиция по подписке владельца; DNS/SPF для calentiq.com (домен в использовании; чеклист BRANDING.md).
Продукт (фаза 2):
- серверный logout / revoke session (
POST /v1/logout+{ refresh_token }; клиент зовёт до clear storage) — сделано - список/отзыв сессий в профиле (Back#74 / Front#72): устройство (
device_name/UA), revoke одной и revoke-others - загрузка файлов: avatar
POST /v1/user/me/avatar, coverPOST /v1/calendars/:id/cover(Back#71); UI — Front#69; вложения — позже - полноценный push / reminders (email-reminder: Back#70; Web Push + prefs:
Back#75 / Front#71 — тумблеры в профиле, SW
/sw-push.js) - waitlist: API Back#72; включение — чекбокс «Лист ожидания»
(
settings.waitlist_enabled) в настройках commercial; join/leave на карточке события при полном слоте — Front#70 - шаринг календаря с правами (
calendar_share) — Back#73 API + Front list/invites/mirror; не путать со specialist_invite / Following - нативный
client_type=mobileклиент — хвост (API уже принимаетmobile)
Вне горизонта (не фаза 2): оплата услуги клиентом (B2C).
Лайки/дизлайки отзывов: PUT/DELETE /v1/reviews/:id/vote, поле my_vote в ответах отзывов (EventHubBack#47).
7.1. Фаза 3 — архив (Spec#19 / Spec#20)
Канон: ARCHIVE.md. Hot = текущий месяц + будущее; warm = 3 мес;
cold = файлы. UI истории — React, не iframe. Код: Back#76, Front#73.
7.2. Фаза 3 — гео (Spec#19 / Spec#21)
Канон: GEO.md. Стек: MapLibre + OpenFreeMap + OpenCage (внешний геокодер через бэк).
Пикер адреса/пина; карточка с мини-картой и outbound Google/Яндекс/OSM;
Discover ближайшие с опциональным радиусом. Код: Back#77, Front#74, DevOps#16.
7.3. Фаза 3 — ИИ клиента (Spec#19 / Spec#22)
Канон: PRODUCT-AI.md. Позиция «ИИ молчит и делает»: L0 умность в
ячейках Time Arc (shipped, без LLM); L1 опциональные подсказки — ghosts kind: 'hint'
с причиной (Front#75); L2 голос-кнопка Time Arc mark, press-to-talk → ActionPlan-чипы
с подтверждением пишущих действий, палитра команд как fallback (Front#76). STT/intent
через Back-прокси /v1/ai/*, как GEO/OpenCage. Anti-scope: чат, Composer, AI-home,
авто-booking без подтверждения, подводка AiRouter к Front. Код — после гейтов канона.
8. Источники истины
- Маршруты и поведение:
EventHubBack/src/eventhub_app.erl+ handlers - Продукт и auth:
EventHubBackSpec.md§1–2, §6.1 - Swagger
client-swagger.json— вторичный (возможен drift, например/v1/auth/refreshvs/v1/refresh)