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 дней на лицензию; исчерпали — напишите в поддержку, перенесём вручную.
Порядок переезда:
- Остановите трекер на старом сервере (
cd /opt/inotrack && sudo docker compose down). - Освободите его слот в кабинете.
- Запустите новый сервер с тем же ключом (установка — «Установка на свой
сервер», перенос данных — «Обслуживание инстанса»,
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-режиме перестаёт отвечать вся управляющая часть продукта целиком (403license_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-проверки апстрима; dockerhealthcheckконтейнера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). Сегодня — два независимых
рабочих пути, оба проверены автотестами:
Если в команде есть второй
admin. Он логинится как обычно (паролем + своей 2FA) и сбрасывает 2FA пострадавшему:curl -s -b cookies-второго-админа.txt -X POST \ "$TRACKER/api/internal/auth/users/<id-пострадавшего>/2fa/reset"Работает без рестарта процесса. Украденный пароль жертвы здесь бесполезен — нужна живая сессия второго администратора, а не что-либо, что знает атакующий.
Если администратор один (второго завести не успели, либо это единственный сотрудник в системе) — аварийный выключатель через переменную окружения на сервере:
# в .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».