Files
tripz-llc/docs/34-siro-backend-multitenancy.md
T

306 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 34 — معمارية تعدد المستأجرين: سيرفر مستقل لكل مستأجر
> أُنشئ 2026-07-24. **حُدِّث في نفس اليوم بقرار المالك: سيرفر منفصل لكل مستأجر** (بدل قاعدة بيانات لكل مستأجر على سيرفر مشترك).
> كل رقم هنا مقروء من الكود مباشرة. المستودع: `~/development/App/Siro`.
> يُقرأ مع: `docs/21` (عيوب سيرو) · `docs/33` (الوحدات) · `docs/06` (نموذج المستأجر) · `docs/19` (الاستحقاقات).
---
## 0) القرار
**نسخة كاملة مستقلة لكل مستأجر على سيرفره الخاص.** لا كود تعدد مستأجرين إطلاقاً — لا `tenant_id`، ولا `TenantContext`، ولا حتى تعديل على `Database::get()`.
| البديل | الحكم |
|---|---|
| عمود `tenant_id` في 175+ جدول | ❌ مرفوض — أشهر عمل + خطر تسرّب دائم |
| قاعدة لكل مستأجر على سيرفر مشترك | 🟡 كان اقتراحي الأول (~5 أسابيع كود) |
| **سيرفر كامل لكل مستأجر** | ✅ **المعتمد — صفر كود تعدد مستأجرين** |
### لماذا هذا القرار صحيح فعلاً (وليس مجاملة)
1. **`docker/docker-compose.yml` موجود مسبقاً ومكتوب حرفياً لهذا الغرض** — التعليق في أول الملف: «سيرو في حاويات — **نسخة واحدة كاملة على سيرفر واحد**». البنية التي تحتاجها مبنية بالفعل.
2. **يُلغي كل كود تعدد المستأجرين** الذي قدّرته بـ5 أسابيع. تقديرك «يوم عمل لكل مستأجر» **واقعي — بشرط الأتمتة** (§3).
3. **العزل مطلق:** لا شرط `WHERE` منسي، لا بادئة Redis مشتركة، لا استعلام يخترق. المخاطرة الأمنية الأولى في أي SaaS تختفي كلياً.
4. **نصف قطر الانفجار = مستأجر واحد.** سيرفر يسقط أو يُهاجَم أو تمتلئ قاعدته → البقية لا يشعرون.
5. **يطابق وعد الموقع حرفياً:** «عزل بياناتك» لم يعد شعاراً تسويقياً بل حقيقة معمارية. **وباقة «سيادة» تصبح هي المعيار لا الاستثناء.**
6. **التصدير والنسخ الاحتياطي تافهان** — `mysqldump` واحد. أقوى نقطة بيع في العقد صارت أرخص ميزة عندك.
### التكلفة الحقيقية للقرار — وهي حقيقية
**أنت لم تُلغِ التعقيد، بل نقلته من الكود إلى التشغيل.**
بدل «كيف أعزل المستأجرين في كود واحد؟» صار السؤال **«كيف أدير 50 سيرفراً بلا فريق DevOps؟»**
هذا سؤال أسهل وأوضح — لكنه **يقتلك إن لم تحلّه بالأتمتة من المستأجر الثاني**، لا من العاشر. باقي هذا المستند هو الجواب.
---
## 1) 🔴 تصحيح فوري: «سيرفرات رخيصة» غير دقيق
جمعت حدود الذاكرة من `docker-compose.yml` الحالي:
| الحاوية | الحد |
|---|---|
| mysql | 2,560 م.ب |
| php-fpm | 2,048 |
| socket_driver | 768 |
| socket_passenger | 512 |
| redis | 512 |
| nginx | 256 |
| phpmyadmin | 256 |
| **المجموع** | **≈ 6.9 غ.ب** |
**سيرفر بـ2 غ.ب لن يشغّل هذه النسخة كما هي.** الحل ليس شراء سيرفرات ضخمة، بل **ثلاث وصفات compose حسب حجم المستأجر:**
| المقاس | الموارد | حدود مضبوطة | يناسب |
|---|---|---|---|
| **S** | 2 نواة / 4 غ.ب / 80 غ.ب SSD | mysql 1g · php 768m · redis 256m · بلا phpmyadmin | < 300 رحلة/يوم |
| **M** | 4 نواة / 8 غ.ب / 160 غ.ب | الحدود الحالية | < 3,000 رحلة/يوم |
| **L** | 8 نواة / 16 غ.ب + قرص منفصل لـMySQL | موسّعة + `pricing-engine` | > 3,000 رحلة/يوم |
**الوحدات تغيّر المقاس:** `pricing-engine` (Node) و`marketing_engine` يرفعان الحاجة درجة كاملة. اربط مقاس السيرفر بالباقة والوحدات في نفس عرض السعر.
**التكلفة التقديرية:** 50 مستأجراً على مزيج S/M ≈ **$700–1,200/شهر** مقابل إيراد $10k–35k/شهر. الهامش سليم تماماً — لكن **احسبها في التسعير من الآن**، لا لاحقاً.
> 🔴 **`phpmyadmin` يُحذف من وصفة الإنتاج.** لوحة قاعدة بيانات مكشوفة على 50 سيرفر = 50 باب خلفي. للصيانة: نفق SSH عند الحاجة فقط.
---
## 2) تقسيم الملفات: مشترك ↔ خاص بالمستأجر
سألت: هل نقدر نفصل المشترك عن الخاص بالمستأجر؟ **نعم — وهذا بالضبط الانضباط الوحيد الذي يجعل 50 سيرفراً قابلة للإدارة.**
```
/srv/tripz/
├── app/ ← من git. مشترك. ❌ لا يُعدَّل على السيرفر أبداً
│ ├── backend/ payment_server/ loction_server/ docker/
├── config/ ← خاص بالمستأجر. ليس في git إطلاقاً
│ ├── .env (DB · Redis · JWT · APP_DOMAIN · GLOBAL_COUNTRY)
│ ├── tenant.json (slug · اسم العرض · الباقة · الوحدات المفعّلة)
│ └── secrets/ (PayMob · Gemini · SMS · Firebase · مفاتيح JWT)
├── branding/ ← خاص: شعار · ألوان · اسم التطبيق · نصوص
└── storage/ ← خاص: uploads · logs · backups
```
### القاعدة الحديدية
> **إن عدّلت يوماً ملفاً داخل `app/` على سيرفر مستأجر — فقد صنعت فرعاً (fork)، والتحديث القادم إما يمسحه أو يفشل بتعارض.**
> أي سلوك خاص بمستأجر يُقاد من `config/` أو من أعلام الميزات — **لا بتعديل الكود. أبداً.**
هذه الجملة هي الفرق بين 50 سيرفراً تُحدَّث بأمر واحد، و50 فرعاً يستحيل صيانتها. اكتبها على الحائط.
الوضع اليوم جيد: `.gitignore` يستثني `.env` و`*.pem` و`service-account.json` بشكل صحيح. **الناقص:** فصل `config/` و`branding/` كمجلدات مستقلة خارج شجرة الكود.
---
## 3) التزويد: من «يوم عمل» إلى «أمر واحد»
**المستأجر الأول يدوياً — لكن وأنت تكتب كل خطوة.** ملاحظاتك هي السكربت. من المستأجر الثاني: السكربت فقط.
```bash
provision-tenant.sh --slug=acme --host=api.acme.com \
--size=M --plan=brand --country=EG \
--modules=ai,pricing_intel
```
ما يفعله بالترتيب:
1. إنشاء السيرفر عبر API المزوّد (Hetzner/DO) بالمقاس المطلوب
2. تقوية أساسية: مستخدم غير جذري · SSH بمفتاح فقط · جدار ناري (22/80/443 فقط) · fail2ban · تحديثات أمنية تلقائية
3. `git clone --branch <آخر وسم مستقر>` إلى `app/`
4. توليد `config/.env` بكلمات سرّ عشوائية + `tenant.json`
5. `docker compose up -d` بوصفة المقاس
6. تشغيل الهجرات + بذرة البيانات + `schema_version`
7. Let's Encrypt للنطاق
8. تركيب الكرونات (حسب الوحدات المفعّلة فقط)
9. تسجيل النسخ الاحتياطي اليومي
10. **تسجيل المستأجر في السجل المركزي** + توليد مفتاح التخاطر (§5)
11. **فحص دخان:** تسجيل دخول → عرض سعر → طلب رحلة وهمية → تأكيد وصول أول تخاطر
**الخطوة 11 غير قابلة للتفاوض.** «تم النشر» بلا فحص دخان يعني أن أول من يكتشف العطل هو العميل.
> 🔴 **مصيدة كلمات السرّ:** لا تستنسخ نفس `JWT_SECRET` أو كلمة سرّ MySQL عبر المستأجرين. اختراق واحد = سقوط الجميع. السكربت **يولّد** لكل مستأجر أسراره.
---
## 4) التحديثات عبر Git — النموذج الآمن
فكرتك صحيحة (git كمصدر واحد)، لكن `git pull` الساذج على 50 سيرفراً كارثة تنتظر. الفروق الأربعة التي تحوّلها لنظام:
### 4.1 وسوم لا فروع
**لا تنشر `main` أبداً.** انشر وسماً: `git fetch --tags && git checkout v1.4.2`. بلا هذا لا تعرف ما الذي يعمل عند أي مستأجر، ولا تستطيع التراجع.
### 4.2 نشر على حلقات (Rings)
```
الحلقة 0: سيرو (سيرفرك أنت) ← 24 ساعة مراقبة
الحلقة 1: 3 مستأجرين متعاونين ← 24 ساعة
الحلقة 2: الباقون ← دفعات من 10
```
**لا تنشر على الخمسين دفعة واحدة.** يوم واحد من الصبر يوفّر ليلة انهيار كامل.
### 4.3 هجرات + `schema_version`
كل سيرفر يسجّل نسخة مخططه. المنشِّط يرفض التشغيل إن كان الفارق أكثر من نسخة، ويوقف عند أول فشل بدل المتابعة.
**انجراف المخططات هو القاتل رقم واحد في هذا النموذج** — لأن كل سيرفر عنده MySQL خاص به.
### 4.4 تراجع جاهز قبل النشر لا بعده
`rollback-tenant.sh --slug=acme --to=v1.4.1` — يشمل هجرة عكسية. **إن لم يكن التراجع مجرَّباً فهو غير موجود.**
### 4.5 المنسّق
Ansible مع ملف جرد المستأجرين، أو سكربت bash + ssh حلقي إن أردت البقاء بسيطاً. الجوهر: **أمر واحد على جهازك ينفّذ على مجموعة محددة من السيرفرات ويعيد تقرير نجاح/فشل لكل واحد.**
> 🟠 **`deploy.sh` الحالي (`git add . && git push origin --all`) لا يصلح لهذا النموذج** — يدفع كل الفروع ويضيف كل شيء بلا تمييز. يُستبدل بمسار إصدار: فرع ميزة → مراجعة → دمج في main → وسم → نشر بالحلقات.
---
## 5) لوحة السوبر-أدمن والتخاطر المركزي
فكرتك: كل مستأجر يرسل بياناته لنقطة نهاية مركزية كل ساعتين. **الاتجاه صحيح تماماً** — دفع من المستأجر إلينا لا سحب: لا نحتاج منفذاً مفتوحاً في سيرفراتهم، يعمل خلف أي جدار ناري، وأنت لا تملك وصولاً دائماً لبياناتهم (وهذا **يقوّي** وعد الخصوصية على الموقع بدل أن يناقضه).
### 5.1 🔴 القاعدة التي تحمي الشركة: تجميعات فقط، لا بيانات شخصية
```jsonc
// ✅ يُرسَل
{ "slug":"acme", "period_start":"2026-07-24T10:00:00Z", "period_hours":2,
"trips":{"completed":412,"cancelled":37}, "gmv":{"amount":8420.50,"currency":"EGP"},
"drivers":{"active":88,"online_peak":61}, "passengers":{"new":24},
"health":{"version":"v1.4.2","schema_version":37,"uptime_s":864000,
"errors_5xx":3,"db_size_mb":2140,"disk_free_pct":62},
"modules":["ai","pricing_intel"] }
// ❌ لا يُرسَل أبداً
// أسماء · هواتف · إحداثيات · مسارات رحلات · صفوف مستخدمين · أي PII
```
سحب البيانات الخام لسيرفرك المركزي **يهدم وعد «بياناتك تبقى لك» حرفياً**، ويضعك تحت طائلة قوانين حماية بيانات في كل دولة يعمل فيها مستأجروك. **تجميعات فقط. بلا استثناء.**
### 5.2 عقد نقطة النهاية
| البند | القرار |
|---|---|
| المسار | `POST /telemetry/v1/report` |
| المصادقة | مفتاح لكل مستأجر + **توقيع HMAC** على الجسم + طابع زمني (حماية من إعادة الإرسال) |
| مفتاح التكرار | `(slug, period_start)` — إعادة الإرسال لا تُضاعف الحساب |
| التخزين | **append-only** للحمولة الخام + جداول مجمّعة مشتقة منها |
| فشل المركز | المستأجر **يصطفّ محلياً** ويعيد الإرسال لاحقاً — وإلا خسرت بيانات فوترة |
| فشل المستأجر | تنبيه بعد فوات نافذتين متتاليتين |
### 5.3 🟠 ساعتان بطيئة جداً للأعطال
الساعتان ممتازتان للفوترة والتحليل. **لكن انقطاع مستأجر يجب أن تعرفه خلال دقيقة لا ساعتين.**
➡️ **نبضة خفيفة كل 60 ثانية** (بضع بايتات: أنا حيّ + النسخة) + **الحمولة الكاملة كل ساعتين**. النبضة تكشف السقوط، والحمولة تروي القصة.
### 5.4 🔴 لا تفوتر من رقم يملكه العميل
المستأجر يملك سيرفره → يستطيع تعديل ما يرسله. **لا تبنِ فاتورة نسبة GMV على تخاطر غير موثوق وحده.**
الخيارات: ① مطابقة مع سجلات بوابة الدفع (المصدر المستقل الوحيد) · ② توقيع سجلات الرحلات بمفتاح لا يملكه المستأجر · ③ قبول المخاطرة صراحةً لباقة انطلاقة، والمطابقة للباقات الكبرى.
**اختر واحداً بوعي — لا تتركه للصدفة.**
### 5.5 الوصول للدعم
التخاطر يخبرك **أن** هناك مشكلة، لا **ما** هي. للتشخيص تحتاج مساراً استثنائياً:
مفتاح SSH دعم تحتفظ به + **بند صريح في العقد** يمنحك حق الدخول للصيانة، ويفضَّل أن يكون مؤقتاً ومسجّلاً. **لا تدخل سيرفر عميل بلا غطاء تعاقدي مكتوب.**
---
## 6) لوحات الأدمن وخدمة العملاء
فحصت `siro_admin` و`siro_service`: **كلاهما تطبيق Flutter وفيهما مجلد `web/`** — أي أن `flutter build web` يخرج صفحة ويب من نفس الكود مباشرة. **جوابك: نعم، ومن نفس المصدر بالضبط، بلا إعادة كتابة.**
المعمارية الصحيحة — **لوحتان منفصلتان تماماً، ولا تخلطهما أبداً:**
| | **لوحة المستأجر** | **لوحة السوبر-أدمن** |
|---|---|---|
| المصدر | `siro_admin` + `siro_service` (Flutter Web) | تطبيق مركزي جديد |
| أين تعمل | على سيرفر المستأجر نفسه | سيرفرك المركزي |
| ماذا ترى | بيانات هذا المستأجر فقط | تجميعات كل المستأجرين |
| العنوان | `admin.acme.com` · `support.acme.com` | `console.tripz.com` |
| العلامة | علامة المستأجر من `branding/` | علامتك |
> 🔴 **لا تبنِ لوحة واحدة تسجّل الدخول إلى كل سيرفرات المستأجرين.** ستحمل حينها مفاتيح دخول لكل عملائك في مكان واحد — وتُعيد بناء كل مخاطر تعدد المستأجرين التي تخلّصت منها للتوّ. السوبر-أدمن يقرأ من **قاعدة التخاطر المركزية فقط**.
**العمل المطلوب:** تعمية العلامة في `siro_admin`/`siro_service` (الاسم والشعار والألوان تُقرأ من `branding/` وقت البناء أو التشغيل) + بوابة استحقاقات تخفي الوحدات غير المشتراة (docs/19).
---
## 7) خطة العمل بالترتيب
| # | المهمة | المدة | البوابة |
|---|---|---|---|
| 1 | فصل `app/ ↔ config/ ↔ branding/ ↔ storage/` | 3 أيام | 🔴 كل ما بعده يعتمد عليها |
| 2 | ثلاث وصفات compose (S/M/L) + حذف phpmyadmin | 2 أيام | |
| 3 | **إصلاح عيوب المال في `docs/21`** | 5 أيام | 🔴 **قبل أول مستأجر مدفوع** |
| 4 | `provision-tenant.sh` + فحص الدخان | 4 أيام | |
| 5 | مسار الإصدار: وسوم + حلقات + `schema_version` + تراجع | 4 أيام | |
| 6 | التخاطر: مرسِل عند المستأجر + نقطة نهاية + نبضة 60ث | 4 أيام | |
| 7 | لوحة السوبر-أدمن (تقرأ التخاطر) | 5 أيام | |
| 8 | تعمية العلامة في admin/service + بوابة الاستحقاقات | 4 أيام | |
| | **المجموع** | **~5 أسابيع** | |
**لماذا ما زالت 5 أسابيع رغم أن كود تعدد المستأجرين اختفى؟** لأن العمل انتقل — من كتابة عزل في الكود، إلى بناء أدوات أسطول. **الفارق الحاسم: هذه الأدوات تُبنى مرة وتخدم كل مستأجر إلى الأبد، أما عزل الكود فكان سيبقى خطراً دائماً في كل استعلام جديد.** الصفقة رابحة.
**وضع الحد الأدنى (لأول 3 مستأجرين):** البنود 1 · 3 · 4 فقط ≈ **12 يوماً**. الباقي يُبنى بينما تبيع — لكن **لا تتجاوز الخامس** بلا البند 5 (التراجع)، ولا **العاشر** بلا البند 6 (التخاطر).
---
## 7.5) مدة التسليم وإيقاع التحديثات — مقارنة بالسوق
### ما يعد به المنافسون فعلياً (مقروء من مواقعهم 2026-07-24)
| المنصة | الوعد المعلن |
|---|---|
| **Onde** — الصفحة التسويقية | «أطلق خلال **14 يوماً**» |
| **Onde** — صفحة الأسئلة الشائعة | «إطلاق المنصة يستغرق **حوالي 4 أسابيع**» |
| متوسط القطاع (مزوّدو white-label) | **2 – 8 أسابيع** |
**الملاحظة الأهم:** Onde نفسها **تعد بـ14 يوماً في الإعلان و4 أسابيع في الأسئلة الشائعة.** هذا ليس تناقضاً عشوائياً — الرقم التسويقي يقيس *جاهزية المنصة*، والرقم الواقعي يشمل **مراجعة المتاجر** التي لا يملك أحد التحكم بها.
**وعدنا (30 يوماً) في الموضع الصحيح تماماً:** أصدق من «14 يوماً» وأسرع من سقف القطاع. **لا تخفّضه** — تأخير أسبوع على وعد 14 يوماً يحرق الثقة، بينما التسليم في 20 يوماً على وعد 30 يوماً يصنع بطلاً.
> **صياغة موصى بها في العقد:** «المنصة جاهزة وتعمل خلال 10 أيام عمل · النشر على المتجرين خلال 30 يوماً، وهو خاضع لمراجعة آبل وجوجل.» تفصل ما تتحكم به عمّا لا تتحكم به.
### إيقاع تحديثات المنافسين
Onde تقول: «تحديثات منتظمة لكلا النظامين بلا رسوم إضافية» — **بلا جدول معلن ولا ذكر لتحديث هوائي.** أي أن **التحديث الهوائي (Shorebird) تمايز حقيقي لك، لا لحاق بالركب.**
---
## 7.6) التحديث الهوائي (Shorebird) — الفرصة والمصيدة
### القاعدة التقنية الحاكمة
**Shorebird يرقّع كود Dart فقط.** أي تغيير في الطبقة الأصلية — Kotlin/Swift · إضافات (plugins) · أذونات · ترقية محرك Flutter — **يستوجب إصداراً كاملاً في المتجر.**
هذا يتطابق مع القرار المثبّت في `docs/22 §1.5`: **الطبقة الأصلية تبقى موحّدة عبر كل المستأجرين والمستويات** — وهذا بالضبط ما يجعل الترقيع الهوائي ممكناً أصلاً. **أي انحراف في الطبقة الأصلية بين مستأجر وآخر يقتل الميزة كلها.**
### 🔴 المصيدة: الرقعة تتضاعف بعدد المستأجرين
كل مستأجر = تطبيقان بهويتَي حزمة خاصتين = **تطبيقان مستقلان في Shorebird**.
50 مستأجراً = **100 تطبيق × كل إصلاح**. الترقيع اليدوي مستحيل عند هذا الحجم — ولا تكتشف ذلك عند المستأجر الأربعين.
**اللازم:** `patch-all.sh` يمرّ على جرد المستأجرين، ويرقّع بنفس الحلقات المعتمدة في §4.2 (سيرو أولاً → متعاونون → الباقون). **ونفس قاعدة التراجع: رقعة قابلة للسحب، مجرَّبة قبل الاعتماد عليها.**
**وتنبيه تكلفة:** تسعير Shorebird يقوم على التطبيقات وعمليات تثبيت الرقع — تكلفتك تنمو خطياً مع عدد المستأجرين. **احسبها ضمن الكلفة التشغيلية لكل مستأجر من الآن**، مع مقاسات السيرفرات في §1.
### 🟠 حدّ آبل
Shorebird مصمَّم للامتثال، لكن بند **App Store 3.3.2** يبقى منطقة الخطر: استعمل الرقع **لإصلاح الأعطال وتعديل السلوك**، لا لشحن ميزات تغيّر الغرض الأساسي للتطبيق. **الميزات الكبيرة تمرّ عبر المتجر.** انضباط بسيط يحميك من رفض جماعي يطال كل مستأجريك دفعة واحدة.
### الإيقاع المقترح
| النوع | القناة | التكرار |
|---|---|---|
| إصلاح عاجل (Dart) | رقعة Shorebird | خلال ساعات |
| تحسينات دورية (Dart) | رقعة | كل أسبوعين |
| ميزة كبيرة أو تغيير أصلي | إصدار متجر | كل 6–8 أسابيع |
| الباك إند | git بالحلقات (§4) | مستقل تماماً عن التطبيقات |
**الرسالة البيعية:** «إصلاح عندك خلال ساعات، لا خلال أسبوعين انتظاراً لمراجعة المتجر.» — **لا أحد في القطاع يقدر يقولها.**
---
## 8) القرارات المطلوبة منك
1. **المزوّد**: Hetzner (الأرخص بفارق كبير، أوروبا) · DigitalOcean · مزوّد محلي في كل دولة؟ *(للسيادة في مصر/السعودية قد يفرض العقد مزوّداً محلياً — احسبها الآن)*
2. ~~النطاقات~~ ✅ **محسوم (المالك 2026-07-24): نطاق فرعي لكل مستأجر تحت نطاقنا** — `acme.tripz.app` · `api.acme.tripz.app` · `admin.acme.tripz.app`. **لا IP في التطبيقات إطلاقاً.** الفوائد: شهادة wildcard واحدة (`*.tripz.app`) بدل 50 شهادة منفصلة تنتهي في 50 موعداً · إنشاء المستأجر = سجل DNS واحد بلا انتظار العميل · نقل السيرفر = تغيير سجل A بلا تحديث متجر. **يبقى مسموحاً** للمستأجر الكبير أن يوجّه `api.acme.com` بـCNAME إلى نطاقه الفرعي لاحقاً (تُعالج بشهادة منفصلة) — لكن الافتراضي هو نطاقنا.
3. **الفوترة**: تُبنى على التخاطر وحده أم بمطابقة بوابة الدفع؟ (§5.4)
4. **حق دخول الدعم**: بند تعاقدي دائم أم إذن مؤقت لكل حادثة؟
5. **مستودع Tripz أم مستودع سيرو؟** هل يصبح كود سيرو هو `backend/` في مستودع Tripz، أم يبقى مستودعاً مستقلاً ويُسحب كـsubmodule؟ *(توصيتي: مستودع واحد باسم Tripz — مصدر حقيقة واحد للنشر)*
---
## سجل التغييرات
| التاريخ | التغيير |
|---|---|
| 2026-07-24 | إنشاء الدراسة (DB لكل مستأجر) |
| 2026-07-24 | **تحديث جذري بقرار المالك: سيرفر كامل لكل مستأجر.** أُضيف: مقاسات السيرفرات، تقسيم الملفات، مسار الإصدار بالحلقات، عقد التخاطر، فصل لوحة المستأجر عن السوبر-أدمن |