diff --git a/.gitignore b/.gitignore index 7841221..4c2f2b0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,5 +42,5 @@ brand_pages/ *.old *.docx -# State file written by scripts/fetch-competitor-news.mjs -scripts/news-history.json +# State file written by frontend/scripts/fetch-competitor-news.mjs +frontend/scripts/news-history.json diff --git a/CLAUDE.md b/CLAUDE.md index 4a497d0..8c0bd92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,15 +6,16 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Two independent apps in one repo: -1. **Frontend** (repo root) — a Vite + React 19 + TypeScript marketing/content site for "اندیشکده فولاد آینده" (a steel-industry think tank). Persian-first, RTL, bilingual (fa/en). Deploys as a static build via Docker to Hugging Face Spaces on port 7860. -2. **Admin panel** (`panel/`) — a **standalone** Node/Express + SQLite app with its own `package.json` and `node_modules`. Manages content the site is meant to publish. **The frontend does NOT consume the panel yet** — every section renders hardcoded mock data. Wiring them is a deliberate future step. +1. **Frontend** (`frontend/`) — a Vite + React 19 + TypeScript marketing/content site for "اندیشکده فولاد آینده" (a steel-industry think tank). Persian-first, RTL, bilingual (fa/en). Deploys as a static build via Docker to Hugging Face Spaces on port 7860. +2. **Admin panel** (`panel/`) — a **standalone** Node/Express + Postgres app with its own `package.json` and `node_modules`, that is BOTH the backend API and the CMS admin UI (served from `panel/public/`). Manages content the site publishes. -These two have **no shared build or dependency tree**. Treat `panel/` as a separate project. +These two have **no shared build or dependency tree**. `frontend/` and `panel/` are sibling folders at the repo root — neither references the other at build time (the frontend reads the panel only at runtime via `VITE_PANEL_API`). ## Commands -Frontend (run from repo root): +Frontend (run from `frontend/`): ```bash +cd frontend npm run dev # Vite dev server (HMR) npm run build # tsc -b && vite build (type-checks then bundles) npm run lint # eslint . @@ -30,12 +31,8 @@ npm install # first time only npm run seed # create the admin user from .env (ADMIN_USERNAME/PASSWORD) npm start # node server.js → http://localhost:3001 npm run dev # node --watch server.js -npm rebuild better-sqlite3 # REQUIRED if Node's ABI changed — native addon, fails to load otherwise ``` - -## Critical build gotcha - -`panel/` lives inside the frontend's project root, so **Vite's dependency scanner reads `panel/package.json`**. If that file (or anything Vite scans) has unresolved git merge-conflict markers (`<<<<<<<`), `npm run dev`/`build` fails with a cryptic `JSONError ... expected value at line 1`. Keep `panel/package.json` valid JSON. +The panel uses Postgres (`pg` Pool over `DATABASE_URL`) behind a better-sqlite3-style `db.prepare().get/all/run()` facade — every call is async and must be awaited. ## Frontend architecture diff --git a/README_FA.md b/README_FA.md index 914d8d7..63f590f 100644 --- a/README_FA.md +++ b/README_FA.md @@ -1,249 +1,210 @@ # راهنمای جامع پروژه اندیشکده فولاد آینده (Andishkade Foolad) -این سند راهنمای فنی و معماری پروژه **اندیشکده فولاد آینده** است که شامل جزئیات ساختار فرانت‌اند، پنل مدیریت بک‌اند، پایگاه داده، پشته فناوری‌ها و نحوه راه‌اندازی و توسعه پروژه می‌باشد. +این سند، راهنمای فنی و معماریِ پروژه **اندیشکده فولاد آینده** است — یک پلتفرم تحلیلیِ صنعت فولاد، شاملِ یک وب‌سایتِ محتوایی و یک پنلِ مدیریت/بک‌اند. در این سند ساختار پوشه‌ها، پشته‌ی فناوری، پایگاه داده، احراز هویت، قابلیت‌ها و نحوه‌ی استقرار توضیح داده شده است. --- ## فهرست مطالب - [۱. ساختار کلی پروژه](#۱-ساختار-کلی-پروژه) -- [۲. فرانت‌اند (Frontend)](#۲-فرانت‌اند-frontend) - - [پشته فناوری فرانت‌اند](#پشته-فناوری-فرانت‌اند) - - [ویژگی‌ها و قابلیت‌های بصری فرانت‌اند](#ویژگی‌ها-و-قابلیت‌های-بصری-فرانت‌اند) - - [دستورات بخش فرانت‌اند](#دستورات-بخش-فرانت‌اند) -- [۳. پنل مدیریت (Backend / Admin Panel)](#۳-پنل-مدیریت-backend--admin-panel) - - [پشته فناوری بک‌اند](#پشته-فناوری-بک‌اند) - - [ساختار و معماری پایگاه داده](#ساختار-و-معماری-پایگاه-داده) - - [سرویس‌های ویژه پنل مدیریت](#سرویس‌های-ویژه-پنل-مدیریت) - - [دستورات بخش پنل مدیریت](#دستورات-بخش-پنل-مدیریت) - - [لیست APIهای پنل مدیریت](#لیست-apiهای-پنل-مدیریت) -- [۴. نحوه اتصال فرانت‌اند به پنل مدیریت (Wiring)](#۴-نحوه-اتصال-فرانت‌اند-به-پنل-مدیریت-wiring) -- [۵. استقرار و دگرگونی‌ها (Deployment)](#۵-استقرار-و-دگرگونی‌ها-deployment) +- [۲. فرانت‌اند (`frontend/`)](#۲-فرانت‌اند-frontend) +- [۳. پنل مدیریت و بک‌اند (`panel/`)](#۳-پنل-مدیریت-و-بک‌اند-panel) +- [۴. پایگاه داده](#۴-پایگاه-داده) +- [۵. احراز هویت و اعضا](#۵-احراز-هویت-و-اعضا) +- [۶. اتصال فرانت‌اند به پنل (Wiring)](#۶-اتصال-فرانت‌اند-به-پنل-wiring) +- [۷. فهرست APIها](#۷-فهرست-apiها) +- [۸. استقرار (Deployment)](#۸-استقرار-deployment) --- ## ۱. ساختار کلی پروژه -پروژه به صورت یک ساختار تک‌مخزنی (Monorepo) طراحی شده است که شامل دو اپلیکیشن مستقل و بدون وابستگی اشتراکی در زمان ساخت (Build) است: - -1. **فرانت‌اند (Frontend - ریشه پروژه)**: یک وب‌سایت بازاریابی و محتوایی تعاملی، مدرن و با اولویت زبان فارسی (Persian-first) و دو زبانه (فارسی / انگلیسی) که با React 19 و Vite توسعه یافته است. -2. **پنل مدیریت (Admin Panel - پوشه `panel/`)**: یک وب‌اپلیکیشن مستقل SPA با معماری Node.js/Express و پایگاه داده PostgreSQL که محتوای پویا وب‌سایت را مدیریت می‌کند. +پروژه یک **مونوریپو** با دو اپلیکیشنِ مستقل و کنارهم (sibling) است که **هیچ build یا وابستگیِ مشترکی** ندارند؛ فرانت‌اند فقط در زمانِ اجرا (runtime) و از طریق متغیر `VITE_PANEL_API` با پنل حرف می‌زند. ``` andishkade-foolad/ -├── panel/ # پروژه بک‌اند و پنل مدیریت -│ ├── public/ # فایل‌های استاتیک و رابط کاربری پنل (Vanilla JS) -│ ├── uploads/ # فایل‌های آپلود شده محلی (در صورت عدم استفاده از S3) -│ ├── server.js # سرور اصلی Express -│ ├── db.js # پیکربندی پایگاه داده PostgreSQL و کوئری‌ها -│ └── package.json # وابستگی‌های بک‌اند -├── src/ # کدهای منبع فرانت‌اند (React 19) -│ ├── app/ # مسیریابی و قالب اصلی (Layout) -│ ├── components/ # کامپوننت‌های اشتراکی و UI -│ ├── context/ # کانتکست‌های ری‌اکت (مانند زبان سیستم) -│ ├── data/ # داده‌های اولیه و ماک (Mock Data) -│ └── pages/ # صفحات مختلف وب‌سایت -├── package.json # وابستگی‌های فرانت‌اند -├── Dockerfile # داکرفایل اجرای فرانت‌اند به عنوان سرور استاتیک Nginx -└── docker-compose.yml # تنظیمات داکر کامپوز برای اجرای لوکال فرانت‌اند +├── frontend/ # وب‌سایت عمومی — Vite + React 19 + TypeScript +│ ├── src/ +│ │ ├── app/ # مسیریابی (router) و قالب اصلی (RootLayout) +│ │ ├── components/ # کامپوننت‌های اشتراکی و UI +│ │ ├── context/ # کانتکست‌ها: زبان (LangContext) و احراز هویت (AuthContext) +│ │ ├── content/ data/ # داده‌های اولیه/ماک + ماژول‌های bootstrap که از پنل واکشی می‌کنند +│ │ └── pages/ # صفحات سایت +│ ├── public/ # دارایی‌های استاتیک، فونت‌ها، manifest و service worker (PWA) +│ ├── index.html +│ ├── vite.config.ts tsconfig*.json eslint.config.js +│ ├── Dockerfile nginx.conf liara.json # استقرار به‌صورت سرور استاتیک Nginx روی پورت ۷۸۶۰ +│ └── package.json +├── panel/ # بک‌اند (Express API) + پنلِ مدیریت (CMS) — یک اپ واحد +│ ├── server.js # سرور و همه‌ی Routeهای API +│ ├── db.js # اتصال PostgreSQL + facade شبیه better-sqlite3 + مپ ردیف‌ها +│ ├── public/ # رابط کاربری پنل (SPA با Vanilla JS، بدون build) +│ ├── uploads/ # آپلودِ محلی (در صورت نبودِ S3) +│ ├── liara.json +│ └── package.json +├── docker-compose.yml # اجرای لوکالِ فرانت (context: ./frontend) +├── CLAUDE.md README.md README_FA.md SECURITY.md design-system.md +``` + +> **نکته:** پنل **هم بک‌اندِ API است و هم CMS** (رابطِ مدیریت را از `panel/public/` سرو می‌کند). این دو به‌هم گره خورده‌اند و یک اپلیکیشنِ واحدند. + +--- + +## ۲. فرانت‌اند (`frontend/`) + +پورتالِ محتواییِ فارسی‌محور (Persian-first)، دوزبانه (fa/en) و RTL با انیمیشن‌های غنی. + +### پشته‌ی فناوری +- **هسته:** React 19 + TypeScript. +- **بیلد:** Vite (HMR در توسعه، خروجی بهینه در پروداکشن). اعتبارسنجیِ سریع: `npx tsc -b --noEmit`. +- **استایل:** عمدتاً `style` اینلاین + کلاس‌های Tailwind CSS v4 برای واکنش‌گرایی (`max-md:` …) + متغیرهای CSS در `src/index.css`. رنگِ سازمانی: طلایی `#CD9E53` و سورمه‌ای `#032340` (در بسیاری فایل‌ها به‌صورت `const GOLD`/`const NAVY` هاردکد شده‌اند). +- **مسیریابی:** React Router DOM v7 (`createBrowserRouter`). `RootLayout` همه‌ی صفحات را با Header/Footer و موتورِ اسکرول می‌پیچد. +- **انیمیشن و اسکرول:** + - **GSAP + ScrollTrigger:** انیمیشن‌های مبتنی بر اسکرول. + - **Lenis:** اسکرولِ نرم هماهنگ با تیکرِ GSAP — **فقط دسکتاپ** (روی لمسی به مومنتومِ بومیِ سیستم‌عامل واگذار می‌شود تا لگ نشود). + - **Framer Motion:** میکروانیمیشن‌ها، پاپ‌آپ‌ها و ترنزیشن‌ها. +- **سه‌بعدی و نقشه:** Three.js + React Three Fiber، `three-globe` و `react-globe.gl` برای کره‌ی زمینِ تعاملی. +- **نمودار:** ApexCharts (صفحه‌ی Market). +- **مدیریت وضعیت:** صرفاً **React Context** (زبان و احراز هویت). بدون کتابخانه‌ی state خارجی. + +### قابلیت‌های کلیدی +1. **دوزبانه/RTL:** هوک `useLang()` (`src/context/LangContext`) مقدار `{ lang, toggle }` می‌دهد؛ کامپوننت‌ها دیکشنریِ درون‌خطیِ `{ fa, en }` دارند و جهتِ صفحه با `dir` تنظیم می‌شود. +2. **اعدادِ فارسی:** یک «بومی‌سازِ ارقام» در `RootLayout` همه‌ی ارقامِ لاتینِ نمایش‌داده‌شده را در حالتِ فارسی به فارسی تبدیل می‌کند (با MutationObserver برای محتوای دیرلودشده). +3. **تاریخ شمسی:** بدون کتابخانه‌ی تاریخ — مستقیماً با `Intl.DateTimeFormat('fa-IR-u-ca-persian', …)`. +4. **PWA و موبایل:** manifest + service worker (نصب‌پذیری + کشِ آفلاین)؛ صفحه روی موبایل **غیرقابلِ زوم** است (viewport + بلوکِ pinch/double-tap برای iOS). +5. **جستجوی سراسری:** آیکونِ سرچِ هدر → صفحه‌ی `/search` که به‌صورت کلاینت‌ساید روی همه‌ی محتوا (گزارش‌ها، مطالب، «در یک نگاه»، رادار آینده، ویدیوکست/پادکست، رویدادها) می‌گردد و به صفحه‌ی جزئیاتِ هر مورد لینک می‌دهد. + +### دستورات (در پوشه‌ی `frontend/`) +```bash +cd frontend +npm install +npm run dev # سرورِ توسعه‌ی Vite (HMR) +npx tsc -b --noEmit # فقط type-check (سریع‌ترین حلقه‌ی اعتبارسنجی) +npm run lint # ESLint +npm run build # tsc -b && vite build → خروجی در dist/ +npm run preview # پیش‌نمایشِ بیلدِ پروداکشن +``` +برای دیدنِ دادهٔ واقعیِ پنل در حالتِ توسعه: +```bash +VITE_PANEL_API=https://cms-steelforesight.liara.run npm run dev +``` +> هیچ فریم‌ورکِ تستی پیکربندی نشده؛ «تأیید» یعنی `tsc` تمیز + بازدیدِ سرورِ در حالِ اجرا. + +--- + +## ۳. پنل مدیریت و بک‌اند (`panel/`) + +سرورِ Express که هم API می‌دهد و هم رابطِ مدیریت (CMS) را سرو می‌کند. + +### پشته‌ی فناوری +- **سرور:** Express 4 روی Node.js. +- **پایگاه داده:** PostgreSQL (لیارا) با ماژول `pg` (Pool روی `DATABASE_URL`). در `db.js` یک facadeِ شبیهِ `better-sqlite3` ساخته شده: `db.prepare(sql).get/all/run(...)` — **همه async و باید await شوند**. جداول و ایندکس‌ها در زمانِ استارت‌آپ به‌صورت idempotent ساخته/مهاجرت می‌شوند. +- **امنیت/احراز هویت:** `bcryptjs` (هشِ گذرواژه)، `jsonwebtoken` (کوکیِ HttpOnly با `Secure`/`SameSite` پشتِ TLS)، `helmet` (هدر/CSP)، `express-rate-limit` (محدودسازیِ ورود و فرمِ تماس). CORS با `ALLOWED_ORIGINS` کنترل می‌شود (و در توسعه هر `localhost`). +- **آپلود و تصویر:** `multer` (بافرِ حافظه) + `sharp` (چرخشِ EXIF، ریسایز تا ۲۰۰۰px و خروجیِ **WebP**). `pdf-parse` برای استخراجِ متنِ PDF. +- **ذخیره‌سازی:** **S3-compatible** (object storage لیارا) در صورتِ تنظیمِ متغیرها؛ وگرنه fallback به دیسکِ محلی `panel/uploads/`. +- **هوش مصنوعی:** SDKهای **OpenAI** و **Anthropic (Claude)** برای خلاصه‌سازی و استخراجِ دادهٔ کلیدی از اسناد. +- **رابطِ مدیریت:** SPA با **Vanilla JS** در `panel/public/` (بدون build). تب‌ها در دو گروه: **محتوا** (در یک نگاه، رادار آینده، ویدیوکست/پادکست، رویدادها، بنرها) و **کاربران** (کاربران سایت، خبرنامه). + +### دستورات (در پوشه‌ی `panel/`) +```bash +cd panel +npm install +npm run seed # ساختِ ادمینِ اولیه از روی .env (ADMIN_USERNAME/PASSWORD) +npm start # node server.js → http://localhost:3001 +npm run dev # node --watch server.js ``` --- -## ۲. فرانت‌اند (Frontend) +## ۴. پایگاه داده -بخش کاربری وب‌سایت اندیشکده فولاد آینده یک پورتال محتوایی بسیار پیشرفته و پویا با انیمیشن‌های غنی است. +جداولِ اصلی (PostgreSQL): -### پشته فناوری فرانت‌اند +| جدول | کاربرد | +| :--- | :--- | +| `users` | ادمین‌های پنل با نقشِ `admin` یا `owner` | +| `articles` | مقالات/گزارش‌ها (مدلِ منعطف؛ با `category`/`type` تفکیک می‌شود) | +| `radar_items` | آیتم‌های «در یک نگاه» در ۵ دسته: market/tech/commodity/geo/energy | +| `radar_pages` | صفحاتِ «رادار آینده» (مقاله‌ی بلوکیِ هر زیرمنو) | +| `risk_signals` | سیگنال‌های ریسک با سطوحِ critical…opportunity | +| `events` | رویدادهای تقویم | +| `team_members` | اعضای تیم/خبرگان | +| `market_prices`, `market_chart_points` | قیمت‌ها و نمودارهای صفحه‌ی Market (**دستی، ادیت‌شونده در پنل**) | +| `banners`, `integrations`, `institute_stats`, `vision_items`, `advisory_board`, `plans`, `factory_reports` | بخش‌های مختلفِ محتواییِ سایت | +| `members` | اعضای ثبت‌نامیِ سایت (موبایل/OTP) + `last_login`, `is_active` | +| `purchases` | خریدِ گزارش‌های غیررایگانِ اعضا | +| `member_activity` | لاگِ فعالیتِ اعضا (بازدیدِ صفحه + کلیک) | +| `subscribers` | ایمیل‌های عضویت در خبرنامه (فرمِ فوتر) | -* **هسته اصلی:** React 19 همراه با TypeScript برای ایمنی کدها و مدل‌سازی داده‌ها. -* **سیستم بیلد:** Vite جهت بارگذاری سریع در زمان توسعه (HMR) و خروجی بهینه در زمان پروداکشن. -* **استایل‌دهی:** Tailwind CSS v4 (با پیکربندی جدید مبتنی بر پلاگین Vite) به همراه متغیرهای سفارشی CSS در `src/index.css` برای رنگ‌بندی سازمانی طلایی (`#CD9E53`) و سورمه‌ای (`#032340`). -* **مسیریابی:** React Router DOM v7 با استفاده از ساختار شیءگرا (`createBrowserRouter`). -* **انیمیشن‌ها و اسکرول:** - * **GSAP (GreenSock) & ScrollTrigger:** برای پیاده‌سازی انیمیشن‌های پیچیده مبتنی بر اسکرول (مانند بخش Hero تعاملی). - * **Lenis Smooth Scroll:** موتور اسکرول بسیار نرم هماهنگ شده با تیکر GSAP (مخصوص دسکتاپ). - * **Framer Motion:** برای انیمیشن‌های خرد (Micro-animations)، پاپ‌آپ‌ها و ترنزیشن کامپوننت‌ها. -* **کامپوننت‌های سه‌بعدی:** - * **Three.js & React Three Fiber (R3F):** برای رندر کردن اشیاء سه‌بعدی در وب. - * **Three Globe & Cobe:** جهت نمایش کره زمین سه‌بعدی تعاملی و زیبا در بخش ژئوپلیتیک و ارتباطات بین‌الملل. -* **نمودارها:** React ApexCharts جهت نمایش نوسانات بازار فولاد و قیمت‌ها. -* **مدیریت وضعیت:** Zustand جهت مدیریت وضعیت‌های سراسری وب‌سایت. - -### ویژگی‌ها و قابلیت‌های بصری فرانت‌اند - -1. **بومی‌سازی و پشتیبانی دو زبانه (Localization):** - * پیاده‌سازی شده با استفاده از `useLang()` در `src/context/LangContext`. - * تغییر خودکار چیدمان صفحه به RTL برای زبان فارسی و LTR برای زبان انگلیسی با هدایت ویژگی `dir`. - * ترجمه درون‌برنامه‌ای کامپوننت‌ها با ساختار دیکشنری‌های محلی درون هر ماژول. -2. **تقویم رویدادهای جلالی بومی (`EventCalendar.tsx`):** - * طراحی شده به صورت کاملاً اختصاصی و بدون استفاده از کتابخانه‌های سنگین خارجی. - * استفاده از موتور بومی مرورگر با متد `Intl.DateTimeFormat('en-US-u-ca-persian')` برای ساخت گرید روزها و ماه‌های هجری شمسی. -3. **مدیریت عملکرد و اسکرول:** - * غیرفعال‌سازی Lenis در مرورگرهای موبایل و تبلت جهت جلوگیری از لگ یا تاخیر لمسی و سپردن اسکرول به سیستم‌عامل بومی دستگاه. - * بهینه‌سازی تصاویر با فرمت مدرن WebP. - -### دستورات بخش فرانت‌اند - -این دستورات را در پوشه ریشه (Root) پروژه اجرا کنید: - -```bash -# نصب پکیج‌ها -npm install - -# اجرای سرور توسعه محلی ری‌اکت -npm run dev - -# بررسی خطاهای تایپ‌اسکریپت (سریع‌ترین ابزار اعتبارسنجی لوپ توسعه) -npx tsc -b --noEmit - -# بررسی و رفع کدهای غیراستاندارد با ESLint -npm run lint - -# ایجاد پکیج خروجی نهایی (Production Build) -npm run build - -# پیش‌نمایش نسخه نهایی بیلد شده روی سیستم محلی -npm run preview -``` +> جدولِ `prices` (اسکرپرِ قدیمیِ tgju) دیگر استفاده نمی‌شود؛ اسکرپرِ زنده حذف شده و صفحه‌ی Market از `market_prices` می‌خواند. --- -## ۳. پنل مدیریت (Backend / Admin Panel) +## ۵. احراز هویت و اعضا -پنل مدیریت در پوشه `panel/` قرار دارد و وظیفه ذخیره‌سازی داده‌های ساختاریافته وب‌سایت و مدیریت کاربران ادمین را بر عهده دارد. +دو نشستِ مجزا: +- **ادمینِ پنل:** کوکیِ `session` (جدولِ `users`). نقشِ `owner` می‌تواند ادمین‌ها و اعضا را حذف کند. +- **عضوِ سایت:** کوکیِ `member_session` (جدولِ `members`). -### پشته فناوری بک‌اند +**ورود/ثبت‌نامِ اعضا با موبایل + کدِ یک‌بارمصرف (OTP):** ارسالِ پیامک با **کاوه‌نگار** (`KAVENEGAR_API_KEY`). در نبودِ کلید (محیطِ لوکال) کد در کنسولِ سرور چاپ می‌شود تا تست ممکن باشد. بازنشانیِ رمز هم از طریقِ SMS ممکن است. -* **فریم‌ورک سرور:** Express.js 4 مبتنی بر Node.js (نسخه ۲۰ به بالا). -* **پایگاه داده:** PostgreSQL (سیستم میزبانی ابری لیارا) به همراه ماژول `pg`. برای تسهیل مهاجرت از SQLite قبلی، یک نمای هماهنگ شبیه به متدهای `better-sqlite3` در فایل `db.js` ایجاد شده است. -* **امنیت و احراز هویت:** - * `bcryptjs` برای رمزنگاری امن گذرواژه‌ها. - * `jsonwebtoken` (JWT) برای احراز هویت کاربران ادمین و اعضا. کوکی‌های امن با ویژگی‌های `HttpOnly` و `Secure/SameSite` به مرورگر فرستاده می‌شوند تا از حملات XSS جلوگیری شود. - * `helmet` جهت اضافه کردن هدرهای امنیتی HTTP. - * `express-rate-limit` برای محدود کردن تعداد درخواست‌ها به اندپوینت‌های حساس نظیر فرم ورود و فرم تماس با ما. -* **پردازش تصاویر و فایل‌ها:** - * `multer` برای مدیریت آپلود فایل‌های چندرسانه‌ای به صورت بافر حافظه. - * `sharp` جهت پردازش خودکار تصاویر آپلود شده (فشرده‌سازی داینامیک، چرخش صحیح بر اساس EXIF، ریسایز تا سقف ۲۰۰۰ پیکسل و خروجی با فرمت فوق‌العاده بهینه WebP). - * `pdf-parse` برای خواندن و استخراج متون داخل اسناد PDF گزارش‌ها جهت تحلیل توسط هوش مصنوعی. -* **ذخیره‌سازی فایل (Storage):** - * پشتیبانی از پروتکل **S3-compatible Object Storage** (مانند فضای ذخیره‌سازی ابری لیارا). در صورت تنظیم متغیرهای محیطی S3، فایل‌ها مستقیماً آپلود شده و آدرس مستقیم CDN دریافت می‌شود. در غیر این صورت، پروژه به صورت خودکار به حالت آپلود روی دیسک محلی (`panel/uploads/`) تغییر مسیر می‌دهد. -* **یکپارچه‌سازی هوش مصنوعی (AI Integration):** - * استفاده از SDKهای رسمی **OpenAI** و **Anthropic (Claude)** برای ایجاد خلاصه‌ها، استخراج داده‌های کلیدی از اسناد آپلود شده و کمک به آماده‌سازی مطالب برای ادمین. +**مدیریتِ کاربران در پنل** (تب «کاربران سایت»): فهرستِ اعضا با شماره و **آخرین ورود**، صفحه‌ی جزئیات (خریدها + **تایم‌لاینِ فعالیت: بازدید/کلیک**)، **مسدودسازی/رفعِ مسدودی** (`is_active`) و **حذف**. عضوِ مسدودشده در سمتِ سایت هم اثر می‌گیرد: `AuthContext` نشست را هنگامِ لود و بازگشتِ فوکوسِ تب با `/api/members/me` اعتبارسنجی می‌کند و در صورتِ `403` کاربر را خارج می‌کند. -### ساختار و معماری پایگاه داده +--- -پایگاه داده در زمان استارت آپ اپلیکیشن به صورت خودکار با جداول زیر مقداردهی اولیه (ایجاد ایندکس‌ها و جداول در صورت عدم وجود) می‌شود: +## ۶. اتصال فرانت‌اند به پنل (Wiring) -1. **`users`**: ذخیره ادمین‌ها با نقش‌های `admin` یا `owner` (صاحب پنل که دسترسی ویرایش سایر ادمین‌ها را دارد). -2. **`articles`**: مقالات و گزارش‌های تحلیلی. شامل فیلدهای عنوان، دسته‌بندی، نویسنده، مشخصات صفحات، قیمت، تگ‌ها و متن اصلی. -3. **`risk_signals`**: سیگنال‌های ریسک ژئوپلیتیک و صنعتی با سطوح بحرانی متفاوت (`critical`, `high`, `medium`, `low`, `opportunity`). -4. **`events`**: رویدادهای تقویم اندیشکده (سمینارها، همایش‌ها، نمایشگاه‌ها). -5. **`team_members`**: اعضای تیم پژوهشی، تخصص‌ها، راه‌های ارتباطی و شمارش گزارش‌های منتشر شده. -6. **`radar_items`**: گزارش‌های رادار بازار در ۵ دسته‌بندی اصلی: بازار (Market)، فناوری (Tech)، کالا (Commodity)، ژئوپلیتیک (Geo) و انرژی (Energy). -7. **`prices`**: قیمت‌های ثبت شده و شاخص‌های مرتبط. -8. **`members` & `purchases`**: مدیریت خرید اعضا و دسترسی به فایل‌های گزارشات غیر رایگان. +فرانت‌اند هنگامِ استارت‌آپ، محتوای چند بخش را از پنل واکشی می‌کند (در `frontend/src/main.tsx` توابعِ `bootstrap*` فراخوانی می‌شوند) و در صورتِ در دسترس نبودنِ پنل به دادهٔ ایستای داخلِ `src/content` و `src/data` به‌عنوان fallback برمی‌گردد. نمونه‌ها: `/api/articles` (گزارش‌ها)، `/api/radar` (در یک نگاه)، `/api/events`، `/api/market-prices` و غیره. احراز هویتِ اعضا از طریقِ `AuthContext` و مسیرهای `/api/members/*` انجام می‌شود. -### سرویس‌های ویژه پنل مدیریت +آدرسِ پنل از متغیرِ `VITE_PANEL_API` خوانده می‌شود (در پروداکشن هنگامِ build در Dockerfile مقدارِ `https://cms-steelforesight.liara.run` به باندل تزریق می‌شود). -* **واردکننده خودکار داده (Import API):** - امکان وارد کردن فایل‌های JSON تولید شده توسط هوش مصنوعی Gemini که شامل خلاصه‌سازی‌ها، تگ‌ها و آیتم‌های رادار است با قابلیت پاک‌سازی خودکار کاراکترهای اضافی ارجاع دهی هوش مصنوعی (`[cite: ...]`). -* **مبدل خودکار تصاویر به WebP:** - تبدیل و بهینه‌سازی حجم تصاویر آپلود شده به صورت بی‌درنگ جهت بهبود سرعت لود کلاینت. +--- -### دستورات بخش پنل مدیریت +## ۷. فهرست APIها -ابتدا به پوشه پنل بروید: `cd panel` +خواندنی‌ها عمومی‌اند؛ نوشتنی‌ها (POST/PUT/PATCH/DELETE) نیازمندِ کوکیِ نشست یا هدرِ `Authorization: Bearer ` هستند. -```bash -# نصب وابستگی‌های بک‌اند -npm install - -# ساخت حساب کاربری ادمین اولیه با استفاده از مقادیر .env -npm run seed - -# اجرای سرور در حالت پروداکشن -npm start - -# اجرای سرور در حالت توسعه با قابلیت راه‌اندازی مجدد خودکار هنگام تغییر کدها (Watch mode) -npm run dev - -# بازسازی ماژول پایگاه داده در صورت تغییر نسخه Node.js سیستم -npm rebuild better-sqlite3 -``` - -### لیست APIهای پنل مدیریت - -تمامی متدهای ویرایشی (POST/PUT/DELETE) نیازمند ارسال توکن JWT در کوکی مرورگر یا هدر درخواست (`Authorization: Bearer `) هستند. - -| متد | مسیر | نیاز به احراز هویت | توضیحات | +| متد | مسیر | احراز هویت | توضیح | | :--- | :--- | :--- | :--- | -| **POST** | `/api/auth/login` | خیر | ورود ادمین و ثبت کوکی نشست امن | -| **POST** | `/api/auth/logout` | خیر | خروج از حساب کاربری و پاک کردن کوکی | -| **GET** | `/api/auth/me` | بله | دریافت مشخصات کاربر جاری | -| **GET** | `/api/articles` | خیر | لیست مقالات و گزارش‌ها (دارای فیلتر دسته‌بندی و نوع) | -| **POST** | `/api/articles` | بله (ادمین) | ایجاد مقاله جدید | -| **PUT** | `/api/articles/:id` | بله (ادمین) | ویرایش مقاله موجود | -| **DELETE** | `/api/articles/:id` | بله (ادمین) | حذف مقاله | -| **POST** | `/api/uploads` | bله (ادمین) | آپلود تصویر و تبدیل خودکار به WebP بهینه | -| **POST** | `/api/uploads/raw` | بله (ادمین) | آپلود ویدیو و اسناد PDF به فضای ابری S3 | -| **GET** | `/api/events` | خیر | لیست کل رویدادهای تقویم | -| **GET** | `/api/radar` | خیر | لیست تحلیل‌های رادار بازار | -| **POST** | `/api/admin/import` | بله (ادمین) | ایمپورت مستقیم داده‌های تحلیلی هوش مصنوعی | -| **GET** | `/api/health` | خیر | بررسی سلامت سرور | +| POST | `/api/auth/login` `/logout` | — / — | ورود/خروجِ ادمین | +| GET | `/api/auth/me` | ادمین | مشخصاتِ ادمینِ جاری | +| GET/POST/PUT/DELETE | `/api/articles[...]` | خواندن آزاد / نوشتن ادمین | CRUD مقالات | +| GET/POST/PUT/DELETE | `/api/radar`, `/api/radar-pages`, `/api/events`, `/api/banners`, `/api/risks`, `/api/team`, `/api/market-prices` … | خواندن آزاد / نوشتن ادمین | CRUD بخش‌های محتوایی | +| POST | `/api/uploads`, `/api/uploads/raw` | ادمین | آپلودِ تصویر (→WebP) / ویدیو و PDF (→S3) | +| POST | `/api/admin/import` | ادمین | ایمپورتِ دادهٔ تحلیلیِ هوش مصنوعی | +| POST | `/api/members/otp/send` `/otp/verify` | — | ورود/ثبت‌نام با OTP | +| POST | `/api/members/register-verify` | — | تکمیلِ ثبت‌نام پس از تأییدِ کد | +| GET/PUT | `/api/members/me` | عضو | پروفایلِ عضو | +| GET | `/api/members/me/purchases[...]` | عضو | خریدها و دانلودِ گزارش | +| POST | `/api/members/activity` | عضو | ثبتِ فعالیت (بازدید/کلیک) | +| GET | `/api/members` `/api/members/:id` `/:id/activity` | ادمین | فهرست/جزئیات/فعالیتِ اعضا | +| PATCH/DELETE | `/api/members/:id` | ادمین/owner | مسدودسازی/حذفِ عضو | +| POST/GET/DELETE | `/api/newsletter[...]` | نوشتن آزاد / خواندن ادمین | عضویت در خبرنامه و مدیریتِ آن | +| POST | `/api/contact` | — | فرمِ تماس با ما | +| GET | `/api/health` | — | سلامتِ سرور | --- -## ۴. نحوه اتصال فرانت‌اند به پنل مدیریت (Wiring) +## ۸. استقرار (Deployment) -در حال حاضر بخش زیادی از اطلاعات فرانت‌اند به صورت ایستا (Static / Mock Data) از پوشه `src/data/` یا `src/content/` لود می‌شود تا فرانت‌بخش بدون وابستگی به سرور قابل نمایش باشد. +هر دو اپ روی **لیارا (Liara)** مستقر می‌شوند (دو اپِ جدا). -برای داینامیک کردن داده‌ها و اتصال فرانت‌اند به پنل مدیریت مراحل زیر پیشنهاد می‌شود: +### فرانت‌اند — اپِ `steelforesight` +خروجیِ استاتیک، با Dockerfileِ دو‌مرحله‌ای: مرحله‌ی Node برای `vite build` و سپس Nginxِ سبک برای سروِ `dist/` روی پورتِ **۷۸۶۰** (سازگار با Hugging Face Spaces). +```bash +cd frontend +liara deploy --app steelforesight --platform docker --port 7860 --build-location iran +``` +`VITE_PANEL_API` هنگامِ build از طریقِ `ARG` در Dockerfile تزریق می‌شود. -1. در ریشه پروژه، متغیر محیطی آدرس بک‌اند را در فایل `.env.local` تعریف کنید: - ```env - VITE_PANEL_API=http://localhost:3001 - ``` -2. یک سرویس کلاینت (مثلا `src/lib/api.ts`) برای واکشی داده‌ها بسازید: - ```typescript - const API_URL = import.meta.env.VITE_PANEL_API || 'http://localhost:3001'; - - export async function getArticles(category?: string) { - const url = category ? `${API_URL}/api/articles?category=${encodeURIComponent(category)}` : `${API_URL}/api/articles`; - const res = await fetch(url); - if (!res.ok) throw new Error('خطا در دریافت اطلاعات از سرور'); - return res.json(); - } - ``` -3. در کامپوننت‌های فرانت‌اند (مانند بخش نمایش گزارش‌های صفحه اصلی)، از React Query (که پکیج آن در پروژه نصب است) یا هوک `useEffect` برای دریافت پویای داده‌ها به شکل زیر استفاده کنید: - ```typescript - import { useQuery } from '@tanstack/react-query'; - import { getArticles } from '@/lib/api'; - - // در داخل کامپوننت ری‌اکت - const { data: reports, isLoading } = useQuery({ - queryKey: ['articles', category], - queryFn: () => getArticles(category), - placeholderData: fallbackMockData // استفاده از داده‌های ماک قبلی در زمان لودینگ یا خطا - }); - ``` - ---- - -## ۵. استقرار و دگرگونی‌ها (Deployment) - -### فرانت‌اند -فرانت‌اند پروژه به دلیل ایستا (Static) بودن خروجی نهایی، به راحتی به کمک **Dockerfile** موجود استقرار می‌یابد. -* داکرفایل از ساختار دو مرحله‌ای (Multi-stage build) استفاده می‌کند: - 1. مرحله اول: استفاده از ایمیج Node جهت نصب پکیج‌ها و ایجاد خروجی نهایی بیلد در پوشه `dist`. - 2. مرحله دوم: استفاده از سرور بسیار سبک Nginx جهت کپی و سرو کردن فایل‌های پوشه `dist` روی پورت `7860`. -* این پورت برای آپلود مستقیم در پلتفرم‌هایی مانند **Hugging Face Spaces** بهینه شده است (تنظیمات متا دیتا در بالای فایل `README.md` ریشه قید شده است). - -### پنل مدیریت (بک‌اند) -* پنل مدیریت به پایگاه داده و در صورت تمایل سیستم فایل نیاز دارد. -* برای دیپلوی روی **لیارا (Liara)**، فایل‌های کانفیگ نظیر `liara.json` و فایل نادیده‌گیری `.liaraignore` تعبیه شده‌اند. -* حتما باید متغیرهای محیطی زیر در کنترل پنل هاست تعریف شوند: - * `PORT`: پورت سرور (به عنوان مثال ۳۰۰۱). - * `JWT_SECRET`: یک رشته هگزادسیمل تصادفی طولانی (حداقل ۳۲ کاراکتر). - * `DATABASE_URL`: آدرس اتصال به پایگاه داده PostgreSQL ابری شما. - * `ALLOWED_ORIGINS`: آدرس دامنه فرانت‌اند جهت عبور از گیت CORS. - * متغیرهای سرویس فایل ابری در صورت نیاز (`LIARA_ENDPOINT`, `LIARA_BUCKET`, `LIARA_ACCESS_KEY`, `LIARA_SECRET_KEY`). +### پنل — اپِ `cms-steelforesight` +```bash +cd panel +liara deploy --app cms-steelforesight --platform node --build-location iran +``` +متغیرهای محیطیِ لازم (در کنترل‌پنلِ هاست): +- `DATABASE_URL` — اتصالِ PostgreSQL. +- `JWT_SECRET` — رشته‌ی تصادفیِ بلند (≥۳۲ کاراکتر). +- `ADMIN_USERNAME` / `ADMIN_PASSWORD` — برای `npm run seed`. +- `ALLOWED_ORIGINS` — دامنه‌ی فرانت برای عبور از CORS. +- `KAVENEGAR_API_KEY` — ارسالِ پیامکِ OTP. +- اختیاری: متغیرهای ایمیل و object storage (`LIARA_ENDPOINT`, `LIARA_BUCKET`, `LIARA_ACCESS_KEY`, `LIARA_SECRET_KEY`). diff --git a/docker-compose.yml b/docker-compose.yml index c4ad717..6e451e8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: web: build: - context: . + context: ./frontend dockerfile: Dockerfile image: andishkade-foolad:latest container_name: andishkade-foolad diff --git a/fig-profile.png b/fig-profile.png deleted file mode 100644 index 97bda40..0000000 Binary files a/fig-profile.png and /dev/null differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a65670c..9f59a10 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,16 +11,11 @@ "@radix-ui/react-slot": "^1.3.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.1", - "@tabler/icons-react": "^3.44.0", - "@tanstack/react-query": "^5.100.14", "apexcharts": "^5.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cobe": "^2.0.1", - "dotted-map": "^3.1.0", "framer-motion": "^12.40.0", "gsap": "^3.15.0", - "jalaali-js": "^1.2.8", "lenis": "^1.3.23", "lucide-react": "^1.16.0", "react": "^19.2.6", @@ -30,14 +25,11 @@ "react-router-dom": "^7.15.1", "tailwind-merge": "^3.6.0", "three": "^0.184.0", - "three-globe": "^2.45.2", - "vazir-font": "^30.1.0", - "zustand": "^5.0.13" + "three-globe": "^2.45.2" }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.0", - "@types/jalaali-js": "^1.2.0", "@types/node": "^24.12.4", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -1017,32 +1009,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tabler/icons": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz", - "integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - } - }, - "node_modules/@tabler/icons-react": { - "version": "3.44.0", - "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz", - "integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==", - "license": "MIT", - "dependencies": { - "@tabler/icons": "3.44.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/codecalm" - }, - "peerDependencies": { - "react": ">= 16" - } - }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -1315,32 +1281,6 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@tanstack/query-core": { - "version": "5.100.14", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.14.tgz", - "integrity": "sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/react-query": { - "version": "5.100.14", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.14.tgz", - "integrity": "sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==", - "license": "MIT", - "dependencies": { - "@tanstack/query-core": "5.100.14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": "^18 || ^19" - } - }, "node_modules/@turf/boolean-point-in-polygon": { "version": "7.3.5", "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.5.tgz", @@ -1427,13 +1367,6 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, - "node_modules/@types/jalaali-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/jalaali-js/-/jalaali-js-1.2.0.tgz", - "integrity": "sha512-DRQKjbfFe0/v3siCou9eFzYjDxJItkJpxbU+/CSptvoRqq2/VmTA6y8QvqK/VELKW6xX3qb174nLZlrFV9OT3Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2031,12 +1964,6 @@ "node": ">=6" } }, - "node_modules/cobe": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/cobe/-/cobe-2.0.1.tgz", - "integrity": "sha512-aaa6vcIlaC8C1SF50LDH0Anybo/EAXnrxqe+bwvr4+YUtZydqjeBjTTD7ziCCkbRrRGSns3I3F6cZsf3W+L+ag==", - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2322,19 +2249,6 @@ "node": ">=8" } }, - "node_modules/dotted-map": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/dotted-map/-/dotted-map-3.1.0.tgz", - "integrity": "sha512-E0z9o5IaTf44FnWHvbyg4QQcBwXgzZJr82HJASWb1dKUCULHfnlWKRpfe5EFHTk/yaoS1sQSLMyMrL6o74/sIw==", - "license": "MIT", - "dependencies": { - "@turf/boolean-point-in-polygon": "^7.3.4", - "proj4": "^2.20.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/draco3d": { "version": "1.5.7", "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz", @@ -2951,12 +2865,6 @@ "react": "^19.0.0" } }, - "node_modules/jalaali-js": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", - "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", - "license": "MIT" - }, "node_modules/jerrypick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", @@ -3454,12 +3362,6 @@ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "license": "MIT" }, - "node_modules/mgrs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz", - "integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==", - "license": "MIT" - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -3708,19 +3610,6 @@ "node": ">= 0.8.0" } }, - "node_modules/proj4": { - "version": "2.20.8", - "resolved": "https://registry.npmjs.org/proj4/-/proj4-2.20.8.tgz", - "integrity": "sha512-1C8sfT4xY4PAPwk0MroFBTGF4R4bzDXdmPQTGYVLsoNssrZ9odzObxS2dTeGBty8jW8KO7h16C1Hs2JP+ctfFw==", - "license": "MIT", - "dependencies": { - "mgrs": "1.0.0", - "wkt-parser": "^1.5.5" - }, - "funding": { - "url": "https://github.com/sponsors/ahocevar" - } - }, "node_modules/promise-worker-transferable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", @@ -4420,13 +4309,6 @@ "node": ">= 4" } }, - "node_modules/vazir-font": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/vazir-font/-/vazir-font-30.1.0.tgz", - "integrity": "sha512-XN2Uprw/Q3QhAAApITykf+v0l4p4FpqvIh0h2XDSJRjwUiYeDpwQxECQF/93ZOCDO2t8haBi6BZZqO8sifYNRg==", - "deprecated": "vazir-font no longer supported. Use vazirmatn instead.", - "license": "OFL" - }, "node_modules/vite": { "version": "8.0.14", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", @@ -4531,15 +4413,6 @@ "node": ">= 8" } }, - "node_modules/wkt-parser": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.5.5.tgz", - "integrity": "sha512-/zMYi94/7D7fxcOSlVmWn6vnOMj3Gq5d1xvVjaYOS9n6h0qOJ4I7YYVxBWYcH1vq9+suhqzXkn05Yx47zQNUIA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ahocevar" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4cf56c8..8fda804 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,16 +13,11 @@ "@radix-ui/react-slot": "^1.3.0", "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.6.1", - "@tabler/icons-react": "^3.44.0", - "@tanstack/react-query": "^5.100.14", "apexcharts": "^5.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "cobe": "^2.0.1", - "dotted-map": "^3.1.0", "framer-motion": "^12.40.0", "gsap": "^3.15.0", - "jalaali-js": "^1.2.8", "lenis": "^1.3.23", "lucide-react": "^1.16.0", "react": "^19.2.6", @@ -32,14 +27,11 @@ "react-router-dom": "^7.15.1", "tailwind-merge": "^3.6.0", "three": "^0.184.0", - "three-globe": "^2.45.2", - "vazir-font": "^30.1.0", - "zustand": "^5.0.13" + "three-globe": "^2.45.2" }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.0", - "@types/jalaali-js": "^1.2.0", "@types/node": "^24.12.4", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/hero-bg.original.mp4 b/hero-bg.original.mp4 deleted file mode 100644 index f90c69a..0000000 --- a/hero-bg.original.mp4 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e22f4390856dd4995a930e22c5026008b333d6b5429d4201ae71d6b3a397c41 -size 43690639 diff --git a/live_dashboard.html b/live_dashboard.html deleted file mode 100644 index b7f8b3d..0000000 --- a/live_dashboard.html +++ /dev/null @@ -1,681 +0,0 @@ - - - - - - 🪙 داشبورد زنده قیمت سکه — اندیشکده فولاد آینده - - - - - - - - -
- - -
-
- -
-

داشبورد زنده قیمت سکه

-
اندیشکده فولاد آینده — مرکز تحلیل راهبردی بازار طلا و صنعت
-
-
- - -
-
-
- بروزرسانی زنده فعال -
-
- داده جدید در: - ۶۰ ثانیه -
-
-
- - -
- -
- - -
-
-

⏳ تاریخچه نوسانات اخیر (ثبت خودکار هر ۱ دقیقه)

-
-
- - - - - - - - - - - - - - - - - -
زمان ثبت سیستمنام سکهقیمت فعلی (تومان)میزان و درصد تغییرکمترین قیمت روزبیشترین قیمت روززمان بروزرسانی سایت
در حال لود اطلاعات از سرور...
-
-
- -
- مرکز مستقل تحلیل راهبردی و سیاست‌گذاری بازار و صنایع — تمامی نرخ‌ها به صورت مستقیم و بدون کش از بازار تهران دریافت می‌شوند. -
- -
- - - - - - diff --git a/live_prices.js b/live_prices.js deleted file mode 100644 index 11776ef..0000000 --- a/live_prices.js +++ /dev/null @@ -1,50 +0,0 @@ -window.LIVE_COIN_PRICES = { - "last_fetch": "2026-06-01 16:15:58", - "prices": [ - { - "name": "سکه امامی", - "price_rial": 1845050000, - "price_toman": 184505000, - "change": "(3.07%) 55,000,000", - "min_toman": 182480000, - "max_toman": 184510000, - "last_update": "16:13:54" - }, - { - "name": "سکه بهار آزادی", - "price_rial": 1810100000, - "price_toman": 181010000, - "change": "(2.26%) 39,950,000", - "min_toman": 177980000, - "max_toman": 181020000, - "last_update": "16:14:00" - }, - { - "name": "نیم سکه", - "price_rial": 940000000, - "price_toman": 94000000, - "change": "(2.1%) 19,300,000", - "min_toman": 93500000, - "max_toman": 94000000, - "last_update": "11:10:35" - }, - { - "name": "ربع سکه", - "price_rial": 535000000, - "price_toman": 53500000, - "change": "(2.88%) 15,000,000", - "min_toman": 52500000, - "max_toman": 53500000, - "last_update": "15:38:23" - }, - { - "name": "سکه گرمی", - "price_rial": 275000000, - "price_toman": 27500000, - "change": "(1.85%) 5,000,000", - "min_toman": 27000000, - "max_toman": 27500000, - "last_update": "15:55:20" - } - ] -}; \ No newline at end of file diff --git a/live_prices.json b/live_prices.json deleted file mode 100644 index 2254c00..0000000 --- a/live_prices.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "last_fetch": "2026-06-01 16:15:58", - "prices": [ - { - "name": "سکه امامی", - "price_rial": 1845050000, - "price_toman": 184505000, - "change": "(3.07%) 55,000,000", - "min_toman": 182480000, - "max_toman": 184510000, - "last_update": "16:13:54" - }, - { - "name": "سکه بهار آزادی", - "price_rial": 1810100000, - "price_toman": 181010000, - "change": "(2.26%) 39,950,000", - "min_toman": 177980000, - "max_toman": 181020000, - "last_update": "16:14:00" - }, - { - "name": "نیم سکه", - "price_rial": 940000000, - "price_toman": 94000000, - "change": "(2.1%) 19,300,000", - "min_toman": 93500000, - "max_toman": 94000000, - "last_update": "11:10:35" - }, - { - "name": "ربع سکه", - "price_rial": 535000000, - "price_toman": 53500000, - "change": "(2.88%) 15,000,000", - "min_toman": 52500000, - "max_toman": 53500000, - "last_update": "15:38:23" - }, - { - "name": "سکه گرمی", - "price_rial": 275000000, - "price_toman": 27500000, - "change": "(1.85%) 5,000,000", - "min_toman": 27000000, - "max_toman": 27500000, - "last_update": "15:55:20" - } - ] -} \ No newline at end of file diff --git a/panel/server.js b/panel/server.js index 542301e..fe004f4 100644 --- a/panel/server.js +++ b/panel/server.js @@ -15,7 +15,6 @@ import path from 'node:path'; import fs from 'node:fs'; import { fileURLToPath } from 'node:url'; import { db, rowToArticle, rowToRiskSignal, rowToPrice, rowToEvent, rowToTeamMember, rowToPlan, rowToBanner, rowToRadarItem, rowToRadarPage, rowToFactoryReport, rowToIntegration, rowToInstituteStat, rowToMarketPrice, rowToMarketChartPoint, rowToVisionItem, rowToAdvisoryMember } from './db.js'; -import { startScraperLoop, scrapeOnce } from './scraper.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PORT = Number(process.env.PORT || 3001); @@ -1402,24 +1401,6 @@ app.delete('/api/newsletter/:id', authRequired, async (req, res) => { res.status(204).end(); }); -// ---------- prices (scraped from tgju.org every 2 min) ---------- -app.get('/api/prices', async (_req, res) => { - const rows = await db - .prepare('SELECT symbol, name, value, unit, change_value, change_pct, source_url, fetched_at FROM prices ORDER BY name') - .all(); - res.json(rows.map(rowToPrice)); -}); - -app.post('/api/prices/refresh', authRequired, async (_req, res) => { - try { - const n = await scrapeOnce(); - res.json({ ok: true, scraped: n }); - } catch (err) { - console.error('[scraper] refresh error:', err); - res.status(500).json({ error: 'scrape_failed' }); - } -}); - // ---------- banners ---------- const BANNER_FIELDS = `id,overline_fa,overline_en,title_fa,title_en,subtitle_fa,subtitle_en,cta_label_fa,cta_label_en,cta_url,image,position,active,sort_order,created_at,updated_at`; @@ -1865,11 +1846,4 @@ app.get('/api/members/me/purchases/:purchaseId/download', memberRequired, async app.listen(PORT, () => { console.log(`Panel running at ${PUBLIC_ORIGIN}`); - const intervalMs = Number(process.env.SCRAPER_INTERVAL_MS) || 2 * 60 * 1000; - if (process.env.SCRAPER_DISABLED !== '1') { - startScraperLoop(intervalMs); - console.log(`[scraper] tgju.org polling every ${intervalMs / 1000}s`); - } else { - console.log('[scraper] disabled via SCRAPER_DISABLED=1'); - } }); diff --git a/rtk.exe b/rtk.exe deleted file mode 100644 index 52beb71..0000000 Binary files a/rtk.exe and /dev/null differ diff --git a/scraper/scraper.py b/scraper/scraper.py deleted file mode 100644 index fecb26f..0000000 --- a/scraper/scraper.py +++ /dev/null @@ -1,300 +0,0 @@ -import os -import sys -import urllib.request -import ssl -import re -import time -import json -from datetime import datetime - -# Ensure terminal output supports UTF-8 on Windows -if sys.platform == 'win32': - try: - sys.stdout.reconfigure(encoding='utf-8') - except AttributeError: - pass - -# Persian/Arabic to English digit mapping -FA_TO_EN = { - '۰': '0', '۱': '1', '۲': '2', '۳': '3', '۴': '4', - '۵': '5', '۶': '6', '۷': '7', '۸': '8', '۹': '9', - '٠': '0', '١': '1', '٢': '2', '٣': '3', '٤': '4', - '٥': '5', '٦': '6', '٧': '7', '٨': '8', '٩': '9' -} - -def clean_persian_text(text): - """Clean HTML tags and standardize spacing.""" - if not text: - return "" - text = re.sub(r'<[^>]+>', '', text) - text = re.sub(r'\s+', ' ', text).strip() - return text - -def to_english_digits(text): - """Convert Persian/Arabic digits in a string to English digits.""" - if not text: - return "" - return "".join(FA_TO_EN.get(char, char) for char in text) - -def parse_numeric(text): - """Convert a price string with commas and Persian digits into an integer.""" - if not text: - return 0 - clean_text = to_english_digits(text) - digits_only = re.sub(r'[^\d]', '', clean_text) - try: - return int(digits_only) if digits_only else 0 - except ValueError: - return 0 - -def fetch_html_with_retry(url, retries=3, delay=2): - """Fetch URL with retries, custom headers, and disabled SSL checks.""" - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE - - headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36', - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', - 'Accept-Language': 'fa,en-US;q=0.9,en;q=0.8', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - } - - # Make it a POST request by passing empty data (b"") to completely bypass CDN/ArvanCloud HTML caching - req = urllib.request.Request(url, data=b"", headers=headers) - - for attempt in range(1, retries + 1): - try: - with urllib.request.urlopen(req, context=ctx, timeout=10) as response: - if response.status == 200: - html = response.read().decode('utf-8') - if "sekee" in html: - return html - else: - raise ValueError("Incorrect page content returned (missing expected coin tags).") - except Exception as e: - if attempt < retries: - time.sleep(delay) - delay *= 1.5 - else: - print(f"⚠️ Connection error on attempt {attempt}: {e}") - return None - -HISTORY_LOG = [] -HISTORY_INITIALIZED = False - -def initialize_history_from_excel(): - global HISTORY_LOG, HISTORY_INITIALIZED - excel_filename = "coin_prices.xlsx" - if not HISTORY_INITIALIZED: - if os.path.exists(excel_filename): - try: - import pandas as pd - df = pd.read_excel(excel_filename, sheet_name="تاریخچه تغییرات") - HISTORY_LOG = df.to_dict(orient="records") - print(f"📥 Loaded {len(HISTORY_LOG)} history logs from existing '{excel_filename}'.") - except Exception as e: - print(f"💡 Initialized new history log in memory (failed to load: {e})") - HISTORY_INITIALIZED = True - -def save_live_js_and_json(scraped_data): - """Save scraped data to JSON and JS files for real-time HTML dashboard.""" - current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - live_records = [] - for row in scraped_data: - live_records.append({ - "name": row["Name"], - "price_rial": row["Price_Rial"], - "price_toman": row["Price_Toman"], - "change": row["Change"], - "min_toman": row["Min_Price_Toman"], - "max_toman": row["Max_Price_Toman"], - "last_update": row["Last_Update"] - }) - - data_to_save = { - "last_fetch": current_time_str, - "prices": live_records - } - - # Save as JSON - try: - with open("live_prices.json", "w", encoding="utf-8") as f: - json.dump(data_to_save, f, ensure_ascii=False, indent=2) - except Exception as e: - print(f"⚠️ Error saving live_prices.json: {e}") - - # Save as JS (for file:// protocol CORS bypass) - try: - js_content = f"window.LIVE_COIN_PRICES = {json.dumps(data_to_save, ensure_ascii=False, indent=2)};" - with open("live_prices.js", "w", encoding="utf-8") as f: - f.write(js_content) - except Exception as e: - print(f"⚠️ Error saving live_prices.js: {e}") - -def save_to_excel(scraped_data): - """Save scraped data to Excel file with Latest Prices and History sheets.""" - global HISTORY_LOG - excel_filename = "coin_prices.xlsx" - - # Initialize history from Excel if we haven't already - initialize_history_from_excel() - - # Create list for current scraped data - new_rows = [] - current_time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - for row in scraped_data: - new_rows.append({ - "زمان ثبت": current_time_str, - "نام سکه": row["Name"], - "قیمت فعلی (ریال)": row["Price_Rial"], - "قیمت فعلی (تومان)": row["Price_Toman"], - "میزان و درصد تغییر": row["Change"], - "کمترین قیمت روز (تومان)": row["Min_Price_Toman"], - "بیشترین قیمت روز (تومان)": row["Max_Price_Toman"], - "زمان به‌روزرسانی سایت": row["Last_Update"] - }) - - import pandas as pd - df_new = pd.DataFrame(new_rows) - df_latest = df_new.drop(columns=["زمان ثبت"]) - - # Accumulate history in memory list - temp_history_log = HISTORY_LOG + new_rows - df_history = pd.DataFrame(temp_history_log) - - # Write both sheets to Excel - try: - with pd.ExcelWriter(excel_filename, engine='openpyxl') as writer: - df_latest.to_excel(writer, sheet_name="قیمت‌های لحظه‌ای", index=False) - df_history.to_excel(writer, sheet_name="تاریخچه تغییرات", index=False) - - # Write succeeded! Commit the temp log to our persistent memory HISTORY_LOG - HISTORY_LOG = temp_history_log - print(f"📊 Excel file updated: '{excel_filename}' (Latest & History sheets)") - return True - except Exception as e: - print(f"⚠️ Warning: Excel file is locked (likely open in Microsoft Excel).") - print(f"⚠️ {len(new_rows)} rows kept in memory and will auto-save on the next minute cycle when you close Excel. (Error: {e})") - return False - -def run_scraper_cycle(): - """Runs a single cycle of the scraper.""" - url = "https://www.tgju.org/coin" - html = fetch_html_with_retry(url) - - if not html: - print("📡 Connection to TGJU failed. Retrying in next cycle...") - return False - - market_rows = re.findall(r']*data-market-row="([^"]+)"[^>]*>([\s\S]*?)<\/tr>', html) - - target_keys = { - "sekee": "سکه امامی", - "sekeb": "سکه بهار آزادی", - "nim": "نیم سکه", - "rob": "ربع سکه", - "gerami": "سکه گرمی" - } - - scraped_data = [] - - for row_name, row_content in market_rows: - if row_name in target_keys: - coin_display_name = target_keys[row_name] - tds = re.findall(r']*>([\s\S]*?)<\/td>', row_content) - - if len(tds) >= 5: - raw_price_rial = clean_persian_text(tds[0]) - raw_change = clean_persian_text(tds[1]) - raw_min = clean_persian_text(tds[2]) - raw_max = clean_persian_text(tds[3]) - raw_time = clean_persian_text(tds[4]) - - price_rial = parse_numeric(raw_price_rial) - price_toman = price_rial // 10 - - min_rial = parse_numeric(raw_min) - min_toman = min_rial // 10 - - max_rial = parse_numeric(raw_max) - max_toman = max_rial // 10 - - clean_change = to_english_digits(raw_change) - clean_time = to_english_digits(raw_time) - - scraped_data.append({ - "Key": row_name, - "Name": coin_display_name, - "Price_Rial": price_rial, - "Price_Toman": price_toman, - "Change": clean_change, - "Min_Price_Toman": min_toman, - "Max_Price_Toman": max_toman, - "Last_Update": clean_time - }) - - if not scraped_data: - print("⚠️ No coin rows were matched in the HTML. Table structure may have changed.") - return False - - # Save Excel - try: - save_to_excel(scraped_data) - except Exception as e: - print(f"⚠️ Error preparing Excel: {e}") - - # Save JSON and JS for the dynamic HTML dashboard - try: - save_live_js_and_json(scraped_data) - except Exception as e: - print(f"⚠️ Error saving live assets: {e}") - - # Print safely to console - print("\n" + "=" * 80) - print(f"{'نام سکه':<20} | {'قیمت (تومان)':<18} | {'تغییر':<18} | {'به‌روزرسانی':<12}") - print("-" * 80) - for row in scraped_data: - name_ascii = row["Name"].ljust(20) - price_str = f"{row['Price_Toman']:,}" - change_str = row["Change"] - time_str = row["Last_Update"] - try: - print(f"{name_ascii} | {price_str:<18} | {change_str:<18} | {time_str:<12}") - except Exception: - print(f"{row['Key'].ljust(20)} | {price_str:<18} | {change_str:<18} | {time_str:<12}") - print("=" * 80 + "\n") - return True - -def main(): - print("=" * 60) - print(" 🪙 TGJU AUTOMATED COIN SCRAPER & SCHEDULER 🪙") - print(" 🔄 Running every 1 minute | Output: Excel & Realtime HTML") - print("=" * 60) - - print(f"🚀 Initializing scraper at {datetime.now().strftime('%H:%M:%S')}...") - run_scraper_cycle() - - while True: - try: - for remaining in range(60, 0, -1): - sys.stdout.write(f"\r⏳ Next fetch in {remaining:02d} seconds... ") - sys.stdout.flush() - time.sleep(1) - sys.stdout.write("\r📡 Fetching live prices... \n") - sys.stdout.flush() - - run_scraper_cycle() - except KeyboardInterrupt: - print("\n👋 Scraper stopped by user. Goodbye!") - sys.exit(0) - except Exception as e: - print(f"\n⚠️ Unexpected error in loop: {e}") - print("⏳ Retrying in 10 seconds...") - time.sleep(10) - -if __name__ == "__main__": - main()