# LATTERDAY API v2.7.0 — для разработчика приложения

Локальный backend: `http://127.0.0.1:8000`. В релизе используйте свой HTTPS origin. Все запросы/ответы JSON UTF-8, кроме скачивания ZIP/обложек и 204. Время — Unix seconds UTC. ID непрозрачные строки. JSON-запросы должны иметь Content-Length: стандартные HTTP-клиенты выставляют его сами. Полная машинная схема всех маршрутов и тел — `openapi.json` из архива или `GET /openapi.json` работающего backend. Портал `/docs` доступен независимо от старого ENABLE_DOCS.

## Раздельный сайт и API, документация

HTML-сайт может находиться на другом хостинге. Используйте frontend/ либо отдельный frontend ZIP, настройте panel-assets/config.js (`apiBaseUrl` — HTTPS origin API или /api-прокси). На backend: PUBLIC_BASE_URL — origin API, FRONTEND_URL — origin сайта. CORS разрешает только точные FRONTEND_URL/CORS_ORIGINS; credentials передаются явно, `*` запрещён. Для поддоменов одного домена подходит COOKIE_SAMESITE=strict. Для независимых доменов предпочтителен /api reverse proxy; SameSite=None+Secure не отменяет блокировку сторонних cookie браузером. Полная настройка — SPLIT_HOSTING_RU.md. Native-клиент использует API напрямую и cookie не требует.

`GET /docs` — визуальный портал API: меню, поиск, параметры, curl и OpenAPI. При API_ONLY=1 портал также в корне API. Руководство доступно по /guide.md. Веб-страницы входа на API-хосте нужны только для локального/совместимого режима; production-сайт размещается отдельно.

Все относительные пути media/covers, media/avatars и content_path разрешайте относительно API base URL, а не сайта. Если base=/api, добавляйте путь к этому префиксу: `/api` + `/media/...`. Не используйте new URL('/media/...', apiBase), если apiBase содержит /api — это сбросит префикс. Учитывайте Range, Content-Range, ETag, Retry-After и X-Install-Token; нужные заголовки включены в CORS. Большие admin-upload также должны идти на API base URL.

## Что изменилось

Вход снова обычный и доступен напрямую: логин/пароль или Telegram QR. Скрытого тройного клика и обязательного перехода из EXE больше нет. На сайте (и на API при API_ONLY=0) `/`, `/login`, `/account`, `/profile`, `/panel` открывают один интерфейс: без сессии форму, после входа личный профиль. Владелец и пользователь с правами управления (`can_manage:true`) видят ссылку `/studio`; сам `/studio` и управляющие API всё равно проверяют права. Google-вход для пользователей отсутствует. Существующий Google OAuth владельца остаётся только для доступа backend к Google Drive.

Для нативного приложения есть два равноправных варианта: собственные формы/Telegram QR по API либо системный браузер с PKCE handoff. После входа получите `/me/profile` и покажите профиль. В browser handoff профиль также остаётся открыт на сайте, а приложение получает отдельную сессию. Пароли, токен бота и Google-секреты в приложение не вшиваются.

## Пара токенов и вход один раз

Успешный вход возвращает:

```json
{"access_token":"opaque-secret","refresh_token":"opaque-refresh-secret","token_type":"bearer","expires_at":1800001800,"refresh_expires_at":1802592000}
```

Защищённые запросы: `Authorization: Bearer <access_token>`. Access живёт 30 минут по умолчанию. Refresh — 30 дней без обновления; при обновлении срок продлевается, но абсолютный предел сессии 365 дней. Настройки сервера могут отличаться: используйте поля сроков. На ПК храните refresh в Windows Credential Manager/DPAPI, access — в памяти. Пароль и секретную фразу ради автоматического входа сохранять не нужно.

`POST /auth/refresh`, без Bearer, `{refresh_token}` → новая пара. Старые access/refresh сразу недействительны. Повторное применение старого refresh **отзывает всю эту сессию**. Все запросы клиента должны ждать одну общую операцию обновления (mutex/single-flight), а запись нового refresh в защищённое хранилище должна быть атомарной. Обновляйте за 30–60 секунд до expires_at. Другие устройства не отзываются при обычном обновлении.

После неоднозначного сетевого сбоя на refresh не повторяйте старый токен бесконечно: сервер мог заменить его, а ответ потеряться. При 401 refresh удалите сохранённые секреты и покажите вход. Восстановление, отзыв сессии, блокировка или истечение срока также могут потребовать вход заново. «Вход один раз» означает сохранённую обновляемую сессию, а не бессрочный доступ.

`POST /auth/logout` с Bearer → 204: отзыв текущей сессии. `POST /auth/revoke`, `{refresh_token}`, без Bearer → `{revoked:true}`: отзыв по текущему refresh. После выхода очистите локальное хранилище.

## Изменения v2.7

Для сайта и API нужно обновить оба архива. База мигрирует автоматически (schema 8); перед обновлением сохраните её резервную копию. Не удаляйте data/, secrets/ и .env. История аналитики накапливается с обновления; существующие неочищенные установки импортируются, старые скачивания восстановить нельзя.

## Логин и регистрация

`GET /auth/methods` → `{telegram_enabled,telegram_bot_username,telegram_status,password_enabled,registration_enabled}`. Показывайте доступные методы по этим флагам.

`POST /auth/login`:

```json
{"username":"player123","password":"a-long-unique-password","device_name":"My Windows PC"}
```

200 → пара токенов. Затем `GET /me/profile`. username: 3–40 символов, первая буква/цифра, только строчные a–z, 0–9, _, . и -. Пароль при входе 1–128 символов; при создании/смене — 12–128. device_name необязателен, 1–80 символов. Если на сервере администратору запрещён пароль (`ADMIN_PASSWORD_LOGIN=0` при настроенном боте), он входит через Telegram.

`POST /auth/register` → 201:

```json
{"username":"player123","password":"a-long-unique-password","telegram_contact":"@example","discord_contact":"example","device_name":"My Windows PC"}
```

Контакты необязательны, до 100 символов каждый. Ответ: пара токенов плюс `user:{id,username,is_admin:0}`, `recovery_phrase`, `recovery_notice`. Фраза содержит 12 английских слов (128 бит энтропии), показывается **один раз**. Покажите отдельный экран сохранения и подтверждение пользователя. Не логируйте слова и не отправляйте их в аналитику. На сервере только солёный PBKDF2-хеш. Это секрет LATTERDAY, не криптокошелёк.

Новая регистрация не даёт административных прав и индивидуальных разрешений на пакеты.

## Telegram QR — как в Nixora

Протокол адаптирован из предоставленного проекта: QR → личный чат с ботом → кнопка подтверждения → polling. QR генерируется самим backend; внешний QR-сервис не получает ссылку.

1. `POST /auth/telegram/qr`, тело `{device_name:"My Windows PC"}` → 201:

```json
{"token":"32-character-secret","binding":"separate-random-secret","deep_link":"https://t.me/YOUR_BOT?start=auth_SECRET","expires_at":1800000300,"expires_in":300,"poll_interval":2,"qr_data_url":"data:image/png;base64,..."}
```

2. Покажите qr_data_url и кнопку deep_link. Сохраняйте token и binding только в памяти текущего окна. **Binding не входит в QR**, ссылку или URL опроса.
3. Пользователь сканирует QR/открывает ссылку, нажимает Start, затем «Подтвердить вход» в личном чате. Само открытие бота ещё не подтверждает вход. Подтверждение и отказ принимаются только от того Telegram ID, который открыл эту QR-сессию. Групповые сообщения и чужие callbacks не принимаются.
4. Раз в 2 секунды, без параллельных запросов: `POST /auth/telegram/qr/poll`, `{token,binding}`. До подтверждения → 200 `{status:"pending",expires_in}`. Отказ → `{status:"rejected",expires_in}`: остановить опрос. Просрочка/неверный binding/повторный обмен → 401.
5. Успех → 200: пара токенов плюс `{status:"approved",user:{id,username,is_admin},created:false}`. QR сразу потреблён; второй раз пару получить нельзя.
6. При **первом** входе ранее не связанного Telegram создаётся обычный аккаунт: `created:true`, дополнительно `recovery_phrase` и `recovery_notice`. Сохраните фразу так же, как при регистрации паролем. Начальный username вида `tg_123456` или безопасный свободный вариант; пароль случайный и пользователю не сообщается. Установить пароль можно через `/me/credentials`, доказав доступ фразой. Логин меняется через заявку `/me/username-requests` после одобрения администратором.

Сопоставление выполняется по **числовому Telegram ID** из настоящего bot update, не по @нику. Уже привязанный ID входит в прежний аккаунт с прежними grants/is_admin. Имя и ник обновляются при QR-входе, аккаунт не дублируется. Совпавший ник или username не объединяет аккаунты. Отключённый пользователь не включается автоматически. При `REGISTRATION_ENABLED=0` незнакомый Telegram не регистрируется, существующий может войти.

Root по умолчанию автоматически привязывается к Telegram ID `1131790272` (подпись @vvNaGI не является доказательством). ADMIN_PASSWORD для первого старта не требуется. Приватный ROOT_TELEGRAM_ID может явно переопределить владельца для другой установки. Если ID уже связан с аккаунтом, сохраняется его user ID и grants. QR не принимает is_admin от клиента. База Nixora и её пользователи не импортируются: переносится сценарий входа, а аккаунты LATTERDAY сопоставляются по своей таблице привязок.

Публичного HTTP-маршрута «подтвердить QR с таким Telegram ID» нет. Подтверждение выполняет интегрированный бот после подлинного callback Telegram. Не делайте собственный небезопасный endpoint для этого.

## Данные профиля

`GET /me` → `{id,username,is_admin,roles,permissions,can_manage,is_owner}`. Для интерфейса используйте `GET /me/profile`:

```json
{"id":"opaque-id","username":"player123","auth_method":"telegram","avatar_url":"/media/avatars/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.jpg","display_name":"Name Surname","is_admin":0,"telegram_username":"example","telegram_first_name":"Name","telegram_verified_id":"123456","telegram_contact":"@example","discord_contact":"example","discord_verified":false,"has_recovery_phrase":true,"created_at":1800000000,"stats":{"installations":3,"reported_completed":2,"available_packages":4}}
```

`display_name` берётся из имени/фамилии подтверждённого Telegram, иначе из login. Числовой ID — строка. Не принимайте display_name или Telegram-ник за уникальный идентификатор. Новые поля: `auth_method` (`password` или `telegram`) и `avatar_url`. При Telegram-входе avatar_url — `/media/avatars/<hash>.jpg`, если фото доступно боту и кешировано, иначе `/media/avatars/default.svg`. При входе паролем всегда общая заглушка, независимо от наличия Telegram-привязки. Метод сохраняется при refresh и PKCE exchange. URL содержит только путь API, без Telegram token. Картинка JPEG 256×256, EXIF удалён; отсутствие/приватность/сбой фото не блокируют вход. После обновления существующая сессия без записи метода считается password до нового Telegram-входа. Статистика считается по текущей базе, не является бессрочной статистикой всех загрузок. reported_completed — отчёт приложения, не независимая серверная проверка.

- `PATCH /me/profile`, `{telegram_contact,discord_contact}` → `{updated:true,contacts_verified:false}`. Передайте оба поля, пропущенное очищается. Редактирование контакта не меняет подтверждённый Telegram ID.
- `GET /me/activity` → `{items:[{at,action}]}`, последние 20 собственных событий. action — строка события; неизвестные значения отображайте нейтрально.
- `GET /me/installations` → `{items:[{id,created_at,status,title,version}]}`, последние 20 сохранённых установок. Статусы pending/completed/failed. Билеты установки и Google ID не раскрываются.
- `GET /me/sessions` → `{items:[{id,device_name,created_at,last_used,expires}]}`.
- `DELETE /me/sessions/{id}` → `{revoked:true}`. Только собственные сессии; отзыв текущей потребует входа снова.

`POST /me/telegram-link` с Bearer, пустое тело → `{code,expires_at,bot_url}`. Нужен для привязки Telegram к **уже существующему** аккаунту с паролем. Откройте bot_url, подтвердите в боте, затем повторите GET /me/profile. Ссылка одноразовая, 10 минут. Один ID — один аккаунт. Если пользователь сначала отдельно вошёл через Telegram и создал второй аккаунт, автоматического слияния нет. Для администратора привязка задаётся через TELEGRAM_ADMIN_ID.

### Логин, пароль, фраза

`PATCH /me/credentials`:

```json
{"username":"current_login","new_password":"new-long-password","recovery_phrase":"the actual twelve secret words"}
```

Вместо recovery_phrase можно передать `current_password`. Нужен хотя бы один из них. Если указаны оба, проверяется фраза. Успех → `{updated:true,reauthentication_required:true}`; при доказательстве фразой возвращается **новая recovery_phrase**, старая перестаёт действовать. Все сессии и ожидающие одобренные восстановления отзываются. Покажите новую фразу, затем вход. Поле username должно совпадать с текущим логином: попытка изменить его этим методом → 409, изменения не сохраняются. Для смены логина используется отдельная заявка. Telegram остаётся связан с тем же user ID.

`POST /me/password`: `{current_password,new_password}` → `{changed:true,reauthentication_required:true}`, все устройства отзываются. `POST /me/recovery-phrase`: `{current_password}` → `{recovery_phrase,shown_once:true}`, старая фраза заменяется. Для Telegram-аккаунта без известного пароля применяйте /me/credentials с сохранённой фразой или восстановление через связанный Telegram.

## Восстановление доступа

Доступно без авторизации, с ограничением частоты. Каждый успешный reset меняет пароль, выдаёт новую фразу, отзывает прежние сессии и возвращает `{recovered:true,recovery_phrase,reauthentication_required:true}`. Сначала показать и сохранить новую фразу, затем войти заново. Отключённый аккаунт восстановление не включает.

| POST маршрут | JSON тело | Подтверждение |
|---|---|---|
| `/auth/recover/phrase` | username, recovery_phrase, new_password | Текущая секретная фраза |
| `/auth/recover/telegram` | code, new_password | 5-минутный одноразовый `/login` код связанного Telegram |
| `/auth/recover/manual` | reset_token, new_password | Одноразовый код администратора после ручной проверки |

В связанный Telegram можно просто войти по QR, даже без пароля/фразы. Для сброса пароля и новой фразы бот по команде `/login` выдаёт отдельный одноразовый code.

Если нет доказательства: `POST /auth/recovery-requests` с `{username,contact,message}` → 202 `{request_id,message}`. contact 3–150 символов, message 10–2000. Ответ не подтверждает существование логина. Не просите пользователя присылать пароль или фразу в поддержку. Очередь до 500 ожидающих заявок; при заполнении нейтральный ответ не гарантирует сохранение. Срок хранения заявок — 30 дней.

Администратор видит заявку в «Восстановление» панели. Контакты Telegram/Discord, введённые вручную, **не доказательство**. Совпадение ника не даёт автоматического доступа; нужны независимые проверяемые сведения. Подтверждение Telegram ID считается отдельно от текстового контакта. Discord OAuth/верификации нет.

Admin API: GET `/admin/recovery-requests?offset=0`; GET `/admin/recovery-requests/{id}/evidence`; PATCH `/admin/recovery-requests/{id}`, `{decision:"approve"|"reject",note}`. Одобрение возвращает reset_token на 1 час, показанный один раз. Передавать только проверенному владельцу. Автоматической отправки по произвольному контакту нет.

## Сайт: HttpOnly-сессии

Нативному приложению этот раздел не нужен. Сайт после логина вызывает `POST /auth/web/attach` с Bearer и `{refresh_token}`. Сервер проверяет принадлежность пары одной сессии и устанавливает HttpOnly cookie с выбранным COOKIE_SAMESITE (по умолчанию Strict) `latterday_web`; в HTTPS также Secure. Старый браузерный вход заменяется, другие устройства сохраняются.

`POST /auth/web/session`, `{}`, cookie → новый access, token_type, expires_at, refresh_expires_at. Новый refresh остаётся только в HttpOnly cookie и не возвращается JS. Это **ротация**, поэтому запросы между вкладками должны быть сериализованы. Предоставленный JS использует Web Locks (`latterday-web-session`); в браузере без Web Locks используйте одну вкладку.

`POST /auth/web/logout`, `{}` с cookie → `{logged_out:true}` и очистка cookie. Browser endpoints отвергают Origin вне точного списка разрешённых сайтов. Разрешённый FRONTEND_URL может быть cross-site; SameSite и политика браузера отдельно определяют отправку cookie. Управляющие и профильные API по-прежнему используют Bearer, не принимают cookie как самостоятельную авторизацию.

После обновления страницы сайт обновляет cookie-сессию и загружает профиль, включая настоящий is_admin и Telegram identity. Административная панель берёт имя из того же `/me/profile`, а не из константы admin.

## Системный браузер из приложения (опционально)

Если не хотите строить формы в EXE, используйте `tools/browser_login.py` (`pip install httpx`): функция `login_in_browser(base_url,device_name)` возвращает пару; хранение refresh реализует вызывающее приложение.

1. Создать случайный code_verifier 43–128 символов `[A-Za-z0-9._~-]`. code_challenge = base64url(SHA256(ASCII(verifier))), без `=`, 43 символа.
2. POST `/auth/browser/start`, `{code_challenge,device_name}` → 201 `{request_id,browser_url,expires_at,poll_interval:2}`. Срок 10 минут.
3. Открыть browser_url. JS удалит fragment из истории, выполнит POST `/auth/browser/open` с `{request_id,launch_token}`, получит временную HttpOnly flow-cookie. Прямой вход на сайт также разрешён, этот шаг только связывает окно с приложением.
4. Пользователь входит любым методом. После сохранения фразы открывается профиль, сайт автоматически вызывает POST `/auth/browser/complete` с Bearer+flow-cookie. Браузерная сессия сохраняется; flow-cookie удаляется. GET `/auth/browser/context` показывает `{active,device_name}`.
5. Приложение опрашивает POST `/auth/browser/exchange`, `{request_id,code_verifier}`, раз в 2 секунды: 202 pending или 200 новая отдельная пара приложения. Повторный exchange/чужой verifier/просрочка → 401. Секрет verifier никогда не входит в браузерную ссылку.
6. Сохранить refresh в защищённом хранилище. При следующих запусках использовать refresh без открытия браузера.

После неоднозначного сетевого сбоя успешного exchange начните новый вход: обмен одноразовый. PKCE защищает передачу от кражи одной ссылки, но не доказывает, что EXE официальный. Вшитый общий секрет проблему не решает.

## Каталог и установка

GET `/packages?offset=0&limit=100` → `{items:[Package],offset,limit}`; limit 1–200, Bearer. GET `/packages/{id}` → Package. Возвращаются только активные общедоступные/выданные пользователю или его ролям пакеты, администратору все активные.

Package: `{id,title,description,version,size,sha256,created_at,cover_path,category,game_edition,changelog}`. size — байты ZIP, sha256 — 64 строчных hex символа, cover_path пустой или путь `/media/covers/...jpg`, edition legacy/enhanced/both. Google Drive ID и токены конечному пользователю не раскрываются.

POST `/packages/{id}/installations`, пустое тело, Bearer → 201 `{installation_id,install_token,expires_at,content_path,package,game}`. По умолчанию 6 часов, максимум 5 незавершённых установок на пользователя. game содержит id gta5 и required_files: GTA5.exe для Legacy, GTA5_Enhanced.exe для Enhanced, один из двух для both. Билет привязан к аккаунту и сессии устройства, при refresh переносится на новую пару той же сессии. Не передавайте install_token в URL.

GET или HEAD content_path:

```http
Authorization: Bearer <current-access>
X-Install-Token: <install_token>
Range: bytes=1048576-
If-Range: "<previous-ETag>"
```

Без Range → 200 ZIP, Content-Length/ETag. С корректным Range → 206 и Content-Range. Поддерживается один диапазон. Если сервер вернул 200 вместо 206 — начинать файл заново, не дописывать к прежним байтам. 416 — проверить позицию. HEAD без тела. На 429 учитывать Retry-After. Поток Google → backend → клиент не требует полного ZIP на серверном диске, но зависит от лимитов Google и исходящего канала хостинга.

На ПК сохраняйте временный ZIP, проверьте полный SHA256 и размер до распаковки. Нужны место для ZIP, распаковки и резервных копий. Запрещайте zip-slip, абсолютные пути, `..`, symlink/junction и выход за корень GTA. Проверка папки игры полезна для установки, не защищает полученные пользователем файлы от копирования. Пример установщика `tools/installer_client.py` демонстрирует распаковку и докачку; постоянное OS-хранилище токенов в нём не реализовано.

POST `/installations/{id}/finish` с Bearer и X-Install-Token, `{status:"completed"|"failed"}` → `{status,verified_by_server:false}`. Отправляйте completed только после установки. Это отчёт клиента, а не независимое доказательство.

## Ошибки и настройки

Ошибка: `{detail:"message"}`; при validation 422 detail может быть массивом. Не используйте английский текст как стабильный машинный код.

| HTTP | Значение |
|---|---|
| 401 | Недействительная/отозванная сессия, доказательство или QR |
| 403 | Недостаточно прав, отключённый аккаунт/метод, чужой Origin |
| 404 | Объект не найден/недоступен; закрытый /studio |
| 409 | Username/Telegram занят, конфликт состояния |
| 411/413 | Нет Content-Length / слишком большой запрос |
| 416 | Неверный диапазон |
| 422 | Невалидные поля |
| 429 | Ограничение частоты/передач, учитывать Retry-After |
| 502/503 | Хранилище/бот/сервис временно недоступны |

HTTPS обязателен в релизе. PUBLIC_BASE_URL — точный origin backend без пути; локально `http://127.0.0.1:8000`. Не меняйте localhost/127.0.0.1 в середине cookie-входа. Бот запускается интегрированным polling-процессом: используйте отдельного бота для LATTERDAY, не запускайте параллельно Nixora bot с тем же токеном. Backend использует облачную MySQL через пул SQLAlchemy; штатный запуск сохраняет один worker и одного polling-бота. Telegram bot token, Google client_secret/token.json, .env и база никогда не передаются разработчику клиентского приложения. Пользовательские API ограничены по частоте; «без ограничений» не обещается.

Административные методы управления пользователями, grants, пакетами, обложками, потоковой загрузкой и аудитом описаны в openapi.json; обычному приложению они не нужны. Старые `/auth/telegram` (одноразовый код) и `/auth/admin/code` сохранены для совместимости, но новая форма использует QR. Старое APP_BROWSER_AUTH_REQUIRED больше не ограничивает обычный вход.


## Смена логина по заявке

| Метод | Тело / результат | Доступ |
|---|---|---|
| `POST /me/username-requests` | `{username:"new_login"}` → 202 `{request_id,status:"pending"}` | Bearer пользователя |
| `GET /me/username-requests` | `{items:[{id,old_username,new_username,status,note,created_at,reviewed_at}]}`; последние 20 | Только свои заявки |
| `GET /admin/username-requests?offset=0` | `{items:[...]}`, страницы по 100 | `usernames.review` |
| `PATCH /admin/username-requests/{id}` | `{decision:"approve" или "reject",note:"Комментарий"}` → `{status}` | `usernames.review`; аккаунт владельца — только владелец |

Логин: 3–40 символов `[a-z0-9_.-]`, первый — буква/цифра. Одна pending-заявка на аккаунт; повтор → 409. При одобрении логин повторно проверяется на уникальность в транзакции. Заявка не резервирует имя. Если оно уже занято, администратор может отклонить заявку. Статусы: pending/approved/rejected. Пароль, ID аккаунта, Telegram, доступы и действующие сессии при смене логина сохраняются. После одобрения приложение повторяет GET /me/profile. Все остальные методы изменения пароля больше не позволяют менять логин напрямую.

## Роли, права панели и доступ к модификациям

`GET /me` и `GET /me/profile` дополнительно возвращают `roles`, `permissions`, `can_manage`, `is_owner`. `is_admin` остаётся совместимым признаком владельца. **Не выдавайте админ-права на клиенте по имени роли.** Сервер перечитывает разрешения из базы на каждом запросе. Изменение/отзыв роли действует и для уже выданного Bearer.

```json
{"roles":[{"id":"role-id","name":"Поддержка","icon":"support","color":"#acc95e","permissions":["recovery.review"],"created_at":1793404800}],"permissions":["recovery.review"],"can_manage":true,"is_owner":false}
```

Пользователь может иметь несколько ролей; права складываются. Роль без прав управления может служить значком профиля и выдавать доступ к модификациям. Запрет отдельной роли не перекрывает разрешение другой. Управлять ролями, их правами и участниками может только владелец. Этот доступ не делегируется; роль не превращает пользователя в владельца. Сотрудники не могут сбрасывать пароль/Telegram, блокировать или восстанавливать аккаунт владельца либо другого сотрудника.

| Право | Функции |
|---|---|
| `analytics.read` | `/admin/summary`, `/admin/analytics` |
| `catalog.read` | Просмотр `/admin/packages` |
| `catalog.write` | Публикация и изменение карточек; автоматически включает `catalog.read` |
| `storage.upload` | Загрузка ZIP и обложек; включает `catalog.read`. Для публикации также нужно `catalog.write` |
| `users.read` | Список пользователей |
| `users.write` | Создание, блокировка, сброс пароля, Telegram; включает `users.read` |
| `grants.write` | Индивидуальные разрешения; включает `users.read` и `catalog.read` |
| `recovery.review` | Проверка и одобрение заявок восстановления |
| `usernames.review` | Заявки на смену логина |
| `audit.read` | Журнал действий |
| `roles.manage` | Зарезервировано владельцу, не назначается в роли |

| Метод | Формат |
|---|---|
| `GET /admin/roles` | `{items,permissions,icons,owner_only,prerequisites}`; каталог разрешений и иконок |
| `POST /admin/roles` | `{name,icon,color,permissions:[...]}` → 201 `{id,...}` |
| `PATCH /admin/roles/{id}` | Полная замена тех же полей, включая массив permissions |
| `DELETE /admin/roles/{id}` | Удаление роли и назначений |
| `GET /admin/users/{user_id}/roles` | `{role_ids:[...]}` |
| `PUT /admin/users/{user_id}/roles` | `{role_ids:[...]}`; заменить все роли пользователя, `[]` — снять все |
| `GET /admin/roles/{id}/packages` | `{package_ids:[...]}` |
| `PUT /admin/roles/{id}/packages` | `{package_ids:[...]}`; заменить модификации роли, `[]` — убрать доступ роли |

Иконки: shield/star/crown/code/support/bolt/eye/wrench/user/check. Цвет — `#RRGGBB`, название 1–50 символов. Удаление роли автоматически убирает её у пользователей. Владелец не может лишиться доступа через изменение ролей. Для модификаций действуют общедоступность **или** индивидуальный grant **или** доступ одной из ролей. Скрытая модификация (`active=0`) недоступна по всем этим разрешениям. Отзыв роли блокирует последующие запросы файла, но не отменяет уже открытый поток и не удаляет уже полученные файлы.

## Онлайн приложения и сайта: heartbeat

Приложение должно отправлять каждые **30 секунд**, пока оно активно:

```http
POST /me/presence
Authorization: Bearer ACCESS_TOKEN
Content-Type: application/json

{"channel":"app"}
```

Ответ: `{online_window_seconds:90,heartbeat_interval_seconds:30}`. Для сайта `channel:"site"`; фронт уже отправляет heartbeat для видимой вкладки. При закрытии/скрытии вкладки либо остановке приложения отправку прекращают. Допустимы только site/app; чаще 6 запросов в минуту на сессию → 429. При 401 приложение штатно обновляет access token и повторяет запрос с новой парой. Секретов сверх Bearer не требуется. Без интеграции heartbeat онлайн приложения останется нулевым, даже если работают скачивания.

Онлайн — уникальные авторизованные аккаунты с действующей сессией и heartbeat за последние 90 секунд. Один аккаунт в нескольких вкладках считается один раз в site; одновременно в site/app — один раз в каждом канале, один раз в online_unique. Это не подсчёт гостей и не доказательство запуска официального клиента: channel передаёт сам клиент. Мобильный Telegram-бот сам по себе не считается онлайн приложения/сайта.

## Аналитика и значения счётчиков

`GET /admin/analytics?days=30` — Bearer + `analytics.read`. days=1…365. Все границы суток — UTC.

Ответ:

- `generated_at`, `days`, `online_window_seconds`;
- `online:{site,app}`, `online_unique`, `active_transfers`, `max_transfers`;
- `totals:{users,enabled_users,packages,package_bytes,transfer_bytes,downloads,installations,installed,failed,username_pending,recovery_pending}` — накопленные значения за всё время доступной истории;
- `series:[{day,attempts,bytes,responses,interrupted,downloads,installations,installed,failed,registrations,api_requests,api_errors,latency_ms,site_active,app_active}]` — каждый день, включая пустые; `responses` — завершённые передачи/ответы файла;
- `top_packages:[{id,title,downloads,bytes}]` — до 10 модификаций за выбранный период;
- `definitions` — пояснения счётчиков.

`downloads` — уникальные установочные сессии, по которым backend передал все байты файла. Перекрывающиеся Range-запросы объединяются; скачивание по частям засчитывается один раз после полного покрытия. Повторная передача файла в той же установочной сессии не увеличивает downloads, но увеличивает bytes. HEAD не считается скачиванием. Неполные/прерванные ответы не закрывают интервалы покрытия. Новая установочная сессия считается отдельным скачиванием.

`bytes` — байты, выданные потоковым генератором backend; это не подтверждение записи на диск клиента. Включает повторные передачи и известную перед завершением потока часть оборванной передачи. Текущий поток попадает в агрегаты после завершения/прерывания. При аварийном завершении процесса точный объём незавершённого потока восстановить нельзя; при старте он помечается interrupted. `installed/failed` — сообщения клиента `/installations/{id}/finish`, а не проверенный backend результат распаковки. `site_active/app_active` — уникальные аккаунты с heartbeat за сутки, не пиковый одновременный онлайн.

`api_requests/api_errors` — ответы API, прошедшие middleware метрик; API errors — HTTP 4xx/5xx. Статика/аватары/обложки исключены, ранние отказы ограничения размера тела и ошибки без сформированного ответа могут не попасть в счётчик. `latency_ms/api_requests` — среднее время до готовности заголовков, **не** длительность потоковой передачи ZIP. Дашборд обновляется раз в 30 секунд при открытом разделе.

Счётчики передач/установок и дневные агрегаты отделены от короткоживущих установочных токенов и журнала; обычная очистка сессий не стирает статистику. Сервер не хранит тела запросов, пароли или токены в аналитике. Старые очищенные события до обновления не восстанавливаются.


# Дополнение API 3.0

- `GET /me/preferences`, `PATCH /me/preferences` → theme: carbon / steel / ember / graphite.
- `GET /me/sessions`: current, user_agent, ip; текущая сессия не удаляется через DELETE /me/sessions/{id}, используй POST /auth/logout.
- `GET /me/activity?offset=0&limit=6`: items, total.
- `GET /admin/audit?offset=0&limit=25`: items, total.
- `GET /public/settings`, `GET /public/categories`.
- `GET /admin/settings`, `PUT /admin/settings`: только владелец.
- `GET/POST /admin/categories`, `PATCH/DELETE /admin/categories/{id}`.
- `GET /admin/revision`: lightweight revision для асинхронного обновления панели.
- `POST /admin/uploads` JSON {name: package.zip, size: number}; `PUT /admin/uploads/{id}?offset=N` raw binary до 4 МиБ; `POST /admin/uploads/{id}/finish` JSON {}; `GET /admin/uploads/{id}`; `DELETE /admin/uploads/{id}`. Все требуют storage.upload и ограничены владельцем загрузки. После restart сервера upload-id недействителен. Поля статуса: phase, size, received, cloud_received, result, error.
- В карточке пакета: install_path (относительно GTA V), video_url (HTTPS), gallery (массив путей загруженных изображений; в ответе хранится JSON строкой), category_meta. Пустую версию клиент отправляет как 1.0; поле необязательно при создании.
- `GET /packages/{id}/community`: видимые downloads, views, reviews согласно настройкам. Просмотры уникальны для аккаунта/мода/UTC-дня. `PUT /packages/{id}/review` JSON {rating: 1..5, text: string}; один отзыв на аккаунт и модификацию, повтор обновляет его. Требуется доступ к пакету.
- `GET /auth/discord/start` → Discord authorization code + PKCE; `GET /auth/discord/callback` завершает вход через сервер и HttpOnly web-cookie. Секрет и provider tokens не передаются JavaScript.
- `latterday://mod/<32-hex-id>` открывает карточку в установленном Windows-приложении; сайт делится URL `/profile?mod=<id>`.

HTTP 409 от скачивания не подавляется при реальном изменении содержимого. Изменение только Drive version при совпадающем sha256Checksum больше не блокирует файл.
