Files
maps-saas/DOCUMENTATION_PORTAL.html

515 lines
38 KiB
HTML

<!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>
<meta name="description" content="دليل تعليمي شامل لمنصة انطلق - تعلم NestJS و PostGIS و Docker من خلال مشروع حقيقي">
<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=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{
--bg:#0b0f1a;--bg2:#111827;--bg3:#1e293b;
--fg:#f1f5f9;--muted:#94a3b8;--dim:#475569;
--teal:#00f5d4;--gold:#fee440;--blue:#818cf8;
--pink:#f472b6;--red:#fb7185;--green:#34d399;
--card:#151e2e;--border:#1e3a5f;--code-bg:#0d1117;
}
body{font-family:'IBM Plex Sans Arabic',system-ui,sans-serif;background:var(--bg);color:var(--fg);line-height:1.9;overflow-x:hidden}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg)}::-webkit-scrollbar-thumb{background:var(--teal);border-radius:10px}
/* Layout */
.sidebar{position:fixed;top:0;right:0;width:300px;height:100vh;background:var(--bg2);border-left:1px solid var(--border);padding:1.5rem;z-index:100;overflow-y:auto;display:flex;flex-direction:column}
.main{margin-right:300px;min-height:100vh}
@media(max-width:1024px){.sidebar{display:none}.main{margin-right:0}}
/* Sidebar */
.logo{display:flex;align-items:center;gap:.75rem;padding-bottom:1.5rem;border-bottom:1px solid var(--border);margin-bottom:1.5rem}
.logo-icon{width:42px;height:42px;background:linear-gradient(135deg,var(--teal),var(--blue));border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:1.2rem;font-weight:900;color:var(--bg)}
.logo h1{font-size:1.3rem;font-weight:700}.logo span{font-size:.65rem;color:var(--muted);display:block}
.nav-group{margin-bottom:1.5rem}
.nav-group-title{font-size:.65rem;text-transform:uppercase;letter-spacing:3px;color:var(--dim);font-weight:700;margin-bottom:.5rem;padding-right:.5rem}
.nav-link{display:flex;align-items:center;gap:.6rem;padding:.6rem .75rem;border-radius:10px;color:var(--muted);text-decoration:none;font-size:.85rem;font-weight:500;transition:all .2s;border:1px solid transparent}
.nav-link:hover,.nav-link.active{background:rgba(0,245,212,.06);color:var(--teal);border-color:rgba(0,245,212,.15)}
.nav-link .num{font-family:'JetBrains Mono',monospace;font-size:.7rem;color:var(--dim);min-width:20px}
/* Hero */
.hero{padding:5rem 4rem;background:linear-gradient(180deg,rgba(0,245,212,.03) 0%,transparent 60%);border-bottom:1px solid var(--border)}
.hero .badge{display:inline-flex;align-items:center;gap:.5rem;background:rgba(0,245,212,.08);border:1px solid rgba(0,245,212,.2);border-radius:999px;padding:.35rem 1rem;font-size:.75rem;color:var(--teal);margin-bottom:1.5rem}
.hero h1{font-size:3.2rem;font-weight:800;line-height:1.3;margin-bottom:1rem;max-width:700px}
.hero h1 em{font-style:normal;background:linear-gradient(135deg,var(--teal),var(--blue));background-clip:text;-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.hero p{font-size:1.1rem;color:var(--muted);max-width:600px;line-height:1.8}
.hero-stats{display:flex;gap:2.5rem;margin-top:2.5rem;flex-wrap:wrap}
.hero-stat{text-align:center}.hero-stat .val{font-size:2rem;font-weight:800;color:var(--teal)}.hero-stat .lbl{font-size:.75rem;color:var(--muted)}
/* Sections */
.chapter{padding:4rem;border-bottom:1px solid var(--border);scroll-margin-top:2rem}
.chapter-header{display:flex;align-items:center;gap:1rem;margin-bottom:2rem}
.chapter-num{font-family:'JetBrains Mono',monospace;font-size:.75rem;color:var(--bg);background:var(--teal);width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-weight:700}
.chapter h2{font-size:1.8rem;font-weight:800}
.chapter p,.chapter li{color:var(--muted);font-size:.95rem}
.chapter ul,.chapter ol{padding-right:1.5rem}
.chapter li{margin-bottom:.5rem}
/* Code Blocks */
.code-card{background:var(--code-bg);border:1px solid var(--border);border-radius:16px;overflow:hidden;margin:1.5rem 0}
.code-header{display:flex;align-items:center;justify-content:space-between;padding:.6rem 1rem;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border);font-size:.75rem}
.code-header .file{color:var(--muted);font-family:'JetBrains Mono',monospace;display:flex;align-items:center;gap:.5rem}
.code-header .file .dot{width:8px;height:8px;border-radius:50%}
.code-header .lang{color:var(--teal);font-family:'JetBrains Mono',monospace;font-weight:600}
pre.code{padding:1.25rem;font-family:'JetBrains Mono',monospace;font-size:.8rem;line-height:1.8;color:#c9d1d9;direction:ltr;text-align:left;overflow-x:auto;margin:0}
.hl-key{color:#ff7b72}.hl-str{color:#a5d6ff}.hl-fn{color:#d2a8ff}.hl-cmt{color:#8b949e;font-style:italic}.hl-dec{color:#79c0ff}.hl-type{color:#ffa657}.hl-num{color:var(--teal)}
/* Info boxes */
.callout{padding:1.25rem 1.5rem;border-radius:12px;margin:1.5rem 0;border-right:4px solid;font-size:.9rem}
.callout.info{background:rgba(129,140,248,.06);border-color:var(--blue);color:var(--blue)}
.callout.tip{background:rgba(0,245,212,.06);border-color:var(--teal)}
.callout.warn{background:rgba(254,228,64,.06);border-color:var(--gold);color:var(--gold)}
.callout strong{display:block;margin-bottom:.3rem}
/* Grid Cards */
.card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.25rem;margin:1.5rem 0}
.card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:1.5rem;transition:all .3s}
.card:hover{border-color:var(--teal);transform:translateY(-4px);box-shadow:0 12px 40px rgba(0,0,0,.4)}
.card .icon{font-size:1.5rem;margin-bottom:.75rem;display:block}
.card h4{font-size:1rem;font-weight:700;margin-bottom:.5rem}
.card p{font-size:.85rem;color:var(--muted)}
/* Diagram */
.diagram{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:2rem;margin:1.5rem 0;text-align:center}
.diagram-flow{display:flex;align-items:center;justify-content:center;gap:.5rem;flex-wrap:wrap;margin:1rem 0}
.diagram-node{background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:.75rem 1.25rem;font-size:.8rem;font-weight:600;min-width:100px}
.diagram-node.active{border-color:var(--teal);color:var(--teal);box-shadow:0 0 20px rgba(0,245,212,.15)}
.diagram-arrow{color:var(--dim);font-size:1.2rem}
/* API Table */
.api-table{width:100%;border-collapse:collapse;margin:1.5rem 0;font-size:.85rem}
.api-table th{text-align:right;padding:.75rem 1rem;background:var(--card);color:var(--teal);font-size:.7rem;text-transform:uppercase;letter-spacing:1px;border-bottom:2px solid var(--border)}
.api-table td{padding:.75rem 1rem;border-bottom:1px solid rgba(255,255,255,.05);color:var(--muted);vertical-align:top}
.api-table tr:hover td{background:rgba(255,255,255,.02)}
.method{font-family:'JetBrains Mono',monospace;font-size:.75rem;font-weight:700;padding:.15rem .5rem;border-radius:4px}
.method.get{color:#34d399;background:rgba(52,211,153,.1)}.method.post{color:#818cf8;background:rgba(129,140,248,.1)}.method.patch{color:#fbbf24;background:rgba(251,191,36,.1)}.method.delete{color:#fb7185;background:rgba(251,113,133,.1)}
.endpoint{font-family:'JetBrains Mono',monospace;font-size:.8rem;color:var(--fg);direction:ltr;display:inline-block}
/* Footer */
footer{padding:3rem 4rem;text-align:center;border-top:1px solid var(--border);color:var(--dim);font-size:.8rem}
/* Animations */
.fade-in{opacity:0;transform:translateY(25px);transition:all .7s cubic-bezier(.16,1,.3,1)}
.fade-in.visible{opacity:1;transform:translateY(0)}
</style>
</head>
<body>
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="logo">
<div class="logo-icon">🧭</div>
<div><h1>انطلق</h1><span>الدليل الهندسي التعليمي v3.0</span></div>
</div>
<nav>
<div class="nav-group">
<div class="nav-group-title">الأساسيات</div>
<a href="#hero" class="nav-link active"><span class="num">00</span> نظرة عامة</a>
<a href="#nestjs-paradigm" class="nav-link"><span class="num">01</span> فلسفة NestJS</a>
<a href="#modules" class="nav-link"><span class="num">02</span> الوحدات والتبعيات</a>
</div>
<div class="nav-group">
<div class="nav-group-title">قاعدة البيانات</div>
<a href="#entities" class="nav-link"><span class="num">03</span> الكيانات و TypeORM</a>
<a href="#geocoding" class="nav-link"><span class="num">04</span> نظام البحث المكاني</a>
</div>
<div class="nav-group">
<div class="nav-group-title">الذكاء المكاني</div>
<a href="#telemetry" class="nav-link"><span class="num">05</span> تتبع السائقين</a>
<a href="#intelligence" class="nav-link"><span class="num">06</span> دورة الـ 10 أيام</a>
</div>
<div class="nav-group">
<div class="nav-group-title">البنية التحتية</div>
<a href="#api-reference" class="nav-link"><span class="num">07</span> مرجع الـ API</a>
<a href="#docker" class="nav-link"><span class="num">08</span> بيئة Docker</a>
<a href="#nginx" class="nav-link"><span class="num">09</span> إعدادات NGINX</a>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="main">
<!-- Hero -->
<header class="hero fade-in" id="hero">
<div class="badge">📘 دليل تعليمي تفاعلي — من الصفر إلى الإنتاج</div>
<h1>تعلّم بناء <em>منصة خرائط ذكية</em> بتقنيات حقيقية.</h1>
<p>هذا الدليل يأخذك في رحلة عملية داخل كود منصة "انطلق" — من تصميم قاعدة البيانات المكانية إلى نشر الحاويات. كل سطر كود هنا مأخوذ من المشروع الفعلي.</p>
<div class="hero-stats">
<div class="hero-stat"><div class="val">4</div><div class="lbl">وحدات NestJS</div></div>
<div class="hero-stat"><div class="val">6</div><div class="lbl">حاويات Docker</div></div>
<div class="hero-stat"><div class="val">3</div><div class="lbl">دول مدعومة</div></div>
<div class="hero-stat"><div class="val">∞</div><div class="lbl">نقاط تتبع</div></div>
</div>
</header>
<!-- Chapter 01: NestJS Paradigm -->
<section class="chapter fade-in" id="nestjs-paradigm">
<div class="chapter-header"><div class="chapter-num">01</div><h2>فلسفة NestJS — لماذا هذا الإطار؟</h2></div>
<p>NestJS يقوم على ثلاثة مبادئ أساسية: <strong>الوحدات (Modules)</strong> لتنظيم الكود، <strong>المتحكمات (Controllers)</strong> لاستقبال الطلبات، و<strong>الخدمات (Services)</strong> لمنطق العمل. كل شيء مربوط بـ <strong>حقن التبعيات (Dependency Injection)</strong>.</p>
<div class="callout info"><strong>💡 المبدأ الأساسي</strong> كل وحدة (Module) هي صندوق مستقل يحتوي على متحكماته وخدماته. الوحدة لا تعرف شيئاً عن الوحدات الأخرى إلا إذا تم "تصديرها" (Export) و"استيرادها" (Import) بشكل صريح.</div>
<p style="margin-top:1rem">نقطة البداية هي ملف <code style="color:var(--teal)">main.ts</code> — هنا يبدأ كل شيء:</p>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--teal)"></span> main.ts</div><div class="lang">TypeScript</div></div>
<pre class="code"><span class="hl-key">import</span> { NestFactory } <span class="hl-key">from</span> <span class="hl-str">'@nestjs/core'</span>;
<span class="hl-key">import</span> { DocumentBuilder, SwaggerModule } <span class="hl-key">from</span> <span class="hl-str">'@nestjs/swagger'</span>;
<span class="hl-key">import</span> { AppModule } <span class="hl-key">from</span> <span class="hl-str">'./app.module'</span>;
<span class="hl-key">async function</span> <span class="hl-fn">bootstrap</span>() {
<span class="hl-key">const</span> app = <span class="hl-key">await</span> NestFactory.<span class="hl-fn">create</span>(AppModule);
<span class="hl-cmt">// تفعيل تبادل الموارد مع الواجهة الأمامية</span>
app.<span class="hl-fn">enableCors</span>();
app.<span class="hl-fn">setGlobalPrefix</span>(<span class="hl-str">'api'</span>); <span class="hl-cmt">// كل المسارات تبدأ بـ /api</span>
<span class="hl-cmt">// إعداد وثائق Swagger التلقائية</span>
<span class="hl-key">const</span> config = <span class="hl-key">new</span> <span class="hl-fn">DocumentBuilder</span>()
.<span class="hl-fn">setTitle</span>(<span class="hl-str">'Jordan Map Platform API'</span>)
.<span class="hl-fn">setVersion</span>(<span class="hl-str">'1.0'</span>).<span class="hl-fn">build</span>();
<span class="hl-key">const</span> port = process.env.API_PORT || <span class="hl-num">3000</span>;
<span class="hl-key">await</span> app.<span class="hl-fn">listen</span>(port);
console.<span class="hl-fn">log</span>(<span class="hl-str">`🚀 API is running on port ${port}`</span>);
}
<span class="hl-fn">bootstrap</span>();</pre>
</div>
<div class="callout tip"><strong>🔗 العلاقة:</strong> <code>main.ts</code> → يُنشئ التطبيق من <code>AppModule</code> → الذي يستورد <code>TelemetryModule</code> + <code>MapsModule</code> + <code>GeocodingModule</code>.</div>
</section>
<!-- Chapter 02: Modules -->
<section class="chapter fade-in" id="modules">
<div class="chapter-header"><div class="chapter-num">02</div><h2>الوحدات والتبعيات — كيف يُنظَّم الكود؟</h2></div>
<p>المشروع مقسم لـ 4 وحدات رئيسية. كل وحدة تعريفها في ملف <code style="color:var(--teal)">*.module.ts</code>:</p>
<div class="diagram">
<div style="color:var(--muted);font-size:.8rem;margin-bottom:1rem">🏗️ خريطة الوحدات</div>
<div class="diagram-flow">
<div class="diagram-node active">AppModule</div>
<div class="diagram-arrow">←</div>
<div class="diagram-node">TelemetryModule</div>
<div class="diagram-arrow">←</div>
<div class="diagram-node">MapsModule</div>
<div class="diagram-arrow">←</div>
<div class="diagram-node">GeocodingModule</div>
</div>
</div>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--blue)"></span> app.module.ts</div><div class="lang">TypeScript</div></div>
<pre class="code"><span class="hl-key">@Module</span>({
<span class="hl-type">imports</span>: [
ConfigModule.<span class="hl-fn">forRoot</span>({ isGlobal: <span class="hl-num">true</span> }),
ScheduleModule.<span class="hl-fn">forRoot</span>(), <span class="hl-cmt">// لتشغيل المهام الدورية (Cron)</span>
TypeOrmModule.<span class="hl-fn">forRootAsync</span>({ <span class="hl-cmt">// اتصال قاعدة البيانات</span>
<span class="hl-type">useFactory</span>: (config) => ({
type: <span class="hl-str">'postgres'</span>,
url: config.<span class="hl-fn">get</span>(<span class="hl-str">'DATABASE_URL'</span>),
autoLoadEntities: <span class="hl-num">true</span>,
}),
}),
TelemetryModule, <span class="hl-cmt">// وحدة تتبع السائقين</span>
MapsModule, <span class="hl-cmt">// وحدة التوجيه والخرائط</span>
GeocodingModule, <span class="hl-cmt">// وحدة البحث المكاني</span>
],
})
<span class="hl-key">export class</span> <span class="hl-type">AppModule</span> {}</pre>
</div>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--gold)"></span> telemetry.module.ts</div><div class="lang">TypeScript</div></div>
<pre class="code"><span class="hl-key">@Module</span>({
<span class="hl-type">imports</span>: [
TypeOrmModule.<span class="hl-fn">forFeature</span>([ <span class="hl-cmt">// ← تسجيل الكيانات</span>
TelemetryLog,
RoadSegmentStat,
CandidateRoad
]),
RedisModule, <span class="hl-cmt">// ← استيراد خدمة Redis</span>
],
<span class="hl-type">controllers</span>: [TelemetryController, MapRefinementController],
<span class="hl-type">providers</span>: [TelemetryService, TelemetryAnalyzerService,
RedisService, ExternalTelemetryService],
<span class="hl-type">exports</span>: [TelemetryService, TelemetryAnalyzerService],
})
<span class="hl-key">export class</span> <span class="hl-type">TelemetryModule</span> {}</pre>
</div>
<div class="callout warn"><strong>⚠️ قاعدة ذهبية:</strong> أي خدمة (Service) يجب أن تكون مُسجلة في <code>providers</code> وأي خدمة تريد مشاركتها يجب وضعها في <code>exports</code>.</div>
</section>
<!-- Chapter 03: Entities -->
<section class="chapter fade-in" id="entities">
<div class="chapter-header"><div class="chapter-num">03</div><h2>الكيانات و TypeORM — تصميم قاعدة البيانات</h2></div>
<p>كل جدول في PostgreSQL يُمثَّل بكيان (Entity). الكيان هو كلاس TypeScript مزيّن بـ <code style="color:var(--teal)">@Entity()</code> يُعرِّف الأعمدة والفهارس:</p>
<div class="card-grid">
<div class="card"><div class="icon">📍</div><h4>TelemetryLog</h4><p>يخزن نقاط GPS من السائقين. يحتوي عمود <code>location</code> من نوع PostGIS <code>geography(Point)</code> مع فهرس مكاني.</p></div>
<div class="card"><div class="icon">🛣️</div><h4>RoadSegmentStat</h4><p>إحصائيات السرعة لكل مقطع طريق. يحتوي <code>congestionFactor</code> و<code>geometry</code> من نوع <code>LineString</code>.</p></div>
<div class="card"><div class="icon">🔍</div><h4>CandidateRoad</h4><p>طرق مُكتشفة من تحليل DBSCAN. تحتوي <code>confidence</code> (0-1) و<code>status</code> (pending/approved/rejected).</p></div>
<div class="card"><div class="icon">🌍</div><h4>BasePlace</h4><p>الكيان الأب لجداول <code>places_syria</code>, <code>places_jordan</code>, <code>places_egypt</code>. يُعرّف الأعمدة المشتركة.</p></div>
</div>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--pink)"></span> telemetry.entity.ts</div><div class="lang">TypeScript</div></div>
<pre class="code"><span class="hl-key">@Entity</span>(<span class="hl-str">'telemetry_logs'</span>)
<span class="hl-key">export class</span> <span class="hl-type">TelemetryLog</span> {
<span class="hl-key">@PrimaryGeneratedColumn</span>()
id: <span class="hl-type">number</span>;
<span class="hl-key">@Column</span>() <span class="hl-key">@Index</span>()
driverId: <span class="hl-type">string</span>;
<span class="hl-key">@Column</span>(<span class="hl-str">'decimal'</span>, { precision: <span class="hl-num">10</span>, scale: <span class="hl-num">7</span> })
latitude: <span class="hl-type">number</span>;
<span class="hl-key">@Column</span>(<span class="hl-str">'float'</span>)
speed: <span class="hl-type">number</span>;
<span class="hl-cmt">// نقطة جغرافية PostGIS للبحث المكاني السريع</span>
<span class="hl-key">@Column</span>({ type: <span class="hl-str">'geography'</span>, spatialFeatureType: <span class="hl-str">'Point'</span>, srid: <span class="hl-num">4326</span> })
<span class="hl-key">@Index</span>({ spatial: <span class="hl-num">true</span> }) <span class="hl-cmt">// ← فهرس GIST للبحث بالمسافة</span>
location: <span class="hl-type">any</span>;
}</pre>
</div>
</section>
<!-- Chapter 04: Geocoding -->
<section class="chapter fade-in" id="geocoding">
<div class="chapter-header"><div class="chapter-num">04</div><h2>نظام البحث المكاني — Forward & Reverse Geocoding</h2></div>
<p>الخدمة تبحث في 3 جداول إقليمية + جدول OSM العالمي. المفتاح: استخدام <code style="color:var(--teal)">::float</code> في استعلامات SQL لتجنب خطأ "النتائج الفارغة":</p>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--green)"></span> geocoding.service.ts — searchPlaces()</div><div class="lang">SQL + TypeScript</div></div>
<pre class="code"><span class="hl-cmt">// تحديد الجدول المناسب بناءً على الإحداثيات</span>
<span class="hl-key">private</span> <span class="hl-fn">getRepositoryForCoords</span>(lat, lng) {
<span class="hl-key">if</span> (lat >= <span class="hl-num">29</span> && lat <= <span class="hl-num">37.5</span> && lng >= <span class="hl-num">34.5</span>) {
<span class="hl-key">if</span> (lat > <span class="hl-num">32.5</span> && lng > <span class="hl-num">35.8</span>) <span class="hl-key">return</span> placesSyriaRepo;
<span class="hl-key">return</span> placesJordanRepo;
}
<span class="hl-key">if</span> (lat >= <span class="hl-num">22</span> && lat <= <span class="hl-num">32</span>) <span class="hl-key">return</span> placesEgyptRepo;
}
<span class="hl-cmt">// الاستعلام المكاني — لاحظ ::float في كل مكان</span>
<span class="hl-key">const</span> query = <span class="hl-str">`
SELECT id, name_ar, latitude, longitude,
ST_DistanceSphere(
location,
ST_SetSRID(ST_MakePoint($3<span class="hl-dec">::float</span>, $2<span class="hl-dec">::float</span>), 4326)
) as distance
FROM ${tableName}
WHERE name_ar % $1 -- بحث تشابهي (Trigram)
ORDER BY distance ASC LIMIT 15
`</span>;</pre>
</div>
<div class="callout tip"><strong>🐛 الخطأ الذي حللناه:</strong> PostGIS كان يقرأ الإحداثيات كـ <code>text</code> بدلاً من <code>float</code>، مما يُنتج نتائج فارغة. الحل: إضافة <code>::float</code> لكل مُعامل رقمي.</div>
</section>
<!-- Chapter 05: Telemetry -->
<section class="chapter fade-in" id="telemetry">
<div class="chapter-header"><div class="chapter-num">05</div><h2>تتبع السائقين — من GPS إلى قاعدة البيانات</h2></div>
<p>كل 3 ثوانٍ، يرسل تطبيق السائق بيانات الموقع. المتحكم يستقبلها والخدمة تحفظها:</p>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--gold)"></span> telemetry.service.ts — ingest()</div><div class="lang">TypeScript</div></div>
<pre class="code"><span class="hl-key">async</span> <span class="hl-fn">ingest</span>(data: DriverTelemetryDto) {
<span class="hl-key">const</span> log = <span class="hl-key">this</span>.telemetryRepo.<span class="hl-fn">create</span>({
driverId: data.driver_id,
latitude: data.latitude, longitude: data.longitude,
speed: data.speed, heading: data.heading,
location: {
type: <span class="hl-str">'Point'</span>,
coordinates: [data.longitude, data.latitude], <span class="hl-cmt">// GeoJSON: [lng, lat]</span>
},
});
<span class="hl-key">await this</span>.telemetryRepo.<span class="hl-fn">save</span>(log);
<span class="hl-key">return</span> { success: <span class="hl-num">true</span>, timestamp: <span class="hl-key">new</span> Date() };
}</pre>
</div>
<div class="callout info"><strong>🔗 سلسلة الاستدعاء:</strong> <code>Flutter App</code> → <code>POST /api/telemetry</code> → <code>TelemetryController.ingest()</code> → <code>TelemetryService.ingest()</code> → <code>PostgreSQL + PostGIS</code></div>
</section>
<!-- Chapter 06: Intelligence Cycle -->
<section class="chapter fade-in" id="intelligence">
<div class="chapter-header"><div class="chapter-num">06</div><h2>دورة الذكاء المكاني — كل 10 أيام</h2></div>
<p>هذا هو قلب المنصة. الخدمة <code style="color:var(--teal)">TelemetryAnalyzerService</code> تعمل تلقائياً عبر <code>@Cron</code> كل فجر لتحليل البيانات:</p>
<div class="diagram">
<div style="color:var(--muted);font-size:.8rem;margin-bottom:1rem">🧠 دورة المعالجة الذكية</div>
<div class="diagram-flow">
<div class="diagram-node">1. مزامنة البيانات</div>
<div class="diagram-arrow">→</div>
<div class="diagram-node active">2. تحليل السرعات</div>
<div class="diagram-arrow">→</div>
<div class="diagram-node">3. اكتشاف الطرق</div>
<div class="diagram-arrow">→</div>
<div class="diagram-node">4. تحديث Redis</div>
</div>
</div>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--red)"></span> telemetry-analyzer.service.ts — discoverNewRoads()</div><div class="lang">PostGIS SQL</div></div>
<pre class="code"><span class="hl-cmt">-- الخطوة 1: إيجاد نقاط بعيدة عن أي طريق معروف (> 15 متر)</span>
<span class="hl-key">WITH</span> off_road_points <span class="hl-key">AS</span> (
<span class="hl-key">SELECT</span> t.id, t.location, t.timestamp, t."driverId"
<span class="hl-key">FROM</span> telemetry_logs t
<span class="hl-key">WHERE</span> t.speed > <span class="hl-num">5</span> <span class="hl-cmt">-- متحرك وليس متوقف</span>
<span class="hl-key">AND NOT EXISTS</span> (
<span class="hl-key">SELECT</span> <span class="hl-num">1</span> <span class="hl-key">FROM</span> planet_osm_line l
<span class="hl-key">WHERE</span> <span class="hl-fn">ST_DWithin</span>(t.location::geography,
<span class="hl-fn">ST_Transform</span>(l.way, <span class="hl-num">4326</span>)::geography, <span class="hl-num">15</span>)
)
),
<span class="hl-cmt">-- الخطوة 2: تجميع النقاط القريبة باستخدام DBSCAN</span>
clustered <span class="hl-key">AS</span> (
<span class="hl-key">SELECT</span> *,
<span class="hl-fn">ST_ClusterDBSCAN</span>(location::geometry,
eps := <span class="hl-num">0.0003</span>, minpoints := <span class="hl-num">5</span>) <span class="hl-key">OVER</span> () <span class="hl-key">AS</span> cluster_id
<span class="hl-key">FROM</span> off_road_points
)
<span class="hl-cmt">-- الخطوة 3: تحويل كل تجمع إلى خط طريق مرشح</span>
<span class="hl-key">SELECT</span> cluster_id,
<span class="hl-fn">COUNT</span>(<span class="hl-key">DISTINCT</span> "driverId") <span class="hl-key">AS</span> unique_drivers,
<span class="hl-fn">ST_AsGeoJSON</span>(<span class="hl-fn">ST_MakeLine</span>(location <span class="hl-key">ORDER BY</span> timestamp)) <span class="hl-key">AS</span> geojson
<span class="hl-key">FROM</span> clustered <span class="hl-key">WHERE</span> cluster_id <span class="hl-key">IS NOT NULL</span>
<span class="hl-key">GROUP BY</span> cluster_id
<span class="hl-key">HAVING COUNT</span>(<span class="hl-key">DISTINCT</span> "driverId") >= <span class="hl-num">2</span></pre>
</div>
</section>
<!-- Chapter 07: API Reference -->
<section class="chapter fade-in" id="api-reference">
<div class="chapter-header"><div class="chapter-num">07</div><h2>مرجع الـ API — كل نقاط النهاية</h2></div>
<p>جميع المسارات محمية بـ <code style="color:var(--teal)">ApiKeyGuard</code> عبر الترويسة <code>x-api-key</code>.</p>
<table class="api-table">
<thead><tr><th>الطريقة</th><th>المسار</th><th>الوصف</th><th>الملف المصدر</th></tr></thead>
<tbody>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/telemetry</span></td><td>استقبال بيانات تتبع السائقين كل 3 ثوانٍ</td><td style="color:var(--dim)">telemetry.controller.ts</td></tr>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/telemetry/sync</span></td><td>مزامنة يدوية من الخادم الخارجي</td><td style="color:var(--dim)">telemetry.controller.ts</td></tr>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/telemetry/process-intelligence</span></td><td>🚀 تنفيذ دورة الذكاء كاملة (مزامنة + تحليل)</td><td style="color:var(--dim)">telemetry.controller.ts</td></tr>
<tr><td><span class="method get">GET</span></td><td><span class="endpoint">/api/maps/route?fromLat&fromLng&toLat&toLng</span></td><td>حساب أقصر مسار عبر GraphHopper</td><td style="color:var(--dim)">maps.controller.ts</td></tr>
<tr><td><span class="method get">GET</span></td><td><span class="endpoint">/api/geocoding/search?q&lat&lng&country</span></td><td>بحث مكاني متعدد الأقاليم</td><td style="color:var(--dim)">geocoding.controller.ts</td></tr>
<tr><td><span class="method get">GET</span></td><td><span class="endpoint">/api/geocoding/reverse?lat&lng</span></td><td>تحويل إحداثيات → عنوان</td><td style="color:var(--dim)">geocoding.controller.ts</td></tr>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/geocoding/upsert-batch</span></td><td>إدخال أماكن بالجملة (Scraper)</td><td style="color:var(--dim)">geocoding.controller.ts</td></tr>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/map-refinement/analyze-speeds</span></td><td>تحليل سرعات الطرق</td><td style="color:var(--dim)">map-refinement.controller.ts</td></tr>
<tr><td><span class="method post">POST</span></td><td><span class="endpoint">/api/map-refinement/discover-roads</span></td><td>اكتشاف طرق جديدة (DBSCAN)</td><td style="color:var(--dim)">map-refinement.controller.ts</td></tr>
<tr><td><span class="method get">GET</span></td><td><span class="endpoint">/api/map-refinement/congestion?north&south&east&west</span></td><td>بيانات الازدحام للخريطة الحرارية</td><td style="color:var(--dim)">map-refinement.controller.ts</td></tr>
<tr><td><span class="method patch">PATCH</span></td><td><span class="endpoint">/api/map-refinement/candidates/:id/approve</span></td><td>الموافقة على طريق مكتشف</td><td style="color:var(--dim)">map-refinement.controller.ts</td></tr>
<tr><td><span class="method delete">DELETE</span></td><td><span class="endpoint">/api/geocoding/places?country&name</span></td><td>حذف مكان حسب الاسم أو المعرّف</td><td style="color:var(--dim)">geocoding.controller.ts</td></tr>
</tbody>
</table>
</section>
<!-- Chapter 08: Docker -->
<section class="chapter fade-in" id="docker">
<div class="chapter-header"><div class="chapter-num">08</div><h2>بيئة Docker — حاويات مترابطة</h2></div>
<p>كل خدمة تعمل في حاوية معزولة. ملف <code style="color:var(--teal)">docker-compose.yml</code> يربطها عبر شبكة داخلية:</p>
<div class="card-grid">
<div class="card"><div class="icon">🐘</div><h4>db (PostGIS)</h4><p>postgis/postgis:15-3.3 — القلب. تخزين كل البيانات المكانية مع فهارس GIST.</p></div>
<div class="card"><div class="icon">🚀</div><h4>api (NestJS)</h4><p>node:20-alpine — العقل المدبر. يعالج الطلبات وينفذ التحليلات.</p></div>
<div class="card"><div class="icon">🗺️</div><h4>martin (Tiles)</h4><p>maplibre/martin — يولّد Vector Tiles مباشرة من PostGIS بسرعة فائقة.</p></div>
<div class="card"><div class="icon">🧭</div><h4>routing (GraphHopper)</h4><p>يحسب أقصر المسارات باستخدام ملف OSM محدث + بيانات الازدحام.</p></div>
<div class="card"><div class="icon">⚡</div><h4>redis (Cache)</h4><p>redis:7-alpine — تخزين مؤقت لحالة الازدحام اللحظية.</p></div>
<div class="card"><div class="icon">🌐</div><h4>web (React)</h4><p>لوحة تحكم الخرائط التفاعلية مع عرض البيانات.</p></div>
</div>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--blue)"></span> docker-compose.yml (مقتطف)</div><div class="lang">YAML</div></div>
<pre class="code"><span class="hl-type">services</span>:
<span class="hl-fn">db</span>:
image: <span class="hl-str">postgis/postgis:15-3.3</span>
volumes:
- <span class="hl-str">postgres_data:/var/lib/postgresql/data</span>
healthcheck:
test: [<span class="hl-str">"CMD-SHELL"</span>, <span class="hl-str">"pg_isready"</span>]
<span class="hl-fn">api</span>:
build: ./infrastructure/docker/api/Dockerfile
ports: [<span class="hl-str">"3200:3200"</span>]
environment:
- <span class="hl-str">DATABASE_URL=postgresql://user:pass@db:5432/mapdb</span>
- <span class="hl-str">REDIS_URL=redis://redis:6379</span>
depends_on:
db: { condition: <span class="hl-str">service_healthy</span> }
redis: { condition: <span class="hl-str">service_healthy</span> }
<span class="hl-fn">martin</span>:
image: <span class="hl-str">maplibre/martin:latest</span>
ports: [<span class="hl-str">"3202:3000"</span>]
command: <span class="hl-str">postgresql://user:pass@db:5432/mapdb</span></pre>
</div>
<div class="callout info"><strong>💻 أوامر التشغيل الأساسية:</strong><br>
<code style="color:var(--teal)">$ docker-compose up -d --build</code> — بناء وتشغيل كل الحاويات<br>
<code style="color:var(--teal)">$ docker-compose ps</code> — عرض حالة الحاويات<br>
<code style="color:var(--teal)">$ docker-compose logs -f api</code> — مراقبة سجلات الـ API
</div>
</section>
<!-- Chapter 09: NGINX -->
<section class="chapter fade-in" id="nginx">
<div class="chapter-header"><div class="chapter-num">09</div><h2>إعدادات NGINX — البوابة الأمامية</h2></div>
<p>NGINX يعمل كـ <strong>Reverse Proxy</strong> — يستقبل كل الطلبات على المنفذ 80 ويوجهها للحاوية المناسبة:</p>
<div class="code-card">
<div class="code-header"><div class="file"><span class="dot" style="background:var(--green)"></span> nginx.conf</div><div class="lang">NGINX</div></div>
<pre class="code"><span class="hl-key">server</span> {
listen <span class="hl-num">80</span>;
server_name portal.intaleq.map;
<span class="hl-cmt"># توجيه طلبات الـ API إلى حاوية NestJS</span>
<span class="hl-key">location</span> /api {
proxy_pass <span class="hl-str">http://api:3200</span>;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
<span class="hl-cmt"># توجيه طلبات بلاط الخرائط إلى Martin</span>
<span class="hl-key">location</span> /tiles {
proxy_pass <span class="hl-str">http://martin:3000</span>;
}
<span class="hl-cmt"># توجيه طلبات التوجيه إلى GraphHopper</span>
<span class="hl-key">location</span> /routing {
proxy_pass <span class="hl-str">http://routing:8989</span>;
}
}</pre>
</div>
</section>
<footer>
<p>نظام "انطلق" — الدليل الهندسي التعليمي الشامل © 2026</p>
<p style="margin-top:.5rem">تم بناء هذا الدليل من الكود المصدري الفعلي للمنصة</p>
</footer>
<script>
const obs = new IntersectionObserver(e => e.forEach(el => {if(el.isIntersecting)el.target.classList.add('visible')}),{threshold:.1});
document.querySelectorAll('.fade-in').forEach(r => obs.observe(r));
// Active nav link tracking
const sections = document.querySelectorAll('.chapter, .hero');
const navLinks = document.querySelectorAll('.nav-link');
window.addEventListener('scroll', () => {
let current = '';
sections.forEach(s => { if(scrollY >= s.offsetTop - 200) current = s.id; });
navLinks.forEach(l => { l.classList.toggle('active', l.getAttribute('href') === '#' + current); });
});
</script>
</body>
</html>