feat(stack): full overhaul — hardening, ops tooling, monitoring, analytics, chatbot

Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live;
rate-limited public forms with honeypots; real client IP behind Traefik
(--proxy-headers); TipTap URL sanitization on write (nh3); non-root
containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1;
HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code.

Reliability: Alembic migrations (create_all baseline); graceful shutdown
(exec uvicorn); hardened backups (checksums, sanity check, freshness gauge,
optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops
profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring);
.gitattributes LF for shell scripts.

Orchestration: json-file log rotation, pinned images, per-service memory
limits, edge/internal networks, Traefik ping/accesslog/metrics + flood
limiter, conditional minio-setup, pgvector db image, one-shot db-init
(umami + aramesh_chatbot databases).

Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget).

Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor,
node/postgres/blackbox exporters, provisioned dashboards and 10 alert
rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible).

Analytics: Umami on shared Postgres, build-time tracker injection, custom
events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts.

Hygiene: CSV export for contact messages and subscribers; upload image
optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh;
gen-env.sh for strong credentials.
This commit is contained in:
MMD
2026-09-19 00:24:57 +03:30
parent 805f4594d4
commit ce0735d775
75 changed files with 4287 additions and 153 deletions
+16 -2
View File
@@ -9,12 +9,13 @@ import { useFieldSaver } from '../admin/AdminContext'
import { InlineEdit } from './admin/InlineEdit'
import { SingletonPill } from '../admin/Editable'
import { API_URL } from '../config'
import { track } from '../analytics'
export function ContactSection() {
const { brand, contact_section: content } = useSite()
const { t } = useLang()
const save = useFieldSaver()
const [form, setForm] = useState({ name: '', phone: '', note: '' })
const [form, setForm] = useState({ name: '', phone: '', note: '', website: '' })
const [sent, setSent] = useState(false)
const [sending, setSending] = useState(false)
@@ -79,7 +80,7 @@ export function ContactSection() {
const res = await fetch(`${API_URL}/api/contact`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: nameValue, phone: phoneValue, note: form.note.trim() }),
body: JSON.stringify({ name: nameValue, phone: phoneValue, note: form.note.trim(), website: form.website }),
})
if (!res.ok) {
@@ -90,6 +91,7 @@ export function ContactSection() {
return
}
track('contact-submit')
setSent(true)
} catch {
showErrorAlert(t('connectionErrorTitle'), t('connectionErrorText'))
@@ -152,6 +154,7 @@ export function ContactSection() {
<a
href={brand.phone_href}
dir="ltr"
onClick={() => track('phone-click', { from: 'contact-section' })}
className="mt-1 block text-sm font-light text-ivory/70 transition-colors hover:text-copper"
>
<InlineEdit value={brand.phone} onSave={(v) => saveBrand('phone', v)} />
@@ -208,6 +211,17 @@ export function ContactSection() {
</div>
) : (
<form onSubmit={submit} noValidate className="grid gap-6 md:grid-cols-2">
{/* honeypot — خارج از دید؛ فقط ربات‌ها آن را پر می‌کنند */}
<input
type="text"
name="website"
value={form.website}
onChange={(e) => setForm({ ...form, website: e.target.value })}
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
className="pointer-events-none absolute -left-[9999px] h-0 w-0 opacity-0"
/>
<div>
<label htmlFor="contact-name" className="block text-sm font-medium text-ivory/90">
<InlineEdit value={content.name_label} onSave={(v) => saveContent('name_label', v)} />