Files
Siro/docs/31-siro-operations-runbook-AR.md
T

322 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# دليل التشغيل والأعطال المعروفة — سيرو
> **الغرض:** مرجع عملي لكل ما يلزم عند النشر أو النقل إلى سيرفر جديد.
> كل بند هنا عطلٌ وقع فعلاً في الإنتاج، لا احتمالٌ نظري.
>
> **آخر تحديث:** 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` أسقطتها دفعة واحدة.