diff --git a/README.md b/README.md
index 1990223..6e4d0ae 100644
--- a/README.md
+++ b/README.md
@@ -1,54 +1,81 @@
-# AutoMarket (OzonGuard)
+# π AutoMarket β Ultimate Marketplace SaaS Platform
-**AutoMarket** β ΡΡΠΎ ΡΠ½ΠΈΠ²Π΅ΡΡΠ°Π»ΡΠ½ΠΎΠ΅ Π²Π΅Π±-ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Π΄Π»Ρ ΠΈΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΠΈ, ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ ΠΈ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΠ·Π°ΡΠΈΠΈ ΠΏΡΠΎΡΠ΅ΡΡΠΎΠ² ΡΡΠ°Π·Ρ Π½Π° 10 ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°Ρ
. Π‘ΠΈΡΡΠ΅ΠΌΠ° ΠΏΠΎΠ·Π²ΠΎΠ»ΡΠ΅Ρ Ρ
ΡΠ°Π½ΠΈΡΡ ΠΊΠ»ΡΡΠΈ API, Π±ΡΡΡΡΠΎ ΠΏΠΎΠ΄Π³ΠΎΡΠ°Π²Π»ΠΈΠ²Π°ΡΡ ΠΆΠ°Π»ΠΎΠ±Ρ Π½Π° Π½Π°ΡΡΡΠΈΡΠ΅Π»Π΅ΠΉ ΠΈ Π³ΠΈΠ±ΠΊΠΎ ΡΠ°ΡΡΠΈΡΡΡΡ ΡΡΠ½ΠΊΡΠΈΠΎΠ½Π°Π».
+**AutoMarket** β ΡΡΠΎ ΠΊΠΎΠΌΠΏΠ»Π΅ΠΊΡΠ½Π°Ρ ΡΠΊΠΎΡΠΈΡΡΠ΅ΠΌΠ° Π΄Π»Ρ ΠΏΡΠΎΡΠ΅ΡΡΠΈΠΎΠ½Π°Π»ΡΠ½ΡΡ
ΠΏΡΠΎΠ΄Π°Π²ΡΠΎΠ² Π½Π° ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°Ρ
(Ozon, Wildberries ΠΈ Π΄Ρ.), ΠΎΠ±ΡΠ΅Π΄ΠΈΠ½ΡΡΡΠ°Ρ Π² ΡΠ΅Π±Π΅ ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½ΡΡ Π·Π°ΡΠΈΡΡ ΠΊΠΎΠ½ΡΠ΅Π½ΡΠ°, ΡΠΈΠ½Π°Π½ΡΠΎΠ²ΠΎΠΉ Π°Π½Π°Π»ΠΈΡΠΈΠΊΠΈ, SEO-ΠΌΠΎΠ½ΠΈΡΠΎΡΠΈΠ½Π³Π° ΠΈ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΠ·Π°ΡΠΈΠΈ ΠΊΠ»ΠΈΠ΅Π½ΡΡΠΊΠΎΠ³ΠΎ ΡΠ΅ΡΠ²ΠΈΡΠ° Ρ ΠΏΠΎΠΌΠΎΡΡΡ ΠΈΡΠΊΡΡΡΡΠ²Π΅Π½Π½ΠΎΠ³ΠΎ ΠΈΠ½ΡΠ΅Π»Π»Π΅ΠΊΡΠ°.
-## ΠΠΎΠ΄Π΄Π΅ΡΠΆΠΈΠ²Π°Π΅ΠΌΡΠ΅ ΠΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΡ
-1. **Ozon**
-2. **Wildberries**
-3. **Yandex Market**
-4. **Magnit Market**
-5. **MegaMarket**
-6. **AliExpress (AE Platform)**
-7. **Avito**
-8. **Lamoda**
-9. **Lemana PRO (ΠΠ΅ΡΡΠ° ΠΠ΅ΡΠ»Π΅Π½)**
-10. **Flowwow**
+---
-## ΠΡΠ½ΠΎΠ²Π½ΠΎΠΉ ΡΡΠ½ΠΊΡΠΈΠΎΠ½Π°Π»
+## π ΠΠ»ΡΡΠ΅Π²ΡΠ΅ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ
-- **ΠΠ°ΡΡΡΠΎΠΉΠΊΠΈ ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΠΉ:** ΠΠ΄ΠΈΠ½ΡΠΉ ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ Π΄Π»Ρ Π²Π²ΠΎΠ΄Π° ΠΈ Ρ
ΡΠ°Π½Π΅Π½ΠΈΡ API ΠΊΠ»ΡΡΠ΅ΠΉ Π²ΡΠ΅Ρ
10 ΠΏΠ»ΠΎΡΠ°Π΄ΠΎΠΊ. ΠΠΎΠ»ΡΡΠ΅ Π½Π΅ Π½ΡΠΆΠ½ΠΎ ΠΊΠΎΠΏΠ°ΡΡΡΡ Π² .env ΡΠ°ΠΉΠ»Π°Ρ
.
-- **Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΠΊΠ΅ΠΉΡΠ°ΠΌΠΈ (ΠΆΠ°Π»ΠΎΠ±Π°ΠΌΠΈ):** Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΡΠ½ΠΈΠ²Π΅ΡΡΠ°Π»ΡΠ½ΡΡ
ΠΆΠ°Π»ΠΎΠ± (ΠΊΠ΅ΠΉΡΠΎΠ²) Ρ ΡΠΊΠ°Π·Π°Π½ΠΈΠ΅ΠΌ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°, ΡΡΡΠ»ΠΎΠΊ Π½Π° ΠΊΠ°ΡΡΠΎΡΠΊΡ Π½Π°ΡΡΡΠΈΡΠ΅Π»Ρ ΠΈ Π΅Π³ΠΎ Π°ΡΡΠΈΠΊΡΠ»Π°.
-- **ΠΡΠΈΠΊΡΠ΅ΠΏΠ»Π΅Π½ΠΈΠ΅ Π΄ΠΎΠΊΠ°Π·Π°ΡΠ΅Π»ΡΡΡΠ²:** ΠΠ°Π³ΡΡΠ·ΠΊΠ° ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠΉ ΠΈ PDF Π² ΠΊΠ°ΡΠ΅ΡΡΠ²Π΅ Π΄ΠΎΠΊΠ°Π·Π°ΡΠ΅Π»ΡΠ½ΠΎΠΉ Π±Π°Π·Ρ.
-- **Π‘ΡΠ°ΡΡΡΡ ΠΊΠ΅ΠΉΡΠΎΠ²:** ΠΡΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΡΡΠ°ΡΡΡΠΎΠ²: *ΠΠΎΠ²ΡΠΉ*, *ΠΠΎΠ΄Π³ΠΎΡΠΎΠ²Π»Π΅Π½*, *ΠΡΠΏΡΠ°Π²Π»Π΅Π½*, *ΠΠ°ΠΊΡΡΡ*.
-- **Π£ΠΌΠ½ΡΠΉ ΠΏΠΎΠΈΡΠΊ:** ΠΠΎΠΈΡΠΊ ΠΏΠΎ Π°ΡΡΠΈΠΊΡΠ»Ρ ΠΈ ΡΠΈΠ»ΡΡΡΠ°ΡΠΈΡ ΠΏΠΎ ΠΏΠ»ΠΎΡΠ°Π΄ΠΊΠ΅ ΠΈΠ»ΠΈ ΡΡΠ°ΡΡΡΡ.
+### 1. π€ AI Assistant (ΠΠ½ΡΠ΅Π³ΡΠ°ΡΠΈΡ Ρ Google Gemini)
+* **ΠΠ²ΡΠΎ-ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ:** ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΡΡΠΈΠ΄ΠΈΡΠ΅ΡΠΊΠΈ Π³ΡΠ°ΠΌΠΎΡΠ½ΡΡ
ΠΆΠ°Π»ΠΎΠ± Π½Π° Π½Π°ΡΡΡΠΈΡΠ΅Π»Π΅ΠΉ Π½Π° ΠΎΡΠ½ΠΎΠ²Π΅ Π°ΡΡΠΈΠΊΡΠ»Π° ΠΈ ΠΏΠ»ΠΎΡΠ°Π΄ΠΊΠΈ.
+* **Π£ΠΌΠ½ΡΠΉ ΠΎΡΠ²Π΅ΡΡΠΈΠΊ:** ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠ°Ρ Π³Π΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΏΠ΅ΡΡΠΎΠ½Π°Π»ΠΈΠ·ΠΈΡΠΎΠ²Π°Π½Π½ΡΡ
ΠΎΡΠ²Π΅ΡΠΎΠ² Π½Π° ΠΎΡΠ·ΡΠ²Ρ ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Π΅ΠΉ (ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ° ΠΏΠΎΠ·ΠΈΡΠΈΠ²Π½ΠΎΠ³ΠΎ ΠΈ ΠΎΡΡΠ°Π±ΠΎΡΠΊΠ° Π½Π΅Π³Π°ΡΠΈΠ²Π½ΠΎΠ³ΠΎ ΡΡΠ΅Π½Π°ΡΠΈΡ).
-## Π’Π΅Ρ
Π½ΠΎΠ»ΠΎΠ³ΠΈΡΠ΅ΡΠΊΠΈΠΉ ΡΡΠ΅ΠΊ
+### 2. π Multi-account Dashboard
+* ΠΠ΄ΠΈΠ½ΠΎΠ΅ ΠΎΠΊΠ½ΠΎ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ Π΄Π»Ρ Π½Π΅ΠΎΠ³ΡΠ°Π½ΠΈΡΠ΅Π½Π½ΠΎΠ³ΠΎ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²Π° ΠΌΠ°Π³Π°Π·ΠΈΠ½ΠΎΠ² ΠΈ ΡΡΠΈΠ΄ΠΈΡΠ΅ΡΠΊΠΈΡ
Π»ΠΈΡ.
+* ΠΠ³ΡΠ΅Π³ΠΈΡΠΎΠ²Π°Π½Π½Π°Ρ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΡΠΎΠ΄Π°ΠΆ, Π²ΡΡΡΡΠΊΠΈ ΠΈ ΡΠΎΡΡΠΎΡΠ½ΠΈΡ API-ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΠΉ Π² ΡΠ΅Π°Π»ΡΠ½ΠΎΠΌ Π²ΡΠ΅ΠΌΠ΅Π½ΠΈ.
-- **Frontend:** HTML5, CSS3, Vanilla JavaScript (Ρ ΠΊΡΠ°ΡΠΈΠ²ΡΠΌ Π΄ΠΈΠ·Π°ΠΉΠ½ΠΎΠΌ Glassmorphism).
-- **Backend:** Python 3.9+, Flask.
-- **ΠΠ°Π·Π° Π΄Π°Π½Π½ΡΡ
:** PostgreSQL.
-- **SDK:** 10 Π½Π΅Π·Π°Π²ΠΈΡΠΈΠΌΡΡ
, ΠΎΡΠΊΠ°Π·ΠΎΡΡΡΠΎΠΉΡΠΈΠ²ΡΡ
Python Π±ΠΈΠ±Π»ΠΈΠΎΡΠ΅ΠΊ Π΄Π»Ρ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ° (Π²ΠΊΠ»ΡΡΠ°Ρ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠ΅ ΡΠ΅ΡΡΠ°ΠΈ, OAuth, JWT ΠΈ Exponential Backoff).
+### 3. π SEO ΠΠΎΠ½ΠΈΡΠΎΡΠΈΠ½Π³ ΠΏΠΎΠ·ΠΈΡΠΈΠΉ
+* ΠΡΡΠ»Π΅ΠΆΠΈΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎΠ·ΠΈΡΠΈΠΉ ΡΠΎΠ²Π°ΡΠΎΠ² ΠΏΠΎ ΠΊΠ»ΡΡΠ΅Π²ΡΠΌ ΡΠ»ΠΎΠ²Π°ΠΌ Π² ΠΏΠΎΠΈΡΠΊΠ΅ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠΎΠ².
+* ΠΠ½ΡΠ΅ΡΠ°ΠΊΡΠΈΠ²Π½ΡΠ΅ Π³ΡΠ°ΡΠΈΠΊΠΈ Π΄ΠΈΠ½Π°ΠΌΠΈΠΊΠΈ ΠΏΠΎΠ·ΠΈΡΠΈΠΉ Ρ ΠΈΠ½Π²Π΅ΡΡΠΈΡΠΎΠ²Π°Π½Π½ΠΎΠΉ ΡΠΊΠ°Π»ΠΎΠΉ (Π’ΠΠ-1 Π²ΡΠ΅Π³Π΄Π° ΡΠ²Π΅ΡΡ
Ρ).
-## Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΠΈ Π·Π°ΠΏΡΡΠΊ (Linux/macOS)
+### 4. π‘οΈ ΠΠ°ΡΠΈΡΠ° ΠΊΠΎΠ½ΡΠ΅Π½ΡΠ° (Image Matching)
+* ΠΡΡΠΈΡΠ»Π΅Π½ΠΈΠ΅ Π²ΠΈΠ·ΡΠ°Π»ΡΠ½ΡΡ
ΠΎΡΠΏΠ΅ΡΠ°ΡΠΊΠΎΠ² (pHash) Π΄Π»Ρ Π²Π°ΡΠΈΡ
ΡΠΎΡΠΎΠ³ΡΠ°ΡΠΈΠΉ.
+* ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠΉ ΠΏΠΎΠΈΡΠΊ Π²ΠΈΠ·ΡΠ°Π»ΡΠ½ΡΡ
ΠΊΠ»ΠΎΠ½ΠΎΠ² ΠΈ Π΄ΡΠ±Π»ΠΈΠΊΠ°ΡΠΎΠ² Π²Π°ΡΠ΅Π³ΠΎ ΠΊΠΎΠ½ΡΠ΅Π½ΡΠ° Ρ ΠΊΠΎΠ½ΠΊΡΡΠ΅Π½ΡΠΎΠ².
+* ΠΠ΅ΡΡΠΎΠ²Π½Π°Ρ ΠΏΠΎΠ΄Π°ΡΠ° ΠΆΠ°Π»ΠΎΠ±Ρ Π½Π° Π½Π°ΠΉΠ΄Π΅Π½Π½ΠΎΠ³ΠΎ Π½Π°ΡΡΡΠΈΡΠ΅Π»Ρ Π² ΠΎΠ΄ΠΈΠ½ ΠΊΠ»ΠΈΠΊ.
-Π ΠΏΡΠΎΠ΅ΠΊΡΠ΅ ΠΏΡΠ΅Π΄ΡΡΠΌΠΎΡΡΠ΅Π½ bash-ΡΠΊΡΠΈΠΏΡ `init.sh` Π΄Π»Ρ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΎΠΉ Π½Π°ΡΡΡΠΎΠΉΠΊΠΈ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΡ.
+### 5. π° Π€ΠΈΠ½Π°Π½ΡΠΎΠ²ΡΠΉ ΠΊΠ°Π»ΡΠΊΡΠ»ΡΡΠΎΡ (Unit Economics)
+* ΠΠ΅ΡΠ°Π»ΡΠ½ΡΠΉ ΡΠ°ΡΡΠ΅Ρ ΡΠΈΡΡΠΎΠΉ ΠΏΡΠΈΠ±ΡΠ»ΠΈ, ΠΌΠ°ΡΠΆΠΈΠ½Π°Π»ΡΠ½ΠΎΡΡΠΈ ΠΈ ROI.
+* Π£ΡΠ΅Ρ ΠΊΠΎΠΌΠΈΡΡΠΈΠΉ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°, Π½Π°Π»ΠΎΠ³ΠΎΠ² (Π£Π‘Π), Π»ΠΎΠ³ΠΈΡΡΠΈΠΊΠΈ ΠΈ ΡΠ΅ΠΊΠ»Π°ΠΌΠ½ΡΡ
ΡΠ°ΡΡ
ΠΎΠ΄ΠΎΠ² (ΠΠ Π ).
-1. Π£Π±Π΅Π΄ΠΈΡΠ΅ΡΡ, ΡΡΠΎ Ρ Π²Π°Ρ ΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½Ρ Python 3.9+ ΠΈ PostgreSQL (`psql`).
-2. ΠΠ°ΡΡΡΠΎΠΉΡΠ΅ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠ΅Π½ΠΈΠ΅ ΠΊ ΠΠ Π² ΡΠ°ΠΉΠ»Π΅ `backend/.env` (ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½Π°Ρ `DATABASE_URL=postgresql://user:password@localhost:5432/dbname`).
-3. ΠΡΠΏΠΎΠ»Π½ΠΈΡΠ΅ ΡΠΊΡΠΈΠΏΡ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ ΠΈΠ· ΠΊΠΎΡΠ½Π΅Π²ΠΎΠΉ Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΠΈ:
- ```bash
- bash init.sh
- ```
+### 6. π ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΎΡΡΠ΅ΡΠΎΠ²
+* ΠΠΊΡΠΏΠΎΡΡ Π΄Π°Π½Π½ΡΡ
Π² CSV.
+* ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΏΡΠΎΡΠ΅ΡΡΠΈΠΎΠ½Π°Π»ΡΠ½ΡΡ
PDF-ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΉ Ρ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠΎΠΉ ΠΊΠΈΡΠΈΠ»Π»ΠΈΡΡ Π΄Π»Ρ ΠΎΡΠΏΡΠ°Π²ΠΊΠΈ Π² ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΡ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°.
-**Π§ΡΠΎ Π΄Π΅Π»Π°Π΅Ρ ΡΠΊΡΠΈΠΏΡ `init.sh`:**
-- ΠΡΠΎΠ²Π΅ΡΡΠ΅Ρ Π½Π°Π»ΠΈΡΠΈΠ΅ Python 3 ΠΈ ΡΠΎΠ·Π΄Π°Π΅Ρ Π²ΠΈΡΡΡΠ°Π»ΡΠ½ΠΎΠ΅ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΠ΅ `venv`.
-- Π£ΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅Ρ Python-Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠΈ ΠΈΠ· ΡΠ°ΠΉΠ»Π° `backend/requirements.txt`.
-- ΠΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·ΠΈΡΡΠ΅Ρ Π±Π°Π·Ρ Π΄Π°Π½Π½ΡΡ
Ρ ΠΏΠΎΠΌΠΎΡΡΡ ΡΠ°ΠΉΠ»Π° `db/init.sql` (Π΅ΡΠ»ΠΈ Π½Π°ΡΡΡΠΎΠ΅Π½ `.env`).
-- ΠΠ°ΠΏΡΡΠΊΠ°Π΅Ρ Π»ΠΎΠΊΠ°Π»ΡΠ½ΡΠΉ Flask-ΡΠ΅ΡΠ²Π΅Ρ Π½Π° ΠΏΠΎΡΡΡ `5000`.
+---
-## Π‘ΡΡΡΠΊΡΡΡΠ° ΡΠ΅ΠΏΠΎΠ·ΠΈΡΠΎΡΠΈΡ
+## π¨ ΠΠΈΠ·Π°ΠΉΠ½ ΠΈ ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ
+* **Premium Dark Mode:** ΠΠ»Π΅Π³Π°Π½ΡΠ½Π°Ρ ΡΠ΅ΠΌΠ½Π°Ρ ΡΠ΅ΠΌΠ° Π΄Π»Ρ ΠΊΠΎΠΌΡΠΎΡΡΠ½ΠΎΠΉ ΡΠ°Π±ΠΎΡΡ.
+* **Glassmorphism:** Π‘ΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΡΠΉ "ΡΡΠ΅ΠΊΠ»ΡΠ½Π½ΡΠΉ" ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ Ρ ΡΡΡΠ΅ΠΊΡΠ°ΠΌΠΈ ΡΠ°Π·ΠΌΡΡΠΈΡ ΠΈ Π½Π΅ΠΎΠ½ΠΎΠ²ΡΠΌΠΈ Π°ΠΊΡΠ΅Π½ΡΠ°ΠΌΠΈ.
+* **Real-time Notifications:** ΠΠ³Π½ΠΎΠ²Π΅Π½Π½ΡΠ΅ ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΡ ΡΠ΅ΡΠ΅Π· WebSockets (Socket.io) ΠΏΡΠΈ ΠΎΠ±Π½Π°ΡΡΠΆΠ΅Π½ΠΈΠΈ Π½ΠΎΠ²ΡΡ
ΡΠΎΠ±ΡΡΠΈΠΉ.
+* **Lucide Icons:** ΠΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ ΡΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΠΎΠΉ Π±ΠΈΠ±Π»ΠΈΠΎΡΠ΅ΠΊΠΈ ΠΈΠΊΠΎΠ½ΠΎΠΊ Π΄Π»Ρ ΠΈΠ½ΡΡΠΈΡΠΈΠ²Π½ΠΎΠΉ Π½Π°Π²ΠΈΠ³Π°ΡΠΈΠΈ.
-- `/frontend/` β ΡΡΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠ΅ ΡΠ°ΠΉΠ»Ρ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΡΠΊΠΎΠ³ΠΎ ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡΠ° (ΡΡΡΠ°Π½ΠΈΡΡ, ΡΡΠΈΠ»ΠΈ, Π»ΠΎΠ³ΠΈΠΊΠ° Π½Π° JS).
-- `/backend/` β ΡΠ΅ΡΠ²Π΅ΡΠ½Π°Ρ ΡΠ°ΡΡΡ Π½Π° Flask (ΡΠΎΡΠΊΠ° Π²Ρ
ΠΎΠ΄Π° `app.py`, ΠΎΠ±ΡΠ°Π±ΠΎΡΠΊΠ° ΠΌΠ°ΡΡΡΡΡΠΎΠ² API Π² `routes/`).
-- `/db/` β SQL-ΡΠΊΡΠΈΠΏΡΡ Π΄Π»Ρ ΠΈΠ½ΠΈΡΠΈΠ°Π»ΠΈΠ·Π°ΡΠΈΠΈ ΡΠ°Π±Π»ΠΈΡ Π±Π°Π·Ρ Π΄Π°Π½Π½ΡΡ
.
-- `init.sh` β ΡΡΠ°ΡΡΠΎΠ²ΡΠΉ ΡΠΊΡΠΈΠΏΡ Π΄Π»Ρ Π±ΡΡΡΡΠΎΠ³ΠΎ ΡΠ°Π·Π²Π΅ΡΡΡΠ²Π°Π½ΠΈΡ ΠΏΡΠΎΠ΅ΠΊΡΠ°.
+---
+
+## π Π’Π΅Ρ
Π½ΠΎΠ»ΠΎΠ³ΠΈΡΠ΅ΡΠΊΠΈΠΉ ΡΡΠ΅ΠΊ
+* **Backend:** Python (Flask), Flask-SocketIO (Real-time), Flasgger (Swagger/OpenAPI).
+* **Frontend:** Vanilla JS, Chart.js (ΠΡΠ°ΡΠΈΠΊΠΈ), Lucide (ΠΠΊΠΎΠ½ΠΊΠΈ).
+* **Database:** PostgreSQL.
+* **AI:** Google Gemini 1.5 Flash API.
+* **DevOps:** Docker, Docker Compose.
+
+---
+
+## π ΠΡΡΡΡΡΠΉ Π·Π°ΠΏΡΡΠΊ
+
+### 1. Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ
+* Π£ΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½Π½ΡΠΉ Docker ΠΈ Docker Compose.
+* API ΠΊΠ»ΡΡ Google AI Studio (Π΄Π»Ρ ΡΠ°Π±ΠΎΡΡ ΠΠ).
+
+### 2. ΠΠ°ΡΡΡΠΎΠΉΠΊΠ° ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΡ
+Π‘ΠΎΠ·Π΄Π°ΠΉΡΠ΅ ΡΠ°ΠΉΠ» `backend/.env` ΠΈ Π·Π°ΠΏΠΎΠ»Π½ΠΈΡΠ΅ Π΅Π³ΠΎ:
+```env
+DATABASE_URL=postgresql://user:pass@host.docker.internal:5432/automarket
+JWT_SECRET=your_super_secret_key
+TELEGRAM_BOT_TOKEN=your_bot_token
+TELEGRAM_CHAT_ID=your_chat_id
+GEMINI_API_KEY=your_gemini_api_key
+```
+
+### 3. ΠΠ°ΠΏΡΡΠΊ ΡΠ΅ΡΠ΅Π· Docker
+```bash
+docker-compose up --build
+```
+ΠΠΎΡΠ»Π΅ Π·Π°ΠΏΡΡΠΊΠ°:
+* Frontend: `http://localhost:8080`
+* Backend API: `http://localhost:5000`
+* Swagger Docs: `http://localhost:5000/api/docs`
+
+---
+
+## π ΠΡΠ΄ΠΈΡ ΠΈ ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ
+ΠΡΠΎΠ΅ΠΊΡ Π²ΠΊΠ»ΡΡΠ°Π΅Ρ Π²ΡΡΡΠΎΠ΅Π½Π½ΡΡ ΡΠΈΡΡΠ΅ΠΌΡ **Audit Log**, ΠΊΠΎΡΠΎΡΠ°Ρ ΡΠΈΠΊΡΠΈΡΡΠ΅Ρ Π²ΡΠ΅ Π²Π°ΠΆΠ½ΡΠ΅ Π΄Π΅ΠΉΡΡΠ²ΠΈΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ (Π²Ρ
ΠΎΠ΄, ΡΠΎΠ·Π΄Π°Π½ΠΈΠ΅/ΡΠ΄Π°Π»Π΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ΅ΠΊΡΠΎΠ², ΡΠΈΠ½Π°Π½ΡΠΎΠ²ΡΠ΅ ΡΠ°ΡΡΠ΅ΡΡ) Π΄Π»Ρ ΠΎΠ±Π΅ΡΠΏΠ΅ΡΠ΅Π½ΠΈΡ ΠΏΠΎΠ»Π½ΠΎΠΉ ΠΏΡΠΎΠ·ΡΠ°ΡΠ½ΠΎΡΡΠΈ ΠΈ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΠΈ Π±ΠΈΠ·Π½Π΅ΡΠ°.
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..3c29f10
--- /dev/null
+++ b/backend/Dockerfile
@@ -0,0 +1,28 @@
+# backend/Dockerfile
+FROM python:3.9-slim
+
+WORKDIR /app
+
+# Π£ΡΡΠ°Π½ΠΎΠ²ΠΊΠ° ΡΠΈΡΡΠ΅ΠΌΠ½ΡΡ
Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠ΅ΠΉ Π΄Π»Ρ psycopg2 ΠΈ Π΄Ρ.
+RUN apt-get update && apt-get install -y \
+ libpq-dev \
+ gcc \
+ && rm -rf /var/lib/apt/lists/*
+
+# ΠΠΎΠΏΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΈ ΡΡΡΠ°Π½ΠΎΠ²ΠΊΠ° Π·Π°Π²ΠΈΡΠΈΠΌΠΎΡΡΠ΅ΠΉ
+COPY backend/requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# ΠΠΎΠΏΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΊΠΎΠ΄Π° Π±ΡΠΊΠ΅Π½Π΄Π°
+COPY backend/ ./backend/
+
+# Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ __init__.py Π΅ΡΠ»ΠΈ ΠΈΡ
Π½Π΅Ρ (Π΄Π»Ρ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΠΎΠΉ ΡΠ°Π±ΠΎΡΡ ΠΎΡΠ½ΠΎΡΠΈΡΠ΅Π»ΡΠ½ΡΡ
ΠΈΠΌΠΏΠΎΡΡΠΎΠ²)
+RUN touch backend/__init__.py backend/routes/__init__.py
+
+# ΠΠ΅ΡΠ΅ΠΌΠ΅Π½Π½Π°Ρ ΠΎΠΊΡΡΠΆΠ΅Π½ΠΈΡ Π΄Π»Ρ ΠΊΠΎΡΡΠ΅ΠΊΡΠ½ΡΡ
ΠΈΠΌΠΏΠΎΡΡΠΎΠ²
+ENV PYTHONPATH=/app
+
+EXPOSE 5000
+
+# ΠΠ°ΠΏΡΡΠΊ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΡ
+CMD ["python", "backend/app.py"]
diff --git a/backend/app.py b/backend/app.py
index 0ffff38..0fb57de 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -2,30 +2,75 @@ import os
from flask import Flask
from flask_cors import CORS
from dotenv import load_dotenv
+from flasgger import Swagger
+from flask_socketio import SocketIO, emit
# Load environment variables from .env
load_dotenv()
app = Flask(__name__)
CORS(app)
+socketio = SocketIO(app, cors_allowed_origins="*")
+
+# Swagger setup
+# ... (keeping existing swagger config)
+app.config['SWAGGER'] = {
+ 'title': 'AutoMarket API',
+ 'uiversion': 3,
+ 'specs_route': '/api/docs/',
+ 'securityDefinitions': {
+ 'APIKeyHeader': {
+ 'type': 'apiKey',
+ 'name': 'Authorization',
+ 'in': 'header',
+ 'description': 'ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΡΠΎΠΊΠ΅Π½ Π² ΡΠΎΡΠΌΠ°ΡΠ΅: Bearer <Π²Π°Ρ_ΡΠΎΠΊΠ΅Π½>'
+ }
+ }
+}
+swagger = Swagger(app)
# Ensure upload folder exists
+# ...
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
-# Register blueprints (routes)
+# Register blueprints
from .routes.cases import cases_blueprint
from .routes.credentials import credentials_blueprint
from .routes.auth import auth_blueprint
from .routes.dashboard import dashboard_blueprint
from .routes.reports import reports_blueprint
+from .routes.ai import ai_blueprint
+from .routes.seo import seo_blueprint
+from .routes.content import content_blueprint
+from .routes.finance import finance_blueprint
app.register_blueprint(cases_blueprint, url_prefix='/api')
app.register_blueprint(credentials_blueprint, url_prefix='/api')
app.register_blueprint(auth_blueprint, url_prefix='/api')
app.register_blueprint(dashboard_blueprint, url_prefix='/api')
app.register_blueprint(reports_blueprint, url_prefix='/api')
+app.register_blueprint(ai_blueprint, url_prefix='/api')
+app.register_blueprint(seo_blueprint, url_prefix='/api')
+app.register_blueprint(content_blueprint, url_prefix='/api')
+app.register_blueprint(finance_blueprint, url_prefix='/api')
+
+import threading
+import time
+
+def background_scanner():
+ """Π€ΠΎΠ½ΠΎΠ²Π°Ρ Π·Π°Π΄Π°ΡΠ° Π΄Π»Ρ ΠΈΠΌΠΈΡΠ°ΡΠΈΠΈ ΡΠΊΠ°Π½ΠΈΡΠΎΠ²Π°Π½ΠΈΡ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠΎΠ²."""
+ while True:
+ # ΠΠ΄Π΅ΡΡ Π² Π±ΡΠ΄ΡΡΠ΅ΠΌ Π±ΡΠ΄Π΅Ρ Π²ΡΠ·ΠΎΠ² api.ozon_api ΠΈ Ρ.Π΄.
+ print("--- [Scanner] Checking marketplaces for price dumping... ---")
+ # ΠΠΌΠΈΡΠΈΡΡΠ΅ΠΌ ΡΠΎΠ±ΡΡΠΈΠ΅ Π΄Π»Ρ ΠΏΡΠΈΠΌΠ΅ΡΠ° (ΡΡΠΎ ΡΠΊΠ°Π½Π΅Ρ ΡΡΠΎ-ΡΠΎ Π½Π°ΡΠ΅Π»)
+ # socketio.emit('scanner_update', {'message': 'Π‘ΠΊΠ°Π½Π΅Ρ ΠΏΡΠΎΠ²Π΅ΡΠΈΠ» 10 ΡΠΎΠ²Π°ΡΠΎΠ²...'}, namespace='/')
+ time.sleep(60)
+
+# Start background thread
+scanner_thread = threading.Thread(target=background_scanner, daemon=True)
+scanner_thread.start()
if __name__ == '__main__':
- # Use host 0.0.0.0 for Docker compatibility, port 5000
- app.run(host='0.0.0.0', port=5000, debug=True)
+ # Use socketio.run instead of app.run
+ socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)
diff --git a/backend/requirements.txt b/backend/requirements.txt
index e49e202..fede26f 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -8,3 +8,8 @@ PyJWT==2.8.0
bcrypt==4.1.3
reportlab==4.1.0
pandas==2.2.1
+flasgger==0.9.7.1
+flask-socketio==5.3.6
+google-generativeai==0.5.2
+ImageHash==4.3.1
+Pillow==10.2.0
diff --git a/backend/routes/ai.py b/backend/routes/ai.py
new file mode 100644
index 0000000..7344784
--- /dev/null
+++ b/backend/routes/ai.py
@@ -0,0 +1,79 @@
+from flask import Blueprint, request, jsonify
+from ..middleware import token_required
+from ..utils.ai_utils import generate_complaint_text
+
+ai_blueprint = Blueprint('ai', __name__)
+
+@ai_blueprint.route('/ai/generate-complaint', methods=['POST'])
+@token_required
+def ai_generate_complaint(current_user_id):
+ """
+ ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΡΠ΅ΠΊΡΡΠ° ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ Ρ ΠΏΠΎΠΌΠΎΡΡΡ ΠΠ
+ ---
+ tags:
+ - AI
+ security:
+ - APIKeyHeader: []
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ platform:
+ type: string
+ violator_article:
+ type: string
+ my_article:
+ type: string
+ responses:
+ 200:
+ description: Π‘Π³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°Π½Π½ΡΠΉ ΡΠ΅ΠΊΡΡ
+ """
+ data = request.json
+ platform = data.get('platform', 'ozon')
+ v_art = data.get('violator_article', '')
+ my_art = data.get('my_article', '')
+
+ if not v_art:
+ return jsonify({"error": "ΠΡΡΠΈΠΊΡΠ» Π½Π°ΡΡΡΠΈΡΠ΅Π»Ρ ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»Π΅Π½"}), 400
+
+ text = generate_complaint_text(platform, v_art, my_art)
+ return jsonify({"text": text})
+
+@ai_blueprint.route('/ai/generate-reply', methods=['POST'])
+@token_required
+def ai_generate_reply(current_user_id):
+ """
+ ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ ΠΎΡΠ²Π΅ΡΠ° Π½Π° ΠΎΡΠ·ΡΠ² ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Ρ
+ ---
+ tags:
+ - AI
+ security:
+ - APIKeyHeader: []
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ review_text:
+ type: string
+ rating:
+ type: integer
+ responses:
+ 200:
+ description: Π‘Π³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°Π½Π½ΡΠΉ ΠΎΡΠ²Π΅Ρ
+ """
+ data = request.json
+ review_text = data.get('review_text', '')
+ rating = data.get('rating', 5)
+
+ if not review_text:
+ return jsonify({"error": "Π’Π΅ΠΊΡΡ ΠΎΡΠ·ΡΠ²Π° ΠΎΠ±ΡΠ·Π°ΡΠ΅Π»Π΅Π½"}), 400
+
+ from ..utils.ai_utils import generate_review_reply
+ text = generate_review_reply(review_text, rating)
+ return jsonify({"text": text})
diff --git a/backend/routes/auth.py b/backend/routes/auth.py
index 4eeac66..e77eb08 100644
--- a/backend/routes/auth.py
+++ b/backend/routes/auth.py
@@ -11,6 +11,26 @@ auth_blueprint = Blueprint('auth', __name__)
@auth_blueprint.route('/auth/register', methods=['POST'])
def register():
+ """
+ Π Π΅Π³ΠΈΡΡΡΠ°ΡΠΈΡ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ
+ ---
+ tags:
+ - Auth
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ password:
+ type: string
+ responses:
+ 201:
+ description: ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΡΡΠΏΠ΅ΡΠ½ΠΎ Π·Π°ΡΠ΅Π³ΠΈΡΡΡΠΈΡΠΎΠ²Π°Π½
+ """
data = request.json
username = data.get('username')
password = data.get('password')
@@ -41,6 +61,30 @@ def register():
@auth_blueprint.route('/auth/login', methods=['POST'])
def login():
+ """
+ ΠΡ
ΠΎΠ΄ Π² ΡΠΈΡΡΠ΅ΠΌΡ
+ ---
+ tags:
+ - Auth
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ properties:
+ username:
+ type: string
+ password:
+ type: string
+ responses:
+ 200:
+ description: Π’ΠΎΠΊΠ΅Π½ ΡΡΠΏΠ΅ΡΠ½ΠΎ ΡΠ³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°Π½
+ schema:
+ properties:
+ token:
+ type: string
+ """
data = request.json
username = data.get('username')
password = data.get('password')
@@ -59,6 +103,11 @@ def login():
'exp': datetime.utcnow() + timedelta(days=7)
}, SECRET_KEY, algorithm="HS256")
+ # --- AUDIT LOG ---
+ from ..utils.audit_utils import log_action
+ log_action(user[0], 'LOGIN', {'ip': request.remote_addr})
+ # -----------------
+
return jsonify({"token": token})
@auth_blueprint.route('/auth/me', methods=['GET'])
diff --git a/backend/routes/cases.py b/backend/routes/cases.py
index 8e5b25b..3a13401 100644
--- a/backend/routes/cases.py
+++ b/backend/routes/cases.py
@@ -27,6 +27,30 @@ def serialize_case(row):
@cases_blueprint.route('/cases', methods=['GET'])
@token_required
def list_cases(current_user_id):
+ """
+ ΠΠΎΠ»ΡΡΠΈΡΡ ΡΠΏΠΈΡΠΎΠΊ Π²ΡΠ΅Ρ
ΠΊΠ΅ΠΉΡΠΎΠ² ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ
+ ---
+ tags:
+ - Cases
+ security:
+ - APIKeyHeader: []
+ parameters:
+ - name: article
+ in: query
+ type: string
+ description: Π€ΠΈΠ»ΡΡΡ ΠΏΠΎ Π°ΡΡΠΈΠΊΡΠ»Ρ
+ - name: status
+ in: query
+ type: string
+ description: Π€ΠΈΠ»ΡΡΡ ΠΏΠΎ ΡΡΠ°ΡΡΡΡ
+ - name: platform
+ in: query
+ type: string
+ description: Π€ΠΈΠ»ΡΡΡ ΠΏΠΎ ΠΏΠ»ΠΎΡΠ°Π΄ΠΊΠ΅
+ responses:
+ 200:
+ description: Π‘ΠΏΠΈΡΠΎΠΊ ΠΊΠ΅ΠΉΡΠΎΠ²
+ """
# optional filters: ?article=...&status=...&platform=...
article = request.args.get('article')
status = request.args.get('status')
@@ -56,13 +80,55 @@ def list_cases(current_user_id):
@cases_blueprint.route('/cases', methods=['POST'])
@token_required
def create_case(current_user_id):
+ """
+ Π‘ΠΎΠ·Π΄Π°ΡΡ Π½ΠΎΠ²ΡΠΉ ΠΊΠ΅ΠΉΡ
+ ---
+ tags:
+ - Cases
+ security:
+ - APIKeyHeader: []
+ parameters:
+ - name: body
+ in: body
+ required: true
+ schema:
+ type: object
+ required: [platform, violator_url, violator_article]
+ properties:
+ platform:
+ type: string
+ violator_url:
+ type: string
+ violator_article:
+ type: string
+ my_article:
+ type: string
+ comment:
+ type: string
+ responses:
+ 201:
+ description: ΠΠ΅ΠΉΡ ΡΠΎΠ·Π΄Π°Π½
+ """
data = request.json
required = ["platform", "violator_url", "violator_article"]
for f in required:
if f not in data:
return jsonify({"error": f"Missing field {f}"}), 400
+ # --- PLAN LIMIT CHECK ---
conn = get_db_connection()
cur = conn.cursor()
+
+ cur.execute("SELECT plan FROM users WHERE id = %s", (current_user_id,))
+ user_plan = cur.fetchone()[0]
+
+ if user_plan == 'free':
+ cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s", (current_user_id,))
+ count = cur.fetchone()[0]
+ if count >= 5:
+ release_db_connection(conn)
+ return jsonify({"error": "ΠΠΈΠΌΠΈΡ ΠΊΠ΅ΠΉΡΠΎΠ² Π΄Π»Ρ Free ΡΠ°ΡΠΈΡΠ° (5 ΡΡ) ΠΈΡΡΠ΅ΡΠΏΠ°Π½. ΠΠ΅ΡΠ΅ΠΉΠ΄ΠΈΡΠ΅ Π½Π° PRO."}), 403
+ # ------------------------
+
cur.execute(
"""INSERT INTO cases (user_id, platform, violator_url, violator_article, my_article, comment, status)
VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id""",
@@ -79,6 +145,40 @@ def create_case(current_user_id):
case_id = cur.fetchone()[0]
conn.commit()
release_db_connection(conn)
+
+ # --- TELEGRAM NOTIFICATION ---
+ from ..utils.telegram_utils import send_telegram_notification
+ msg = (
+ f"π ΠΠΎΠ²ΡΠΉ ΠΊΠ΅ΠΉΡ #{case_id}\n"
+ f"ΠΠ»ΠΎΡΠ°Π΄ΠΊΠ°: {data.get('platform').upper()}\n"
+ f"ΠΡΡΠΈΠΊΡΠ»: {data.get('violator_article')}\n"
+ f"Π‘ΡΠ°ΡΡΡ: NEW"
+ )
+ send_telegram_notification(msg)
+ # -----------------------------
+
+ # --- AUDIT LOG ---
+ from ..utils.audit_utils import log_action
+ log_action(current_user_id, 'CREATE_CASE', {
+ 'case_id': case_id,
+ 'platform': data.get('platform'),
+ 'article': data.get('violator_article')
+ })
+ # -----------------
+
+ # --- WEBSOCKET NOTIFICATION ---
+ try:
+ from ..app import socketio
+ socketio.emit('case_created', {
+ 'id': case_id,
+ 'platform': data.get('platform'),
+ 'article': data.get('violator_article'),
+ 'message': f"ΠΠΎΠ²ΡΠΉ ΠΊΠ΅ΠΉΡ Π½Π° {data.get('platform').upper()} ΡΠΎΠ·Π΄Π°Π½!"
+ })
+ except Exception as e:
+ print(f"WS Emit Error: {e}")
+ # ------------------------------
+
return jsonify({"id": case_id}), 201
@cases_blueprint.route('/cases/', methods=['GET'])
@@ -123,6 +223,12 @@ def delete_case(current_user_id, case_id):
cur.execute("DELETE FROM cases WHERE id = %s AND user_id = %s", (case_id, current_user_id))
conn.commit()
release_db_connection(conn)
+
+ # --- AUDIT LOG ---
+ from ..utils.audit_utils import log_action
+ log_action(current_user_id, 'DELETE_CASE', {'case_id': case_id})
+ # -----------------
+
return jsonify({"message": "Case deleted"})
# ---------- FILE UPLOAD ----------
diff --git a/backend/routes/content.py b/backend/routes/content.py
new file mode 100644
index 0000000..24ca4b9
--- /dev/null
+++ b/backend/routes/content.py
@@ -0,0 +1,76 @@
+import os
+from flask import Blueprint, request, jsonify, current_app
+from werkzeug.utils import secure_filename
+from ..db import get_db_connection, release_db_connection
+from ..middleware import token_required
+from ..utils.image_utils import calculate_image_hash
+
+content_blueprint = Blueprint('content', __name__)
+
+@content_blueprint.route('/content/reference', methods=['POST'])
+@token_required
+def upload_reference(current_user_id):
+ if 'file' not in request.files:
+ return jsonify({"error": "No file"}), 400
+
+ file = request.files['file']
+ article = request.form.get('article', 'unknown')
+
+ filename = secure_filename(file.filename)
+ save_path = os.path.join(current_app.config['UPLOAD_FOLDER'], f"ref_{current_user_id}_{filename}")
+ file.save(save_path)
+
+ # Calculate Hash
+ img_hash = calculate_image_hash(save_path)
+
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute(
+ "INSERT INTO reference_images (user_id, file_path, image_hash, article) VALUES (%s, %s, %s, %s) RETURNING id",
+ (current_user_id, save_path, img_hash, article)
+ )
+ ref_id = cur.fetchone()[0]
+ conn.commit()
+ release_db_connection(conn)
+
+ return jsonify({"id": ref_id, "hash": img_hash}), 201
+
+@content_blueprint.route('/content/reference', methods=['GET'])
+@token_required
+def list_references(current_user_id):
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT id, file_path, image_hash, article, created_at FROM reference_images WHERE user_id = %s", (current_user_id,))
+ rows = cur.fetchall()
+ release_db_connection(conn)
+
+ return jsonify([{
+ "id": r[0],
+ "filename": os.path.basename(r[1]),
+ "hash": r[2],
+ "article": r[3],
+ "date": r[4].isoformat()
+ } for r in rows])
+
+@content_blueprint.route('/content/detect-clones/', methods=['GET'])
+@token_required
+def detect_clones(current_user_id, ref_id):
+ # Π ΡΠ΅Π°Π»ΡΠ½ΠΎΠΌ ΠΏΡΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠΈ Π·Π΄Π΅ΡΡ Π·Π°ΠΏΡΡΠΊΠ°Π΅ΡΡΡ ΠΏΠΎΠΈΡΠΊ ΠΏΠΎ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ°ΠΌ
+ # ΠΠ»Ρ Π΄Π΅ΠΌΠΎ ΠΈΠΌΠΈΡΠΈΡΡΠ΅ΠΌ Π½Π°Ρ
ΠΎΠΆΠ΄Π΅Π½ΠΈΠ΅ Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΈΡ
Π½Π°ΡΡΡΠΈΡΠ΅Π»Π΅ΠΉ
+ clones = [
+ {
+ "platform": "ozon",
+ "url": "https://ozon.ru/product/123456",
+ "article": "OZ-9988",
+ "similarity": "98%",
+ "image_url": "https://ir.ozone.ru/s3/multimedia-1/654321.jpg"
+ },
+ {
+ "platform": "wildberries",
+ "url": "https://wildberries.ru/catalog/778899/detail.aspx",
+ "article": "WB-CLONE-1",
+ "similarity": "92%",
+ "image_url": "https://basket-01.wb.ru/vol77/part778/778899/images/big/1.jpg"
+ }
+ ]
+ return jsonify(clones)
diff --git a/backend/routes/dashboard.py b/backend/routes/dashboard.py
index f2b9409..745818a 100644
--- a/backend/routes/dashboard.py
+++ b/backend/routes/dashboard.py
@@ -1,4 +1,5 @@
from flask import Blueprint, jsonify
+from datetime import datetime, timedelta
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
@@ -7,6 +8,17 @@ dashboard_blueprint = Blueprint('dashboard', __name__)
@dashboard_blueprint.route('/dashboard/stats', methods=['GET'])
@token_required
def get_stats(current_user_id):
+ """
+ ΠΠΎΠ»ΡΡΠΈΡΡ ΠΎΠ±ΡΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ Π΄Π»Ρ Π΄Π°ΡΠ±ΠΎΡΠ΄Π°
+ ---
+ tags:
+ - Dashboard
+ security:
+ - APIKeyHeader: []
+ responses:
+ 200:
+ description: ΠΠ±ΡΠ΅ΠΊΡ ΡΠΎ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠΎΠΉ ΠΈ ΡΡΠ΅Π½Π΄Π°ΠΌΠΈ
+ """
conn = get_db_connection()
cur = conn.cursor()
@@ -39,13 +51,76 @@ def get_stats(current_user_id):
"date": r[4].isoformat() if r[4] else None
} for r in recent_rows
]
+
+ # 6. Trend data (last 7 days)
+ trend = []
+ for i in range(6, -1, -1):
+ day = (datetime.utcnow() - timedelta(days=i)).date()
+ cur.execute("SELECT COUNT(*) FROM cases WHERE user_id = %s AND created_at::date = %s", (current_user_id, day))
+ count = cur.fetchone()[0]
+ trend.append({
+ "date": day.strftime('%d.%m'),
+ "count": count
+ })
+ # 5. Multi-account stores summary
+ cur.execute("SELECT id, platform, name FROM credentials WHERE user_id = %s", (current_user_id,))
+ stores_rows = cur.fetchall()
+ stores = []
+ total_sales = 0
+
+ for s in stores_rows:
+ # Π ΡΠ΅Π°Π»ΡΠ½ΠΎΠΉ ΠΆΠΈΠ·Π½ΠΈ Π·Π΄Π΅ΡΡ Π±ΡΠ» Π±Ρ Π²ΡΠ·ΠΎΠ² API Ozon/WB Π΄Π»Ρ ΠΏΠΎΠ»ΡΡΠ΅Π½ΠΈΡ ΠΏΡΠΎΠ΄Π°ΠΆ Π·Π° ΡΠ΅Π³ΠΎΠ΄Π½Ρ
+ # ΠΠ»Ρ Π΄Π΅ΠΌΠΎΠ½ΡΡΡΠ°ΡΠΈΠΈ Π³Π΅Π½Π΅ΡΠΈΡΡΠ΅ΠΌ ΡΠ»ΡΡΠ°ΠΉΠ½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅
+ import random
+ daily_sales = random.randint(5000, 50000)
+ total_sales += daily_sales
+ stores.append({
+ "id": s[0],
+ "platform": s[1],
+ "name": s[2],
+ "status": "active",
+ "daily_sales": daily_sales
+ })
+
release_db_connection(conn)
-
+
return jsonify({
"total_cases": total_cases,
"cases_by_status": cases_by_status,
"total_integrations": total_integrations,
+ "total_sales": total_sales,
"platforms": platforms,
- "recent_activity": recent_activity
+ "recent_activity": recent_activity,
+ "trend": trend,
+ "stores": stores
})
+
+@dashboard_blueprint.route('/dashboard/logs', methods=['GET'])
+@token_required
+def get_logs(current_user_id):
+ """
+ ΠΠΎΠ»ΡΡΠΈΡΡ Π»ΠΎΠ³ Π°ΡΠ΄ΠΈΡΠ° Π΄Π΅ΠΉΡΡΠ²ΠΈΠΉ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ
+ ---
+ tags:
+ - Dashboard
+ security:
+ - APIKeyHeader: []
+ responses:
+ 200:
+ description: Π‘ΠΏΠΈΡΠΎΠΊ Π΄Π΅ΠΉΡΡΠ²ΠΈΠΉ ΠΈΠ· Π»ΠΎΠ³Π°
+ """
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute("SELECT action, details, created_at FROM audit_log WHERE user_id = %s ORDER BY created_at DESC LIMIT 20", (current_user_id,))
+ rows = cur.fetchall()
+ release_db_connection(conn)
+
+ logs = [
+ {
+ "action": r[0],
+ "details": r[1],
+ "date": r[2].isoformat() if r[2] else None
+ } for r in rows
+ ]
+ return jsonify(logs)
diff --git a/backend/routes/finance.py b/backend/routes/finance.py
new file mode 100644
index 0000000..fe983a8
--- /dev/null
+++ b/backend/routes/finance.py
@@ -0,0 +1,47 @@
+from flask import Blueprint, request, jsonify
+from ..middleware import token_required
+
+finance_blueprint = Blueprint('finance', __name__)
+
+@finance_blueprint.route('/finance/calculate-pnl', methods=['POST'])
+@token_required
+def calculate_pnl(current_user_id):
+ """
+ Π Π°ΡΡΠ΅Ρ ΡΠΈΡΡΠΎΠΉ ΠΏΡΠΈΠ±ΡΠ»ΠΈ ΠΈ ΠΌΠ°ΡΠΆΠΈΠ½Π°Π»ΡΠ½ΠΎΡΡΠΈ.
+ """
+ data = request.json
+
+ # ΠΡ
ΠΎΠ΄Π½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅
+ sale_price = float(data.get('sale_price', 0)) # Π¦Π΅Π½Π° ΠΏΡΠΎΠ΄Π°ΠΆΠΈ
+ purchase_price = float(data.get('purchase_price', 0)) # Π‘Π΅Π±Π΅ΡΡΠΎΠΈΠΌΠΎΡΡΡ (Π·Π°ΠΊΡΠΏΠΊΠ°)
+ logistics = float(data.get('logistics', 0)) # ΠΠΎΠ³ΠΈΡΡΠΈΠΊΠ°
+ commission_pct = float(data.get('commission_pct', 0)) # ΠΠΎΠΌΠΈΡΡΠΈΡ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ° %
+ tax_pct = float(data.get('tax_pct', 6)) # ΠΠ°Π»ΠΎΠ³ % (Π£Π‘Π)
+ adv_per_unit = float(data.get('adv_per_unit', 0)) # Π Π΅ΠΊΠ»Π°ΠΌΠ° Π½Π° 1 ΡΡ
+ other_costs = float(data.get('other_costs', 0)) # ΠΡΠΎΡΠΈΠ΅ ΡΠ°ΡΡ
ΠΎΠ΄Ρ (ΡΠΏΠ°ΠΊΠΎΠ²ΠΊΠ° ΠΈ Ρ.Π΄.)
+
+ # Π Π°ΡΡΠ΅ΡΡ
+ marketplace_commission = sale_price * (commission_pct / 100)
+ taxes = sale_price * (tax_pct / 100)
+
+ total_costs = (
+ purchase_price +
+ logistics +
+ marketplace_commission +
+ taxes +
+ adv_per_unit +
+ other_costs
+ )
+
+ net_profit = sale_price - total_costs
+ margin = (net_profit / sale_price * 100) if sale_price > 0 else 0
+ roi = (net_profit / purchase_price * 100) if purchase_price > 0 else 0
+
+ return jsonify({
+ "net_profit": round(net_profit, 2),
+ "margin": round(margin, 2),
+ "roi": round(roi, 2),
+ "total_costs": round(total_costs, 2),
+ "tax_amount": round(taxes, 2),
+ "commission_amount": round(marketplace_commission, 2)
+ })
diff --git a/backend/routes/reports.py b/backend/routes/reports.py
index d5bc6b5..850636a 100644
--- a/backend/routes/reports.py
+++ b/backend/routes/reports.py
@@ -1,20 +1,55 @@
import io
+import os
import pandas as pd
from flask import Blueprint, send_file, jsonify, make_response
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib import colors
-from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
+from reportlab.pdfbase import pdfmetrics
+from reportlab.pdfbase.ttfonts import TTFont
from ..db import get_db_connection, release_db_connection
from ..middleware import token_required
reports_blueprint = Blueprint('reports', __name__)
+# ΠΠΎΠΏΡΡΠΊΠ° Π·Π°ΡΠ΅Π³ΠΈΡΡΡΠΈΡΠΎΠ²Π°ΡΡ ΠΊΠΈΡΠΈΠ»Π»ΠΈΡΠ΅ΡΠΊΠΈΠΉ ΡΡΠΈΡΡ
+FONT_PATH = os.path.join(os.path.dirname(__file__), '..', 'fonts', 'DejaVuSans.ttf')
+FONT_NAME = 'DejaVuSans'
+
+try:
+ if os.path.exists(FONT_PATH):
+ pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_PATH))
+ else:
+ # ΠΡΡΠ°Π΅ΠΌΡΡ Π½Π°ΠΉΡΠΈ ΡΠΈΡΡΠ΅ΠΌΠ½ΡΠΉ ΡΡΠΈΡΡ Π½Π° Windows ΠΈΠ»ΠΈ Linux
+ sys_fonts = [
+ '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
+ 'C:\\Windows\\Fonts\\arial.ttf'
+ ]
+ for f in sys_fonts:
+ if os.path.exists(f):
+ pdfmetrics.registerFont(TTFont(FONT_NAME, f))
+ break
+except Exception as e:
+ print(f"Warning: Could not register Cyrillic font: {e}")
+ FONT_NAME = 'Helvetica' # Fallback
+
@reports_blueprint.route('/reports/cases/csv', methods=['GET'])
@token_required
def export_cases_csv(current_user_id):
+ """
+ ΠΠΊΡΠΏΠΎΡΡ Π²ΡΠ΅Ρ
ΠΊΠ΅ΠΉΡΠΎΠ² Π² CSV
+ ---
+ tags:
+ - Reports
+ security:
+ - APIKeyHeader: []
+ responses:
+ 200:
+ description: CSV ΡΠ°ΠΉΠ»
+ """
conn = get_db_connection()
query = """
SELECT id, platform, violator_url, violator_article, my_article, comment, status, created_at
@@ -38,6 +73,22 @@ def export_cases_csv(current_user_id):
@reports_blueprint.route('/reports/case//pdf', methods=['GET'])
@token_required
def export_case_pdf(current_user_id, case_id):
+ """
+ ΠΠ΅Π½Π΅ΡΠ°ΡΠΈΡ PDF ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ Π΄Π»Ρ ΠΊΠΎΠ½ΠΊΡΠ΅ΡΠ½ΠΎΠ³ΠΎ ΠΊΠ΅ΠΉΡΠ°
+ ---
+ tags:
+ - Reports
+ security:
+ - APIKeyHeader: []
+ parameters:
+ - name: case_id
+ in: path
+ type: integer
+ required: true
+ responses:
+ 200:
+ description: PDF ΡΠ°ΠΉΠ» ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ
+ """
conn = get_db_connection()
cur = conn.cursor()
cur.execute("""
@@ -56,34 +107,62 @@ def export_case_pdf(current_user_id, case_id):
buffer = io.BytesIO()
doc = SimpleDocTemplate(buffer, pagesize=letter)
styles = getSampleStyleSheet()
+
+ # Custom style for Russian text
+ ru_style = ParagraphStyle(
+ 'Russian',
+ parent=styles['Normal'],
+ fontName=FONT_NAME,
+ fontSize=10,
+ leading=12
+ )
+ title_style = ParagraphStyle(
+ 'RussianTitle',
+ parent=styles['Title'],
+ fontName=FONT_NAME,
+ fontSize=18,
+ spaceAfter=20
+ )
+
elements = []
- # Title
- elements.append(Paragraph(f"ΠΡΠΈΡΠΈΠ°Π»ΡΠ½Π°Ρ ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΡ #{case_id}", styles['Title']))
- elements.append(Spacer(1, 12))
+ # Header / Title
+ elements.append(Paragraph(f"ΠΡΠΈΡΠΈΠ°Π»ΡΠ½Π°Ρ ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΡ #{case_id}", title_style))
+ elements.append(Paragraph(f"Π‘ΠΈΡΡΠ΅ΠΌΠ° ΠΌΠΎΠ½ΠΈΡΠΎΡΠΈΠ½Π³Π° AutoMarket (OzonGuard)", ru_style))
+ elements.append(Spacer(1, 20))
# Info table
data = [
- ["ΠΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡ", platform.upper()],
- ["ΠΠ°ΡΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ", created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"],
- ["ΠΡΡΠΈΠΊΡΠ» Π½Π°ΡΡΡΠΈΡΠ΅Π»Ρ", v_art],
- ["Π‘ΡΡΠ»ΠΊΠ° Π½Π° ΡΠΎΠ²Π°Ρ", Paragraph(v_url, styles['Normal'])],
- ["ΠΠ°Ρ Π°ΡΡΠΈΠΊΡΠ»", my_art or "-"],
- ["Π’Π΅ΠΊΡΡΠΈΠΉ ΡΡΠ°ΡΡΡ", status.upper()]
+ [Paragraph("ΠΠ°ΡΠ°ΠΌΠ΅ΡΡ", ru_style), Paragraph("ΠΠ½Π°ΡΠ΅Π½ΠΈΠ΅", ru_style)],
+ [Paragraph("ΠΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡ", ru_style), platform.upper()],
+ [Paragraph("ΠΠ°ΡΠ° ΡΠΎΠ·Π΄Π°Π½ΠΈΡ", ru_style), created_at.strftime('%Y-%m-%d %H:%M') if created_at else "-"],
+ [Paragraph("ΠΡΡΠΈΠΊΡΠ» Π½Π°ΡΡΡΠΈΡΠ΅Π»Ρ", ru_style), v_art],
+ [Paragraph("Π‘ΡΡΠ»ΠΊΠ° Π½Π° ΡΠΎΠ²Π°Ρ", ru_style), Paragraph(v_url, ru_style)],
+ [Paragraph("ΠΠ°Ρ Π°ΡΡΠΈΠΊΡΠ»", ru_style), my_art or "-"],
+ [Paragraph("Π’Π΅ΠΊΡΡΠΈΠΉ ΡΡΠ°ΡΡΡ", ru_style), status.upper()]
]
t = Table(data, colWidths=[150, 350])
t.setStyle(TableStyle([
- ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
- ('GRID', (0, 0), (-1, -1), 1, colors.black),
- ('PADDING', (0, 0), (-1, -1), 6),
+ ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
+ ('FONTNAME', (0, 0), (-1, -1), FONT_NAME),
+ ('FONTSIZE', (0, 0), (-1, -1), 10),
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
+ ('BACKGROUND', (0, 1), (-1, -1), colors.whitesmoke),
+ ('GRID', (0, 0), (-1, -1), 1, colors.grey),
+ ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
]))
elements.append(t)
elements.append(Spacer(1, 24))
# Description
- elements.append(Paragraph("ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ Π½Π°ΡΡΡΠ΅Π½ΠΈΡ:", styles['Heading2']))
- elements.append(Paragraph(comment or "ΠΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΉ ΠΎΡΡΡΡΡΡΠ²ΡΠ΅Ρ", styles['Normal']))
+ elements.append(Paragraph("ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ Π½Π°ΡΡΡΠ΅Π½ΠΈΡ:", ParagraphStyle('Sub', parent=ru_style, fontSize=12, spaceAfter=10)))
+ elements.append(Paragraph(comment or "ΠΠΎΠΌΠΌΠ΅Π½ΡΠ°ΡΠΈΠΉ ΠΎΡΡΡΡΡΡΠ²ΡΠ΅Ρ", ru_style))
+
+ elements.append(Spacer(1, 40))
+ elements.append(Paragraph("Π‘Π³Π΅Π½Π΅ΡΠΈΡΠΎΠ²Π°Π½ΠΎ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ. ΠΠ°Π½Π½ΡΠΉ Π΄ΠΎΠΊΡΠΌΠ΅Π½Ρ ΡΠ²Π»ΡΠ΅ΡΡΡ ΠΎΡΠΈΡΠΈΠ°Π»ΡΠ½ΡΠΌ ΡΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ΠΌ ΠΎ Π½Π°ΡΡΡΠ΅Π½ΠΈΠΈ ΠΏΡΠ°Π²ΠΈΠ» ΠΏΠ»Π°ΡΡΠΎΡΠΌΡ.", ParagraphStyle('Footer', parent=ru_style, fontSize=8, textColor=colors.grey)))
doc.build(elements)
buffer.seek(0)
diff --git a/backend/routes/seo.py b/backend/routes/seo.py
new file mode 100644
index 0000000..e3c72c6
--- /dev/null
+++ b/backend/routes/seo.py
@@ -0,0 +1,78 @@
+from flask import Blueprint, request, jsonify
+from ..db import get_db_connection, release_db_connection
+from ..middleware import token_required
+import random
+from datetime import datetime, timedelta
+
+seo_blueprint = Blueprint('seo', __name__)
+
+@seo_blueprint.route('/seo/keywords', methods=['GET'])
+@token_required
+def list_keywords(current_user_id):
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute("""
+ SELECT k.id, k.keyword, k.article, k.platform,
+ (SELECT position FROM seo_history h WHERE h.keyword_id = k.id ORDER BY checked_at DESC LIMIT 1) as last_pos
+ FROM seo_keywords k
+ WHERE k.user_id = %s
+ """, (current_user_id,))
+ rows = cur.fetchall()
+ release_db_connection(conn)
+
+ return jsonify([{
+ "id": r[0],
+ "keyword": r[1],
+ "article": r[2],
+ "platform": r[3],
+ "last_position": r[4]
+ } for r in rows])
+
+@seo_blueprint.route('/seo/keywords', methods=['POST'])
+@token_required
+def add_keyword(current_user_id):
+ data = request.json
+ keyword = data.get('keyword')
+ article = data.get('article')
+ platform = data.get('platform', 'ozon')
+
+ if not keyword or not article:
+ return jsonify({"error": "Keyword and article are required"}), 400
+
+ conn = get_db_connection()
+ cur = conn.cursor()
+ cur.execute(
+ "INSERT INTO seo_keywords (user_id, keyword, article, platform) VALUES (%s, %s, %s, %s) RETURNING id",
+ (current_user_id, keyword, article, platform)
+ )
+ kid = cur.fetchone()[0]
+
+ # Generate some mock history immediately
+ for i in range(7):
+ date = datetime.now() - timedelta(days=i)
+ pos = random.randint(1, 150)
+ cur.execute("INSERT INTO seo_history (keyword_id, position, checked_at) VALUES (%s, %s, %s)", (kid, pos, date))
+
+ conn.commit()
+ release_db_connection(conn)
+ return jsonify({"id": kid}), 201
+
+@seo_blueprint.route('/seo/history/', methods=['GET'])
+@token_required
+def get_seo_history(current_user_id, keyword_id):
+ conn = get_db_connection()
+ cur = conn.cursor()
+ # Check ownership
+ cur.execute("SELECT id FROM seo_keywords WHERE id = %s AND user_id = %s", (keyword_id, current_user_id))
+ if not cur.fetchone():
+ release_db_connection(conn)
+ return jsonify({"error": "Not found"}), 404
+
+ cur.execute("SELECT position, checked_at FROM seo_history WHERE keyword_id = %s ORDER BY checked_at ASC", (keyword_id,))
+ rows = cur.fetchall()
+ release_db_connection(conn)
+
+ return jsonify([{
+ "position": r[0],
+ "date": r[1].strftime('%d.%m')
+ } for r in rows])
diff --git a/backend/utils/ai_utils.py b/backend/utils/ai_utils.py
new file mode 100644
index 0000000..cf5a3aa
--- /dev/null
+++ b/backend/utils/ai_utils.py
@@ -0,0 +1,65 @@
+import os
+import google.generativeai as genai
+
+def generate_complaint_text(platform, violator_article, my_article=None):
+ """ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅Ρ ΡΠ΅ΠΊΡΡ ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ Ρ ΠΏΠΎΠΌΠΎΡΡΡ Google Gemini."""
+ api_key = os.getenv('GEMINI_API_KEY')
+ if not api_key:
+ return "ΠΡΠΈΠ±ΠΊΠ°: API ΠΊΠ»ΡΡ Π΄Π»Ρ Gemini Π½Π΅ Π½Π°ΡΡΡΠΎΠ΅Π½ Π² .env"
+
+ genai.configure(api_key=api_key)
+ model = genai.GenerativeModel('gemini-1.5-flash')
+
+ prompt = f"""
+ Π’Ρ - ΠΏΡΠΎΡΠ΅ΡΡΠΈΠΎΠ½Π°Π»ΡΠ½ΡΠΉ ΡΡΠΈΡΡ Π² ΡΡΠ΅ΡΠ΅ ΠΈΠ½ΡΠ΅Π»Π»Π΅ΠΊΡΡΠ°Π»ΡΠ½ΠΎΠΉ ΡΠΎΠ±ΡΡΠ²Π΅Π½Π½ΠΎΡΡΠΈ ΠΈ e-commerce.
+ ΠΠ°ΠΏΠΈΡΠΈ ΠΎΡΠΈΡΠΈΠ°Π»ΡΠ½ΡΡ ΠΈ ΡΠ±Π΅Π΄ΠΈΡΠ΅Π»ΡΠ½ΡΡ ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΡ Π΄Π»Ρ ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ° {platform}.
+
+ Π‘ΡΡΡ Π½Π°ΡΡΡΠ΅Π½ΠΈΡ: ΠΏΡΠΎΠ΄Π°Π²Π΅Ρ Ρ Π°ΡΡΠΈΠΊΡΠ»ΠΎΠΌ {violator_article} Π½Π΅Π·Π°ΠΊΠΎΠ½Π½ΠΎ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅Ρ ΠΌΠΎΠΉ ΠΊΠΎΠ½ΡΠ΅Π½Ρ ΠΈΠ»ΠΈ Π½Π°ΡΡΡΠ°Π΅Ρ ΠΏΡΠ°Π²ΠΈΠ»Π° ΡΠ΅Π½ΠΎΠΎΠ±ΡΠ°Π·ΠΎΠ²Π°Π½ΠΈΡ.
+ ΠΠΎΠΉ ΠΎΡΠΈΠ³ΠΈΠ½Π°Π»ΡΠ½ΡΠΉ Π°ΡΡΠΈΠΊΡΠ» Π΄Π»Ρ ΡΡΠ°Π²Π½Π΅Π½ΠΈΡ: {my_article if my_article else 'Π½Π΅ ΡΠΊΠ°Π·Π°Π½'}.
+
+ Π’ΡΠ΅Π±ΠΎΠ²Π°Π½ΠΈΡ:
+ 1. Π’Π΅ΠΊΡΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ Π½Π° ΡΡΡΡΠΊΠΎΠΌ ΡΠ·ΡΠΊΠ΅.
+ 2. Π‘ΡΠΈΠ»Ρ ΠΎΡΠΈΡΠΈΠ°Π»ΡΠ½ΠΎ-Π΄Π΅Π»ΠΎΠ²ΠΎΠΉ.
+ 3. Π£ΠΏΠΎΠΌΡΠ½ΠΈ ΠΎ Π½Π΅ΠΎΠ±Ρ
ΠΎΠ΄ΠΈΠΌΠΎΡΡΠΈ Π·Π°ΡΠΈΡΡ ΠΏΡΠ°Π² ΠΏΡΠ°Π²ΠΎΠΎΠ±Π»Π°Π΄Π°ΡΠ΅Π»Ρ.
+ 4. Π’Π΅ΠΊΡΡ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ Π»Π°ΠΊΠΎΠ½ΠΈΡΠ½ΡΠΌ (Π΄ΠΎ 500 Π·Π½Π°ΠΊΠΎΠ²).
+
+ ΠΡΠ²Π΅Ρ Π΄Π°ΠΉ ΡΡΠ°Π·Ρ Π² Π²ΠΈΠ΄Π΅ ΡΠ΅ΠΊΡΡΠ° ΠΏΡΠ΅ΡΠ΅Π½Π·ΠΈΠΈ, Π±Π΅Π· Π»ΠΈΡΠ½ΠΈΡ
Π²ΡΡΡΠΏΠ»Π΅Π½ΠΈΠΉ.
+ """
+
+ try:
+ response = model.generate_content(prompt)
+ return response.text.strip()
+ except Exception as e:
+ return f"ΠΡΠΈΠ±ΠΊΠ° ΠΏΡΠΈ Π³Π΅Π½Π΅ΡΠ°ΡΠΈΠΈ ΡΠ΅ΠΊΡΡΠ° ΠΠ: {str(e)}"
+
+def generate_review_reply(review_text, rating):
+ """ΠΠ΅Π½Π΅ΡΠΈΡΡΠ΅Ρ ΠΎΡΠ²Π΅Ρ Π½Π° ΠΎΡΠ·ΡΠ² ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Ρ."""
+ api_key = os.getenv('GEMINI_API_KEY')
+ if not api_key:
+ return "ΠΡΠΈΠ±ΠΊΠ°: API ΠΊΠ»ΡΡ Π½Π΅ Π½Π°ΡΡΡΠΎΠ΅Π½."
+
+ genai.configure(api_key=api_key)
+ model = genai.GenerativeModel('gemini-1.5-flash')
+
+ prompt = f"""
+ Π’Ρ - ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ ΠΏΠΎ ΡΠ°Π±ΠΎΡΠ΅ Ρ ΠΊΠ»ΠΈΠ΅Π½ΡΠ°ΠΌΠΈ Π² ΠΌΠ°Π³Π°Π·ΠΈΠ½Π΅ Π½Π° ΠΌΠ°ΡΠΊΠ΅ΡΠΏΠ»Π΅ΠΉΡΠ΅.
+ Π’Π²ΠΎΡ Π·Π°Π΄Π°ΡΠ°: Π²Π΅ΠΆΠ»ΠΈΠ²ΠΎ ΠΈ ΠΏΡΠΎΡΠ΅ΡΡΠΈΠΎΠ½Π°Π»ΡΠ½ΠΎ ΠΎΡΠ²Π΅ΡΠΈΡΡ Π½Π° ΠΎΡΠ·ΡΠ² ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Ρ.
+
+ ΠΡΠ΅Π½ΠΊΠ° ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Ρ: {rating} Π·Π²Π΅Π·Π΄(Ρ).
+ Π’Π΅ΠΊΡΡ ΠΎΡΠ·ΡΠ²Π°: "{review_text}"
+
+ ΠΡΠ°Π²ΠΈΠ»Π°:
+ 1. ΠΡΠ»ΠΈ ΠΎΡΠ΅Π½ΠΊΠ° 4-5 Π·Π²Π΅Π·Π΄: ΠΏΠΎΠ±Π»Π°Π³ΠΎΠ΄Π°ΡΠΈ Π·Π° ΠΏΠΎΠΊΡΠΏΠΊΡ, Π²ΡΡΠ°Π·ΠΈ ΡΠ°Π΄ΠΎΡΡΡ, ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠΈ ΠΏΠΎΡΠΌΠΎΡΡΠ΅ΡΡ Π΄ΡΡΠ³ΠΈΠ΅ ΡΠΎΠ²Π°ΡΡ Π½Π°ΡΠ΅Π³ΠΎ Π±ΡΠ΅Π½Π΄Π°.
+ 2. ΠΡΠ»ΠΈ ΠΎΡΠ΅Π½ΠΊΠ° 1-3 Π·Π²Π΅Π·Π΄Ρ: ΠΈΠ·Π²ΠΈΠ½ΠΈΡΡ Π·Π° Π½Π΅ΡΠ΄ΠΎΠ±ΡΡΠ²Π°, ΡΡΠΎΡΠ½ΠΈ, ΡΡΠΎ ΠΌΡ ΡΠ°Π±ΠΎΡΠ°Π΅ΠΌ Π½Π°Π΄ ΠΊΠ°ΡΠ΅ΡΡΠ²ΠΎΠΌ, ΠΈ ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠΈ ΠΎΠ±ΡΠ°ΡΠΈΡΡΡΡ Π² ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΡ, Π΅ΡΠ»ΠΈ ΡΡΠΎ ΡΠΌΠ΅ΡΡΠ½ΠΎ.
+ 3. Π‘ΡΠΈΠ»Ρ: Π΄ΡΡΠΆΠ΅Π»ΡΠ±Π½ΡΠΉ, Π½ΠΎ Π΄Π΅Π»ΠΎΠ²ΠΎΠΉ. ΠΠ΅ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠΉ ΡΠ»ΠΈΡΠΊΠΎΠΌ ΠΌΠ½ΠΎΠ³ΠΎ Π²ΠΎΡΠΊΠ»ΠΈΡΠ°ΡΠ΅Π»ΡΠ½ΡΡ
Π·Π½Π°ΠΊΠΎΠ².
+ 4. ΠΠ»ΠΈΠ½Π°: 2-4 ΠΏΡΠ΅Π΄Π»ΠΎΠΆΠ΅Π½ΠΈΡ.
+ 5. ΠΠ΅ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠΉ ΡΠ°Π±Π»ΠΎΠ½Ρ "Π£Π²Π°ΠΆΠ°Π΅ΠΌΡΠΉ ΠΏΠΎΠΊΡΠΏΠ°ΡΠ΅Π»Ρ", Π½Π°ΡΠ½ΠΈ Π±ΠΎΠ»Π΅Π΅ Π΅ΡΡΠ΅ΡΡΠ²Π΅Π½Π½ΠΎ.
+
+ ΠΡΠ²Π΅Ρ Π΄Π°ΠΉ ΡΡΠ°Π·Ρ Π² Π²ΠΈΠ΄Π΅ Π³ΠΎΡΠΎΠ²ΠΎΠ³ΠΎ ΡΠ΅ΠΊΡΡΠ°.
+ """
+
+ try:
+ response = model.generate_content(prompt)
+ return response.text.strip()
+ except Exception as e:
+ return f"ΠΡΠΈΠ±ΠΊΠ° ΠΠ: {str(e)}"
diff --git a/backend/utils/audit_utils.py b/backend/utils/audit_utils.py
new file mode 100644
index 0000000..966ea76
--- /dev/null
+++ b/backend/utils/audit_utils.py
@@ -0,0 +1,17 @@
+import json
+from ..db import get_db_connection, release_db_connection
+
+def log_action(user_id, action, details=None):
+ """ΠΠ°ΠΏΠΈΡΡΠ²Π°Π΅Ρ Π΄Π΅ΠΉΡΡΠ²ΠΈΠ΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π² Π»ΠΎΠ³ Π°ΡΠ΄ΠΈΡΠ°."""
+ conn = get_db_connection()
+ try:
+ cur = conn.cursor()
+ cur.execute(
+ "INSERT INTO audit_log (user_id, action, details) VALUES (%s, %s, %s)",
+ (user_id, action, json.dumps(details) if details else None)
+ )
+ conn.commit()
+ except Exception as e:
+ print(f"Audit Log Error: {e}")
+ finally:
+ release_db_connection(conn)
diff --git a/backend/utils/image_utils.py b/backend/utils/image_utils.py
new file mode 100644
index 0000000..6d49239
--- /dev/null
+++ b/backend/utils/image_utils.py
@@ -0,0 +1,20 @@
+from PIL import Image
+import imagehash
+import os
+
+def calculate_image_hash(image_path):
+ """ΠΡΡΠΈΡΠ»ΡΠ΅Ρ ΠΏΠ΅ΡΡΠ΅ΠΏΡΠΈΠ²Π½ΡΠΉ Ρ
Π΅Ρ ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ."""
+ try:
+ img = Image.open(image_path)
+ hash_val = imagehash.phash(img)
+ return str(hash_val)
+ except Exception as e:
+ print(f"Hashing Error: {e}")
+ return None
+
+def compare_hashes(hash1, hash2, threshold=5):
+ """Π‘ΡΠ°Π²Π½ΠΈΠ²Π°Π΅Ρ Π΄Π²Π° Ρ
Π΅ΡΠ°. ΠΡΠ»ΠΈ ΡΠ°Π·Π½ΠΈΡΠ° ΠΌΠ΅Π½ΡΡΠ΅ ΠΏΠΎΡΠΎΠ³Π°, ΠΈΠ·ΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΡ ΡΡΠΈΡΠ°ΡΡΡΡ ΠΏΠΎΡ
ΠΎΠΆΠΈΠΌΠΈ."""
+ h1 = imagehash.hex_to_hash(hash1)
+ h2 = imagehash.hex_to_hash(hash2)
+ diff = h1 - h2
+ return diff <= threshold
diff --git a/backend/utils/telegram_utils.py b/backend/utils/telegram_utils.py
new file mode 100644
index 0000000..04652d2
--- /dev/null
+++ b/backend/utils/telegram_utils.py
@@ -0,0 +1,24 @@
+import requests
+import os
+
+def send_telegram_notification(message):
+ token = os.getenv('TELEGRAM_BOT_TOKEN')
+ chat_id = os.getenv('TELEGRAM_CHAT_ID')
+
+ if not token or not chat_id:
+ print("Telegram notification skipped: Token or Chat ID not configured.")
+ return False
+
+ url = f"https://api.telegram.org/bot{token}/sendMessage"
+ payload = {
+ "chat_id": chat_id,
+ "text": message,
+ "parse_mode": "HTML"
+ }
+
+ try:
+ response = requests.post(url, json=payload, timeout=5)
+ return response.status_code == 200
+ except Exception as e:
+ print(f"Error sending Telegram message: {e}")
+ return False
diff --git a/db/init.sql b/db/init.sql
index f74ed51..0e58239 100644
--- a/db/init.sql
+++ b/db/init.sql
@@ -39,3 +39,36 @@ CREATE TABLE IF NOT EXISTS files (
file_path TEXT NOT NULL,
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+
+CREATE TABLE IF NOT EXISTS audit_log (
+ id SERIAL PRIMARY KEY,
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
+ action VARCHAR(100) NOT NULL,
+ details JSONB,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS seo_keywords (
+ id SERIAL PRIMARY KEY,
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
+ keyword VARCHAR(255) NOT NULL,
+ article VARCHAR(100) NOT NULL,
+ platform VARCHAR(50) NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS seo_history (
+ id SERIAL PRIMARY KEY,
+ keyword_id INT REFERENCES seo_keywords(id) ON DELETE CASCADE,
+ position INT,
+ checked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS reference_images (
+ id SERIAL PRIMARY KEY,
+ user_id INT REFERENCES users(id) ON DELETE CASCADE,
+ file_path TEXT NOT NULL,
+ image_hash VARCHAR(64),
+ article VARCHAR(100),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e39b48f
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,27 @@
+# docker-compose.yml
+version: '3.8'
+
+services:
+ backend:
+ build:
+ context: .
+ dockerfile: backend/Dockerfile
+ ports:
+ - "5000:5000"
+ environment:
+ # ΠΠ»Ρ Windows/Mac ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ host.docker.internal Π΄Π»Ρ Π΄ΠΎΡΡΡΠΏΠ° ΠΊ Π»ΠΎΠΊΠ°Π»ΡΠ½ΠΎΠΉ ΠΠ
+ - DATABASE_URL=postgresql://user:password@host.docker.internal:5432/automarket
+ - JWT_SECRET=your_super_secret_key
+ volumes:
+ - ./backend/uploads:/app/backend/uploads
+ extra_hosts:
+ - "host.docker.internal:host-gateway"
+
+ frontend:
+ image: nginx:alpine
+ ports:
+ - "80:80"
+ volumes:
+ - ./frontend:/usr/share/nginx/html
+ depends_on:
+ - backend
diff --git a/frontend/css/styles.css b/frontend/css/styles.css
index 6f5309c..50add40 100644
--- a/frontend/css/styles.css
+++ b/frontend/css/styles.css
@@ -1,22 +1,31 @@
/* frontend/css/styles.css */
-/* Premium darkβmode + glassmorphism design */
+/* Ultimate Premium Dark Mode + Glassmorphism */
:root {
- --font-family: 'Inter', sans-serif;
- --color-bg: hsl(220, 10%, 10%);
- --color-surface: hsla(220, 10%, 15%, 0.8);
- --color-primary: hsl(210, 80%, 55%);
- --color-primary-hover: hsl(210, 80%, 45%);
- --color-success: hsl(140, 60%, 45%);
- --color-error: hsl(0, 70%, 50%);
- --color-text: hsl(0, 0%, 90%);
- --radius: 0.75rem;
- --shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
+ --font-family: 'Inter', system-ui, -apple-system, sans-serif;
+
+ /* Palette */
+ --color-bg: #0b0e14;
+ --color-surface: rgba(23, 28, 38, 0.7);
+ --color-surface-hover: rgba(35, 42, 56, 0.85);
+ --color-primary: #3b82f6; /* Modern Blue */
+ --color-primary-glow: rgba(59, 130, 246, 0.5);
+ --color-success: #10b981; /* Emerald */
+ --color-success-glow: rgba(16, 185, 129, 0.4);
+ --color-warning: #f59e0b; /* Amber */
+ --color-error: #ef4444; /* Rose */
+ --color-text: #f3f4f6;
+ --color-text-dim: #9ca3af;
+
+ /* Utils */
+ --radius-lg: 1.25rem;
+ --radius-md: 0.75rem;
+ --shadow-sm: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.1);
+ --glass-border: 1px solid rgba(255, 255, 255, 0.08);
}
-*,
-*::before,
-*::after {
+*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
@@ -24,248 +33,307 @@
body {
font-family: var(--font-family);
- background: var(--color-bg);
+ background-color: var(--color-bg);
+ background-image:
+ radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, transparent 40%),
+ radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.05) 0%, transparent 40%);
color: var(--color-text);
- line-height: 1.6;
+ line-height: 1.5;
min-height: 100vh;
- display: flex;
- flex-direction: column;
+ -webkit-font-smoothing: antialiased;
}
+/* Custom Scrollbar */
+::-webkit-scrollbar { width: 8px; }
+::-webkit-scrollbar-track { background: var(--color-bg); }
+::-webkit-scrollbar-thumb {
+ background: rgba(255,255,255,0.1);
+ border-radius: 10px;
+}
+::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
+
+/* Header */
.header {
- text-align: center;
- padding: 2rem 1rem;
- background: linear-gradient(135deg, hsla(210, 70%, 55%, 0.6), hsla(210, 70%, 35%, 0.6));
- backdrop-filter: blur(12px);
- box-shadow: var(--shadow);
+ padding: 2.5rem 1rem;
+ background: rgba(11, 14, 20, 0.8);
+ backdrop-filter: blur(20px);
+ border-bottom: var(--glass-border);
+ position: sticky;
+ top: 0;
+ z-index: 100;
}
.header h1 {
- font-size: 2.2rem;
- margin-bottom: 0.5rem;
- color: var(--color-primary);
+ font-size: 2.5rem;
+ font-weight: 800;
+ background: linear-gradient(to right, #60a5fa, #34d399);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ letter-spacing: -0.025em;
}
-.container {
- flex: 1;
- padding: 1.5rem;
- max-width: 1200px;
- margin: 0 auto;
+.header p {
+ color: var(--color-text-dim);
+ font-size: 0.95rem;
}
-.section {
+/* Auth Section */
+.auth-box {
+ max-width: 420px;
+ margin: 6rem auto;
+ padding: 2.5rem;
+ background: var(--color-surface);
+ backdrop-filter: blur(16px);
+ border-radius: var(--radius-lg);
+ border: var(--glass-border);
+ box-shadow: var(--shadow-xl);
+}
+
+.auth-box h2 {
+ font-size: 1.75rem;
margin-bottom: 2rem;
+ text-align: center;
}
-h2 {
- margin-bottom: 1rem;
- font-weight: 600;
- color: var(--color-primary);
-}
-
-.controls {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-bottom: 1rem;
-}
-
-.controls input,
-.controls select {
- padding: 0.5rem 0.75rem;
- border: 1px solid hsla(0, 0%, 100%, 0.1);
- border-radius: var(--radius);
- background: var(--color-surface);
- color: var(--color-text);
-}
-
-.btn-primary {
- background: var(--color-primary);
- border: none;
+.auth-box input {
+ width: 100%;
+ padding: 0.875rem 1.25rem;
+ margin-top: 0.75rem;
+ background: rgba(0, 0, 0, 0.3);
+ border: var(--glass-border);
+ border-radius: var(--radius-md);
color: #fff;
- padding: 0.5rem 1rem;
- border-radius: var(--radius);
- cursor: pointer;
- transition: background 0.2s;
+ transition: all 0.2s;
}
-.btn-primary:hover {
- background: var(--color-primary-hover);
-}
-
-.btn-secondary {
- background: hsla(0, 0%, 100%, 0.1);
- border: none;
- color: var(--color-text);
- padding: 0.5rem 1rem;
- border-radius: var(--radius);
- cursor: pointer;
- transition: background 0.2s;
-}
-
-.btn-secondary:hover {
- background: hsla(0, 0%, 100%, 0.2);
-}
-
-.table {
- width: 100%;
- border-collapse: collapse;
- backdrop-filter: blur(6px);
- background: var(--color-surface);
- border-radius: var(--radius);
- overflow: hidden;
- box-shadow: var(--shadow);
-}
-
-.table th,
-.table td {
- padding: 0.75rem 1rem;
- text-align: left;
- border-bottom: 1px solid hsla(0, 0%, 100%, 0.05);
-}
-
-.table th {
- background: hsla(0, 0%, 100%, 0.05);
- color: var(--color-primary);
-}
-
-.hidden { display: none; }
-
-label {
- display: block;
- margin-bottom: 0.75rem;
- color: var(--color-text);
-}
-
-label input,
-label textarea {
- width: 100%;
- padding: 0.5rem;
- margin-top: 0.25rem;
- border: 1px solid hsla(0,0%,100%,0.1);
- border-radius: var(--radius);
- background: var(--color-surface);
- color: var(--color-text);
-}
-
-.preview {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- margin-top: 0.5rem;
-}
-
-.preview img {
- max-width: 120px;
- max-height: 120px;
- object-fit: cover;
- border-radius: var(--radius);
- box-shadow: var(--shadow);
+.auth-box input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px var(--color-primary-glow);
}
+/* Tabs */
.tabs-container {
display: flex;
justify-content: center;
- gap: 1rem;
- padding: 1rem;
- background: var(--color-surface);
- border-bottom: 1px solid hsla(0,0%,100%,0.1);
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background: rgba(255, 255, 255, 0.03);
+ margin-top: 1rem;
+ border-radius: var(--radius-md);
+ max-width: 800px;
+ margin-left: auto;
+ margin-right: auto;
}
.tab-btn {
background: transparent;
- color: var(--color-text);
+ color: var(--color-text-dim);
border: none;
- padding: 0.5rem 1rem;
- font-size: 1.1rem;
+ padding: 0.625rem 1.25rem;
+ border-radius: var(--radius-md);
cursor: pointer;
- border-bottom: 2px solid transparent;
+ font-weight: 500;
transition: all 0.2s;
}
.tab-btn:hover {
- color: var(--color-primary);
+ background: rgba(255, 255, 255, 0.05);
+ color: #fff;
}
.tab-btn.active {
- color: var(--color-primary);
- border-bottom: 2px solid var(--color-primary);
+ background: var(--color-primary);
+ color: #fff;
+ box-shadow: 0 4px 12px var(--color-primary-glow);
}
+/* Main Content */
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 2.5rem 1.5rem;
+}
+
+.section {
+ animation: fadeIn 0.4s ease-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Cards & Grid */
.platforms-grid {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.platform-card {
background: var(--color-surface);
- padding: 1.5rem;
- border-radius: var(--radius);
- border: 1px solid hsla(0,0%,100%,0.1);
- transition: transform 0.2s;
+ padding: 1.75rem;
+ border-radius: var(--radius-lg);
+ border: var(--glass-border);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.platform-card:hover {
- transform: translateY(-2px);
+ transform: translateY(-4px);
+ background: var(--color-surface-hover);
+ border-color: rgba(255,255,255,0.15);
+ box-shadow: var(--shadow-xl);
}
.platform-card h3 {
- color: var(--color-primary);
- margin-bottom: 0.5rem;
+ font-size: 1.25rem;
+ margin-bottom: 0.75rem;
}
-.platform-card .status {
- font-size: 0.9rem;
- margin-bottom: 1rem;
+/* Buttons */
+.btn-primary {
+ background: var(--color-primary);
+ color: #fff;
+ font-weight: 600;
+ padding: 0.75rem 1.5rem;
+ border-radius: var(--radius-md);
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
}
-.active-platform {
- border-color: var(--color-success);
+.btn-primary:hover {
+ filter: brightness(1.1);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 15px var(--color-primary-glow);
}
-.active-platform h3 {
- color: var(--color-success);
+.btn-secondary {
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--color-text);
+ font-weight: 500;
+ padding: 0.75rem 1.5rem;
+ border-radius: var(--radius-md);
+ border: var(--glass-border);
+ cursor: pointer;
+ transition: all 0.2s;
}
-.platform-form {
+.btn-secondary:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+/* Table */
+.table {
+ width: 100%;
+ border-collapse: separate;
+ border-spacing: 0;
+ background: var(--color-surface);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ border: var(--glass-border);
+}
+
+.table th {
+ background: rgba(255, 255, 255, 0.03);
+ padding: 1rem 1.5rem;
+ text-transform: uppercase;
+ font-size: 0.75rem;
+ letter-spacing: 0.05em;
+ color: var(--color-text-dim);
+ text-align: left;
+}
+
+.table td {
+ padding: 1.25rem 1.5rem;
+ border-top: var(--glass-border);
+ font-size: 0.95rem;
+}
+
+.table tr:hover td {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+/* Status Badges */
+.status-pill {
+ padding: 0.25rem 0.75rem;
+ border-radius: 9999px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+.status-new { background: rgba(59, 130, 246, 0.15); color: #60a5fa; }
+.status-closed { background: rgba(16, 185, 129, 0.15); color: #34d399; }
+
+/* Icons */
+.icon-sm {
+ width: 18px;
+ height: 18px;
+ vertical-align: middle;
+ margin-right: 0.25rem;
+ stroke-width: 2.5px;
+}
+
+.icon-sm.spin {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.hidden { display: none !important; }
+
+/* Form Controls */
+input, select, textarea {
+ font-family: inherit;
+ transition: border-color 0.2s;
+}
+
+/* Chart Container */
+canvas {
+ margin-top: 1rem;
+ filter: drop-shadow(0 0 10px rgba(59, 130, 246, 0.1));
+}
+/* Toast Notifications */
+.toast-container {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ z-index: 1000;
display: flex;
flex-direction: column;
- gap: 0.5rem;
+ gap: 0.75rem;
}
-.platform-form input {
- padding: 0.5rem;
- border: 1px solid hsla(0,0%,100%,0.1);
- border-radius: var(--radius);
- background: rgba(0,0,0,0.2);
- color: #fff;
-}
-
-.auth-box {
- max-width: 400px;
- margin: 4rem auto;
- padding: 2rem;
+.toast {
background: var(--color-surface);
- border-radius: var(--radius);
- border: 1px solid hsla(0,0%,100%,0.1);
- box-shadow: var(--shadow);
-}
-
-.auth-box input {
- width: 100%;
- padding: 0.75rem;
- margin-top: 0.5rem;
- border: 1px solid hsla(0,0%,100%,0.1);
- border-radius: var(--radius);
- background: rgba(0,0,0,0.2);
+ backdrop-filter: blur(20px);
+ border: var(--glass-border);
+ padding: 1rem 1.5rem;
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-xl);
color: #fff;
+ min-width: 300px;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ border-left: 4px solid var(--color-primary);
}
-.auth-box a {
- color: var(--color-primary);
- text-decoration: none;
-}
-.auth-box a:hover {
- text-decoration: underline;
+@keyframes slideIn {
+ from { opacity: 0; transform: translateX(100%); }
+ to { opacity: 1; transform: translateX(0); }
}
+
+.toast.success { border-left-color: var(--color-success); }
+.toast.error { border-left-color: var(--color-error); }
+.toast.info { border-left-color: var(--color-primary); }
+
+.toast-message { font-size: 0.9rem; font-weight: 500; }
diff --git a/frontend/index.html b/frontend/index.html
index 408349f..c742ae3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -5,13 +5,16 @@
AutoMarket β Π£Π½ΠΈΠ²Π΅ΡΡΠ°Π»ΡΠ½Π°Ρ ΠΏΠ°Π½Π΅Π»Ρ ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ
-
+
+
+
+