send_fcm.php هي نقطة كل الرسائل والمكالمات بين الراكب والسائق (التطبيقان
يستدعيانها مباشرة)، ولم تكن تحتوي أي error_log إطلاقاً — بخلاف FcmService
التي تطبع [FCM_DEBUG] وتخدم دورة حياة الرحلة فقط.
النتيجة أن فشل الرسائل كان غير قابل للتشخيص: لا سبب، ولا رد Google، ولا
حتى معرفة إن كان الطلب وصل. وقد وعدت المستخدم بقراءة [FCM_DEBUG] لهذا
المسار وهو وعد خاطئ — لا وجود له هنا.
أُضيفت ثلاث نقاط:
- نتيجة الإرسال: category + طرف من التوكن + http + طول التوكن + رد Google
عند الفشل. طول التوكن مقصود: توكن FCM الحقيقي ~163 محرفاً، والمشفّر في
جدول tokens 216 — فيُكشف أي blob مشفّر من سطر واحد.
- رفض 403: التطبيقان لا يرسلان x-api-key، فلحظة ضبط FCM_INTERNAL_API_KEY
تموت كل الرسائل بينهما بصمت. الآن يُسجَّل السبب صريحاً.
- رفض 400 على target فارغ: يعني أن المُرسِل لا يملك توكن الطرف الآخر
(tokenPassenger أو driverToken لم يصله في حمولة القبول).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
الموضعان كانا يقرآن مفتاحاً لا يُكتب في أي مكان في المشروع:
$redisLocation->hGetAll("driver:location:$driverId")
الكاتب الفعلي هو معالج الدفعات في loction_server/driver_socket.php، وهو
يكتب hmset على driver:profile:{id} و driver:public:{id} معاً بنفس الحقول
(lat/lng/heading/speed/status/updated_at). و driver:public له TTL 86400
بينما driver:profile له 900 فقط، فالعام هو الأنسب للقراءة.
الأثر: كانت حمولة القبول تصل الراكب بلا إحداثيات أولية للسائق، فلا يظهر
الماركر إلا بعد أول تحديث موقع من السوكيت أو الـ polling.
أحد الموضعين ملف getRideOrderID.php الذي أضفته في f66db7db — نسخت النمط
من acceptRide.php فنقلت الخطأ معه.
مثبَّت على الإنتاج: redis-cli --scan --pattern 'driver:*' أرجع
driver:public:<id> فقط، ولا شيء باسم driver:location.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
تطبيق الراكب يطلب /ride/rides/getRideOrderID.php منذ البداية، والموجود على
السيرفر getRideOrderIDNew.php فقط ⇒ 404 صامت في getUpdatedRideForDriverApply.
هذا يفسّر عرضين ظنّاهما منفصلين:
- الـ polling يسبق الـ FCM فيضبط _isAcceptanceProcessed=true ثم يأخذ 404،
فيُرفض بعدها payload الـ FCM الذي يحمل driver_info كاملاً
("Already processed") ⇒ الرحلة مقبولة بلا معلومات سائق.
- ولأن driverToken يبقى فارغاً، ترد send_fcm.php بـ 400 Missing: target
⇒ رسائل الراكب للسائق لا تُرسَل إطلاقاً. (الاتجاه المعاكس كان يعمل بعد
إصلاح getRideWaiting.php — نفس خطأ التشفير معكوساً.)
getRideOrderIDNew.php لا يصلح بديلاً: داخلي عبر get_connect.php →
validateInternalKey فلا يستطيع التطبيق مناداته، ولا يرجع
ratingCount/completedRides/driverTier.
النقطة الجديدة:
- connect.php (JWT)، والراكب من الـ JWT فقط لا من الطلب (حماية IDOR).
- نفس استعلام acceptRide.php وشكل رده حرفياً حتى يقرأه
_fillDriverDataLocally بنفس المفاتيح.
- فك تشفير driverToken.token — بلا ذلك يصل التطبيق blob يستخدمه كـ FCM
target فيرفضه FCM بـ 400.
- تفادي تصادم المفاتيح: getUpdatedRideForDriverApply يقرأ
passengerName + last_name كاسم الراكب، فلقب السائق نُقل إلى
driver_last_name واسم الراكب الحقيقي يُرجَّع في مكانه.
- ride DB هو المرجع مع fallback على primary، ورحلة بلا سائق ترد success
لا failure.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
سوكيت الراكب (سبب عدم ظهور معلومات السائق عند القبول):
- تطبيق الراكب كان يرسل id فقط بلا jwt، و passenger_socket.php يرفض أي
اتصال بلا jwt ⇒ الراكب لا ينضم لغرفته أبداً ولا يستلم ride_status_change
ولا driver_location_update. تظهر حالة القبول عبر الـ polling فقط بينما
driver_info يصل بالسوكيت وحده. (سوكيت السائق يعتبر الـ jwt اختيارياً،
ومن هنا جاء التباين بين التطبيقين.)
- cancelled_by_driver كان يسقط من switch حالات الراكب فيبقى معلّقاً بعد
إلغاء السائق.
- حماية socket (late) من القراءة قبل التهيئة عند الانسحاب بلا jwt.
الإشعارات والرسائل (سبب "مرات توصل ومرات لا"):
- جدول tokens يخزّن توكن الراكب مشفّراً، و getRideWaiting.php كان يرجعه
بلا فك تشفير ⇒ من يقبل من قائمة السوق يحمل blob مشفّراً يستخدمه كـ FCM
target فيرفضه FCM بـ 400: لا إشعار قبول ولا رسائل. ومن يقبل من الـ
dispatch/FCM يحمل نصاً صريحاً فتعمل. الفرق كان في طريقة القبول.
- acceptRide.php يحلّ التوكن من القاعدة دائماً ولا يثق بالعميل (أصحّ أمنياً).
- market_new_ride كان لا يحمل passengerId ولا الإحداثيات فتصل "null"؛
أُضيفت بلا أي PII لأن الحمولة تُبَثّ لكل سائق قريب لا للفائز فقط.
- send_fcm.php: مهلة على OAuth (كان يعلّق حتى مهلة PHP فتُسقط الرسالة
بصمت)، توحيد ding→default لأندرويد، وحقن title/body/tone في data
مطابقةً لـ FcmService.
- تطبيق السائق يقرأ title/body من data أولاً مثل الراكب، ولا يعرض فقاعة
فارغة للرسائل الصامتة.
السوكيت والإعدادات:
- forwardLocationToPassengerSocket كان يقرأ lat/lng والحمولة فيها
latitude/longitude ⇒ المسافة تخرج ضخمة والـ throttle معطّل تماماً
فيُعاد التوجيه مع كل نبضة GPS.
- notifyPassengerOnRideServer كان يرجع null بصمت مطلق عند حجب العنوان.
- العنوان الافتراضي لسيرفر الموقع كان nginx/loction_server/driver_socket.php
وهو ديمون Workerman لا يُخدَم عبر nginx ⇒ صار socket_driver:2021.
(LOCATION_API_URL بقي على nginx لأن api_get_nearby.php سكربت عادي.)
- ride_server/passenger_socket.php (النسخة التي يشغّلها Docker) كانت ناقصة
كل كود مواصلاتي الموجود في passenger_server/ ⇒ نُقل مع REDIS_HOST.
- .env.example: ALLOWED_SOCKET_URLS يغطّي أسماء حاويات Docker، وإضافة
PASSENGER_SOCKET_INTERNAL_URL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Broadcasts never reached anyone. The internal FCM call defaulted to
127.0.0.1, which inside the php container is the php container itself — the
web server runs in a separate nginx container, reachable by service name on
the Compose network. Every send failed the curl and returned a generic 502.
The default now points at nginx, the URL is overridable via FCM_INTERNAL_URL,
and the error carries the actual reason and target instead of a bare status.
Transit organisations were a read-only count table with nothing to act on.
The module now supports the operations an admin actually needs:
- open an organisation for its counts, routes, recent trips and admins
- create one, including the founding administrator create.php requires
- edit city, contact details, contract status and trial end, with a
confirmation when the contract changes since suspending cuts off service
- add an administrator, and enable or disable an existing one
Verified end to end against the endpoints' real payload shapes, including
that an incomplete create form is rejected before any request is sent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>