صور المنتجات لا تظهر في المتجر الإلكتروني

تحليل جذر المشكلة: الصور المرفوعة كـ Attachments من داخل Moon ERP تُخزَّن على قرص خاص غير منشور، ولا يمكن لأي وسم <img> في متجر عام أن يقرأها — بينما تطبيق الإدارة يقرأها بطريقة مختلفة تمامًا لا يمكن تكرارها في المتجر.

moonui4 · فرع hazemdev4 2026-07-19 مرتبط بالتذكرة STORE-040 لم يُكتب أي كود بعد

١. المشكلة The problem

المنتج له صورة ظاهرة داخل Moon ERP، لكن نفس المنتج يظهر في المتجر الإلكتروني بدون صورة (صورة بديلة رمادية).

الخلاصة في سطر واحد: الصورة المرفوعة عبر واجهة المرفقات (Attachments) تُحفَظ في storage/app/private/attachments/… — وهو مجلد خاص لا يخدمه الويب سيرفر إطلاقًا. تطبيق الإدارة يعرضها لأنه لا يستخدم رابط الصورة أصلًا: هو ينزّلها كـ blob عبر طلب XHR موثّق بالتوكن. المتجر يستخدم <img src> عادي، ووسم <img> لا يرسل أي هيدر توثيق — فالطلب يرجع 404/401 دائمًا.

أي أن هذه ليست مشكلة في كود المتجر، ولا في التصميم، ولا في الكاش. هي فجوة معمارية في طبقة تخزين الصور في الباك-إند، وستظهر بنفس الشكل في تطبيق الموبايل وأي مستهلك عام آخر.

٢. الوضع الحالي — بالأدلة Current state, verified

٢.١ مساران مختلفان تمامًا لنفس الصورة

ADMIN (يعمل ✔)
Angular admin → XHR GET /api/attachments/{id}/download (+ X-Authorization: Bearer)
  → Laravel Storage::download($attachment->file_path) — يقرأ من القرص الخاص
  → blob → URL.createObjectURL()[src]="blobUrl" ✔ تظهر

STOREFRONT (لا يعمل ✘)
<img src="https://…/storage/attachments/1/2026-02/abc.png"> (بدون أي هيدر — المتصفح لا يرسلها في img)
  → الويب سيرفر يبحث في public/storage → symlink إلى storage/app/public
  → لكن الملف في storage/app/private/attachments/… ✘ 404

٢.٢ الكود المسؤول — تم قراءته سطرًا بسطر

أ) رفع المرفق يذهب للقرص الافتراضي (الخاص): Modules/Core/app/Http/Controllers/AttachmentController.php:106-115

$directory = "attachments/{$companyId}/".now()->format('Y-m');
$path = $file->store($directory);   // ← بدون تحديد disk ⇒ يستخدم FILESYSTEM_DISK
...
'file_path' => $path,

وفي .env: FILESYSTEM_DISK=local. وفي config/filesystems.php:33-45:

'local'  => root: storage_path('app/private')   ← خاص، بلا رابط عام
'public' => root: storage_path('app/public'),
           url:  https://moonui4.elbaset.com/moon-erp-be/storage   ← المنشور فعليًا

ب) الأكسسور يبني رابطًا عامًا لمسار خاص — وهنا الخطأ المباشر: Modules/Core/app/Models/Product.php:207-222

public function getImageUrlAttribute(): ?string
{
    if ($this->image) {
        return url('storage/'.$this->image);          // ✔ يعمل لو المسار داخل القرص العام
    }
    $attachment = ... $this->attachments()->first();
    if ($attachment) {
        return url('storage/'.$attachment->file_path); // ✘ file_path = attachments/… على القرص الخاص ⇒ 404 مضمون
    }
    return null;
}
هذا الفرع الثاني لا يمكن أن ينجح أبدًا. إنه يركّب بادئة /storage/ (التي تشير للقرص العام) فوق مسار يعيش على القرص الخاص. النتيجة رابط صحيح الشكل، خاطئ المحتوى — وهذا أسوأ من إرجاع null، لأن الواجهة تظنه رابطًا سليمًا وتحاول تحميله بدل عرض البديل فورًا.

٢.٣ التذكرة موجودة أصلًا ومعترف بها في الكود

التعليق في Modules/WebStore/app/Console/FixImagePaths.php:11-19 يصف المشكلة حرفيًا:

historical records store their image in the generic Attachment path
(attachments/{company}/{Y-m}/…). Those files live on the private `local`
disk, so /storage/attachments/… returns 404 for the storefront/mobile apps.
New uploads are already copied into the public area by
ensurePubliclyServable on save, but pre-existing rows were never re-saved.

أي أن الفريق شخّص المشكلة سابقًا (STORE-040) وكتب لها أمر إصلاح، لكن الإصلاح جزئي — انظر الفجوة في القسم ٤.

٢.٤ حالة قاعدة بيانات moonui4 اليوم تم القياس

24إجمالي المنتجات
18لها صورة (تجريبية)
0صفوف في جدول attachments
18/18روابط الصور ترجع 200
مهم جدًا لفهم الموقف: على moonui4 لا توجد مشكلة قابلة للتكرار حاليًا — لأنه لا توجد بيانات حقيقية أصلًا. كل الـ24 منتج هي بذور تجريبية صورها في demo/products/pNNN.png على القرص العام، وكل الـ18 رابطًا ترجع HTTP 200 (تم التحقق بـcurl -sI على كل رابط)، وجدول attachments فارغ تمامًا.
إذن المشكلة التي تصفها ليست في moonui4 — هي في بيانات مون الحقيقية (نسخة moonui وما فيها من ١٧ ألف صنف بصورهم). هذا التحليل يصف الفخ الذي سينفجر لحظة وصول تلك البيانات، وهو مرتبط مباشرةً بطلب الاستيراد المعلّق. لم يتم — ولن يتم — فحص قاعدة أو ملفات /home/moonui من هنا، فهذا يخالف عزل بيئات التطوير المتوازية.

٣. جذر المشكلة Root cause

هناك نظامان لتخزين الصور في Moon ERP، وُلدا في وقتين مختلفين ولم يتم توحيدهما:

المسارأين يُخزَّنكيف يُقرأيصلح لمتجر عام؟
عمود products.image
الرفع من شاشة المنتج
storage/app/public/uploads/products/…
قرص عام
رابط مباشر عبر symlink نعم ✔
جدول attachments
الرفع من واجهة المرفقات العامة
storage/app/private/attachments/…
قرص خاص
API موثّق: GET /api/attachments/{id}/download لا ✘

الأول مصمَّم للنشر العام. الثاني مصمَّم للمستندات الداخلية (فواتير، عقود، تقارير) ولذلك هو خاص عمدًا وهذا صحيح أمنيًا. المشكلة أن صور المنتجات تُرفع أحيانًا عبر المسار الثاني، ثم يحاول getImageUrlAttribute() تقديمها كأنها من المسار الأول.

ملاحظة أمنية تستحق الانتباه: الحل ليس "افتح مجلد المرفقات للعامة". مجلد attachments/ يحتوي — بحكم تصميمه — على مستندات داخلية لكل الشركات. فتحه للعامة يعني تسريبها كلها. لذلك أي حل مقبول يجب أن ينقل/ينسخ صور المنتجات فقط إلى المنطقة العامة، لا أن يوسّع صلاحية المجلد.

٣٫٥ سؤالان محدَّدان — بالإجابة والدليل Two direct questions, answered

س١: هل في جدول attachments أي علامة تقول «ده صورة المنتج»؟

لا. ولا واحدة. ده كل أعمدة الجدول — مقروءة من قاعدة البيانات مباشرةً:

id · company_id · attachable_type · attachable_id · file_name
file_path · file_size · mime_type · uploaded_by
created_at · updated_at · deleted_at
مفيش: type · collection · is_primary · is_main · label · title · position · sort_order. يعني مافيش أي طريقة مباشرة نعرف بيها إن مرفقًا بعينه هو صورة المنتج الرئيسية، ولا حتى إنه صورة "مقصودة للعرض" أصلًا.

الإشارات المتاحة الوحيدة، وكل واحدة فيها ضعف:

الإشارةتنفع لإيهضعفها
mime_type LIKE 'image/%'تستبعد الـPDF والوورد بثقةلا تفرّق بين صورة المنتج وصورة إيصال/شهادة تحليل مرفقة
attachable_type = Productتحصر النطاق في المنتجاتلا تحدد أيّ صورة هي الرئيسية لو فيه أكتر من واحدة
file_nameممكن يحمل الكود (SKU-123.jpg)اسم الملف الأصلي من جهاز المستخدم — غير مضمون إطلاقًا
ترتيب id / created_at"الأقدم = الرئيسية"افتراض وليس حقيقة. ده بالظبط اللي الكود بيعمله دلوقتي وهو مصدر عيب اختيار الـPDF
نتيجة عملية مهمة لطلبك «ننقل الصور زي ما هي»: النقل نفسه سهل. الصعب هو «أنهي صورة هي صورة المنتج» — والجدول لا يجيب. لو المنتج له مرفق واحد صورة، القرار محسوم. لو له أكتر من واحد، محتاجين قاعدة تحكيم (أو نضيف العَلَم الناقص — انظر ق٥).

س٢: خدمة رفع صورة المنتج في المتجر — بتفرّع Attachment ولا حاجة تانية؟

حاجة تانية خالص. مافيش أي تفريع. النظامان منفصلان تمامًا ولا يعرف أحدهما الآخر.

الدليل — HandlesImageUpload::resolveImageField() في app/Http/Controllers/Concerns/HandlesImageUpload.php:19-22:

if ($request->hasFile($field)) {
    $stored = $request->file($field)->store("uploads/{$folder}", 'public');
    // ← يكتب على القرص العام صراحةً، ولا يُنشئ أي صف Attachment
    return $stored;
}

وبحث شامل عن كل مواضع إنشاء المرفقات في المشروع كله:

$ grep -rn "Attachment::query()->create\|attachments()->create" Modules app

Modules/Core/.../AttachmentController.php:110        ← الـAPI العام
Modules/Clinic/.../SubmitExternalResult.php:51       ← نتائج معامل خارجية
Modules/Production/.../MfgCoaController.php:291      ← شهادات تحليل
Modules/Production/.../MfgTollContractController.php:391  ← عقود

→ ولا موضع واحد لصور المنتجات.
الخلاصة: رفع صورة المنتج (من شاشة مون أو من POST /api/products) بيعمل حاجتين بس: (١) يحفظ الملف في storage/app/public/uploads/products/ على القرص العام، (٢) يحطّ المسار النسبي في عمود products.image (وصفوف product_images للمعرض). صفر تعامل مع جدول attachments.
وده بيوضّح المصدر الحقيقي للمشكلة: الصور اللي في مون كـattachments مادخلتش من مسار صور المنتجات أصلًا — دخلت من واجهة المرفقات العامة، اللي مصمَّمة للمستندات الداخلية وبتكتب على القرص الخاص عمدًا. الأكسسور getImageUrlAttribute() بيحاول يعمل «جسر» بين النظامين في فرعه التاني — وبيفشل، لأنه بيبني رابط القرص العام فوق مسار القرص الخاص. هو الجسر الوحيد بين النظامين، وهو مكسور.

وماذا يعني هذا لخيار «ننقل الصور زي ما هي»؟

النقل الحرفي (نسخ ملفات attachments/ ونخلي الصفوف زي ما هي) لن يُصلِح شيئًا — لأن العطل مش في مكان الملف وبس، هو في إن لا وجود لعَلَم يربط المرفق بكونه صورة المنتج. الخيارات الواقعية:

الطريقةالحكم
أ الترقية (Promotion): ننسخ ملف المرفق-الصورة إلى المنطقة العامة ونكتب مساره في products.image — فيتحوّل لمنتج «عادي» له صورة زي أي منتج مرفوع من شاشة مون. مُوصى به — يوحّد النظامين، ويخلي نتيجة الاستيراد مطابقة تمامًا للرفع اليدوي. وهو المسار اللي أمر FixImagePaths بيعمله فعلًا في تمريرته التانية.
ب إضافة العَلَم الناقص: عمود جديد على attachments (مثلاً is_product_image أو collection)، ويقرأ منه الأكسسور. يحتاج migration — أنظف مفاهيميًا وبيحل المشكلة لكل الموديلات مستقبلًا، لكنه تغيير في جدول مشترك بيستخدمه ٤ موديولات.
ج فتح مجلد attachments للعامة. مرفوض — بيسرّب المستندات الداخلية لكل الشركات. مذكور هنا فقط لاستبعاده صراحةً.

(أ) و(ب) مش متعارضين: ممكن (أ) دلوقتي لأنها بتحل الاستيراد فورًا بلا migration، و(ب) لاحقًا كتنظيف معماري.

★ الحل الجذري — الجدول الصحيح موجود بالفعل ولم يُستخدم The radical fix

تحديث من المالك (2026-07-19): الـ١٧ ألف صنف لم يستخدمها أي عميل حقيقي وتم إدخالها آليًا، فيمكن إعادة إدخالها من الصفر. وهذا يفتح الحل الصحيح بدل الترقيع.

الاكتشاف

Moon ERP عنده أصلًا نظام كامل لصور المنتجات المتعددة، بعلامات صحيحة، على القرص العام — ومُهمَل تمامًا:

$ SHOW COLUMNS FROM product_images;
id · product_id · image · position · created_at · updated_at

$ SELECT COUNT(*) FROM product_images;
0   ← صفر صفوف. الجدول موجود ومربوط ولم يُستعمل قط.

والعلاقة موجودة ومرتّبة في Modules/Core/app/Models/Product.php:198-201:

public function images(): HasMany
{
    return $this->hasMany(ProductImage::class)->orderBy('position');
}
هنا «العلامات» التي تسأل عنها — وهي موجودة أصلًا ولا تحتاج أي migration:
  • products.imageالصورة الرئيسية (علامة «ده الصورة الأساسية» = وجودها في هذا العمود)
  • product_images.positionترتيب صور المعرض (علامة «ده الصورة رقم ٢، ٣، ٤…»)
كلاهما على القرص العام، ويُخدَمان مباشرةً بدون توثيق. وهذا بالضبط ما يفعله رفع الصور من شاشة مون اليوم (ProductController:92-108: الصورة الأولى تُرقَّى إلى image والباقي يدخل product_images بترتيبه).

إذن ما الخطأ الحقيقي؟

الصور دخلت من الباب الغلط. الاستيراد الآلي رفعها عبر واجهة المرفقات العامة — وهي مصمَّمة للمستندات الداخلية، خاصة عمدًا، وبلا أي مفهوم «صورة رئيسية» أو «ترتيب». بينما كان الباب الصحيح — products.image + product_imagesموجودًا وجاهزًا وفارغًا.

أي أن هذه ليست ثغرة في التصميم أصلًا؛ التصميم سليم. الخلل أن البيانات أُدخِلت في المسار الخطأ. ولأنه لم يستخدمها أحد بعد، فالحل الجذري متاح: إعادة الإدخال في المسار الصحيح بدل بناء جسور فوق الخطأ.

الخطة الجذرية

#الخطوةلماذا
١ الاستيراد يكتب في المسار الصحيح: ملفات الصور تُنسَخ إلى storage/app/public/uploads/products/ بأسماء فريدة (مبنية على id/hash وليس basename)، ثم الصورة الأولى → products.image، وباقي الصور → product_images بـposition تصاعدي. نتيجة مطابقة تمامًا للرفع اليدوي من شاشة مون. صفر تعامل مع المرفقات.
٢ إصلاح ProductImageResource — انظر التحذير أسفله. بدونه المعرض يفشل حتى لو البيانات صحيحة ١٠٠٪.
٣ حذف الجسر المكسور من getImageUrlAttribute() (الفرع الثاني الذي يبني رابطًا عامًا لمسار خاص)، أو جعله يُرجِع null. بعد (١) لا داعي له إطلاقًا، ووجوده يعيد إنتاج المشكلة صامتًا.
٤ سدّ الباب: منع/تحذير عند رفع صورة منتج عبر واجهة المرفقات. وإلا تتكرر نفس الغلطة في أول استيراد قادم.
⚠️ عطل يجب إصلاحه قبل أي استيراد — وإلا سيفشل المعرض كله:
Modules/WebStore/app/Http/Resources/ProductImageResource.php:13 يعيد المسار الخام بلا بناء رابط:
'image' => $this->image,      // → "uploads/products/x.jpg" — مسار نسبي مكسور
بينما كل المصادر الأخرى في نفس المجلد تبني الرابط: url('storage/'.$this->image). النتيجة: الصورة الرئيسية تعمل، وكل صور المعرض تفشل — ولأن الأصناف اليوم بلا معرض، العطل كامن وغير مرئي وسينفجر لحظة وصول أول منتج متعدد الصور.
الحصيلة: لا حاجة لعمود is_product_image على المرفقات (ق٥ سقط)، ولا لفتح مجلد المرفقات، ولا حتى لأمر الترحيل FixImagePaths — فهو يعالج بيانات لن نُبقي عليها. العلامات المطلوبة موجودة بالفعل: products.image للرئيسية وposition للترتيب.

٤. الفجوة The gap

الأمر webstore:fix-image-paths موجود ويعالج جزءًا من المشكلة، لكنه لا يكفي. ما يغطيه وما لا يغطيه:

الحالةيغطيها الأمر؟التفصيل
products.image LIKE 'attachments/%'نعمينسخ الملف إلى uploads/products/ ويصحّح العمود.
image IS NULL + يوجد مرفقنعميرقّي أول مرفق إلى عمود image.
المرفق ليس صورة أصلًا (PDF، Word)لاخلل حقيقي: يأخذ أول مرفق حسب الـid بلا أي فحص لنوع الملف. كارت داتا PDF مرفوع قبل الصورة سيصبح "صورة المنتج". ونفس العيب في getImageUrlAttribute() نفسه.
الملف المصدر غير موجود على القرصيُبلّغ فقطيطبع [missing source] ويتخطاه. هذه الصفوف تبقى مكسورة للأبد ولا حل برمجي لها — الملف ضاع.
معرض الصور product_imagesلاالأمر يعالج ٧ نماذج، وليس ProductImage من بينها.
الأمر غير موصول بأي مسار تلقائييدويليس في local-deploy.sh ولا في الـscheduler. لو نُسي بعد الاستيراد، لن يعمل شيء.
الفجوة الأخطر عمليًا: الأمر يعمل مرة واحدة على البيانات القديمة. لكن الاستيراد القادم من مون (١٧ ألف صنف) سيُدخل صفوفًا جديدة — ولن تمرّ على ensurePubliclyServable لأن تلك تعمل فقط عند الحفظ من الـController، لا عند الإدخال المجمّع بالـseeder/import. أي أن المشكلة ستتكرر بالكامل ما لم يُعالَج الاستيراد نفسه.

٥. الملفات المتأثرة Affected files

الملفالدور
Modules/Core/app/Models/Product.php:207الأكسسور image_urlمصدر الرابط الخاطئ (الفرع الثاني)
Modules/Core/app/Http/Controllers/AttachmentController.php:106$file->store() بلا disk ⇒ يهبط على القرص الخاص
app/Http/Controllers/Concerns/HandlesImageUpload.php:56ensurePubliclyServable() — العلاج الصحيح، لكنه يعمل عند الحفظ من Controller فقط
Modules/WebStore/app/Console/FixImagePaths.phpأمر الترحيل (STORE-040) — تغطية جزئية
Modules/WebStore/app/Http/Resources/StorefrontProductResource.php:31
…/StorefrontProductListResource.php:20
يمرّران image_url للمتجر كما هو
Modules/WebStore/app/Http/Resources/ProductImageResource.php:13يعيد image الخام بدون بناء رابط — يحتاج مراجعة
projects/storefront/src/app/shared/product-image.component.tsواجهة المتجر — سليمة، لا تحتاج تعديلًا (بها بالفعل بديل عند الفشل)
لا يوجد ما يُصلَح في كود المتجر. مكوّن الصورة يتعامل مع الفشل بشكل صحيح ويعرض البديل. أي إصلاح حقيقي يكون في الباك-إند.

٦. الحالات الحدّية Edge cases

٧. خطة التنفيذ — كـ Work Packages Proposed plan

مقترحة فقط. لن يُكتب أي كود قبل موافقتك.

WPالعملالريبولماذا
WP-IMG-1 إصلاح الأكسسور ليصدق. في Product::getImageUrlAttribute(): لا تُرجِع رابطًا لمسار على القرص الخاص. إما تُرجَع null (فيظهر البديل فورًا وبصدق)، أو يُبنى رابط عبر مسار عام مخصَّص للصور. + فلترة المرفقات على image/%. BE يوقف "الرابط الكاذب" من المنبع. أرخص خطوة وأعلاها أثرًا.
WP-IMG-2 تحصين أمر الترحيل: فلترة نوع الملف، اسم هدف فريد (id/hash بدل basename) لمنع الدهس، تغطية product_images، وتقرير --dry-run يذكر الحجم المطلوب وعدد الملفات المفقودة. BE يجعل تشغيله على ١٧ ألف صف آمنًا بدل مقامرة.
WP-IMG-3 سدّ باب التكرار: جعل نشر الصورة يحدث في طبقة الموديل/الخدمة المشتركة (لا في الـController فقط) حتى يمرّ عليها أي استيراد أو seeder. BE القاعدة المتبعة في المشروع: الإصلاح في الطبقة المشتركة، لا شاشة بشاشة.
WP-IMG-4 ربطه باستيراد مون: أن ينقل الاستيراد الصور مباشرةً إلى المنطقة العامة بأسماء فريدة، ويُشغَّل تقرير تحقق بعده. BE وإلا تعود المشكلة كاملةً مع أول استيراد.

الترتيب مقصود: WP-IMG-1 وحده يجعل السلوك صادقًا فورًا (بديل بدل صورة مكسورة) حتى لو تأخر الباقي.

٨. قرارات تحتاج المالك Owner decisions

ق١ — سلوك الصورة غير المنشورة مُوصى به

أن يُرجِع الأكسسور null بدل رابط سيقع 404.

البديل يظهر فورًا وبلا وميض، والواجهة تقول الحقيقة. البديل الآخر (ترك الرابط الكاذب) يخفي المشكلة ويبطئ الصفحة.

ق٢ — أي مرفق هو "الصورة"؟

عند وجود عدة مرفقات لمنتج: أول صورة حسب الـid؟ أم يُعتمد فقط ما رُفع من شاشة المنتج؟

الوضع الحالي يأخذ أول مرفق أيًا كان نوعه — وهذا خطأ محسوم.

ق٣ — نسخ أم نقل؟

النسخ آمن لكنه يضاعف المساحة (١٧ ألف صورة). النقل يوفّر لكنه يكسر أي مرجع قديم للمرفق.

أحتاج قياس الحجم الفعلي أولًا قبل التوصية.

ق٥ — أنهي مرفق هو صورة المنتج؟ جديد

الجدول مافيهوش عَلَم يقول ده صورة المنتج (القسم ٣٫٥). المقترح: أول مرفق نوعه image/% حسب أقدم id، ويُرقَّى إلى products.image.

لو ده مش كافي لبياناتك الحقيقية، البديل إضافة عمود is_product_image بـmigration — قرارك.

ق٤ — الملفات المفقودة

الصفوف التي ضاع ملفها لا حل برمجي لها. أُخرج قائمة بالأصناف المتأثرة؟

وسؤال معلّق من قبل، صار الآن مرتبطًا مباشرةً: استيراد منتجات مون — بصور أم بدونها؟ كل الأصناف أم عينة؟ وهل هي بيانات عميل حقيقية (لأن نشرها في متجر عام قرار مختلف تمامًا)؟

٩. ما تحقّقت منه مقابل ما استنتجته Verified vs inferred

البند
تحقّقمواضع الكود كلها المذكورة أعلاه — قُرئت سطرًا بسطر، والأسطر مثبتة.
تحقّقإعدادات الأقراص، وجود الـsymlink ووجهته، وأن storage/app/private لا يحوي إلا .gitignore.
تحقّقأرقام moonui4: ٢٤ منتج، ١٨ بصورة تجريبية، صفر مرفقات، و١٨/١٨ رابطًا ترجع 200.
تحقّق--dry-run لا يكتب شيئًا (كل الكتابات داخل if (! $dryRun)) — وشُغّل فأعطى أصفارًا.
استُنتجأن بيانات مون الحقيقية تستخدم مسار المرفقات — مبني على تعليق STORE-040 الصريح، وليس على فحص مباشر (فحص moonui ممنوع من هنا).
استُنتجأن الاستيراد المجمّع لن يمرّ على ensurePubliclyServable — مبني على قراءة الكود، ولم يُختبر باستيراد فعلي.
لم يُتحققلم يُفتح متصفح. كل ما سبق على مستوى الكود وقاعدة البيانات وcurl. لم أرَ الصفحة بعيني.
وتصحيح لفرضيتي الأولى: بدأت التحقيق وأنا أظن أن تطبيق الإدارة يعرض صور هذه المنتجات وأن المتجر وحده مكسور. الفحص أثبت العكس على moonui4: جدول المرفقات فارغ، فشاشة كتالوج الإدارة — التي تعتمد على المرفقات — لا تعرض صورًا لهذه المنتجات أصلًا، بينما المتجر يعرضها بنجاح. الفرق الحقيقي ليس "إدارة تعمل ومتجر لا يعمل"، بل مساران للتخزين، أحدهما غير صالح للنشر العام.

تم إعداده على moonui4 · فرع hazemdev4 · 2026-07-19 — لم يُعدَّل أي ملف كود ولم تُنفَّذ أي كتابة على قاعدة البيانات.