{formatBytes(item.size, i18n.language)}
diff --git a/apps/web/src/pages/uploads/uploads.module.css b/apps/web/src/pages/uploads/uploads.module.css
index 373088f..cceef70 100644
--- a/apps/web/src/pages/uploads/uploads.module.css
+++ b/apps/web/src/pages/uploads/uploads.module.css
@@ -25,6 +25,15 @@
font-weight: 600;
}
+.formatWarn {
+ max-width: 640px;
+ margin: 0;
+ font-size: 14px;
+ font-weight: 600;
+ line-height: 1.45;
+ color: var(--warn);
+}
+
.dropActions {
display: flex;
flex-wrap: wrap;
diff --git a/apps/web/src/widgets/AppShell.module.css b/apps/web/src/widgets/AppShell.module.css
index 5154108..ba32f07 100644
--- a/apps/web/src/widgets/AppShell.module.css
+++ b/apps/web/src/widgets/AppShell.module.css
@@ -1,7 +1,8 @@
.shell {
+ --side-w: 240px;
position: relative;
display: grid;
- grid-template-columns: 240px 1fr;
+ grid-template-columns: var(--side-w) 1fr;
grid-template-rows: 60px 1fr 88px;
grid-template-areas:
'top top'
@@ -234,7 +235,7 @@
@media (max-width: 1023px) {
.shell {
- grid-template-columns: 72px 1fr;
+ --side-w: 72px;
}
.navItem {
@@ -269,6 +270,7 @@
@media (max-width: 767px) {
.shell {
+ --side-w: 0px;
grid-template-columns: 1fr;
grid-template-rows: 60px 1fr 72px 64px;
grid-template-areas:
diff --git a/apps/web/src/widgets/SearchBox.tsx b/apps/web/src/widgets/SearchBox.tsx
index a9bfa9b..7d01e2f 100644
--- a/apps/web/src/widgets/SearchBox.tsx
+++ b/apps/web/src/widgets/SearchBox.tsx
@@ -29,7 +29,7 @@ type Item = {
*/
export function SearchBox({
align = 'center',
- collapsible = true,
+ collapsible = false,
}: {
align?: 'left' | 'center' | 'right';
collapsible?: boolean;
diff --git a/docs/IDEA.md b/docs/IDEA.md
index d7d5790..5396595 100644
--- a/docs/IDEA.md
+++ b/docs/IDEA.md
@@ -1,7 +1,6 @@
# Pipu Music — исходная идея продукта
-> Документ описывает **что** должно быть в приложении с точки зрения пользователя.
-> Используется как входные данные для создания дизайна. Технические решения — в [README документации](README.md).
+> Как продукт выглядит для пользователя. Устройство системы — в [README документации](README.md).
## Суть
@@ -165,5 +164,3 @@
- Музыка хранится на сервере, подключение к хранилищу по NFS; хранилища подключает админ из админки.
- Одобренные новые треки приложение само записывает на файловый сервер, соблюдая принятую там структуру папок.
-- Один репозиторий: веб, мобильное приложение, бэкенд, тесты, CI/CD.
-- CI/CD на Gitea: по тегу собирается релиз.
diff --git a/docs/README.md b/docs/README.md
index 68d61bb..27ed382 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -6,10 +6,9 @@ Pipu Music — self-hosted музыкальный сервис (веб + Android
| Документ | О чём |
|---|---|
-| [IDEA.md](IDEA.md) | Исходная идея продукта глазами пользователя (вход для дизайна) |
-| [design.md](design.md) | Дизайн-система (токены, шрифты, анимация), кнопка волны, решения по вариантам прототипа, ссылки на макеты |
+| [IDEA.md](IDEA.md) | Продукт глазами пользователя |
+| [design.md](design.md) | Дизайн-система: токены, шрифты, анимация, кнопка волны, каркас экранов |
| [architecture.md](architecture.md) | Общая архитектура, компоненты, потоки данных, структура репозитория |
-| [decisions.md](decisions.md) | ADR — принятые архитектурные решения и их обоснование |
| [data-model.md](data-model.md) | Схема БД PostgreSQL |
| [api.md](api.md) | REST API: соглашения, эндпоинты, ошибки |
| [auth-and-roles.md](auth-and-roles.md) | Аутентификация, токены, роли и права, первый запуск |
@@ -20,17 +19,13 @@ Pipu Music — self-hosted музыкальный сервис (веб + Android
| [web.md](web.md) | Веб-клиент |
| [mobile.md](mobile.md) | Android-приложение, офлайн, самообновление |
| [i18n.md](i18n.md) | Локализация (ru / en, расширяемость) |
-| [testing.md](testing.md) | Стратегия тестирования |
-| [ci-cd.md](ci-cd.md) | Gitea Actions: проверки и релиз по тегу |
| [install-guide.md](install-guide.md) | **Пошагово:** установка сервера на домен с HTTPS, подключение веба и Android |
| [admin-guide.md](admin-guide.md) | **Администратору:** пользователи, хранилища, очередь, волна, обновления, диагностика |
| [deployment.md](deployment.md) | Справочник по развёртыванию: сервисы, каталоги, переменные |
-| [conventions.md](conventions.md) | Соглашения по коду, git, версиям |
-| [roadmap.md](roadmap.md) | Этапы разработки и критерии готовности |
## Стек в одном абзаце
Backend API — **Go**; рекомендации и аудиоанализ — **Python**; БД — **PostgreSQL + pgvector**;
поиск — **Meilisearch**; кэш / очереди событий — **Redis**; раздача аудио — **nginx (X-Accel-Redirect)**;
веб — **React + TypeScript + Vite**; мобильное — **React Native (Expo, Android)**;
-монорепо — **pnpm + Turborepo**; CI/CD — **Gitea Actions**; деплой — **Docker Compose**.
+деплой — **Docker Compose**.
diff --git a/docs/admin-guide.md b/docs/admin-guide.md
index 2502ad3..4a5e5b9 100644
--- a/docs/admin-guide.md
+++ b/docs/admin-guide.md
@@ -114,7 +114,6 @@ Android-приложение обновляется само: при запус
| Логи | `docker compose logs -f --tail=100 api` (или `recommender`, `analyzer`, `storage-agent`) |
| Место на диске | `df -h /srv/pipu`; больше всего занимает `cache/transcode` (чистится сам) |
| Версия | `grep PIPU_VERSION .env`, `GET /api/v1/server-info`; Android — внизу вкладки «Профиль» |
-| Нагрузка | `tests/load/smoke.js` — см. [testing.md](testing.md#нагрузочный-smoke) |
## 8. Частые ситуации
diff --git a/docs/api.md b/docs/api.md
index dc3aeb8..bc85e93 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -136,7 +136,7 @@
| GET | `/uploads/quota` | moderator, admin | `{limit_bytes, used_bytes, per_user_limit_bytes, user_used_bytes, max_file_bytes}` |
| POST | `/uploads/precheck` | moderator, admin | `{files:[{client_id, size, sha256, tags:{artist,title,album,duration_ms}}]}` → для каждого `accepted` / `duplicate` (+ `track`) / `too_large`; ничего не резервирует |
| POST | `/uploads` | moderator, admin | Создать пачку `{comment, files:[{client_id, name, rel_path, size, sha256}]}` → резерв квоты; `{batch_id, files:[{client_id, upload_file_id, tus_url} | {client_id, rejected: code}]}`; при нехватке места — `uploads.queue_full` для не поместившихся |
-| tus | `/uploads/files/{upload_file_id}` | автор | Resumable-загрузка (`HEAD`/`PATCH`, ядро tus 1.0.0 — ADR-015) |
+| tus | `/uploads/files/{upload_file_id}` | автор | Докачка (`HEAD`/`PATCH`, ядро tus 1.0.0) |
| GET | `/uploads/mine?status=&cursor=` | moderator, admin | Мои предложения (`status` — через запятую) |
| DELETE | `/uploads/files/{id}` | автор | Отозвать (`withdrawn`), только до решения |
| GET | `/admin/uploads?status=&author=&possible_duplicate=&cursor=` | admin | Очередь, сгруппированная по пачкам |
diff --git a/docs/architecture.md b/docs/architecture.md
index 6494b99..53205fb 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -101,7 +101,7 @@ pipu-music/
│ └── recommender/ # Python (uv)
│ ├── pipu_rec/
│ │ ├── api/ # FastAPI
-│ │ ├── analysis/ # Essentia
+│ │ ├── analysis/ # признаки и эмбеддинги (numpy)
│ │ ├── candidates/ ranking/ wave/ training/
│ │ └── storage/ # доступ к Postgres / Redis
│ └── tests/
@@ -111,10 +111,6 @@ pipu-music/
│ ├── compose.dev.yaml
│ ├── nginx/
│ └── .env.example
-├── tests/
-│ ├── e2e/ # Playwright
-│ └── fixtures/music/ # генерируемые тестовые аудиофайлы
-├── .gitea/workflows/ # ci-*.yml, docker.yml (по областям), release.yml
├── docs/
├── turbo.json pnpm-workspace.yaml package.json
└── README.md
@@ -127,7 +123,7 @@ pipu-music/
не лезут в чужие таблицы напрямую.
- **Contract-first**: `api/openapi.yaml` — источник истины. Go-сервер генерирует типы и интерфейс
хендлеров (`oapi-codegen`, strict server), TS-клиент генерируется в `packages/api-client`.
- CI падает, если сгенерированный код не совпадает с контрактом.
+ Сгенерированный код совпадает с контрактом.
- **Внутренний API** `api ↔ recommender` и `api ↔ storage-agent` — HTTP/JSON во внутренней docker-сети,
описан отдельными `internal-*.yaml` спеками.
- **Идемпотентность**: события и мутирующие запросы клиентов принимают `Idempotency-Key` / `event_id` (UUIDv7),
diff --git a/docs/ci-cd.md b/docs/ci-cd.md
deleted file mode 100644
index b96c6ae..0000000
--- a/docs/ci-cd.md
+++ /dev/null
@@ -1,138 +0,0 @@
-# CI/CD (Gitea Actions)
-
-Workflows лежат в `.gitea/workflows/`. Синтаксис совместим с GitHub Actions; runner — существующий `act_runner`
-с доступом к Docker.
-
-Сборке APK нужно ~15 ГБ свободного места на хосте раннера (Android SDK, NDK ~2.5 ГБ, кэш Gradle,
-C++-сборка React Native). APK собирается только под `arm64-v8a`. Если job падает с `No space left on device`,
-освободите место на хосте раннера: `docker system prune -af` и старые кэши act в `~/.cache/act`.
-
-## Версионирование
-
-- SemVer, тег `vMAJOR.MINOR.PATCH` (предрелизы: `v1.2.0-rc.1` — помечаются как pre-release, мобильное их
- предлагает только при включённой настройке «бета-версии»).
-- Одна версия на весь продукт: образы, APK и веб собираются с одним номером.
-- Android `versionCode = MAJOR·1 000 000 + MINOR·10 000 + PATCH·100 + (99 для релиза | N для -rc.N)`,
- так что `1.2.0-rc.1 < 1.2.0-rc.2 < 1.2.0 < 1.2.1`; MINOR и PATCH ≤ 99. Считает `scripts/version.mjs`
- (тесты — `pnpm test:scripts`).
-- Changelog — `git-cliff` из Conventional Commits.
-
-## CI — на каждый push в `main` и PR
-
-Проверки разбиты на workflow по областям, у каждого фильтр путей (`on.push.paths`): запускается только то,
-что затронуто коммитом. Коммиты только в `docs/` и `*.md` CI не запускают вовсе.
-
-| Workflow | Job | Что делает | Когда |
-|---|---|---|---|
-| `ci-api.yml` | `api` | gofmt, go vet, `go test -race`, сборка | `services/api/**`, `api/openapi.yaml` |
-| `ci-contract.yml` | `contract` | регенерация Go-сервера, TS-клиента и токенов; всё ли закоммичено | `openapi.yaml`, `gen/`, `packages/api-client`, `packages/design-tokens`, общие файлы JS |
-| `ci-recommender.yml` | `recommender` | ruff, mypy, pytest | `services/recommender/**` |
-| `ci-ts.yml` | `ts` | i18n, тесты скриптов, typecheck (вкл. mobile), vitest, сборка веба | `apps/**`, `packages/**`, `scripts/**`, общие файлы JS |
-| `ci-mobile.yml` | `mobile` | `expo-doctor`, Metro-бандл Android | `apps/mobile/**`, `packages/**`, общие файлы JS |
-| `docker.yml` | `docker` | сборка всех 4 образов без push | `Dockerfile`, `.dockerignore`, `deploy/**`, манифесты зависимостей |
-
-Общие файлы JS: `package.json`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `turbo.json`, `tsconfig.base.json`.
-Изменение самого workflow запускает его. У каждого workflow своя группа `concurrency`: новый push в ту же
-ветку отменяет устаревший прогон.
-
-`docker` собирает образы обычным `docker build` через Docker хоста раннера, а не в buildx-контейнере:
-кэш слоёв переживает job, и повторная сборка занимает секунды. Кэш растёт — на хосте раннера раз в сутки:
-`docker builder prune -af --filter until=72h` (см. [настройку раннера](#настройка-раннера)).
-
-### Кэши
-
-| Что | Где кэшируется | Чем |
-|---|---|---|
-| pnpm store | все job с `pnpm install` | `setup-node` `cache: pnpm` (кэш actions раннера) |
-| Go-модули и build cache | `api`, `contract` | `setup-go` |
-| uv | `recommender` | `setup-uv` `enable-cache` |
-| Результаты turbo (typecheck, test, build) | `ts` | `actions/cache` для `.turbo/cache` |
-| `npx expo-doctor`, трансформации Metro | `mobile` | `actions/cache` для `~/.npm/_npx` и `/tmp/metro-cache` |
-| Слои Docker и `--mount=type=cache` | `docker`, сборка на сервере | Docker хоста; манифесты копируются раньше кода |
-| Gradle (дистрибутив, зависимости, build cache), Android SDK и NDK | `android` в `release.yml` | тома `/opt/ci-cache/*` |
-| Node, Go, Java, скачанные setup-action'ами | все job | том `act-toolcache` раннера |
-
-Кэш actions работает, только если он включён на раннере ([настройка](#настройка-раннера)); без него в логах
-`Cache action is only supported on GHES…`, и всё скачивается заново.
-
-## `release.yml` — по тегу
-
-```yaml
-on:
- push:
- tags: ['v*.*.*']
-
-jobs:
- release: # версия из тега (scripts/version.mjs) и сразу Gitea Release (scripts/release.mjs create)
- android: # needs: release — собирает APK и докладывает его в релиз
-```
-
-Тесты в релизе не повторяются: тег ставится на коммит, который CI уже проверил в `main`. **Ставьте тег
-после зелёного CI** — релиз сам этого не проверяет.
-
-Релиз появляется в первую же минуту, не дожидаясь APK. Текст строит `scripts/release.mjs`:
-
-- **статус Android** вверху: «⏳ собирается» со ссылкой на ход сборки → «✅ собран» (имя файла, размер,
- versionCode, SHA-256; предупреждение, если подпись отладочная) или «❌ не удалась» со ссылкой на лог;
-- **изменения** — коммиты (Conventional Commits) с прошлого тега по разделам «Новое», «Исправления»,
- «Сборка и CI» и т. д., отдельно — несовместимые (`feat!:`); для финального релиза отсчёт от прошлого
- финального, а не от последнего `-rc`; ссылка на полный diff;
-- **обновление сервера** — `./update.sh vX.Y.Z`, проверка версии; **установка APK** и проверка SHA-256.
-
-Job `android`: Android SDK, `expo prebuild`, `./gradlew assembleRelease --build-cache` (только arm64-v8a), подпись `apksigner`,
-затем `release.mjs android success ` прикладывает APK и `.sha256` и меняет статус. При любом сбое —
-`release.mjs android failure`. Перезапуск job безопасен: статус снова «собирается», файлы заменяются.
-Перезапуск всего workflow обновляет текст существующего релиза, а не создаёт второй.
-
-Docker-образы **не публикуются**: сервер собирает их сам из клонированного репозитория (ADR-013).
-Чтобы сборка на сервере не ломалась, job `docker` в `ci.yml` собирает все образы без push на каждый PR.
-
-## Настройка раннера
-
-Раннер — `act_runner` на отдельной VM. Что ускоряет CI/CD (`config.yaml` раннера, затем перезапуск сервиса):
-
-```yaml
-runner:
- capacity: 2 # два job параллельно; только если у VM ≥ 8 ГБ RAM (иначе APK-сборка упирается в OOM)
-cache:
- enabled: true # кэш actions/cache: pnpm store, Go-модули, uv
- host: 10.10.1.x # IP хоста раннера, доступный из job-контейнеров (не 127.0.0.1)
- port: 8088 # фиксированный порт; открыть его для docker-сетей
-container:
- valid_volumes:
- - /opt/ci-cache/** # постоянные кэши Gradle и Android SDK для job android в release.yml
-```
-
-Сборка APK ограничена сама (`release.yml`: контейнер `--cpus=3 --memory=7g` без свопа, Gradle `-Xmx2g`, Metro `--max-old-space-size=1536`, таймаут 60 мин, CMake / ninja / Gradle
-по 2 потока, `nice` / `ionice`). Если раннер — VM в гипервизоре (Proxmox), дополнительно ограничьте её там:
-фиксированная RAM без ballooning, сумма RAM всех VM меньше RAM хоста, `cpulimit` ≤ половины ядер хоста,
-лимит дискового I/O — иначе тяжёлая сборка может подвесить весь хост.
-
-Без `valid_volumes` раннер пропускает тома из `release.yml`, и каждый релиз заново качает Gradle
-и NDK (~4 мин). Признак выключенного кэша actions в логах — `Cache action is only supported on GHES…`.
-
-Обслуживание хоста раннера (cron, раз в сутки):
-
-```
-0 4 * * * docker image prune -af --filter "until=72h"; docker builder prune -af --filter "until=72h"
-```
-
-Кэши в `/opt/ci-cache` занимают ~5–6 ГБ (Gradle, SDK, NDK); если место кончается — их можно удалить целиком,
-следующий релиз скачает всё заново.
-
-## Секреты
-
-| Секрет | Назначение |
-|---|---|
-| `RELEASE_TOKEN` | Необязательно. Создание и редактирование релиза, загрузка ассетов (scope `write:repository`). Без него используется встроенный токен job (`GITHUB_TOKEN`), у которого есть права на запись в этот репозиторий |
-| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | Подпись APK. **Ключ один на всю жизнь приложения** — хранить резервную копию вне Gitea. Без них финальный релиз падает; `-rc` собирается с отладочной подписью. |
-
-Переменная репозитория `PUBLIC_URL` (Settings → Actions → Variables) — внешний адрес Gitea для ссылок
-в тексте релиза, по умолчанию `https://gitea.hsrv.site`. Нужна потому, что job обращается к API по внутреннему
-адресу, и ссылки из ответа API ведут туда же.
-
-## Деплой
-
-Автодеплой не обязателен. На сервере в клоне репозитория: `deploy/update.sh [vX.Y.Z]` — checkout тега,
-`docker compose build` и `up -d`; миграции применяются при старте `api` ([install-guide.md](install-guide.md)).
-Опционально — job `deploy` в `release.yml` по SSH на сервер для не-pre-release тегов (с ручным approval через environment).
diff --git a/docs/conventions.md b/docs/conventions.md
deleted file mode 100644
index c191d47..0000000
--- a/docs/conventions.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Соглашения
-
-## Git
-
-- Trunk-based: короткие ветки от `main`, merge через PR (squash).
-- Имена веток: `feat/wave-moods`, `fix/scan-rename`, `chore/ci-cache`.
-- Conventional Commits: `feat(wave): add running profile`, `fix(api): ...`. Скоупы: `api`, `agent`, `rec`, `web`,
- `mobile`, `core`, `i18n`, `ci`, `deploy`, `docs`. Breaking change — `!` и раздел `BREAKING CHANGE:`.
-- Изменение API = изменение `openapi.yaml` + регенерация клиентов в том же PR.
-- Существенные архитектурные решения — новый ADR в [decisions.md](decisions.md).
-
-## Go (`services/api`)
-
-- `gofumpt`, `golangci-lint` (конфиг в репо). Ошибки оборачиваются `fmt.Errorf("...: %w", err)`;
- доменные ошибки — типизированные (`ErrForbidden`, `ErrNotFound`) и маппятся в problem+json в одном месте.
-- Контекст первым аргументом, без глобального состояния, зависимости через конструкторы (`NewService(repo, clock, ...)`).
-- `clock` и генератор ID — интерфейсы (детерминированные тесты).
-- SQL — только в `queries/*.sql` (sqlc), без ORM.
-
-## Python (`services/recommender`)
-
-- `uv`, `ruff` (lint + format), `mypy --strict`, Pydantic v2 для схем.
-- Тяжёлые вычисления — `numpy`, без циклов Python по трекам.
-- Конфиги контекстов и весов — YAML, валидируются Pydantic при старте.
-
-## TypeScript
-
-- `strict`, ESLint (flat config) + Prettier, без `any` (кроме сгенерированного кода).
-- Общая логика — в `packages/core`, платформенная — только адаптеры в `apps/*`.
-- Все строки UI — через i18n, хардкод текста ловит `eslint-plugin-i18next`.
-
-## API
-
-- `snake_case` в JSON, курсорная пагинация, problem+json с `code`.
-- Новые поля — только дополнительные; удаление / переименование — через новую версию эндпоинта или депрекацию на один минорный релиз.
-
-## Безопасность
-
-- Секреты только через env / docker secrets, никогда в репозитории.
-- Любой ввод, попадающий в команды (`mount`, `ffmpeg`), — только как отдельные аргументы `exec.Command`, без shell,
- с whitelist-валидацией.
-- Пути файлов — проверка, что итоговый путь внутри корня источника (`filepath.Rel` без `..`).
diff --git a/docs/decisions.md b/docs/decisions.md
deleted file mode 100644
index e81ae6b..0000000
--- a/docs/decisions.md
+++ /dev/null
@@ -1,205 +0,0 @@
-# Архитектурные решения (ADR)
-
-Формат: контекст → решение → последствия. Новое решение — новый раздел с номером; старые не переписываются,
-а помечаются `Заменено ADR-NNN`.
-
----
-
-## ADR-001. Монорепозиторий
-
-**Контекст.** Веб, мобильное, бэкенд, тесты и CI должны жить в одном репозитории; у веба и мобильного много общего
-(API-клиент, логика очереди и событий, словари i18n).
-
-**Решение.** Монорепо: pnpm workspaces + Turborepo для TS-части; Go-модуль в `services/api`; Python-проект
-(uv) в `services/recommender`. Единый `api/openapi.yaml`.
-
-**Последствия.** Атомарные изменения контракта и клиентов в одном PR. CI использует фильтры путей,
-чтобы не гонять всё на каждый коммит.
-
----
-
-## ADR-002. Backend API на Go
-
-**Контекст.** Нагрузка — IO (стриминг, NFS, БД), десятки одновременных слушателей; важна простота деплоя и
-низкое потребление памяти.
-
-**Решение.** Go 1.23+: роутер `chi`, `oapi-codegen` (strict server), `pgx` + `sqlc`, миграции `goose`,
-фоновые задачи `River` (очередь в Postgres), логирование `log/slog`, конфиг через env (`caarlos0/env`).
-
-**Последствия.** Один статический бинарник на сервис, быстрые холодные старты, тайпсейф SQL.
-ML-код в Go не пишем — для этого отдельный сервис (ADR-003).
-
----
-
-## ADR-003. Рекомендации и аудиоанализ — отдельный Python-сервис
-
-**Контекст.** Нужны: аудиоанализ (Essentia и её предобученные модели), коллаборативная фильтрация (implicit ALS),
-learning-to-rank (LightGBM). Экосистема есть только в Python.
-
-**Решение.** `services/recommender`: FastAPI (онлайн-инференс), отдельный процесс `analyzer` (тот же образ),
-обучение по расписанию (APScheduler внутри сервиса). Python 3.12, `uv`, `numpy`, `implicit`, `lightgbm`,
-`essentia-tensorflow`, `psycopg`, `redis`.
-
-**Последствия.** Два языка на бэкенде. Граница чёткая: Python не обслуживает пользователей напрямую,
-только внутренние вызовы `api`. При падении сервиса работает fallback в `api`.
-
-⚠️ Предобученные модели Essentia распространяются под CC BY-NC-SA 4.0 — допустимо для частного
-некоммерческого self-hosted использования. При коммерческом использовании нужно заменить модели.
-
----
-
-## ADR-004. Мобильное приложение — React Native (Expo), только Android
-
-**Контекст.** Только Android, самообновление APK из релизов, фоновое воспроизведение, офлайн, анимированная кнопка волны.
-Рассматривались Flutter и React Native.
-
-**Решение.** React Native + Expo SDK (prebuild / dev client, не Expo Go), `expo-router`,
-`react-native-track-player` (ExoPlayer под капотом, MediaSession, уведомление, кэш), `@shopify/react-native-skia`
-+ `react-native-reanimated` для волны, `expo-sqlite` для офлайн-данных, `expo-file-system` для загрузок.
-
-**Почему не Flutter.** Flutter не хуже по возможностям, но React Native даёт общий TypeScript-код с вебом:
-API-клиент, типы, логику очереди / событий / офлайн-синхронизации, словари i18n, дизайн-токены.
-Для команды из одного-двух человек это существенно сокращает объём работы.
-
-**Последствия.** iOS не поддерживается (самообновление в обход стора там невозможно); при необходимости
-iOS добавляется позже через TestFlight без смены стека.
-
----
-
-## ADR-005. Раздача аудио через nginx X-Accel-Redirect
-
-**Контекст.** Нужна перемотка (HTTP Range), высокая пропускная способность, минимум нагрузки на бэкенд.
-
-**Решение.** Клиент обращается к `api`, `api` проверяет права и подпись ссылки и отвечает заголовком
-`X-Accel-Redirect` на `internal` location nginx. Файл отдаёт nginx.
-
-**Последствия.** Go не держит долгих соединений со стримами. Тома библиотеки и кэша транскодов
-должны быть смонтированы и в `api`, и в `nginx` по одинаковым путям.
-
----
-
-## ADR-006. Подписанные ссылки на стрим
-
-**Контекст.** `