# فاز ۱۵ — پنل ادمین حرفه‌ای: چند-ادمینی، نقش‌ها، داستان‌سازی، نمای کامل والدین

**نسخه: 1.10.0** · تاریخ: 1404/07/04 (2026-09-26)
درخواست کاربر: «بروزرسانی و اصلاح پنل ادمین + تعریف ادمین جدید + تعیین سطح دسترسی + تعریف داستان مطابق استانداردهای برنامه + مشاهده اطلاعات کامل والدین/کودکان (براساس دسترسی) + ظاهر حرفه‌ای‌تر»

---

## ۱. مدل نقش‌ها — سه سطح، اجرای سمت سرور

| نقش | مجوزها | توضیح |
|---|---|---|
| **superadmin** (مدیر ارشد) | همه: stats·parents:view·parents:delete·audio·words·stories·admins·audit | تنها نقشی که می‌تواند ادمین بسازد/نقش عوض کند/والد حذف کند |
| **content** (مدیر محتوا) | stats · audio:manage · words:manage · stories:manage | واژه، داستان، صدا — بدون دسترسی به داده‌ی کاربران |
| **support** (پشتیبان) | stats:view · parents:view | مشاهده‌ی کامل والدین/کودکان — بدون دسترسی به محتوا |

قواعد امنیتی (ادامه‌ی اصول فاز ۷ — «دقت بالا و رعایت اصول امنیتی»):
- **مجوزها همیشه از DB خوانده می‌شوند نه از JWT** → تغییر نقش یا غیرفعال‌سازی، حتی با
  توکنِ هنوز-معتبر، **بلافاصله** اعمال می‌شود (`server/roles.js` → `requirePerm`)
- ردِ دسترسی هم در `admin_audit` ثبت می‌شود (`perm_denied`) — دید کامل برای مدیر ارشد
- ادمین **غیرفعال** در login با همان پیام عمومی 401 رد می‌شود (ضد user-enumeration)
- **حفاظت خود**: تغییر نقش/غیرفعال‌سازی خود → 400 SELF_LOCK
- **آخرین مدیر ارشد**: تنزل یا غیرفعال‌سازی → 409 LAST_SUPERADMIN (قفل‌شدن کل سیستم ممنوع)
- رمز ادمین: ≥۱۰ نویسه + حرف + رقم · bcrypt cost ۱۲ · بازنشانی فقط توسط مدیر ارشد + audit
- hash رمز هرگز در هیچ پاسخی برنمی‌گردد؛ رمز هرگز در audit نمی‌آید (تست‌شده)

**مهاجرت DB**: ستون‌های `name/role/active/created_by` به `admins` اضافه شد؛ ادمین‌های
موجود → `superadmin` فعال (رفتار قبلی حفظ می‌شود). بوت‌استرپ env همچنان فقط وقتی
جدول خالی است.

## ۲. تعریف ادمین و مدیریت او (superadmin)

- `GET /api/v1/admin/admins` — فهرست (بدون hash)
- `POST /api/v1/admin/admins` — ساخت: اعتبارسنجی ایمیل/رمز/نقش + یکتایی ایمیل (409)
- `PATCH /api/v1/admin/admins/:id` — تغییر نقش / فعال-غیرفعال (با هر دو حفاظت طلایی)
- `POST /api/v1/admin/admins/:id/password` — بازنشانی رمز
- UI: جدول مدیران با نشان نقش‌رنگی، انتخابگر نقش درجا، دکمه‌ی فعال/غیرفعال، مودال ساخت
  و مودال بازنشانی رمز؛ ردیف خودِ کاربر هایلایت و بدون دکمه‌ی تغییر (قفل خودکار)

## ۳. داستان‌سازی «مطابق استانداردهای برنامه»

معماری مثل واژه‌های فاز ۱۰ — **داده در DB، اُورلی عمومی قابل کش SW**:

- `custom_stories` جدول جدید (pack/title یکتا · sentences و question به‌صورت JSON)
- `GET /data/stories-custom/:packId.json` → `{ stories }` (خارج از /api → کش SW، آفلاین ✓)
- کلاینت `Stories.load` بعد از داستان‌های استاتیک، اُورلی را best-effort می‌گیرد،
  **با همان Stories.validate اعتبارسنجی** و براساس id ادغام می‌کند (بدون تکرار)

**تک‌منبع حقیقتِ اعتبارسنجی**: سرور همان `Stories.validate` کلاینت را اجرا می‌کند
(import مستقیم ماژول خالص) — یعنی داستان ادمین دقیقاً همان قواعدی را می‌گذرد که
داستان‌های استاتیکِ برنامه: جمله‌ها فقط از حروفِ همان پک (با موتور `parseWordText`
مشترک)، focusLetters معتبر، requiredMastery ۱..۴، سؤال با گزینه و پاسخ درست،
id با پیشوند `{pack}_st_` (+ عنوان یکتا در برابر استاتیک و سفارشی → 409).

**ویرایشگر در UI**: انتخاب حروف کانونی (چیدمان تراشه‌ای، حداکثر ۳)، جمله‌های داینامیک
(۲..۶) با ایموجی، سؤال اختیاری با ۳ گزینه، **اعتبارسنجی و پیش‌نمایش زنده** (همان
ماژول Stories سمت مرورگر — خطاها فارسی و قابل فهم، دکمه‌ی ذخیره تا سبز شدن قفل است)،
سطح تسلط لازم، ویرایش کامل با بازاعتبارسنجی، حذف با تأیید. id نهایی =
`{pack}_st_c_{hex}` (پیشوند الزامی را رعایت می‌کند).

## ۴. نمای کامل والدین/کودکان (براساس دسترسی)

- `GET /parents` (parents:view) — **ایمیل کامل** (ماسک قبلی متعلق به دوران تک-ادمینی بود؛
  حالا دسترسی نقش‌محور و بازدیدها audited است)
- `GET /parents/:id` — جزئیات کامل: والد + هر کودک با ستاره‌ها، **حروف آشنا (L1+)/مسلط
  (L2+)/گویا (L5)**، نشان‌ها، رویداد ۷ روز، دستگاه‌ها، آخرین فعالیت (کوئری تجمعی واحد)
- `DELETE /parents/:id` → فقط superadmin (parents:delete)
- UI: جدول با جست‌وجوی زنده + مودال جزئیات با «کارت کودک» (۶ آمار کلیدی)؛ هر بازدید
  در وقایع `parents_view` ثبت می‌شود (پیوست حسابرسی PII)

## ۵. ظاهر حرفه‌ای — بازطراحی کامل پنل

- **سایدبار ثابت راست (RTL)** با برند، بخش‌بندی ناوبری (مرور/کاربران/محتوا/امنیت)،
  کارت هویت مدیر (آواتار حرف اول، نام، ایمیل، نشان نقش + تعداد مجوز) — در موبایل به
  نوار بالای افقی جمع می‌شود
- **ناوبری مجوزمحور**: تب‌های بی‌اجازه اصلاً رندر نمی‌شوند (سمت سرور هم اجبار می‌شود)
- دیزاین‌سیستم جدید در `assets/css/admin.css` (بدون CDN — در سندباکس/آفلاین کامل):
  تم سرمه‌ای با نئونِ برند، کارت‌های گرادیانی با سایه، جدول‌های hover، نشان‌های
  نقش‌رنگی، مودال‌های بلر-پس‌زمینه، توست، `prefers-reduced-motion`
- داشبورد ۸ کارت آماری (۲ جدید: داستان سفارشی، مدیر فعال) + پک‌ها
- SW: `admin.html`، `js/admin/` و حالا `assets/css/admin` همیشه از شبکه (کد مدیریتی
  هرگز کش نمی‌شود)

## ۶. تست‌ها — همه سبز

| سوئیت | نتیجه |
|---|---|
| `tests/admin.test.js` (جدید، سرور ایزوله) | ✅ 17/17 — بوت‌استرپ نقش · اعتبارسنجی ساخت ادمین · ماتریس مجوز ۳ نقش × مسیرها (401/403/200) · توکن والد → 403 · داستان: استاندارد + حرف خارج از پک + جمله‌ی تجزیه‌ناپذیر + عنوان تکراری (409) + PATCH/DELETE + اورلی عمومی · جزئیات والد (L1+/L2+/L5/نشان) · SELF_LOCK · LAST_SUPERADMIN · بازنشانی رمز · audit (بدون رمز) |
| E2E ادمین (+۴ استپ، ۱۵ کل) | ✅ 15/15 — ایمیل کامل + کارت کودک · ساخت ادمین محتوا از فرم · ورود او → تب‌های ممنوع مخفی/مجوز نمایان · ویرایشگر داستان: جمله‌ی «hello world» → ⚠️ زنده + دکمه‌ی قفل → جمله‌های درست → ✅ → ذخیره → اورلی عمومی · پاکسازی: حذف داستان + غیرفعال‌سازی + ورودِ مسدود 401 |
| واحد (کل زنجیره) | ✅ 189/189 در ۱۲ سوئیت (تصحیح دفترچه: مجموع v1.9 واقعی ۱۷۲ بود؛ رقم ۱۹۹ در خلاصه‌ی قبلی اشتباه ثبت شده بود) |
| e2e اپ · packs | ✅ 24/24 · 6/6 |
| Lighthouse (اپ کودک) | ✅ میانه ۸۶ · a11y/BP/SEO ۱۰۰ |

**باگ‌های واقعی که تست‌ها شکار کردند** (درس‌های فاز):
1. **TDZ در ویرایشگر**: `addEventListener('input', live)` قبل از `const live` → کل
   مودال می‌مرد — ترتیب تعریف توابع در JS ماژول‌ها مرگبار است
2. id موقت `'new'` پیشوند `{pack}_st_` نداشت → اعتبارسنجی همیشه می‌بُاخت → id موقتِ
   سازگار با پیشوند
3. `page.waitForFunction(fn, {timeout})` دو-آرگومانی در Playwright آرگومان دوم را
   **arg** می‌گیرد نه options (باید `fn, arg, options`) — تایم‌اوت بی‌اثر
4. متغیر Node داخل کال‌بک مرورگر (waitForFunction/evaluate) تعریف نیست — باید آرگومان
   داده شود (MARYAM/STORY_TITLE)
5. داده‌ی تست روی سرور زنده بین اجراها می‌ماند → ایمیل/عنوان یکتا در هر اجرا (TS)؛
   اجرای دوم به 409 می‌خورد و مودالِ باز، استپ‌های بعدی را می‌بست
6. معماری lazy (پر شدن selectها فقط بعد از کلیک تب) ترتیب انتظار/کلیک در e2e قدیمی
   را بن‌بست کرد
7. `tr:nth-child(2)` با ساختار thead/tbody جدید هیچ‌وقت match نمی‌شد (شمارش از tbody)

## ۷. فایل‌های کلیدی

```
server/roles.js                     ← جدید: ROLE_PERMS + requirePerm/requireActiveAdmin
server/routes/stories.js            ← جدید: CRUD داستان + اورلی عمومی
server/routes/admin.js              ← نقش‌ها + CRUD ادمین + جزئیات والد + مجوز مسیرها
server/routes/words.js              ← requirePerm('words:manage')
server/db.js                        ← مهاجرت admins + جدول custom_stories
client/assets/css/admin.css         ← جدید: دیزاین‌سیستم پنل
client/admin.html                   ← بازطراحی: سایدبار + ۷ صفحه
client/js/admin/app.js              ← بازنویسی کامل (~۸۰۰ خط)
client/js/core/stories.js           ← ادغام اورلی داستان‌های سفارشی
client/sw.js                        ← عدم کش CSS ادمین · VERSION 1.10.0
```

**نکته‌ی استقرار**: اولین ادمین از env ساخته می‌شود (superadmin)؛ بعد از آن مدیر ارشد
می‌تواند از پنل ادمین بسازد. برای محدودکردن دسترسی، به ادمین‌های جدید نقش content یا
support بدهید — همه‌ی مسیرهای قدیمی همان رفتار را برای مدیر ارشد دارند.
