18 KiB
دليل التشغيل والأعطال المعروفة — سيرو
الغرض: مرجع عملي لكل ما يلزم عند النشر أو النقل إلى سيرفر جديد. كل بند هنا عطلٌ وقع فعلاً في الإنتاج، لا احتمالٌ نظري.
آخر تحديث: 2026-08-08 — بعد جلسة إصلاح شاملة على إنتاج الأردن. مكمّل لـ 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 وعن العناوين المهجورة. اقرأ المنفذ قبل أي قياس:docker compose port nginx 80; grep '^HTTP_PORT' .env
قاعدة السوكيت الجديد — ثلاث خطوات لا واحدة
حاويات Workerman تفتح منافذها نصاً صريحاً، والتطبيق يطلب https://. مصافحة TLS
ضد منفذ لا يتكلم TLS تتجمّد حتى المهلة. لذلك كل سوكيت يحتاج:
- نشر داخلي على
127.0.0.1:1XXXXفيdocker-compose.yml - كتلة TLS في
docker/nginx/siro-sockets-tls.conf← تُنسخ يدوياً إلى/etc/nginx/sites-enabled/ ufw allow XXXX/tcp
نسيان أيٍّ منها يعطي timeout. وقد وقع هذا مرتين: أولاً في 2020/3030، ثم تكرّر
حرفياً مع الطعام (4040) رغم أن الملف نفسه يشرح العلّة في تعليقه.
تشخيص أي عطل سوكيت — ثلاثة أوامر
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:
git pull && grep -c "listen 4040" docker/nginx/siro-sockets-tls.conf
وخذ نسخة احتياطية قبل النسخ — الملف يحمل سوكيت الرحلات العامل:
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/ يُطبَّق يدوياً ولا سجل لما نُفِّذ.
docker compose exec -T mysql mysql -uroot -p"$MP" jorSiroDB < backend/migrations/FILE.sql
⚠️
2026_08_07_verify_all.sqlلا يفحص كل جداول دفعته — لا يذكرscheduled_ridesولا جداول SMS. خروجه نظيفاً ليس دليلاً على الاكتمال.
عند أي خطأ قاعدة من endpoint جديد، افحص وجود الجدول مباشرة قبل قراءة الكود:
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.
تحقّق بعد أي إعادة إنشاء:
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 فارغ) — الصفحة تعرض حالة فارغة صحيحة.
الرحلات المجدولة للسائق تعرض ما أُسند له فقط. إن أُريد أن يرى حجوزات منطقته قبل الإسناد فذاك تصميم مختلف.
٨. ترتيب النشر الآمن
# ١. اسحب وتحقّق أن ما تتوقعه وصل فعلاً
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أسقطتها دفعة واحدة.