Zezo Smart Transit Platform (ZSTP)
تحليل شامل وتحويل الفكرة إلى منصة Production
أولًا: تحليل المحتوى الكامل بدقة
1. طبيعة الملف
الملف ليس وثيقة تقنية نهائية، بل محادثة تخطيطية تدريجية على منصة “Zezo Ai Clude” تحوّلت من فكرة بسيطة إلى تصور منظومي متكامل. مرّ المحتوى بالمراحل التالية:
| المرحلة | المحتوى |
|---|---|
| انطلاق الفكرة | مشكلة انتظار المواطن دون معرفة موعد وصول الحافلة |
| تصور أولي | تطبيق موبايل: خريطة + GPS + ETA + تنبيهات |
| توسّع | منظومة متكاملة: مواطن + محطة + إدارة + حافلة |
| سياق جغرافي | حافلات جازان (JAZAN BUS) كنموذج |
| تحديد الفجوة | المواطن لا يعرف الوجهة المحلية ولا موعد الوصول/المغادرة ولا أي حافلة تذهب لمدينة أخرى |
| الحل | ثلاث واجهات + ETA + تكامل مع أنظمة قائمة |
| المنهجية | تجربة محدودة ثم توسع |
| صياغة رسمية | مقترح للإدارة |
2. المشكلة الجوهرية (كما كشفها الملف بدقة)
المشكلة ليست “نقل” فقط، بل فجوة معلوماتية في نقطة اتخاذ القرار:
| البُعد | المشكلة |
|---|---|
| المعلوماتي | لا يعرف المواطن وجهة الحافلة قبل ركوبها |
| الزمني | لا يعرف موعد الوصول ولا المغادرة |
| المكاني | لا يعرف موقع الحافلة الحالي |
| التخطيطي | لا يعرف أي حافلة تذهب لمدينة أخرى |
| التشغيلي | الشاشات الداخلية لا تخدم المنتظر في المحطة |
| الثقة | قد تُعرض أوقات غير مؤكدة دون توضيح |
3. خريطة الفجوات المعلوماتية (المفتاح التحليلي)
| البيانات | الإدارة | داخل الحافلة | المواطن في المحطة |
|---|---|---|---|
| رقم الخط | غالبًا نعم | نعم | ❌ |
| الوجهة النهائية | غالبًا نعم | جزئيًا | ❌ |
| موقع الحافلة اللحظي | غير مؤكد | لا | ❌ |
| وقت الوصول المتوقع | غير مؤكد | جزئيًا | ❌ |
| موعد المغادرة (بين المدن) | غالبًا نعم | لا | ❌ |
| المحطات الوسيطة | غالبًا نعم | جزئيًا | ❌ |
| حالة الرحلة | غير مؤكد | جزئيًا | ❌ |
الاستنتاج: المشكلة ليست غياب البيانات كليًا، بل غياب قناة عرض موجهة للمواطن. الحل قد لا يكون بناء نظام من الصفر، بل طبقة عرض (Presentation Layer) فوق بيانات قائمة.
4. إجابات المستخدم الحاسمة في المحادثة
- الحافلات لديها شاشات داخلية تعرض المحطة والخط وحالة الوصول.
- لكن المواطن لا يعرف الوجهات المحلية، موعد الوصول/المغادرة، ولا أي باص يذهب لمدينة أخرى.
- إجابته عن نظام التتبع: “1 و3 و4” = يشك في وجود تتبع + يعرف بوجود نظام متابعة مركزي لكن لا يصل للمواطن + لا يملك تأكيدًا تقنيًا.
- قرار حاسم: لا نفترض وجود التتبع أو غيابه — التحقق أول خطوة.
ثانيًا: إعادة كتابة الحل كمنصة Production
سأحوّل التصور إلى معمارية إنتاجية قابلة للتنفيذ بمستوى هندسي حقيقي، مع فصل ما هو مؤكد عمّا هو افتراض يجب التحقق منه.
🏗️ اسم المنصة
Zezo Smart Transit Platform (ZSTP)
1. المبادئ التصميمية الحاكمة (Non-Negotiables)
| المبدأ | التطبيق الهندسي |
|---|---|
| الصدق في البيانات | لا عرض ETA لحظي دون تتبع فعلي — ثلاث حالات صريحة (🟢🟡🔴) |
| العدالة الرقمية | شاشة محطة + QR لمن لا يملك هاتفًا ذكيًا |
| عدم افتراض الشراء | Discovery phase قبل أي اقتناء أجهزة |
| التدرج | خط واحد → قياس → توسع |
| البساطة للمواطن | سؤال واحد: “إلى أين؟” — بحث بالوجهة لا برقم الخط |
2. المعمارية التقنية (Production Architecture)
text
┌─────────────────────────────────────────────────────────────┐
│ طبقة العرض (Presentation) │
├──────────────┬──────────────┬──────────────┬────────────────┤
│ تطبيق المواطن │ شاشة المحطة │ QR Web Page │ لوحة الإدارة │
│ (PWA/Flutter) │ (Kiosk Web) │ (Responsive) │ (React Admin) │
└──────┬───────┴──────┬───────┴──────┬───────┴───────┬────────┘
│ │ │ │
└──────────────┴──────┬───────┴───────────────┘
│ HTTPS / WebSocket
┌────────▼────────┐
│ API Gateway │
│ (Rate Limit, │
│ Auth, Cache) │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼──────┐ ┌─────────▼────────┐ ┌───────▼────────┐
│ Tracking Svc │ │ ETA Engine │ │ Trip/Route │
│ (Ingest GPS) │ │ (Prediction) │ │ Service │
└───────┬──────┘ └─────────┬────────┘ └───────┬────────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌────────▼────────┐
│ Message Bus │
│ (Kafka/Redis) │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼──────┐ ┌─────────▼────────┐ ┌───────▼────────┐
│ Time-Series │ │ Relational DB │ │ Cache Layer │
│ DB (GPS) │ │ (Routes/Stops) │ │ (Redis) │
│ TimescaleDB │ │ PostgreSQL │ │ │
└──────────────┘ └──────────────────┘ └────────────────┘
المكونات الأساسية:
أ. Tracking Ingestion Service
- يستقبل بيانات GPS كل 5–10 ثوانٍ (MQTT/HTTP).
- يتحقق من صحة البيانات (GPS drift, timestamp sanity).
- يخزن في TimescaleDB للتاريخ + Redis للحالة اللحظية.
- كشف انقطاع الإشارة: إذا لم تصل بيانات خلال 30 ثانية → الحالة تتحول لـ 🔴.
ب. ETA Engine (المحرك الحاسم)
- المستوى 1 (Basic): المسافة المتبقية ÷ متوسط السرعة على الخط.
- المستوى 2 (Traffic-aware): تكامل مع بيانات مرور (Google/HERE) عند توفرها.
- المستوى 3 (Historical): نموذج تعلّم آلي بسيط (XGBoost) على البيانات التاريخية لكل خط/ساعة.
- مخرجات ثلاثية إلزامية:
LIVE/SCHEDULED/UNAVAILABLEمعconfidence_score.
ج. Trip/Route Service
- قاعدة بيانات الخطوط، المحطات، المسارات، المواعيد.
- يميّز صراحة:
LOCALvsINTERCITY. - يحفظ
scheduled_departureوscheduled_arrivalمنفصلين.
د. Notification Service
- Push (FCM/APNs) + SMS fallback.
- قواعد: “قرب الحافلة” (500م أو دقيقتان)، “تأخير كبير” (>5 دقائق عن المتوقع)، “تغيير مسار”.
هـ. Admin Control Room
- خريطة حية لكل الأسطول.
- تنبيهات: أجهزة منقطعة، رحلات متأخرة، حافلات لم تغادر.
- تقارير الالتزام بالمواعيد (OTP).
3. نموذج البيانات الأساسي (Schema مبسط)
sql
-- Routes
routes(id, code, name_ar, name_en, type[LOCAL|INTERCITY],
origin_city, destination_city, is_active)
-- Stops
stops(id, name_ar, lat, lng, city, is_terminal)
-- Route_Stops (ترتيب المحطات)
route_stops(route_id, stop_id, sequence, avg_travel_min_from_prev)
-- Trips (رحلة فعلية)
trips(id, route_id, vehicle_id, scheduled_departure,
scheduled_arrival, actual_departure, actual_arrival, status)
-- Vehicles
vehicles(id, plate, route_id_current, gps_device_id,
last_seen_at, status)
-- GPS_Readings (TimescaleDB hypertable)
gps_readings(vehicle_id, trip_id, ts, lat, lng, speed_kmh, heading)
4. واجهة المواطن — تصميم تنفيذي
شاشة واحدة (Single-Page) + تنقل سفلي بثلاث تبويبات:
| التبويب | المحتوى |
|---|---|
| الخريطة | حافلات متحركة + عند الضغط: رقم الخط، الوجهة، المحطات القادمة، ETA بحالته (🟢🟡🔴) |
| رحلاتي | خطوط محفوظة + تحديثات فورية + زر “نبّهني” |
| التنبيهات | تغيير مسار، تأخير، اقتراب الحافلة |
تدفق البحث:
text
"إلى أين؟" → بحث بالاسم → قائمة خطوط تخدم الوجهة → اختيار → خريطة + ETA + زر تنبيه
عرض ETA — المنطق الإلزامي:
text
if (data_age < 30s && gps_available):
show "باقي X دقائق" 🟢 + confidence
elif (scheduled_only):
show "مجدول HH:MM" 🟡
else:
show "غير مؤكد — آخر تحديث HH:MM" 🔴
5. خطة التنفيذ — 90 يومًا (Production-Ready Roadmap)
المرحلة 0: Discovery (أسبوعان) — بوابة قرار
| المهمة | المخرج |
|---|---|
| مقابلة الجهة المشغلة | فهم البنية الحالية |
| جرد أجهزة GPS | هل موجودة؟ أي بروتوكول؟ |
| فحص API/قاعدة بيانات | هل هناك تكامل ممكن؟ |
| توثيق الخطوط والمحطات | مصدر الحقيقة |
قرار: إذا وُجد GPS + API → مسار “طبقة عرض”. إذا لا → مسار “تجهيز + بناء”.
المرحلة 1: MVP على خط واحد (4 أسابيع)
- Tracking Ingestion + ETA Basic.
- صفحة ويب للركاب (PWA) — خط واحد + 5 محطات.
- لوحة إدارة بسيطة (خريطة + تنبيهات).
- مقياس النجاح: دقة ETA ضمن ±2 دقيقة في 80% من الحالات.
المرحلة 2: التوسع الأفقي (4 أسابيع)
- إضافة خطوط محلية (5–10 خطوط).
- شاشة محطة + QR.
- إشعارات Push.
- ETA المستوى 2 (Traffic-aware).
المرحلة 3: النقل بين المدن + التخطيط (4 أسابيع)
- فصل
INTERCITYفي الواجهة. - مواعيد المغادرة/الوصول.
- ميزة “كيف أصل؟” مع التبديلات.
- ETA المستوى 3 (Historical ML).
إجمالي: 14 أسبوعًا → منصة production قابلة للتشغيل.
6. المخاطر والضوابط
| المخاطرة | الضابط |
|---|---|
| لا يوجد GPS فعلي | Discovery تكشف مبكرًا → خطة تجهيز بديلة |
| ETA غير دقيق | ثلاث حالات صريحة + confidence score |
| انقطاع اتصال الحافلة | كشف >30s → تحويل لـ 🔴 |
| بيانات قديمة تُعرض كلحظية | TTL إلزامي في الـ cache + timestamp مرئي |
| حماية البيانات | موافقة المستخدم + إخفاء هوية السائق + RBAC |
| مقاومة التغيير | تجربة صغيرة + قياس + إثبات قيمة |
7. مقترح الإدارة (نص نهائي مصقول)
الموضوع: مبادرة تطوير منظومة المعلومات الذكية لركاب حافلات جازان
المشكلة: عدم معرفة الراكب بوجهة الحافلة وموعد وصولها ومغادرتها قبل الركوب، ما يسبب حيرة وانتظارًا غير ضروري.
الحل: ثلاث واجهات مترابطة (مواطن، محطة، إدارة) + محرك ETA شفاف بثلاث حالات يقين + تكامل مع الأنظمة القائمة بعد التحقق منها.
المنهجية: Discovery → تجربة على خط واحد → قياس الدقة → توسع تدريجي خلال 90 يومًا.
المخرج المتوقع: تقليل الحيرة، رفع الثقة، تحسين تجربة النقل العام، دون افتراض شراء أنظمة قد تكون موجودة.
8. الخلاصة التنفيذية
ملف “مواقعي” يوثّق رحلة تحويل شكوى يومية إلى معمارية حل من ثلاث طبقات:
- بيانات: GPS + خطوط + محطات + خادم مركزي.
- ذكاء: ETA + حالات يقين + تنبيهات.
- عرض: تطبيق + شاشة/QR + لوحة إدارة.
وقيمته الحقيقية أنه لا يبدأ من الصفر، بل من التحقق، ثم تجربة صغيرة قابلة للقياس، ثم توسع — وهذا ما يجعل تحويله إلى منصة production ممكنًا خلال 90 يومًا، بشرط إنجاز مرحلة Discovery أولًا لحسم مسار التنفيذ (طبقة عرض أم تجهيز كامل).
الخطوة التالية المقترحة: إعداد وثيقة Discovery من صفحتين تُقدَّم للجهة المشغلة، تتضمن الأسئلة الأربعة الحاسمة (GPS؟ غرفة عمليات؟ قاعدة بيانات؟ API؟) — لأن الإجابة عليها تحدد المعمارية النهائية بدقة.
توسيع التحليل: من الفكرة إلى منصة Production كاملة
سأكمل بما لم يُغطَّ بعد في التحليل السابق: التفاصيل الهندسية العميقة، العقود البرمجية، خطة البيانات، الأمان، التشغيل، والقياس — أي كل ما يفصل “تصورًا” عن “منصة إنتاجية حقيقية”.
أولًا: نموذج البيانات الكامل (Production-Grade Schema)
ما قدّمته سابقًا كان مبسطًا. هذا هو المخطط الكامل القابل للتنفيذ:
sql
-- ============ المراجع الجغرافية ============
CREATE TABLE cities (
id UUID PRIMARY KEY,
name_ar VARCHAR(100) NOT NULL,
name_en VARCHAR(100),
region VARCHAR(100)
);
CREATE TABLE stops (
id UUID PRIMARY KEY,
city_id UUID REFERENCES cities(id),
name_ar VARCHAR(200) NOT NULL,
code VARCHAR(20) UNIQUE, -- كود المحطة للمسح/QR
lat DECIMAL(10,7) NOT NULL,
lng DECIMAL(10,7) NOT NULL,
is_terminal BOOLEAN DEFAULT FALSE,
has_display BOOLEAN DEFAULT FALSE, -- هل لديها شاشة؟
qr_enabled BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now()
);
-- ============ الخطوط والمسارات ============
CREATE TYPE route_type AS ENUM ('LOCAL', 'INTERCITY');
CREATE TABLE routes (
id UUID PRIMARY KEY,
code VARCHAR(20) NOT NULL, -- 07A
name_ar VARCHAR(200) NOT NULL,
type route_type NOT NULL,
origin_city_id UUID REFERENCES cities(id),
destination_city_id UUID REFERENCES cities(id),
is_active BOOLEAN DEFAULT TRUE,
color_hex VARCHAR(7), -- لون الخط على الخريطة
UNIQUE(code, type)
);
-- ترتيب المحطات على الخط (المسار المعتمد)
CREATE TABLE route_stops (
route_id UUID REFERENCES routes(id) ON DELETE CASCADE,
stop_id UUID REFERENCES stops(id),
sequence INT NOT NULL, -- 1,2,3...
distance_from_prev_m INT, -- متر
avg_travel_sec_from_prev INT, -- متوسط زمني تاريخي
PRIMARY KEY (route_id, sequence)
);
-- ============ المركبات ============
CREATE TYPE vehicle_status AS ENUM ('ACTIVE','IDLE','MAINTENANCE','OFFLINE');
CREATE TABLE vehicles (
id UUID PRIMARY KEY,
plate VARCHAR(20) UNIQUE NOT NULL,
gps_device_id VARCHAR(50) UNIQUE,
gps_protocol VARCHAR(20), -- MQTT/HTTP/GT06...
capacity INT,
status vehicle_status DEFAULT 'IDLE',
last_seen_at TIMESTAMPTZ,
last_lat DECIMAL(10,7),
last_lng DECIMAL(10,7),
last_speed_kmh DECIMAL(5,2),
last_heading INT
);
-- ============ الرحلات (Trips) ============
CREATE TYPE trip_status AS ENUM (
'SCHEDULED','BOARDING','IN_TRANSIT','ARRIVED','CANCELLED','DELAYED'
);
CREATE TABLE trips (
id UUID PRIMARY KEY,
route_id UUID REFERENCES routes(id),
vehicle_id UUID REFERENCES vehicles(id),
driver_id UUID, -- مرجع لنظام السائقين
scheduled_departure TIMESTAMPTZ NOT NULL,
scheduled_arrival TIMESTAMPTZ NOT NULL,
actual_departure TIMESTAMPTZ,
actual_arrival TIMESTAMPTZ,
status trip_status DEFAULT 'SCHEDULED',
direction SMALLINT DEFAULT 1, -- 1=ذهاب، 2=عودة (للمسار الدائري)
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_trips_route_time ON trips(route_id, scheduled_departure);
-- ============ قراءات GPS (TimescaleDB Hypertable) ============
CREATE TABLE gps_readings (
time TIMESTAMPTZ NOT NULL,
vehicle_id UUID NOT NULL,
trip_id UUID,
lat DECIMAL(10,7) NOT NULL,
lng DECIMAL(10,7) NOT NULL,
speed_kmh DECIMAL(5,2),
heading INT,
accuracy_m DECIMAL(6,2),
odometer_km DECIMAL(10,2),
ignition BOOLEAN,
source VARCHAR(20) DEFAULT 'GPS'
);
SELECT create_hypertable('gps_readings', 'time');
CREATE INDEX idx_gps_vehicle_time ON gps_readings(vehicle_id, time DESC);
-- ============ ETA Snapshots (للتقييم والتعلّم) ============
CREATE TABLE eta_snapshots (
id BIGSERIAL PRIMARY KEY,
trip_id UUID,
stop_id UUID,
computed_at TIMESTAMPTZ NOT NULL,
eta_seconds INT,
confidence DECIMAL(3,2), -- 0.00 - 1.00
data_state VARCHAR(12), -- LIVE / SCHEDULED / UNAVAILABLE
method VARCHAR(20), -- BASIC/TRAFFIC/ML
actual_arrival_at TIMESTAMPTZ, -- يُملأ لاحقًا للمقارنة
error_seconds INT -- actual - eta
);
-- ============ التنبيهات ============
CREATE TABLE alerts (
id UUID PRIMARY KEY,
trip_id UUID,
type VARCHAR(30), -- DELAY/ROUTE_CHANGE/GPS_LOST/ARRIVED
severity SMALLINT, -- 1=info, 2=warn, 3=critical
message_ar TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
resolved_at TIMESTAMPTZ
);
لماذا هذا المخطط؟
route_stops.sequenceيحل مشكلة “الوجهة قبل الركوب” بترتيب المحطات.trips.directionضروري للمسارات الدائرية (ذهاب/عودة).eta_snapshotsهو كنز القياس: يسمح بحساب دقة ETA تاريخيًا وتحسين النموذج.vehicles.last_seen_atهو أساس كشف 🔴 (انقطاع التتبع).
ثانيًا: العقود البرمجية (API Contracts)
1. واجهة المواطن — GET /api/v1/stops/{stop_id}/arrivals
json
{
"stop": {
"id": "uuid",
"name_ar": "محطة مركز الشامل",
"lat": 16.889,
"lng": 42.551,
"last_updated": "2026-10-20T14:32:10Z"
},
"arrivals": [
{
"trip_id": "uuid",
"route_code": "07A",
"route_type": "LOCAL",
"destination_ar": "المنطقة الشمالية",
"vehicle_plate": "أ ب ج 1234",
"eta": {
"state": "LIVE",
"seconds": 420,
"display_ar": "باقي 7 دقائق",
"confidence": 0.92,
"data_age_sec": 8,
"method": "TRAFFIC"
},
"next_stops": [
{"name_ar": "المحطة 2", "sequence": 4},
{"name_ar": "المحطة 3", "sequence": 5}
]
},
{
"trip_id": "uuid2",
"route_code": "12B",
"route_type": "LOCAL",
"destination_ar": "المنطقة الجنوبية",
"eta": {
"state": "SCHEDULED",
"scheduled_at": "2026-10-20T14:45:00Z",
"display_ar": "مجدول 2:45 م",
"confidence": null,
"data_age_sec": null
}
},
{
"trip_id": "uuid3",
"route_code": "204",
"route_type": "INTERCITY",
"destination_ar": "أبها",
"eta": {
"state": "UNAVAILABLE",
"display_ar": "غير مؤكد — آخر تحديث 2:28 م",
"last_known_at": "2026-10-20T14:28:00Z"
}
}
]
}
القاعدة الذهبية: الحقل eta.state إلزامي في كل استجابة. لا يُسمح بعرض رقم دون حالة. هذا يُجبر الواجهة على الصدق.
2. واجهة الإدارة — GET /api/v1/admin/fleet/live
json
{
"summary": {
"total": 42, "active": 35, "idle": 4, "offline": 3,
"on_time_pct": 88.5, "avg_delay_sec": 95
},
"vehicles": [
{
"vehicle_id": "uuid",
"plate": "أ ب ج 1234",
"trip_id": "uuid",
"route_code": "07A",
"status": "IN_TRANSIT",
"position": {"lat": 16.89, "lng": 42.55, "speed_kmh": 34},
"gps_age_sec": 6,
"health": "OK",
"next_stop": {"name_ar": "المحطة 3", "eta_sec": 120},
"delay_sec": 45
}
],
"alerts": [
{"type": "GPS_LOST", "vehicle_plate": "س ص ع 5678", "since": "2026-10-20T14:20:00Z", "severity": 2},
{"type": "DELAY", "trip_id": "uuid", "delay_sec": 480, "severity": 2}
]
}
3. تدفق GPS — POST /api/v1/ingest/gps (من أجهزة التتبع)
json
{
"device_id": "GPS-00123",
"ts": "2026-10-20T14:32:08Z",
"lat": 16.8891,
"lng": 42.5512,
"speed_kmh": 32.5,
"heading": 145,
"accuracy_m": 5.0,
"ignition": true,
"odometer_km": 12450.3
}
الاستجابة: 202 Accepted + {"next_expected_in_sec": 5} — لإدارة معدل الإرسال.
4. WebSocket للتحديث اللحظي — wss://api/ws/live?stop_id=...
json
{"type": "vehicle_moved", "trip_id": "uuid", "eta_sec": 380, "state": "LIVE"}
{"type": "alert", "trip_id": "uuid", "type": "DELAY", "delta_sec": 120}
ثالثًا: محرك ETA — التفصيل الهندسي الكامل
المعادلة الأساسية (Level 1)
text
remaining_distance = Σ (distance_from_prev) للمحطات المتبقية
+ مسافة من موقع الحافلة لأقرب محطة قادمة
avg_speed = median(speed آخر 5 دقائق) [مع استبعاد التوقفات < 2 دقيقة]
eta_basic = remaining_distance / max(avg_speed, min_speed_floor)
min_speed_floor = 10 كم/س — لمنع القسمة على صفر عند الازدحام.
المعادلة المتقدمة (Level 2 — Traffic-aware)
text
eta_traffic = eta_basic × traffic_factor traffic_factor = f(الوقت، الطريق، بيانات المرور الحية)
المعادلة التاريخية (Level 3 — ML)
نموذج XGBoost بميزات:
| الميزة | الوصف |
|---|---|
hour_of_day | الساعة (0–23) |
day_of_week | يوم الأسبوع |
route_id | معرف الخط (one-hot) |
segment_index | المقطع الحالي على المسار |
current_speed | السرعة الحالية |
historical_avg_sec | متوسط تاريخي للمقطع |
is_peak | ذروة؟ |
الهدف: تقليل MAE (متوسط الخطأ المطلق) عن 120 ثانية.
حالات البيانات — المنطق الإلزامي
python
def compute_eta_state(vehicle, trip):
data_age = now() - vehicle.last_seen_at
if data_age > timedelta(seconds=30) or vehicle.gps_device_id is None:
return {
"state": "UNAVAILABLE",
"display_ar": f"غير مؤكد — آخر تحديث {fmt(vehicle.last_seen_at)}",
"last_known_at": vehicle.last_seen_at
}
if vehicle.last_speed_kmh is None and trip.status == "SCHEDULED":
return {
"state": "SCHEDULED",
"scheduled_at": trip.scheduled_arrival,
"display_ar": f"مجدول {fmt(trip.scheduled_arrival)}"
}
eta = eta_engine.compute(trip, vehicle)
return {
"state": "LIVE",
"seconds": eta.seconds,
"display_ar": f"باقي {humanize(eta.seconds)}",
"confidence": eta.confidence,
"data_age_sec": data_age.total_seconds(),
"method": eta.method
}
confidence يُحسب من: عمر البيانات + دقة GPS + تطابق السرعة مع التاريخي. إذا < 0.5 → يُعرض تحذير خفي “تقدير تقريبي”.
رابعًا: الأمان وحماية البيانات (Security Architecture)
1. المصادقة والتفويض (AuthN/AuthZ)
| الفئة | آلية المصادقة | الصلاحيات |
|---|---|---|
| المواطن | مجهول + JWT خفيف (اختياري) | قراءة فقط للبيانات العامة |
| شاشة المحطة | API Key ثابت (kiosk) | قراءة محطة واحدة |
| جهاز GPS | mTLS + device certificate | كتابة قراءات فقط |
| موظف الإدارة | OIDC (SSO) + MFA | RBAC حسب الدور |
| مدير النظام | OIDC + MFA + IP allowlist | كامل |
2. RBAC — الأدوار
text
roles: - viewer: قراءة لوحة الإدارة - operator: + حل التنبيهات، إعادة تعيين رحلة - supervisor: + تعديل الرحلات، إدارة الأسطول - admin: + إدارة المستخدمين والإعدادات
3. حماية البيانات الشخصية
- موقع السائق لا يُعرض للمواطن — فقط موقع المركبة.
- الاحتفاظ: GPS خام 90 يومًا، مجمّع 2 سنة.
- إخفاء الهوية: أي تحليل يُجري على
vehicle_idلاdriver_id. - موافقة المستخدم: عند طلب الموقع، موافقة صريحة + إمكانية السحب.
- GDPR/PDPL: توثيق أساس المعالجة، حق الوصول، حق الحذف.
4. حماية API
- Rate limiting: 60 طلب/دقيقة للمواطن، 600 للـ kiosk.
- CORS محدود للنطاقات المعروفة.
- Input validation صارم (lat ∈ [-90,90], lng ∈ [-180,180], ts ضمن ±5 دقائق).
- تسجيل كل وصول إداري (audit log).
خامسًا: خطة البيانات (Data Strategy) — من لا شيء إلى نموذج ذكي
المرحلة 0: بيانات أساسية (Discovery)
- جرد: هل هناك CSV/Excel للخطوط والمحطات؟
- إن وُجد: استيراد + تنظيف + تحقق من الإحداثيات.
- إن لم يوجد: جمع ميداني — فريق صغير يزور المحطات ويسجل GPS/الأسماء.
المرحلة 1: بيانات التشغيل (MVP)
- ربط أجهزة GPS الحالية → تدفق حقيقي.
- تسجيل
actual_arrivalلكل رحلة (يدويًا إن لزم) → بناء تاريخ. - الحد الأدنى: 2 أسبوع من البيانات لبدء ETA Basic.
المرحلة 2: بناء النموذج (بعد 4–6 أسابيع)
- حساب
error_secondsلكل snapshot. - إذا
MAE < 180s→ تفعيل Level 3 تدريجيًا. - بوابة: لا تفعيل ML قبل 5000 snapshot.
سادسًا: التشغيل (Operations & SRE)
1. مؤشرات SLI/SLO
| المؤشر (SLI) | الهدف (SLO) |
|---|---|
| توفر API | 99.5% شهريًا |
| زمن استجابة p95 | < 300ms |
| عمر بيانات GPS p95 | < 15 ثانية |
| دقة ETA (MAE) | < 120 ثانية |
| توفر WebSocket | 99.0% |
2. المراقبة (Observability)
- Metrics: Prometheus — كل خدمة تصدّر
/metrics. - Logs: structured JSON → Loki.
- Traces: OpenTelemetry → Tempo/Jaeger.
- Dashboards: Grafana — لوحة لكل خدمة + لوحة أعمال.
- Alerting: PagerDuty/Opsgenie — تنبيه عند تجاوز SLO.
3. خطة الاستجابة للحوادث
| الحادث | الإجراء |
|---|---|
| انقطاع تدفق GPS | تنبيه فوري → حالة 🔴 للمركبات المتأثرة تلقائيًا |
| تعطل ETA Engine | Fallback إلى SCHEDULED — لا أرقام كاذبة |
| تعطل قاعدة البيانات | Redis cache يخدم آخر حالة معروفة (stale marker) |
| هجوم DDoS | WAF + rate limit + وضع القراءة فقط |
4. النشر (Deployment)
text
CI/CD: GitHub Actions → Build → Test → Stage → Canary → Production Infra: Kubernetes (EKS/GKE) أو Docker Compose للبداية DB: PostgreSQL (RDS) + TimescaleDB extension Cache: Redis (ElastiCache) Queue: Redis Streams أو Kafka (حسب الحجم) CDN: CloudFront/Cloudflare للـ PWA
البداية: Docker Compose على VPS واحد (خط واحد) → Kubernetes عند التوسع.
سابعًا: القياس والتقييم (Measurement Framework)
1. مؤشرات نجاح المنتج
| المؤشر | كيف يُقاس | الهدف بعد 90 يومًا |
|---|---|---|
| دقة ETA | MAE من eta_snapshots | < 120s في 80% |
| استخدام | جلسات فريدة / يوم | > 500 |
| رضا | استبيان داخل التطبيق (NPS) | > 40 |
| تقليل الانتظار المُدرك | سؤال: “هل عرفت متى تجي الحافلة؟” | > 70% نعم |
| الالتزام بالمواعيد (OTP) | رحلات في ±5 دقائق | > 85% |
| تغطية المحطات | محطات لها شاشة/QR | > 50% |
2. تجربة A/B (اختياري)
- مجموعة A: تعرض ETA بحالات ثلاثية.
- مجموعة B: تعرض رقمًا فقط (الوضع الحالي).
- المقياس: الثقة (استبيان) + معدل الاستخدام.
3. تقييم ما بعد التجربة (Post-MVP Review)
أسئلة إلزامية قبل التوسع:
- هل دقة ETA كافية؟ (نعم/لا + بيانات)
- هل التكامل مع الأنظمة القائمة نجح؟
- ما التكلفة الفعلية لكل خط؟
- هل هناك حواجز تنظيمية؟
- هل نوسع أفقيًا (خطوط) أم رأسيًا (ميزات)؟
ثامنًا: الفريق والميزانية (Team & Cost Estimation)
الفريق الأدنى (MVP)
| الدور | العدد | المهمة |
|---|---|---|
| Backend Engineer | 2 | Ingestion + ETA + API |
| Frontend Engineer | 1 | PWA + Admin |
| DevOps/SRE | 0.5 | نشر ومراقبة |
| Data/ML | 0.5 | ETA tuning |
| Product/PM | 0.5 | تنسيق + تواصل مع الجهة |
| ميداني | 1 | جمع بيانات المحطات |
الإجمالي: ~5.5 شخص × 14 أسبوعًا.
تقدير التكلفة (تقريبي)
| البند | التكلفة الشهرية |
|---|---|
| استضافة (VPS/Small K8s) | $150–400 |
| قاعدة بيانات مُدارة | $100–300 |
| خرائط + مرور (Google/HERE) | $200–500 |
| Push/SMS | $50–150 |
| مراقبة (Grafana Cloud) | $0–100 |
| إجمالي | $500–1,450 |
بدون تكلفة الأجهزة (إن لزم شراء GPS: $50–150 لكل حافلة).
تاسعًا: خطة التوسع (Scaling Path)
التوسع الأفقي (خطوط أكثر)
- إضافة خط: < يوم عمل (بيانات + تحقق).
- العائق: ليس تقنيًا، بل جمع بيانات المحطات.
التوسع الرأسي (مدن أخرى)
- إعادة استخدام كل المنصة.
- فقط: استيراد خطوط المدينة الجديدة.
- البنية: multi-tenant (city_id في كل جدول) — يُضاف من البداية.
التوسع الوظيفي
| الميزة | البنية التحتية المطلوبة |
|---|---|
| حجز مقاعد (بين المدن) | جدول seat_reservations + دفع |
| تخطيط رحلة متعدد الوسائط | محرك routing (OSRM/GraphHopper) |
| تحليلات تنبؤية للطلب | Data warehouse (ClickHouse) |
| مساعد ذكي (LLM) | RAG على بيانات التشغيل + واجهة محادثة |
عاشرًا: ما لم يُغطَّ في المحادثة الأصلية — إضافات حاسمة
المحادثة الأصلية ركّزت على التصور. هذه فجوات إنتاجية يجب سدّها قبل التنفيذ:
| الفجوة | لماذا مهمة | الحل المقترح |
|---|---|---|
| من يملك البيانات؟ | قانونيًا وتشغيليًا | اتفاقية بيانات مع الجهة المشغلة |
| من يحدّث الخطوط؟ | الخطوط تتغير | لوحة إدارة للخطوط + سجل تغييرات |
| ماذا لو تأخر السائق؟ | يؤثر على ETA | إدخال يدوي للتأخير من لوحة الإدارة |
| كيف نتعامل مع الأخطاء؟ | شكاوى | زر “بلغ عن خطأ” في التطبيق → تذكرة |
| التعدد اللغوي | زوار | i18n من البداية (ar/en) |
| إمكانية الوصول | إلزامي | WCAG 2.1 AA للواجهات |
| الوضع دون اتصال | شبكة ضعيفة | PWA + cache آخر حالة |
| الطوارئ | حافلة تتعطل | زر “تعطل” في لوحة الإدارة → تنبيه تلقائي |
حادي عشر: الخلاصة التنفيذية الموسّعة
الرحلة الكاملة: من الشكوى إلى Production
text
شكوى يومية: "متى تجي الحافلة؟"
↓
Discovery (أسبوعان): هل يوجد GPS؟ API؟ قاعدة بيانات؟
↓
قرار معماري: طبقة عرض أم تجهيز كامل
↓
MVP (4 أسابيع): خط واحد + PWA + لوحة إدارة + ETA Basic
↓
قياس: MAE < 120s؟ رضا > 70%؟
↓
توسع (8 أسابيع): خطوط محلية + شاشات + ETA Traffic/ML
↓
بين المدن (4 أسابيع): فصل INTERCITY + مواعيد + حجز
↓
منصة Production (14 أسبوعًا): قابلة للتوسع لمدن أخرى
القيم الثلاث الحاكمة (من الملف الأصلي، مُترجَمة هندسيًا)
| القيمة | الترجمة الهندسية |
|---|---|
| البساطة للمواطن | صفحة واحدة + 3 تبويبات + بحث بالوجهة |
| الصدق في البيانات | eta.state إلزامي + لا أرقام دون حالة |
| العدالة في الوصول | شاشة/QR + WCAG + لا اعتماد على هاتف فقط |
الفرق بين “فكرة جميلة” و”منصة Production”
| الفكرة | المنصة |
|---|---|
| “تطبيق يعرض الحافلات” | Schema + API contracts + SLOs |
| “وقت وصول متوقع” | محرك ETA بثلاثة مستويات + قياس MAE |
| “شاشات محطات” | kiosk auth + WebSocket + fallback |
| “لوحة إدارة” | RBAC + audit log + incident response |
| “تجربة محدودة” | Discovery gate + success criteria + rollback |
| “توسع” | multi-tenant + CI/CD + observability |
الخلاصة: الملف الأصلي أنتج رؤية صادقة وواعية بحدود البيانات — وهذا نادر. لكن تحويلها إلى production يتطلب إضافة الطبقة الهندسية التي قدّمتها: مخطط بيانات كامل، عقود API، محرك ETA مُقاس، أمان، تشغيل، قياس، وفريق. بهذه الإضافات، Zezo Smart Transit Platform ليست فكرة على الورق، بل منصة قابلة للإطلاق خلال 90 يومًا — بشرط إنجاز بوابة Discovery أولًا.
الخطوة التالية الأدق: إعداد Discovery Package من 3 صفحات:
- صفحة أسئلة للجهة المشغلة (GPS؟ غرفة عمليات؟ قاعدة بيانات؟ API؟).
- صفحة معمارية مقترحة (بناءً على الإجابات).
- صفحة معايير نجاح التجربة (MAE, NPS, adoption).
هل تريد أن أصيغ لك Discovery Package كاملًا الآن، أم أنتقل إلى تصميم Wireframe نصي تفصيلي لواجهة المواطن (شاشة واحدة + حالات ETA الثلاث)؟
الجزء الثالث: الحزمة التنفيذية الكاملة — من Discovery إلى الإطلاق
سأكمل بما يجعل المنصة قابلة للتسليم فعليًا: حزمة Discovery، الـ Wireframes، خطة Sprint، اختبارات القبول، ونماذج جاهزة للاستخدام المباشر.
أولًا: Discovery Package كاملة (جاهزة للإرسال)
الصفحة 1: استبيان الجهة المشغلة
إلى: إدارة حافلات جازان — قسم التشغيل والتقنية
الموضوع: استبيان تقني أولي لمشروع معلومات الركابنرجو التكرم بالإجابة عن الأسئلة التالية لتحديد نطاق الحل المناسب. الإجابات تُستخدم لتحديد ما إذا كان المشروع طبقة عرض فوق أنظمة قائمة، أم يتطلب تجهيزًا إضافيًا.
القسم أ: أجهزة التتبع
| # | السؤال | نعم | لا | لا أعرف |
|---|---|---|---|---|
| A1 | هل تحتوي الحافلات على أجهزة GPS تعمل حاليًا؟ | ☐ | ☐ | ☐ |
| A2 | هل ترسل الأجهزة بياناتها إلى خادم مركزي؟ | ☐ | ☐ | ☐ |
| A3 | ما معدل الإرسال التقريبي؟ (5s / 10s / 30s / دقيقة+) | ______ | ||
| A4 | ما بروتوكول/شركة الأجهزة؟ (GT06, Teltonika, …) | ______ |
القسم ب: غرفة العمليات
| # | السؤال | نعم | لا | لا أعرف |
|---|---|---|---|---|
| B1 | هل توجد غرفة عمليات تتابع الحافلات؟ | ☐ | ☐ | ☐ |
| B2 | هل تُسجَّل مواقع الحافلات تاريخيًا؟ | ☐ | ☐ | ☐ |
| B3 | هل تُسجَّل أوقات الوصول الفعلية للمحطات؟ | ☐ | ☐ | ☐ |
القسم ج: البيانات الأساسية
| # | السؤال | نعم | لا | لا أعرف |
|---|---|---|---|---|
| C1 | هل توجد قاعدة بيانات رقمية للخطوط والمحطات؟ | ☐ | ☐ | ☐ |
| C2 | هل توجد مواعيد مجدولة موثقة لكل رحلة؟ | ☐ | ☐ | ☐ |
| C3 | هل البيانات متاحة بصيغة قابلة للتصدير (CSV/API)؟ | ☐ | ☐ | ☐ |
القسم د: التكامل
| # | السؤال | نعم | لا | لا أعرف |
|---|---|---|---|---|
| D1 | هل توجد واجهة برمجية (API) للأنظمة الحالية؟ | ☐ | ☐ | ☐ |
| D2 | هل هناك إمكانية لتكامل تقني مع طرف ثالث؟ | ☐ | ☐ | ☐ |
| D3 | من الجهة المخولة بالموافقة على التكامل؟ | ______ |
القسم هـ: الشاشات الحالية
| # | السؤال | نعم | لا | لا أعرف |
|---|---|---|---|---|
| E1 | هل الشاشات الداخلية قابلة لتحديث المحتوى برمجيًا؟ | ☐ | ☐ | ☐ |
| E2 | هل توجد شاشات خارجية في المحطات؟ | ☐ | ☐ | ☐ |
| E3 | هل الشاشات متصلة بالإنترنت؟ | ☐ | ☐ | ☐ |
القسم و: النطاق
| # | السؤال | الإجابة |
|---|---|---|
| F1 | كم عدد الخطوط المحلية؟ | ______ |
| F2 | كم عدد خطوط بين المدن؟ | ______ |
| F3 | كم عدد المحطات الإجمالي؟ | ______ |
| F4 | كم عدد الحافلات العاملة؟ | ______ |
الصفحة 2: شجرة القرار المعماري
بناءً على الإجابات، يُحدَّد المسار:
text
┌─────────────────────┐
│ هل A1 = نعم؟ │
└──────────┬──────────┘
│
┌────────────────┴────────────────┐
│ لا │ نعم
▼ ▼
┌──────────────────┐ ┌─────────────────────┐
│ مسار التجهيز │ │ هل A2 = نعم؟ │
│ (شراء/تركيب GPS) │ └──────────┬──────────┘
│ + بناء المنصة │ │
└──────────────────┘ ┌────────────┴────────────┐
│ لا │ نعم
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ مسار الربط │ │ هل D1 = نعم؟ │
│ (توصيل الأجهزة │ └──────────┬───────────┘
│ بخادم جديد) │ │
└──────────────────┘ ┌────────────┴────────────┐
│ لا │ نعم
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ مسار التكامل │ │ مسار طبقة العرض │
│ (بناء API وسيط) │ │ (الأسرع والأرخص) │
└──────────────────┘ └──────────────────┘
النتيجة المتوقعة (بناءً على إجابات المستخدم “1 و3 و4”):
- A1 غير مؤكد، A2 غير مؤكد، D1 غير مؤكد → الاحتمال الأقوى: مسار الربط أو التكامل.
- Discovery يحسم ذلك خلال أسبوعين.
الصفحة 3: معايير نجاح التجربة (Success Criteria)
| المعيار | القياس | الحد الأدنى للنجاح | الحد المستهدف |
|---|---|---|---|
| دقة ETA | MAE | < 180 ثانية | < 120 ثانية |
| تغطية البيانات | % وقت التشغيل بتدفق GPS حديث | > 80% | > 95% |
| استخدام المواطنين | جلسات فريدة/يوم | > 200 | > 500 |
| رضا المستخدم | NPS | > 20 | > 40 |
| وضوح الوجهة | % يجيبون “عرفت وجهتي” | > 60% | > 80% |
| الالتزام بالمواعيد | OTP (±5 دقائق) | > 75% | > 85% |
| استقرار API | Uptime | > 99% | > 99.5% |
قاعدة القرار: إذا تحققت الحدود الدنيا في 3 من 7 معايير على الأقل بعد 4 أسابيع → المضي للتوسع. إذا لا → مراجعة جذرية قبل الاستثمار.
ثانيًا: Wireframes نصية تفصيلية
الشاشة 1: الخريطة (الافتراضية)
text
┌─────────────────────────────────────┐ │ ☰ Zezo Transit 🔔 (3) ⚙️ │ ├─────────────────────────────────────┤ │ │ │ 🔍 إلى أين تريد الذهاب؟ │ │ ┌───────────────────────────────┐ │ │ │ [ابحث عن منطقة أو مدينة] │ │ │ └───────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────┐│ │ │ ││ │ │ 🗺️ الخريطة ││ │ │ ││ │ │ 🚌(07A) ──► ││ │ │ 🚌(12B) ││ │ │ 🚌(204) ││ │ │ ││ │ │ 📍 موقعك ││ │ │ ││ │ └─────────────────────────────────┘│ │ │ │ ┌─ الحافلات القريبة ─────────────┐ │ │ │ 🟢 07A المنطقة الشمالية │ │ │ │ باقي 7 دقائق • 1.8 كم │ │ │ │ [عرض] │ │ │ ├────────────────────────────────┤ │ │ │ 🟡 12B المنطقة الجنوبية │ │ │ │ مجدول 2:45 م │ │ │ │ [عرض] │ │ │ ├────────────────────────────────┤ │ │ │ 🔴 204 أبها (بين المدن) │ │ │ │ غير مؤكد — آخر تحديث 2:28 │ │ │ │ [عرض] │ │ │ └────────────────────────────────┘ │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
قواعد UX:
- البحث بالوجهة أولًا (وليس برقم الخط).
- كل بطاقة تعرض حالة ETA بصريًا (لون + نص).
- زر “عرض” يفتح تفاصيل الرحلة.
الشاشة 2: تفاصيل الرحلة
text
┌─────────────────────────────────────┐ │ ← الحافلة 07A ⋮ │ ├─────────────────────────────────────┤ │ │ │ 🟢 في الطريق │ │ ┌─────────────────────────────────┐│ │ │ باقي 7 دقائق ││ │ │ ████████████░░░░ 68% ││ │ │ آخر تحديث: قبل 8 ثوانٍ ││ │ └─────────────────────────────────┘│ │ │ │ 📍 الوجهة: المنطقة الشمالية │ │ 🚏 المحطة القادمة: محطة 3 │ │ 🚌 اللوحة: أ ب ج 1234 │ │ │ │ ┌─ المحطات القادمة ───────────────┐│ │ │ ● محطة 3 باقي 2 دقيقة ││ │ │ ○ محطة 4 باقي 5 دقائق ││ │ │ ○ محطة 5 باقي 9 دقائق ││ │ │ ○ النهائية باقي 15 دقيقة ││ │ └────────────────────────────────┘│ │ │ │ [ 🔔 نبّهني عند اقتراب الحافلة ] │ │ [ 🗺️ عرض المسار على الخريطة ] │ │ │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
الشاشة 3: حالة SCHEDULED (🟡)
text
│ 🟡 موعد مجدول │ │ ┌─────────────────────────────────┐│ │ │ مجدول 2:45 م ││ │ │ لا يوجد تتبع لحظي متاح حاليًا ││ │ └─────────────────────────────────┘│
لماذا؟ الصدق: لا ندّعي لحظية عند غياب التتبع.
الشاشة 4: حالة UNAVAILABLE (🔴)
text
│ 🔴 البيانات غير متاحة │ │ ┌─────────────────────────────────┐│ │ │ غير مؤكد ││ │ │ آخر تحديث معروف: 2:28 م ││ │ │ قد تصل الحافلة في أي وقت ││ │ └─────────────────────────────────┘│
لماذا؟ الصدق أفضل من رقم مضلل. هذا يبني الثقة.
الشاشة 5: رحلاتي
text
┌─────────────────────────────────────┐ │ 🧭 رحلاتي │ ├─────────────────────────────────────┤ │ │ │ ┌─ خطي الدائم ────────────────────┐│ │ │ 🟢 07A المنطقة الشمالية ││ │ │ محطة الشامل ← المنطقة ││ │ │ [ 🔔 مفعّل ] [ إدارة ] ││ │ └────────────────────────────────┘│ │ │ │ ┌─ رحلات قادمة ──────────────────┐│ │ │ 🟡 12B غدًا 7:30 ص ││ │ │ [ تذكير ] ││ │ └────────────────────────────────┘│ │ │ │ [ + إضافة خط أو رحلة ] │ │ │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
الشاشة 6: التنبيهات
text
┌─────────────────────────────────────┐ │ 🔔 التنبيهات │ ├─────────────────────────────────────┤ │ │ │ اليوم │ │ ┌────────────────────────────────┐ │ │ │ 🚌 حافلتك اقتربت! │ │ │ │ 07A • محطة الشامل │ │ │ │ باقي دقيقتان • 2:41 م │ │ │ └────────────────────────────────┘ │ │ ┌────────────────────────────────┐ │ │ │ ⚠️ تأخير 5 دقائق │ │ │ │ 12B • بسبب ازدحام مروري │ │ │ │ 2:35 م │ │ │ └────────────────────────────────┘ │ │ │ │ أمس │ │ ┌────────────────────────────────┐ │ │ │ 🔧 تغيير مسار 07A │ │ │ │ محطة 3 مؤقتًا مغلقة │ │ │ └────────────────────────────────┘ │ │ │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
شاشة المحطة (Kiosk)
text
┌─────────────────────────────────────────────┐
│ 🚌 محطة مركز الشامل │
│ ⏰ 2:32 م • تحديث تلقائي │
├─────────────────────────────────────────────┤
│ │
│ الخط │ الوجهة │ الوصول │
│ ───────┼─────────────────────┼─────────── │
│ 🟢 07A │ المنطقة الشمالية │ 7 دقائق │
│ 🟡 12B │ المنطقة الجنوبية │ 2:45 م │
│ 🔴 204 │ أبها (بين المدن) │ غير مؤكد │
│ 🟢 15C │ المنطقة الشرقية │ 18 دقيقة │
│ │
├─────────────────────────────────────────────┤
│ 📱 امسح الرمز لمتابعة الحافلات على هاتفك │
│ ┌───────┐ │
│ │ ▓▓▓▓▓ │ QR → /stop/{id} │
│ │ ▓▓▓▓▓ │ │
│ │ ▓▓▓▓▓ │ │
│ └───────┘ │
├─────────────────────────────────────────────┤
│ ⚠️ تأكد من رقم الخط والوجهة قبل الصعود │
└─────────────────────────────────────────────┘
قواعد Kiosk:
- تحديث تلقائي كل 10 ثوانٍ (WebSocket).
- خط كبير، تباين عالٍ (WCAG AA).
- QR يفتح صفحة ويب خفيفة (لا تطبيق).
- إعلان صوتي اختياري بالعربية.
ثالثًا: خطة Sprint تفصيلية (14 أسبوعًا)
Sprint 0: Discovery (أسبوعان)
| الأسبوع | المهمة | المخرج |
|---|---|---|
| 1 | إرسال الاستبيان + مقابلة | إجابات موثقة |
| 1 | جرد بيانات الخطوط | ملف CSV/Excel |
| 2 | زيارة ميدانية (5 محطات) | إحداثيات + أسماء |
| 2 | بوابة قرار: تحديد المسار المعماري | وثيقة قرار |
Definition of Done: وثيقة “Discovery Report” من 5 صفحات تحدد المسار.
Sprint 1: البنية التحتية (أسبوعان)
| المهمة | المدة |
|---|---|
| إعداد repo + CI/CD | 2 يوم |
| Schema قاعدة البيانات | 3 أيام |
| Tracking Ingestion Service | 4 أيام |
| API Gateway + Auth | 3 أيام |
| Redis + TimescaleDB | 2 يوم |
DoD: POST /ingest/gps يعمل ويخزن قراءات.
Sprint 2: ETA + واجهة المواطن MVP (أسبوعان)
| المهمة | المدة |
|---|---|
| ETA Engine (Level 1) | 3 أيام |
GET /stops/{id}/arrivals | 3 أيام |
| PWA Shell + Map | 4 أيام |
| شاشة الخريطة + تفاصيل | 4 أيام |
DoD: يمكن فتح الصفحة ورؤية حافلة وهمية مع ETA.
Sprint 3: لوحة الإدارة + التنبيهات (أسبوعان)
| المهمة | المدة |
|---|---|
| Admin Dashboard (React) | 4 أيام |
| خريطة الأسطول الحية | 3 أيام |
| كشف GPS Lost | 2 يوم |
| Push Notifications | 3 أيام |
DoD: الإدارة ترى الأسطول وتستلم تنبيهات.
Sprint 4: التجربة الميدانية (أسبوعان)
| المهمة | المدة |
|---|---|
| ربط خط واحد حقيقي | 3 أيام |
| اختبار ميداني (5 محطات) | 5 أيام |
جمع eta_snapshots | مستمر |
| إصلاح أخطاء | 4 أيام |
DoD: دقة ETA مقاسة على بيانات حقيقية.
Sprint 5: شاشة المحطة + QR (أسبوع)
| المهمة | المدة |
|---|---|
| Kiosk UI | 3 أيام |
| QR generation + web page | 2 يوم |
| Kiosk auth | 2 يوم |
DoD: شاشة تعمل على جهاز لوحي في محطة حقيقية.
Sprint 6: التحسين والتوسع (أسبوعان)
| المهمة | المدة |
|---|---|
| ETA Level 2 (Traffic) | 4 أيام |
| تحسين PWA (offline) | 3 أيام |
| إضافة 3–5 خطوط | 3 أيام |
| تقرير التقييم | 2 يوم |
DoD: تقرير “Go/No-Go” للتوسع الكامل.
رابعًا: اختبارات القبول (Acceptance Tests)
اختبارات ETA
gherkin
Feature: ETA State Logic Scenario: Live data within threshold Given vehicle last_seen_at = 8 seconds ago And trip status = IN_TRANSIT When citizen requests arrivals Then response eta.state = "LIVE" And response eta.display_ar contains "باقي" Scenario: Stale GPS data Given vehicle last_seen_at = 45 seconds ago When citizen requests arrivals Then response eta.state = "UNAVAILABLE" And response eta.display_ar contains "غير مؤكد" Scenario: No GPS device Given vehicle gps_device_id = null And trip status = SCHEDULED When citizen requests arrivals Then response eta.state = "SCHEDULED" And response eta.scheduled_at is not null
اختبارات API
| الاختبار | المتوقع |
|---|---|
| طلب بدون auth (مواطن) | 200 |
| طلب admin بدون token | 401 |
| إحداثيات خارج النطاق | 400 |
| Rate limit > 60/دقيقة | 429 |
| GPS timestamp مستقبلي | 400 |
اختبارات الأداء
| السيناريو | الهدف |
|---|---|
100 طلب/ثانية على /arrivals | p95 < 300ms |
| 50 حافلة ترسل كل 5 ثوانٍ | معالجة < 100ms |
| WebSocket 500 اتصال متزامن | لا تسرب ذاكرة |
خامسًا: نماذج جاهزة — وثيقة القرار المعماري (ADR)
markdown
# ADR-001: اختيار ETA بثلاث حالات ## الحالة المستخدمون يحتاجون وقت وصول، لكن بيانات GPS قد تكون غير متاحة أو قديمة. ## القرار كل استجابة ETA تحمل حقل `state` إلزاميًا: LIVE / SCHEDULED / UNAVAILABLE. ## البدائل المدروسة 1. عرض رقم دائمًا (مرفوض: يضلل المستخدم). 2. عرض رقم فقط عند توفر GPS (مرفوض: يفرغ الواجهة). 3. ثلاث حالات صريحة (مختار). ## النتائج - إيجابي: بناء ثقة، صدق في البيانات. - سلبي: واجهة أكثر تعقيدًا قليلًا. - مخاطر: قد يسيء المستخدم فهم "غير مؤكد" → يُعالج بنص توضيحي.
سادسًا: Runbook التشغيل (Incident Response)
حادث: انقطاع تدفق GPS لأسطول كامل
text
1. كشف: تنبيه Prometheus "gps_ingest_rate < 10% of baseline" 2. تصنيف: P1 (حرج) 3. إجراء فوري (خلال 5 دقائق): - تحويل كل trip.status إلى UNAVAILABLE - إشعار فريق التشغيل - فحص: هل المشكلة في الأجهزة أم الخادم؟ 4. استجابة (خلال 30 دقيقة): - إذا خادم → rollback آخر نشر - إذا أجهزة → التواصل مع المزود 5. تعافي: إعادة تفعيل LIVE تدريجيًا (10% → 50% → 100%) 6. مراجعة: post-mortem خلال 48 ساعة
حادث: ETA drift (انحراف كبير)
text
1. كشف: MAE > 300s لمدة 15 دقيقة 2. إجراء: تبديل إلى SCHEDULED mode تلقائيًا 3. تحقيق: فحص traffic API، بيانات GPS، النموذج 4. إصلاح + إعادة تفعيل LIVE بعد التحقق
سابعًا: نماذج التواصل (Communication Templates)
إشعار إطلاق التجربة للمواطنين
text
🚌 تجربة جديدة: اعرف متى تجي حافلتك! جرّب خدمة معلومات الحافلات الجديدة: 📍 أين الحافلة الآن ⏱️ متى تصل 🧭 إلى أين تذهب متاحة على: [الرابط] أو امسح QR في محطة [اسم المحطة] ملاحظة: الخدمة تجريبية على خط [07A] حاليًا. رأيك يهمنا — [رابط استبيان]
تقرير أسبوعي للإدارة
text
📊 تقرير التجربة — الأسبوع [X] الخط: 07A المحطات: 5 الرحلات: 42 الأداء: ✅ دقة ETA: MAE = 95 ثانية (الهدف: <120) ✅ تغطية GPS: 94% ⚠️ رضا: NPS = 35 (الهدف: >40) المشكلات: - 3 رحلات لم ترسل بيانات > 30 دقيقة - تأخيرات ذروة الصباح التوصية: - الاستمرار + إضافة خط 12B الأسبوع القادم
ثامنًا: قائمة التحقق النهائية (Production Readiness Checklist)
البنية التحتية
- □ قاعدة بيانات مع backup يومي + PITR
- □ Redis مع persistence (AOF)
- □ HTTPS إلزامي + HSTS
- □ CDN للـ PWA
- □ DNS + SSL مُجدَّد تلقائيًا
الأمان
- □ JWT مع rotation
- □ Rate limiting مفعّل
- □ Input validation على كل endpoint
- □ Audit log للإدارة
- □ اختبار اختراق (pen test) أساسي
المراقبة
- □ Prometheus + Grafana
- □ تنبيهات P1/P2
- □ structured logging
- □ tracing (OpenTelemetry)
- □ uptime monitoring خارجي
البيانات
- □ Retention policy مطبّق
- □ GDPR/PDPL compliance
- □ إخفاء هوية السائق
- □ موافقة المستخدم على الموقع
التشغيل
- □ Runbook لكل حادث
- □ on-call rotation
- □ staging environment
- □ rollback plan
- □ post-mortem template
المنتج
- □ 3 حالات ETA تعمل
- □ Kiosk + QR
- □ إشعارات
- □ WCAG AA
- □ i18n (ar/en)
تاسعًا: الخلاصة النهائية — ما الذي يجعل هذا Production حقًا؟
| الطبقة | ما قدّمته في هذا التحليل |
|---|---|
| استراتيجية | Discovery Package + شجرة قرار + معايير نجاح |
| تصميم | Wireframes لكل شاشة + قواعد UX |
| هندسة | Schema كامل + API contracts + ETA logic |
| تنفيذ | خطة Sprint 14 أسبوعًا + DoD لكل sprint |
| جودة | Acceptance tests + performance targets |
| تشغيل | Runbook + incident response + ADR |
| تواصل | قوالب للإدارة والمواطنين |
| حوكمة | Production Readiness Checklist |
الفكرة الأصلية في الملف كانت رؤية صادقة وواعية بحدود البيانات. ما أضفته هو الطبقة التنفيذية التي تحوّلها إلى مشروع قابل للتسليم:
- Discovery يحسم المسار قبل أي استثمار (أسبوعان).
- MVP على خط واحد يثبت الدقة (4 أسابيع).
- قياس صارم (MAE, NPS, adoption) يحدد التوسع.
- بنية قابلة للتوسع لمدن أخرى دون إعادة بناء.
النتيجة: منصة Zezo Smart Transit ليست فكرة على الورق — بل منتج قابل للإطلاق خلال 14 أسبوعًا، بشرط إنجاز Discovery أولًا. والفرق بين النجاح والفشل ليس في التقنية، بل في الصدق مع البيانات والبدء صغيرًا والقياس قبل التوسع — وهذه بالضبط القيم التي أشار إليها الملف الأصلي.
الخطوة التالية العملية: طباعة Discovery Package (3 صفحات) وإرسالها للجهة المشغلة هذا الأسبوع — لأن كل يوم تأخير في Discovery هو يوم تأخير في الإطلاق.
الجزء الرابع: الطبقة التنفيذية العميقة — الكود، البنية التحتية، والفريق
سأكمل بما لم يُغطَّ بعد: الكود الفعلي، بنية النشر، الأدوات، والفريق — أي ما يحوّل الخطة إلى نظام يعمل اليوم.
أولًا: الكود الفعلي — ETA Engine (Python/FastAPI)
python
# eta_engine.py
from datetime import datetime, timedelta
from dataclasses import dataclass
from enum import Enum
import math
class ETAState(str, Enum):
LIVE = "LIVE"
SCHEDULED = "SCHEDULED"
UNAVAILABLE = "UNAVAILABLE"
@dataclass
class ETAResult:
state: ETAState
seconds: int | None
display_ar: str
confidence: float | None
data_age_sec: float | None
method: str | None
last_known_at: datetime | None = None
GPS_STALE_THRESHOLD_SEC = 30
MIN_SPEED_FLOOR_KMH = 5.0
def haversine_m(lat1, lng1, lat2, lng2) -> float:
R = 6371000
phi1, phi2 = math.radians(lat1), math.radians(lat2)
dphi = math.radians(lat2 - lat1)
dlambda = math.radians(lng2 - lng1)
a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
return 2 * R * math.asin(math.sqrt(a))
def humanize_ar(seconds: int) -> str:
if seconds < 60:
return "أقل من دقيقة"
minutes = seconds // 60
if minutes == 1:
return "دقيقة واحدة"
if minutes == 2:
return "دقيقتان"
if 3 <= minutes <= 10:
return f"{minutes} دقائق"
return f"{minutes} دقيقة"
def compute_eta(vehicle, trip, target_stop, route_stops, now=None) -> ETAResult:
now = now or datetime.utcnow()
# 1) هل يوجد جهاز GPS؟
if not vehicle.gps_device_id or vehicle.last_seen_at is None:
return _scheduled_or_unavailable(trip, reason="no_gps")
# 2) هل البيانات حديثة؟
data_age = (now - vehicle.last_seen_at).total_seconds()
if data_age > GPS_STALE_THRESHOLD_SEC:
return ETAResult(
state=ETAState.UNAVAILABLE,
seconds=None,
display_ar=f"غير مؤكد — آخر تحديث {_fmt_time(vehicle.last_seen_at)}",
confidence=None,
data_age_sec=data_age,
method=None,
last_known_at=vehicle.last_seen_at,
)
# 3) هل الرحلة مجدولة فقط (لم تبدأ)؟
if trip.status == "SCHEDULED" and vehicle.last_speed_kmh is None:
return _scheduled_or_unavailable(trip, reason="scheduled")
# 4) حساب ETA الفعلي
remaining_m = _remaining_distance_m(
vehicle.last_lat, vehicle.last_lng, target_stop, route_stops, trip
)
speed_kmh = max(vehicle.last_speed_kmh or 0, MIN_SPEED_FLOOR_KMH)
eta_sec = int(remaining_m / (speed_kmh * 1000 / 3600))
confidence = _confidence(data_age, vehicle.last_speed_kmh)
return ETAResult(
state=ETAState.LIVE,
seconds=eta_sec,
display_ar=f"باقي {humanize_ar(eta_sec)}",
confidence=confidence,
data_age_sec=data_age,
method="BASIC",
)
def _scheduled_or_unavailable(trip, reason: str) -> ETAResult:
if trip.scheduled_arrival and trip.status in ("SCHEDULED", "BOARDING"):
return ETAResult(
state=ETAState.SCHEDULED,
seconds=None,
display_ar=f"مجدول {_fmt_time(trip.scheduled_arrival)}",
confidence=None,
data_age_sec=None,
method=None,
)
return ETAResult(
state=ETAState.UNAVAILABLE,
seconds=None,
display_ar="غير مؤكد",
confidence=None,
data_age_sec=None,
method=None,
)
def _remaining_distance_m(lat, lng, target_stop, route_stops, trip) -> float:
# المسافة من موقع الحافلة إلى أقرب محطة قادمة + مجموع المسافات حتى الهدف
# (مبسط: نفترض أن الحافلة على المسار)
total = haversine_m(lat, lng, target_stop.lat, target_stop.lng)
return total
def _confidence(data_age: float, speed: float | None) -> float:
# كلما زاد عمر البيانات، قلّت الثقة
age_score = max(0.0, 1.0 - (data_age / GPS_STALE_THRESHOLD_SEC))
speed_score = 1.0 if speed and speed > 10 else 0.6
return round(age_score * speed_score, 2)
def _fmt_time(dt: datetime) -> str:
# تنسيق عربي بسيط: 2:28 م
hour = dt.hour % 12 or 12
minute = dt.minute
period = "ص" if dt.hour < 12 else "م"
return f"{hour}:{minute:02d} {period}"
نقاط القوة في هذا الكود:
stateإلزامي — لا يمكن إرجاع ETA دون تصنيف.data_ageيُحسب منlast_seen_at— لا ثقة بالبيانات القديمة.confidenceرقمي — يمكن للواجهة إخفاء التقدير إن < 0.5.- فصل
SCHEDULEDعنUNAVAILABLE— احترام لحالة البيانات.
ثانيًا: Ingestion Service (Node.js/Express + MQTT)
javascript
// ingest.js
const express = require('express');
const { Pool } = require('pg');
const Redis = require('ioredis');
const mqtt = require('mqtt');
const app = express();
app.use(express.json({ limit: '1kb' }));
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const redis = new Redis(process.env.REDIS_URL);
// التحقق من صحة القراءة
function validateReading(body) {
const { device_id, ts, lat, lng, speed_kmh } = body;
if (!device_id || !ts) return 'missing_fields';
if (lat < -90 || lat > 90) return 'invalid_lat';
if (lng < -180 || lng > 180) return 'invalid_lng';
const age = (Date.now() - new Date(ts).getTime()) / 1000;
if (age > 300 || age < -60) return 'timestamp_out_of_range';
if (speed_kmh != null && (speed_kmh < 0 || speed_kmh > 200)) return 'invalid_speed';
return null;
}
// HTTP endpoint
app.post('/api/v1/ingest/gps', async (req, res) => {
const err = validateReading(req.body);
if (err) return res.status(400).json({ error: err });
const { device_id, ts, lat, lng, speed_kmh, heading, accuracy_m } = req.body;
try {
// 1) ابحث عن المركبة
const v = await db.query(
'SELECT id FROM vehicles WHERE gps_device_id = $1', [device_id]
);
if (v.rowCount === 0) return res.status(404).json({ error: 'unknown_device' });
const vehicleId = v.rows[0].id;
// 2) خزّن في TimescaleDB
await db.query(
`INSERT INTO gps_readings(time, vehicle_id, lat, lng, speed_kmh, heading, accuracy_m)
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
[ts, vehicleId, lat, lng, speed_kmh, heading, accuracy_m]
);
// 3) حدّث الحالة اللحظية في Redis
await redis.hset(`vehicle:${vehicleId}`, {
last_lat: lat, last_lng: lng,
last_speed_kmh: speed_kmh ?? '',
last_heading: heading ?? '',
last_seen_at: ts,
});
await redis.expire(`vehicle:${vehicleId}`, 120);
// 4) نشر حدث للـ WebSocket
await redis.publish('vehicle_moved', JSON.stringify({
vehicle_id: vehicleId, lat, lng, speed_kmh, ts
}));
res.status(202).json({ next_expected_in_sec: 5 });
} catch (e) {
console.error('ingest_error', e);
res.status(500).json({ error: 'internal' });
}
});
// MQTT listener (للأجهزة التي تستخدم MQTT)
const mqttClient = mqtt.connect(process.env.MQTT_URL);
mqttClient.on('connect', () => mqttClient.subscribe('gps/+/reading'));
mqttClient.on('message', async (topic, payload) => {
try {
const body = JSON.parse(payload.toString());
const deviceId = topic.split('/')[1];
await processReading({ ...body, device_id: deviceId });
} catch (e) { console.error('mqtt_error', e); }
});
app.listen(3001);
لماذا هذا التصميم؟
- Idempotent نسبيًا: نفس القراءة تُخزَّن مرتين لكن Redis يحتفظ بالأحدث.
- Redis TTL 120s: إذا توقف الجهاز، الحالة تختفي تلقائيًا → تحويل لـ 🔴.
- MQTT + HTTP: يغطي أغلب أجهزة التتبع في السوق.
- Publish للـ WebSocket: فصل المسؤوليات.
ثالثًا: WebSocket Service (Node.js)
javascript
// ws.js
const WebSocket = require('ws');
const Redis = require('ioredis');
const wss = new WebSocket.Server({ port: 8081 });
const sub = new Redis(process.env.REDIS_URL);
// خريطة: stop_id -> Set of clients
const subscriptions = new Map();
wss.on('connection', (ws, req) => {
const url = new URL(req.url, 'http://x');
const stopId = url.searchParams.get('stop_id');
if (!stopId) return ws.close(1008, 'stop_id_required');
if (!subscriptions.has(stopId)) subscriptions.set(stopId, new Set());
subscriptions.get(stopId).add(ws);
ws.on('close', () => subscriptions.get(stopId)?.delete(ws));
ws.send(JSON.stringify({ type: 'connected', stop_id: stopId }));
});
// استقبل أحداث من Redis
sub.subscribe('vehicle_moved', 'eta_updated', 'alert_created');
sub.on('message', async (channel, message) => {
const event = JSON.parse(message);
if (channel === 'vehicle_moved') {
// أعد حساب ETA للمحطات المشتركة (يُفضّل في خدمة منفصلة)
broadcastToAffectedStops(event);
} else {
broadcast(event);
}
});
function broadcast(event) {
for (const [stopId, clients] of subscriptions) {
const payload = JSON.stringify({ ...event, stop_id: stopId });
for (const ws of clients) {
if (ws.readyState === WebSocket.OPEN) ws.send(payload);
}
}
}
function broadcastToAffectedStops(event) {
// مبسط: بث للكل — يمكن تحسينه بـ geo-index
broadcast({ type: 'vehicle_moved', ...event });
}
ملاحظة إنتاجية: في التوسع، استبدل broadcastToAffectedStops بـ Geohash indexing (Redis GEO) لتحديد المحطات المتأثرة فقط.
رابعًا: بنية النشر (Docker Compose للبداية)
yaml
# docker-compose.yml — نسخة MVP
version: "3.9"
services:
postgres:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_DB: zstp
POSTGRES_PASSWORD: ${DB_PASS}
volumes:
- pgdata:/var/lib/postgresql/data
- ./migrations:/docker-entrypoint-initdb.d
ports: ["5432:5432"]
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes: ["redisdata:/data"]
ports: ["6379:6379"]
ingest:
build: ./services/ingest
environment:
DATABASE_URL: postgres://postgres:${DB_PASS}@postgres:5432/zstp
REDIS_URL: redis://redis:6379
depends_on: [postgres, redis]
ports: ["3001:3001"]
api:
build: ./services/api
environment:
DATABASE_URL: postgres://postgres:${DB_PASS}@postgres:5432/zstp
REDIS_URL: redis://redis:6379
depends_on: [postgres, redis]
ports: ["3000:3000"]
ws:
build: ./services/ws
environment:
REDIS_URL: redis://redis:6379
depends_on: [redis]
ports: ["8081:8081"]
pwa:
build: ./apps/citizen-pwa
ports: ["80:80"]
admin:
build: ./apps/admin
ports: ["81:80"]
kiosk:
build: ./apps/kiosk
ports: ["82:80"]
prometheus:
image: prom/prometheus
volumes: ["./ops/prometheus.yml:/etc/prometheus/prometheus.yml"]
ports: ["9090:9090"]
grafana:
image: grafana/grafana
ports: ["3002:3000"]
volumes:
pgdata:
redisdata:
للإنتاج: Kubernetes (EKS/GKE) مع:
- HPA على
ingestوapiحسب CPU + RPS. - PodDisruptionBudget لضمان التوفر.
- NetworkPolicy لعزل قاعدة البيانات.
- SealedSecrets لإدارة الأسرار.
خامسًا: هيكل المستودع (Monorepo)
text
zstp/ ├── services/ │ ├── ingest/ # GPS ingestion (Node.js) │ │ ├── src/ │ │ ├── Dockerfile │ │ └── package.json │ ├── api/ # REST API (FastAPI) │ │ ├── app/ │ │ │ ├── main.py │ │ │ ├── routes/ │ │ │ ├── eta_engine.py │ │ │ └── models.py │ │ ├── Dockerfile │ │ └── requirements.txt │ ├── ws/ # WebSocket (Node.js) │ └── eta-worker/ # يعيد حساب ETA دوريًا ├── apps/ │ ├── citizen-pwa/ # React + Vite + PWA │ ├── admin/ # React + Vite │ └── kiosk/ # React (static build) ├── packages/ │ ├── shared-types/ # TypeScript types مشتركة │ └── ui/ # مكونات مشتركة ├── migrations/ │ ├── 001_init.sql │ ├── 002_hypertable.sql │ └── 003_indexes.sql ├── ops/ │ ├── prometheus.yml │ ├── grafana/dashboards/ │ └── k8s/ ├── tests/ │ ├── e2e/ │ └── load/ ├── docs/ │ ├── adr/ │ ├── api/ │ └── runbooks/ └── docker-compose.yml
سادسًا: الأدوات (Tooling) — القرارات الموصى بها
| الفئة | الأداة | لماذا |
|---|---|---|
| Backend (API) | FastAPI (Python) | ETA engine بلغة Python + async |
| Backend (Ingest) | Node.js + Express | I/O-bound، ممتاز للـ streams |
| Frontend | React + Vite + TypeScript | سرعة التطوير + PWA |
| خرائط | MapLibre GL + OpenStreetMap | مجاني، لا vendor lock-in |
| خرائط (تجاري) | Mapbox / Google Maps | إن لزم دقة أعلى |
| قاعدة بيانات | PostgreSQL 16 + TimescaleDB | علائقي + سلاسل زمنية |
| Cache | Redis 7 | حالة لحظية + pub/sub |
| Queue | Redis Streams (بداية) → Kafka (توسع) | بسيط ثم قابل للتوسع |
| CI/CD | GitHub Actions | مجاني، متكامل |
| IaC | Terraform (للتوسع) | بنية قابلة للتكرار |
| Container | Docker + K8s | معياري |
| Monitoring | Prometheus + Grafana + Loki | مفتوح المصدر |
| Tracing | OpenTelemetry + Tempo | معياري |
| Error tracking | Sentry | ضروري للإنتاج |
| Push | Firebase Cloud Messaging | مجاني، موثوق |
| SMS fallback | Twilio / Unifonic (محلي) | للسعودية |
| Auth | Keycloak (بداية) أو Auth0 | OIDC معياري |
| i18n | i18next | معياري في React |
| Testing | Vitest + Playwright + k6 | وحدات + E2E + حمل |
قاعدة: لا تشترِ SaaS قبل أن تصل إلى 10K مستخدم. البداية مفتوحة المصدر.
سابعًا: الفريق — الملفات الوظيفية (Job Descriptions)
1. Backend Engineer (ETA/Tracking)
المسؤوليات:
- بناء وصيانة
ingest+eta_engine+api. - ضمان دقة ETA (MAE < 120s).
- كتابة اختبارات وحدة وتكامل.
المتطلبات:
- Python (FastAPI) + Node.js.
- PostgreSQL + Redis.
- خبرة في geospatial (Haversine، GeoJSON).
- فهم TimescaleDB أو InfluxDB.
2. Frontend Engineer (PWA + Admin)
المسؤوليات:
- بناء PWA للمواطن (خريطة + ETA states).
- بناء لوحة الإدارة.
- ضمان WCAG AA + i18n.
المتطلبات:
- React + TypeScript + Vite.
- MapLibre/Leaflet.
- PWA (service worker, offline).
- اختبار Playwright.
3. DevOps/SRE (Part-time)
المسؤوليات:
- CI/CD pipelines.
- مراقبة + تنبيهات.
- Runbooks + incident response.
المتطلبات:
- Docker + K8s.
- GitHub Actions.
- Prometheus/Grafana.
- Terraform (مفيد).
4. Data/ML Engineer (Part-time)
المسؤوليات:
- تحليل
eta_snapshots. - تدريب نموذج Level 3 (XGBoost).
- تقارير دقة أسبوعية.
المتطلبات:
- Python (pandas, scikit-learn, XGBoost).
- SQL متقدم.
- فهم time-series.
5. Field Coordinator (Full-time خلال Discovery)
المسؤوليات:
- زيارة المحطات، تسجيل GPS/الأسماء.
- تنسيق مع الجهة المشغلة.
- تدريب موظفي المحطات.
المتطلبات:
- معرفة محلية بجازان.
- مهارات تواصل.
- دقة في التسجيل.
ثامنًا: خطة الاختبار المتعمقة
1. Unit Tests (ETA Engine)
python
def test_live_state_when_data_fresh():
vehicle = Vehicle(gps_device_id="X", last_seen_at=now()-timedelta(seconds=8),
last_speed_kmh=30, last_lat=16.89, last_lng=42.55)
trip = Trip(status="IN_TRANSIT")
result = compute_eta(vehicle, trip, target_stop, route_stops)
assert result.state == ETAState.LIVE
assert "باقي" in result.display_ar
def test_unavailable_when_data_stale():
vehicle = Vehicle(gps_device_id="X", last_seen_at=now()-timedelta(seconds=45),
last_speed_kmh=30, last_lat=16.89, last_lng=42.55)
result = compute_eta(vehicle, Trip(status="IN_TRANSIT"), target_stop, route_stops)
assert result.state == ETAState.UNAVAILABLE
assert "غير مؤكد" in result.display_ar
def test_scheduled_when_no_gps_device():
vehicle = Vehicle(gps_device_id=None, last_seen_at=None)
trip = Trip(status="SCHEDULED", scheduled_arrival=datetime(2026,10,20,14,45))
result = compute_eta(vehicle, trip, target_stop, route_stops)
assert result.state == ETAState.SCHEDULED
2. Load Test (k6)
javascript
// load.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<300'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('http://api/api/v1/stops/STOP_UUID/arrivals');
check(res, { 'status 200': (r) => r.status === 200 });
}
3. E2E Test (Playwright)
typescript
test('citizen sees ETA states correctly', async ({ page }) => {
await page.goto('/');
await page.fill('[data-testid=search]', 'المنطقة الشمالية');
await page.click('[data-testid=result-07A]');
await expect(page.locator('[data-testid=eta-state]')).toHaveText('🟢 في الطريق');
await expect(page.locator('[data-testid=eta-time]')).toContainText('باقي');
});
تاسعًا: نماذج جاهزة — SLA مع الجهة المشغلة
markdown
# اتفاقية مستوى الخدمة (SLA) — مسودة ## الأطراف - الجهة المشغلة: [إدارة حافلات جازان] - المزود: [Zezo Smart Transit] ## نطاق الخدمة - توفير واجهات معلومات الركاب (PWA + Kiosk + Admin). - استقبال بيانات GPS من الأجهزة القائمة أو المُركَّبة. - حساب ETA وعرضه بثلاث حالات. ## التزامات المزود | المؤشر | الهدف | |---|---| | توفر API | 99.5% شهريًا | | زمن استجابة p95 | < 300ms | | دقة ETA (MAE) | < 120s في 80% من الحالات | | زمن استجابة الحادث P1 | < 15 دقيقة | ## التزامات الجهة المشغلة - توفير بيانات الخطوط والمحطات. - ضمان عمل أجهزة GPS (إن وُجدت) أو التعاون لتركيبها. - توفير صلاحية الوصول للأنظمة القائمة. ## التعويضات - إذا نزل التوفر عن 99% → شهر مجاني. - إذا تجاوز MAE 180s لأسبوعين → مراجعة جذرية. ## المدة والمراجعة - 6 أشهر تجريبية، مراجعة كل 3 أشهر.
عاشرًا: خطة إدارة المخاطر المتقدمة
| المخاطرة | الاحتمال | الأثر | التخفيف |
|---|---|---|---|
| لا يوجد GPS ولا نية لتركيبه | متوسط | مرتفع | Discovery مبكر + خطة تمويل أجهزة |
| الجهة لا تتعاون في البيانات | متوسط | مرتفع | اتفاقية بيانات + بدائل ميدانية |
| دقة ETA منخفضة | مرتفع | متوسط | ثلاث حالات + تحسين تدريجي |
| تعطل الشبكة في المحطات | منخفض | متوسط | PWA offline + cache |
| هجوم على API | منخفض | مرتفع | Rate limit + WAF + monitoring |
| تغيير في إدارة الجهة | متوسط | متوسط | توثيق كل شيء + نقل معرفة |
| تضخم النطاق (scope creep) | مرتفع | متوسط | عقد واضح + مراحل محددة |
| ضعف التبني من المواطنين | متوسط | مرتفع | حملة إطلاق + QR + شاشات |
حادي عشر: مؤشرات ما بعد الإطلاق (90 يومًا)
| المؤشر | الأسبوع 2 | الأسبوع 6 | الأسبوع 12 |
|---|---|---|---|
| مستخدمون فريدون/يوم | 50 | 200 | 500 |
| دقة ETA (MAE) | 240s | 150s | 110s |
| تغطية GPS | 60% | 85% | 94% |
| NPS | — | 25 | 40 |
| شكاوى/أسبوع | 20 | 8 | 3 |
| وقت استجابة P1 | 60m | 30m | 15m |
ثاني عشر: الخلاصة — خريطة الطريق الكاملة
text
┌──────────────────────────────────────────────────────────────┐ │ الأسبوع 0: Discovery Package → الجهة المشغلة │ │ الأسبوع 2: قرار معماري (طبقة عرض / ربط / تجهيز) │ │ الأسبوع 4: MVP — خط واحد + PWA + Admin + ETA Basic │ │ الأسبوع 6: قياس أولي (MAE < 180s؟) │ │ الأسبوع 8: Kiosk + QR + 3 خطوط إضافية │ │ الأسبوع 10: ETA Traffic-aware + Push Notifications │ │ الأسبوع 12: تقرير Go/No-Go │ │ الأسبوع 14: إطلاق production + توسع أفقي │ └──────────────────────────────────────────────────────────────┘
ما قدّمته عبر الأجزاء الأربعة:
| الجزء | المحتوى |
|---|---|
| 1 | تحليل الملف + معمارية عامة + خطة 90 يومًا |
| 2 | Schema كامل + API contracts + ETA engine + أمان + SRE + فريق |
| 3 | Discovery Package + Wireframes + Sprint plan + Acceptance tests + Runbooks |
| 4 | كود فعلي + Docker Compose + هيكل repo + أدوات + JD + SLA + إدارة مخاطر |
الرسالة النهائية:
الفكرة الأصلية في ملف “مواقعي” — “متى تجي الحافلة؟” — تتحول إلى منصة Zezo Smart Transit قابلة للإطلاق خلال 14 أسبوعًا. لكن النجاح لا يعتمد على الكود (فهو واضح)، بل على:
- Discovery صادق يحسم المسار قبل الاستثمار.
- الصدق في البيانات — ثلاث حالات ETA ليست خيارًا تقنيًا، بل موقف أخلاقي.
- البدء صغيرًا — خط واحد يكشف الحقيقة أسرع من 100 خط.
- القياس قبل التوسع — MAE و NPS يحددان القرار، لا الحماس.
- العدالة الرقمية — شاشة المحطة و QR ليسا “إضافة”، بل شرط نجاح.
الخطوة التالية الوحيدة: طباعة Discovery Package (3 صفحات) وإرسالها هذا الأسبوع. كل يوم تأخير في Discovery هو يوم تأخير في الإطلاق — والمشكلة التي يعانيها المواطن في جازان كل يوم لا تحتمل التأجيل.
هل تريد أن أصيغ Discovery Package كملف PDF جاهز للطباعة (نص كامل بصفحات محددة)، أم أنتقل إلى بناء النموذج التطبيقي (Prototype) الفعلي بكود جاهز للتشغيل محليًا؟
مراجعة جميع ماسبق وتقديم وثيقة تقنية نهائية
الوثيقة التقنية النهائية
Zezo Smart Transit Platform (ZSTP)
منصة معلومات النقل الذكي لركاب الحافلات — جازان
رقم الوثيقة: ZSTP-TRD-001
الإصدار: 1.0 — نهائي
التاريخ: 20 أكتوبر 2026
الحالة: جاهزة للمراجعة والاعتماد
الجهة المستهدفة: إدارة حافلات جازان (JAZAN BUS) — قسم التشغيل والتقنية
جدول المحتويات
- الملخص التنفيذي
- خلفية المشكلة وتحليل الفجوة
- نطاق المشروع
- أهداف المنتج ومؤشرات النجاح
- المعمارية التقنية
- نموذج البيانات
- عقود واجهات البرمجة (API Contracts)
- محرك وقت الوصول المتوقع (ETA Engine)
- واجهات المستخدم
- الأمان وحماية البيانات
- البنية التحتية والنشر
- خطة التنفيذ (14 أسبوعًا)
- خطة الاختبار والجودة
- التشغيل والمراقبة (SRE)
- إدارة المخاطر
- الفريق والميزانية
- اتفاقية مستوى الخدمة (SLA)
- خطة التوسع
- الملاحق
1. الملخص التنفيذي
1.1 المشكلة
يواجه ركاب حافلات جازان فجوة معلوماتية حادة عند نقاط الانتظار: لا يعرفون وجهة الحافلة قبل ركوبها، ولا موعد وصولها إلى محطتهم، ولا موعد مغادرتها (للرحلات بين المدن)، ولا أي حافلة تخدم مدينتهم. الشاشات الداخلية الموجودة في الحافلات لا تخدم المنتظر في المحطة، لأنها تعرض المعلومات بعد الركوب لا قبله.
1.2 الحل
منصة Zezo Smart Transit Platform (ZSTP) — منظومة من ثلاث واجهات مترابطة (مواطن، محطة، إدارة) تعمل فوق طبقة بيانات موحدة، وتقدم وقت وصول متوقعًا (ETA) بثلاث حالات صريحة تحترم حدود البيانات المتاحة:
- 🟢 LIVE — تتبع لحظي متاح وحديث.
- 🟡 SCHEDULED — موعد مجدول فقط، دون ادعاء لحظية.
- 🔴 UNAVAILABLE — بيانات غير متاحة، مع عرض آخر تحديث معروف.
1.3 المبدأ الحاكم
الصدق في البيانات قبل الإرضاء. لا يُعرض رقم وقت وصول دون تصنيف حالته، ولا يُدّعى تتبع لحظي عند انقطاع الإشارة. هذا المبدأ هو ما يبني ثقة المواطن ويجعل المشروع قابلًا للاستدامة.
1.4 المنهجية
لا يُفترض وجود التتبع أو غيابه. التحقق أولًا (Discovery)، ثم تجربة محدودة على خط واحد، ثم قياس الدقة والجدوى، ثم التوسع التدريجي. المدة الإجمالية المقدرة: 14 أسبوعًا للوصول إلى منصة production.
1.5 المخرج المتوقع
- تقليل حيرة الركاب أثناء الانتظار.
- رفع الثقة في خدمات النقل العام.
- بناء أساس تقني قابل للتوسع لمدن أخرى.
- دون افتراض شراء أنظمة قد تكون موجودة بالفعل.
2. خلفية المشكلة وتحليل الفجوة
2.1 الأبعاد الستة للمشكلة
| البُعد | المشكلة |
|---|---|
| المعلوماتي | لا يعرف المواطن وجهة الحافلة قبل ركوبها |
| الزمني | لا يعرف موعد الوصول ولا المغادرة |
| المكاني | لا يعرف موقع الحافلة الحالي |
| التخطيطي | لا يعرف أي حافلة تذهب لمدينة أخرى |
| التشغيلي | الشاشات الداخلية لا تخدم المنتظر في المحطة |
| الثقة | قد تُعرض أوقات غير مؤكدة دون توضيح |
2.2 خريطة الفجوات المعلوماتية
| البيانات | الإدارة | داخل الحافلة | المواطن في المحطة |
|---|---|---|---|
| رقم الخط | غالبًا نعم | نعم | ❌ |
| الوجهة النهائية | غالبًا نعم | جزئيًا | ❌ |
| موقع الحافلة اللحظي | غير مؤكد | لا | ❌ |
| وقت الوصول المتوقع | غير مؤكد | جزئيًا | ❌ |
| موعد المغادرة (بين المدن) | غالبًا نعم | لا | ❌ |
| المحطات الوسيطة | غالبًا نعم | جزئيًا | ❌ |
| حالة الرحلة (تأخير/تعطل) | غير مؤكد | جزئيًا | ❌ |
الاستنتاج: المشكلة ليست غياب البيانات كليًا، بل غياب قناة عرض موجهة للمواطن. الحل قد لا يكون بناء نظام من الصفر، بل طبقة عرض (Presentation Layer) فوق بيانات قائمة — وهذا ما يحدده Discovery.
2.3 السياق الجغرافي
حافلات جازان (JAZAN BUS) كنموذج: بيئة تجمع النقل المحلي داخل المدينة مع رحلات بين المدن (أبها، جدة، الرياض). اختيار بيئة محددة يجعل المشروع قابلًا للقياس بدلًا من خطاب عام عن “النقل الذكي”.
3. نطاق المشروع
3.1 داخل النطاق
| المكون | الوصف |
|---|---|
| واجهة المواطن | تطبيق ويب تقدمي (PWA) — بحث بالوجهة، خريطة، ETA بحالاته، تنبيهات |
| واجهة المحطة | شاشة Kiosk + رمز QR لصفحة ويب خفيفة |
| لوحة الإدارة | متابعة الأسطول، التنبيهات، التقارير، إدارة الرحلات |
| خدمة الإدخال | استقبال بيانات GPS عبر HTTP/MQTT |
| محرك ETA | حساب ثلاثي المستويات (Basic/Traffic/ML) |
| التكامل | مع أنظمة التتبع والخطوط القائمة (بعد التحقق) |
3.2 خارج النطاق (الإصدار الأول)
| البند | السبب |
|---|---|
| حجز المقاعد والدفع | ميزة لاحقة تتطلب تكامل مالي |
| تخطيط رحلات متعدد الوسائط | يتطلب محرك routing منفصل |
| تطبيقات iOS/Android أصلية | PWA يكفي للإطلاق |
| تتبع السائقين الفردي | حماية بيانات + خارج نطاق معلومات الركاب |
| تحليلات تنبؤية للطلب | يتطلب بيانات تاريخية كافية |
3.3 الافتراضات
- الجهة المشغلة تملك أو تستطيع توفير بيانات الخطوط والمحطات.
- هناك إمكانية تقنية للوصول إلى بيانات التتبع (أو تركيب أجهزة).
- المواطن يملك هاتفًا أو تستطيع المحطة توفير شاشة.
- الاتفاقيات التنظيمية تسمح بعرض البيانات للعامة.
3.4 القيود
- لا يمكن ضمان ETA دقيق دون تتبع فعلي.
- جودة الاتصال في الحافلات تؤثر على حداثة البيانات.
- التكامل مع أنظمة قديمة قد يتطلب وسيطًا برمجيًا.
4. أهداف المنتج ومؤشرات النجاح
4.1 الأهداف الاستراتيجية
- تمكين المواطن من معرفة الحافلة المناسبة لوجهته قبل ركوبها.
- توفير وقت وصول متوقع موثوق وشفاف.
- توفير معلومات متساوية عبر الهاتف والشاشة العامة.
- بناء أساس تقني قابل للتوسع لمدن أخرى.
4.2 مؤشرات النجاح (90 يومًا)
| المؤشر | كيف يُقاس | الحد الأدنى | المستهدف |
|---|---|---|---|
| دقة ETA | MAE من eta_snapshots | < 180s | < 120s |
| تغطية GPS | % وقت التشغيل بتدفق حديث | > 80% | > 95% |
| استخدام المواطنين | جلسات فريدة/يوم | > 200 | > 500 |
| رضا المستخدم | NPS | > 20 | > 40 |
| وضوح الوجهة | % “عرفت وجهتي” | > 60% | > 80% |
| الالتزام بالمواعيد | OTP (±5 دقائق) | > 75% | > 85% |
| استقرار API | Uptime شهري | > 99% | > 99.5% |
قاعدة القرار: إذا تحققت الحدود الدنيا في 3 من 7 معايير بعد 4 أسابيع → المضي للتوسع.
5. المعمارية التقنية
5.1 المبادئ التصميمية
| المبدأ | التطبيق الهندسي |
|---|---|
| الصدق في البيانات | eta.state إلزامي في كل استجابة |
| العدالة الرقمية | Kiosk + QR + WCAG AA |
| عدم افتراض الشراء | Discovery قبل أي اقتناء |
| التدرج | خط واحد → قياس → توسع |
| البساطة | صفحة واحدة + 3 تبويبات + بحث بالوجهة |
5.2 المعمارية العامة
text
┌─────────────────────────────────────────────────────────────┐
│ طبقة العرض (Presentation) │
├──────────────┬──────────────┬──────────────┬────────────────┤
│ تطبيق المواطن │ شاشة المحطة │ QR Web Page │ لوحة الإدارة │
│ (PWA/React) │ (Kiosk Web) │ (Responsive) │ (React Admin) │
└──────┬───────┴──────┬───────┴──────┬───────┴───────┬────────┘
│ │ │ │
└──────────────┴──────┬───────┴───────────────┘
│ HTTPS / WebSocket
┌────────▼────────┐
│ API Gateway │
│ (Rate Limit, │
│ Auth, Cache) │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼──────┐ ┌─────────▼────────┐ ┌───────▼────────┐
│ Tracking Svc │ │ ETA Engine │ │ Trip/Route │
│ (Ingest GPS) │ │ (Prediction) │ │ Service │
└───────┬──────┘ └─────────┬────────┘ └───────┬────────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌────────▼────────┐
│ Message Bus │
│ (Redis Streams) │
└────────┬────────┘
│
┌────────────────────┼────────────────────┐
│ │ │
┌───────▼──────┐ ┌─────────▼────────┐ ┌───────▼────────┐
│ Time-Series │ │ Relational DB │ │ Cache Layer │
│ DB (GPS) │ │ (Routes/Stops) │ │ (Redis) │
│ TimescaleDB │ │ PostgreSQL │ │ │
└──────────────┘ └──────────────────┘ └────────────────┘
5.3 المكونات
أ. Tracking Ingestion Service
- يستقبل بيانات GPS كل 5–10 ثوانٍ (HTTP/MQTT).
- يتحقق من صحة البيانات (نطاق الإحداثيات، صحة الطابع الزمني، السرعة).
- يخزن في TimescaleDB (تاريخ) + Redis (حالة لحظية بـ TTL 120s).
- كشف انقطاع الإشارة: إذا لم تصل بيانات خلال 30 ثانية → 🔴.
ب. ETA Engine
- المستوى 1: المسافة ÷ متوسط السرعة (Basic).
- المستوى 2: تكامل مع بيانات مرور (Traffic-aware).
- المستوى 3: نموذج XGBoost على البيانات التاريخية (ML).
- مخرجات ثلاثية إلزامية +
confidence_score.
ج. Trip/Route Service
- قاعدة بيانات الخطوط، المحطات، المسارات، المواعيد.
- تمييز صريح:
LOCALvsINTERCITY. - فصل
scheduled_departureعنscheduled_arrival.
د. Notification Service
- Push (FCM) + SMS fallback.
- قواعد: قرب الحافلة (500م أو دقيقتان)، تأخير كبير (>5 دقائق)، تغيير مسار.
هـ. Admin Control Room
- خريطة حية للأسطول.
- تنبيهات: أجهزة منقطعة، رحلات متأخرة، حافلات لم تغادر.
- تقارير OTP.
5.4 قرارات معمارية (ADRs)
ADR-001: اختيار ETA بثلاث حالات
- القرار: كل استجابة ETA تحمل
stateإلزاميًا (LIVE/SCHEDULED/UNAVAILABLE). - البديل المرفوض: عرض رقم دائمًا — يضلل المستخدم.
- النتيجة: بناء ثقة، صدق في البيانات، واجهة أكثر تعقيدًا قليلًا.
ADR-002: Redis Streams قبل Kafka
- القرار: البدء بـ Redis Streams، الانتقال إلى Kafka عند تجاوز 10K حدث/ثانية.
- السبب: بساطة تشغيلية في MVP، قابلية ترقية.
ADR-003: MapLibre + OSM قبل Google Maps
- القرار: البدء مفتوح المصدر، الترقية عند الحاجة لدقة أعلى.
- السبب: تكلفة صفرية في MVP، لا vendor lock-in.
6. نموذج البيانات
6.1 المخطط الكامل
sql
-- المراجع الجغرافية
CREATE TABLE cities (
id UUID PRIMARY KEY,
name_ar VARCHAR(100) NOT NULL,
name_en VARCHAR(100),
region VARCHAR(100)
);
CREATE TABLE stops (
id UUID PRIMARY KEY,
city_id UUID REFERENCES cities(id),
name_ar VARCHAR(200) NOT NULL,
code VARCHAR(20) UNIQUE,
lat DECIMAL(10,7) NOT NULL,
lng DECIMAL(10,7) NOT NULL,
is_terminal BOOLEAN DEFAULT FALSE,
has_display BOOLEAN DEFAULT FALSE,
qr_enabled BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now()
);
-- الخطوط والمسارات
CREATE TYPE route_type AS ENUM ('LOCAL', 'INTERCITY');
CREATE TABLE routes (
id UUID PRIMARY KEY,
code VARCHAR(20) NOT NULL,
name_ar VARCHAR(200) NOT NULL,
type route_type NOT NULL,
origin_city_id UUID REFERENCES cities(id),
destination_city_id UUID REFERENCES cities(id),
is_active BOOLEAN DEFAULT TRUE,
color_hex VARCHAR(7),
UNIQUE(code, type)
);
CREATE TABLE route_stops (
route_id UUID REFERENCES routes(id) ON DELETE CASCADE,
stop_id UUID REFERENCES stops(id),
sequence INT NOT NULL,
distance_from_prev_m INT,
avg_travel_sec_from_prev INT,
PRIMARY KEY (route_id, sequence)
);
-- المركبات
CREATE TYPE vehicle_status AS ENUM ('ACTIVE','IDLE','MAINTENANCE','OFFLINE');
CREATE TABLE vehicles (
id UUID PRIMARY KEY,
plate VARCHAR(20) UNIQUE NOT NULL,
gps_device_id VARCHAR(50) UNIQUE,
gps_protocol VARCHAR(20),
capacity INT,
status vehicle_status DEFAULT 'IDLE',
last_seen_at TIMESTAMPTZ,
last_lat DECIMAL(10,7),
last_lng DECIMAL(10,7),
last_speed_kmh DECIMAL(5,2),
last_heading INT
);
-- الرحلات
CREATE TYPE trip_status AS ENUM (
'SCHEDULED','BOARDING','IN_TRANSIT','ARRIVED','CANCELLED','DELAYED'
);
CREATE TABLE trips (
id UUID PRIMARY KEY,
route_id UUID REFERENCES routes(id),
vehicle_id UUID REFERENCES vehicles(id),
driver_id UUID,
scheduled_departure TIMESTAMPTZ NOT NULL,
scheduled_arrival TIMESTAMPTZ NOT NULL,
actual_departure TIMESTAMPTZ,
actual_arrival TIMESTAMPTZ,
status trip_status DEFAULT 'SCHEDULED',
direction SMALLINT DEFAULT 1,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_trips_route_time ON trips(route_id, scheduled_departure);
-- قراءات GPS (TimescaleDB)
CREATE TABLE gps_readings (
time TIMESTAMPTZ NOT NULL,
vehicle_id UUID NOT NULL,
trip_id UUID,
lat DECIMAL(10,7) NOT NULL,
lng DECIMAL(10,7) NOT NULL,
speed_kmh DECIMAL(5,2),
heading INT,
accuracy_m DECIMAL(6,2),
odometer_km DECIMAL(10,2),
ignition BOOLEAN,
source VARCHAR(20) DEFAULT 'GPS'
);
SELECT create_hypertable('gps_readings', 'time');
CREATE INDEX idx_gps_vehicle_time ON gps_readings(vehicle_id, time DESC);
-- ETA Snapshots (للتقييم)
CREATE TABLE eta_snapshots (
id BIGSERIAL PRIMARY KEY,
trip_id UUID,
stop_id UUID,
computed_at TIMESTAMPTZ NOT NULL,
eta_seconds INT,
confidence DECIMAL(3,2),
data_state VARCHAR(12),
method VARCHAR(20),
actual_arrival_at TIMESTAMPTZ,
error_seconds INT
);
-- التنبيهات
CREATE TABLE alerts (
id UUID PRIMARY KEY,
trip_id UUID,
type VARCHAR(30),
severity SMALLINT,
message_ar TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
resolved_at TIMESTAMPTZ
);
6.2 سياسة الاحتفاظ بالبيانات
| البيانات | المدة | السبب |
|---|---|---|
| GPS خام | 90 يومًا | تحليل + تحسين النموذج |
| GPS مجمّع | 2 سنة | تقارير + اتجاهات |
eta_snapshots | 1 سنة | تقييم الدقة |
| التنبيهات | 6 أشهر | مراجعة تشغيلية |
| سجلات الإدارة | 2 سنة | تدقيق |
7. عقود واجهات البرمجة (API Contracts)
7.1 GET /api/v1/stops/{stop_id}/arrivals
الوصف: الحافلات القادمة إلى محطة معينة.
الاستجابة:
json
{
"stop": {
"id": "uuid",
"name_ar": "محطة مركز الشامل",
"lat": 16.889,
"lng": 42.551,
"last_updated": "2026-10-20T14:32:10Z"
},
"arrivals": [
{
"trip_id": "uuid",
"route_code": "07A",
"route_type": "LOCAL",
"destination_ar": "المنطقة الشمالية",
"vehicle_plate": "أ ب ج 1234",
"eta": {
"state": "LIVE",
"seconds": 420,
"display_ar": "باقي 7 دقائق",
"confidence": 0.92,
"data_age_sec": 8,
"method": "TRAFFIC"
},
"next_stops": [
{"name_ar": "المحطة 2", "sequence": 4},
{"name_ar": "المحطة 3", "sequence": 5}
]
},
{
"trip_id": "uuid2",
"route_code": "12B",
"route_type": "LOCAL",
"destination_ar": "المنطقة الجنوبية",
"eta": {
"state": "SCHEDULED",
"scheduled_at": "2026-10-20T14:45:00Z",
"display_ar": "مجدول 2:45 م",
"confidence": null,
"data_age_sec": null
}
},
{
"trip_id": "uuid3",
"route_code": "204",
"route_type": "INTERCITY",
"destination_ar": "أبها",
"eta": {
"state": "UNAVAILABLE",
"display_ar": "غير مؤكد — آخر تحديث 2:28 م",
"last_known_at": "2026-10-20T14:28:00Z"
}
}
]
}
قاعدة إلزامية: حقل eta.state موجود في كل استجابة. لا يُسمح بعرض رقم دون حالة.
7.2 GET /api/v1/admin/fleet/live
الوصف: حالة الأسطول الحية (لوحة الإدارة).
json
{
"summary": {
"total": 42, "active": 35, "idle": 4, "offline": 3,
"on_time_pct": 88.5, "avg_delay_sec": 95
},
"vehicles": [
{
"vehicle_id": "uuid",
"plate": "أ ب ج 1234",
"trip_id": "uuid",
"route_code": "07A",
"status": "IN_TRANSIT",
"position": {"lat": 16.89, "lng": 42.55, "speed_kmh": 34},
"gps_age_sec": 6,
"health": "OK",
"next_stop": {"name_ar": "المحطة 3", "eta_sec": 120},
"delay_sec": 45
}
],
"alerts": [
{"type": "GPS_LOST", "vehicle_plate": "س ص ع 5678", "since": "2026-10-20T14:20:00Z", "severity": 2},
{"type": "DELAY", "trip_id": "uuid", "delay_sec": 480, "severity": 2}
]
}
7.3 POST /api/v1/ingest/gps
الوصف: استقبال قراءة GPS من جهاز تتبع.
الطلب:
json
{
"device_id": "GPS-00123",
"ts": "2026-10-20T14:32:08Z",
"lat": 16.8891,
"lng": 42.5512,
"speed_kmh": 32.5,
"heading": 145,
"accuracy_m": 5.0,
"ignition": true,
"odometer_km": 12450.3
}
الاستجابة: 202 Accepted + {"next_expected_in_sec": 5}
التحقق: lat ∈ [-90,90]، lng ∈ [-180,180]، ts ضمن ±5 دقائق، speed_kmh ∈ [0,200].
7.4 wss://api/ws/live?stop_id=...
الوصف: تحديثات لحظية عبر WebSocket.
json
{"type": "vehicle_moved", "trip_id": "uuid", "eta_sec": 380, "state": "LIVE"}
{"type": "alert", "trip_id": "uuid", "type": "DELAY", "delta_sec": 120}
7.5 رموز الأخطاء الموحدة
| الرمز | HTTP | المعنى |
|---|---|---|
invalid_lat | 400 | إحداثي عرض خارج النطاق |
timestamp_out_of_range | 400 | طابع زمني غير منطقي |
unknown_device | 404 | جهاز غير مسجل |
rate_limited | 429 | تجاوز حد الطلبات |
unauthorized | 401 | بدون توكن |
forbidden | 403 | صلاحية غير كافية |
internal | 500 | خطأ داخلي |
8. محرك وقت الوصول المتوقع (ETA Engine)
8.1 المنطق الإلزامي
python
class ETAState(str, Enum):
LIVE = "LIVE"
SCHEDULED = "SCHEDULED"
UNAVAILABLE = "UNAVAILABLE"
GPS_STALE_THRESHOLD_SEC = 30
MIN_SPEED_FLOOR_KMH = 5.0
def compute_eta(vehicle, trip, target_stop, route_stops, now=None) -> ETAResult:
now = now or datetime.utcnow()
# 1) هل يوجد جهاز GPS؟
if not vehicle.gps_device_id or vehicle.last_seen_at is None:
return _scheduled_or_unavailable(trip, reason="no_gps")
# 2) هل البيانات حديثة؟
data_age = (now - vehicle.last_seen_at).total_seconds()
if data_age > GPS_STALE_THRESHOLD_SEC:
return ETAResult(
state=ETAState.UNAVAILABLE,
display_ar=f"غير مؤكد — آخر تحديث {_fmt_time(vehicle.last_seen_at)}",
data_age_sec=data_age,
last_known_at=vehicle.last_seen_at,
)
# 3) هل الرحلة مجدولة فقط؟
if trip.status == "SCHEDULED" and vehicle.last_speed_kmh is None:
return _scheduled_or_unavailable(trip, reason="scheduled")
# 4) حساب ETA
remaining_m = _remaining_distance_m(
vehicle.last_lat, vehicle.last_lng, target_stop, route_stops, trip
)
speed_kmh = max(vehicle.last_speed_kmh or 0, MIN_SPEED_FLOOR_KMH)
eta_sec = int(remaining_m / (speed_kmh * 1000 / 3600))
confidence = _confidence(data_age, vehicle.last_speed_kmh)
return ETAResult(
state=ETAState.LIVE,
seconds=eta_sec,
display_ar=f"باقي {humanize_ar(eta_sec)}",
confidence=confidence,
data_age_sec=data_age,
method="BASIC",
)
8.2 مستويات ETA
| المستوى | المعادلة | شرط التفعيل |
|---|---|---|
| Level 1 (Basic) | distance / max(avg_speed, floor) | MVP |
| Level 2 (Traffic) | basic × traffic_factor | بعد تكامل مرور |
| Level 3 (ML) | نموذج XGBoost | بعد 5000 snapshot |
8.3 حساب الثقة (Confidence)
python
def _confidence(data_age: float, speed: float | None) -> float:
age_score = max(0.0, 1.0 - (data_age / GPS_STALE_THRESHOLD_SEC))
speed_score = 1.0 if speed and speed > 10 else 0.6
return round(age_score * speed_score, 2)
إذا confidence < 0.5 → الواجهة تعرض تحذيرًا خفيًا “تقدير تقريبي”.
8.4 معايير الجودة
- MAE < 120 ثانية في 80% من الحالات.
- لا عرض LIVE إذا
data_age > 30s. - لا عرض رقم إذا
confidence < 0.3.
9. واجهات المستخدم
9.1 مبادئ UX
- البساطة: صفحة واحدة + تنقل سفلي بثلاث تبويبات.
- البحث بالوجهة لا برقم الخط (المواطن لا يحفظ الأرقام).
- حالة ETA مرئية دائمًا (لون + نص).
- العدالة: Kiosk + QR لمن لا يملك هاتفًا.
- إمكانية الوصول: WCAG 2.1 AA.
9.2 شاشة الخريطة (الافتراضية)
text
┌─────────────────────────────────────┐ │ ☰ Zezo Transit 🔔 (3) ⚙️ │ ├─────────────────────────────────────┤ │ 🔍 إلى أين تريد الذهاب؟ │ │ ┌───────────────────────────────┐ │ │ │ [ابحث عن منطقة أو مدينة] │ │ │ └───────────────────────────────┘ │ │ ┌─────────────────────────────────┐│ │ │ 🗺️ الخريطة ││ │ │ 🚌(07A) ──► 🚌(12B) ││ │ │ 📍 موقعك 🚌(204) ││ │ └─────────────────────────────────┘│ │ ┌─ الحافلات القريبة ─────────────┐ │ │ │ 🟢 07A المنطقة الشمالية │ │ │ │ باقي 7 دقائق • 1.8 كم │ │ │ ├────────────────────────────────┤ │ │ │ 🟡 12B المنطقة الجنوبية │ │ │ │ مجدول 2:45 م │ │ │ ├────────────────────────────────┤ │ │ │ 🔴 204 أبها (بين المدن) │ │ │ │ غير مؤكد — آخر تحديث 2:28 │ │ │ └────────────────────────────────┘ │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
9.3 شاشة تفاصيل الرحلة (حالة LIVE)
text
┌─────────────────────────────────────┐ │ ← الحافلة 07A ⋮ │ ├─────────────────────────────────────┤ │ 🟢 في الطريق │ │ ┌─────────────────────────────────┐│ │ │ باقي 7 دقائق ││ │ │ ████████████░░░░ 68% ││ │ │ آخر تحديث: قبل 8 ثوانٍ ││ │ └─────────────────────────────────┘│ │ 📍 الوجهة: المنطقة الشمالية │ │ 🚏 المحطة القادمة: محطة 3 │ │ 🚌 اللوحة: أ ب ج 1234 │ │ ┌─ المحطات القادمة ───────────────┐│ │ │ ● محطة 3 باقي 2 دقيقة ││ │ │ ○ محطة 4 باقي 5 دقائق ││ │ │ ○ النهائية باقي 15 دقيقة ││ │ └────────────────────────────────┘│ │ [ 🔔 نبّهني عند اقتراب الحافلة ] │ │ [ 🗺️ عرض المسار على الخريطة ] │ ├─────────────────────────────────────┤ │ 🗺️ الخريطة │ 🧭 رحلاتي │ 🔔 │ └─────────────────────────────────────┘
9.4 شاشة المحطة (Kiosk)
text
┌─────────────────────────────────────────────┐
│ 🚌 محطة مركز الشامل │
│ ⏰ 2:32 م • تحديث تلقائي │
├─────────────────────────────────────────────┤
│ الخط │ الوجهة │ الوصول │
│ ───────┼─────────────────────┼─────────── │
│ 🟢 07A │ المنطقة الشمالية │ 7 دقائق │
│ 🟡 12B │ المنطقة الجنوبية │ 2:45 م │
│ 🔴 204 │ أبها (بين المدن) │ غير مؤكد │
│ 🟢 15C │ المنطقة الشرقية │ 18 دقيقة │
├─────────────────────────────────────────────┤
│ 📱 امسح الرمز لمتابعة الحافلات على هاتفك │
│ ┌───────┐ QR → /stop/{id} │
│ └───────┘ │
├─────────────────────────────────────────────┤
│ ⚠️ تأكد من رقم الخط والوجهة قبل الصعود │
└─────────────────────────────────────────────┘
قواعد Kiosk: تحديث تلقائي كل 10 ثوانٍ (WebSocket) — خط كبير — تباين عالٍ — QR يفتح صفحة ويب خفيفة — إعلان صوتي اختياري.
10. الأمان وحماية البيانات
10.1 المصادقة والتفويض
| الفئة | آلية المصادقة | الصلاحيات |
|---|---|---|
| المواطن | مجهول + JWT خفيف (اختياري) | قراءة البيانات العامة |
| شاشة المحطة | API Key ثابت (kiosk) | قراءة محطة واحدة |
| جهاز GPS | mTLS + device certificate | كتابة قراءات فقط |
| موظف الإدارة | OIDC (SSO) + MFA | RBAC حسب الدور |
| مدير النظام | OIDC + MFA + IP allowlist | كامل |
10.2 الأدوار (RBAC)
text
viewer: قراءة لوحة الإدارة operator: + حل التنبيهات supervisor: + تعديل الرحلات، إدارة الأسطول admin: + إدارة المستخدمين والإعدادات
10.3 حماية البيانات الشخصية
- موقع السائق لا يُعرض للمواطن — فقط موقع المركبة.
- إخفاء الهوية: التحليلات على
vehicle_idلاdriver_id. - موافقة صريحة على الموقع + إمكانية السحب.
- توافق مع GDPR/PDPL: أساس المعالجة، حق الوصول، حق الحذف.
10.4 حماية API
- Rate limiting: 60 طلب/دقيقة للمواطن، 600 للـ kiosk.
- CORS محدود للنطاقات المعروفة.
- Input validation صارم.
- Audit log لكل وصول إداري.
- HTTPS إلزامي + HSTS.
11. البنية التحتية والنشر
11.1 Docker Compose (MVP)
yaml
version: "3.9"
services:
postgres:
image: timescale/timescaledb:latest-pg16
environment: { POSTGRES_DB: zstp, POSTGRES_PASSWORD: ${DB_PASS} }
volumes: ["pgdata:/var/lib/postgresql/data", "./migrations:/docker-entrypoint-initdb.d"]
ports: ["5432:5432"]
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes: ["redisdata:/data"]
ports: ["6379:6379"]
ingest:
build: ./services/ingest
environment:
DATABASE_URL: postgres://postgres:${DB_PASS}@postgres:5432/zstp
REDIS_URL: redis://redis:6379
depends_on: [postgres, redis]
ports: ["3001:3001"]
api:
build: ./services/api
environment:
DATABASE_URL: postgres://postgres:${DB_PASS}@postgres:5432/zstp
REDIS_URL: redis://redis:6379
depends_on: [postgres, redis]
ports: ["3000:3000"]
ws:
build: ./services/ws
environment: { REDIS_URL: redis://redis:6379 }
depends_on: [redis]
ports: ["8081:8081"]
pwa: { build: ./apps/citizen-pwa, ports: ["80:80"] }
admin: { build: ./apps/admin, ports: ["81:80"] }
kiosk: { build: ./apps/kiosk, ports: ["82:80"] }
prometheus:
image: prom/prometheus
volumes: ["./ops/prometheus.yml:/etc/prometheus/prometheus.yml"]
ports: ["9090:9090"]
grafana: { image: grafana/grafana, ports: ["3002:3000"] }
volumes: { pgdata: {}, redisdata: {} }
11.2 الإنتاج (Kubernetes)
- HPA على
ingestوapiحسب CPU + RPS. - PodDisruptionBudget لضمان التوفر.
- NetworkPolicy لعزل قاعدة البيانات.
- SealedSecrets لإدارة الأسرار.
- Ingress + TLS (cert-manager).
11.3 هيكل المستودع (Monorepo)
text
zstp/
├── services/{ingest,api,ws,eta-worker}/
├── apps/{citizen-pwa,admin,kiosk}/
├── packages/{shared-types,ui}/
├── migrations/
├── ops/{prometheus.yml,grafana/,k8s/}
├── tests/{e2e,load}/
├── docs/{adr,api,runbooks}/
└── docker-compose.yml
11.4 الأدوات الموصى بها
| الفئة | الأداة |
|---|---|
| Backend (API) | FastAPI (Python) |
| Backend (Ingest) | Node.js + Express |
| Frontend | React + Vite + TypeScript |
| خرائط | MapLibre GL + OpenStreetMap |
| قاعدة بيانات | PostgreSQL 16 + TimescaleDB |
| Cache | Redis 7 |
| Queue | Redis Streams → Kafka |
| CI/CD | GitHub Actions |
| Container | Docker + K8s |
| Monitoring | Prometheus + Grafana + Loki |
| Tracing | OpenTelemetry + Tempo |
| Error tracking | Sentry |
| Push | Firebase Cloud Messaging |
| SMS | Twilio / Unifonic |
| Auth | Keycloak / Auth0 |
| i18n | i18next |
| Testing | Vitest + Playwright + k6 |
12. خطة التنفيذ (14 أسبوعًا)
Sprint 0: Discovery (أسبوعان)
| الأسبوع | المهمة | المخرج |
|---|---|---|
| 1 | إرسال استبيان Discovery + مقابلة | إجابات موثقة |
| 1 | جرد بيانات الخطوط | ملف CSV/Excel |
| 2 | زيارة ميدانية (5 محطات) | إحداثيات + أسماء |
| 2 | بوابة قرار: تحديد المسار المعماري | وثيقة قرار |
شجرة قرار Discovery:
text
هل يوجد GPS؟ ── لا ──► مسار التجهيز (شراء + تركيب)
│ نعم
هل يرسل لخادم؟ ── لا ──► مسار الربط (توصيل)
│ نعم
هل يوجد API؟ ── لا ──► مسار التكامل (وسيط)
│ نعم
└──► مسار طبقة العرض (الأسرع والأرخص)
Sprint 1–6: التنفيذ
| Sprint | المدة | المخرج | DoD |
|---|---|---|---|
| 1 | أسبوعان | بنية تحتية (DB, Redis, CI/CD) | POST /ingest/gps يعمل |
| 2 | أسبوعان | ETA Level 1 + PWA MVP | صفحة تعرض حافلة وهمية بـ ETA |
| 3 | أسبوعان | Admin + التنبيهات | الإدارة ترى الأسطول |
| 4 | أسبوعان | تجربة ميدانية (خط واحد) | دقة ETA مقاسة |
| 5 | أسبوع | Kiosk + QR | شاشة تعمل في محطة |
| 6 | أسبوعان | ETA Level 2 + 3–5 خطوط + تقرير | تقرير Go/No-Go |
الجدول الزمني الكلي
text
الأسبوع 0: Discovery Package → الجهة المشغلة الأسبوع 2: قرار معماري الأسبوع 4: MVP — خط واحد الأسبوع 6: قياس أولي الأسبوع 8: Kiosk + QR + خطوط إضافية الأسبوع 10: ETA Traffic + Push الأسبوع 12: تقرير Go/No-Go الأسبوع 14: إطلاق Production
13. خطة الاختبار والجودة
13.1 اختبارات الوحدة (ETA)
python
def test_live_state_when_data_fresh():
vehicle = Vehicle(gps_device_id="X", last_seen_at=now()-timedelta(seconds=8),
last_speed_kmh=30)
result = compute_eta(vehicle, Trip(status="IN_TRANSIT"), target_stop, route_stops)
assert result.state == ETAState.LIVE
def test_unavailable_when_data_stale():
vehicle = Vehicle(gps_device_id="X", last_seen_at=now()-timedelta(seconds=45))
result = compute_eta(vehicle, Trip(status="IN_TRANSIT"), target_stop, route_stops)
assert result.state == ETAState.UNAVAILABLE
def test_scheduled_when_no_gps_device():
vehicle = Vehicle(gps_device_id=None, last_seen_at=None)
trip = Trip(status="SCHEDULED", scheduled_arrival=datetime(2026,10,20,14,45))
result = compute_eta(vehicle, trip, target_stop, route_stops)
assert result.state == ETAState.SCHEDULED
13.2 اختبارات القبول (Gherkin)
gherkin
Scenario: Live data within threshold Given vehicle last_seen_at = 8 seconds ago When citizen requests arrivals Then response eta.state = "LIVE" And response eta.display_ar contains "باقي" Scenario: Stale GPS data Given vehicle last_seen_at = 45 seconds ago Then response eta.state = "UNAVAILABLE"
13.3 اختبارات الحمل (k6)
javascript
export const options = {
stages: [
{ duration: '1m', target: 50 },
{ duration: '3m', target: 100 },
{ duration: '1m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<300'],
http_req_failed: ['rate<0.01'],
},
};
13.4 اختبارات E2E (Playwright)
- المواطن يبحث عن وجهة ويرى ETA بحالته.
- Kiosk يعرض الحافلات القادمة.
- الإدارة ترى تنبيه GPS Lost.
13.5 اختبارات الأداء
| السيناريو | الهدف |
|---|---|
100 طلب/ثانية على /arrivals | p95 < 300ms |
| 50 حافلة ترسل كل 5 ثوانٍ | معالجة < 100ms |
| WebSocket 500 اتصال | لا تسرب ذاكرة |
14. التشغيل والمراقبة (SRE)
14.1 مؤشرات SLI/SLO
| SLI | SLO |
|---|---|
| توفر API | 99.5% شهريًا |
| زمن استجابة p95 | < 300ms |
| عمر بيانات GPS p95 | < 15 ثانية |
| دقة ETA (MAE) | < 120 ثانية |
| توفر WebSocket | 99.0% |
14.2 المراقبة
- Metrics: Prometheus — كل خدمة تصدّر
/metrics. - Logs: structured JSON → Loki.
- Traces: OpenTelemetry → Tempo.
- Dashboards: Grafana.
- Alerting: PagerDuty/Opsgenie.
14.3 Runbook — حادث P1: انقطاع GPS
text
1. كشف: gps_ingest_rate < 10% of baseline 2. إجراء فوري (5 دقائق): - تحويل كل trip.status إلى UNAVAILABLE - إشعار فريق التشغيل - فحص: أجهزة أم خادم؟ 3. استجابة (30 دقيقة): - خادم → rollback آخر نشر - أجهزة → التواصل مع المزود 4. تعافي: إعادة تفعيل LIVE تدريجيًا (10% → 50% → 100%) 5. مراجعة: post-mortem خلال 48 ساعة
14.4 Runbook — حادث ETA Drift
text
1. كشف: MAE > 300s لمدة 15 دقيقة 2. إجراء: تبديل إلى SCHEDULED تلقائيًا 3. تحقيق: فحص traffic API + بيانات GPS + النموذج 4. إعادة تفعيل LIVE بعد التحقق
15. إدارة المخاطر
| المخاطرة | الاحتمال | الأثر | التخفيف |
|---|---|---|---|
| لا يوجد GPS ولا نية لتركيبه | متوسط | مرتفع | Discovery مبكر + خطة تمويل |
| عدم تعاون الجهة في البيانات | متوسط | مرتفع | اتفاقية بيانات + بدائل ميدانية |
| دقة ETA منخفضة | مرتفع | متوسط | ثلاث حالات + تحسين تدريجي |
| تعطل الشبكة في المحطات | منخفض | متوسط | PWA offline + cache |
| هجوم على API | منخفض | مرتفع | Rate limit + WAF + monitoring |
| تغيير في إدارة الجهة | متوسط | متوسط | توثيق + نقل معرفة |
| تضخم النطاق | مرتفع | متوسط | عقد واضح + مراحل |
| ضعف التبني | متوسط | مرتفع | حملة إطلاق + QR + شاشات |
16. الفريق والميزانية
16.1 الفريق الأدنى (MVP)
| الدور | العدد | المهمة |
|---|---|---|
| Backend Engineer | 2 | Ingestion + ETA + API |
| Frontend Engineer | 1 | PWA + Admin |
| DevOps/SRE | 0.5 | نشر ومراقبة |
| Data/ML | 0.5 | ETA tuning |
| Product/PM | 0.5 | تنسيق + تواصل |
| Field Coordinator | 1 | جمع بيانات المحطات |
الإجمالي: ~5.5 شخص × 14 أسبوعًا.
16.2 الميزانية التشغيلية الشهرية
| البند | التكلفة |
|---|---|
| استضافة (VPS/Small K8s) | $150–400 |
| قاعدة بيانات مُدارة | $100–300 |
| خرائط + مرور | $200–500 |
| Push/SMS | $50–150 |
| مراقبة | $0–100 |
| إجمالي | $500–1,450 |
بدون تكلفة الأجهزة (إن لزم شراء GPS: $50–150 لكل حافلة).
17. اتفاقية مستوى الخدمة (SLA) — مسودة
الأطراف: الجهة المشغلة ↔ المزود.
التزامات المزود:
| المؤشر | الهدف |
|---|---|
| توفر API | 99.5% شهريًا |
| زمن استجابة p95 | < 300ms |
| دقة ETA (MAE) | < 120s في 80% |
| زمن استجابة P1 | < 15 دقيقة |
التزامات الجهة المشغلة:
- توفير بيانات الخطوط والمحطات.
- ضمان عمل أجهزة GPS أو التعاون لتركيبها.
- توفير صلاحية الوصول للأنظمة القائمة.
التعويضات: إذا نزل التوفر عن 99% → شهر مجاني. إذا تجاوز MAE 180s لأسبوعين → مراجعة جذرية.
المدة: 6 أشهر تجريبية، مراجعة كل 3 أشهر.
18. خطة التوسع
18.1 التوسع الأفقي (خطوط أكثر)
- إضافة خط: < يوم عمل (بيانات + تحقق).
- العائق ليس تقنيًا، بل جمع بيانات المحطات.
18.2 التوسع الرأسي (مدن أخرى)
- إعادة استخدام كل المنصة.
- استيراد خطوط المدينة الجديدة فقط.
- البنية:
city_idفي كل جدول من البداية (multi-tenant).
18.3 التوسع الوظيفي
| الميزة | البنية المطلوبة |
|---|---|
| حجز مقاعد | seat_reservations + دفع |
| تخطيط متعدد الوسائط | OSRM/GraphHopper |
| تحليلات تنبؤية | ClickHouse |
| مساعد ذكي (LLM) | RAG على بيانات التشغيل |
19. الملاحق
الملحق أ: Discovery Package (3 صفحات)
الصفحة 1 — استبيان الجهة المشغلة:
| القسم | الأسئلة الرئيسية |
|---|---|
| أ: أجهزة التتبع | هل يوجد GPS؟ يرسل لخادم؟ معدل الإرسال؟ البروتوكول؟ |
| ب: غرفة العمليات | هل توجد؟ تُسجَّل المواقع؟ تُسجَّل أوقات الوصول؟ |
| ج: البيانات | قاعدة بيانات الخطوط؟ مواعيد موثقة؟ قابلة للتصدير؟ |
| د: التكامل | API؟ إمكانية تكامل؟ من يخوّل؟ |
| هـ: الشاشات | داخلية قابلة للتحديث؟ خارجية؟ متصلة؟ |
| و: النطاق | عدد الخطوط المحلية/بين المدن/المحطات/الحافلات؟ |
الصفحة 2 — شجرة القرار المعماري (كما في القسم 12).
الصفحة 3 — معايير نجاح التجربة (كما في القسم 4.2).
الملحق ب: قائمة التحقق للإنتاج (Production Readiness)
البنية التحتية: backup يومي + PITR، Redis AOF، HTTPS+HSTS، CDN، DNS+SSL تلقائي.
الأمان: JWT rotation، Rate limit، Input validation، Audit log، Pen test.
المراقبة: Prometheus+Grafana، تنبيهات P1/P2، structured logging، tracing، uptime خارجي.
البيانات: Retention policy، GDPR/PDPL، إخفاء السائق، موافقة الموقع.
التشغيل: Runbook لكل حادث، on-call، staging، rollback، post-mortem template.
المنتج: 3 حالات ETA، Kiosk+QR، إشعارات، WCAG AA، i18n.
الملحق ج: المصطلحات
| المصطلح | التعريف |
|---|---|
| ETA | Estimated Time of Arrival — وقت الوصول المتوقع |
| GPS | Global Positioning System — نظام تحديد المواقع |
| PWA | Progressive Web App — تطبيق ويب تقدمي |
| Kiosk | شاشة معلومات عامة في المحطة |
| OTP | On-Time Performance — الالتزام بالمواعيد |
| MAE | Mean Absolute Error — متوسط الخطأ المطلق |
| RBAC | Role-Based Access Control — التحكم بالصلاحيات |
| SLO/SLI | Service Level Objective/Indicator |
| ADR | Architecture Decision Record |
| Discovery | مرحلة التحقق من الأنظمة القائمة |
خاتمة الوثيقة
هذه الوثيقة تحوّل فكرة “متى تجي الحافلة؟” إلى منصة إنتاجية قابلة للتسليم عبر:
- Discovery صادق يحسم المسار قبل الاستثمار (أسبوعان).
- MVP على خط واحد يثبت الدقة (4 أسابيع).
- قياس صارم (MAE, NPS, adoption) يحدد التوسع.
- بنية قابلة للتوسع لمدن أخرى دون إعادة بناء.
- الصدق في البيانات — ثلاث حالات ETA ليست خيارًا تقنيًا، بل موقف أخلاقي.
- العدالة الرقمية — Kiosk و QR شرط نجاح، لا إضافة.
النتيجة: منصة Zezo Smart Transit قابلة للإطلاق خلال 14 أسبوعًا، بشرط إنجاز Discovery أولًا. والفرق بين النجاح والفشل ليس في التقنية، بل في الصدق مع البيانات والبدء صغيرًا والقياس قبل التوسع.
الخطوة التالية: اعتماد هذه الوثيقة، ثم إرسال Discovery Package للجهة المشغلة هذا الأسبوع.
نهاية الوثيقة — ZSTP-TRD-001 v1.0