INOTrackбыстрота, надежность и ахуенность

FAQ: честные ответы на неудобные вопросы

Что будет, если ClickHouse станет недоступен?

Клик-путь не останавливается. Записи кликов копятся в кольцевом буфере в памяти и батчатся в ClickHouse раз в 1–2 секунды; если ClickHouse недоступен, запись уходит на диск в WAL-файл (JSON lines, с fsync), а отдельная горутина периодически пытается вычитать WAL и дослать накопленное в ClickHouse — при восстановлении связи данные догружаются автоматически, без ручного вмешательства. Реплей идемпотентен: если клик уже успел записаться, а WAL всё равно переиграл его повторно, дублей в таблице не будет.

Крайний случай: если и кольцевой буфер, и очередь спилла в WAL переполнены одновременно (экстремальный всплеск нагрузки при одновременной недоступности ClickHouse) — клик теряется, и это видно как рост отдельного счётчика в метриках (не молча).

⚠️ Отдельная находка, уже закрытая по умолчанию: соединение с ClickHouse у трекера асинхронное (async_insert=1, wait_for_async_insert=0) — сервер отвечает «ок», как только положил батч в СВОЮ очередь, а не когда физически записал его на диск. При падении ClickHouse ровно в это окно (проверено намеренным SIGKILL контейнера при нагрузочном тесте) батч пропадал бесследно — ни в ClickHouse, ни в WAL, потому что код уже считал вставку успешной. Дефолт с 24.09.2026 — INOTRACK_CLICK_CONFIRM_INSERT=true: вставка кликов дожидается физического подтверждения перед тем, как считать батч доставленным (цена — не задержка ответа посетителю, батчи пишутся фоново, а пропускная способность фонового флаша: ~130–150 тыс. вставок/с вместо ~500–640 тыс./с на одно соединение, целевые 41 000 кликов/с всё равно укладываются с запасом). Выключать (=false) — сознательный выбор оператора ради пропускной способности выше типичной нагрузки, а не поведение по умолчанию: потерянный клик — это потерянные деньги клиента.

Обратная сторона: постбеки при недоступном ClickHouse деградируют хуже. Приём конверсий на каждый запрос обращается к ClickHouse (антифрод-статистика) — если он недоступен, обработчик постбека паникует (перехватывается per-request recover, процесс не падает целиком), но конкретный входящий постбек рвётся с разрывом соединения вместо мягкой деградации, которую получил клик-путь. Это известное ограничение, оно исправляется отдельно от клик-пути.

Что будет, если недоступен Postgres?

Критичный путь клика (/go/{token}, /api/click) в Postgres не ходит вообще — конфиг потока читается из Redis. Если недоступен именно Redis (не Postgres) — резолв потока не удаётся, но клик всё равно фиксируется и уходит на глобальный fallback-URL, потому что трафик оплачен и терять его нельзя.

Postgres нужен управляющему контуру: CRUD сущностей через API, отчёты (частично), финансы, приём постбеков (резолв эндпоинта по токену — SQL-запрос на каждый входящий постбек). Если Postgres недоступен, эта часть отвечает ошибками, но приём кликов продолжается.

Теряются ли клики при перезапуске/обновлении?

При штатной остановке (tracker-ctl update, docker compose restart и т.п.) процесс ждёт до 20 секунд (настраивается INOTRACK_CLICK_SHUTDOWN_FLUSH_MS), чтобы полностью опустошить буфер в памяти — либо записать в ClickHouse, либо (если ClickHouse недоступен именно в этот момент) спиллить остаток в WAL на диск, а не отбросить. tracker-ctl update дополнительно ждёт, пока обновлённый инстанс станет healthy, и автоматически откатывает версию, если не дождался.

При аварийном обрыве процесса (kill -9, OOM, падение хоста) содержимое кольцевого буфера в памяти, не успевшее ни записаться, ни спиллиться в WAL, теряется — это неизбежное следствие того, что путь клика не пишет в Postgres/на диск синхронно на каждый запрос (иначе не будет заявленной пропускной способности).

Как поменять трекер-домен, если его заблокировали?

tracker-ctl domain add новый-домен.example.com
# направить A-запись нового домена на этот сервер, дождаться выпуска TLS
tracker-ctl domain rm заблокированный-домен.example.com

Пул трекер-доменов хранится одной переменной .env (TRACKER_DOMAINS), обслуживается одним и тем же Caddy — добавление/удаление применяется пересозданием контейнера Caddy (короткий перезапуск TLS-фронта, остальные сервисы не затрагиваются). Нельзя удалить последний домен из пула, не добавив новый — иначе приём кликов останется совсем без домена.

С 26.09.2026 это не единственный путь: домен, добавленный в панели «Трекер-домены», обслуживается сразу, как только его A/CNAME смотрит на сервер, — Caddy выпускает сертификат по on-demand TLS, спросив ядро, есть ли домен в пуле (в k3s ядро само ведёт Ingress пула). «Сделать основным» меняет домен во всех НОВЫХ ссылках — потоков, смартлинков, лендингов и воронок; уже разосланные ссылки остаются на своём домене, пока он жив. Панель помечает домен «не обслуживается этим сервером», если последняя проверка здоровья получила ответ не от этого трекера, и такой домен нельзя сделать основным. Подробности — «Обслуживание инстанса», раздел «Добавление/удаление трекер-домена».

Блокирует ли антискрейп-шлюз мой трафик по умолчанию?

Нет. С 24.09.2026 модуль guard подключён к клик-пути (/go/{token}, /api/click) и оценивает каждый клик, но действие «заблокировать» срабатывает только если запрос попал под явное правило (guard_rules — UA/IP/CIDR/ASN/ referer/гео/вердикт бот-модуля) или в явный список (свой блок-лист IP/CIDR/UA/ASN, или фид датацентровых сетей/скрейперов, если вы его подключили). Без единого настроенного правила или списка антискрейп никого не блокирует — подтверждённые поисковые краулеры (Google/Bing/Yandex) проходят всегда, раньше любых правил, а весь остальной трафик, если под правило не попал, — тоже проходит, даже если бот-модуль внутри пометил его как подозрительный (метка всё равно пишется в клик, её видно в отчётах и можно решить руками).

⚠️ Так было не всегда — это исправленная ошибка, а не изначальный дизайн: до исправления любой неверифицированный «бот»-вердикт (датацентровый IP, прокси, всплеск частоты с подсети — не только явный список) блокировался вслепую действием по умолчанию сайта, даже если ни одно правило не сработало. Поскольку site_id почти никогда не известен на пути /go/{token} (рекламные сети его не передают), это означало, что дефолт применялся практически к каждому клику — мобильные операторы с NAT-адресами, офисные сети, любой IP из подключённого фида получали пустой ответ вместо редиректа, оплаченный трафик молча терялся. Сейчас без известного site_id (то есть почти всегда на клик-пути) применяются только явные правила/списки, а сайт-дефолт (sites.guard_action) участвует лишь там, где site_id действительно известен — на реверс-прокси перед лендингами/сайтами сетки.

Тест правила перед боем — POST /api/internal/guard/test в панели (раздел «Антибот») или напрямую: подставить UA/IP/referer/гео и увидеть, какое правило сработает и что вернётся, без записи в лог и без риска для реального трафика.

Как купить лицензию — нужно ли кому-то писать?

Нет, покупка самостоятельная: https://inotrack.run/buy/ — тариф, срок (1, 3 или 12 месяцев), e-mail → страница заказа с адресом и суммой в USDT (TRC20) → после оплаты и 19 подтверждений сети ключ появляется на той же странице (один раз) и приходит письмом с [email protected]. Пошагово, с тем, что делать при недоплате и опоздании, — «Установка на свой сервер», «Как получить лицензионный ключ». Продление — только из личного кабинета (https://inotrack.run/account/, вход по ссылке из письма): срок текущей лицензии сдвигается, ключ остаётся прежним.

Как работает пробный период?

7 дней, лимиты тарифа Starter, один инстанс, без оплаты — нужен только e-mail. Взять можно на https://inotrack.run/trial/ или прямо в установщике (Enter вместо ключа, либо --trial-email, см. «Установка на свой сервер»). Ограничения:

  • Один пробный период на e-mail и один на сервер. Ответ на заявку всегда одинаковый — сайт не раскрывает, выдавался ли уже триал на этот адрес; если выдавался, нового письма просто не будет. Повторный триал на том же сервере с другим e-mail отклонит активация (trial_machine_used).
  • Продлить пробную лицензию нельзя, но купить тариф на тот же ключ — можно: в личном кабинете (следующий вопрос). Переустанавливать трекер и менять ключ не нужно.
  • После 7 дней пробная лицензия истекает так же, как любая другая (см. «Что происходит при истечении лицензии?» ниже).

Как перейти с пробного ключа на купленный?

Проще всего — купить тариф на сам пробный ключ. В личном кабинете https://inotrack.run/account/ (вход по ссылке из письма на тот же e-mail) у пробной лицензии есть кнопка «Купить и сохранить ключ»: выберите тариф и срок, оплатите заказ — и та же лицензия станет платной (тариф, лимиты, срок — с момента оплаты). Ключ, данные, серверы и бесплатные домены inotrack остаются прежними: трекер получит новый тариф сам при следующей связи с сервером лицензий, обычно в течение 15 минут. Работает и для уже истёкшего триала. Туда же ведёт «Купить» в плашке пробного периода в панели трекера, а с https://inotrack.run/buy/ — ссылка «Купите на эту лицензию». Неоплаченный заказ на пробную лицензию бывает один: повторное «Купить и сохранить ключ» с тем же тарифом и сроком откроет тот же заказ (адрес и сумма прежние; если по нему пришла часть суммы — с остатком к доплате), а другой тариф можно выбрать, когда неоплаченный заказ истечёт: через час после создания, у тарифа с ограниченным числом мест — через 20 минут, у недоплаченного — через сутки после недоплаты. Кабинет подскажет, какой заказ ждёт и сколько осталось.

Если вместо этого купили новую лицензию (через /buy/) — у неё свой ключ, его нужно прописать на сервере:

# 1. новый ключ — в INOTRACK_LICENSE_KEY
sudo nano /opt/inotrack/.env
# 2. кэш активации tracker-ctl привязан к прежнему ключу — убрать, иначе refresh
#    продолжит продлевать пробную лицензию
sudo rm /opt/inotrack/state/license.json
# 3. активация по новому ключу (и вход в registry обновлений с ним же)
sudo tracker-ctl license refresh
# 4. пересоздать контейнеры с новым .env — трекер увидит смену ключа и активируется заново
cd /opt/inotrack && sudo docker compose up -d

⚠️ Бесплатные домены inotrack привязаны к ключу, по которому их выдали. Домены, полученные на пробный ключ, удаляются через 30 дней после истечения пробной лицензии — и ссылки на них перестанут работать. После перехода получите домены на новый ключ (tracker-ctl domain claim --purpose clicks, при необходимости --purpose panel; это другие имена), сделайте новый трекер-домен основным в «Доменах» и переставьте ссылки в рекламных кабинетах, пока старые ещё живы. Свои домены этого не касается.

Как перенести лицензию на другой сервер?

Без поддержки — в личном кабинете https://inotrack.run/account/: у лицензии в списке «Серверы» рядом с действующим сервером есть кнопка «Освободить слот». Освобождённый сервер перестаёт продлевать лицензию и переходит в ограниченный режим (данные не удаляются), а лицензию можно сразу активировать на другом сервере тем же ключом. Самостоятельных освобождений — не больше 3 за 30 дней на лицензию; исчерпали — напишите в поддержку, перенесём вручную.

Порядок переезда:

  1. Остановите трекер на старом сервере (cd /opt/inotrack && sudo docker compose down).
  2. Освободите его слот в кабинете.
  3. Запустите новый сервер с тем же ключом (установка — «Установка на свой сервер», перенос данных — «Обслуживание инстанса», tracker-ctl backup/restore).

⚠️ Порядок важен. Работающий старый трекер после освобождения слота обратно его не займёт — он помнит, что слот освобождён. Но если старый сервер пересоздан без сохранённого состояния (чистая установка с тем же ключом, потерян каталог /var/lib/inotrack/license) и запустится раньше нового, он активируется заново как обычная машина и снова займёт слот — новому серверу места не останется, а освобождение уже потрачено. Поэтому сначала остановить старый, потом освободить слот, и только потом запускать новый.

Какие письма приходят после покупки и во время триала, и как от них отписаться?

Служебные письма приходят всегда: ключ, заказ, ссылка входа в кабинет, напоминания о сроке лицензии. Кроме них есть письма-подсказки — они помогают дойти до первого клика:

  • После покупки (по каждой платной лицензии, отсчёт от оплаты): в день оплаты — «как установить» (команда установки, установка, быстрый старт, переезд с Keitaro); через 2 дня, если ключ ещё ни разу не активирован, — «нужна помощь с установкой?» со ссылкой на поддержку; с 5-го дня, если трекер установлен и на связи, а кликов за последние сутки нет, — «подключите первый поток» (постбеки, макросы). Если трекер уже работает на этом ключе, первые два письма не приходят.
  • Во время триала: на 1-й, 3-й и 6-й день и после окончания; на 2-й день, если трекер установлен, а кликов за сутки нет, — то же «подключите первый поток».

Писем-подсказок — не больше одного в сутки на e-mail, язык — тот, на котором оформлен заказ или триал. «Кликов за сутки нет» сервер лицензий узнаёт из счётчика usage.clicks_24h в heartbeat (см. «Какую телеметрию инстанс отправляет license-server'у?») — по серверам той лицензии, о которой письмо: пробный трекер без кликов не вызовет письмо по платной лицензии. Трекеру, который этот счётчик не присылает или не выходит на связь, письмо про поток не уходит.

Отписаться — по ссылке внизу любого письма-подсказки (или кнопкой «Отписаться» в почтовом клиенте). Отписка действует на все письма-подсказки сразу; служебные продолжат приходить.

Оплата в USDT: что если сумма, сеть или время не те?

  • Сеть и токен — только USDT в сети TRON (TRC20). Платёж в другой сети (ERC20, BEP20) или другим токеном магазин не увидит — пишите в поддержку (https://inotrack.run/support/) с хешем транзакции, автоматически он не засчитается.
  • Недоплата — заказ переходит в «Сумма пришла не полностью» и ждёт доплату на тот же адрес 24 часа.
  • Переплата — заказ засчитывается как оплаченный, излишек магазин отмечает у себя (на странице заказа его не видно); вернуть или зачесть его — через поддержку с номером заказа.
  • Не успели за 60 минут — полная сумма, отправленная на адрес заказа в течение 7 дней, всё равно засчитается. Если ещё не платили — оформите новый заказ: адрес у каждого заказа свой.
  • Картой оплатить пока нельзя.

Что за бесплатные домены inotrack и в чём подвох?

Если своего домена нет, установщик (--free-domains) или tracker-ctl domain claim выдают поддомен вида abcd2345.lnkhop.online в одной из зон inotrack (lnkhop.online, redirly.online, nxtclk.online, gohopr.site, clkpath.site) — под панель и под приём кликов. A-запись ставит license-server на IP сервера, с которого пришёл запрос; TLS выпускает ваш же Caddy. Подробности установки — «Установка на свой сервер», «Бесплатные домены inotrack». Что важно знать:

  • Зоны общие для всех клиентов inotrack. Репутация зоны у рекламных площадок и антивирусов — общая: если площадка заблокирует зону целиком, это заденет и ваш поддомен. Для серьёзного трафика заведите свои домены (их можно добавить позже и сделать основными, см. «Как поменять трекер-домен, если его заблокировали?»).
  • Запись без прокси Cloudflare (proxied=false, TTL 300): трафик идёт прямо на ваш сервер, нужен публичный IPv4 — за NAT или только с IPv6 license-server откажет (ip_not_allowed).
  • Число доменов ограничено тарифом (limit_exceeded). Повторный запрос того же назначения возвращает тот же домен.
  • Переезд на другой сервер — повторите tracker-ctl domain claim уже с нового сервера: запись переедет на его IP, но не чаще раза в час (ip_change_too_soon).
  • Домены живут, пока жива лицензия: у отозванной или истёкшей больше 30 дней назад лицензии они удаляются. Пробный ключ → купленный — см. вопрос выше.

Что происходит при истечении лицензии?

Протокол лицензирования (активация ключа, heartbeat к license-server, офлайн-грейс 72 часа по умолчанию, режимы soft/hard) работает и со стороны CLI — tracker-ctl license info/refresh показывают реальный статус, реальный дедлайн грейса и реально ходят на license-server.

С 24.09.2026 бинарь трекера, который принимает трафик, тоже проверяет лицензию: подпись токена проверяется вшитым Ed25519-ключом, состояние кэшируется на диске (с привязкой к ключу лицензии и отпечатку машины, с защитой от отката системных часов назад), режим ok/soft/hard считается по правилам протокола лицензирования. До этого в трекере стояла заглушка, всегда отвечавшая «лицензия действительна».

С 03.10.2026 в релизном образе разрешающей заглушки нет вовсе:

  • Dev-режим — только в сборке разработчика. INOTRACK_LICENSE_DEV=1 отключает проверку лишь в бинаре, собранном из исходников с тегом licensedev (go build -tags licensedev) — для тестов и локальных стендов. Релизный образ собирается без тега: там переменная игнорируется, в лог пишется ERROR, а лицензия проверяется как обычно.
  • Клиент не удалось создать (публичный ключ license-server не вшит в образ и не задан INOTRACK_LICENSE_PUBKEY). Инстанс не падает — админка открывается, причина видна в логе уровнем ERROR, — но лицензия считается недействительной: режим hard (что это значит для трафика — ниже). Раньше здесь был откат к «разрешено всё».
  • Публичный ключ вшивается в образ на сборке: тогда INOTRACK_LICENSE_PUBKEY из окружения игнорируется, и подставить свой ключ вместе со своим license-server (INOTRACK_LICENSE_URL) нельзя — чужой сервер не подпишет токен, проходящий проверку.

Как это работает на практике — и где это работает не совсем так, как описывает протокол:

  • soft (лицензия просрочена, но офлайн-грейс ещё не исчерпан, либо сервер сам прислал soft) закрывает весь /api/v1 разом, а не только платные модули: проверка лицензии на каждом запросе к API отказывает для любой функции, если текущий режим не ok. Под /api/v1 живут не только платные прокси (отчёты, правила, финансы), но и обычные офферы/потоки/источники/сайты/вебхуки, и панель тоже ходит туда же по сессионной куке. Практически в soft-режиме перестаёт отвечать вся управляющая часть продукта целиком (403 license_required), а не только тарифицируемые модули; сам вход в панель (/api/internal/auth, отдельный от /api/v1 контур) при этом ещё работает, но любое действие внутри уже нет.
  • hard (грейс исчерпан, либо сервер явно прислал hard) даёт тот же эффект, что и soft, плюс /readyz начинает отвечать 503. Но сам приём кликов (/go/{token}, /api/click) и постбеков (/pb/{token}) режим лицензии не проверяет вообще — эти пути не ходят через /api/v1 и режим лицензии не спрашивают. Остановка трафика в hard-режиме работает, только если что-то снаружи реально следит за /readyz и убирает инстанс из балансировки: в deploy/k8s/*.yaml это подключено, а в поставляемом deploy/docker-compose.yml — нет. Caddy (deploy/Caddyfile) проксирует трекер-домены на tracker-core:8080 безусловно, без health-проверки апстрима; docker healthcheck контейнера tracker-core — это голый TCP-коннект на админский порт (tracker-ctl healthcheck-local), он тоже не спрашивает /readyz. Вывод: в стандартной самостоятельной установке (docker compose) даже исчерпанный офлайн-грейс не останавливает приём кликов и постбеков — гасится только /api/v1 и подсвечивается /readyz, на который в этой сборке никто не смотрит.

Не gated лицензией вовсе, независимо от режима: smartlink, landing, funnel, finance, guard (антибот-шлюз), MCP, мультипользовательский доступ. В лицензии заведено 12 переключателей функций, но реально трекер проверяет только четыре: public_api (описан выше, проверяется на каждый запрос), rules_engine, push_service и rank_tracker — причём последние три проверяются один раз при старте процесса (решают, монтировать ли модуль вообще), а не на каждый запрос: если лицензия деградирует уже во время работы инстанса, уведомления браузеру и раздел рейтинг-трекера продолжат отвечать до перезапуска.

Какую телеметрию инстанс отправляет license-server'у?

Коротко: счётчики, служебные идентификаторы активации, домены самой установки и внешний адрес сервера — не офферы, не креативы, не домены сайтов и источников трафика, не суммы и не тексты ошибок. Полный, дословный список полей POST /v1/heartbeat уходит раз в ~15 минут по умолчанию (INOTRACK_LICENSE_HEARTBEAT_MIN, с джиттером ±20%):

Поле Что это
license_token текущий подписанный токен лицензии (сам не содержит ни доменов, ни офферов)
instance_id id активации, выдан сервером один раз при установке
fingerprint sha256(machine-id инстанса + отсортированный список трекер-доменов) — непрозрачная строка; почему по ней нельзя восстановить домены — см. ниже
usage.clicks число кликов за период — счётчик, не список кликов
usage.conversions число конверсий за период
usage.users число пользователей админки
usage.domains число подключённых трекер-доменов, не их перечень
usage.clicks_24h число кликов за последние сутки (без тестовых) — одно число на весь инстанс, без разбивки по потокам, доменам и источникам. Считается в фоне раз в час запросом к ClickHouse, heartbeat берёт готовое значение; поля нет, если ClickHouse не подключён или не отвечает дольше трёх часов
version версия сборки трекера (например 1.4.2)
domains домены самой установки списком: домен панели, трекер-домены (включая бесплатный домен пула) и домены edge-нод. Не домены ваших сайтов, лендингов и источников трафика. Нужны для лицензирования и защиты от нелегального использования — по ним видно, где работает лицензия
public_ip внешний IP сервера, который установщик определил и записал в .env (INOTRACK_PUBLIC_IP); поля нет, если значение пустое или none. Отключить: INOTRACK_PUBLIC_IP=none при установке (см. установку). Адрес, с которого пришёл сам запрос, license-server видит в любом случае

Домены и адреса установки хранятся с историей 12 месяцев; свои вы видите в кабинете, при удалении аккаунта они стираются.

Это и есть весь состав — не пересказ, а точная схема, зафиксированная автотестами на обеих сторонах протокола (в трекере и в license-server): они падают, если кто-то добавит в отчёт новое поле, даже если оно не будет использовано сразу.

Реальное тело запроса, перехваченное на тестовом инстансе (значения подставлены для примера):

{"license_token":"<ILT>","instance_id":"ino_9f2c...","fingerprint":"82a2d405ab91bc9b414e2ba15e257324aa4793bebfb5297600bdfdbf78f153e3","usage":{"clicks":424242,"conversions":777,"users":3,"domains":5},"version":"1.4.2"}

Почему fingerprint не восстанавливается перебором доменов. Это не хэш одного домена с фиксированной солью — такую схему действительно можно взломать перебором: доменных имён в интернете конечное и не такое большое число, GPU перебирает миллиарды sha256 в секунду. Здесь состав другой: sha256(machine-id + "|" + отсортированные_домены_через_запятую), где machine-id — случайные 16 байт (128 бит энтропии), сгенерированные самим инстансом при первой установке и хранящиеся только на диске клиента (/var/lib/inotrack/license/machine-id, права 0600, права root-only контейнера). На license-server и куда-либо ещё этот machine-id отдельно никогда не передаётся — только уже захэшированный вместе с доменами результат. Чтобы перебором подобрать исходные домены по одному лишь fingerprint, атакующему нужно одновременно перебрать и домен, и 128-битный секрет, которого он не знает и не видит ни в одном сообщении протокола — это вычислительно неосуществимо (в отличие от перебора одного лишь домена по известной соли). Единственное, для чего годится fingerprint, — сравнение на равенство с уже известным конкретным значением (например, в расследовании конкретного инцидента, если домен и так уже известен стороне, которая расследует), а не восстановление списка доменов с нуля.

Отдельно от heartbeat-телеметрии — активация. POST /v1/activate (один раз при установке инстанса, и заново — только если лицензия принудительно сброшена в админке или переехала на другую машину) действительно передаёт домены трекера в открытом виде, полем domains. Это не телеметрия, а отдельный, самостоятельный механизм привязки лицензии к месту установки (домен-лок — та же логика, что у большинства node-locked лицензий любого другого ПО). После активации heartbeat сообщает домены установки полем domains (см. таблицу выше) — так license-server узнаёт о смене доменов без повторной активации; статистики по доменам в нём нет, только их число (usage.domains).

По какому курсу считаются деньги в разных валютах, и что если источник курсов недоступен?

Курс фиксируется на дату события, а не на момент, когда вы открыли отчёт: для начисления это occurred_at, для расхода — spent_on, для периода отчёта — его конец. Один и тот же запрос с одной и той же датой обязан вернуть одну и ту же сумму сегодня и через месяц — это не общее пожелание, а поведение, явно проверенное автотестом, потому что именно его нарушение делает отчёт за прошлый период невоспроизводимым. История курсов хранится в fx_rates (не перезаписывается, только дополняется) — можно всегда посчитать, что показал бы отчёт неделю назад на тогдашних курсах.

₽/$/€ равноправны (ни одна не «главная валюта хранения» — суммы в finance_entries лежат в исходной валюте события), но технически рубль используется как расчётный пивот: ЦБ РФ публикует курсы только парами «валюта → рубль», прямого курса USD/EUR взять неоткуда, кроме как посчитать через рубль. Источники — ЦБ РФ (USD/EUR) и CoinGecko (USDT), синхронизация раз в сутки фоновым воркером.

Если на дату события в fx_rates есть хоть какой-то курс не позже неё — используется последний известный до этой даты, и это не деградация: ЦБ не публикует курс по выходным и праздникам, а бухгалтерский стандарт — курс последнего рабочего дня перед операцией. Если курса на эту дату вообще нет ни на одну более раннюю дату (fx_rates пуст, синхронизация ни разу не отработала, либо дата раньше самой первой сохранённой) — конвертация отвечает громкой ошибкой, а не тихо считает 1:1 или берёт курс из будущего: соврать цифрой в P&L/балансе хуже, чем показать понятную ошибку.

Видимость свежести источника — GET /api/v1/finance/fx/status (роль admin): по USD и EUR отдаёт дату последнего известного курса, возраст в часах и флаг stale (сегодня — старше 3 суток подряд без обновления, не считая обычных выходных). Это единственный способ увидеть «источник курсов не отвечает уже который день» отдельно от «сегодня суббота, курса ещё не было» — если stale=true, синхронизация ЦБ/CoinGecko сломана и это стоит проверить.

Кому доступна ссылка на отчёт, если я поделился ею?

В трекере есть секретные read-only ссылки на отчёт (создание, список выданных, отзыв, ограничение по сроку жизни, скрытие финансовых полей) — и по ссылке отчёт действительно доступен тем, у кого нет учётной записи в системе. Управление ссылками (POST /shares, GET /shares, POST /shares/{id}/revoke) по-прежнему живёт под авторизованным /api/v1/reports/* и требует Bearer-токен, как и весь остальной прокси отчётов. А сам просмотр по ссылке (GET /shares/view/{token}) открыт публично под /shares без Bearer-токена и без сессии — только по неугадываемому токену в URL, ровно так, как и задумано. Ограничение частоты применяется по самой ссылке (а не по IP запроса), чтобы утёкшая ссылка не превратилась в бесплатную нагрузку на ClickHouse.

Отзыв ссылки мгновенно закрывает доступ всем, у кого она есть — проверка токена шаринга идёт по активной записи в базе, по отозванной ссылке ответ — 410 Gone.

Кто ещё может входить в панель, кроме первого администратора?

Второго (и третьего) пользователя заводит владелец (роль admin) — приглашением, штатно, без правки БД. Ролевая модель — три роли, admin/buyer/viewer, buyer видит только свои потоки/источники, viewer — read-only.

# только роль admin, кука сессии владельца (см. «Быстрый старт», приложение «через API», шаг 0)
curl -s -b cookies.txt -X POST "$TRACKER/api/internal/auth/users/invite" \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","role":"buyer"}'
# в ответе — token приглашения, отдаётся ОДИН РАЗ (в БД хранится только его sha256-хэш).
# Ссылка для приглашённого: https://<панель>/invite?token=<token>, срок жизни 7 дней.

Приглашённый открывает ссылку, вводит имя и пароль (планка сложности — см. «Обслуживание инстанса», раздел «Переменные окружения») и сразу получает рабочую сессию — POST /api/internal/auth/invite/accept, публичный маршрут: у него ещё нет аккаунта, значит и сессии, токен приглашения — единственная защита.

Владелец управляет командой той же группой ручек (все — только роль admin):

  • GET /api/internal/auth/users — список сотрудников;
  • PATCH /api/internal/auth/users/{id}/role — сменить роль;
  • POST /api/internal/auth/users/{id}/disable / /enable — отключить/вернуть доступ. Отключение форсирует релогин немедленно (отзывает все сессии), а не просто запрещает следующий /login;
  • POST /api/internal/auth/users/{id}/2fa/reset — сбросить 2FA другому пользователю (не себе — см. восстановление доступа ниже).

Нельзя понизить или отключить последнего активного администратора — попытка вернёт 409 conflict, продукт не даёт запереть команду без владельца.

В панели то же самое — раздел «Пользователи и аудит» (/users): список сотрудников, приглашения и журнал действий; curl выше нужен для скриптов.

Администратор потерял телефон с 2FA — как вернуть доступ?

Раньше — никак штатно (только прямой UPDATE в Postgres). Сегодня — два независимых рабочих пути, оба проверены автотестами:

  1. Если в команде есть второй admin. Он логинится как обычно (паролем + своей 2FA) и сбрасывает 2FA пострадавшему:

    curl -s -b cookies-второго-админа.txt -X POST \
      "$TRACKER/api/internal/auth/users/<id-пострадавшего>/2fa/reset"
    

    Работает без рестарта процесса. Украденный пароль жертвы здесь бесполезен — нужна живая сессия второго администратора, а не что-либо, что знает атакующий.

  2. Если администратор один (второго завести не успели, либо это единственный сотрудник в системе) — аварийный выключатель через переменную окружения на сервере:

    # в .env инстанса (/opt/inotrack/.env), затем перезапуск процесса
    [email protected]:incident-2026-09-24
    

    Метка после : — любая непустая строка, уникальная для конкретного инцидента (проще всего дата или номер тикета): именно по ней (а не по факту наличия переменной) продукт решает, применять сброс ещё раз или нет. Это защита от самой частой ошибки такого рода рубильников — забыть убрать переменную после инцидента: без метки следующий обычный рестарт (обновление, перезапуск контейнера) снёс бы 2FA ещё раз, уже после того как администратор настроил её заново. С меткой повторный рестарт с тем же значением — no-op; для нового инцидента нужно осознанно сменить метку.

    Сброс выполняется синхронно при старте процесса, до открытия портов приёма трафика — применение видно в логе уровнем ERROR («АВАРИЙНЫЙ СБРОС 2FA выполнен... немедленно войдите паролем, настройте 2FA заново и уберите переменную из конфигурации») и записью в audit log. Сразу после входа настройте 2FA заново (POST /2fa/setup → /2fa/confirm) и уберите переменную из .env — она не самоочищается.

    Это ровно тот механизм, который отличает «физический доступ к серверу» (может отредактировать .env и перезапустить контейнер) от «украденный пароль/кука» (ни один HTTP-запрос этот путь не запускает) — что и требовалось: доступ к серверу даёт выход, украденный пароль — нет.

И в обоих случаях, и без них — то, что не меняется по дизайну (осознанное решение продукта, не дефект): POST /2fa/disable безусловно отвечает 403 для роли admin независимо от пароля или recovery-кода — 2FA для владельца обязательна, и её нельзя снять самому себе паролём, только одним из двух путей выше. У ролей buyer/viewer self-service работает штатно: POST /2fa/verify по recovery-коду пускает войти, затем POST /2fa/disable паролем снимает 2FA, затем POST /2fa/setup заводит новое устройство.

Что за уведомления в браузер (push), и как их включить?

Свой Web Push сервис (RFC 8291/8030 доставка, RFC 8292 VAPID) — не сторонний провайдер вроде OneSignal, ключи и подписки живут у вас. Собирает web-push подписки посетителей сайтов сетки и лендингов, сегментирует базу по сайту/вертикали/гео, шлёт рассылки по расписанию или триггерам на офферы. Клик по уведомлению — не отдельная аналитика в вакууме: ссылка кампании — обычная трекинговая ссылка (/go/{token}), редирект идёт тем же клик-путём и попадает в общие отчёты как любой другой источник трафика; свои push_sends/CTR — это метрика открываемости самой рассылки, а не альтернативный источник правды о трафике.

Включение — переменная INOTRACK_PUSH_ENC_KEY (AES-256-GCM ключ для приватного ключа VAPID в БД, см. «Обслуживание инстанса»). Без неё воркер рассылки не стартует, а GET /push/vapid- public-key не отдаёт ключ — это поддерживаемый режим (модуль выключен), не ошибка конфигурации, ядро логирует понятную причину и продолжает работать дальше. С ключом публичный ключ VAPID генерируется автоматически при первом старте.

В панели — раздел «Push-уведомления» (/push): кампании, сегментация и ручной запуск рассылки, вкладка мобильных приложений. HTTP-ручки модуля (подписка/отписка с сайта, CRUD кампаний, запуск рассылки) — для сайтов и скриптов.

Дальше

Установка — «Установка на свой сервер». Обслуживание — «Обслуживание инстанса». API — «Публичный API и MCP».

Поддержка