# سند ۳۲ — موج ۲ §۲.۵: موارد تکمیلی (ایمیل · بکاپ ریموت · نشانگر آنلاین · i18n ادمین · ابزار پایلوت)

**نسخه:** 1.11.0 · **تاریخ:** ۲۰۲۶-۰۹-۲۶ · **مرجع:** docs/29 §۲.۳ و §۲.۵ · **تست‌ها:** `tests/mailer.test.js` (۱۱) · `tests/pilot.test.js` (۳) · ۱ تست api

## ۱. ایمیل والدین (SMTP — تصمیم ممیزی ۲۷ §۶/۲)

`server/mailer.js` — بدون وابستگی جدید، سه transport:
| transport | پیکربندی |
|---|---|
| **Resend** | `ALPHABET_MAIL_RESEND_KEY` (+ `ALPHABET_MAIL_FROM`) |
| **Mailgun** | `ALPHABET_MAIL_MAILGUN_KEY` + `ALPHABET_MAIL_MAILGUN_DOMAIN` |
| **file** (توسعه/تست) | `ALPHABET_MAIL_TRANSPORT=file` + `ALPHABET_MAIL_OUTBOX=/مسیر.jsonl` — هر ایمیل یک خط JSON |
| log (پیش‌فرض) | هیچ — فقط لاگ JSON (نصب self-host بدون SMTP سالم می‌ماند) |

- **ایمیل خوش‌آمد** بعد از ثبت‌نام (best-effort — پاسخ را نگه نمی‌دارد).
- **گزارش هفتگی** (`server/weeklyReport.js`): `ALPHABET_MAIL_WEEKLY=1` → روز `ALPHABET_MAIL_WEEKLY_DAY` (پیش‌فرض شنبه) بعد از ساعت بکاپ؛ فقط والدینِ دارای فعالیت هفته‌ی اخیر (ضد spam). سنجه‌ها از `session_events`: جلسه (game_complete) · ستاره (json_extract result.stars) · قصه · روز فعال · حرف mastery≥3.
- همه‌ی ارسال‌ها هرگز جریان اصلی را نمی‌شکنند؛ شکست → لاگ error.

## ۲. فراموشی رمز خودخدمت (ریزش کاربر ممیزی ۲۷)

`server/selfReset.js` + دو مسیر در `routes/auth.js` + UI دو مرحله‌ای در `parentsScreen.js` (۵ زبان، ۱۲ کلید i18n):
- `POST /auth/parent/forgot-password` → کد ۶رقمی؛ **پاسخ همیشه 202** (ضد enumeration) · 503 `MAIL_NOT_CONFIGURED` اگر SMTP نباشد (پیام راهنما به کانال AP-).
- `POST /auth/parent/reset-password` → تغییر رمز + **بستن همه‌ی refreshها و دستگاه‌ها** + حسابرسی `parent_self_reset`.
- امنیت: فقط **SHA-256 کد** در DB (جدول `password_reset_codes`) · TTL ۱۵دقیقه (`ALPHABET_RESET_TTL_MIN`) · یک‌بارمصرف · محدودسازهای جدا: `FORGOT_RATE_MAX=5` و `RESET_RATE_MAX=10` در ۱۵ دقیقه.
- کانال ادمین (رمز AP-) دست‌نخورده باقی می‌ماند — مسیر پشتیبان وقتی SMTP نیست.

## ۳. بکاپ ریموت — قاعده‌ی ۳-۲-۱ (سند ۲۸ §۴)

`server/remoteBackup.js`: بعد از بکاپ شبانه‌ی محلی، اگر `ALPHABET_RESTIC_REPOSITORY` ست باشد:
```
restic backup <backupsDir> --tag alphabetplay-nightly
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 6 --prune
```
- احراز: `ALPHABET_RESTIC_PASSWORD_FILE` یا `ALPHABET_RESTIC_PASSWORD` (+ کلیدهای S3/R2 استاندارد).
- نبود باینری restic = خطای لاگ‌شده + رویداد پایش، نه کرش. تست با runner فیک (آرگومان‌ها + ترتیب backup→forget).
- Docker: باینری restic باید در ایمیج اضافه شود (`apt-get install restic` یا ایمیج sidecar — تصمیم استقرار).

## ۴. نشانگر آنلاین/آفلاین کودک

`GET /children` اکنون `lastSeen` (MAX آخرین سینک دستگاه‌ها) برمی‌گرداند؛ UI والدین نقطه‌ی سبز (آنلاین = فعالیت < ۵ دقیقه) + «آخرین فعالیت: X دقیقه پیش» (۷ کلید i18n × ۵ زبان).

## ۵. i18n پنل ادمین (fa/en)

`client/js/admin/i18n.js` (~۹۰ کلید × ۲ زبان) + `data-i18n`/`data-i18n-ph` در admin.html + رشته‌های پویای اصلی (جدول‌ها، نقش‌ها، آمار داشبورد، مودال‌های تأیید).
- دکمه‌ی تغییر زبان در سایدبار · انتخاب در `localStorage ap.adminLang` · پیش‌فرض fa · en → `dir=ltr` (با اُوِرراید CSS).
- **پوشش صادقانه:** کروم پنل + جدول‌های اصلی + برچسب‌های آماری ترجمه شده؛ فرم‌های عمیق (ویرایشگر داستان، فرم‌های CRUD ادمین/واژه) و پیام‌های سرور (فارسی از API) ترجمه نشده‌اند — اپراتور فعلی فارسی‌زبان است؛ برای همکار بین‌المللی کروم کافی است. بدهی ثبت شد (سند ۳۳ §بدهی).
- تست e2e-admin (۱۶) با پیش‌فرض fa دست‌نخورده سبز ماند.

## ۶. ابزار سنجه‌ی پایلوت (سند ۲۹ §۲.۳ — بخش بدون‌کاربر)

- **`scripts/pilot-report.mjs`** (`npm run pilot:report`): از DB — per کودک (جلسه/روز/دقیقه/ستاره/حرف L3+/قصه) · retention هفته‌ی ۲ (کوهورت هفته‌ی ۱ که در هفته‌ی ۲ برگشته) · crash-free (درصد روزهای فعال بدون خطای کلاینت). سنجه‌های انسانی (آزمون پیش/پس سند ۲۳، NPS) جای خالی جدول دارند.
- **جدول `client_errors`**: diagnostics کلاینت اکنون در DB هم می‌نشیند (۹۰ روز retention — `retention.js`)؛ سوخت crash-free.
- exit code ۲ اگر سنجه‌ها زیر هدف (retention<۶۰ یا crash-free<۹۹) — قابل اتصال به CI پایلوت.

## ۷. درس‌های این قطعه (صادقانه)

- **محدودساز نرخ ورود ادمین (۱۰/۱۵دقیقه، حافظه‌ای) اجراهای پشت‌سرهمِ e2e-admin را می‌کشد** — راه‌حل فعلی: ری‌استارت سرور بین اجراها؛ بدهی: `ADMIN_AUTH_MAX` در env تست بالاتر.
- **flake واقعی در e2e-admin پیدا و رفع شد:** هر اجرا یک والد بی‌فرزند (rst_) می‌ساخت که در اجرای بعدی ردیف اول می‌شد → `.kid-card` هر نمی‌آمد. تست اکنون والد+کودک اختصاصی می‌سازد و همان را باز می‌کند.
- تغییر نام متغیر جهانی (ROLE_FA) با grep همه‌ی استفاده‌ها انجام نشد → خطای بی‌صدا در try/catch (جدول ⏳ ماند). درس: استفاده‌ها را grep کن.
