feat: N1d/N2/N3 + قانون فلاتر وإعادة ضبط تطبيق السائق

الباك إند:
- تعيين أول أدمن صار يُنشئ المستخدم إن لم يوجد — كان يشترط دخولاً سابقاً،
  وهي بيضة ودجاجة تمنع الدخول إلى لوحة أي مستأجر جديد أصلاً.
- OTP_DEV_MODE كان يفشل مفتوحاً (`!== 'false'`): غياب المتغيّر أو خطأ مطبعي
  يترك الإنتاج برمز ثابت يفتح كل حساب، ويحرس السحب المالي كذلك. صار
  `=== 'true'` في الإعداد وفي قارئَيه، مع تحذير عند الإقلاع.
- N2: التحقّق من اللوغو عند الرفع (PNG/JPEG · 512+ · مربّع · سقف 5MB) عبر
  قراءة الترويسة بلا اعتمادية — بدل اكتشاف أيقونة ممطوطة بعد النشر.

N3: السكربت يولّد build_config.dart بأعلام const (طبقات docs/22 §1.5)،
ويضبط bundle IDs واسم التطبيق والأيقونات/splash، و--build يشغّل Shorebird.

فلاتر:
- docs/23: قانون مُلزِم للتطبيقين (البنية · Cubit · طبقة الشبكة · الأعلام).
- السائق: أُزيل Dart القديم (GetX) مع الإبقاء على الأصيل والإضافات وشهادات
  التوقيع وShorebird وFirebase الخاص به، وأُعيد هيكلته مطابقاً للراكب.
- طبقة الشبكة ترسل x-device-id وتجدّد التوكن عند 401 مرة واحدة فقط.
- الحزم موحّدة بين التطبيقين، والـAPI https حصراً في الاثنين.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-18 13:06:46 +03:00
co-authored by Claude Fable 5
parent 3ab74a13a8
commit de4bbd00ac
271 changed files with 1333 additions and 81068 deletions
+33
View File
@@ -182,12 +182,45 @@
<td><span class="badge ${suspended ? '' : 'active'}">${esc(t.status)}</span></td>
<td>${t.completed_trips}/${t.trips}</td><td>${money(t.gmv)}</td><td>${money(t.revenue)}</td>
<td><button class="ghost" data-act="manifest">مانيفست</button>
<button class="ghost" data-act="admin">تعيين أدمن</button>
<button class="ghost" data-act="logo">اللوغو</button>
<button class="ghost" data-act="status">${suspended ? 'تفعيل' : 'تعليق'}</button></td>`;
tr.querySelector('[data-act=manifest]').onclick = async () => {
const m = await api('GET', `/admin/tenants/${t.id}/app-manifest`);
alert(JSON.stringify(m, null, 2));
};
// أول أدمن للمستأجر: تُنشئ الحساب إن لم يوجد (docs/22 — N1d).
// بلا هذا لا يمكن الدخول إلى لوحة مستأجر مزوَّد حديثاً إطلاقاً.
tr.querySelector('[data-act=admin]').onclick = async () => {
const phone = prompt(`رقم هاتف أدمن «${t.name}» (بصيغة دولته: ${t.countryPack})`);
if (!phone) return;
try {
const r = await api('POST', '/platform/users/role', {
tenantSlug: t.slug, phone: phone.trim(), role: 'admin',
});
toast(r.created ? 'أُنشئ الحساب وعُيّن أدمن ✅' : 'صار أدمن ✅');
} catch (e) { toast(e.message, false); }
};
// رفع اللوغو (docs/22 — N2). لا نمرّ بـapi() لأن الرفع multipart
// لا JSON: ضبط Content-Type يدوياً يكسر حدّ الأجزاء (boundary).
tr.querySelector('[data-act=logo]').onclick = () => {
const inp = document.createElement('input');
inp.type = 'file'; inp.accept = 'image/png,image/jpeg';
inp.onchange = async () => {
const f = inp.files?.[0]; if (!f) return;
const fd = new FormData(); fd.append('file', f);
try {
const res = await fetch(`${API}/admin/tenants/${t.id}/logo`, {
method: 'POST', headers: { 'x-platform-secret': SECRET }, body: fd,
});
const j = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(j.message || `HTTP ${res.status}`);
toast(`رُفع اللوغو ${j.logo?.width}×${j.logo?.height} ✅`);
} catch (e) { toast(e.message, false); }
};
inp.click();
};
tr.querySelector('[data-act=status]').onclick = async () => {
// التعليق يقطع الخدمة عن مستأجر حيّ فوراً — لذلك تأكيد صريح.
const next = suspended ? 'active' : 'suspended';