# دليل التشغيل والأعطال المعروفة — سيرو > **الغرض:** مرجع عملي لكل ما يلزم عند النشر أو النقل إلى سيرفر جديد. > كل بند هنا عطلٌ وقع فعلاً في الإنتاج، لا احتمالٌ نظري. > > **آخر تحديث:** 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)
` — والعطل يبدو في التطبيق بينما مصدره سطر تحذير في 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` أسقطتها دفعة واحدة.