diff --git a/tradein-mvp/backend/data/sql/184_user_events.sql b/tradein-mvp/backend/data/sql/184_user_events.sql new file mode 100644 index 00000000..af2e0c09 --- /dev/null +++ b/tradein-mvp/backend/data/sql/184_user_events.sql @@ -0,0 +1,60 @@ +-- 184_user_events.sql +-- Foundation schema for Features 2 & 3: unified user-event tracking. +-- +-- WHY: +-- trade-in had no durable event log for login/IP audit or behavior +-- analytics. A near-identical design already existed once — `audit_log` +-- (event_type, ip_address inet, user_agent, estimate_id uuid, payload +-- jsonb, created_at) was defined in 002_core_tables.sql and DROPPED in +-- 095_dead_schema.sql as unused dead schema at the time. Product now +-- needs exactly that shape again, so this migration resurrects the +-- design under a new name, `user_events`, as the ONE unified +-- append-only table serving: +-- - login-audit (who logged in, from what IP/UA, when) +-- - search-audit (estimate requests, listing lookups) +-- - behavior-analytics (pdf_download, listing_click, page_view, +-- drawer_open, and future event_type values) +-- +-- WHAT: +-- `user_events` — append-only, admin-read-only. No 152-ФЗ consent +-- gating and no mandatory retention policy for MVP (product-owner +-- decision): we log IP/UA/path/method/payload unconditionally for every +-- tracked event. `estimate_id` is a plain uuid column with NO FK +-- constraint on purpose — this keeps the log decoupled/append-only so +-- estimate deletion (or any future estimate lifecycle change) never +-- blocks or cascades into event rows. +-- +-- IDEMPOTENCY / SAFETY: +-- CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS throughout — +-- safe re-run. Purely additive: no existing table/view/column is +-- touched. +-- +-- Dependencies: none (new standalone table). Auto-applied on deploy via +-- _schema_migrations tracking (tradein-mvp/backend/data/sql convention). + +BEGIN; + +CREATE TABLE IF NOT EXISTS user_events ( + id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + event_type text NOT NULL, + username text NOT NULL, + ip_address inet, + user_agent text, + path text, + method text, + estimate_id uuid, + payload jsonb NOT NULL DEFAULT '{}'::jsonb, + created_at timestamptz NOT NULL DEFAULT now() +); + +COMMENT ON TABLE user_events IS 'Unified append-only event log (login/IP audit + behavior analytics), admin-read-only. Resurrects the design of audit_log (002_core_tables.sql, dropped in 095_dead_schema.sql).'; +COMMENT ON COLUMN user_events.event_type IS 'e.g. login / estimate_request / pdf_download / listing_click / page_view / drawer_open.'; +COMMENT ON COLUMN user_events.username IS 'X-Authenticated-User value at the time of the event.'; +COMMENT ON COLUMN user_events.estimate_id IS 'No FK constraint by design — keeps the log decoupled/append-only from trade_in_estimates lifecycle.'; + +CREATE INDEX IF NOT EXISTS user_events_username_created_at_idx ON user_events (username, created_at DESC); +CREATE INDEX IF NOT EXISTS user_events_event_type_created_at_idx ON user_events (event_type, created_at DESC); +CREATE INDEX IF NOT EXISTS user_events_ip_address_idx ON user_events (ip_address); +CREATE INDEX IF NOT EXISTS user_events_created_at_idx ON user_events (created_at DESC); + +COMMIT; diff --git a/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx b/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx index 3c716ef7..d1e9628d 100644 --- a/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx +++ b/tradein-mvp/frontend/src/components/trade-in/Topbar.tsx @@ -9,6 +9,58 @@ import { safeUrl } from "@/lib/safeUrl"; import { useBrand } from "@/lib/useBrand"; import { useMe } from "@/lib/useMe"; +/** Telegram-канал обратной связи для пилота. Build-time env — пусто до тех + пор, пока devops не настроит реальную ссылку (см. FeedbackButton ниже). */ +const FEEDBACK_TG_URL = process.env.NEXT_PUBLIC_FEEDBACK_TG_URL ?? ""; + +/** Inline Send icon (lucide-react `Send` SVG path, stroke 1.5) — та же + конвенция, что и LogOutIcon в UserMenu.tsx: tradein-mvp не тянет + lucide-react в deps, поэтому иконка — inline SVG. */ +function TelegramIcon() { + return ( + + ); +} + +/** + * Кнопка обратной связи в Telegram — рендерится для ВСЕХ авторизованных + * пользователей (не зависит от RBAC allowed_paths, в отличие от NAV_ITEMS). + * + * Env-gated: пока `NEXT_PUBLIC_FEEDBACK_TG_URL` не задан на билде, кнопка не + * рендерится вообще — безопасно шипить до появления реальной Telegram-ссылки. + * safeUrl() дополнительно блокирует не-http(s) схемы (#766-class XSS). + */ +function FeedbackButton() { + const href = safeUrl(FEEDBACK_TG_URL); + if (!href) return null; + + return ( + + + Обратная связь + + ); +} + /** «Мера» mark — буква М над размерной линией («мера» = измерение). Глиф в currentColor; тёмный фон даёт CSS-бейдж .brand-mark. */ function MeraMark() { @@ -156,6 +208,10 @@ export function Topbar({ active }: TopbarProps) { {/* UserMenu — личный кабинет справа от nav. В dev (401) или до /me UserMenu сам рендерит null. */} + {/* FeedbackButton — вне items.map/RBAC: видна всем авторизованным + пользователям независимо от allowed_paths. Сама себя прячет, + пока NEXT_PUBLIC_FEEDBACK_TG_URL не задан. */} +