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
+23 -12
View File
@@ -1,8 +1,9 @@
---
description: "انشر SnapOtter في بيئة الإنتاج باستخدام Docker. متطلبات العتاد وإعداد GPU وإعدادات الوكيل العكسي لـ Nginx و Traefik و Cloudflare."
i18n_output_hash: 44503f8d944c
i18n_source_hash: 98172965118b
i18n_source_hash: 2a722f86da75
i18n_provenance: human
i18n_output_hash: f61512a98aa3
i18n_hash_version: 2
---
# النشر {#deployment}
@@ -47,7 +48,7 @@ services:
# - MAX_USERS=0 # Max user accounts
# --- Networking ---
# - TRUST_PROXY=true # Trust X-Forwarded-For headers (set false if not behind a proxy)
# - TRUST_PROXY=loopback,linklocal,uniquelocal # Which peers may set the client IP via X-Forwarded-For (default shown)
# --- Bind mount permissions ---
# - PUID=1000 # Match your host user's UID (run: id -u)
@@ -82,7 +83,7 @@ services:
- 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
@@ -170,13 +171,13 @@ services:
container_name: SnapOtter-postgres
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
@@ -207,6 +208,8 @@ volumes:
docker compose -f docker-compose-gpu.yml up -d
```
### التحقق من تسريع GPU {#verify-gpu-acceleration}
تحقق من اكتشاف CUDA في السجلات:
```bash
@@ -214,6 +217,8 @@ docker logs SnapOtter 2>&1 | head -20
# Look for: [gpu] CUDA available via torch
```
إذا تم تشغيل أدوات الذكاء الاصطناعي على وحدة المعالجة المركزية (CPU) على الرغم من إعداد `--gpus all` ومجموعة أدوات حاوية NVIDIA بشكل صحيح، فأعد تثبيت الحزمة المتأثرة (على سبيل المثال إزالة الخلفية) من **الإعدادات → ميزات الذكاء الاصطناعي**. يقوم المثبت باستعادة بنية GPU الخاصة بـ ONNX Runtime، والتي يمكن أن تظل بنية وحدة المعالجة المركزية فقط التي تم سحبها بواسطة حزمة أخرى (مثل النسخ) في بيئة الذكاء الاصطناعي المشتركة. إذا لم تؤدي إعادة التثبيت من واجهة المستخدم إلى استعادة وحدة معالجة الرسومات على صورة قديمة، فراجع الإصلاح اليدوي في [الإصدار رقم 490](https://github.com/snapotter-hq/SnapOtter/issues/490).
## متطلبات العتاد {#hardware-requirements}
تأتي هذه الأرقام من اختبارات أداء عبر مجموعة من الأنظمة، من محطة عمل amd64 حديثة مزوّدة ببطاقة NVIDIA RTX 4070 وصولًا إلى Raspberry Pi، حيث شُغِّل كامل كتالوج الأدوات على كل منها مع مسح حدود موارد Docker لإيجاد الحد الأدنى الفعلي.
@@ -436,11 +441,11 @@ securityContext:
| `AUTH_ENABLED` | `true` | تفعيل/تعطيل متطلب تسجيل الدخول |
| `DEFAULT_USERNAME` | `admin` | اسم المستخدم المسؤول الأولي |
| `DEFAULT_PASSWORD` | `admin` | كلمة مرور المسؤول الأولية (يُفرض تغييرها عند أول تسجيل دخول) |
| `MAX_UPLOAD_SIZE_MB` | `100` | حد الرفع لكل ملف |
| `MAX_BATCH_SIZE` | `100` | الحد الأقصى للملفات لكل طلب دفعة |
| `MAX_UPLOAD_SIZE_MB` | `0` (غير محدود) | حد الرفع لكل ملف بالميجابايت. تأتي الصورة بالقيمة `0`؛ أما البناء من الشيفرة المصدرية فيبدأ من 100 |
| `MAX_BATCH_SIZE` | `0` (غير محدود) | الحد الأقصى للملفات لكل طلب دفعة. تأتي الصورة بالقيمة `0`؛ أما البناء من الشيفرة المصدرية فيبدأ من 100 |
| `RATE_LIMIT_PER_MIN` | `1000` | طلبات API في الدقيقة لكل عنوان IP (عيّن 0 للتعطيل) |
| `MAX_USERS` | `0` (غير محدود) | الحد الأقصى لحسابات المستخدمين |
| `TRUST_PROXY` | `true` | الوثوق برؤوس X-Forwarded-For من الوكيل العكسي |
| `TRUST_PROXY` | `loopback,linklocal,uniquelocal` | أي النظائر يُسمح لها بتعيين عنوان IP الخاص بالعميل عبر `X-Forwarded-For`. الشبكات الخاصة فقط افتراضيًا |
| `PUID` | `999` | التشغيل بهذا UID (لأذونات التركيب الرابط) |
| `PGID` | `999` | التشغيل بهذا GID (لأذونات التركيب الرابط) |
| `LOG_LEVEL` | `info` | إسهاب السجل: fatal، و error، و warn، و info، و debug، و trace |
@@ -483,7 +488,13 @@ curl http://localhost:1349/api/v1/health
## الوكيل العكسي {#reverse-proxy}
يعيّن SnapOtter `TRUST_PROXY=true` افتراضيًا حتى يستخدم تحديد المعدل والتسجيل عنوان IP الحقيقي للعميل من رؤوس `X-Forwarded-For`.
القيمة الافتراضية لـ `TRUST_PROXY` هي `loopback,linklocal,uniquelocal`، لذا لا يصدّق SnapOtter ترويسة `X-Forwarded-For` إلا من نظير موجود على شبكة خاصة. ويُوثق تلقائيًا بأي وكيل عكسي على المضيف نفسه أو على شبكة Docker أو على شبكتك المحلية، ما يعني أن تحديد المعدل، ومحدد محاولات تسجيل الدخول بالقوة الغاشمة، وسجل التدقيق، وقائمة عناوين IP المسموح بها في إصدار enterprise ترى جميعها عنوان IP الحقيقي للعميل دون أي إعداد.
اضبط `TRUST_PROXY=true` فقط عندما يصل الوكيل الأمامي إلى SnapOtter من عنوان **عام**، مثل موازن حِمل سحابي على شبكة أخرى. أما على نسخة مكشوفة مباشرةً فتجعل هذه القيمة `request.ip` تحت سيطرة المهاجم، لأن من يبدّل الترويسة يحصل على عدّاد جديد لحد المعدل مع كل طلب.
هناك أمران ينبغي معرفتهما قبل الشروع في قياس عناوين IP للعملاء. يقدّم Docker Desktop على macOS وWindows المنفذ المنشور عبر وكيل في فضاء المستخدم يعيد كتابة كل عنوان مصدر إلى بوابة الجهاز الافتراضي `192.168.65.1`، فلا تستعيد أي قيمة لـ `TRUST_PROXY` العميل الحقيقي هناك؛ لذا انشر على Linux كل ما يواجه الإنترنت. وعلى أي نظام، يُرى الوصول إلى منفذ منشور عبر `localhost` على أنه بوابة الجسر لا عميلك، لذلك لا يخبرك اختبار من localhost بشيء عن كيفية نسب عميل حقيقي. الجدول الكامل لقيم `TRUST_PROXY` وتحذير Docker Desktop موجودان في [SECURITY.md](https://github.com/snapotter-hq/SnapOtter/blob/main/SECURITY.md#client-ip-resolution-trust_proxy).
هناك شيئان مهمان لكل وكيل أدناه: السماح بنصوص الطلبات الكبيرة (التحميلات)، وعدم تخزين الاستجابات مؤقتًا. يعطل وكيل التخزين المؤقت للاستجابة تقدم SSE، وبشكل أكثر وضوحًا، يجعل تنزيل ملف كبير "يبدأ ولكن لا ينتهي أبدًا"، لأن الوكيل يحتفظ بالملف بأكمله قبل تمريره. يرسل SnapOtter `X-Accel-Buffering: no` عند التنزيلات، لذا يقوم nginx ببثها حتى لو تم ترك التخزين المؤقت في مكان آخر، لكن الوكلاء بخلاف nginx يحتاجون إلى تعطيل التخزين المؤقت للاستجابة بشكل صريح (كما هو موضح في كل تكوين أدناه). إذا توقف التنزيل جزئيًا، فإن وكيل التخزين المؤقت الموجود في المقدمة هو أول شيء يجب التحقق منه.
### Nginx {#nginx}
@@ -505,7 +516,7 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE support (batch progress, feature install progress)
# تدفق الاستجابات بدلاً من التخزين المؤقت: مطلوب لتقدم SSE (الدفعة، الذكاء الاصطناعي، عمليات تثبيت الميزات) ولتنزيلات الملفات الكبيرة.
proxy_buffering off;
proxy_read_timeout 300s;
}
@@ -549,7 +560,7 @@ images.example.com {
}
```
يعطّل `flush_interval -1` التخزين المؤقت للاستجابة، وهو مطلوب لأحداث تقدم SSE (المعالجة على دفعات، وأدوات الذكاء الاصطناعي، وتثبيت الميزات). تسمح مهل الانتظار الممتدة لعمليات رفع الملفات الكبيرة بالاكتمال دون أن يغلق Caddy الاتصال مبكرًا.
يقوم `flush_interval -1` بتعطيل التخزين المؤقت للاستجابة، وهو أمر مطلوب لأحداث تقدم SSE (معالجة الدفعات، وأدوات الذكاء الاصطناعي، وعمليات تثبيت الميزات) ولتنزيلات الملفات الكبيرة للتدفق من خلالها بدلاً من المماطلة. تسمح المهلات الممتدة بإكمال عمليات تحميل الملفات الكبيرة دون إغلاق Caddy الاتصال مبكرًا.
### أنفاق Cloudflare {#cloudflare-tunnels}