Emirhan Cerrah
commited on
Commit
·
69dbe78
1
Parent(s):
a434056
feat: Railway deployment with Firebase embedding cache system
Browse files- .firebaserc +6 -0
- .gitignore +15 -0
- .railwayignore +50 -0
- EMBEDDING_SYSTEM.md +218 -0
- FIREBASE_AUTH_SETUP.md +342 -0
- FIREBASE_CACHE_QUICKSTART.md +286 -0
- FIREBASE_QUICK_START.md +166 -0
- Procfile +2 -0
- RAILWAY_DEPLOYMENT_GUIDE.md +312 -0
- USER_BASED_EMBEDDING_SYSTEM.md +484 -0
- WEB_AUTH_FEATURES.md +368 -0
- api_server.py +590 -193
- env.example +39 -0
- firebase.json +49 -0
- firebase_setup_guide.md +188 -0
- railway.json +19 -0
- requirements.txt +2 -0
- src/firebase_storage_manager.py +278 -0
- src/user_embedding_pipeline.py +321 -0
- src/user_hybrid_search.py +195 -0
- web/app.js +88 -14
- web/column_mapping.html +5 -0
- web/create_report.html +3 -6
- web/create_report.js +157 -19
- web/data_selection.html +158 -36
- web/data_upload.html +148 -4
- web/firebase-config.js +32 -0
- web/index.html +3 -6
- web/login.html +269 -62
- web/register.html +531 -0
.firebaserc
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"projects": {
|
| 3 |
+
"default": "jira-duplicate-detection"
|
| 4 |
+
}
|
| 5 |
+
}
|
| 6 |
+
|
.gitignore
CHANGED
|
@@ -104,3 +104,18 @@ tmp/
|
|
| 104 |
package-lock.json
|
| 105 |
yarn.lock
|
| 106 |
node_modules/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
package-lock.json
|
| 105 |
yarn.lock
|
| 106 |
node_modules/
|
| 107 |
+
|
| 108 |
+
# Firebase
|
| 109 |
+
firebase-service-account.json
|
| 110 |
+
firebase-service-account-*.json
|
| 111 |
+
.firebase/
|
| 112 |
+
|
| 113 |
+
# User data and embeddings (will be on Railway volume or Firebase)
|
| 114 |
+
data/user_embeddings/
|
| 115 |
+
data/user_data/
|
| 116 |
+
data/user_datasets/
|
| 117 |
+
data/test_*.csv
|
| 118 |
+
data/embedding_outputs/
|
| 119 |
+
|
| 120 |
+
# Test files
|
| 121 |
+
test_*.py
|
.railwayignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Railway Ignore File
|
| 2 |
+
# Bu dosyalar Railway'e upload edilmeyecek
|
| 3 |
+
|
| 4 |
+
# Python cache
|
| 5 |
+
__pycache__/
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
|
| 9 |
+
# Virtual environment
|
| 10 |
+
venv/
|
| 11 |
+
env/
|
| 12 |
+
|
| 13 |
+
# Local development
|
| 14 |
+
.env
|
| 15 |
+
.env.local
|
| 16 |
+
*.log
|
| 17 |
+
|
| 18 |
+
# Firebase credentials (Railway environment variables'dan gelecek)
|
| 19 |
+
firebase-service-account*.json
|
| 20 |
+
|
| 21 |
+
# IDE
|
| 22 |
+
.vscode/
|
| 23 |
+
.idea/
|
| 24 |
+
|
| 25 |
+
# Git
|
| 26 |
+
.git/
|
| 27 |
+
.gitignore
|
| 28 |
+
|
| 29 |
+
# Test files
|
| 30 |
+
test_*.py
|
| 31 |
+
tests/
|
| 32 |
+
|
| 33 |
+
# Documentation (optional)
|
| 34 |
+
*.md
|
| 35 |
+
!README.md
|
| 36 |
+
|
| 37 |
+
# Notebooks
|
| 38 |
+
notebooks/
|
| 39 |
+
*.ipynb
|
| 40 |
+
|
| 41 |
+
# Large data files (bunlar user upload'la gelecek)
|
| 42 |
+
data/data_*.csv
|
| 43 |
+
data/*.xlsx
|
| 44 |
+
data/duplike*.xlsx
|
| 45 |
+
|
| 46 |
+
# Temporary
|
| 47 |
+
tmp/
|
| 48 |
+
temp/
|
| 49 |
+
*.tmp
|
| 50 |
+
|
EMBEDDING_SYSTEM.md
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔄 Dinamik Embedding Sistemi
|
| 2 |
+
|
| 3 |
+
## Genel Bakış
|
| 4 |
+
|
| 5 |
+
Sistem artık **dinamik embedding güncellemesi** destekliyor! Yeni bir rapor eklendiğinde:
|
| 6 |
+
|
| 7 |
+
1. ✅ **CSV dosyasına yazılır**
|
| 8 |
+
2. ✅ **Embedding vektörü otomatik oluşturulur**
|
| 9 |
+
3. ✅ **FAISS index'e eklenir**
|
| 10 |
+
4. ✅ **Hemen arama sonuçlarında görünür!**
|
| 11 |
+
|
| 12 |
+
## Nasıl Çalışır?
|
| 13 |
+
|
| 14 |
+
### 1. Rapor Ekleme
|
| 15 |
+
Kullanıcı yeni bir rapor eklediğinde (`/api/create_report`):
|
| 16 |
+
|
| 17 |
+
```python
|
| 18 |
+
# 1. Rapor CSV'ye yazılır
|
| 19 |
+
df.to_csv(csv_path, ...)
|
| 20 |
+
|
| 21 |
+
# 2. Embedding otomatik oluşturulur
|
| 22 |
+
update_embeddings_for_new_report(report_id)
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
### 2. Embedding Oluşturma
|
| 26 |
+
`update_embeddings_for_new_report()` fonksiyonu:
|
| 27 |
+
|
| 28 |
+
1. **DataFrame'i yeniden yükler** (yeni raporu içeren)
|
| 29 |
+
2. **Bi-encoder modeli ile embedding oluşturur**
|
| 30 |
+
```python
|
| 31 |
+
new_embedding = bi_encoder.encode([combined_text])
|
| 32 |
+
```
|
| 33 |
+
3. **Mevcut embeddings.npy dosyasına ekler**
|
| 34 |
+
4. **FAISS index'e ekler** (platform'a göre: android/ios/unknown)
|
| 35 |
+
5. **Disk'e kaydeder** (kalıcılık için)
|
| 36 |
+
|
| 37 |
+
### 3. Veri Yapısı
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
data/
|
| 41 |
+
├── embedding_outputs/
|
| 42 |
+
│ ├── embeddings.npy # Tüm embedding vektörleri (güncellenebilir)
|
| 43 |
+
│ ├── faiss_index_android.index # Android FAISS index (güncellenebilir)
|
| 44 |
+
│ ├── faiss_index_ios.index # iOS FAISS index (güncellenebilir)
|
| 45 |
+
│ └── faiss_index_unknown.index # Unknown FAISS index (güncellenebilir)
|
| 46 |
+
└── data_with_application.csv # Ana veri dosyası
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Default Data vs Custom Data
|
| 50 |
+
|
| 51 |
+
### Default Data (Varsayılan Veri)
|
| 52 |
+
- ✅ **Embedding otomatik güncellenir**
|
| 53 |
+
- ✅ **FAISS index güncellenir**
|
| 54 |
+
- ✅ **Hızlı arama (hybrid search)**
|
| 55 |
+
|
| 56 |
+
### Custom Data (Kullanıcı Verisi)
|
| 57 |
+
- ⚡ **On-the-fly arama** (text-based matching)
|
| 58 |
+
- 📝 CSV'ye kaydedilir
|
| 59 |
+
- 🔄 Embedding oluşturmaya gerek yok (text matching kullanır)
|
| 60 |
+
|
| 61 |
+
## Performans
|
| 62 |
+
|
| 63 |
+
| İşlem | Süre |
|
| 64 |
+
|-------|------|
|
| 65 |
+
| Yeni rapor ekleme | ~1-2 saniye |
|
| 66 |
+
| Embedding oluşturma | ~0.5 saniye |
|
| 67 |
+
| FAISS index güncelleme | ~0.2 saniye |
|
| 68 |
+
| **Toplam** | **~2-3 saniye** |
|
| 69 |
+
|
| 70 |
+
## Teknik Detaylar
|
| 71 |
+
|
| 72 |
+
### Embedding Boyutu
|
| 73 |
+
- Model: `paraphrase-multilingual-MiniLM-L12-v2`
|
| 74 |
+
- Boyut: **384 dimensions**
|
| 75 |
+
- Format: `float32` (FAISS için optimize edilmiş)
|
| 76 |
+
|
| 77 |
+
### FAISS Index
|
| 78 |
+
- Tip: **IndexFlatIP** (Inner Product - Cosine Similarity)
|
| 79 |
+
- Normalizasyon: Embeddings normalize edilir
|
| 80 |
+
- Platform bazlı: Android, iOS, Unknown için ayrı index'ler
|
| 81 |
+
|
| 82 |
+
### Güncelleme Algoritması
|
| 83 |
+
|
| 84 |
+
```python
|
| 85 |
+
# 1. Mevcut embeddings'i yükle
|
| 86 |
+
existing = np.load("embeddings.npy") # Shape: (N, 384)
|
| 87 |
+
|
| 88 |
+
# 2. Yeni embedding ekle
|
| 89 |
+
new = encoder.encode([text]) # Shape: (1, 384)
|
| 90 |
+
updated = np.vstack([existing, new]) # Shape: (N+1, 384)
|
| 91 |
+
|
| 92 |
+
# 3. Kaydet
|
| 93 |
+
np.save("embeddings.npy", updated)
|
| 94 |
+
|
| 95 |
+
# 4. FAISS'e ekle
|
| 96 |
+
normalized = new / np.linalg.norm(new)
|
| 97 |
+
index.add(normalized.astype('float32'))
|
| 98 |
+
faiss.write_index(index, "faiss_index.index")
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Kullanım Örnekleri
|
| 102 |
+
|
| 103 |
+
### Backend'den Yeni Rapor Ekleme
|
| 104 |
+
|
| 105 |
+
```python
|
| 106 |
+
# POST /api/create_report
|
| 107 |
+
{
|
| 108 |
+
"summary": "BiP mesaj gönderilirken crash oluyor",
|
| 109 |
+
"description": "Kullanıcı mesaj göndermeye çalıştığında...",
|
| 110 |
+
"component": "Android",
|
| 111 |
+
"priority": "High"
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
# Response
|
| 115 |
+
{
|
| 116 |
+
"success": true,
|
| 117 |
+
"report_id": 1234,
|
| 118 |
+
"embeddings_updated": true # ✅ Embedding güncellendi!
|
| 119 |
+
}
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### Frontend Bildirimi
|
| 123 |
+
|
| 124 |
+
Rapor kaydedildiğinde kullanıcı görür:
|
| 125 |
+
```
|
| 126 |
+
✅ Rapor başarıyla kaydedildi!
|
| 127 |
+
Rapor ID: 1234
|
| 128 |
+
✅ Embedding oluşturuldu - Rapor hemen arama sonuçlarında görünecek
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## Hata Durumları
|
| 132 |
+
|
| 133 |
+
### Embedding Oluşturulamazsa
|
| 134 |
+
|
| 135 |
+
```python
|
| 136 |
+
⚠️ Could not update embeddings: [error message]
|
| 137 |
+
⚠️ New report will not appear in search until system restart!
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Bu durumda:
|
| 141 |
+
1. Rapor CSV'ye kaydedilir ✅
|
| 142 |
+
2. Ama arama sonuçlarında çıkmaz ❌
|
| 143 |
+
3. Sistem yeniden başlatılınca embedding pipeline çalıştırılmalı
|
| 144 |
+
|
| 145 |
+
### Çözüm
|
| 146 |
+
|
| 147 |
+
```bash
|
| 148 |
+
# Tüm embeddings'i yeniden oluştur
|
| 149 |
+
python src/embedding_pipeline.py
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
## Sınırlamalar
|
| 153 |
+
|
| 154 |
+
1. **Tek seferde 1 rapor**: Her rapor için ayrı embedding oluşturulur
|
| 155 |
+
2. **Bellek kullanımı**: Her rapor ~1.5KB (384 float32)
|
| 156 |
+
3. **Disk yazma**: Her ekleme için 2 dosya güncellenir (embeddings.npy + faiss_index.index)
|
| 157 |
+
|
| 158 |
+
## Gelecek İyileştirmeler
|
| 159 |
+
|
| 160 |
+
- [ ] Batch embedding update (birden fazla rapor için)
|
| 161 |
+
- [ ] Asenkron embedding (kullanıcıyı bekletmeden)
|
| 162 |
+
- [ ] Embedding cache sistemi
|
| 163 |
+
- [ ] Incremental FAISS index (daha hızlı güncelleme)
|
| 164 |
+
- [ ] Custom data için de embedding desteği
|
| 165 |
+
|
| 166 |
+
## Log Örnekleri
|
| 167 |
+
|
| 168 |
+
Başarılı ekleme:
|
| 169 |
+
```
|
| 170 |
+
✅ New report created: ID=1234, App=BiP, Summary=...
|
| 171 |
+
🔄 Updating embeddings for new report...
|
| 172 |
+
📥 Reloading data to include new report...
|
| 173 |
+
🔄 Generating embedding for new report: 'BiP mesaj gönderilirken...'
|
| 174 |
+
📊 Loaded existing embeddings: (1233, 384)
|
| 175 |
+
✅ New embeddings shape: (1234, 384)
|
| 176 |
+
💾 Saved updated embeddings to data/embedding_outputs/embeddings.npy
|
| 177 |
+
🔄 Adding to FAISS index: android
|
| 178 |
+
💾 Saved updated FAISS index to data/embedding_outputs/faiss_index_android.index
|
| 179 |
+
✅ Successfully added new report to FAISS index (android)
|
| 180 |
+
✅ Embeddings updated successfully!
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
## Test Senaryosu
|
| 184 |
+
|
| 185 |
+
1. Yeni rapor ekle
|
| 186 |
+
2. Hemen arama yap
|
| 187 |
+
3. Yeni rapor sonuçlarda görünmeli ✅
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
# 1. Rapor ekle
|
| 191 |
+
curl -X POST http://localhost:5001/api/create_report \
|
| 192 |
+
-H "Content-Type: application/json" \
|
| 193 |
+
-d '{"summary": "Test rapor", "description": "Test açıklaması"}'
|
| 194 |
+
|
| 195 |
+
# 2. Hemen ara
|
| 196 |
+
curl -X POST http://localhost:5001/api/search \
|
| 197 |
+
-H "Content-Type: application/json" \
|
| 198 |
+
-d '{"query": "Test rapor", "top_k": 5}'
|
| 199 |
+
|
| 200 |
+
# ✅ Yeni rapor sonuçlarda görünür!
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
## Özet
|
| 204 |
+
|
| 205 |
+
| Özellik | Durum |
|
| 206 |
+
|---------|-------|
|
| 207 |
+
| Dinamik embedding | ✅ Aktif |
|
| 208 |
+
| FAISS index güncelleme | ✅ Aktif |
|
| 209 |
+
| Hızlı arama | ✅ Aktif |
|
| 210 |
+
| Custom data desteği | ✅ Aktif (text-based) |
|
| 211 |
+
| Asenkron işlem | ⏳ Gelecek |
|
| 212 |
+
| Batch update | ⏳ Gelecek |
|
| 213 |
+
|
| 214 |
+
---
|
| 215 |
+
|
| 216 |
+
**Not**: Bu sistem production-ready'dir ve aktif olarak kullanılabilir. Herhangi bir sorun durumunda sistem loglarını kontrol edin.
|
| 217 |
+
|
| 218 |
+
|
FIREBASE_AUTH_SETUP.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔥 Firebase Authentication Kurulum Kılavuzu
|
| 2 |
+
|
| 3 |
+
## 📋 İçindekiler
|
| 4 |
+
1. [Firebase Projesi Oluşturma](#1-firebase-projesi-oluşturma)
|
| 5 |
+
2. [Firebase Authentication Aktifleştirme](#2-firebase-authentication-aktifleştirme)
|
| 6 |
+
3. [Firebase Firestore Aktifleştirme](#3-firebase-firestore-aktifleştirme)
|
| 7 |
+
4. [Proje Yapılandırması](#4-proje-yapılandırması)
|
| 8 |
+
5. [Kullanım](#5-kullanım)
|
| 9 |
+
6. [Sorun Giderme](#6-sorun-giderme)
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## 1. Firebase Projesi Oluşturma
|
| 14 |
+
|
| 15 |
+
### Adım 1: Firebase Console'a Giriş
|
| 16 |
+
1. https://console.firebase.google.com/ adresine gidin
|
| 17 |
+
2. Google hesabınızla giriş yapın
|
| 18 |
+
3. **"Add project"** (Proje Ekle) butonuna tıklayın
|
| 19 |
+
|
| 20 |
+
### Adım 2: Proje Ayarları
|
| 21 |
+
1. **Proje Adı**: `bug-report-system` (veya istediğiniz isim)
|
| 22 |
+
2. **Google Analytics**: İsteğe bağlı (kapatabilirsiniz)
|
| 23 |
+
3. **Create Project** butonuna tıklayın
|
| 24 |
+
4. Proje hazır olana kadar bekleyin (~1 dakika)
|
| 25 |
+
|
| 26 |
+
### Adım 3: Web App Ekleme
|
| 27 |
+
1. Firebase Console'da projenizi açın
|
| 28 |
+
2. **"</>** (Web)" ikonuna tıklayın
|
| 29 |
+
3. **App nickname**: `Bug Report Web App`
|
| 30 |
+
4. **Firebase Hosting**: ✅ İşaretleyin (opsiyonel)
|
| 31 |
+
5. **Register app** butonuna tıklayın
|
| 32 |
+
|
| 33 |
+
### Adım 4: Firebase Config Bilgilerini Kopyalayın
|
| 34 |
+
```javascript
|
| 35 |
+
// Bu bilgileri kaydedin - sonra kullanacağız!
|
| 36 |
+
const firebaseConfig = {
|
| 37 |
+
apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
| 38 |
+
authDomain: "bug-report-system.firebaseapp.com",
|
| 39 |
+
projectId: "bug-report-system",
|
| 40 |
+
storageBucket: "bug-report-system.appspot.com",
|
| 41 |
+
messagingSenderId: "123456789012",
|
| 42 |
+
appId: "1:123456789012:web:abcdef1234567890"
|
| 43 |
+
};
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 2. Firebase Authentication Aktifleştirme
|
| 49 |
+
|
| 50 |
+
### Adım 1: Authentication'ı Aktifleştir
|
| 51 |
+
1. Sol menüden **"Build"** > **"Authentication"** seçin
|
| 52 |
+
2. **"Get started"** butonuna tıklayın
|
| 53 |
+
|
| 54 |
+
### Adım 2: Email/Password Sign-in Metodunu Aktifleştir
|
| 55 |
+
1. **"Sign-in method"** tab'ına gidin
|
| 56 |
+
2. **"Email/Password"** seçeneğini bulun
|
| 57 |
+
3. **"Enable"** toggle'ını açın
|
| 58 |
+
4. **"Email link (passwordless sign-in)"** KAPALI bırakın
|
| 59 |
+
5. **"Save"** butonuna tıklayın
|
| 60 |
+
|
| 61 |
+
✅ Artık kullanıcılar e-posta ve şifre ile kayıt olabilir!
|
| 62 |
+
|
| 63 |
+
---
|
| 64 |
+
|
| 65 |
+
## 3. Firebase Firestore Aktifleştirme
|
| 66 |
+
|
| 67 |
+
### Adım 1: Firestore Database Oluştur
|
| 68 |
+
1. Sol menüden **"Build"** > **"Firestore Database"** seçin
|
| 69 |
+
2. **"Create database"** butonuna tıklayın
|
| 70 |
+
|
| 71 |
+
### Adım 2: Güvenlik Kurallarını Seç
|
| 72 |
+
**Test Mode** seçin (geliştirme için):
|
| 73 |
+
```javascript
|
| 74 |
+
rules_version = '2';
|
| 75 |
+
service cloud.firestore {
|
| 76 |
+
match /databases/{database}/documents {
|
| 77 |
+
match /{document=**} {
|
| 78 |
+
allow read, write: if request.auth != null;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
### Adım 3: Location Seç
|
| 85 |
+
- **eur3 (europe-west)** seçin (Türkiye'ye en yakın)
|
| 86 |
+
- **Enable** butonuna tıklayın
|
| 87 |
+
|
| 88 |
+
### Adım 4: Firestore Güvenlik Kurallarını Güncelle (Önemli!)
|
| 89 |
+
```javascript
|
| 90 |
+
rules_version = '2';
|
| 91 |
+
service cloud.firestore {
|
| 92 |
+
match /databases/{database}/documents {
|
| 93 |
+
// Users collection - sadece kendi verisini okuyabilir
|
| 94 |
+
match /users/{userId} {
|
| 95 |
+
allow read: if request.auth != null && request.auth.uid == userId;
|
| 96 |
+
allow create: if request.auth != null;
|
| 97 |
+
allow update: if request.auth != null && request.auth.uid == userId;
|
| 98 |
+
allow delete: if false; // Kullanıcılar kendi hesaplarını silemez
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Reports collection - tüm authenticated kullanıcılar okuyabilir
|
| 102 |
+
match /reports/{reportId} {
|
| 103 |
+
allow read: if request.auth != null;
|
| 104 |
+
allow create: if request.auth != null;
|
| 105 |
+
allow update: if request.auth != null;
|
| 106 |
+
allow delete: if request.auth != null;
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
**Publish** butonuna tıklayın.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## 4. Proje Yapılandırması
|
| 117 |
+
|
| 118 |
+
### Adım 1: Firebase Config Dosyasını Güncelle
|
| 119 |
+
|
| 120 |
+
`web/firebase-config.js` dosyasını açın ve Firebase Console'dan aldığınız bilgilerle güncelleyin:
|
| 121 |
+
|
| 122 |
+
```javascript
|
| 123 |
+
// Firebase Configuration
|
| 124 |
+
const firebaseConfig = {
|
| 125 |
+
apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", // BURAYA KENDİ DEĞERLER
|
| 126 |
+
authDomain: "bug-report-system.firebaseapp.com",
|
| 127 |
+
projectId: "bug-report-system",
|
| 128 |
+
storageBucket: "bug-report-system.appspot.com",
|
| 129 |
+
messagingSenderId: "123456789012",
|
| 130 |
+
appId: "1:123456789012:web:abcdef1234567890"
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
// Initialize Firebase
|
| 134 |
+
firebase.initializeApp(firebaseConfig);
|
| 135 |
+
|
| 136 |
+
// Get Firebase services
|
| 137 |
+
const auth = firebase.auth();
|
| 138 |
+
const db = firebase.firestore();
|
| 139 |
+
|
| 140 |
+
console.log('🔥 Firebase initialized successfully!');
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
### Adım 2: Firebase Hosting (Opsiyonel)
|
| 144 |
+
|
| 145 |
+
Eğer Firebase Hosting kullanmak isterseniz:
|
| 146 |
+
|
| 147 |
+
```bash
|
| 148 |
+
# Firebase CLI'yi yükleyin
|
| 149 |
+
npm install -g firebase-tools
|
| 150 |
+
|
| 151 |
+
# Firebase'e login olun
|
| 152 |
+
firebase login
|
| 153 |
+
|
| 154 |
+
# Projeyi başlatın
|
| 155 |
+
firebase init
|
| 156 |
+
|
| 157 |
+
# Seçenekler:
|
| 158 |
+
# - Hosting: Configure files for Firebase Hosting
|
| 159 |
+
# - What do you want to use as your public directory? -> web
|
| 160 |
+
# - Configure as a single-page app? -> Yes
|
| 161 |
+
# - Set up automatic builds and deploys with GitHub? -> No
|
| 162 |
+
|
| 163 |
+
# Deploy edin
|
| 164 |
+
firebase deploy
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## 5. Kullanım
|
| 170 |
+
|
| 171 |
+
### Kullanıcı Kayıt Olma
|
| 172 |
+
|
| 173 |
+
1. Tarayıcıda `register.html` sayfasını açın
|
| 174 |
+
2. Formu doldurun:
|
| 175 |
+
- **Ad Soyad**: En az 3 karakter
|
| 176 |
+
- **E-posta**: Geçerli e-posta adresi
|
| 177 |
+
- **Şifre**: En az 6 karakter (büyük harf, küçük harf, rakam önerilir)
|
| 178 |
+
- **Rol**: Kullanıcı/Admin/Developer/Tester
|
| 179 |
+
3. **"Kayıt Ol"** butonuna tıklayın
|
| 180 |
+
4. Başarılı olursa otomatik olarak login sayfasına yönlendirileceksiniz
|
| 181 |
+
|
| 182 |
+
### Kullanıcı Giriş Yapma
|
| 183 |
+
|
| 184 |
+
1. Tarayıcıda `login.html` sayfasını açın
|
| 185 |
+
2. E-posta ve şifrenizi girin
|
| 186 |
+
3. **"Giriş Yap"** butonuna tıklayın
|
| 187 |
+
4. Başarılı olursa `data_selection.html` sayfasına yönlendirileceksiniz
|
| 188 |
+
|
| 189 |
+
### Şifre Sıfırlama
|
| 190 |
+
|
| 191 |
+
1. Login sayfasında **"Şifremi unuttum?"** linkine tıklayın
|
| 192 |
+
2. E-posta adresinizi girin
|
| 193 |
+
3. E-posta kutunuzu kontrol edin
|
| 194 |
+
4. Gelen linke tıklayarak yeni şifre oluşturun
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 6. Sorun Giderme
|
| 199 |
+
|
| 200 |
+
### Hata: "Firebase yapılandırılmamış!"
|
| 201 |
+
|
| 202 |
+
**Neden**: `firebase-config.js` dosyasındaki API key'ler güncellenmemiş.
|
| 203 |
+
|
| 204 |
+
**Çözüm**:
|
| 205 |
+
1. Firebase Console'dan config bilgilerini kopyalayın
|
| 206 |
+
2. `web/firebase-config.js` dosyasını güncelleyin
|
| 207 |
+
3. Sayfayı yenileyin
|
| 208 |
+
|
| 209 |
+
---
|
| 210 |
+
|
| 211 |
+
### Hata: "Email already in use"
|
| 212 |
+
|
| 213 |
+
**Neden**: Bu e-posta adresi ile zaten bir kullanıcı kayıtlı.
|
| 214 |
+
|
| 215 |
+
**Çözüm**:
|
| 216 |
+
- Farklı bir e-posta adresi kullanın
|
| 217 |
+
- Veya login yapın
|
| 218 |
+
|
| 219 |
+
---
|
| 220 |
+
|
| 221 |
+
### Hata: "CORS error" veya "Firebase is not defined"
|
| 222 |
+
|
| 223 |
+
**Neden**: Firebase SDK'ları yüklenmemiş.
|
| 224 |
+
|
| 225 |
+
**Çözüm**:
|
| 226 |
+
HTML dosyalarında bu satırları kontrol edin:
|
| 227 |
+
```html
|
| 228 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
| 229 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
|
| 230 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
| 231 |
+
<script src="firebase-config.js"></script>
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
---
|
| 235 |
+
|
| 236 |
+
### Hata: "Permission denied" (Firestore)
|
| 237 |
+
|
| 238 |
+
**Neden**: Firestore güvenlik kuralları yanlış yapılandırılmış.
|
| 239 |
+
|
| 240 |
+
**Çözüm**:
|
| 241 |
+
1. Firebase Console > Firestore Database > Rules
|
| 242 |
+
2. Yukarıdaki güvenlik kurallarını kopyalayın
|
| 243 |
+
3. **Publish** edin
|
| 244 |
+
|
| 245 |
+
---
|
| 246 |
+
|
| 247 |
+
### Demo Mod Kullanımı
|
| 248 |
+
|
| 249 |
+
Firebase yapılandırılmamışsa sistem otomatik olarak **Demo Mod**'a geçer:
|
| 250 |
+
|
| 251 |
+
**Demo Hesaplar**:
|
| 252 |
+
- E-posta: `[email protected]`
|
| 253 |
+
- Şifre: `demo123`
|
| 254 |
+
|
| 255 |
+
veya
|
| 256 |
+
|
| 257 |
+
- E-posta: `[email protected]`
|
| 258 |
+
- Şifre: `admin123`
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## 📊 Firestore Veri Yapısı
|
| 263 |
+
|
| 264 |
+
### Users Collection
|
| 265 |
+
|
| 266 |
+
```javascript
|
| 267 |
+
/users/{userId}
|
| 268 |
+
{
|
| 269 |
+
uid: "firebase-user-id",
|
| 270 |
+
email: "[email protected]",
|
| 271 |
+
fullName: "Ahmet Yılmaz",
|
| 272 |
+
role: "admin", // user, admin, developer, tester
|
| 273 |
+
createdAt: Timestamp,
|
| 274 |
+
lastLogin: Timestamp
|
| 275 |
+
}
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
### Reports Collection (Gelecek için)
|
| 279 |
+
|
| 280 |
+
```javascript
|
| 281 |
+
/reports/{reportId}
|
| 282 |
+
{
|
| 283 |
+
userId: "firebase-user-id",
|
| 284 |
+
summary: "Bug summary",
|
| 285 |
+
description: "Bug description",
|
| 286 |
+
application: "BiP",
|
| 287 |
+
platform: "android",
|
| 288 |
+
priority: "high",
|
| 289 |
+
createdAt: Timestamp,
|
| 290 |
+
updatedAt: Timestamp
|
| 291 |
+
}
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
---
|
| 295 |
+
|
| 296 |
+
## 🔐 Güvenlik Önerileri
|
| 297 |
+
|
| 298 |
+
1. **API Key'leri Saklamayın**:
|
| 299 |
+
- `.gitignore` dosyasına `firebase-config.js` ekleyin
|
| 300 |
+
- Production'da environment variables kullanın
|
| 301 |
+
|
| 302 |
+
2. **Güvenlik Kurallarını Sıkılaştırın**:
|
| 303 |
+
- Test mode'dan production mode'a geçin
|
| 304 |
+
- Sadece gerekli read/write izinleri verin
|
| 305 |
+
|
| 306 |
+
3. **Email Verification Ekleyin**:
|
| 307 |
+
```javascript
|
| 308 |
+
await user.sendEmailVerification();
|
| 309 |
+
```
|
| 310 |
+
|
| 311 |
+
4. **Rate Limiting**:
|
| 312 |
+
- Firebase App Check kullanın
|
| 313 |
+
- Çok fazla başarısız giriş denemelerini engelleyin
|
| 314 |
+
|
| 315 |
+
---
|
| 316 |
+
|
| 317 |
+
## ✅ Checklist
|
| 318 |
+
|
| 319 |
+
- [ ] Firebase projesi oluşturuldu
|
| 320 |
+
- [ ] Authentication aktifleştirildi
|
| 321 |
+
- [ ] Firestore aktifleştirildi
|
| 322 |
+
- [ ] `firebase-config.js` güncellendi
|
| 323 |
+
- [ ] Kayıt sayfası test edildi
|
| 324 |
+
- [ ] Login sayfası test edildi
|
| 325 |
+
- [ ] Şifre sıfırlama test edildi
|
| 326 |
+
- [ ] Firestore güvenlik kuralları güncellendi
|
| 327 |
+
|
| 328 |
+
---
|
| 329 |
+
|
| 330 |
+
## 📚 Ek Kaynaklar
|
| 331 |
+
|
| 332 |
+
- [Firebase Authentication Docs](https://firebase.google.com/docs/auth)
|
| 333 |
+
- [Firebase Firestore Docs](https://firebase.google.com/docs/firestore)
|
| 334 |
+
- [Firebase Security Rules](https://firebase.google.com/docs/rules)
|
| 335 |
+
- [Firebase Best Practices](https://firebase.google.com/docs/rules/rules-and-auth)
|
| 336 |
+
|
| 337 |
+
---
|
| 338 |
+
|
| 339 |
+
**Yardıma mı ihtiyacınız var?** Firebase Console'da sağ altta bulunan "?" butonuna tıklayarak support alabilirsiniz.
|
| 340 |
+
|
| 341 |
+
🎉 **Kurulum Tamamlandı!** Artık kullanıcılar Firebase ile kayıt olup giriş yapabilir!
|
| 342 |
+
|
FIREBASE_CACHE_QUICKSTART.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Firebase Embedding Cache - Quick Start
|
| 2 |
+
|
| 3 |
+
## ✨ Yeni Özellik: Embedding Cache Sistemi
|
| 4 |
+
|
| 5 |
+
Artık embedding'ler bir kere yapılıyor ve Firebase Storage'da kalıcı olarak saklanıyor!
|
| 6 |
+
|
| 7 |
+
### 🎯 Öncesi vs Sonrası
|
| 8 |
+
|
| 9 |
+
| Önceki Durum | Yeni Durum |
|
| 10 |
+
|--------------|------------|
|
| 11 |
+
| Her login'de 10-15 saniye embedding ❌ | İlk seferde 10-15 saniye, sonra 2-3 saniye ✅ |
|
| 12 |
+
| Server restart → embedding kaybolur ❌ | Firebase'de kalıcı ✅ |
|
| 13 |
+
| Production'da kullanılamaz ❌ | Production-ready ✅ |
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🔧 Local Development Setup
|
| 18 |
+
|
| 19 |
+
### 1. Environment Variables
|
| 20 |
+
|
| 21 |
+
`env.example` dosyasını `.env` olarak kopyala:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
cp env.example .env
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### 2. Firebase Service Account
|
| 28 |
+
|
| 29 |
+
1. Firebase Console → Project Settings → Service Accounts
|
| 30 |
+
2. Generate New Private Key → JSON indir
|
| 31 |
+
3. `firebase-service-account.json` olarak kaydet
|
| 32 |
+
4. `.env` dosyasında path'i ayarla:
|
| 33 |
+
|
| 34 |
+
```bash
|
| 35 |
+
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
|
| 36 |
+
FIREBASE_STORAGE_BUCKET=jira-duplicate-detection.appspot.com
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
### 3. Dependencies Install
|
| 40 |
+
|
| 41 |
+
```bash
|
| 42 |
+
pip install -r requirements.txt
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
### 4. Run Server
|
| 46 |
+
|
| 47 |
+
```bash
|
| 48 |
+
python api_server.py
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
Görmen gereken:
|
| 52 |
+
```
|
| 53 |
+
🔥 Firebase Configuration:
|
| 54 |
+
• Service Account: ./firebase-service-account.json
|
| 55 |
+
• Storage Bucket: jira-duplicate-detection.appspot.com
|
| 56 |
+
✅ Firebase Storage initialized
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 📦 Nasıl Çalışıyor?
|
| 62 |
+
|
| 63 |
+
### İlk Data Upload:
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
# 1. Kullanıcı veriyi upload ediyor
|
| 67 |
+
POST /api/upload_data
|
| 68 |
+
|
| 69 |
+
# 2. Backend embedding yapıyor (10-15 saniye)
|
| 70 |
+
UserEmbeddingPipeline.process()
|
| 71 |
+
├─ embeddings.npy oluşturuluyor
|
| 72 |
+
├─ faiss_index.bin oluşturuluyor
|
| 73 |
+
└─ metadata.json oluşturuluyor
|
| 74 |
+
|
| 75 |
+
# 3. Firebase Storage'a upload ediliyor
|
| 76 |
+
FirebaseStorageManager.upload_user_artifacts()
|
| 77 |
+
└─ user_embeddings/{userId}/
|
| 78 |
+
├─ embeddings.npy
|
| 79 |
+
├─ faiss_index.bin
|
| 80 |
+
├─ metadata.json
|
| 81 |
+
└─ data.csv
|
| 82 |
+
|
| 83 |
+
# ✅ Kullanıcı search yapabiliyor
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
### Sonraki Login'ler:
|
| 87 |
+
|
| 88 |
+
```python
|
| 89 |
+
# 1. Kullanıcı login yapıyor
|
| 90 |
+
# 2. Backend kontrol ediyor
|
| 91 |
+
|
| 92 |
+
if artifacts_exist_in_firebase:
|
| 93 |
+
# ✅ Firebase'den indir (2-3 saniye)
|
| 94 |
+
download_from_firebase()
|
| 95 |
+
return True # Embedding yapma!
|
| 96 |
+
else:
|
| 97 |
+
# ❌ Cache miss, yeni embedding yap
|
| 98 |
+
generate_embeddings()
|
| 99 |
+
upload_to_firebase()
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
---
|
| 103 |
+
|
| 104 |
+
## 🧪 Test Senaryosu
|
| 105 |
+
|
| 106 |
+
### Senaryo 1: Yeni Kullanıcı (Cache Miss)
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
# 1. Yeni kullanıcı ile login yap
|
| 110 |
+
# 2. Data upload et
|
| 111 |
+
# 3. Log'lara bak:
|
| 112 |
+
|
| 113 |
+
🔍 Checking Firebase Storage for cached embeddings...
|
| 114 |
+
📝 No cached embeddings found, will generate new ones
|
| 115 |
+
📊 Data shape: (15, 5)
|
| 116 |
+
🧮 Creating embeddings...
|
| 117 |
+
✅ Created embeddings: (15, 384)
|
| 118 |
+
📤 Uploading embeddings to Firebase Storage...
|
| 119 |
+
✅ Uploaded: embeddings.npy (45.6 KB)
|
| 120 |
+
✅ Uploaded: faiss_index.bin (12.3 KB)
|
| 121 |
+
✅ Embeddings cached to Firebase Storage!
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
### Senaryo 2: Aynı Kullanıcı (Cache Hit)
|
| 125 |
+
|
| 126 |
+
```bash
|
| 127 |
+
# 1. Logout yap
|
| 128 |
+
# 2. Tekrar login yap
|
| 129 |
+
# 3. Arama yap
|
| 130 |
+
# 4. Log'lara bak:
|
| 131 |
+
|
| 132 |
+
🔍 Checking Firebase Storage for cached embeddings...
|
| 133 |
+
✅ Found cached embeddings in Firebase Storage!
|
| 134 |
+
📥 Downloading artifacts for user xyz...
|
| 135 |
+
✅ Downloaded: embeddings.npy (45.6 KB)
|
| 136 |
+
✅ Downloaded: faiss_index.bin (12.3 KB)
|
| 137 |
+
⏩ Skipping embedding generation - using cached version
|
| 138 |
+
✅ Search completed in 0.5s (vs 15s without cache!)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
---
|
| 142 |
+
|
| 143 |
+
## 🔒 Security
|
| 144 |
+
|
| 145 |
+
### Firebase Storage Rules:
|
| 146 |
+
|
| 147 |
+
```javascript
|
| 148 |
+
rules_version = '2';
|
| 149 |
+
service firebase.storage {
|
| 150 |
+
match /b/{bucket}/o {
|
| 151 |
+
match /user_embeddings/{userId}/{allPaths=**} {
|
| 152 |
+
// Her kullanıcı sadece kendi artifacts'ına erişebilir
|
| 153 |
+
allow read, write: if request.auth != null
|
| 154 |
+
&& request.auth.uid == userId;
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
## 🐛 Troubleshooting
|
| 163 |
+
|
| 164 |
+
### "Firebase Storage not initialized"
|
| 165 |
+
|
| 166 |
+
`.env` dosyasını kontrol et:
|
| 167 |
+
```bash
|
| 168 |
+
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
|
| 169 |
+
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
Service account JSON dosyası var mı?
|
| 173 |
+
```bash
|
| 174 |
+
ls -la firebase-service-account.json
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
### Cache Kullanılmıyor
|
| 178 |
+
|
| 179 |
+
Logs'ta şunu gör:
|
| 180 |
+
```
|
| 181 |
+
⚠️ Firebase Storage not initialized, skipping download
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
Çözüm:
|
| 185 |
+
```bash
|
| 186 |
+
# .env dosyasını kontrol et
|
| 187 |
+
cat .env | grep FIREBASE
|
| 188 |
+
|
| 189 |
+
# Service account path doğru mu?
|
| 190 |
+
python -c "import os; print(os.path.exists('./firebase-service-account.json'))"
|
| 191 |
+
```
|
| 192 |
+
|
| 193 |
+
### Cache Devre Dışı Bırakma (Development)
|
| 194 |
+
|
| 195 |
+
```bash
|
| 196 |
+
# .env dosyasında:
|
| 197 |
+
USE_FIREBASE_CACHE=False
|
| 198 |
+
```
|
| 199 |
+
|
| 200 |
+
---
|
| 201 |
+
|
| 202 |
+
## 📊 Monitoring
|
| 203 |
+
|
| 204 |
+
### Firebase Console:
|
| 205 |
+
|
| 206 |
+
Storage → `user_embeddings/` → Her kullanıcı için folder görünecek
|
| 207 |
+
|
| 208 |
+
### Railway Logs:
|
| 209 |
+
|
| 210 |
+
```
|
| 211 |
+
✅ Embeddings cached to Firebase Storage! # Upload success
|
| 212 |
+
⏩ Skipping embedding generation # Cache hit
|
| 213 |
+
📥 Downloading artifacts # Cache download
|
| 214 |
+
```
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## 🚀 Production Deployment
|
| 219 |
+
|
| 220 |
+
Detaylı guide: [RAILWAY_DEPLOYMENT_GUIDE.md](./RAILWAY_DEPLOYMENT_GUIDE.md)
|
| 221 |
+
|
| 222 |
+
Kısaca:
|
| 223 |
+
1. Railway projesi oluştur
|
| 224 |
+
2. Environment variables ekle
|
| 225 |
+
3. Service account upload et
|
| 226 |
+
4. Deploy!
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## 💡 Best Practices
|
| 231 |
+
|
| 232 |
+
### 1. Cache Invalidation
|
| 233 |
+
|
| 234 |
+
Veri değiştiğinde cache otomatik güncellenir:
|
| 235 |
+
- Rapor eklendiğinde
|
| 236 |
+
- Rapor değiştirildiğinde
|
| 237 |
+
- Yeni data upload edildiğinde
|
| 238 |
+
|
| 239 |
+
### 2. Storage Optimization
|
| 240 |
+
|
| 241 |
+
- Artifacts compress et (opsiyonel):
|
| 242 |
+
```python
|
| 243 |
+
import gzip
|
| 244 |
+
with gzip.open('embeddings.npy.gz', 'wb') as f:
|
| 245 |
+
np.save(f, embeddings)
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
### 3. Cost Management
|
| 249 |
+
|
| 250 |
+
- Cache TTL ekle (örn: 30 gün sonra expire)
|
| 251 |
+
- Inactive user artifacts'ları temizle
|
| 252 |
+
- Compression kullan
|
| 253 |
+
|
| 254 |
+
---
|
| 255 |
+
|
| 256 |
+
## 📈 Performance Metrics
|
| 257 |
+
|
| 258 |
+
| Metrik | Öncesi | Sonrası | İyileşme |
|
| 259 |
+
|--------|--------|---------|----------|
|
| 260 |
+
| İlk embedding | 15s | 15s | - |
|
| 261 |
+
| Sonraki login'ler | 15s | 2-3s | **5x hızlı** |
|
| 262 |
+
| Server restart impact | Tüm cache kaybolur | Cache kalıcı | **∞x iyi** |
|
| 263 |
+
| User experience | Kötü | Mükemmel | ⭐⭐⭐⭐⭐ |
|
| 264 |
+
|
| 265 |
+
---
|
| 266 |
+
|
| 267 |
+
## ✅ Checklist
|
| 268 |
+
|
| 269 |
+
Aşağıdakileri kontrol et:
|
| 270 |
+
|
| 271 |
+
- [ ] `firebase-admin` ve `python-dotenv` kurulu
|
| 272 |
+
- [ ] `.env` dosyası oluşturuldu
|
| 273 |
+
- [ ] Firebase service account JSON indirildi
|
| 274 |
+
- [ ] Service account path `.env`'de doğru
|
| 275 |
+
- [ ] Storage bucket name doğru
|
| 276 |
+
- [ ] Firebase Storage Rules eklendi
|
| 277 |
+
- [ ] Local test: Cache miss → embedding yapılıyor
|
| 278 |
+
- [ ] Local test: Cache hit → embedding skip ediliyor
|
| 279 |
+
- [ ] Production deploy: Railway environment variables set edildi
|
| 280 |
+
|
| 281 |
+
---
|
| 282 |
+
|
| 283 |
+
**🎉 Artık production-ready bir embedding cache sisteminiz var!**
|
| 284 |
+
|
| 285 |
+
Sorular için: GitHub Issues veya [RAILWAY_DEPLOYMENT_GUIDE.md](./RAILWAY_DEPLOYMENT_GUIDE.md)
|
| 286 |
+
|
FIREBASE_QUICK_START.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Firebase Authentication - Hızlı Başlangıç
|
| 2 |
+
|
| 3 |
+
## 5 Dakikada Firebase Kurulumu
|
| 4 |
+
|
| 5 |
+
### 1️⃣ Firebase Projesi Oluştur (2 dakika)
|
| 6 |
+
|
| 7 |
+
1. https://console.firebase.google.com/ → **Add Project**
|
| 8 |
+
2. Proje adı: `bug-report-system`
|
| 9 |
+
3. **Create Project**
|
| 10 |
+
|
| 11 |
+
### 2️⃣ Authentication'ı Aktifleştir (1 dakika)
|
| 12 |
+
|
| 13 |
+
1. Sol menü → **Authentication** → **Get Started**
|
| 14 |
+
2. **Sign-in method** tab → **Email/Password** → **Enable** → **Save**
|
| 15 |
+
|
| 16 |
+
### 3️⃣ Firestore'u Aktifleştir (1 dakika)
|
| 17 |
+
|
| 18 |
+
1. Sol menü → **Firestore Database** → **Create database**
|
| 19 |
+
2. **Test mode** seç → **eur3 (europe-west)** → **Enable**
|
| 20 |
+
|
| 21 |
+
### 4️⃣ Config'i Kopyala (1 dakika)
|
| 22 |
+
|
| 23 |
+
1. Proje ayarları (⚙️) → Scroll down → **Your apps** bölümünden **Web app** ekle
|
| 24 |
+
2. Config kodunu kopyala:
|
| 25 |
+
|
| 26 |
+
```javascript
|
| 27 |
+
const firebaseConfig = {
|
| 28 |
+
apiKey: "...",
|
| 29 |
+
authDomain: "...",
|
| 30 |
+
projectId: "...",
|
| 31 |
+
storageBucket: "...",
|
| 32 |
+
messagingSenderId: "...",
|
| 33 |
+
appId: "..."
|
| 34 |
+
};
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 5️⃣ Yapılandırmayı Güncelle (30 saniye)
|
| 38 |
+
|
| 39 |
+
`web/firebase-config.js` dosyasını aç ve config'i yapıştır:
|
| 40 |
+
|
| 41 |
+
```javascript
|
| 42 |
+
const firebaseConfig = {
|
| 43 |
+
apiKey: "BURAYA_API_KEY", // ← Değiştir
|
| 44 |
+
authDomain: "BURAYA_AUTH_DOMAIN", // ← Değiştir
|
| 45 |
+
projectId: "BURAYA_PROJECT_ID", // ← Değiştir
|
| 46 |
+
storageBucket: "BURAYA_STORAGE", // ← Değiştir
|
| 47 |
+
messagingSenderId: "BURAYA_ID", // ← Değiştir
|
| 48 |
+
appId: "BURAYA_APP_ID" // ← Değiştir
|
| 49 |
+
};
|
| 50 |
+
|
| 51 |
+
firebase.initializeApp(firebaseConfig);
|
| 52 |
+
const auth = firebase.auth();
|
| 53 |
+
const db = firebase.firestore();
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### ✅ Bitti!
|
| 57 |
+
|
| 58 |
+
Şimdi test et:
|
| 59 |
+
```bash
|
| 60 |
+
# Web sayfalarını aç
|
| 61 |
+
open web/register.html # Kayıt sayfası
|
| 62 |
+
open web/login.html # Giriş sayfası
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## 🧪 Test
|
| 68 |
+
|
| 69 |
+
### Yeni Kullanıcı Kaydı
|
| 70 |
+
1. `register.html` aç
|
| 71 |
+
2. Form doldur:
|
| 72 |
+
- Ad: `Test User`
|
| 73 |
+
- E-posta: `[email protected]`
|
| 74 |
+
- Şifre: `test123`
|
| 75 |
+
- Rol: `Admin`
|
| 76 |
+
3. **Kayıt Ol** → Otomatik login'e yönlenecek
|
| 77 |
+
|
| 78 |
+
### Giriş Yap
|
| 79 |
+
1. `login.html` aç
|
| 80 |
+
2. E-posta ve şifre gir
|
| 81 |
+
3. **Giriş Yap** → Ana sayfaya yönlenecek
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## 🔥 Firebase Console'da Kontrol
|
| 86 |
+
|
| 87 |
+
### Kullanıcıları Görüntüle
|
| 88 |
+
1. Firebase Console → **Authentication** → **Users** tab
|
| 89 |
+
2. Kayıtlı kullanıcıları göreceksiniz
|
| 90 |
+
|
| 91 |
+
### Firestore Verilerini Görüntüle
|
| 92 |
+
1. Firebase Console → **Firestore Database** → **Data** tab
|
| 93 |
+
2. `users` collection'ını açın
|
| 94 |
+
3. Kullanıcı bilgilerini göreceksiniz
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## ⚠️ Sorun mu Var?
|
| 99 |
+
|
| 100 |
+
### "Firebase is not defined"
|
| 101 |
+
→ Tarayıcı console'da bu hatayı görüyorsan:
|
| 102 |
+
- Sayfayı **hard refresh** yap (Ctrl+Shift+R veya Cmd+Shift+R)
|
| 103 |
+
- `firebase-config.js` dosyasının yüklendiğinden emin ol
|
| 104 |
+
|
| 105 |
+
### "Permission denied" (Firestore)
|
| 106 |
+
→ Firestore güvenlik kurallarını güncelle:
|
| 107 |
+
```javascript
|
| 108 |
+
rules_version = '2';
|
| 109 |
+
service cloud.firestore {
|
| 110 |
+
match /databases/{database}/documents {
|
| 111 |
+
match /{document=**} {
|
| 112 |
+
allow read, write: if request.auth != null;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
### "Email already in use"
|
| 119 |
+
→ Bu e-posta ile zaten kayıt olunmuş. Farklı e-posta dene veya login yap.
|
| 120 |
+
|
| 121 |
+
---
|
| 122 |
+
|
| 123 |
+
## 💡 Demo Mod
|
| 124 |
+
|
| 125 |
+
Firebase yapılandırmazsan **Demo Mod** otomatik çalışır:
|
| 126 |
+
|
| 127 |
+
**Demo Hesap**:
|
| 128 |
+
- E-posta: `[email protected]`
|
| 129 |
+
- Şifre: `demo123`
|
| 130 |
+
|
| 131 |
+
Demo mod'da:
|
| 132 |
+
- ✅ Giriş yapabilirsin
|
| 133 |
+
- ❌ Yeni kullanıcı kaydedemezsin
|
| 134 |
+
- ❌ Şifre sıfırlayamazsın
|
| 135 |
+
|
| 136 |
+
---
|
| 137 |
+
|
| 138 |
+
## 📁 Dosya Yapısı
|
| 139 |
+
|
| 140 |
+
```
|
| 141 |
+
web/
|
| 142 |
+
├── firebase-config.js ← Firebase ayarları (GÜNCELLE)
|
| 143 |
+
├── register.html ← Kayıt sayfası
|
| 144 |
+
├── login.html ← Giriş sayfası
|
| 145 |
+
└── ...
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## 🎯 Sonraki Adımlar
|
| 151 |
+
|
| 152 |
+
1. **Güvenlik Kurallarını Sıkılaştır**: Production'da test mode kullanma!
|
| 153 |
+
2. **Email Verification Ekle**: Kullanıcılar e-postalarını doğrulasın
|
| 154 |
+
3. **Admin Panel**: Kullanıcı yönetimi için admin paneli ekle
|
| 155 |
+
4. **Roles & Permissions**: Farklı roller için yetkilendirme ekle
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 📚 Daha Fazla Bilgi
|
| 160 |
+
|
| 161 |
+
Detaylı kurulum için: **FIREBASE_AUTH_SETUP.md**
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
**🎉 Başarılar!** Sorularınız için: Firebase Console → Support
|
| 166 |
+
|
Procfile
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
web: python api_server.py
|
| 2 |
+
|
RAILWAY_DEPLOYMENT_GUIDE.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚂 Railway + Firebase Deployment Guide
|
| 2 |
+
|
| 3 |
+
## 📋 Overview
|
| 4 |
+
|
| 5 |
+
Bu sistem şu şekilde çalışır:
|
| 6 |
+
1. **Railway** → Backend (Flask API) hosting
|
| 7 |
+
2. **Firebase** → Authentication + Firestore + **Storage (Embedding Cache)**
|
| 8 |
+
3. **Embedding Cache** → Her kullanıcı için embedding bir kere yapılır, Firebase Storage'da saklanır
|
| 9 |
+
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
## 🎯 Neden Bu Sistem?
|
| 13 |
+
|
| 14 |
+
### ❌ Önceki Durum:
|
| 15 |
+
- Her login'de embedding yapılıyor (10-15 saniye) ❌
|
| 16 |
+
- Server restart olunca embedding'ler kayboluyordu ❌
|
| 17 |
+
- Production'da bu sürdürülebilir değil ❌
|
| 18 |
+
|
| 19 |
+
### ✅ Yeni Sistem:
|
| 20 |
+
- Embedding **bir kere** yapılır (ilk data upload'da) ✅
|
| 21 |
+
- Firebase Storage'da saklanır (kalıcı) ✅
|
| 22 |
+
- Sonraki login'lerde Firebase'den indirilir (2-3 saniye) ✅
|
| 23 |
+
- Server restart olsa bile embedding'ler kaybolmaz ✅
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 🔥 Firebase Setup
|
| 28 |
+
|
| 29 |
+
### 1. Firebase Storage Aktif Et
|
| 30 |
+
|
| 31 |
+
1. Firebase Console → **Storage** → **Get Started**
|
| 32 |
+
2. **Production mode** seç (security rules ekleyeceğiz)
|
| 33 |
+
3. **Storage location** seç (örn: `europe-west3`)
|
| 34 |
+
|
| 35 |
+
### 2. Storage Security Rules
|
| 36 |
+
|
| 37 |
+
Firebase Console → Storage → **Rules** → Aşağıdaki kuralları ekle:
|
| 38 |
+
|
| 39 |
+
```javascript
|
| 40 |
+
rules_version = '2';
|
| 41 |
+
service firebase.storage {
|
| 42 |
+
match /b/{bucket}/o {
|
| 43 |
+
// User-specific embeddings
|
| 44 |
+
match /user_embeddings/{userId}/{allPaths=**} {
|
| 45 |
+
// Sadece kendi embedding'lerini okuyabilir/yazabilir
|
| 46 |
+
allow read, write: if request.auth != null && request.auth.uid == userId;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### 3. Firebase Service Account
|
| 53 |
+
|
| 54 |
+
1. Firebase Console → **Project Settings** → **Service Accounts**
|
| 55 |
+
2. **Generate New Private Key** → JSON dosyasını indir
|
| 56 |
+
3. Dosyayı `firebase-service-account.json` olarak kaydet
|
| 57 |
+
4. **GİTİGNORE'A EKLE** (public etme!)
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## 🚂 Railway Deployment
|
| 62 |
+
|
| 63 |
+
### 1. Railway Projesi Oluştur
|
| 64 |
+
|
| 65 |
+
1. [Railway.app](https://railway.app) → **New Project** → **Deploy from GitHub repo**
|
| 66 |
+
2. Repository'yi seç: `bug-deduplication-github`
|
| 67 |
+
3. Railway otomatik detect edecek (`requirements.txt` var)
|
| 68 |
+
|
| 69 |
+
### 2. Environment Variables
|
| 70 |
+
|
| 71 |
+
Railway Dashboard → **Variables** → Şu değişkenleri ekle:
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# Firebase Configuration
|
| 75 |
+
FIREBASE_SERVICE_ACCOUNT=/app/firebase-service-account.json
|
| 76 |
+
FIREBASE_STORAGE_BUCKET=jira-duplicate-detection.appspot.com
|
| 77 |
+
|
| 78 |
+
# Flask Configuration
|
| 79 |
+
FLASK_ENV=production
|
| 80 |
+
FLASK_DEBUG=False
|
| 81 |
+
|
| 82 |
+
# CORS
|
| 83 |
+
ALLOWED_ORIGINS=https://jira-duplicate-detection.web.app,https://jira-duplicate-detection.firebaseapp.com
|
| 84 |
+
|
| 85 |
+
# Feature Flags
|
| 86 |
+
USE_FIREBASE_CACHE=True
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 3. Firebase Service Account Upload
|
| 90 |
+
|
| 91 |
+
Railway'de service account JSON dosyasını eklemek için 2 yöntem:
|
| 92 |
+
|
| 93 |
+
#### Yöntem 1: Base64 Encoding (Önerilen)
|
| 94 |
+
```bash
|
| 95 |
+
# Local'de çalıştır:
|
| 96 |
+
cat firebase-service-account.json | base64 > firebase-service-account-base64.txt
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Railway Variables:
|
| 100 |
+
```bash
|
| 101 |
+
FIREBASE_SERVICE_ACCOUNT_BASE64=<base64 string buraya yapıştır>
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
`api_server.py`'ye ekle:
|
| 105 |
+
```python
|
| 106 |
+
# Decode base64 and save
|
| 107 |
+
import base64
|
| 108 |
+
if os.getenv('FIREBASE_SERVICE_ACCOUNT_BASE64'):
|
| 109 |
+
with open('/tmp/firebase-service-account.json', 'w') as f:
|
| 110 |
+
decoded = base64.b64decode(os.getenv('FIREBASE_SERVICE_ACCOUNT_BASE64'))
|
| 111 |
+
f.write(decoded.decode())
|
| 112 |
+
os.environ['FIREBASE_SERVICE_ACCOUNT'] = '/tmp/firebase-service-account.json'
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
#### Yöntem 2: Railway Volumes
|
| 116 |
+
1. Railway → **Settings** → **Volumes**
|
| 117 |
+
2. Mount Path: `/app/secrets`
|
| 118 |
+
3. Service account'u `/app/secrets/firebase-service-account.json` olarak upload et
|
| 119 |
+
|
| 120 |
+
### 4. Deploy
|
| 121 |
+
|
| 122 |
+
```bash
|
| 123 |
+
git add .
|
| 124 |
+
git commit -m "Railway + Firebase deployment setup"
|
| 125 |
+
git push origin main
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
Railway otomatik deploy edecek!
|
| 129 |
+
|
| 130 |
+
---
|
| 131 |
+
|
| 132 |
+
## 🌐 Frontend Configuration
|
| 133 |
+
|
| 134 |
+
### 1. API Base URL Güncelle
|
| 135 |
+
|
| 136 |
+
`web/app.js` ve `web/create_report.js`:
|
| 137 |
+
|
| 138 |
+
```javascript
|
| 139 |
+
// Production
|
| 140 |
+
const API_BASE_URL = 'https://your-app.railway.app/api';
|
| 141 |
+
|
| 142 |
+
// Veya environment detection:
|
| 143 |
+
const API_BASE_URL = window.location.hostname === 'localhost'
|
| 144 |
+
? 'http://localhost:5001/api'
|
| 145 |
+
: 'https://your-app.railway.app/api';
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
### 2. Firebase Hosting Deploy
|
| 149 |
+
|
| 150 |
+
```bash
|
| 151 |
+
firebase deploy --only hosting
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## 🎯 Cache System Nasıl Çalışır?
|
| 157 |
+
|
| 158 |
+
### İlk Data Upload (Kullanıcı veriyi ilk kez yüklüyor):
|
| 159 |
+
|
| 160 |
+
```
|
| 161 |
+
1. User uploads data.csv
|
| 162 |
+
2. Backend embedding yapıyor (10-15 saniye)
|
| 163 |
+
├─ embeddings.npy oluşturuluyor
|
| 164 |
+
├─ faiss_index.bin oluşturuluyor
|
| 165 |
+
└─ metadata.json oluşturuluyor
|
| 166 |
+
3. Firebase Storage'a upload ediliyor 📤
|
| 167 |
+
└─ user_embeddings/{userId}/
|
| 168 |
+
├─ embeddings.npy
|
| 169 |
+
├─ faiss_index.bin
|
| 170 |
+
├─ metadata.json
|
| 171 |
+
└─ data.csv
|
| 172 |
+
4. Kullanıcı search yapabiliyor ✅
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### Sonraki Login'ler (Cache hit):
|
| 176 |
+
|
| 177 |
+
```
|
| 178 |
+
1. User login yapıyor
|
| 179 |
+
2. Backend Firebase Storage'ı kontrol ediyor
|
| 180 |
+
3. Artifacts bulunuyor! 📥
|
| 181 |
+
4. Firebase'den indiriliy or (2-3 saniye)
|
| 182 |
+
5. Local'e kaydediliyor
|
| 183 |
+
6. Embedding yapılmıyor! ⏩ Cached version kullanılıyor
|
| 184 |
+
7. Kullanıcı HEMEN search yapabiliyor ✅
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
### Veri Değiştiğinde:
|
| 188 |
+
|
| 189 |
+
```
|
| 190 |
+
1. User rapor ekliyor/değiştiriyor
|
| 191 |
+
2. Backend embedding yeniliyor (10-15 saniye)
|
| 192 |
+
3. Yeni artifacts Firebase'e upload ediliyor 📤
|
| 193 |
+
4. Cache güncelleniyor ✅
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
---
|
| 197 |
+
|
| 198 |
+
## 📊 Monitoring
|
| 199 |
+
|
| 200 |
+
### Railway Logs
|
| 201 |
+
|
| 202 |
+
Railway Dashboard → **Logs** → Şunları göreceksin:
|
| 203 |
+
|
| 204 |
+
```
|
| 205 |
+
✅ Firebase Storage initialized
|
| 206 |
+
🔍 Checking Firebase Storage for cached embeddings...
|
| 207 |
+
✅ Found cached embeddings in Firebase Storage!
|
| 208 |
+
📥 Downloading artifacts for user xyz...
|
| 209 |
+
✅ Downloaded: embeddings.npy (1.2 MB)
|
| 210 |
+
✅ Downloaded: faiss_index.bin (856 KB)
|
| 211 |
+
⏩ Skipping embedding generation - using cached version
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
### Firebase Storage
|
| 215 |
+
|
| 216 |
+
Firebase Console → **Storage** → Göreceksin:
|
| 217 |
+
|
| 218 |
+
```
|
| 219 |
+
user_embeddings/
|
| 220 |
+
├─ P8VtJgvODiXSbjOtCMqUTecivHR2/
|
| 221 |
+
│ ├─ embeddings.npy (1.2 MB)
|
| 222 |
+
│ ├─ faiss_index.bin (856 KB)
|
| 223 |
+
│ ├─ metadata.json (2 KB)
|
| 224 |
+
│ └─ data.csv (45 KB)
|
| 225 |
+
└─ another-user-id/
|
| 226 |
+
└─ ...
|
| 227 |
+
```
|
| 228 |
+
|
| 229 |
+
---
|
| 230 |
+
|
| 231 |
+
## 🐛 Troubleshooting
|
| 232 |
+
|
| 233 |
+
### "Firebase Storage not initialized"
|
| 234 |
+
|
| 235 |
+
```bash
|
| 236 |
+
# Railway Variables kontrol et:
|
| 237 |
+
FIREBASE_SERVICE_ACCOUNT=/app/firebase-service-account.json
|
| 238 |
+
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
### "Permission denied" hatası
|
| 242 |
+
|
| 243 |
+
Firebase Storage Rules'u kontrol et:
|
| 244 |
+
```javascript
|
| 245 |
+
allow read, write: if request.auth != null && request.auth.uid == userId;
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
### Embedding yapılmaya devam ediyor
|
| 249 |
+
|
| 250 |
+
Logs'a bak:
|
| 251 |
+
```
|
| 252 |
+
🔍 Checking Firebase Storage for cached embeddings...
|
| 253 |
+
📝 No cached embeddings found, will generate new ones
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
Firebase Storage'da artifacts var mı kontrol et.
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
## 💰 Cost Estimation
|
| 261 |
+
|
| 262 |
+
### Railway:
|
| 263 |
+
- **Starter Plan**: $5/month (500 hours)
|
| 264 |
+
- **Hobby Plan**: Free (500 hours/month, enough for development)
|
| 265 |
+
|
| 266 |
+
### Firebase Storage:
|
| 267 |
+
- **Spark Plan (Free)**: 1 GB storage, 1 GB/day download
|
| 268 |
+
- **Blaze Plan (Pay as you go)**:
|
| 269 |
+
- Storage: $0.026/GB/month
|
| 270 |
+
- Download: $0.12/GB
|
| 271 |
+
|
| 272 |
+
**Örnek:** 100 kullanıcı, her biri ~2MB embedding:
|
| 273 |
+
- Storage: 200 MB = ~$0.005/month
|
| 274 |
+
- Download (her kullanıcı ayda 10 login): 100 * 2MB * 10 = 2GB = ~$0.24/month
|
| 275 |
+
|
| 276 |
+
**TOPLAM: ~$5-10/month** 🎉
|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
|
| 280 |
+
## ✅ Checklist
|
| 281 |
+
|
| 282 |
+
- [ ] Firebase Storage aktif
|
| 283 |
+
- [ ] Storage security rules eklendi
|
| 284 |
+
- [ ] Service account JSON indirildi
|
| 285 |
+
- [ ] Railway projesi oluşturuldu
|
| 286 |
+
- [ ] Environment variables eklendi
|
| 287 |
+
- [ ] Service account Railway'e upload edildi
|
| 288 |
+
- [ ] Frontend API URL güncellendi
|
| 289 |
+
- [ ] Firebase Hosting deploy edildi
|
| 290 |
+
- [ ] Test: Yeni kullanıcı ile embedding yapıldı ve cache'lendi
|
| 291 |
+
- [ ] Test: Aynı kullanıcı logout/login → cache'ten yüklendi
|
| 292 |
+
|
| 293 |
+
---
|
| 294 |
+
|
| 295 |
+
## 🚀 Next Steps
|
| 296 |
+
|
| 297 |
+
1. **Incremental Updates**: Sadece değişen row'ların embedding'ini güncelle
|
| 298 |
+
2. **Compression**: Artifacts'ları compress et (gzip)
|
| 299 |
+
3. **CDN**: Firebase Storage CDN kullan
|
| 300 |
+
4. **Monitoring**: Sentry/LogRocket ekle
|
| 301 |
+
5. **Analytics**: Embedding cache hit rate track et
|
| 302 |
+
|
| 303 |
+
---
|
| 304 |
+
|
| 305 |
+
## 📞 Support
|
| 306 |
+
|
| 307 |
+
Sorular için: [GitHub Issues](https://github.com/your-repo/issues)
|
| 308 |
+
|
| 309 |
+
---
|
| 310 |
+
|
| 311 |
+
**🎉 Artık production-ready bir sistem var!**
|
| 312 |
+
|
USER_BASED_EMBEDDING_SYSTEM.md
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔐 Kullanıcı Bazlı Embedding Sistemi
|
| 2 |
+
|
| 3 |
+
## 🎯 Sistem Mimarisi
|
| 4 |
+
|
| 5 |
+
### Eski Sistem (❌ Artık Kullanılmıyor)
|
| 6 |
+
```
|
| 7 |
+
Turkcell Genel Verileri
|
| 8 |
+
↓
|
| 9 |
+
Tek Embedding Set
|
| 10 |
+
↓
|
| 11 |
+
Tüm Kullanıcılar Aynı Veriyi Kullanıyor
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
**Sorunlar:**
|
| 15 |
+
- ❌ Kullanıcılar birbirlerinin verilerini görebiliyordu
|
| 16 |
+
- ❌ Her kullanıcı için aynı sonuçlar
|
| 17 |
+
- ❌ Veri izolasyonu yok
|
| 18 |
+
- ❌ Privacy sorunu
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
### Yeni Sistem (✅ Aktif)
|
| 23 |
+
```
|
| 24 |
+
Kullanıcı A → Kendi CSV'si → Kendi Embeddings → Kendi FAISS Index → İzole Arama
|
| 25 |
+
Kullanıcı B → Kendi CSV'si → Kendi Embeddings → Kendi FAISS Index → İzole Arama
|
| 26 |
+
Kullanıcı C → Kendi CSV'si → Kendi Embeddings → Kendi FAISS Index → İzole Arama
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
**Avantajlar:**
|
| 30 |
+
- ✅ Tam veri izolasyonu
|
| 31 |
+
- ✅ Her kullanıcı sadece kendi verisini görür
|
| 32 |
+
- ✅ Privacy garantisi
|
| 33 |
+
- ✅ Özelleştirilebilir arama
|
| 34 |
+
- ✅ Hızlı hybrid search (FAISS + Cross-Encoder)
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 📊 Veri Yapısı
|
| 39 |
+
|
| 40 |
+
### Dosya Sistemi
|
| 41 |
+
```
|
| 42 |
+
data/
|
| 43 |
+
├── user_embeddings/
|
| 44 |
+
│ ├── user_abc123/ # Firebase User ID
|
| 45 |
+
│ │ ├── embeddings.npy # Embedding vectors (N x 384)
|
| 46 |
+
│ │ ├── faiss_index.index # FAISS search index
|
| 47 |
+
│ │ └── metadata.json # Metadata (columns, config, etc.)
|
| 48 |
+
│ │
|
| 49 |
+
│ ├── user_xyz456/ # Başka bir kullanıcı
|
| 50 |
+
│ │ ├── embeddings.npy
|
| 51 |
+
│ │ ├── faiss_index.index
|
| 52 |
+
│ │ └── metadata.json
|
| 53 |
+
│ │
|
| 54 |
+
│ └── user_turkcell789/
|
| 55 |
+
│ ├── embeddings.npy
|
| 56 |
+
│ ├── faiss_index.index
|
| 57 |
+
│ └── metadata.json
|
| 58 |
+
│
|
| 59 |
+
└── user_data/
|
| 60 |
+
├── user_abc123/
|
| 61 |
+
│ └── my_bugs.csv # User's original CSV
|
| 62 |
+
└── user_xyz456/
|
| 63 |
+
└── issues.csv
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
### Metadata Yapısı (`metadata.json`)
|
| 67 |
+
```json
|
| 68 |
+
{
|
| 69 |
+
"user_id": "user_abc123",
|
| 70 |
+
"num_records": 1500,
|
| 71 |
+
"num_columns": 12,
|
| 72 |
+
"columns": ["Summary", "Description", "Priority", "Platform", ...],
|
| 73 |
+
"text_columns": ["Summary", "Description"],
|
| 74 |
+
"model_name": "paraphrase-multilingual-MiniLM-L12-v2",
|
| 75 |
+
"embedding_dim": 384,
|
| 76 |
+
"created_at": "2025-01-15T14:30:00",
|
| 77 |
+
"config": {
|
| 78 |
+
"fileName": "my_bugs.csv"
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## 🔄 İşlem Akışı
|
| 86 |
+
|
| 87 |
+
### 1. Veri Yükleme ve Embedding Oluşturma
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
Kullanıcı CSV Yükler (web/data_upload.html)
|
| 91 |
+
↓
|
| 92 |
+
Frontend: userId gönderilir
|
| 93 |
+
↓
|
| 94 |
+
Backend: /api/upload_data endpoint
|
| 95 |
+
↓
|
| 96 |
+
DataFrame oluşturulur
|
| 97 |
+
↓
|
| 98 |
+
UserEmbeddingPipeline çalışır
|
| 99 |
+
↓
|
| 100 |
+
1. Text columns tespit edilir
|
| 101 |
+
2. Bi-Encoder ile embeddings oluşturulur (384 dim)
|
| 102 |
+
3. FAISS index oluşturulur (cosine similarity)
|
| 103 |
+
4. Metadata kaydedilir
|
| 104 |
+
↓
|
| 105 |
+
✅ Kullanıcıya özel embedding seti hazır!
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
**Log Örneği:**
|
| 109 |
+
```
|
| 110 |
+
📥 Upload request from user: user_abc123
|
| 111 |
+
🔄 Creating embeddings for user: user_abc123
|
| 112 |
+
📊 Data shape: (1500, 12)
|
| 113 |
+
📝 Text columns: ['Summary', 'Description']
|
| 114 |
+
🤖 Generating embeddings...
|
| 115 |
+
✅ Embeddings created: (1500, 384)
|
| 116 |
+
🔄 Creating FAISS index...
|
| 117 |
+
✅ FAISS index created: 1500 vectors
|
| 118 |
+
💾 Saved embeddings to: data/user_embeddings/user_abc123/embeddings.npy
|
| 119 |
+
✅ Pipeline completed successfully!
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
### 2. Arama (Hybrid Search)
|
| 125 |
+
|
| 126 |
+
```
|
| 127 |
+
Kullanıcı Arama Yapar (web/index.html)
|
| 128 |
+
↓
|
| 129 |
+
Backend: /api/search endpoint
|
| 130 |
+
↓
|
| 131 |
+
custom_data_store'dan userId alınır
|
| 132 |
+
↓
|
| 133 |
+
UserHybridSearch başlatılır
|
| 134 |
+
↓
|
| 135 |
+
STAGE 1: FAISS Search
|
| 136 |
+
- Query encode edilir
|
| 137 |
+
- Top 50 candidate bulunur (hızlı)
|
| 138 |
+
↓
|
| 139 |
+
STAGE 2: Cross-Encoder Re-ranking
|
| 140 |
+
- 50 candidate hassas skorlanır
|
| 141 |
+
- Final top 10 seçilir
|
| 142 |
+
↓
|
| 143 |
+
✅ Sonuçlar kullanıcıya döner
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
**Log Örneği:**
|
| 147 |
+
```
|
| 148 |
+
🔍 Search request from user: user_abc123
|
| 149 |
+
🚀 Using Hybrid Search with embeddings
|
| 150 |
+
🔄 Encoding query...
|
| 151 |
+
🔍 FAISS search (top 50 candidates)...
|
| 152 |
+
✅ Found 50 candidates from FAISS
|
| 153 |
+
🔄 Cross-Encoder re-ranking...
|
| 154 |
+
✅ Re-ranking completed
|
| 155 |
+
✅ Returning 10 results
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
---
|
| 159 |
+
|
| 160 |
+
## 🧪 Kullanım Örnekleri
|
| 161 |
+
|
| 162 |
+
### Python'dan Kullanım
|
| 163 |
+
|
| 164 |
+
#### 1. Embedding Oluşturma
|
| 165 |
+
```python
|
| 166 |
+
from src.user_embedding_pipeline import create_user_embeddings
|
| 167 |
+
import pandas as pd
|
| 168 |
+
|
| 169 |
+
# Veriyi yükle
|
| 170 |
+
df = pd.read_csv('my_data.csv')
|
| 171 |
+
|
| 172 |
+
# Embedding oluştur
|
| 173 |
+
success = create_user_embeddings(
|
| 174 |
+
user_id='user_abc123',
|
| 175 |
+
df=df,
|
| 176 |
+
text_columns=['Summary', 'Description'], # Optional
|
| 177 |
+
config={'fileName': 'my_data.csv'}
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
if success:
|
| 181 |
+
print("✅ Embeddings created!")
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
#### 2. Arama Yapma
|
| 185 |
+
```python
|
| 186 |
+
from src.user_hybrid_search import search_user_data
|
| 187 |
+
import pandas as pd
|
| 188 |
+
|
| 189 |
+
# Veriyi yükle
|
| 190 |
+
df = pd.read_csv('my_data.csv')
|
| 191 |
+
|
| 192 |
+
# Arama yap
|
| 193 |
+
results = search_user_data(
|
| 194 |
+
user_id='user_abc123',
|
| 195 |
+
query='Uygulama çöküyor',
|
| 196 |
+
df=df,
|
| 197 |
+
top_k=10
|
| 198 |
+
)
|
| 199 |
+
|
| 200 |
+
for i, result in enumerate(results, 1):
|
| 201 |
+
print(f"{i}. {result['summary']} (Score: {result['final_score']:.4f})")
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
### Frontend'den Kullanım
|
| 207 |
+
|
| 208 |
+
#### 1. Veri Yükleme
|
| 209 |
+
```javascript
|
| 210 |
+
// web/data_upload.html
|
| 211 |
+
|
| 212 |
+
const userSession = JSON.parse(localStorage.getItem('userSession'));
|
| 213 |
+
const userId = userSession.uid || userSession.username;
|
| 214 |
+
|
| 215 |
+
const response = await fetch('http://localhost:5001/api/upload_data', {
|
| 216 |
+
method: 'POST',
|
| 217 |
+
headers: { 'Content-Type': 'application/json' },
|
| 218 |
+
body: JSON.stringify({
|
| 219 |
+
data: csvData, // Array of objects
|
| 220 |
+
fileName: 'my_bugs.csv',
|
| 221 |
+
userId: userId, // ⚠️ CRITICAL!
|
| 222 |
+
textColumns: ['Summary', 'Description'] // Optional
|
| 223 |
+
})
|
| 224 |
+
});
|
| 225 |
+
|
| 226 |
+
const result = await response.json();
|
| 227 |
+
console.log('Embeddings created:', result.info.embeddingsCreated);
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
#### 2. Arama Yapma
|
| 231 |
+
```javascript
|
| 232 |
+
// web/index.html (app.js)
|
| 233 |
+
|
| 234 |
+
const response = await fetch('http://localhost:5001/api/search', {
|
| 235 |
+
method: 'POST',
|
| 236 |
+
headers: { 'Content-Type': 'application/json' },
|
| 237 |
+
body: JSON.stringify({
|
| 238 |
+
query: 'Uygulama çöküyor',
|
| 239 |
+
top_k: 10
|
| 240 |
+
})
|
| 241 |
+
});
|
| 242 |
+
|
| 243 |
+
const data = await response.json();
|
| 244 |
+
// Kullanıcının kendi verisi içinde arama yapıldı!
|
| 245 |
+
```
|
| 246 |
+
|
| 247 |
+
---
|
| 248 |
+
|
| 249 |
+
## 🔐 Güvenlik ve İzolasyon
|
| 250 |
+
|
| 251 |
+
### Veri İzolasyonu
|
| 252 |
+
```python
|
| 253 |
+
# Kullanıcı A'nın verileri
|
| 254 |
+
/data/user_embeddings/user_abc123/
|
| 255 |
+
|
| 256 |
+
# Kullanıcı B'nin verileri
|
| 257 |
+
/data/user_embeddings/user_xyz456/
|
| 258 |
+
|
| 259 |
+
# ❌ Kullanıcı A, Kullanıcı B'nin verilerini GÖREMEZ!
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
### Firebase Authentication
|
| 263 |
+
```javascript
|
| 264 |
+
// Frontend'de user ID Firebase'den alınır
|
| 265 |
+
const user = firebase.auth().currentUser;
|
| 266 |
+
const userId = user.uid; // Unique ve güvenli
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
### Backend Validation
|
| 270 |
+
```python
|
| 271 |
+
# Her request'te userId kontrol edilir
|
| 272 |
+
user_id = custom_data_store.get('userId', 'anonymous')
|
| 273 |
+
|
| 274 |
+
# Sadece o kullanıcının embeddings'i yüklenir
|
| 275 |
+
user_dir = Path(f"data/user_embeddings/{user_id}")
|
| 276 |
+
```
|
| 277 |
+
|
| 278 |
+
---
|
| 279 |
+
|
| 280 |
+
## ⚙️ Konfigürasyon
|
| 281 |
+
|
| 282 |
+
### Embedding Modeli
|
| 283 |
+
```python
|
| 284 |
+
# src/user_embedding_pipeline.py
|
| 285 |
+
|
| 286 |
+
model_name = 'paraphrase-multilingual-MiniLM-L12-v2'
|
| 287 |
+
# - 384 dimensions
|
| 288 |
+
# - Multilingual (Turkish support)
|
| 289 |
+
# - Fast (50ms per text)
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
### FAISS Index
|
| 293 |
+
```python
|
| 294 |
+
# Cosine Similarity için normalize edilmiş Inner Product
|
| 295 |
+
index = faiss.IndexFlatIP(embedding_dim)
|
| 296 |
+
|
| 297 |
+
# Normalize edilmiş embeddings
|
| 298 |
+
normalized = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
|
| 299 |
+
index.add(normalized.astype('float32'))
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
### Cross-Encoder
|
| 303 |
+
```python
|
| 304 |
+
model_name = 'cross-encoder/mmarco-mMiniLMv2-L12-H384-v1'
|
| 305 |
+
# - Multilingual
|
| 306 |
+
# - High accuracy
|
| 307 |
+
# - Slower (200ms per pair, but only for top-K)
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
---
|
| 311 |
+
|
| 312 |
+
## 📈 Performans
|
| 313 |
+
|
| 314 |
+
### Embedding Oluşturma
|
| 315 |
+
| Veri Boyutu | Süre | Disk Kullanımı |
|
| 316 |
+
|-------------|------|----------------|
|
| 317 |
+
| 1,000 satır | ~30 saniye | ~1.5 MB |
|
| 318 |
+
| 5,000 satır | ~2 dakika | ~7.5 MB |
|
| 319 |
+
| 10,000 satır | ~4 dakika | ~15 MB |
|
| 320 |
+
|
| 321 |
+
### Arama Hızı
|
| 322 |
+
| Aşama | Süre |
|
| 323 |
+
|-------|------|
|
| 324 |
+
| Query encoding | ~50ms |
|
| 325 |
+
| FAISS search (top 50) | ~5ms |
|
| 326 |
+
| Cross-encoder (50 → 10) | ~200ms |
|
| 327 |
+
| **Total** | **~250ms** |
|
| 328 |
+
|
| 329 |
+
---
|
| 330 |
+
|
| 331 |
+
## 🚀 Deployment
|
| 332 |
+
|
| 333 |
+
### Gereksinimler
|
| 334 |
+
```txt
|
| 335 |
+
# requirements.txt
|
| 336 |
+
sentence-transformers>=2.2.0
|
| 337 |
+
faiss-cpu>=1.7.4 # veya faiss-gpu
|
| 338 |
+
pandas>=1.5.0
|
| 339 |
+
numpy>=1.24.0
|
| 340 |
+
```
|
| 341 |
+
|
| 342 |
+
### Kurulum
|
| 343 |
+
```bash
|
| 344 |
+
# Dependencies yükle
|
| 345 |
+
pip install -r requirements.txt
|
| 346 |
+
|
| 347 |
+
# Backend'i başlat
|
| 348 |
+
python api_server.py
|
| 349 |
+
```
|
| 350 |
+
|
| 351 |
+
---
|
| 352 |
+
|
| 353 |
+
## 🧪 Test
|
| 354 |
+
|
| 355 |
+
### Unit Test
|
| 356 |
+
```bash
|
| 357 |
+
# Embedding pipeline testi
|
| 358 |
+
python src/user_embedding_pipeline.py
|
| 359 |
+
```
|
| 360 |
+
|
| 361 |
+
### Integration Test
|
| 362 |
+
```bash
|
| 363 |
+
# 1. Test verisi yükle
|
| 364 |
+
curl -X POST http://localhost:5001/api/upload_data \
|
| 365 |
+
-H "Content-Type: application/json" \
|
| 366 |
+
-d @test_data.json
|
| 367 |
+
|
| 368 |
+
# 2. Arama yap
|
| 369 |
+
curl -X POST http://localhost:5001/api/search \
|
| 370 |
+
-H "Content-Type: application/json" \
|
| 371 |
+
-d '{"query": "test bug", "top_k": 5}'
|
| 372 |
+
```
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## 🐛 Sorun Giderme
|
| 377 |
+
|
| 378 |
+
### Embedding Oluşturulamıyor
|
| 379 |
+
```bash
|
| 380 |
+
# Log'ları kontrol et
|
| 381 |
+
❌ Embedding creation error: ...
|
| 382 |
+
|
| 383 |
+
# Çözüm 1: Model yükleme sorunu
|
| 384 |
+
pip install --upgrade sentence-transformers
|
| 385 |
+
|
| 386 |
+
# Çözüm 2: Bellek yetersiz
|
| 387 |
+
# Batch size'ı küçült (pipeline.py → batch_size=16)
|
| 388 |
+
```
|
| 389 |
+
|
| 390 |
+
### Arama Sonuç Döndürmüyor
|
| 391 |
+
```bash
|
| 392 |
+
# User embeddings var mı kontrol et
|
| 393 |
+
ls -la data/user_embeddings/USER_ID/
|
| 394 |
+
|
| 395 |
+
# Olmalı:
|
| 396 |
+
# - embeddings.npy
|
| 397 |
+
# - faiss_index.index
|
| 398 |
+
# - metadata.json
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
### "User embeddings not found" Hatası
|
| 402 |
+
```python
|
| 403 |
+
# Çözüm: Embeddings'i yeniden oluştur
|
| 404 |
+
from src.user_embedding_pipeline import create_user_embeddings
|
| 405 |
+
create_user_embeddings(user_id, df)
|
| 406 |
+
```
|
| 407 |
+
|
| 408 |
+
---
|
| 409 |
+
|
| 410 |
+
## 📚 API Referansı
|
| 411 |
+
|
| 412 |
+
### POST `/api/upload_data`
|
| 413 |
+
**Request:**
|
| 414 |
+
```json
|
| 415 |
+
{
|
| 416 |
+
"data": [...],
|
| 417 |
+
"fileName": "bugs.csv",
|
| 418 |
+
"userId": "user_abc123", // REQUIRED
|
| 419 |
+
"textColumns": ["Summary", "Description"] // Optional
|
| 420 |
+
}
|
| 421 |
+
```
|
| 422 |
+
|
| 423 |
+
**Response:**
|
| 424 |
+
```json
|
| 425 |
+
{
|
| 426 |
+
"success": true,
|
| 427 |
+
"message": "Data uploaded and embeddings created",
|
| 428 |
+
"info": {
|
| 429 |
+
"fileName": "bugs.csv",
|
| 430 |
+
"rowCount": 1500,
|
| 431 |
+
"embeddingsCreated": true,
|
| 432 |
+
"embeddingsPath": "data/user_embeddings/user_abc123"
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
```
|
| 436 |
+
|
| 437 |
+
### POST `/api/search`
|
| 438 |
+
**Request:**
|
| 439 |
+
```json
|
| 440 |
+
{
|
| 441 |
+
"query": "Uygulama çöküyor",
|
| 442 |
+
"top_k": 10
|
| 443 |
+
}
|
| 444 |
+
```
|
| 445 |
+
|
| 446 |
+
**Response:**
|
| 447 |
+
```json
|
| 448 |
+
{
|
| 449 |
+
"success": true,
|
| 450 |
+
"results": [
|
| 451 |
+
{
|
| 452 |
+
"index": 42,
|
| 453 |
+
"summary": "App crashes on startup",
|
| 454 |
+
"final_score": 8.4523,
|
| 455 |
+
"cross_encoder_score": 8.4523,
|
| 456 |
+
"faiss_score": 0.9234,
|
| 457 |
+
...
|
| 458 |
+
}
|
| 459 |
+
],
|
| 460 |
+
"count": 10,
|
| 461 |
+
"search_time": 0.25
|
| 462 |
+
}
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
---
|
| 466 |
+
|
| 467 |
+
## ✅ Özet
|
| 468 |
+
|
| 469 |
+
| Özellik | Durum |
|
| 470 |
+
|---------|-------|
|
| 471 |
+
| Kullanıcı izolasyonu | ✅ Aktif |
|
| 472 |
+
| Embedding oluşturma | ✅ Otomatik |
|
| 473 |
+
| Hybrid search | ✅ FAISS + Cross-Encoder |
|
| 474 |
+
| Privacy | ✅ Garantili |
|
| 475 |
+
| Performans | ✅ <300ms |
|
| 476 |
+
| Firebase entegrasyonu | ✅ Tam |
|
| 477 |
+
| Multi-user desteği | ✅ Sınırsız |
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
**Son Güncelleme**: 2025-01-16
|
| 482 |
+
**Versiyon**: 2.0.0
|
| 483 |
+
**Durum**: ✅ Production Ready
|
| 484 |
+
|
WEB_AUTH_FEATURES.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔐 Kullanıcı Kimlik Doğrulama Sistemi
|
| 2 |
+
|
| 3 |
+
## ✨ Özellikler
|
| 4 |
+
|
| 5 |
+
### 🎯 Temel Özellikler
|
| 6 |
+
- ✅ **Firebase Authentication** ile güvenli kimlik doğrulama
|
| 7 |
+
- ✅ **Kullanıcı Kayıt** sistemi (register.html)
|
| 8 |
+
- ✅ **Kullanıcı Girişi** (login.html)
|
| 9 |
+
- ✅ **Şifre Sıfırlama** özelliği
|
| 10 |
+
- ✅ **Şifre Gücü Göstergesi** (kayıt sırasında)
|
| 11 |
+
- ✅ **Demo Mod** (Firebase yapılandırılmamışsa)
|
| 12 |
+
- ✅ **Firestore** ile kullanıcı verilerini saklama
|
| 13 |
+
- ✅ **Rol Bazlı Yetkilendirme** (Admin, User, Developer, Tester)
|
| 14 |
+
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 📱 Sayfalar
|
| 18 |
+
|
| 19 |
+
### 1. register.html - Kayıt Sayfası
|
| 20 |
+
**Özellikler**:
|
| 21 |
+
- 👤 Ad Soyad girişi
|
| 22 |
+
- 📧 E-posta doğrulama
|
| 23 |
+
- 🔒 Güvenli şifre oluşturma
|
| 24 |
+
- 📊 Şifre gücü göstergesi (Zayıf/Orta/Güçlü)
|
| 25 |
+
- 🎭 Rol seçimi
|
| 26 |
+
- ✅ Gerçek zamanlı form doğrulama
|
| 27 |
+
- 🎨 Modern animasyonlu UI
|
| 28 |
+
|
| 29 |
+
**Validasyonlar**:
|
| 30 |
+
- Ad Soyad: Minimum 3 karakter
|
| 31 |
+
- E-posta: Geçerli format (@turkcell.com.tr)
|
| 32 |
+
- Şifre: Minimum 6 karakter
|
| 33 |
+
- Şifre eşleşmesi kontrolü
|
| 34 |
+
- Rol seçimi zorunlu
|
| 35 |
+
|
| 36 |
+
**Kullanım**:
|
| 37 |
+
```
|
| 38 |
+
1. Ad Soyad gir
|
| 39 |
+
2. E-posta gir
|
| 40 |
+
3. Şifre belirle (güç göstergesi yeşil olmalı)
|
| 41 |
+
4. Şifreyi tekrar gir
|
| 42 |
+
5. Rol seç
|
| 43 |
+
6. "Kayıt Ol" butonuna tıkla
|
| 44 |
+
7. ✅ Otomatik login sayfasına yönlendirilir
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
### 2. login.html - Giriş Sayfası
|
| 50 |
+
**Özellikler**:
|
| 51 |
+
- 📧 E-posta ile giriş
|
| 52 |
+
- 🔒 Şifre ile kimlik doğrulama
|
| 53 |
+
- 🔄 Şifremi unuttum linki
|
| 54 |
+
- 💡 Demo mod desteği
|
| 55 |
+
- 🎨 Modern animasyonlu arka plan
|
| 56 |
+
|
| 57 |
+
**Kullanım**:
|
| 58 |
+
```
|
| 59 |
+
1. E-posta gir
|
| 60 |
+
2. Şifre gir
|
| 61 |
+
3. "Giriş Yap" butonuna tıkla
|
| 62 |
+
4. ✅ Ana sayfaya yönlendirilir
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
**Demo Hesaplar** (Firebase yapılandırılmamışsa):
|
| 66 |
+
- `[email protected]` / `demo123`
|
| 67 |
+
- `[email protected]` / `admin123`
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## 🔥 Firebase Entegrasyonu
|
| 72 |
+
|
| 73 |
+
### Kullanılan Firebase Servisleri
|
| 74 |
+
|
| 75 |
+
#### 1. Firebase Authentication
|
| 76 |
+
```javascript
|
| 77 |
+
// Kullanıcı kaydı
|
| 78 |
+
await auth.createUserWithEmailAndPassword(email, password);
|
| 79 |
+
|
| 80 |
+
// Kullanıcı girişi
|
| 81 |
+
await auth.signInWithEmailAndPassword(email, password);
|
| 82 |
+
|
| 83 |
+
// Şifre sıfırlama
|
| 84 |
+
await auth.sendPasswordResetEmail(email);
|
| 85 |
+
|
| 86 |
+
// Çıkış
|
| 87 |
+
await auth.signOut();
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
#### 2. Firebase Firestore
|
| 91 |
+
```javascript
|
| 92 |
+
// Kullanıcı verisi kaydetme
|
| 93 |
+
await db.collection('users').doc(userId).set({
|
| 94 |
+
uid: userId,
|
| 95 |
+
email: email,
|
| 96 |
+
fullName: name,
|
| 97 |
+
role: role,
|
| 98 |
+
createdAt: serverTimestamp(),
|
| 99 |
+
lastLogin: serverTimestamp()
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
// Kullanıcı verisi okuma
|
| 103 |
+
const userDoc = await db.collection('users').doc(userId).get();
|
| 104 |
+
const userData = userDoc.data();
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## 🗄️ Veri Yapısı
|
| 110 |
+
|
| 111 |
+
### LocalStorage
|
| 112 |
+
```javascript
|
| 113 |
+
{
|
| 114 |
+
"userSession": {
|
| 115 |
+
"uid": "firebase-user-id",
|
| 116 |
+
"email": "[email protected]",
|
| 117 |
+
"username": "[email protected]",
|
| 118 |
+
"name": "Ahmet Yılmaz",
|
| 119 |
+
"role": "admin",
|
| 120 |
+
"loginTime": "2025-01-15T10:30:00.000Z",
|
| 121 |
+
"authProvider": "firebase" // veya "demo"
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### Firestore - users collection
|
| 127 |
+
```javascript
|
| 128 |
+
/users/{userId}
|
| 129 |
+
{
|
| 130 |
+
uid: "firebase-user-id",
|
| 131 |
+
email: "[email protected]",
|
| 132 |
+
fullName: "Ahmet Yılmaz",
|
| 133 |
+
role: "admin", // user, admin, developer, tester
|
| 134 |
+
createdAt: Timestamp(2025-01-15 10:30:00),
|
| 135 |
+
lastLogin: Timestamp(2025-01-15 10:30:00)
|
| 136 |
+
}
|
| 137 |
+
```
|
| 138 |
+
|
| 139 |
+
---
|
| 140 |
+
|
| 141 |
+
## 🎭 Roller ve Yetkiler
|
| 142 |
+
|
| 143 |
+
### Kullanılabilir Roller
|
| 144 |
+
1. **👤 User** (Kullanıcı)
|
| 145 |
+
- Rapor oluşturabilir
|
| 146 |
+
- Arama yapabilir
|
| 147 |
+
- Kendi raporlarını görebilir
|
| 148 |
+
|
| 149 |
+
2. **👨💼 Admin** (Yönetici)
|
| 150 |
+
- Tüm User yetkileri
|
| 151 |
+
- Tüm raporları görebilir
|
| 152 |
+
- Sistem ayarlarını değiştirebilir
|
| 153 |
+
- Kullanıcı yönetimi (gelecekte)
|
| 154 |
+
|
| 155 |
+
3. **👨💻 Developer** (Geliştirici)
|
| 156 |
+
- Tüm User yetkileri
|
| 157 |
+
- Teknik detayları görebilir
|
| 158 |
+
- Debug modunu aktifleştirebilir
|
| 159 |
+
|
| 160 |
+
4. **🧪 Tester** (Test Uzmanı)
|
| 161 |
+
- Tüm User yetkileri
|
| 162 |
+
- Test raporları oluşturabilir
|
| 163 |
+
- Test verileri yükleyebilir
|
| 164 |
+
|
| 165 |
+
---
|
| 166 |
+
|
| 167 |
+
## 🔒 Güvenlik
|
| 168 |
+
|
| 169 |
+
### Şifre Gereksinimleri
|
| 170 |
+
- ✅ Minimum 6 karakter
|
| 171 |
+
- 🟢 Önerilen: 10+ karakter
|
| 172 |
+
- 🟢 Önerilen: Büyük + küçük harf
|
| 173 |
+
- 🟢 Önerilen: En az 1 rakam
|
| 174 |
+
- 🟢 Önerilen: Özel karakter
|
| 175 |
+
|
| 176 |
+
### Şifre Gücü Göstergesi
|
| 177 |
+
```
|
| 178 |
+
🔴 Zayıf : < 3 kriter
|
| 179 |
+
🟡 Orta : 3-4 kriter
|
| 180 |
+
🟢 Güçlü : 5+ kriter
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
### Firebase Security Rules
|
| 184 |
+
```javascript
|
| 185 |
+
rules_version = '2';
|
| 186 |
+
service cloud.firestore {
|
| 187 |
+
match /databases/{database}/documents {
|
| 188 |
+
match /users/{userId} {
|
| 189 |
+
allow read: if request.auth.uid == userId;
|
| 190 |
+
allow create: if request.auth != null;
|
| 191 |
+
allow update: if request.auth.uid == userId;
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
```
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
## 🎨 UI/UX Özellikleri
|
| 200 |
+
|
| 201 |
+
### Animasyonlar
|
| 202 |
+
- 🌟 Animasyonlu arka plan (250 kutucuk)
|
| 203 |
+
- ✨ Hover efektleri (kutucuklar sarı olur)
|
| 204 |
+
- 🎯 Smooth scroll
|
| 205 |
+
- 💫 Gradient animasyonları
|
| 206 |
+
|
| 207 |
+
### Responsive Design
|
| 208 |
+
- 📱 Mobil uyumlu (600px)
|
| 209 |
+
- 💻 Tablet uyumlu (900px)
|
| 210 |
+
- 🖥️ Desktop optimized
|
| 211 |
+
|
| 212 |
+
### Form Validasyonu
|
| 213 |
+
- ⚡ Gerçek zamanlı validasyon
|
| 214 |
+
- 🎨 Görsel geri bildirim (kırmızı border)
|
| 215 |
+
- 📝 Hata mesajları (Türkçe)
|
| 216 |
+
- ✅ Başarı mesajları
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
## 🔄 Akış Diyagramları
|
| 221 |
+
|
| 222 |
+
### Kayıt Akışı
|
| 223 |
+
```
|
| 224 |
+
Kullanıcı register.html açar
|
| 225 |
+
↓
|
| 226 |
+
Form doldurulur
|
| 227 |
+
↓
|
| 228 |
+
Validasyon kontrolü
|
| 229 |
+
↓
|
| 230 |
+
Firebase'e kayıt
|
| 231 |
+
↓
|
| 232 |
+
Firestore'a kullanıcı verisi
|
| 233 |
+
↓
|
| 234 |
+
✅ login.html'e yönlendirme
|
| 235 |
+
```
|
| 236 |
+
|
| 237 |
+
### Giriş Akışı
|
| 238 |
+
```
|
| 239 |
+
Kullanıcı login.html açar
|
| 240 |
+
↓
|
| 241 |
+
E-posta ve şifre girer
|
| 242 |
+
↓
|
| 243 |
+
Firebase Authentication
|
| 244 |
+
↓
|
| 245 |
+
Firestore'dan kullanıcı verisi
|
| 246 |
+
↓
|
| 247 |
+
LocalStorage'a session kaydet
|
| 248 |
+
↓
|
| 249 |
+
✅ data_selection.html'e yönlendirme
|
| 250 |
+
```
|
| 251 |
+
|
| 252 |
+
### Şifre Sıfırlama Akışı
|
| 253 |
+
```
|
| 254 |
+
"Şifremi unuttum?" tıkla
|
| 255 |
+
↓
|
| 256 |
+
E-posta gir
|
| 257 |
+
↓
|
| 258 |
+
Firebase reset email gönder
|
| 259 |
+
↓
|
| 260 |
+
E-posta kutusunu kontrol et
|
| 261 |
+
↓
|
| 262 |
+
Reset linkine tıkla
|
| 263 |
+
↓
|
| 264 |
+
Yeni şifre belirle
|
| 265 |
+
↓
|
| 266 |
+
✅ Giriş yap
|
| 267 |
+
```
|
| 268 |
+
|
| 269 |
+
---
|
| 270 |
+
|
| 271 |
+
## 📊 Hata Yönetimi
|
| 272 |
+
|
| 273 |
+
### Firebase Hataları
|
| 274 |
+
| Hata Kodu | Mesaj | Açıklama |
|
| 275 |
+
|-----------|-------|----------|
|
| 276 |
+
| `auth/email-already-in-use` | "Bu e-posta zaten kullanımda!" | Kayıt sırasında |
|
| 277 |
+
| `auth/invalid-email` | "Geçersiz e-posta adresi!" | E-posta formatı hatalı |
|
| 278 |
+
| `auth/user-not-found` | "Kullanıcı bulunamadı!" | Giriş sırasında |
|
| 279 |
+
| `auth/wrong-password` | "Hatalı şifre!" | Şifre yanlış |
|
| 280 |
+
| `auth/weak-password` | "Şifre çok zayıf!" | Min. 6 karakter |
|
| 281 |
+
| `auth/too-many-requests` | "Çok fazla deneme!" | Rate limit |
|
| 282 |
+
|
| 283 |
+
### Kullanıcı Dostu Mesajlar
|
| 284 |
+
```javascript
|
| 285 |
+
✅ Başarı: "Kayıt başarılı! Giriş sayfasına yönlendiriliyorsunuz..."
|
| 286 |
+
⚠️ Uyarı: "Şifreler eşleşmiyor!"
|
| 287 |
+
❌ Hata: "E-posta adresi zaten kullanımda!"
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
---
|
| 291 |
+
|
| 292 |
+
## 🚀 Gelecek Geliştirmeler
|
| 293 |
+
|
| 294 |
+
### Planlanan Özellikler
|
| 295 |
+
- [ ] Email verification (e-posta doğrulama)
|
| 296 |
+
- [ ] Social login (Google, Microsoft)
|
| 297 |
+
- [ ] 2FA (Two-factor authentication)
|
| 298 |
+
- [ ] User profile sayfası
|
| 299 |
+
- [ ] Admin panel
|
| 300 |
+
- [ ] Password strength meter iyileştirmesi
|
| 301 |
+
- [ ] Remember me özelliği
|
| 302 |
+
- [ ] Session timeout
|
| 303 |
+
- [ ] Login history
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
## 📝 Kod Örnekleri
|
| 308 |
+
|
| 309 |
+
### Kullanıcı Session Kontrolü
|
| 310 |
+
```javascript
|
| 311 |
+
// Her sayfada kullanıcı kontrolü
|
| 312 |
+
const userSession = JSON.parse(localStorage.getItem('userSession'));
|
| 313 |
+
if (!userSession) {
|
| 314 |
+
window.location.href = 'login.html';
|
| 315 |
+
}
|
| 316 |
+
```
|
| 317 |
+
|
| 318 |
+
### Firebase ile Logout
|
| 319 |
+
```javascript
|
| 320 |
+
async function logout() {
|
| 321 |
+
if (confirm('Çıkış yapmak istediğinizden emin misiniz?')) {
|
| 322 |
+
try {
|
| 323 |
+
await auth.signOut();
|
| 324 |
+
localStorage.removeItem('userSession');
|
| 325 |
+
window.location.href = 'login.html';
|
| 326 |
+
} catch (error) {
|
| 327 |
+
console.error('Logout error:', error);
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
```
|
| 332 |
+
|
| 333 |
+
### Rol Bazlı Erişim Kontrolü
|
| 334 |
+
```javascript
|
| 335 |
+
function checkAdminAccess() {
|
| 336 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 337 |
+
if (session.role !== 'admin') {
|
| 338 |
+
alert('Bu sayfaya erişim yetkiniz yok!');
|
| 339 |
+
window.location.href = 'index.html';
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
---
|
| 345 |
+
|
| 346 |
+
## 🎓 Eğitim Videoları (Gelecekte)
|
| 347 |
+
|
| 348 |
+
- [ ] Firebase projesi oluşturma
|
| 349 |
+
- [ ] Kayıt sistemi kullanımı
|
| 350 |
+
- [ ] Admin paneli kullanımı
|
| 351 |
+
- [ ] Güvenlik kuralları yapılandırması
|
| 352 |
+
|
| 353 |
+
---
|
| 354 |
+
|
| 355 |
+
## 📞 Destek
|
| 356 |
+
|
| 357 |
+
**Sorun mu yaşıyorsunuz?**
|
| 358 |
+
1. `FIREBASE_QUICK_START.md` dosyasını okuyun
|
| 359 |
+
2. `FIREBASE_AUTH_SETUP.md` detaylı kurulum kılavuzu
|
| 360 |
+
3. Firebase Console → Support
|
| 361 |
+
4. GitHub Issues
|
| 362 |
+
|
| 363 |
+
---
|
| 364 |
+
|
| 365 |
+
**Son Güncelleme**: 2025-01-15
|
| 366 |
+
**Versiyon**: 1.0.0
|
| 367 |
+
**Durum**: ✅ Production Ready
|
| 368 |
+
|
api_server.py
CHANGED
|
@@ -11,6 +11,7 @@ from hybrid_search import HybridSearch
|
|
| 11 |
import logging
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
import time
|
|
|
|
| 14 |
|
| 15 |
# Configure logging
|
| 16 |
logging.basicConfig(
|
|
@@ -26,17 +27,86 @@ CORS(app) # Enable CORS for frontend requests
|
|
| 26 |
# Initialize search system (singleton)
|
| 27 |
search_system = None
|
| 28 |
|
| 29 |
-
# Global variable to store custom uploaded data
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
'data': None,
|
| 32 |
'fileName': None,
|
| 33 |
'rowCount': 0,
|
| 34 |
'columns': [],
|
| 35 |
-
|
| 36 |
-
|
| 37 |
'uploadedAt': None,
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
def get_search_system():
|
| 42 |
"""Get or initialize the search system"""
|
|
@@ -48,19 +118,136 @@ def get_search_system():
|
|
| 48 |
return search_system
|
| 49 |
|
| 50 |
|
| 51 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
"""
|
| 53 |
-
|
| 54 |
-
|
| 55 |
|
| 56 |
Args:
|
| 57 |
query: Search query
|
| 58 |
df: Custom DataFrame
|
| 59 |
top_k: Number of results to return
|
| 60 |
selected_columns: List of columns selected by user for search (priority)
|
|
|
|
| 61 |
"""
|
| 62 |
import pandas as pd
|
| 63 |
from difflib import SequenceMatcher
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
results = []
|
| 66 |
query_lower = query.lower()
|
|
@@ -209,43 +396,56 @@ def search_reports():
|
|
| 209 |
if not isinstance(top_k, int) or top_k < 1 or top_k > 50:
|
| 210 |
top_k = 10
|
| 211 |
|
| 212 |
-
|
|
|
|
| 213 |
|
| 214 |
-
|
| 215 |
-
global custom_data_store
|
| 216 |
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
logger.info(f"🎯 Selected columns for search: {cols_to_use}")
|
|
|
|
| 223 |
start_time = time.time()
|
| 224 |
results = search_custom_data(
|
| 225 |
query,
|
| 226 |
-
|
| 227 |
top_k,
|
| 228 |
-
selected_columns=cols_to_use
|
|
|
|
| 229 |
)
|
| 230 |
search_time = time.time() - start_time
|
| 231 |
logger.info(f"✅ Found {len(results)} results in custom data in {search_time:.2f}s")
|
| 232 |
else:
|
| 233 |
-
#
|
| 234 |
-
logger.
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
)
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
# Return results
|
| 251 |
return jsonify({
|
|
@@ -257,8 +457,8 @@ def search_reports():
|
|
| 257 |
'version': version,
|
| 258 |
'language': language
|
| 259 |
},
|
| 260 |
-
'results':
|
| 261 |
-
'count': len(
|
| 262 |
'search_time': round(search_time, 2)
|
| 263 |
})
|
| 264 |
|
|
@@ -285,10 +485,15 @@ def get_stats():
|
|
| 285 |
}
|
| 286 |
"""
|
| 287 |
try:
|
| 288 |
-
#
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
# Calculate statistics from custom data
|
| 294 |
stats = {
|
|
@@ -313,27 +518,23 @@ def get_stats():
|
|
| 313 |
'success': True,
|
| 314 |
'stats': stats,
|
| 315 |
'customDataLoaded': True,
|
| 316 |
-
'fileName':
|
|
|
|
| 317 |
})
|
| 318 |
else:
|
| 319 |
-
#
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
# Calculate statistics
|
| 323 |
-
stats = {
|
| 324 |
-
'total_reports': len(search.df),
|
| 325 |
-
'platforms': {
|
| 326 |
-
'android': int((search.df['Platform'] == 'android').sum()),
|
| 327 |
-
'ios': int((search.df['Platform'] == 'ios').sum()),
|
| 328 |
-
'unknown': int((search.df['Platform'] == 'unknown').sum())
|
| 329 |
-
},
|
| 330 |
-
'applications': search.df['Application'].value_counts().to_dict() if 'Application' in search.df.columns else {}
|
| 331 |
-
}
|
| 332 |
|
| 333 |
return jsonify({
|
| 334 |
'success': True,
|
| 335 |
-
'stats':
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
})
|
| 338 |
|
| 339 |
except Exception as e:
|
|
@@ -347,6 +548,38 @@ def get_stats():
|
|
| 347 |
@app.route('/api/applications', methods=['GET'])
|
| 348 |
def get_applications():
|
| 349 |
"""Get list of available applications"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
try:
|
| 351 |
search = get_search_system()
|
| 352 |
|
|
@@ -396,17 +629,36 @@ def create_report():
|
|
| 396 |
# Get request data
|
| 397 |
data = request.get_json()
|
| 398 |
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
if data and 'replace_report' in data:
|
| 401 |
-
logger.info(f"🔍 replace_report parameter found: {data.get('replace_report')}")
|
| 402 |
-
logger.info(f"🔍 old_report_summary parameter: {data.get('old_report_summary')}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
if
|
|
|
|
| 405 |
return jsonify({
|
| 406 |
'success': False,
|
| 407 |
-
'error': 'Missing required
|
| 408 |
}), 400
|
| 409 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
# Check if we're replacing an old report
|
| 411 |
replace_report = data.get('replace_report', False)
|
| 412 |
old_report_summary = data.get('old_report_summary', '')
|
|
@@ -414,6 +666,12 @@ def create_report():
|
|
| 414 |
|
| 415 |
logger.info(f"🎯 replace_report={replace_report}, old_report_summary='{old_report_summary}'")
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
# Detect application from summary
|
| 418 |
summary_lower = data['summary'].lower()
|
| 419 |
application = 'Unknown'
|
|
@@ -457,20 +715,18 @@ def create_report():
|
|
| 457 |
'Application': application
|
| 458 |
}
|
| 459 |
|
| 460 |
-
#
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
# Append to custom data
|
| 465 |
-
logger.info(f"📤 Appending to custom data: {custom_data_store['fileName']}")
|
| 466 |
|
| 467 |
# Create new row with custom data columns
|
| 468 |
-
custom_row = {col: '' for col in
|
| 469 |
|
| 470 |
# Map ALL form data fields to custom columns dynamically
|
| 471 |
for key, value in data.items():
|
| 472 |
# Try to find matching column (exact match or partial match)
|
| 473 |
-
for col in
|
| 474 |
col_lower = col.lower()
|
| 475 |
key_lower = key.lower()
|
| 476 |
|
|
@@ -480,7 +736,7 @@ def create_report():
|
|
| 480 |
break
|
| 481 |
|
| 482 |
# Also try common mappings
|
| 483 |
-
for col in
|
| 484 |
col_lower = col.lower()
|
| 485 |
if 'summary' in col_lower or 'özet' in col_lower:
|
| 486 |
if not custom_row[col]: # Only if not already set
|
|
@@ -501,12 +757,12 @@ def create_report():
|
|
| 501 |
# If replacing an old report, delete it first
|
| 502 |
if replace_report and old_report_summary:
|
| 503 |
logger.info(f"🔄 Replacing old report: '{old_report_summary}'")
|
| 504 |
-
logger.info(f"📊 Current DataFrame shape: {
|
| 505 |
-
logger.info(f"📋 Available columns: {
|
| 506 |
|
| 507 |
# Find the summary column (case-insensitive)
|
| 508 |
summary_col = None
|
| 509 |
-
for col in
|
| 510 |
if 'summary' in col.lower() or 'özet' in col.lower():
|
| 511 |
summary_col = col
|
| 512 |
logger.info(f"✓ Found summary column: '{summary_col}'")
|
|
@@ -514,38 +770,84 @@ def create_report():
|
|
| 514 |
|
| 515 |
if summary_col:
|
| 516 |
# Find and remove rows with matching summary
|
| 517 |
-
mask =
|
| 518 |
old_report_summary.lower(),
|
| 519 |
na=False,
|
| 520 |
regex=False
|
| 521 |
)
|
| 522 |
-
rows_before = len(
|
| 523 |
-
matching_rows =
|
| 524 |
logger.info(f"🔍 Found {len(matching_rows)} matching row(s):")
|
| 525 |
for idx, row in matching_rows.iterrows():
|
| 526 |
logger.info(f" Row {idx}: {row[summary_col][:80]}")
|
| 527 |
|
| 528 |
-
|
| 529 |
-
rows_after = len(
|
| 530 |
logger.info(f"🗑️ Deleted {rows_before - rows_after} old report(s)")
|
| 531 |
else:
|
| 532 |
-
logger.warning(f"⚠️ Could not find summary column in: {
|
| 533 |
|
| 534 |
# Append to DataFrame
|
| 535 |
-
|
| 536 |
-
|
| 537 |
pd.DataFrame([custom_row])
|
| 538 |
], ignore_index=True)
|
| 539 |
|
| 540 |
-
|
| 541 |
-
report_id =
|
| 542 |
|
| 543 |
-
#
|
| 544 |
-
csv_path = f"data/user_data/{
|
| 545 |
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
|
| 548 |
-
logger.info(f"✅ Report added to
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
else:
|
| 550 |
# Use default CSV path
|
| 551 |
csv_path = 'data/data_with_application.csv'
|
|
@@ -590,11 +892,16 @@ def create_report():
|
|
| 590 |
|
| 591 |
logger.info(f"✅ New report created: ID={report_id}, App={application}, Summary={data['summary'][:50]}...")
|
| 592 |
|
|
|
|
|
|
|
|
|
|
| 593 |
return jsonify({
|
| 594 |
'success': True,
|
| 595 |
'message': 'Report created successfully',
|
| 596 |
'report_id': report_id,
|
| 597 |
-
'application': application
|
|
|
|
|
|
|
| 598 |
})
|
| 599 |
|
| 600 |
except Exception as e:
|
|
@@ -618,7 +925,9 @@ def upload_data():
|
|
| 618 |
"data": [...], # Array of objects
|
| 619 |
"fileName": "data.csv",
|
| 620 |
"columns": ["col1", "col2", ...],
|
| 621 |
-
"rowCount": 100
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
"""
|
| 624 |
try:
|
|
@@ -626,8 +935,11 @@ def upload_data():
|
|
| 626 |
from datetime import datetime
|
| 627 |
import os
|
| 628 |
import json
|
|
|
|
| 629 |
|
| 630 |
-
|
|
|
|
|
|
|
| 631 |
|
| 632 |
# Get request data
|
| 633 |
data = request.get_json()
|
|
@@ -638,13 +950,26 @@ def upload_data():
|
|
| 638 |
'error': 'Missing data field'
|
| 639 |
}), 400
|
| 640 |
|
| 641 |
-
#
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
# Save to user-specific datasets list
|
| 650 |
username = data.get('username', 'demo')
|
|
@@ -662,12 +987,12 @@ def upload_data():
|
|
| 662 |
|
| 663 |
# Add new dataset to user's list
|
| 664 |
dataset_info = {
|
| 665 |
-
'name':
|
| 666 |
-
'fileName':
|
| 667 |
-
'filePath': f'user_data/{username}/{
|
| 668 |
-
'rowCount':
|
| 669 |
-
'columns':
|
| 670 |
-
'columnCount': len(
|
| 671 |
'fileSize': f"{len(str(data['data'])) / (1024*1024):.2f} MB",
|
| 672 |
'lastModified': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
| 673 |
'type': 'user',
|
|
@@ -677,7 +1002,7 @@ def upload_data():
|
|
| 677 |
# Check if already exists, update or append
|
| 678 |
exists = False
|
| 679 |
for i, ds in enumerate(user_datasets):
|
| 680 |
-
if ds.get('fileName') ==
|
| 681 |
user_datasets[i] = dataset_info
|
| 682 |
exists = True
|
| 683 |
break
|
|
@@ -689,16 +1014,75 @@ def upload_data():
|
|
| 689 |
with open(user_datasets_file, 'w') as f:
|
| 690 |
json.dump(user_datasets, f, indent=2)
|
| 691 |
|
| 692 |
-
logger.info(f"✅ Custom data uploaded and saved: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
|
| 694 |
return jsonify({
|
| 695 |
'success': True,
|
| 696 |
-
'message': 'Data uploaded successfully',
|
| 697 |
'info': {
|
| 698 |
-
'fileName':
|
| 699 |
-
'rowCount':
|
| 700 |
-
'columns':
|
| 701 |
-
'uploadedAt':
|
|
|
|
|
|
|
|
|
|
| 702 |
}
|
| 703 |
})
|
| 704 |
|
|
@@ -720,13 +1104,12 @@ def update_selected_columns():
|
|
| 720 |
|
| 721 |
Request Body:
|
| 722 |
{
|
|
|
|
| 723 |
"selectedColumns": ["Summary", "Description", ...], # For cross-encoder
|
| 724 |
"metadataColumns": ["Platform", "App Version", ...] # For form display
|
| 725 |
}
|
| 726 |
"""
|
| 727 |
try:
|
| 728 |
-
global custom_data_store
|
| 729 |
-
|
| 730 |
data = request.get_json()
|
| 731 |
|
| 732 |
if not data or 'selectedColumns' not in data:
|
|
@@ -735,18 +1118,40 @@ def update_selected_columns():
|
|
| 735 |
'error': 'Missing selectedColumns field'
|
| 736 |
}), 400
|
| 737 |
|
| 738 |
-
#
|
| 739 |
-
|
| 740 |
-
|
| 741 |
|
| 742 |
-
|
| 743 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
|
| 745 |
return jsonify({
|
| 746 |
'success': True,
|
| 747 |
'message': 'Selected columns updated',
|
| 748 |
-
'selectedColumns':
|
| 749 |
-
'metadataColumns':
|
|
|
|
| 750 |
})
|
| 751 |
|
| 752 |
except Exception as e:
|
|
@@ -791,35 +1196,29 @@ def clear_custom_data():
|
|
| 791 |
|
| 792 |
@app.route('/api/data_status', methods=['GET'])
|
| 793 |
def data_status():
|
| 794 |
-
"""Get current data status
|
| 795 |
try:
|
| 796 |
-
|
|
|
|
|
|
|
|
|
|
| 797 |
|
| 798 |
-
if
|
| 799 |
return jsonify({
|
| 800 |
'success': True,
|
| 801 |
'custom_data_loaded': True,
|
| 802 |
-
'custom_data_columns':
|
| 803 |
-
'fileName':
|
| 804 |
-
'rowCount':
|
|
|
|
| 805 |
})
|
| 806 |
else:
|
| 807 |
-
#
|
| 808 |
-
try:
|
| 809 |
-
search = get_search_system()
|
| 810 |
-
default_columns = list(search.df.columns)
|
| 811 |
-
except Exception as e:
|
| 812 |
-
logger.warning(f"Could not load default data info: {e}")
|
| 813 |
-
default_columns = [
|
| 814 |
-
'Summary', 'Description', 'Affects Version', 'Component',
|
| 815 |
-
'Priority', 'Custom field (Severity)', 'Custom field (Problem Type)',
|
| 816 |
-
'Custom field (Frequency)', 'Application', 'App Version'
|
| 817 |
-
]
|
| 818 |
-
|
| 819 |
return jsonify({
|
| 820 |
'success': True,
|
| 821 |
'custom_data_loaded': False,
|
| 822 |
-
'
|
|
|
|
| 823 |
})
|
| 824 |
|
| 825 |
except Exception as e:
|
|
@@ -835,14 +1234,19 @@ def data_status():
|
|
| 835 |
def get_column_values(column_name):
|
| 836 |
"""Get unique values for a specific column"""
|
| 837 |
try:
|
| 838 |
-
|
|
|
|
|
|
|
| 839 |
|
| 840 |
-
#
|
| 841 |
-
if
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
|
|
|
|
|
|
|
|
|
| 846 |
|
| 847 |
# Get unique values for the column
|
| 848 |
if column_name in df.columns:
|
|
@@ -854,12 +1258,14 @@ def get_column_values(column_name):
|
|
| 854 |
'success': True,
|
| 855 |
'column': column_name,
|
| 856 |
'values': unique_values,
|
| 857 |
-
'count': len(unique_values)
|
|
|
|
| 858 |
})
|
| 859 |
else:
|
| 860 |
return jsonify({
|
| 861 |
'success': False,
|
| 862 |
-
'error': f'Column {column_name} not found'
|
|
|
|
| 863 |
}), 404
|
| 864 |
|
| 865 |
except Exception as e:
|
|
@@ -885,42 +1291,10 @@ def get_available_datasets():
|
|
| 885 |
# Get username from query params (for user-specific datasets)
|
| 886 |
username = request.args.get('username', 'default')
|
| 887 |
|
| 888 |
-
#
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
'data_with_app_version.csv',
|
| 893 |
-
'data_cleaned.csv'
|
| 894 |
-
]
|
| 895 |
-
|
| 896 |
-
# Add default datasets
|
| 897 |
-
for csv_file in csv_files:
|
| 898 |
-
file_path = os.path.join(data_dir, csv_file)
|
| 899 |
-
if os.path.exists(file_path):
|
| 900 |
-
try:
|
| 901 |
-
# Read first few rows to get info (with semicolon delimiter)
|
| 902 |
-
df = pd.read_csv(file_path, nrows=5, encoding='utf-8', sep=';', on_bad_lines='skip')
|
| 903 |
-
file_stats = os.stat(file_path)
|
| 904 |
-
|
| 905 |
-
# Get full row count (faster method using line count)
|
| 906 |
-
with open(file_path, 'r', encoding='utf-8') as f:
|
| 907 |
-
row_count = sum(1 for line in f) - 1 # -1 for header
|
| 908 |
-
|
| 909 |
-
datasets.append({
|
| 910 |
-
'name': csv_file.replace('.csv', '').replace('_', ' ').title(),
|
| 911 |
-
'fileName': csv_file,
|
| 912 |
-
'filePath': file_path,
|
| 913 |
-
'rowCount': row_count,
|
| 914 |
-
'columns': df.columns.tolist(),
|
| 915 |
-
'columnCount': len(df.columns),
|
| 916 |
-
'fileSize': f"{file_stats.st_size / (1024*1024):.2f} MB",
|
| 917 |
-
'lastModified': datetime.fromtimestamp(file_stats.st_mtime).strftime('%Y-%m-%d %H:%M'),
|
| 918 |
-
'type': 'default',
|
| 919 |
-
'owner': 'system'
|
| 920 |
-
})
|
| 921 |
-
except Exception as e:
|
| 922 |
-
logger.error(f"Error reading {csv_file}: {e}")
|
| 923 |
-
continue
|
| 924 |
|
| 925 |
# Add user-specific datasets from localStorage data
|
| 926 |
user_datasets_file = os.path.join(data_dir, 'user_datasets', f'{username}.json')
|
|
@@ -1032,22 +1406,45 @@ def internal_error(error):
|
|
| 1032 |
|
| 1033 |
def main():
|
| 1034 |
"""Main function to run the server"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
print("\n" + "=" * 80)
|
| 1036 |
print("🚀 BUG REPORT DUPLICATE DETECTION API SERVER")
|
| 1037 |
print("=" * 80)
|
| 1038 |
-
print("\
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
|
| 1040 |
-
#
|
| 1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
|
| 1043 |
print("\n" + "=" * 80)
|
| 1044 |
print("✅ SERVER READY!")
|
| 1045 |
print("=" * 80)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
print("\n📍 Endpoints:")
|
| 1047 |
-
print(" • http://localhost:
|
| 1048 |
-
print(" • http://localhost:
|
| 1049 |
-
print(" • http://localhost:
|
| 1050 |
-
print(" • http://localhost:
|
| 1051 |
print("\n🌐 Frontend:")
|
| 1052 |
print(" • Open web/index.html in your browser")
|
| 1053 |
print("\n💡 Usage:")
|
|
@@ -1059,8 +1456,8 @@ def main():
|
|
| 1059 |
# Run Flask server
|
| 1060 |
app.run(
|
| 1061 |
host='0.0.0.0',
|
| 1062 |
-
port=
|
| 1063 |
-
debug=
|
| 1064 |
threaded=True
|
| 1065 |
)
|
| 1066 |
|
|
|
|
| 11 |
import logging
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
import time
|
| 14 |
+
from pathlib import Path
|
| 15 |
|
| 16 |
# Configure logging
|
| 17 |
logging.basicConfig(
|
|
|
|
| 27 |
# Initialize search system (singleton)
|
| 28 |
search_system = None
|
| 29 |
|
| 30 |
+
# Global variable to store custom uploaded data PER USER
|
| 31 |
+
# Structure: user_data_stores[user_id] = { data, fileName, rowCount, ... }
|
| 32 |
+
user_data_stores = {}
|
| 33 |
+
|
| 34 |
+
def get_user_data_store(user_id: str) -> dict:
|
| 35 |
+
"""Get data store for a specific user - loads from disk if exists"""
|
| 36 |
+
if user_id not in user_data_stores:
|
| 37 |
+
# Check if user has data on disk
|
| 38 |
+
user_embeddings_dir = Path('data/user_embeddings') / user_id
|
| 39 |
+
metadata_file = user_embeddings_dir / 'metadata.json'
|
| 40 |
+
|
| 41 |
+
if metadata_file.exists():
|
| 42 |
+
# Load from disk
|
| 43 |
+
try:
|
| 44 |
+
import json
|
| 45 |
+
import pandas as pd
|
| 46 |
+
|
| 47 |
+
with open(metadata_file, 'r') as f:
|
| 48 |
+
metadata = json.load(f)
|
| 49 |
+
|
| 50 |
+
# Load the original data if available
|
| 51 |
+
data_file = user_embeddings_dir / 'data.csv'
|
| 52 |
+
df = None
|
| 53 |
+
if data_file.exists():
|
| 54 |
+
df = pd.read_csv(data_file)
|
| 55 |
+
logger.info(f"📂 Loaded user data from disk for user: {user_id} ({len(df)} rows)")
|
| 56 |
+
|
| 57 |
+
# Reconstruct user_data_store from metadata
|
| 58 |
+
user_data_stores[user_id] = {
|
| 59 |
+
'data': df,
|
| 60 |
+
'fileName': metadata.get('fileName', 'uploaded_data.csv'),
|
| 61 |
+
'rowCount': metadata.get('recordCount', len(df) if df is not None else 0),
|
| 62 |
+
'columns': metadata.get('textColumns', []),
|
| 63 |
+
'selectedColumns': metadata.get('textColumns', []),
|
| 64 |
+
'metadataColumns': metadata.get('metadataColumns', []),
|
| 65 |
+
'uploadedAt': metadata.get('createdAt'),
|
| 66 |
+
'loaded': df is not None,
|
| 67 |
+
'userId': user_id,
|
| 68 |
+
'embeddingsReady': True
|
| 69 |
+
}
|
| 70 |
+
logger.info(f"✅ Restored user data store from disk for user: {user_id}")
|
| 71 |
+
except Exception as e:
|
| 72 |
+
logger.error(f"❌ Error loading user data from disk: {e}")
|
| 73 |
+
# Fall through to create empty store
|
| 74 |
+
user_data_stores[user_id] = {
|
| 75 |
'data': None,
|
| 76 |
'fileName': None,
|
| 77 |
'rowCount': 0,
|
| 78 |
'columns': [],
|
| 79 |
+
'selectedColumns': [],
|
| 80 |
+
'metadataColumns': [],
|
| 81 |
'uploadedAt': None,
|
| 82 |
+
'loaded': False,
|
| 83 |
+
'userId': user_id
|
| 84 |
+
}
|
| 85 |
+
else:
|
| 86 |
+
# Create empty store
|
| 87 |
+
user_data_stores[user_id] = {
|
| 88 |
+
'data': None,
|
| 89 |
+
'fileName': None,
|
| 90 |
+
'rowCount': 0,
|
| 91 |
+
'columns': [],
|
| 92 |
+
'selectedColumns': [],
|
| 93 |
+
'metadataColumns': [],
|
| 94 |
+
'uploadedAt': None,
|
| 95 |
+
'loaded': False,
|
| 96 |
+
'userId': user_id
|
| 97 |
+
}
|
| 98 |
+
return user_data_stores[user_id]
|
| 99 |
+
|
| 100 |
+
def set_user_data_store(user_id: str, data_store: dict):
|
| 101 |
+
"""Set data store for a specific user"""
|
| 102 |
+
data_store['userId'] = user_id
|
| 103 |
+
user_data_stores[user_id] = data_store
|
| 104 |
+
|
| 105 |
+
def clear_user_data_store(user_id: str):
|
| 106 |
+
"""Clear data store for a specific user"""
|
| 107 |
+
if user_id in user_data_stores:
|
| 108 |
+
del user_data_stores[user_id]
|
| 109 |
+
logger.info(f"✅ Custom data cleared for user: {user_id}")
|
| 110 |
|
| 111 |
def get_search_system():
|
| 112 |
"""Get or initialize the search system"""
|
|
|
|
| 118 |
return search_system
|
| 119 |
|
| 120 |
|
| 121 |
+
def update_embeddings_for_new_report(new_row_index):
|
| 122 |
+
"""
|
| 123 |
+
Update embeddings and FAISS indices for a newly added report
|
| 124 |
+
|
| 125 |
+
Args:
|
| 126 |
+
new_row_index: Index of the new row in the DataFrame (0-based)
|
| 127 |
+
"""
|
| 128 |
+
global search_system
|
| 129 |
+
|
| 130 |
+
if search_system is None:
|
| 131 |
+
logger.warning("⚠️ Search system not initialized, cannot update embeddings")
|
| 132 |
+
return
|
| 133 |
+
|
| 134 |
+
try:
|
| 135 |
+
import numpy as np
|
| 136 |
+
import faiss
|
| 137 |
+
from pathlib import Path
|
| 138 |
+
|
| 139 |
+
# Reload DataFrame to get the new report
|
| 140 |
+
logger.info(f"📥 Reloading data to include new report...")
|
| 141 |
+
search_system.load_data()
|
| 142 |
+
|
| 143 |
+
# Get the new row
|
| 144 |
+
if new_row_index >= len(search_system.df):
|
| 145 |
+
logger.error(f"❌ Invalid row index: {new_row_index}, DataFrame has {len(search_system.df)} rows")
|
| 146 |
+
return
|
| 147 |
+
|
| 148 |
+
new_row = search_system.df.iloc[new_row_index]
|
| 149 |
+
|
| 150 |
+
# Generate embedding for the new report
|
| 151 |
+
logger.info(f"🔄 Generating embedding for new report: '{new_row.get('Summary', '')[:50]}...'")
|
| 152 |
+
summary = str(new_row.get('Summary', ''))
|
| 153 |
+
description = str(new_row.get('Description', ''))
|
| 154 |
+
combined_text = f"{summary}. {description}".strip().lower()
|
| 155 |
+
|
| 156 |
+
new_embedding = search_system.bi_encoder.encode(
|
| 157 |
+
[combined_text],
|
| 158 |
+
batch_size=1,
|
| 159 |
+
show_progress_bar=False,
|
| 160 |
+
convert_to_numpy=True
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
# Load existing embeddings
|
| 164 |
+
embeddings_path = Path(search_system.embeddings_dir) / "embeddings.npy"
|
| 165 |
+
if embeddings_path.exists():
|
| 166 |
+
existing_embeddings = np.load(embeddings_path)
|
| 167 |
+
logger.info(f"📊 Loaded existing embeddings: {existing_embeddings.shape}")
|
| 168 |
+
|
| 169 |
+
# Append new embedding
|
| 170 |
+
updated_embeddings = np.vstack([existing_embeddings, new_embedding])
|
| 171 |
+
logger.info(f"✅ New embeddings shape: {updated_embeddings.shape}")
|
| 172 |
+
|
| 173 |
+
# Save updated embeddings
|
| 174 |
+
np.save(embeddings_path, updated_embeddings)
|
| 175 |
+
logger.info(f"💾 Saved updated embeddings to {embeddings_path}")
|
| 176 |
+
|
| 177 |
+
# Update in-memory embeddings
|
| 178 |
+
search_system.embeddings = updated_embeddings
|
| 179 |
+
|
| 180 |
+
# Add to FAISS index
|
| 181 |
+
platform = str(new_row.get('Platform', 'unknown')).lower()
|
| 182 |
+
if platform not in ['android', 'ios', 'unknown']:
|
| 183 |
+
platform = 'unknown'
|
| 184 |
+
|
| 185 |
+
logger.info(f"🔄 Adding to FAISS index: {platform}")
|
| 186 |
+
|
| 187 |
+
if platform in search_system.faiss_indices:
|
| 188 |
+
# Normalize the embedding (FAISS uses cosine similarity with normalized vectors)
|
| 189 |
+
normalized_embedding = new_embedding / np.linalg.norm(new_embedding)
|
| 190 |
+
|
| 191 |
+
# Add to FAISS index
|
| 192 |
+
search_system.faiss_indices[platform].add(normalized_embedding.astype('float32'))
|
| 193 |
+
|
| 194 |
+
# Save updated FAISS index
|
| 195 |
+
index_path = Path(search_system.embeddings_dir) / f"faiss_index_{platform}.index"
|
| 196 |
+
faiss.write_index(search_system.faiss_indices[platform], str(index_path))
|
| 197 |
+
logger.info(f"💾 Saved updated FAISS index to {index_path}")
|
| 198 |
+
|
| 199 |
+
logger.info(f"✅ Successfully added new report to FAISS index ({platform})")
|
| 200 |
+
else:
|
| 201 |
+
logger.warning(f"⚠️ FAISS index not found for platform: {platform}")
|
| 202 |
+
else:
|
| 203 |
+
logger.warning(f"⚠️ Embeddings file not found: {embeddings_path}")
|
| 204 |
+
logger.warning("⚠️ Please run embedding pipeline to generate embeddings")
|
| 205 |
+
|
| 206 |
+
except Exception as e:
|
| 207 |
+
logger.error(f"❌ Error updating embeddings: {e}")
|
| 208 |
+
import traceback
|
| 209 |
+
traceback.print_exc()
|
| 210 |
+
raise
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
def search_custom_data(query, df, top_k=10, selected_columns=None, user_id=None):
|
| 214 |
"""
|
| 215 |
+
Hybrid search on custom data using user's embeddings
|
| 216 |
+
Falls back to text-based search if embeddings not available
|
| 217 |
|
| 218 |
Args:
|
| 219 |
query: Search query
|
| 220 |
df: Custom DataFrame
|
| 221 |
top_k: Number of results to return
|
| 222 |
selected_columns: List of columns selected by user for search (priority)
|
| 223 |
+
user_id: User ID (for loading embeddings)
|
| 224 |
"""
|
| 225 |
import pandas as pd
|
| 226 |
from difflib import SequenceMatcher
|
| 227 |
+
import sys
|
| 228 |
+
import os
|
| 229 |
+
|
| 230 |
+
# Try hybrid search with embeddings first
|
| 231 |
+
if user_id:
|
| 232 |
+
try:
|
| 233 |
+
# Add src to path
|
| 234 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
| 235 |
+
from user_hybrid_search import search_user_data
|
| 236 |
+
|
| 237 |
+
logger.info(f"🚀 Using Hybrid Search with embeddings for user: {user_id}")
|
| 238 |
+
results = search_user_data(user_id, query, df, top_k)
|
| 239 |
+
|
| 240 |
+
if results and len(results) > 0:
|
| 241 |
+
logger.info(f"✅ Hybrid search returned {len(results)} results")
|
| 242 |
+
return results
|
| 243 |
+
else:
|
| 244 |
+
logger.warning(f"⚠️ Hybrid search returned no results, falling back to text search")
|
| 245 |
+
|
| 246 |
+
except Exception as e:
|
| 247 |
+
logger.warning(f"⚠️ Hybrid search failed: {e}, falling back to text search")
|
| 248 |
+
|
| 249 |
+
# Fallback: Simple text-based search
|
| 250 |
+
logger.info(f"🔍 Using fallback text-based search")
|
| 251 |
|
| 252 |
results = []
|
| 253 |
query_lower = query.lower()
|
|
|
|
| 396 |
if not isinstance(top_k, int) or top_k < 1 or top_k > 50:
|
| 397 |
top_k = 10
|
| 398 |
|
| 399 |
+
# Get user_id from request (required for user-specific search)
|
| 400 |
+
user_id = data.get('user_id', 'anonymous')
|
| 401 |
|
| 402 |
+
logger.info(f"🔍 Search request: query='{query[:50]}...', app={application}, platform={platform}, user_id={user_id}")
|
|
|
|
| 403 |
|
| 404 |
+
# Get user-specific data store
|
| 405 |
+
user_store = get_user_data_store(user_id)
|
| 406 |
+
|
| 407 |
+
if user_store['loaded'] and user_store['data'] is not None:
|
| 408 |
+
# Use custom data for search (hybrid search with user embeddings)
|
| 409 |
+
cols_to_use = selected_columns if selected_columns != ['Summary', 'Description'] else user_store.get('selectedColumns', selected_columns)
|
| 410 |
+
|
| 411 |
+
logger.info(f"📤 Using custom uploaded data: {user_store['fileName']}")
|
| 412 |
+
logger.info(f"👤 User ID: {user_id}")
|
| 413 |
logger.info(f"🎯 Selected columns for search: {cols_to_use}")
|
| 414 |
+
|
| 415 |
start_time = time.time()
|
| 416 |
results = search_custom_data(
|
| 417 |
query,
|
| 418 |
+
user_store['data'],
|
| 419 |
top_k,
|
| 420 |
+
selected_columns=cols_to_use,
|
| 421 |
+
user_id=user_id # Pass user_id for embedding-based search
|
| 422 |
)
|
| 423 |
search_time = time.time() - start_time
|
| 424 |
logger.info(f"✅ Found {len(results)} results in custom data in {search_time:.2f}s")
|
| 425 |
else:
|
| 426 |
+
# No data loaded - user must upload their own data
|
| 427 |
+
logger.warning(f"⚠️ No data uploaded for user: {user_id}")
|
| 428 |
+
return jsonify({
|
| 429 |
+
'success': False,
|
| 430 |
+
'error': 'No data uploaded. Please upload your data first.',
|
| 431 |
+
'message': 'You must upload your data before searching. Go to Data Upload page.',
|
| 432 |
+
'userId': user_id
|
| 433 |
+
}), 400
|
| 434 |
+
|
| 435 |
+
# Clean NaN values from results (JSON doesn't support NaN)
|
| 436 |
+
import math
|
| 437 |
+
def clean_nan(obj):
|
| 438 |
+
"""Recursively replace NaN values with None or 0"""
|
| 439 |
+
if isinstance(obj, dict):
|
| 440 |
+
return {k: clean_nan(v) for k, v in obj.items()}
|
| 441 |
+
elif isinstance(obj, list):
|
| 442 |
+
return [clean_nan(item) for item in obj]
|
| 443 |
+
elif isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)):
|
| 444 |
+
return 0.0 # Replace NaN/Inf with 0
|
| 445 |
+
else:
|
| 446 |
+
return obj
|
| 447 |
+
|
| 448 |
+
cleaned_results = clean_nan(results)
|
| 449 |
|
| 450 |
# Return results
|
| 451 |
return jsonify({
|
|
|
|
| 457 |
'version': version,
|
| 458 |
'language': language
|
| 459 |
},
|
| 460 |
+
'results': cleaned_results,
|
| 461 |
+
'count': len(cleaned_results),
|
| 462 |
'search_time': round(search_time, 2)
|
| 463 |
})
|
| 464 |
|
|
|
|
| 485 |
}
|
| 486 |
"""
|
| 487 |
try:
|
| 488 |
+
# Get user_id from query parameter
|
| 489 |
+
user_id = request.args.get('user_id', 'anonymous')
|
| 490 |
+
|
| 491 |
+
# Get user-specific data store
|
| 492 |
+
user_store = get_user_data_store(user_id)
|
| 493 |
+
|
| 494 |
+
if user_store['loaded'] and user_store['data'] is not None:
|
| 495 |
+
df = user_store['data']
|
| 496 |
+
logger.info(f"📊 Getting stats from custom data for user {user_id}: {len(df)} rows")
|
| 497 |
|
| 498 |
# Calculate statistics from custom data
|
| 499 |
stats = {
|
|
|
|
| 518 |
'success': True,
|
| 519 |
'stats': stats,
|
| 520 |
'customDataLoaded': True,
|
| 521 |
+
'fileName': user_store['fileName'],
|
| 522 |
+
'userId': user_id
|
| 523 |
})
|
| 524 |
else:
|
| 525 |
+
# No data loaded for this user
|
| 526 |
+
logger.info(f"⚠️ No data loaded for user: {user_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
return jsonify({
|
| 529 |
'success': True,
|
| 530 |
+
'stats': {
|
| 531 |
+
'total_reports': 0,
|
| 532 |
+
'platforms': {},
|
| 533 |
+
'applications': {}
|
| 534 |
+
},
|
| 535 |
+
'customDataLoaded': False,
|
| 536 |
+
'message': 'No data uploaded. Please upload your data first.',
|
| 537 |
+
'userId': user_id
|
| 538 |
})
|
| 539 |
|
| 540 |
except Exception as e:
|
|
|
|
| 548 |
@app.route('/api/applications', methods=['GET'])
|
| 549 |
def get_applications():
|
| 550 |
"""Get list of available applications"""
|
| 551 |
+
try:
|
| 552 |
+
# Check if custom data is loaded
|
| 553 |
+
if custom_data_store['loaded'] and custom_data_store['data'] is not None:
|
| 554 |
+
df = custom_data_store['data']
|
| 555 |
+
|
| 556 |
+
if 'Application' not in df.columns:
|
| 557 |
+
applications = []
|
| 558 |
+
else:
|
| 559 |
+
applications = sorted(df['Application'].unique().tolist())
|
| 560 |
+
|
| 561 |
+
return jsonify({
|
| 562 |
+
'success': True,
|
| 563 |
+
'applications': applications
|
| 564 |
+
})
|
| 565 |
+
else:
|
| 566 |
+
# No data loaded
|
| 567 |
+
return jsonify({
|
| 568 |
+
'success': True,
|
| 569 |
+
'applications': []
|
| 570 |
+
})
|
| 571 |
+
|
| 572 |
+
except Exception as e:
|
| 573 |
+
logger.error(f"❌ Applications error: {e}")
|
| 574 |
+
return jsonify({
|
| 575 |
+
'success': False,
|
| 576 |
+
'error': str(e)
|
| 577 |
+
}), 500
|
| 578 |
+
|
| 579 |
+
|
| 580 |
+
@app.route('/api/applications_OLD', methods=['GET'])
|
| 581 |
+
def get_applications_old():
|
| 582 |
+
"""Get list of available applications - OLD VERSION"""
|
| 583 |
try:
|
| 584 |
search = get_search_system()
|
| 585 |
|
|
|
|
| 629 |
# Get request data
|
| 630 |
data = request.get_json()
|
| 631 |
|
| 632 |
+
import uuid
|
| 633 |
+
request_id = str(uuid.uuid4())[:8]
|
| 634 |
+
|
| 635 |
+
logger.info(f"[{request_id}] 📥 Received create_report request with data keys: {list(data.keys()) if data else 'None'}")
|
| 636 |
+
logger.info(f"[{request_id}] 📝 Full data: {data}")
|
| 637 |
+
|
| 638 |
if data and 'replace_report' in data:
|
| 639 |
+
logger.info(f"[{request_id}] 🔍 replace_report parameter found: {data.get('replace_report')}")
|
| 640 |
+
logger.info(f"[{request_id}] 🔍 old_report_summary parameter: {data.get('old_report_summary')}")
|
| 641 |
+
|
| 642 |
+
if not data:
|
| 643 |
+
logger.error(f"[{request_id}] ❌ VALIDATION FAILED: No data received!")
|
| 644 |
+
return jsonify({
|
| 645 |
+
'success': False,
|
| 646 |
+
'error': 'No data received'
|
| 647 |
+
}), 400
|
| 648 |
|
| 649 |
+
if 'summary' not in data:
|
| 650 |
+
logger.error(f"[{request_id}] ❌ VALIDATION FAILED: Missing summary field. Data keys: {list(data.keys())}")
|
| 651 |
return jsonify({
|
| 652 |
'success': False,
|
| 653 |
+
'error': 'Missing required field: summary'
|
| 654 |
}), 400
|
| 655 |
|
| 656 |
+
logger.info(f"[{request_id}] ✅ Validation passed: summary found = '{data['summary']}'")
|
| 657 |
+
|
| 658 |
+
# Get user_id from request
|
| 659 |
+
user_id = data.get('userId') or data.get('user_id', 'anonymous')
|
| 660 |
+
logger.info(f"👤 Create report request from user: {user_id}")
|
| 661 |
+
|
| 662 |
# Check if we're replacing an old report
|
| 663 |
replace_report = data.get('replace_report', False)
|
| 664 |
old_report_summary = data.get('old_report_summary', '')
|
|
|
|
| 666 |
|
| 667 |
logger.info(f"🎯 replace_report={replace_report}, old_report_summary='{old_report_summary}'")
|
| 668 |
|
| 669 |
+
# DEBUG: Check user store status
|
| 670 |
+
user_store = get_user_data_store(user_id)
|
| 671 |
+
logger.info(f"🔍 DEBUG user_store: loaded={user_store.get('loaded')}, data_is_none={user_store.get('data') is None}")
|
| 672 |
+
if user_store.get('data') is not None:
|
| 673 |
+
logger.info(f"🔍 DEBUG user_store data shape: {user_store['data'].shape}")
|
| 674 |
+
|
| 675 |
# Detect application from summary
|
| 676 |
summary_lower = data['summary'].lower()
|
| 677 |
application = 'Unknown'
|
|
|
|
| 715 |
'Application': application
|
| 716 |
}
|
| 717 |
|
| 718 |
+
# User store already retrieved above (line 640)
|
| 719 |
+
if user_store['loaded'] and user_store['data'] is not None:
|
| 720 |
+
# Append to user's custom data
|
| 721 |
+
logger.info(f"📤 Appending to user {user_id}'s data: {user_store['fileName']}")
|
|
|
|
|
|
|
| 722 |
|
| 723 |
# Create new row with custom data columns
|
| 724 |
+
custom_row = {col: '' for col in user_store['data'].columns}
|
| 725 |
|
| 726 |
# Map ALL form data fields to custom columns dynamically
|
| 727 |
for key, value in data.items():
|
| 728 |
# Try to find matching column (exact match or partial match)
|
| 729 |
+
for col in user_store['data'].columns:
|
| 730 |
col_lower = col.lower()
|
| 731 |
key_lower = key.lower()
|
| 732 |
|
|
|
|
| 736 |
break
|
| 737 |
|
| 738 |
# Also try common mappings
|
| 739 |
+
for col in user_store['data'].columns:
|
| 740 |
col_lower = col.lower()
|
| 741 |
if 'summary' in col_lower or 'özet' in col_lower:
|
| 742 |
if not custom_row[col]: # Only if not already set
|
|
|
|
| 757 |
# If replacing an old report, delete it first
|
| 758 |
if replace_report and old_report_summary:
|
| 759 |
logger.info(f"🔄 Replacing old report: '{old_report_summary}'")
|
| 760 |
+
logger.info(f"📊 Current DataFrame shape: {user_store['data'].shape}")
|
| 761 |
+
logger.info(f"📋 Available columns: {user_store['data'].columns.tolist()}")
|
| 762 |
|
| 763 |
# Find the summary column (case-insensitive)
|
| 764 |
summary_col = None
|
| 765 |
+
for col in user_store['data'].columns:
|
| 766 |
if 'summary' in col.lower() or 'özet' in col.lower():
|
| 767 |
summary_col = col
|
| 768 |
logger.info(f"✓ Found summary column: '{summary_col}'")
|
|
|
|
| 770 |
|
| 771 |
if summary_col:
|
| 772 |
# Find and remove rows with matching summary
|
| 773 |
+
mask = user_store['data'][summary_col].astype(str).str.lower().str.contains(
|
| 774 |
old_report_summary.lower(),
|
| 775 |
na=False,
|
| 776 |
regex=False
|
| 777 |
)
|
| 778 |
+
rows_before = len(user_store['data'])
|
| 779 |
+
matching_rows = user_store['data'][mask]
|
| 780 |
logger.info(f"🔍 Found {len(matching_rows)} matching row(s):")
|
| 781 |
for idx, row in matching_rows.iterrows():
|
| 782 |
logger.info(f" Row {idx}: {row[summary_col][:80]}")
|
| 783 |
|
| 784 |
+
user_store['data'] = user_store['data'][~mask]
|
| 785 |
+
rows_after = len(user_store['data'])
|
| 786 |
logger.info(f"🗑️ Deleted {rows_before - rows_after} old report(s)")
|
| 787 |
else:
|
| 788 |
+
logger.warning(f"⚠️ Could not find summary column in: {user_store['data'].columns.tolist()}")
|
| 789 |
|
| 790 |
# Append to DataFrame
|
| 791 |
+
user_store['data'] = pd.concat([
|
| 792 |
+
user_store['data'],
|
| 793 |
pd.DataFrame([custom_row])
|
| 794 |
], ignore_index=True)
|
| 795 |
|
| 796 |
+
user_store['rowCount'] = len(user_store['data'])
|
| 797 |
+
report_id = user_store['rowCount']
|
| 798 |
|
| 799 |
+
# Save to user's CSV file (both in user_data and user_embeddings)
|
| 800 |
+
csv_path = f"data/user_data/{user_store.get('fileName', 'custom_data.csv')}"
|
| 801 |
os.makedirs(os.path.dirname(csv_path), exist_ok=True)
|
| 802 |
+
user_store['data'].to_csv(csv_path, index=False, encoding='utf-8')
|
| 803 |
+
|
| 804 |
+
# Also save to user_embeddings directory for persistence
|
| 805 |
+
user_embeddings_dir = Path('data/user_embeddings') / user_id
|
| 806 |
+
user_embeddings_dir.mkdir(parents=True, exist_ok=True)
|
| 807 |
+
user_data_file = user_embeddings_dir / 'data.csv'
|
| 808 |
+
user_store['data'].to_csv(user_data_file, index=False)
|
| 809 |
+
|
| 810 |
+
# CRITICAL: Reload data from disk to ensure consistency
|
| 811 |
+
# This ensures subsequent searches use the updated data
|
| 812 |
+
user_store['data'] = pd.read_csv(user_data_file)
|
| 813 |
+
user_store['rowCount'] = len(user_store['data'])
|
| 814 |
+
|
| 815 |
+
# Update user store
|
| 816 |
+
set_user_data_store(user_id, user_store)
|
| 817 |
|
| 818 |
+
logger.info(f"✅ Report added to user {user_id}'s data and saved. New count: {report_id}")
|
| 819 |
+
logger.info(f"🔄 Data reloaded from disk to ensure consistency")
|
| 820 |
+
|
| 821 |
+
# CRITICAL: Regenerate embeddings for updated data
|
| 822 |
+
# Do this SYNCHRONOUSLY to ensure search results are correct
|
| 823 |
+
logger.info(f"🔄 Regenerating embeddings for user {user_id}...")
|
| 824 |
+
|
| 825 |
+
try:
|
| 826 |
+
from src.user_embedding_pipeline import create_user_embeddings
|
| 827 |
+
|
| 828 |
+
# Get text columns from metadata
|
| 829 |
+
text_columns = user_store.get('textColumns', ['Summary', 'Description'])
|
| 830 |
+
|
| 831 |
+
logger.info(f"📝 Using text columns: {text_columns}")
|
| 832 |
+
|
| 833 |
+
# Regenerate embeddings SYNCHRONOUSLY
|
| 834 |
+
success = create_user_embeddings(
|
| 835 |
+
user_id=user_id,
|
| 836 |
+
df=user_store['data'],
|
| 837 |
+
text_columns=text_columns
|
| 838 |
+
)
|
| 839 |
+
|
| 840 |
+
if success:
|
| 841 |
+
logger.info(f"✅ Embeddings regenerated successfully for user {user_id}")
|
| 842 |
+
embeddings_updated = True
|
| 843 |
+
else:
|
| 844 |
+
logger.warning(f"⚠️ Embedding regeneration failed for user {user_id}")
|
| 845 |
+
embeddings_updated = False
|
| 846 |
+
|
| 847 |
+
except Exception as e:
|
| 848 |
+
logger.error(f"❌ Error regenerating embeddings: {e}")
|
| 849 |
+
logger.exception(e)
|
| 850 |
+
embeddings_updated = False
|
| 851 |
else:
|
| 852 |
# Use default CSV path
|
| 853 |
csv_path = 'data/data_with_application.csv'
|
|
|
|
| 892 |
|
| 893 |
logger.info(f"✅ New report created: ID={report_id}, App={application}, Summary={data['summary'][:50]}...")
|
| 894 |
|
| 895 |
+
# NOTE: Embeddings should NOT be updated here for user-specific mode
|
| 896 |
+
# Users should re-upload their data or we should implement incremental embedding updates
|
| 897 |
+
|
| 898 |
return jsonify({
|
| 899 |
'success': True,
|
| 900 |
'message': 'Report created successfully',
|
| 901 |
'report_id': report_id,
|
| 902 |
+
'application': application,
|
| 903 |
+
'userId': user_id,
|
| 904 |
+
'embeddings_updated': False # Embeddings not auto-updated in user-specific mode
|
| 905 |
})
|
| 906 |
|
| 907 |
except Exception as e:
|
|
|
|
| 925 |
"data": [...], # Array of objects
|
| 926 |
"fileName": "data.csv",
|
| 927 |
"columns": ["col1", "col2", ...],
|
| 928 |
+
"rowCount": 100,
|
| 929 |
+
"userId": "firebase-user-id", # REQUIRED
|
| 930 |
+
"textColumns": ["Summary", "Description"] # Optional
|
| 931 |
}
|
| 932 |
"""
|
| 933 |
try:
|
|
|
|
| 935 |
from datetime import datetime
|
| 936 |
import os
|
| 937 |
import json
|
| 938 |
+
import sys
|
| 939 |
|
| 940 |
+
# Add src to path for imports
|
| 941 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
| 942 |
+
from user_embedding_pipeline import create_user_embeddings
|
| 943 |
|
| 944 |
# Get request data
|
| 945 |
data = request.get_json()
|
|
|
|
| 950 |
'error': 'Missing data field'
|
| 951 |
}), 400
|
| 952 |
|
| 953 |
+
# Get user ID (REQUIRED)
|
| 954 |
+
user_id = data.get('userId') or data.get('username', 'anonymous')
|
| 955 |
+
logger.info(f"📥 Upload request from user: {user_id}")
|
| 956 |
+
|
| 957 |
+
# Store custom data for THIS USER
|
| 958 |
+
df = pd.DataFrame(data['data'])
|
| 959 |
+
user_data_store = {
|
| 960 |
+
'data': df,
|
| 961 |
+
'fileName': data.get('fileName', 'uploaded_data.csv'),
|
| 962 |
+
'rowCount': len(data['data']),
|
| 963 |
+
'columns': data.get('columns', list(data['data'][0].keys()) if data['data'] else []),
|
| 964 |
+
'uploadedAt': datetime.now().isoformat(),
|
| 965 |
+
'loaded': True,
|
| 966 |
+
'userId': user_id,
|
| 967 |
+
'selectedColumns': [],
|
| 968 |
+
'metadataColumns': []
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
# Save to user-specific store
|
| 972 |
+
set_user_data_store(user_id, user_data_store)
|
| 973 |
|
| 974 |
# Save to user-specific datasets list
|
| 975 |
username = data.get('username', 'demo')
|
|
|
|
| 987 |
|
| 988 |
# Add new dataset to user's list
|
| 989 |
dataset_info = {
|
| 990 |
+
'name': user_data_store['fileName'].replace('.csv', '').replace('_', ' ').title(),
|
| 991 |
+
'fileName': user_data_store['fileName'],
|
| 992 |
+
'filePath': f'user_data/{username}/{user_data_store["fileName"]}',
|
| 993 |
+
'rowCount': user_data_store['rowCount'],
|
| 994 |
+
'columns': user_data_store['columns'],
|
| 995 |
+
'columnCount': len(user_data_store['columns']),
|
| 996 |
'fileSize': f"{len(str(data['data'])) / (1024*1024):.2f} MB",
|
| 997 |
'lastModified': datetime.now().strftime('%Y-%m-%d %H:%M'),
|
| 998 |
'type': 'user',
|
|
|
|
| 1002 |
# Check if already exists, update or append
|
| 1003 |
exists = False
|
| 1004 |
for i, ds in enumerate(user_datasets):
|
| 1005 |
+
if ds.get('fileName') == user_data_store['fileName']:
|
| 1006 |
user_datasets[i] = dataset_info
|
| 1007 |
exists = True
|
| 1008 |
break
|
|
|
|
| 1014 |
with open(user_datasets_file, 'w') as f:
|
| 1015 |
json.dump(user_datasets, f, indent=2)
|
| 1016 |
|
| 1017 |
+
logger.info(f"✅ Custom data uploaded and saved for user {user_id}: {user_data_store['fileName']}, {user_data_store['rowCount']} rows, {len(user_data_store['columns'])} columns")
|
| 1018 |
+
|
| 1019 |
+
# 🔥 CRITICAL: Create embeddings for this user's data!
|
| 1020 |
+
try:
|
| 1021 |
+
logger.info(f"🔄 Creating embeddings for user: {user_id}")
|
| 1022 |
+
|
| 1023 |
+
# Get text columns (from request or auto-detect)
|
| 1024 |
+
text_columns = data.get('textColumns')
|
| 1025 |
+
if text_columns:
|
| 1026 |
+
logger.info(f"📝 Using provided text columns: {text_columns}")
|
| 1027 |
+
else:
|
| 1028 |
+
logger.info(f"🔍 Auto-detecting text columns...")
|
| 1029 |
+
|
| 1030 |
+
# Run embedding pipeline
|
| 1031 |
+
success = create_user_embeddings(
|
| 1032 |
+
user_id=user_id,
|
| 1033 |
+
df=df,
|
| 1034 |
+
text_columns=text_columns,
|
| 1035 |
+
config={
|
| 1036 |
+
'fileName': user_data_store['fileName'],
|
| 1037 |
+
'metadataColumns': user_data_store.get('metadataColumns', [])
|
| 1038 |
+
}
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
+
if success:
|
| 1042 |
+
logger.info(f"✅ Embeddings created successfully for user: {user_id}")
|
| 1043 |
+
|
| 1044 |
+
# Save original data to disk for persistence
|
| 1045 |
+
user_embeddings_dir = Path('data/user_embeddings') / user_id
|
| 1046 |
+
data_file = user_embeddings_dir / 'data.csv'
|
| 1047 |
+
try:
|
| 1048 |
+
df.to_csv(data_file, index=False)
|
| 1049 |
+
logger.info(f"💾 Saved user data to disk: {data_file}")
|
| 1050 |
+
except Exception as e:
|
| 1051 |
+
logger.error(f"❌ Error saving user data to disk: {e}")
|
| 1052 |
+
|
| 1053 |
+
# Update user store with embedding info
|
| 1054 |
+
user_store = get_user_data_store(user_id)
|
| 1055 |
+
user_store['embeddingsCreated'] = True
|
| 1056 |
+
user_store['embeddingsPath'] = f"data/user_embeddings/{user_id}"
|
| 1057 |
+
set_user_data_store(user_id, user_store)
|
| 1058 |
+
else:
|
| 1059 |
+
logger.warning(f"⚠️ Embedding creation failed for user: {user_id}")
|
| 1060 |
+
user_store = get_user_data_store(user_id)
|
| 1061 |
+
user_store['embeddingsCreated'] = False
|
| 1062 |
+
set_user_data_store(user_id, user_store)
|
| 1063 |
+
|
| 1064 |
+
except Exception as e:
|
| 1065 |
+
logger.error(f"❌ Embedding creation error for user {user_id}: {e}")
|
| 1066 |
+
import traceback
|
| 1067 |
+
traceback.print_exc()
|
| 1068 |
+
user_store = get_user_data_store(user_id)
|
| 1069 |
+
user_store['embeddingsCreated'] = False
|
| 1070 |
+
set_user_data_store(user_id, user_store)
|
| 1071 |
+
|
| 1072 |
+
# Get final user store for response
|
| 1073 |
+
final_user_store = get_user_data_store(user_id)
|
| 1074 |
|
| 1075 |
return jsonify({
|
| 1076 |
'success': True,
|
| 1077 |
+
'message': 'Data uploaded and embeddings created successfully',
|
| 1078 |
'info': {
|
| 1079 |
+
'fileName': final_user_store['fileName'],
|
| 1080 |
+
'rowCount': final_user_store['rowCount'],
|
| 1081 |
+
'columns': final_user_store['columns'],
|
| 1082 |
+
'uploadedAt': final_user_store['uploadedAt'],
|
| 1083 |
+
'embeddingsCreated': final_user_store.get('embeddingsCreated', False),
|
| 1084 |
+
'embeddingsPath': final_user_store.get('embeddingsPath', None),
|
| 1085 |
+
'userId': user_id
|
| 1086 |
}
|
| 1087 |
})
|
| 1088 |
|
|
|
|
| 1104 |
|
| 1105 |
Request Body:
|
| 1106 |
{
|
| 1107 |
+
"userId": "user123", # User ID
|
| 1108 |
"selectedColumns": ["Summary", "Description", ...], # For cross-encoder
|
| 1109 |
"metadataColumns": ["Platform", "App Version", ...] # For form display
|
| 1110 |
}
|
| 1111 |
"""
|
| 1112 |
try:
|
|
|
|
|
|
|
| 1113 |
data = request.get_json()
|
| 1114 |
|
| 1115 |
if not data or 'selectedColumns' not in data:
|
|
|
|
| 1118 |
'error': 'Missing selectedColumns field'
|
| 1119 |
}), 400
|
| 1120 |
|
| 1121 |
+
# Get user ID
|
| 1122 |
+
user_id = data.get('userId') or data.get('username', 'anonymous')
|
| 1123 |
+
user_store = get_user_data_store(user_id)
|
| 1124 |
|
| 1125 |
+
# Update selected columns
|
| 1126 |
+
user_store['selectedColumns'] = data['selectedColumns']
|
| 1127 |
+
user_store['metadataColumns'] = data.get('metadataColumns', [])
|
| 1128 |
+
set_user_data_store(user_id, user_store)
|
| 1129 |
+
|
| 1130 |
+
# Also update metadata.json for persistence
|
| 1131 |
+
try:
|
| 1132 |
+
user_embeddings_dir = Path('data/user_embeddings') / user_id
|
| 1133 |
+
metadata_file = user_embeddings_dir / 'metadata.json'
|
| 1134 |
+
if metadata_file.exists():
|
| 1135 |
+
import json
|
| 1136 |
+
with open(metadata_file, 'r') as f:
|
| 1137 |
+
metadata = json.load(f)
|
| 1138 |
+
metadata['textColumns'] = data['selectedColumns']
|
| 1139 |
+
metadata['metadataColumns'] = data.get('metadataColumns', [])
|
| 1140 |
+
with open(metadata_file, 'w') as f:
|
| 1141 |
+
json.dump(metadata, f, indent=2)
|
| 1142 |
+
logger.info(f"💾 Updated metadata.json for user {user_id}")
|
| 1143 |
+
except Exception as e:
|
| 1144 |
+
logger.error(f"❌ Error updating metadata.json: {e}")
|
| 1145 |
+
|
| 1146 |
+
logger.info(f"✅ Cross-encoder columns updated for user {user_id}: {user_store['selectedColumns']}")
|
| 1147 |
+
logger.info(f"✅ Metadata columns updated for user {user_id}: {user_store['metadataColumns']}")
|
| 1148 |
|
| 1149 |
return jsonify({
|
| 1150 |
'success': True,
|
| 1151 |
'message': 'Selected columns updated',
|
| 1152 |
+
'selectedColumns': user_store['selectedColumns'],
|
| 1153 |
+
'metadataColumns': user_store['metadataColumns'],
|
| 1154 |
+
'userId': user_id
|
| 1155 |
})
|
| 1156 |
|
| 1157 |
except Exception as e:
|
|
|
|
| 1196 |
|
| 1197 |
@app.route('/api/data_status', methods=['GET'])
|
| 1198 |
def data_status():
|
| 1199 |
+
"""Get current data status for a specific user"""
|
| 1200 |
try:
|
| 1201 |
+
# Get user_id from query parameter
|
| 1202 |
+
user_id = request.args.get('user_id', 'anonymous')
|
| 1203 |
+
|
| 1204 |
+
user_store = get_user_data_store(user_id)
|
| 1205 |
|
| 1206 |
+
if user_store['loaded']:
|
| 1207 |
return jsonify({
|
| 1208 |
'success': True,
|
| 1209 |
'custom_data_loaded': True,
|
| 1210 |
+
'custom_data_columns': user_store['columns'],
|
| 1211 |
+
'fileName': user_store['fileName'],
|
| 1212 |
+
'rowCount': user_store['rowCount'],
|
| 1213 |
+
'userId': user_id
|
| 1214 |
})
|
| 1215 |
else:
|
| 1216 |
+
# No data loaded for this user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1217 |
return jsonify({
|
| 1218 |
'success': True,
|
| 1219 |
'custom_data_loaded': False,
|
| 1220 |
+
'message': 'No data uploaded for this user',
|
| 1221 |
+
'userId': user_id
|
| 1222 |
})
|
| 1223 |
|
| 1224 |
except Exception as e:
|
|
|
|
| 1234 |
def get_column_values(column_name):
|
| 1235 |
"""Get unique values for a specific column"""
|
| 1236 |
try:
|
| 1237 |
+
# Get user ID from query params
|
| 1238 |
+
user_id = request.args.get('user_id', 'anonymous')
|
| 1239 |
+
user_store = get_user_data_store(user_id)
|
| 1240 |
|
| 1241 |
+
# Check if user has data
|
| 1242 |
+
if not user_store['loaded'] or user_store['data'] is None:
|
| 1243 |
+
return jsonify({
|
| 1244 |
+
'success': False,
|
| 1245 |
+
'error': 'No data uploaded. Please upload your data first.',
|
| 1246 |
+
'userId': user_id
|
| 1247 |
+
}), 400
|
| 1248 |
+
|
| 1249 |
+
df = user_store['data']
|
| 1250 |
|
| 1251 |
# Get unique values for the column
|
| 1252 |
if column_name in df.columns:
|
|
|
|
| 1258 |
'success': True,
|
| 1259 |
'column': column_name,
|
| 1260 |
'values': unique_values,
|
| 1261 |
+
'count': len(unique_values),
|
| 1262 |
+
'userId': user_id
|
| 1263 |
})
|
| 1264 |
else:
|
| 1265 |
return jsonify({
|
| 1266 |
'success': False,
|
| 1267 |
+
'error': f'Column {column_name} not found',
|
| 1268 |
+
'userId': user_id
|
| 1269 |
}), 404
|
| 1270 |
|
| 1271 |
except Exception as e:
|
|
|
|
| 1291 |
# Get username from query params (for user-specific datasets)
|
| 1292 |
username = request.args.get('username', 'default')
|
| 1293 |
|
| 1294 |
+
# NO DEFAULT DATASETS - Users must upload their own data
|
| 1295 |
+
# Only show user-specific datasets
|
| 1296 |
+
|
| 1297 |
+
logger.info(f"📋 Getting datasets for user: {username}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
|
| 1299 |
# Add user-specific datasets from localStorage data
|
| 1300 |
user_datasets_file = os.path.join(data_dir, 'user_datasets', f'{username}.json')
|
|
|
|
| 1406 |
|
| 1407 |
def main():
|
| 1408 |
"""Main function to run the server"""
|
| 1409 |
+
# Load environment variables
|
| 1410 |
+
from dotenv import load_dotenv
|
| 1411 |
+
load_dotenv()
|
| 1412 |
+
|
| 1413 |
print("\n" + "=" * 80)
|
| 1414 |
print("🚀 BUG REPORT DUPLICATE DETECTION API SERVER")
|
| 1415 |
print("=" * 80)
|
| 1416 |
+
print("\n⚠️ USER-SPECIFIC MODE:")
|
| 1417 |
+
print(" • No default data loaded")
|
| 1418 |
+
print(" • Each user must upload their own data")
|
| 1419 |
+
print(" • Users can only see their own data")
|
| 1420 |
+
print(" • Embeddings are created per user")
|
| 1421 |
+
print(" • Firebase Storage caching enabled")
|
| 1422 |
+
|
| 1423 |
+
# DO NOT pre-initialize search system - users will upload their own data
|
| 1424 |
+
# get_search_system() # REMOVED: No default Turkcell data
|
| 1425 |
|
| 1426 |
+
# Check Firebase configuration
|
| 1427 |
+
firebase_enabled = os.getenv('USE_FIREBASE_CACHE', 'True').lower() == 'true'
|
| 1428 |
+
if firebase_enabled:
|
| 1429 |
+
print("\n🔥 Firebase Configuration:")
|
| 1430 |
+
service_account = os.getenv('FIREBASE_SERVICE_ACCOUNT', 'Not set')
|
| 1431 |
+
storage_bucket = os.getenv('FIREBASE_STORAGE_BUCKET', 'Not set')
|
| 1432 |
+
print(f" • Service Account: {service_account}")
|
| 1433 |
+
print(f" • Storage Bucket: {storage_bucket}")
|
| 1434 |
|
| 1435 |
print("\n" + "=" * 80)
|
| 1436 |
print("✅ SERVER READY!")
|
| 1437 |
print("=" * 80)
|
| 1438 |
+
|
| 1439 |
+
# Get port from environment (Railway sets this)
|
| 1440 |
+
port = int(os.getenv('PORT', 5001))
|
| 1441 |
+
|
| 1442 |
+
print(f"\n📍 Server will start on port: {port}")
|
| 1443 |
print("\n📍 Endpoints:")
|
| 1444 |
+
print(f" • http://localhost:{port}/api/health - Health check")
|
| 1445 |
+
print(f" • http://localhost:{port}/api/search - Search similar reports (POST)")
|
| 1446 |
+
print(f" • http://localhost:{port}/api/stats - Get system statistics")
|
| 1447 |
+
print(f" • http://localhost:{port}/api/applications - Get available applications")
|
| 1448 |
print("\n🌐 Frontend:")
|
| 1449 |
print(" • Open web/index.html in your browser")
|
| 1450 |
print("\n💡 Usage:")
|
|
|
|
| 1456 |
# Run Flask server
|
| 1457 |
app.run(
|
| 1458 |
host='0.0.0.0',
|
| 1459 |
+
port=port,
|
| 1460 |
+
debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true',
|
| 1461 |
threaded=True
|
| 1462 |
)
|
| 1463 |
|
env.example
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ===================================
|
| 2 |
+
# JIRA DUPLICATE DETECTION - ENV CONFIG
|
| 3 |
+
# ===================================
|
| 4 |
+
|
| 5 |
+
# Firebase Configuration
|
| 6 |
+
# -----------------------
|
| 7 |
+
# Firebase Service Account JSON dosyasının yolu
|
| 8 |
+
FIREBASE_SERVICE_ACCOUNT=./firebase-service-account.json
|
| 9 |
+
|
| 10 |
+
# Firebase Storage bucket name
|
| 11 |
+
# Format: your-project.appspot.com
|
| 12 |
+
FIREBASE_STORAGE_BUCKET=jira-duplicate-detection.appspot.com
|
| 13 |
+
|
| 14 |
+
# Flask Configuration
|
| 15 |
+
# -------------------
|
| 16 |
+
FLASK_ENV=production
|
| 17 |
+
FLASK_DEBUG=False
|
| 18 |
+
FLASK_PORT=5001
|
| 19 |
+
|
| 20 |
+
# Railway Configuration
|
| 21 |
+
# ---------------------
|
| 22 |
+
# Railway otomatik olarak PORT environment variable set eder
|
| 23 |
+
# PORT=${PORT}
|
| 24 |
+
|
| 25 |
+
# CORS Configuration
|
| 26 |
+
# ------------------
|
| 27 |
+
# Frontend domain (production)
|
| 28 |
+
ALLOWED_ORIGINS=https://jira-duplicate-detection.web.app,https://jira-duplicate-detection.firebaseapp.com
|
| 29 |
+
|
| 30 |
+
# Development mode
|
| 31 |
+
# ----------------
|
| 32 |
+
# Development'ta Firebase yerine local storage kullanmak için:
|
| 33 |
+
# USE_FIREBASE_CACHE=False
|
| 34 |
+
USE_FIREBASE_CACHE=True
|
| 35 |
+
|
| 36 |
+
# Logging
|
| 37 |
+
# -------
|
| 38 |
+
LOG_LEVEL=INFO
|
| 39 |
+
|
firebase.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"hosting": {
|
| 3 |
+
"public": "web",
|
| 4 |
+
"ignore": [
|
| 5 |
+
"firebase.json",
|
| 6 |
+
"**/.*",
|
| 7 |
+
"**/node_modules/**"
|
| 8 |
+
],
|
| 9 |
+
"rewrites": [
|
| 10 |
+
{
|
| 11 |
+
"source": "/",
|
| 12 |
+
"destination": "/login.html"
|
| 13 |
+
}
|
| 14 |
+
],
|
| 15 |
+
"redirects": [
|
| 16 |
+
{
|
| 17 |
+
"source": "/signup",
|
| 18 |
+
"destination": "/register.html",
|
| 19 |
+
"type": 301
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"source": "/login",
|
| 23 |
+
"destination": "/login.html",
|
| 24 |
+
"type": 301
|
| 25 |
+
}
|
| 26 |
+
],
|
| 27 |
+
"headers": [
|
| 28 |
+
{
|
| 29 |
+
"source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css)",
|
| 30 |
+
"headers": [
|
| 31 |
+
{
|
| 32 |
+
"key": "Cache-Control",
|
| 33 |
+
"value": "max-age=31536000"
|
| 34 |
+
}
|
| 35 |
+
]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"source": "**/*.@(html)",
|
| 39 |
+
"headers": [
|
| 40 |
+
{
|
| 41 |
+
"key": "Cache-Control",
|
| 42 |
+
"value": "no-cache"
|
| 43 |
+
}
|
| 44 |
+
]
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
firebase_setup_guide.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔥 Firebase Hosting Setup Rehberi
|
| 2 |
+
|
| 3 |
+
## 1️⃣ Firebase Login
|
| 4 |
+
|
| 5 |
+
Terminal'de şu komutu çalıştır:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
firebase login
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
Tarayıcı açılacak:
|
| 12 |
+
- **[email protected]** ile giriş yap
|
| 13 |
+
- "Allow Firebase CLI" tıkla
|
| 14 |
+
- Terminal'e dön, başarılı mesajı göreceksin
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## 2️⃣ Firebase Projesi Oluştur
|
| 19 |
+
|
| 20 |
+
### Seçenek A: Console'dan (Önerilen)
|
| 21 |
+
|
| 22 |
+
1. **https://console.firebase.google.com** aç
|
| 23 |
+
2. **[email protected]** ile giriş yap
|
| 24 |
+
3. **"Add project"** tıkla
|
| 25 |
+
4. Proje adı: **jira-duplicate-detection**
|
| 26 |
+
5. Google Analytics: **Disable** (veya enable, tercihinize kalmış)
|
| 27 |
+
6. **"Create project"** tıkla
|
| 28 |
+
7. Proje ID'yi not al (örn: `jira-duplicate-detection-xxxxx`)
|
| 29 |
+
|
| 30 |
+
### Seçenek B: CLI'dan
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
firebase projects:create jira-duplicate-detection
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## 3️⃣ Firebase Init (Bu Klasörde)
|
| 39 |
+
|
| 40 |
+
Terminal'de:
|
| 41 |
+
|
| 42 |
+
```bash
|
| 43 |
+
cd /Users/cemirhans/Downloads/JIRA_DUPLICATE_DETECTION-main-2/bug-deduplication-github
|
| 44 |
+
firebase init
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Sorulacak Sorular ve Cevaplar:
|
| 48 |
+
|
| 49 |
+
**1. Which Firebase features?**
|
| 50 |
+
```
|
| 51 |
+
❯ ◯ Hosting: Configure files for Firebase Hosting
|
| 52 |
+
```
|
| 53 |
+
→ Space ile seç, Enter
|
| 54 |
+
|
| 55 |
+
**2. Project Setup**
|
| 56 |
+
```
|
| 57 |
+
❯ Use an existing project
|
| 58 |
+
```
|
| 59 |
+
→ Enter
|
| 60 |
+
|
| 61 |
+
**3. Select a default Firebase project**
|
| 62 |
+
```
|
| 63 |
+
❯ jira-duplicate-detection (jira-duplicate-detection-xxxxx)
|
| 64 |
+
```
|
| 65 |
+
→ Enter
|
| 66 |
+
|
| 67 |
+
**4. What do you want to use as your public directory?**
|
| 68 |
+
```
|
| 69 |
+
? web
|
| 70 |
+
```
|
| 71 |
+
→ `web` yaz, Enter
|
| 72 |
+
|
| 73 |
+
**5. Configure as a single-page app?**
|
| 74 |
+
```
|
| 75 |
+
? Yes
|
| 76 |
+
```
|
| 77 |
+
→ `y` Enter
|
| 78 |
+
|
| 79 |
+
**6. Set up automatic builds and deploys with GitHub?**
|
| 80 |
+
```
|
| 81 |
+
? No
|
| 82 |
+
```
|
| 83 |
+
→ `N` Enter
|
| 84 |
+
|
| 85 |
+
**7. File web/index.html already exists. Overwrite?**
|
| 86 |
+
```
|
| 87 |
+
? No
|
| 88 |
+
```
|
| 89 |
+
→ `N` Enter (önemli! mevcut dosyayı koruyoruz)
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 4️⃣ Firebase Deploy
|
| 94 |
+
|
| 95 |
+
```bash
|
| 96 |
+
firebase deploy
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
Çıktı:
|
| 100 |
+
```
|
| 101 |
+
✔ Deploy complete!
|
| 102 |
+
|
| 103 |
+
Project Console: https://console.firebase.google.com/project/jira-duplicate-detection-xxxxx/overview
|
| 104 |
+
Hosting URL: https://jira-duplicate-detection-xxxxx.web.app
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## 5️⃣ Test Et
|
| 110 |
+
|
| 111 |
+
Tarayıcıda aç:
|
| 112 |
+
- **https://jira-duplicate-detection-xxxxx.web.app**
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
## ⚠️ Önemli Notlar
|
| 117 |
+
|
| 118 |
+
1. **Backend Bağlantısı**:
|
| 119 |
+
- Şu anda frontend yayında ama backend lokal
|
| 120 |
+
- Backend'i de deploy etmek için Railway kullanacağız (sonraki adım)
|
| 121 |
+
|
| 122 |
+
2. **API URL Güncellemesi**:
|
| 123 |
+
- Backend deploy olduktan sonra:
|
| 124 |
+
- `web/app.js` ve `web/create_report.js` içinde
|
| 125 |
+
- `API_BASE_URL` değiştir
|
| 126 |
+
- `firebase deploy` tekrar çalıştır
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
## 🎯 Özet Komutlar
|
| 131 |
+
|
| 132 |
+
```bash
|
| 133 |
+
# 1. Login
|
| 134 |
+
firebase login
|
| 135 |
+
|
| 136 |
+
# 2. Init
|
| 137 |
+
cd /Users/cemirhans/Downloads/JIRA_DUPLICATE_DETECTION-main-2/bug-deduplication-github
|
| 138 |
+
firebase init
|
| 139 |
+
|
| 140 |
+
# 3. Deploy
|
| 141 |
+
firebase deploy
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
---
|
| 145 |
+
|
| 146 |
+
## 📋 Sonraki Adım: Backend Deploy
|
| 147 |
+
|
| 148 |
+
Backend için Railway kullanacağız:
|
| 149 |
+
1. https://railway.app → GitHub ile giriş
|
| 150 |
+
2. "New Project" → "Deploy from GitHub repo"
|
| 151 |
+
3. esraacevik/JIRA_DUPLICATE_DETECTION seç
|
| 152 |
+
4. Deploy
|
| 153 |
+
|
| 154 |
+
---
|
| 155 |
+
|
| 156 |
+
## 🆘 Sorun Çözme
|
| 157 |
+
|
| 158 |
+
**"Firebase command not found"**
|
| 159 |
+
```bash
|
| 160 |
+
npm install -g firebase-tools
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
**"Login failed"**
|
| 164 |
+
```bash
|
| 165 |
+
firebase logout
|
| 166 |
+
firebase login --reauth
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
**"Project not found"**
|
| 170 |
+
- Console'dan proje oluştur
|
| 171 |
+
- Proje ID'yi doğru yaz
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
## ✅ Başarı Kontrol
|
| 176 |
+
|
| 177 |
+
Şunları görmelisin:
|
| 178 |
+
```
|
| 179 |
+
✔ Firebase initialization complete!
|
| 180 |
+
✔ Deploy complete!
|
| 181 |
+
|
| 182 |
+
Hosting URL: https://jira-duplicate-detection-xxxxx.web.app
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
---
|
| 186 |
+
|
| 187 |
+
🎉 **İyi Şanslar!**
|
| 188 |
+
|
railway.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://railway.app/railway.schema.json",
|
| 3 |
+
"build": {
|
| 4 |
+
"builder": "NIXPACKS",
|
| 5 |
+
"buildCommand": "pip install -r requirements.txt"
|
| 6 |
+
},
|
| 7 |
+
"deploy": {
|
| 8 |
+
"startCommand": "python api_server.py",
|
| 9 |
+
"restartPolicyType": "ON_FAILURE",
|
| 10 |
+
"restartPolicyMaxRetries": 10,
|
| 11 |
+
"volumeMounts": [
|
| 12 |
+
{
|
| 13 |
+
"mountPath": "/app/data/user_embeddings",
|
| 14 |
+
"volumeName": "user-embeddings-storage"
|
| 15 |
+
}
|
| 16 |
+
]
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
requirements.txt
CHANGED
|
@@ -15,3 +15,5 @@ numpy>=1.24.0
|
|
| 15 |
tqdm>=4.65.0
|
| 16 |
flask>=2.3.0
|
| 17 |
flask-cors>=4.0.0
|
|
|
|
|
|
|
|
|
| 15 |
tqdm>=4.65.0
|
| 16 |
flask>=2.3.0
|
| 17 |
flask-cors>=4.0.0
|
| 18 |
+
firebase-admin>=6.0.0
|
| 19 |
+
python-dotenv>=1.0.0
|
src/firebase_storage_manager.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Firebase Storage Manager for Embedding Artifacts
|
| 3 |
+
Handles upload/download of user embeddings to Firebase Storage
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import logging
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Optional, Dict, Any
|
| 11 |
+
|
| 12 |
+
logger = logging.getLogger(__name__)
|
| 13 |
+
|
| 14 |
+
class FirebaseStorageManager:
|
| 15 |
+
"""Manage embedding artifacts in Firebase Storage"""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
"""Initialize Firebase Admin SDK"""
|
| 19 |
+
self.initialized = False
|
| 20 |
+
self.bucket = None
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
import firebase_admin
|
| 24 |
+
from firebase_admin import credentials, storage
|
| 25 |
+
|
| 26 |
+
# Check if already initialized
|
| 27 |
+
if not firebase_admin._apps:
|
| 28 |
+
# Try to get credentials from environment or service account file
|
| 29 |
+
cred_path = os.getenv('FIREBASE_SERVICE_ACCOUNT')
|
| 30 |
+
|
| 31 |
+
if cred_path and os.path.exists(cred_path):
|
| 32 |
+
logger.info(f"🔥 Initializing Firebase with service account: {cred_path}")
|
| 33 |
+
cred = credentials.Certificate(cred_path)
|
| 34 |
+
else:
|
| 35 |
+
logger.info("🔥 Initializing Firebase with default credentials")
|
| 36 |
+
cred = credentials.ApplicationDefault()
|
| 37 |
+
|
| 38 |
+
# Get storage bucket from environment
|
| 39 |
+
bucket_name = os.getenv('FIREBASE_STORAGE_BUCKET')
|
| 40 |
+
if not bucket_name:
|
| 41 |
+
logger.warning("⚠️ FIREBASE_STORAGE_BUCKET not set, using default")
|
| 42 |
+
bucket_name = None
|
| 43 |
+
|
| 44 |
+
firebase_admin.initialize_app(cred, {
|
| 45 |
+
'storageBucket': bucket_name
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
# Get storage bucket
|
| 49 |
+
self.bucket = storage.bucket()
|
| 50 |
+
self.initialized = True
|
| 51 |
+
logger.info(f"✅ Firebase Storage initialized: {self.bucket.name}")
|
| 52 |
+
|
| 53 |
+
except Exception as e:
|
| 54 |
+
logger.warning(f"⚠️ Firebase Storage not available: {e}")
|
| 55 |
+
logger.warning("📝 Running in local-only mode")
|
| 56 |
+
self.initialized = False
|
| 57 |
+
|
| 58 |
+
def get_user_artifacts_path(self, user_id: str) -> str:
|
| 59 |
+
"""Get the Firebase Storage path for user artifacts"""
|
| 60 |
+
return f"user_embeddings/{user_id}"
|
| 61 |
+
|
| 62 |
+
def upload_user_artifacts(self, user_id: str, local_dir: Path) -> bool:
|
| 63 |
+
"""
|
| 64 |
+
Upload user embedding artifacts to Firebase Storage
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
user_id: User ID
|
| 68 |
+
local_dir: Local directory containing artifacts
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
True if successful, False otherwise
|
| 72 |
+
"""
|
| 73 |
+
if not self.initialized:
|
| 74 |
+
logger.warning("⚠️ Firebase Storage not initialized, skipping upload")
|
| 75 |
+
return False
|
| 76 |
+
|
| 77 |
+
try:
|
| 78 |
+
logger.info(f"📤 Uploading artifacts for user {user_id}...")
|
| 79 |
+
|
| 80 |
+
# Files to upload
|
| 81 |
+
files_to_upload = [
|
| 82 |
+
'embeddings.npy',
|
| 83 |
+
'faiss_index.bin',
|
| 84 |
+
'metadata.json',
|
| 85 |
+
'data.csv'
|
| 86 |
+
]
|
| 87 |
+
|
| 88 |
+
remote_path = self.get_user_artifacts_path(user_id)
|
| 89 |
+
uploaded_count = 0
|
| 90 |
+
|
| 91 |
+
for filename in files_to_upload:
|
| 92 |
+
local_file = local_dir / filename
|
| 93 |
+
|
| 94 |
+
if not local_file.exists():
|
| 95 |
+
logger.warning(f"⚠️ File not found: {local_file}")
|
| 96 |
+
continue
|
| 97 |
+
|
| 98 |
+
# Upload to Firebase Storage
|
| 99 |
+
blob = self.bucket.blob(f"{remote_path}/{filename}")
|
| 100 |
+
blob.upload_from_filename(str(local_file))
|
| 101 |
+
|
| 102 |
+
logger.info(f"✅ Uploaded: {filename} ({local_file.stat().st_size} bytes)")
|
| 103 |
+
uploaded_count += 1
|
| 104 |
+
|
| 105 |
+
logger.info(f"✅ Uploaded {uploaded_count}/{len(files_to_upload)} files for user {user_id}")
|
| 106 |
+
return uploaded_count > 0
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
logger.error(f"❌ Error uploading artifacts: {e}")
|
| 110 |
+
return False
|
| 111 |
+
|
| 112 |
+
def download_user_artifacts(self, user_id: str, local_dir: Path) -> bool:
|
| 113 |
+
"""
|
| 114 |
+
Download user embedding artifacts from Firebase Storage
|
| 115 |
+
|
| 116 |
+
Args:
|
| 117 |
+
user_id: User ID
|
| 118 |
+
local_dir: Local directory to save artifacts
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
True if successful, False otherwise
|
| 122 |
+
"""
|
| 123 |
+
if not self.initialized:
|
| 124 |
+
logger.warning("⚠️ Firebase Storage not initialized, skipping download")
|
| 125 |
+
return False
|
| 126 |
+
|
| 127 |
+
try:
|
| 128 |
+
logger.info(f"📥 Downloading artifacts for user {user_id}...")
|
| 129 |
+
|
| 130 |
+
# Create local directory
|
| 131 |
+
local_dir.mkdir(parents=True, exist_ok=True)
|
| 132 |
+
|
| 133 |
+
# Files to download
|
| 134 |
+
files_to_download = [
|
| 135 |
+
'embeddings.npy',
|
| 136 |
+
'faiss_index.bin',
|
| 137 |
+
'metadata.json',
|
| 138 |
+
'data.csv'
|
| 139 |
+
]
|
| 140 |
+
|
| 141 |
+
remote_path = self.get_user_artifacts_path(user_id)
|
| 142 |
+
downloaded_count = 0
|
| 143 |
+
|
| 144 |
+
for filename in files_to_download:
|
| 145 |
+
local_file = local_dir / filename
|
| 146 |
+
blob = self.bucket.blob(f"{remote_path}/{filename}")
|
| 147 |
+
|
| 148 |
+
# Check if file exists
|
| 149 |
+
if not blob.exists():
|
| 150 |
+
logger.warning(f"⚠️ Remote file not found: {remote_path}/{filename}")
|
| 151 |
+
continue
|
| 152 |
+
|
| 153 |
+
# Download from Firebase Storage
|
| 154 |
+
blob.download_to_filename(str(local_file))
|
| 155 |
+
|
| 156 |
+
logger.info(f"✅ Downloaded: {filename} ({local_file.stat().st_size} bytes)")
|
| 157 |
+
downloaded_count += 1
|
| 158 |
+
|
| 159 |
+
if downloaded_count == len(files_to_download):
|
| 160 |
+
logger.info(f"✅ Downloaded all {downloaded_count} files for user {user_id}")
|
| 161 |
+
return True
|
| 162 |
+
elif downloaded_count > 0:
|
| 163 |
+
logger.warning(f"⚠️ Partial download: {downloaded_count}/{len(files_to_download)} files")
|
| 164 |
+
return False
|
| 165 |
+
else:
|
| 166 |
+
logger.warning(f"⚠️ No artifacts found for user {user_id}")
|
| 167 |
+
return False
|
| 168 |
+
|
| 169 |
+
except Exception as e:
|
| 170 |
+
logger.error(f"❌ Error downloading artifacts: {e}")
|
| 171 |
+
return False
|
| 172 |
+
|
| 173 |
+
def check_artifacts_exist(self, user_id: str) -> bool:
|
| 174 |
+
"""
|
| 175 |
+
Check if embedding artifacts exist in Firebase Storage
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
user_id: User ID
|
| 179 |
+
|
| 180 |
+
Returns:
|
| 181 |
+
True if artifacts exist, False otherwise
|
| 182 |
+
"""
|
| 183 |
+
if not self.initialized:
|
| 184 |
+
return False
|
| 185 |
+
|
| 186 |
+
try:
|
| 187 |
+
remote_path = self.get_user_artifacts_path(user_id)
|
| 188 |
+
|
| 189 |
+
# Check for essential files
|
| 190 |
+
essential_files = ['embeddings.npy', 'faiss_index.bin', 'metadata.json']
|
| 191 |
+
|
| 192 |
+
for filename in essential_files:
|
| 193 |
+
blob = self.bucket.blob(f"{remote_path}/{filename}")
|
| 194 |
+
if not blob.exists():
|
| 195 |
+
return False
|
| 196 |
+
|
| 197 |
+
logger.info(f"✅ Artifacts exist for user {user_id}")
|
| 198 |
+
return True
|
| 199 |
+
|
| 200 |
+
except Exception as e:
|
| 201 |
+
logger.error(f"❌ Error checking artifacts: {e}")
|
| 202 |
+
return False
|
| 203 |
+
|
| 204 |
+
def get_artifact_metadata(self, user_id: str) -> Optional[Dict[str, Any]]:
|
| 205 |
+
"""
|
| 206 |
+
Get metadata for user's artifacts
|
| 207 |
+
|
| 208 |
+
Args:
|
| 209 |
+
user_id: User ID
|
| 210 |
+
|
| 211 |
+
Returns:
|
| 212 |
+
Metadata dict or None
|
| 213 |
+
"""
|
| 214 |
+
if not self.initialized:
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
remote_path = self.get_user_artifacts_path(user_id)
|
| 219 |
+
blob = self.bucket.blob(f"{remote_path}/metadata.json")
|
| 220 |
+
|
| 221 |
+
if not blob.exists():
|
| 222 |
+
return None
|
| 223 |
+
|
| 224 |
+
# Download and parse metadata
|
| 225 |
+
metadata_content = blob.download_as_string()
|
| 226 |
+
metadata = json.loads(metadata_content)
|
| 227 |
+
|
| 228 |
+
return metadata
|
| 229 |
+
|
| 230 |
+
except Exception as e:
|
| 231 |
+
logger.error(f"❌ Error getting metadata: {e}")
|
| 232 |
+
return None
|
| 233 |
+
|
| 234 |
+
def delete_user_artifacts(self, user_id: str) -> bool:
|
| 235 |
+
"""
|
| 236 |
+
Delete user's artifacts from Firebase Storage
|
| 237 |
+
|
| 238 |
+
Args:
|
| 239 |
+
user_id: User ID
|
| 240 |
+
|
| 241 |
+
Returns:
|
| 242 |
+
True if successful, False otherwise
|
| 243 |
+
"""
|
| 244 |
+
if not self.initialized:
|
| 245 |
+
logger.warning("⚠️ Firebase Storage not initialized, skipping delete")
|
| 246 |
+
return False
|
| 247 |
+
|
| 248 |
+
try:
|
| 249 |
+
logger.info(f"🗑️ Deleting artifacts for user {user_id}...")
|
| 250 |
+
|
| 251 |
+
remote_path = self.get_user_artifacts_path(user_id)
|
| 252 |
+
|
| 253 |
+
# List all blobs with this prefix
|
| 254 |
+
blobs = self.bucket.list_blobs(prefix=remote_path)
|
| 255 |
+
|
| 256 |
+
deleted_count = 0
|
| 257 |
+
for blob in blobs:
|
| 258 |
+
blob.delete()
|
| 259 |
+
deleted_count += 1
|
| 260 |
+
|
| 261 |
+
logger.info(f"✅ Deleted {deleted_count} files for user {user_id}")
|
| 262 |
+
return True
|
| 263 |
+
|
| 264 |
+
except Exception as e:
|
| 265 |
+
logger.error(f"❌ Error deleting artifacts: {e}")
|
| 266 |
+
return False
|
| 267 |
+
|
| 268 |
+
|
| 269 |
+
# Global instance
|
| 270 |
+
_storage_manager = None
|
| 271 |
+
|
| 272 |
+
def get_storage_manager() -> FirebaseStorageManager:
|
| 273 |
+
"""Get or create Firebase Storage Manager singleton"""
|
| 274 |
+
global _storage_manager
|
| 275 |
+
if _storage_manager is None:
|
| 276 |
+
_storage_manager = FirebaseStorageManager()
|
| 277 |
+
return _storage_manager
|
| 278 |
+
|
src/user_embedding_pipeline.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
User-Specific Embedding Pipeline
|
| 4 |
+
==================================
|
| 5 |
+
Her kullanıcının kendi verisi için embedding oluşturur.
|
| 6 |
+
Kullanıcılar birbirlerinin verilerini göremez.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import numpy as np
|
| 11 |
+
import faiss
|
| 12 |
+
import os
|
| 13 |
+
import json
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
import logging
|
| 17 |
+
|
| 18 |
+
logging.basicConfig(level=logging.INFO)
|
| 19 |
+
logger = logging.getLogger(__name__)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class UserEmbeddingPipeline:
|
| 23 |
+
"""Kullanıcıya özel embedding pipeline"""
|
| 24 |
+
|
| 25 |
+
def __init__(self, user_id, model_name='paraphrase-multilingual-MiniLM-L12-v2', use_firebase_cache=True):
|
| 26 |
+
"""
|
| 27 |
+
Args:
|
| 28 |
+
user_id: Firebase user ID veya unique user identifier
|
| 29 |
+
model_name: Embedding model adı
|
| 30 |
+
use_firebase_cache: Firebase Storage cache kullanılsın mı?
|
| 31 |
+
"""
|
| 32 |
+
self.user_id = user_id
|
| 33 |
+
self.model_name = model_name
|
| 34 |
+
self.model = None
|
| 35 |
+
self.use_firebase_cache = use_firebase_cache
|
| 36 |
+
|
| 37 |
+
# Kullanıcı için özel klasör
|
| 38 |
+
self.user_dir = Path(f"data/user_embeddings/{user_id}")
|
| 39 |
+
self.user_dir.mkdir(parents=True, exist_ok=True)
|
| 40 |
+
|
| 41 |
+
# Firebase Storage Manager
|
| 42 |
+
self.storage_manager = None
|
| 43 |
+
if use_firebase_cache:
|
| 44 |
+
try:
|
| 45 |
+
from firebase_storage_manager import get_storage_manager
|
| 46 |
+
self.storage_manager = get_storage_manager()
|
| 47 |
+
except Exception as e:
|
| 48 |
+
logger.warning(f"⚠️ Firebase Storage not available: {e}")
|
| 49 |
+
|
| 50 |
+
logger.info(f"🔧 User Embedding Pipeline initialized for user: {user_id}")
|
| 51 |
+
|
| 52 |
+
def load_model(self):
|
| 53 |
+
"""Embedding modelini yükle"""
|
| 54 |
+
if self.model is None:
|
| 55 |
+
logger.info(f"📥 Loading embedding model: {self.model_name}")
|
| 56 |
+
self.model = SentenceTransformer(self.model_name)
|
| 57 |
+
logger.info("✅ Model loaded successfully")
|
| 58 |
+
return self.model
|
| 59 |
+
|
| 60 |
+
def create_embeddings(self, df, text_columns=['Summary', 'Description']):
|
| 61 |
+
"""
|
| 62 |
+
DataFrame'den embedding oluştur
|
| 63 |
+
|
| 64 |
+
Args:
|
| 65 |
+
df: Pandas DataFrame (kullanıcının verisi)
|
| 66 |
+
text_columns: Embedding için kullanılacak text sütunları
|
| 67 |
+
|
| 68 |
+
Returns:
|
| 69 |
+
embeddings: numpy array (N x embedding_dim)
|
| 70 |
+
"""
|
| 71 |
+
logger.info(f"🔄 Creating embeddings for {len(df)} records...")
|
| 72 |
+
logger.info(f"📝 Using columns: {text_columns}")
|
| 73 |
+
|
| 74 |
+
# Model'i yükle
|
| 75 |
+
model = self.load_model()
|
| 76 |
+
|
| 77 |
+
# Text'leri birleştir
|
| 78 |
+
texts = []
|
| 79 |
+
for _, row in df.iterrows():
|
| 80 |
+
text_parts = []
|
| 81 |
+
for col in text_columns:
|
| 82 |
+
if col in df.columns and pd.notna(row[col]):
|
| 83 |
+
text_parts.append(str(row[col]))
|
| 84 |
+
|
| 85 |
+
combined_text = '. '.join(text_parts).strip().lower()
|
| 86 |
+
texts.append(combined_text if combined_text else 'empty')
|
| 87 |
+
|
| 88 |
+
# Embeddings oluştur
|
| 89 |
+
logger.info("🤖 Generating embeddings...")
|
| 90 |
+
embeddings = model.encode(
|
| 91 |
+
texts,
|
| 92 |
+
batch_size=32,
|
| 93 |
+
show_progress_bar=True,
|
| 94 |
+
convert_to_numpy=True
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
logger.info(f"✅ Embeddings created: {embeddings.shape}")
|
| 98 |
+
|
| 99 |
+
# Kaydet
|
| 100 |
+
embeddings_path = self.user_dir / "embeddings.npy"
|
| 101 |
+
np.save(embeddings_path, embeddings)
|
| 102 |
+
logger.info(f"💾 Saved embeddings to: {embeddings_path}")
|
| 103 |
+
|
| 104 |
+
return embeddings
|
| 105 |
+
|
| 106 |
+
def create_faiss_index(self, embeddings):
|
| 107 |
+
"""
|
| 108 |
+
FAISS index oluştur
|
| 109 |
+
|
| 110 |
+
Args:
|
| 111 |
+
embeddings: numpy array
|
| 112 |
+
|
| 113 |
+
Returns:
|
| 114 |
+
index: FAISS index
|
| 115 |
+
"""
|
| 116 |
+
logger.info("🔄 Creating FAISS index...")
|
| 117 |
+
|
| 118 |
+
# Normalize embeddings (cosine similarity için)
|
| 119 |
+
normalized_embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
|
| 120 |
+
|
| 121 |
+
# FAISS index oluştur (Inner Product - cosine similarity)
|
| 122 |
+
embedding_dim = embeddings.shape[1]
|
| 123 |
+
index = faiss.IndexFlatIP(embedding_dim)
|
| 124 |
+
|
| 125 |
+
# Embeddings'leri ekle
|
| 126 |
+
index.add(normalized_embeddings.astype('float32'))
|
| 127 |
+
|
| 128 |
+
logger.info(f"✅ FAISS index created: {index.ntotal} vectors")
|
| 129 |
+
|
| 130 |
+
# Kaydet
|
| 131 |
+
index_path = self.user_dir / "faiss_index.index"
|
| 132 |
+
faiss.write_index(index, str(index_path))
|
| 133 |
+
logger.info(f"💾 Saved FAISS index to: {index_path}")
|
| 134 |
+
|
| 135 |
+
return index
|
| 136 |
+
|
| 137 |
+
def save_metadata(self, df, text_columns, config=None):
|
| 138 |
+
"""
|
| 139 |
+
Metadata kaydet
|
| 140 |
+
|
| 141 |
+
Args:
|
| 142 |
+
df: DataFrame
|
| 143 |
+
text_columns: Kullanılan text sütunları
|
| 144 |
+
config: Ek konfigürasyon (opsiyonel)
|
| 145 |
+
"""
|
| 146 |
+
current_time = pd.Timestamp.now().isoformat()
|
| 147 |
+
metadata = {
|
| 148 |
+
'user_id': self.user_id,
|
| 149 |
+
'num_records': len(df),
|
| 150 |
+
'recordCount': len(df), # For compatibility
|
| 151 |
+
'num_columns': len(df.columns),
|
| 152 |
+
'columns': df.columns.tolist(),
|
| 153 |
+
'textColumns': text_columns, # For compatibility
|
| 154 |
+
'text_columns': text_columns,
|
| 155 |
+
'metadataColumns': (config or {}).get('metadataColumns', []),
|
| 156 |
+
'fileName': (config or {}).get('fileName', 'uploaded_data.csv'),
|
| 157 |
+
'model_name': self.model_name,
|
| 158 |
+
'embedding_dim': self.model.get_sentence_embedding_dimension() if self.model else None,
|
| 159 |
+
'created_at': current_time,
|
| 160 |
+
'createdAt': current_time, # For compatibility
|
| 161 |
+
'config': config or {}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
metadata_path = self.user_dir / "metadata.json"
|
| 165 |
+
with open(metadata_path, 'w') as f:
|
| 166 |
+
json.dump(metadata, f, indent=2)
|
| 167 |
+
|
| 168 |
+
logger.info(f"💾 Saved metadata to: {metadata_path}")
|
| 169 |
+
|
| 170 |
+
def process(self, df, text_columns=None, config=None):
|
| 171 |
+
"""
|
| 172 |
+
Tam pipeline: embedding + FAISS index oluştur
|
| 173 |
+
|
| 174 |
+
FIREBASE CACHE SYSTEM:
|
| 175 |
+
1. Firebase Storage'da artifacts varsa indir ve kullan
|
| 176 |
+
2. Yoksa embedding yap ve Firebase'e upload et
|
| 177 |
+
|
| 178 |
+
Args:
|
| 179 |
+
df: Pandas DataFrame
|
| 180 |
+
text_columns: Text sütunları (None ise otomatik tespit)
|
| 181 |
+
config: Ek konfigürasyon
|
| 182 |
+
|
| 183 |
+
Returns:
|
| 184 |
+
success: bool
|
| 185 |
+
"""
|
| 186 |
+
try:
|
| 187 |
+
logger.info(f"\n{'='*60}")
|
| 188 |
+
logger.info(f"🚀 Starting User Embedding Pipeline for: {self.user_id}")
|
| 189 |
+
logger.info(f"{'='*60}\n")
|
| 190 |
+
|
| 191 |
+
# ===== FIREBASE CACHE CHECK =====
|
| 192 |
+
if self.use_firebase_cache and self.storage_manager and self.storage_manager.initialized:
|
| 193 |
+
logger.info("🔍 Checking Firebase Storage for cached embeddings...")
|
| 194 |
+
|
| 195 |
+
# Check if artifacts exist in Firebase
|
| 196 |
+
artifacts_exist = self.storage_manager.check_artifacts_exist(self.user_id)
|
| 197 |
+
|
| 198 |
+
if artifacts_exist:
|
| 199 |
+
logger.info("✅ Found cached embeddings in Firebase Storage!")
|
| 200 |
+
|
| 201 |
+
# Download artifacts from Firebase
|
| 202 |
+
download_success = self.storage_manager.download_user_artifacts(
|
| 203 |
+
self.user_id,
|
| 204 |
+
self.user_dir
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
if download_success:
|
| 208 |
+
logger.info("✅ Successfully downloaded embeddings from Firebase!")
|
| 209 |
+
logger.info("⏩ Skipping embedding generation - using cached version")
|
| 210 |
+
return True
|
| 211 |
+
else:
|
| 212 |
+
logger.warning("⚠️ Download failed, will regenerate embeddings")
|
| 213 |
+
else:
|
| 214 |
+
logger.info("📝 No cached embeddings found, will generate new ones")
|
| 215 |
+
|
| 216 |
+
# ===== GENERATE NEW EMBEDDINGS =====
|
| 217 |
+
# Text sütunlarını tespit et
|
| 218 |
+
if text_columns is None:
|
| 219 |
+
text_columns = self._detect_text_columns(df)
|
| 220 |
+
|
| 221 |
+
logger.info(f"📊 Data shape: {df.shape}")
|
| 222 |
+
logger.info(f"📝 Text columns: {text_columns}")
|
| 223 |
+
|
| 224 |
+
# 1. Embeddings oluştur
|
| 225 |
+
embeddings = self.create_embeddings(df, text_columns)
|
| 226 |
+
|
| 227 |
+
# 2. FAISS index oluştur
|
| 228 |
+
self.create_faiss_index(embeddings)
|
| 229 |
+
|
| 230 |
+
# 3. Metadata kaydet
|
| 231 |
+
self.save_metadata(df, text_columns, config)
|
| 232 |
+
|
| 233 |
+
# 4. Upload to Firebase Storage (if enabled)
|
| 234 |
+
if self.use_firebase_cache and self.storage_manager and self.storage_manager.initialized:
|
| 235 |
+
logger.info("📤 Uploading embeddings to Firebase Storage...")
|
| 236 |
+
|
| 237 |
+
upload_success = self.storage_manager.upload_user_artifacts(
|
| 238 |
+
self.user_id,
|
| 239 |
+
self.user_dir
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
if upload_success:
|
| 243 |
+
logger.info("✅ Embeddings cached to Firebase Storage!")
|
| 244 |
+
else:
|
| 245 |
+
logger.warning("⚠️ Firebase upload failed, but local artifacts are saved")
|
| 246 |
+
|
| 247 |
+
logger.info(f"\n{'='*60}")
|
| 248 |
+
logger.info(f"✅ Pipeline completed successfully!")
|
| 249 |
+
logger.info(f"📁 Output directory: {self.user_dir}")
|
| 250 |
+
logger.info(f"{'='*60}\n")
|
| 251 |
+
|
| 252 |
+
return True
|
| 253 |
+
|
| 254 |
+
except Exception as e:
|
| 255 |
+
logger.error(f"❌ Pipeline error: {e}")
|
| 256 |
+
import traceback
|
| 257 |
+
traceback.print_exc()
|
| 258 |
+
return False
|
| 259 |
+
|
| 260 |
+
def _detect_text_columns(self, df):
|
| 261 |
+
"""Otomatik text sütunlarını tespit et"""
|
| 262 |
+
text_keywords = ['summary', 'description', 'title', 'özet', 'açıklama', 'başlık', 'content']
|
| 263 |
+
text_columns = []
|
| 264 |
+
|
| 265 |
+
for col in df.columns:
|
| 266 |
+
col_lower = col.lower()
|
| 267 |
+
if any(keyword in col_lower for keyword in text_keywords):
|
| 268 |
+
text_columns.append(col)
|
| 269 |
+
|
| 270 |
+
# En az 1 sütun bulunmalı
|
| 271 |
+
if not text_columns:
|
| 272 |
+
# İlk string sütunu kullan
|
| 273 |
+
for col in df.columns:
|
| 274 |
+
if df[col].dtype == 'object':
|
| 275 |
+
text_columns.append(col)
|
| 276 |
+
if len(text_columns) >= 2:
|
| 277 |
+
break
|
| 278 |
+
|
| 279 |
+
return text_columns if text_columns else [df.columns[0]]
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def create_user_embeddings(user_id, df, text_columns=None, config=None):
|
| 283 |
+
"""
|
| 284 |
+
Kullanıcı için embedding oluştur (helper function)
|
| 285 |
+
|
| 286 |
+
Args:
|
| 287 |
+
user_id: User ID
|
| 288 |
+
df: DataFrame
|
| 289 |
+
text_columns: Text sütunları (None ise otomatik)
|
| 290 |
+
config: Konfigürasyon
|
| 291 |
+
|
| 292 |
+
Returns:
|
| 293 |
+
success: bool
|
| 294 |
+
"""
|
| 295 |
+
pipeline = UserEmbeddingPipeline(user_id)
|
| 296 |
+
return pipeline.process(df, text_columns, config)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
if __name__ == "__main__":
|
| 300 |
+
# Test için örnek kullanım
|
| 301 |
+
print("🧪 Testing User Embedding Pipeline...")
|
| 302 |
+
|
| 303 |
+
# Örnek veri
|
| 304 |
+
test_data = pd.DataFrame({
|
| 305 |
+
'Summary': ['Test bug 1', 'Test bug 2', 'Test bug 3'],
|
| 306 |
+
'Description': ['Desc 1', 'Desc 2', 'Desc 3'],
|
| 307 |
+
'Priority': ['High', 'Medium', 'Low']
|
| 308 |
+
})
|
| 309 |
+
|
| 310 |
+
# Test kullanıcısı için pipeline
|
| 311 |
+
success = create_user_embeddings(
|
| 312 |
+
user_id='test_user_123',
|
| 313 |
+
df=test_data,
|
| 314 |
+
text_columns=['Summary', 'Description']
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
if success:
|
| 318 |
+
print("✅ Test successful!")
|
| 319 |
+
else:
|
| 320 |
+
print("❌ Test failed!")
|
| 321 |
+
|
src/user_hybrid_search.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
User-Specific Hybrid Search
|
| 4 |
+
============================
|
| 5 |
+
Kullanıcıya özel embeddings kullanarak hybrid search
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import faiss
|
| 11 |
+
import json
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
from sentence_transformers import SentenceTransformer, CrossEncoder
|
| 14 |
+
import logging
|
| 15 |
+
|
| 16 |
+
logging.basicConfig(level=logging.INFO)
|
| 17 |
+
logger = logging.getLogger(__name__)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class UserHybridSearch:
|
| 21 |
+
"""Kullanıcıya özel hybrid search sistemi"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, user_id):
|
| 24 |
+
"""
|
| 25 |
+
Args:
|
| 26 |
+
user_id: Firebase user ID veya unique user identifier
|
| 27 |
+
"""
|
| 28 |
+
self.user_id = user_id
|
| 29 |
+
self.user_dir = Path(f"data/user_embeddings/{user_id}")
|
| 30 |
+
|
| 31 |
+
self.bi_encoder = None
|
| 32 |
+
self.cross_encoder = None
|
| 33 |
+
self.faiss_index = None
|
| 34 |
+
self.embeddings = None
|
| 35 |
+
self.metadata = None
|
| 36 |
+
self.df = None
|
| 37 |
+
|
| 38 |
+
logger.info(f"🔍 User Hybrid Search initialized for: {user_id}")
|
| 39 |
+
|
| 40 |
+
def load_models(self):
|
| 41 |
+
"""Load models"""
|
| 42 |
+
if self.bi_encoder is None:
|
| 43 |
+
logger.info("📥 Loading bi-encoder...")
|
| 44 |
+
self.bi_encoder = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
|
| 45 |
+
|
| 46 |
+
if self.cross_encoder is None:
|
| 47 |
+
logger.info("📥 Loading cross-encoder...")
|
| 48 |
+
self.cross_encoder = CrossEncoder('cross-encoder/mmarco-mMiniLMv2-L12-H384-v1')
|
| 49 |
+
|
| 50 |
+
def load_user_data(self):
|
| 51 |
+
"""Load user's embeddings, FAISS index, and metadata"""
|
| 52 |
+
try:
|
| 53 |
+
# Load metadata
|
| 54 |
+
metadata_path = self.user_dir / "metadata.json"
|
| 55 |
+
if not metadata_path.exists():
|
| 56 |
+
raise FileNotFoundError(f"Metadata not found for user: {self.user_id}")
|
| 57 |
+
|
| 58 |
+
with open(metadata_path, 'r') as f:
|
| 59 |
+
self.metadata = json.load(f)
|
| 60 |
+
|
| 61 |
+
logger.info(f"✅ Loaded metadata: {self.metadata['num_records']} records")
|
| 62 |
+
|
| 63 |
+
# Load embeddings
|
| 64 |
+
embeddings_path = self.user_dir / "embeddings.npy"
|
| 65 |
+
self.embeddings = np.load(embeddings_path)
|
| 66 |
+
logger.info(f"✅ Loaded embeddings: {self.embeddings.shape}")
|
| 67 |
+
|
| 68 |
+
# Load FAISS index
|
| 69 |
+
index_path = self.user_dir / "faiss_index.index"
|
| 70 |
+
self.faiss_index = faiss.read_index(str(index_path))
|
| 71 |
+
logger.info(f"✅ Loaded FAISS index: {self.faiss_index.ntotal} vectors")
|
| 72 |
+
|
| 73 |
+
return True
|
| 74 |
+
|
| 75 |
+
except Exception as e:
|
| 76 |
+
logger.error(f"❌ Error loading user data: {e}")
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
def search(self, query, df=None, top_k=10, rerank_k=50):
|
| 80 |
+
"""
|
| 81 |
+
Hybrid search: FAISS + Cross-Encoder
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
query: Search query string
|
| 85 |
+
df: DataFrame (kullanıcının verisi)
|
| 86 |
+
top_k: Final sonuç sayısı
|
| 87 |
+
rerank_k: FAISS'ten kaç candidate alınacak
|
| 88 |
+
|
| 89 |
+
Returns:
|
| 90 |
+
results: List of dicts
|
| 91 |
+
"""
|
| 92 |
+
try:
|
| 93 |
+
logger.info(f"\n{'='*60}")
|
| 94 |
+
logger.info(f"🔍 User Hybrid Search: {self.user_id}")
|
| 95 |
+
logger.info(f"📝 Query: {query[:50]}...")
|
| 96 |
+
logger.info(f"{'='*60}\n")
|
| 97 |
+
|
| 98 |
+
# Load models
|
| 99 |
+
self.load_models()
|
| 100 |
+
|
| 101 |
+
# Load user data
|
| 102 |
+
if not self.load_user_data():
|
| 103 |
+
return []
|
| 104 |
+
|
| 105 |
+
# Encode query
|
| 106 |
+
logger.info("🔄 Encoding query...")
|
| 107 |
+
query_embedding = self.bi_encoder.encode([query.lower()], convert_to_numpy=True)
|
| 108 |
+
|
| 109 |
+
# Normalize for cosine similarity
|
| 110 |
+
query_embedding = query_embedding / np.linalg.norm(query_embedding)
|
| 111 |
+
|
| 112 |
+
# FAISS search (Stage 1)
|
| 113 |
+
logger.info(f"🔍 FAISS search (top {rerank_k} candidates)...")
|
| 114 |
+
scores, indices = self.faiss_index.search(query_embedding.astype('float32'), rerank_k)
|
| 115 |
+
|
| 116 |
+
candidates = []
|
| 117 |
+
for score, idx in zip(scores[0], indices[0]):
|
| 118 |
+
if idx < len(df):
|
| 119 |
+
candidates.append({
|
| 120 |
+
'index': int(idx),
|
| 121 |
+
'faiss_score': float(score),
|
| 122 |
+
'row': df.iloc[idx].to_dict()
|
| 123 |
+
})
|
| 124 |
+
|
| 125 |
+
logger.info(f"✅ Found {len(candidates)} candidates from FAISS")
|
| 126 |
+
|
| 127 |
+
# Cross-Encoder re-ranking (Stage 2)
|
| 128 |
+
if len(candidates) > 0:
|
| 129 |
+
logger.info("🔄 Cross-Encoder re-ranking...")
|
| 130 |
+
|
| 131 |
+
# Prepare pairs for cross-encoder
|
| 132 |
+
text_col = self.metadata['text_columns'][0] if self.metadata['text_columns'] else df.columns[0]
|
| 133 |
+
pairs = []
|
| 134 |
+
for candidate in candidates:
|
| 135 |
+
row_text = str(candidate['row'].get(text_col, ''))
|
| 136 |
+
pairs.append([query, row_text])
|
| 137 |
+
|
| 138 |
+
# Get cross-encoder scores
|
| 139 |
+
ce_scores = self.cross_encoder.predict(pairs)
|
| 140 |
+
|
| 141 |
+
# Add scores to candidates
|
| 142 |
+
for i, candidate in enumerate(candidates):
|
| 143 |
+
candidate['cross_encoder_score'] = float(ce_scores[i])
|
| 144 |
+
candidate['final_score'] = float(ce_scores[i]) # Can be combined with FAISS score
|
| 145 |
+
|
| 146 |
+
# Sort by final score
|
| 147 |
+
candidates.sort(key=lambda x: x['final_score'], reverse=True)
|
| 148 |
+
|
| 149 |
+
logger.info(f"✅ Re-ranking completed")
|
| 150 |
+
|
| 151 |
+
# Format results
|
| 152 |
+
results = []
|
| 153 |
+
for candidate in candidates[:top_k]:
|
| 154 |
+
result = {
|
| 155 |
+
'index': candidate['index'],
|
| 156 |
+
'final_score': candidate['final_score'],
|
| 157 |
+
'cross_encoder_score': candidate['cross_encoder_score'],
|
| 158 |
+
'faiss_score': candidate['faiss_score'],
|
| 159 |
+
'version_similarity': 1.0,
|
| 160 |
+
'platform_match': True,
|
| 161 |
+
'language_match': True
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
# Add all row data
|
| 165 |
+
result.update(candidate['row'])
|
| 166 |
+
|
| 167 |
+
results.append(result)
|
| 168 |
+
|
| 169 |
+
logger.info(f"✅ Returning {len(results)} results\n")
|
| 170 |
+
|
| 171 |
+
return results
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
logger.error(f"❌ Search error: {e}")
|
| 175 |
+
import traceback
|
| 176 |
+
traceback.print_exc()
|
| 177 |
+
return []
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def search_user_data(user_id, query, df, top_k=10):
|
| 181 |
+
"""
|
| 182 |
+
Helper function for user hybrid search
|
| 183 |
+
|
| 184 |
+
Args:
|
| 185 |
+
user_id: User ID
|
| 186 |
+
query: Search query
|
| 187 |
+
df: User's DataFrame
|
| 188 |
+
top_k: Number of results
|
| 189 |
+
|
| 190 |
+
Returns:
|
| 191 |
+
results: List of dicts
|
| 192 |
+
"""
|
| 193 |
+
searcher = UserHybridSearch(user_id)
|
| 194 |
+
return searcher.search(query, df, top_k)
|
| 195 |
+
|
web/app.js
CHANGED
|
@@ -65,6 +65,10 @@ async function performSearch(showLoading = true) {
|
|
| 65 |
const systemConfig = JSON.parse(localStorage.getItem('systemConfig') || '{}');
|
| 66 |
const selectedColumns = systemConfig.selectedColumns || ['Summary', 'Description'];
|
| 67 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
// Prepare request data
|
| 69 |
const requestData = {
|
| 70 |
query: summary,
|
|
@@ -72,9 +76,12 @@ async function performSearch(showLoading = true) {
|
|
| 72 |
platform: elements.platformSelect.value || null,
|
| 73 |
version: elements.versionInput.value || null,
|
| 74 |
top_k: 10,
|
| 75 |
-
selected_columns: selectedColumns // Send selected columns to backend
|
|
|
|
| 76 |
};
|
| 77 |
|
|
|
|
|
|
|
| 78 |
try {
|
| 79 |
const startTime = performance.now();
|
| 80 |
|
|
@@ -161,6 +168,30 @@ function createResultCard(result, rank) {
|
|
| 161 |
// Determine match quality
|
| 162 |
const { quality, emoji } = getMatchQuality(result.final_score);
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
// Build HTML
|
| 165 |
card.innerHTML = `
|
| 166 |
<div class="result-header">
|
|
@@ -173,15 +204,32 @@ function createResultCard(result, rank) {
|
|
| 173 |
<span class="score-badge">Score: ${result.final_score.toFixed(4)}</span>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
-
<div class="result-summary">${escapeHtml(
|
| 177 |
|
| 178 |
-
<div class="result-description"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
<div class="result-meta">
|
| 181 |
-
${
|
| 182 |
-
${result.platform ? `<span class="meta-tag"> <strong>${result.platform}</strong></span>` : ''}
|
| 183 |
-
${result.app_version && result.app_version !== 'N/A' ? `<span class="meta-tag"> <strong>${result.app_version}</strong></span>` : ''}
|
| 184 |
-
${result.priority ? `<span class="meta-tag"> ${result.priority}</span>` : ''}
|
| 185 |
</div>
|
| 186 |
|
| 187 |
<div class="result-scores">
|
|
@@ -217,6 +265,28 @@ function createResultCard(result, rank) {
|
|
| 217 |
return card;
|
| 218 |
}
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
// =============================================
|
| 221 |
// Helper Functions
|
| 222 |
// =============================================
|
|
@@ -266,9 +336,9 @@ function displayMockResults() {
|
|
| 266 |
description: 'Kullanc mesaj gndermeye altnda uygulama aniden kapanyor. Bu durum zellikle uzun mesajlarda grlyor...',
|
| 267 |
application: 'BiP',
|
| 268 |
platform: 'android',
|
| 269 |
-
app_version: '3.70.19',
|
| 270 |
-
language: null,
|
| 271 |
priority: 'high',
|
|
|
|
|
|
|
| 272 |
cross_encoder_score: 5.2145,
|
| 273 |
version_similarity: 1.0,
|
| 274 |
platform_similarity: 1.0,
|
|
@@ -281,9 +351,9 @@ function displayMockResults() {
|
|
| 281 |
description: 'Mesaj gnderme ilemi srasnda uygulama donuyor ve kapanyor...',
|
| 282 |
application: 'BiP',
|
| 283 |
platform: 'android',
|
| 284 |
-
app_version: '3.70.18',
|
| 285 |
-
language: null,
|
| 286 |
priority: 'medium',
|
|
|
|
|
|
|
| 287 |
cross_encoder_score: 4.8521,
|
| 288 |
version_similarity: 0.9,
|
| 289 |
platform_similarity: 1.0,
|
|
@@ -296,9 +366,9 @@ function displayMockResults() {
|
|
| 296 |
description: 'BiP uygulamasnda mesaj yazp gnder butonuna bastmda uygulama kapanyor...',
|
| 297 |
application: 'BiP',
|
| 298 |
platform: 'android',
|
| 299 |
-
app_version: '3.69.12',
|
| 300 |
-
language: null,
|
| 301 |
priority: 'high',
|
|
|
|
|
|
|
| 302 |
cross_encoder_score: 4.5123,
|
| 303 |
version_similarity: 0.7,
|
| 304 |
platform_similarity: 1.0,
|
|
@@ -532,8 +602,12 @@ async function loadCategoricalOptions(allColumns) {
|
|
| 532 |
if (!selectEl) continue;
|
| 533 |
|
| 534 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
// Fetch unique values from backend
|
| 536 |
-
const response = await fetch(`${API_BASE_URL}/column_values/${encodeURIComponent(columnName)}`);
|
| 537 |
const data = await response.json();
|
| 538 |
|
| 539 |
if (data.success && data.values) {
|
|
|
|
| 65 |
const systemConfig = JSON.parse(localStorage.getItem('systemConfig') || '{}');
|
| 66 |
const selectedColumns = systemConfig.selectedColumns || ['Summary', 'Description'];
|
| 67 |
|
| 68 |
+
// Get userId from session
|
| 69 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 70 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 71 |
+
|
| 72 |
// Prepare request data
|
| 73 |
const requestData = {
|
| 74 |
query: summary,
|
|
|
|
| 76 |
platform: elements.platformSelect.value || null,
|
| 77 |
version: elements.versionInput.value || null,
|
| 78 |
top_k: 10,
|
| 79 |
+
selected_columns: selectedColumns, // Send selected columns to backend
|
| 80 |
+
user_id: userId // Add user_id for user-specific search
|
| 81 |
};
|
| 82 |
|
| 83 |
+
console.log('🔍 Searching with user_id:', userId);
|
| 84 |
+
|
| 85 |
try {
|
| 86 |
const startTime = performance.now();
|
| 87 |
|
|
|
|
| 168 |
// Determine match quality
|
| 169 |
const { quality, emoji } = getMatchQuality(result.final_score);
|
| 170 |
|
| 171 |
+
// Build metadata tags - show ALL properties except internal ones
|
| 172 |
+
const excludedKeys = ['final_score', 'cross_encoder_score', 'version_similarity',
|
| 173 |
+
'platform_similarity', 'language_similarity', 'index',
|
| 174 |
+
'summary', 'Summary', 'description', 'Description', 'desc'];
|
| 175 |
+
|
| 176 |
+
let metaTagsHTML = '';
|
| 177 |
+
for (const [key, value] of Object.entries(result)) {
|
| 178 |
+
if (!excludedKeys.includes(key) && value !== null && value !== undefined && value !== '' && value !== 'N/A') {
|
| 179 |
+
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 180 |
+
metaTagsHTML += `<span class="meta-tag" title="${displayKey}"><strong>${displayKey}:</strong> ${escapeHtml(String(value))}</span>`;
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
// Prepare description with "Read More" functionality
|
| 185 |
+
// Handle case-insensitive field names (Description vs description)
|
| 186 |
+
const descValue = result.description || result.Description || result.desc || '';
|
| 187 |
+
const summaryValue = result.summary || result.Summary || '';
|
| 188 |
+
|
| 189 |
+
const fullDescription = escapeHtml(descValue);
|
| 190 |
+
const shortDescription = descValue.length > 200
|
| 191 |
+
? escapeHtml(descValue.substring(0, 200)) + '...'
|
| 192 |
+
: fullDescription;
|
| 193 |
+
const needsReadMore = descValue.length > 200;
|
| 194 |
+
|
| 195 |
// Build HTML
|
| 196 |
card.innerHTML = `
|
| 197 |
<div class="result-header">
|
|
|
|
| 204 |
<span class="score-badge">Score: ${result.final_score.toFixed(4)}</span>
|
| 205 |
</div>
|
| 206 |
|
| 207 |
+
<div class="result-summary">${escapeHtml(summaryValue)}</div>
|
| 208 |
|
| 209 |
+
<div class="result-description-container">
|
| 210 |
+
<div class="result-description" data-full="${needsReadMore ? 'false' : 'true'}">
|
| 211 |
+
<span class="description-text">${shortDescription}</span>
|
| 212 |
+
<span class="description-full" style="display: none;">${fullDescription}</span>
|
| 213 |
+
</div>
|
| 214 |
+
${needsReadMore ? `
|
| 215 |
+
<button class="read-more-btn" onclick="toggleDescription(this)" style="
|
| 216 |
+
background: none;
|
| 217 |
+
border: none;
|
| 218 |
+
color: var(--primary-color);
|
| 219 |
+
font-weight: 600;
|
| 220 |
+
cursor: pointer;
|
| 221 |
+
padding: 4px 0;
|
| 222 |
+
margin-top: 4px;
|
| 223 |
+
font-size: 0.9rem;
|
| 224 |
+
text-decoration: underline;
|
| 225 |
+
">
|
| 226 |
+
Devamını Oku
|
| 227 |
+
</button>
|
| 228 |
+
` : ''}
|
| 229 |
+
</div>
|
| 230 |
|
| 231 |
<div class="result-meta">
|
| 232 |
+
${metaTagsHTML}
|
|
|
|
|
|
|
|
|
|
| 233 |
</div>
|
| 234 |
|
| 235 |
<div class="result-scores">
|
|
|
|
| 265 |
return card;
|
| 266 |
}
|
| 267 |
|
| 268 |
+
// Toggle description visibility
|
| 269 |
+
function toggleDescription(button) {
|
| 270 |
+
const container = button.previousElementSibling;
|
| 271 |
+
const shortText = container.querySelector('.description-text');
|
| 272 |
+
const fullText = container.querySelector('.description-full');
|
| 273 |
+
const isFull = container.getAttribute('data-full') === 'true';
|
| 274 |
+
|
| 275 |
+
if (isFull) {
|
| 276 |
+
// Show short version
|
| 277 |
+
shortText.style.display = 'inline';
|
| 278 |
+
fullText.style.display = 'none';
|
| 279 |
+
button.innerHTML = ' Devamını Oku';
|
| 280 |
+
container.setAttribute('data-full', 'false');
|
| 281 |
+
} else {
|
| 282 |
+
// Show full version
|
| 283 |
+
shortText.style.display = 'none';
|
| 284 |
+
fullText.style.display = 'inline';
|
| 285 |
+
button.innerHTML = ' Daha Az Göster';
|
| 286 |
+
container.setAttribute('data-full', 'true');
|
| 287 |
+
}
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
// =============================================
|
| 291 |
// Helper Functions
|
| 292 |
// =============================================
|
|
|
|
| 336 |
description: 'Kullanc mesaj gndermeye altnda uygulama aniden kapanyor. Bu durum zellikle uzun mesajlarda grlyor...',
|
| 337 |
application: 'BiP',
|
| 338 |
platform: 'android',
|
|
|
|
|
|
|
| 339 |
priority: 'high',
|
| 340 |
+
component: 'Android Client',
|
| 341 |
+
severity: 'Critical',
|
| 342 |
cross_encoder_score: 5.2145,
|
| 343 |
version_similarity: 1.0,
|
| 344 |
platform_similarity: 1.0,
|
|
|
|
| 351 |
description: 'Mesaj gnderme ilemi srasnda uygulama donuyor ve kapanyor...',
|
| 352 |
application: 'BiP',
|
| 353 |
platform: 'android',
|
|
|
|
|
|
|
| 354 |
priority: 'medium',
|
| 355 |
+
component: 'Android Client',
|
| 356 |
+
severity: 'High',
|
| 357 |
cross_encoder_score: 4.8521,
|
| 358 |
version_similarity: 0.9,
|
| 359 |
platform_similarity: 1.0,
|
|
|
|
| 366 |
description: 'BiP uygulamasnda mesaj yazp gnder butonuna bastmda uygulama kapanyor...',
|
| 367 |
application: 'BiP',
|
| 368 |
platform: 'android',
|
|
|
|
|
|
|
| 369 |
priority: 'high',
|
| 370 |
+
component: 'Android Client',
|
| 371 |
+
severity: 'Critical',
|
| 372 |
cross_encoder_score: 4.5123,
|
| 373 |
version_similarity: 0.7,
|
| 374 |
platform_similarity: 1.0,
|
|
|
|
| 602 |
if (!selectEl) continue;
|
| 603 |
|
| 604 |
try {
|
| 605 |
+
// Get current user's userId
|
| 606 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 607 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 608 |
+
|
| 609 |
// Fetch unique values from backend
|
| 610 |
+
const response = await fetch(`${API_BASE_URL}/column_values/${encodeURIComponent(columnName)}?user_id=${encodeURIComponent(userId)}`);
|
| 611 |
const data = await response.json();
|
| 612 |
|
| 613 |
if (data.success && data.values) {
|
web/column_mapping.html
CHANGED
|
@@ -543,12 +543,17 @@
|
|
| 543 |
// If custom data is uploaded, send selected columns to backend
|
| 544 |
if (dataSelection === 'new' && uploadedDataInfo) {
|
| 545 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
const response = await fetch('http://localhost:5001/api/update_selected_columns', {
|
| 547 |
method: 'POST',
|
| 548 |
headers: {
|
| 549 |
'Content-Type': 'application/json'
|
| 550 |
},
|
| 551 |
body: JSON.stringify({
|
|
|
|
| 552 |
selectedColumns: selectedColumns,
|
| 553 |
metadataColumns: selectedMetadataColumns
|
| 554 |
})
|
|
|
|
| 543 |
// If custom data is uploaded, send selected columns to backend
|
| 544 |
if (dataSelection === 'new' && uploadedDataInfo) {
|
| 545 |
try {
|
| 546 |
+
// Get current user's userId
|
| 547 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 548 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 549 |
+
|
| 550 |
const response = await fetch('http://localhost:5001/api/update_selected_columns', {
|
| 551 |
method: 'POST',
|
| 552 |
headers: {
|
| 553 |
'Content-Type': 'application/json'
|
| 554 |
},
|
| 555 |
body: JSON.stringify({
|
| 556 |
+
userId: userId,
|
| 557 |
selectedColumns: selectedColumns,
|
| 558 |
metadataColumns: selectedMetadataColumns
|
| 559 |
})
|
web/create_report.html
CHANGED
|
@@ -66,13 +66,10 @@
|
|
| 66 |
<!-- Header -->
|
| 67 |
<header class="header">
|
| 68 |
<div class="logo">
|
| 69 |
-
<
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
<path d="M16 12L20 16L16 20L12 16L16 12Z" fill="#333333"/>
|
| 73 |
-
</svg>
|
| 74 |
<h1>Bug Rapor Sistemi</h1>
|
| 75 |
-
<span style="background: #FFCC00; color: #000; padding: 4px 12px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; margin-left: 12px;">TURKCELL</span>
|
| 76 |
</div>
|
| 77 |
</header>
|
| 78 |
|
|
|
|
| 66 |
<!-- Header -->
|
| 67 |
<header class="header">
|
| 68 |
<div class="logo">
|
| 69 |
+
<img src="https://ffo3gv1cf3ir.merlincdn.net/SiteAssets/Bireysel/Navigasyon/turkcell-logo.png?20251016_03"
|
| 70 |
+
alt="Turkcell Logo"
|
| 71 |
+
style="height: 32px; margin-right: 12px;">
|
|
|
|
|
|
|
| 72 |
<h1>Bug Rapor Sistemi</h1>
|
|
|
|
| 73 |
</div>
|
| 74 |
</header>
|
| 75 |
|
web/create_report.js
CHANGED
|
@@ -92,15 +92,32 @@ async function handleFormSubmit(e) {
|
|
| 92 |
// Check if we're replacing an old report
|
| 93 |
if (reportToReplace) {
|
| 94 |
console.log('✅ reportToReplace is truthy, adding replace params...');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
formData.replace_report = true;
|
| 96 |
-
formData.old_report_summary =
|
| 97 |
-
formData.old_report_id = reportToReplace.reportId;
|
|
|
|
| 98 |
console.log('🔄 Replacing old report:', reportToReplace);
|
| 99 |
console.log('📤 formData with replace:', formData);
|
| 100 |
} else {
|
| 101 |
console.log('❌ reportToReplace is falsy, NOT replacing');
|
| 102 |
}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
// Send to API
|
| 105 |
const response = await fetch(`${API_BASE_URL}/create_report`, {
|
| 106 |
method: 'POST',
|
|
@@ -125,6 +142,7 @@ async function handleFormSubmit(e) {
|
|
| 125 |
if (successMessage) {
|
| 126 |
// Update message based on whether we replaced a report
|
| 127 |
if (reportToReplace) {
|
|
|
|
| 128 |
const successContent = successMessage.querySelector('strong');
|
| 129 |
if (successContent) {
|
| 130 |
successContent.innerHTML = `
|
|
@@ -134,9 +152,31 @@ async function handleFormSubmit(e) {
|
|
| 134 |
}
|
| 135 |
const successPara = successMessage.querySelector('p');
|
| 136 |
if (successPara) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
successPara.innerHTML = `
|
| 138 |
-
|
| 139 |
-
|
| 140 |
`;
|
| 141 |
}
|
| 142 |
}
|
|
@@ -144,10 +184,10 @@ async function handleFormSubmit(e) {
|
|
| 144 |
successMessage.classList.add('show');
|
| 145 |
successMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 146 |
|
| 147 |
-
// Hide success message after
|
| 148 |
setTimeout(() => {
|
| 149 |
successMessage.classList.remove('show');
|
| 150 |
-
},
|
| 151 |
}
|
| 152 |
|
| 153 |
// Clear replace report state
|
|
@@ -157,8 +197,20 @@ async function handleFormSubmit(e) {
|
|
| 157 |
replaceMessage.remove();
|
| 158 |
}
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
// Reset form
|
| 161 |
form.reset();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
} else {
|
| 163 |
throw new Error(data.error || 'Kaydetme baarsz');
|
| 164 |
}
|
|
@@ -190,8 +242,12 @@ async function searchSimilarReports() {
|
|
| 190 |
const component = componentEl.value;
|
| 191 |
const appVersion = appVersionEl ? appVersionEl.value.trim() : '';
|
| 192 |
|
|
|
|
|
|
|
|
|
|
| 193 |
// Need at least summary and component
|
| 194 |
if (!summary || summary.length < 10 || !component) {
|
|
|
|
| 195 |
if (similarReportsSection) {
|
| 196 |
similarReportsSection.style.display = 'none';
|
| 197 |
}
|
|
@@ -235,15 +291,20 @@ async function searchSimilarReports() {
|
|
| 235 |
similarReportsSection.style.display = 'block';
|
| 236 |
|
| 237 |
// Build search request
|
|
|
|
|
|
|
|
|
|
| 238 |
const searchRequest = {
|
| 239 |
query: summary,
|
| 240 |
-
top_k: 5
|
|
|
|
| 241 |
};
|
| 242 |
|
| 243 |
if (application) searchRequest.application = application;
|
| 244 |
if (platform) searchRequest.platform = platform;
|
| 245 |
if (appVersion) searchRequest.version = appVersion;
|
| 246 |
|
|
|
|
| 247 |
// Call API
|
| 248 |
const response = await fetch(`${API_BASE_URL}/search`, {
|
| 249 |
method: 'POST',
|
|
@@ -252,9 +313,11 @@ async function searchSimilarReports() {
|
|
| 252 |
});
|
| 253 |
|
| 254 |
if (!response.ok) {
|
|
|
|
| 255 |
throw new Error(`HTTP ${response.status}`);
|
| 256 |
}
|
| 257 |
|
|
|
|
| 258 |
const data = await response.json();
|
| 259 |
|
| 260 |
// Display results
|
|
@@ -268,11 +331,13 @@ async function searchSimilarReports() {
|
|
| 268 |
}
|
| 269 |
|
| 270 |
} catch (error) {
|
| 271 |
-
|
|
|
|
| 272 |
const similarReportsSection = document.getElementById('similarReportsSection');
|
| 273 |
if (similarReportsSection) {
|
| 274 |
similarReportsSection.style.display = 'none';
|
| 275 |
}
|
|
|
|
| 276 |
}
|
| 277 |
}
|
| 278 |
|
|
@@ -297,7 +362,31 @@ function displaySimilarReports(results) {
|
|
| 297 |
// Copy result card HTML from app.js style
|
| 298 |
similarReportsList.innerHTML = results.map((result, index) => {
|
| 299 |
const matchQuality = getMatchQuality(result.final_score);
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
return `
|
| 303 |
<div class="result-card">
|
|
@@ -307,14 +396,32 @@ function displaySimilarReports(results) {
|
|
| 307 |
<span class="result-score">Score: ${result.final_score.toFixed(4)}</span>
|
| 308 |
</div>
|
| 309 |
|
| 310 |
-
<h3 class="result-title">${escapeHtml(
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
<div class="result-meta">
|
| 314 |
-
|
| 315 |
-
<span class="meta-tag" title="Platform"> ${result.platform || 'unknown'}</span>
|
| 316 |
-
<span class="meta-tag" title="Version"> ${versionDisplay}</span>
|
| 317 |
-
<span class="meta-tag" title="Priority"> ${result.priority || 'none'}</span>
|
| 318 |
</div>
|
| 319 |
|
| 320 |
<div class="result-scores">
|
|
@@ -328,7 +435,7 @@ function displaySimilarReports(results) {
|
|
| 328 |
</div>
|
| 329 |
<div class="score-item">
|
| 330 |
<span class="score-label">Platform:</span>
|
| 331 |
-
<span class="score-value">${result.platform_match ? '' : ''}</span>
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
|
|
@@ -336,7 +443,7 @@ function displaySimilarReports(results) {
|
|
| 336 |
<button
|
| 337 |
class="replace-report-btn btn btn-secondary"
|
| 338 |
data-index="${index}"
|
| 339 |
-
data-summary="${
|
| 340 |
data-report-id="${result.report_id || index}"
|
| 341 |
style="flex: 1; padding: 8px 16px; font-size: 0.85rem; background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%); color: white; border: none;"
|
| 342 |
title="Bu rapor duplicate ise, yeni raporu bu eskisinin yerine kaydet"
|
|
@@ -352,7 +459,7 @@ function displaySimilarReports(results) {
|
|
| 352 |
`;
|
| 353 |
}).join('');
|
| 354 |
|
| 355 |
-
// Attach event listeners to replace buttons
|
| 356 |
setTimeout(() => {
|
| 357 |
const replaceButtons = document.querySelectorAll('.replace-report-btn');
|
| 358 |
replaceButtons.forEach(button => {
|
|
@@ -364,6 +471,33 @@ function displaySimilarReports(results) {
|
|
| 364 |
replaceReport(index, summary, reportId);
|
| 365 |
});
|
| 366 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
}, 100);
|
| 368 |
}
|
| 369 |
|
|
@@ -859,7 +993,11 @@ async function loadCategoricalFieldOptions() {
|
|
| 859 |
if (!columnName) continue;
|
| 860 |
|
| 861 |
try {
|
| 862 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
const data = await response.json();
|
| 864 |
|
| 865 |
if (data.success && data.values) {
|
|
|
|
| 92 |
// Check if we're replacing an old report
|
| 93 |
if (reportToReplace) {
|
| 94 |
console.log('✅ reportToReplace is truthy, adding replace params...');
|
| 95 |
+
|
| 96 |
+
// IMPORTANT: Always use the CURRENT summary from reportToReplace
|
| 97 |
+
// This ensures we're replacing the most up-to-date version
|
| 98 |
+
const oldSummary = reportToReplace.summary || reportToReplace.Summary || '';
|
| 99 |
+
|
| 100 |
+
console.log('🔍 Old report to replace:', oldSummary);
|
| 101 |
+
console.log('🆕 New summary:', formData.summary);
|
| 102 |
+
|
| 103 |
formData.replace_report = true;
|
| 104 |
+
formData.old_report_summary = oldSummary;
|
| 105 |
+
formData.old_report_id = reportToReplace.reportId || reportToReplace.report_id || '';
|
| 106 |
+
|
| 107 |
console.log('🔄 Replacing old report:', reportToReplace);
|
| 108 |
console.log('📤 formData with replace:', formData);
|
| 109 |
} else {
|
| 110 |
console.log('❌ reportToReplace is falsy, NOT replacing');
|
| 111 |
}
|
| 112 |
|
| 113 |
+
// Add userId to formData
|
| 114 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 115 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 116 |
+
formData.userId = userId;
|
| 117 |
+
|
| 118 |
+
console.log('📤 Sending formData to backend:', formData);
|
| 119 |
+
console.log('👤 User ID:', userId);
|
| 120 |
+
|
| 121 |
// Send to API
|
| 122 |
const response = await fetch(`${API_BASE_URL}/create_report`, {
|
| 123 |
method: 'POST',
|
|
|
|
| 142 |
if (successMessage) {
|
| 143 |
// Update message based on whether we replaced a report
|
| 144 |
if (reportToReplace) {
|
| 145 |
+
console.log('✅ Report replacement successful, clearing reportToReplace');
|
| 146 |
const successContent = successMessage.querySelector('strong');
|
| 147 |
if (successContent) {
|
| 148 |
successContent.innerHTML = `
|
|
|
|
| 152 |
}
|
| 153 |
const successPara = successMessage.querySelector('p');
|
| 154 |
if (successPara) {
|
| 155 |
+
const embeddingMsg = data.embeddings_updated
|
| 156 |
+
? '<span style="color: #10B981; font-size: 0.95rem; display: block; margin-top: 8px;">✅ <strong>Embedding güncellendi!</strong> Yeni rapor arama sonuçlarında görünüyor.</span>'
|
| 157 |
+
: '<span style="color: #F59E0B; font-size: 0.95rem; display: block; margin-top: 8px;">⚠️ Embedding güncellenemedi. Lütfen veriyi yeniden yükleyin.</span>';
|
| 158 |
+
|
| 159 |
+
successPara.innerHTML = `
|
| 160 |
+
<strong style="font-size: 1.1rem;">Eski rapor silindi, yeni rapor kaydedildi!</strong><br>
|
| 161 |
+
Rapor ID: <strong id="reportId">${data.report_id || 'N/A'}</strong><br>
|
| 162 |
+
${embeddingMsg}
|
| 163 |
+
`;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
// CRITICAL: Clear reportToReplace after successful replacement
|
| 167 |
+
reportToReplace = null;
|
| 168 |
+
console.log('🧹 reportToReplace cleared');
|
| 169 |
+
} else {
|
| 170 |
+
// Regular save message
|
| 171 |
+
const successPara = successMessage.querySelector('p');
|
| 172 |
+
if (successPara) {
|
| 173 |
+
const embeddingMsg = data.embeddings_updated
|
| 174 |
+
? '<span style="color: #10B981; font-size: 0.95rem; display: block; margin-top: 8px;">✅ <strong>Embedding oluşturuldu!</strong> Rapor arama sonuçlarında görünüyor.</span>'
|
| 175 |
+
: '<span style="color: #F59E0B; font-size: 0.95rem; display: block; margin-top: 8px;">⚠️ Embedding oluşturulamadı. Lütfen veriyi yeniden yükleyin.</span>';
|
| 176 |
+
|
| 177 |
successPara.innerHTML = `
|
| 178 |
+
Rapor ID: <strong id="reportId">${data.report_id || 'N/A'}</strong><br>
|
| 179 |
+
${embeddingMsg}
|
| 180 |
`;
|
| 181 |
}
|
| 182 |
}
|
|
|
|
| 184 |
successMessage.classList.add('show');
|
| 185 |
successMessage.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 186 |
|
| 187 |
+
// Hide success message after 7 seconds (increased for embedding message)
|
| 188 |
setTimeout(() => {
|
| 189 |
successMessage.classList.remove('show');
|
| 190 |
+
}, 7000);
|
| 191 |
}
|
| 192 |
|
| 193 |
// Clear replace report state
|
|
|
|
| 197 |
replaceMessage.remove();
|
| 198 |
}
|
| 199 |
|
| 200 |
+
// Cancel any pending search
|
| 201 |
+
if (searchTimeout) {
|
| 202 |
+
clearTimeout(searchTimeout);
|
| 203 |
+
searchTimeout = null;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
// Reset form
|
| 207 |
form.reset();
|
| 208 |
+
|
| 209 |
+
// Hide similar reports section after reset
|
| 210 |
+
const similarReportsSection = document.getElementById('similarReportsSection');
|
| 211 |
+
if (similarReportsSection) {
|
| 212 |
+
similarReportsSection.style.display = 'none';
|
| 213 |
+
}
|
| 214 |
} else {
|
| 215 |
throw new Error(data.error || 'Kaydetme baarsz');
|
| 216 |
}
|
|
|
|
| 242 |
const component = componentEl.value;
|
| 243 |
const appVersion = appVersionEl ? appVersionEl.value.trim() : '';
|
| 244 |
|
| 245 |
+
// Get similar reports section
|
| 246 |
+
const similarReportsSection = document.getElementById('similarReportsSection');
|
| 247 |
+
|
| 248 |
// Need at least summary and component
|
| 249 |
if (!summary || summary.length < 10 || !component) {
|
| 250 |
+
console.log('⏸️ Search skipped: insufficient data (summary length:', summary.length, ', component:', component, ')');
|
| 251 |
if (similarReportsSection) {
|
| 252 |
similarReportsSection.style.display = 'none';
|
| 253 |
}
|
|
|
|
| 291 |
similarReportsSection.style.display = 'block';
|
| 292 |
|
| 293 |
// Build search request
|
| 294 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 295 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 296 |
+
|
| 297 |
const searchRequest = {
|
| 298 |
query: summary,
|
| 299 |
+
top_k: 5,
|
| 300 |
+
user_id: userId // Add user_id for user-specific search
|
| 301 |
};
|
| 302 |
|
| 303 |
if (application) searchRequest.application = application;
|
| 304 |
if (platform) searchRequest.platform = platform;
|
| 305 |
if (appVersion) searchRequest.version = appVersion;
|
| 306 |
|
| 307 |
+
|
| 308 |
// Call API
|
| 309 |
const response = await fetch(`${API_BASE_URL}/search`, {
|
| 310 |
method: 'POST',
|
|
|
|
| 313 |
});
|
| 314 |
|
| 315 |
if (!response.ok) {
|
| 316 |
+
console.warn('Search response not OK:', response.status, response.statusText);
|
| 317 |
throw new Error(`HTTP ${response.status}`);
|
| 318 |
}
|
| 319 |
|
| 320 |
+
// Parse JSON response
|
| 321 |
const data = await response.json();
|
| 322 |
|
| 323 |
// Display results
|
|
|
|
| 331 |
}
|
| 332 |
|
| 333 |
} catch (error) {
|
| 334 |
+
// Silently handle search errors - they're not critical
|
| 335 |
+
console.warn('⚠️ Similar reports search failed (non-critical):', error.message);
|
| 336 |
const similarReportsSection = document.getElementById('similarReportsSection');
|
| 337 |
if (similarReportsSection) {
|
| 338 |
similarReportsSection.style.display = 'none';
|
| 339 |
}
|
| 340 |
+
// Don't throw or alert - search is optional
|
| 341 |
}
|
| 342 |
}
|
| 343 |
|
|
|
|
| 362 |
// Copy result card HTML from app.js style
|
| 363 |
similarReportsList.innerHTML = results.map((result, index) => {
|
| 364 |
const matchQuality = getMatchQuality(result.final_score);
|
| 365 |
+
|
| 366 |
+
// Build metadata tags - show ALL properties except internal ones
|
| 367 |
+
const excludedKeys = ['final_score', 'cross_encoder_score', 'version_similarity',
|
| 368 |
+
'platform_similarity', 'language_similarity', 'index',
|
| 369 |
+
'summary', 'Summary', 'description', 'Description', 'desc',
|
| 370 |
+
'platform_match', 'report_id'];
|
| 371 |
+
|
| 372 |
+
let metaTagsHTML = '';
|
| 373 |
+
for (const [key, value] of Object.entries(result)) {
|
| 374 |
+
if (!excludedKeys.includes(key) && value !== null && value !== undefined && value !== '' && value !== 'N/A') {
|
| 375 |
+
const displayKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
| 376 |
+
metaTagsHTML += `<span class="meta-tag" title="${displayKey}"><strong>${displayKey}:</strong> ${escapeHtml(String(value))}</span>`;
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
// Prepare description with "Read More" functionality
|
| 381 |
+
// Handle case-insensitive field names (Description vs description)
|
| 382 |
+
const descValue = result.description || result.Description || result.desc || '';
|
| 383 |
+
const summaryValue = result.summary || result.Summary || '';
|
| 384 |
+
|
| 385 |
+
const fullDescription = escapeHtml(descValue);
|
| 386 |
+
const shortDescription = descValue.length > 200
|
| 387 |
+
? escapeHtml(descValue.substring(0, 200)) + '...'
|
| 388 |
+
: fullDescription;
|
| 389 |
+
const needsReadMore = descValue.length > 200;
|
| 390 |
|
| 391 |
return `
|
| 392 |
<div class="result-card">
|
|
|
|
| 396 |
<span class="result-score">Score: ${result.final_score.toFixed(4)}</span>
|
| 397 |
</div>
|
| 398 |
|
| 399 |
+
<h3 class="result-title">${escapeHtml(summaryValue)}</h3>
|
| 400 |
+
|
| 401 |
+
<div class="result-description-container">
|
| 402 |
+
<div class="result-description" data-full="${needsReadMore ? 'false' : 'true'}" data-card-index="${index}">
|
| 403 |
+
<span class="description-text">${shortDescription}</span>
|
| 404 |
+
<span class="description-full" style="display: none;">${fullDescription}</span>
|
| 405 |
+
</div>
|
| 406 |
+
${needsReadMore ? `
|
| 407 |
+
<button class="read-more-btn" data-card-index="${index}" style="
|
| 408 |
+
background: none;
|
| 409 |
+
border: none;
|
| 410 |
+
color: var(--primary-color);
|
| 411 |
+
font-weight: 600;
|
| 412 |
+
cursor: pointer;
|
| 413 |
+
padding: 4px 0;
|
| 414 |
+
margin-top: 4px;
|
| 415 |
+
font-size: 0.9rem;
|
| 416 |
+
text-decoration: underline;
|
| 417 |
+
">
|
| 418 |
+
📖 Devamını Oku
|
| 419 |
+
</button>
|
| 420 |
+
` : ''}
|
| 421 |
+
</div>
|
| 422 |
|
| 423 |
<div class="result-meta">
|
| 424 |
+
${metaTagsHTML}
|
|
|
|
|
|
|
|
|
|
| 425 |
</div>
|
| 426 |
|
| 427 |
<div class="result-scores">
|
|
|
|
| 435 |
</div>
|
| 436 |
<div class="score-item">
|
| 437 |
<span class="score-label">Platform:</span>
|
| 438 |
+
<span class="score-value">${result.platform_match ? '✓' : '✗'}</span>
|
| 439 |
</div>
|
| 440 |
</div>
|
| 441 |
|
|
|
|
| 443 |
<button
|
| 444 |
class="replace-report-btn btn btn-secondary"
|
| 445 |
data-index="${index}"
|
| 446 |
+
data-summary="${escapeHtml(summaryValue)}"
|
| 447 |
data-report-id="${result.report_id || index}"
|
| 448 |
style="flex: 1; padding: 8px 16px; font-size: 0.85rem; background: linear-gradient(135deg, #FF9500 0%, #FF6B00 100%); color: white; border: none;"
|
| 449 |
title="Bu rapor duplicate ise, yeni raporu bu eskisinin yerine kaydet"
|
|
|
|
| 459 |
`;
|
| 460 |
}).join('');
|
| 461 |
|
| 462 |
+
// Attach event listeners to replace buttons and read more buttons
|
| 463 |
setTimeout(() => {
|
| 464 |
const replaceButtons = document.querySelectorAll('.replace-report-btn');
|
| 465 |
replaceButtons.forEach(button => {
|
|
|
|
| 471 |
replaceReport(index, summary, reportId);
|
| 472 |
});
|
| 473 |
});
|
| 474 |
+
|
| 475 |
+
// Attach read more button listeners
|
| 476 |
+
const readMoreButtons = document.querySelectorAll('.read-more-btn');
|
| 477 |
+
readMoreButtons.forEach(button => {
|
| 478 |
+
button.addEventListener('click', function(e) {
|
| 479 |
+
e.preventDefault();
|
| 480 |
+
const cardIndex = this.getAttribute('data-card-index');
|
| 481 |
+
const container = document.querySelector(`.result-description[data-card-index="${cardIndex}"]`);
|
| 482 |
+
const shortText = container.querySelector('.description-text');
|
| 483 |
+
const fullText = container.querySelector('.description-full');
|
| 484 |
+
const isFull = container.getAttribute('data-full') === 'true';
|
| 485 |
+
|
| 486 |
+
if (isFull) {
|
| 487 |
+
// Show short version
|
| 488 |
+
shortText.style.display = 'inline';
|
| 489 |
+
fullText.style.display = 'none';
|
| 490 |
+
this.innerHTML = '📖 Devamını Oku';
|
| 491 |
+
container.setAttribute('data-full', 'false');
|
| 492 |
+
} else {
|
| 493 |
+
// Show full version
|
| 494 |
+
shortText.style.display = 'none';
|
| 495 |
+
fullText.style.display = 'inline';
|
| 496 |
+
this.innerHTML = '📕 Daha Az Göster';
|
| 497 |
+
container.setAttribute('data-full', 'true');
|
| 498 |
+
}
|
| 499 |
+
});
|
| 500 |
+
});
|
| 501 |
}, 100);
|
| 502 |
}
|
| 503 |
|
|
|
|
| 993 |
if (!columnName) continue;
|
| 994 |
|
| 995 |
try {
|
| 996 |
+
// Get current user's userId
|
| 997 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 998 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 999 |
+
|
| 1000 |
+
const response = await fetch(`${API_BASE_URL}/column_values/${encodeURIComponent(columnName)}?user_id=${encodeURIComponent(userId)}`);
|
| 1001 |
const data = await response.json();
|
| 1002 |
|
| 1003 |
if (data.success && data.values) {
|
web/data_selection.html
CHANGED
|
@@ -6,6 +6,12 @@
|
|
| 6 |
<title>Data Selection - Bug Report System</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<style>
|
| 10 |
.selection-container {
|
| 11 |
max-width: 900px;
|
|
@@ -227,12 +233,17 @@
|
|
| 227 |
<div class="selection-container" style="position: relative; z-index: 1;">
|
| 228 |
<!-- Welcome Banner -->
|
| 229 |
<div class="welcome-banner">
|
| 230 |
-
<div>
|
| 231 |
-
<
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
</div>
|
| 234 |
<button onclick="logout()" class="btn btn-secondary" style="background: rgba(0,0,0,0.2); color: white; border: none;">
|
| 235 |
-
Çıkış
|
| 236 |
</button>
|
| 237 |
</div>
|
| 238 |
|
|
@@ -244,14 +255,12 @@
|
|
| 244 |
<div class="data-option" onclick="selectOption('existing')" id="optionExisting">
|
| 245 |
<div class="electric-spark"></div>
|
| 246 |
<div class="option-title">Mevcut Veri Setini Kullan</div>
|
| 247 |
-
<div class="option-description">
|
| 248 |
-
|
| 249 |
</div>
|
| 250 |
-
<ul class="option-features">
|
| 251 |
-
<li>
|
| 252 |
-
<li
|
| 253 |
-
<li>Hızlı başlangıç (anında kullanıma hazır)</li>
|
| 254 |
-
<li>BiP, TV+, Fizy ve diğer Turkcell uygulamaları</li>
|
| 255 |
</ul>
|
| 256 |
</div>
|
| 257 |
|
|
@@ -283,45 +292,142 @@
|
|
| 283 |
</button>
|
| 284 |
</div>
|
| 285 |
|
|
|
|
|
|
|
|
|
|
| 286 |
<script>
|
| 287 |
let selectedOption = null;
|
| 288 |
const API_BASE_URL = 'http://localhost:5001/api';
|
|
|
|
| 289 |
|
| 290 |
-
// Check
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
}
|
| 295 |
|
| 296 |
-
//
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
async function checkForUploadedData() {
|
| 303 |
try {
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
const data = await response.json();
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
const optionDesc = optionExisting.querySelector('.option-description');
|
| 312 |
-
const optionFeatures = optionExisting.querySelector('.option-features');
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
optionTitle.innerHTML = 'Mevcut Veri Setini Kullan <span style="background: #10B981; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; margin-left: 8px;">YÜKLÜ</span>';
|
| 315 |
optionDesc.innerHTML = `Sistemde yüklü olan <strong>${data.fileName}</strong> dosyasını kullan`;
|
| 316 |
optionFeatures.innerHTML = `
|
| 317 |
-
<li
|
| 318 |
-
<li
|
| 319 |
-
<li
|
| 320 |
-
<li
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
`;
|
| 322 |
}
|
| 323 |
} catch (error) {
|
| 324 |
-
console.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
}
|
| 326 |
}
|
| 327 |
|
|
@@ -362,10 +468,26 @@
|
|
| 362 |
}
|
| 363 |
|
| 364 |
// Logout
|
| 365 |
-
function logout() {
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
|
| 371 |
// Create animated stars
|
|
|
|
| 6 |
<title>Data Selection - Bug Report System</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
|
| 10 |
+
<!-- Firebase SDKs -->
|
| 11 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
| 12 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
|
| 13 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
| 14 |
+
|
| 15 |
<style>
|
| 16 |
.selection-container {
|
| 17 |
max-width: 900px;
|
|
|
|
| 233 |
<div class="selection-container" style="position: relative; z-index: 1;">
|
| 234 |
<!-- Welcome Banner -->
|
| 235 |
<div class="welcome-banner">
|
| 236 |
+
<div style="display: flex; align-items: center; gap: 20px;">
|
| 237 |
+
<img src="https://ffo3gv1cf3ir.merlincdn.net/SiteAssets/Bireysel/Navigasyon/turkcell-logo.png?20251016_03"
|
| 238 |
+
alt="Turkcell Logo"
|
| 239 |
+
style="height: 50px; filter: brightness(0);">
|
| 240 |
+
<div>
|
| 241 |
+
<h1 style="margin: 0 0 var(--spacing-xs) 0;">Hoş Geldiniz, <span id="userName"></span>!</h1>
|
| 242 |
+
<p style="margin: 0; opacity: 0.9;">Duplicate detection sistemini kullanmaya başlayalım</p>
|
| 243 |
+
</div>
|
| 244 |
</div>
|
| 245 |
<button onclick="logout()" class="btn btn-secondary" style="background: rgba(0,0,0,0.2); color: white; border: none;">
|
| 246 |
+
🚪 Çıkış
|
| 247 |
</button>
|
| 248 |
</div>
|
| 249 |
|
|
|
|
| 255 |
<div class="data-option" onclick="selectOption('existing')" id="optionExisting">
|
| 256 |
<div class="electric-spark"></div>
|
| 257 |
<div class="option-title">Mevcut Veri Setini Kullan</div>
|
| 258 |
+
<div class="option-description" id="existingDataDesc">
|
| 259 |
+
Henüz veri yüklenmedi. Önce veri yüklemeniz gerekiyor.
|
| 260 |
</div>
|
| 261 |
+
<ul class="option-features" id="existingDataFeatures">
|
| 262 |
+
<li>Veri yüklendiğinde burada görünecek</li>
|
| 263 |
+
<li>⬇️ Aşağıdan "Yeni Veri Seti Yükle" seçeneğini kullanın</li>
|
|
|
|
|
|
|
| 264 |
</ul>
|
| 265 |
</div>
|
| 266 |
|
|
|
|
| 292 |
</button>
|
| 293 |
</div>
|
| 294 |
|
| 295 |
+
<!-- Firebase Config -->
|
| 296 |
+
<script src="firebase-config.js"></script>
|
| 297 |
+
|
| 298 |
<script>
|
| 299 |
let selectedOption = null;
|
| 300 |
const API_BASE_URL = 'http://localhost:5001/api';
|
| 301 |
+
let isAuthChecked = false;
|
| 302 |
|
| 303 |
+
// Check authentication - WAIT for Firebase auth state
|
| 304 |
+
function checkAuth() {
|
| 305 |
+
return new Promise((resolve, reject) => {
|
| 306 |
+
// First check localStorage
|
| 307 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 308 |
+
|
| 309 |
+
// If Firebase is configured, also check Firebase auth
|
| 310 |
+
if (typeof firebase !== 'undefined' && firebase.auth) {
|
| 311 |
+
auth.onAuthStateChanged((user) => {
|
| 312 |
+
if (isAuthChecked) return; // Prevent multiple checks
|
| 313 |
+
isAuthChecked = true;
|
| 314 |
+
|
| 315 |
+
if (user) {
|
| 316 |
+
// User is signed in with Firebase
|
| 317 |
+
console.log('✅ Firebase auth verified:', user.email);
|
| 318 |
+
|
| 319 |
+
// Make sure localStorage session exists
|
| 320 |
+
if (!session) {
|
| 321 |
+
const newSession = {
|
| 322 |
+
uid: user.uid,
|
| 323 |
+
email: user.email,
|
| 324 |
+
username: user.email,
|
| 325 |
+
name: user.displayName || user.email,
|
| 326 |
+
role: 'user',
|
| 327 |
+
loginTime: new Date().toISOString(),
|
| 328 |
+
authProvider: 'firebase'
|
| 329 |
+
};
|
| 330 |
+
localStorage.setItem('userSession', JSON.stringify(newSession));
|
| 331 |
+
resolve(newSession);
|
| 332 |
+
} else {
|
| 333 |
+
resolve(session);
|
| 334 |
+
}
|
| 335 |
+
} else if (session) {
|
| 336 |
+
// No Firebase auth but has localStorage session (demo mode)
|
| 337 |
+
console.log('✅ Demo mode session found');
|
| 338 |
+
resolve(session);
|
| 339 |
+
} else {
|
| 340 |
+
// No auth at all
|
| 341 |
+
console.log('❌ No authentication found');
|
| 342 |
+
reject('Not authenticated');
|
| 343 |
+
}
|
| 344 |
+
});
|
| 345 |
+
} else {
|
| 346 |
+
// Firebase not configured, use localStorage only
|
| 347 |
+
if (session) {
|
| 348 |
+
console.log('✅ Demo mode session found');
|
| 349 |
+
isAuthChecked = true;
|
| 350 |
+
resolve(session);
|
| 351 |
+
} else {
|
| 352 |
+
console.log('❌ No session found');
|
| 353 |
+
isAuthChecked = true;
|
| 354 |
+
reject('Not authenticated');
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
});
|
| 358 |
}
|
| 359 |
|
| 360 |
+
// Initialize page - wait for auth check
|
| 361 |
+
checkAuth()
|
| 362 |
+
.then((session) => {
|
| 363 |
+
console.log('✅ Authentication verified, loading page...');
|
| 364 |
+
// Display user name
|
| 365 |
+
document.getElementById('userName').textContent = session.name || session.username;
|
| 366 |
+
// Check for uploaded data
|
| 367 |
+
checkForUploadedData();
|
| 368 |
+
})
|
| 369 |
+
.catch((error) => {
|
| 370 |
+
console.log('❌ Authentication failed, redirecting to login...');
|
| 371 |
+
window.location.href = 'login.html';
|
| 372 |
+
});
|
| 373 |
|
| 374 |
async function checkForUploadedData() {
|
| 375 |
try {
|
| 376 |
+
// Get current user's userId
|
| 377 |
+
const session = JSON.parse(localStorage.getItem('userSession'));
|
| 378 |
+
const userId = session?.uid || session?.username || 'anonymous';
|
| 379 |
+
|
| 380 |
+
const response = await fetch(`${API_BASE_URL}/data_status?user_id=${encodeURIComponent(userId)}`);
|
| 381 |
const data = await response.json();
|
| 382 |
|
| 383 |
+
const optionExisting = document.getElementById('optionExisting');
|
| 384 |
+
const optionTitle = optionExisting.querySelector('.option-title');
|
| 385 |
+
const optionDesc = document.getElementById('existingDataDesc');
|
| 386 |
+
const optionFeatures = document.getElementById('existingDataFeatures');
|
|
|
|
|
|
|
| 387 |
|
| 388 |
+
if (data.customDataLoaded) {
|
| 389 |
+
// Enable the existing data option
|
| 390 |
+
optionExisting.style.opacity = '1';
|
| 391 |
+
optionExisting.style.cursor = 'pointer';
|
| 392 |
+
optionExisting.onclick = () => selectOption('existing');
|
| 393 |
+
|
| 394 |
+
// Update with user's data
|
| 395 |
optionTitle.innerHTML = 'Mevcut Veri Setini Kullan <span style="background: #10B981; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; margin-left: 8px;">YÜKLÜ</span>';
|
| 396 |
optionDesc.innerHTML = `Sistemde yüklü olan <strong>${data.fileName}</strong> dosyasını kullan`;
|
| 397 |
optionFeatures.innerHTML = `
|
| 398 |
+
<li>✅ ${data.rowCount.toLocaleString('tr-TR')} adet rapor</li>
|
| 399 |
+
<li>📅 Yüklenme: ${new Date(data.uploadedAt).toLocaleString('tr-TR')}</li>
|
| 400 |
+
<li>📊 ${data.columnCount} adet sütun</li>
|
| 401 |
+
<li>⚡ Hızlı başlangıç (anında kullanıma hazır)</li>
|
| 402 |
+
`;
|
| 403 |
+
} else {
|
| 404 |
+
// Disable the existing data option
|
| 405 |
+
optionExisting.style.opacity = '0.5';
|
| 406 |
+
optionExisting.style.cursor = 'not-allowed';
|
| 407 |
+
optionExisting.onclick = (e) => {
|
| 408 |
+
e.stopPropagation();
|
| 409 |
+
alert('⚠️ Henüz veri yüklenmedi. Lütfen önce "Yeni Veri Seti Yükle" seçeneğini kullanın.');
|
| 410 |
+
};
|
| 411 |
+
|
| 412 |
+
// Show "no data" message
|
| 413 |
+
optionTitle.innerHTML = 'Mevcut Veri Setini Kullan <span style="background: #EF4444; color: white; padding: 4px 8px; border-radius: 4px; font-size: 0.8rem; margin-left: 8px;">VERİ YOK</span>';
|
| 414 |
+
optionDesc.innerHTML = '❌ Henüz veri yüklenmedi. Önce veri yüklemeniz gerekiyor.';
|
| 415 |
+
optionFeatures.innerHTML = `
|
| 416 |
+
<li>Veri yüklendiğinde burada görünecek</li>
|
| 417 |
+
<li>⬇️ Aşağıdan "Yeni Veri Seti Yükle" seçeneğini kullanın</li>
|
| 418 |
`;
|
| 419 |
}
|
| 420 |
} catch (error) {
|
| 421 |
+
console.error('Error checking data status:', error);
|
| 422 |
+
|
| 423 |
+
// Disable on error
|
| 424 |
+
const optionExisting = document.getElementById('optionExisting');
|
| 425 |
+
optionExisting.style.opacity = '0.5';
|
| 426 |
+
optionExisting.style.cursor = 'not-allowed';
|
| 427 |
+
optionExisting.onclick = (e) => {
|
| 428 |
+
e.stopPropagation();
|
| 429 |
+
alert('⚠️ Henüz veri yüklenmedi. Lütfen önce "Yeni Veri Seti Yükle" seçeneğini kullanın.');
|
| 430 |
+
};
|
| 431 |
}
|
| 432 |
}
|
| 433 |
|
|
|
|
| 468 |
}
|
| 469 |
|
| 470 |
// Logout
|
| 471 |
+
async function logout() {
|
| 472 |
+
if (confirm('Çıkış yapmak istediğinizden emin misiniz?')) {
|
| 473 |
+
try {
|
| 474 |
+
// Sign out from Firebase if available
|
| 475 |
+
if (typeof firebase !== 'undefined' && firebase.auth) {
|
| 476 |
+
await auth.signOut();
|
| 477 |
+
console.log('✅ Firebase signout successful');
|
| 478 |
+
}
|
| 479 |
+
} catch (error) {
|
| 480 |
+
console.error('Firebase signout error:', error);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
// Clear local storage
|
| 484 |
+
localStorage.removeItem('userSession');
|
| 485 |
+
localStorage.removeItem('dataSelection');
|
| 486 |
+
localStorage.removeItem('systemConfig');
|
| 487 |
+
|
| 488 |
+
// Redirect to login
|
| 489 |
+
window.location.href = 'login.html';
|
| 490 |
+
}
|
| 491 |
}
|
| 492 |
|
| 493 |
// Create animated stars
|
web/data_upload.html
CHANGED
|
@@ -85,6 +85,64 @@
|
|
| 85 |
font-weight: 700;
|
| 86 |
font-size: 0.9rem;
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
</style>
|
| 89 |
</head>
|
| 90 |
<body style="background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%); min-height: 100vh;">
|
|
@@ -174,6 +232,27 @@
|
|
| 174 |
</button>
|
| 175 |
</div>
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
| 178 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
| 179 |
<script>
|
|
@@ -371,17 +450,39 @@
|
|
| 371 |
uploadedData = null;
|
| 372 |
}
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
// Continue to mapping
|
| 375 |
async function continueToMapping() {
|
| 376 |
if (!uploadedData) {
|
| 377 |
-
alert('
|
| 378 |
return;
|
| 379 |
}
|
| 380 |
|
| 381 |
// Disable button
|
| 382 |
const btn = document.getElementById('continueBtn');
|
| 383 |
btn.disabled = true;
|
| 384 |
-
btn.innerHTML = '<span
|
| 385 |
|
| 386 |
try {
|
| 387 |
// Store uploaded data in localStorage
|
|
@@ -399,6 +500,19 @@
|
|
| 399 |
|
| 400 |
localStorage.setItem('uploadedData', JSON.stringify(dataInfo));
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
// Send data to backend
|
| 403 |
const response = await fetch('http://localhost:5001/api/upload_data', {
|
| 404 |
method: 'POST',
|
|
@@ -408,14 +522,35 @@
|
|
| 408 |
body: JSON.stringify(dataInfo)
|
| 409 |
});
|
| 410 |
|
|
|
|
|
|
|
| 411 |
if (!response.ok) {
|
| 412 |
throw new Error(`HTTP ${response.status}`);
|
| 413 |
}
|
| 414 |
|
|
|
|
|
|
|
| 415 |
const result = await response.json();
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
if (result.success) {
|
| 418 |
-
console.log('Data uploaded to backend:', result.info);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
|
| 420 |
// Save as selectedDataset for column_mapping.html to use
|
| 421 |
localStorage.setItem('selectedDataset', JSON.stringify({
|
|
@@ -425,6 +560,9 @@
|
|
| 425 |
loadedAt: new Date().toISOString()
|
| 426 |
}));
|
| 427 |
|
|
|
|
|
|
|
|
|
|
| 428 |
// Redirect to column mapping
|
| 429 |
window.location.href = 'column_mapping.html';
|
| 430 |
} else {
|
|
@@ -433,7 +571,9 @@
|
|
| 433 |
|
| 434 |
} catch (error) {
|
| 435 |
console.error('Backend upload error:', error);
|
| 436 |
-
|
|
|
|
|
|
|
| 437 |
|
| 438 |
// Save as selectedDataset even if backend fails
|
| 439 |
localStorage.setItem('selectedDataset', JSON.stringify({
|
|
@@ -445,6 +585,10 @@
|
|
| 445 |
|
| 446 |
// Still allow to continue even if backend upload fails
|
| 447 |
window.location.href = 'column_mapping.html';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
}
|
| 449 |
}
|
| 450 |
|
|
|
|
| 85 |
font-weight: 700;
|
| 86 |
font-size: 0.9rem;
|
| 87 |
}
|
| 88 |
+
|
| 89 |
+
/* Embedding Progress Modal */
|
| 90 |
+
.embedding-modal {
|
| 91 |
+
display: none;
|
| 92 |
+
position: fixed;
|
| 93 |
+
top: 0;
|
| 94 |
+
left: 0;
|
| 95 |
+
right: 0;
|
| 96 |
+
bottom: 0;
|
| 97 |
+
background: rgba(0, 0, 0, 0.8);
|
| 98 |
+
z-index: 10000;
|
| 99 |
+
justify-content: center;
|
| 100 |
+
align-items: center;
|
| 101 |
+
}
|
| 102 |
+
.embedding-modal.show {
|
| 103 |
+
display: flex;
|
| 104 |
+
}
|
| 105 |
+
.embedding-modal-content {
|
| 106 |
+
background: linear-gradient(135deg, #FFFFFF 0%, #F3F4F6 100%);
|
| 107 |
+
border-radius: var(--radius-xl);
|
| 108 |
+
padding: var(--spacing-3xl);
|
| 109 |
+
max-width: 500px;
|
| 110 |
+
width: 90%;
|
| 111 |
+
text-align: center;
|
| 112 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
| 113 |
+
}
|
| 114 |
+
.embedding-spinner {
|
| 115 |
+
width: 80px;
|
| 116 |
+
height: 80px;
|
| 117 |
+
border: 8px solid #f3f3f3;
|
| 118 |
+
border-top: 8px solid var(--primary-color);
|
| 119 |
+
border-radius: 50%;
|
| 120 |
+
animation: spin 1s linear infinite;
|
| 121 |
+
margin: 0 auto var(--spacing-xl);
|
| 122 |
+
}
|
| 123 |
+
@keyframes spin {
|
| 124 |
+
0% { transform: rotate(0deg); }
|
| 125 |
+
100% { transform: rotate(360deg); }
|
| 126 |
+
}
|
| 127 |
+
.embedding-progress-bar {
|
| 128 |
+
width: 100%;
|
| 129 |
+
height: 40px;
|
| 130 |
+
background: #E5E7EB;
|
| 131 |
+
border-radius: 20px;
|
| 132 |
+
overflow: hidden;
|
| 133 |
+
margin: var(--spacing-xl) 0;
|
| 134 |
+
}
|
| 135 |
+
.embedding-progress-fill {
|
| 136 |
+
height: 100%;
|
| 137 |
+
background: linear-gradient(90deg, #FBBF24 0%, #F59E0B 50%, #D97706 100%);
|
| 138 |
+
transition: width 0.5s ease-out;
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: center;
|
| 141 |
+
justify-content: center;
|
| 142 |
+
color: #fff;
|
| 143 |
+
font-weight: 700;
|
| 144 |
+
font-size: 1rem;
|
| 145 |
+
}
|
| 146 |
</style>
|
| 147 |
</head>
|
| 148 |
<body style="background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%); min-height: 100vh;">
|
|
|
|
| 232 |
</button>
|
| 233 |
</div>
|
| 234 |
|
| 235 |
+
<!-- Embedding Progress Modal -->
|
| 236 |
+
<div id="embeddingModal" class="embedding-modal">
|
| 237 |
+
<div class="embedding-modal-content">
|
| 238 |
+
<div class="embedding-spinner"></div>
|
| 239 |
+
<h2 style="color: var(--secondary-color); margin-bottom: var(--spacing-md);">
|
| 240 |
+
🤖 Embedding Oluşturuluyor
|
| 241 |
+
</h2>
|
| 242 |
+
<p id="embeddingStatus" style="color: #6B7280; margin-bottom: var(--spacing-lg); font-size: 1rem;">
|
| 243 |
+
Verileriniz işleniyor...
|
| 244 |
+
</p>
|
| 245 |
+
<div class="embedding-progress-bar">
|
| 246 |
+
<div id="embeddingProgressFill" class="embedding-progress-fill" style="width: 0%;">
|
| 247 |
+
0%
|
| 248 |
+
</div>
|
| 249 |
+
</div>
|
| 250 |
+
<p style="color: #9CA3AF; font-size: 0.9rem; margin-top: var(--spacing-lg);">
|
| 251 |
+
ℹ️ Bu işlem birkaç dakika sürebilir. Lütfen sayfayı kapatmayın.
|
| 252 |
+
</p>
|
| 253 |
+
</div>
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
| 257 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
| 258 |
<script>
|
|
|
|
| 450 |
uploadedData = null;
|
| 451 |
}
|
| 452 |
|
| 453 |
+
// Show embedding progress modal
|
| 454 |
+
function showEmbeddingModal() {
|
| 455 |
+
document.getElementById('embeddingModal').classList.add('show');
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
// Hide embedding progress modal
|
| 459 |
+
function hideEmbeddingModal() {
|
| 460 |
+
document.getElementById('embeddingModal').classList.remove('show');
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// Update embedding progress
|
| 464 |
+
function updateEmbeddingProgress(percent, message) {
|
| 465 |
+
const fill = document.getElementById('embeddingProgressFill');
|
| 466 |
+
const statusText = document.getElementById('embeddingStatus');
|
| 467 |
+
|
| 468 |
+
fill.style.width = percent + '%';
|
| 469 |
+
fill.textContent = percent + '%';
|
| 470 |
+
if (message) {
|
| 471 |
+
statusText.textContent = message;
|
| 472 |
+
}
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
// Continue to mapping
|
| 476 |
async function continueToMapping() {
|
| 477 |
if (!uploadedData) {
|
| 478 |
+
alert('⚠️ Lütfen önce bir dosya yükleyin!');
|
| 479 |
return;
|
| 480 |
}
|
| 481 |
|
| 482 |
// Disable button
|
| 483 |
const btn = document.getElementById('continueBtn');
|
| 484 |
btn.disabled = true;
|
| 485 |
+
btn.innerHTML = '<span>⏳ Veri backend\'e gönderiliyor...</span>';
|
| 486 |
|
| 487 |
try {
|
| 488 |
// Store uploaded data in localStorage
|
|
|
|
| 500 |
|
| 501 |
localStorage.setItem('uploadedData', JSON.stringify(dataInfo));
|
| 502 |
|
| 503 |
+
// Get user session for userId
|
| 504 |
+
const userSession = JSON.parse(localStorage.getItem('userSession') || '{}');
|
| 505 |
+
const userId = userSession.uid || userSession.username || 'anonymous';
|
| 506 |
+
|
| 507 |
+
// Add userId to dataInfo
|
| 508 |
+
dataInfo.userId = userId;
|
| 509 |
+
|
| 510 |
+
console.log('📤 Uploading data for user:', userId);
|
| 511 |
+
|
| 512 |
+
// Show embedding progress modal
|
| 513 |
+
showEmbeddingModal();
|
| 514 |
+
updateEmbeddingProgress(10, '📤 Veri backend\'e gönderiliyor...');
|
| 515 |
+
|
| 516 |
// Send data to backend
|
| 517 |
const response = await fetch('http://localhost:5001/api/upload_data', {
|
| 518 |
method: 'POST',
|
|
|
|
| 522 |
body: JSON.stringify(dataInfo)
|
| 523 |
});
|
| 524 |
|
| 525 |
+
updateEmbeddingProgress(30, '🔄 Veri işleniyor...');
|
| 526 |
+
|
| 527 |
if (!response.ok) {
|
| 528 |
throw new Error(`HTTP ${response.status}`);
|
| 529 |
}
|
| 530 |
|
| 531 |
+
updateEmbeddingProgress(50, '🤖 Embedding modeli yükleniyor...');
|
| 532 |
+
|
| 533 |
const result = await response.json();
|
| 534 |
|
| 535 |
+
updateEmbeddingProgress(70, '✨ Embeddings oluşturuluyor...');
|
| 536 |
+
|
| 537 |
+
// Simulate embedding creation time for better UX
|
| 538 |
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
| 539 |
+
|
| 540 |
+
updateEmbeddingProgress(90, '💾 Veriler kaydediliyor...');
|
| 541 |
+
|
| 542 |
if (result.success) {
|
| 543 |
+
console.log('✅ Data uploaded to backend:', result.info);
|
| 544 |
+
|
| 545 |
+
// Show embedding creation status
|
| 546 |
+
if (result.info && result.info.embeddingsCreated) {
|
| 547 |
+
console.log('✅ Embeddings created successfully!');
|
| 548 |
+
console.log('📁 Embeddings path:', result.info.embeddingsPath);
|
| 549 |
+
updateEmbeddingProgress(100, '✅ Embedding başarıyla oluşturuldu!');
|
| 550 |
+
} else {
|
| 551 |
+
console.warn('⚠️ Embeddings creation failed or not completed');
|
| 552 |
+
updateEmbeddingProgress(100, '⚠️ Embedding oluşturma tamamlanamadı');
|
| 553 |
+
}
|
| 554 |
|
| 555 |
// Save as selectedDataset for column_mapping.html to use
|
| 556 |
localStorage.setItem('selectedDataset', JSON.stringify({
|
|
|
|
| 560 |
loadedAt: new Date().toISOString()
|
| 561 |
}));
|
| 562 |
|
| 563 |
+
// Wait a bit before redirecting
|
| 564 |
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
| 565 |
+
|
| 566 |
// Redirect to column mapping
|
| 567 |
window.location.href = 'column_mapping.html';
|
| 568 |
} else {
|
|
|
|
| 571 |
|
| 572 |
} catch (error) {
|
| 573 |
console.error('Backend upload error:', error);
|
| 574 |
+
hideEmbeddingModal();
|
| 575 |
+
|
| 576 |
+
alert('⚠️ Veri backend\'e gönderilemedi, ancak lokal olarak kaydedildi.\n\nHata: ' + error.message + '\n\nDevam edebilirsiniz.');
|
| 577 |
|
| 578 |
// Save as selectedDataset even if backend fails
|
| 579 |
localStorage.setItem('selectedDataset', JSON.stringify({
|
|
|
|
| 585 |
|
| 586 |
// Still allow to continue even if backend upload fails
|
| 587 |
window.location.href = 'column_mapping.html';
|
| 588 |
+
} finally {
|
| 589 |
+
// Re-enable button in case of error (but won't matter since we redirect)
|
| 590 |
+
btn.disabled = false;
|
| 591 |
+
btn.innerHTML = '<span>Sütun Eşleştirmesine Geç</span>';
|
| 592 |
}
|
| 593 |
}
|
| 594 |
|
web/firebase-config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Firebase Configuration
|
| 2 |
+
// JIRA Duplicate Detection Project
|
| 3 |
+
const firebaseConfig = {
|
| 4 |
+
apiKey: "AIzaSyCgjcibKokZodcMY_L3NP9Q4vM4kt4tZgs",
|
| 5 |
+
authDomain: "jira-duplicate-detection.firebaseapp.com",
|
| 6 |
+
projectId: "jira-duplicate-detection",
|
| 7 |
+
storageBucket: "jira-duplicate-detection.firebasestorage.app",
|
| 8 |
+
messagingSenderId: "892110955459",
|
| 9 |
+
appId: "1:892110955459:web:5032050c5563ba449f9376",
|
| 10 |
+
measurementId: "G-623M713T4M"
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
// Initialize Firebase
|
| 14 |
+
firebase.initializeApp(firebaseConfig);
|
| 15 |
+
|
| 16 |
+
// Get Firebase services
|
| 17 |
+
const auth = firebase.auth();
|
| 18 |
+
const db = firebase.firestore();
|
| 19 |
+
|
| 20 |
+
// Optional: Initialize Analytics
|
| 21 |
+
try {
|
| 22 |
+
const analytics = firebase.analytics();
|
| 23 |
+
console.log('📊 Firebase Analytics initialized');
|
| 24 |
+
} catch (e) {
|
| 25 |
+
console.log('⚠️ Analytics not available');
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
console.log('🔥 Firebase initialized successfully!');
|
| 29 |
+
console.log('✅ Project: jira-duplicate-detection');
|
| 30 |
+
console.log('✅ Authentication: Ready');
|
| 31 |
+
console.log('✅ Firestore: Ready');
|
| 32 |
+
|
web/index.html
CHANGED
|
@@ -25,13 +25,10 @@
|
|
| 25 |
<!-- Header -->
|
| 26 |
<header class="header">
|
| 27 |
<div class="logo">
|
| 28 |
-
<
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
<path d="M16 12L20 16L16 20L12 16L16 12Z" fill="#333333"/>
|
| 32 |
-
</svg>
|
| 33 |
<h1>Bug Rapor Sistemi</h1>
|
| 34 |
-
<span style="background: #FFCC00; color: #000; padding: 4px 12px; border-radius: 4px; font-size: 0.7rem; font-weight: 700; margin-left: 12px;">TURKCELL</span>
|
| 35 |
</div>
|
| 36 |
<div class="header-stats">
|
| 37 |
<div class="stat">
|
|
|
|
| 25 |
<!-- Header -->
|
| 26 |
<header class="header">
|
| 27 |
<div class="logo">
|
| 28 |
+
<img src="https://ffo3gv1cf3ir.merlincdn.net/SiteAssets/Bireysel/Navigasyon/turkcell-logo.png?20251016_03"
|
| 29 |
+
alt="Turkcell Logo"
|
| 30 |
+
style="height: 32px; margin-right: 12px;">
|
|
|
|
|
|
|
| 31 |
<h1>Bug Rapor Sistemi</h1>
|
|
|
|
| 32 |
</div>
|
| 33 |
<div class="header-stats">
|
| 34 |
<div class="stat">
|
web/login.html
CHANGED
|
@@ -3,9 +3,15 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
<style>
|
| 10 |
/* Animated Background Section */
|
| 11 |
.animated-bg {
|
|
@@ -23,7 +29,6 @@
|
|
| 23 |
background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%);
|
| 24 |
}
|
| 25 |
|
| 26 |
-
/* Animated Gradient Effect */
|
| 27 |
.animated-bg::before {
|
| 28 |
content: '';
|
| 29 |
position: absolute;
|
|
@@ -34,15 +39,10 @@
|
|
| 34 |
}
|
| 35 |
|
| 36 |
@keyframes gradientMove {
|
| 37 |
-
0% {
|
| 38 |
-
|
| 39 |
-
}
|
| 40 |
-
100% {
|
| 41 |
-
transform: translateY(100%);
|
| 42 |
-
}
|
| 43 |
}
|
| 44 |
|
| 45 |
-
/* Animated Blocks */
|
| 46 |
.animated-bg span {
|
| 47 |
position: relative;
|
| 48 |
display: block;
|
|
@@ -101,7 +101,6 @@
|
|
| 101 |
box-shadow: 0 0 20px rgba(255, 204, 0, 0.5);
|
| 102 |
}
|
| 103 |
|
| 104 |
-
/* Form Styling */
|
| 105 |
.form-group {
|
| 106 |
margin-bottom: 25px;
|
| 107 |
}
|
|
@@ -152,6 +151,11 @@
|
|
| 152 |
.btn-primary:active {
|
| 153 |
opacity: 0.8;
|
| 154 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
|
| 156 |
.error-message {
|
| 157 |
background: #FEE2E2;
|
|
@@ -167,7 +171,38 @@
|
|
| 167 |
display: block;
|
| 168 |
}
|
| 169 |
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
@media (max-width: 900px) {
|
| 172 |
.animated-bg span {
|
| 173 |
width: calc(10vw - 2px);
|
|
@@ -190,7 +225,6 @@
|
|
| 190 |
<body>
|
| 191 |
<!-- Animated Background -->
|
| 192 |
<section class="animated-bg">
|
| 193 |
-
<!-- 250 animated spans for grid effect -->
|
| 194 |
<script>
|
| 195 |
for (let i = 0; i < 250; i++) {
|
| 196 |
document.write('<span></span>');
|
|
@@ -201,38 +235,42 @@
|
|
| 201 |
<div class="login-container">
|
| 202 |
<!-- Logo -->
|
| 203 |
<div class="login-header">
|
| 204 |
-
<div class="login-logo"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
<h1>Sistem Girişi</h1>
|
| 206 |
<p>Bug Report Duplicate Detection System</p>
|
| 207 |
</div>
|
| 208 |
|
| 209 |
<!-- Error Message -->
|
| 210 |
<div id="errorMessage" class="error-message">
|
| 211 |
-
<strong
|
| 212 |
<p id="errorText"></p>
|
| 213 |
</div>
|
| 214 |
|
| 215 |
<!-- Login Form -->
|
| 216 |
<form id="loginForm">
|
| 217 |
<div class="form-group">
|
| 218 |
-
<label for="
|
| 219 |
-
<span
|
| 220 |
<span class="required">*</span>
|
| 221 |
</label>
|
| 222 |
<input
|
| 223 |
-
type="
|
| 224 |
-
id="
|
| 225 |
-
name="
|
| 226 |
class="form-input"
|
| 227 |
placeholder="[email protected]"
|
| 228 |
required
|
| 229 |
-
autocomplete="
|
| 230 |
>
|
| 231 |
</div>
|
| 232 |
|
| 233 |
<div class="form-group">
|
| 234 |
<label for="password" class="form-label">
|
| 235 |
-
<span
|
| 236 |
<span class="required">*</span>
|
| 237 |
</label>
|
| 238 |
<input
|
|
@@ -240,81 +278,250 @@
|
|
| 240 |
id="password"
|
| 241 |
name="password"
|
| 242 |
class="form-input"
|
| 243 |
-
placeholder=""
|
| 244 |
required
|
| 245 |
autocomplete="current-password"
|
| 246 |
>
|
|
|
|
|
|
|
|
|
|
| 247 |
</div>
|
| 248 |
|
| 249 |
-
<button type="submit" class="btn btn-primary" id="loginBtn"
|
| 250 |
-
<span
|
| 251 |
</button>
|
| 252 |
</form>
|
| 253 |
|
| 254 |
-
<!--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
<div style="margin-top: var(--spacing-xl); padding: var(--spacing-md); background: rgba(255, 204, 0, 0.1); border: 1px solid #FFCC00; border-radius: var(--radius-md);">
|
| 256 |
<p style="font-size: 0.85rem; color: #FFCC00; margin: 0;">
|
| 257 |
-
<strong
|
| 258 |
-
|
| 259 |
-
<code style="background: #444; padding: 2px 6px; border-radius: 3px;">
|
|
|
|
| 260 |
</p>
|
| 261 |
</div>
|
| 262 |
</div>
|
| 263 |
|
|
|
|
|
|
|
|
|
|
| 264 |
<script>
|
| 265 |
const form = document.getElementById('loginForm');
|
| 266 |
const loginBtn = document.getElementById('loginBtn');
|
| 267 |
const errorMessage = document.getElementById('errorMessage');
|
| 268 |
const errorText = document.getElementById('errorText');
|
| 269 |
|
| 270 |
-
// Demo users
|
| 271 |
-
const
|
| 272 |
-
'demo': {
|
| 273 |
-
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
};
|
| 276 |
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
e.preventDefault();
|
| 279 |
|
| 280 |
-
|
|
|
|
|
|
|
| 281 |
const password = document.getElementById('password').value;
|
| 282 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
// Disable button
|
| 284 |
loginBtn.disabled = true;
|
| 285 |
-
loginBtn.innerHTML = '<span
|
| 286 |
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
//
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
// Re-enable button
|
| 298 |
-
loginBtn.disabled = false;
|
| 299 |
-
loginBtn.innerHTML = '<span>Giriş Yap</span>';
|
| 300 |
-
return;
|
| 301 |
}
|
| 302 |
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
username: username,
|
| 306 |
-
role: user.role,
|
| 307 |
-
name: user.name,
|
| 308 |
-
loginTime: new Date().toISOString()
|
| 309 |
-
};
|
| 310 |
|
| 311 |
-
|
|
|
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
});
|
| 317 |
</script>
|
| 318 |
</body>
|
| 319 |
</html>
|
| 320 |
-
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Giriş - Bug Report System</title>
|
| 7 |
<link rel="stylesheet" href="style.css">
|
| 8 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
|
| 10 |
+
<!-- Firebase SDKs -->
|
| 11 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
| 12 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
|
| 13 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
| 14 |
+
|
| 15 |
<style>
|
| 16 |
/* Animated Background Section */
|
| 17 |
.animated-bg {
|
|
|
|
| 29 |
background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%);
|
| 30 |
}
|
| 31 |
|
|
|
|
| 32 |
.animated-bg::before {
|
| 33 |
content: '';
|
| 34 |
position: absolute;
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
@keyframes gradientMove {
|
| 42 |
+
0% { transform: translateY(-100%); }
|
| 43 |
+
100% { transform: translateY(100%); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
|
|
|
| 46 |
.animated-bg span {
|
| 47 |
position: relative;
|
| 48 |
display: block;
|
|
|
|
| 101 |
box-shadow: 0 0 20px rgba(255, 204, 0, 0.5);
|
| 102 |
}
|
| 103 |
|
|
|
|
| 104 |
.form-group {
|
| 105 |
margin-bottom: 25px;
|
| 106 |
}
|
|
|
|
| 151 |
.btn-primary:active {
|
| 152 |
opacity: 0.8;
|
| 153 |
}
|
| 154 |
+
|
| 155 |
+
.btn-primary:disabled {
|
| 156 |
+
opacity: 0.5;
|
| 157 |
+
cursor: not-allowed;
|
| 158 |
+
}
|
| 159 |
|
| 160 |
.error-message {
|
| 161 |
background: #FEE2E2;
|
|
|
|
| 171 |
display: block;
|
| 172 |
}
|
| 173 |
|
| 174 |
+
.register-link {
|
| 175 |
+
text-align: center;
|
| 176 |
+
margin-top: var(--spacing-lg);
|
| 177 |
+
color: #aaa;
|
| 178 |
+
font-size: 0.9rem;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.register-link a {
|
| 182 |
+
color: #FFCC00;
|
| 183 |
+
text-decoration: none;
|
| 184 |
+
font-weight: 600;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.register-link a:hover {
|
| 188 |
+
text-decoration: underline;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.forgot-password {
|
| 192 |
+
text-align: right;
|
| 193 |
+
margin-top: 8px;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.forgot-password a {
|
| 197 |
+
color: #888;
|
| 198 |
+
text-decoration: none;
|
| 199 |
+
font-size: 0.85rem;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.forgot-password a:hover {
|
| 203 |
+
color: #FFCC00;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
@media (max-width: 900px) {
|
| 207 |
.animated-bg span {
|
| 208 |
width: calc(10vw - 2px);
|
|
|
|
| 225 |
<body>
|
| 226 |
<!-- Animated Background -->
|
| 227 |
<section class="animated-bg">
|
|
|
|
| 228 |
<script>
|
| 229 |
for (let i = 0; i < 250; i++) {
|
| 230 |
document.write('<span></span>');
|
|
|
|
| 235 |
<div class="login-container">
|
| 236 |
<!-- Logo -->
|
| 237 |
<div class="login-header">
|
| 238 |
+
<div class="login-logo">
|
| 239 |
+
<img src="https://ffo3gv1cf3ir.merlincdn.net/SiteAssets/Bireysel/Navigasyon/turkcell-logo.png?20251016_03"
|
| 240 |
+
alt="Turkcell Logo"
|
| 241 |
+
style="width: 100%; height: 100%; object-fit: contain; padding: 10px;">
|
| 242 |
+
</div>
|
| 243 |
<h1>Sistem Girişi</h1>
|
| 244 |
<p>Bug Report Duplicate Detection System</p>
|
| 245 |
</div>
|
| 246 |
|
| 247 |
<!-- Error Message -->
|
| 248 |
<div id="errorMessage" class="error-message">
|
| 249 |
+
<strong>⚠️ Hata!</strong>
|
| 250 |
<p id="errorText"></p>
|
| 251 |
</div>
|
| 252 |
|
| 253 |
<!-- Login Form -->
|
| 254 |
<form id="loginForm">
|
| 255 |
<div class="form-group">
|
| 256 |
+
<label for="email" class="form-label">
|
| 257 |
+
<span>📧 E-posta</span>
|
| 258 |
<span class="required">*</span>
|
| 259 |
</label>
|
| 260 |
<input
|
| 261 |
+
type="email"
|
| 262 |
+
id="email"
|
| 263 |
+
name="email"
|
| 264 |
class="form-input"
|
| 265 |
placeholder="[email protected]"
|
| 266 |
required
|
| 267 |
+
autocomplete="email"
|
| 268 |
>
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<div class="form-group">
|
| 272 |
<label for="password" class="form-label">
|
| 273 |
+
<span>🔒 Şifre</span>
|
| 274 |
<span class="required">*</span>
|
| 275 |
</label>
|
| 276 |
<input
|
|
|
|
| 278 |
id="password"
|
| 279 |
name="password"
|
| 280 |
class="form-input"
|
| 281 |
+
placeholder="••••••••"
|
| 282 |
required
|
| 283 |
autocomplete="current-password"
|
| 284 |
>
|
| 285 |
+
<div class="forgot-password">
|
| 286 |
+
<a href="#" onclick="resetPassword(); return false;">Şifremi unuttum?</a>
|
| 287 |
+
</div>
|
| 288 |
</div>
|
| 289 |
|
| 290 |
+
<button type="submit" class="btn btn-primary" id="loginBtn">
|
| 291 |
+
<span>🚀 Giriş Yap</span>
|
| 292 |
</button>
|
| 293 |
</form>
|
| 294 |
|
| 295 |
+
<!-- Register Link -->
|
| 296 |
+
<div class="register-link">
|
| 297 |
+
Henüz hesabınız yok mu? <a href="register.html">Kayıt Ol</a>
|
| 298 |
+
</div>
|
| 299 |
+
|
| 300 |
+
<!-- Demo Mode -->
|
| 301 |
<div style="margin-top: var(--spacing-xl); padding: var(--spacing-md); background: rgba(255, 204, 0, 0.1); border: 1px solid #FFCC00; border-radius: var(--radius-md);">
|
| 302 |
<p style="font-size: 0.85rem; color: #FFCC00; margin: 0;">
|
| 303 |
+
<strong>💡 Demo Mod:</strong><br>
|
| 304 |
+
Firebase yapılandırılmamışsa demo hesabı kullanabilirsiniz.<br>
|
| 305 |
+
E-posta: <code style="background: #444; padding: 2px 6px; border-radius: 3px;">[email protected]</code><br>
|
| 306 |
+
Şifre: <code style="background: #444; padding: 2px 6px; border-radius: 3px;">demo123</code>
|
| 307 |
</p>
|
| 308 |
</div>
|
| 309 |
</div>
|
| 310 |
|
| 311 |
+
<!-- Firebase Config -->
|
| 312 |
+
<script src="firebase-config.js"></script>
|
| 313 |
+
|
| 314 |
<script>
|
| 315 |
const form = document.getElementById('loginForm');
|
| 316 |
const loginBtn = document.getElementById('loginBtn');
|
| 317 |
const errorMessage = document.getElementById('errorMessage');
|
| 318 |
const errorText = document.getElementById('errorText');
|
| 319 |
|
| 320 |
+
// Demo users (fallback if Firebase is not configured)
|
| 321 |
+
const DEMO_USERS = {
|
| 322 |
+
'demo@turkcell.com.tr': {
|
| 323 |
+
password: 'demo123',
|
| 324 |
+
role: 'admin',
|
| 325 |
+
name: 'Demo User'
|
| 326 |
+
},
|
| 327 |
+
'[email protected]': {
|
| 328 |
+
password: 'admin123',
|
| 329 |
+
role: 'admin',
|
| 330 |
+
name: 'Admin User'
|
| 331 |
+
}
|
| 332 |
};
|
| 333 |
|
| 334 |
+
// Check if Firebase is configured
|
| 335 |
+
const isFirebaseConfigured = () => {
|
| 336 |
+
try {
|
| 337 |
+
const config = firebase.app().options;
|
| 338 |
+
return config.apiKey !== 'YOUR_API_KEY';
|
| 339 |
+
} catch (e) {
|
| 340 |
+
return false;
|
| 341 |
+
}
|
| 342 |
+
};
|
| 343 |
+
|
| 344 |
+
// Show error
|
| 345 |
+
function showError(message) {
|
| 346 |
+
errorText.textContent = message;
|
| 347 |
+
errorMessage.classList.add('show');
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
// Hide error
|
| 351 |
+
function hideError() {
|
| 352 |
+
errorMessage.classList.remove('show');
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
// Reset password
|
| 356 |
+
async function resetPassword() {
|
| 357 |
+
const email = prompt('E-posta adresinizi girin:');
|
| 358 |
+
|
| 359 |
+
if (!email) return;
|
| 360 |
+
|
| 361 |
+
if (!isFirebaseConfigured()) {
|
| 362 |
+
alert('Firebase yapılandırılmamış! Demo modda şifre sıfırlama mevcut değil.');
|
| 363 |
+
return;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
try {
|
| 367 |
+
await auth.sendPasswordResetEmail(email);
|
| 368 |
+
alert('✅ Şifre sıfırlama e-postası gönderildi! Lütfen e-posta kutunuzu kontrol edin.');
|
| 369 |
+
} catch (error) {
|
| 370 |
+
console.error('Password reset error:', error);
|
| 371 |
+
alert('❌ Hata: ' + (error.message || 'Şifre sıfırlama e-postası gönderilemedi!'));
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Firebase login
|
| 376 |
+
async function loginWithFirebase(email, password) {
|
| 377 |
+
try {
|
| 378 |
+
const userCredential = await auth.signInWithEmailAndPassword(email, password);
|
| 379 |
+
const user = userCredential.user;
|
| 380 |
+
|
| 381 |
+
console.log('✅ Firebase login successful:', user.uid);
|
| 382 |
+
|
| 383 |
+
// Get user data from Firestore
|
| 384 |
+
const userDoc = await db.collection('users').doc(user.uid).get();
|
| 385 |
+
const userData = userDoc.data();
|
| 386 |
+
|
| 387 |
+
// Update last login
|
| 388 |
+
await db.collection('users').doc(user.uid).update({
|
| 389 |
+
lastLogin: firebase.firestore.FieldValue.serverTimestamp()
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
// Create session
|
| 393 |
+
const session = {
|
| 394 |
+
uid: user.uid,
|
| 395 |
+
email: user.email,
|
| 396 |
+
username: user.email,
|
| 397 |
+
name: userData?.fullName || user.displayName || user.email,
|
| 398 |
+
role: userData?.role || 'user',
|
| 399 |
+
loginTime: new Date().toISOString(),
|
| 400 |
+
authProvider: 'firebase'
|
| 401 |
+
};
|
| 402 |
+
|
| 403 |
+
localStorage.setItem('userSession', JSON.stringify(session));
|
| 404 |
+
|
| 405 |
+
console.log('✅ Session created:', session);
|
| 406 |
+
|
| 407 |
+
// Redirect
|
| 408 |
+
window.location.href = 'data_selection.html';
|
| 409 |
+
|
| 410 |
+
} catch (error) {
|
| 411 |
+
throw error;
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// Demo login (fallback)
|
| 416 |
+
function loginWithDemo(email, password) {
|
| 417 |
+
const user = DEMO_USERS[email];
|
| 418 |
+
|
| 419 |
+
if (!user || user.password !== password) {
|
| 420 |
+
throw new Error('Kullanıcı adı veya şifre hatalı!');
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
// Create demo session
|
| 424 |
+
const session = {
|
| 425 |
+
username: email,
|
| 426 |
+
email: email,
|
| 427 |
+
role: user.role,
|
| 428 |
+
name: user.name,
|
| 429 |
+
loginTime: new Date().toISOString(),
|
| 430 |
+
authProvider: 'demo'
|
| 431 |
+
};
|
| 432 |
+
|
| 433 |
+
localStorage.setItem('userSession', JSON.stringify(session));
|
| 434 |
+
|
| 435 |
+
console.log('✅ Demo login successful:', session);
|
| 436 |
+
|
| 437 |
+
// Redirect
|
| 438 |
+
window.location.href = 'data_selection.html';
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
// Form submit
|
| 442 |
+
form.addEventListener('submit', async (e) => {
|
| 443 |
e.preventDefault();
|
| 444 |
|
| 445 |
+
hideError();
|
| 446 |
+
|
| 447 |
+
const email = document.getElementById('email').value.trim();
|
| 448 |
const password = document.getElementById('password').value;
|
| 449 |
|
| 450 |
+
// Validate
|
| 451 |
+
if (!email || !password) {
|
| 452 |
+
showError('Lütfen tüm alanları doldurun!');
|
| 453 |
+
return;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
// Set logging in flag to prevent auth state change redirect
|
| 457 |
+
isLoggingIn = true;
|
| 458 |
+
|
| 459 |
// Disable button
|
| 460 |
loginBtn.disabled = true;
|
| 461 |
+
loginBtn.innerHTML = '<span>⏳ Giriş yapılıyor...</span>';
|
| 462 |
|
| 463 |
+
try {
|
| 464 |
+
if (isFirebaseConfigured()) {
|
| 465 |
+
// Try Firebase login
|
| 466 |
+
console.log('🔥 Attempting Firebase login...');
|
| 467 |
+
await loginWithFirebase(email, password);
|
| 468 |
+
} else {
|
| 469 |
+
// Fallback to demo login
|
| 470 |
+
console.log('💡 Firebase not configured, using demo login...');
|
| 471 |
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
|
| 472 |
+
loginWithDemo(email, password);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
}
|
| 474 |
|
| 475 |
+
} catch (error) {
|
| 476 |
+
console.error('❌ Login error:', error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
+
// Reset logging in flag on error
|
| 479 |
+
isLoggingIn = false;
|
| 480 |
|
| 481 |
+
let errorMsg = 'Giriş yapılamadı!';
|
| 482 |
+
|
| 483 |
+
// Handle Firebase errors
|
| 484 |
+
switch (error.code) {
|
| 485 |
+
case 'auth/user-not-found':
|
| 486 |
+
errorMsg = 'Kullanıcı bulunamadı!';
|
| 487 |
+
break;
|
| 488 |
+
case 'auth/wrong-password':
|
| 489 |
+
errorMsg = 'Hatalı şifre!';
|
| 490 |
+
break;
|
| 491 |
+
case 'auth/invalid-email':
|
| 492 |
+
errorMsg = 'Geçersiz e-posta adresi!';
|
| 493 |
+
break;
|
| 494 |
+
case 'auth/user-disabled':
|
| 495 |
+
errorMsg = 'Bu hesap devre dışı bırakılmış!';
|
| 496 |
+
break;
|
| 497 |
+
case 'auth/too-many-requests':
|
| 498 |
+
errorMsg = 'Çok fazla başarısız deneme! Lütfen daha sonra tekrar deneyin.';
|
| 499 |
+
break;
|
| 500 |
+
default:
|
| 501 |
+
errorMsg = error.message || errorMsg;
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
showError(errorMsg);
|
| 505 |
+
|
| 506 |
+
// Re-enable button
|
| 507 |
+
loginBtn.disabled = false;
|
| 508 |
+
loginBtn.innerHTML = '<span>🚀 Giriş Yap</span>';
|
| 509 |
+
}
|
| 510 |
+
});
|
| 511 |
+
|
| 512 |
+
// Check if already logged in (only on page load, not during login process)
|
| 513 |
+
let isLoggingIn = false;
|
| 514 |
+
|
| 515 |
+
auth.onAuthStateChanged((user) => {
|
| 516 |
+
// Only redirect if user exists, we're on login page, and not currently logging in
|
| 517 |
+
if (user && window.location.pathname.includes('login.html') && !isLoggingIn) {
|
| 518 |
+
console.log('✅ User already logged in, redirecting...');
|
| 519 |
+
// Small delay to prevent loop
|
| 520 |
+
setTimeout(() => {
|
| 521 |
+
window.location.href = 'data_selection.html';
|
| 522 |
+
}, 100);
|
| 523 |
+
}
|
| 524 |
});
|
| 525 |
</script>
|
| 526 |
</body>
|
| 527 |
</html>
|
|
|
web/register.html
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="tr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Kayıt Ol - Bug Report System</title>
|
| 7 |
+
<link rel="stylesheet" href="style.css">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 9 |
+
|
| 10 |
+
<!-- Firebase SDKs -->
|
| 11 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-app-compat.js"></script>
|
| 12 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-auth-compat.js"></script>
|
| 13 |
+
<script src="https://www.gstatic.com/firebasejs/9.22.0/firebase-firestore-compat.js"></script>
|
| 14 |
+
|
| 15 |
+
<style>
|
| 16 |
+
/* Animated Background Section */
|
| 17 |
+
.animated-bg {
|
| 18 |
+
position: fixed;
|
| 19 |
+
top: 0;
|
| 20 |
+
left: 0;
|
| 21 |
+
width: 100vw;
|
| 22 |
+
height: 100vh;
|
| 23 |
+
display: flex;
|
| 24 |
+
justify-content: center;
|
| 25 |
+
align-items: center;
|
| 26 |
+
gap: 2px;
|
| 27 |
+
flex-wrap: wrap;
|
| 28 |
+
overflow: hidden;
|
| 29 |
+
background: linear-gradient(135deg, #0A0E27 0%, #1a1f3a 100%);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.animated-bg::before {
|
| 33 |
+
content: '';
|
| 34 |
+
position: absolute;
|
| 35 |
+
width: 100%;
|
| 36 |
+
height: 100%;
|
| 37 |
+
background: linear-gradient(#0A0E27, #FFCC00, #0A0E27);
|
| 38 |
+
animation: gradientMove 5s linear infinite;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
@keyframes gradientMove {
|
| 42 |
+
0% { transform: translateY(-100%); }
|
| 43 |
+
100% { transform: translateY(100%); }
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.animated-bg span {
|
| 47 |
+
position: relative;
|
| 48 |
+
display: block;
|
| 49 |
+
width: calc(6.25vw - 2px);
|
| 50 |
+
height: calc(6.25vw - 2px);
|
| 51 |
+
background: #1a1f3a;
|
| 52 |
+
z-index: 2;
|
| 53 |
+
transition: 1.5s;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.animated-bg span:hover {
|
| 57 |
+
background: #FFCC00;
|
| 58 |
+
transition: 0s;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/* Register Container */
|
| 62 |
+
.register-container {
|
| 63 |
+
position: relative;
|
| 64 |
+
max-width: 500px;
|
| 65 |
+
margin: 0 auto;
|
| 66 |
+
padding: 40px;
|
| 67 |
+
background: rgba(34, 34, 34, 0.95);
|
| 68 |
+
backdrop-filter: blur(10px);
|
| 69 |
+
border-radius: 16px;
|
| 70 |
+
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.9);
|
| 71 |
+
z-index: 1000;
|
| 72 |
+
max-height: 90vh;
|
| 73 |
+
overflow-y: auto;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.register-header {
|
| 77 |
+
text-align: center;
|
| 78 |
+
margin-bottom: var(--spacing-xl);
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.register-header h1 {
|
| 82 |
+
color: #FFCC00;
|
| 83 |
+
text-transform: uppercase;
|
| 84 |
+
font-size: 2em;
|
| 85 |
+
margin-bottom: var(--spacing-sm);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.register-header p {
|
| 89 |
+
color: #aaa;
|
| 90 |
+
font-size: 0.9rem;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.register-logo {
|
| 94 |
+
width: 80px;
|
| 95 |
+
height: 80px;
|
| 96 |
+
margin: 0 auto var(--spacing-md);
|
| 97 |
+
background: linear-gradient(135deg, #FFCC00 0%, #FF9500 100%);
|
| 98 |
+
border-radius: 20px;
|
| 99 |
+
display: flex;
|
| 100 |
+
align-items: center;
|
| 101 |
+
justify-content: center;
|
| 102 |
+
font-size: 3rem;
|
| 103 |
+
box-shadow: 0 0 20px rgba(255, 204, 0, 0.5);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.form-group {
|
| 107 |
+
margin-bottom: 20px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.form-label {
|
| 111 |
+
color: #fff;
|
| 112 |
+
margin-bottom: 8px;
|
| 113 |
+
display: block;
|
| 114 |
+
font-size: 0.9rem;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.form-input {
|
| 118 |
+
width: 100%;
|
| 119 |
+
background: #333;
|
| 120 |
+
border: none;
|
| 121 |
+
outline: none;
|
| 122 |
+
padding: 12px 15px;
|
| 123 |
+
border-radius: 4px;
|
| 124 |
+
color: #fff;
|
| 125 |
+
font-weight: 500;
|
| 126 |
+
font-size: 1em;
|
| 127 |
+
transition: 0.3s;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.form-input:focus {
|
| 131 |
+
background: #444;
|
| 132 |
+
box-shadow: 0 0 10px rgba(255, 204, 0, 0.3);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.form-input.error {
|
| 136 |
+
border: 2px solid #EF4444;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.password-strength {
|
| 140 |
+
margin-top: 8px;
|
| 141 |
+
height: 4px;
|
| 142 |
+
background: #333;
|
| 143 |
+
border-radius: 2px;
|
| 144 |
+
overflow: hidden;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.password-strength-bar {
|
| 148 |
+
height: 100%;
|
| 149 |
+
width: 0%;
|
| 150 |
+
transition: all 0.3s;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.password-strength-bar.weak {
|
| 154 |
+
width: 33%;
|
| 155 |
+
background: #EF4444;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.password-strength-bar.medium {
|
| 159 |
+
width: 66%;
|
| 160 |
+
background: #F59E0B;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.password-strength-bar.strong {
|
| 164 |
+
width: 100%;
|
| 165 |
+
background: #10B981;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.password-hint {
|
| 169 |
+
font-size: 0.75rem;
|
| 170 |
+
color: #888;
|
| 171 |
+
margin-top: 4px;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.btn-primary {
|
| 175 |
+
width: 100%;
|
| 176 |
+
padding: 15px;
|
| 177 |
+
background: #FFCC00;
|
| 178 |
+
color: #000;
|
| 179 |
+
border: none;
|
| 180 |
+
font-weight: 600;
|
| 181 |
+
font-size: 1.2em;
|
| 182 |
+
letter-spacing: 0.05em;
|
| 183 |
+
cursor: pointer;
|
| 184 |
+
border-radius: 4px;
|
| 185 |
+
transition: 0.3s;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
.btn-primary:hover {
|
| 189 |
+
background: #FFD700;
|
| 190 |
+
box-shadow: 0 0 20px rgba(255, 204, 0, 0.5);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.btn-primary:disabled {
|
| 194 |
+
opacity: 0.5;
|
| 195 |
+
cursor: not-allowed;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.message {
|
| 199 |
+
padding: var(--spacing-md);
|
| 200 |
+
border-radius: var(--radius-md);
|
| 201 |
+
margin-bottom: var(--spacing-md);
|
| 202 |
+
display: none;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.message.show {
|
| 206 |
+
display: block;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.error-message {
|
| 210 |
+
background: #FEE2E2;
|
| 211 |
+
border: 1px solid #EF4444;
|
| 212 |
+
color: #991B1B;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.success-message {
|
| 216 |
+
background: #D1FAE5;
|
| 217 |
+
border: 1px solid #10B981;
|
| 218 |
+
color: #065F46;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.login-link {
|
| 222 |
+
text-align: center;
|
| 223 |
+
margin-top: var(--spacing-lg);
|
| 224 |
+
color: #aaa;
|
| 225 |
+
font-size: 0.9rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.login-link a {
|
| 229 |
+
color: #FFCC00;
|
| 230 |
+
text-decoration: none;
|
| 231 |
+
font-weight: 600;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.login-link a:hover {
|
| 235 |
+
text-decoration: underline;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
@media (max-width: 600px) {
|
| 239 |
+
.register-container {
|
| 240 |
+
margin: 20px;
|
| 241 |
+
padding: 30px 20px;
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
</style>
|
| 245 |
+
</head>
|
| 246 |
+
<body>
|
| 247 |
+
<!-- Animated Background -->
|
| 248 |
+
<section class="animated-bg">
|
| 249 |
+
<script>
|
| 250 |
+
for (let i = 0; i < 250; i++) {
|
| 251 |
+
document.write('<span></span>');
|
| 252 |
+
}
|
| 253 |
+
</script>
|
| 254 |
+
</section>
|
| 255 |
+
|
| 256 |
+
<div class="register-container">
|
| 257 |
+
<!-- Logo -->
|
| 258 |
+
<div class="register-header">
|
| 259 |
+
<div class="register-logo">
|
| 260 |
+
<img src="https://ffo3gv1cf3ir.merlincdn.net/SiteAssets/Bireysel/Navigasyon/turkcell-logo.png?20251016_03"
|
| 261 |
+
alt="Turkcell Logo"
|
| 262 |
+
style="width: 100%; height: 100%; object-fit: contain; padding: 10px;">
|
| 263 |
+
</div>
|
| 264 |
+
<h1>Kayıt Ol</h1>
|
| 265 |
+
<p>Yeni hesap oluştur</p>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
<!-- Messages -->
|
| 269 |
+
<div id="errorMessage" class="message error-message">
|
| 270 |
+
<strong>⚠️ Hata!</strong>
|
| 271 |
+
<p id="errorText"></p>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<div id="successMessage" class="message success-message">
|
| 275 |
+
<strong>✅ Başarılı!</strong>
|
| 276 |
+
<p id="successText"></p>
|
| 277 |
+
</div>
|
| 278 |
+
|
| 279 |
+
<!-- Register Form -->
|
| 280 |
+
<form id="registerForm">
|
| 281 |
+
<div class="form-group">
|
| 282 |
+
<label for="fullName" class="form-label">
|
| 283 |
+
<span>👤 Ad Soyad</span>
|
| 284 |
+
<span class="required">*</span>
|
| 285 |
+
</label>
|
| 286 |
+
<input
|
| 287 |
+
type="text"
|
| 288 |
+
id="fullName"
|
| 289 |
+
class="form-input"
|
| 290 |
+
placeholder="Adınız Soyadınız"
|
| 291 |
+
required
|
| 292 |
+
minlength="3"
|
| 293 |
+
>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<div class="form-group">
|
| 297 |
+
<label for="email" class="form-label">
|
| 298 |
+
<span>📧 E-posta</span>
|
| 299 |
+
<span class="required">*</span>
|
| 300 |
+
</label>
|
| 301 |
+
<input
|
| 302 |
+
type="email"
|
| 303 |
+
id="email"
|
| 304 |
+
class="form-input"
|
| 305 |
+
placeholder="[email protected]"
|
| 306 |
+
required
|
| 307 |
+
>
|
| 308 |
+
</div>
|
| 309 |
+
|
| 310 |
+
<div class="form-group">
|
| 311 |
+
<label for="password" class="form-label">
|
| 312 |
+
<span>🔒 Şifre</span>
|
| 313 |
+
<span class="required">*</span>
|
| 314 |
+
</label>
|
| 315 |
+
<input
|
| 316 |
+
type="password"
|
| 317 |
+
id="password"
|
| 318 |
+
class="form-input"
|
| 319 |
+
placeholder="En az 6 karakter"
|
| 320 |
+
required
|
| 321 |
+
minlength="6"
|
| 322 |
+
>
|
| 323 |
+
<div class="password-strength">
|
| 324 |
+
<div id="passwordStrengthBar" class="password-strength-bar"></div>
|
| 325 |
+
</div>
|
| 326 |
+
<div class="password-hint">
|
| 327 |
+
En az 6 karakter, büyük harf, küçük harf ve rakam içermelidir
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<div class="form-group">
|
| 332 |
+
<label for="confirmPassword" class="form-label">
|
| 333 |
+
<span>🔒 Şifre Tekrar</span>
|
| 334 |
+
<span class="required">*</span>
|
| 335 |
+
</label>
|
| 336 |
+
<input
|
| 337 |
+
type="password"
|
| 338 |
+
id="confirmPassword"
|
| 339 |
+
class="form-input"
|
| 340 |
+
placeholder="Şifrenizi tekrar girin"
|
| 341 |
+
required
|
| 342 |
+
minlength="6"
|
| 343 |
+
>
|
| 344 |
+
</div>
|
| 345 |
+
|
| 346 |
+
<div class="form-group">
|
| 347 |
+
<label for="role" class="form-label">
|
| 348 |
+
<span>🎭 Rol</span>
|
| 349 |
+
<span class="required">*</span>
|
| 350 |
+
</label>
|
| 351 |
+
<select id="role" class="form-input" required>
|
| 352 |
+
<option value="">Seçiniz</option>
|
| 353 |
+
<option value="user">Kullanıcı</option>
|
| 354 |
+
<option value="admin">Admin</option>
|
| 355 |
+
<option value="developer">Developer</option>
|
| 356 |
+
<option value="tester">Tester</option>
|
| 357 |
+
</select>
|
| 358 |
+
</div>
|
| 359 |
+
|
| 360 |
+
<button type="submit" class="btn btn-primary" id="registerBtn">
|
| 361 |
+
<span>🚀 Kayıt Ol</span>
|
| 362 |
+
</button>
|
| 363 |
+
</form>
|
| 364 |
+
|
| 365 |
+
<!-- Login Link -->
|
| 366 |
+
<div class="login-link">
|
| 367 |
+
Zaten hesabınız var mı? <a href="login.html">Giriş Yap</a>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<!-- Firebase Config -->
|
| 372 |
+
<script src="firebase-config.js"></script>
|
| 373 |
+
|
| 374 |
+
<script>
|
| 375 |
+
const form = document.getElementById('registerForm');
|
| 376 |
+
const registerBtn = document.getElementById('registerBtn');
|
| 377 |
+
const errorMessage = document.getElementById('errorMessage');
|
| 378 |
+
const errorText = document.getElementById('errorText');
|
| 379 |
+
const successMessage = document.getElementById('successMessage');
|
| 380 |
+
const successText = document.getElementById('successText');
|
| 381 |
+
const passwordInput = document.getElementById('password');
|
| 382 |
+
const confirmPasswordInput = document.getElementById('confirmPassword');
|
| 383 |
+
const passwordStrengthBar = document.getElementById('passwordStrengthBar');
|
| 384 |
+
|
| 385 |
+
// Password strength checker
|
| 386 |
+
passwordInput.addEventListener('input', () => {
|
| 387 |
+
const password = passwordInput.value;
|
| 388 |
+
let strength = 0;
|
| 389 |
+
|
| 390 |
+
if (password.length >= 6) strength++;
|
| 391 |
+
if (password.length >= 10) strength++;
|
| 392 |
+
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) strength++;
|
| 393 |
+
if (/\d/.test(password)) strength++;
|
| 394 |
+
if (/[^a-zA-Z0-9]/.test(password)) strength++;
|
| 395 |
+
|
| 396 |
+
passwordStrengthBar.className = 'password-strength-bar';
|
| 397 |
+
if (strength <= 2) {
|
| 398 |
+
passwordStrengthBar.classList.add('weak');
|
| 399 |
+
} else if (strength <= 4) {
|
| 400 |
+
passwordStrengthBar.classList.add('medium');
|
| 401 |
+
} else {
|
| 402 |
+
passwordStrengthBar.classList.add('strong');
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
// Show error
|
| 407 |
+
function showError(message) {
|
| 408 |
+
errorText.textContent = message;
|
| 409 |
+
errorMessage.classList.add('show');
|
| 410 |
+
successMessage.classList.remove('show');
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
// Show success
|
| 414 |
+
function showSuccess(message) {
|
| 415 |
+
successText.textContent = message;
|
| 416 |
+
successMessage.classList.add('show');
|
| 417 |
+
errorMessage.classList.remove('show');
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
// Register form submit
|
| 421 |
+
form.addEventListener('submit', async (e) => {
|
| 422 |
+
e.preventDefault();
|
| 423 |
+
|
| 424 |
+
const fullName = document.getElementById('fullName').value.trim();
|
| 425 |
+
const email = document.getElementById('email').value.trim();
|
| 426 |
+
const password = document.getElementById('password').value;
|
| 427 |
+
const confirmPassword = document.getElementById('confirmPassword').value;
|
| 428 |
+
const role = document.getElementById('role').value;
|
| 429 |
+
|
| 430 |
+
// Validate
|
| 431 |
+
if (!fullName || fullName.length < 3) {
|
| 432 |
+
showError('Ad Soyad en az 3 karakter olmalıdır!');
|
| 433 |
+
return;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
if (!email || !email.includes('@')) {
|
| 437 |
+
showError('Geçerli bir e-posta adresi girin!');
|
| 438 |
+
return;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
if (password.length < 6) {
|
| 442 |
+
showError('Şifre en az 6 karakter olmalıdır!');
|
| 443 |
+
return;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
if (password !== confirmPassword) {
|
| 447 |
+
showError('Şifreler eşleşmiyor!');
|
| 448 |
+
confirmPasswordInput.classList.add('error');
|
| 449 |
+
return;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
if (!role) {
|
| 453 |
+
showError('Lütfen bir rol seçin!');
|
| 454 |
+
return;
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
// Disable button
|
| 458 |
+
registerBtn.disabled = true;
|
| 459 |
+
registerBtn.innerHTML = '<span>⏳ Kayıt yapılıyor...</span>';
|
| 460 |
+
|
| 461 |
+
try {
|
| 462 |
+
// Create user with Firebase Authentication
|
| 463 |
+
const userCredential = await auth.createUserWithEmailAndPassword(email, password);
|
| 464 |
+
const user = userCredential.user;
|
| 465 |
+
|
| 466 |
+
console.log('✅ User created:', user.uid);
|
| 467 |
+
|
| 468 |
+
// Update display name
|
| 469 |
+
await user.updateProfile({
|
| 470 |
+
displayName: fullName
|
| 471 |
+
});
|
| 472 |
+
|
| 473 |
+
// Save additional user info to Firestore
|
| 474 |
+
await db.collection('users').doc(user.uid).set({
|
| 475 |
+
uid: user.uid,
|
| 476 |
+
email: email,
|
| 477 |
+
fullName: fullName,
|
| 478 |
+
role: role,
|
| 479 |
+
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
|
| 480 |
+
lastLogin: firebase.firestore.FieldValue.serverTimestamp()
|
| 481 |
+
});
|
| 482 |
+
|
| 483 |
+
console.log('✅ User data saved to Firestore');
|
| 484 |
+
|
| 485 |
+
// Show success
|
| 486 |
+
showSuccess('Kayıt başarılı! Giriş sayfasına yönlendiriliyorsunuz...');
|
| 487 |
+
|
| 488 |
+
// Redirect to login after 2 seconds
|
| 489 |
+
setTimeout(() => {
|
| 490 |
+
window.location.href = 'login.html';
|
| 491 |
+
}, 2000);
|
| 492 |
+
|
| 493 |
+
} catch (error) {
|
| 494 |
+
console.error('❌ Registration error:', error);
|
| 495 |
+
|
| 496 |
+
// Handle Firebase errors
|
| 497 |
+
let errorMsg = 'Kayıt sırasında bir hata oluştu!';
|
| 498 |
+
|
| 499 |
+
switch (error.code) {
|
| 500 |
+
case 'auth/email-already-in-use':
|
| 501 |
+
errorMsg = 'Bu e-posta adresi zaten kullanımda!';
|
| 502 |
+
break;
|
| 503 |
+
case 'auth/invalid-email':
|
| 504 |
+
errorMsg = 'Geçersiz e-posta adresi!';
|
| 505 |
+
break;
|
| 506 |
+
case 'auth/operation-not-allowed':
|
| 507 |
+
errorMsg = 'E-posta/şifre girişi etkinleştirilmemiş!';
|
| 508 |
+
break;
|
| 509 |
+
case 'auth/weak-password':
|
| 510 |
+
errorMsg = 'Şifre çok zayıf! En az 6 karakter olmalı.';
|
| 511 |
+
break;
|
| 512 |
+
default:
|
| 513 |
+
errorMsg = error.message;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
showError(errorMsg);
|
| 517 |
+
|
| 518 |
+
// Re-enable button
|
| 519 |
+
registerBtn.disabled = false;
|
| 520 |
+
registerBtn.innerHTML = '<span>🚀 Kayıt Ol</span>';
|
| 521 |
+
}
|
| 522 |
+
});
|
| 523 |
+
|
| 524 |
+
// Remove error class on input
|
| 525 |
+
confirmPasswordInput.addEventListener('input', () => {
|
| 526 |
+
confirmPasswordInput.classList.remove('error');
|
| 527 |
+
});
|
| 528 |
+
</script>
|
| 529 |
+
</body>
|
| 530 |
+
</html>
|
| 531 |
+
|