بناء التطبيق
لوحة تحكم التطبيق (App Control Panel)
المطوّر يبني لوحة تحكم تطبيقه بنفسه داخل كود التطبيق؛ المنصة تملك ضوابط التصميم والوصول، وتفتحها من بوابة الشركاء عبر رابط موقَّع. لكل تطبيق لوحتان — واحدة لكل بيئة (الإنتاج والتجربة) — حسب الإصدار المنشور/المثبَّت فيها. بالإضافة لما تبنيه بنفسك، تضيف المنصة تلقائياً قسم «بيانات التطبيق» إلى chrome كل لوحة — مخزن key/value معزول لتطبيقك تديره بلا أي كود (راجع بيانات التطبيق (App Data) أدناه).
كيف تعمل
- تُصرّح باللوحة في الـ manifest عبر كتلة
developer_ui. - تظهر في بوابة الشركاء داخل تبويب التكامل بطاقتان: الإنتاج والتجربة. كل بطاقة تُفعَّل فقط عندما يكون هناك إصدار حيّ في تلك البيئة و اللوحة مُصرَّحة في الـ manifest.
- زر «فتح اللوحة» يستدعي endpoint خفيف في okta-partners
(
GET /partner/modules/{slug}/dev-panel/{env}) يُصدر JWT قصير العمر (5 دقائق) ويعيد توجيهك إلى صفحة اللوحة على بيئة okta-web المستهدفة:<web base>/partner-dev/{slug}/panel?token=<jwt>. - الـ JWT موقَّع بـ HS256 بنفس السر المشترك للجسر الذي تستخدمه نداءات
الجسر الصادرة لكل بيئة (production ⇒
outboundToken، sandbox ⇒sandboxOutboundToken)، فلا يوجد سرّ جديد. الـ claims:{ iss:"okta-partners", aud:"okta-web-devpanel", sub:<partner user id>, module:<slug>, env:"production"|"sandbox", iat, exp:iat+300, jti }.
كتلة الـ manifest
لأي نوع تكامل (embedded / payment / notification / external):
"developer_ui": {
"has_developer_page": true,
"livewire_component": "my-app-developer-panel"
}
- embedded / payment / notification ⇒ تعرّف
livewire_component(اسم مكوّن Livewire يُعرَض داخل okta-web؛ الصيغة^[a-z0-9][a-z0-9-]*(::|-)[a-z0-9-.]+$— استخدم صيغة الشرطة مثلmy-app-developer-panel؛ الصيغة بـ::لا يحلّها Livewire 4). - external ⇒ تعرّف
urlبدلاً منه (رابط HTTPS تستضيفه على بيئتك الخاصة — بيئاتك ملكك، فتظهر بطاقة واحدة تفتح الرابط مباشرةً في تبويب جديد):
"developer_ui": {
"has_developer_page": true,
"url": "https://partner.example/console"
}
الحقلان متنافيان (XOR): يُصدِّر الـ manifest الحقل المطابق لنوع التكامل فقط. الكتلة قابلة للتخصيص لكل إصدار من تبويب التكامل في صفحة الإصدار؛ القيمة الفارغة ترث من الـ module.
أقسام اللوحة (sections)
مَن يملك ماذا — هذا هو الأساس الذي يُبنى عليه كل ما تحته:
| المنصة تملك | أنت تملك |
|---|---|
| هيكل اللوحة (chrome) والإطار العام | محتوى أقسامك أنت |
| الترويسة، اسم التطبيق، أيقونته | مكوّن Livewire لكل قسم |
| شريط التنقّل بين الأقسام وترتيبه | عنوان القسم وأيقونته (من قائمة المنصة) |
| الأقسام الأساسية (بيانات التطبيق، الصحة، …) | — |
المنصة تثبّت الأقسام الأساسية في كل لوحة؛ وsections هي نصيبك: أقسام
تخصّ تطبيقك تُعلَّق على تنقّل المنصة، كل قسم يعرض أحد مكوّنات Livewire لديك
داخل الهيكل. المفتاح اختياري — غيابه يعني لوحة بقسم واحد، وهو
السلوك نفسه الذي كان قبل وجوده.
"developer_ui": {
"has_developer_page": true,
"livewire_component": "my-app-developer-panel",
"sections": [
{ "key": "trips", "label": "الرحلات", "label_en": "Trips",
"icon": "map", "livewire_component": "my-app-trips" },
{ "key": "drivers", "label": "السائقون", "label_en": "Drivers",
"icon": "users", "livewire_component": "my-app-drivers" }
]
}
| الحقل | مطلوب | القاعدة |
|---|---|---|
key |
^[a-z][a-z0-9-]*$ — يبدأ بحرف صغير (لا برقم)، ثم أحرف صغيرة/أرقام/شرطات، بلا شرطة سفلية، بحد 32 خانة، وفريد داخل المصفوفة (تنقّل اللوحة يُفهرَس به). |
|
label |
بحد 60 خانة (العنوان العربي في التنقّل). | |
label_en |
بحد 60 خانة. | |
livewire_component |
نفس قاعدة alias اللوحة الرئيسية — وتُسطَّح :: بالطريقة نفسها، لأن Livewire 4 لا يحلّ اسماً فيه ::. |
|
icon |
واحدة من: home, chart, list, map, settings, bell, users, box, calendar, file. |
- الحد الأقصى 10 أقسام، وترتيب المصفوفة هو ترتيب التنقّل.
- صيغة
keyهنا أضيق عمداً من التي يقبلها okta-web (^[a-z][a-z0-9_-]{0,39}$): okta-partners يُنتج الـ manifest وokta-web يستهلكه، فالمنتِج هو الطرف الأصرم. مفتاح نُجيزه هنا ويرفضه okta-web يعني نشراً يفشل بسبب بيانات أجزناها نحن. - التطبيقات External لا تملك
sections: هي تستضيف لوحتها كاملةً بنفسها، فلا يوجد هيكل من المنصة تُعلَّق عليه أقسام — والمفتاح يُسقَط كما يُسقَطlivewire_component. - لا تُشحن كتلة نصف صالحة: قسم ينقصه
keyأوlabelأوlivewire_component(أو يخالف صيغتها) يُسقَط كاملاً — مدخل تنقّل لا يؤدي إلى شيء أسوأ من مدخل غائب.keyمكرَّر يُبقي أول ظهور فقط، وأيقونة خارج القائمة تُسقَط بصمت (القسم يبقى بلا أيقونة — الزخرفة لا تُفشِل إعلاناً)، والقائمة تُقصّ عند 10.
سياسة تصميم اللوحة
اللوحة تعيش داخل هيكل تملكه المنصة، ومن هنا تأتي القواعد — كلها قابلة للتنفيذ حرفياً:
- لا تبنِ ترويسة أو شريط تنقّل داخل القسم. الهيكل يوفّرهما أصلاً؛ أي ترويسة/تنقّل تضيفه يظهر مكرَّراً فوق ترويسة المنصة.
- مكوّنات
<x-…>فقط. لا<button>/<input>/<select>/<textarea>/<table>خام — الهوية البصرية تعيش داخل المكوّنات. - لا ألوان ثابتة من نوع
gray/zinc/slate/indigo؛ استخدم tokens المنصة (neutral,primary,success,warning,danger). - لا
max-w-*. الهيكل يفرض حدود العرض؛ إضافتها تكسر الشبكة وتقطع المحتوى الطويل. - اسم القسم من
label/label_enلا من داخل الـ blade — المنصة هي التي ترسم العنوان في التنقّل. - أيقونتك من القائمة المسموحة فقط؛ لا تشحن SVG خاصاً بك في التنقّل.
القواعد 2–4 يفرضها فاحص التصميم UiScanner وبوّابة الـ CI على كل
*.blade.php في مستودعك، فما يخالفها لا يصل الإنتاج أصلاً.
الضوابط
- التصميم: blade اللوحة يمرّ على نفس فاحص التصميم (UiScanner) وبوابة الـ CI
مثل بقية صفحاتك — مكوّنات
<x-…>وtokens المنصة فقط، لا HTML خام، لا ألوانgray/zinc/slate/indigo، لاmax-w-*. - الوصول: رابط موقَّع من المنصة فقط. سياق المطوّر بلا مستأجر — أنت
تنظر إلى صحّة تطبيقك التجميعية عبر كل التثبيتات، لا إلى بيانات جهة بعينها.
الواجهة المتاحة من مكوّن اللوحة الخاص بك هي namespace واحد فقط:
App\Services\PartnerApi\DeveloperPanel\*— التجميعات للقراءة فقط (GetInstallsCount, ...) بالإضافة إلى CRUD بيانات التطبيق تحتDeveloperPanel\Settings\*(راجع القسم التالي).
مثال كامل
مكوّن Livewire (يُشحن جاهزاً في الـ boilerplate تحت
app/DeveloperPanel/Panel.php.example):
namespace Modules\MyApp\app\DeveloperPanel;
use App\Services\PartnerApi\DeveloperPanel\GetInstallsCount;
use Livewire\Component;
class Panel extends Component
{
// المُدخلان الوحيدان اللذان تمرّرهما المنصة — لا مُعرّف جهة أبداً.
public string $moduleSlug = '';
public string $environment = 'production';
public function render(): \Illuminate\View\View
{
$installs = app(GetInstallsCount::class)($this->moduleSlug);
return view('my-app::developer-panel.panel', [
'installs' => $installs,
]);
}
}
الـ blade يستخدم مكوّنات المنصة فقط:
<div class="space-y-6">
<x-badge :color="$environment === 'production' ? 'success' : 'warning'" variant="soft">
{{ $environment }}
</x-badge>
<x-card>
<div class="p-4">
<p class="text-3xl font-bold text-[var(--color-neutral-900)]">{{ $installs }}</p>
</div>
</x-card>
</div>
الإحصاءات المتاحة
سطح القراءة App\Services\PartnerApi\DeveloperPanel\* على okta-web يوفّر تجميعات
عبر كل تثبيتات تطبيقك (لا بيانات جهة مفردة) — مثل عدد التثبيتات
(GetInstallsCount)، التثبيتات النشطة، وعدّادات الأخطاء. أي عملية دفع تظهر في
هذه التجميعات تُسمّى «دفعة».
بيانات التطبيق (App Data)
جدول key/value (JSON) مرتبط حصراً بتطبيقك — طريقة منخفضة الاحتكاك لتخزين إعدادات/أعلام تشغيلية تريد تعديلها بين الإصدارات دون نشر جديد.
- واجهة إدارة جاهزة بلا كود: قسم «بيانات التطبيق» يظهر تلقائياً داخل chrome اللوحة — كل تطبيق يحصل على CRUD كامل لمفاتيحه (إضافة/تعديل/حذف) من دون أي كود من جهتك.
- سطحان من الخدمات:
- من داخل مكوّن اللوحة الخاص بك (namespace مسموح ضمن سياق اللوحة فقط):
App\Services\PartnerApi\DeveloperPanel\Settings\{ListSettings, PutSetting, DeleteSetting}— كل واحدة تأخذmoduleSlugتطبيقك. - من كود تطبيقك المُشحَّن وقت التشغيل الفعلي عند الجهات (قراءة فقط، لا
كتابة):
App\Services\PartnerApi\AppSettings\{GetAppSetting, GetAppSettings}— نتيجة مكاشة ~60 ثانية.
- من داخل مكوّن اللوحة الخاص بك (namespace مسموح ضمن سياق اللوحة فقط):
- الحدود: 100 مفتاح كحد أقصى لكل تطبيق، قيمة كل مفتاح ≤ 64KB، صيغة
المفتاح
^[a-z][a-z0-9_.-]{0,119}$. - العزل: مضمون من الخادم بالكامل — سياق جلسة اللوحة يُشتق فقط من الـ JWT
الموقَّع (
iss=okta-partners,aud=okta-web-devpanel)؛ جلسة تطبيقك لا يمكن أبداً أن تلمس بيانات تطبيق آخر. - النمط الموصى به: اضبط سلوك تطبيقك (أعلام الميزات، الحدود، معاملات المزوّد، نصوص العرض) من اللوحة بين الإصدارات — بدون إعادة نشر.
قراءة وقت التشغيل من كود تطبيقك (مثلاً داخل service تحت App\Services\PartnerApi\*):
use App\Services\PartnerApi\AppSettings\GetAppSetting;
$maxItems = app(GetAppSetting::class)('max_items_per_page', 20);
كتابة من عمل (action) داخل مكوّن اللوحة نفسه:
use App\Services\PartnerApi\DeveloperPanel\Settings\PutSetting;
public function saveMaxItems(): void
{
app(PutSetting::class)($this->moduleSlug, 'max_items_per_page', (string) $this->maxItems);
$this->dispatch('saved');
}