diff --git a/backlog.md b/backlog.md
index 81d0964..a6d6adb 100644
--- a/backlog.md
+++ b/backlog.md
@@ -1,38 +1,33 @@
# Закрыто 21.07-27.07
-1. MVP frontend: один чат с компанией без истории диалогов; пункт «Чат» открывает текущий активный диалог или создаёт его при отсутствии
-2. При создании пользователя номер телефона копировать в профиль Russian_Phone
-3. Убрать хеширование устройства клиента в devise_json - хочу видеть его параметры.
-4. Добавить параметр, ограничивающих кол-во неуспешных попыток ввода смс.
-5. Поправить чтение сообщений от Битрикса. Сейчас они выглядят так: "[b]Антон Пичугин:[/b] [br]опять ты?" Надо убрать из текста сообщения Отправителя в битриксе
-6. Если неавторизованный пользователь вводит сообщение, после отправки идет на регистрацию, после окончания регистрации его сообщение пропадает. Надо чтобы сохранялось и отправлялось (по аналогии с нажатием на кнопку из раздела "Популярные вопросы")
-7. Изменение в БД по аудитам (заполнение IP, сквозное заполнение UserSession)
-8. Яндекс.капчу добавить
-9. Формы согласий поправить (Согласие на обработку ПД + Политика, Пользовательское соглашение, Реклама)
-10. При повторном запросе OTP кода при авторизации не нужно указывать ошибку "Новый код заказан. Предыдущий код больше не действует."
-11. Интеграция с СМС-провайдером — спецификация и план rollout зафиксированы в `modules/module-11-idgtl-sms.md`; пункт не закрыт до реализации `sms-service`/worker, Keycloak lifecycle, schema `sms`, callback/nginx, env validation, observability и общего DoD. Production prerequisites: согласованные sender/template, Direct `TOKEN_1`, callback credentials/подтверждённый source IP и статический egress IP.
-12. Моделирование уведомлений — постановка v6 синхронизирована с `functional_blocks (business logic)/notification-requirements.md`, arch-00…05 и module-01/03/07. Зафиксированы: instruction только в новой вкладке; TTL только при отсутствии `date_expired`; первое скачивание любого связанного документа скрывает; `producer_test` только для smoke, secret/hash раздельно.
-13. Кнопка "Позвонить оператору" (ссылка tel:+74999591007)
-14. На главном экране две кнопки: чат и звонок оператору. На кнопке с чатом уведомление при наличии непрочитанных сообщений.
+## MVP frontend: один чат с компанией без истории диалогов; пункт «Чат» открывает текущий активный диалог или создаёт его при отсутствии
+## При создании пользователя номер телефона копировать в профиль Russian_Phone
+## Убрать хеширование устройства клиента в devise_json - хочу видеть его параметры.
+## Добавить параметр, ограничивающих кол-во неуспешных попыток ввода смс.
+## Поправить чтение сообщений от Битрикса. Сейчас они выглядят так: "[b]Антон Пичугин:[/b] [br]опять ты?" Надо убрать из текста сообщения Отправителя в битриксе
+## Если неавторизованный пользователь вводит сообщение, после отправки идет на регистрацию, после окончания регистрации его сообщение пропадает. Надо чтобы сохранялось и отправлялось (по аналогии с нажатием на кнопку из раздела "Популярные вопросы")
+## Изменение в БД по аудитам (заполнение IP, сквозное заполнение UserSession)
+## Яндекс.капчу добавить
+## Формы согласий поправить (Согласие на обработку ПД + Политика, Пользовательское соглашение, Реклама)
+## При повторном запросе OTP кода при авторизации не нужно указывать ошибку "Новый код заказан. Предыдущий код больше не действует."
+## Интеграция с СМС-провайдером — спецификация и план rollout зафиксированы в `modules/module-11-idgtl-sms.md`; пункт не закрыт до реализации `sms-service`/worker, Keycloak lifecycle, schema `sms`, callback/nginx, env validation, observability и общего DoD. Production prerequisites: согласованные sender/template, Direct `TOKEN_1`, callback credentials/подтверждённый source IP и статический egress IP.
+## Моделирование уведомлений — постановка v6 синхронизирована с `functional_blocks (business logic)/notification-requirements.md`, arch-00…05 и module-01/03/07. Зафиксированы: instruction только в новой вкладке; TTL только при отсутствии `date_expired`; первое скачивание любого связанного документа скрывает; `producer_test` только для smoke, secret/hash раздельно.
+## Кнопка "Позвонить оператору" (ссылка tel:+74999591007)
+## На главном экране две кнопки: чат и звонок оператору. На кнопке с чатом уведомление при наличии непрочитанных сообщений.
# Закрыто 28.07-03.08
-19. Подключить OTLP-провайдер
-1. Отправлять на UI информацию разные ошибки при попытках авторизации в зависимости от события: код неверен, истёк или уже использован; превышен лимит попыток авторизации, попробуйте через 24 часа (в случаях превышения otp.phone.max_send_attempts_per_24h); превышен лимит неуспешных авторизаций, начните процедуру заново (в случае превышения otp.phone.max_verify_attempts).
-23. При отрицательном результате проверки сообщения через message-safety, если сообщение отправлялось с главного экрана, то пользователь не переводится в чат, ему под окном главного экрана выпадает сообщение об ошибке. Не на всех устройствах это видно. Воспринимается как UX-дефект. Как надо: вне зависимости от решения message-safety, если пользователь отправил сообщение, то он переводится на экран с чатом. Далее, сейчас отрицательный результат message-safety выводится пользователю как техническая ошибка (красным цветом под полем ввода сообщения) и опять же воспринимается не как бизнес-логика, а как техническая ошибка. Это поведение нужно поменять. Если сообщение пользователя не прошло проверку, нужно ему в окне чата прислать ответ: Для сообщений: К сожалению, ваше сообщение не соответствует правилам данного чата и не может быть отправлено. Попробуйте переформулировать. Для документов: К сожалению, ваш документ не прошел проверку и не может быть доставлен.
-8. UX-дефект: frontend показывает «Не удалось завершить вход» при ошибке отправки отложенного сообщения, хотя вход завершён. Это следует исправить: завершать экран авторизации после bootstrap, а ошибку Bitrix показывать уже в чате (если сообщение отклонено сервисом message-safety, учесть реализацию предыдущего пункта)
-22. Ограничить кол-во символов в сообщении на фронте. Показывать в моменте счетчик: n/max, где n сколько символов уже напечатано, max сколько может быть отправлено. Максимальное кол-во символов - положить в app_settings.
-7. Убрать с экрана ввода номера телефона тексты согласий внизу экрана: Нажимая «Получить код», вы соглашаетесь с условиями использования и политикой конфиденциальности. Согласия пользователь дает ранее на отдельном экране.
-
-
+## Подключить OTLP-провайдер
+## Отправлять на UI информацию разные ошибки при попытках авторизации в зависимости от события: код неверен, истёк или уже использован; превышен лимит попыток авторизации, попробуйте через 24 часа (в случаях превышения otp.phone.max_send_attempts_per_24h); превышен лимит неуспешных авторизаций, начните процедуру заново (в случае превышения otp.phone.max_verify_attempts).
+## При отрицательном результате проверки сообщения через message-safety, если сообщение отправлялось с главного экрана, то пользователь не переводится в чат, ему под окном главного экрана выпадает сообщение об ошибке. Не на всех устройствах это видно. Воспринимается как UX-дефект. Как надо: вне зависимости от решения message-safety, если пользователь отправил сообщение, то он переводится на экран с чатом. Далее, сейчас отрицательный результат message-safety выводится пользователю как техническая ошибка (красным цветом под полем ввода сообщения) и опять же воспринимается не как бизнес-логика, а как техническая ошибка. Это поведение нужно поменять. Если сообщение пользователя не прошло проверку, нужно ему в окне чата прислать ответ: Для сообщений: К сожалению, ваше сообщение не соответствует правилам данного чата и не может быть отправлено. Попробуйте переформулировать. Для документов: К сожалению, ваш документ не прошел проверку и не может быть доставлен.
+## UX-дефект: frontend показывает «Не удалось завершить вход» при ошибке отправки отложенного сообщения, хотя вход завершён. Это следует исправить: завершать экран авторизации после bootstrap, а ошибку Bitrix показывать уже в чате (если сообщение отклонено сервисом message-safety, учесть реализацию предыдущего пункта)
+## Ограничить кол-во символов в сообщении на фронте. Показывать в моменте счетчик: n/max, где n сколько символов уже напечатано, max сколько может быть отправлено. Максимальное кол-во символов - положить в app_settings.
+## Убрать с экрана ввода номера телефона тексты согласий внизу экрана: Нажимая «Получить код», вы соглашаетесь с условиями использования и политикой конфиденциальности. Согласия пользователь дает ранее на отдельном экране.
+## Перенести секреты из .env в KM Selectel.
+## Провести аудит безопасности вм
+## Унифицированы гостевые экраны Центра уведомлений, Профиля и Чата: единый стиль сообщения о необходимости входа и кнопка «Авторизоваться».
# В разработку:
-3. Унифицировать сообщения гостевого режима о необходмости
-На экране профиля в гостевом режиме добавить кнопку "Авторизоваться"
-
-
2. После интеграции с смс провайдером, реализовать debounce механизм при авторизации - каждая след. смс можно отправить через все большее окно.
-3. аудит безопасности вм
-
5. Store-review вход: точечный bypass в Keycloak OTP SPI по номеру из `.env` (`STORE_REVIEW_ENABLED` / `STORE_REVIEW_PHONE` / `STORE_REVIEW_OTP`) — для этого телефона SMS не шлётся, verify принимает фиксированный OTP; остальные номера идут обычным OTP/SMS. Не путать с глобальным `KEYCLOAK_OTP_MOCK_*`. Учётные данные только в Review Notes стора (не в бинарнике/UI); пользователь с демо-контентом; в production включать только на время ревью.
6. Хранить историю устройств, с которых пользователь входил в ЛК (Ид юзера, идентификатор устройства, дата последнего входа, способ входа - веб\приложение)
9. Веб-пуши для PWA
@@ -40,7 +35,7 @@
12. Описание бизнес сущностей: Пользователь
12. Описание бизнес сущностей: Сообщение
13. Вынести за пределы ВМ1 сервисы message-safety и sync-service.
-14. Сделать страницу с инстркцией по установке приложения
+14. Сделать страницу с инструкцией по установке приложения
15. Написать пользовательское соглашение.
16. Разработка message-safety
17. Разработка sync-service
@@ -49,7 +44,6 @@
21. Спрятать сеть за балансировщиком нагрузки
22. Автопродление TLS падает при перезагрузке nginx; сертификат действует до 14.10.2026. (Исправить reload внутри контейнера и проверить systemctl start an-chat-ssl-renew.service до успешного завершения.)
23. WireGuard-only SSH.
-25. Перенести секреты из .env в KMS Selectel.
26. Запрет входа под root: В /etc/ssh/sshd_config установите PermitRootLogin no. Заходите под обычным пользователем (например, deploy) и используйте sudo для админских задач.
27. Удалите все ненужные пакеты, компиляторы (gcc, make) и сервисы. Чем меньше программ на сервере, тем меньше потенциальных уязвимостей.
28. Монтирование с флагами безопасности: Разделы диска (особенно /tmp и /var/tmp) следует монтировать с флагами noexec (запрет запуска исполняемых файлов) и nosuid (игнорирование битов setuid).
@@ -58,7 +52,6 @@
ProtectSystem=strict # Делает всю ОС доступной только для чтения
PrivateTmp=yes # Дает процессу свой изолированный /tmp
ProtectHome=yes # Скрывает домашние директории пользователей
-
24. Добавить логи (Для Python-сервисов добавить OTLP Log Exporter: api-backend; sms-service; sms-worker. Подключить LoggerProvider, BatchLogRecordProcessor и bounded queue. Передавать resource attributes: service.name; service.version; deployment.environment; service.namespace=han-chat.) Экспортировать структурированные поля request_id, trace_id, span_id, severity и event name. Оставить stdout как аварийный локальный журнал. Добавить canary-тесты, запрещающие экспорт токенов, cookie, телефонов, email, текстов сообщений, SQL и object keys.).
25. Nginx metrics/tracing в signoz
@@ -66,7 +59,6 @@
На будущее (после доработки отдельных функциональностей):
1. Определение итогового перечня мнемоник, перевод фронтенда на мнемоники, seed заливка мнемоник в БД (?)
2. Моделирование профиля клиента.
-3. Реализация мнемоник.
4. Реализовать в полноценном `bitrix-sync` обработчик `document.client_uploaded`: claim/retry/DLQ, идемпотентность по `client_document_id`, группировка по `submission_id`; до этого stub задачи не claim-ит.
На анализ:
diff --git a/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx b/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx
index b09f888..545de33 100644
--- a/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx
+++ b/codebase/backend/frontend-test-site/app/dialogs/[dialogId].tsx
@@ -5,6 +5,7 @@ import { FlatList, Platform, Text, View } from "react-native";
import { useApp } from "../../src/app-context";
import { ChatInputBar } from "../../src/components/ChatInputBar";
import { ChatScreenHeader } from "../../src/components/ChatScreenHeader";
+import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { MessageBubble } from "../../src/components/MessageBubble";
import { ScreenShell } from "../../src/components/ScreenShell";
import { isMessageBlockedError } from "../../src/api";
@@ -179,11 +180,12 @@ export default function ChatScreen() {
if (app.authStatus !== "authenticated") {
return (
-
-
- Для просмотра чата требуется авторизация.
-
+
+
);
}
diff --git a/codebase/backend/frontend-test-site/app/dialogs/index.tsx b/codebase/backend/frontend-test-site/app/dialogs/index.tsx
index 73d14ff..2400cef 100644
--- a/codebase/backend/frontend-test-site/app/dialogs/index.tsx
+++ b/codebase/backend/frontend-test-site/app/dialogs/index.tsx
@@ -1,9 +1,11 @@
import { useQuery } from "@tanstack/react-query";
-import { Link, useRouter } from "expo-router";
+import { useRouter } from "expo-router";
import React, { useEffect, useRef } from "react";
-import { ScrollView, Text, View } from "react-native";
+import { Text, View } from "react-native";
import { useApp } from "../../src/app-context";
import { AppHeader } from "../../src/components/AppHeader";
+import { ChatScreenHeader } from "../../src/components/ChatScreenHeader";
+import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { ScreenShell } from "../../src/components/ScreenShell";
import {
bindPendingFileIntent,
@@ -37,12 +39,12 @@ export default function DialogsScreen() {
if (app.authStatus !== "authenticated") {
return (
-
-
- Чат
- Чат с компанией доступен после авторизации. Отправьте сообщение на главной странице, чтобы войти.
- На главную
-
+
+
);
}
diff --git a/codebase/backend/frontend-test-site/app/notifications/index.tsx b/codebase/backend/frontend-test-site/app/notifications/index.tsx
index 0babd0c..e195c9c 100644
--- a/codebase/backend/frontend-test-site/app/notifications/index.tsx
+++ b/codebase/backend/frontend-test-site/app/notifications/index.tsx
@@ -4,13 +4,14 @@ import { useRouter } from "expo-router";
import React, { useMemo, useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { useApp } from "../../src/app-context";
+import { GuestAuthGate } from "../../src/components/GuestAuthGate";
import { NotificationCard } from "../../src/components/NotificationCard";
import { ScreenShell } from "../../src/components/ScreenShell";
import { useNotificationAction } from "../../src/notification-actions";
import { notificationApi, notificationKeys } from "../../src/notification-api";
import { typeMap } from "../../src/notification-presenter";
import { colors, radii, spacing } from "../../src/theme";
-import { Button, ErrorNotice, Loading, styles } from "../../src/ui";
+import { ErrorNotice, Loading, styles } from "../../src/ui";
export default function NotificationCenterScreen() {
const { authStatus } = useApp();
@@ -44,19 +45,11 @@ export default function NotificationCenterScreen() {
{!authenticated ? (
-
-
-
-
- Уведомления доступны после входа
-
- Авторизуйтесь, чтобы видеть важные напоминания, документы и статусы услуг.
-
-
+
) : (
{(notifications.isLoading || catalog.isLoading) && }
@@ -99,9 +92,6 @@ const local = StyleSheet.create({
borderBottomColor: colors.border,
},
back: { width: 36, height: 36, borderRadius: radii.full, alignItems: "center", justifyContent: "center" },
- gate: { flex: 1, justifyContent: "center", alignItems: "center", gap: spacing.md, padding: spacing.xl },
- bell: { width: 68, height: 68, borderRadius: radii.full, alignItems: "center", justifyContent: "center", backgroundColor: colors.primary },
- centerText: { textAlign: "center" },
content: { padding: spacing.lg, gap: spacing.md, paddingBottom: 40 },
empty: { alignItems: "center", gap: spacing.sm, paddingVertical: 48 },
});
diff --git a/codebase/backend/frontend-test-site/app/profile.tsx b/codebase/backend/frontend-test-site/app/profile.tsx
index 5e328b0..166a9ca 100644
--- a/codebase/backend/frontend-test-site/app/profile.tsx
+++ b/codebase/backend/frontend-test-site/app/profile.tsx
@@ -5,7 +5,7 @@ import React, { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import { useApp } from "../src/app-context";
import { AccordionSection } from "../src/components/AccordionSection";
-import { AppHeader } from "../src/components/AppHeader";
+import { GuestAuthGate } from "../src/components/GuestAuthGate";
import { ScreenShell } from "../src/components/ScreenShell";
import { isProduction } from "../src/config";
import { profileApi } from "../src/services";
@@ -30,12 +30,17 @@ export default function ProfileScreen() {
if (!enabled) {
return (
-
-
+
+ router.back()} style={({ pressed }) => [stylesLocal.backButton, pressed && stylesLocal.pressed]}>
+
+ Профиль
- Профиль доступен после авторизации.
- Перейти на главную для входа
+
);
}
diff --git a/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx b/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx
new file mode 100644
index 0000000..3631df5
--- /dev/null
+++ b/codebase/backend/frontend-test-site/src/components/GuestAuthGate.tsx
@@ -0,0 +1,51 @@
+import { Feather } from "@expo/vector-icons";
+import { useRouter } from "expo-router";
+import React from "react";
+import { StyleSheet, Text, View } from "react-native";
+import { colors, radii, spacing } from "../theme";
+import { Button, styles } from "../ui";
+
+export function GuestAuthGate({
+ icon,
+ title,
+ description,
+}: {
+ icon: React.ComponentProps["name"];
+ title: string;
+ description: string;
+}) {
+ const router = useRouter();
+
+ return (
+
+
+
+
+ {title}
+ {description}
+
+ );
+}
+
+const local = StyleSheet.create({
+ gate: {
+ flex: 1,
+ justifyContent: "center",
+ alignItems: "center",
+ gap: spacing.md,
+ padding: spacing.xl,
+ },
+ icon: {
+ width: 68,
+ height: 68,
+ borderRadius: radii.full,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: colors.primary,
+ },
+ centerText: { textAlign: "center" },
+});
diff --git a/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts b/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts
index 548d126..2596090 100644
--- a/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts
+++ b/codebase/backend/frontend-test-site/tests/e2e/smoke.spec.ts
@@ -67,7 +67,8 @@ test("Колокольчик открывает центр уведомлени
await page.getByRole("link", { name: "Уведомления" }).click();
await expect(page).toHaveURL(/\/notifications$/);
await expect(page.getByRole("heading", { name: "Центр уведомлений" })).toBeVisible();
- await expect(page.getByText(/доступны после входа/)).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Уведомления доступны после входа" })).toBeVisible();
+ await expect(page.getByRole("button", { name: "Авторизоваться" })).toBeVisible();
});
test("первое сообщение требует обязательные согласия", async ({ page }) => {
@@ -99,6 +100,18 @@ test("профиль гостя не делает защищённый запр
let protectedCalls = 0;
await page.route("**/api/v1/me", (route) => { protectedCalls++; return route.abort(); });
await page.goto("/profile");
- await expect(page.getByText("Профиль доступен после авторизации.")).toBeVisible();
+ await expect(page.getByRole("heading", { name: "Профиль доступен после входа" })).toBeVisible();
+ await expect(page.getByText("Авторизуйтесь, чтобы видеть личные данные и документы компании.")).toBeVisible();
+ await expect(page.getByRole("button", { name: "Авторизоваться" })).toBeVisible();
expect(protectedCalls).toBe(0);
});
+
+test("гостевой чат предлагает авторизоваться", async ({ page }) => {
+ await page.goto("/dialogs");
+ await expect(page.getByRole("heading", { name: "Чат доступен после входа" })).toBeVisible();
+ await expect(page.getByText("Авторизуйтесь, чтобы переписываться с оператором и получать ответы.")).toBeVisible();
+
+ await page.getByRole("button", { name: "Авторизоваться" }).click();
+ await expect(page).toHaveURL(/\/\?authorize=1$/);
+ await expect(page.getByRole("heading", { name: "Перед началом работы" })).toBeVisible();
+});