Update: 2026-07-31 03:00:06
This commit is contained in:
@@ -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`
|
||||||
@@ -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
@@ -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>© 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
@@ -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
@@ -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>© 2026 Siro Platform. هذا الموقع يعتبر المدخل الرئيسي والإرث الرقمي للمشروع.</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+370
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>© 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) {
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user