fix: release QA hardening across processing, media, security, and CI gates (#649)

A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
This commit is contained in:
SnapOtter
2026-07-27 15:37:30 +08:00
committed by GitHub
parent bc32f86a07
commit d10d0f544f
855 changed files with 54564 additions and 13092 deletions
+40 -17
View File
@@ -1,8 +1,9 @@
---
description: "Усі змінні середовища SnapOtter зі значеннями за замовчуванням. Налаштуйте автентифікацію, сховище, моделі AI, аналітику тощо."
i18n_source_hash: 8e9e9ca2840c
i18n_source_hash: 25970c776f7c
i18n_provenance: human
i18n_output_hash: 498d74ca4a81
i18n_output_hash: 52fb4064af72
i18n_hash_version: 2
---
# Конфігурація {#configuration}
@@ -19,28 +20,51 @@ i18n_output_hash: 498d74ca4a81
| `RATE_LIMIT_PER_MIN` | `1000` | Максимальна кількість запитів за хвилину на одну IP. Задайте 0, щоб вимкнути обмеження частоти запитів. |
| `CORS_ORIGIN` | (порожньо) | Розділені комами дозволені джерела для CORS або порожньо лише для того самого джерела. |
| `LOG_LEVEL` | `info` | Детальність журналювання. Одне з: `fatal`, `error`, `warn`, `info`, `debug`, `trace`. |
| `TRUST_PROXY` | `true` | Довіряти заголовкам `X-Forwarded-For` від зворотного проксі. Задайте `false`, якщо не за проксі. |
| `TRUST_PROXY` | `loopback,linklocal,uniquelocal` | Яким вузлам дозволено задавати IP клієнта через `X-Forwarded-For`. Значення за замовчуванням вірить лише вузлу з приватної мережі, тож зворотному проксі в мережі Docker або в локальній мережі довіра є, а підробленому заголовку від публічного клієнта немає. Задавайте `true`, лише якщо попереду стоїть підконтрольний вам проксі з публічною адресою. |
### Автентифікація {#authentication}
Дві булеві змінні нижче приймають лише `true` і `false`. Будь-що інше, чи то `1`, `yes` чи `on`, не проходить перевірку, і сервер завершує роботу, так і не почавши слухати.
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `AUTH_ENABLED` | `false` | Задайте `true`, щоб вимагати входу. Образ Docker за замовчуванням має `true`. |
| `AUTH_ENABLED` | `true` | Вимагати вхід. Задайте `false`, щоб працювати взагалі без облікових записів: тоді будь-який запит отримує права адміністратора, тож лишайте так лише в довіреній мережі. |
| `DEFAULT_USERNAME` | `admin` | Ім'я користувача для початкового облікового запису адміністратора. Використовується лише під час першого запуску. |
| `DEFAULT_PASSWORD` | `admin` | Пароль для початкового облікового запису адміністратора. Змініть його після першого входу. |
| `MAX_USERS` | `0` (необмежено) | Максимальна кількість зареєстрованих облікових записів користувачів. Задайте 0 для необмеженої кількості. |
| `SESSION_DURATION_HOURS` | `168` | Тривалість сеансу входу в годинах (за замовчуванням 7 днів). |
| `SKIP_MUST_CHANGE_PASSWORD` | - | Задайте будь-яке непорожнє значення, щоб обійти примусовий запит на зміну пароля під час першого входу |
| `SKIP_MUST_CHANGE_PASSWORD` | `false` | Задайте `true`, щоб пропустити примусовий запит на зміну пароля під час першого входу. |
### Сховище {#storage}
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `STORAGE_MODE` | `local` | `local` або `s3`. S3/MinIO вимагає ліцензії з функцією s3_storage. |
| `DATABASE_URL` | `postgres://snapotter:snapotter@postgres:5432/snapotter` | Рядок підключення до PostgreSQL. |
| `REDIS_URL` | `redis://redis:6379` | Рядок підключення до Redis (використовується для черг завдань BullMQ). |
| `WORKSPACE_PATH` | `./tmp/workspace` | Каталог для тимчасових файлів під час обробки. Очищується автоматично. |
| `FILES_STORAGE_PATH` | `./data/files` | Каталог для постійних файлів користувача (завантажені зображення, збережені результати). |
| `STORAGE_MODE` | `local` | `local` або `s3`. S3 і MinIO потребують ліцензії з функцією s3_storage, а також змінних `S3_*` нижче. |
| `DATABASE_URL` | `postgres://snapotter:snapotter@localhost:5432/snapotter` | Рядок підключення до PostgreSQL. Стек Compose спрямовує його на власний сервіс `postgres`; лишіть його незаданим (разом із `REDIS_URL`), щоб отримати вбудований режим. |
| `REDIS_URL` | `redis://localhost:6379` | Рядок підключення до Redis (використовується для черг завдань BullMQ). Compose спрямовує його на власний сервіс `redis`. |
| `WORKSPACE_PATH` | `./tmp/workspace` | Каталог для тимчасових файлів під час обробки. Очищується автоматично. Образ задає `/tmp/workspace`. |
| `FILES_STORAGE_PATH` | `./data/files` | Каталог для постійних файлів користувача (завантажені зображення, збережені результати). Образ задає `/data/files`. |
### Об'єктне сховище S3 {#s3-object-storage}
Читається лише тоді, коли задано `STORAGE_MODE=s3`. Пропустіть будь-яку з трьох обов'язкових змінних, і запуск завершиться помилкою з назвою тієї змінної, яку ви не задали.
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `S3_BUCKET` | (порожньо) | Бакет, у якому зберігаються завантаження та результати. Обов'язкова. |
| `S3_ACCESS_KEY_ID` | (порожньо) | Ключ доступу. Обов'язкова. У контейнері його можна натомість змонтувати файлом через `S3_ACCESS_KEY_ID_FILE`. |
| `S3_SECRET_ACCESS_KEY` | (порожньо) | Секретний ключ. Обов'язкова. Та сама домовленість щодо файлів: `S3_SECRET_ACCESS_KEY_FILE`. |
| `S3_REGION` | `us-east-1` | Регіон бакета. |
| `S3_ENDPOINT` | (порожньо) | Власна кінцева точка для MinIO, R2, Backblaze та інших S3-сумісних сховищ. Порожньо означає AWS. |
| `S3_FORCE_PATH_STYLE` | `false` | Задайте `true` для MinIO та всього іншого, що очікує `endpoint/bucket/key` замість адресації за віртуальним хостом. |
| `S3_PREFIX` | (порожньо) | Префікс ключів, щоб один бакет міг обслуговувати кілька екземплярів. |
### Шифрування збережених даних {#encryption-at-rest}
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `DATA_ENCRYPTION_KEY` | (порожньо) | 64 шістнадцяткові символи (32 байти). Шифрує чутливі налаштування, збережені в базі даних. Будь-що, що не є 64 шістнадцятковими символами, відхиляється під час запуску. |
| `DATA_ENCRYPTION_KEY_PREVIOUS` | (порожньо) | Ключ, від якого ви відходите під час ротації, у тому самому форматі. Задайте обидва на час ротації, щоб наявні рядки й далі розшифровувалися, а потім приберіть цей. |
### Вбудований режим {#embedded-mode}
@@ -59,16 +83,15 @@ i18n_output_hash: 498d74ca4a81
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `MAX_UPLOAD_SIZE_MB` | `100` | Максимальний розмір файлу на одне завантаження в мегабайтах. Задайте 0 для необмеженого. |
| `MAX_BATCH_SIZE` | `100` | Максимальна кількість файлів в одному пакетному запиті. Задайте 0 для необмеженого. |
| `MAX_UPLOAD_SIZE_MB` | `0` (необмежено) | Максимальний розмір файлу на одне завантаження в мегабайтах. Задайте 0 для необмеженого. Опублікований образ постачається зі значенням `0`; збірка з вихідного коду починає зі 100. |
| `MAX_BATCH_SIZE` | `0` (необмежено) | Максимальна кількість файлів в одному пакетному запиті. Задайте 0 для необмеженого. Опублікований образ постачається зі значенням `0`; збірка з вихідного коду починає зі 100. |
| `CONCURRENT_JOBS` | `0` (авто) | Кількість пакетних завдань, що виконуються паралельно. Задайте 0 для автовизначення на основі доступних ядер CPU. |
| `MAX_MEGAPIXELS` | `0` (необмежено) | Максимальна дозволена роздільна здатність зображення в мегапікселях. Задайте 0 для необмеженого. |
| `MAX_WORKER_THREADS` | `0` (авто) | Максимальна кількість робочих потоків для обробки зображень. Задайте 0 для автовизначення на основі доступних ядер CPU. |
| `PROCESSING_TIMEOUT_S` | `0` (без обмеження) | Максимальний час обробки на один запит у секундах. Задайте 0 для відсутності таймауту. |
| `MAX_PIPELINE_STEPS` | `20` | Максимальна кількість кроків у конвеєрі. Задайте 0 для відсутності обмеження. |
| `MAX_CANVAS_PIXELS` | `0` (без обмеження) | Максимальний розмір полотна в пікселях для вихідних зображень. Задайте 0 для відсутності обмеження. |
| `MAX_SVG_SIZE_MB` | `0` (необмежено) | Максимальний розмір файлу SVG у мегабайтах. Задайте 0 для необмеженого. |
| `MAX_SPLIT_GRID` | `100` | Максимальний розмір сітки для інструмента розділення зображення. |
| `MAX_SVG_SIZE_MB` | `50` | Найбільший SVG, який приймається перед очищенням, у мегабайтах. Тут `0` поводиться інакше, ніж у сусідніх рядках: він повністю знімає обмеження розміру до розбору, а не піднімає його, тож цю змінну краще лишити заданою. |
| `MAX_PDF_PAGES` | `0` (необмежено) | Максимальна кількість сторінок PDF для конвертації PDF-у-зображення. Задайте 0 для необмеженого. |
### Очищення {#cleanup}
@@ -82,7 +105,7 @@ i18n_output_hash: 498d74ca4a81
| Змінна | За замовчуванням | Опис |
|---|---|---|
| `DEFAULT_THEME` | `light` | Типова тема для нових сеансів. `light` або `dark`. |
| `DEFAULT_THEME` | `light` | Типова тема для нових сеансів. `light`, `dark` або `system`. |
| `DEFAULT_LOCALE` | `en` | Типова мова інтерфейсу. |
| `DEFAULT_TOOL_VIEW` | `sidebar` | Типове компонування інструментів. `sidebar` або `fullscreen`. |
@@ -124,13 +147,13 @@ services:
image: postgres:17-alpine
environment:
POSTGRES_USER: snapotter
POSTGRES_PASSWORD: snapotter
POSTGRES_PASSWORD: snapotter # Змініть це для нелокальних розгортань
POSTGRES_DB: snapotter
volumes:
- SnapOtter-pgdata:/var/lib/postgresql/data
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U snapotter"]
test: ["CMD-SHELL", "pg_isready -U snapotter -d snapotter"]
interval: 10s
timeout: 5s
retries: 12