Update: 2026-07-31 03:00:06

This commit is contained in:
Hamza-Ayed
2026-07-31 03:00:06 +03:00
parent 7a576b7327
commit f94a9478ac
13 changed files with 1609 additions and 20 deletions
+333
View File
@@ -0,0 +1,333 @@
# خطة إضافة «طلبات الطعام» إلى منصة سيرو — المعمارية والدوكر والتنفيذ
> الحالة: مقترح للتنفيذ — لم يُكتب أي كود بعد.
> التاريخ: 2026-07-30
> المرجع المعماري: [docker/docker-compose.yml](../../docker/docker-compose.yml) و [docs/30-siro-port-plan.md](../30-siro-port-plan.md)
---
## ١. الخلاصة التنفيذية (القرار في عشرة أسطر)
- «طلبات الطعام» تُضاف كـ **وحدة (module) مستقلة داخل نفس منصة سيرو**، لا كمشروع منفصل ولا كخدمة مصغّرة (microservice) بقاعدة مستخدمين خاصة بها.
- الحاويات: نضيف **حاويتين فقط** إلى نفس `docker compose`: حاوية `php_food` (fpm للطلبات المتزامنة) وحاوية `socket_food` (عملية Workerman دائمة لحالة الطلب اللحظية).
- المشترَك يبقى مشترَكاً: **نفس nginx، نفس MySQL، نفس Redis، نفس JWT، نفس المحفظة** — لأن هذا هو ما يجعل التطبيق «متكاملاً» بدل أن يكون تطبيقين ملصوقين.
- المعزول يبقى معزولاً: **قاعدة بيانات `siro_food` مستقلة**، مساحة أسماء مفاتيح Redis مستقلة `food:*`، وحدود ذاكرة ولوغات مستقلة.
- الفائدة الحقيقية من العزل ليست الأداء (الحاوية على لينكس = عملية عادية)، بل: انهيار الطعام لا يُسقط الرحلات، ونشر الطعام لا يتطلب إعادة تشغيل الباك إند، وأي عميل وايت-ليبل يشغّل الطعام أو يطفئه بمتغيّر واحد.
---
## ٢. المبدأ الحاكم: ما الذي يُشارَك وما الذي يُعزل؟
القاعدة التي بُني عليها هذا التصميم — وهي نفس فلسفة `docker-compose.yml` الحالية «التقسيم بالدور، لا حاوية-واحدة-فيها-كل-شيء ولا تفتيت microservices»:
| المكوّن | القرار | السبب |
|---|---|---|
| هوية المستخدم (JWT) | **مشترك** | الراكب نفسه هو الزبون نفسه. حسابان لشخص واحد = كارثة منتج ودعم فني. |
| المحفظة والدفع | **مشترك** (`payment_server/v2`) | رصيد واحد للراكب يُنفَق على الرحلة والوجبة. تكرار المحفظة يعني تسويتين ماليتين متضاربتين. |
| الإشعارات (FCM) | **مشترك** (`core/Services/FcmService.php`) | مسار الرسائل مُشخَّص ومُسجَّل بالفعل. |
| Redis | **مشترك، بمساحة أسماء منفصلة** | نفس المثيل، لكن كل مفاتيح الطعام تبدأ بـ `food:` — لا تصادم مع حالة الرحلة. |
| قاعدة البيانات | **معزولة: `siro_food`** | نفس نمط `siro_transit`. يمنع أن يقفل جدول طلبات مزدحم استعلامات الرحلات. |
| كود PHP وحاوية fpm | **معزولة: `backend/food/` + `php_food`** | نشر مستقل، حد ذاكرة مستقل، وانهيار مستقل. |
| السوكيت | **معزول: `socket_food`** | العملية الدائمة لا تُخلَط أبداً مع fpm (نفس سبب فصل `socket_driver`). |
| أسطول التوصيل | **مشترك مع تمييز بالدور** | نفس السائقين، مع علم `can_deliver`. تفصيل هذا في §٦. |
**ما لن نفعله (قرارات مرفوضة صراحةً):**
1. لن نبني تطبيق Flutter خامساً للزبون — الطعام يدخل داخل `siro_rider` كتبويب.
2. لن ننشئ جدول مستخدمين جديداً في `siro_food`؛ نخزّن `passenger_id` كمرجع منطقي فقط.
3. لن نضع الطعام داخل حاوية `php` الحالية — لأن أي خطأ فادح في الطعام سيستهلك حوض fpm نفسه الذي يخدم الرحلات.
4. لن نفتح بورت السوكيت للعالم مباشرة (انظر §٤ — درس مؤلم مدفوع الثمن سابقاً).
---
## ٣. طبقة الدوكر — الشكل النهائي
### ٣.١ الحاويات الجديدة
تُضاف إلى نفس `docker/docker-compose.yml` (لا ملف compose ثانٍ — ملفّان يعنيان شبكتين وحيرة تشغيلية):
```yaml
# ‏وحدة الطعام — fpm مستقلة عن fpm الرحلات عمداً:
# ‏انهيار الطعام يجب ألا يبتلع حوض العمليات الذي يخدم الرحلات.
php_food:
build:
context: ./php
dockerfile: Dockerfile.fpm # نفس الصورة تماماً — لا صيانة مزدوجة
args:
PHP_VERSION: "${PHP_VERSION:-8.2}"
volumes:
- ../backend:/var/www/backend # يحتاج core/ و functions.php المشتركة
- ./php/opcache.ini:/usr/local/etc/php/conf.d/zz-opcache.ini:ro
- ./php/food-pool.conf:/usr/local/etc/php-fpm.d/zz-pool.conf:ro
- ./keys:/keys:ro
env_file: .env
depends_on: [mysql, redis]
mem_limit: 1g
restart: unless-stopped
# ‏سوكيت الطعام — WS بورت 4040 + HTTP داخلي 4041
socket_food:
build:
context: ./php
dockerfile: Dockerfile.socket
args:
PHP_VERSION: "${PHP_VERSION:-8.2}"
command: ["php", "food_socket.php", "start"]
working_dir: /app
volumes:
- ../food_server:/app
- ./keys:/keys:ro
env_file: .env
ports:
# ‏لا نفتح 4040 للعالم: Workerman نصّ صريح والتطبيق يطلب TLS فتتجمّد المصافحة.
# ‏nginx على المضيف يستمع 4040 بالشهادة ويمرّر إلى 14040 هنا.
- "127.0.0.1:14040:4040"
# ‏و4041 داخلي فقط: الباك إند يناديه عبر http://socket_food:4041
depends_on: [redis]
mem_limit: 512m
restart: unless-stopped
```
### ٣.٢ تعديل nginx (حاوية البوابة)
في [docker/nginx/default.conf](../../docker/nginx/default.conf) يُضاف توجيه مسار الطعام إلى حوض fpm الخاص به:
```nginx
# كل ما تحت /backend/food/ يذهب إلى حوض fpm المستقل
location ~ ^/backend/food/.*\.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass php_food:9000; # ← لا php:9000
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 60s;
}
```
> ترتيب البلوكات مهم: هذا البلوك يجب أن يسبق `location ~ \.php$` العام، وإلا ابتلعه الأخير وذهبت الطلبات إلى الحوض الخطأ بصمت.
وتُضاف نقطة حالة الحوض `/(fpm-status-food)` بنفس قيود العنوان الحالية، وإلا سنراقب حوضاً واحداً ونظن أننا نراقب اثنين.
### ٣.٣ nginx المضيف (TLS للسوكيت) — إلزامي لا اختياري
يُضاف مقطع في `nginx/siro-sockets-tls.conf` على المضيف: استماع `4040` بالشهادة → تمرير إلى `127.0.0.1:14040`. هذا **ليس تحسيناً**: تكرار خطأ الماضي (نشر بورت السوكيت خاماً) ينتج مصافحة مجمّدة و timeout في التطبيق بلا أي سطر خطأ يدل عليه.
### ٣.٤ جدار الحماية
لا تعتمد على UFW لحجب بورت الطعام: `docker-proxy` يلتفّ على قواعد UFW. الحجب يتم بربط البورت بـ `127.0.0.1` في compose كما هو مكتوب أعلاه — وهذا كافٍ وحده.
### ٣.٥ المتغيّرات الجديدة في `docker/.env.example`
```
FOOD_ENABLED=true
DB_FOOD_NAME=siro_food
DB_FOOD_HOST=mysql
DB_FOOD_USER=siro_food
DB_FOOD_PASS=
FOOD_SOCKET_URL=http://socket_food:4041
FOOD_COMMISSION_PERCENT=15
FOOD_DELIVERY_BASE_FEE=
FOOD_MAX_ACTIVE_ORDERS_PER_USER=3
```
### ٣.٦ فخ النشر الذي يجب تفاديه مسبقاً
`vendor/` و `composer.lock` مُستثنيان من git في بعض مسارات المشروع — وهذا ما قتل `payment_server/v2` على نشر نظيف سابقاً. لذلك:
- `food_server/composer.lock` **يُلتزم به في git إلزامياً**.
- تُضاف إلى [docker/README.md](../../docker/README.md) خطوة صريحة:
`docker compose run --rm socket_food composer install`
---
## ٤. طبقة البيانات — قاعدة `siro_food`
### ٤.١ التسجيل في طبقة الاتصال
يُضاف مدخل واحد إلى الخريطة في [backend/core/Database/Database.php](../../backend/core/Database/Database.php):
```php
'food' => [
'name' => 'DB_FOOD_NAME',
'host' => 'DB_FOOD_HOST',
'user' => 'DB_FOOD_USER',
'pass' => 'DB_FOOD_PASS',
],
```
وقاعدة صارمة تُوثَّق وتُراجَع في كل PR — نفس قاعدة transit: **ممنوع `Database::get('main')` داخل `backend/food/`**. أي حاجة لبيانات الراكب تُقرأ عبر دالة خدمة واحدة معرّفة في `food/functions.php`، لا باستعلامات متفرقة عبر القواعد.
### ٤.٢ المخطط `schema_food.sql` (الجداول الأساسية)
```
merchants المتجر/المطعم: الاسم، الموقع (lat/lng)، الحالة، نسبة العمولة، أوقات العمل
merchant_users حساب دخول صاحب المطعم (هوية منفصلة عن الراكب — دور merchant)
menu_categories أقسام القائمة داخل المطعم
menu_items الصنف: السعر، الصورة، متاح/غير متاح، وقت التحضير التقديري
item_options الإضافات والخيارات (حجم، إضافات) وسعر كل خيار
orders الطلب: passenger_id، merchant_id، courier_id، الحالة، الإجماليات، العنوان
order_items أصناف الطلب بسعر **مجمّد وقت الطلب** (لا JOIN على menu_items للسعر)
order_status_log كل انتقال حالة مع الوقت والفاعل — مصدر الحقيقة للنزاعات
order_payments مرجع معاملة المحفظة/الدفع + حالة التسوية
courier_assignments محاولات إسناد الطلب للسائق: عُرض/قُبل/رُفض/انتهت المهلة
merchant_payouts مستحقات المطاجر ودورات التسوية
food_promo_codes أكواد الخصم (منفصلة عن أكواد الرحلات)
```
**قواعد مخطط غير قابلة للتفاوض:**
1. **تجميد الأسعار**: `order_items` يحمل `unit_price` و `option_price_json` منسوخين وقت الإنشاء. تغيير المطعم لسعره لاحقاً يجب ألا يعيد كتابة تاريخ طلب مكتمل.
2. **المال بالأعداد الصحيحة**: كل المبالغ `BIGINT` بأصغر وحدة نقدية، لا `FLOAT` مطلقاً.
3. **مفتاح تعطيل التكرار (idempotency)**: عمود `client_order_uuid UNIQUE` على `orders` — الضغط المزدوج على «تأكيد» أو إعادة محاولة الشبكة يجب ألا ينتج طلبين ولا خصمين.
4. **الحالة كسلسلة محكومة**: `pending → merchant_accepted → preparing → ready → courier_assigned → picked_up → delivered` وفروع `cancelled_by_*` و `rejected`. الانتقالات تُفرَض في دالة واحدة `food_transition_status()` — لا `UPDATE orders SET status` مبعثرة في الملفات.
5. **الفهارس منذ اليوم الأول**: `(passenger_id, created_at)`، `(merchant_id, status)`، `(courier_id, status)`، ومكاني على `merchants(lat,lng)`.
---
## ٥. طبقة الـ API — البنية والمسارات
نتبع نمط «بوابة لكل جمهور» المستخدَم في `backend/transit/` حرفياً، لأنه ما يجعل الوحدة تبدو جزءاً أصيلاً من المشروع لا طُعماً غريباً.
```
backend/food/
├── connect_app.php بوابة الزبون (JWT الراكب)
├── connect_merchant.php بوابة المطعم (JWT بدور merchant)
├── connect_courier.php بوابة السائق (JWT السائق)
├── connect_admin.php بوابة الإدارة (JWT admin/super_admin)
├── functions.php منطق مشترك: انتقال الحالة، الحسابات، صياغة الردود
├── schema_food.sql
├── merchant/ browse.php details.php menu.php search.php
├── cart/ validate.php quote.php
├── order/ create.php status.php cancel.php rate.php history.php
├── merchant_ops/ incoming.php accept.php reject.php ready.php items_toggle.php
├── courier/ offer_respond.php picked_up.php delivered.php active.php
├── admin/ merchants.php orders.php payouts.php settings.php
└── cron_*.php انتهاء مهلة الطلبات، التسويات، تنبيهات التأخير
```
كل بوابة تكرّر نفس الافتتاحية المثبتة في المشروع: `core/bootstrap.php` ثم `RateLimiter` ثم `JwtService::authenticate()` ثم `Database::get('food')` مع رد `503` نظيف عند فشل الاتصال.
**قواعد التعامل مع API الحالي — مأخوذة من سلوكه الفعلي:**
- لا تعتمد `$_GET`؛ المدخلات تُقرأ كما يقرؤها باقي المشروع (جسم الطلب)، وترويسة `X-Device-FP` مطلوبة.
- شكل الرد يطابق مغلّف الرسائل القائم (`status` + `message` + الحمولة) — التطبيق الحالي يفكّه بهذا الشكل، وأي شكل جديد سيكسر المُحلِّل المشترك.
- تعدد اللغات: نصوص الحالة تُعاد كمفاتيح، والترجمة في التطبيق.
**الأمن — البنود التي أُخذت من إخفاقات وحدة مواصلاتي، فلا تتكرر:**
1. **IDOR أولاً وقبل كل شيء**: كل نقطة تأخذ `order_id` تتحقق أن الطلب يخص الفاعل (زبونه أو مطعمه أو سائقه). يُكتب اختبار سلبي واحد على الأقل لكل نقطة قبل الدمج.
2. **تفويض الدور على مستوى البوابة**: `connect_merchant.php` يرفض توكن الراكب حتى لو كان صالحاً.
3. **الخوادم لا تثق بالأسعار**: السعر النهائي يُحسب في الخادم من `menu_items` — إجمالي يرسله العميل يُتجاهل ويُسجَّل كإشارة احتيال.
4. **تحديد المعدل**: حد أشدّ على `order/create.php` (منع فيض الطلبات الوهمية) وعلى `merchant_ops/*`.
5. **رفع صور القائمة**: تحقق من النوع والحجم، إعادة ترميز، أسماء عشوائية، ونشر من مسار لا ينفّذ PHP.
6. **بيانات شخصية**: عنوان الزبون ورقمه يظهران للسائق **فقط** بعد `courier_assigned` و**يُحجبان** بعد `delivered`.
---
## ٦. التوصيل — إعادة استخدام أسطول الرحلات
هذا أهم قرار منتجي/تقني في الخطة.
**القرار:** لا أسطول ثانٍ. نستخدم نفس السائقين مع علم `can_deliver` وحالة تفرّغ، اعتماداً على بنية `geo:drivers:available` القائمة في Redis.
**الآلية:**
1. عند `ready` (أو قبله بوقت التحضير التقديري)، يستدعي الباك إند بحثاً جغرافياً حول موقع المطعم — نفس نمط `georadius` في [loction_server/find_drivers_redis.php](../../loction_server/find_drivers_redis.php)، لكن على مفتاح `geo:couriers:available` الذي يُملأ بالسائقين ذوي `can_deliver=1` وليسوا في رحلة.
2. العرض يُرسل لسائق واحد في كل مرة بمهلة قصيرة (15–20 ثانية)، ويُسجَّل كل عرض في `courier_assignments`. الصمت = رفض ضمني وانتقال للتالي. هذا يمنع «سباق القبول» الذي ينتج طلباً بسائقين.
3. القفل: `SET food:order:{id}:lock <courier_id> NX EX 20` — القابل الأول فقط يفوز، ذرّياً.
4. سائق في رحلة نقل لا يظهر لعروض التوصيل والعكس — حالة السائق مصدر حقيقة واحد في Redis، لا علمان متنافسان.
5. الأثر على أرباح السائق: التوصيل يدخل نفس دفتر الأرباح ونفس تلميح الأرباح المعتمد في تطبيق السائق، لا شاشة أرباح موازية.
**المخاطرة الواجب مراقبتها:** في ساعة الذروة تتنافس الوجبات والرحلات على الأسطول نفسه. المؤشر الحارس: نسبة الطلبات التي لم تجد سائقاً خلال 5 دقائق. إن تجاوزت ١٠٪ نُفعّل تخصيص جزء من الأسطول للتوصيل في نطاق زمني/جغرافي — لكن **لا نبني هذا التعقيد قبل أن يثبت الرقم أنه لازم**.
---
## ٧. المال — الدفع والعمولة والتسوية
- الدفع يمر عبر `payment_server/v2` نفسه؛ الطعام لا يفتح قناة دفع جديدة.
- **الحجز ثم الالتقاط**: عند إنشاء الطلب يُحجز المبلغ من المحفظة (`hold`)، ويُلتقط عند `delivered`، ويُفكّ الحجز فوراً عند `rejected` أو `cancelled`. أي مسار إلغاء لا يفكّ الحجز يعني مالاً محتجزاً بلا سبب — وهذا أسرع طريق لفقدان ثقة المستخدم.
- الدفع نقداً عند الاستلام: يُحصّله السائق، فيُقيَّد ديناً على محفظته ويُسوّى مع المطعم في `merchant_payouts` — نفس آلية تسوية النقد القائمة للرحلات.
- تفكيك كل طلب مسجَّل صراحةً: `items_total + delivery_fee + service_fee − discount`، ونصيب المنصة = `commission_percent` من `items_total` فقط (لا من رسوم التوصيل)، ونصيب السائق من رسوم التوصيل.
- رسوم التوصيل تُحسب في **محرك التسعير القائم** [backend/pricing-engine](../../backend/pricing-engine) بمعامل خاص بالطعام، لا بمعادلة جديدة مكرّرة — تكرار منطق التسعير هو المصدر التاريخي لتذبذب الأسعار في هذا المشروع.
- التسعير **مثبّت لحظة عرض السلة**: عرض السعر يُوقَّع ويصلح لمدة 10 دقائق. لا يجوز أن يتغير الإجمالي بين شاشة التأكيد وشاشة الدفع.
---
## ٨. الزمن الحقيقي — سوكيت الطعام
- قنوات الاشتراك: `food:order:{id}` (الزبون)، `food:merchant:{id}` (لوحة المطعم)، `food:courier:{id}` (السائق).
- المصدر الوحيد للحقيقة هو قاعدة البيانات؛ السوكيت **ناقل إشعار لا مخزن حالة**. عند إعادة الاتصال يسحب التطبيق `order/status.php` ويُصحّح نفسه — هذا ما يمنع «الطلب معلّق للأبد» بعد انقطاع شبكة.
- موقع السائق أثناء التوصيل يُبثّ من نفس تدفق المواقع القائم؛ لا مسار تتبّع ثانٍ.
- كل حدث سوكيت **مصحوب بإشعار FCM** لحالات المفصل (قُبل، جاهز، خرج للتوصيل، وصل) — لأن التطبيق في الخلفية لا يملك سوكيتاً حياً.
- تشغيل السوكيت **داخل الحاوية فقط**: لا يُطلق أبداً على المضيف مباشرة (المضيف لا يصل إلى Redis داخل الشبكة، والنتيجة انقطاع صامت). إعادة التشغيل: `docker compose restart socket_food`.
---
## ٩. طبقة التطبيقات
| التطبيق | العمل المطلوب |
|---|---|
| `siro_rider` | تبويب «طعام»: تصفح المطاعم، القائمة، السلة، الدفع، تتبّع الطلب، السجل والتقييم. خلف علم `FOOD_ENABLED` يأتي من إعدادات الخادم — لا نسخة تطبيق جديدة لإطفائه. |
| `siro_driver` | نوع مهمة جديد «توصيل» داخل تدفّق العروض القائم: بطاقة عرض، استلام من المطعم، تسليم، إثبات تسليم. |
| لوحة المطعم | **ويب متجاوب داخل `dashboard/`** لا تطبيق أصلي. صاحب المطعم يعمل على شاشة المحل، والويب يُنشر فوراً بلا دورة متجر. هذا يوفّر أشهر عمل. |
| `siro_admin` | إدارة المطاعم والاعتماد، مراقبة الطلبات، التسويات، إعدادات العمولة والرسوم. |
قاعدة توحيد الواجهة: الطعام يستخدم نفس نظام الألوان والمكوّنات وطبقة الشبكة الحالية في التطبيق. أي مكوّن «مقتبس من تطبيق طعام آخر» بمظهر مختلف يجعل الميزة تبدو ملصقة.
---
## ١٠. خطة التنفيذ على مراحل
كل مرحلة تنتهي بشيء **قابل للتشغيل والاختبار**، لا بكود على الرف.
**المرحلة صفر — الأساس (بلا منطق منتج)**
حاويتان جديدتان في compose، مسار nginx، `Database::get('food')`، `schema_food.sql`، بوابة `connect_app.php` ترد على `ping`. معيار الإنجاز: `docker compose up -d` يرفع ثماني خدمات، ونداء ping يرد 200 من الحوض الجديد (يُتحقق من الحوض عبر `fpm-status-food`).
**المرحلة الأولى — الكتالوج (للقراءة فقط)**
المطاعم والأقسام والأصناف، تصفح وبحث، لوحة الإدارة لإنشاء مطعم. معيار الإنجاز: مطعم حقيقي واحد بقائمة كاملة يظهر في التطبيق.
**المرحلة الثانية — الطلب بلا مال**
السلة، التسعير من الخادم، إنشاء الطلب، آلة الحالة، لوحة المطعم، السوكيت والإشعارات. الدفع نقداً فقط. معيار الإنجاز: طلب حقيقي يمر `pending → delivered` وسجل الحالات مكتمل.
**المرحلة الثالثة — التوصيل**
`geo:couriers:available`، حلقة العروض والقفل، مهام السائق، تتبّع الموقع. معيار الإنجاز: ٢٠ طلباً تجريبياً بلا طلب واحد بسائقين ولا طلب يتيم.
**المرحلة الرابعة — المال**
حجز/التقاط المحفظة، العمولة، تسويات المطاعم، تسوية نقد السائق، تقارير الإدارة. معيار الإنجاز: مطابقة مالية لمئة طلب تجريبي بفرق صفر.
**المرحلة الخامسة — التقسية والإطلاق**
مراجعة أمنية (تركيزها IDOR والتفويض)، اختبار ضغط، سجلات ومؤشرات، إطلاق تدريجي على منطقة واحدة ومطاعم محدودة.
---
## ١١. الاختبار ومعايير القبول
- **اختبار ضغط** بنفس أدوات [stress_test](../../stress_test) وبنفس قاعدة القراءة الصادقة المعتمدة في `docker/README.md`: الرقم المُلتزَم به هو الرقم الذي عبر الاختبار فعلاً، لا أكثر. الهدف الابتدائي: ١٠٠٠ طلب/ساعة بـ p95 < 500ms، مع التحقق أن **زمن استجابة الرحلات لم يتأثر** أثناء الحمل — هذا هو اختبار العزل الحقيقي.
- **حالات حافة إلزامية**: ضغط مزدوج على التأكيد، انقطاع الشبكة بين الحجز والإنشاء، رفض المطعم بعد الدفع، صنف نفد أثناء التحضير، إلغاء الزبون بعد استلام السائق، سائق تعطّل تطبيقه وهو حامل الطلب.
- **اختبارات سلبية للتفويض** لكل نقطة نهاية — تُدمج مع الكود لا بعده.
- **مراقبة**: لوحة تحمل الأربعة أرقام التي تصف صحة الخدمة فعلاً — نسبة قبول المطاعم، زمن التحضير، زمن إيجاد سائق، نسبة الإلغاء ومصدره.
---
## ١٢. النشر والتراجع
- `FOOD_ENABLED=false` يخفي الميزة من التطبيق كلياً بلا نشر جديد — هذا هو مفتاح التراجع الأول والأسرع.
- التراجع الكامل: `docker compose stop php_food socket_food`. الرحلات لا تتأثر إطلاقاً — وهذا بالضبط ما اشتريناه بالعزل.
- لا تُشغَّل ترحيلات مخطط الطعام على قواعد الرحلات؛ `siro_food` منفصلة تماماً ونسخها الاحتياطي منفصل.
- **تنبيه بيئي**: توجد مهمة مجدولة تلتزم وتدفع كل تعديل تلقائياً على `main`. أي عمل على وحدة الطعام يجب أن يجري على فرع مستقل، وإلا وصل كود نصف مكتمل إلى `main` برسالة التزام آلية لا تصف شيئاً.
- تسجيل الأخطاء: تأكد أن `error_log` و `access_log` فعّالان لحاويات الطعام منذ اليوم الأول — الخدمة غير المسجَّلة تبدو سليمة حتى تكذب عليك في أول عطل.
---
## ١٣. المخاطر المفتوحة والقرارات التي تحتاج حسماً
1. **تنازع الأسطول** بين الرحلات والتوصيل في الذروة — مقاسة بمؤشر، والحل يؤجَّل حتى يثبت الرقم لزومه.
2. **مصدر السعر النهائي**: مثبَّت في محرك التسعير القائم؛ أي استثناء يُطلب لاحقاً يجب رفضه.
3. **هوية صاحب المطعم**: جدول `merchant_users` مستقل بدور `merchant`. القرار البديل (توسيع جدول المستخدمين الرئيسي) مرفوض لأنه يخلط نطاقات التفويض في قاعدة الرحلات.
4. **حدّ الاعتماد**: هل تُنشر المطاعم بعد اعتماد إداري يدوي؟ الافتراض في هذه الخطة: **نعم**، اعتماد يدوي إلزامي في الإطلاق الأول — والقرار قابل للمراجعة من صاحب المنتج.
5. **بند مفتوح خارج نطاق هذه الوحدة لكنه يمسّها**: مسائل تعرّض بيانات شخصية موثّقة في [08_security](../08_security) يجب ألا تتكرر في أي نقطة نهاية للطعام — خصوصاً في نقاط تُعيد عناوين وأرقاماً.
---
## ١٤. قائمة تحقق قبل أول دمج
- [ ] `docker compose config` يمرّ، والحاويتان تعملان بحدود ذاكرة معلنة
- [ ] بلوك nginx للطعام **قبل** البلوك العام، ومُتحقق منه بنداء فعلي
- [ ] nginx المضيف يخدم 4040 بـ TLS → 14040
- [ ] `food_server/composer.lock` ملتزم به في git
- [ ] لا استدعاء لـ `Database::get('main')` داخل `backend/food/`
- [ ] كل نقطة تأخذ `order_id` تملك اختبار تفويض سلبياً
- [ ] كل المبالغ أعداد صحيحة، و`client_order_uuid` فريد
- [ ] انتقالات الحالة تمر جميعها عبر `food_transition_status()` فقط
- [ ] العمل على فرع مستقل لا على `main`
+5
View File
@@ -96,6 +96,11 @@
|---|---| |---|---|
| [siro_digital_legacy_report.html](09_reports/siro_digital_legacy_report.html) | ⭐ تقرير الإرث الرقمي الشامل (يشمل مواصلاتي وسير الرحلة والتوتوريال) | | [siro_digital_legacy_report.html](09_reports/siro_digital_legacy_report.html) | ⭐ تقرير الإرث الرقمي الشامل (يشمل مواصلاتي وسير الرحلة والتوتوريال) |
## 10_food_orders — وحدة طلبات الطعام (مقترح)
| الملف | المحتوى |
|---|---|
| [FOOD_ORDERS_PLAN_AR.md](10_food_orders/FOOD_ORDERS_PLAN_AR.md) | خطة إضافة طلبات الطعام: حاويتان جديدتان في نفس compose، قاعدة `siro_food` منفصلة، إعادة استخدام الأسطول والمحفظة، مراحل التنفيذ |
## 99_archive — أرشيف ## 99_archive — أرشيف
| الملف | المحتوى | | الملف | المحتوى |
|---|---| |---|---|
+125
View File
@@ -0,0 +1,125 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Ensure marked and highlight.js are installed
try {
require.resolve('marked');
require.resolve('highlight.js');
} catch (e) {
console.log("Installing marked and highlight.js...");
execSync('npm install marked highlight.js', { stdio: 'inherit' });
}
const { marked } = require('marked');
const hljs = require('highlight.js');
marked.setOptions({
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
langPrefix: 'hljs language-',
gfm: true,
breaks: true
});
const docsDir = __dirname;
const indexFile = path.join(docsDir, 'index.html');
// Read the viewer template
const template = `<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{TITLE}} - Siro Docs</title>
<meta name="theme-color" content="#0f172a">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<link rel="stylesheet" href="../style.css">
<link rel="stylesheet" href="../viewer.css">
</head>
<body>
<div class="bg-animation">
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="glow-orb orb-3"></div>
</div>
<nav class="glass-nav viewer-nav">
<div class="nav-brand">
<a href="../index.html" class="back-link">
<i class="fa-solid fa-arrow-right"></i>
<span>العودة للرئيسية</span>
</a>
</div>
<div class="nav-links">
<span class="doc-path-display">{{TITLE}}</span>
</div>
</nav>
<main class="viewer-container">
<article class="markdown-body glass-panel">
{{CONTENT}}
</article>
</main>
<footer>
<div class="footer-content">
<p>&copy; 2026 Siro Platform.</p>
</div>
</footer>
</body>
</html>`;
function traverseDir(dir) {
const files = fs.readdirSync(dir);
files.forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
traverseDir(fullPath);
} else if (fullPath.endsWith('.md') && !fullPath.includes('node_modules')) {
console.log(`Processing: ${fullPath}`);
const markdown = fs.readFileSync(fullPath, 'utf8');
const htmlContent = marked.parse(markdown);
const title = path.basename(fullPath);
const finalHtml = template
.replace(/{{TITLE}}/g, title)
.replace('{{CONTENT}}', htmlContent);
const htmlPath = fullPath.replace(/\.md$/, '.html');
fs.writeFileSync(htmlPath, finalHtml);
console.log(`Generated: ${htmlPath}`);
}
});
}
// 1. Generate HTML files for all MD files
const dirsToProcess = ['01_overview', '02_journeys_and_tutorials', '03_pricing', '04_features', '05_transit_mawasalati', '06_investors', '07_marketing', '08_security', '10_food_orders'];
dirsToProcess.forEach(dir => {
const fullPath = path.join(docsDir, dir);
if (fs.existsSync(fullPath)) {
traverseDir(fullPath);
}
});
// 2. Update index.html to point to .html files instead of viewer.html?doc=...
let indexHtml = fs.readFileSync(indexFile, 'utf8');
indexHtml = indexHtml.replace(/href="viewer\.html\?doc=([^"]+)\.md"/g, 'href="$1.html"');
fs.writeFileSync(indexFile, indexHtml);
console.log('Updated index.html links to point to static .html files.');
// 3. Remove viewer.html since it's no longer needed
const viewerHtml = path.join(docsDir, 'viewer.html');
if (fs.existsSync(viewerHtml)) {
fs.unlinkSync(viewerHtml);
console.log('Deleted viewer.html');
}
console.log('Build complete!');
File diff suppressed because one or more lines are too long
+30
View File
@@ -0,0 +1,30 @@
import os
import json
docs_dir = os.path.dirname(os.path.abspath(__file__))
dirs_to_process = ['01_overview', '02_journeys_and_tutorials', '03_pricing', '04_features', '05_transit_mawasalati', '06_investors', '07_marketing', '08_security', '10_food_orders']
docs_data = {}
for d in dirs_to_process:
dir_path = os.path.join(docs_dir, d)
if os.path.exists(dir_path):
for root, _, files in os.walk(dir_path):
for file in files:
if file.endswith('.md'):
full_path = os.path.join(root, file)
rel_path = os.path.relpath(full_path, docs_dir)
# Use forward slashes for URLs
rel_path = rel_path.replace('\\', '/')
with open(full_path, 'r', encoding='utf-8') as f:
docs_data[rel_path] = f.read()
# Write to a JS file
output_path = os.path.join(docs_dir, 'docs-data.js')
with open(output_path, 'w', encoding='utf-8') as f:
f.write('const docsData = ')
json.dump(docs_data, f, ensure_ascii=False)
f.write(';\n')
print(f"Successfully generated {output_path} with {len(docs_data)} files.")
+175
View File
@@ -0,0 +1,175 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>منصة Siro - مركز التوثيق الشامل</title>
<!-- SEO & Meta -->
<meta name="description" content="البوابة الرئيسية الشاملة لتوثيق منصة Siro. تحتوي على النظرة العامة، معماريات النظام، دراسات الجدوى، التقارير الفنية، ورحلات المستخدم.">
<meta name="theme-color" content="#0f172a">
<!-- Google Fonts: Cairo for Arabic typography -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Background Animated Orbs -->
<div class="bg-animation">
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="glow-orb orb-3"></div>
</div>
<!-- Navigation / Header (Optional, kept minimal) -->
<nav class="glass-nav">
<div class="nav-brand">
<i class="fa-solid fa-car-side"></i>
<span>Siro Platform</span>
</div>
<div class="nav-links">
<a href="06_investors/SIRO_INVESTOR_DECK.html" target="_blank" class="nav-link"><i class="fa-solid fa-presentation-screen"></i> عرض المستثمرين</a>
</div>
</nav>
<header class="hero">
<div class="hero-content">
<h1 class="hero-title">مركز توثيق منصة <span class="highlight">Siro</span></h1>
<p class="hero-subtitle">المصدر الوحيد لكل وثائق المشروع. استكشف المعمارية، دراسات الجدوى، رحلات المستخدمين، والمزيد في منصة واحدة متكاملة.</p>
<div class="hero-actions">
<a href="09_reports/siro_digital_legacy_report.html" class="btn btn-primary pulse-btn" target="_blank">
<i class="fa-solid fa-bolt"></i>
ابدأ من هنا: التقرير التفاعلي الشامل
</a>
</div>
</div>
</header>
<main class="container">
<div class="cards-grid">
<!-- 01_overview -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-network-wired"></i></div>
<h2 class="card-title">نظرة عامة والمعمارية</h2>
<p class="card-desc">فهم البنية الكاملة: 4 تطبيقات Flutter، باك اند PHP، خوادم Socket، و Redis.</p>
<div class="card-links">
<a href="viewer.html?doc=01_overview/PROJECT_OVERVIEW.md" class="doc-link"><i class="fa-regular fa-file-lines"></i> تعريف المشروع الرئيسي</a>
<a href="viewer.html?doc=01_overview/SYSTEM_ARCHITECTURE.md" class="doc-link"><i class="fa-solid fa-sitemap"></i> المعمارية التقنية الكاملة</a>
<a href="viewer.html?doc=01_overview/siro_comprehensive_report.md" class="doc-link"><i class="fa-solid fa-chart-column"></i> التقرير الشامل الأحدث</a>
<a href="viewer.html?doc=01_overview/API_DEPENDENCY_MATRIX.md" class="doc-link"><i class="fa-solid fa-plug"></i> مصفوفة اعتماديات الـ API</a>
<a href="viewer.html?doc=01_overview/DATABASE_DEPENDENCY_MATRIX.md" class="doc-link"><i class="fa-solid fa-database"></i> مصفوفة جداول قواعد البيانات</a>
</div>
</article>
<!-- 02_journeys_and_tutorials -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-route"></i></div>
<h2 class="card-title">الرحلات والتوتوريال</h2>
<p class="card-desc">سير الرحلة خطوة بخطوة للراكب والسائق، مع خطة التوتوريال الشاملة ومحاكاة الرحلات.</p>
<div class="card-links">
<a href="viewer.html?doc=02_journeys_and_tutorials/TUTORIAL_PLAN_AR.md" class="doc-link highlight-link"><i class="fa-solid fa-star"></i> خطة التوتوريال الكاملة</a>
<a href="viewer.html?doc=02_journeys_and_tutorials/PASSENGER_JOURNEY.md" class="doc-link"><i class="fa-solid fa-user"></i> دورة حياة الراكب</a>
<a href="viewer.html?doc=02_journeys_and_tutorials/DRIVER_JOURNEY.md" class="doc-link"><i class="fa-solid fa-id-card"></i> دورة حياة السائق</a>
<a href="viewer.html?doc=02_journeys_and_tutorials/ride_simulation_report.md" class="doc-link"><i class="fa-solid fa-laptop-code"></i> تقرير محاكاة الراكب</a>
<a href="viewer.html?doc=02_journeys_and_tutorials/driver_ride_simulation_report.md" class="doc-link"><i class="fa-solid fa-car-on"></i> تقرير محاكاة السائق</a>
</div>
</article>
<!-- 03_pricing -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-tags"></i></div>
<h2 class="card-title">محرك التسعير</h2>
<p class="card-desc">معمارية التسعير الذكي، أتمتة مراقبة المنافسين، وخطة حالة الرحلة في Redis.</p>
<div class="card-links">
<a href="viewer.html?doc=03_pricing/PRICING_ENGINE_ARCHITECTURE.md" class="doc-link"><i class="fa-solid fa-cogs"></i> معمارية محرك التسعير</a>
<a href="viewer.html?doc=03_pricing/pricing_system_report.md" class="doc-link"><i class="fa-solid fa-chart-pie"></i> تقرير نظام التسعير</a>
<a href="viewer.html?doc=03_pricing/دراسة_نظام_أتمتة_السوق_الذكي.md" class="doc-link"><i class="fa-solid fa-robot"></i> نظام أتمتة السوق الذكي</a>
<a href="viewer.html?doc=03_pricing/redis_state_plan.md" class="doc-link"><i class="fa-solid fa-memory"></i> خطة حالة Redis</a>
</div>
</article>
<!-- 04_features -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-rocket"></i></div>
<h2 class="card-title">الميزات والتنافسية</h2>
<p class="card-desc">المزايا التقنية الخاصة، والميزات التنافسية مقابل كريم، أوبر، ويلا-جو.</p>
<div class="card-links">
<a href="viewer.html?doc=04_features/COMPETITIVE_FEATURES.md" class="doc-link"><i class="fa-solid fa-medal"></i> الميزات التنافسية</a>
<a href="viewer.html?doc=04_features/TECHNICAL_ADVANTAGES_AR.md" class="doc-link"><i class="fa-solid fa-microchip"></i> المزايا التقنية الداخلية</a>
<a href="viewer.html?doc=04_features/AUDIO_MUSIC_FEATURES_AR.md" class="doc-link"><i class="fa-solid fa-music"></i> ميزات الصوت والموسيقى</a>
<a href="viewer.html?doc=04_features/BATTERY_PERFORMANCE_AR.md" class="doc-link"><i class="fa-solid fa-battery-full"></i> أداء البطارية والتحسينات</a>
<a href="viewer.html?doc=04_features/auto_carplay_checklist_arabic.md" class="doc-link"><i class="fa-brands fa-android"></i> دعم Android Auto / CarPlay</a>
</div>
</article>
<!-- 05_transit_mawasalati -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-bus"></i></div>
<h2 class="card-title">نظام "مواصلاتي"</h2>
<p class="card-desc">نظام باصات الجامعات والنقل الجماعي، سير العمليات الكامل، والنموذج المجاني SaaS.</p>
<div class="card-links">
<a href="viewer.html?doc=05_transit_mawasalati/transit_comprehensive_workflow_report.md" class="doc-link highlight-link"><i class="fa-solid fa-arrows-spin"></i> سير العمليات الكامل (الأحدث)</a>
<a href="viewer.html?doc=05_transit_mawasalati/mawasalati_full_system_review.md" class="doc-link"><i class="fa-solid fa-magnifying-glass-chart"></i> المراجعة الفنية الكاملة</a>
<a href="viewer.html?doc=05_transit_mawasalati/mowasalaty_strategy_report.md" class="doc-link"><i class="fa-solid fa-chess-knight"></i> التقرير الاستراتيجي</a>
</div>
</article>
<!-- 06_investors -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-hand-holding-dollar"></i></div>
<h2 class="card-title">وثائق المستثمرين</h2>
<p class="card-desc">تقارير المستثمرين، عروض Pitch، دراسات الجدوى، وتقديرات كلفة إعادة البناء.</p>
<div class="alert-box">
<i class="fa-solid fa-triangle-exclamation"></i>
تنبيه: يوجد سرديتان تمويليتان مختلفتان يجب توحيدهما قبل أي عرض رسمي.
</div>
<div class="card-links mt-3">
<a href="viewer.html?doc=06_investors/rebuild_cost_estimate_egypt_2026.md" class="doc-link highlight-link"><i class="fa-solid fa-money-bill-trend-up"></i> دراسة كلفة البناء (مصر 2026)</a>
<a href="viewer.html?doc=06_investors/SIRO_INVESTOR_REPORT_AR.md" class="doc-link"><i class="fa-solid fa-file-invoice-dollar"></i> تقرير المستثمر الرئيسي</a>
<a href="06_investors/SIRO_INVESTOR_DECK.html" class="doc-link" target="_blank"><i class="fa-solid fa-desktop"></i> عرض المستثمرين التفاعلي</a>
<a href="06_investors/Siro_Feasibility_Study_v4.pdf" class="doc-link"><i class="fa-solid fa-file-pdf"></i> دراسة الجدوى v4</a>
<a href="viewer.html?doc=06_investors/Siro_Egypt_Competitive_Study_AR.md" class="doc-link"><i class="fa-solid fa-earth-africa"></i> دراسة سوق مصر التنافسية</a>
</div>
</article>
<!-- 07_marketing -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-bullhorn"></i></div>
<h2 class="card-title">التسويق والنمو</h2>
<p class="card-desc">استراتيجيات التسويق والنمو، خطط التكامل، وخرائط توزيع المناطق.</p>
<div class="card-links">
<a href="viewer.html?doc=07_marketing/SIRO_MARKETING_REPORT_AR.md" class="doc-link"><i class="fa-solid fa-chart-line"></i> تقرير التسويق الرئيسي</a>
<a href="viewer.html?doc=07_marketing/marketing_and_growth_strategy.md" class="doc-link"><i class="fa-solid fa-chess-board"></i> استراتيجية التسويق والنمو</a>
<a href="07_marketing/damascus_zones_map.html" class="doc-link" target="_blank"><i class="fa-solid fa-map-location-dot"></i> خريطة مناطق دمشق (تفاعلي)</a>
</div>
</article>
<!-- 08_security & 10_food_orders -->
<article class="glass-card">
<div class="card-icon"><i class="fa-solid fa-shield-halved"></i></div>
<h2 class="card-title">الأمن والميزات المستقبلية</h2>
<p class="card-desc">مراجعات الأمان، تدقيق الكود، ومقترحات لوحدات وتوسعات جديدة (كطلبات الطعام).</p>
<div class="card-links">
<a href="viewer.html?doc=08_security/التقرير_الأمني_Backend_Siro.md" class="doc-link"><i class="fa-solid fa-lock"></i> التدقيق الأمني (Backend)</a>
<a href="viewer.html?doc=08_security/CODE_REVIEW_REPORT_AR.md" class="doc-link"><i class="fa-solid fa-code-commit"></i> مراجعة الكود الشاملة</a>
<a href="viewer.html?doc=10_food_orders/FOOD_ORDERS_PLAN_AR.md" class="doc-link"><i class="fa-solid fa-burger"></i> مقترح خطة طلبات الطعام</a>
</div>
</article>
</div>
</main>
<footer>
<div class="footer-content">
<p>&copy; 2026 Siro Platform. هذا الموقع يعتبر المدخل الرئيسي والإرث الرقمي للمشروع.</p>
</div>
</footer>
</body>
</html>
+370
View File
@@ -0,0 +1,370 @@
/* --- Base & Variables --- */
:root {
--bg-color: #0b0f19;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--accent-primary: #3b82f6;
--accent-secondary: #8b5cf6;
--accent-gradient: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
--glass-bg: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-hover: rgba(255, 255, 255, 0.08);
--transition-smooth: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Cairo', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
line-height: 1.6;
overflow-x: hidden;
position: relative;
min-height: 100vh;
}
/* --- Animated Background Orbs --- */
.bg-animation {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
overflow: hidden;
}
.glow-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.5;
animation: float 20s infinite ease-in-out;
}
.orb-1 {
width: 400px;
height: 400px;
background: rgba(59, 130, 246, 0.3);
top: -100px;
right: -100px;
animation-delay: 0s;
}
.orb-2 {
width: 350px;
height: 350px;
background: rgba(139, 92, 246, 0.3);
bottom: -50px;
left: -100px;
animation-delay: -5s;
}
.orb-3 {
width: 300px;
height: 300px;
background: rgba(6, 182, 212, 0.2);
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
animation-delay: -10s;
}
@keyframes float {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
}
/* --- Navigation --- */
.glass-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 5%;
background: rgba(11, 15, 25, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--glass-border);
position: sticky;
top: 0;
z-index: 100;
}
.nav-brand {
font-size: 1.5rem;
font-weight: 700;
display: flex;
align-items: center;
gap: 0.75rem;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.nav-brand i {
color: var(--accent-primary);
-webkit-text-fill-color: initial;
}
.nav-link {
color: var(--text-primary);
text-decoration: none;
font-weight: 500;
transition: var(--transition-smooth);
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-link:hover {
color: var(--accent-secondary);
}
/* --- Hero Section --- */
.hero {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 1.5rem;
}
.hero-content {
max-width: 800px;
animation: fadeUp 1s ease-out;
}
.hero-title {
font-size: clamp(2.5rem, 5vw, 4rem);
font-weight: 800;
margin-bottom: 1.5rem;
line-height: 1.2;
}
.highlight {
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2.5rem;
font-weight: 400;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 2rem;
border-radius: 50px;
font-size: 1.125rem;
font-weight: 600;
text-decoration: none;
transition: var(--transition-smooth);
cursor: pointer;
border: none;
}
.btn-primary {
background: var(--accent-gradient);
color: white;
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
}
.btn-primary:hover {
transform: translateY(-3px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.6);
}
.pulse-btn {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); }
70% { box-shadow: 0 0 0 15px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- Container & Grid --- */
.container {
max-width: 1400px;
margin: 0 auto;
padding: 0 2rem 5rem;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
}
/* --- Glassmorphism Cards --- */
.glass-card {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 2rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
transition: var(--transition-smooth);
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.glass-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle at top right, rgba(255,255,255,0.05), transparent 60%);
pointer-events: none;
}
.glass-card:hover {
transform: translateY(-10px);
background: var(--glass-hover);
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.card-icon {
font-size: 2.5rem;
margin-bottom: 1.5rem;
color: var(--accent-primary);
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
.card-title {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1rem;
}
.card-desc {
color: var(--text-secondary);
margin-bottom: 2rem;
font-size: 0.95rem;
flex-grow: 1;
}
.card-links {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.doc-link {
color: var(--text-primary);
text-decoration: none;
font-size: 0.95rem;
padding: 0.75rem 1rem;
border-radius: 12px;
background: rgba(255, 255, 255, 0.02);
border: 1px solid transparent;
display: flex;
align-items: center;
gap: 0.75rem;
transition: var(--transition-smooth);
}
.doc-link i {
color: var(--text-secondary);
font-size: 1.1rem;
width: 20px;
text-align: center;
transition: var(--transition-smooth);
}
.doc-link:hover {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.3);
color: var(--accent-primary);
}
.doc-link:hover i {
color: var(--accent-primary);
}
.highlight-link {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.3);
}
.highlight-link:hover {
background: rgba(139, 92, 246, 0.2);
border-color: var(--accent-secondary);
color: var(--accent-secondary);
}
.highlight-link i {
color: var(--accent-secondary);
}
.highlight-link:hover i {
color: var(--accent-secondary);
}
/* --- Alerts --- */
.alert-box {
background: rgba(245, 158, 11, 0.1);
border-right: 4px solid #f59e0b; /* RTL border right instead of left */
border-radius: 8px;
padding: 1rem;
font-size: 0.85rem;
color: #fcd34d;
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
}
.mt-3 {
margin-top: 1rem;
}
/* --- Footer --- */
footer {
text-align: center;
padding: 2rem;
border-top: 1px solid var(--glass-border);
color: var(--text-secondary);
font-size: 0.9rem;
background: rgba(11, 15, 25, 0.8);
}
/* --- Responsive --- */
@media (max-width: 768px) {
.cards-grid {
grid-template-columns: 1fr;
}
.hero {
padding: 3rem 1.5rem;
min-height: auto;
}
}
+214
View File
@@ -0,0 +1,214 @@
/* --- Viewer Specific Styles --- */
.viewer-nav {
padding: 1rem 5%;
}
.back-link {
color: var(--text-primary);
text-decoration: none;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
transition: var(--transition-smooth);
}
.back-link:hover {
color: var(--accent-primary);
transform: translateX(5px); /* Move right slightly on hover for RTL */
}
.doc-path-display {
color: var(--text-secondary);
font-size: 0.9rem;
background: rgba(255, 255, 255, 0.05);
padding: 0.4rem 1rem;
border-radius: 20px;
border: 1px solid var(--glass-border);
}
.viewer-container {
max-width: 900px;
margin: 3rem auto 5rem;
padding: 0 1.5rem;
}
.glass-panel {
background: rgba(11, 15, 25, 0.6);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 3rem;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
.spinner-container, .error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 40vh;
text-align: center;
gap: 1rem;
background: var(--glass-bg);
border-radius: 24px;
padding: 3rem;
border: 1px solid var(--glass-border);
}
.spinner-container i {
font-size: 3rem;
color: var(--accent-primary);
}
.error-container i {
font-size: 4rem;
color: #ef4444;
}
/* --- Markdown Body Styling --- */
.markdown-body {
color: var(--text-primary);
font-family: 'Cairo', sans-serif;
font-size: 1.1rem;
line-height: 1.8;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4 {
margin-top: 2rem;
margin-bottom: 1rem;
font-weight: 700;
color: #ffffff;
}
.markdown-body h1 {
font-size: 2.5rem;
border-bottom: 2px solid var(--glass-border);
padding-bottom: 0.5rem;
color: var(--accent-primary);
}
.markdown-body h2 {
font-size: 1.8rem;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 0.3rem;
}
.markdown-body h3 { font-size: 1.4rem; }
.markdown-body p {
margin-bottom: 1.5rem;
}
.markdown-body a {
color: var(--accent-primary);
text-decoration: none;
transition: var(--transition-smooth);
}
.markdown-body a:hover {
color: var(--accent-secondary);
text-decoration: underline;
}
.markdown-body strong {
color: #ffffff;
}
.markdown-body ul, .markdown-body ol {
margin-bottom: 1.5rem;
padding-right: 2rem; /* RTL padding */
}
.markdown-body li {
margin-bottom: 0.5rem;
}
.markdown-body blockquote {
border-right: 4px solid var(--accent-primary); /* RTL border */
border-left: none;
margin: 1.5rem 0;
padding: 1rem 1.5rem;
background: rgba(59, 130, 246, 0.05);
color: var(--text-secondary);
border-radius: 8px 0 0 8px;
}
.markdown-body code {
font-family: monospace;
background: rgba(255, 255, 255, 0.1);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-size: 0.9em;
color: #e2e8f0;
}
.markdown-body pre {
background: #1e1e1e;
padding: 1.5rem;
border-radius: 12px;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--glass-border);
direction: ltr; /* Always LTR for code blocks */
text-align: left;
}
.markdown-body pre code {
background: none;
padding: 0;
color: inherit;
font-size: 0.95em;
}
/* Tables */
.markdown-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
background: rgba(255, 255, 255, 0.02);
border-radius: 12px;
overflow: hidden;
}
.markdown-body th, .markdown-body td {
padding: 1rem;
border: 1px solid var(--glass-border);
text-align: right;
}
.markdown-body th {
background: rgba(255, 255, 255, 0.05);
font-weight: 600;
}
.markdown-body tr:nth-child(even) {
background: rgba(255, 255, 255, 0.01);
}
.markdown-body img {
max-width: 100%;
border-radius: 12px;
margin: 1.5rem 0;
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
}
/* Responsive viewer */
@media (max-width: 768px) {
.glass-panel {
padding: 1.5rem;
}
.markdown-body h1 {
font-size: 2rem;
}
.markdown-body h2 {
font-size: 1.5rem;
}
}
+135
View File
@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Siro Documentation Viewer</title>
<!-- SEO & Meta -->
<meta name="theme-color" content="#0f172a">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- highlight.js for code syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
<!-- Custom CSS (imports base styles from style.css + viewer specific styles) -->
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="viewer.css">
</head>
<body>
<div class="bg-animation">
<div class="glow-orb orb-1"></div>
<div class="glow-orb orb-2"></div>
<div class="glow-orb orb-3"></div>
</div>
<!-- Toolbar Navigation -->
<nav class="glass-nav viewer-nav">
<div class="nav-brand">
<a href="index.html" class="back-link">
<i class="fa-solid fa-arrow-right"></i> <!-- arrow right for RTL -->
<span>العودة للرئيسية</span>
</a>
</div>
<div class="nav-links">
<span id="doc-title-display" class="doc-path-display">جاري تحميل المستند...</span>
</div>
</nav>
<main class="viewer-container">
<div id="loading-spinner" class="spinner-container">
<i class="fa-solid fa-circle-notch fa-spin"></i>
<p>جاري جلب المستند...</p>
</div>
<div id="error-message" class="error-container" style="display: none;">
<i class="fa-solid fa-circle-exclamation"></i>
<h2>عذراً، لم نتمكن من جلب الملف</h2>
<p>تأكد من أنك تتصفح الموقع عبر سيرفر محلي (Live Server) وليس كملف مباشر (file://) بسبب حماية المتصفحات، أو تأكد من صحة الرابط.</p>
<a href="index.html" class="btn btn-primary mt-3">العودة للرئيسية</a>
</div>
<article id="markdown-content" class="markdown-body glass-panel" style="display: none;">
<!-- Content will be injected here -->
</article>
</main>
<footer>
<div class="footer-content">
<p>&copy; 2026 Siro Platform.</p>
</div>
</footer>
<!-- Libraries -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<!-- Load static markdown data to bypass file:// CORS -->
<script src="docs-data.js"></script>
<!-- Viewer Script -->
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get ?doc= parameter
const urlParams = new URLSearchParams(window.location.search);
const docPath = urlParams.get('doc');
const loadingEl = document.getElementById('loading-spinner');
const errorEl = document.getElementById('error-message');
const contentEl = document.getElementById('markdown-content');
const titleEl = document.getElementById('doc-title-display');
if (!docPath) {
loadingEl.style.display = 'none';
errorEl.style.display = 'flex';
errorEl.querySelector('p').innerText = "لم يتم تحديد أي ملف لعرضه.";
return;
}
titleEl.innerText = docPath.split('/').pop();
document.title = `${docPath.split('/').pop()} - Siro Docs`;
// Configure marked with highlight.js
marked.setOptions({
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
langPrefix: 'hljs language-',
gfm: true,
breaks: true
});
try {
// Get the markdown text from the static JS object
const markdownText = docsData[docPath];
if (!markdownText) {
throw new Error(`Document not found in docsData: ${docPath}`);
}
// Parse markdown
const htmlContent = marked.parse(markdownText);
// Inject content
contentEl.innerHTML = htmlContent;
// Show content
loadingEl.style.display = 'none';
contentEl.style.display = 'block';
} catch (error) {
console.error("Error parsing document:", error);
loadingEl.style.display = 'none';
errorEl.style.display = 'flex';
}
});
</script>
</body>
</html>
@@ -36,6 +36,8 @@
<queries> <queries>
<package android:name="com.whatsapp" /> <package android:name="com.whatsapp" />
<package android:name="com.google.android.apps.maps" /> <package android:name="com.google.android.apps.maps" />
<!-- لفحص توفّر خدمات جوجل (أجهزة هواوي بلا GMS) -->
<package android:name="com.google.android.gms" />
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -20,8 +20,21 @@ import kotlin.concurrent.schedule
class MainActivity : FlutterFragmentActivity() { class MainActivity : FlutterFragmentActivity() {
private val SECURITY_CHANNEL = "com.siro.siro_driver/security" private val SECURITY_CHANNEL = "com.siro.siro_driver/security"
private val APP_CONTROL_CHANNEL = "com.siro.siro_driver/app_control" private val APP_CONTROL_CHANNEL = "com.siro.siro_driver/app_control"
private val DEVICE_SERVICES_CHANNEL = "com.siro.siro_driver/device_services"
private var appControlChannel: MethodChannel? = null private var appControlChannel: MethodChannel? = null
/**
* هل خدمات جوجل مثبّتة ومفعّلة؟ يتطلب إدخال <queries> في المانيفست
* حتى تكون الحزمة مرئية على أندرويد 11+.
*/
private fun isGmsAvailable(): Boolean =
try {
val info = packageManager.getApplicationInfo("com.google.android.gms", 0)
info.enabled
} catch (e: Exception) {
false
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
@@ -37,6 +50,20 @@ class MainActivity : FlutterFragmentActivity() {
} }
} }
// Channel for device service availability (GMS vs Huawei/AOSP).
// نفحص الحزمة مباشرة بدل GoogleApiAvailability لأن الأخيرة قد لا
// تكون على الـ classpath أصلاً على أجهزة بلا خدمات جوجل.
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
DEVICE_SERVICES_CHANNEL
)
.setMethodCallHandler { call, result ->
when (call.method) {
"isGmsAvailable" -> result.success(isGmsAvailable())
else -> result.notImplemented()
}
}
// Channel for app control (bringing to foreground) // Channel for app control (bringing to foreground)
appControlChannel?.setMethodCallHandler { call, result -> appControlChannel?.setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
+4
View File
@@ -129,4 +129,8 @@ class BoxName {
static const String isBusMode = 'isBusMode'; static const String isBusMode = 'isBusMode';
static const String busModeTripId = 'busModeTripId'; static const String busModeTripId = 'busModeTripId';
static const String busModeRouteId = 'busModeRouteId'; static const String busModeRouteId = 'busModeRouteId';
// توفّر Google Play Services — يحدّد شراسة ملف تتبّع الموقع
// (بلا GMS يسقط الباكيج إلى LocationManager الخام: بطارية أعلى، دقة أقل)
static const String isGmsAvailable = 'isGmsAvailable';
} }
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:geolocator/geolocator.dart' as geo; import 'package:geolocator/geolocator.dart' as geo;
import 'package:intaleq_maps/intaleq_maps.dart'; import 'package:intaleq_maps/intaleq_maps.dart';
@@ -25,6 +26,37 @@ import 'background_service.dart';
import 'crud.dart'; import 'crud.dart';
import '../transit/transit_driver_controller.dart'; import '../transit/transit_driver_controller.dart';
/// إعدادات تتبّع الموقع لحالة معيّنة من حالات السائق.
/// الهدف: عدم تشغيل الـ GPS بأقصى طاقته إلا أثناء الرحلة الفعلية.
class _LocationProfile {
final String name;
final LocationAccuracy accuracy;
final int interval; // ميلي ثانية
final double distanceFilter; // متر
const _LocationProfile(
this.name, this.accuracy, this.interval, this.distanceFilter);
_LocationProfile copyWith(
{String? name,
LocationAccuracy? accuracy,
int? interval,
double? distanceFilter}) =>
_LocationProfile(
name ?? this.name,
accuracy ?? this.accuracy,
interval ?? this.interval,
distanceFilter ?? this.distanceFilter,
);
/// بصمة تُستخدم لتفادي استدعاء changeSettings بلا داعٍ.
String get signature => '$accuracy|$interval|$distanceFilter';
@override
String toString() =>
'$name(acc: ${accuracy.name}, ${interval}ms, ${distanceFilter}m)';
}
class LocationController extends GetxController with WidgetsBindingObserver { class LocationController extends GetxController with WidgetsBindingObserver {
// =================================================================== // ===================================================================
// ====== Tunables ====== // ====== Tunables ======
@@ -38,6 +70,29 @@ class LocationController extends GetxController with WidgetsBindingObserver {
static const int powerSaveTriggerLevel = 20; static const int powerSaveTriggerLevel = 20;
static const int powerSaveExitLevel = 25; static const int powerSaveExitLevel = 25;
// ===================================================================
// ====== ملفات التتبّع التكيّفية (أ) ======
// ===================================================================
/// السائق غير متاح (off/blocked): أرخص إعداد ممكن مع إبقاء آخر موقع معروف.
static const _profileIdle =
_LocationProfile('idle', LocationAccuracy.balanced, 30000, 100);
/// متصل وينتظر طلباً: لا حاجة لدقة الملاحة — يكفي أن يعرفه السيرفر بالحي.
static const _profileWaiting =
_LocationProfile('waiting', LocationAccuracy.high, 15000, 50);
/// قَبِل الطلب وفي طريقه للراكب: الراكب يراقب السهم، نحتاج تحديثاً معقولاً.
static const _profileEnRoute =
_LocationProfile('enRoute', LocationAccuracy.high, 5000, 15);
/// رحلة جارية (أو وضع الباص): أعلى دقة — هنا تُحتسب المسافة والأجرة.
static const _profileOnTrip =
_LocationProfile('onTrip', LocationAccuracy.navigation, 4000, 10);
/// بعد هذه المدة من السكون نضاعف الفترة (سائق واقف في مرآب/إشارة طويلة).
static const Duration stationaryGrace = Duration(seconds: 90);
static const double stationarySpeedThreshold = 1.0; // م/ث ≈ 3.6 كم/س
// =================================================================== // ===================================================================
// ====== Services & Variables ====== // ====== Services & Variables ======
// =================================================================== // ===================================================================
@@ -84,6 +139,17 @@ class LocationController extends GetxController with WidgetsBindingObserver {
bool _isReady = false; bool _isReady = false;
bool _isPowerSavingMode = false; bool _isPowerSavingMode = false;
/// هل خدمات جوجل متوفرة على الجهاز؟ (ج)
/// null = لم يُفحص بعد. على أجهزة هواوي بلا GMS يسقط الباكيج إلى
/// LocationManager الخام: دقة أقل واستهلاك بطارية أعلى ⇒ نخفّف الإعدادات.
bool? _gmsAvailable;
static const MethodChannel _deviceServicesChannel =
MethodChannel('com.siro.siro_driver/device_services');
/// آخر إعداد طُبّق فعلياً — لتفادي استدعاء changeSettings بلا تغيير.
String? _appliedProfileSignature;
DateTime? _stationarySince;
final List<Map<String, dynamic>> _trackBuffer = []; final List<Map<String, dynamic>> _trackBuffer = [];
final List<Map<String, dynamic>> _behaviorBuffer = []; final List<Map<String, dynamic>> _behaviorBuffer = [];
@@ -130,6 +196,7 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_isReady = true; _isReady = true;
initSocket(); initSocket();
await _detectGmsAvailability();
await _initLocationSettings(); await _initLocationSettings();
_listenToBatteryChanges(); _listenToBatteryChanges();
@@ -555,13 +622,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
Future<void> _subscribeLocationStream() async { Future<void> _subscribeLocationStream() async {
_locSub?.cancel(); _locSub?.cancel();
int interval = _isPowerSavingMode ? 10000 : 5000;
await location.enableBackgroundMode(enable: true); await location.enableBackgroundMode(enable: true);
location.changeSettings( // مصدر الحقيقة الوحيد لإعدادات الموقع: _applyProfile.
accuracy: LocationAccuracy.navigation, await _applyProfile(force: true);
interval: interval,
distanceFilter: _isPowerSavingMode ? 20 : 10,
);
_locSub = location.onLocationChanged.listen((LocationData loc) async { _locSub = location.onLocationChanged.listen((LocationData loc) async {
if (loc.latitude == null || loc.longitude == null) return; if (loc.latitude == null || loc.longitude == null) return;
@@ -577,6 +640,11 @@ class LocationController extends GetxController with WidgetsBindingObserver {
speed = loc.speed ?? 0.0; speed = loc.speed ?? 0.0;
heading = loc.heading ?? 0.0; heading = loc.heading ?? 0.0;
// إعادة تقييم ملف التتبّع مع كل قراءة — لا يستدعي changeSettings
// إلا إذا تغيّرت الحالة فعلاً (حالة السائق/الرحلة/السكون/البطارية).
_trackStationary();
await _applyProfile();
box.write('last_lat', pos.latitude); box.write('last_lat', pos.latitude);
box.write('last_lng', pos.longitude); box.write('last_lng', pos.longitude);
box.write('last_heading', heading); box.write('last_heading', heading);
@@ -635,6 +703,8 @@ class LocationController extends GetxController with WidgetsBindingObserver {
_locSub?.cancel(); _locSub?.cancel();
_locSub = null; _locSub = null;
_appliedProfileSignature = null;
_stationarySince = null;
_recordTimer?.cancel(); _recordTimer?.cancel();
_uploadBatchTimer?.cancel(); _uploadBatchTimer?.cancel();
_socketHeartbeat?.cancel(); _socketHeartbeat?.cancel();
@@ -746,24 +816,123 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (level >= powerSaveExitLevel) _isPowerSavingMode = false; if (level >= powerSaveExitLevel) _isPowerSavingMode = false;
if (previousMode != _isPowerSavingMode) { if (previousMode != _isPowerSavingMode) {
_startBatchTimers(); _startBatchTimers();
_updateLocationSettings(); _applyProfile();
} }
}); });
} }
Future<void> _updateLocationSettings() async { // ===================================================================
if (_locSub == null) return; // ====== محرّك الملفات التكيّفية (أ + ج) ======
int interval = _isPowerSavingMode ? 10000 : 5000; // ===================================================================
/// (ج) فحص توفّر Google Play Services مرة واحدة عند الإقلاع.
/// النتيجة تُحفظ في الصندوق ليقرأها الـ background isolate أيضاً،
/// وتُبَث مع بيانات السائق لمعرفة حجم شريحة الأجهزة بلا GMS.
Future<void> _detectGmsAvailability() async {
if (!Platform.isAndroid) {
_gmsAvailable = true; // iOS: CoreLocation دائماً متاح
return;
}
try {
final bool available =
await _deviceServicesChannel.invokeMethod<bool>('isGmsAvailable') ??
true;
_gmsAvailable = available;
box.write(BoxName.isGmsAvailable, available);
Log.print(available
? "✅ GMS available — FusedLocationProvider in use."
: "⚠️ No GMS (Huawei/AOSP) — falling back to raw LocationManager, "
"relaxing location profile to protect battery.");
} catch (e) {
// القناة غير مسجّلة (نسخة قديمة) — نفترض التوفّر ولا نغيّر السلوك.
_gmsAvailable = true;
Log.print("⚠️ GMS check failed, assuming available: $e");
}
}
/// تتبّع السكون: سائق واقف لا يحتاج قراءات متلاحقة.
void _trackStationary() {
if (speed >= stationarySpeedThreshold) {
_stationarySince = null;
} else {
_stationarySince ??= DateTime.now();
}
}
bool get _isStationary =>
_stationarySince != null &&
DateTime.now().difference(_stationarySince!) >= stationaryGrace;
/// يختار الملف المناسب من حالة السائق والرحلة، ثم يطبّق عليه
/// مُعدِّلات البطارية والسكون وغياب GMS.
_LocationProfile _resolveProfile() {
final String driverStatus =
box.read(BoxName.statusDriverLocation) ?? 'off';
final String rideStatus = (box.read(BoxName.rideStatus) ?? '').toString();
_LocationProfile p;
if (isBusMode) {
p = _profileOnTrip;
} else if (driverStatus == 'off' || driverStatus == 'blocked') {
p = _profileIdle;
} else if (rideStatus == 'Begin') {
p = _profileOnTrip;
} else if (rideStatus == 'Apply' || rideStatus == 'Arrived') {
p = _profileEnRoute;
} else {
p = _profileWaiting;
}
// وضع توفير الطاقة: ضاعف الفترة والمسافة، وانزل عن دقة الملاحة.
if (_isPowerSavingMode) {
p = p.copyWith(
name: '${p.name}+save',
accuracy: p.accuracy == LocationAccuracy.navigation
? LocationAccuracy.high
: p.accuracy,
interval: p.interval * 2,
distanceFilter: p.distanceFilter * 2,
);
}
// (ج) بلا GMS: كل قراءة تُشعل شريحة الـ GPS منفردة ⇒ خفّف التردد.
if (_gmsAvailable == false) {
p = p.copyWith(
name: '${p.name}+nogms',
interval: (p.interval * 1.5).round(),
distanceFilter: p.distanceFilter * 2,
);
}
// السكون المطوّل: ضاعف الفترة ثلاثاً — إلا في رحلة جارية (عدّاد الانتظار).
if (_isStationary && p.accuracy != LocationAccuracy.navigation) {
p = p.copyWith(
name: '${p.name}+idle',
interval: p.interval * 3,
);
}
return p;
}
/// يطبّق الملف الحالي. لا يستدعي changeSettings إلا عند تغيّر فعلي،
/// لأن كل استدعاء يعيد تشغيل مزوّد الموقع في الطبقة الأصلية.
Future<void> _applyProfile({bool force = false}) async {
if (_locSub == null && !force) return;
final p = _resolveProfile();
if (!force && p.signature == _appliedProfileSignature) return;
try { try {
await location.changeSettings( await location.changeSettings(
accuracy: LocationAccuracy.navigation, accuracy: p.accuracy,
interval: interval, interval: p.interval,
distanceFilter: _isPowerSavingMode ? 20 : 10, distanceFilter: p.distanceFilter,
); );
Log.print( _appliedProfileSignature = p.signature;
"🔋 Location settings updated. Power Save: $_isPowerSavingMode"); Log.print("📍 Location profile → $p");
} catch (e) { } catch (e) {
Log.print("❌ Failed to update location settings: $e"); Log.print("❌ Failed to apply location profile $p: $e");
} }
} }
@@ -832,10 +1001,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
if (await _ensureServiceAndPermission()) { if (await _ensureServiceAndPermission()) {
try { try {
await location.enableBackgroundMode(enable: true); await location.enableBackgroundMode(enable: true);
location.changeSettings( // (ب) لا نضبط الإعدادات هنا: كانت interval: 1000 تتعارض مع إعدادات
accuracy: LocationAccuracy.navigation, // _subscribeLocationStream وتُبقي الـ GPS مشتعلاً كل ثانية.
interval: 1000, // مصدر الحقيقة الوحيد الآن هو _applyProfile.
distanceFilter: 10);
} catch (e) { } catch (e) {
Log.print("Warning: $e"); Log.print("Warning: $e");
} }