UI v1 این کامپوننت را حذف کرده بود؛ ساختار قدیمی صفحهٔ جزئیات پروژه به آن وابسته است (دکمهٔ بازگشت به فهرست پروژهها).
گروه ساختمانی آرامش — پروژه کامل (فرانتاند + بکاند)
مونوریپوی سایت گروه ساختمانی آرامش شامل:
frontend/— سایت React + Vite + Tailwind (تمام متنها و تصاویر از API خوانده میشود)backend/— بکاند FastAPI + SQLAlchemy 2 + PostgreSQL (pgvector) + JWT + MinIOchatbot/— دستیار هوشمند (FastAPI + LangGraph + RAG با pgvector، پاسخ استریمی)- مانیتورینگ و آنالیتیکس — Prometheus/Grafana/Loki و Umami بهصورت لایههای compose
معماری
Traefik (تنها نقطه ورود — 80/443 در تولید)
├── / → Caddy (فایلهای استاتیک SPA)
├── /api → FastAPI (بکاند) + متریک /metrics فقط داخلی
├── /media → MinIO (باکت aramesh-media)
├── /chat → چتبات (SSE استریم پاسخ)
├── grafana.* → Grafana (مانیتورینگ — لایه monitoring)
└── analytics.* → Umami (آنالیتیکس محصول)
- همه محتوای متنی سایت در PostgreSQL است — هیچ متنی در کد فرانتاند هاردکد نشده؛
با یک درخواست
GET /api/siteبارگذاری و از پنل مدیریت ویرایش میشود - تصاویر آپلودی در MinIO (Object Storage سازگار با S3) ذخیره میشوند و با مسیر نسبی
/media/...برگردانده میشوند — بکاند ذخیرهسازی بدون تغییر محتوای دیتابیس قابل تعویض است - مسیریابی همهچیز روی Traefik است (با Docker labels) — nginx در پروژه وجود ندارد
- شبکههای compose تفکیک شدهاند:
edge(سرویسهای پشت Traefik) وinternal(دیتابیس/ذخیرهسازی) — فرانتاند هیچ راه مستقیمی به دیتابیس ندارد - لاگها: json-file با سقف ۱۰MB×۳ برای هر سرویس؛ علاوه بر آن Promtail همه را به Loki میفرستد تا در Grafana جستوجو شوند
ساختار
├── frontend/ # سایت React (بیلد روی هاست — ایمیج فقط سرو میکند)
│ ├── src/ # کد منبع (api.tsx = لایه اتصال به بکاند)
│ ├── Caddyfile # سرو استاتیک + کش immutable برای /assets/*
│ └── Dockerfile # Caddy — بدون root
├── backend/ # بکاند FastAPI
│ ├── app/ # کد منبع (آپلود: app/api/admin.py — سئو: app/seo_pages.py)
│ ├── migrations/ # مهاجرتهای نسخهدار Alembic
│ ├── tests/ # تستهای API (pytest)
│ ├── alembic.ini
│ └── Dockerfile # بدون root — خاموشی تمیز با exec
├── chatbot/ # دستیار هوشمند (RAG + pgvector)
├── monitoring/ # پیکربندی Prometheus/Alertmanager/Loki/Promtail/Grafana
├── scripts/
│ ├── stack.sh # ابزار اصلی عملیات — deploy/logs/health/backup/restore/update
│ ├── backup.sh # پشتیبان کامل (DB + تصاویر) با چکسام و گزینه انتشار بیرونی
│ ├── restore.sh # بازگردانی پشتیبان دیتابیس
│ ├── gen-env.sh # ساخت .env با رمزهای قوی تصادفی
│ ├── audit.sh # ممیزی آسیبپذیری وابستگیها و ایمیجها
│ ├── migrate-media.py # انتقال یکباره تصاویر CDN خارجی به MinIO
│ └── ci.sh # تست + بیلد ایمیج (محلی یا هر CI)
├── docker-compose.yml # استک اصلی (شامل چتبات و Umami)
├── docker-compose.monitoring.yml # لایه مانیتورینگ
├── docker-compose.prod.yml # لایه تولید: TLS، دامنه، سختسازی پورتها
└── docker-compose.override.yml # تنظیمات محلی این ماشین (در گیت نیست)
اجرا با Docker
پیشنیاز: Docker Desktop. بیلد فرانتاند روی هاست انجام میشود (کانتینر فقط فایلهای
dist/ را سرو میکند) — scripts/stack.sh همین را به یک دستور تبدیل میکند:
cp .env.example .env # یا: bash scripts/gen-env.sh (رمزهای قوی تصادفی)
cd frontend && npm install && cd ..
bash scripts/stack.sh deploy # بیلد فرانت + بیلد و بالا آوردن استک + انتظار برای سلامت
- سایت: http://localhost:18081 (پورت از
TRAEFIK_HTTP_PORTدر.env) — ورود مدیریت:/admin/login - دستورهای عملیات:
bash scripts/stack.sh—logs،ps،health،backup،restore،update،seed-force - مستندات API: http://localhost:19001/docs (با
docker-compose.override.ymlمحلی) - کنسول MinIO: http://localhost:9001 — کاربر/رمز از متغیرهای
MINIO_ROOT_*در.env - دیتابیس PostgreSQL (تصویر pgvector)، آبجکتاستور MinIO و کش مدلهای چتبات هر یک در کانتینر جدا با volume دائمی
- بکاند هنگام شروع، جداول پایه را میسازد، مهاجرتهای Alembic را اعمال میکند و فقط جدولهای خالی را seed میکند — ارتقای نسخه دادههای فعلی را از بین نمیبرد
minio-setupباکتaramesh-mediaرا میسازد و فقط وقتی باکت خالی است تصاویرseed-media/را میریزد —db-initهم دیتابیسهایumamiوaramesh_chatbotرا روی همان نمونهٔ Postgres میسازد
نکته برای تازهواردها: هیچ فایل رمزدار در گیت نیست. فقط
.env.exampleرا کپی کنید؛ فایلهای.env/envدر.gitignoreهستند و هرگز commit نشوند.پایهٔ API فرانتاند در زمان بیلد از
VITE_API_URLدر.envخوانده میشود — در توسعه ویندوزیِ این ماشینVITE_API_URL=http://localhost:19001بگذارید.
انتشار واقعی (TLS)
پیشنیاز: رکوردهای DNS (example.com، analytics.example.com و اگر مانیتورینگ فعال
است grafana.example.com) به این سرور، سپس در .env: DOMAIN، ACME_EMAIL و
SITE_URL=https://example.com:
bash scripts/stack.sh deploy-prod
- Traefik گواهی Let's Encrypt میگیرد و تمدید میکند؛ همه درخواستهای HTTP به HTTPS هدایت میشوند
- پورتهای مدیریتی MinIO (۹۰۰۱ کنسول و ۱۹۰۰۰ API) در تولید فقط روی
127.0.0.1بسته میشوند — دسترسی از راه دور فقط با تونل SSH:ssh -L 9001:127.0.0.1:9001 server - همه روترهای websecure هدرهای امنیتی میگیرند (HSTS + nosniff)
پیشنمایش خصوصی (کار روی سایت، قبل از انتشار عمومی)
سایت روی اینترنت بالا میآید و تیم با همان پنل مدیریت محتوا را ویرایش میکند (ویرایش درجای ایمیل/تلفن/متنها و نوشتن مقاله)، اما نه عموم به آن دسترسی دارند و نه گوگل چیزی ایندکس میکند. دو لایه محافظت:
- گیت ورود Traefik (Basic Auth) — همه مسیرها (سایت، API، تصاویر و چت) پشت یک نام کاربری/رمز مشترکاند؛ بدون آن فقط ۴۰۱ برمیگردد
- noindex — هدر
X-Robots-Tagروی همه پاسخها +robots.txtمسدودکننده + نقشه سایت خالی؛ حتی اگر گیت برداشته شود، گوگل چیزی ذخیره نمیکند
bash scripts/preview-mode.sh on # فعالسازی (بار اول نام کاربری/رمز میسازد و چاپ میکند)
bash scripts/stack.sh deploy-prod # اعمال
bash scripts/preview-mode.sh status # وضعیت فعلی
- دامنه اصلی (
DOMAIN) و دامنه دوم (DOMAIN_ALIAS) هر دو سرو میشوند و هر دو گیت دارند - برای دادن دسترسی به کارفرما، همان نام کاربری/رمز مشترک را بدهید
- رمز مشترک در
.env(PREVIEW_AUTH_USERS) میماند — فقط یکبار چاپ میشود
انتشار عمومی (برداشتن پیشنمایش)
bash scripts/preview-mode.sh off # حذف گیت ورود + اجازه ایندکس
bash scripts/stack.sh deploy-prod
بعد از این مرحله:
- دامنه دوم بهصورت ۳۰۱ دائمی به دامنه اصلی ریدایرکت میشود (جلوگیری از محتوای تکراری)
- در Search Console دامنه را تأیید و
https://DOMAIN/sitemap.xmlرا ثبت کنید - اگر پیشتر چیزی ایندکس شده بود، حذفش را از همان کنسول درخواست کنید
چکلیست رفتن به تولید
bash scripts/gen-env.sh— رمزهای قوی جدید (رمزهای.envتوسعه را کپی نکنید)- DNS: رکوردهای دامنه و زیردامنهها + باز بودن پورتهای ۸۰/۴۴۳ در فایروال
DOMAIN،DOMAIN_ALIAS(دامنه دوم)،ACME_EMAIL،SITE_URLرا در.envتنظیم کنیدbash scripts/preview-mode.sh on— اولین انتشار بهصورت پیشنمایش خصوصی میرود؛ محتوا را روی سایت آنلاین نهایی کنید و بعدpreview-mode.sh off+ deploy مجددpython scripts/migrate-media.py— انتقال یکباره تصاویر CDN خارجی به MinIO- پشتیبانگیری خودکار:
docker compose --profile ops up -d db-backup(پشتیبان ساعتی)scripts/backup.shشبانه با cron/Task Scheduler
- یک تمرین بازگردانی:
bash scripts/restore.sh backups/db-<زمان>.sql.gzروی محیط آزمایشی - مانیتورینگ (بخش پایین) و سپس چتبات/آنالیتیکس را با متغیرهای LLM و Umami فعال کنید
مانیتورینگ (Prometheus + Grafana + Loki)
bash scripts/stack.sh monitoring
# در تولید (سه فایل با هم):
docker compose -f docker-compose.yml -f docker-compose.monitoring.yml -f docker-compose.prod.yml up -d
- داشبورد:
grafana.localhost(توسعه) یاgrafana.example.com(تولید — برچسبهای روتر آن را درdocker-compose.override.ymlسرور بگذارید؛ نمونه در انتهایdocker-compose.prod.yml) — کاربر/رمز ازGRAFANA_ADMIN_*در.env - دو داشبورد آمادهٔ «زیرساخت» و «اپلیکیشن» بهصورت code-provisioned سوار میشوند
- هشدارها به بله: در
.envمقدارALERT_BOT_TOKEN(بات بله — همان شکل API تلگرام، از BotFather بله) وALERT_CHAT_IDرا بگذارید (شناسه چت: به بات یک پیام بدهید سپسcurl "https://tapi.bale.ai/bot<توکن>/getUpdates") — قواعد آماده درmonitoring/prometheus/alerts.yml: پایینبودن هر سرویس، شکست بررسی عمق سلامت، پرشدن دیسک، نرخ خطای 5xx، انقضای گواهی TLS، کهنهشدن پشتیبان (سنجهٔbackup_success_timeازscripts/backup.sh) و... - لاگها در Grafana: Promtail لاگ همهٔ کانتینرهای استک را به Loki میفرستد — برای خطاهای بکاند از داشبورد «اپلیکیشن» پنل لاگ را ببینید
- سلامت واقعی:
/api/healthاتصال DB و MinIO را واقعاً میآزماید (503 در خرابی)؛ پروب blackbox هم همین را از بیرون و نشانیSITE_URLرا عمومی چک میکند - مصرف منابع مجموع لایه حدود ۱ تا ۱.۵ گیگابایت RAM است؛ حذف هر سرویس بقیه را نمیشکند
آنالیتیکس محصول (Umami)
استک شامل سرویس Umami است؛ داشبورد: analytics.localhost (توسعه) یا
analytics.example.com (تولید). بدون کوکی — بدون نیاز به بنر رضایت.
راهاندازی (یکبار):
- استک را بالا بیاورید و به
analytics.localhostوارد شوید (پیشفرضadmin/umami— بلافاصله رمز را عوض کنید) - یک وبسایت با دامنهٔ سایت بسازید؛ «شناسه وبسایت» را در
.envبگذارید:VITE_UMAMI_WEBSITE_ID=...و آدرس اسکریپت:VITE_UMAMI_SRC=https://analytics.example.com/script.js bash scripts/stack.sh deploy— اسکریپت ردیاب درindex.htmlتزریق میشود
رخدادهای سفارشی که خودکار ارسال میشوند: ارسال فرم تماس (contact-submit)، عضویت
خبرنامه (newsletter-signup)، کلیک تلفن (phone-click)، باز کردن چتبات
(chat-open)، پرسش از چتبات (chat-question)، تغییر زبان (lang-switch) —
سرچشمه: frontend/src/analytics.ts.
چتبات (دستیار هوشمند)
- RAG روی محتوای همان دیتابیس سایت: امبدینگ MiniLM چندزبانه + BM25 + رنکر — بردارها
در دیتابیس جدا
aramesh_chatbot(pgvector) روی همان نمونهٔ Postgres - مسیرها:
GET /chat/healthz،GET /chat/suggestions،GET /chat/stream(SSE) — رابط کاربری: ویجت شناور پایین صفحه (ChatWidget.tsx) - راهاندازی:
LLM_BASE_URL/LLM_API_KEY/LLM_MODELدر.env(هر LLM سازگار با OpenAI) — بدون آن سرویس بالا میآید اما پاسخ نمیدهد - اولین اجرا مدلها را دانلود میکند (~۵۰۰MB در حجم
chatbot_hf) — کند است، طبیعی است - ایندکس هر ۱۰ دقیقه خودش با تغییرهای محتوا همگام میشود؛ همگامسازی فوری با
POST /chat/reindexو هدرx-reindex-token(ازCHAT_REINDEX_TOKEN)
امکانات بکاند
- API عمومی: برند، منو، هیرو، خدمات، پروژهها، مقالات، نظرات، سوالات متداول، تیم، تایملاین، آمار، دفاتر، مدلهای سرمایهگذاری و... — همه در دیتابیس
- فرم تماس
POST /api/contactو خبرنامهPOST /api/newsletter— هر دو با محدودسازی نرخ (تماس: ۵ در ۱۵ دقیقه، خبرنامه: ۳ در ساعت برای هر IP) + دامنهٔ پنهان (honeypot) ضدربات + میانافزار محدودسازی سیلاب روی Traefik - پنل مدیریت با JWT: ورود
POST /api/auth/login(مقادیر ازADMIN_*در.env— هیچ رمز پیشفرضی در کد وجود ندارد) و سپس CRUD کامل همه محتواها در/api/admin/... - تغییر رمز
POST /api/auth/change-password— از نوار مدیریت پایین سایت (دکمه «رمز») هم در دسترس است - محدودسازی ورود: بعد از ۵ ورود ناموفق در ۱۵ دقیقه از یک IP، درخواست با 429 رد میشود
(IP واقعی کلاینت با
--proxy-headersاز پشت Traefik درست خوانده میشود) - آپلود تصویر
POST /api/admin/upload(فقط توکن ادمین، حداکثر ۱۰MB) — فایل در MinIO ذخیره و URL نسبی/media/...برمیگردد؛ در همان لحظه تا ۲۰۰۰ پیکسل کوچک و دوباره فشرده میشود (Pillow) تا سرعت صفحه حفظ شود - خروجی CSV از نوار مدیریت: پیامهای تماس و مشترکان خبرنامه (با BOM برای Excel)
- ویرایش جزئی
PATCH /api/admin/{resource}/{id}— فقط فیلدهای ارسالشده عوض میشوند - ترتیببندی
POST /api/admin/{resource}/reorder— با{"ids": [...]} - همه محتوا در یک درخواست:
GET /api/site(مقالههای پیشنویس در آن نمیآیند) - مهاجرتهای اسکیما: Alembic (
backend/migrations/) — هر تغییر اسکیمای جدید باید revision شود:cd backend && alembic revision --autogenerate -m "..."و پس از بازبینی commit؛ در استارت کانتینر خودکارupgrade headاجرا میشود - تستها:
cd backend && .venv/Scripts/activate && pytest tests(۲۴ تست — شامل honeypot، محدودسازی نرخ و پاکسازی)
در محیط واقعی حتماً
SECRET_KEY،ADMIN_PASSWORDوMINIO_ROOT_PASSWORDرا قوی بگیرید (bash scripts/gen-env.sh) و بعد از اولین ورود، رمز را از نوار مدیریت عوض کنید.
ویرایش زنده سایت (دوقلوی دیجیتال)
ادمینها نیازی به داشبورد جدا ندارند — خود سایت ویرایشگر است:
- به
/admin/loginبروید و وارد شوید - نوار شناور پایین صفحه ظاهر میشود؛ حالت ویرایش را روشن کنید
- روی هر متن کلیک کنید و ویرایش کنید — خروج از کادر، بلافاصله ذخیره میشود (Enter ذخیره، Esc لغو)؛ وضعیت ذخیره در نوار پایین دیده میشود
- آیتمهای مجموعهها (خدمات، پروژهها، مقالات، نظرات، تیم و...) با هاور، دکمههای ویرایش / حذف / جابهجایی ترتیب نشان میدهند؛ دکمه «افزودن» هم انتهای هر لیست هست
- تصویرها از همان کشوها به MinIO آپلود میشوند
- در حالت ویرایش، لینکها کلیک نمیشوند تا ویرایش متن ممکن باشد
- بازدیدکنندههای عادی هیچ اثری از این لایه نمیبینند (بدون توکن، همه درخواستهای
مدیریتی
401برمیگردند) - تمام تغییرها بلافاصله در PostgreSQL ثبت میشوند — با Ctrl+F5 هم میمانند
وبلاگ — نوشتن با ویرایشگر WYSIWYG
مقالات با ویرایشگر غنی TipTap نوشته میشوند (همان تجربه Medium):
- در حالت ویرایش، روی هر مقاله در صفحه وبلاگ دکمه ویرایش را بزنید — کشوی نوشتن باز میشود
- تیتر (H2/H3)، بولد/ایتالیک/زیرخط، لیست نقطهای و شمارهای، نقلقول، لینک و واگرد
- درج تصویر داخل متن: دکمه تصویر، یا چسباندن/کشیدن تصویر داخل نوشته — فایل مستقیم در MinIO ذخیره و در متن قرار میگیرد
- راستبهچپ کامل با فونت و تم سایت
- ذخیره خودکار: هر مکث کوتاه در تایپ، تغییرها را ذخیره میکند؛ بستن کشو هم ذخیره میکند
پیشنویس و انتشار: مقاله تازه همیشه پیشنویس شروع میشود — در سایت عمومی دیده
نمیشود و تا انتشار، تغییرهای خودکار آن چیزی را خراب نمیکند. با دکمه «انتشار در سایت»
عمومی میشود؛ دوباره میتوان آن را به پیشنویس برگرداند. پیشنویسها فقط برای ادمین
(در صفحه وبلاگ، با برچسب «پیشنویس») دیده میشوند و در GET /api/site هرگز نمیآیند.
فرمت ذخیره: بدنه مقاله یک سند ساختاریافته TipTap (JSON) در PostgreSQL است — نه HTML خام.
صفحه مقاله آن را با رندر استاتیک TipTap به HTML تبدیل میکند، پس فقط تگهای شناختهشده
(پاراگراف، تیتر، لیست، نقلقول، تصویر، لینک) تولید میشوند. مقالات قدیمی (لیست پاراگراف)
در اولین اجرا بهصورت خودکار به این فرمت تبدیل میشوند. در زمان ذخیره، آدرسهای
href/src سند در سمت سرور پاکسازی میشوند (app/sanitize.py) تا اسکیمهای خطرناک
مثل javascript: هرگز ذخیره نشوند.
سئو
- بررسی سئو در صفحه نوشتن: فهرست سبز/نارنجی/قرمز به سبک یوست ولی سازگار با فارسی —
طول عنوان، کلمه کلیدی هدف (در عنوان/پاراگراف اول/تیترها)، طول چکیده، تعداد واژه،
تیتربندی H2، متن جایگزین تصاویر، پیوندها و نشانی (slug) — کلمه کلیدی هدف در دیتابیس
ذخیره میشود (فیلد
seo_keyphraseمقاله) - سئوی هر صفحه: در حالت ویرایش، دکمه سئو روی هیرو هر صفحه (و کنار هیرو صفحه اصلی) — عنوان تب و توضیح متای هر صفحه از دیتابیس خوانده و اعمال میشود؛ اگر ردیف سئوی صفحهای هنوز ساخته نشده باشد، دکمه + همانجا آن را میسازد
- صفحه مقاله و پروژه بهطور خودکار عنوان و توضیح متا را از محتوای خودشان میگیرند
- تگهای OG و توییتر و canonical برای همه صفحات از
src/seo.tsست میشود و مقالهها اسکیمای JSON-LD نوع Article میگیرند - robots.txt و sitemap.xml از بکاند سرو میشوند و مقاله/پروژههای منتشرشده را
شامل میشوند (پیشنویسها نمیآیند) — برای آدرس مطلق،
SITE_URLرا در.envبگذارید - متا برای خزندههای بدون جاوااسکریپت (تلگرام، واتساپ و...): بکاند صفحات
/blog،/blog/{slug}،/projectsو/projects/{slug}را با همانindex.htmlفرانتاند سرو میکند و پیش از ارسال، title / description / OG / JSON-LD را از دیتابیس داخل آن تزریق میکند (app/seo_pages.py) — بازدیدکنندههای عادی همان SPA همیشگی را میبینند
پشتیبانگیری و بازگردانی
همه محتوای سایت در PostgreSQL و تصاویر در MinIO است:
bash scripts/stack.sh backup # پشتیبان کامل: DB + تصاویر MinIO
docker compose --profile ops up -d db-backup # پشتیبان ساعتی خودکار DB (بدون cron میزبان)
- خروجی:
backups/db-<زمان>.sql.gz(فشرده + فایل.sha256) وbackups/images-<زمان>/ - اسکریپت پشتیبان، خروجی خالی/شکسته را خطا میگیرد، سنجهٔ تازگی برای هشدار Grafana
مینویسد و اگر
OFFSITE_RCLONE_REMOTEدر.envتنظیم باشد، پشتیبان را به حافظهٔ دیگری هم کپی میکند (rclone) - بازگردانی:
bash scripts/stack.sh restore backups/db-<زمان>.sql.gz(احتیاط: محتوای فعلی جایگزین میشود — قبل از انتشار واقعی یکبار تمرین کنید!) - زمانبندی پشتیبان کامل با cron یا Task Scheduler: نمونه دستور داخل خود اسکریپت هست
تست و CI
bash scripts/ci.sh— تستهای pytest + بیلد ایمیجها + ممیزی وابستگیهاbash scripts/audit.sh— pip-audit / npm audit / trivy (هر کدام نصب بود).github/workflows/ci.yml— همین مراحل؛ سینتکس GitHub Actions روی Gitea Actions/ Forgejo هم اجرا میشود
نکات امنیتی
- مقادیر محرمانه در
.envریشه هستند (در.gitignore) و در compose با سینتکس${VAR:?}اجباری شدهاند — بدون.envاستک بالا نمیآید؛ هیچ رمز پیشفرضی در کد نیست - رمز عبور ادمین با bcrypt هش میشود و از نوار مدیریت پایین سایت (دکمه «رمز») قابل تعویض است
- توکنهای JWT ۸ ساعت اعتبار دارند (stateless — تغییر
SECRET_KEYهمه را خارج میکند) - همهٔ کانتینرها بدون root اجرا میشوند؛ کانتکست بیلد هر ایمیج با
.dockerignoreباریک است و لاگها سقف دارند - فرمهای عمومی ضدرباتاند (honeypot + محدودسازی نرخ در بکاند + سقف سیلاب در Traefik)
- کنسول MinIO و API مستقیمش در تولید فقط از
127.0.0.1در دسترساند (تونل SSH) - تصاویر باکت MinIO عمومی (فقط خواندن) هستند؛ آپلود فقط با توکن ادمین ممکن است
- مقالههای پیشنویس در هیچ انتهای عمومی نمیآیند و در HTML تزریقشده سئو هم افشا نمیشوند