Really-amin commited on
Commit
c19afcd
·
verified ·
1 Parent(s): 5cd2b89

Upload 143 files

Browse files
Files changed (20) hide show
  1. .dockerignore +7 -112
  2. DEPLOYMENT_GUIDE.md +600 -0
  3. Dockerfile +21 -24
  4. HUGGINGFACE_DEPLOYMENT.md +349 -0
  5. QUICK_START.md +152 -191
  6. README.md +19 -482
  7. app.py +334 -1471
  8. backend/__pycache__/__init__.cpython-313.pyc +0 -0
  9. config.js +1 -1
  10. config.py +314 -193
  11. dashboard.html +21 -72
  12. database.py +396 -581
  13. database/__init__.py +5 -44
  14. docker-compose.yml +67 -77
  15. index.html +896 -1216
  16. main.py +164 -26
  17. requirements.txt +3 -58
  18. start.bat +6 -11
  19. start_server.py +17 -239
  20. utils/__init__.py +0 -20
.dockerignore CHANGED
@@ -1,121 +1,16 @@
1
- # Python
2
  __pycache__/
3
  *.py[cod]
4
  *$py.class
5
  *.so
6
- .Python
7
- build/
8
- develop-eggs/
9
- dist/
10
- downloads/
11
- eggs/
12
- .eggs/
13
- lib/
14
- lib64/
15
- parts/
16
- sdist/
17
- var/
18
- wheels/
19
- *.egg-info/
20
- .installed.cfg
21
- *.egg
22
- MANIFEST
23
- pip-log.txt
24
- pip-delete-this-directory.txt
25
-
26
- # Virtual environments
27
  venv/
28
  ENV/
29
- env/
30
- .venv
31
-
32
- # IDE
33
- .vscode/
34
- .idea/
35
- *.swp
36
- *.swo
37
- *~
38
  .DS_Store
39
-
40
- # Git
41
- .git/
42
  .gitignore
43
- .gitattributes
44
-
45
- # Documentation
46
- *.md
47
- docs/
48
- README*.md
49
- CHANGELOG.md
50
- LICENSE
51
-
52
- # Testing
53
- .pytest_cache/
54
- .coverage
55
- htmlcov/
56
- .tox/
57
- .hypothesis/
58
- tests/
59
- test_*.py
60
-
61
- # Logs and databases (will be created in container)
62
  *.log
63
- logs/
64
- data/*.db
65
- data/*.sqlite
66
- data/*.db-journal
67
-
68
- # Environment files (should be set via docker-compose or HF Secrets)
69
- .env
70
- .env.*
71
- !.env.example
72
-
73
- # Docker
74
- docker-compose*.yml
75
- !docker-compose.yml
76
- Dockerfile
77
- .dockerignore
78
-
79
- # CI/CD
80
- .github/
81
- .gitlab-ci.yml
82
- .travis.yml
83
- azure-pipelines.yml
84
-
85
- # Temporary files
86
- *.tmp
87
- *.bak
88
- *.swp
89
- temp/
90
- tmp/
91
-
92
- # Node modules (if any)
93
- node_modules/
94
- package-lock.json
95
- yarn.lock
96
-
97
- # OS files
98
- Thumbs.db
99
- .DS_Store
100
- desktop.ini
101
-
102
- # Jupyter notebooks
103
- .ipynb_checkpoints/
104
- *.ipynb
105
-
106
- # Model cache (models will be downloaded in container)
107
- models/
108
- .cache/
109
- .huggingface/
110
-
111
- # Large files that shouldn't be in image
112
- *.tar
113
- *.tar.gz
114
- *.zip
115
- *.rar
116
- *.7z
117
-
118
- # Screenshots and assets not needed
119
- screenshots/
120
- assets/*.png
121
- assets/*.jpg
 
 
1
  __pycache__/
2
  *.py[cod]
3
  *$py.class
4
  *.so
5
+ .env
6
+ .venv
7
+ env/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  venv/
9
  ENV/
 
 
 
 
 
 
 
 
 
10
  .DS_Store
11
+ .git
 
 
12
  .gitignore
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  *.log
14
+ *.sqlite
15
+ .idea/
16
+ .vscode/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
DEPLOYMENT_GUIDE.md CHANGED
@@ -0,0 +1,600 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide - Crypto Resource Aggregator
2
+
3
+ ## Quick Deployment to Hugging Face Spaces
4
+
5
+ ### Method 1: Web Interface (Recommended for Beginners)
6
+
7
+ 1. **Create a Hugging Face Account**
8
+ - Go to https://huggingface.co/join
9
+ - Sign up for a free account
10
+
11
+ 2. **Create a New Space**
12
+ - Go to https://huggingface.co/new-space
13
+ - Choose a name (e.g., `crypto-resource-aggregator`)
14
+ - Select SDK: **Docker**
15
+ - Choose visibility: **Public** or **Private**
16
+ - Click "Create Space"
17
+
18
+ 3. **Upload Files**
19
+ Upload the following files to your Space:
20
+ - `app.py` - Main application file
21
+ - `requirements.txt` - Python dependencies
22
+ - `all_apis_merged_2025.json` - Resource configuration
23
+ - `README.md` - Documentation
24
+ - `Dockerfile` - Docker configuration
25
+
26
+ 4. **Wait for Build**
27
+ - The Space will automatically build and deploy
28
+ - This may take 2-5 minutes
29
+ - You'll see the build logs in real-time
30
+
31
+ 5. **Access Your API**
32
+ - Once deployed, your API will be available at:
33
+ `https://[your-username]-[space-name].hf.space`
34
+ - Example: `https://username-crypto-resource-aggregator.hf.space`
35
+
36
+ ### Method 2: Git CLI (Recommended for Advanced Users)
37
+
38
+ ```bash
39
+ # Clone your Space repository
40
+ git clone https://huggingface.co/spaces/[your-username]/[space-name]
41
+ cd [space-name]
42
+
43
+ # Copy all files to the repository
44
+ cp app.py requirements.txt all_apis_merged_2025.json README.md Dockerfile .
45
+
46
+ # Commit and push
47
+ git add .
48
+ git commit -m "Initial deployment of Crypto Resource Aggregator"
49
+ git push
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Alternative Deployment Options
55
+
56
+ ### Option 1: Heroku
57
+
58
+ ```bash
59
+ # Install Heroku CLI
60
+ # https://devcenter.heroku.com/articles/heroku-cli
61
+
62
+ # Create a new app
63
+ heroku create crypto-resource-aggregator
64
+
65
+ # Create Procfile
66
+ echo "web: python app.py" > Procfile
67
+
68
+ # Deploy
69
+ git add .
70
+ git commit -m "Deploy to Heroku"
71
+ git push heroku main
72
+
73
+ # Open your app
74
+ heroku open
75
+ ```
76
+
77
+ ### Option 2: Railway
78
+
79
+ ```bash
80
+ # Install Railway CLI
81
+ npm i -g @railway/cli
82
+
83
+ # Login
84
+ railway login
85
+
86
+ # Initialize project
87
+ railway init
88
+
89
+ # Deploy
90
+ railway up
91
+
92
+ # Get deployment URL
93
+ railway domain
94
+ ```
95
+
96
+ ### Option 3: Render
97
+
98
+ 1. Go to https://render.com
99
+ 2. Click "New +" → "Web Service"
100
+ 3. Connect your GitHub repository
101
+ 4. Configure:
102
+ - **Build Command**: `pip install -r requirements.txt`
103
+ - **Start Command**: `python app.py`
104
+ - **Environment**: Python 3
105
+ 5. Click "Create Web Service"
106
+
107
+ ### Option 4: Docker (Self-Hosted)
108
+
109
+ ```bash
110
+ # Build the Docker image
111
+ docker build -t crypto-aggregator .
112
+
113
+ # Run the container
114
+ docker run -d -p 7860:7860 --name crypto-aggregator crypto-aggregator
115
+
116
+ # Check logs
117
+ docker logs crypto-aggregator
118
+
119
+ # Stop the container
120
+ docker stop crypto-aggregator
121
+
122
+ # Remove the container
123
+ docker rm crypto-aggregator
124
+ ```
125
+
126
+ ### Option 5: Docker Compose (Self-Hosted)
127
+
128
+ Create `docker-compose.yml`:
129
+
130
+ ```yaml
131
+ version: '3.8'
132
+
133
+ services:
134
+ aggregator:
135
+ build: .
136
+ ports:
137
+ - "7860:7860"
138
+ restart: unless-stopped
139
+ volumes:
140
+ - ./history.db:/app/history.db
141
+ environment:
142
+ - ENVIRONMENT=production
143
+ ```
144
+
145
+ Run:
146
+ ```bash
147
+ docker-compose up -d
148
+ ```
149
+
150
+ ### Option 6: AWS EC2
151
+
152
+ ```bash
153
+ # Connect to your EC2 instance
154
+ ssh -i your-key.pem ubuntu@your-instance-ip
155
+
156
+ # Install Python and dependencies
157
+ sudo apt update
158
+ sudo apt install python3-pip python3-venv -y
159
+
160
+ # Create virtual environment
161
+ python3 -m venv venv
162
+ source venv/bin/activate
163
+
164
+ # Upload files (from local machine)
165
+ scp -i your-key.pem app.py requirements.txt all_apis_merged_2025.json ubuntu@your-instance-ip:~/
166
+
167
+ # Install dependencies
168
+ pip install -r requirements.txt
169
+
170
+ # Run with nohup
171
+ nohup python app.py > output.log 2>&1 &
172
+
173
+ # Or use systemd service (recommended)
174
+ sudo nano /etc/systemd/system/crypto-aggregator.service
175
+ ```
176
+
177
+ Create systemd service file:
178
+ ```ini
179
+ [Unit]
180
+ Description=Crypto Resource Aggregator
181
+ After=network.target
182
+
183
+ [Service]
184
+ User=ubuntu
185
+ WorkingDirectory=/home/ubuntu/crypto-aggregator
186
+ ExecStart=/home/ubuntu/venv/bin/python app.py
187
+ Restart=always
188
+
189
+ [Install]
190
+ WantedBy=multi-user.target
191
+ ```
192
+
193
+ Enable and start:
194
+ ```bash
195
+ sudo systemctl enable crypto-aggregator
196
+ sudo systemctl start crypto-aggregator
197
+ sudo systemctl status crypto-aggregator
198
+ ```
199
+
200
+ ### Option 7: Google Cloud Run
201
+
202
+ ```bash
203
+ # Install gcloud CLI
204
+ # https://cloud.google.com/sdk/docs/install
205
+
206
+ # Authenticate
207
+ gcloud auth login
208
+
209
+ # Set project
210
+ gcloud config set project YOUR_PROJECT_ID
211
+
212
+ # Build and deploy
213
+ gcloud run deploy crypto-aggregator \
214
+ --source . \
215
+ --platform managed \
216
+ --region us-central1 \
217
+ --allow-unauthenticated
218
+
219
+ # Get URL
220
+ gcloud run services describe crypto-aggregator --region us-central1 --format 'value(status.url)'
221
+ ```
222
+
223
+ ### Option 8: DigitalOcean App Platform
224
+
225
+ 1. Go to https://cloud.digitalocean.com/apps
226
+ 2. Click "Create App"
227
+ 3. Connect your GitHub repository
228
+ 4. Configure:
229
+ - **Run Command**: `python app.py`
230
+ - **Environment**: Python 3.11
231
+ - **HTTP Port**: 7860
232
+ 5. Click "Deploy"
233
+
234
+ ---
235
+
236
+ ## Environment Variables (Optional)
237
+
238
+ You can configure the following environment variables:
239
+
240
+ ```bash
241
+ # Port (default: 7860)
242
+ export PORT=8000
243
+
244
+ # Log level (default: INFO)
245
+ export LOG_LEVEL=DEBUG
246
+
247
+ # Database path (default: history.db)
248
+ export DATABASE_PATH=/path/to/history.db
249
+ ```
250
+
251
+ ---
252
+
253
+ ## Post-Deployment Testing
254
+
255
+ ### 1. Test Health Endpoint
256
+
257
+ ```bash
258
+ curl https://your-deployment-url.com/health
259
+ ```
260
+
261
+ Expected response:
262
+ ```json
263
+ {
264
+ "status": "healthy",
265
+ "timestamp": "2025-11-10T...",
266
+ "resources_loaded": true,
267
+ "database_connected": true
268
+ }
269
+ ```
270
+
271
+ ### 2. Test Resource Listing
272
+
273
+ ```bash
274
+ curl https://your-deployment-url.com/resources
275
+ ```
276
+
277
+ ### 3. Test Query Endpoint
278
+
279
+ ```bash
280
+ curl -X POST https://your-deployment-url.com/query \
281
+ -H "Content-Type: application/json" \
282
+ -d '{
283
+ "resource_type": "market_data",
284
+ "resource_name": "coingecko",
285
+ "endpoint": "/simple/price",
286
+ "params": {
287
+ "ids": "bitcoin",
288
+ "vs_currencies": "usd"
289
+ }
290
+ }'
291
+ ```
292
+
293
+ ### 4. Test Status Monitoring
294
+
295
+ ```bash
296
+ curl https://your-deployment-url.com/status
297
+ ```
298
+
299
+ ### 5. Run Full Test Suite
300
+
301
+ From your local machine:
302
+
303
+ ```bash
304
+ # Update BASE_URL in test_aggregator.py
305
+ # Change: BASE_URL = "http://localhost:7860"
306
+ # To: BASE_URL = "https://your-deployment-url.com"
307
+
308
+ # Run tests
309
+ python test_aggregator.py
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Performance Optimization
315
+
316
+ ### 1. Enable Caching
317
+
318
+ Add Redis for caching (optional):
319
+
320
+ ```python
321
+ import redis
322
+ import json
323
+
324
+ # Connect to Redis
325
+ redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
326
+
327
+ # Cache resource data
328
+ def get_cached_data(key, ttl=300):
329
+ cached = redis_client.get(key)
330
+ if cached:
331
+ return json.loads(cached)
332
+ return None
333
+
334
+ def set_cached_data(key, data, ttl=300):
335
+ redis_client.setex(key, ttl, json.dumps(data))
336
+ ```
337
+
338
+ ### 2. Use Connection Pooling
339
+
340
+ Already implemented with `aiohttp.ClientSession`
341
+
342
+ ### 3. Add Rate Limiting
343
+
344
+ Install:
345
+ ```bash
346
+ pip install slowapi
347
+ ```
348
+
349
+ Add to `app.py`:
350
+ ```python
351
+ from slowapi import Limiter, _rate_limit_exceeded_handler
352
+ from slowapi.util import get_remote_address
353
+ from slowapi.errors import RateLimitExceeded
354
+
355
+ limiter = Limiter(key_func=get_remote_address)
356
+ app.state.limiter = limiter
357
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
358
+
359
+ @app.post("/query")
360
+ @limiter.limit("60/minute")
361
+ async def query_resource(request: Request, query: ResourceQuery):
362
+ # ... existing code
363
+ ```
364
+
365
+ ### 4. Add Monitoring
366
+
367
+ Use Sentry for error tracking:
368
+
369
+ ```bash
370
+ pip install sentry-sdk
371
+ ```
372
+
373
+ ```python
374
+ import sentry_sdk
375
+ from sentry_sdk.integrations.fastapi import FastApiIntegration
376
+
377
+ sentry_sdk.init(
378
+ dsn="your-sentry-dsn",
379
+ integrations=[FastApiIntegration()],
380
+ traces_sample_rate=1.0,
381
+ )
382
+ ```
383
+
384
+ ---
385
+
386
+ ## Security Best Practices
387
+
388
+ ### 1. API Key Management
389
+
390
+ Store API keys in environment variables:
391
+
392
+ ```python
393
+ import os
394
+
395
+ API_KEYS = {
396
+ 'etherscan': os.getenv('ETHERSCAN_API_KEY', 'default-key'),
397
+ 'coinmarketcap': os.getenv('CMC_API_KEY', 'default-key'),
398
+ }
399
+ ```
400
+
401
+ ### 2. Enable HTTPS
402
+
403
+ Most platforms (Hugging Face, Heroku, etc.) provide HTTPS by default.
404
+
405
+ For self-hosted, use Let's Encrypt:
406
+
407
+ ```bash
408
+ # Install Certbot
409
+ sudo apt install certbot python3-certbot-nginx
410
+
411
+ # Get certificate
412
+ sudo certbot --nginx -d your-domain.com
413
+ ```
414
+
415
+ ### 3. Add Authentication (Optional)
416
+
417
+ ```bash
418
+ pip install python-jose[cryptography] passlib[bcrypt]
419
+ ```
420
+
421
+ ```python
422
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
423
+ from fastapi import Security
424
+
425
+ security = HTTPBearer()
426
+
427
+ @app.post("/query")
428
+ async def query_resource(
429
+ query: ResourceQuery,
430
+ credentials: HTTPAuthorizationCredentials = Security(security)
431
+ ):
432
+ # Verify token
433
+ if credentials.credentials != "your-secret-token":
434
+ raise HTTPException(status_code=401, detail="Invalid token")
435
+ # ... existing code
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Monitoring & Maintenance
441
+
442
+ ### 1. Monitor Logs
443
+
444
+ Hugging Face Spaces:
445
+ - View logs in the Space settings → "Logs" tab
446
+
447
+ Docker:
448
+ ```bash
449
+ docker logs -f crypto-aggregator
450
+ ```
451
+
452
+ Systemd:
453
+ ```bash
454
+ journalctl -u crypto-aggregator -f
455
+ ```
456
+
457
+ ### 2. Database Maintenance
458
+
459
+ Backup database regularly:
460
+
461
+ ```bash
462
+ # Local backup
463
+ cp history.db history_backup_$(date +%Y%m%d).db
464
+
465
+ # Remote backup
466
+ scp user@server:/path/to/history.db ./backups/
467
+ ```
468
+
469
+ Clean old records:
470
+
471
+ ```sql
472
+ -- Remove records older than 30 days
473
+ DELETE FROM query_history WHERE timestamp < datetime('now', '-30 days');
474
+ DELETE FROM resource_status WHERE last_check < datetime('now', '-30 days');
475
+ ```
476
+
477
+ ### 3. Update Resources
478
+
479
+ To add new resources, update `all_apis_merged_2025.json` and redeploy.
480
+
481
+ ### 4. Health Checks
482
+
483
+ Set up automated health checks:
484
+
485
+ ```bash
486
+ # Cron job (every 5 minutes)
487
+ */5 * * * * curl https://your-deployment-url.com/health || echo "API is down!"
488
+ ```
489
+
490
+ Use UptimeRobot or similar service for monitoring.
491
+
492
+ ---
493
+
494
+ ## Troubleshooting
495
+
496
+ ### Issue: Server won't start
497
+
498
+ **Solution:**
499
+ ```bash
500
+ # Check if port 7860 is in use
501
+ lsof -i :7860
502
+
503
+ # Kill existing process
504
+ kill -9 $(lsof -t -i:7860)
505
+
506
+ # Or use a different port
507
+ PORT=8000 python app.py
508
+ ```
509
+
510
+ ### Issue: Database locked
511
+
512
+ **Solution:**
513
+ ```bash
514
+ # Stop all instances
515
+ pkill -f app.py
516
+
517
+ # Remove lock (if exists)
518
+ rm history.db-journal
519
+
520
+ # Restart
521
+ python app.py
522
+ ```
523
+
524
+ ### Issue: High memory usage
525
+
526
+ **Solution:**
527
+ - Add connection limits
528
+ - Implement request queuing
529
+ - Scale horizontally with multiple instances
530
+
531
+ ### Issue: API rate limits
532
+
533
+ **Solution:**
534
+ - Implement caching
535
+ - Add multiple API keys for rotation
536
+ - Use fallback resources
537
+
538
+ ---
539
+
540
+ ## Scaling
541
+
542
+ ### Horizontal Scaling
543
+
544
+ Use a load balancer with multiple instances:
545
+
546
+ ```yaml
547
+ # docker-compose-scaled.yml
548
+ version: '3.8'
549
+
550
+ services:
551
+ aggregator:
552
+ build: .
553
+ deploy:
554
+ replicas: 3
555
+ environment:
556
+ - WORKER_ID=${HOSTNAME}
557
+
558
+ nginx:
559
+ image: nginx:alpine
560
+ ports:
561
+ - "80:80"
562
+ volumes:
563
+ - ./nginx.conf:/etc/nginx/nginx.conf
564
+ depends_on:
565
+ - aggregator
566
+ ```
567
+
568
+ ### Vertical Scaling
569
+
570
+ Increase resources on your hosting platform:
571
+ - Hugging Face: Upgrade to paid tier
572
+ - AWS: Use larger EC2 instance
573
+ - Docker: Adjust container resources
574
+
575
+ ---
576
+
577
+ ## Support
578
+
579
+ For issues or questions:
580
+ 1. Check `/health` endpoint
581
+ 2. Review application logs
582
+ 3. Test individual resources with `/status`
583
+ 4. Verify database with SQLite browser
584
+
585
+ ---
586
+
587
+ ## Next Steps
588
+
589
+ After deployment:
590
+
591
+ 1. **Integrate with your main app** using the provided client examples
592
+ 2. **Set up monitoring** with health checks and alerts
593
+ 3. **Configure backups** for the history database
594
+ 4. **Add custom resources** by updating the JSON file
595
+ 5. **Implement caching** for frequently accessed data
596
+ 6. **Enable authentication** if needed for security
597
+
598
+ ---
599
+
600
+ **Congratulations! Your Crypto Resource Aggregator is now deployed and ready to use!** 🚀
Dockerfile CHANGED
@@ -1,41 +1,38 @@
1
- # Use Python 3.11 Slim base image
2
- FROM python:3.11-slim
3
 
4
- # Set environment variables
5
- ENV PYTHONUNBUFFERED=1 \
6
- PYTHONDONTWRITEBYTECODE=1 \
7
- PIP_NO_CACHE_DIR=1 \
8
- PIP_DISABLE_PIP_VERSION_CHECK=1 \
9
- ENABLE_AUTO_DISCOVERY=false
10
 
11
  # Install system dependencies
12
- RUN apt-get update && apt-get install -y \
13
- gcc \
14
- g++ \
15
  curl \
 
 
16
  && rm -rf /var/lib/apt/lists/*
17
 
18
- # Set working directory
19
- WORKDIR /app
20
 
21
- # Copy dependency files
22
  COPY requirements.txt .
23
 
24
  # Install Python dependencies
25
- RUN pip install --no-cache-dir -r requirements.txt
 
26
 
27
  # Copy application code
28
  COPY . .
29
 
30
- # Create necessary directories
31
- RUN mkdir -p logs data data/exports data/backups
32
 
33
- # Expose ports (Hugging Face uses PORT env variable, default 7860)
34
- EXPOSE 7860 8000
 
35
 
36
- # Health check (simplified to avoid requests dependency in healthcheck)
37
- HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
38
- CMD curl -f http://localhost:${PORT:-8000}/health || exit 1
39
 
40
- # Run server with uvicorn (supports Hugging Face PORT env variable)
41
- CMD ["sh", "-c", "uvicorn api_server_extended:app --host 0.0.0.0 --port ${PORT:-8000}"]
 
1
+ FROM python:3.9-slim
 
2
 
3
+ WORKDIR /code
 
 
 
 
 
4
 
5
  # Install system dependencies
6
+ RUN apt-get update && \
7
+ apt-get install -y --no-install-recommends \
8
+ build-essential \
9
  curl \
10
+ git \
11
+ && apt-get clean \
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
+ # Create necessary directories
15
+ RUN mkdir -p /code/data /code/logs
16
 
17
+ # Copy requirements first to leverage Docker cache
18
  COPY requirements.txt .
19
 
20
  # Install Python dependencies
21
+ RUN pip install --no-cache-dir --upgrade pip && \
22
+ pip install --no-cache-dir -r requirements.txt
23
 
24
  # Copy application code
25
  COPY . .
26
 
27
+ # Ensure directories have correct permissions
28
+ RUN chmod -R 755 /code/data /code/logs
29
 
30
+ # Set environment variables
31
+ ENV PYTHONPATH=/code
32
+ ENV PORT=7860
33
 
34
+ # Expose the port
35
+ EXPOSE 7860
 
36
 
37
+ # Run the application
38
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
HUGGINGFACE_DEPLOYMENT.md CHANGED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🤗 HuggingFace Spaces Deployment Guide
2
+
3
+ This guide explains how to deploy the Crypto API Monitoring System to HuggingFace Spaces.
4
+
5
+ ## Overview
6
+
7
+ The application is fully optimized for HuggingFace Spaces deployment with:
8
+ - **Docker-based deployment** using the standard HF Spaces port (7860)
9
+ - **Automatic environment detection** for frontend API calls
10
+ - **HuggingFace ML integration** for crypto sentiment analysis
11
+ - **WebSocket support** for real-time data streaming
12
+ - **Persistent data storage** with SQLite
13
+
14
+ ## Prerequisites
15
+
16
+ 1. A HuggingFace account ([sign up here](https://huggingface.co/join))
17
+ 2. Git installed on your local machine
18
+ 3. Basic familiarity with Docker and HuggingFace Spaces
19
+
20
+ ## Deployment Steps
21
+
22
+ ### 1. Create a New Space
23
+
24
+ 1. Go to [HuggingFace Spaces](https://huggingface.co/spaces)
25
+ 2. Click "Create new Space"
26
+ 3. Configure your Space:
27
+ - **Name**: `Datasourceforcryptocurrency` (or your preferred name)
28
+ - **License**: Choose appropriate license (e.g., MIT)
29
+ - **SDK**: Select **Docker**
30
+ - **Visibility**: Public or Private (your choice)
31
+ 4. Click "Create Space"
32
+
33
+ ### 2. Clone Your Space Repository
34
+
35
+ ```bash
36
+ # Clone your newly created space
37
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
38
+ cd YOUR_SPACE_NAME
39
+ ```
40
+
41
+ ### 3. Copy Application Files
42
+
43
+ Copy all files from this repository to your Space directory:
44
+
45
+ ```bash
46
+ # Copy all files (adjust paths as needed)
47
+ cp -r /path/to/crypto-dt-source/* .
48
+ ```
49
+
50
+ **Essential files for HuggingFace Spaces:**
51
+ - `Dockerfile` - Docker configuration optimized for HF Spaces
52
+ - `requirements.txt` - Python dependencies including transformers
53
+ - `app.py` - Main FastAPI application
54
+ - `config.js` - Frontend configuration with environment detection
55
+ - `*.html` - UI files (index.html, hf_console.html, etc.)
56
+ - All backend directories (`api/`, `backend/`, `monitoring/`, etc.)
57
+
58
+ ### 4. Configure Environment Variables (Optional but Recommended)
59
+
60
+ In your HuggingFace Space settings, add these secrets:
61
+
62
+ **Required:**
63
+ - `HUGGINGFACE_TOKEN` - Your HF token for accessing models (optional if using public models)
64
+
65
+ **Optional API Keys (for enhanced data collection):**
66
+ - `ETHERSCAN_KEY_1` - Etherscan API key
67
+ - `COINMARKETCAP_KEY_1` - CoinMarketCap API key
68
+ - `NEWSAPI_KEY` - NewsAPI key
69
+ - `CRYPTOCOMPARE_KEY` - CryptoCompare API key
70
+
71
+ **HuggingFace Configuration:**
72
+ - `ENABLE_SENTIMENT=true` - Enable sentiment analysis
73
+ - `SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert` - Social sentiment model
74
+ - `SENTIMENT_NEWS_MODEL=kk08/CryptoBERT` - News sentiment model
75
+ - `HF_REGISTRY_REFRESH_SEC=21600` - Registry refresh interval (6 hours)
76
+
77
+ ### 5. Push to HuggingFace
78
+
79
+ ```bash
80
+ # Add all files
81
+ git add .
82
+
83
+ # Commit changes
84
+ git commit -m "Initial deployment of Crypto API Monitor"
85
+
86
+ # Push to HuggingFace
87
+ git push
88
+ ```
89
+
90
+ ### 6. Wait for Build
91
+
92
+ HuggingFace Spaces will automatically:
93
+ 1. Build your Docker image (takes 5-10 minutes)
94
+ 2. Download required ML models
95
+ 3. Start the application on port 7860
96
+ 4. Run health checks
97
+
98
+ Monitor the build logs in your Space's "Logs" tab.
99
+
100
+ ### 7. Access Your Application
101
+
102
+ Once deployed, your application will be available at:
103
+ ```
104
+ https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
105
+ ```
106
+
107
+ ## Features Available in HuggingFace Spaces
108
+
109
+ ### 🎯 Real-Time Dashboard
110
+ - Access the main dashboard at the root URL
111
+ - Real-time WebSocket updates for all metrics
112
+ - Provider health monitoring
113
+ - System status and analytics
114
+
115
+ ### 🤗 HuggingFace Console
116
+ - Access at `/hf_console.html`
117
+ - Test HF model registry
118
+ - Run sentiment analysis
119
+ - Search crypto-related models and datasets
120
+
121
+ ### 📊 API Documentation
122
+ - Swagger UI: `/docs`
123
+ - ReDoc: `/redoc`
124
+ - API Info: `/api-info`
125
+
126
+ ### 🔌 WebSocket Endpoints
127
+ All WebSocket endpoints are available for real-time data:
128
+ - `/ws` - Master WebSocket endpoint
129
+ - `/ws/market_data` - Market data updates
130
+ - `/ws/news` - News updates
131
+ - `/ws/sentiment` - Sentiment analysis updates
132
+ - `/ws/health` - Health monitoring
133
+ - `/ws/huggingface` - HF integration updates
134
+
135
+ ## Local Development & Testing
136
+
137
+ ### Using Docker Compose
138
+
139
+ ```bash
140
+ # Build and start the application
141
+ docker-compose up --build
142
+
143
+ # Access at http://localhost:7860
144
+ ```
145
+
146
+ ### Using Docker Directly
147
+
148
+ ```bash
149
+ # Build the image
150
+ docker build -t crypto-api-monitor .
151
+
152
+ # Run the container
153
+ docker run -p 7860:7860 \
154
+ -e HUGGINGFACE_TOKEN=your_token \
155
+ -e ENABLE_SENTIMENT=true \
156
+ -v $(pwd)/data:/app/data \
157
+ crypto-api-monitor
158
+ ```
159
+
160
+ ### Using Python Directly
161
+
162
+ ```bash
163
+ # Install dependencies
164
+ pip install -r requirements.txt
165
+
166
+ # Set environment variables
167
+ export ENABLE_SENTIMENT=true
168
+ export HUGGINGFACE_TOKEN=your_token
169
+
170
+ # Run the application
171
+ python app.py
172
+ ```
173
+
174
+ ## Configuration
175
+
176
+ ### Frontend Configuration (`config.js`)
177
+
178
+ The frontend automatically detects the environment:
179
+ - **HuggingFace Spaces**: Uses relative URLs with Space origin
180
+ - **Localhost**: Uses `http://localhost:7860`
181
+ - **Custom Deployment**: Uses current window origin
182
+
183
+ No manual configuration needed!
184
+
185
+ ### Backend Configuration
186
+
187
+ Edit `.env` or set environment variables:
188
+
189
+ ```bash
190
+ # HuggingFace
191
+ HUGGINGFACE_TOKEN=your_token_here
192
+ ENABLE_SENTIMENT=true
193
+ SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
194
+ SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
195
+ HF_REGISTRY_REFRESH_SEC=21600
196
+ HF_HTTP_TIMEOUT=8.0
197
+
198
+ # API Keys (optional)
199
+ ETHERSCAN_KEY_1=your_key
200
+ COINMARKETCAP_KEY_1=your_key
201
+ NEWSAPI_KEY=your_key
202
+ ```
203
+
204
+ ## Architecture
205
+
206
+ ```
207
+ ┌─────────────────────────────────────────────────┐
208
+ │ HuggingFace Spaces (Docker) │
209
+ ├─────────────────────────────────────────────────┤
210
+ │ │
211
+ │ Frontend (HTML/JS) │
212
+ │ ├── config.js (auto-detects environment) │
213
+ │ ├── index.html (main dashboard) │
214
+ │ └── hf_console.html (HF integration UI) │
215
+ │ │
216
+ │ Backend (FastAPI) │
217
+ │ ├── app.py (main application) │
218
+ │ ├── WebSocket Manager (real-time updates) │
219
+ │ ├── HF Integration (sentiment analysis) │
220
+ │ ├── Data Collectors (200+ APIs) │
221
+ │ └── SQLite Database (persistent storage) │
222
+ │ │
223
+ │ ML Models (HuggingFace Transformers) │
224
+ │ ├── ElKulako/cryptobert │
225
+ │ └── kk08/CryptoBERT │
226
+ │ │
227
+ └─────────────────────────────────────────────────┘
228
+ ```
229
+
230
+ ## Troubleshooting
231
+
232
+ ### Build Fails
233
+
234
+ 1. Check Docker logs in HF Spaces
235
+ 2. Verify `requirements.txt` has all dependencies
236
+ 3. Ensure Dockerfile uses Python 3.10
237
+ 4. Check for syntax errors in Python files
238
+
239
+ ### Application Won't Start
240
+
241
+ 1. Check health endpoint: `https://your-space-url/health`
242
+ 2. Review application logs in HF Spaces
243
+ 3. Verify port 7860 is exposed in Dockerfile
244
+ 4. Check environment variables are set correctly
245
+
246
+ ### WebSocket Connections Fail
247
+
248
+ 1. Ensure your Space URL uses HTTPS
249
+ 2. WebSockets automatically upgrade to WSS on HTTPS
250
+ 3. Check browser console for connection errors
251
+ 4. Verify CORS settings in `app.py`
252
+
253
+ ### Sentiment Analysis Not Working
254
+
255
+ 1. Set `HUGGINGFACE_TOKEN` in Space secrets
256
+ 2. Verify models are accessible: `ElKulako/cryptobert`, `kk08/CryptoBERT`
257
+ 3. Check HF console at `/hf_console.html`
258
+ 4. Review logs for model download errors
259
+
260
+ ### Performance Issues
261
+
262
+ 1. Increase Space hardware tier (if available)
263
+ 2. Reduce number of concurrent API monitors
264
+ 3. Adjust `HF_REGISTRY_REFRESH_SEC` to longer interval
265
+ 4. Consider disabling sentiment analysis if not needed
266
+
267
+ ## Resource Requirements
268
+
269
+ **Minimum (Free Tier):**
270
+ - 2 CPU cores
271
+ - 2GB RAM
272
+ - 1GB disk space
273
+
274
+ **Recommended:**
275
+ - 4 CPU cores
276
+ - 4GB RAM
277
+ - 2GB disk space
278
+ - For better ML model performance
279
+
280
+ ## Updating Your Space
281
+
282
+ ```bash
283
+ # Pull latest changes
284
+ git pull
285
+
286
+ # Make your modifications
287
+ # ...
288
+
289
+ # Commit and push
290
+ git add .
291
+ git commit -m "Update: description of changes"
292
+ git push
293
+ ```
294
+
295
+ HuggingFace will automatically rebuild and redeploy.
296
+
297
+ ## Security Best Practices
298
+
299
+ 1. **Use HF Secrets** for sensitive data (API keys, tokens)
300
+ 2. **Don't commit** `.env` files with actual keys
301
+ 3. **Review API keys** permissions (read-only when possible)
302
+ 4. **Monitor usage** of external APIs to avoid rate limits
303
+ 5. **Keep dependencies updated** for security patches
304
+
305
+ ## Advanced Configuration
306
+
307
+ ### Custom ML Models
308
+
309
+ To use custom sentiment analysis models:
310
+
311
+ ```bash
312
+ # Set environment variables in HF Spaces
313
+ SENTIMENT_SOCIAL_MODEL=your-username/your-model
314
+ SENTIMENT_NEWS_MODEL=your-username/another-model
315
+ ```
316
+
317
+ ### Custom Port (Not Recommended for HF Spaces)
318
+
319
+ HuggingFace Spaces requires port 7860. Don't change unless deploying elsewhere.
320
+
321
+ ### Multiple Workers
322
+
323
+ Edit Dockerfile CMD:
324
+ ```dockerfile
325
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
326
+ ```
327
+
328
+ **Note**: More workers = more memory usage. Adjust based on Space tier.
329
+
330
+ ## Support & Resources
331
+
332
+ - **HuggingFace Docs**: https://huggingface.co/docs/hub/spaces
333
+ - **FastAPI Docs**: https://fastapi.tiangolo.com/
334
+ - **Transformers Docs**: https://huggingface.co/docs/transformers/
335
+ - **Project Issues**: https://github.com/nimazasinich/crypto-dt-source/issues
336
+
337
+ ## License
338
+
339
+ [Specify your license here]
340
+
341
+ ## Contributing
342
+
343
+ Contributions are welcome! Please read the contributing guidelines before submitting PRs.
344
+
345
+ ---
346
+
347
+ **Need help?** Open an issue or contact the maintainers.
348
+
349
+ **Enjoy your crypto monitoring dashboard on HuggingFace Spaces! 🚀**
QUICK_START.md CHANGED
@@ -1,221 +1,182 @@
1
- # 🚀 راهنمای سریع شروع - Quick Start Guide
2
-
3
- ## نصب و راه‌اندازی سریع
4
-
5
- ### 1️⃣ نصب وابستگی‌ها
6
- ```bash
7
- pip install -r requirements.txt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ```
9
 
10
- ### 2️⃣ Import منابع از فایل‌های JSON
11
- ```bash
12
- python import_resources.py
13
  ```
14
- این اسکریپت به‌طور خودکار همه منابع را از فایل‌های JSON موجود import می‌کند.
15
-
16
- ### 3️⃣ راه‌اندازی سرور
17
- ```bash
18
- # روش 1: استفاده از اسکریپت راه‌انداز
19
- python start_server.py
20
 
21
- # روش 2: مستقیم
22
- python api_server_extended.py
23
-
24
- # روش 3: با uvicorn
25
- uvicorn api_server_extended:app --reload --host 0.0.0.0 --port 8000
26
  ```
27
 
28
- ### 4️⃣ دسترسی به داشبورد
29
- ```
30
- http://localhost:8000
 
31
  ```
32
 
33
- ## 📋 تب‌های داشبورد
34
-
35
- ### 📊 Market
36
- - آمار کلی بازار
37
- - لیست کریپتوکارنسی‌ها
38
- - نمودارها و ترندینگ
39
-
40
- ### 📡 API Monitor
41
- - وضعیت همه ارائه‌دهندگان
42
- - زمان پاسخ
43
- - Health Check
44
-
45
- ### ⚡ Advanced
46
- - Export JSON/CSV
47
- - Backup
48
- - Clear Cache
49
- - Activity Logs
50
-
51
- ### ⚙️ Admin
52
- - افزودن API جدید
53
- - تنظیمات
54
- - آمار کلی
55
-
56
- ### 🤗 HuggingFace
57
- - مدل‌های Sentiment Analysis
58
- - Datasets
59
- - جستجو در Registry
60
-
61
- ### 🔄 Pools
62
- - مدیریت Pool‌ها
63
- - افزودن/حذف اعضا
64
- - چرخش دستی
65
-
66
- ### 📋 Logs (جدید!)
67
- - نمایش لاگ‌ها با فیلتر
68
- - Export به JSON/CSV
69
- - جستجو و آمار
70
-
71
- ### 📦 Resources (جدید!)
72
- - مدیریت منابع API
73
- - Import/Export
74
- - Backup
75
- - فیلتر بر اساس Category
76
-
77
- ## 🔧 استفاده از API
78
-
79
- ### دریافت لاگ‌ها
80
- ```bash
81
- # همه لاگ‌ها
82
- curl http://localhost:8000/api/logs
83
-
84
- # فیلتر بر اساس Level
85
- curl http://localhost:8000/api/logs?level=error
86
-
87
- # جستجو
88
- curl http://localhost:8000/api/logs?search=timeout
89
- ```
90
-
91
- ### Export لاگ‌ها
92
- ```bash
93
- # Export به JSON
94
- curl http://localhost:8000/api/logs/export/json?level=error
95
-
96
- # Export به CSV
97
- curl http://localhost:8000/api/logs/export/csv
98
- ```
99
-
100
- ### مدیریت منابع
101
- ```bash
102
- # دریافت همه منابع
103
- curl http://localhost:8000/api/resources
104
-
105
- # Export منابع
106
- curl http://localhost:8000/api/resources/export/json
107
 
108
- # Backup
109
- curl -X POST http://localhost:8000/api/resources/backup
 
110
 
111
- # Import
112
- curl -X POST "http://localhost:8000/api/resources/import/json?file_path=api-resources/crypto_resources_unified_2025-11-11.json&merge=true"
113
- ```
114
-
115
- ## 📝 مثال‌های استفاده
 
116
 
117
- ### افزودن Provider جدید
118
- ```python
119
- from resource_manager import ResourceManager
 
120
 
121
- manager = ResourceManager()
122
 
123
- provider = {
124
- "id": "my_new_api",
125
- "name": "My New API",
126
- "category": "market_data",
127
- "base_url": "https://api.example.com",
128
- "requires_auth": False,
129
- "priority": 5,
130
- "weight": 50,
131
- "free": True
132
- }
133
 
134
- manager.add_provider(provider)
135
- manager.save_resources()
136
- ```
137
 
138
- ### ثبت لاگ
139
- ```python
140
- from log_manager import log_info, log_error, LogCategory
141
 
142
- # لاگ Info
143
- log_info(LogCategory.PROVIDER, "Provider health check completed",
144
- provider_id="coingecko", response_time=234.5)
145
 
146
- # لاگ Error
147
- log_error(LogCategory.PROVIDER, "Provider failed",
148
- provider_id="etherscan", error="Timeout")
149
- ```
150
 
151
- ### استفاده از Provider Manager
152
- ```python
153
- from provider_manager import ProviderManager
154
- import asyncio
155
-
156
- async def main():
157
- manager = ProviderManager()
158
-
159
- # Health Check
160
- await manager.health_check_all()
161
-
162
- # دریافت Provider از Pool
163
- provider = manager.get_next_from_pool("primary_market_data_pool")
164
- if provider:
165
- print(f"Selected: {provider.name}")
166
-
167
- await manager.close_session()
168
-
169
- asyncio.run(main())
170
  ```
171
 
172
- ## 🐳 استفاده با Docker
173
-
174
- ```bash
175
- # Build
176
- docker build -t crypto-monitor .
177
 
178
- # Run
179
- docker run -p 8000:8000 crypto-monitor
180
 
181
- # یا با docker-compose
182
- docker-compose up -d
183
- ```
184
 
185
- ## 🔍 عیب‌یابی
186
 
187
- ### مشکل: Port در حال استفاده است
188
- ```bash
189
- # تغییر پورت
190
- uvicorn api_server_extended:app --port 8001
191
  ```
192
 
193
- ### مشکل: فایل‌های JSON یافت نشد
194
- ```bash
195
- # بررسی وجود فایل‌ها
196
- ls -la api-resources/
197
- ls -la providers_config*.json
198
- ```
199
-
200
- ### مشکل: Import منابع ناموفق
201
- ```bash
202
- # بررسی ساختار JSON
203
- python -m json.tool api-resources/crypto_resources_unified_2025-11-11.json | head -20
204
- ```
205
-
206
- ## 📚 مستندات بیشتر
207
-
208
- - [README.md](README.md) - مستندات کامل انگلیسی
209
- - [README_FA.md](README_FA.md) - مستندات کامل فارسی
210
- - [api-resources/README.md](api-resources/README.md) - راهنمای منابع API
211
 
212
- ## 🆘 پشتیبانی
 
 
 
 
213
 
214
- در صورت بروز مشکل:
215
- 1. لاگ‌ها را بررسی کنید: `logs/app.log`
216
- 2. از تب Logs در داشبورد استفاده کنید
217
- 3. آمار سیستم را بررسی کنید: `/api/status`
218
 
219
- ---
220
 
221
- **موفق باشید! 🚀**
 
 
1
+ # 🚀 Quick Start Guide - Crypto API Monitor with HuggingFace Integration
2
+
3
+ ## Server is Running!
4
+
5
+ Your application is now live at: **http://localhost:7860**
6
+
7
+ ## 📱 Access Points
8
+
9
+ ### 1. Main Dashboard (Full Features)
10
+ **URL:** http://localhost:7860/index.html
11
+
12
+ Features:
13
+ - Real-time API monitoring
14
+ - Provider inventory
15
+ - Rate limit tracking
16
+ - Connection logs
17
+ - Schedule management
18
+ - Data freshness monitoring
19
+ - Failure analysis
20
+ - **🤗 HuggingFace Tab** (NEW!)
21
+
22
+ ### 2. HuggingFace Console (Standalone)
23
+ **URL:** http://localhost:7860/hf_console.html
24
+
25
+ Features:
26
+ - HF Health Status
27
+ - Models Registry Browser
28
+ - Datasets Registry Browser
29
+ - Local Search (snapshot)
30
+ - Sentiment Analysis (local pipeline)
31
+
32
+ ### 3. API Documentation
33
+ **URL:** http://localhost:7860/docs
34
+
35
+ Interactive API documentation with all endpoints
36
+
37
+ ## 🤗 HuggingFace Features
38
+
39
+ ### Available Endpoints:
40
+
41
+ 1. **Health Check**
42
+ ```
43
+ GET /api/hf/health
44
+ ```
45
+ Returns: Registry health, last refresh time, model/dataset counts
46
+
47
+ 2. **Force Refresh Registry**
48
+ ```
49
+ POST /api/hf/refresh
50
+ ```
51
+ Manually trigger registry update from HuggingFace Hub
52
+
53
+ 3. **Get Models Registry**
54
+ ```
55
+ GET /api/hf/registry?kind=models
56
+ ```
57
+ Returns: List of all cached crypto-related models
58
+
59
+ 4. **Get Datasets Registry**
60
+ ```
61
+ GET /api/hf/registry?kind=datasets
62
+ ```
63
+ Returns: List of all cached crypto-related datasets
64
+
65
+ 5. **Search Registry**
66
+ ```
67
+ GET /api/hf/search?q=crypto&kind=models
68
+ ```
69
+ Search local snapshot for models or datasets
70
+
71
+ 6. **Run Sentiment Analysis**
72
+ ```
73
+ POST /api/hf/run-sentiment
74
+ Body: {"texts": ["BTC strong", "ETH weak"]}
75
+ ```
76
+ Analyze crypto sentiment using local transformers
77
+
78
+ ## 🎯 How to Use
79
+
80
+ ### Option 1: Main Dashboard
81
+ 1. Open http://localhost:7860/index.html in your browser
82
+ 2. Click on the **"🤗 HuggingFace"** tab at the top
83
+ 3. Explore:
84
+ - Health status
85
+ - Models and datasets registries
86
+ - Search functionality
87
+ - Sentiment analysis
88
+
89
+ ### Option 2: Standalone HF Console
90
+ 1. Open http://localhost:7860/hf_console.html
91
+ 2. All HF features in a clean, focused interface
92
+ 3. Perfect for testing and development
93
+
94
+ ## 🧪 Test the Integration
95
+
96
+ ### Test 1: Check Health
97
+ ```powershell
98
+ Invoke-WebRequest -Uri "http://localhost:7860/api/hf/health" -UseBasicParsing | Select-Object -ExpandProperty Content
99
  ```
100
 
101
+ ### Test 2: Refresh Registry
102
+ ```powershell
103
+ Invoke-WebRequest -Uri "http://localhost:7860/api/hf/refresh" -Method POST -UseBasicParsing | Select-Object -ExpandProperty Content
104
  ```
 
 
 
 
 
 
105
 
106
+ ### Test 3: Get Models
107
+ ```powershell
108
+ Invoke-WebRequest -Uri "http://localhost:7860/api/hf/registry?kind=models" -UseBasicParsing | Select-Object -ExpandProperty Content
 
 
109
  ```
110
 
111
+ ### Test 4: Run Sentiment Analysis
112
+ ```powershell
113
+ $body = @{texts = @("BTC strong breakout", "ETH looks weak")} | ConvertTo-Json
114
+ Invoke-WebRequest -Uri "http://localhost:7860/api/hf/run-sentiment" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing | Select-Object -ExpandProperty Content
115
  ```
116
 
117
+ ## 📊 What's Included
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ ### Seed Models (Always Available):
120
+ - ElKulako/cryptobert
121
+ - kk08/CryptoBERT
122
 
123
+ ### Seed Datasets (Always Available):
124
+ - linxy/CryptoCoin
125
+ - WinkingFace/CryptoLM-Bitcoin-BTC-USDT
126
+ - WinkingFace/CryptoLM-Ethereum-ETH-USDT
127
+ - WinkingFace/CryptoLM-Solana-SOL-USDT
128
+ - WinkingFace/CryptoLM-Ripple-XRP-USDT
129
 
130
+ ### Auto-Discovery:
131
+ - Searches HuggingFace Hub for crypto-related models
132
+ - Searches for sentiment-analysis models
133
+ - Auto-refreshes every 6 hours (configurable)
134
 
135
+ ## ⚙️ Configuration
136
 
137
+ Edit `.env` file to customize:
 
 
 
 
 
 
 
 
 
138
 
139
+ ```env
140
+ # HuggingFace Token (optional, for higher rate limits)
141
+ HUGGINGFACE_TOKEN=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
142
 
143
+ # Enable/disable local sentiment analysis
144
+ ENABLE_SENTIMENT=true
 
145
 
146
+ # Model selection
147
+ SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
148
+ SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
149
 
150
+ # Refresh interval (seconds)
151
+ HF_REGISTRY_REFRESH_SEC=21600
 
 
152
 
153
+ # HTTP timeout (seconds)
154
+ HF_HTTP_TIMEOUT=8.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  ```
156
 
157
+ ## 🛑 Stop the Server
 
 
 
 
158
 
159
+ Press `CTRL+C` in the terminal where the server is running
 
160
 
161
+ Or use the process manager to stop process ID 6
 
 
162
 
163
+ ## 🔄 Restart the Server
164
 
165
+ ```powershell
166
+ python simple_server.py
 
 
167
  ```
168
 
169
+ ## 📝 Notes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
+ - **First Load**: The first sentiment analysis may take 30-60 seconds as models download
172
+ - **Registry**: Auto-refreshes every 6 hours, or manually via the UI
173
+ - **Free Resources**: All endpoints use free HuggingFace APIs
174
+ - **No API Key Required**: Works without authentication (with rate limits)
175
+ - **Local Inference**: Sentiment analysis runs locally using transformers
176
 
177
+ ## 🎉 You're All Set!
 
 
 
178
 
179
+ The application is running and ready to use. Open your browser and explore!
180
 
181
+ **Main Dashboard:** http://localhost:7860/index.html
182
+ **HF Console:** http://localhost:7860/hf_console.html
README.md CHANGED
@@ -1,492 +1,29 @@
1
- ---
2
- sdk: gradio
3
- ---
4
- # Crypto-DT-Source
5
 
6
- <div align="center">
 
 
 
 
 
 
7
 
8
- **Production-Ready Cryptocurrency Data Aggregator**
 
9
 
10
- *Real-time data collection AI-powered analysis Enterprise-grade security*
11
 
12
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
13
- [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
14
- [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
15
 
16
- [Quick Start](#-quick-start) [Features](#-features) [Documentation](#-documentation) • [فارسی](docs/persian/README_FA.md)
17
 
18
- </div>
19
-
20
- ---
21
-
22
- ## 🚀 Quick Start
23
-
24
- Get up and running in 3 simple steps:
25
-
26
- ```bash
27
- # 1. Clone the repository
28
- git clone https://github.com/nimazasinich/crypto-dt-source.git
29
- cd crypto-dt-source
30
-
31
- # 2. Install dependencies
32
- pip install -r requirements.txt
33
-
34
- # 3. Run the application
35
- python app.py
36
- ```
37
-
38
- Open your browser to **http://localhost:7860** 🎉
39
-
40
- > **Need more help?** See the [complete Quick Start guide](QUICK_START.md) or [Installation Guide](docs/deployment/INSTALL.md)
41
-
42
- ---
43
-
44
- ## ✨ Features
45
-
46
- ### 🔥 Core Capabilities
47
-
48
- - **Real-Time Data** - Monitor 100+ cryptocurrencies with live price updates
49
- - **AI-Powered Analysis** - Sentiment analysis using HuggingFace transformers
50
- - **200+ Free Data Sources** - No API keys required for basic features
51
- - **Interactive Dashboards** - 6-tab Gradio interface + 10+ HTML dashboards
52
- - **WebSocket Streaming** - Real-time data streaming via WebSocket API
53
- - **REST API** - 20+ endpoints for programmatic access
54
- - **SQLite Database** - Persistent storage with automatic migrations
55
-
56
- ### 🆕 Production Features (Nov 2024)
57
-
58
- - ✅ **Authentication & Authorization** - JWT tokens + API key management
59
- - ✅ **Rate Limiting** - Multi-tier protection (30/min, 1000/hour)
60
- - ✅ **Async Architecture** - 5x faster data collection
61
- - ✅ **Database Migrations** - Version-controlled schema updates
62
- - ✅ **Testing Suite** - pytest with 60%+ coverage
63
- - ✅ **CI/CD Pipeline** - Automated testing & deployment
64
- - ✅ **Code Quality Tools** - black, flake8, mypy, pylint
65
- - ✅ **Security Scanning** - Automated vulnerability checks
66
-
67
- > **See what's new:** [Implementation Fixes](IMPLEMENTATION_FIXES.md) • [Fixes Summary](FIXES_SUMMARY.md)
68
-
69
- ---
70
-
71
- ## 📊 Data Sources
72
-
73
- ### Price & Market Data
74
- - **CoinGecko** - Top 100+ cryptocurrencies, market cap rankings
75
- - **CoinCap** - Real-time prices, backup data source
76
- - **Binance** - Trading volumes, OHLCV data
77
- - **Kraken** - Historical price data
78
- - **Messari** - Advanced analytics
79
-
80
- ### News & Sentiment
81
- - **RSS Feeds** - CoinDesk, Cointelegraph, Bitcoin Magazine, Decrypt
82
- - **CryptoPanic** - Aggregated crypto news
83
- - **Reddit** - r/cryptocurrency, r/bitcoin, r/ethtrader
84
- - **Alternative.me** - Fear & Greed Index
85
-
86
- ### Blockchain Data
87
- - **Etherscan** - Ethereum blockchain (optional key)
88
- - **BscScan** - Binance Smart Chain
89
- - **TronScan** - Tron blockchain
90
- - **Blockchair** - Multi-chain explorer
91
-
92
- **All basic features work without API keys!** 🎁
93
-
94
- ---
95
-
96
- ## 🏗️ Architecture
97
-
98
- ```
99
- crypto-dt-source/
100
- ├── 📱 UI Layer
101
- │ ├── app.py # Main Gradio dashboard
102
- │ ├── ui/ # Modular UI components (NEW)
103
- │ │ ├── dashboard_live.py # Live price dashboard
104
- │ │ ├── dashboard_charts.py # Historical charts
105
- │ │ ├── dashboard_news.py # News & sentiment
106
- │ │ └── ...
107
- │ └── *.html # 10+ HTML dashboards
108
-
109
- ├── 🔌 API Layer
110
- │ ├── api/
111
- │ │ ├── endpoints.py # 20+ REST endpoints
112
- │ │ ├── websocket.py # WebSocket streaming
113
- │ │ ├── data_endpoints.py # Data delivery
114
- │ │ └── pool_endpoints.py # Provider management
115
- │ └── api_server_extended.py # FastAPI server
116
-
117
- ├── 💾 Data Layer
118
- │ ├── database.py # SQLite manager
119
- │ ├── database/
120
- │ │ ├── db_manager.py # Connection pooling
121
- │ │ ├── migrations.py # Schema migrations (NEW)
122
- │ │ └── models.py # Data models
123
- │ └── collectors/
124
- │ ├���─ market_data.py # Price collection
125
- │ ├── news.py # News aggregation
126
- │ ├── sentiment.py # Sentiment analysis
127
- │ └── ...
128
-
129
- ├── 🤖 AI Layer
130
- │ ├── ai_models.py # HuggingFace integration
131
- │ └── crypto_data_bank/ai/ # Alternative AI engine
132
-
133
- ├── 🛠️ Utilities
134
- │ ├── utils.py # General utilities
135
- │ ├── utils/
136
- │ │ ├── async_api_client.py # Async HTTP client (NEW)
137
- │ │ ├── auth.py # Authentication (NEW)
138
- │ │ └── rate_limiter_enhanced.py # Rate limiting (NEW)
139
- │ └── monitoring/
140
- │ ├── health_monitor.py # Health checks
141
- │ └── scheduler.py # Background tasks
142
-
143
- ├── 🧪 Testing
144
- │ ├── tests/
145
- │ │ ├── test_database.py # Database tests (NEW)
146
- │ │ ├── test_async_api_client.py # Async tests (NEW)
147
- │ │ └── ...
148
- │ └── pytest.ini # Test configuration
149
-
150
- ├── ⚙️ Configuration
151
- │ ├── config.py # Application config
152
- │ ├── .env.example # Environment template
153
- │ ├── requirements.txt # Production deps
154
- │ ├── requirements-dev.txt # Dev dependencies (NEW)
155
- │ ├── pyproject.toml # Tool config (NEW)
156
- │ └── .flake8 # Linting config (NEW)
157
-
158
- └── 📚 Documentation
159
- ├── README.md # This file
160
- ├── CHANGELOG.md # Version history
161
- ├── QUICK_START.md # Quick start guide
162
- ├── IMPLEMENTATION_FIXES.md # Latest improvements (NEW)
163
- ├── FIXES_SUMMARY.md # Fixes summary (NEW)
164
- └── docs/ # Organized documentation (NEW)
165
- ├── INDEX.md # Documentation index
166
- ├── deployment/ # Deployment guides
167
- ├── components/ # Component docs
168
- ├── reports/ # Analysis reports
169
- ├── guides/ # How-to guides
170
- ├── persian/ # Persian/Farsi docs
171
- └── archive/ # Historical docs
172
- ```
173
-
174
- ---
175
-
176
- ## 🎯 Use Cases
177
-
178
- ### For Traders
179
- - Real-time price monitoring across 100+ coins
180
- - AI sentiment analysis from news and social media
181
- - Technical indicators (RSI, MACD, Moving Averages)
182
- - Fear & Greed Index tracking
183
-
184
- ### For Developers
185
- - REST API for building crypto applications
186
- - WebSocket streaming for real-time updates
187
- - 200+ free data sources aggregated
188
- - Well-documented, modular codebase
189
-
190
- ### For Researchers
191
- - Historical price data and analysis
192
- - Sentiment analysis on crypto news
193
- - Database of aggregated market data
194
- - Export data to CSV for analysis
195
-
196
- ### For DevOps
197
- - Docker containerization ready
198
- - HuggingFace Spaces deployment
199
- - Health monitoring endpoints
200
- - Automated testing and CI/CD
201
-
202
- ---
203
-
204
- ## 🔧 Installation & Setup
205
-
206
- ### Prerequisites
207
- - Python 3.8 or higher
208
- - 4GB+ RAM (for AI models)
209
- - Internet connection
210
-
211
- ### Basic Installation
212
-
213
- ```bash
214
- # Install dependencies
215
- pip install -r requirements.txt
216
-
217
- # Run application
218
- python app.py
219
- ```
220
-
221
- ### Development Setup
222
-
223
- ```bash
224
- # Install dev dependencies
225
- pip install -r requirements-dev.txt
226
-
227
- # Run tests
228
- pytest --cov=.
229
-
230
- # Format code
231
- black .
232
- isort .
233
-
234
- # Lint
235
- flake8 .
236
- mypy .
237
- ```
238
-
239
- ### Production Deployment
240
-
241
- ```bash
242
- # Set environment variables
243
- cp .env.example .env
244
- # Edit .env with your configuration
245
-
246
- # Run database migrations
247
- python -c "from database.migrations import auto_migrate; auto_migrate('data/database/crypto_aggregator.db')"
248
-
249
- # Enable authentication
250
- export ENABLE_AUTH=true
251
- export SECRET_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
252
-
253
- # Start application
254
- python app.py
255
- ```
256
-
257
- ### Docker Deployment
258
 
 
259
  ```bash
260
- # Build image
261
- docker build -t crypto-dt-source .
262
-
263
- # Run container
264
- docker run -p 7860:7860 -v $(pwd)/data:/app/data crypto-dt-source
265
-
266
- # Or use docker-compose
267
- docker-compose up -d
268
  ```
269
 
270
- > **Detailed guides:** [Deployment Guide](docs/deployment/DEPLOYMENT_GUIDE.md) • [Production Guide](docs/deployment/PRODUCTION_DEPLOYMENT_GUIDE.md) • [HuggingFace Spaces](docs/deployment/HUGGINGFACE_DEPLOYMENT.md)
271
-
272
- ---
273
-
274
- ## 📖 Documentation
275
-
276
- ### Getting Started
277
- - 📘 [Quick Start Guide](QUICK_START.md) - Get running in 3 steps
278
- - 📘 [Installation Guide](docs/deployment/INSTALL.md) - Detailed installation
279
- - 📘 [راهنمای فارسی](docs/persian/README_FA.md) - Persian/Farsi guide
280
-
281
- ### Core Documentation
282
- - 📗 [Implementation Fixes](IMPLEMENTATION_FIXES.md) - Latest production improvements
283
- - 📗 [Fixes Summary](FIXES_SUMMARY.md) - Quick reference
284
- - 📗 [Changelog](CHANGELOG.md) - Version history
285
-
286
- ### Component Documentation
287
- - 📙 [WebSocket API](docs/components/WEBSOCKET_API_DOCUMENTATION.md) - Real-time streaming
288
- - 📙 [Data Collectors](docs/components/COLLECTORS_README.md) - Data collection system
289
- - 📙 [Gradio Dashboard](docs/components/GRADIO_DASHBOARD_README.md) - UI documentation
290
- - 📙 [Backend Services](docs/components/README_BACKEND.md) - Backend architecture
291
-
292
- ### Deployment & DevOps
293
- - 📕 [Deployment Guide](docs/deployment/DEPLOYMENT_GUIDE.md) - General deployment
294
- - 📕 [Production Guide](docs/deployment/PRODUCTION_DEPLOYMENT_GUIDE.md) - Production setup
295
- - 📕 [HuggingFace Deployment](docs/deployment/HUGGINGFACE_DEPLOYMENT.md) - Cloud deployment
296
-
297
- ### Reports & Analysis
298
- - 📔 [Project Analysis](docs/reports/PROJECT_ANALYSIS_COMPLETE.md) - 40,600+ line analysis
299
- - 📔 [Production Audit](docs/reports/PRODUCTION_AUDIT_COMPREHENSIVE.md) - Security audit
300
- - 📔 [System Capabilities](docs/reports/SYSTEM_CAPABILITIES_REPORT.md) - Feature overview
301
-
302
- ### Complete Index
303
- 📚 **[Full Documentation Index](docs/INDEX.md)** - Browse all 60+ documentation files
304
-
305
- ---
306
-
307
- ## 🔐 Security & Authentication
308
-
309
- ### Authentication (Optional)
310
-
311
- Enable authentication for production deployments:
312
-
313
- ```bash
314
- # .env configuration
315
- ENABLE_AUTH=true
316
- SECRET_KEY=your-secret-key-here
317
- ADMIN_USERNAME=admin
318
- ADMIN_PASSWORD=secure-password
319
- ACCESS_TOKEN_EXPIRE_MINUTES=60
320
- API_KEYS=key1,key2,key3
321
- ```
322
-
323
- **Features:**
324
- - JWT token authentication
325
- - API key management
326
- - Password hashing (SHA-256)
327
- - Token expiration
328
- - Usage tracking
329
-
330
- > **Learn more:** [Authentication Guide](IMPLEMENTATION_FIXES.md#3-authentication--authorization-system)
331
-
332
- ### Rate Limiting
333
-
334
- Protect your API from abuse:
335
-
336
- - **30 requests/minute** per client
337
- - **1,000 requests/hour** per client
338
- - **Burst protection** up to 10 requests
339
-
340
- > **Learn more:** [Rate Limiting Guide](IMPLEMENTATION_FIXES.md#4-enhanced-rate-limiting-system)
341
-
342
- ---
343
-
344
- ## 🧪 Testing
345
-
346
- ```bash
347
- # Install test dependencies
348
- pip install -r requirements-dev.txt
349
-
350
- # Run all tests
351
- pytest
352
-
353
- # Run with coverage
354
- pytest --cov=. --cov-report=html
355
-
356
- # Run specific test file
357
- pytest tests/test_database.py -v
358
-
359
- # Run integration tests
360
- pytest tests/test_integration.py
361
- ```
362
-
363
- **Test Coverage:** 60%+ (target: 80%)
364
-
365
- > **Learn more:** [Testing Guide](IMPLEMENTATION_FIXES.md#6-comprehensive-testing-suite)
366
-
367
- ---
368
-
369
- ## 🚢 CI/CD Pipeline
370
-
371
- Automated testing on every push:
372
-
373
- - ✅ Code quality checks (black, flake8, mypy)
374
- - ✅ Tests on Python 3.8, 3.9, 3.10, 3.11
375
- - ✅ Security scanning (bandit, safety)
376
- - ✅ Docker build verification
377
- - ✅ Integration tests
378
- - ✅ Performance benchmarks
379
-
380
- > **See:** [.github/workflows/ci.yml](.github/workflows/ci.yml)
381
-
382
- ---
383
-
384
- ## 📊 Performance
385
-
386
- ### Optimizations Implemented
387
- - ⚡ **5x faster** data collection (async parallel requests)
388
- - ⚡ **3x faster** database queries (optimized indices)
389
- - ⚡ **10x reduced** API calls (TTL-based caching)
390
- - ⚡ **Better resource** utilization (async I/O)
391
-
392
- ### Benchmarks
393
- - Data collection: ~30 seconds for 100 coins
394
- - Database queries: <10ms average
395
- - WebSocket latency: <100ms
396
- - Memory usage: ~500MB (with AI models loaded)
397
-
398
- ---
399
-
400
- ## 🤝 Contributing
401
-
402
- We welcome contributions! Here's how:
403
-
404
- 1. **Fork** the repository
405
- 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`)
406
- 3. **Make** your changes with tests
407
- 4. **Run** quality checks (`black . && flake8 . && pytest`)
408
- 5. **Commit** with descriptive message
409
- 6. **Push** to your branch
410
- 7. **Open** a Pull Request
411
-
412
- **Guidelines:**
413
- - Follow code style (black, isort)
414
- - Add tests for new features
415
- - Update documentation
416
- - Check [Pull Request Checklist](docs/guides/PR_CHECKLIST.md)
417
-
418
- ---
419
-
420
- ## 📜 License
421
-
422
- This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
423
-
424
- ---
425
-
426
- ## 🙏 Acknowledgments
427
-
428
- ### AI Models
429
- - [HuggingFace](https://huggingface.co/) - Transformers library
430
- - [Cardiff NLP](https://huggingface.co/cardiffnlp) - Twitter sentiment model
431
- - [ProsusAI](https://huggingface.co/ProsusAI) - FinBERT model
432
- - [Facebook](https://huggingface.co/facebook) - BART summarization
433
-
434
- ### Data Sources
435
- - [CoinGecko](https://www.coingecko.com/) - Free crypto API
436
- - [CoinCap](https://coincap.io/) - Real-time data
437
- - [Binance](https://www.binance.com/) - Trading data
438
- - [Alternative.me](https://alternative.me/) - Fear & Greed Index
439
-
440
- ### Frameworks & Libraries
441
- - [Gradio](https://gradio.app/) - Web UI framework
442
- - [FastAPI](https://fastapi.tiangolo.com/) - REST API
443
- - [Plotly](https://plotly.com/) - Interactive charts
444
- - [PyTorch](https://pytorch.org/) - Deep learning
445
-
446
- ---
447
-
448
- ## 📞 Support
449
-
450
- - **Issues:** [GitHub Issues](https://github.com/nimazasinich/crypto-dt-source/issues)
451
- - **Documentation:** [docs/](docs/INDEX.md)
452
- - **Changelog:** [CHANGELOG.md](CHANGELOG.md)
453
-
454
- ---
455
-
456
- ## 🗺️ Roadmap
457
-
458
- ### Short-term (Q4 2024)
459
- - [x] Modular UI architecture
460
- - [x] Authentication system
461
- - [x] Rate limiting
462
- - [x] Database migrations
463
- - [x] Testing suite
464
- - [x] CI/CD pipeline
465
- - [ ] 80%+ test coverage
466
- - [ ] GraphQL API
467
-
468
- ### Medium-term (Q1 2025)
469
- - [ ] Microservices architecture
470
- - [ ] Message queue (Redis/RabbitMQ)
471
- - [ ] Database replication
472
- - [ ] Multi-tenancy support
473
- - [ ] Advanced ML models
474
-
475
- ### Long-term (2025)
476
- - [ ] Kubernetes deployment
477
- - [ ] Multi-region support
478
- - [ ] Premium data sources
479
- - [ ] Enterprise features
480
- - [ ] Mobile app
481
-
482
- ---
483
-
484
- <div align="center">
485
-
486
- **Made with ❤️ for the crypto community**
487
-
488
- ⭐ **Star us on GitHub** if you find this project useful!
489
-
490
- [Documentation](docs/INDEX.md) • [Quick Start](QUICK_START.md) • [فارسی](docs/persian/README_FA.md) • [Changelog](CHANGELOG.md)
491
-
492
- </div>
 
1
+ # 🚀 Crypto API Monitor Pro v2.0
 
 
 
2
 
3
+ ## ویژگی‌ها
4
+ ✅ 40+ Provider (Exchanges, Data, DeFi, NFT, Blockchain)
5
+ ✅ 20 Cryptocurrency با داده کامل
6
+ ✅ UI حرفه‌ای Dark Mode
7
+ ✅ Real-time WebSocket
8
+ ✅ نمودارهای تعاملی
9
+ ✅ آمار و تحلیل کامل
10
 
11
+ ## Providers:
12
+ **Exchanges:** Binance, Coinbase, Kraken, Huobi, KuCoin, Bitfinex, Bitstamp, Gemini, OKX, Bybit, Gate.io, Crypto.com, Bittrex, Poloniex, MEXC
13
 
14
+ **Data:** CoinGecko, CoinMarketCap, CryptoCompare, Messari, Glassnode, Santiment, Kaiko, Nomics
15
 
16
+ **DeFi:** Uniswap, SushiSwap, PancakeSwap, Curve, 1inch, Aave, Compound, MakerDAO
 
 
17
 
18
+ **NFT:** OpenSea, Blur, Magic Eden, Rarible
19
 
20
+ **Blockchain:** Etherscan, BscScan, Polygonscan, Blockchair, Blockchain.com
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ ## راه‌اندازی
23
  ```bash
24
+ 1. دابل کلیک start.bat
25
+ 2. برو http://localhost:8000/dashboard
 
 
 
 
 
 
26
  ```
27
 
28
+ ## نیاز
29
+ Python 3.8+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,1495 +1,358 @@
1
  #!/usr/bin/env python3
2
  """
3
- Crypto Data Aggregator - Complete Gradio Dashboard
4
- 6-tab comprehensive interface for cryptocurrency data analysis
5
  """
6
 
7
- import gradio as gr
8
- import pandas as pd
9
- import plotly.graph_objects as go
10
- from plotly.subplots import make_subplots
11
- from datetime import datetime, timedelta
 
12
  import json
13
- import threading
14
- import time
15
- import logging
16
- from typing import List, Dict, Optional, Tuple, Any
17
- import traceback
18
-
19
- # Import local modules
20
- import config
21
- from database import Database
22
- import collectors
23
- import ai_models
24
- import utils
25
-
26
- # Setup logging
27
- logger = utils.setup_logging()
28
-
29
- # Initialize database
30
- db = Database()
31
-
32
- # Global state for background collection
33
- _collection_started = False
34
- _collection_lock = threading.Lock()
35
-
36
- # ==================== TAB 1: LIVE DASHBOARD ====================
37
-
38
- def get_live_dashboard(search_filter: str = "") -> pd.DataFrame:
39
- """
40
- Get live dashboard data with top 100 cryptocurrencies
41
-
42
- Args:
43
- search_filter: Search/filter text for cryptocurrencies
44
-
45
- Returns:
46
- DataFrame with formatted cryptocurrency data
47
- """
48
- try:
49
- logger.info("Fetching live dashboard data...")
50
-
51
- # Get latest prices from database
52
- prices = db.get_latest_prices(100)
53
 
54
- if not prices:
55
- logger.warning("No price data available")
56
- return pd.DataFrame({
57
- "Rank": [],
58
- "Name": [],
59
- "Symbol": [],
60
- "Price (USD)": [],
61
- "24h Change (%)": [],
62
- "Volume": [],
63
- "Market Cap": []
64
- })
65
 
66
- # Convert to DataFrame
67
- df_data = []
68
- for price in prices:
69
- # Apply search filter if provided
70
- if search_filter:
71
- search_lower = search_filter.lower()
72
- name_lower = (price.get('name') or '').lower()
73
- symbol_lower = (price.get('symbol') or '').lower()
74
 
75
- if search_lower not in name_lower and search_lower not in symbol_lower:
76
- continue
 
77
 
78
- df_data.append({
79
- "Rank": price.get('rank', 999),
80
- "Name": price.get('name', 'Unknown'),
81
- "Symbol": price.get('symbol', 'N/A').upper(),
82
- "Price (USD)": f"${price.get('price_usd', 0):,.2f}" if price.get('price_usd') else "N/A",
83
- "24h Change (%)": f"{price.get('percent_change_24h', 0):+.2f}%" if price.get('percent_change_24h') is not None else "N/A",
84
- "Volume": utils.format_number(price.get('volume_24h', 0)),
85
- "Market Cap": utils.format_number(price.get('market_cap', 0))
86
- })
87
 
88
- df = pd.DataFrame(df_data)
 
89
 
90
- if df.empty:
91
- logger.warning("No data matches filter criteria")
92
- return pd.DataFrame({
93
- "Rank": [],
94
- "Name": [],
95
- "Symbol": [],
96
- "Price (USD)": [],
97
- "24h Change (%)": [],
98
- "Volume": [],
99
- "Market Cap": []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  })
101
-
102
- # Sort by rank
103
- df = df.sort_values('Rank')
104
-
105
- logger.info(f"Dashboard loaded with {len(df)} cryptocurrencies")
106
- return df
107
-
108
- except Exception as e:
109
- logger.error(f"Error in get_live_dashboard: {e}\n{traceback.format_exc()}")
110
- return pd.DataFrame({
111
- "Error": [f"Failed to load dashboard: {str(e)}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  })
113
-
114
-
115
- def refresh_price_data() -> Tuple[pd.DataFrame, str]:
116
- """
117
- Manually trigger price data collection and refresh dashboard
118
-
119
- Returns:
120
- Tuple of (DataFrame, status_message)
121
- """
122
- try:
123
- logger.info("Manual refresh triggered...")
124
-
125
- # Collect fresh price data
126
- success, count = collectors.collect_price_data()
127
-
128
- if success:
129
- message = f" Successfully refreshed! Collected {count} price records."
130
- else:
131
- message = f"⚠️ Refresh completed with warnings. Collected {count} records."
132
-
133
- # Return updated dashboard
134
- df = get_live_dashboard()
135
-
136
- return df, message
137
-
138
- except Exception as e:
139
- logger.error(f"Error in refresh_price_data: {e}")
140
- return get_live_dashboard(), f" Refresh failed: {str(e)}"
141
-
142
-
143
- # ==================== TAB 2: HISTORICAL CHARTS ====================
144
-
145
- def get_available_symbols() -> List[str]:
146
- """Get list of available cryptocurrency symbols from database"""
147
- try:
148
- prices = db.get_latest_prices(100)
149
- symbols = sorted(list(set([
150
- f"{p.get('name', 'Unknown')} ({p.get('symbol', 'N/A').upper()})"
151
- for p in prices if p.get('symbol')
152
- ])))
153
-
154
- if not symbols:
155
- return ["BTC", "ETH", "BNB"]
156
-
157
- return symbols
158
-
159
- except Exception as e:
160
- logger.error(f"Error getting symbols: {e}")
161
- return ["BTC", "ETH", "BNB"]
162
-
163
-
164
- def generate_chart(symbol_display: str, timeframe: str) -> go.Figure:
165
- """
166
- Generate interactive plotly chart with price history and technical indicators
167
-
168
- Args:
169
- symbol_display: Display name like "Bitcoin (BTC)"
170
- timeframe: Time period (1d, 7d, 30d, 90d, 1y, All)
171
-
172
- Returns:
173
- Plotly figure with price chart, volume, MA, and RSI
174
- """
175
- try:
176
- logger.info(f"Generating chart for {symbol_display} - {timeframe}")
177
-
178
- # Extract symbol from display name
179
- if '(' in symbol_display and ')' in symbol_display:
180
- symbol = symbol_display.split('(')[1].split(')')[0].strip().upper()
181
- else:
182
- symbol = symbol_display.strip().upper()
183
-
184
- # Determine hours to look back
185
- timeframe_hours = {
186
- "1d": 24,
187
- "7d": 24 * 7,
188
- "30d": 24 * 30,
189
- "90d": 24 * 90,
190
- "1y": 24 * 365,
191
- "All": 24 * 365 * 10 # 10 years
192
- }
193
- hours = timeframe_hours.get(timeframe, 168)
194
-
195
- # Get price history
196
- history = db.get_price_history(symbol, hours)
197
-
198
- if not history:
199
- # Try to find by name instead
200
- prices = db.get_latest_prices(100)
201
- matching = [p for p in prices if symbol.lower() in (p.get('name') or '').lower()]
202
-
203
- if matching:
204
- symbol = matching[0].get('symbol', symbol)
205
- history = db.get_price_history(symbol, hours)
206
-
207
- if not history or len(history) < 2:
208
- # Create empty chart with message
209
- fig = go.Figure()
210
- fig.add_annotation(
211
- text=f"No historical data available for {symbol}<br>Try refreshing or selecting a different cryptocurrency",
212
- xref="paper", yref="paper",
213
- x=0.5, y=0.5, showarrow=False,
214
- font=dict(size=16)
215
- )
216
- fig.update_layout(
217
- title=f"{symbol} - No Data Available",
218
- height=600
219
- )
220
- return fig
221
-
222
- # Extract data
223
- timestamps = [datetime.fromisoformat(h['timestamp'].replace('Z', '+00:00')) if isinstance(h['timestamp'], str) else datetime.now() for h in history]
224
- prices_data = [h.get('price_usd', 0) for h in history]
225
- volumes = [h.get('volume_24h', 0) for h in history]
226
-
227
- # Calculate technical indicators
228
- ma7_values = []
229
- ma30_values = []
230
- rsi_values = []
231
-
232
- for i in range(len(prices_data)):
233
- # MA7
234
- if i >= 6:
235
- ma7 = utils.calculate_moving_average(prices_data[:i+1], 7)
236
- ma7_values.append(ma7)
237
- else:
238
- ma7_values.append(None)
239
-
240
- # MA30
241
- if i >= 29:
242
- ma30 = utils.calculate_moving_average(prices_data[:i+1], 30)
243
- ma30_values.append(ma30)
244
- else:
245
- ma30_values.append(None)
246
-
247
- # RSI
248
- if i >= 14:
249
- rsi = utils.calculate_rsi(prices_data[:i+1], 14)
250
- rsi_values.append(rsi)
251
- else:
252
- rsi_values.append(None)
253
-
254
- # Create subplots: Price + Volume + RSI
255
- fig = make_subplots(
256
- rows=3, cols=1,
257
- shared_xaxes=True,
258
- vertical_spacing=0.05,
259
- row_heights=[0.5, 0.25, 0.25],
260
- subplot_titles=(f'{symbol} Price Chart', 'Volume', 'RSI (14)')
261
- )
262
-
263
- # Price line
264
- fig.add_trace(
265
- go.Scatter(
266
- x=timestamps,
267
- y=prices_data,
268
- name='Price',
269
- line=dict(color='#2962FF', width=2),
270
- hovertemplate='<b>Price</b>: $%{y:,.2f}<br><b>Date</b>: %{x}<extra></extra>'
271
- ),
272
- row=1, col=1
273
- )
274
-
275
- # MA7
276
- fig.add_trace(
277
- go.Scatter(
278
- x=timestamps,
279
- y=ma7_values,
280
- name='MA(7)',
281
- line=dict(color='#FF6D00', width=1, dash='dash'),
282
- hovertemplate='<b>MA(7)</b>: $%{y:,.2f}<extra></extra>'
283
- ),
284
- row=1, col=1
285
- )
286
-
287
- # MA30
288
- fig.add_trace(
289
- go.Scatter(
290
- x=timestamps,
291
- y=ma30_values,
292
- name='MA(30)',
293
- line=dict(color='#00C853', width=1, dash='dot'),
294
- hovertemplate='<b>MA(30)</b>: $%{y:,.2f}<extra></extra>'
295
- ),
296
- row=1, col=1
297
- )
298
-
299
- # Volume bars
300
- fig.add_trace(
301
- go.Bar(
302
- x=timestamps,
303
- y=volumes,
304
- name='Volume',
305
- marker=dict(color='rgba(100, 149, 237, 0.5)'),
306
- hovertemplate='<b>Volume</b>: %{y:,.0f}<extra></extra>'
307
- ),
308
- row=2, col=1
309
- )
310
-
311
- # RSI
312
- fig.add_trace(
313
- go.Scatter(
314
- x=timestamps,
315
- y=rsi_values,
316
- name='RSI',
317
- line=dict(color='#9C27B0', width=2),
318
- hovertemplate='<b>RSI</b>: %{y:.2f}<extra></extra>'
319
- ),
320
- row=3, col=1
321
- )
322
-
323
- # Add RSI reference lines
324
- fig.add_hline(y=70, line_dash="dash", line_color="red", opacity=0.5, row=3, col=1)
325
- fig.add_hline(y=30, line_dash="dash", line_color="green", opacity=0.5, row=3, col=1)
326
-
327
- # Update layout
328
- fig.update_layout(
329
- title=f'{symbol} - {timeframe} Analysis',
330
- height=800,
331
- hovermode='x unified',
332
- showlegend=True,
333
- legend=dict(
334
- orientation="h",
335
- yanchor="bottom",
336
- y=1.02,
337
- xanchor="right",
338
- x=1
339
- )
340
- )
341
-
342
- # Update axes
343
- fig.update_xaxes(title_text="Date", row=3, col=1)
344
- fig.update_yaxes(title_text="Price (USD)", row=1, col=1)
345
- fig.update_yaxes(title_text="Volume", row=2, col=1)
346
- fig.update_yaxes(title_text="RSI", row=3, col=1, range=[0, 100])
347
-
348
- logger.info(f"Chart generated successfully for {symbol}")
349
- return fig
350
-
351
- except Exception as e:
352
- logger.error(f"Error generating chart: {e}\n{traceback.format_exc()}")
353
-
354
- # Return error chart
355
- fig = go.Figure()
356
- fig.add_annotation(
357
- text=f"Error generating chart:<br>{str(e)}",
358
- xref="paper", yref="paper",
359
- x=0.5, y=0.5, showarrow=False,
360
- font=dict(size=14, color="red")
361
- )
362
- fig.update_layout(title="Chart Error", height=600)
363
- return fig
364
-
365
-
366
- # ==================== TAB 3: NEWS & SENTIMENT ====================
367
-
368
- def get_news_feed(sentiment_filter: str = "All", coin_filter: str = "All") -> str:
369
- """
370
- Get news feed with sentiment analysis as HTML cards
371
-
372
- Args:
373
- sentiment_filter: Filter by sentiment (All, Positive, Neutral, Negative)
374
- coin_filter: Filter by coin (All, BTC, ETH, etc.)
375
-
376
- Returns:
377
- HTML string with news cards
378
- """
379
- try:
380
- logger.info(f"Fetching news feed: sentiment={sentiment_filter}, coin={coin_filter}")
381
-
382
- # Map sentiment filter
383
- sentiment_map = {
384
- "All": None,
385
- "Positive": "positive",
386
- "Neutral": "neutral",
387
- "Negative": "negative",
388
- "Very Positive": "very_positive",
389
- "Very Negative": "very_negative"
390
  }
 
391
 
392
- sentiment_db = sentiment_map.get(sentiment_filter)
393
-
394
- # Get news from database
395
- if coin_filter != "All":
396
- news_list = db.get_news_by_coin(coin_filter, limit=50)
397
- else:
398
- news_list = db.get_latest_news(limit=50, sentiment=sentiment_db)
399
-
400
- if not news_list:
401
- return """
402
- <div style='text-align: center; padding: 40px; color: #666;'>
403
- <h3>No news articles found</h3>
404
- <p>Try adjusting your filters or refresh the data</p>
405
- </div>
406
- """
407
-
408
- # Calculate overall market sentiment
409
- sentiment_scores = [n.get('sentiment_score', 0) for n in news_list if n.get('sentiment_score') is not None]
410
- avg_sentiment = sum(sentiment_scores) / len(sentiment_scores) if sentiment_scores else 0
411
- sentiment_gauge = int((avg_sentiment + 1) * 50) # Convert -1 to 1 -> 0 to 100
412
-
413
- # Determine gauge color
414
- if sentiment_gauge >= 60:
415
- gauge_color = "#4CAF50"
416
- gauge_label = "Bullish"
417
- elif sentiment_gauge <= 40:
418
- gauge_color = "#F44336"
419
- gauge_label = "Bearish"
420
- else:
421
- gauge_color = "#FF9800"
422
- gauge_label = "Neutral"
423
-
424
- # Build HTML
425
- html = f"""
426
- <style>
427
- .sentiment-gauge {{
428
- background: linear-gradient(90deg, #F44336 0%, #FF9800 50%, #4CAF50 100%);
429
- height: 30px;
430
- border-radius: 15px;
431
- position: relative;
432
- margin: 20px 0;
433
- }}
434
- .sentiment-indicator {{
435
- position: absolute;
436
- left: {sentiment_gauge}%;
437
- top: -5px;
438
- width: 40px;
439
- height: 40px;
440
- background: white;
441
- border: 3px solid {gauge_color};
442
- border-radius: 50%;
443
- transform: translateX(-50%);
444
- }}
445
- .news-card {{
446
- background: white;
447
- border: 1px solid #e0e0e0;
448
- border-radius: 8px;
449
- padding: 16px;
450
- margin: 12px 0;
451
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
452
- transition: box-shadow 0.3s;
453
- }}
454
- .news-card:hover {{
455
- box-shadow: 0 4px 8px rgba(0,0,0,0.2);
456
- }}
457
- .news-title {{
458
- font-size: 18px;
459
- font-weight: bold;
460
- color: #333;
461
- margin-bottom: 8px;
462
- }}
463
- .news-meta {{
464
- font-size: 12px;
465
- color: #666;
466
- margin-bottom: 8px;
467
- }}
468
- .sentiment-badge {{
469
- display: inline-block;
470
- padding: 4px 12px;
471
- border-radius: 12px;
472
- font-size: 11px;
473
- font-weight: bold;
474
- margin-left: 8px;
475
- }}
476
- .sentiment-positive {{ background: #C8E6C9; color: #2E7D32; }}
477
- .sentiment-very_positive {{ background: #81C784; color: #1B5E20; }}
478
- .sentiment-neutral {{ background: #FFF9C4; color: #F57F17; }}
479
- .sentiment-negative {{ background: #FFCDD2; color: #C62828; }}
480
- .sentiment-very_negative {{ background: #EF5350; color: #B71C1C; }}
481
- .news-summary {{
482
- color: #555;
483
- line-height: 1.5;
484
- margin-bottom: 8px;
485
- }}
486
- .news-link {{
487
- color: #2962FF;
488
- text-decoration: none;
489
- font-weight: 500;
490
- }}
491
- .news-link:hover {{
492
- text-decoration: underline;
493
- }}
494
- </style>
495
-
496
- <div style='margin-bottom: 30px;'>
497
- <h2 style='margin-bottom: 10px;'>Market Sentiment Gauge</h2>
498
- <div style='text-align: center; font-size: 24px; font-weight: bold; color: {gauge_color};'>
499
- {gauge_label} ({sentiment_gauge}/100)
500
- </div>
501
- <div class='sentiment-gauge'>
502
- <div class='sentiment-indicator'></div>
503
- </div>
504
- </div>
505
-
506
- <h2>Latest News ({len(news_list)} articles)</h2>
507
- """
508
-
509
- # Add news cards
510
- for news in news_list:
511
- title = news.get('title', 'No Title')
512
- summary = news.get('summary', '')
513
- url = news.get('url', '#')
514
- source = news.get('source', 'Unknown')
515
- published = news.get('published_date', news.get('timestamp', ''))
516
-
517
- # Format date
518
- try:
519
- if published:
520
- dt = datetime.fromisoformat(published.replace('Z', '+00:00'))
521
- date_str = dt.strftime('%b %d, %Y %H:%M')
522
- else:
523
- date_str = 'Unknown date'
524
- except:
525
- date_str = 'Unknown date'
526
-
527
- # Get sentiment
528
- sentiment_label = news.get('sentiment_label', 'neutral')
529
- sentiment_class = f"sentiment-{sentiment_label}"
530
- sentiment_display = sentiment_label.replace('_', ' ').title()
531
-
532
- # Related coins
533
- related_coins = news.get('related_coins', [])
534
- if isinstance(related_coins, str):
535
- try:
536
- related_coins = json.loads(related_coins)
537
- except:
538
- related_coins = []
539
-
540
- coins_str = ', '.join(related_coins[:5]) if related_coins else 'General'
541
-
542
- html += f"""
543
- <div class='news-card'>
544
- <div class='news-title'>
545
- <a href='{url}' target='_blank' class='news-link'>{title}</a>
546
- </div>
547
- <div class='news-meta'>
548
- <strong>{source}</strong> | {date_str} | Coins: {coins_str}
549
- <span class='sentiment-badge {sentiment_class}'>{sentiment_display}</span>
550
- </div>
551
- <div class='news-summary'>{summary}</div>
552
- </div>
553
- """
554
-
555
- return html
556
-
557
- except Exception as e:
558
- logger.error(f"Error in get_news_feed: {e}\n{traceback.format_exc()}")
559
- return f"""
560
- <div style='color: red; padding: 20px;'>
561
- <h3>Error Loading News</h3>
562
- <p>{str(e)}</p>
563
- </div>
564
- """
565
-
566
-
567
- # ==================== TAB 4: AI ANALYSIS ====================
568
-
569
- def generate_ai_analysis(symbol_display: str) -> str:
570
- """
571
- Generate AI-powered market analysis for a cryptocurrency
572
-
573
- Args:
574
- symbol_display: Display name like "Bitcoin (BTC)"
575
-
576
- Returns:
577
- HTML with analysis results
578
- """
579
- try:
580
- logger.info(f"Generating AI analysis for {symbol_display}")
581
-
582
- # Extract symbol
583
- if '(' in symbol_display and ')' in symbol_display:
584
- symbol = symbol_display.split('(')[1].split(')')[0].strip().upper()
585
- else:
586
- symbol = symbol_display.strip().upper()
587
-
588
- # Get price history (last 30 days)
589
- history = db.get_price_history(symbol, hours=24*30)
590
-
591
- if not history or len(history) < 2:
592
- return f"""
593
- <div style='padding: 20px; text-align: center; color: #666;'>
594
- <h3>Insufficient Data</h3>
595
- <p>Not enough historical data available for {symbol} to perform analysis.</p>
596
- <p>Please try a different cryptocurrency or wait for more data to be collected.</p>
597
- </div>
598
- """
599
 
600
- # Prepare price history for AI analysis
601
- price_history = [
602
- {
603
- 'price': h.get('price_usd', 0),
604
- 'timestamp': h.get('timestamp', ''),
605
- 'volume': h.get('volume_24h', 0)
606
- }
607
- for h in history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  ]
609
-
610
- # Call AI analysis
611
- analysis = ai_models.analyze_market_trend(price_history)
612
-
613
- # Get trend info
614
- trend = analysis.get('trend', 'Neutral')
615
- current_price = analysis.get('current_price', 0)
616
- support = analysis.get('support_level', 0)
617
- resistance = analysis.get('resistance_level', 0)
618
- prediction = analysis.get('prediction', 'No prediction available')
619
- confidence = analysis.get('confidence', 0)
620
- rsi = analysis.get('rsi', 50)
621
- ma7 = analysis.get('ma7', 0)
622
- ma30 = analysis.get('ma30', 0)
623
-
624
- # Determine trend color and icon
625
- if trend == "Bullish":
626
- trend_color = "#4CAF50"
627
- trend_icon = "📈"
628
- elif trend == "Bearish":
629
- trend_color = "#F44336"
630
- trend_icon = "📉"
631
- else:
632
- trend_color = "#FF9800"
633
- trend_icon = "➡️"
634
-
635
- # Format confidence as percentage
636
- confidence_pct = int(confidence * 100)
637
-
638
- # Build HTML
639
- html = f"""
640
- <style>
641
- .analysis-container {{
642
- padding: 20px;
643
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
644
- border-radius: 12px;
645
- color: white;
646
- margin-bottom: 20px;
647
- }}
648
- .analysis-header {{
649
- text-align: center;
650
- margin-bottom: 30px;
651
- }}
652
- .trend-indicator {{
653
- font-size: 48px;
654
- margin: 20px 0;
655
- }}
656
- .metric-grid {{
657
- display: grid;
658
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
659
- gap: 15px;
660
- margin: 20px 0;
661
- }}
662
- .metric-card {{
663
- background: rgba(255, 255, 255, 0.1);
664
- padding: 15px;
665
- border-radius: 8px;
666
- backdrop-filter: blur(10px);
667
- }}
668
- .metric-label {{
669
- font-size: 12px;
670
- opacity: 0.8;
671
- margin-bottom: 5px;
672
- }}
673
- .metric-value {{
674
- font-size: 24px;
675
- font-weight: bold;
676
- }}
677
- .prediction-box {{
678
- background: rgba(255, 255, 255, 0.15);
679
- padding: 20px;
680
- border-radius: 8px;
681
- margin: 20px 0;
682
- border-left: 4px solid {trend_color};
683
- }}
684
- .confidence-bar {{
685
- background: rgba(255, 255, 255, 0.2);
686
- height: 30px;
687
- border-radius: 15px;
688
- overflow: hidden;
689
- margin-top: 10px;
690
- }}
691
- .confidence-fill {{
692
- background: {trend_color};
693
- height: 100%;
694
- width: {confidence_pct}%;
695
- transition: width 0.5s ease;
696
- display: flex;
697
- align-items: center;
698
- justify-content: center;
699
- font-weight: bold;
700
- }}
701
- .history-section {{
702
- background: white;
703
- padding: 20px;
704
- border-radius: 8px;
705
- margin-top: 20px;
706
- color: #333;
707
- }}
708
- </style>
709
-
710
- <div class='analysis-container'>
711
- <div class='analysis-header'>
712
- <h1>{symbol} Market Analysis</h1>
713
- <div class='trend-indicator'>{trend_icon}</div>
714
- <h2 style='color: {trend_color};'>{trend} Trend</h2>
715
- </div>
716
-
717
- <div class='metric-grid'>
718
- <div class='metric-card'>
719
- <div class='metric-label'>Current Price</div>
720
- <div class='metric-value'>${current_price:,.2f}</div>
721
- </div>
722
- <div class='metric-card'>
723
- <div class='metric-label'>Support Level</div>
724
- <div class='metric-value'>${support:,.2f}</div>
725
- </div>
726
- <div class='metric-card'>
727
- <div class='metric-label'>Resistance Level</div>
728
- <div class='metric-value'>${resistance:,.2f}</div>
729
- </div>
730
- <div class='metric-card'>
731
- <div class='metric-label'>RSI (14)</div>
732
- <div class='metric-value'>{rsi:.1f}</div>
733
- </div>
734
- <div class='metric-card'>
735
- <div class='metric-label'>MA (7)</div>
736
- <div class='metric-value'>${ma7:,.2f}</div>
737
- </div>
738
- <div class='metric-card'>
739
- <div class='metric-label'>MA (30)</div>
740
- <div class='metric-value'>${ma30:,.2f}</div>
741
- </div>
742
- </div>
743
-
744
- <div class='prediction-box'>
745
- <h3>📊 Market Prediction</h3>
746
- <p style='font-size: 16px; line-height: 1.6;'>{prediction}</p>
747
- </div>
748
-
749
- <div>
750
- <h3>Confidence Score</h3>
751
- <div class='confidence-bar'>
752
- <div class='confidence-fill'>{confidence_pct}%</div>
753
- </div>
754
- </div>
755
- </div>
756
-
757
- <div class='history-section'>
758
- <h3>📜 Recent Analysis History</h3>
759
- <p>Latest analysis generated on {datetime.now().strftime('%B %d, %Y at %H:%M:%S')}</p>
760
- <p><strong>Data Points Analyzed:</strong> {len(price_history)}</p>
761
- <p><strong>Time Range:</strong> {len(price_history)} hours of historical data</p>
762
- </div>
763
- """
764
-
765
- # Save analysis to database
766
- db.save_analysis({
767
- 'symbol': symbol,
768
- 'timeframe': '30d',
769
- 'trend': trend,
770
- 'support_level': support,
771
- 'resistance_level': resistance,
772
- 'prediction': prediction,
773
- 'confidence': confidence
774
- })
775
-
776
- logger.info(f"AI analysis completed for {symbol}")
777
- return html
778
-
779
- except Exception as e:
780
- logger.error(f"Error in generate_ai_analysis: {e}\n{traceback.format_exc()}")
781
- return f"""
782
- <div style='padding: 20px; color: red;'>
783
- <h3>Analysis Error</h3>
784
- <p>Failed to generate analysis: {str(e)}</p>
785
- <p>Please try again or select a different cryptocurrency.</p>
786
- </div>
787
- """
788
-
789
-
790
- # ==================== TAB 5: DATABASE EXPLORER ====================
791
-
792
- def execute_database_query(query_type: str, custom_query: str = "") -> Tuple[pd.DataFrame, str]:
793
- """
794
- Execute database query and return results
795
-
796
- Args:
797
- query_type: Type of pre-built query or "Custom"
798
- custom_query: Custom SQL query (if query_type is "Custom")
799
-
800
- Returns:
801
- Tuple of (DataFrame with results, status message)
802
- """
803
- try:
804
- logger.info(f"Executing database query: {query_type}")
805
-
806
- if query_type == "Top 10 gainers in last 24h":
807
- results = db.get_top_gainers(10)
808
- message = f"✅ Found {len(results)} gainers"
809
-
810
- elif query_type == "All news with positive sentiment":
811
- results = db.get_latest_news(limit=100, sentiment="positive")
812
- message = f"✅ Found {len(results)} positive news articles"
813
-
814
- elif query_type == "Price history for BTC":
815
- results = db.get_price_history("BTC", 168)
816
- message = f"✅ Found {len(results)} BTC price records"
817
-
818
- elif query_type == "Database statistics":
819
- stats = db.get_database_stats()
820
- # Convert stats to DataFrame
821
- results = [{"Metric": k, "Value": str(v)} for k, v in stats.items()]
822
- message = "✅ Database statistics retrieved"
823
-
824
- elif query_type == "Latest 100 prices":
825
- results = db.get_latest_prices(100)
826
- message = f"✅ Retrieved {len(results)} latest prices"
827
-
828
- elif query_type == "Recent news (50)":
829
- results = db.get_latest_news(50)
830
- message = f"✅ Retrieved {len(results)} recent news articles"
831
-
832
- elif query_type == "All market analyses":
833
- results = db.get_all_analyses(100)
834
- message = f"✅ Retrieved {len(results)} market analyses"
835
-
836
- elif query_type == "Custom Query":
837
- if not custom_query.strip():
838
- return pd.DataFrame(), "⚠️ Please enter a custom query"
839
-
840
- # Security check
841
- if not custom_query.strip().upper().startswith('SELECT'):
842
- return pd.DataFrame(), "❌ Only SELECT queries are allowed for security reasons"
843
-
844
- results = db.execute_safe_query(custom_query)
845
- message = f"✅ Custom query returned {len(results)} rows"
846
-
847
- else:
848
- return pd.DataFrame(), "❌ Unknown query type"
849
-
850
- # Convert to DataFrame
851
- if results:
852
- df = pd.DataFrame(results)
853
-
854
- # Truncate long text fields for display
855
- for col in df.columns:
856
- if df[col].dtype == 'object':
857
- df[col] = df[col].apply(lambda x: str(x)[:100] + '...' if isinstance(x, str) and len(str(x)) > 100 else x)
858
-
859
- return df, message
860
- else:
861
- return pd.DataFrame(), f"⚠️ Query returned no results"
862
-
863
- except Exception as e:
864
- logger.error(f"Error executing query: {e}\n{traceback.format_exc()}")
865
- return pd.DataFrame(), f"❌ Query failed: {str(e)}"
866
-
867
-
868
- def export_query_results(df: pd.DataFrame) -> Tuple[str, str]:
869
- """
870
- Export query results to CSV file
871
-
872
- Args:
873
- df: DataFrame to export
874
-
875
- Returns:
876
- Tuple of (file_path, status_message)
877
- """
878
- try:
879
- if df.empty:
880
- return None, "⚠️ No data to export"
881
-
882
- # Create export filename with timestamp
883
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
884
- filename = f"query_export_{timestamp}.csv"
885
- filepath = config.DATA_DIR / filename
886
-
887
- # Export using utils
888
- success = utils.export_to_csv(df.to_dict('records'), str(filepath))
889
-
890
- if success:
891
- return str(filepath), f"✅ Exported {len(df)} rows to {filename}"
892
- else:
893
- return None, "❌ Export failed"
894
-
895
- except Exception as e:
896
- logger.error(f"Error exporting results: {e}")
897
- return None, f"❌ Export error: {str(e)}"
898
-
899
-
900
- # ==================== TAB 6: DATA SOURCES STATUS ====================
901
-
902
- def get_data_sources_status() -> Tuple[pd.DataFrame, str]:
903
- """
904
- Get status of all data sources
905
-
906
- Returns:
907
- Tuple of (DataFrame with status, HTML with error log)
908
- """
909
- try:
910
- logger.info("Checking data sources status...")
911
-
912
- status_data = []
913
-
914
- # Check CoinGecko
915
- try:
916
- import requests
917
- response = requests.get(f"{config.COINGECKO_BASE_URL}/ping", timeout=5)
918
- if response.status_code == 200:
919
- coingecko_status = "🟢 Online"
920
- coingecko_error = 0
921
- else:
922
- coingecko_status = f"🟡 Status {response.status_code}"
923
- coingecko_error = 1
924
- except:
925
- coingecko_status = "🔴 Offline"
926
- coingecko_error = 1
927
-
928
- status_data.append({
929
- "Data Source": "CoinGecko API",
930
- "Status": coingecko_status,
931
- "Last Update": datetime.now().strftime("%H:%M:%S"),
932
- "Errors": coingecko_error
933
- })
934
-
935
- # Check CoinCap
936
- try:
937
- import requests
938
- response = requests.get(f"{config.COINCAP_BASE_URL}/assets", timeout=5)
939
- if response.status_code == 200:
940
- coincap_status = "🟢 Online"
941
- coincap_error = 0
942
- else:
943
- coincap_status = f"🟡 Status {response.status_code}"
944
- coincap_error = 1
945
- except:
946
- coincap_status = "🔴 Offline"
947
- coincap_error = 1
948
-
949
- status_data.append({
950
- "Data Source": "CoinCap API",
951
- "Status": coincap_status,
952
- "Last Update": datetime.now().strftime("%H:%M:%S"),
953
- "Errors": coincap_error
954
- })
955
-
956
- # Check Binance
957
- try:
958
- import requests
959
- response = requests.get(f"{config.BINANCE_BASE_URL}/ping", timeout=5)
960
- if response.status_code == 200:
961
- binance_status = "🟢 Online"
962
- binance_error = 0
963
- else:
964
- binance_status = f"🟡 Status {response.status_code}"
965
- binance_error = 1
966
- except:
967
- binance_status = "🔴 Offline"
968
- binance_error = 1
969
-
970
- status_data.append({
971
- "Data Source": "Binance API",
972
- "Status": binance_status,
973
- "Last Update": datetime.now().strftime("%H:%M:%S"),
974
- "Errors": binance_error
975
- })
976
-
977
- # Check RSS Feeds
978
- rss_ok = 0
979
- rss_failed = 0
980
- for feed_name in config.RSS_FEEDS.keys():
981
- if feed_name in ["coindesk", "cointelegraph"]:
982
- rss_ok += 1
983
- else:
984
- rss_ok += 1 # Assume OK for now
985
-
986
- status_data.append({
987
- "Data Source": f"RSS Feeds ({len(config.RSS_FEEDS)} sources)",
988
- "Status": f"🟢 {rss_ok} active",
989
- "Last Update": datetime.now().strftime("%H:%M:%S"),
990
- "Errors": rss_failed
991
- })
992
-
993
- # Check Reddit
994
- reddit_ok = 0
995
- for subreddit in config.REDDIT_ENDPOINTS.keys():
996
- reddit_ok += 1 # Assume OK
997
-
998
- status_data.append({
999
- "Data Source": f"Reddit ({len(config.REDDIT_ENDPOINTS)} subreddits)",
1000
- "Status": f"🟢 {reddit_ok} active",
1001
- "Last Update": datetime.now().strftime("%H:%M:%S"),
1002
- "Errors": 0
1003
- })
1004
-
1005
- # Check Database
1006
- try:
1007
- stats = db.get_database_stats()
1008
- db_status = "🟢 Connected"
1009
- db_error = 0
1010
- last_update = stats.get('latest_price_update', 'Unknown')
1011
- except:
1012
- db_status = "🔴 Error"
1013
- db_error = 1
1014
- last_update = "Unknown"
1015
-
1016
- status_data.append({
1017
- "Data Source": "SQLite Database",
1018
- "Status": db_status,
1019
- "Last Update": last_update if last_update != 'Unknown' else datetime.now().strftime("%H:%M:%S"),
1020
- "Errors": db_error
1021
- })
1022
-
1023
- df = pd.DataFrame(status_data)
1024
-
1025
- # Get error log
1026
- error_html = get_error_log_html()
1027
-
1028
- return df, error_html
1029
-
1030
- except Exception as e:
1031
- logger.error(f"Error getting data sources status: {e}")
1032
- return pd.DataFrame(), f"<p style='color: red;'>Error: {str(e)}</p>"
1033
-
1034
-
1035
- def get_error_log_html() -> str:
1036
- """Get last 10 errors from log file as HTML"""
1037
- try:
1038
- if not config.LOG_FILE.exists():
1039
- return "<p>No error log file found</p>"
1040
-
1041
- # Read last 100 lines of log file
1042
- with open(config.LOG_FILE, 'r') as f:
1043
- lines = f.readlines()
1044
-
1045
- # Get lines with ERROR or WARNING
1046
- error_lines = [line for line in lines[-100:] if 'ERROR' in line or 'WARNING' in line]
1047
-
1048
- if not error_lines:
1049
- return "<p style='color: green;'>✅ No recent errors or warnings</p>"
1050
-
1051
- # Take last 10
1052
- error_lines = error_lines[-10:]
1053
-
1054
- html = "<h3>Recent Errors & Warnings</h3><div style='background: #f5f5f5; padding: 10px; border-radius: 5px; font-family: monospace; font-size: 12px;'>"
1055
-
1056
- for line in error_lines:
1057
- # Color code by severity
1058
- if 'ERROR' in line:
1059
- color = 'red'
1060
- elif 'WARNING' in line:
1061
- color = 'orange'
1062
- else:
1063
- color = 'black'
1064
-
1065
- html += f"<div style='color: {color}; margin: 5px 0;'>{line.strip()}</div>"
1066
-
1067
- html += "</div>"
1068
-
1069
- return html
1070
-
1071
- except Exception as e:
1072
- logger.error(f"Error reading log file: {e}")
1073
- return f"<p style='color: red;'>Error reading log: {str(e)}</p>"
1074
-
1075
-
1076
- def manual_data_collection() -> Tuple[pd.DataFrame, str, str]:
1077
- """
1078
- Manually trigger data collection for all sources
1079
-
1080
- Returns:
1081
- Tuple of (status DataFrame, status HTML, message)
1082
- """
1083
- try:
1084
- logger.info("Manual data collection triggered...")
1085
-
1086
- message = "🔄 Collecting data from all sources...\n\n"
1087
-
1088
- # Collect price data
1089
- try:
1090
- success, count = collectors.collect_price_data()
1091
- if success:
1092
- message += f"✅ Prices: {count} records collected\n"
1093
- else:
1094
- message += f"⚠️ Prices: Collection had issues\n"
1095
- except Exception as e:
1096
- message += f"❌ Prices: {str(e)}\n"
1097
-
1098
- # Collect news data
1099
- try:
1100
- count = collectors.collect_news_data()
1101
- message += f"✅ News: {count} articles collected\n"
1102
- except Exception as e:
1103
- message += f"❌ News: {str(e)}\n"
1104
-
1105
- # Collect sentiment data
1106
- try:
1107
- sentiment = collectors.collect_sentiment_data()
1108
- if sentiment:
1109
- message += f"✅ Sentiment: {sentiment.get('classification', 'N/A')}\n"
1110
- else:
1111
- message += "⚠️ Sentiment: No data collected\n"
1112
- except Exception as e:
1113
- message += f"❌ Sentiment: {str(e)}\n"
1114
-
1115
- message += "\n✅ Data collection complete!"
1116
-
1117
- # Get updated status
1118
- df, html = get_data_sources_status()
1119
-
1120
- return df, html, message
1121
-
1122
- except Exception as e:
1123
- logger.error(f"Error in manual data collection: {e}")
1124
- df, html = get_data_sources_status()
1125
- return df, html, f"❌ Collection failed: {str(e)}"
1126
-
1127
-
1128
- # ==================== GRADIO INTERFACE ====================
1129
-
1130
- def create_gradio_interface():
1131
- """Create the complete Gradio interface with all 6 tabs"""
1132
-
1133
- # Custom CSS for better styling
1134
- custom_css = """
1135
- .gradio-container {
1136
- max-width: 1400px !important;
1137
- }
1138
- .tab-nav button {
1139
- font-size: 16px !important;
1140
- font-weight: 600 !important;
1141
  }
1142
- """
1143
-
1144
- with gr.Blocks(
1145
- title="Crypto Data Aggregator - Complete Dashboard",
1146
- theme=gr.themes.Soft(),
1147
- css=custom_css
1148
- ) as interface:
1149
-
1150
- # Header
1151
- gr.Markdown("""
1152
- # 🚀 Crypto Data Aggregator - Complete Dashboard
1153
-
1154
- **Comprehensive cryptocurrency analytics platform** with real-time data, AI-powered insights, and advanced technical analysis.
1155
-
1156
- **Key Features:**
1157
- - 📊 Live price tracking for top 100 cryptocurrencies
1158
- - 📈 Historical charts with technical indicators (MA, RSI)
1159
- - 📰 News aggregation with sentiment analysis
1160
- - 🤖 AI-powered market trend predictions
1161
- - 🗄️ Powerful database explorer with export functionality
1162
- - 🔍 Real-time data source monitoring
1163
- """)
1164
-
1165
- with gr.Tabs():
1166
-
1167
- # ==================== TAB 1: LIVE DASHBOARD ====================
1168
- with gr.Tab("📊 Live Dashboard"):
1169
- gr.Markdown("### Real-time cryptocurrency prices and market data")
1170
-
1171
- with gr.Row():
1172
- search_box = gr.Textbox(
1173
- label="Search/Filter",
1174
- placeholder="Enter coin name or symbol (e.g., Bitcoin, BTC)...",
1175
- scale=3
1176
- )
1177
- refresh_btn = gr.Button("🔄 Refresh Data", variant="primary", scale=1)
1178
-
1179
- dashboard_table = gr.Dataframe(
1180
- label="Top 100 Cryptocurrencies",
1181
- interactive=False,
1182
- wrap=True,
1183
- height=600
1184
- )
1185
-
1186
- refresh_status = gr.Textbox(label="Status", interactive=False)
1187
-
1188
- # Auto-refresh timer
1189
- timer = gr.Timer(value=config.AUTO_REFRESH_INTERVAL)
1190
-
1191
- # Load initial data
1192
- interface.load(
1193
- fn=get_live_dashboard,
1194
- outputs=dashboard_table
1195
- )
1196
-
1197
- # Search/filter functionality
1198
- search_box.change(
1199
- fn=get_live_dashboard,
1200
- inputs=search_box,
1201
- outputs=dashboard_table
1202
- )
1203
-
1204
- # Refresh button
1205
- refresh_btn.click(
1206
- fn=refresh_price_data,
1207
- outputs=[dashboard_table, refresh_status]
1208
- )
1209
-
1210
- # Auto-refresh
1211
- timer.tick(
1212
- fn=get_live_dashboard,
1213
- outputs=dashboard_table
1214
- )
1215
-
1216
- # ==================== TAB 2: HISTORICAL CHARTS ====================
1217
- with gr.Tab("📈 Historical Charts"):
1218
- gr.Markdown("### Interactive price charts with technical analysis")
1219
-
1220
- with gr.Row():
1221
- symbol_dropdown = gr.Dropdown(
1222
- label="Select Cryptocurrency",
1223
- choices=get_available_symbols(),
1224
- value=get_available_symbols()[0] if get_available_symbols() else "BTC",
1225
- scale=2
1226
- )
1227
-
1228
- timeframe_buttons = gr.Radio(
1229
- label="Timeframe",
1230
- choices=["1d", "7d", "30d", "90d", "1y", "All"],
1231
- value="7d",
1232
- scale=2
1233
- )
1234
-
1235
- chart_plot = gr.Plot(label="Price Chart with Indicators")
1236
-
1237
- with gr.Row():
1238
- generate_chart_btn = gr.Button("📊 Generate Chart", variant="primary")
1239
- export_chart_btn = gr.Button("💾 Export Chart (PNG)")
1240
-
1241
- # Generate chart
1242
- generate_chart_btn.click(
1243
- fn=generate_chart,
1244
- inputs=[symbol_dropdown, timeframe_buttons],
1245
- outputs=chart_plot
1246
- )
1247
-
1248
- # Also update on dropdown/timeframe change
1249
- symbol_dropdown.change(
1250
- fn=generate_chart,
1251
- inputs=[symbol_dropdown, timeframe_buttons],
1252
- outputs=chart_plot
1253
- )
1254
-
1255
- timeframe_buttons.change(
1256
- fn=generate_chart,
1257
- inputs=[symbol_dropdown, timeframe_buttons],
1258
- outputs=chart_plot
1259
- )
1260
-
1261
- # Load initial chart
1262
- interface.load(
1263
- fn=generate_chart,
1264
- inputs=[symbol_dropdown, timeframe_buttons],
1265
- outputs=chart_plot
1266
- )
1267
-
1268
- # ==================== TAB 3: NEWS & SENTIMENT ====================
1269
- with gr.Tab("📰 News & Sentiment"):
1270
- gr.Markdown("### Latest cryptocurrency news with AI sentiment analysis")
1271
-
1272
- with gr.Row():
1273
- sentiment_filter = gr.Dropdown(
1274
- label="Filter by Sentiment",
1275
- choices=["All", "Positive", "Neutral", "Negative", "Very Positive", "Very Negative"],
1276
- value="All",
1277
- scale=1
1278
- )
1279
-
1280
- coin_filter = gr.Dropdown(
1281
- label="Filter by Coin",
1282
- choices=["All", "BTC", "ETH", "BNB", "XRP", "ADA", "SOL", "DOT", "DOGE"],
1283
- value="All",
1284
- scale=1
1285
- )
1286
-
1287
- news_refresh_btn = gr.Button("🔄 Refresh News", variant="primary", scale=1)
1288
-
1289
- news_html = gr.HTML(label="News Feed")
1290
-
1291
- # Load initial news
1292
- interface.load(
1293
- fn=get_news_feed,
1294
- inputs=[sentiment_filter, coin_filter],
1295
- outputs=news_html
1296
- )
1297
-
1298
- # Update on filter change
1299
- sentiment_filter.change(
1300
- fn=get_news_feed,
1301
- inputs=[sentiment_filter, coin_filter],
1302
- outputs=news_html
1303
- )
1304
-
1305
- coin_filter.change(
1306
- fn=get_news_feed,
1307
- inputs=[sentiment_filter, coin_filter],
1308
- outputs=news_html
1309
- )
1310
-
1311
- # Refresh button
1312
- news_refresh_btn.click(
1313
- fn=get_news_feed,
1314
- inputs=[sentiment_filter, coin_filter],
1315
- outputs=news_html
1316
- )
1317
 
1318
- # ==================== TAB 4: AI ANALYSIS ====================
1319
- with gr.Tab("🤖 AI Analysis"):
1320
- gr.Markdown("### AI-powered market trend analysis and predictions")
1321
-
1322
- with gr.Row():
1323
- analysis_symbol = gr.Dropdown(
1324
- label="Select Cryptocurrency for Analysis",
1325
- choices=get_available_symbols(),
1326
- value=get_available_symbols()[0] if get_available_symbols() else "BTC",
1327
- scale=3
1328
- )
1329
-
1330
- analyze_btn = gr.Button("🔮 Generate Analysis", variant="primary", scale=1)
1331
-
1332
- analysis_html = gr.HTML(label="AI Analysis Results")
1333
-
1334
- # Generate analysis
1335
- analyze_btn.click(
1336
- fn=generate_ai_analysis,
1337
- inputs=analysis_symbol,
1338
- outputs=analysis_html
1339
- )
1340
-
1341
- # ==================== TAB 5: DATABASE EXPLORER ====================
1342
- with gr.Tab("🗄️ Database Explorer"):
1343
- gr.Markdown("### Query and explore the cryptocurrency database")
1344
-
1345
- query_type = gr.Dropdown(
1346
- label="Select Query",
1347
- choices=[
1348
- "Top 10 gainers in last 24h",
1349
- "All news with positive sentiment",
1350
- "Price history for BTC",
1351
- "Database statistics",
1352
- "Latest 100 prices",
1353
- "Recent news (50)",
1354
- "All market analyses",
1355
- "Custom Query"
1356
- ],
1357
- value="Database statistics"
1358
- )
1359
-
1360
- custom_query_box = gr.Textbox(
1361
- label="Custom SQL Query (SELECT only)",
1362
- placeholder="SELECT * FROM prices WHERE symbol = 'BTC' LIMIT 10",
1363
- lines=3,
1364
- visible=False
1365
- )
1366
-
1367
- with gr.Row():
1368
- execute_btn = gr.Button("▶️ Execute Query", variant="primary")
1369
- export_btn = gr.Button("💾 Export to CSV")
1370
-
1371
- query_results = gr.Dataframe(label="Query Results", interactive=False, wrap=True)
1372
- query_status = gr.Textbox(label="Status", interactive=False)
1373
- export_status = gr.Textbox(label="Export Status", interactive=False)
1374
-
1375
- # Show/hide custom query box
1376
- def toggle_custom_query(query_type):
1377
- return gr.update(visible=(query_type == "Custom Query"))
1378
-
1379
- query_type.change(
1380
- fn=toggle_custom_query,
1381
- inputs=query_type,
1382
- outputs=custom_query_box
1383
- )
1384
-
1385
- # Execute query
1386
- execute_btn.click(
1387
- fn=execute_database_query,
1388
- inputs=[query_type, custom_query_box],
1389
- outputs=[query_results, query_status]
1390
- )
1391
-
1392
- # Export results
1393
- export_btn.click(
1394
- fn=export_query_results,
1395
- inputs=query_results,
1396
- outputs=[gr.Textbox(visible=False), export_status]
1397
- )
1398
-
1399
- # Load initial query
1400
- interface.load(
1401
- fn=execute_database_query,
1402
- inputs=[query_type, custom_query_box],
1403
- outputs=[query_results, query_status]
1404
- )
1405
-
1406
- # ==================== TAB 6: DATA SOURCES STATUS ====================
1407
- with gr.Tab("🔍 Data Sources Status"):
1408
- gr.Markdown("### Monitor the health of all data sources")
1409
-
1410
- with gr.Row():
1411
- status_refresh_btn = gr.Button("🔄 Refresh Status", variant="primary")
1412
- collect_btn = gr.Button("📥 Run Manual Collection", variant="secondary")
1413
-
1414
- status_table = gr.Dataframe(label="Data Sources Status", interactive=False)
1415
- error_log_html = gr.HTML(label="Error Log")
1416
- collection_status = gr.Textbox(label="Collection Status", lines=8, interactive=False)
1417
-
1418
- # Load initial status
1419
- interface.load(
1420
- fn=get_data_sources_status,
1421
- outputs=[status_table, error_log_html]
1422
- )
1423
-
1424
- # Refresh status
1425
- status_refresh_btn.click(
1426
- fn=get_data_sources_status,
1427
- outputs=[status_table, error_log_html]
1428
- )
1429
-
1430
- # Manual collection
1431
- collect_btn.click(
1432
- fn=manual_data_collection,
1433
- outputs=[status_table, error_log_html, collection_status]
1434
- )
1435
-
1436
- # Footer
1437
- gr.Markdown("""
1438
- ---
1439
- **Crypto Data Aggregator** | Powered by CoinGecko, CoinCap, Binance APIs | AI Models by HuggingFace
1440
- """)
1441
-
1442
- return interface
1443
-
1444
-
1445
- # ==================== MAIN ENTRY POINT ====================
1446
-
1447
- def main():
1448
- """Main function to initialize and launch the Gradio app"""
1449
-
1450
- logger.info("=" * 60)
1451
- logger.info("Starting Crypto Data Aggregator Dashboard")
1452
- logger.info("=" * 60)
1453
-
1454
- # Initialize database
1455
- logger.info("Initializing database...")
1456
- db = Database()
1457
- logger.info("Database initialized successfully")
1458
-
1459
- # Start background data collection
1460
- global _collection_started
1461
- with _collection_lock:
1462
- if not _collection_started:
1463
- logger.info("Starting background data collection...")
1464
- collectors.schedule_data_collection()
1465
- _collection_started = True
1466
- logger.info("Background collection started")
1467
-
1468
- # Create Gradio interface
1469
- logger.info("Creating Gradio interface...")
1470
- interface = create_gradio_interface()
1471
-
1472
- # Launch Gradio
1473
- logger.info("Launching Gradio dashboard...")
1474
- logger.info(f"Server: {config.GRADIO_SERVER_NAME}:{config.GRADIO_SERVER_PORT}")
1475
- logger.info(f"Share: {config.GRADIO_SHARE}")
1476
 
 
 
 
1477
  try:
1478
- interface.launch(
1479
- share=config.GRADIO_SHARE,
1480
- server_name=config.GRADIO_SERVER_NAME,
1481
- server_port=config.GRADIO_SERVER_PORT,
1482
- show_error=True,
1483
- quiet=False
1484
- )
1485
- except KeyboardInterrupt:
1486
- logger.info("\nShutting down...")
1487
- collectors.stop_scheduled_collection()
1488
- logger.info("Shutdown complete")
1489
- except Exception as e:
1490
- logger.error(f"Error launching Gradio: {e}\n{traceback.format_exc()}")
1491
- raise
1492
 
 
 
 
 
 
 
 
1493
 
1494
  if __name__ == "__main__":
1495
- main()
 
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ Crypto API Monitor - Complete Professional Backend
4
+ Full coverage of all major crypto providers
5
  """
6
 
7
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
8
+ from fastapi.responses import HTMLResponse
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from typing import List, Dict
11
+ import asyncio
12
+ import random
13
  import json
14
+ from datetime import datetime, timedelta
15
+ import uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ app = FastAPI(title="Crypto API Monitor Pro", version="2.0.0")
 
 
 
 
 
 
 
 
 
 
18
 
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"],
22
+ allow_credentials=True,
23
+ allow_methods=["*"],
24
+ allow_headers=["*"],
25
+ )
 
26
 
27
+ class ConnectionManager:
28
+ def __init__(self):
29
+ self.active_connections: List[WebSocket] = []
30
 
31
+ async def connect(self, websocket: WebSocket):
32
+ await websocket.accept()
33
+ self.active_connections.append(websocket)
 
 
 
 
 
 
34
 
35
+ def disconnect(self, websocket: WebSocket):
36
+ self.active_connections.remove(websocket)
37
 
38
+ async def broadcast(self, message: dict):
39
+ for connection in self.active_connections:
40
+ try:
41
+ await connection.send_json(message)
42
+ except:
43
+ pass
44
+
45
+ manager = ConnectionManager()
46
+
47
+ # Complete list of crypto providers
48
+ PROVIDERS = {
49
+ "exchanges": [
50
+ {"name": "Binance", "type": "Exchange", "region": "Global", "base_price": 180},
51
+ {"name": "Coinbase", "type": "Exchange", "region": "US", "base_price": 220},
52
+ {"name": "Kraken", "type": "Exchange", "region": "US", "base_price": 150},
53
+ {"name": "Huobi", "type": "Exchange", "region": "Asia", "base_price": 140},
54
+ {"name": "KuCoin", "type": "Exchange", "region": "Global", "base_price": 130},
55
+ {"name": "Bitfinex", "type": "Exchange", "region": "Global", "base_price": 160},
56
+ {"name": "Bitstamp", "type": "Exchange", "region": "EU", "base_price": 145},
57
+ {"name": "Gemini", "type": "Exchange", "region": "US", "base_price": 200},
58
+ {"name": "OKX", "type": "Exchange", "region": "Global", "base_price": 135},
59
+ {"name": "Bybit", "type": "Exchange", "region": "Global", "base_price": 125},
60
+ {"name": "Gate.io", "type": "Exchange", "region": "Global", "base_price": 120},
61
+ {"name": "Crypto.com", "type": "Exchange", "region": "Global", "base_price": 155},
62
+ {"name": "Bittrex", "type": "Exchange", "region": "US", "base_price": 140},
63
+ {"name": "Poloniex", "type": "Exchange", "region": "Global", "base_price": 110},
64
+ {"name": "MEXC", "type": "Exchange", "region": "Global", "base_price": 105},
65
+ ],
66
+ "data_providers": [
67
+ {"name": "CoinGecko", "type": "Data Provider", "region": "Global", "base_price": 100},
68
+ {"name": "CoinMarketCap", "type": "Data Provider", "region": "Global", "base_price": 120},
69
+ {"name": "CryptoCompare", "type": "Data Provider", "region": "Global", "base_price": 110},
70
+ {"name": "Messari", "type": "Analytics", "region": "Global", "base_price": 180},
71
+ {"name": "Glassnode", "type": "Analytics", "region": "Global", "base_price": 200},
72
+ {"name": "Santiment", "type": "Analytics", "region": "Global", "base_price": 170},
73
+ {"name": "Kaiko", "type": "Data Provider", "region": "Global", "base_price": 190},
74
+ {"name": "Nomics", "type": "Data Provider", "region": "Global", "base_price": 95},
75
+ ],
76
+ "blockchain": [
77
+ {"name": "Etherscan", "type": "Block Explorer", "region": "Global", "base_price": 85},
78
+ {"name": "BscScan", "type": "Block Explorer", "region": "Global", "base_price": 80},
79
+ {"name": "Polygonscan", "type": "Block Explorer", "region": "Global", "base_price": 75},
80
+ {"name": "Blockchair", "type": "Block Explorer", "region": "Global", "base_price": 90},
81
+ {"name": "Blockchain.com", "type": "Block Explorer", "region": "Global", "base_price": 95},
82
+ ],
83
+ "defi": [
84
+ {"name": "Uniswap", "type": "DEX", "region": "Global", "base_price": 70},
85
+ {"name": "SushiSwap", "type": "DEX", "region": "Global", "base_price": 65},
86
+ {"name": "PancakeSwap", "type": "DEX", "region": "Global", "base_price": 60},
87
+ {"name": "Curve", "type": "DEX", "region": "Global", "base_price": 75},
88
+ {"name": "1inch", "type": "DEX Aggregator", "region": "Global", "base_price": 80},
89
+ {"name": "Aave", "type": "Lending", "region": "Global", "base_price": 85},
90
+ {"name": "Compound", "type": "Lending", "region": "Global", "base_price": 90},
91
+ {"name": "MakerDAO", "type": "Stablecoin", "region": "Global", "base_price": 100},
92
+ ],
93
+ "nft": [
94
+ {"name": "OpenSea", "type": "NFT Marketplace", "region": "Global", "base_price": 120},
95
+ {"name": "Blur", "type": "NFT Marketplace", "region": "Global", "base_price": 110},
96
+ {"name": "Magic Eden", "type": "NFT Marketplace", "region": "Global", "base_price": 95},
97
+ {"name": "Rarible", "type": "NFT Marketplace", "region": "Global", "base_price": 85},
98
+ ]
99
+ }
100
+
101
+ CRYPTOCURRENCIES = [
102
+ {"symbol": "BTC", "name": "Bitcoin", "base_price": 43500, "category": "Layer 1"},
103
+ {"symbol": "ETH", "name": "Ethereum", "base_price": 2280, "category": "Smart Contract"},
104
+ {"symbol": "BNB", "name": "Binance Coin", "base_price": 315, "category": "Exchange"},
105
+ {"symbol": "SOL", "name": "Solana", "base_price": 98, "category": "Layer 1"},
106
+ {"symbol": "XRP", "name": "Ripple", "base_price": 0.53, "category": "Payment"},
107
+ {"symbol": "ADA", "name": "Cardano", "base_price": 0.39, "category": "Smart Contract"},
108
+ {"symbol": "AVAX", "name": "Avalanche", "base_price": 24, "category": "Layer 1"},
109
+ {"symbol": "DOGE", "name": "Dogecoin", "base_price": 0.08, "category": "Meme"},
110
+ {"symbol": "DOT", "name": "Polkadot", "base_price": 5.3, "category": "Layer 0"},
111
+ {"symbol": "MATIC", "name": "Polygon", "base_price": 0.74, "category": "Layer 2"},
112
+ {"symbol": "LINK", "name": "Chainlink", "base_price": 14.5, "category": "Oracle"},
113
+ {"symbol": "UNI", "name": "Uniswap", "base_price": 6.2, "category": "DeFi"},
114
+ {"symbol": "ATOM", "name": "Cosmos", "base_price": 8.9, "category": "Layer 0"},
115
+ {"symbol": "LTC", "name": "Litecoin", "base_price": 72, "category": "Payment"},
116
+ {"symbol": "APT", "name": "Aptos", "base_price": 7.8, "category": "Layer 1"},
117
+ {"symbol": "ARB", "name": "Arbitrum", "base_price": 1.2, "category": "Layer 2"},
118
+ {"symbol": "OP", "name": "Optimism", "base_price": 2.1, "category": "Layer 2"},
119
+ {"symbol": "NEAR", "name": "NEAR Protocol", "base_price": 3.4, "category": "Layer 1"},
120
+ {"symbol": "ICP", "name": "Internet Computer", "base_price": 4.7, "category": "Layer 1"},
121
+ {"symbol": "FIL", "name": "Filecoin", "base_price": 4.2, "category": "Storage"},
122
+ ]
123
+
124
+ def generate_all_providers():
125
+ """Generate complete provider data"""
126
+ result = []
127
+
128
+ for category, providers in PROVIDERS.items():
129
+ for provider in providers:
130
+ status = random.choices(
131
+ ["operational", "degraded", "maintenance"],
132
+ weights=[85, 10, 5]
133
+ )[0]
134
+
135
+ uptime = random.uniform(97, 99.99) if status == "operational" else random.uniform(85, 97)
136
+ response_time = provider["base_price"] + random.randint(-20, 40)
137
+
138
+ result.append({
139
+ "name": provider["name"],
140
+ "type": provider["type"],
141
+ "category": category,
142
+ "region": provider["region"],
143
+ "status": status,
144
+ "uptime": round(uptime, 2),
145
+ "response_time_ms": response_time,
146
+ "requests_today": random.randint(50000, 2000000),
147
+ "requests_per_minute": random.randint(100, 5000),
148
+ "error_rate": round(random.uniform(0.01, 2.5), 2),
149
+ "last_check": datetime.now().isoformat(),
150
+ "api_version": f"v{random.randint(1,3)}.{random.randint(0,9)}",
151
+ "rate_limit": random.randint(100, 10000),
152
+ "endpoint": f"https://api.{provider['name'].lower().replace(' ', '').replace('.', '')}.com"
153
  })
154
+
155
+ return result
156
+
157
+ def generate_crypto_prices():
158
+ """Generate realistic cryptocurrency prices"""
159
+ result = []
160
+
161
+ for crypto in CRYPTOCURRENCIES:
162
+ change_24h = random.uniform(-15, 18)
163
+ change_7d = random.uniform(-25, 30)
164
+ volume = crypto["base_price"] * random.uniform(1e9, 5e10)
165
+ market_cap = crypto["base_price"] * random.uniform(1e9, 8e11)
166
+
167
+ result.append({
168
+ "symbol": crypto["symbol"],
169
+ "name": crypto["name"],
170
+ "category": crypto["category"],
171
+ "price": round(crypto["base_price"] * (1 + random.uniform(-0.08, 0.08)), 4),
172
+ "change_24h": round(change_24h, 2),
173
+ "change_7d": round(change_7d, 2),
174
+ "volume_24h": int(volume),
175
+ "market_cap": int(market_cap),
176
+ "circulating_supply": int(market_cap / crypto["base_price"]),
177
+ "total_supply": int(market_cap / crypto["base_price"] * random.uniform(1, 1.5)),
178
+ "ath": round(crypto["base_price"] * random.uniform(1.5, 8), 2),
179
+ "atl": round(crypto["base_price"] * random.uniform(0.01, 0.3), 4),
180
+ "rank": CRYPTOCURRENCIES.index(crypto) + 1,
181
+ "last_updated": datetime.now().isoformat()
182
  })
183
+
184
+ return sorted(result, key=lambda x: x["market_cap"], reverse=True)
185
+
186
+ def generate_system_health():
187
+ """Enhanced system health data"""
188
+ providers = generate_all_providers()
189
+
190
+ healthy = len([p for p in providers if p["status"] == "operational"])
191
+ degraded = len([p for p in providers if p["status"] == "degraded"])
192
+ down = len([p for p in providers if p["status"] == "maintenance"])
193
+ total = len(providers)
194
+
195
+ return {
196
+ "status": "healthy" if healthy / total > 0.9 else "degraded",
197
+ "timestamp": datetime.now().isoformat(),
198
+ "uptime_percentage": round((healthy / total) * 100, 2),
199
+ "summary": {
200
+ "total_providers": total,
201
+ "operational": healthy,
202
+ "degraded": degraded,
203
+ "maintenance": down,
204
+ "total_requests_today": sum(p["requests_today"] for p in providers),
205
+ "avg_response_time": round(sum(p["response_time_ms"] for p in providers) / total, 1),
206
+ "total_api_calls": random.randint(10000000, 50000000)
207
+ },
208
+ "by_category": {
209
+ category: {
210
+ "total": len([p for p in providers if p["category"] == category]),
211
+ "operational": len([p for p in providers if p["category"] == category and p["status"] == "operational"])
212
+ }
213
+ for category in PROVIDERS.keys()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  }
215
+ }
216
 
217
+ @app.get("/")
218
+ async def root():
219
+ return {
220
+ "name": "Crypto API Monitor Pro",
221
+ "version": "2.0.0",
222
+ "total_providers": sum(len(p) for p in PROVIDERS.values()),
223
+ "categories": list(PROVIDERS.keys()),
224
+ "endpoints": ["/health", "/api/providers", "/api/crypto/prices", "/api/stats", "/ws/live"]
225
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
+ @app.get("/health")
228
+ @app.get("/api/health")
229
+ async def health():
230
+ return generate_system_health()
231
+
232
+ @app.get("/api/providers")
233
+ async def get_providers(category: str = None):
234
+ providers = generate_all_providers()
235
+ if category:
236
+ providers = [p for p in providers if p["category"] == category]
237
+ return providers
238
+
239
+ @app.get("/api/providers/{name}")
240
+ async def get_provider_detail(name: str):
241
+ providers = generate_all_providers()
242
+ provider = next((p for p in providers if p["name"].lower() == name.lower()), None)
243
+ if provider:
244
+ provider["history"] = [
245
+ {"timestamp": (datetime.now() - timedelta(minutes=i*5)).isoformat(),
246
+ "response_time": random.randint(80, 250),
247
+ "status": "operational"}
248
+ for i in range(12)
249
  ]
250
+ return provider
251
+
252
+ @app.get("/api/crypto/prices")
253
+ async def get_crypto_prices(limit: int = 20):
254
+ return generate_crypto_prices()[:limit]
255
+
256
+ @app.get("/api/crypto/{symbol}")
257
+ async def get_crypto_detail(symbol: str):
258
+ prices = generate_crypto_prices()
259
+ crypto = next((c for c in prices if c["symbol"].upper() == symbol.upper()), None)
260
+ if crypto:
261
+ crypto["price_history"] = [
262
+ {"timestamp": (datetime.now() - timedelta(hours=i)).isoformat(),
263
+ "price": crypto["price"] * random.uniform(0.95, 1.05)}
264
+ for i in range(24)
265
+ ]
266
+ return crypto
267
+
268
+ @app.get("/api/stats")
269
+ async def get_stats():
270
+ providers = generate_all_providers()
271
+ prices = generate_crypto_prices()
272
+
273
+ return {
274
+ "providers": {
275
+ "total": len(providers),
276
+ "by_type": {
277
+ "exchanges": len([p for p in providers if p["type"] == "Exchange"]),
278
+ "data_providers": len([p for p in providers if "Data" in p["type"]]),
279
+ "analytics": len([p for p in providers if p["type"] == "Analytics"]),
280
+ "defi": len([p for p in providers if p["category"] == "defi"]),
281
+ },
282
+ "by_status": {
283
+ "operational": len([p for p in providers if p["status"] == "operational"]),
284
+ "degraded": len([p for p in providers if p["status"] == "degraded"]),
285
+ "maintenance": len([p for p in providers if p["status"] == "maintenance"]),
286
+ }
287
+ },
288
+ "market": {
289
+ "total_market_cap": sum(c["market_cap"] for c in prices),
290
+ "total_volume_24h": sum(c["volume_24h"] for c in prices),
291
+ "avg_change_24h": round(sum(c["change_24h"] for c in prices) / len(prices), 2),
292
+ "btc_dominance": round((prices[0]["market_cap"] / sum(c["market_cap"] for c in prices)) * 100, 2),
293
+ },
294
+ "performance": {
295
+ "total_requests": sum(p["requests_today"] for p in providers),
296
+ "avg_response_time": round(sum(p["response_time_ms"] for p in providers) / len(providers), 1),
297
+ "uptime": round((len([p for p in providers if p["status"] == "operational"]) / len(providers)) * 100, 2),
298
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
 
301
+ @app.get("/api/categories")
302
+ async def get_categories():
303
+ return [
304
+ {"id": k, "name": k.title(), "count": len(v), "providers": [p["name"] for p in v]}
305
+ for k, v in PROVIDERS.items()
306
+ ]
307
+
308
+ @app.get("/api/alerts")
309
+ async def get_alerts():
310
+ providers = generate_all_providers()
311
+ alerts = []
312
+
313
+ for p in providers:
314
+ if p["status"] == "degraded":
315
+ alerts.append({
316
+ "severity": "warning",
317
+ "provider": p["name"],
318
+ "message": f"{p['name']} is experiencing degraded performance",
319
+ "timestamp": datetime.now().isoformat()
320
+ })
321
+ if p["response_time_ms"] > 200:
322
+ alerts.append({
323
+ "severity": "info",
324
+ "provider": p["name"],
325
+ "message": f"High response time: {p['response_time_ms']}ms",
326
+ "timestamp": datetime.now().isoformat()
327
+ })
328
+
329
+ return alerts[:10]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ @app.websocket("/ws/live")
332
+ async def websocket_endpoint(websocket: WebSocket):
333
+ await manager.connect(websocket)
334
  try:
335
+ while True:
336
+ await asyncio.sleep(3)
337
+ health = generate_system_health()
338
+ await websocket.send_json({
339
+ "type": "status_update",
340
+ "data": health,
341
+ "timestamp": datetime.now().isoformat()
342
+ })
343
+ except WebSocketDisconnect:
344
+ manager.disconnect(websocket)
 
 
 
 
345
 
346
+ @app.get("/dashboard", response_class=HTMLResponse)
347
+ async def dashboard():
348
+ try:
349
+ with open("index.html", "r", encoding="utf-8") as f:
350
+ return HTMLResponse(content=f.read())
351
+ except:
352
+ return HTMLResponse("<h1>Dashboard not found</h1>", 404)
353
 
354
  if __name__ == "__main__":
355
+ print("🚀 Crypto API Monitor Pro v2.0")
356
+ print(f"📊 {sum(len(p) for p in PROVIDERS.values())} Providers")
357
+ print("🌐 http://localhost:8000/dashboard")
358
+ uvicorn.run(app, host="0.0.0.0", port=8000)
backend/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/backend/__pycache__/__init__.cpython-313.pyc and b/backend/__pycache__/__init__.cpython-313.pyc differ
 
config.js CHANGED
@@ -143,4 +143,4 @@ console.log('🚀 Crypto API Monitor - Configuration loaded:', {
143
  CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
144
  apiBase: CONFIG.API_BASE,
145
  wsBase: CONFIG.WS_BASE,
146
- });
 
143
  CONFIG.IS_LOCALHOST ? 'Localhost' : 'Custom Deployment',
144
  apiBase: CONFIG.API_BASE,
145
  wsBase: CONFIG.WS_BASE,
146
+ });
config.py CHANGED
@@ -1,199 +1,320 @@
1
- #!/usr/bin/env python3
2
  """
3
- Configuration constants for Crypto Data Aggregator
4
- All configuration in one place - no hardcoded values
5
  """
6
 
 
7
  import os
 
8
  from pathlib import Path
 
9
 
10
- # ==================== DIRECTORIES ====================
11
- BASE_DIR = Path(__file__).parent
12
- DATA_DIR = BASE_DIR / "data"
13
- LOG_DIR = BASE_DIR / "logs"
14
- DB_DIR = DATA_DIR / "database"
15
-
16
- # Create directories if they don't exist
17
- for directory in [DATA_DIR, LOG_DIR, DB_DIR]:
18
- directory.mkdir(parents=True, exist_ok=True)
19
-
20
- # ==================== DATABASE ====================
21
- DATABASE_PATH = DB_DIR / "crypto_aggregator.db"
22
- DATABASE_BACKUP_DIR = DATA_DIR / "backups"
23
- DATABASE_BACKUP_DIR.mkdir(parents=True, exist_ok=True)
24
-
25
- # ==================== API ENDPOINTS (NO KEYS REQUIRED) ====================
26
-
27
- # CoinGecko API (Free, no key)
28
- COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3"
29
- COINGECKO_ENDPOINTS = {
30
- "ping": "/ping",
31
- "price": "/simple/price",
32
- "coins_list": "/coins/list",
33
- "coins_markets": "/coins/markets",
34
- "coin_data": "/coins/{id}",
35
- "trending": "/search/trending",
36
- "global": "/global",
37
- }
38
-
39
- # CoinCap API (Free, no key)
40
- COINCAP_BASE_URL = "https://api.coincap.io/v2"
41
- COINCAP_ENDPOINTS = {
42
- "assets": "/assets",
43
- "asset_detail": "/assets/{id}",
44
- "asset_history": "/assets/{id}/history",
45
- "markets": "/markets",
46
- "rates": "/rates",
47
- }
48
-
49
- # Binance Public API (Free, no key)
50
- BINANCE_BASE_URL = "https://api.binance.com/api/v3"
51
- BINANCE_ENDPOINTS = {
52
- "ping": "/ping",
53
- "ticker_24h": "/ticker/24hr",
54
- "ticker_price": "/ticker/price",
55
- "klines": "/klines",
56
- "trades": "/trades",
57
- }
58
-
59
- # Alternative.me Fear & Greed Index (Free, no key)
60
- ALTERNATIVE_ME_URL = "https://api.alternative.me/fng/"
61
-
62
- # ==================== RSS FEEDS ====================
63
- RSS_FEEDS = {
64
- "coindesk": "https://www.coindesk.com/arc/outboundfeeds/rss/",
65
- "cointelegraph": "https://cointelegraph.com/rss",
66
- "bitcoin_magazine": "https://bitcoinmagazine.com/.rss/full/",
67
- "decrypt": "https://decrypt.co/feed",
68
- "bitcoinist": "https://bitcoinist.com/feed/",
69
- }
70
-
71
- # ==================== REDDIT ENDPOINTS (NO AUTH) ====================
72
- REDDIT_ENDPOINTS = {
73
- "cryptocurrency": "https://www.reddit.com/r/cryptocurrency/.json",
74
- "bitcoin": "https://www.reddit.com/r/bitcoin/.json",
75
- "ethtrader": "https://www.reddit.com/r/ethtrader/.json",
76
- "cryptomarkets": "https://www.reddit.com/r/CryptoMarkets/.json",
77
- }
78
-
79
- # ==================== HUGGING FACE MODELS ====================
80
- HUGGINGFACE_MODELS = {
81
- "sentiment_twitter": "cardiffnlp/twitter-roberta-base-sentiment-latest",
82
- "sentiment_financial": "ProsusAI/finbert",
83
- "summarization": "facebook/bart-large-cnn",
84
- }
85
-
86
- # ==================== DATA COLLECTION SETTINGS ====================
87
- COLLECTION_INTERVALS = {
88
- "price_data": 300, # 5 minutes in seconds
89
- "news_data": 1800, # 30 minutes in seconds
90
- "sentiment_data": 1800, # 30 minutes in seconds
91
- }
92
-
93
- # Number of top cryptocurrencies to track
94
- TOP_COINS_LIMIT = 100
95
-
96
- # Request timeout in seconds
97
- REQUEST_TIMEOUT = 10
98
-
99
- # Max retries for failed requests
100
- MAX_RETRIES = 3
101
-
102
- # ==================== CACHE SETTINGS ====================
103
- CACHE_TTL = 300 # 5 minutes in seconds
104
- CACHE_MAX_SIZE = 1000 # Maximum number of cached items
105
-
106
- # ==================== LOGGING SETTINGS ====================
107
- LOG_FILE = LOG_DIR / "crypto_aggregator.log"
108
- LOG_LEVEL = "INFO"
109
- LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
110
- LOG_MAX_BYTES = 10 * 1024 * 1024 # 10 MB
111
- LOG_BACKUP_COUNT = 5
112
-
113
- # ==================== GRADIO SETTINGS ====================
114
- GRADIO_SHARE = False
115
- GRADIO_SERVER_NAME = "0.0.0.0"
116
- GRADIO_SERVER_PORT = 7860
117
- GRADIO_THEME = "default"
118
- AUTO_REFRESH_INTERVAL = 30 # seconds
119
-
120
- # ==================== DATA VALIDATION ====================
121
- MIN_PRICE = 0.0
122
- MAX_PRICE = 1000000000.0 # 1 billion
123
- MIN_VOLUME = 0.0
124
- MIN_MARKET_CAP = 0.0
125
-
126
- # ==================== CHART SETTINGS ====================
127
- CHART_TIMEFRAMES = {
128
- "1d": {"days": 1, "interval": "1h"},
129
- "7d": {"days": 7, "interval": "4h"},
130
- "30d": {"days": 30, "interval": "1d"},
131
- "90d": {"days": 90, "interval": "1d"},
132
- "1y": {"days": 365, "interval": "1w"},
133
- }
134
-
135
- # Technical indicators
136
- MA_PERIODS = [7, 30] # Moving Average periods
137
- RSI_PERIOD = 14 # RSI period
138
-
139
- # ==================== SENTIMENT THRESHOLDS ====================
140
- SENTIMENT_LABELS = {
141
- "very_negative": (-1.0, -0.6),
142
- "negative": (-0.6, -0.2),
143
- "neutral": (-0.2, 0.2),
144
- "positive": (0.2, 0.6),
145
- "very_positive": (0.6, 1.0),
146
- }
147
-
148
- # ==================== AI ANALYSIS SETTINGS ====================
149
- AI_CONFIDENCE_THRESHOLD = 0.6
150
- PREDICTION_HORIZON_HOURS = 72
151
-
152
- # ==================== USER AGENT ====================
153
- USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
154
-
155
- # ==================== RATE LIMITING ====================
156
- RATE_LIMIT_CALLS = 50
157
- RATE_LIMIT_PERIOD = 60 # seconds
158
-
159
- # ==================== COIN SYMBOLS ====================
160
- # Top cryptocurrencies to focus on
161
- FOCUS_COINS = [
162
- "bitcoin", "ethereum", "binancecoin", "ripple", "cardano",
163
- "solana", "polkadot", "dogecoin", "avalanche-2", "polygon",
164
- "chainlink", "uniswap", "litecoin", "cosmos", "algorand"
165
- ]
166
-
167
- COIN_SYMBOL_MAPPING = {
168
- "bitcoin": "BTC",
169
- "ethereum": "ETH",
170
- "binancecoin": "BNB",
171
- "ripple": "XRP",
172
- "cardano": "ADA",
173
- "solana": "SOL",
174
- "polkadot": "DOT",
175
- "dogecoin": "DOGE",
176
- "avalanche-2": "AVAX",
177
- "polygon": "MATIC",
178
- }
179
-
180
- # ==================== ERROR MESSAGES ====================
181
- ERROR_MESSAGES = {
182
- "api_unavailable": "API service is currently unavailable. Using cached data.",
183
- "no_data": "No data available at the moment.",
184
- "database_error": "Database operation failed.",
185
- "network_error": "Network connection error.",
186
- "invalid_input": "Invalid input provided.",
187
- }
188
-
189
- # ==================== SUCCESS MESSAGES ====================
190
- SUCCESS_MESSAGES = {
191
- "data_collected": "Data successfully collected and saved.",
192
- "cache_cleared": "Cache cleared successfully.",
193
- "database_initialized": "Database initialized successfully.",
194
- }
195
-
196
- # Backward-compatible alias so that `from config import config`
197
- # returns the config module itself.
198
- import sys as _sys
199
- config = _sys.modules[__name__]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ Configuration Module for Crypto API Monitor
3
+ Loads and manages API registry from all_apis_merged_2025.json
4
  """
5
 
6
+ import json
7
  import os
8
+ from typing import Dict, List, Any, Optional
9
  from pathlib import Path
10
+ from utils.logger import setup_logger
11
 
12
+ logger = setup_logger("config")
13
+
14
+
15
+ class ProviderConfig:
16
+ """Provider configuration data class"""
17
+
18
+ def __init__(
19
+ self,
20
+ name: str,
21
+ category: str,
22
+ endpoint_url: str,
23
+ requires_key: bool = False,
24
+ api_key: Optional[str] = None,
25
+ rate_limit_type: Optional[str] = None,
26
+ rate_limit_value: Optional[int] = None,
27
+ timeout_ms: int = 10000,
28
+ priority_tier: int = 3,
29
+ health_check_endpoint: Optional[str] = None
30
+ ):
31
+ self.name = name
32
+ self.category = category
33
+ self.endpoint_url = endpoint_url
34
+ self.requires_key = requires_key
35
+ self.api_key = api_key
36
+ self.rate_limit_type = rate_limit_type
37
+ self.rate_limit_value = rate_limit_value
38
+ self.timeout_ms = timeout_ms
39
+ self.priority_tier = priority_tier
40
+ self.health_check_endpoint = health_check_endpoint or endpoint_url
41
+
42
+ def to_dict(self) -> Dict:
43
+ """Convert to dictionary"""
44
+ return {
45
+ "name": self.name,
46
+ "category": self.category,
47
+ "endpoint_url": self.endpoint_url,
48
+ "requires_key": self.requires_key,
49
+ "api_key_masked": self._mask_key() if self.api_key else None,
50
+ "rate_limit_type": self.rate_limit_type,
51
+ "rate_limit_value": self.rate_limit_value,
52
+ "timeout_ms": self.timeout_ms,
53
+ "priority_tier": self.priority_tier,
54
+ "health_check_endpoint": self.health_check_endpoint
55
+ }
56
+
57
+ def _mask_key(self) -> str:
58
+ """Mask API key for security"""
59
+ if not self.api_key:
60
+ return None
61
+ if len(self.api_key) < 10:
62
+ return "***"
63
+ return f"{self.api_key[:8]}...{self.api_key[-4:]}"
64
+
65
+
66
+ class Config:
67
+ """Configuration manager for API resources"""
68
+
69
+ def __init__(self, config_file: str = "all_apis_merged_2025.json"):
70
+ """
71
+ Initialize configuration
72
+
73
+ Args:
74
+ config_file: Path to JSON configuration file
75
+ """
76
+ self.base_dir = Path(__file__).parent
77
+ self.config_file = self.base_dir / config_file
78
+ self.providers: Dict[str, ProviderConfig] = {}
79
+ self.api_keys: Dict[str, List[str]] = {}
80
+ self.cors_proxies: List[str] = [
81
+ 'https://api.allorigins.win/get?url=',
82
+ 'https://proxy.cors.sh/',
83
+ 'https://proxy.corsfix.com/?url=',
84
+ 'https://api.codetabs.com/v1/proxy?quest=',
85
+ 'https://thingproxy.freeboard.io/fetch/'
86
+ ]
87
+
88
+ # Load environment variables
89
+ self._load_env_keys()
90
+
91
+ # Load from JSON
92
+ self._load_from_json()
93
+
94
+ # Build provider registry
95
+ self._build_provider_registry()
96
+
97
+ def _load_env_keys(self):
98
+ """Load API keys from environment variables"""
99
+ env_keys = {
100
+ 'etherscan': [
101
+ os.getenv('ETHERSCAN_KEY_1', ''),
102
+ os.getenv('ETHERSCAN_KEY_2', '')
103
+ ],
104
+ 'bscscan': [os.getenv('BSCSCAN_KEY', '')],
105
+ 'tronscan': [os.getenv('TRONSCAN_KEY', '')],
106
+ 'coinmarketcap': [
107
+ os.getenv('COINMARKETCAP_KEY_1', ''),
108
+ os.getenv('COINMARKETCAP_KEY_2', '')
109
+ ],
110
+ 'newsapi': [os.getenv('NEWSAPI_KEY', '')],
111
+ 'cryptocompare': [os.getenv('CRYPTOCOMPARE_KEY', '')],
112
+ 'huggingface': [os.getenv('HUGGINGFACE_KEY', '')]
113
+ }
114
+
115
+ # Filter out empty keys
116
+ for provider, keys in env_keys.items():
117
+ self.api_keys[provider] = [k for k in keys if k]
118
+
119
+ def _load_from_json(self):
120
+ """Load configuration from JSON file"""
121
+ try:
122
+ if not self.config_file.exists():
123
+ logger.warning(f"Config file not found: {self.config_file}")
124
+ return
125
+
126
+ with open(self.config_file, 'r', encoding='utf-8') as f:
127
+ data = json.load(f)
128
+
129
+ # Load discovered keys
130
+ discovered_keys = data.get('discovered_keys', {})
131
+ for provider, keys in discovered_keys.items():
132
+ if isinstance(keys, list):
133
+ # Merge with env keys, preferring env keys
134
+ if provider not in self.api_keys or not self.api_keys[provider]:
135
+ self.api_keys[provider] = keys
136
+ else:
137
+ # Add discovered keys that aren't in env
138
+ for key in keys:
139
+ if key not in self.api_keys[provider]:
140
+ self.api_keys[provider].append(key)
141
+
142
+ logger.info(f"Loaded {len(self.api_keys)} provider keys from config")
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error loading config file: {e}")
146
+
147
+ def _build_provider_registry(self):
148
+ """Build provider registry from configuration"""
149
+
150
+ # Market Data Providers
151
+ self.providers['CoinGecko'] = ProviderConfig(
152
+ name='CoinGecko',
153
+ category='market_data',
154
+ endpoint_url='https://api.coingecko.com/api/v3',
155
+ requires_key=False,
156
+ rate_limit_type='per_minute',
157
+ rate_limit_value=50,
158
+ timeout_ms=10000,
159
+ priority_tier=1,
160
+ health_check_endpoint='https://api.coingecko.com/api/v3/ping'
161
+ )
162
+
163
+ # CoinMarketCap
164
+ cmc_keys = self.api_keys.get('coinmarketcap', [])
165
+ self.providers['CoinMarketCap'] = ProviderConfig(
166
+ name='CoinMarketCap',
167
+ category='market_data',
168
+ endpoint_url='https://pro-api.coinmarketcap.com/v1',
169
+ requires_key=True,
170
+ api_key=cmc_keys[0] if cmc_keys else None,
171
+ rate_limit_type='per_hour',
172
+ rate_limit_value=100,
173
+ timeout_ms=10000,
174
+ priority_tier=2,
175
+ health_check_endpoint='https://pro-api.coinmarketcap.com/v1/cryptocurrency/map?limit=1'
176
+ )
177
+
178
+ # Blockchain Explorers
179
+ etherscan_keys = self.api_keys.get('etherscan', [])
180
+ self.providers['Etherscan'] = ProviderConfig(
181
+ name='Etherscan',
182
+ category='blockchain_explorers',
183
+ endpoint_url='https://api.etherscan.io/api',
184
+ requires_key=True,
185
+ api_key=etherscan_keys[0] if etherscan_keys else None,
186
+ rate_limit_type='per_second',
187
+ rate_limit_value=5,
188
+ timeout_ms=10000,
189
+ priority_tier=1,
190
+ health_check_endpoint='https://api.etherscan.io/api?module=stats&action=ethsupply'
191
+ )
192
+
193
+ bscscan_keys = self.api_keys.get('bscscan', [])
194
+ self.providers['BscScan'] = ProviderConfig(
195
+ name='BscScan',
196
+ category='blockchain_explorers',
197
+ endpoint_url='https://api.bscscan.com/api',
198
+ requires_key=True,
199
+ api_key=bscscan_keys[0] if bscscan_keys else None,
200
+ rate_limit_type='per_second',
201
+ rate_limit_value=5,
202
+ timeout_ms=10000,
203
+ priority_tier=1,
204
+ health_check_endpoint='https://api.bscscan.com/api?module=stats&action=bnbsupply'
205
+ )
206
+
207
+ tronscan_keys = self.api_keys.get('tronscan', [])
208
+ self.providers['TronScan'] = ProviderConfig(
209
+ name='TronScan',
210
+ category='blockchain_explorers',
211
+ endpoint_url='https://apilist.tronscanapi.com/api',
212
+ requires_key=True,
213
+ api_key=tronscan_keys[0] if tronscan_keys else None,
214
+ rate_limit_type='per_minute',
215
+ rate_limit_value=60,
216
+ timeout_ms=10000,
217
+ priority_tier=2,
218
+ health_check_endpoint='https://apilist.tronscanapi.com/api/system/status'
219
+ )
220
+
221
+ # News APIs
222
+ self.providers['CryptoPanic'] = ProviderConfig(
223
+ name='CryptoPanic',
224
+ category='news',
225
+ endpoint_url='https://cryptopanic.com/api/v1',
226
+ requires_key=False,
227
+ rate_limit_type='per_hour',
228
+ rate_limit_value=100,
229
+ timeout_ms=10000,
230
+ priority_tier=2,
231
+ health_check_endpoint='https://cryptopanic.com/api/v1/posts/?auth_token=free&public=true'
232
+ )
233
+
234
+ newsapi_keys = self.api_keys.get('newsapi', [])
235
+ self.providers['NewsAPI'] = ProviderConfig(
236
+ name='NewsAPI',
237
+ category='news',
238
+ endpoint_url='https://newsdata.io/api/1',
239
+ requires_key=True,
240
+ api_key=newsapi_keys[0] if newsapi_keys else None,
241
+ rate_limit_type='per_day',
242
+ rate_limit_value=200,
243
+ timeout_ms=10000,
244
+ priority_tier=3,
245
+ health_check_endpoint='https://newsdata.io/api/1/news?category=business'
246
+ )
247
+
248
+ # Sentiment APIs
249
+ self.providers['AlternativeMe'] = ProviderConfig(
250
+ name='AlternativeMe',
251
+ category='sentiment',
252
+ endpoint_url='https://api.alternative.me',
253
+ requires_key=False,
254
+ rate_limit_type='per_minute',
255
+ rate_limit_value=60,
256
+ timeout_ms=10000,
257
+ priority_tier=2,
258
+ health_check_endpoint='https://api.alternative.me/fng/'
259
+ )
260
+
261
+ # CryptoCompare
262
+ cryptocompare_keys = self.api_keys.get('cryptocompare', [])
263
+ self.providers['CryptoCompare'] = ProviderConfig(
264
+ name='CryptoCompare',
265
+ category='market_data',
266
+ endpoint_url='https://min-api.cryptocompare.com/data',
267
+ requires_key=True,
268
+ api_key=cryptocompare_keys[0] if cryptocompare_keys else None,
269
+ rate_limit_type='per_hour',
270
+ rate_limit_value=250,
271
+ timeout_ms=10000,
272
+ priority_tier=2,
273
+ health_check_endpoint='https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=USD'
274
+ )
275
+
276
+ logger.info(f"Built provider registry with {len(self.providers)} providers")
277
+
278
+ def get_provider(self, name: str) -> Optional[ProviderConfig]:
279
+ """Get provider configuration by name"""
280
+ return self.providers.get(name)
281
+
282
+ def get_all_providers(self) -> List[ProviderConfig]:
283
+ """Get all provider configurations"""
284
+ return list(self.providers.values())
285
+
286
+ def get_providers_by_category(self, category: str) -> List[ProviderConfig]:
287
+ """Get providers by category"""
288
+ return [p for p in self.providers.values() if p.category == category]
289
+
290
+ def get_providers_by_tier(self, tier: int) -> List[ProviderConfig]:
291
+ """Get providers by priority tier"""
292
+ return [p for p in self.providers.values() if p.priority_tier == tier]
293
+
294
+ def get_api_key(self, provider: str, index: int = 0) -> Optional[str]:
295
+ """Get API key for provider"""
296
+ keys = self.api_keys.get(provider.lower(), [])
297
+ if keys and 0 <= index < len(keys):
298
+ return keys[index]
299
+ return None
300
+
301
+ def get_categories(self) -> List[str]:
302
+ """Get all unique categories"""
303
+ return list(set(p.category for p in self.providers.values()))
304
+
305
+ def stats(self) -> Dict[str, Any]:
306
+ """Get configuration statistics"""
307
+ return {
308
+ 'total_providers': len(self.providers),
309
+ 'categories': len(self.get_categories()),
310
+ 'providers_with_keys': sum(1 for p in self.providers.values() if p.requires_key),
311
+ 'tier1_count': len(self.get_providers_by_tier(1)),
312
+ 'tier2_count': len(self.get_providers_by_tier(2)),
313
+ 'tier3_count': len(self.get_providers_by_tier(3)),
314
+ 'api_keys_loaded': len(self.api_keys),
315
+ 'categories_list': self.get_categories()
316
+ }
317
+
318
+
319
+ # Global config instance
320
+ config = Config()
dashboard.html CHANGED
@@ -492,90 +492,39 @@ Market is bullish today</textarea>
492
  <script>
493
  async function loadData() {
494
  try {
495
- // Show loading state
496
- const tbody = document.getElementById('providersTable');
497
- if (tbody) {
498
- tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;"><div class="loading"></div><div style="margin-top: 10px; color: #6c757d;">در حال بارگذاری...</div></td></tr>';
499
- }
500
- if (document.getElementById('lastUpdate')) {
501
- document.getElementById('lastUpdate').textContent = 'در حال بارگذاری...';
502
- }
503
-
504
  // Load status
505
  const statusRes = await fetch('/api/status');
506
- if (!statusRes.ok) {
507
- throw new Error(`خطا در دریافت وضعیت: ${statusRes.status} ${statusRes.statusText}`);
508
- }
509
  const status = await statusRes.json();
510
 
511
- if (!status || typeof status.total_providers === 'undefined') {
512
- throw new Error('داده‌های وضعیت نامعتبر است');
513
- }
514
-
515
- if (document.getElementById('totalAPIs')) {
516
- document.getElementById('totalAPIs').textContent = status.total_providers || 0;
517
- }
518
- if (document.getElementById('onlineAPIs')) {
519
- document.getElementById('onlineAPIs').textContent = status.online || 0;
520
- }
521
- if (document.getElementById('offlineAPIs')) {
522
- document.getElementById('offlineAPIs').textContent = status.offline || 0;
523
- }
524
- if (document.getElementById('avgResponse')) {
525
- document.getElementById('avgResponse').textContent = (status.avg_response_time_ms || 0) + 'ms';
526
- }
527
- if (document.getElementById('lastUpdate')) {
528
- document.getElementById('lastUpdate').textContent = status.timestamp ? new Date(status.timestamp).toLocaleString('fa-IR') : 'نامشخص';
529
- }
530
 
531
  // Load providers
532
  const providersRes = await fetch('/api/providers');
533
- if (!providersRes.ok) {
534
- throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status} ${providersRes.statusText}`);
535
- }
536
  const providers = await providersRes.json();
537
 
538
- if (!providers || !Array.isArray(providers)) {
539
- throw new Error('لیست APIها نامعتبر است');
540
- }
541
-
542
- if (tbody) {
543
- if (providers.length === 0) {
544
- tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px; color: #6c757d;">هیچ APIای یافت نشد</td></tr>';
545
- } else {
546
- tbody.innerHTML = providers.map(p => {
547
- let responseClass = 'response-fast';
548
- const responseTime = p.response_time_ms || p.avg_response_time_ms || 0;
549
- if (responseTime > 3000) responseClass = 'response-slow';
550
- else if (responseTime > 1000) responseClass = 'response-medium';
551
-
552
- return `
553
- <tr>
554
- <td><strong style="font-size: 15px;">${p.name || 'نامشخص'}</strong></td>
555
- <td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category || 'نامشخص'}</span></td>
556
- <td><span class="status-badge status-${p.status || 'unknown'}">${(p.status || 'unknown').toUpperCase()}</span></td>
557
- <td><span class="response-time ${responseClass}">${responseTime}ms</span></td>
558
- <td style="color: #6c757d; font-size: 13px;">${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString('fa-IR') : 'نامشخص'}</td>
559
- </tr>
560
- `}).join('');
561
- }
562
- }
563
 
564
  } catch (error) {
565
  console.error('Error loading data:', error);
566
- const tbody = document.getElementById('providersTable');
567
- if (tbody) {
568
- tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 40px; color: #ef4444;">
569
- <div style="font-size: 24px; margin-bottom: 10px;">❌</div>
570
- <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری داده‌ها</div>
571
- <div style="font-size: 14px; color: #6c757d; margin-bottom: 15px;">${error.message || 'خطای نامشخص'}</div>
572
- <button onclick="loadData()" style="padding: 10px 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; border-radius: 12px; color: white; cursor: pointer; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;">تلاش مجدد</button>
573
- </td></tr>`;
574
- }
575
- if (document.getElementById('lastUpdate')) {
576
- document.getElementById('lastUpdate').textContent = 'خطا در بارگذاری';
577
- }
578
- alert('❌ خطا در بارگذاری داده‌ها:\n' + (error.message || 'خطای نامشخص'));
579
  }
580
  }
581
 
 
492
  <script>
493
  async function loadData() {
494
  try {
 
 
 
 
 
 
 
 
 
495
  // Load status
496
  const statusRes = await fetch('/api/status');
 
 
 
497
  const status = await statusRes.json();
498
 
499
+ document.getElementById('totalAPIs').textContent = status.total_providers;
500
+ document.getElementById('onlineAPIs').textContent = status.online;
501
+ document.getElementById('offlineAPIs').textContent = status.offline;
502
+ document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms';
503
+ document.getElementById('lastUpdate').textContent = new Date(status.timestamp).toLocaleString();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
 
505
  // Load providers
506
  const providersRes = await fetch('/api/providers');
 
 
 
507
  const providers = await providersRes.json();
508
 
509
+ const tbody = document.getElementById('providersTable');
510
+ tbody.innerHTML = providers.map(p => {
511
+ let responseClass = 'response-fast';
512
+ if (p.response_time_ms > 3000) responseClass = 'response-slow';
513
+ else if (p.response_time_ms > 1000) responseClass = 'response-medium';
514
+
515
+ return `
516
+ <tr>
517
+ <td><strong style="font-size: 15px;">${p.name}</strong></td>
518
+ <td><span style="background: #f8f9fa; padding: 4px 10px; border-radius: 8px; font-size: 12px; font-weight: 600;">${p.category}</span></td>
519
+ <td><span class="status-badge status-${p.status}">${p.status.toUpperCase()}</span></td>
520
+ <td><span class="response-time ${responseClass}">${p.response_time_ms}ms</span></td>
521
+ <td style="color: #6c757d; font-size: 13px;">${new Date(p.last_fetch).toLocaleTimeString()}</td>
522
+ </tr>
523
+ `}).join('');
 
 
 
 
 
 
 
 
 
 
524
 
525
  } catch (error) {
526
  console.error('Error loading data:', error);
527
+ alert('Error loading data: ' + error.message);
 
 
 
 
 
 
 
 
 
 
 
 
528
  }
529
  }
530
 
database.py CHANGED
@@ -1,665 +1,480 @@
1
- #!/usr/bin/env python3
2
  """
3
- Database module for Crypto Data Aggregator
4
- Complete CRUD operations with the exact schema specified
5
  """
6
 
7
  import sqlite3
8
- import threading
9
  import json
 
 
10
  from datetime import datetime, timedelta
11
- from typing import List, Dict, Optional, Any, Tuple
12
  from contextlib import contextmanager
13
- import logging
14
 
15
- import config
16
-
17
- # Setup logging
18
- logging.basicConfig(
19
- level=getattr(logging, config.LOG_LEVEL),
20
- format=config.LOG_FORMAT,
21
- handlers=[
22
- logging.FileHandler(config.LOG_FILE),
23
- logging.StreamHandler()
24
- ]
25
- )
26
  logger = logging.getLogger(__name__)
27
 
28
 
29
- class CryptoDatabase:
30
- """
31
- Database manager for cryptocurrency data with full CRUD operations
32
- Thread-safe implementation using context managers
33
- """
34
 
35
- def __init__(self, db_path: str = None):
36
- """Initialize database with connection pooling"""
37
- self.db_path = str(db_path or config.DATABASE_PATH)
38
- self._local = threading.local()
39
  self._init_database()
40
- logger.info(f"Database initialized at {self.db_path}")
41
 
42
  @contextmanager
43
  def get_connection(self):
44
- """Get thread-safe database connection"""
45
- if not hasattr(self._local, 'conn'):
46
- self._local.conn = sqlite3.connect(
47
- self.db_path,
48
- check_same_thread=False,
49
- timeout=30.0
50
- )
51
- self._local.conn.row_factory = sqlite3.Row
52
-
53
  try:
54
- yield self._local.conn
 
55
  except Exception as e:
56
- self._local.conn.rollback()
57
  logger.error(f"Database error: {e}")
58
  raise
 
 
59
 
60
  def _init_database(self):
61
- """Initialize all database tables with exact schema"""
62
  with self.get_connection() as conn:
63
  cursor = conn.cursor()
64
 
65
- # ==================== PRICES TABLE ====================
66
  cursor.execute("""
67
- CREATE TABLE IF NOT EXISTS prices (
68
  id INTEGER PRIMARY KEY AUTOINCREMENT,
69
- symbol TEXT NOT NULL,
70
- name TEXT,
71
- price_usd REAL NOT NULL,
72
- volume_24h REAL,
73
- market_cap REAL,
74
- percent_change_1h REAL,
75
- percent_change_24h REAL,
76
- percent_change_7d REAL,
77
- rank INTEGER,
78
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
79
  )
80
  """)
81
 
82
- # ==================== NEWS TABLE ====================
83
  cursor.execute("""
84
- CREATE TABLE IF NOT EXISTS news (
85
  id INTEGER PRIMARY KEY AUTOINCREMENT,
86
- title TEXT NOT NULL,
87
- summary TEXT,
88
- url TEXT UNIQUE,
89
- source TEXT,
90
- sentiment_score REAL,
91
- sentiment_label TEXT,
92
- related_coins TEXT,
93
- published_date DATETIME,
94
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
95
  )
96
  """)
97
 
98
- # ==================== MARKET ANALYSIS TABLE ====================
99
  cursor.execute("""
100
- CREATE TABLE IF NOT EXISTS market_analysis (
101
  id INTEGER PRIMARY KEY AUTOINCREMENT,
102
- symbol TEXT NOT NULL,
103
- timeframe TEXT,
104
- trend TEXT,
105
- support_level REAL,
106
- resistance_level REAL,
107
- prediction TEXT,
108
- confidence REAL,
109
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
 
 
110
  )
111
  """)
112
 
113
- # ==================== USER QUERIES TABLE ====================
114
  cursor.execute("""
115
- CREATE TABLE IF NOT EXISTS user_queries (
116
  id INTEGER PRIMARY KEY AUTOINCREMENT,
117
- query TEXT,
118
- result_count INTEGER,
119
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
 
 
 
 
120
  )
121
  """)
122
 
123
- # ==================== CREATE INDEXES ====================
124
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_symbol ON prices(symbol)")
125
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_timestamp ON prices(timestamp)")
126
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_prices_rank ON prices(rank)")
127
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_url ON news(url)")
128
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_published ON news(published_date)")
129
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_news_sentiment ON news(sentiment_label)")
130
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_analysis_symbol ON market_analysis(symbol)")
131
- cursor.execute("CREATE INDEX IF NOT EXISTS idx_analysis_timestamp ON market_analysis(timestamp)")
132
-
133
- conn.commit()
134
- logger.info("Database tables and indexes created successfully")
135
-
136
- # ==================== PRICES CRUD OPERATIONS ====================
137
-
138
- def save_price(self, price_data: Dict[str, Any]) -> bool:
139
- """
140
- Save a single price record
141
-
142
- Args:
143
- price_data: Dictionary containing price information
144
-
145
- Returns:
146
- bool: True if successful, False otherwise
147
- """
148
- try:
149
- with self.get_connection() as conn:
150
- cursor = conn.cursor()
151
- cursor.execute("""
152
- INSERT INTO prices
153
- (symbol, name, price_usd, volume_24h, market_cap,
154
- percent_change_1h, percent_change_24h, percent_change_7d, rank)
155
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
156
- """, (
157
- price_data.get('symbol'),
158
- price_data.get('name'),
159
- price_data.get('price_usd', 0.0),
160
- price_data.get('volume_24h'),
161
- price_data.get('market_cap'),
162
- price_data.get('percent_change_1h'),
163
- price_data.get('percent_change_24h'),
164
- price_data.get('percent_change_7d'),
165
- price_data.get('rank')
166
- ))
167
- conn.commit()
168
- return True
169
- except Exception as e:
170
- logger.error(f"Error saving price: {e}")
171
- return False
172
-
173
- def save_prices_batch(self, prices: List[Dict[str, Any]]) -> int:
174
- """
175
- Save multiple price records in batch (minimum 100 records for efficiency)
176
 
177
- Args:
178
- prices: List of price dictionaries
 
 
 
 
 
 
 
 
 
 
 
179
 
180
- Returns:
181
- int: Number of records saved
182
- """
183
- saved_count = 0
184
- try:
185
- with self.get_connection() as conn:
186
- cursor = conn.cursor()
187
- for price_data in prices:
188
- try:
189
- cursor.execute("""
190
- INSERT INTO prices
191
- (symbol, name, price_usd, volume_24h, market_cap,
192
- percent_change_1h, percent_change_24h, percent_change_7d, rank)
193
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
194
- """, (
195
- price_data.get('symbol'),
196
- price_data.get('name'),
197
- price_data.get('price_usd', 0.0),
198
- price_data.get('volume_24h'),
199
- price_data.get('market_cap'),
200
- price_data.get('percent_change_1h'),
201
- price_data.get('percent_change_24h'),
202
- price_data.get('percent_change_7d'),
203
- price_data.get('rank')
204
- ))
205
- saved_count += 1
206
- except Exception as e:
207
- logger.warning(f"Error saving individual price: {e}")
208
- continue
209
- conn.commit()
210
- logger.info(f"Batch saved {saved_count} price records")
211
- except Exception as e:
212
- logger.error(f"Error in batch save: {e}")
213
- return saved_count
214
 
215
- def get_latest_prices(self, limit: int = 100) -> List[Dict[str, Any]]:
216
- """
217
- Get latest prices for top cryptocurrencies
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- Args:
220
- limit: Maximum number of records to return
221
 
222
- Returns:
223
- List of price dictionaries
224
- """
225
- try:
226
- with self.get_connection() as conn:
227
- cursor = conn.cursor()
228
- cursor.execute("""
229
- SELECT DISTINCT ON (symbol) *
230
- FROM prices
231
- WHERE timestamp >= datetime('now', '-1 hour')
232
- ORDER BY symbol, timestamp DESC, rank ASC
233
- LIMIT ?
234
- """, (limit,))
235
-
236
- # SQLite doesn't support DISTINCT ON, use subquery instead
237
- cursor.execute("""
238
- SELECT p1.*
239
- FROM prices p1
240
- INNER JOIN (
241
- SELECT symbol, MAX(timestamp) as max_ts
242
- FROM prices
243
- WHERE timestamp >= datetime('now', '-1 hour')
244
- GROUP BY symbol
245
- ) p2 ON p1.symbol = p2.symbol AND p1.timestamp = p2.max_ts
246
- ORDER BY p1.rank ASC, p1.market_cap DESC
247
  LIMIT ?
248
- """, (limit,))
249
-
250
- return [dict(row) for row in cursor.fetchall()]
251
- except Exception as e:
252
- logger.error(f"Error getting latest prices: {e}")
253
- return []
254
-
255
- def get_price_history(self, symbol: str, hours: int = 24) -> List[Dict[str, Any]]:
256
- """
257
- Get price history for a specific symbol
258
-
259
- Args:
260
- symbol: Cryptocurrency symbol
261
- hours: Number of hours to look back
262
-
263
- Returns:
264
- List of price dictionaries
265
- """
266
- try:
267
- with self.get_connection() as conn:
268
- cursor = conn.cursor()
269
- cursor.execute("""
270
- SELECT * FROM prices
271
- WHERE symbol = ?
272
- AND timestamp >= datetime('now', '-' || ? || ' hours')
273
- ORDER BY timestamp ASC
274
- """, (symbol, hours))
275
- return [dict(row) for row in cursor.fetchall()]
276
- except Exception as e:
277
- logger.error(f"Error getting price history: {e}")
278
- return []
279
-
280
- def get_top_gainers(self, limit: int = 10) -> List[Dict[str, Any]]:
281
- """Get top gaining cryptocurrencies in last 24h"""
282
- try:
283
- with self.get_connection() as conn:
284
- cursor = conn.cursor()
285
- cursor.execute("""
286
- SELECT p1.*
287
- FROM prices p1
288
- INNER JOIN (
289
- SELECT symbol, MAX(timestamp) as max_ts
290
- FROM prices
291
- WHERE timestamp >= datetime('now', '-1 hour')
292
- GROUP BY symbol
293
- ) p2 ON p1.symbol = p2.symbol AND p1.timestamp = p2.max_ts
294
- WHERE p1.percent_change_24h IS NOT NULL
295
- ORDER BY p1.percent_change_24h DESC
296
  LIMIT ?
297
- """, (limit,))
298
- return [dict(row) for row in cursor.fetchall()]
299
- except Exception as e:
300
- logger.error(f"Error getting top gainers: {e}")
301
- return []
302
-
303
- def delete_old_prices(self, days: int = 30) -> int:
304
- """
305
- Delete price records older than specified days
306
-
307
- Args:
308
- days: Number of days to keep
309
-
310
- Returns:
311
- Number of deleted records
312
- """
313
- try:
314
- with self.get_connection() as conn:
315
- cursor = conn.cursor()
316
- cursor.execute("""
317
- DELETE FROM prices
318
- WHERE timestamp < datetime('now', '-' || ? || ' days')
319
- """, (days,))
320
- conn.commit()
321
- deleted = cursor.rowcount
322
- logger.info(f"Deleted {deleted} old price records")
323
- return deleted
324
- except Exception as e:
325
- logger.error(f"Error deleting old prices: {e}")
326
- return 0
327
 
328
- # ==================== NEWS CRUD OPERATIONS ====================
329
 
330
- def save_news(self, news_data: Dict[str, Any]) -> bool:
331
- """
332
- Save a single news record
333
-
334
- Args:
335
- news_data: Dictionary containing news information
336
-
337
- Returns:
338
- bool: True if successful, False otherwise
339
- """
340
- try:
341
- with self.get_connection() as conn:
342
- cursor = conn.cursor()
343
- cursor.execute("""
344
- INSERT OR IGNORE INTO news
345
- (title, summary, url, source, sentiment_score,
346
- sentiment_label, related_coins, published_date)
347
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
348
- """, (
349
- news_data.get('title'),
350
- news_data.get('summary'),
351
- news_data.get('url'),
352
- news_data.get('source'),
353
- news_data.get('sentiment_score'),
354
- news_data.get('sentiment_label'),
355
- json.dumps(news_data.get('related_coins', [])),
356
- news_data.get('published_date')
357
- ))
358
- conn.commit()
359
- return True
360
- except Exception as e:
361
- logger.error(f"Error saving news: {e}")
362
- return False
363
-
364
- def get_latest_news(self, limit: int = 50, sentiment: Optional[str] = None) -> List[Dict[str, Any]]:
365
- """
366
- Get latest news articles
367
-
368
- Args:
369
- limit: Maximum number of articles
370
- sentiment: Filter by sentiment label (optional)
371
 
372
- Returns:
373
- List of news dictionaries
374
- """
375
- try:
376
- with self.get_connection() as conn:
377
- cursor = conn.cursor()
378
-
379
- if sentiment:
380
- cursor.execute("""
381
- SELECT * FROM news
382
- WHERE sentiment_label = ?
383
- ORDER BY published_date DESC, timestamp DESC
384
- LIMIT ?
385
- """, (sentiment, limit))
386
- else:
387
- cursor.execute("""
388
- SELECT * FROM news
389
- ORDER BY published_date DESC, timestamp DESC
390
- LIMIT ?
391
- """, (limit,))
392
-
393
- results = []
394
- for row in cursor.fetchall():
395
- news_dict = dict(row)
396
- if news_dict.get('related_coins'):
397
- try:
398
- news_dict['related_coins'] = json.loads(news_dict['related_coins'])
399
- except:
400
- news_dict['related_coins'] = []
401
- results.append(news_dict)
402
-
403
- return results
404
- except Exception as e:
405
- logger.error(f"Error getting latest news: {e}")
406
- return []
407
 
408
- def get_news_by_coin(self, coin: str, limit: int = 20) -> List[Dict[str, Any]]:
409
- """Get news related to a specific coin"""
410
- try:
411
- with self.get_connection() as conn:
412
- cursor = conn.cursor()
413
- cursor.execute("""
414
- SELECT * FROM news
415
- WHERE related_coins LIKE ?
416
- ORDER BY published_date DESC
417
- LIMIT ?
418
- """, (f'%{coin}%', limit))
419
-
420
- results = []
421
- for row in cursor.fetchall():
422
- news_dict = dict(row)
423
- if news_dict.get('related_coins'):
424
- try:
425
- news_dict['related_coins'] = json.loads(news_dict['related_coins'])
426
- except:
427
- news_dict['related_coins'] = []
428
- results.append(news_dict)
429
-
430
- return results
431
- except Exception as e:
432
- logger.error(f"Error getting news by coin: {e}")
433
- return []
434
 
435
- def update_news_sentiment(self, news_id: int, sentiment_score: float, sentiment_label: str) -> bool:
436
- """Update sentiment for a news article"""
437
- try:
438
- with self.get_connection() as conn:
439
- cursor = conn.cursor()
440
- cursor.execute("""
441
- UPDATE news
442
- SET sentiment_score = ?, sentiment_label = ?
443
- WHERE id = ?
444
- """, (sentiment_score, sentiment_label, news_id))
445
- conn.commit()
446
- return True
447
- except Exception as e:
448
- logger.error(f"Error updating news sentiment: {e}")
449
- return False
450
 
451
- def delete_old_news(self, days: int = 30) -> int:
452
- """Delete news older than specified days"""
453
- try:
454
- with self.get_connection() as conn:
455
- cursor = conn.cursor()
456
- cursor.execute("""
457
- DELETE FROM news
458
- WHERE timestamp < datetime('now', '-' || ? || ' days')
459
- """, (days,))
460
- conn.commit()
461
- deleted = cursor.rowcount
462
- logger.info(f"Deleted {deleted} old news records")
463
- return deleted
464
- except Exception as e:
465
- logger.error(f"Error deleting old news: {e}")
466
- return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
- # ==================== MARKET ANALYSIS CRUD OPERATIONS ====================
 
 
 
 
469
 
470
- def save_analysis(self, analysis_data: Dict[str, Any]) -> bool:
471
- """Save market analysis"""
472
- try:
473
- with self.get_connection() as conn:
474
- cursor = conn.cursor()
475
- cursor.execute("""
476
- INSERT INTO market_analysis
477
- (symbol, timeframe, trend, support_level, resistance_level,
478
- prediction, confidence)
479
- VALUES (?, ?, ?, ?, ?, ?, ?)
480
- """, (
481
- analysis_data.get('symbol'),
482
- analysis_data.get('timeframe'),
483
- analysis_data.get('trend'),
484
- analysis_data.get('support_level'),
485
- analysis_data.get('resistance_level'),
486
- analysis_data.get('prediction'),
487
- analysis_data.get('confidence')
488
- ))
489
- conn.commit()
490
- return True
491
- except Exception as e:
492
- logger.error(f"Error saving analysis: {e}")
493
- return False
494
 
495
- def get_latest_analysis(self, symbol: str) -> Optional[Dict[str, Any]]:
496
- """Get latest analysis for a symbol"""
497
- try:
498
- with self.get_connection() as conn:
499
- cursor = conn.cursor()
500
- cursor.execute("""
501
- SELECT * FROM market_analysis
502
- WHERE symbol = ?
503
- ORDER BY timestamp DESC
504
- LIMIT 1
505
- """, (symbol,))
506
- row = cursor.fetchone()
507
- return dict(row) if row else None
508
- except Exception as e:
509
- logger.error(f"Error getting latest analysis: {e}")
510
- return None
511
 
512
- def get_all_analyses(self, limit: int = 100) -> List[Dict[str, Any]]:
513
- """Get all market analyses"""
514
- try:
515
- with self.get_connection() as conn:
516
- cursor = conn.cursor()
517
- cursor.execute("""
518
- SELECT * FROM market_analysis
519
- ORDER BY timestamp DESC
520
- LIMIT ?
521
- """, (limit,))
522
- return [dict(row) for row in cursor.fetchall()]
523
- except Exception as e:
524
- logger.error(f"Error getting all analyses: {e}")
525
- return []
526
 
527
- # ==================== USER QUERIES CRUD OPERATIONS ====================
 
 
 
528
 
529
- def log_user_query(self, query: str, result_count: int) -> bool:
530
- """Log a user query"""
531
- try:
532
- with self.get_connection() as conn:
533
- cursor = conn.cursor()
534
- cursor.execute("""
535
- INSERT INTO user_queries (query, result_count)
536
- VALUES (?, ?)
537
- """, (query, result_count))
538
- conn.commit()
539
- return True
540
- except Exception as e:
541
- logger.error(f"Error logging user query: {e}")
542
- return False
543
 
544
- def get_recent_queries(self, limit: int = 50) -> List[Dict[str, Any]]:
545
- """Get recent user queries"""
546
- try:
547
- with self.get_connection() as conn:
548
- cursor = conn.cursor()
549
- cursor.execute("""
550
- SELECT * FROM user_queries
551
- ORDER BY timestamp DESC
552
- LIMIT ?
553
- """, (limit,))
554
- return [dict(row) for row in cursor.fetchall()]
555
- except Exception as e:
556
- logger.error(f"Error getting recent queries: {e}")
557
- return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
558
 
559
- # ==================== UTILITY OPERATIONS ====================
 
 
 
 
 
 
 
 
560
 
561
- def execute_safe_query(self, query: str, params: Tuple = ()) -> List[Dict[str, Any]]:
562
- """
563
- Execute a safe read-only query
 
564
 
565
- Args:
566
- query: SQL query (must start with SELECT)
567
- params: Query parameters
568
 
569
- Returns:
570
- List of result dictionaries
571
- """
572
- try:
573
- # Security: Only allow SELECT queries
574
- if not query.strip().upper().startswith('SELECT'):
575
- logger.warning(f"Attempted non-SELECT query: {query}")
576
- return []
577
-
578
- with self.get_connection() as conn:
579
- cursor = conn.cursor()
580
- cursor.execute(query, params)
581
- return [dict(row) for row in cursor.fetchall()]
582
- except Exception as e:
583
- logger.error(f"Error executing safe query: {e}")
584
- return []
 
 
 
 
 
 
 
585
 
586
- def get_database_stats(self) -> Dict[str, Any]:
587
- """Get database statistics"""
588
- try:
589
- with self.get_connection() as conn:
590
- cursor = conn.cursor()
591
 
592
- stats = {}
 
 
 
 
 
593
 
594
- # Count records in each table
595
- for table in ['prices', 'news', 'market_analysis', 'user_queries']:
596
- cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
597
- stats[f'{table}_count'] = cursor.fetchone()['count']
 
 
598
 
599
- # Get unique symbols
600
- cursor.execute("SELECT COUNT(DISTINCT symbol) as count FROM prices")
601
- stats['unique_symbols'] = cursor.fetchone()['count']
 
 
 
 
 
 
 
 
602
 
603
- # Get latest price update
604
- cursor.execute("SELECT MAX(timestamp) as latest FROM prices")
605
- stats['latest_price_update'] = cursor.fetchone()['latest']
 
606
 
607
- # Get latest news update
608
- cursor.execute("SELECT MAX(timestamp) as latest FROM news")
609
- stats['latest_news_update'] = cursor.fetchone()['latest']
610
 
611
- # Database file size
612
- import os
613
- if os.path.exists(self.db_path):
614
- stats['database_size_bytes'] = os.path.getsize(self.db_path)
615
- stats['database_size_mb'] = stats['database_size_bytes'] / (1024 * 1024)
 
 
 
 
616
 
617
- return stats
618
- except Exception as e:
619
- logger.error(f"Error getting database stats: {e}")
620
- return {}
621
 
622
- def vacuum_database(self) -> bool:
623
- """Vacuum database to reclaim space"""
624
- try:
625
- with self.get_connection() as conn:
626
- conn.execute("VACUUM")
627
- logger.info("Database vacuumed successfully")
628
- return True
629
- except Exception as e:
630
- logger.error(f"Error vacuuming database: {e}")
631
- return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
 
633
- def backup_database(self, backup_path: Optional[str] = None) -> bool:
634
- """Create database backup"""
635
- try:
636
- import shutil
637
- if backup_path is None:
638
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
639
- backup_path = config.DATABASE_BACKUP_DIR / f"backup_{timestamp}.db"
640
-
641
- shutil.copy2(self.db_path, backup_path)
642
- logger.info(f"Database backed up to {backup_path}")
643
- return True
644
- except Exception as e:
645
- logger.error(f"Error backing up database: {e}")
646
- return False
647
 
648
- def close(self):
649
- """Close database connection"""
650
- if hasattr(self._local, 'conn'):
651
- self._local.conn.close()
652
- delattr(self._local, 'conn')
653
- logger.info("Database connection closed")
654
 
 
 
 
 
 
655
 
656
- # Singleton instance
657
- _db_instance = None
658
 
 
 
 
 
 
 
659
 
660
- def get_database() -> CryptoDatabase:
661
- """Get database singleton instance"""
662
- global _db_instance
663
- if _db_instance is None:
664
- _db_instance = CryptoDatabase()
665
- return _db_instance
 
 
1
  """
2
+ SQLite Database Module for Persistent Storage
3
+ Stores health metrics, incidents, and historical data
4
  """
5
 
6
  import sqlite3
 
7
  import json
8
+ import logging
9
+ from typing import List, Dict, Optional, Tuple
10
  from datetime import datetime, timedelta
11
+ from pathlib import Path
12
  from contextlib import contextmanager
13
+ from monitor import HealthCheckResult, HealthStatus
14
 
 
 
 
 
 
 
 
 
 
 
 
15
  logger = logging.getLogger(__name__)
16
 
17
 
18
+ class Database:
19
+ """SQLite database manager for metrics and history"""
 
 
 
20
 
21
+ def __init__(self, db_path: str = "data/health_metrics.db"):
22
+ """Initialize database connection"""
23
+ self.db_path = Path(db_path)
24
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
25
  self._init_database()
 
26
 
27
  @contextmanager
28
  def get_connection(self):
29
+ """Context manager for database connections"""
30
+ conn = sqlite3.connect(self.db_path)
31
+ conn.row_factory = sqlite3.Row # Enable column access by name
 
 
 
 
 
 
32
  try:
33
+ yield conn
34
+ conn.commit()
35
  except Exception as e:
36
+ conn.rollback()
37
  logger.error(f"Database error: {e}")
38
  raise
39
+ finally:
40
+ conn.close()
41
 
42
  def _init_database(self):
43
+ """Initialize database schema"""
44
  with self.get_connection() as conn:
45
  cursor = conn.cursor()
46
 
47
+ # Status log table
48
  cursor.execute("""
49
+ CREATE TABLE IF NOT EXISTS status_log (
50
  id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ provider_name TEXT NOT NULL,
52
+ category TEXT NOT NULL,
53
+ status TEXT NOT NULL,
54
+ response_time REAL,
55
+ status_code INTEGER,
56
+ error_message TEXT,
57
+ endpoint_tested TEXT,
58
+ timestamp REAL NOT NULL,
59
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 
60
  )
61
  """)
62
 
63
+ # Response times table (aggregated)
64
  cursor.execute("""
65
+ CREATE TABLE IF NOT EXISTS response_times (
66
  id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ provider_name TEXT NOT NULL,
68
+ avg_response_time REAL NOT NULL,
69
+ min_response_time REAL NOT NULL,
70
+ max_response_time REAL NOT NULL,
71
+ sample_count INTEGER NOT NULL,
72
+ period_start TIMESTAMP NOT NULL,
73
+ period_end TIMESTAMP NOT NULL,
74
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
 
75
  )
76
  """)
77
 
78
+ # Incidents table
79
  cursor.execute("""
80
+ CREATE TABLE IF NOT EXISTS incidents (
81
  id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ provider_name TEXT NOT NULL,
83
+ category TEXT NOT NULL,
84
+ incident_type TEXT NOT NULL,
85
+ description TEXT,
86
+ severity TEXT,
87
+ start_time TIMESTAMP NOT NULL,
88
+ end_time TIMESTAMP,
89
+ duration_seconds INTEGER,
90
+ resolved BOOLEAN DEFAULT 0,
91
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
92
  )
93
  """)
94
 
95
+ # Alerts table
96
  cursor.execute("""
97
+ CREATE TABLE IF NOT EXISTS alerts (
98
  id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ provider_name TEXT NOT NULL,
100
+ alert_type TEXT NOT NULL,
101
+ message TEXT,
102
+ threshold_value REAL,
103
+ actual_value REAL,
104
+ triggered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
105
+ acknowledged BOOLEAN DEFAULT 0
106
  )
107
  """)
108
 
109
+ # Configuration table
110
+ cursor.execute("""
111
+ CREATE TABLE IF NOT EXISTS configuration (
112
+ key TEXT PRIMARY KEY,
113
+ value TEXT NOT NULL,
114
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
115
+ )
116
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
 
118
+ # Create indexes
119
+ cursor.execute("""
120
+ CREATE INDEX IF NOT EXISTS idx_status_log_provider
121
+ ON status_log(provider_name, timestamp)
122
+ """)
123
+ cursor.execute("""
124
+ CREATE INDEX IF NOT EXISTS idx_status_log_timestamp
125
+ ON status_log(timestamp)
126
+ """)
127
+ cursor.execute("""
128
+ CREATE INDEX IF NOT EXISTS idx_incidents_provider
129
+ ON incidents(provider_name, start_time)
130
+ """)
131
 
132
+ logger.info("Database initialized successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ def save_health_check(self, result: HealthCheckResult):
135
+ """Save a single health check result"""
136
+ with self.get_connection() as conn:
137
+ cursor = conn.cursor()
138
+ cursor.execute("""
139
+ INSERT INTO status_log
140
+ (provider_name, category, status, response_time, status_code,
141
+ error_message, endpoint_tested, timestamp)
142
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
143
+ """, (
144
+ result.provider_name,
145
+ result.category,
146
+ result.status.value,
147
+ result.response_time,
148
+ result.status_code,
149
+ result.error_message,
150
+ result.endpoint_tested,
151
+ result.timestamp
152
+ ))
153
+
154
+ def save_health_checks(self, results: List[HealthCheckResult]):
155
+ """Save multiple health check results"""
156
+ with self.get_connection() as conn:
157
+ cursor = conn.cursor()
158
+ cursor.executemany("""
159
+ INSERT INTO status_log
160
+ (provider_name, category, status, response_time, status_code,
161
+ error_message, endpoint_tested, timestamp)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
163
+ """, [
164
+ (r.provider_name, r.category, r.status.value, r.response_time,
165
+ r.status_code, r.error_message, r.endpoint_tested, r.timestamp)
166
+ for r in results
167
+ ])
168
+ logger.info(f"Saved {len(results)} health check results")
169
+
170
+ def get_recent_status(
171
+ self,
172
+ provider_name: Optional[str] = None,
173
+ hours: int = 24,
174
+ limit: int = 1000
175
+ ) -> List[Dict]:
176
+ """Get recent status logs"""
177
+ with self.get_connection() as conn:
178
+ cursor = conn.cursor()
179
 
180
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
181
 
182
+ if provider_name:
183
+ query = """
184
+ SELECT * FROM status_log
185
+ WHERE provider_name = ? AND created_at >= ?
186
+ ORDER BY timestamp DESC
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  LIMIT ?
188
+ """
189
+ cursor.execute(query, (provider_name, cutoff_time, limit))
190
+ else:
191
+ query = """
192
+ SELECT * FROM status_log
193
+ WHERE created_at >= ?
194
+ ORDER BY timestamp DESC
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  LIMIT ?
196
+ """
197
+ cursor.execute(query, (cutoff_time, limit))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
 
199
+ return [dict(row) for row in cursor.fetchall()]
200
 
201
+ def get_uptime_percentage(
202
+ self,
203
+ provider_name: str,
204
+ hours: int = 24
205
+ ) -> float:
206
+ """Calculate uptime percentage from database"""
207
+ with self.get_connection() as conn:
208
+ cursor = conn.cursor()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ cursor.execute("""
213
+ SELECT
214
+ COUNT(*) as total,
215
+ SUM(CASE WHEN status = 'online' THEN 1 ELSE 0 END) as online
216
+ FROM status_log
217
+ WHERE provider_name = ? AND created_at >= ?
218
+ """, (provider_name, cutoff_time))
219
+
220
+ row = cursor.fetchone()
221
+ if row['total'] > 0:
222
+ return round((row['online'] / row['total']) * 100, 2)
223
+ return 0.0
224
+
225
+ def get_avg_response_time(
226
+ self,
227
+ provider_name: str,
228
+ hours: int = 24
229
+ ) -> float:
230
+ """Get average response time from database"""
231
+ with self.get_connection() as conn:
232
+ cursor = conn.cursor()
 
 
 
 
 
233
 
234
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
+ cursor.execute("""
237
+ SELECT AVG(response_time) as avg_time
238
+ FROM status_log
239
+ WHERE provider_name = ?
240
+ AND created_at >= ?
241
+ AND response_time IS NOT NULL
242
+ """, (provider_name, cutoff_time))
243
+
244
+ row = cursor.fetchone()
245
+ return round(row['avg_time'], 2) if row['avg_time'] else 0.0
246
+
247
+ def create_incident(
248
+ self,
249
+ provider_name: str,
250
+ category: str,
251
+ incident_type: str,
252
+ description: str,
253
+ severity: str = "medium"
254
+ ) -> int:
255
+ """Create a new incident"""
256
+ with self.get_connection() as conn:
257
+ cursor = conn.cursor()
258
+ cursor.execute("""
259
+ INSERT INTO incidents
260
+ (provider_name, category, incident_type, description, severity, start_time)
261
+ VALUES (?, ?, ?, ?, ?, ?)
262
+ """, (provider_name, category, incident_type, description, severity, datetime.now()))
263
+ return cursor.lastrowid
264
+
265
+ def resolve_incident(self, incident_id: int):
266
+ """Resolve an incident"""
267
+ with self.get_connection() as conn:
268
+ cursor = conn.cursor()
269
 
270
+ # Get start time
271
+ cursor.execute("SELECT start_time FROM incidents WHERE id = ?", (incident_id,))
272
+ row = cursor.fetchone()
273
+ if not row:
274
+ return
275
 
276
+ start_time = datetime.fromisoformat(row['start_time'])
277
+ end_time = datetime.now()
278
+ duration = int((end_time - start_time).total_seconds())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
+ cursor.execute("""
281
+ UPDATE incidents
282
+ SET end_time = ?, duration_seconds = ?, resolved = 1
283
+ WHERE id = ?
284
+ """, (end_time, duration, incident_id))
 
 
 
 
 
 
 
 
 
 
 
285
 
286
+ def get_active_incidents(self) -> List[Dict]:
287
+ """Get all active incidents"""
288
+ with self.get_connection() as conn:
289
+ cursor = conn.cursor()
290
+ cursor.execute("""
291
+ SELECT * FROM incidents
292
+ WHERE resolved = 0
293
+ ORDER BY start_time DESC
294
+ """)
295
+ return [dict(row) for row in cursor.fetchall()]
 
 
 
 
296
 
297
+ def get_incident_history(self, hours: int = 24, limit: int = 100) -> List[Dict]:
298
+ """Get incident history"""
299
+ with self.get_connection() as conn:
300
+ cursor = conn.cursor()
301
 
302
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
 
 
 
 
 
 
 
 
 
 
 
 
303
 
304
+ cursor.execute("""
305
+ SELECT * FROM incidents
306
+ WHERE start_time >= ?
307
+ ORDER BY start_time DESC
308
+ LIMIT ?
309
+ """, (cutoff_time, limit))
310
+
311
+ return [dict(row) for row in cursor.fetchall()]
312
+
313
+ def create_alert(
314
+ self,
315
+ provider_name: str,
316
+ alert_type: str,
317
+ message: str,
318
+ threshold_value: Optional[float] = None,
319
+ actual_value: Optional[float] = None
320
+ ) -> int:
321
+ """Create a new alert"""
322
+ with self.get_connection() as conn:
323
+ cursor = conn.cursor()
324
+ cursor.execute("""
325
+ INSERT INTO alerts
326
+ (provider_name, alert_type, message, threshold_value, actual_value)
327
+ VALUES (?, ?, ?, ?, ?)
328
+ """, (provider_name, alert_type, message, threshold_value, actual_value))
329
+ return cursor.lastrowid
330
+
331
+ def get_unacknowledged_alerts(self) -> List[Dict]:
332
+ """Get all unacknowledged alerts"""
333
+ with self.get_connection() as conn:
334
+ cursor = conn.cursor()
335
+ cursor.execute("""
336
+ SELECT * FROM alerts
337
+ WHERE acknowledged = 0
338
+ ORDER BY triggered_at DESC
339
+ """)
340
+ return [dict(row) for row in cursor.fetchall()]
341
 
342
+ def acknowledge_alert(self, alert_id: int):
343
+ """Acknowledge an alert"""
344
+ with self.get_connection() as conn:
345
+ cursor = conn.cursor()
346
+ cursor.execute("""
347
+ UPDATE alerts
348
+ SET acknowledged = 1
349
+ WHERE id = ?
350
+ """, (alert_id,))
351
 
352
+ def aggregate_response_times(self, period_hours: int = 1):
353
+ """Aggregate response times for the period"""
354
+ with self.get_connection() as conn:
355
+ cursor = conn.cursor()
356
 
357
+ period_start = datetime.now() - timedelta(hours=period_hours)
 
 
358
 
359
+ cursor.execute("""
360
+ INSERT INTO response_times
361
+ (provider_name, avg_response_time, min_response_time, max_response_time,
362
+ sample_count, period_start, period_end)
363
+ SELECT
364
+ provider_name,
365
+ AVG(response_time) as avg_time,
366
+ MIN(response_time) as min_time,
367
+ MAX(response_time) as max_time,
368
+ COUNT(*) as count,
369
+ ? as period_start,
370
+ ? as period_end
371
+ FROM status_log
372
+ WHERE created_at >= ? AND response_time IS NOT NULL
373
+ GROUP BY provider_name
374
+ """, (period_start, datetime.now(), period_start))
375
+
376
+ logger.info(f"Aggregated response times for period: {period_start}")
377
+
378
+ def cleanup_old_data(self, days: int = 7):
379
+ """Clean up data older than specified days"""
380
+ with self.get_connection() as conn:
381
+ cursor = conn.cursor()
382
 
383
+ cutoff_date = datetime.now() - timedelta(days=days)
 
 
 
 
384
 
385
+ # Delete old status logs
386
+ cursor.execute("""
387
+ DELETE FROM status_log
388
+ WHERE created_at < ?
389
+ """, (cutoff_date,))
390
+ deleted_logs = cursor.rowcount
391
 
392
+ # Delete old resolved incidents
393
+ cursor.execute("""
394
+ DELETE FROM incidents
395
+ WHERE resolved = 1 AND end_time < ?
396
+ """, (cutoff_date,))
397
+ deleted_incidents = cursor.rowcount
398
 
399
+ # Delete old acknowledged alerts
400
+ cursor.execute("""
401
+ DELETE FROM alerts
402
+ WHERE acknowledged = 1 AND triggered_at < ?
403
+ """, (cutoff_date,))
404
+ deleted_alerts = cursor.rowcount
405
+
406
+ logger.info(
407
+ f"Cleanup: {deleted_logs} logs, {deleted_incidents} incidents, "
408
+ f"{deleted_alerts} alerts older than {days} days"
409
+ )
410
 
411
+ def get_provider_stats(self, provider_name: str, hours: int = 24) -> Dict:
412
+ """Get comprehensive stats for a provider"""
413
+ with self.get_connection() as conn:
414
+ cursor = conn.cursor()
415
 
416
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
 
417
 
418
+ # Get status distribution
419
+ cursor.execute("""
420
+ SELECT
421
+ status,
422
+ COUNT(*) as count
423
+ FROM status_log
424
+ WHERE provider_name = ? AND created_at >= ?
425
+ GROUP BY status
426
+ """, (provider_name, cutoff_time))
427
 
428
+ status_dist = {row['status']: row['count'] for row in cursor.fetchall()}
 
 
 
429
 
430
+ # Get response time stats
431
+ cursor.execute("""
432
+ SELECT
433
+ AVG(response_time) as avg_time,
434
+ MIN(response_time) as min_time,
435
+ MAX(response_time) as max_time,
436
+ COUNT(*) as total_checks
437
+ FROM status_log
438
+ WHERE provider_name = ?
439
+ AND created_at >= ?
440
+ AND response_time IS NOT NULL
441
+ """, (provider_name, cutoff_time))
442
+
443
+ row = cursor.fetchone()
444
+
445
+ return {
446
+ 'provider_name': provider_name,
447
+ 'period_hours': hours,
448
+ 'status_distribution': status_dist,
449
+ 'avg_response_time': round(row['avg_time'], 2) if row['avg_time'] else 0,
450
+ 'min_response_time': round(row['min_time'], 2) if row['min_time'] else 0,
451
+ 'max_response_time': round(row['max_time'], 2) if row['max_time'] else 0,
452
+ 'total_checks': row['total_checks'] or 0,
453
+ 'uptime_percentage': self.get_uptime_percentage(provider_name, hours)
454
+ }
455
+
456
+ def export_to_csv(self, output_path: str, hours: int = 24):
457
+ """Export recent data to CSV"""
458
+ import csv
459
 
460
+ with self.get_connection() as conn:
461
+ cursor = conn.cursor()
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
+ cutoff_time = datetime.now() - timedelta(hours=hours)
 
 
 
 
 
464
 
465
+ cursor.execute("""
466
+ SELECT * FROM status_log
467
+ WHERE created_at >= ?
468
+ ORDER BY timestamp DESC
469
+ """, (cutoff_time,))
470
 
471
+ rows = cursor.fetchall()
 
472
 
473
+ if rows:
474
+ with open(output_path, 'w', newline='') as csvfile:
475
+ writer = csv.DictWriter(csvfile, fieldnames=rows[0].keys())
476
+ writer.writeheader()
477
+ for row in rows:
478
+ writer.writerow(dict(row))
479
 
480
+ logger.info(f"Exported {len(rows)} rows to {output_path}")
 
 
 
 
 
database/__init__.py CHANGED
@@ -1,47 +1,8 @@
1
- """Database package exports.
2
 
3
- This package exposes both the new SQLAlchemy-based ``DatabaseManager`` and the
4
- legacy SQLite-backed ``Database`` class that the existing application modules
5
- still import via ``from database import Database``. During the transition phase
6
- we dynamically load the legacy implementation from the root ``database.py``
7
- module (renamed here as ``legacy_database`` when importing) and fall back to the
8
- new manager if that module is unavailable.
9
- """
10
 
11
- from importlib import util as _importlib_util
12
- from pathlib import Path as _Path
13
- from typing import Optional as _Optional
14
 
15
- from .db_manager import DatabaseManager
16
-
17
- def _load_legacy_database() -> _Optional[type]:
18
- """Load the legacy Database class from the root-level ``database.py`` if it exists."""
19
- legacy_path = _Path(__file__).resolve().parent.parent / "database.py"
20
- if not legacy_path.exists():
21
- return None
22
-
23
- spec = _importlib_util.spec_from_file_location("legacy_database", legacy_path)
24
- if spec is None or spec.loader is None:
25
- return None
26
-
27
- module = _importlib_util.module_from_spec(spec)
28
- try:
29
- spec.loader.exec_module(module)
30
- except Exception:
31
- # If loading the legacy module fails we silently fall back to DatabaseManager
32
- return None
33
-
34
- return getattr(module, "Database", None)
35
-
36
-
37
- _LegacyDatabase = _load_legacy_database()
38
-
39
- if _LegacyDatabase is not None:
40
- Database = _LegacyDatabase
41
- else:
42
- Database = DatabaseManager
43
-
44
- __all__ = ["DatabaseManager", "Database", "CryptoDatabase"]
45
-
46
- # Backward-compatible alias for older imports
47
- CryptoDatabase = Database
 
1
+ """Database module for crypto API monitoring"""
2
 
3
+ from database.db_manager import DatabaseManager
 
 
 
 
 
 
4
 
5
+ # For backward compatibility
6
+ Database = DatabaseManager
 
7
 
8
+ __all__ = ['DatabaseManager', 'Database']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docker-compose.yml CHANGED
@@ -1,102 +1,92 @@
1
  version: '3.8'
2
 
3
  services:
4
- # سرور اصلی Crypto Monitor
5
  crypto-monitor:
6
- build: .
7
- container_name: crypto-monitor-app
 
 
 
 
 
8
  ports:
9
- - "8000:8000"
 
 
10
  environment:
11
- - HOST=0.0.0.0
12
- - PORT=8000
13
- - LOG_LEVEL=INFO
14
- - ENABLE_AUTO_DISCOVERY=false
 
 
 
 
 
 
 
 
 
 
 
 
15
  volumes:
16
- - ./logs:/app/logs
17
  - ./data:/app/data
18
- restart: unless-stopped
19
- networks:
20
- - crypto-network
 
 
 
 
 
 
 
 
 
 
 
21
  healthcheck:
22
- test: ["CMD", "python", "-c", "import requests; requests.get('http://localhost:8000/health')"]
23
  interval: 30s
24
  timeout: 10s
 
25
  retries: 3
26
- start_period: 10s
27
 
28
- # Redis برای Cache (اختیاری)
29
- redis:
30
- image: redis:7-alpine
31
- container_name: crypto-monitor-redis
32
- profiles: ["observability"]
33
- ports:
34
- - "6379:6379"
35
- volumes:
36
- - redis-data:/data
37
  restart: unless-stopped
38
- networks:
39
- - crypto-network
40
- command: redis-server --appendonly yes
41
 
42
- # PostgreSQL برای ذخیره داده‌ها (اختیاری)
43
- postgres:
44
- image: postgres:15-alpine
45
- container_name: crypto-monitor-db
46
- profiles: ["observability"]
47
- environment:
48
- POSTGRES_DB: crypto_monitor
49
- POSTGRES_USER: crypto_user
50
- POSTGRES_PASSWORD: crypto_pass_change_me
51
- ports:
52
- - "5432:5432"
53
- volumes:
54
- - postgres-data:/var/lib/postgresql/data
55
- restart: unless-stopped
56
- networks:
57
- - crypto-network
58
 
59
- # Prometheus برای مانیتورینگ (اختیاری)
60
- prometheus:
61
- image: prom/prometheus:latest
62
- container_name: crypto-monitor-prometheus
63
- profiles: ["observability"]
64
- ports:
65
- - "9090:9090"
66
- volumes:
67
- - ./prometheus.yml:/etc/prometheus/prometheus.yml
68
- - prometheus-data:/prometheus
69
- command:
70
- - '--config.file=/etc/prometheus/prometheus.yml'
71
- - '--storage.tsdb.path=/prometheus'
72
- restart: unless-stopped
73
  networks:
74
  - crypto-network
75
 
76
- # Grafana برای نمایش داده‌ها (اختیاری)
77
- grafana:
78
- image: grafana/grafana:latest
79
- container_name: crypto-monitor-grafana
80
- profiles: ["observability"]
81
- ports:
82
- - "3000:3000"
83
- environment:
84
- - GF_SECURITY_ADMIN_PASSWORD=admin_change_me
85
- - GF_USERS_ALLOW_SIGN_UP=false
86
- volumes:
87
- - grafana-data:/var/lib/grafana
88
- restart: unless-stopped
89
- networks:
90
- - crypto-network
91
- depends_on:
92
- - prometheus
93
 
 
94
  networks:
95
  crypto-network:
96
  driver: bridge
 
97
 
 
98
  volumes:
99
- redis-data:
100
- postgres-data:
101
- prometheus-data:
102
- grafana-data:
 
1
  version: '3.8'
2
 
3
  services:
 
4
  crypto-monitor:
5
+ build:
6
+ context: .
7
+ dockerfile: Dockerfile
8
+ container_name: crypto-api-monitor
9
+ image: crypto-api-monitor:latest
10
+
11
+ # Port mapping (HuggingFace Spaces standard port)
12
  ports:
13
+ - "7860:7860"
14
+
15
+ # Environment variables
16
  environment:
17
+ - PYTHONUNBUFFERED=1
18
+ - ENABLE_SENTIMENT=true
19
+ - HF_REGISTRY_REFRESH_SEC=21600
20
+ - HF_HTTP_TIMEOUT=8.0
21
+ # Add your HuggingFace token here or via .env file
22
+ # - HUGGINGFACE_TOKEN=your_token_here
23
+
24
+ # Sentiment models (optional customization)
25
+ - SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
26
+ - SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
27
+
28
+ # Optional: Load environment variables from .env file
29
+ env_file:
30
+ - .env
31
+
32
+ # Volume mounts for data persistence
33
  volumes:
34
+ # Persist SQLite database
35
  - ./data:/app/data
36
+
37
+ # Persist logs
38
+ - ./logs:/app/logs
39
+
40
+ # Optional: Mount config for live updates during development
41
+ # - ./config.py:/app/config.py
42
+ # - ./all_apis_merged_2025.json:/app/all_apis_merged_2025.json
43
+
44
+ # Optional: Mount frontend files for live updates
45
+ # - ./index.html:/app/index.html
46
+ # - ./hf_console.html:/app/hf_console.html
47
+ # - ./config.js:/app/config.js
48
+
49
+ # Health check configuration
50
  healthcheck:
51
+ test: ["CMD", "curl", "-f", "http://localhost:7860/health"]
52
  interval: 30s
53
  timeout: 10s
54
+ start_period: 40s
55
  retries: 3
 
56
 
57
+ # Restart policy
 
 
 
 
 
 
 
 
58
  restart: unless-stopped
 
 
 
59
 
60
+ # Resource limits (adjust based on your system)
61
+ deploy:
62
+ resources:
63
+ limits:
64
+ cpus: '2.0'
65
+ memory: 4G
66
+ reservations:
67
+ cpus: '0.5'
68
+ memory: 1G
 
 
 
 
 
 
 
69
 
70
+ # Network configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  networks:
72
  - crypto-network
73
 
74
+ # Logging configuration
75
+ logging:
76
+ driver: "json-file"
77
+ options:
78
+ max-size: "10m"
79
+ max-file: "3"
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ # Network definition
82
  networks:
83
  crypto-network:
84
  driver: bridge
85
+ name: crypto-monitor-network
86
 
87
+ # Volume definitions (optional - for named volumes instead of bind mounts)
88
  volumes:
89
+ crypto-data:
90
+ name: crypto-monitor-data
91
+ crypto-logs:
92
+ name: crypto-monitor-logs
index.html CHANGED
@@ -1,1216 +1,896 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Crypto API Monitor</title>
7
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
8
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
- <style>
10
- * { box-sizing: border-box; }
11
- :root {
12
- --bg: #f5f6fb;
13
- --card: #ffffff;
14
- --muted: #6b7280;
15
- --text: #1f2937;
16
- --primary: #2563eb;
17
- --success: #16a34a;
18
- --danger: #dc2626;
19
- --warning: #d97706;
20
- --border: #e5e7eb;
21
- }
22
- body {
23
- margin: 0;
24
- font-family: 'Inter', sans-serif;
25
- background: var(--bg);
26
- color: var(--text);
27
- }
28
- .container {
29
- max-width: 1400px;
30
- margin: 0 auto;
31
- padding: 20px;
32
- }
33
- .header {
34
- background: var(--card);
35
- padding: 18px 24px;
36
- border-radius: 10px;
37
- box-shadow: 0 1px 3px rgba(15,23,42,0.08);
38
- display: flex;
39
- justify-content: space-between;
40
- align-items: center;
41
- flex-wrap: wrap;
42
- gap: 16px;
43
- }
44
- .logo h1 { margin: 0; font-size: 20px; color: var(--primary); }
45
- .header-actions { display: flex; gap: 12px; align-items: center; }
46
- .status-pill {
47
- padding: 6px 12px;
48
- border-radius: 999px;
49
- font-size: 12px;
50
- font-weight: 600;
51
- background: #fee2e2;
52
- color: var(--danger);
53
- }
54
- .btn {
55
- padding: 10px 18px;
56
- border-radius: 8px;
57
- border: none;
58
- font-weight: 600;
59
- font-size: 13px;
60
- cursor: pointer;
61
- background: var(--primary);
62
- color: #fff;
63
- transition: all 0.2s ease;
64
- display: inline-flex;
65
- align-items: center;
66
- gap: 6px;
67
- box-shadow: 0 2px 4px rgba(37,99,235,0.2);
68
- }
69
- .btn:hover {
70
- background: #1d4ed8;
71
- transform: translateY(-1px);
72
- box-shadow: 0 4px 8px rgba(37,99,235,0.3);
73
- }
74
- .btn:active {
75
- transform: translateY(0);
76
- box-shadow: 0 1px 2px rgba(37,99,235,0.2);
77
- }
78
- .btn.secondary {
79
- background: var(--card);
80
- border: 1px solid var(--border);
81
- color: var(--text);
82
- box-shadow: 0 1px 2px rgba(15,23,42,0.1);
83
- }
84
- .btn.secondary:hover {
85
- background: #f9fafb;
86
- border-color: var(--primary);
87
- color: var(--primary);
88
- box-shadow: 0 2px 4px rgba(37,99,235,0.15);
89
- }
90
- .kpi-grid {
91
- display: grid;
92
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
93
- gap: 10px;
94
- margin: 16px 0;
95
- }
96
- .kpi-card {
97
- background: var(--card);
98
- padding: 14px;
99
- border-radius: 8px;
100
- box-shadow: 0 1px 3px rgba(15,23,42,0.08);
101
- border: 1px solid var(--border);
102
- transition: all 0.2s ease;
103
- display: flex;
104
- flex-direction: column;
105
- gap: 8px;
106
- }
107
- .kpi-card:hover {
108
- box-shadow: 0 4px 12px rgba(37,99,235,0.15);
109
- transform: translateY(-2px);
110
- border-color: var(--primary);
111
- }
112
- .kpi-header {
113
- display: flex;
114
- align-items: center;
115
- justify-content: space-between;
116
- gap: 8px;
117
- }
118
- .kpi-icon {
119
- width: 32px;
120
- height: 32px;
121
- padding: 6px;
122
- border-radius: 6px;
123
- display: flex;
124
- align-items: center;
125
- justify-content: center;
126
- flex-shrink: 0;
127
- }
128
- .kpi-icon svg {
129
- width: 20px;
130
- height: 20px;
131
- stroke-width: 2;
132
- }
133
- .kpi-icon.blue { background: #dbeafe; color: #2563eb; }
134
- .kpi-icon.green { background: #dcfce7; color: #16a34a; }
135
- .kpi-icon.orange { background: #fed7aa; color: #ea580c; }
136
- .kpi-icon.purple { background: #e9d5ff; color: #9333ea; }
137
- .kpi-label {
138
- font-size: 11px;
139
- color: var(--muted);
140
- letter-spacing: 0.3px;
141
- text-transform: uppercase;
142
- font-weight: 600;
143
- flex: 1;
144
- }
145
- .kpi-value {
146
- font-size: 24px;
147
- font-weight: 700;
148
- color: var(--text);
149
- line-height: 1.2;
150
- }
151
- .kpi-trend {
152
- font-size: 11px;
153
- color: var(--muted);
154
- display: flex;
155
- align-items: center;
156
- gap: 4px;
157
- }
158
- .tabs {
159
- background: var(--card);
160
- border-radius: 10px;
161
- padding: 8px;
162
- display: flex;
163
- gap: 6px;
164
- overflow-x: auto;
165
- box-shadow: 0 1px 3px rgba(15,23,42,0.08);
166
- }
167
- .tab {
168
- border: none;
169
- background: transparent;
170
- padding: 8px 16px;
171
- border-radius: 6px;
172
- font-weight: 600;
173
- color: var(--muted);
174
- cursor: pointer;
175
- }
176
- .tab.active { background: var(--primary); color: #fff; }
177
- .tab-content { margin-top: 16px; display: none; }
178
- .tab-content.active { display: block; }
179
- .card {
180
- background: var(--card);
181
- border-radius: 12px;
182
- padding: 20px;
183
- box-shadow: 0 1px 3px rgba(15,23,42,0.08);
184
- border: 1px solid var(--border);
185
- margin-bottom: 16px;
186
- transition: all 0.2s ease;
187
- }
188
- .card:hover {
189
- box-shadow: 0 4px 12px rgba(15,23,42,0.12);
190
- border-color: rgba(37,99,235,0.2);
191
- }
192
- .card-header {
193
- display: flex;
194
- justify-content: space-between;
195
- align-items: center;
196
- margin-bottom: 16px;
197
- padding-bottom: 12px;
198
- border-bottom: 2px solid var(--border);
199
- }
200
- .card-title {
201
- font-size: 16px;
202
- font-weight: 700;
203
- color: var(--text);
204
- display: flex;
205
- align-items: center;
206
- gap: 8px;
207
- }
208
- .grid { display: grid; gap: 16px; }
209
- .grid-2 { grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); }
210
- .table-wrapper {
211
- overflow-x: auto;
212
- border-radius: 8px;
213
- border: 1px solid var(--border);
214
- }
215
- table {
216
- width: 100%;
217
- border-collapse: separate;
218
- border-spacing: 0;
219
- font-size: 13px;
220
- }
221
- thead {
222
- background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
223
- position: sticky;
224
- top: 0;
225
- z-index: 10;
226
- }
227
- thead th {
228
- padding: 14px 16px;
229
- text-align: left;
230
- font-weight: 700;
231
- font-size: 11px;
232
- text-transform: uppercase;
233
- letter-spacing: 0.5px;
234
- color: var(--muted);
235
- border-bottom: 2px solid var(--border);
236
- white-space: nowrap;
237
- }
238
- thead th:first-child { border-top-left-radius: 8px; }
239
- thead th:last-child { border-top-right-radius: 8px; }
240
- tbody tr {
241
- transition: all 0.15s ease;
242
- border-bottom: 1px solid #f3f4f6;
243
- }
244
- tbody tr:hover {
245
- background: #f9fafb;
246
- transform: scale(1.001);
247
- box-shadow: 0 2px 8px rgba(37,99,235,0.08);
248
- }
249
- tbody tr:last-child td:first-child { border-bottom-left-radius: 8px; }
250
- tbody tr:last-child td:last-child { border-bottom-right-radius: 8px; }
251
- tbody td {
252
- padding: 14px 16px;
253
- color: var(--text);
254
- vertical-align: middle;
255
- }
256
- tbody td strong {
257
- font-weight: 600;
258
- color: var(--text);
259
- }
260
- tbody td:first-child {
261
- font-weight: 600;
262
- }
263
- .badge {
264
- padding: 5px 12px;
265
- border-radius: 6px;
266
- font-size: 11px;
267
- font-weight: 700;
268
- display: inline-flex;
269
- align-items: center;
270
- gap: 5px;
271
- text-transform: uppercase;
272
- letter-spacing: 0.3px;
273
- border: 1px solid transparent;
274
- transition: all 0.2s ease;
275
- }
276
- .badge.success {
277
- background: #dcfce7;
278
- color: #15803d;
279
- border-color: #86efac;
280
- }
281
- .badge.warn {
282
- background: #fef3c7;
283
- color: #d97706;
284
- border-color: #fde047;
285
- }
286
- .badge.danger {
287
- background: #fee2e2;
288
- color: #b91c1c;
289
- border-color: #fca5a5;
290
- }
291
- .badge.info {
292
- background: #dbeafe;
293
- color: #1e40af;
294
- border-color: #93c5fd;
295
- }
296
- .list {
297
- display: flex;
298
- flex-direction: column;
299
- gap: 10px;
300
- }
301
- .list-item {
302
- padding: 12px;
303
- border-radius: 8px;
304
- border: 1px solid var(--border);
305
- background: var(--card);
306
- transition: all 0.15s ease;
307
- }
308
- .list-item:hover {
309
- background: #f9fafb;
310
- border-color: var(--primary);
311
- transform: translateX(2px);
312
- box-shadow: 0 2px 8px rgba(37,99,235,0.1);
313
- }
314
- .list-item:last-child {
315
- margin-bottom: 0;
316
- }
317
- .resource-search { display: flex; gap: 12px; }
318
- .resource-search input {
319
- flex: 1;
320
- padding: 10px 12px;
321
- border-radius: 6px;
322
- border: 1px solid var(--border);
323
- font-size: 14px;
324
- }
325
- .resource-columns {
326
- display: grid;
327
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
328
- gap: 12px;
329
- margin-top: 16px;
330
- }
331
- .resource-item {
332
- padding: 10px;
333
- border: 1px solid var(--border);
334
- border-radius: 6px;
335
- margin-bottom: 8px;
336
- font-size: 13px;
337
- }
338
- .form-grid {
339
- display: grid;
340
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
341
- gap: 10px;
342
- }
343
- label { font-size: 12px; font-weight: 600; color: var(--muted); }
344
- input, textarea, select {
345
- width: 100%;
346
- padding: 10px 14px;
347
- border: 1px solid var(--border);
348
- border-radius: 8px;
349
- font-size: 14px;
350
- font-family: inherit;
351
- background: var(--card);
352
- color: var(--text);
353
- transition: all 0.2s ease;
354
- }
355
- input:focus, textarea:focus, select:focus {
356
- outline: none;
357
- border-color: var(--primary);
358
- box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
359
- }
360
- input:hover, textarea:hover, select:hover {
361
- border-color: rgba(37,99,235,0.3);
362
- }
363
- textarea {
364
- min-height: 120px;
365
- resize: vertical;
366
- font-family: 'Monaco', 'Courier New', monospace;
367
- font-size: 13px;
368
- }
369
- .toast-container {
370
- position: fixed;
371
- bottom: 20px;
372
- right: 20px;
373
- display: flex;
374
- flex-direction: column;
375
- gap: 10px;
376
- }
377
- .toast {
378
- background: #111827;
379
- color: white;
380
- padding: 12px 16px;
381
- border-radius: 6px;
382
- min-width: 240px;
383
- box-shadow: 0 8px 20px rgba(15,23,42,0.25);
384
- }
385
- @media (max-width: 720px) {
386
- .kpi-grid { grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); }
387
- .grid-2 { grid-template-columns: 1fr; }
388
- .resource-search { flex-direction: column; }
389
- }
390
- </style>
391
- </head>
392
- <body>
393
- <div class="toast-container" id="toastContainer"></div>
394
- <div class="container">
395
- <div class="header">
396
- <div class="logo">
397
- <h1>🚀 Crypto API Monitor</h1>
398
- <div style="font-size:12px; color:var(--muted);">Real API diagnostics + market intelligence</div>
399
- </div>
400
- <div class="header-actions">
401
- <div class="status-pill" id="wsStatus">Connecting...</div>
402
- <button class="btn secondary" onclick="refreshAll()">Refresh</button>
403
- <button class="btn" onclick="loadErrorSummary()">Diagnostics</button>
404
- </div>
405
- </div>
406
-
407
- <div class="kpi-grid">
408
- <div class="kpi-card">
409
- <div class="kpi-header">
410
- <div class="kpi-label">Total APIs</div>
411
- <div class="kpi-icon blue">
412
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
413
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
414
- </svg>
415
- </div>
416
- </div>
417
- <div class="kpi-value" id="kpiTotalAPIs">--</div>
418
- <div class="kpi-trend" id="kpiTotalTrend">Loading…</div>
419
- </div>
420
- <div class="kpi-card">
421
- <div class="kpi-header">
422
- <div class="kpi-label">Online</div>
423
- <div class="kpi-icon green">
424
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
425
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
426
- </svg>
427
- </div>
428
- </div>
429
- <div class="kpi-value" id="kpiOnline">--</div>
430
- <div class="kpi-trend" id="kpiOnlineTrend">Loading…</div>
431
- </div>
432
- <div class="kpi-card">
433
- <div class="kpi-header">
434
- <div class="kpi-label">Avg Response</div>
435
- <div class="kpi-icon orange">
436
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
437
- <path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z"/>
438
- </svg>
439
- </div>
440
- </div>
441
- <div class="kpi-value" id="kpiAvgResponse">--</div>
442
- <div class="kpi-trend" id="kpiResponseTrend">Loading…</div>
443
- </div>
444
- <div class="kpi-card">
445
- <div class="kpi-header">
446
- <div class="kpi-label">Last Update</div>
447
- <div class="kpi-icon purple">
448
- <svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
449
- <path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
450
- </svg>
451
- </div>
452
- </div>
453
- <div class="kpi-value" id="kpiLastUpdate" style="font-size:16px;">--</div>
454
- <div class="kpi-trend">Auto-refresh</div>
455
- </div>
456
- </div>
457
-
458
- <div class="tabs" id="tabBar">
459
- <button class="tab active" data-tab="dashboard" onclick="switchTab(event, 'dashboard')">Dashboard</button>
460
- <button class="tab" data-tab="providers" onclick="switchTab(event, 'providers')">Providers</button>
461
- <button class="tab" data-tab="market" onclick="switchTab(event, 'market')">Market</button>
462
- <button class="tab" data-tab="sentiment" onclick="switchTab(event, 'sentiment')">Sentiment</button>
463
- <button class="tab" data-tab="news" onclick="switchTab(event, 'news')">News</button>
464
- <button class="tab" data-tab="resources" onclick="switchTab(event, 'resources')">Resources & Tools</button>
465
- </div>
466
-
467
- <div class="tab-content active" id="tab-dashboard">
468
- <div class="grid grid-2">
469
- <div class="card">
470
- <div class="card-header">
471
- <h3 class="card-title">Provider Overview</h3>
472
- <button class="btn secondary" onclick="loadProviders()">Reload</button>
473
- </div>
474
- <div class="table-wrapper" style="overflow:auto;">
475
- <table>
476
- <thead>
477
- <tr><th>Provider</th><th>Status</th><th>Response</th><th>Uptime</th></tr>
478
- </thead>
479
- <tbody id="providersTableBody">
480
- <tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">Loading providers…</td></tr>
481
- </tbody>
482
- </table>
483
- </div>
484
- </div>
485
- <div class="card">
486
- <div class="card-header">
487
- <h3 class="card-title">Error Monitor</h3>
488
- <button class="btn secondary" onclick="loadErrorSummary()">Refresh</button>
489
- </div>
490
- <div id="errorSummaryCard" style="font-size:13px; color:var(--muted);">Gathering diagnostics…</div>
491
- <div id="diagnosticsList" class="list" style="margin-top:12px;"></div>
492
- </div>
493
- </div>
494
- <div class="grid grid-2">
495
- <div class="card">
496
- <div class="card-header"><h3 class="card-title">Health Trend</h3></div>
497
- <div style="height:260px;"><canvas id="healthChart"></canvas></div>
498
- </div>
499
- <div class="card">
500
- <div class="card-header"><h3 class="card-title">Status Distribution</h3></div>
501
- <div style="height:260px;"><canvas id="statusChart"></canvas></div>
502
- </div>
503
- </div>
504
- </div>
505
-
506
- <div class="tab-content" id="tab-providers">
507
- <div class="card">
508
- <div class="card-header">
509
- <h3 class="card-title">Providers Detail</h3>
510
- <button class="btn secondary" onclick="loadProviders()">Reload</button>
511
- </div>
512
- <div id="providersDetail"></div>
513
- </div>
514
- </div>
515
-
516
- <div class="tab-content" id="tab-market">
517
- <div class="grid grid-2">
518
- <div class="card">
519
- <div class="card-header"><h3 class="card-title">Global Stats</h3></div>
520
- <div id="marketGlobalStats" class="list"></div>
521
- </div>
522
- <div class="card">
523
- <div class="card-header"><h3 class="card-title">Top Movers</h3></div>
524
- <div style="height:260px;"><canvas id="marketChart"></canvas></div>
525
- </div>
526
- </div>
527
- <div class="grid grid-2">
528
- <div class="card">
529
- <div class="card-header"><h3 class="card-title">Top Assets</h3></div>
530
- <div class="table-wrapper" style="overflow:auto; max-height:320px;">
531
- <table>
532
- <thead><tr><th>Rank</th><th>Name</th><th>Price</th><th>24h</th><th>Market Cap</th></tr></thead>
533
- <tbody id="marketTableBody"></tbody>
534
- </table>
535
- </div>
536
- </div>
537
- <div class="card">
538
- <div class="card-header"><h3 class="card-title">Trending Now</h3></div>
539
- <div id="trendingList" class="list"></div>
540
- </div>
541
- </div>
542
- </div>
543
-
544
- <div class="tab-content" id="tab-sentiment">
545
- <div class="grid grid-2">
546
- <div class="card">
547
- <div class="card-header"><h3 class="card-title">Fear & Greed Index</h3></div>
548
- <div id="sentimentCard" style="font-size:14px; color:var(--muted);">Loading sentiment…</div>
549
- </div>
550
- <div class="card">
551
- <div class="card-header"><h3 class="card-title">DeFi TVL</h3></div>
552
- <div class="table-wrapper" style="overflow:auto; max-height:280px;">
553
- <table>
554
- <thead><tr><th>Protocol</th><th>TVL</th><th>24h</th><th>Chain</th></tr></thead>
555
- <tbody id="defiTableBody"></tbody>
556
- </table>
557
- </div>
558
- </div>
559
- </div>
560
- </div>
561
-
562
- <div class="tab-content" id="tab-news">
563
- <div class="card">
564
- <div class="card-header">
565
- <h3 class="card-title">Latest Headlines</h3>
566
- <button class="btn secondary" onclick="loadNews()">Reload</button>
567
- </div>
568
- <div id="newsList" class="list"></div>
569
- </div>
570
- </div>
571
-
572
- <div class="tab-content" id="tab-resources">
573
- <div class="card">
574
- <div class="card-header">
575
- <h3 class="card-title">Resource Search</h3>
576
- <span style="font-size:12px;color:var(--muted);">Live search across providers + HuggingFace registry</span>
577
- </div>
578
- <div class="resource-search">
579
- <input type="text" id="resourceSearch" placeholder="Search provider, model or dataset..." />
580
- <select id="resourceFilter" onchange="loadResourceSearch()">
581
- <option value="all">All sources</option>
582
- <option value="providers">Providers</option>
583
- <option value="models">Models</option>
584
- <option value="datasets">Datasets</option>
585
- </select>
586
- </div>
587
- <div class="resource-columns">
588
- <div>
589
- <h4>Providers <span id="resourceCountProviders" style="color:var(--muted);"></span></h4>
590
- <div id="resourceResultsProviders"></div>
591
- </div>
592
- <div>
593
- <h4>Models <span id="resourceCountModels" style="color:var(--muted);"></span></h4>
594
- <div id="resourceResultsModels"></div>
595
- </div>
596
- <div>
597
- <h4>Datasets <span id="resourceCountDatasets" style="color:var(--muted);"></span></h4>
598
- <div id="resourceResultsDatasets"></div>
599
- </div>
600
- </div>
601
- </div>
602
- <div class="grid grid-2">
603
- <div class="card">
604
- <div class="card-header"><h3 class="card-title">Export & Backup</h3></div>
605
- <div style="display:flex; gap:10px; flex-wrap:wrap;">
606
- <button class="btn" onclick="handleExport('json')">Export JSON Snapshot</button>
607
- <button class="btn" onclick="handleExport('csv')">Export CSV</button>
608
- <button class="btn secondary" onclick="handleBackup()">Create Backup</button>
609
- </div>
610
- <div id="exportHistory" class="list" style="margin-top:12px;"></div>
611
- </div>
612
- <div class="card">
613
- <div class="card-header"><h3 class="card-title">Import Provider</h3></div>
614
- <form id="importForm" onsubmit="handleImportSingle(event)">
615
- <div class="form-grid">
616
- <div><label>Name</label><input name="name" required></div>
617
- <div><label>Category</label><input name="category" required></div>
618
- <div><label>Endpoint URL</label><input name="endpoint_url" required></div>
619
- <div><label>Health Endpoint</label><input name="health_check_endpoint"></div>
620
- <div><label>Rate Limit</label><input name="rate_limit"></div>
621
- <div><label>Timeout (ms)</label><input name="timeout_ms" type="number" value="10000"></div>
622
- </div>
623
- <label style="margin-top:12px; display:block;">Notes<textarea name="notes"></textarea></label>
624
- <div style="margin-top:12px; display:flex; gap:10px; align-items:center;">
625
- <label style="display:flex; gap:6px; align-items:center; font-size:13px;">
626
- <input type="checkbox" name="requires_key"> Requires API Key
627
- </label>
628
- <input name="api_key" placeholder="API Key (optional)" style="flex:1;">
629
- </div>
630
- <button class="btn" style="margin-top:12px;" type="submit">Import Provider</button>
631
- </form>
632
- <hr style="margin:20px 0; border:none; border-top:1px solid var(--border);">
633
- <label style="display:block; font-size:13px; color:var(--muted); margin-bottom:6px;">Bulk JSON Import</label>
634
- <textarea id="bulkImportTextarea" placeholder='[{"name":"Sample API","category":"market","endpoint_url":"https://..."}]'></textarea>
635
- <button class="btn secondary" style="margin-top:10px;" onclick="handleImportBulk()">Run Bulk Import</button>
636
- </div>
637
- </div>
638
- </div>
639
- </div>
640
-
641
- <script>
642
- const config = {
643
- apiBaseUrl: '',
644
- wsUrl: (() => {
645
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
646
- return `${protocol}//${window.location.host}/ws`;
647
- })(),
648
- autoRefreshInterval: 30000
649
- };
650
-
651
- const state = {
652
- ws: null,
653
- wsConnected: false,
654
- providers: [],
655
- market: { cryptocurrencies: [], global: {} },
656
- trending: [],
657
- sentiment: null,
658
- defi: [],
659
- news: [],
660
- errorSummary: null,
661
- diagnostics: null,
662
- resources: { providers: [], models: [], datasets: [] },
663
- exports: [],
664
- charts: { health: null, status: null, market: null },
665
- currentTab: 'dashboard',
666
- resourceSearchTimeout: null,
667
- lastUpdate: null
668
- };
669
-
670
- function showToast(message, type = 'info') {
671
- const container = document.getElementById('toastContainer');
672
- const toast = document.createElement('div');
673
- toast.className = 'toast';
674
- toast.style.background = type === 'error' ? '#b91c1c' : (type === 'success' ? '#065f46' : '#111827');
675
- toast.textContent = message;
676
- container.appendChild(toast);
677
- setTimeout(() => toast.remove(), 3000);
678
- }
679
-
680
- async function apiCall(endpoint, options = {}) {
681
- const response = await fetch(config.apiBaseUrl + endpoint, options);
682
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
683
- return await response.json();
684
- }
685
-
686
- function initializeWebSocket() {
687
- try {
688
- state.ws = new WebSocket(config.wsUrl);
689
- state.ws.onopen = () => {
690
- state.wsConnected = true;
691
- const pill = document.getElementById('wsStatus');
692
- pill.textContent = 'Connected';
693
- pill.style.background = '#dcfce7';
694
- pill.style.color = '#15803d';
695
- };
696
- state.ws.onclose = () => {
697
- state.wsConnected = false;
698
- const pill = document.getElementById('wsStatus');
699
- pill.textContent = 'Disconnected';
700
- pill.style.background = '#fee2e2';
701
- pill.style.color = '#b91c1c';
702
- };
703
- state.ws.onmessage = (event) => {
704
- const data = JSON.parse(event.data);
705
- if (data.type === 'status_update' && data.data?.providers) {
706
- updateKPIs(data.data.providers);
707
- }
708
- if (data.type === 'market_update' && Array.isArray(data.data)) {
709
- state.market.cryptocurrencies = data.data;
710
- renderMarketTable();
711
- updateMarketChart();
712
- }
713
- if (data.type === 'sentiment_update') {
714
- state.sentiment = data.data;
715
- renderSentiment();
716
- }
717
- };
718
- } catch (err) {
719
- console.error('WebSocket error', err);
720
- }
721
- }
722
-
723
- async function loadInitialData() {
724
- try {
725
- await Promise.all([
726
- loadProviders(),
727
- loadMarket(),
728
- loadTrending(),
729
- loadSentimentData(),
730
- loadDefi(),
731
- loadErrorSummary(),
732
- loadNews(),
733
- loadResourceSearch()
734
- ]);
735
- initializeCharts();
736
- state.lastUpdate = new Date();
737
- updateLastUpdateDisplay();
738
- showToast('Dashboard ready', 'success');
739
- } catch (err) {
740
- console.error(err);
741
- showToast('Failed to load initial data', 'error');
742
- }
743
- }
744
-
745
- async function loadProviders() {
746
- try {
747
- const data = await apiCall('/api/providers');
748
- state.providers = Array.isArray(data) ? data : [];
749
- renderProvidersTable();
750
- renderProvidersDetail();
751
- updateStatusChart();
752
- updateKPIs(state.providers);
753
- } catch (err) {
754
- console.error(err);
755
- document.getElementById('providersTableBody').innerHTML = `<tr><td colspan="4" style="padding:40px; text-align:center;">${err.message}</td></tr>`;
756
- }
757
- }
758
-
759
- function renderProvidersTable() {
760
- const tbody = document.getElementById('providersTableBody');
761
- if (!state.providers.length) {
762
- tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No providers available</td></tr>';
763
- return;
764
- }
765
- tbody.innerHTML = state.providers.slice(0, 8).map(p => `
766
- <tr>
767
- <td>
768
- <div style="font-weight:600;">${p.name || 'Unknown'}</div>
769
- <div style="font-size:11px;color:var(--muted);">${p.category || 'general'}</div>
770
- </td>
771
- <td>${renderStatusBadge(p.status)}</td>
772
- <td>${p.response_time_ms ? `${p.response_time_ms}ms` : '--'}</td>
773
- <td>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</td>
774
- </tr>
775
- `).join('');
776
- }
777
-
778
- function renderProvidersDetail() {
779
- const container = document.getElementById('providersDetail');
780
- if (!state.providers.length) {
781
- container.innerHTML = '<div style="padding:40px; text-align:center; color:var(--muted);">No providers data available</div>';
782
- return;
783
- }
784
- container.innerHTML = `
785
- <div class="table-wrapper">
786
- <table>
787
- <thead>
788
- <tr>
789
- <th>Name</th>
790
- <th>Status</th>
791
- <th>Response Time</th>
792
- <th>Success Rate</th>
793
- <th>Rate Limit</th>
794
- </tr>
795
- </thead>
796
- <tbody>
797
- ${state.providers.map(p => `
798
- <tr>
799
- <td>
800
- <strong>${p.name || 'Unknown'}</strong>
801
- <div style="font-size:11px;color:var(--muted);margin-top:2px;">${p.category || 'general'}</div>
802
- </td>
803
- <td>${renderStatusBadge(p.status)}</td>
804
- <td><strong>${p.avg_response_time_ms ? `${p.avg_response_time_ms}ms` : (p.response_time_ms ? `${p.response_time_ms}ms` : '--')}</strong></td>
805
- <td><strong>${p.uptime ? `${Math.round(p.uptime)}%` : '--'}</strong></td>
806
- <td style="font-size:12px;color:var(--muted);">${p.rate_limit || '—'}</td>
807
- </tr>`).join('')}
808
- </tbody>
809
- </table>
810
- </div>`;
811
- }
812
-
813
- function renderStatusBadge(status = 'unknown') {
814
- const normalized = (status || '').toLowerCase();
815
- let cls = 'badge warn';
816
- if (['online', 'healthy'].includes(normalized)) cls = 'badge success';
817
- if (['offline', 'error'].includes(normalized)) cls = 'badge danger';
818
- return `<span class="${cls}">${status || 'unknown'}</span>`;
819
- }
820
-
821
- function updateKPIs(data) {
822
- const providers = Array.isArray(data) ? data : (data?.providers || []);
823
- const total = providers.length;
824
- const online = providers.filter(p => ['online', 'healthy'].includes((p.status || '').toLowerCase())).length;
825
- const responseTimes = providers.map(p => p.response_time_ms || p.avg_response_time_ms).filter(Boolean);
826
- const avgResponse = responseTimes.length ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) : 0;
827
-
828
- document.getElementById('kpiTotalAPIs').textContent = total;
829
- document.getElementById('kpiTotalTrend').textContent = `${total} tracked providers`;
830
- document.getElementById('kpiOnline').textContent = online;
831
- document.getElementById('kpiOnlineTrend').textContent = total ? `${Math.round((online / total) * 100)}% uptime` : 'No data';
832
- document.getElementById('kpiAvgResponse').textContent = avgResponse ? `${avgResponse}ms` : '--';
833
- document.getElementById('kpiResponseTrend').textContent = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
834
- updateHealthChart(avgResponse);
835
- }
836
-
837
- function updateLastUpdateDisplay() {
838
- if (!state.lastUpdate) return;
839
- document.getElementById('kpiLastUpdate').textContent = state.lastUpdate.toLocaleTimeString();
840
- }
841
-
842
- function initializeCharts() {
843
- const healthCtx = document.getElementById('healthChart').getContext('2d');
844
- const statusCtx = document.getElementById('statusChart').getContext('2d');
845
- const marketCtx = document.getElementById('marketChart').getContext('2d');
846
-
847
- if (state.charts.health) state.charts.health.destroy();
848
- if (state.charts.status) state.charts.status.destroy();
849
- if (state.charts.market) state.charts.market.destroy();
850
-
851
- state.charts.health = new Chart(healthCtx, {
852
- type: 'line',
853
- data: { labels: [], datasets: [{ label: 'Avg Response (ms)', data: [], borderColor: '#2563eb', fill: false }] },
854
- options: { responsive: true, maintainAspectRatio: false }
855
- });
856
-
857
- state.charts.status = new Chart(statusCtx, {
858
- type: 'doughnut',
859
- data: { labels: ['Online', 'Degraded', 'Offline'], datasets: [{ data: [0, 0, 0], backgroundColor: ['#16a34a', '#fcd34d', '#f87171'] }] },
860
- options: { responsive: true, maintainAspectRatio: false }
861
- });
862
-
863
- state.charts.market = new Chart(marketCtx, {
864
- type: 'bar',
865
- data: { labels: [], datasets: [{ label: 'Market Cap (B USD)', data: [], backgroundColor: '#93c5fd' }] },
866
- options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } }
867
- });
868
- }
869
-
870
- function updateHealthChart(value) {
871
- if (!state.charts.health || !value) return;
872
- const chart = state.charts.health;
873
- chart.data.labels.push(new Date().toLocaleTimeString());
874
- chart.data.datasets[0].data.push(value);
875
- if (chart.data.labels.length > 12) {
876
- chart.data.labels.shift();
877
- chart.data.datasets[0].data.shift();
878
- }
879
- chart.update();
880
- }
881
-
882
- function updateStatusChart() {
883
- if (!state.charts.status) return;
884
- const online = state.providers.filter(p => (p.status || '').toLowerCase() === 'online').length;
885
- const degraded = state.providers.filter(p => (p.status || '').toLowerCase() === 'degraded').length;
886
- const offline = state.providers.length - online - degraded;
887
- state.charts.status.data.datasets[0].data = [online, degraded, offline];
888
- state.charts.status.update();
889
- }
890
-
891
- async function loadMarket() {
892
- const data = await apiCall('/api/market');
893
- state.market = data;
894
- renderMarketCards();
895
- renderMarketTable();
896
- updateMarketChart();
897
- }
898
-
899
- function renderMarketCards() {
900
- const stats = state.market.global || {};
901
- const container = document.getElementById('marketGlobalStats');
902
- container.innerHTML = `
903
- <div><strong>Total Market Cap:</strong> $${formatNumber(stats.total_market_cap)}</div>
904
- <div><strong>Total Volume:</strong> $${formatNumber(stats.total_volume)}</div>
905
- <div><strong>BTC Dominance:</strong> ${stats.btc_dominance ? stats.btc_dominance.toFixed(2) + '%' : '--'}</div>
906
- <div><strong>ETH Dominance:</strong> ${stats.eth_dominance ? stats.eth_dominance.toFixed(2) + '%' : '--'}</div>
907
- <div><strong>Active Cryptos:</strong> ${stats.active_cryptocurrencies || '--'}</div>
908
- <div><strong>Markets:</strong> ${stats.markets || '--'}</div>
909
- `;
910
- }
911
-
912
- function renderMarketTable() {
913
- const tbody = document.getElementById('marketTableBody');
914
- const coins = state.market.cryptocurrencies || [];
915
- if (!coins.length) {
916
- tbody.innerHTML = '<tr><td colspan="5" style="text-align:center; padding:40px; color:var(--muted);">Market data unavailable</td></tr>';
917
- return;
918
- }
919
- tbody.innerHTML = coins.slice(0, 12).map(coin => `
920
- <tr>
921
- <td>${coin.rank || coin.market_cap_rank || '-'}</td>
922
- <td>${coin.name} <span style="color:var(--muted);">${coin.symbol}</span></td>
923
- <td>$${formatNumber(coin.price)}</td>
924
- <td style="color:${coin.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${coin.change_24h ? coin.change_24h.toFixed(2) : '0'}%</td>
925
- <td>$${formatNumber(coin.market_cap)}</td>
926
- </tr>
927
- `).join('');
928
- }
929
-
930
- function updateMarketChart() {
931
- if (!state.charts.market) return;
932
- const coins = state.market.cryptocurrencies || [];
933
- const top = coins.slice(0, 5);
934
- state.charts.market.data.labels = top.map(c => c.name);
935
- state.charts.market.data.datasets[0].data = top.map(c => c.market_cap ? (c.market_cap / 1e9).toFixed(2) : 0);
936
- state.charts.market.update();
937
- }
938
-
939
- async function loadTrending() {
940
- const data = await apiCall('/api/trending');
941
- state.trending = data.trending || [];
942
- const list = document.getElementById('trendingList');
943
- if (!state.trending.length) {
944
- list.innerHTML = '<div class="list-item" style="color:var(--muted);">No trending assets</div>';
945
- return;
946
- }
947
- list.innerHTML = state.trending.map(item => `
948
- <div class="list-item">
949
- <div style="font-weight:600;">${item.name} (${item.symbol})</div>
950
- <div style="font-size:12px;color:var(--muted);">Rank: ${item.rank || '—'}</div>
951
- </div>`).join('');
952
- }
953
-
954
- async function loadSentimentData() {
955
- const data = await apiCall('/api/sentiment');
956
- state.sentiment = data.fear_greed_index;
957
- renderSentiment();
958
- }
959
-
960
- function renderSentiment() {
961
- const container = document.getElementById('sentimentCard');
962
- if (!state.sentiment) {
963
- container.textContent = 'No sentiment data';
964
- return;
965
- }
966
- const timestamp = Number(state.sentiment.timestamp);
967
- container.innerHTML = `
968
- <div style="font-size:32px; font-weight:700;">${state.sentiment.value}</div>
969
- <div style="font-size:14px; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">${state.sentiment.classification}</div>
970
- <div style="font-size:12px; color:var(--muted);">Updated: ${timestamp ? new Date(timestamp * 1000).toLocaleString() : '--'}</div>
971
- `;
972
- }
973
-
974
- async function loadDefi() {
975
- const data = await apiCall('/api/defi');
976
- state.defi = data.protocols || [];
977
- const tbody = document.getElementById('defiTableBody');
978
- if (!state.defi.length) {
979
- tbody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:40px; color:var(--muted);">No DeFi data</td></tr>';
980
- return;
981
- }
982
- tbody.innerHTML = state.defi.slice(0, 10).map(proto => `
983
- <tr>
984
- <td>${proto.name}</td>
985
- <td>$${formatNumber(proto.tvl)}</td>
986
- <td style="color:${proto.change_24h >= 0 ? '#16a34a' : '#dc2626'};">${proto.change_24h ? proto.change_24h.toFixed(2) : 0}%</td>
987
- <td>${proto.chain || '—'}</td>
988
- </tr>`).join('');
989
- }
990
-
991
- async function loadNews() {
992
- const data = await apiCall('/api/news');
993
- state.news = data.articles || [];
994
- const list = document.getElementById('newsList');
995
- if (!state.news.length) {
996
- list.innerHTML = '<div class="list-item" style="color:var(--muted);">No news available</div>';
997
- return;
998
- }
999
- list.innerHTML = state.news.slice(0, 12).map(article => `
1000
- <div class="news-item list-item">
1001
- <h4>${article.title}</h4>
1002
- <div class="news-meta">${article.source || 'Unknown'} • ${article.published_at ? new Date(article.published_at).toLocaleString() : ''}</div>
1003
- <p style="margin:6px 0;">${article.description || ''}</p>
1004
- <a href="${article.link}" target="_blank" style="font-size:12px; color:var(--primary);">Read article →</a>
1005
- </div>`).join('');
1006
- }
1007
-
1008
- async function loadErrorSummary() {
1009
- try {
1010
- const [summary, diagnostics] = await Promise.all([
1011
- apiCall('/api/logs/summary'),
1012
- apiCall('/api/diagnostics/errors')
1013
- ]);
1014
- state.errorSummary = summary;
1015
- state.diagnostics = diagnostics;
1016
- renderErrorSummary();
1017
- } catch (err) {
1018
- console.error(err);
1019
- document.getElementById('errorSummaryCard').textContent = 'Failed to load diagnostics';
1020
- }
1021
- }
1022
-
1023
- function renderErrorSummary() {
1024
- const summary = state.errorSummary;
1025
- const card = document.getElementById('errorSummaryCard');
1026
- if (!summary) {
1027
- card.textContent = 'No diagnostics available';
1028
- return;
1029
- }
1030
- card.innerHTML = `
1031
- <div><strong>Total Logs:</strong> ${summary.total}</div>
1032
- <div><strong>Last Error:</strong> ${summary.last_error ? summary.last_error.provider + ' @ ' + summary.last_error.timestamp : 'None'}</div>
1033
- <div><strong>Top Offenders:</strong> ${Object.keys(summary.by_provider || {}).slice(0,3).join(', ') || '—'}</div>
1034
- `;
1035
- const diag = state.diagnostics || { recent: [] };
1036
- const list = document.getElementById('diagnosticsList');
1037
- list.innerHTML = diag.recent.slice(0,5).map(item => `
1038
- <div class="list-item">
1039
- <div style="font-weight:600;">${item.provider}</div>
1040
- <div style="font-size:12px; color:var(--muted);">${item.timestamp}</div>
1041
- <div style="font-size:13px; color:${item.status === 'offline' ? '#dc2626' : '#d97706'};">${item.message || 'No message'}</div>
1042
- </div>`).join('');
1043
- }
1044
-
1045
- async function loadResourceSearch() {
1046
- const query = document.getElementById('resourceSearch')?.value || '';
1047
- const source = document.getElementById('resourceFilter').value;
1048
- const data = await apiCall(`/api/resources/search?q=${encodeURIComponent(query)}&source=${source}`);
1049
- state.resources = data.results;
1050
- document.getElementById('resourceCountProviders').textContent = `(${data.counts.providers})`;
1051
- document.getElementById('resourceCountModels').textContent = `(${data.counts.models})`;
1052
- document.getElementById('resourceCountDatasets').textContent = `(${data.counts.datasets})`;
1053
- renderResourceResults();
1054
- }
1055
-
1056
- function renderResourceResults() {
1057
- const providersContainer = document.getElementById('resourceResultsProviders');
1058
- const modelsContainer = document.getElementById('resourceResultsModels');
1059
- const datasetsContainer = document.getElementById('resourceResultsDatasets');
1060
-
1061
- providersContainer.innerHTML = state.resources.providers.slice(0,6).map(p => `
1062
- <div class="resource-item">
1063
- <strong>${p.name}</strong>
1064
- <div style="font-size:12px;color:var(--muted);">${p.category}</div>
1065
- <div style="font-size:12px;">Status: ${p.status}</div>
1066
- </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1067
-
1068
- modelsContainer.innerHTML = state.resources.models.slice(0,6).map(m => `
1069
- <div class="resource-item">
1070
- <strong>${m.id}</strong>
1071
- <div style="font-size:12px;color:var(--muted);">${m.description || 'No description'}</div>
1072
- </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1073
-
1074
- datasetsContainer.innerHTML = state.resources.datasets.slice(0,6).map(d => `
1075
- <div class="resource-item">
1076
- <strong>${d.id}</strong>
1077
- <div style="font-size:12px;color:var(--muted);">${d.description || 'No description'}</div>
1078
- </div>`).join('') || '<div class="resource-item" style="color:var(--muted);">No matches</div>';
1079
- }
1080
-
1081
- async function handleExport(type) {
1082
- try {
1083
- const res = await apiCall(`/api/v2/export/${type}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
1084
- state.exports.unshift({ type, url: res.download_url, timestamp: res.timestamp });
1085
- renderExportHistory();
1086
- showToast(`${type.toUpperCase()} export ready`, 'success');
1087
- } catch (err) {
1088
- console.error(err);
1089
- showToast('Export failed', 'error');
1090
- }
1091
- }
1092
-
1093
- async function handleBackup() {
1094
- try {
1095
- const res = await apiCall('/api/v2/backup', { method: 'POST' });
1096
- state.exports.unshift({ type: 'backup', url: res.download_url, timestamp: res.timestamp });
1097
- renderExportHistory();
1098
- showToast('Backup created', 'success');
1099
- } catch (err) {
1100
- console.error(err);
1101
- showToast('Backup failed', 'error');
1102
- }
1103
- }
1104
-
1105
- function renderExportHistory() {
1106
- const container = document.getElementById('exportHistory');
1107
- if (!state.exports.length) {
1108
- container.innerHTML = '<div style="color:var(--muted); font-size:13px;">No exports yet</div>';
1109
- return;
1110
- }
1111
- container.innerHTML = state.exports.slice(0,5).map(entry => `
1112
- <div class="list-item" style="border-bottom:1px solid var(--border);">
1113
- <div style="font-weight:600;">${entry.type.toUpperCase()}</div>
1114
- <div style="font-size:12px; color:var(--muted);">${new Date(entry.timestamp).toLocaleString()}</div>
1115
- <a href="${entry.url}" style="font-size:12px; color:var(--primary);" target="_blank">Download</a>
1116
- </div>`).join('');
1117
- }
1118
-
1119
- async function handleImportSingle(event) {
1120
- event.preventDefault();
1121
- const form = event.target;
1122
- const payload = Object.fromEntries(new FormData(form).entries());
1123
- payload.requires_key = form.elements['requires_key'].checked;
1124
- payload.timeout_ms = Number(payload.timeout_ms) || 10000;
1125
- try {
1126
- await apiCall('/api/providers', {
1127
- method: 'POST',
1128
- headers: { 'Content-Type': 'application/json' },
1129
- body: JSON.stringify(payload)
1130
- });
1131
- showToast('Provider imported', 'success');
1132
- form.reset();
1133
- loadProviders();
1134
- } catch (err) {
1135
- console.error(err);
1136
- showToast('Import failed', 'error');
1137
- }
1138
- }
1139
-
1140
- async function handleImportBulk() {
1141
- const textarea = document.getElementById('bulkImportTextarea');
1142
- if (!textarea.value.trim()) {
1143
- showToast('Paste provider JSON first', 'error');
1144
- return;
1145
- }
1146
- try {
1147
- const providers = JSON.parse(textarea.value);
1148
- await apiCall('/api/v2/import/providers', {
1149
- method: 'POST',
1150
- headers: { 'Content-Type': 'application/json' },
1151
- body: JSON.stringify({ providers })
1152
- });
1153
- showToast('Bulk import complete', 'success');
1154
- textarea.value = '';
1155
- loadProviders();
1156
- } catch (err) {
1157
- console.error(err);
1158
- showToast('Bulk import failed', 'error');
1159
- }
1160
- }
1161
-
1162
- function switchTab(event, tabName) {
1163
- document.querySelectorAll('.tab').forEach(tab => tab.classList.remove('active'));
1164
- document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
1165
- event.currentTarget.classList.add('active');
1166
- document.getElementById(`tab-${tabName}`).classList.add('active');
1167
- state.currentTab = tabName;
1168
- if (tabName === 'market') loadMarket();
1169
- if (tabName === 'sentiment') { loadSentimentData(); loadDefi(); }
1170
- if (tabName === 'news') loadNews();
1171
- }
1172
-
1173
- function startAutoRefresh() {
1174
- setInterval(() => {
1175
- if (state.wsConnected) {
1176
- refreshAll();
1177
- }
1178
- }, config.autoRefreshInterval);
1179
- }
1180
-
1181
- function refreshAll() {
1182
- loadProviders();
1183
- loadMarket();
1184
- loadTrending();
1185
- loadSentimentData();
1186
- loadDefi();
1187
- loadErrorSummary();
1188
- loadNews();
1189
- loadResourceSearch();
1190
- }
1191
-
1192
- function formatNumber(value) {
1193
- if (!value && value !== 0) return '--';
1194
- if (value >= 1e12) return (value / 1e12).toFixed(2) + 'T';
1195
- if (value >= 1e9) return (value / 1e9).toFixed(2) + 'B';
1196
- if (value >= 1e6) return (value / 1e6).toFixed(2) + 'M';
1197
- if (value >= 1e3) return (value / 1e3).toFixed(2) + 'K';
1198
- return Number(value).toFixed(2);
1199
- }
1200
-
1201
- function setupResourceSearch() {
1202
- const input = document.getElementById('resourceSearch');
1203
- input.addEventListener('input', () => {
1204
- clearTimeout(state.resourceSearchTimeout);
1205
- state.resourceSearchTimeout = setTimeout(loadResourceSearch, 400);
1206
- });
1207
- }
1208
-
1209
- initializeWebSocket();
1210
- setupResourceSearch();
1211
- loadInitialData();
1212
- startAutoRefresh();
1213
- </script>
1214
- </body>
1215
- </html>
1216
-
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Crypto API Monitor Pro - Real-time Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ <style>
10
+ * {
11
+ margin: 0;
12
+ padding: 0;
13
+ box-sizing: border-box;
14
+ }
15
+
16
+ :root {
17
+ --bg-dark: #0a0a0f;
18
+ --bg-card: #12121a;
19
+ --bg-card-hover: #1a1a24;
20
+ --text-primary: #ffffff;
21
+ --text-secondary: #a0a0b0;
22
+ --accent-blue: #3b82f6;
23
+ --accent-purple: #8b5cf6;
24
+ --accent-pink: #ec4899;
25
+ --accent-green: #10b981;
26
+ --accent-red: #ef4444;
27
+ --accent-yellow: #f59e0b;
28
+ --border: rgba(255, 255, 255, 0.1);
29
+ --shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
30
+ }
31
+
32
+ body {
33
+ font-family: 'Inter', sans-serif;
34
+ background: var(--bg-dark);
35
+ color: var(--text-primary);
36
+ line-height: 1.6;
37
+ overflow-x: hidden;
38
+ }
39
+
40
+ body::before {
41
+ content: '';
42
+ position: fixed;
43
+ top: 0;
44
+ left: 0;
45
+ right: 0;
46
+ bottom: 0;
47
+ background:
48
+ radial-gradient(circle at 20% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%),
49
+ radial-gradient(circle at 80% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 50%),
50
+ radial-gradient(circle at 50% 50%, rgba(236, 72, 153, 0.05) 0%, transparent 50%);
51
+ pointer-events: none;
52
+ z-index: 0;
53
+ }
54
+
55
+ .container {
56
+ max-width: 1800px;
57
+ margin: 0 auto;
58
+ padding: 20px;
59
+ position: relative;
60
+ z-index: 1;
61
+ }
62
+
63
+ /* Header */
64
+ .header {
65
+ background: linear-gradient(135deg, var(--bg-card) 0%, rgba(59, 130, 246, 0.1) 100%);
66
+ border: 1px solid var(--border);
67
+ border-radius: 24px;
68
+ padding: 30px;
69
+ margin-bottom: 30px;
70
+ backdrop-filter: blur(20px);
71
+ box-shadow: var(--shadow);
72
+ }
73
+
74
+ .header-top {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: space-between;
78
+ flex-wrap: wrap;
79
+ gap: 20px;
80
+ margin-bottom: 25px;
81
+ }
82
+
83
+ .logo {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 15px;
87
+ }
88
+
89
+ .logo-icon {
90
+ width: 50px;
91
+ height: 50px;
92
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
93
+ border-radius: 12px;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
97
+ font-size: 24px;
98
+ box-shadow: 0 10px 30px rgba(59, 130, 246, 0.3);
99
+ }
100
+
101
+ .logo-text h1 {
102
+ font-size: 28px;
103
+ font-weight: 800;
104
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
105
+ -webkit-background-clip: text;
106
+ -webkit-text-fill-color: transparent;
107
+ background-clip: text;
108
+ }
109
+
110
+ .logo-text p {
111
+ font-size: 13px;
112
+ color: var(--text-secondary);
113
+ }
114
+
115
+ .header-actions {
116
+ display: flex;
117
+ gap: 15px;
118
+ align-items: center;
119
+ }
120
+
121
+ .status-badge {
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ padding: 10px 20px;
126
+ background: rgba(16, 185, 129, 0.1);
127
+ border: 1px solid rgba(16, 185, 129, 0.3);
128
+ border-radius: 12px;
129
+ font-size: 13px;
130
+ font-weight: 600;
131
+ }
132
+
133
+ .status-dot {
134
+ width: 8px;
135
+ height: 8px;
136
+ background: var(--accent-green);
137
+ border-radius: 50%;
138
+ animation: pulse 2s infinite;
139
+ }
140
+
141
+ @keyframes pulse {
142
+ 0%, 100% { opacity: 1; transform: scale(1); }
143
+ 50% { opacity: 0.5; transform: scale(1.2); }
144
+ }
145
+
146
+ /* Stats Grid */
147
+ .stats-grid {
148
+ display: grid;
149
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
150
+ gap: 20px;
151
+ margin-bottom: 30px;
152
+ }
153
+
154
+ .stat-card {
155
+ background: var(--bg-card);
156
+ border: 1px solid var(--border);
157
+ border-radius: 20px;
158
+ padding: 25px;
159
+ transition: all 0.3s ease;
160
+ position: relative;
161
+ overflow: hidden;
162
+ }
163
+
164
+ .stat-card::before {
165
+ content: '';
166
+ position: absolute;
167
+ top: 0;
168
+ left: 0;
169
+ right: 0;
170
+ height: 4px;
171
+ background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
172
+ transform: scaleX(0);
173
+ transition: transform 0.3s ease;
174
+ }
175
+
176
+ .stat-card:hover {
177
+ transform: translateY(-5px);
178
+ border-color: rgba(59, 130, 246, 0.5);
179
+ box-shadow: 0 15px 40px rgba(59, 130, 246, 0.2);
180
+ }
181
+
182
+ .stat-card:hover::before {
183
+ transform: scaleX(1);
184
+ }
185
+
186
+ .stat-header {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: space-between;
190
+ margin-bottom: 15px;
191
+ }
192
+
193
+ .stat-icon {
194
+ width: 40px;
195
+ height: 40px;
196
+ border-radius: 10px;
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ font-size: 20px;
201
+ }
202
+
203
+ .stat-value {
204
+ font-size: 32px;
205
+ font-weight: 800;
206
+ margin-bottom: 5px;
207
+ background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
208
+ -webkit-background-clip: text;
209
+ -webkit-text-fill-color: transparent;
210
+ }
211
+
212
+ .stat-label {
213
+ font-size: 13px;
214
+ color: var(--text-secondary);
215
+ text-transform: uppercase;
216
+ letter-spacing: 1px;
217
+ }
218
+
219
+ .stat-change {
220
+ font-size: 13px;
221
+ font-weight: 600;
222
+ margin-top: 10px;
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 5px;
226
+ }
227
+
228
+ .stat-change.positive { color: var(--accent-green); }
229
+ .stat-change.negative { color: var(--accent-red); }
230
+
231
+ /* Tabs */
232
+ .tabs {
233
+ display: flex;
234
+ gap: 10px;
235
+ margin-bottom: 25px;
236
+ padding: 8px;
237
+ background: var(--bg-card);
238
+ border-radius: 16px;
239
+ border: 1px solid var(--border);
240
+ }
241
+
242
+ .tab {
243
+ padding: 12px 24px;
244
+ background: transparent;
245
+ border: none;
246
+ border-radius: 12px;
247
+ color: var(--text-secondary);
248
+ font-weight: 600;
249
+ cursor: pointer;
250
+ transition: all 0.3s ease;
251
+ font-size: 14px;
252
+ }
253
+
254
+ .tab:hover {
255
+ color: var(--text-primary);
256
+ background: rgba(59, 130, 246, 0.1);
257
+ }
258
+
259
+ .tab.active {
260
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
261
+ color: white;
262
+ box-shadow: 0 5px 20px rgba(59, 130, 246, 0.3);
263
+ }
264
+
265
+ .tab-content {
266
+ display: none;
267
+ }
268
+
269
+ .tab-content.active {
270
+ display: block;
271
+ animation: fadeIn 0.3s ease;
272
+ }
273
+
274
+ @keyframes fadeIn {
275
+ from { opacity: 0; transform: translateY(10px); }
276
+ to { opacity: 1; transform: translateY(0); }
277
+ }
278
+
279
+ /* Provider Grid */
280
+ .provider-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
283
+ gap: 20px;
284
+ }
285
+
286
+ .provider-card {
287
+ background: var(--bg-card);
288
+ border: 1px solid var(--border);
289
+ border-radius: 16px;
290
+ padding: 20px;
291
+ transition: all 0.3s ease;
292
+ cursor: pointer;
293
+ }
294
+
295
+ .provider-card:hover {
296
+ transform: translateY(-3px);
297
+ border-color: rgba(59, 130, 246, 0.5);
298
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
299
+ }
300
+
301
+ .provider-header {
302
+ display: flex;
303
+ align-items: center;
304
+ justify-content: space-between;
305
+ margin-bottom: 15px;
306
+ }
307
+
308
+ .provider-name {
309
+ font-size: 18px;
310
+ font-weight: 700;
311
+ }
312
+
313
+ .provider-status {
314
+ padding: 4px 12px;
315
+ border-radius: 20px;
316
+ font-size: 11px;
317
+ font-weight: 600;
318
+ text-transform: uppercase;
319
+ }
320
+
321
+ .provider-status.operational {
322
+ background: rgba(16, 185, 129, 0.2);
323
+ color: var(--accent-green);
324
+ }
325
+
326
+ .provider-status.degraded {
327
+ background: rgba(245, 158, 11, 0.2);
328
+ color: var(--accent-yellow);
329
+ }
330
+
331
+ .provider-status.maintenance {
332
+ background: rgba(239, 68, 68, 0.2);
333
+ color: var(--accent-red);
334
+ }
335
+
336
+ .provider-meta {
337
+ display: flex;
338
+ gap: 15px;
339
+ font-size: 12px;
340
+ color: var(--text-secondary);
341
+ margin-bottom: 12px;
342
+ }
343
+
344
+ .provider-meta span {
345
+ display: flex;
346
+ align-items: center;
347
+ gap: 5px;
348
+ }
349
+
350
+ .provider-stats {
351
+ display: grid;
352
+ grid-template-columns: repeat(2, 1fr);
353
+ gap: 10px;
354
+ margin-top: 15px;
355
+ padding-top: 15px;
356
+ border-top: 1px solid var(--border);
357
+ }
358
+
359
+ .provider-stat {
360
+ font-size: 12px;
361
+ }
362
+
363
+ .provider-stat-value {
364
+ font-size: 16px;
365
+ font-weight: 700;
366
+ color: var(--text-primary);
367
+ }
368
+
369
+ /* Crypto Table */
370
+ .crypto-table {
371
+ background: var(--bg-card);
372
+ border: 1px solid var(--border);
373
+ border-radius: 16px;
374
+ overflow: hidden;
375
+ }
376
+
377
+ .table-wrapper {
378
+ overflow-x: auto;
379
+ }
380
+
381
+ table {
382
+ width: 100%;
383
+ border-collapse: collapse;
384
+ }
385
+
386
+ thead {
387
+ background: rgba(59, 130, 246, 0.05);
388
+ }
389
+
390
+ th {
391
+ padding: 15px;
392
+ text-align: left;
393
+ font-size: 12px;
394
+ font-weight: 600;
395
+ color: var(--text-secondary);
396
+ text-transform: uppercase;
397
+ letter-spacing: 1px;
398
+ }
399
+
400
+ td {
401
+ padding: 15px;
402
+ border-top: 1px solid var(--border);
403
+ }
404
+
405
+ tr:hover {
406
+ background: rgba(59, 130, 246, 0.05);
407
+ }
408
+
409
+ .crypto-name {
410
+ display: flex;
411
+ align-items: center;
412
+ gap: 10px;
413
+ }
414
+
415
+ .crypto-icon {
416
+ width: 32px;
417
+ height: 32px;
418
+ border-radius: 8px;
419
+ background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
420
+ display: flex;
421
+ align-items: center;
422
+ justify-content: center;
423
+ font-weight: 700;
424
+ font-size: 14px;
425
+ }
426
+
427
+ .price-change {
428
+ font-weight: 600;
429
+ padding: 4px 8px;
430
+ border-radius: 6px;
431
+ }
432
+
433
+ .price-change.positive {
434
+ background: rgba(16, 185, 129, 0.1);
435
+ color: var(--accent-green);
436
+ }
437
+
438
+ .price-change.negative {
439
+ background: rgba(239, 68, 68, 0.1);
440
+ color: var(--accent-red);
441
+ }
442
+
443
+ /* Charts */
444
+ .chart-container {
445
+ background: var(--bg-card);
446
+ border: 1px solid var(--border);
447
+ border-radius: 16px;
448
+ padding: 25px;
449
+ margin-bottom: 25px;
450
+ }
451
+
452
+ .chart-header {
453
+ display: flex;
454
+ justify-content: space-between;
455
+ align-items: center;
456
+ margin-bottom: 20px;
457
+ }
458
+
459
+ .chart-title {
460
+ font-size: 18px;
461
+ font-weight: 700;
462
+ }
463
+
464
+ canvas {
465
+ max-height: 300px;
466
+ }
467
+
468
+ /* Loading */
469
+ .loading {
470
+ display: flex;
471
+ align-items: center;
472
+ justify-content: center;
473
+ padding: 50px;
474
+ }
475
+
476
+ .spinner {
477
+ width: 40px;
478
+ height: 40px;
479
+ border: 3px solid var(--border);
480
+ border-top-color: var(--accent-blue);
481
+ border-radius: 50%;
482
+ animation: spin 1s linear infinite;
483
+ }
484
+
485
+ @keyframes spin {
486
+ to { transform: rotate(360deg); }
487
+ }
488
+
489
+ /* Responsive */
490
+ @media (max-width: 768px) {
491
+ .stats-grid {
492
+ grid-template-columns: 1fr;
493
+ }
494
+
495
+ .provider-grid {
496
+ grid-template-columns: 1fr;
497
+ }
498
+
499
+ .header-top {
500
+ flex-direction: column;
501
+ align-items: flex-start;
502
+ }
503
+ }
504
+
505
+ /* Scrollbar */
506
+ ::-webkit-scrollbar {
507
+ width: 8px;
508
+ height: 8px;
509
+ }
510
+
511
+ ::-webkit-scrollbar-track {
512
+ background: var(--bg-dark);
513
+ }
514
+
515
+ ::-webkit-scrollbar-thumb {
516
+ background: var(--border);
517
+ border-radius: 4px;
518
+ }
519
+
520
+ ::-webkit-scrollbar-thumb:hover {
521
+ background: rgba(255, 255, 255, 0.2);
522
+ }
523
+ </style>
524
+ </head>
525
+ <body>
526
+ <div class="container">
527
+ <!-- Header -->
528
+ <div class="header">
529
+ <div class="header-top">
530
+ <div class="logo">
531
+ <div class="logo-icon">₿</div>
532
+ <div class="logo-text">
533
+ <h1>Crypto API Monitor Pro</h1>
534
+ <p>Real-time monitoring of 40+ providers</p>
535
+ </div>
536
+ </div>
537
+ <div class="header-actions">
538
+ <div class="status-badge" id="systemStatus">
539
+ <div class="status-dot"></div>
540
+ <span id="statusText">System Operational</span>
541
+ </div>
542
+ </div>
543
+ </div>
544
+
545
+ <!-- Stats Grid -->
546
+ <div class="stats-grid">
547
+ <div class="stat-card">
548
+ <div class="stat-header">
549
+ <div class="stat-icon" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6);">🌐</div>
550
+ </div>
551
+ <div class="stat-value" id="totalProviders">--</div>
552
+ <div class="stat-label">Total Providers</div>
553
+ <div class="stat-change positive" id="providersChange">
554
+ <span>↑</span> <span>100% Active</span>
555
+ </div>
556
+ </div>
557
+
558
+ <div class="stat-card">
559
+ <div class="stat-header">
560
+ <div class="stat-icon" style="background: linear-gradient(135deg, #10b981, #06b6d4);">⚡</div>
561
+ </div>
562
+ <div class="stat-value" id="avgResponseTime">--</div>
563
+ <div class="stat-label">Avg Response Time</div>
564
+ <div class="stat-change positive">
565
+ <span>↓</span> <span>12% faster</span>
566
+ </div>
567
+ </div>
568
+
569
+ <div class="stat-card">
570
+ <div class="stat-header">
571
+ <div class="stat-icon" style="background: linear-gradient(135deg, #ec4899, #f59e0b);">💰</div>
572
+ </div>
573
+ <div class="stat-value" id="marketCap">--</div>
574
+ <div class="stat-label">Total Market Cap</div>
575
+ <div class="stat-change positive" id="marketChange">
576
+ <span>↑</span> <span>5.2%</span>
577
+ </div>
578
+ </div>
579
+
580
+ <div class="stat-card">
581
+ <div class="stat-header">
582
+ <div class="stat-icon" style="background: linear-gradient(135deg, #8b5cf6, #ec4899);">📊</div>
583
+ </div>
584
+ <div class="stat-value" id="totalRequests">--</div>
585
+ <div class="stat-label">API Requests Today</div>
586
+ <div class="stat-change positive">
587
+ <span>↑</span> <span>15K/min</span>
588
+ </div>
589
+ </div>
590
+ </div>
591
+ </div>
592
+
593
+ <!-- Tabs -->
594
+ <div class="tabs">
595
+ <button class="tab active" onclick="switchTab('overview')">📊 Overview</button>
596
+ <button class="tab" onclick="switchTab('providers')">🌐 Providers</button>
597
+ <button class="tab" onclick="switchTab('crypto')">₿ Cryptocurrencies</button>
598
+ <button class="tab" onclick="switchTab('analytics')">📈 Analytics</button>
599
+ </div>
600
+
601
+ <!-- Overview Tab -->
602
+ <div class="tab-content active" id="overview">
603
+ <div class="chart-container">
604
+ <div class="chart-header">
605
+ <div class="chart-title">Provider Status Distribution</div>
606
+ </div>
607
+ <canvas id="statusChart"></canvas>
608
+ </div>
609
+
610
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 25px;">
611
+ <div class="chart-container">
612
+ <div class="chart-header">
613
+ <div class="chart-title">Response Time Trends</div>
614
+ </div>
615
+ <canvas id="responseChart"></canvas>
616
+ </div>
617
+
618
+ <div class="chart-container">
619
+ <div class="chart-header">
620
+ <div class="chart-title">Requests by Category</div>
621
+ </div>
622
+ <canvas id="categoryChart"></canvas>
623
+ </div>
624
+ </div>
625
+ </div>
626
+
627
+ <!-- Providers Tab -->
628
+ <div class="tab-content" id="providers">
629
+ <div class="provider-grid" id="providerGrid">
630
+ <div class="loading"><div class="spinner"></div></div>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- Crypto Tab -->
635
+ <div class="tab-content" id="crypto">
636
+ <div class="crypto-table">
637
+ <div class="table-wrapper">
638
+ <table>
639
+ <thead>
640
+ <tr>
641
+ <th>#</th>
642
+ <th>Name</th>
643
+ <th>Price</th>
644
+ <th>24h Change</th>
645
+ <th>7d Change</th>
646
+ <th>Market Cap</th>
647
+ <th>Volume 24h</th>
648
+ <th>Category</th>
649
+ </tr>
650
+ </thead>
651
+ <tbody id="cryptoTableBody">
652
+ <tr><td colspan="8" style="text-align: center;"><div class="loading"><div class="spinner"></div></div></td></tr>
653
+ </tbody>
654
+ </table>
655
+ </div>
656
+ </div>
657
+ </div>
658
+
659
+ <!-- Analytics Tab -->
660
+ <div class="tab-content" id="analytics">
661
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 25px; margin-bottom: 25px;">
662
+ <div class="chart-container">
663
+ <div class="chart-header">
664
+ <div class="chart-title">Market Dominance</div>
665
+ </div>
666
+ <canvas id="dominanceChart"></canvas>
667
+ </div>
668
+
669
+ <div class="chart-container">
670
+ <div class="chart-header">
671
+ <div class="chart-title">Provider Performance</div>
672
+ </div>
673
+ <canvas id="performanceChart"></canvas>
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </div>
678
+
679
+ <script>
680
+ let charts = {};
681
+ let wsConnection = null;
682
+
683
+ // Initialize
684
+ document.addEventListener('DOMContentLoaded', async () => {
685
+ await loadData();
686
+ initCharts();
687
+ connectWebSocket();
688
+ setInterval(loadData, 30000);
689
+ });
690
+
691
+ async function loadData() {
692
+ try {
693
+ const [stats, providers, crypto] = await Promise.all([
694
+ fetch('/api/stats').then(r => r.json()),
695
+ fetch('/api/providers').then(r => r.json()),
696
+ fetch('/api/crypto/prices').then(r => r.json())
697
+ ]);
698
+
699
+ updateStats(stats);
700
+ updateProviders(providers);
701
+ updateCrypto(crypto);
702
+ updateCharts(providers, crypto);
703
+ } catch (error) {
704
+ console.error('Error loading data:', error);
705
+ }
706
+ }
707
+
708
+ function updateStats(stats) {
709
+ document.getElementById('totalProviders').textContent = stats.providers.total;
710
+ document.getElementById('avgResponseTime').textContent = stats.performance.avg_response_time + 'ms';
711
+ document.getElementById('marketCap').textContent = '$' + (stats.market.total_market_cap / 1e12).toFixed(2) + 'T';
712
+ document.getElementById('totalRequests').textContent = (stats.performance.total_requests / 1e6).toFixed(1) + 'M';
713
+
714
+ const change = stats.market.avg_change_24h;
715
+ const changeEl = document.getElementById('marketChange');
716
+ changeEl.className = 'stat-change ' + (change > 0 ? 'positive' : 'negative');
717
+ changeEl.innerHTML = `<span>${change > 0 ? '↑' : '↓'}</span> <span>${Math.abs(change).toFixed(1)}%</span>`;
718
+ }
719
+
720
+ function updateProviders(providers) {
721
+ const grid = document.getElementById('providerGrid');
722
+ grid.innerHTML = providers.map(p => `
723
+ <div class="provider-card">
724
+ <div class="provider-header">
725
+ <div class="provider-name">${p.name}</div>
726
+ <div class="provider-status ${p.status}">${p.status}</div>
727
+ </div>
728
+ <div class="provider-meta">
729
+ <span>📍 ${p.region}</span>
730
+ <span>🏷️ ${p.type}</span>
731
+ <span>📂 ${p.category}</span>
732
+ </div>
733
+ <div class="provider-stats">
734
+ <div class="provider-stat">
735
+ <div style="color: var(--text-secondary); font-size: 11px;">Uptime</div>
736
+ <div class="provider-stat-value">${p.uptime}%</div>
737
+ </div>
738
+ <div class="provider-stat">
739
+ <div style="color: var(--text-secondary); font-size: 11px;">Response</div>
740
+ <div class="provider-stat-value">${p.response_time_ms}ms</div>
741
+ </div>
742
+ <div class="provider-stat">
743
+ <div style="color: var(--text-secondary); font-size: 11px;">Requests</div>
744
+ <div class="provider-stat-value">${(p.requests_today / 1000).toFixed(0)}K</div>
745
+ </div>
746
+ <div class="provider-stat">
747
+ <div style="color: var(--text-secondary); font-size: 11px;">Rate</div>
748
+ <div class="provider-stat-value">${p.rate_limit}/min</div>
749
+ </div>
750
+ </div>
751
+ </div>
752
+ `).join('');
753
+ }
754
+
755
+ function updateCrypto(crypto) {
756
+ const tbody = document.getElementById('cryptoTableBody');
757
+ tbody.innerHTML = crypto.map(c => `
758
+ <tr>
759
+ <td>${c.rank}</td>
760
+ <td>
761
+ <div class="crypto-name">
762
+ <div class="crypto-icon">${c.symbol[0]}</div>
763
+ <div>
764
+ <div style="font-weight: 600;">${c.name}</div>
765
+ <div style="font-size: 12px; color: var(--text-secondary);">${c.symbol}</div>
766
+ </div>
767
+ </div>
768
+ </td>
769
+ <td style="font-weight: 600;">$${c.price.toLocaleString()}</td>
770
+ <td><span class="price-change ${c.change_24h > 0 ? 'positive' : 'negative'}">${c.change_24h > 0 ? '+' : ''}${c.change_24h.toFixed(2)}%</span></td>
771
+ <td><span class="price-change ${c.change_7d > 0 ? 'positive' : 'negative'}">${c.change_7d > 0 ? '+' : ''}${c.change_7d.toFixed(2)}%</span></td>
772
+ <td>$${(c.market_cap / 1e9).toFixed(2)}B</td>
773
+ <td>$${(c.volume_24h / 1e9).toFixed(2)}B</td>
774
+ <td><span style="padding: 4px 8px; background: rgba(59, 130, 246, 0.1); border-radius: 6px; font-size: 11px;">${c.category}</span></td>
775
+ </tr>
776
+ `).join('');
777
+ }
778
+
779
+ function initCharts() {
780
+ Chart.defaults.color = '#a0a0b0';
781
+ Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
782
+
783
+ charts.status = new Chart(document.getElementById('statusChart'), {
784
+ type: 'doughnut',
785
+ data: {
786
+ labels: ['Operational', 'Degraded', 'Maintenance'],
787
+ datasets: [{
788
+ data: [85, 10, 5],
789
+ backgroundColor: ['#10b981', '#f59e0b', '#ef4444'],
790
+ borderWidth: 0
791
+ }]
792
+ },
793
+ options: {
794
+ responsive: true,
795
+ plugins: {
796
+ legend: { position: 'bottom' }
797
+ }
798
+ }
799
+ });
800
+
801
+ charts.response = new Chart(document.getElementById('responseChart'), {
802
+ type: 'line',
803
+ data: {
804
+ labels: Array(12).fill(0).map((_, i) => `${i*5}m`),
805
+ datasets: [{
806
+ label: 'Response Time',
807
+ data: Array(12).fill(0).map(() => Math.random() * 100 + 80),
808
+ borderColor: '#3b82f6',
809
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
810
+ fill: true,
811
+ tension: 0.4
812
+ }]
813
+ },
814
+ options: {
815
+ responsive: true,
816
+ plugins: { legend: { display: false } }
817
+ }
818
+ });
819
+
820
+ charts.category = new Chart(document.getElementById('categoryChart'), {
821
+ type: 'bar',
822
+ data: {
823
+ labels: ['Exchanges', 'Data', 'DeFi', 'NFT', 'Blockchain'],
824
+ datasets: [{
825
+ label: 'Providers',
826
+ data: [15, 8, 8, 4, 5],
827
+ backgroundColor: ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981']
828
+ }]
829
+ },
830
+ options: {
831
+ responsive: true,
832
+ plugins: { legend: { display: false } }
833
+ }
834
+ });
835
+ }
836
+
837
+ function updateCharts(providers, crypto) {
838
+ if (charts.dominance) {
839
+ const top5 = crypto.slice(0, 5);
840
+ charts.dominance.data.labels = top5.map(c => c.symbol);
841
+ charts.dominance.data.datasets[0].data = top5.map(c => c.market_cap);
842
+ charts.dominance.update();
843
+ } else {
844
+ charts.dominance = new Chart(document.getElementById('dominanceChart'), {
845
+ type: 'pie',
846
+ data: {
847
+ labels: crypto.slice(0, 5).map(c => c.symbol),
848
+ datasets: [{
849
+ data: crypto.slice(0, 5).map(c => c.market_cap),
850
+ backgroundColor: ['#3b82f6', '#8b5cf6', '#ec4899', '#f59e0b', '#10b981']
851
+ }]
852
+ }
853
+ });
854
+ }
855
+
856
+ if (charts.performance) {
857
+ charts.performance.data.datasets[0].data = providers.slice(0, 8).map(p => p.uptime);
858
+ charts.performance.update();
859
+ } else {
860
+ charts.performance = new Chart(document.getElementById('performanceChart'), {
861
+ type: 'radar',
862
+ data: {
863
+ labels: providers.slice(0, 8).map(p => p.name),
864
+ datasets: [{
865
+ label: 'Uptime %',
866
+ data: providers.slice(0, 8).map(p => p.uptime),
867
+ borderColor: '#8b5cf6',
868
+ backgroundColor: 'rgba(139, 92, 246, 0.1)'
869
+ }]
870
+ }
871
+ });
872
+ }
873
+ }
874
+
875
+ function switchTab(tab) {
876
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
877
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
878
+ event.target.classList.add('active');
879
+ document.getElementById(tab).classList.add('active');
880
+ }
881
+
882
+ function connectWebSocket() {
883
+ const ws = new WebSocket(`ws://${window.location.host}/ws/live`);
884
+ ws.onopen = () => console.log('WebSocket connected');
885
+ ws.onmessage = (e) => {
886
+ const data = JSON.parse(e.data);
887
+ if (data.type === 'status_update') {
888
+ document.getElementById('statusText').textContent =
889
+ data.data.status === 'healthy' ? 'System Operational' : 'System Degraded';
890
+ }
891
+ };
892
+ ws.onerror = () => console.log('WebSocket error');
893
+ }
894
+ </script>
895
+ </body>
896
+ </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
main.py CHANGED
@@ -1,30 +1,168 @@
1
- #!/usr/bin/env python3
2
- """
3
- Local Development Entry Point
4
- This file is for local development only and is NOT used by Hugging Face Docker runtime.
5
- For production deployment, use: uvicorn api_server_extended:app
6
- """
 
 
7
 
8
- if __name__ == "__main__":
9
- import uvicorn
10
- import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- # Get port from environment or use default
13
- port = int(os.getenv("PORT", "8000"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
- print(f"""
16
- ╔═══════════════════════════════════════════════════════════╗
17
- ║ 🚀 Crypto Monitor - Local Development Server ║
18
- ║ Port: {port}
19
- ║ Docs: http://localhost:{port}/docs ║
20
- ╚═══════════════════════════════════════════════════════════╝
21
- """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- # Run with reload for local development
24
- uvicorn.run(
25
- "api_server_extended:app",
26
- host="0.0.0.0",
27
- port=port,
28
- reload=True,
29
- log_level="info"
30
- )
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Depends
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import ccxt
4
+ import os
5
+ import logging
6
+ from datetime import datetime
7
+ import pandas as pd
8
+ import numpy as np
9
 
10
+ # Setup logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ # Create data and logs directories
15
+ os.makedirs("data", exist_ok=True)
16
+ os.makedirs("logs", exist_ok=True)
17
+
18
+ # Initialize FastAPI app
19
+ app = FastAPI(
20
+ title="Cryptocurrency Data Source API",
21
+ description="API for fetching cryptocurrency market data and technical indicators",
22
+ version="1.0.0",
23
+ )
24
+
25
+ # Configure CORS
26
+ app.add_middleware(
27
+ CORSMiddleware,
28
+ allow_origins=["*"],
29
+ allow_credentials=True,
30
+ allow_methods=["*"],
31
+ allow_headers=["*"],
32
+ )
33
+
34
+ @app.get("/")
35
+ async def root():
36
+ """Root endpoint showing API status"""
37
+ return {
38
+ "status": "online",
39
+ "version": "1.0.0",
40
+ "timestamp": datetime.now().isoformat(),
41
+ "ccxt_version": ccxt.__version__
42
+ }
43
+
44
+ @app.get("/exchanges")
45
+ async def get_exchanges():
46
+ """List available exchanges"""
47
+ # Get list of exchanges that support OHLCV data
48
+ exchanges = []
49
+ for exchange_id in ccxt.exchanges:
50
+ try:
51
+ exchange = getattr(ccxt, exchange_id)()
52
+ if exchange.has.get('fetchOHLCV'):
53
+ exchanges.append({
54
+ "id": exchange_id,
55
+ "name": exchange.name if hasattr(exchange, 'name') else exchange_id,
56
+ "url": exchange.urls.get('www') if hasattr(exchange, 'urls') and exchange.urls else None
57
+ })
58
+ except:
59
+ # Skip exchanges that fail to initialize
60
+ continue
61
 
62
+ return {"total": len(exchanges), "exchanges": exchanges}
63
+
64
+ @app.get("/markets/{exchange_id}")
65
+ async def get_markets(exchange_id: str):
66
+ """Get markets for a specific exchange"""
67
+ try:
68
+ # Check if exchange exists
69
+ if exchange_id not in ccxt.exchanges:
70
+ raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
71
+
72
+ # Initialize exchange
73
+ exchange = getattr(ccxt, exchange_id)({
74
+ 'enableRateLimit': True
75
+ })
76
+
77
+ # Fetch markets
78
+ markets = exchange.load_markets()
79
+
80
+ # Format response
81
+ result = []
82
+ for symbol, market in markets.items():
83
+ if market.get('active', False):
84
+ result.append({
85
+ "symbol": symbol,
86
+ "base": market.get('base', ''),
87
+ "quote": market.get('quote', ''),
88
+ "type": market.get('type', 'spot')
89
+ })
90
+
91
+ return {"exchange": exchange_id, "total": len(result), "markets": result[:100]}
92
 
93
+ except ccxt.BaseError as e:
94
+ raise HTTPException(status_code=500, detail=str(e))
95
+ except Exception as e:
96
+ logger.error(f"Error fetching markets for {exchange_id}: {str(e)}")
97
+ raise HTTPException(status_code=500, detail=str(e))
98
+
99
+ @app.get("/ohlcv/{exchange_id}/{symbol}")
100
+ async def get_ohlcv(exchange_id: str, symbol: str, timeframe: str = "1h", limit: int = 100):
101
+ """Get OHLCV data for a specific market"""
102
+ try:
103
+ # Check if exchange exists
104
+ if exchange_id not in ccxt.exchanges:
105
+ raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
106
+
107
+ # Initialize exchange
108
+ exchange = getattr(ccxt, exchange_id)({
109
+ 'enableRateLimit': True
110
+ })
111
+
112
+ # Check if exchange supports OHLCV
113
+ if not exchange.has.get('fetchOHLCV'):
114
+ raise HTTPException(
115
+ status_code=400,
116
+ detail=f"Exchange {exchange_id} does not support OHLCV data"
117
+ )
118
+
119
+ # Check timeframe
120
+ if timeframe not in exchange.timeframes:
121
+ raise HTTPException(
122
+ status_code=400,
123
+ detail=f"Timeframe {timeframe} not supported by {exchange_id}"
124
+ )
125
+
126
+ # Fetch OHLCV data
127
+ ohlcv = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
128
+
129
+ # Convert to readable format
130
+ result = []
131
+ for candle in ohlcv:
132
+ timestamp, open_price, high, low, close, volume = candle
133
+ result.append({
134
+ "timestamp": timestamp,
135
+ "datetime": datetime.fromtimestamp(timestamp / 1000).isoformat(),
136
+ "open": open_price,
137
+ "high": high,
138
+ "low": low,
139
+ "close": close,
140
+ "volume": volume
141
+ })
142
+
143
+ return {
144
+ "exchange": exchange_id,
145
+ "symbol": symbol,
146
+ "timeframe": timeframe,
147
+ "data": result
148
+ }
149
 
150
+ except ccxt.BaseError as e:
151
+ raise HTTPException(status_code=500, detail=str(e))
152
+ except Exception as e:
153
+ logger.error(f"Error fetching OHLCV data: {str(e)}")
154
+ raise HTTPException(status_code=500, detail=str(e))
155
+
156
+ @app.get("/health")
157
+ async def health_check():
158
+ """Health check endpoint"""
159
+ return {
160
+ "status": "healthy",
161
+ "timestamp": datetime.now().isoformat(),
162
+ "version": "1.0.0"
163
+ }
164
+
165
+ if __name__ == "__main__":
166
+ import uvicorn
167
+ port = int(os.getenv("PORT", 7860))
168
+ uvicorn.run("main:app", host="0.0.0.0", port=port, log_level="info")
requirements.txt CHANGED
@@ -1,58 +1,3 @@
1
- # Crypto Data Aggregator - Complete Requirements
2
- # Production-ready dependencies with exact versions
3
-
4
- # ==================== GRADIO INTERFACE ====================
5
- gradio>=4.44.0
6
-
7
- # ==================== DATA PROCESSING ====================
8
- pandas>=2.0.0
9
- numpy>=1.24.0
10
-
11
- # ==================== HTTP CLIENTS ====================
12
- requests>=2.31.0
13
- aiohttp>=3.8.0
14
- httpx>=0.26.0
15
-
16
- # ==================== WEB BACKEND ====================
17
- fastapi>=0.109.0
18
- uvicorn[standard]>=0.27.0
19
- slowapi>=0.1.9
20
- python-multipart>=0.0.6
21
- websockets>=12.0
22
-
23
- # ==================== DATA MODELS & CONFIG ====================
24
- pydantic>=2.5.3
25
- pydantic-settings>=2.1.0
26
-
27
- # ==================== WEB SCRAPING & RSS ====================
28
- beautifulsoup4>=4.12.0
29
- feedparser>=6.0.10
30
-
31
- # ==================== AI/ML - HUGGING FACE ====================
32
- transformers>=4.30.0
33
- torch>=2.0.0
34
- sentencepiece>=0.1.99
35
- tokenizers>=0.13.0
36
- huggingface-hub>=0.16.0
37
-
38
- # ==================== PLOTTING & VISUALIZATION ====================
39
- plotly>=5.14.0
40
- kaleido>=0.2.1
41
-
42
- # ==================== DATABASE & STORAGE ====================
43
- sqlalchemy>=2.0.25
44
-
45
- # ==================== AUTHENTICATION & SECURITY ====================
46
- PyJWT>=2.8.0
47
-
48
- # ==================== DATE/TIME HELPERS ====================
49
- python-dateutil>=2.8.2
50
-
51
- # ==================== OPTIONAL: ACCELERATED INFERENCE ====================
52
- # accelerate>=0.20.0 # Uncomment for faster model loading
53
- # bitsandbytes>=0.39.0 # Uncomment for quantization
54
-
55
- # ==================== NOTES ====================
56
- # No API keys required - all data sources are free
57
- # SQLite is included in Python standard library
58
- # All packages are production-tested and stable
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ websockets==12.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.bat CHANGED
@@ -1,16 +1,17 @@
1
  @echo off
2
  chcp 65001 > nul
3
- title Crypto Monitor ULTIMATE - Real APIs
4
 
5
  echo ========================================
6
- echo 🚀 Crypto Monitor ULTIMATE
7
- echo Real-time Data from 100+ Free APIs
8
  echo ========================================
9
  echo.
10
 
11
  python --version > nul 2>&1
12
  if %errorlevel% neq 0 (
13
  echo ❌ Python not found!
 
14
  pause
15
  exit /b 1
16
  )
@@ -31,20 +32,14 @@ pip install -q -r requirements.txt
31
 
32
  echo.
33
  echo ========================================
34
- echo 🎯 Starting Real-time Server...
35
  echo ========================================
36
  echo.
37
  echo 📊 Dashboard: http://localhost:8000/dashboard
38
  echo 📡 API Docs: http://localhost:8000/docs
39
  echo.
40
- echo 💡 Real APIs:
41
- echo ✓ CoinGecko - Market Data
42
- echo ✓ CoinCap - Price Data
43
- echo ✓ Binance - Exchange Data
44
- echo ✓ Fear & Greed Index
45
- echo ✓ DeFi Llama - TVL Data
46
  echo.
47
- echo Press Ctrl+C to stop
48
  echo ========================================
49
  echo.
50
 
 
1
  @echo off
2
  chcp 65001 > nul
3
+ title Crypto API Monitor Pro - Starting...
4
 
5
  echo ========================================
6
+ echo 🚀 Crypto API Monitor Pro v2.0
7
+ echo Professional Dashboard - 40+ Providers
8
  echo ========================================
9
  echo.
10
 
11
  python --version > nul 2>&1
12
  if %errorlevel% neq 0 (
13
  echo ❌ Python not found!
14
+ echo Install from: https://python.org/downloads/
15
  pause
16
  exit /b 1
17
  )
 
32
 
33
  echo.
34
  echo ========================================
35
+ echo 🎯 Starting Server...
36
  echo ========================================
37
  echo.
38
  echo 📊 Dashboard: http://localhost:8000/dashboard
39
  echo 📡 API Docs: http://localhost:8000/docs
40
  echo.
41
+ echo 💡 Press Ctrl+C to stop
 
 
 
 
 
42
  echo.
 
43
  echo ========================================
44
  echo.
45
 
start_server.py CHANGED
@@ -1,241 +1,19 @@
1
- #!/usr/bin/env python3
2
- """
3
- 🚀 Crypto Monitor ULTIMATE - Launcher Script
4
- اسکریپت راه‌انداز سریع برای سرور
5
- """
6
-
7
- import sys
8
- import subprocess
9
- import os
10
- from pathlib import Path
11
-
12
-
13
- def check_dependencies():
14
- """بررسی وابستگی‌های لازم"""
15
- print("🔍 بررسی وابستگی‌ها...")
16
-
17
- required_packages = [
18
- 'fastapi',
19
- 'uvicorn',
20
- 'aiohttp',
21
- 'pydantic'
22
- ]
23
-
24
- missing = []
25
- for package in required_packages:
26
- try:
27
- __import__(package)
28
- print(f" ✅ {package}")
29
- except ImportError:
30
- missing.append(package)
31
- print(f" ❌ {package} - نصب نشده")
32
-
33
- if missing:
34
- print(f"\n⚠️ {len(missing)} پکیج نصب نشده است!")
35
- response = input("آیا می‌خواهید الان نصب شوند? (y/n): ")
36
- if response.lower() == 'y':
37
- install_dependencies()
38
- else:
39
- print("❌ بدون نصب وابستگی‌ها، سرور نمی‌تواند اجرا شود.")
40
- sys.exit(1)
41
- else:
42
- print("✅ همه وابستگی‌ها نصب شده‌اند\n")
43
-
44
-
45
- def install_dependencies():
46
- """نصب وابستگی‌ها از requirements.txt"""
47
- print("\n📦 در حال نصب وابستگی‌ها...")
48
- try:
49
- subprocess.check_call([
50
- sys.executable, "-m", "pip", "install", "-r", "requirements.txt"
51
- ])
52
- print("✅ همه وابستگی‌ها با موفقیت نصب شدند\n")
53
- except subprocess.CalledProcessError:
54
- print("❌ خطا در نصب وابستگی‌ها")
55
- sys.exit(1)
56
-
57
-
58
- def check_config_files():
59
- """بررسی فایل‌های پیکربندی"""
60
- print("🔍 بررسی فایل‌های پیکربندی...")
61
-
62
- config_file = Path("providers_config_extended.json")
63
- if not config_file.exists():
64
- print(f" ❌ {config_file} یافت نشد!")
65
- print(" لطفاً این فایل را از مخزن دانلود کنید.")
66
- sys.exit(1)
67
- else:
68
- print(f" ✅ {config_file}")
69
-
70
- dashboard_file = Path("unified_dashboard.html")
71
- if not dashboard_file.exists():
72
- print(f" ⚠️ {dashboard_file} یافت نشد - داشبورد در دسترس نخواهد بود")
73
- else:
74
- print(f" ✅ {dashboard_file}")
75
-
76
- print()
77
-
78
-
79
- def show_banner():
80
- """نمایش بنر استارت"""
81
- banner = """
82
- ╔═══════════════════════════════════════════════════════════╗
83
- ║ ║
84
- ║ 🚀 Crypto Monitor ULTIMATE 🚀 ║
85
- ║ ║
86
- ║ نسخه توسعه‌یافته با ۱۰۰+ ارائه‌دهنده API رایگان ║
87
- ║ + سیستم پیشرفته Provider Pool Management ║
88
- ║ ║
89
- ║ Version: 2.0.0 ║
90
- ║ Author: Crypto Monitor Team ║
91
- ║ ║
92
- ╚═══════════════════════════════════════════════════════════╝
93
- """
94
- print(banner)
95
-
96
-
97
- def show_menu():
98
- """نمایش منوی انتخاب"""
99
- print("\n📋 انتخاب کنید:")
100
- print(" 1️⃣ اجرای سرور (Production Mode)")
101
- print(" 2️⃣ اجرای سرور (Development Mode - با Auto Reload)")
102
- print(" 3️⃣ تست Provider Manager")
103
- print(" 4️⃣ نمایش آمار ارائه‌دهندگان")
104
- print(" 5️⃣ نصب/بروزرسانی وابستگی‌ها")
105
- print(" 0️⃣ خروج")
106
- print()
107
-
108
-
109
- def run_server_production():
110
- """اجرای سرور در حالت Production"""
111
- print("\n🚀 راه‌اندازی سرور در حالت Production...")
112
- print("📡 آدرس: http://localhost:8000")
113
- print("📊 داشبورد: http://localhost:8000")
114
- print("📖 API Docs: http://localhost:8000/docs")
115
- print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید\n")
116
-
117
- try:
118
- subprocess.run([
119
- sys.executable, "-m", "uvicorn",
120
- "api_server_extended:app",
121
- "--host", "0.0.0.0",
122
- "--port", "8000",
123
- "--log-level", "info"
124
- ])
125
- except KeyboardInterrupt:
126
- print("\n\n🛑 سرور متوقف شد")
127
-
128
-
129
- def run_server_development():
130
- """اجرای سرور در حالت Development"""
131
- print("\n🔧 راه‌اندازی سرور در حالت Development (Auto Reload)...")
132
- print("📡 آدرس: http://localhost:8000")
133
- print("📊 داشبورد: http://localhost:8000")
134
- print("📖 API Docs: http://localhost:8000/docs")
135
- print("\n⏸️ برای توقف سرور Ctrl+C را فشار دهید")
136
- print("♻️ تغییرات فایل‌ها به‌طور خودکار اعمال می‌شود\n")
137
-
138
- try:
139
- subprocess.run([
140
- sys.executable, "-m", "uvicorn",
141
- "api_server_extended:app",
142
- "--host", "0.0.0.0",
143
- "--port", "8000",
144
- "--reload",
145
- "--log-level", "debug"
146
- ])
147
- except KeyboardInterrupt:
148
- print("\n\n🛑 سرور متوقف شد")
149
-
150
-
151
- def test_provider_manager():
152
- """تست Provider Manager"""
153
- print("\n🧪 اجرای تست Provider Manager...\n")
154
- try:
155
- subprocess.run([sys.executable, "provider_manager.py"])
156
- except FileNotFoundError:
157
- print("❌ فایل provider_manager.py یافت نشد")
158
- except KeyboardInterrupt:
159
- print("\n\n🛑 تست متوقف شد")
160
-
161
-
162
- def show_stats():
163
- """نمایش آمار ارائه‌دهندگان"""
164
- print("\n📊 نمایش آمار ارائه‌دهندگان...\n")
165
- try:
166
- from provider_manager import ProviderManager
167
- manager = ProviderManager()
168
- stats = manager.get_all_stats()
169
-
170
- summary = stats['summary']
171
- print("=" * 60)
172
- print(f"📈 آمار کلی سیستم")
173
- print("=" * 60)
174
- print(f" کل ارائه‌دهندگان: {summary['total_providers']}")
175
- print(f" آنلاین: {summary['online']}")
176
- print(f" آفلاین: {summary['offline']}")
177
- print(f" Degraded: {summary['degraded']}")
178
- print(f" کل درخواست‌ها: {summary['total_requests']}")
179
- print(f" درخواست‌های موفق: {summary['successful_requests']}")
180
- print(f" نرخ موفقیت: {summary['overall_success_rate']:.2f}%")
181
- print("=" * 60)
182
-
183
- print(f"\n🔄 Pool‌های موجود: {len(stats['pools'])}")
184
- for pool_id, pool_data in stats['pools'].items():
185
- print(f"\n 📦 {pool_data['pool_name']}")
186
- print(f" دسته: {pool_data['category']}")
187
- print(f" استراتژی: {pool_data['rotation_strategy']}")
188
- print(f" اعضا: {pool_data['total_providers']}")
189
- print(f" در دسترس: {pool_data['available_providers']}")
190
-
191
- print("\n✅ برای جزئیات بیشتر، سرور را اجرا کرده و به داشبورد مراجعه کنید")
192
-
193
- except ImportError:
194
- print("❌ خطا: provider_manager.py یافت نشد یا وابستگی‌ها نصب نشده‌اند")
195
- except Exception as e:
196
- print(f"❌ خطا: {e}")
197
-
198
-
199
- def main():
200
- """تابع اصلی"""
201
- show_banner()
202
-
203
- # بررسی وابستگی‌ها
204
- check_dependencies()
205
-
206
- # بررسی فایل‌های پیکربندی
207
- check_config_files()
208
-
209
- # حلقه منو
210
- while True:
211
- show_menu()
212
- choice = input("انتخاب شما: ").strip()
213
-
214
- if choice == "1":
215
- run_server_production()
216
- break
217
- elif choice == "2":
218
- run_server_development()
219
- break
220
- elif choice == "3":
221
- test_provider_manager()
222
- input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
223
- elif choice == "4":
224
- show_stats()
225
- input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
226
- elif choice == "5":
227
- install_dependencies()
228
- input("\n⏎ Enter را برای بازگشت به منو فشار دهید...")
229
- elif choice == "0":
230
- print("\n👋 خداحافظ!")
231
- sys.exit(0)
232
- else:
233
- print("\n❌ انتخاب نامعتبر! لطفاً دوباره تلاش کنید.")
234
-
235
 
236
  if __name__ == "__main__":
237
- try:
238
- main()
239
- except KeyboardInterrupt:
240
- print("\n\n👋 برنامه متوقف شد")
241
- sys.exit(0)
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple server startup script"""
2
+ import uvicorn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  if __name__ == "__main__":
5
+ print("=" * 60)
6
+ print("Starting Crypto API Monitor Backend")
7
+ print("Server will be available at: http://localhost:7860")
8
+ print("Frontend: http://localhost:7860/index.html")
9
+ print("HF Console: http://localhost:7860/hf_console.html")
10
+ print("API Docs: http://localhost:7860/docs")
11
+ print("=" * 60)
12
+
13
+ uvicorn.run(
14
+ "app:app",
15
+ host="0.0.0.0",
16
+ port=7860,
17
+ log_level="info",
18
+ access_log=True
19
+ )
utils/__init__.py CHANGED
@@ -1,20 +0,0 @@
1
- """Utility package for API monitor/crypto aggregator.
2
-
3
- Provides convenient accessors like `setup_logging` and shared helpers.
4
- """
5
-
6
- from .logger import setup_logger, app_logger
7
-
8
- def setup_logging(name: str = "crypto_monitor", level: str = "INFO"):
9
- """Backward-compatible wrapper expected by older code.
10
-
11
- Older modules call `utils.setup_logging()`. We now route that to the
12
- structured logger configuration in `utils.logger`.
13
- """
14
- return setup_logger(name=name, level=level)
15
-
16
- __all__ = [
17
- "setup_logger",
18
- "setup_logging",
19
- "app_logger",
20
- ]