# فاز ۱۳ — درون‌ریزی بکاپ + Lighthouse در CI (بستن معیارهای باز)

**نسخه: 1.8.0** · تاریخ: 1404/07/03 (2026-09-25)
درخواست کاربر: «بستن معیارهای باز» → (۱) درون‌ریزی بکاپ JSON (۲) Lighthouse در CI (۳) پروتکل تست UX (سند ۲۳)

---

## ۱. درون‌ریزی بکاپ — `POST /api/v1/parent/import`

همان فرمتی که `GET /parent/export` می‌دهد، برمی‌گرداند (round-trip کامل).

### معناشناسی بازیابی — «merge، هرگز تنزل نمی‌دهد» (همان فلسفه‌ی `progress:batch`)

| وضعیت کودکِ فایل | رفتار |
|---|---|
| id موجود و **همین والد** | به‌روزرسانی: mastery/correct/wrong/stars → `max` · box/dueAt از رکوردِ بیشترتلاش‌شده · نشان‌ها فقط افزودنی (`INSERT OR IGNORE`) · فیلدِ نامعتبر در فایل = مقدار سالمِ فعلی حفظ می‌شود |
| id موجود ولی **والد دیگر** | کودکِ جدید با id تصادفی و پسوند «(بازیابی)» — **هرگز** به داده‌ی والد دیگر دست نمی‌زند |
| id ناشناخته | درج با همان id — پیوند جلسه‌ی دستگاه‌های قبلی حفظ می‌شود |
| رویدادها | dedup روی کلید کامل `verb\|ts\|result\|letter\|game` — درون‌ریزی دوباره = صفر رویداد تکراری |
| صداها | بازنویسی فایل با همان الگوی نام‌گذاری آپلود (`pa_{pid8}_{pack}_{type}_{item}`) → idempotent، بدون خطر پیمایش مسیر (نام فایل از داده ساخته می‌شود، نه از فایل خوانده) |

رمز عبور/ایمیل والد **هرگز** از فایل خوانده نمی‌شود — فایل فقط «داده‌ی کودک» است
و همه‌ی نوشتن‌ها مقید به `req.auth.parentId` (تست‌شده: حساب والد دیگر دست‌نخورده).

### سقف‌های ضد سوءاستفاده (همگی تست‌شده)

۲۰ کودک · ۸ پک/کودک · ۱۲۸ حرف/پک · ۵۰۰۰ رویداد · ۲۰۰ صدا · ۳MB/صدا.
بدنه: مسیر `/import` پارسر اختصاصی **24MB** دارد (قبل از پارسر سراسری 120KB —
`req._body` جلوی پارس دوباره را می‌گیرد). کل عملیات در **یک تراکنش** SQLite —
خطا = rollback کامل. موارد نامعتبر skip + هشدار (نه خطای مهلک).

### رابط کاربری (parentsScreen — دکمه‌ی «📥 بازیابی از بکاپ»)

دو راه ورود، هر دو در iframe سندباکس‌شده کار می‌کنند: **چسباندن متن JSON**
(مسیر اصلی) و انتخاب فایل (FileReader). جریان: بررسی (پیش‌نمایش تعداد کودک/
رویداد/صدا + تاریخ) → بازیابی → نتیجه (به‌روزرسانی‌شده/جدید + هشدارها) →
اگر کودکِ این دستگاه در بازیابی بود، همان لحظه `SyncService.pull()` + موتور
دوباره init می‌شود تا گزارش/اتاق جوایز تازه شود.

### باگ واقعی که E2E شکار کرد (و رفع شد)

رویدادهایی که `result` آن‌ها به‌صورت **رشته‌ی JSON** ذخیره شده بود (مثل seed
خود e2e)، در export با `JSON.parse` «رشته» می‌مانند و درون‌ریزی ابتدا آن‌ها را
به `{}` تبدیل می‌کرد → کلید dedup نمی‌خواند → ۳ رویداد تکراری (10→13).
فیکس: `JSON.stringify` بی‌آلایش (رشته‌ی دیتابیس دقیقاً بازتولید می‌شود) +
رگرسیون‌تست واحد. درس: **round-trip داده باید «بی‌آلایش» باشد، نه نرمال‌ساز**.

## ۲. Lighthouse در CI — `npm run test:lighthouse` + `.github/workflows/ci.yml`

### بودجه‌ها (میانه‌ی ۳ اجرا — واریانس ±۲ ذاتی Lighthouse با میانه حذف می‌شود)

| دسته | بودجه | نتیجه‌ی نهایی (میانه) |
|---|---|---|
| کارایی (Performance) | ≥ ۸۵ | **۸۵-۸۶** (از ۶۷ اولیه) |
| دسترس‌پذیری (Accessibility) | ≥ ۹۵ | **۱۰۰** (از ۸۶) |
| شیوه‌های خوب (Best-Practices) | ≥ ۹۰ | **۱۰۰** |
| SEO | ≥ ۹۰ | **۱۰۰** |

`scripts/lighthouse.mjs`: سرور ایزوله با دیتای موقت روی 8093 → ۳ اجرا →
میانه → مقایسه با بودجه → exit غیرصفر در CI قرمز. گزارش‌ها با
`LH_ARTIFACT_DIR` به‌عنوان artifact آپلود می‌شوند.

### بهینه‌سازی‌های واقعی (نه شل‌کردن بودجه) — کارایی ۶۷ → ۸۵

| تغییر | اثر | فایل |
|---|---|---|
| **gzip سرور** (`compression`) | انتقال کل بوت **۸۱۲KB → ۳۵۱KB** (bootstrap 228→30KB) | `server/app.js` |
| **modulepreload کل گراف main.js** (۵۴ ماژول — تولیدشده با اسکریپت، نه دستی) | حذف آبشار کشف ماژول؛ همه از @۵۰ms موازی | `client/index.html` + `scripts/gen-preloads.mjs` |
| CSSهای دیرهنگام غیرمسدودکننده (parents/games/**bootstrap**) — اپ کودک از کلاس‌های bootstrap استفاده نمی‌کند (بررسی‌شده) | ~۲.۶s از مسیر اولین رنگ حذف شد | `client/index.html` |
| `defer` روی gsap | ۱.۳۵s مسدودسازی پارسر حذف (ترتیب اجرا حفظ می‌شود) | `client/index.html` |
| اسپلش درون‌خطی 🦊 در `#app` | FCP 4.2s → **2.0s** — کودک روی تبلت کند سفیدی نمی‌بیند | `client/index.html` |
| preload داده‌های بوت (`packs/fa.json` + `ui-fa.json`) | fetchهای زنجیره‌ایِ بعد از main.js موازی شدند | `client/index.html` |

### رفع‌های دسترس‌پذیری (۸۶ → ۱۰۰)

- `--c-ink-soft` از `#8A7A85` (کنتراست 4.04) به `#6E5F69` (کنتراست **5.99**) —
  کل رابط، نه فقط کارت زبان
- حذف `user-scalable=no, maximum-scale=1` از viewport — زوم آزاد است (حق کاربر)

### GitHub Actions (`.github/workflows/ci.yml`)

- **job `test`**: `npm ci` → ۱۱ سوئیت واحد/یکپارچه → نصب Chromium → سرور ایزوله
  روی 8080 (env کامل + rateهای بالا) → سه سوئیت E2E (اپ ۲۲ · پک‌ها ۶ · ادمین ۱۱)
- **job `lighthouse`**: `npm ci` → Chromium → `npm run test:lighthouse` → artifact

## ۳. تست‌های این فاز

| سوئیت | نتیجه |
|---|---|
| `tests/import.test.js` (جدید — ۱۰ تست) | ✅ 10/10 — 401/400 · حذف→بازیابی با همان id · idempotent · merge بدون تنزل · والد دیگر (کودک جدید + صدای بازسازی‌شده با تطبیق بایت + حساب اول دست‌نخورده) · بدنه >120kb · رگرسیون result رشته‌ای · موارد نامعتبر |
| E2E اپ (استپ جدید درون‌ریزی) | ✅ 22/22 — بکاپ → درون‌ریزی → «بدون کودک/رویداد تکراری» با تأیید مستقیم از export سرور |
| Lighthouse | ✅ بودجه‌ها سبز (میانه‌ی ۳ اجرا) |

## ۴. نکته‌های نگهداشت

- بعد از هر تغییر `import` در ماژول‌ها، لیست modulepreload را با
  `node scripts/gen-preloads.mjs` بازتولید و در `index.html` جایگزین کن.
- بودجه‌ی ۸۵ «حداقلِ قابل‌قبول» است؛ اگر تغییری نمره را پایین آورد، یا درستش کن
  یا آگاهانه بودجه را بازبینی کن — گزارش‌های artifact دلیلشان را می‌گویند.
