StatistaAmeri/README.md

223 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div dir="rtl">
# سامانهٔ جامع آمار و اطلاعات «دیدوان»
داشبورد زندهٔ بازار فلزات، فولاد، ارز، طلا/سکه، کامودیتی و کریپتو — به‌همراه آرشیو گزارش‌های منابع معتبر جهانی. این سند برای **توسعه‌دهندهٔ بعدی** نوشته شده؛ کل معماری، نحوهٔ کار، راه‌اندازی، امنیت و دام‌ها (gotchas) را پوشش می‌دهد. کامل بخوان.
---
## ۱) این سیستم چیست؟
یک اپلیکیشن سه‌بخشی:
| بخش | تکنولوژی | پورت | نقش |
|---|---|---|---|
| **فرانت‌اند** | TanStack Start + React 19 + Vite 7 + Tailwind v4 | `5174` | لندینگ، ورود/ثبت‌نام، داشبورد |
| **بک‌اند (API)** | FastAPI + SQLite + Playwright | `8000` | اسکرپرها، API دادهٔ بازار، احراز هویت |
| **پنل ادمین** | FastAPI (HTML درون‌خطی) | `8001` | بازبینی/تأیید کاربران، مدیریت توکن، سلامت سرویس‌ها، گزارش‌ها |
دیتای بازار با **اسکرپرهای پس‌زمینه** جمع می‌شود و در **SQLite** ذخیره می‌شود (فقط مقادیرِ تغییرکرده — dedup). فرانت‌اند هر چند ثانیه از API می‌خواند و «زنده» نشان می‌دهد.
---
## ۲) ساختار پوشه‌ها
```
asianmetal-dashboard/
├─ src/ # فرانت‌اند
│ ├─ routes/ # مسیرهای فایل‌محورِ TanStack
│ │ ├─ index.tsx # لندینگ (LaserFlow، پنل زنده، گالری‌ها، …)
│ │ ├─ login.tsx # ورود (کاراکترهای کارتونی + احراز واقعی)
│ │ ├─ register.tsx # ثبت‌نام (→ pending)
│ │ ├─ dashboard.tsx # داشبورد + همهٔ تب‌ها/کارت‌ها/چارت‌ها
│ │ └─ __root.tsx # لایهٔ ریشه (RTL، فونت Vazirmatn، لود styles)
│ ├─ components/ # LaserFlow, CircularGallery, CardSwap, TubelightNavbar, …
│ ├─ components/ui/ # shadcn/ui
│ ├─ lib/ # labels.ts، metals-data.ts، utils.ts
│ ├─ styles.css # توکن‌های تم (oklch)، .tile/.glass/.panel، انیمیشن‌ها
│ └─ routeTree.gen.ts # خودکار توسط TanStack (دستی ویرایش نکن)
├─ backend/ # API + اسکرپرها
│ ├─ main.py # اپ FastAPI، CORS/هدرها/ریت‌لیمیت، همهٔ اندپوینت‌ها، lifespan
│ ├─ db.py # اتصال SQLite + init_db (ساخت/مهاجرت جداول)
│ ├─ auth.py # هشِ رمز (PBKDF2) + نشست‌های سمت‌سرور
│ ├─ currency_stream.py # اسکرپِ ارز/طلا/سکهٔ داخلی (Playwright، هر ۶۰ث)
│ ├─ crypto_market.py # کریپتو از API رایگانِ CoinGecko (هر ۶۰ث)
│ ├─ world_economy.py # شاخص‌های کلانِ اقتصادهای بزرگ (هفتگی)
│ ├─ steel_stocks.py # سهام شرکت‌های فولادی (companiesmarketcap، هر ۵ دقیقه)
│ ├─ live_stream.py # کامودیتی لحظه‌ای (Yahoo Finance) روی WebSocket
│ ├─ reports.py # اسکنِ آرشیو PDFِ گزارش‌ها (فایل‌سیستم)
│ └─ sync.py # اسکریپت دستیِ سینکِ قیمت فلزات از API منبعِ بالادست
├─ admin/admin_app.py # پنل ادمین (مستقل)
├─ vite.config.ts # پورت ۵۱۷۴ + proxy (/api,/ws → :8000) + allowedHosts
├─ .gitignore # شاملِ .env، __pycache__، *.db
└─ README.md # همین فایل
```
---
## ۳) راه‌اندازی محلی
**پیش‌نیازها:** Node 20+ (یا Bun)، Python 3.11+، و برای اسکرپرها `playwright install chromium`.
```powershell
# نصب وابستگی‌های فرانت
npm install
# نصب وابستگی‌های پایتون (نمونه)
pip install fastapi uvicorn playwright requests yfinance pandas websockets
python -m playwright install chromium
```
### اجرای هر سه سرویس (هرکدام در ترمینال جدا)
```powershell
# ۱) بک‌اند (پورت 8000)
$env:PYTHONUTF8="1"; python -m uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend
# ۲) فرانت‌اند (پورت 5174)
npm run dev
# ۳) پنل ادمین (پورت 8001) — ADMIN_PASSWORD الزامی است
$env:ADMIN_USER="admin"; $env:ADMIN_PASSWORD="یک‌رمزِ‌قوی"; python -m uvicorn admin_app:app --host 127.0.0.1 --port 8001 --app-dir admin
```
سپس: فرانت روی `http://localhost:5174` ، پنل ادمین روی `http://localhost:8001`.
---
## ۴) متغیرهای محیطی
| متغیر | کجا | پیش‌فرض | توضیح |
|---|---|---|---|
| `DB_PATH` | backend, admin | مسیر محلی | محل فایل SQLite (هر دو سرویس باید یکی باشند) |
| `ASIANMETAL_TOKEN` | sync.py | — | توکنِ API بالادست (یا از پنل ادمین در DB) — **هیچ‌وقت در کد هاردکد نکن** |
| `ADMIN_USER` / `ADMIN_PASSWORD` | admin | `admin` / **الزامی** | احراز Basic پنل ادمین؛ بدون `ADMIN_PASSWORD` بالا نمی‌آید (fail-closed) |
| `ALLOWED_ORIGINS` | backend | `localhost:5174,127.0.0.1:5174` | originهای مجاز CORS (با کاما) |
| `COOKIE_SECURE` | backend | `0` | در پروداکشن (HTTPS) روی `1` بگذار تا کوکیِ نشست Secure شود |
| `REPORTS_DIR` | reports.py | پوشهٔ آرشیو | محل PDFهای گزارش (ساختار `<منبع> - <دسته>/<file>.pdf`) |
---
## ۵) دیتابیس (SQLite)
`init_db()` در `db.py` جداول را می‌سازد و **مهاجرتِ امن** انجام می‌دهد (ستون‌ها را اگر نبودند `ALTER` می‌کند):
- **`prices`** — تاریخچهٔ روزانهٔ قیمت فلزات: `grp, category, title, date, low, mid, high` (یکتا روی `title,date`). منبعِ تب «فلزات و فولاد».
- **`users`** — کاربران: `id, name, email, phone, organization, password_hash, status('pending'|'approved'|'rejected'), created_at`.
- **`sessions`** — نشست‌های سمت‌سرور: `token, user_id, created_at, expires_at`.
- **`app_config`** — جفت‌های `key/value` (مثلاً `asianmetal_token`، هارت‌بیت اسکرپرها).
- جداول اسکرپرها: `currency_prices, gold_prices, coin_prices`, `te_prices`, `world_economy`, `steel_stocks` — هرکدام آخرین/تاریخچهٔ مقادیر را نگه می‌دارند (dedup روی تغییر).
> نکته: قیمت‌های لحظه‌ای (ارز/کریپتو/کامودیتی) عمدتاً در **حافظه** کش می‌شوند (`_latest`) و فقط تغییرها در DB ذخیره می‌شوند تا حجم کم بماند.
---
## ۶) اسکرپرها (در `lifespan` بک‌اند خودکار استارت می‌شوند)
| ماژول | منبع | بازه | جدول/خروجی |
|---|---|---|---|
| `currency_stream` | نرخ داخلیِ ارز/طلا/سکه (URL در همان فایل) | ۶۰ ثانیه | `currency_prices`/`gold_prices`/`coin_prices` |
| `crypto_market` | **CoinGecko** (API رایگان، بدون کلید) | ۶۰ ثانیه | حافظه (`get_latest`) |
| `world_economy` | شاخص‌های کلانِ اقتصادهای بزرگ | هفتگی | `world_economy` |
| `steel_stocks` | companiesmarketcap.com | ۵ دقیقه | `steel_stocks` |
| `live_stream` | **Yahoo Finance** | لحظه‌ای | WebSocket `/ws/live` |
| `reports` | فایل‌سیستمِ `REPORTS_DIR` | کشِ ۱۰ دقیقه | اسکنِ PDF |
**دامِ مهم:** سایت‌های منبع قیمت‌ها را با JS لِیزی‌لود می‌کنند. اسکرپرهای Playwright بعد از `goto` یک `wait_for_timeout(50009000)` دارند؛ **حذفش نکن** وگرنه مقادیرِ کهنه/خالی خوانده می‌شود. هر اسکرپر هم try/except دارد تا یک خطای گذرا کلِ تسک پس‌زمینه را نکُشد.
`sync.py` یک اسکریپتِ **دستی** است (در lifespan نیست) که قیمت فلزات را از API بالادست می‌گیرد؛ توکن را از `app_config` یا env `ASIANMETAL_TOKEN` می‌خواند.
---
## ۷) اندپوینت‌های API (بک‌اند `:8000`)
**عمومی (دادهٔ بازار):** `GET /api/tree`, `/api/history?title=&days=`, `/api/search?q=`, `/api/latest`, `/api/live`, `/api/currency`, `/api/gold`, `/api/coin`, `/api/world-economy`, `/api/steel-stocks`, `/api/crypto`, `/api/reports`, `/api/reports/files?folder=`, `/api/reports/file?folder=&name=` (استریمِ inline PDF با گاردِ traversal)، WebSocket `/ws/live`.
**احراز هویت:** `POST /api/register`، `POST /api/login`، `GET /api/me`، `POST /api/logout`.
**پنل ادمین `:8001`** (پشت HTTP Basic): `/api/users`، `POST /api/users/{id}/approve|reject`، `GET|POST /api/token`، `/api/health`، `/api/test-scrape`، `/api/reports`، `/api/export?dataset=`.
---
## ۸) احراز هویت و گردشِ تأیید (مهم)
> اصل: **هیچ اعتمادی به کلاینت نیست.** ورود کاملاً سمت‌سرور تأیید می‌شود؛ نشست در **کوکیِ httpOnly** است، نه `localStorage`.
```
ثبت‌نام (نام، نام‌خانوادگی، سازمان، ایمیل، موبایل، رمز)
│ POST /api/register → رمز با PBKDF2 هش می‌شود، رکورد با status='pending'
کاربر در وضعیت «در انتظار تأیید» ──► ادمین در پنل (تبِ کاربران) تأیید/رد می‌کند
│ (POST /api/users/{id}/approve | reject)
ورود POST /api/login → اگر approved بود: کوکیِ نشست (httpOnly, SameSite=Lax) ست می‌شود
│ pending → 403 «در انتظار تأیید» ، rejected → 403 ، رمز غلط → 401
داشبورد → گاردِ سمت‌سرور: fetch('/api/me')؛ اگر 401 شد → ری‌دایرکت به /login
خروج POST /api/logout → نشست از DB حذف + کوکی پاک می‌شود
```
نکات پیاده‌سازی:
- هشِ رمز: `auth.hash_password` با `hashlib.pbkdf2_hmac('sha256', ..., 200000)` — بدونِ وابستگیِ خارجی. مقایسه با `hmac.compare_digest`.
- توکنِ نشست: `secrets.token_urlsafe(32)`، انقضای ۷ روزه، در جدول `sessions`.
- ریت‌لیمیتِ per-IP (در حافظه) روی `register`/`login` (۵ در دقیقه).
- اعتبارسنجیِ Pydantic: فرمت/طولِ ایمیل و موبایل، حداقل ۸ کاراکترِ رمز.
- رد کردنِ کاربر، نشست‌های فعالش را هم پاک می‌کند (دسترسی فوراً قطع).
---
## ۹) پنل ادمین (`:8001`)
- ورود با HTTP Basic؛ `ADMIN_PASSWORD` **الزامی** است (بدونش بالا نمی‌آید).
- تب‌ها: **کاربران** (لیست + تأیید/رد + سازمان/وضعیت)، **توکن منبع داده** (ذخیره/تستِ توکن بالادست)، **سلامت سرویس‌ها** (هارت‌بیتِ هر اسکرپر + تست اسکرپ)، **گزارشات** (دیتاست‌ها + خروجی CSV).
- خروجیِ کاربران در UI با `esc()` فرار داده می‌شود (جلوگیری از XSSِ ذخیره‌شده).
---
## ۱۰) فرانت‌اند
- مسیرها: `/` (لندینگ)، `/login`، `/register`، `/dashboard`.
- داشبورد، تب‌ها (`VIEW_NAV`): **بازار داخلی**، **کامودیتی**، **فلزات و فولاد** (کاتالوگِ کارت + چارت)، **اقتصاد جهانی** (جدول حرارتی)، **سهام فولادی**، **کریپتو**، **گزارش‌ها**.
- زبان بصری: تم **لایتِ کرم + لهجهٔ کهربایی (فولاد مذاب)**؛ توکن‌ها در `styles.css` (oklch). قیمت‌کارت‌ها سبکِ **Spark-canvas** (نمودار پس‌زمینه + رقم‌های غلتانِ `Odometer` + پیلِ درصد).
- API نسبی است (`const API = ""`) تا از طریق **پراکسیِ Vite** هم‌مبدأ کار کند → برای share فقط یک تونل لازم است.
- RTL سراسری، فونت **Vazirmatn**. اعداد همه با همین فونت (نه مونو).
---
## ۱۱) اشتراک‌گذاری (ngrok)
چون `vite.config.ts` مسیرهای `/api` و `/ws` را به `:8000` پراکسی می‌کند و فرانت از URLهای نسبی استفاده می‌کند، **فقط یک تونل روی ۵۱۷۴ کافی است**:
```powershell
ngrok http 5174
# یا مستقیم باینری: & "$env:APPDATA\npm\node_modules\ngrok\bin\ngrok.exe" http 5174
```
نکته: شیمِ `ngrok.ps1`ی npm گاهی پورت را اشتباه پاس می‌دهد؛ اگر تونل به پورت غلط رفت، **مستقیم `ngrok.exe`** را صدا بزن. URL را از `http://127.0.0.1:4040/api/tunnels` هم می‌توانی بخوانی. (پلنِ رایگان: هر بار URL عوض می‌شود و یک صفحهٔ هشدارِ اولیه دارد.)
---
## ۱۲) امنیت — وضعیت و کارهای واجب
سخت‌سازی‌های انجام‌شده: CORS محدود، هدرهای امنیتی (`nosniff`, `X-Frame-Options`, `Referrer-Policy`)، اعتبارسنجی + ریت‌لیمیتِ ورودی‌ها، محدودیتِ کوئریِ `history`/`search`، گاردِ path-traversal روی استریمِ PDF، کوئری‌های پارامتری، احراز هویتِ سمت‌سرور با کوکیِ httpOnly، و حذفِ توکنِ هاردکد از کد.
**واجب قبل از پروداکشن:**
1. توکنِ بالادست (SteelStatista) که قبلاً در git history لو رفته را **باطل/تمدید** کن؛ مقدارِ جدید فقط در پنل ادمین/`env`.
2. `ADMIN_PASSWORD` قوی بگذار (پیش‌فرضِ ضعیف حذف شده).
3. در HTTPS واقعی: `COOKIE_SECURE=1` و `ALLOWED_ORIGINS` را به دامنهٔ واقعی محدود کن.
4. پنل ادمین (`:8001`) را **عمومی نکن** (در تونل نگذار؛ فقط لوکال/VPN/SSH).
---
## ۱۳) دام‌ها و بدهیِ فنی (برای دفعهٔ بعد)
- `DB_PATH`/مسیرها در `db.py` و `admin_app.py` پیش‌فرضِ ویندوزیِ مطلق دارند (شاملِ نام کاربری) — بهتره به مسیر نسبی/`env` منتقل شود.
- وابستگی‌های `package.json` با `^` پین نشده‌اند؛ برای بیلدِ تکرارپذیر `npm ci` + پین.
- فایل‌های `__pycache__/*.pyc` قبلاً کامیت شده‌اند؛ untrack کن: `git rm -r --cached backend/__pycache__`.
- `routeTree.gen.ts` خودکار است — دستی ویرایش نکن (با اضافه‌شدن فایل route، خودِ Vite بازتولید می‌کند).
- اسکرپرها به ساختار HTMLِ منابع وابسته‌اند؛ اگر منبع قالبش را عوض کند، سلکتورها/`wait_for_timeout` باید تنظیم شوند.
</div>