docs: دليل التشغيل والأعطال المعروفة (المنافذ، nginx، القواعد، التوكن، سجل الملفات)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-08-08 15:07:59 +03:00
co-authored by Claude Opus 5
parent 875ce763bd
commit dd44b84e81
+321
View File
@@ -0,0 +1,321 @@
# دليل التشغيل والأعطال المعروفة — سيرو
> **الغرض:** مرجع عملي لكل ما يلزم عند النشر أو النقل إلى سيرفر جديد.
> كل بند هنا عطلٌ وقع فعلاً في الإنتاج، لا احتمالٌ نظري.
>
> **آخر تحديث:** 2026-08-08 — بعد جلسة إصلاح شاملة على إنتاج الأردن.
> مكمّل لـ [30-siro-port-plan.md](30-siro-port-plan.md).
---
## ١. خريطة المنافذ
المنافذ أكثر ما يُنسى عند النقل، ونسيانها يعطي `timeout` لا رسالة خطأ.
| المنفذ الخارجي | الخدمة | داخل دوكر | مَن ينهي TLS | UFW |
|---|---|---|---|---|
| 443 | الباك إند + المدفوعات | `nginx` على `HTTP_PORT` | nginx المضيف | مفتوح |
| 2020 | سوكيت السائقين (GPS) | `127.0.0.1:12020` | **nginx المضيف** | يلزم فتحه |
| 3030 | سوكيت الركاب | `127.0.0.1:13030` | **nginx المضيف** | يلزم فتحه |
| 4040 | سوكيت الطعام | `127.0.0.1:14040` | **nginx المضيف** | يلزم فتحه |
| — | HTTP داخلي للطعام | `socket_food:4041` | لا شيء | داخلي |
| — | HTTP داخلي للسائقين | `socket_driver:2021` | لا شيء | داخلي |
> ⚠️ **`HTTP_PORT` ليس 8080.** في إنتاج الأردن هو **8182**، ويُقرأ من `docker/.env`.
> افتراضه خطأً أضاع في هذه الجلسة ثلاث محاولات تشخيص متتالية: كل `curl` على 8080
> كان يضرب خدمة أخرى تماماً، فبُنيت على نتائجه نظريات خاطئة عن DNS وعن العناوين
> المهجورة. **اقرأ المنفذ قبل أي قياس:**
> ```bash
> docker compose port nginx 80; grep '^HTTP_PORT' .env
> ```
### قاعدة السوكيت الجديد — ثلاث خطوات لا واحدة
حاويات Workerman تفتح منافذها **نصاً صريحاً**، والتطبيق يطلب `https://`. مصافحة TLS
ضد منفذ لا يتكلم TLS تتجمّد حتى المهلة. لذلك كل سوكيت يحتاج:
1. نشر داخلي على `127.0.0.1:1XXXX` في `docker-compose.yml`
2. كتلة TLS في `docker/nginx/siro-sockets-tls.conf` ← **تُنسخ يدوياً** إلى `/etc/nginx/sites-enabled/`
3. `ufw allow XXXX/tcp`
نسيان أيٍّ منها يعطي `timeout`. وقد وقع هذا **مرتين**: أولاً في 2020/3030، ثم تكرّر
حرفياً مع الطعام (4040) رغم أن الملف نفسه يشرح العلّة في تعليقه.
### تشخيص أي عطل سوكيت — ثلاثة أوامر
```bash
ss -ltnp | grep ':PORT\b' # هل يستمع المضيف؟
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:INTERNAL/ # هل الحاوية حيّة؟
ufw status numbered | grep PORT # هل المنفذ مفتوح؟
```
ردّ **403** من الأمر الثاني = الحاوية سليمة وترفض لغياب JWT. هذا نجاح لا فشل.
> **فخ UFW:** نشر منافذ Docker **يتجاوز UFW** عبر قواعد iptables خاصة. فما دام
> `docker-proxy` يملك المنفذ يكون مفتوحاً للعالم رغم أن UFW لا يسمح به — ولحظة
> نقله إلى nginx تنطبق قواعد UFW ويُحجب بصمت. `curl` من السيرفر نفسه **لا يُثبت**
> الوصول الخارجي.
---
## ٢. طبقتا nginx — أخطر التباس في المعمارية
```
العميل → nginx المضيف (CloudPanel، 443) → nginx الحاوية (HTTP_PORT) → php-fpm
```
**تمييز مصدر الخطأ من جسم الردّ:**
| الجسم | المصدر | المعنى |
|---|---|---|
| `File not found.` (16 بايت) | **php-fpm** | الملف غير موجود في الحاوية التي وصلها الطلب |
| صفحة HTML بعنوان 404 | nginx | `try_files` لم يجد الملف |
| `502 Bad Gateway` | nginx | لم يصل الـ upstream إطلاقاً |
| `{"error":"..."}` | التطبيق | وصل ونُفِّذ |
### فخ عنوان الـ upstream المهجور
nginx يترجم الاسم المكتوب حرفياً في `fastcgi_pass` **مرة واحدة** عند تحميل
الإعدادات ويحتفظ به للأبد. عند إعادة إنشاء الحاويات تتبدّل العناوين، فيبقى nginx
يطرق عنواناً مهجوراً ⇒ **502 والحاوية سليمة وسجلّها صامت** لأن الطلبات لا تصلها.
**الحل المطبَّق** في `docker/nginx/default.conf`: وجهات كمتغيّرات مع
`resolver 127.0.0.11 valid=10s`، فتُؤجَّل الترجمة إلى وقت الطلب.
فائدة ثانية: بالاسم الحرفي كان nginx **يرفض الإقلاع** إن كانت إحدى حاويات fpm
متوقفة — أي أن سقوط حاوية الطعام وحدها كان قادراً على إسقاط الموقع بأكمله عند أول
إعادة تشغيل.
### فخ المزامنة اليدوية
`siro-sockets-tls.conf` يعيش على المضيف ويُنسخ يدوياً من المستودع. لا شيء يربط
الاثنين، ولا شيء يربط إضافة سوكيت في `docker-compose.yml` بإضافة كتلته هنا.
**تحقّق دائماً بعد الـ pull وقبل الـ cp:**
```bash
git pull && grep -c "listen 4040" docker/nginx/siro-sockets-tls.conf
```
وخذ نسخة احتياطية قبل النسخ — الملف يحمل سوكيت الرحلات العامل:
```bash
cp /etc/nginx/sites-enabled/siro-sockets-tls.conf /root/siro-sockets-tls.conf.bak
cp docker/nginx/siro-sockets-tls.conf /etc/nginx/sites-enabled/ && nginx -t
```
لا تُعِد التحميل إلا بعد `test is successful`. تحذيرات `ssl_stapling` ضجيج قديم
لا علاقة له.
---
## ٣. قواعد البيانات
### الخريطة
`backend/core/Database/Database.php` يُفرد لكل قاعدة **host وuser وpass مستقلة** —
الفصل على خوادم مختلفة مفترضٌ في التصميم:
| الاسم | متغيّر البيئة | القيمة في الأردن |
|---|---|---|
| `main` | `DB_PRIMARY_NAME_V2` | `jorSiroDB` |
| `ride` | `DB_RIDE_NAME` | `intaleq-ridesDB` |
| `tracking` | `DB_TRACKING_NAME` | `locationDB` |
| `transit` | `DB_TRANSIT_NAME` | `siroTransitDb` |
| `food` | `DB_FOOD_NAME` | `siro_food` |
| **(خارج الخريطة)** | `DB_PAYMENT_NAME` | `payment` |
> `DB_PRIMARY_NAME` (بلا `_V2`) = `intaleqDB1` وهي **قاعدة أخرى**. أي كود يقرأ
> المتغيّر بلا `_V2` يعمل على قاعدة خاطئة صامتاً.
### ممنوع: الإشارة المؤهَّلة عبر القواعد
`SELECT ... FROM jorSiroDB.ride` تعمل **فقط** إن كانت القاعدتان على نفس خادم
MySQL. تعمل اليوم بالصدفة وتسقط يوم يُفصل الدفع.
**الصحيح:** اتصال ثانٍ مستقل — انظر `payment_server/v2/main/db_primary.php`.
### قاعدة الفصل بين القراءة والكتابة
- **قراءة** بيانات غير مالية (عدّ رحلات، إحصاء): اتصال مباشر ✅
- **كتابة** في المحفظة: **S2S عبر خادم الدفع حصراً** ❌ لا اتصال مباشر
السبب أن نقطة S2S هي البوابة الوحيدة التي تجمع JWT وHMAC ومنع التكرار والأثر.
وهي ناقصة أصلاً: `driverWallet.paymentID` **بلا مفتاح فريد وبلا فحص تكرار** رغم
أن التعليقات تدّعي عكس ذلك. توزيع الكتابة على كل من يملك اتصالاً يجعل إصلاح ذلك
مستحيلاً لاحقاً.
### الترحيلات — لا جدول تتبّع
كل ملف في `backend/migrations/` يُطبَّق يدوياً ولا سجل لما نُفِّذ.
```bash
docker compose exec -T mysql mysql -uroot -p"$MP" jorSiroDB < backend/migrations/FILE.sql
```
> ⚠️ `2026_08_07_verify_all.sql` **لا يفحص كل جداول دفعته** — لا يذكر
> `scheduled_rides` ولا جداول SMS. خروجه نظيفاً **ليس** دليلاً على الاكتمال.
عند أي خطأ قاعدة من endpoint جديد، افحص وجود الجدول مباشرة قبل قراءة الكود:
```sql
SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.TABLES WHERE TABLE_NAME='X';
```
إعادة التشغيل: ما يستعمل `siro_add_column` من `_helpers.sql` آمن؛ وما يستعمل
`ALTER TABLE ... ADD COLUMN` خاماً **يفشل** إن أُعيد (مثل `2026_08_09_obligation_credit_gate`).
---
## ٤. PHP في الحاويات
**صورة `php:fpm` الرسمية لا تشحن `php.ini` إطلاقاً**، فتسري القيم المدمجة و
`display_errors` **مُفعّل**. أي أن كل تحذير يُطبع **داخل جسم الردّ** قبل الـ JSON.
الأثر الحقيقي: شاشة الرصيد كانت تسقط بـ
`FormatException: Unexpected character (at character 1) <br />` — والعطل يبدو في
التطبيق بينما مصدره سطر تحذير في PHP. وأمنياً، نص التحذير يحمل المسار المطلق
وأحياناً جزءاً من الاستعلام ويُسلَّم للعميل.
**الحل:** `docker/php/php-prod.ini` مركّب على `php` و`php_food` و`php_transit`.
تحقّق بعد أي إعادة إنشاء:
```bash
docker compose exec -T php php -i | grep -E "^display_errors|^upload_max_filesize"
# المتوقّع: Off / 25M
```
> ضبط `upload_max_filesize=25M` ليوافق `client_max_body_size` في nginx — كان على
> 2M الافتراضية، وهو ما يقطع رفع صور المستندات بصمت.
**النشر:** `backend` مربوط bind-mount و opcache على `validate_timestamps=1`
بتحديث كل 60ث ⇒ **لا حاجة لإعادة بناء**، يكفي `git pull`. أما تغيير
`docker-compose.yml` فيلزمه `docker compose up -d`.
---
## ٥. نظام التوكن (JWT)
### الأنواع
| النوع | مَن يُصدره | المسموح له |
|---|---|---|
| `registration` | `loginFirstTimeDriver.php` | مسارات التسجيل فقط (`REGISTRATION_ENDPOINTS`) |
| `access` role=driver | `loginJwtDriver.php` / `auth/otp/verify.php` | كل شيء |
توكن التسجيل صالح **ساعة كاملة** لكنه بلا صلاحيات. وأي endpoint محمي يردّه بـ
**403 `Token not authorized for this action`**.
### العطل الذي عطّل كل شيء
`getJWT` كان يفحص `exp` وحده. فتوكن التسجيل يبقى مخزّناً بعد اكتمال الدخول، ويردّ
كل endpoint محمي 403 — و`restart` لا ينفع لأن `force` يمسح الـ cooldown فقط.
وسطر الترقية بعد الدخول كان معطَّلاً بالتعليق.
**الحل (مطبَّق):** فحص `token_type` لا `exp` وحده، وترقية فورية بعد `loginDriver`،
و`CRUD` يعالج 403 كما يعالج 401 (تجديد واحد ثم إعادة).
### بصمة الجهاز
`driverToken.fingerPrint` كانت تُكتب **بصيغتين** حسب آخر endpoint لمسها: خام من
`verify.php` و`addDriver.php`، ومُهشّمة (`sha256(fp+FP_PEPPER)`) من
`loginJwtWalletDriver.php`. و`loginJwtDriver.php` يقارن بالمُهشّمة وحدها ⇒ رفض دائم
لكل سائق OTP لم يفتح المحفظة.
**الحل:** `loginJwtDriver.php` يقبل الصيغتين ويوحّد الصفّ على المُهشّمة. والتطبيق
يتخطّى المقارنة عند رؤية 64 خانة hex (لا يملك الـ pepper).
### مسار الجهاز الجديد
جهاز جديد ⇒ `loginJwtDriver` يردّ 401 ⇒ العلم `needsDeviceVerification` ⇒ شاشة OTP
⇒ `verify.php` يربط البصمة **ويُصدر توكن driver** ⇒ يُحفظ. كان الفرع `token_change`
لا يفعل أياً من الاثنين، فتنجح شاشة الـ OTP ولا يعمل بعدها شيء.
---
## ٦. سجل الملفات المعدَّلة
### الباك إند
| الملف | العطل | الدرس |
|---|---|---|
| `core/Auth/JwtService.php` | — (مرجع) | `REGISTRATION_ENDPOINTS` تُطابق بـ `basename`؛ أي endpoint جديد في التسجيل يجب إضافته |
| `loginJwtDriver.php` | بصمة بصيغتين ⇒ 401 دائم | وحّد صيغة التخزين، واقبل القديمة للتوافق |
| `auth/otp/verify.php` | `token_change` لا يُصدر توكناً ولا يربط البصمة | التحقق وحده لا يكفي — لا بد من ربط وإصدار |
| `ride/gamification/getLeaderboard.php` | 4 أعمدة غير موجودة (`d.name`, `nameArabic`, `firstName`, `personal_photo`) | الأعمدة الفعلية `first_name`/`last_name` و**مشفّرة** — تحتاج فكّ تشفير |
| `ride/scheduled/list.php` | `WHERE driver_id` وجدول `scheduled_rides` **بلا هذا العمود** | الحجز يُنشئه الراكب؛ السائق يُسند لحظة توليد الرحلة — الربط عبر `ride_id` |
| `ride/invitor/referral_code_helper.php` | **جديد** | المولّد كان في نقطة لا يناديها أحد ⇒ الكود `null` للجميع. القراءة نفسها تُنشئ |
| `ride/invitor/get_driver_referrals.php` | تقرأ ولا تولّد | — |
| `ride/invitor/get_passenger_referrals.php` | نفسه | — |
| `ride/invitor/get_unified_code.php` | `while(true)` تُعلّق العامل لو امتلأت الأكواد | سقف محاولات ثم بديل أطول |
### خادم الدفع
| الملف | العطل | الدرس |
|---|---|---|
| `db_primary.php` | **جديد** | اتصال ثانٍ للقاعدة الأساسية — لا إشارة مؤهَّلة |
| `ride/payment/getAllPayment.php` | `Table 'payment.ride' doesn't exist` بلا `try/catch` ⇒ 500 | افصل الرصيد عن الإحصاء: المالي لا يسقط لأجل المساعد |
| `ride/driverWallet/driverStatistic.php` | نفسه (`driver_orders` + `ride`) | — |
### تطبيق السائق
| الملف | العطل | الدرس |
|---|---|---|
| `controller/auth/captin/login_captin_controller.dart` | فحص `exp` وحده؛ الترقية معلّقة | صلاحية التوكن ليست زمنه فقط بل نوعه |
| `controller/functions/crud.dart` | 403 بلا معالجة؛ حجب النقاط العامة عند غياب التوكن | حجب `/auth/otp/` أغلق مسار الاستعادة نفسه |
| `controller/auth/captin/phone_helper_controller.dart` | يرمي التوكن الذي يصدره السيرفر | — |
| `controller/auth/captin/opt_token_controller.dart` | نفسه | — |
| `controller/auth/captin/invit_controller.dart` | **يرفع دفتر الهاتف كاملاً** تلقائياً | ⚠️ انظر §7 |
| `controller/home/payment/captain_wallet_controller.dart` | `isLoading` بلا `finally` ⇒ لودينج أبدي لكل سائق جديد | كل مسار تحميل يحتاج `finally` |
| `controller/home/captin/destination_controller.dart` | يفهرس نصاً كخريطة | الردّ قد يكون `String` عند الفراغ |
| `controller/gamification/leaderboard_controller.dart` | `jsonDecode` على Map | **`CRUD().post` تُرجع مفكوكاً، و`get` تُرجع نصاً** |
| `controller/scheduled/driver_scheduled_rides_controller.dart` | يعرض «لا حجوزات» عند فشل 500 | افصل الخطأ عن الفراغ — الأول يُضلّل الكابتن |
| `views/.../drawer_captain.dart` | `onBackgroundImageError` بلا صورة ⇒ assertion | — |
| `views/.../home_captin.dart` | فيض في الشريط العلوي | ما زال 9.8px — الشعار 36px مقابل 26.2px |
| `views/home/statistics/widgets/stat_summary_card.dart` | فيض 9.7px | — |
---
## ٧. بنود مفتوحة
**دفتر الهاتف (أولوية عليا).** أُوقف الرفع التلقائي، لكن ما جُمع سابقاً ما زال في
`ride/egyptPhones` — أسماء وأرقام أطراف ثالثة لم تُستأذن. رفع دفتر العناوين جملةً
يخالف سياسة Google Play ويتطلب إفصاحاً بارزاً وموافقة صريحة؛ إذن النظام وحده لا
يكفي. **قرار الحذف لم يُتخذ بعد.**
**الفيض في الشريط العلوي.** تقلّص من 16px إلى 9.8px؛ يحتاج تصغير الشعار نفسه.
**حارس المزامنة.** لا شيء يقارن `docker/nginx/siro-sockets-tls.conf` بنظيره على
المضيف. مقترح: سطر تحقق في `deploy.sh`.
**سبب انقطاع الـ 404 الشامل** الذي وقع 2026-08-08 لم يُوثَّق — يُرجَّح أن CloudPanel
أعاد توليد ملف الدومين. **إن صحّ فسيتكرّر عند أول تجديد شهادة.**
**خطط التأمين** غير مزروعة (`insurance_plans` فارغ) — الصفحة تعرض حالة فارغة صحيحة.
**الرحلات المجدولة للسائق** تعرض ما أُسند له فقط. إن أُريد أن يرى حجوزات منطقته
قبل الإسناد فذاك تصميم مختلف.
---
## ٨. ترتيب النشر الآمن
```bash
# ١. اسحب وتحقّق أن ما تتوقعه وصل فعلاً
git pull && grep -c "listen 4040" docker/nginx/siro-sockets-tls.conf
# ٢. ملفات المضيف (إن تغيّرت) — بنسخة احتياطية وفحص
cp /etc/nginx/sites-enabled/siro-sockets-tls.conf /root/backup.conf
cp docker/nginx/siro-sockets-tls.conf /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx
# ٣. الحاويات (فقط إن تغيّر compose أو ملفات ini)
cd docker && docker compose up -d php php_food php_transit
# ٤. تحقّق بالقياس لا بالافتراض
docker compose exec -T php php -i | grep ^display_errors
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:$(grep '^HTTP_PORT' .env | cut -d= -f2)/backend/auth/packageInfo.php
# المتوقّع 401
```
> **القاعدة الحاكمة لكل ما سبق:** لا تستنتج من قياس لم تتحقق من صحة أداته.
> ثلاث نظريات خاطئة في هذه الجلسة — عن الرفع، وعن DNS، وعن العناوين المهجورة —
> بُنيت جميعها على `curl` لمنفذ مفترض. قراءة `HTTP_PORT` من `.env` أسقطتها دفعة واحدة.