Really-amin commited on
Commit
bb72f9c
·
verified ·
1 Parent(s): babab88

Upload 322 files

Browse files
Files changed (4) hide show
  1. .dockerignore +115 -12
  2. Dockerfile +14 -6
  3. app.py +1228 -6
  4. index.html +49 -4
.dockerignore CHANGED
@@ -1,18 +1,121 @@
 
1
  __pycache__/
2
- *.pyc
3
- *.pyo
4
- *.pyd
5
- *.sqlite3
6
- *.db
7
- *.log
8
- .env
9
- .venv/
10
- venv/
11
- dist/
12
  build/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  .git/
14
  .gitignore
15
- *.zip
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  *.tar
17
  *.tar.gz
18
- *.tgz
 
 
 
 
 
 
 
 
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
Dockerfile CHANGED
@@ -1,16 +1,24 @@
1
- FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
5
- ENV PIP_NO_CACHE_DIR=1 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
 
6
 
7
- COPY requirements_hf.txt ./requirements.txt
8
- RUN pip install --upgrade pip && pip install -r requirements.txt
 
9
 
 
10
  COPY . .
11
 
12
- ENV HF_MODE=off
 
 
 
13
 
 
14
  EXPOSE 7860
15
 
16
- CMD ["sh", "-c", "uvicorn app:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
 
1
+ FROM python:3.10
2
 
3
  WORKDIR /app
4
 
5
+ # Create required directories
6
+ RUN mkdir -p /app/logs /app/data /app/data/database /app/data/backups
7
 
8
+ # Copy requirements and install dependencies
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
 
12
+ # Copy application code
13
  COPY . .
14
 
15
+ # Set environment variables
16
+ ENV USE_MOCK_DATA=false
17
+ ENV PORT=7860
18
+ ENV PYTHONUNBUFFERED=1
19
 
20
+ # Expose port
21
  EXPOSE 7860
22
 
23
+ # Launch command
24
+ CMD ["uvicorn", "hf_unified_server:app", "--host", "0.0.0.0", "--port", "7860"]
app.py CHANGED
@@ -1,10 +1,1232 @@
1
- from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import sys
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- BASE_DIR = Path(__file__).resolve().parent
5
- FINAL_DIR = BASE_DIR / "final"
6
 
7
- if str(FINAL_DIR) not in sys.path:
8
- sys.path.insert(0, str(FINAL_DIR))
9
 
10
- from hf_unified_server import app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Crypto Data Aggregator - Admin Dashboard (Gradio App)
4
+ STRICT REAL-DATA-ONLY implementation for Hugging Face Spaces
5
+
6
+ 7 Tabs:
7
+ 1. Status - System health & overview
8
+ 2. Providers - API provider management
9
+ 3. Market Data - Live cryptocurrency data
10
+ 4. APL Scanner - Auto Provider Loader
11
+ 5. HF Models - Hugging Face model status
12
+ 6. Diagnostics - System diagnostics & auto-repair
13
+ 7. Logs - System logs viewer
14
+ """
15
+
16
  import sys
17
+ import os
18
+ import logging
19
+ from pathlib import Path
20
+ from typing import Dict, List, Any, Tuple, Optional
21
+ from datetime import datetime
22
+ import json
23
+ import traceback
24
+ import asyncio
25
+ import time
26
+
27
+ # Check for Gradio
28
+ try:
29
+ import gradio as gr
30
+ except ImportError:
31
+ print("ERROR: gradio not installed. Run: pip install gradio")
32
+ sys.exit(1)
33
+
34
+ # Check for optional dependencies
35
+ try:
36
+ import pandas as pd
37
+ PANDAS_AVAILABLE = True
38
+ except ImportError:
39
+ PANDAS_AVAILABLE = False
40
+ print("WARNING: pandas not installed. Some features disabled.")
41
+
42
+ try:
43
+ import plotly.graph_objects as go
44
+ from plotly.subplots import make_subplots
45
+ PLOTLY_AVAILABLE = True
46
+ except ImportError:
47
+ PLOTLY_AVAILABLE = False
48
+ print("WARNING: plotly not installed. Charts disabled.")
49
+
50
+ # Import local modules
51
+ import config
52
+ import database
53
+ import collectors
54
+
55
+ # ==================== INDEPENDENT LOGGING SETUP ====================
56
+ # DO NOT use utils.setup_logging() - set up independently
57
+
58
+ logger = logging.getLogger("app")
59
+ if not logger.handlers:
60
+ level_name = getattr(config, "LOG_LEVEL", "INFO")
61
+ level = getattr(logging, level_name.upper(), logging.INFO)
62
+ logger.setLevel(level)
63
+
64
+ formatter = logging.Formatter(
65
+ getattr(config, "LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
66
+ )
67
+
68
+ # Console handler
69
+ ch = logging.StreamHandler()
70
+ ch.setFormatter(formatter)
71
+ logger.addHandler(ch)
72
+
73
+ # File handler if log file exists
74
+ try:
75
+ if hasattr(config, 'LOG_FILE'):
76
+ fh = logging.FileHandler(config.LOG_FILE)
77
+ fh.setFormatter(formatter)
78
+ logger.addHandler(fh)
79
+ except Exception as e:
80
+ print(f"Warning: Could not setup file logging: {e}")
81
+
82
+ logger.info("=" * 60)
83
+ logger.info("Crypto Admin Dashboard Starting")
84
+ logger.info("=" * 60)
85
+
86
+ # Initialize database
87
+ db = database.get_database()
88
+
89
+
90
+ # ==================== TAB 1: STATUS ====================
91
+
92
+ def get_status_tab() -> Tuple[str, str, str]:
93
+ """
94
+ Get system status overview.
95
+ Returns: (markdown_summary, db_stats_json, system_info_json)
96
+ """
97
+ try:
98
+ # Get database stats
99
+ db_stats = db.get_database_stats()
100
+
101
+ # Count providers
102
+ providers_config_path = config.BASE_DIR / "providers_config_extended.json"
103
+ provider_count = 0
104
+ if providers_config_path.exists():
105
+ with open(providers_config_path, 'r') as f:
106
+ providers_data = json.load(f)
107
+ provider_count = len(providers_data.get('providers', {}))
108
+
109
+ # Pool count (from config)
110
+ pool_count = 0
111
+ if providers_config_path.exists():
112
+ with open(providers_config_path, 'r') as f:
113
+ providers_data = json.load(f)
114
+ pool_count = len(providers_data.get('pool_configurations', []))
115
+
116
+ # Market snapshot
117
+ latest_prices = db.get_latest_prices(3)
118
+ market_snapshot = ""
119
+ if latest_prices:
120
+ for p in latest_prices[:3]:
121
+ symbol = p.get('symbol', 'N/A')
122
+ price = p.get('price_usd', 0)
123
+ change = p.get('percent_change_24h', 0)
124
+ market_snapshot += f"**{symbol}**: ${price:,.2f} ({change:+.2f}%)\n"
125
+ else:
126
+ market_snapshot = "No market data available yet."
127
+
128
+ # Get API request count from health log
129
+ api_requests_count = 0
130
+ try:
131
+ health_log_path = Path("data/logs/provider_health.jsonl")
132
+ if health_log_path.exists():
133
+ with open(health_log_path, 'r', encoding='utf-8') as f:
134
+ api_requests_count = sum(1 for _ in f)
135
+ except Exception as e:
136
+ logger.warning(f"Could not get API request stats: {e}")
137
+
138
+ # Build summary with copy-friendly format
139
+ summary = f"""
140
+ ## 🎯 System Status
141
+
142
+ **Overall Health**: {"🟢 Operational" if db_stats.get('prices_count', 0) > 0 else "🟡 Initializing"}
143
+
144
+ ### Quick Stats
145
+ ```
146
+ Total Providers: {provider_count}
147
+ Active Pools: {pool_count}
148
+ API Requests: {api_requests_count:,}
149
+ Price Records: {db_stats.get('prices_count', 0):,}
150
+ News Articles: {db_stats.get('news_count', 0):,}
151
+ Unique Symbols: {db_stats.get('unique_symbols', 0)}
152
+ ```
153
+
154
+ ### Market Snapshot (Top 3)
155
+ ```
156
+ {market_snapshot}
157
+ ```
158
+
159
+ **Last Update**: `{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}`
160
+
161
+ ---
162
+ ### 📋 Provider Details (Copy-Friendly)
163
+ ```
164
+ Total: {provider_count} providers
165
+ Config File: providers_config_extended.json
166
+ ```
167
+ """
168
+
169
+ # System info
170
+ import platform
171
+ system_info = {
172
+ "Python Version": sys.version.split()[0],
173
+ "Platform": platform.platform(),
174
+ "Working Directory": str(config.BASE_DIR),
175
+ "Database Size": f"{db_stats.get('database_size_mb', 0):.2f} MB",
176
+ "Last Price Update": db_stats.get('latest_price_update', 'N/A'),
177
+ "Last News Update": db_stats.get('latest_news_update', 'N/A')
178
+ }
179
+
180
+ return summary, json.dumps(db_stats, indent=2), json.dumps(system_info, indent=2)
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error in get_status_tab: {e}\n{traceback.format_exc()}")
184
+ return f"⚠️ Error loading status: {str(e)}", "{}", "{}"
185
+
186
+
187
+ def run_diagnostics_from_status(auto_fix: bool) -> str:
188
+ """Run diagnostics from status tab"""
189
+ try:
190
+ from backend.services.diagnostics_service import DiagnosticsService
191
+
192
+ diagnostics = DiagnosticsService()
193
+
194
+ # Run async in sync context
195
+ loop = asyncio.new_event_loop()
196
+ asyncio.set_event_loop(loop)
197
+ report = loop.run_until_complete(diagnostics.run_full_diagnostics(auto_fix=auto_fix))
198
+ loop.close()
199
+
200
+ # Format output
201
+ output = f"""
202
+ # Diagnostics Report
203
+
204
+ **Timestamp**: {report.timestamp}
205
+ **Duration**: {report.duration_ms:.2f}ms
206
+
207
+ ## Summary
208
+ - **Total Issues**: {report.total_issues}
209
+ - **Critical**: {report.critical_issues}
210
+ - **Warnings**: {report.warnings}
211
+ - **Info**: {report.info_issues}
212
+ - **Fixed**: {len(report.fixed_issues)}
213
+
214
+ ## Issues
215
+ """
216
+ for issue in report.issues:
217
+ emoji = {"critical": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
218
+ fixed_mark = " ✅ FIXED" if issue.auto_fixed else ""
219
+ output += f"\n### {emoji} [{issue.category.upper()}] {issue.title}{fixed_mark}\n"
220
+ output += f"{issue.description}\n"
221
+ if issue.fixable and not issue.auto_fixed:
222
+ output += f"**Fix**: `{issue.fix_action}`\n"
223
+
224
+ return output
225
+
226
+ except Exception as e:
227
+ logger.error(f"Error running diagnostics: {e}")
228
+ return f"❌ Diagnostics failed: {str(e)}"
229
+
230
+
231
+ # ==================== TAB 2: PROVIDERS ====================
232
+
233
+ def get_providers_table(category_filter: str = "All") -> Any:
234
+ """
235
+ Get providers from providers_config_extended.json with enhanced formatting
236
+ Returns: DataFrame or dict
237
+ """
238
+ try:
239
+ providers_path = config.BASE_DIR / "providers_config_extended.json"
240
+
241
+ if not providers_path.exists():
242
+ if PANDAS_AVAILABLE:
243
+ return pd.DataFrame({"Error": ["providers_config_extended.json not found"]})
244
+ return {"error": "providers_config_extended.json not found"}
245
+
246
+ with open(providers_path, 'r') as f:
247
+ data = json.load(f)
248
+
249
+ providers = data.get('providers', {})
250
+
251
+ # Build table data with copy-friendly IDs
252
+ table_data = []
253
+ for provider_id, provider_info in providers.items():
254
+ if category_filter != "All":
255
+ if provider_info.get('category', '').lower() != category_filter.lower():
256
+ continue
257
+
258
+ # Format auth status with emoji
259
+ auth_status = "✅ Yes" if provider_info.get('requires_auth', False) else "❌ No"
260
+ validation = "✅ Valid" if provider_info.get('validated', False) else "⏳ Pending"
261
+
262
+ table_data.append({
263
+ "Provider ID": provider_id,
264
+ "Name": provider_info.get('name', provider_id),
265
+ "Category": provider_info.get('category', 'unknown'),
266
+ "Type": provider_info.get('type', 'http_json'),
267
+ "Base URL": provider_info.get('base_url', 'N/A'),
268
+ "Auth Required": auth_status,
269
+ "Priority": provider_info.get('priority', 'N/A'),
270
+ "Status": validation
271
+ })
272
+
273
+ if PANDAS_AVAILABLE:
274
+ return pd.DataFrame(table_data) if table_data else pd.DataFrame({"Message": ["No providers found"]})
275
+ else:
276
+ return {"providers": table_data} if table_data else {"error": "No providers found"}
277
+
278
+ except Exception as e:
279
+ logger.error(f"Error loading providers: {e}")
280
+ if PANDAS_AVAILABLE:
281
+ return pd.DataFrame({"Error": [str(e)]})
282
+ return {"error": str(e)}
283
+
284
+
285
+ def reload_providers_config() -> Tuple[Any, str]:
286
+ """Reload providers config and return updated table + message with stats"""
287
+ try:
288
+ # Count providers
289
+ providers_path = config.BASE_DIR / "providers_config_extended.json"
290
+ with open(providers_path, 'r') as f:
291
+ data = json.load(f)
292
+
293
+ total_providers = len(data.get('providers', {}))
294
+
295
+ # Count by category
296
+ categories = {}
297
+ for provider_info in data.get('providers', {}).values():
298
+ cat = provider_info.get('category', 'unknown')
299
+ categories[cat] = categories.get(cat, 0) + 1
300
+
301
+ # Force reload by re-reading file
302
+ table = get_providers_table("All")
303
+
304
+ # Build detailed message
305
+ message = f"""✅ **Providers Reloaded Successfully!**
306
+
307
+ **Total Providers**: `{total_providers}`
308
+ **Reload Time**: `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
309
+
310
+ **By Category**:
311
+ """
312
+ for cat, count in sorted(categories.items(), key=lambda x: x[1], reverse=True)[:10]:
313
+ message += f"- {cat}: `{count}`\n"
314
+
315
+ return table, message
316
+ except Exception as e:
317
+ logger.error(f"Error reloading providers: {e}")
318
+ return get_providers_table("All"), f"❌ Reload failed: {str(e)}"
319
+
320
+
321
+ def get_provider_categories() -> List[str]:
322
+ """Get unique provider categories"""
323
+ try:
324
+ providers_path = config.BASE_DIR / "providers_config_extended.json"
325
+ if not providers_path.exists():
326
+ return ["All"]
327
+
328
+ with open(providers_path, 'r') as f:
329
+ data = json.load(f)
330
+
331
+ categories = set()
332
+ for provider in data.get('providers', {}).values():
333
+ cat = provider.get('category', 'unknown')
334
+ categories.add(cat)
335
+
336
+ return ["All"] + sorted(list(categories))
337
+ except Exception as e:
338
+ logger.error(f"Error getting categories: {e}")
339
+ return ["All"]
340
+
341
+
342
+ # ==================== TAB 3: MARKET DATA ====================
343
+
344
+ def get_market_data_table(search_filter: str = "") -> Any:
345
+ """Get latest market data from database with enhanced formatting"""
346
+ try:
347
+ prices = db.get_latest_prices(100)
348
+
349
+ if not prices:
350
+ if PANDAS_AVAILABLE:
351
+ return pd.DataFrame({"Message": ["No market data available. Click 'Refresh Prices' to collect data."]})
352
+ return {"error": "No data available"}
353
+
354
+ # Filter if search provided
355
+ filtered_prices = prices
356
+ if search_filter:
357
+ search_lower = search_filter.lower()
358
+ filtered_prices = [
359
+ p for p in prices
360
+ if search_lower in p.get('name', '').lower() or search_lower in p.get('symbol', '').lower()
361
+ ]
362
+
363
+ table_data = []
364
+ for p in filtered_prices:
365
+ # Format change with emoji
366
+ change = p.get('percent_change_24h', 0)
367
+ change_emoji = "🟢" if change > 0 else ("🔴" if change < 0 else "⚪")
368
+
369
+ table_data.append({
370
+ "#": p.get('rank', 999),
371
+ "Symbol": p.get('symbol', 'N/A'),
372
+ "Name": p.get('name', 'Unknown'),
373
+ "Price": f"${p.get('price_usd', 0):,.2f}" if p.get('price_usd') else "N/A",
374
+ "24h Change": f"{change_emoji} {change:+.2f}%" if change is not None else "N/A",
375
+ "Volume 24h": f"${p.get('volume_24h', 0):,.0f}" if p.get('volume_24h') else "N/A",
376
+ "Market Cap": f"${p.get('market_cap', 0):,.0f}" if p.get('market_cap') else "N/A"
377
+ })
378
+
379
+ if PANDAS_AVAILABLE:
380
+ df = pd.DataFrame(table_data)
381
+ return df.sort_values('#') if not df.empty else pd.DataFrame({"Message": ["No matching data"]})
382
+ else:
383
+ return {"prices": table_data}
384
+
385
+ except Exception as e:
386
+ logger.error(f"Error getting market data: {e}")
387
+ if PANDAS_AVAILABLE:
388
+ return pd.DataFrame({"Error": [str(e)]})
389
+ return {"error": str(e)}
390
+
391
+
392
+ def refresh_market_data() -> Tuple[Any, str]:
393
+ """Refresh market data by collecting from APIs with detailed stats"""
394
+ try:
395
+ logger.info("Refreshing market data...")
396
+ start_time = time.time()
397
+ success, count = collectors.collect_price_data()
398
+ duration = time.time() - start_time
399
+
400
+ # Get database stats
401
+ db_stats = db.get_database_stats()
402
+
403
+ if success:
404
+ message = f"""✅ **Market Data Refreshed Successfully!**
405
+
406
+ **Collection Stats**:
407
+ - New Records: `{count}`
408
+ - Duration: `{duration:.2f}s`
409
+ - Time: `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`
410
+
411
+ **Database Stats**:
412
+ - Total Price Records: `{db_stats.get('prices_count', 0):,}`
413
+ - Unique Symbols: `{db_stats.get('unique_symbols', 0)}`
414
+ - Last Update: `{db_stats.get('latest_price_update', 'N/A')}`
415
+ """
416
+ else:
417
+ message = f"""⚠️ **Collection completed with issues**
418
+
419
+ - Records Collected: `{count}`
420
+ - Duration: `{duration:.2f}s`
421
+ - Check logs for details
422
+ """
423
+
424
+ # Return updated table
425
+ table = get_market_data_table("")
426
+ return table, message
427
+
428
+ except Exception as e:
429
+ logger.error(f"Error refreshing market data: {e}")
430
+ return get_market_data_table(""), f"❌ Refresh failed: {str(e)}"
431
+
432
+
433
+ def plot_price_history(symbol: str, timeframe: str) -> Any:
434
+ """Plot price history for a symbol"""
435
+ if not PLOTLY_AVAILABLE:
436
+ return None
437
+
438
+ try:
439
+ # Parse timeframe
440
+ hours_map = {"24h": 24, "7d": 168, "30d": 720, "90d": 2160}
441
+ hours = hours_map.get(timeframe, 168)
442
+
443
+ # Get history
444
+ history = db.get_price_history(symbol.upper(), hours)
445
+
446
+ if not history or len(history) < 2:
447
+ fig = go.Figure()
448
+ fig.add_annotation(
449
+ text=f"No historical data for {symbol}",
450
+ xref="paper", yref="paper",
451
+ x=0.5, y=0.5, showarrow=False
452
+ )
453
+ return fig
454
+
455
+ # Extract data
456
+ timestamps = [datetime.fromisoformat(h['timestamp'].replace('Z', '+00:00')) if isinstance(h['timestamp'], str) else datetime.now() for h in history]
457
+ prices = [h.get('price_usd', 0) for h in history]
458
+
459
+ # Create plot
460
+ fig = go.Figure()
461
+ fig.add_trace(go.Scatter(
462
+ x=timestamps,
463
+ y=prices,
464
+ mode='lines',
465
+ name='Price',
466
+ line=dict(color='#2962FF', width=2)
467
+ ))
468
+
469
+ fig.update_layout(
470
+ title=f"{symbol} - {timeframe}",
471
+ xaxis_title="Time",
472
+ yaxis_title="Price (USD)",
473
+ hovermode='x unified',
474
+ height=400
475
+ )
476
+
477
+ return fig
478
+
479
+ except Exception as e:
480
+ logger.error(f"Error plotting price history: {e}")
481
+ fig = go.Figure()
482
+ fig.add_annotation(text=f"Error: {str(e)}", xref="paper", yref="paper", x=0.5, y=0.5, showarrow=False)
483
+ return fig
484
+
485
+
486
+ # ==================== TAB 4: APL SCANNER ====================
487
+
488
+ def run_apl_scan() -> str:
489
+ """Run Auto Provider Loader scan"""
490
+ try:
491
+ logger.info("Running APL scan...")
492
+
493
+ # Import APL
494
+ import auto_provider_loader
495
+
496
+ # Run scan
497
+ apl = auto_provider_loader.AutoProviderLoader()
498
+
499
+ # Run async in sync context
500
+ loop = asyncio.new_event_loop()
501
+ asyncio.set_event_loop(loop)
502
+ loop.run_until_complete(apl.run())
503
+ loop.close()
504
+
505
+ # Build summary
506
+ stats = apl.stats
507
+ output = f"""
508
+ # APL Scan Complete
509
+
510
+ **Timestamp**: {stats.timestamp}
511
+ **Execution Time**: {stats.execution_time_sec:.2f}s
512
+
513
+ ## HTTP Providers
514
+ - **Candidates**: {stats.total_http_candidates}
515
+ - **Valid**: {stats.http_valid} ✅
516
+ - **Invalid**: {stats.http_invalid} ❌
517
+ - **Conditional**: {stats.http_conditional} ⚠️
518
+
519
+ ## HuggingFace Models
520
+ - **Candidates**: {stats.total_hf_candidates}
521
+ - **Valid**: {stats.hf_valid} ✅
522
+ - **Invalid**: {stats.hf_invalid} ❌
523
+ - **Conditional**: {stats.hf_conditional} ⚠️
524
+
525
+ ## Total Active Providers
526
+ **{stats.total_active_providers}** providers are now active.
527
+
528
+ ---
529
+
530
+ ✅ All valid providers have been integrated into `providers_config_extended.json`.
531
+
532
+ See `PROVIDER_AUTO_DISCOVERY_REPORT.md` for full details.
533
+ """
534
+
535
+ return output
536
+
537
+ except Exception as e:
538
+ logger.error(f"Error running APL: {e}\n{traceback.format_exc()}")
539
+ return f"❌ APL scan failed: {str(e)}\n\nCheck logs for details."
540
+
541
+
542
+ def get_apl_report() -> str:
543
+ """Get last APL report"""
544
+ try:
545
+ report_path = config.BASE_DIR / "PROVIDER_AUTO_DISCOVERY_REPORT.md"
546
+ if report_path.exists():
547
+ with open(report_path, 'r') as f:
548
+ return f.read()
549
+ else:
550
+ return "No APL report found. Run a scan first."
551
+ except Exception as e:
552
+ logger.error(f"Error reading APL report: {e}")
553
+ return f"Error reading report: {str(e)}"
554
+
555
+
556
+ # ==================== TAB 5: HF MODELS ====================
557
+
558
+ def get_hf_models_status() -> Any:
559
+ """Get HuggingFace models status with unified display"""
560
+ try:
561
+ import ai_models
562
+
563
+ model_info = ai_models.get_model_info()
564
+
565
+ # Build unified table - avoid duplicates
566
+ table_data = []
567
+ seen_models = set()
568
+
569
+ # First, add loaded models
570
+ if model_info.get('models_initialized'):
571
+ for model_name, loaded in model_info.get('loaded_models', {}).items():
572
+ if model_name not in seen_models:
573
+ status = "✅ Loaded" if loaded else "❌ Failed"
574
+ model_id = config.HUGGINGFACE_MODELS.get(model_name, 'N/A')
575
+ table_data.append({
576
+ "Model Type": model_name,
577
+ "Model ID": model_id,
578
+ "Status": status,
579
+ "Source": "config.py"
580
+ })
581
+ seen_models.add(model_name)
582
+
583
+ # Then add configured but not loaded models
584
+ for model_type, model_id in config.HUGGINGFACE_MODELS.items():
585
+ if model_type not in seen_models:
586
+ table_data.append({
587
+ "Model Type": model_type,
588
+ "Model ID": model_id,
589
+ "Status": "⏳ Not Loaded",
590
+ "Source": "config.py"
591
+ })
592
+ seen_models.add(model_type)
593
+
594
+ # Add models from providers_config if any
595
+ try:
596
+ providers_path = config.BASE_DIR / "providers_config_extended.json"
597
+ if providers_path.exists():
598
+ with open(providers_path, 'r') as f:
599
+ providers_data = json.load(f)
600
+
601
+ for provider_id, provider_info in providers_data.get('providers', {}).items():
602
+ if provider_info.get('category') == 'hf-model':
603
+ model_name = provider_info.get('name', provider_id)
604
+ if model_name not in seen_models:
605
+ table_data.append({
606
+ "Model Type": model_name,
607
+ "Model ID": provider_id,
608
+ "Status": "📚 Registry",
609
+ "Source": "providers_config"
610
+ })
611
+ seen_models.add(model_name)
612
+ except Exception as e:
613
+ logger.warning(f"Could not load models from providers_config: {e}")
614
+
615
+ if not table_data:
616
+ table_data.append({
617
+ "Model Type": "No models",
618
+ "Model ID": "N/A",
619
+ "Status": "⚠️ None configured",
620
+ "Source": "N/A"
621
+ })
622
+
623
+ if PANDAS_AVAILABLE:
624
+ return pd.DataFrame(table_data)
625
+ else:
626
+ return {"models": table_data}
627
+
628
+ except Exception as e:
629
+ logger.error(f"Error getting HF models status: {e}")
630
+ if PANDAS_AVAILABLE:
631
+ return pd.DataFrame({"Error": [str(e)]})
632
+ return {"error": str(e)}
633
+
634
+
635
+ def test_hf_model(model_name: str, test_text: str) -> str:
636
+ """Test a HuggingFace model with text"""
637
+ try:
638
+ if not test_text or not test_text.strip():
639
+ return "⚠️ Please enter test text"
640
+
641
+ import ai_models
642
+
643
+ if model_name in ["sentiment_twitter", "sentiment_financial", "sentiment"]:
644
+ # Test sentiment analysis
645
+ result = ai_models.analyze_sentiment(test_text)
646
+
647
+ output = f"""
648
+ ## Sentiment Analysis Result
649
+
650
+ **Input**: {test_text}
651
+
652
+ **Label**: {result.get('label', 'N/A')}
653
+ **Score**: {result.get('score', 0):.4f}
654
+ **Confidence**: {result.get('confidence', 0):.4f}
655
+
656
+ **Details**:
657
+ ```json
658
+ {json.dumps(result.get('details', {}), indent=2)}
659
+ ```
660
+ """
661
+ return output
662
+
663
+ elif model_name == "summarization":
664
+ # Test summarization
665
+ summary = ai_models.summarize_text(test_text)
666
+
667
+ output = f"""
668
+ ## Summarization Result
669
+
670
+ **Original** ({len(test_text)} chars):
671
+ {test_text}
672
+
673
+ **Summary** ({len(summary)} chars):
674
+ {summary}
675
+ """
676
+ return output
677
+
678
+ else:
679
+ return f"⚠️ Model '{model_name}' not recognized or not testable"
680
+
681
+ except Exception as e:
682
+ logger.error(f"Error testing HF model: {e}")
683
+ return f"❌ Model test failed: {str(e)}"
684
+
685
+
686
+ def initialize_hf_models() -> Tuple[Any, str]:
687
+ """Initialize HuggingFace models"""
688
+ try:
689
+ import ai_models
690
+
691
+ result = ai_models.initialize_models()
692
+
693
+ if result.get('success'):
694
+ message = f"✅ Models initialized successfully at {datetime.now().strftime('%H:%M:%S')}"
695
+ else:
696
+ message = f"⚠️ Model initialization completed with warnings: {result.get('status')}"
697
+
698
+ # Return updated table
699
+ table = get_hf_models_status()
700
+ return table, message
701
+
702
+ except Exception as e:
703
+ logger.error(f"Error initializing HF models: {e}")
704
+ return get_hf_models_status(), f"❌ Initialization failed: {str(e)}"
705
+
706
+
707
+ # ==================== TAB 6: DIAGNOSTICS ====================
708
+
709
+ def run_full_diagnostics(auto_fix: bool) -> str:
710
+ """Run full system diagnostics"""
711
+ try:
712
+ from backend.services.diagnostics_service import DiagnosticsService
713
+
714
+ logger.info(f"Running diagnostics (auto_fix={auto_fix})...")
715
+
716
+ diagnostics = DiagnosticsService()
717
+
718
+ # Run async in sync context
719
+ loop = asyncio.new_event_loop()
720
+ asyncio.set_event_loop(loop)
721
+ report = loop.run_until_complete(diagnostics.run_full_diagnostics(auto_fix=auto_fix))
722
+ loop.close()
723
+
724
+ # Format detailed output
725
+ output = f"""
726
+ # 🔧 System Diagnostics Report
727
+
728
+ **Generated**: {report.timestamp}
729
+ **Duration**: {report.duration_ms:.2f}ms
730
+
731
+ ---
732
+
733
+ ## 📊 Summary
734
+
735
+ | Metric | Count |
736
+ |--------|-------|
737
+ | **Total Issues** | {report.total_issues} |
738
+ | **Critical** 🔴 | {report.critical_issues} |
739
+ | **Warnings** 🟡 | {report.warnings} |
740
+ | **Info** 🔵 | {report.info_issues} |
741
+ | **Auto-Fixed** ✅ | {len(report.fixed_issues)} |
742
+
743
+ ---
744
+
745
+ ## 🔍 Issues Detected
746
+
747
+ """
748
+
749
+ if not report.issues:
750
+ output += "✅ **No issues detected!** System is healthy.\n"
751
+ else:
752
+ # Group by category
753
+ by_category = {}
754
+ for issue in report.issues:
755
+ cat = issue.category
756
+ if cat not in by_category:
757
+ by_category[cat] = []
758
+ by_category[cat].append(issue)
759
+
760
+ for category, issues in sorted(by_category.items()):
761
+ output += f"\n### {category.upper()}\n\n"
762
+
763
+ for issue in issues:
764
+ emoji = {"critical": "🔴", "warning": "🟡", "info": "🔵"}.get(issue.severity, "⚪")
765
+ fixed_mark = " ✅ **AUTO-FIXED**" if issue.auto_fixed else ""
766
+
767
+ output += f"**{emoji} {issue.title}**{fixed_mark}\n\n"
768
+ output += f"{issue.description}\n\n"
769
+
770
+ if issue.fixable and issue.fix_action and not issue.auto_fixed:
771
+ output += f"💡 **Fix**: `{issue.fix_action}`\n\n"
772
+
773
+ output += "---\n\n"
774
+
775
+ # System info
776
+ output += "\n## 💻 System Information\n\n"
777
+ output += "```json\n"
778
+ output += json.dumps(report.system_info, indent=2)
779
+ output += "\n```\n"
780
+
781
+ return output
782
+
783
+ except Exception as e:
784
+ logger.error(f"Error running diagnostics: {e}\n{traceback.format_exc()}")
785
+ return f"❌ Diagnostics failed: {str(e)}\n\nCheck logs for details."
786
+
787
+
788
+ # ==================== TAB 7: LOGS ====================
789
+
790
+ def get_logs(log_type: str = "recent", lines: int = 100) -> str:
791
+ """Get system logs with copy-friendly format"""
792
+ try:
793
+ log_file = config.LOG_FILE
794
+
795
+ if not log_file.exists():
796
+ return "⚠️ Log file not found"
797
+
798
+ # Read log file
799
+ with open(log_file, 'r') as f:
800
+ all_lines = f.readlines()
801
+
802
+ # Filter based on log_type
803
+ if log_type == "errors":
804
+ filtered_lines = [line for line in all_lines if 'ERROR' in line or 'CRITICAL' in line]
805
+ elif log_type == "warnings":
806
+ filtered_lines = [line for line in all_lines if 'WARNING' in line]
807
+ else: # recent
808
+ filtered_lines = all_lines
809
+
810
+ # Get last N lines
811
+ recent_lines = filtered_lines[-lines:] if len(filtered_lines) > lines else filtered_lines
812
+
813
+ if not recent_lines:
814
+ return f"ℹ️ No {log_type} logs found"
815
+
816
+ # Format output with line numbers for easy reference
817
+ output = f"# 📋 {log_type.upper()} Logs (Last {len(recent_lines)} lines)\n\n"
818
+ output += "**Quick Stats:**\n"
819
+ output += f"- Total lines shown: `{len(recent_lines)}`\n"
820
+ output += f"- Log file: `{log_file}`\n"
821
+ output += f"- Type: `{log_type}`\n\n"
822
+ output += "---\n\n"
823
+ output += "```log\n"
824
+ for i, line in enumerate(recent_lines, 1):
825
+ output += f"{i:4d} | {line}"
826
+ output += "\n```\n"
827
+ output += "\n---\n"
828
+ output += "💡 **Tip**: You can now copy individual lines or the entire log block\n"
829
+
830
+ return output
831
+
832
+ except Exception as e:
833
+ logger.error(f"Error reading logs: {e}")
834
+ return f"❌ Error reading logs: {str(e)}"
835
+
836
+
837
+ def clear_logs() -> str:
838
+ """Clear log file"""
839
+ try:
840
+ log_file = config.LOG_FILE
841
+
842
+ if log_file.exists():
843
+ # Backup first
844
+ backup_path = log_file.parent / f"{log_file.name}.backup.{int(datetime.now().timestamp())}"
845
+ import shutil
846
+ shutil.copy2(log_file, backup_path)
847
+
848
+ # Clear
849
+ with open(log_file, 'w') as f:
850
+ f.write("")
851
+
852
+ logger.info("Log file cleared")
853
+ return f"✅ Logs cleared (backup saved to {backup_path.name})"
854
+ else:
855
+ return "⚠️ No log file to clear"
856
+
857
+ except Exception as e:
858
+ logger.error(f"Error clearing logs: {e}")
859
+ return f"❌ Error clearing logs: {str(e)}"
860
+
861
+
862
+ # ==================== GRADIO INTERFACE ====================
863
+
864
+ def build_interface():
865
+ """Build the complete Gradio Blocks interface"""
866
+
867
+ with gr.Blocks(title="Crypto Admin Dashboard", theme=gr.themes.Soft()) as demo:
868
+
869
+ gr.Markdown("""
870
+ # 🚀 Crypto Data Aggregator - Admin Dashboard
871
+
872
+ **Real-time cryptocurrency data aggregation and analysis platform**
873
+
874
+ Features: Provider Management | Market Data | Auto Provider Loader | HF Models | System Diagnostics
875
+ """)
876
+
877
+ with gr.Tabs():
878
+
879
+ # ==================== TAB 1: STATUS ====================
880
+ with gr.Tab("📊 Status"):
881
+ gr.Markdown("### System Status Overview")
882
+
883
+ with gr.Row():
884
+ status_refresh_btn = gr.Button("🔄 Refresh Status", variant="primary")
885
+ status_diag_btn = gr.Button("🔧 Run Quick Diagnostics")
886
+
887
+ status_summary = gr.Markdown()
888
+
889
+ with gr.Row():
890
+ with gr.Column():
891
+ gr.Markdown("#### Database Statistics")
892
+ db_stats_json = gr.JSON()
893
+
894
+ with gr.Column():
895
+ gr.Markdown("#### System Information")
896
+ system_info_json = gr.JSON()
897
+
898
+ diag_output = gr.Markdown()
899
+
900
+ # Load initial status
901
+ demo.load(
902
+ fn=get_status_tab,
903
+ outputs=[status_summary, db_stats_json, system_info_json]
904
+ )
905
+
906
+ # Refresh button
907
+ status_refresh_btn.click(
908
+ fn=get_status_tab,
909
+ outputs=[status_summary, db_stats_json, system_info_json]
910
+ )
911
+
912
+ # Quick diagnostics
913
+ status_diag_btn.click(
914
+ fn=lambda: run_diagnostics_from_status(False),
915
+ outputs=diag_output
916
+ )
917
+
918
+ # ==================== TAB 2: PROVIDERS ====================
919
+ with gr.Tab("🔌 Providers"):
920
+ gr.Markdown("### API Provider Management")
921
+
922
+ with gr.Row():
923
+ provider_category = gr.Dropdown(
924
+ label="Filter by Category",
925
+ choices=get_provider_categories(),
926
+ value="All"
927
+ )
928
+ provider_reload_btn = gr.Button("🔄 Reload Providers", variant="primary")
929
+
930
+ providers_table = gr.Dataframe(
931
+ label="Providers",
932
+ interactive=False,
933
+ wrap=True
934
+ ) if PANDAS_AVAILABLE else gr.JSON(label="Providers")
935
+
936
+ provider_status = gr.Textbox(label="Status", interactive=False)
937
+
938
+ # Load initial providers
939
+ demo.load(
940
+ fn=lambda: get_providers_table("All"),
941
+ outputs=providers_table
942
+ )
943
+
944
+ # Category filter
945
+ provider_category.change(
946
+ fn=get_providers_table,
947
+ inputs=provider_category,
948
+ outputs=providers_table
949
+ )
950
+
951
+ # Reload button
952
+ provider_reload_btn.click(
953
+ fn=reload_providers_config,
954
+ outputs=[providers_table, provider_status]
955
+ )
956
+
957
+ # ==================== TAB 3: MARKET DATA ====================
958
+ with gr.Tab("📈 Market Data"):
959
+ gr.Markdown("### Live Cryptocurrency Market Data")
960
+
961
+ with gr.Row():
962
+ market_search = gr.Textbox(
963
+ label="Search",
964
+ placeholder="Search by name or symbol..."
965
+ )
966
+ market_refresh_btn = gr.Button("🔄 Refresh Prices", variant="primary")
967
+
968
+ market_table = gr.Dataframe(
969
+ label="Market Data",
970
+ interactive=False,
971
+ wrap=True,
972
+ height=400
973
+ ) if PANDAS_AVAILABLE else gr.JSON(label="Market Data")
974
+
975
+ market_status = gr.Textbox(label="Status", interactive=False)
976
+
977
+ # Price chart section
978
+ if PLOTLY_AVAILABLE:
979
+ gr.Markdown("#### Price History Chart")
980
+
981
+ with gr.Row():
982
+ chart_symbol = gr.Textbox(
983
+ label="Symbol",
984
+ placeholder="BTC",
985
+ value="BTC"
986
+ )
987
+ chart_timeframe = gr.Dropdown(
988
+ label="Timeframe",
989
+ choices=["24h", "7d", "30d", "90d"],
990
+ value="7d"
991
+ )
992
+ chart_plot_btn = gr.Button("📊 Plot")
993
+
994
+ price_chart = gr.Plot(label="Price History")
995
+
996
+ chart_plot_btn.click(
997
+ fn=plot_price_history,
998
+ inputs=[chart_symbol, chart_timeframe],
999
+ outputs=price_chart
1000
+ )
1001
+
1002
+ # Load initial data
1003
+ demo.load(
1004
+ fn=lambda: get_market_data_table(""),
1005
+ outputs=market_table
1006
+ )
1007
+
1008
+ # Search
1009
+ market_search.change(
1010
+ fn=get_market_data_table,
1011
+ inputs=market_search,
1012
+ outputs=market_table
1013
+ )
1014
+
1015
+ # Refresh
1016
+ market_refresh_btn.click(
1017
+ fn=refresh_market_data,
1018
+ outputs=[market_table, market_status]
1019
+ )
1020
+
1021
+ # ==================== TAB 4: APL SCANNER ====================
1022
+ with gr.Tab("🔍 APL Scanner"):
1023
+ gr.Markdown("### Auto Provider Loader")
1024
+ gr.Markdown("Automatically discover, validate, and integrate API providers and HuggingFace models.")
1025
+
1026
+ with gr.Row():
1027
+ apl_scan_btn = gr.Button("▶️ Run APL Scan", variant="primary", size="lg")
1028
+ apl_report_btn = gr.Button("📄 View Last Report")
1029
+
1030
+ apl_output = gr.Markdown()
1031
+
1032
+ apl_scan_btn.click(
1033
+ fn=run_apl_scan,
1034
+ outputs=apl_output
1035
+ )
1036
+
1037
+ apl_report_btn.click(
1038
+ fn=get_apl_report,
1039
+ outputs=apl_output
1040
+ )
1041
+
1042
+ # Load last report on startup
1043
+ demo.load(
1044
+ fn=get_apl_report,
1045
+ outputs=apl_output
1046
+ )
1047
+
1048
+ # ==================== TAB 5: HF MODELS ====================
1049
+ with gr.Tab("🤖 HF Models"):
1050
+ gr.Markdown("### HuggingFace Models Status & Testing")
1051
+
1052
+ with gr.Row():
1053
+ hf_init_btn = gr.Button("🔄 Initialize Models", variant="primary")
1054
+ hf_refresh_btn = gr.Button("🔄 Refresh Status")
1055
+
1056
+ hf_models_table = gr.Dataframe(
1057
+ label="Models",
1058
+ interactive=False
1059
+ ) if PANDAS_AVAILABLE else gr.JSON(label="Models")
1060
+
1061
+ hf_status = gr.Textbox(label="Status", interactive=False)
1062
+
1063
+ gr.Markdown("#### Test Model")
1064
+
1065
+ with gr.Row():
1066
+ test_model_dropdown = gr.Dropdown(
1067
+ label="Model",
1068
+ choices=["sentiment", "sentiment_twitter", "sentiment_financial", "summarization"],
1069
+ value="sentiment"
1070
+ )
1071
+
1072
+ test_input = gr.Textbox(
1073
+ label="Test Input",
1074
+ placeholder="Enter text to test the model...",
1075
+ lines=3
1076
+ )
1077
+
1078
+ test_btn = gr.Button("▶️ Run Test", variant="secondary")
1079
+
1080
+ test_output = gr.Markdown(label="Test Output")
1081
+
1082
+ # Load initial status
1083
+ demo.load(
1084
+ fn=get_hf_models_status,
1085
+ outputs=hf_models_table
1086
+ )
1087
+
1088
+ # Initialize models
1089
+ hf_init_btn.click(
1090
+ fn=initialize_hf_models,
1091
+ outputs=[hf_models_table, hf_status]
1092
+ )
1093
+
1094
+ # Refresh status
1095
+ hf_refresh_btn.click(
1096
+ fn=get_hf_models_status,
1097
+ outputs=hf_models_table
1098
+ )
1099
+
1100
+ # Test model
1101
+ test_btn.click(
1102
+ fn=test_hf_model,
1103
+ inputs=[test_model_dropdown, test_input],
1104
+ outputs=test_output
1105
+ )
1106
+
1107
+ # ==================== TAB 6: DIAGNOSTICS ====================
1108
+ with gr.Tab("🔧 Diagnostics"):
1109
+ gr.Markdown("### System Diagnostics & Auto-Repair")
1110
+
1111
+ with gr.Row():
1112
+ diag_run_btn = gr.Button("▶️ Run Diagnostics", variant="primary")
1113
+ diag_autofix_btn = gr.Button("🔧 Run with Auto-Fix", variant="secondary")
1114
+
1115
+ diagnostics_output = gr.Markdown()
1116
+
1117
+ diag_run_btn.click(
1118
+ fn=lambda: run_full_diagnostics(False),
1119
+ outputs=diagnostics_output
1120
+ )
1121
+
1122
+ diag_autofix_btn.click(
1123
+ fn=lambda: run_full_diagnostics(True),
1124
+ outputs=diagnostics_output
1125
+ )
1126
+
1127
+ # ==================== TAB 7: LOGS ====================
1128
+ with gr.Tab("📋 Logs"):
1129
+ gr.Markdown("### System Logs Viewer")
1130
+
1131
+ with gr.Row():
1132
+ log_type = gr.Dropdown(
1133
+ label="Log Type",
1134
+ choices=["recent", "errors", "warnings"],
1135
+ value="recent"
1136
+ )
1137
+ log_lines = gr.Slider(
1138
+ label="Lines to Show",
1139
+ minimum=10,
1140
+ maximum=500,
1141
+ value=100,
1142
+ step=10
1143
+ )
1144
+
1145
+ with gr.Row():
1146
+ log_refresh_btn = gr.Button("🔄 Refresh Logs", variant="primary")
1147
+ log_clear_btn = gr.Button("🗑️ Clear Logs", variant="secondary")
1148
+
1149
+ logs_output = gr.Markdown()
1150
+ log_clear_status = gr.Textbox(label="Status", interactive=False, visible=False)
1151
+
1152
+ # Load initial logs
1153
+ demo.load(
1154
+ fn=lambda: get_logs("recent", 100),
1155
+ outputs=logs_output
1156
+ )
1157
+
1158
+ # Refresh logs
1159
+ log_refresh_btn.click(
1160
+ fn=get_logs,
1161
+ inputs=[log_type, log_lines],
1162
+ outputs=logs_output
1163
+ )
1164
+
1165
+ # Update when dropdown changes
1166
+ log_type.change(
1167
+ fn=get_logs,
1168
+ inputs=[log_type, log_lines],
1169
+ outputs=logs_output
1170
+ )
1171
+
1172
+ # Clear logs
1173
+ log_clear_btn.click(
1174
+ fn=clear_logs,
1175
+ outputs=log_clear_status
1176
+ ).then(
1177
+ fn=lambda: get_logs("recent", 100),
1178
+ outputs=logs_output
1179
+ )
1180
+
1181
+ # Footer
1182
+ gr.Markdown("""
1183
+ ---
1184
+ **Crypto Data Aggregator Admin Dashboard** | Real Data Only | No Mock/Fake Data
1185
+ """)
1186
+
1187
+ return demo
1188
+
1189
 
1190
+ # ==================== MAIN ENTRY POINT ====================
 
1191
 
1192
+ demo = build_interface()
 
1193
 
1194
+ if __name__ == "__main__":
1195
+ logger.info("Launching Gradio dashboard...")
1196
+
1197
+ # Try to mount FastAPI app for API endpoints
1198
+ try:
1199
+ from fastapi import FastAPI as FastAPIApp
1200
+ from fastapi.middleware.wsgi import WSGIMiddleware
1201
+ import uvicorn
1202
+ from threading import Thread
1203
+ import time
1204
+
1205
+ # Import the FastAPI app from hf_unified_server
1206
+ try:
1207
+ from hf_unified_server import app as fastapi_app
1208
+ logger.info("✅ FastAPI app imported successfully")
1209
+
1210
+ # Start FastAPI server in a separate thread on port 7861
1211
+ def run_fastapi():
1212
+ uvicorn.run(
1213
+ fastapi_app,
1214
+ host="0.0.0.0",
1215
+ port=7861,
1216
+ log_level="info"
1217
+ )
1218
+
1219
+ fastapi_thread = Thread(target=run_fastapi, daemon=True)
1220
+ fastapi_thread.start()
1221
+ time.sleep(2) # Give FastAPI time to start
1222
+ logger.info("✅ FastAPI server started on port 7861")
1223
+ except ImportError as e:
1224
+ logger.warning(f"⚠️ Could not import FastAPI app: {e}")
1225
+ except Exception as e:
1226
+ logger.warning(f"⚠️ Could not start FastAPI server: {e}")
1227
+
1228
+ demo.launch(
1229
+ server_name="0.0.0.0",
1230
+ server_port=7860,
1231
+ share=False
1232
+ )
index.html CHANGED
@@ -268,9 +268,15 @@
268
  </div>
269
 
270
  <div class="glass-card" style="margin-top:1.5rem;">
271
- <h4>Global Sentiment</h4>
272
- <canvas id="sentiment-chart" height="200"></canvas>
273
- </div>
 
 
 
 
 
 
274
  </section>
275
 
276
  <!-- ========== MARKET PAGE ========== -->
@@ -2487,7 +2493,46 @@
2487
  }, index * 50);
2488
  });
2489
  }
2490
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2491
  </script>
2492
 
2493
  <!-- Load App JS as ES6 Module -->
 
268
  </div>
269
 
270
  <div class="glass-card" style="margin-top:1.5rem;">
271
+ <div class="card-header" style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
272
+ <h4>Global Sentiment</h4>
273
+ <button id="market-sentiment-refresh-btn" class="btn-secondary btn-sm">
274
+ Analyze Market Sentiment
275
+ </button>
276
+ </div>
277
+ <canvas id="sentiment-chart" height="200"></canvas>
278
+ <div data-market-sentiment-output style="margin-top:12px;font-size:0.875rem;color:var(--text-muted);"></div>
279
+ </div>
280
  </section>
281
 
282
  <!-- ========== MARKET PAGE ========== -->
 
2493
  }, index * 50);
2494
  });
2495
  }
2496
+
2497
+ const marketSentimentBtn = document.getElementById('market-sentiment-refresh-btn');
2498
+ const marketSentimentOutput = document.querySelector('[data-market-sentiment-output]');
2499
+ if (marketSentimentBtn && marketSentimentOutput) {
2500
+ marketSentimentBtn.addEventListener('click', async () => {
2501
+ const backendUrl = window.BACKEND_URL || window.location.origin;
2502
+ marketSentimentBtn.disabled = true;
2503
+ const originalLabel = marketSentimentBtn.textContent;
2504
+ marketSentimentBtn.textContent = 'Analyzing...';
2505
+ marketSentimentOutput.innerHTML = '<div style="color: var(--text-muted);">Analyzing market sentiment...</div>';
2506
+ try {
2507
+ const res = await fetch(`${backendUrl}/api/sentiment`);
2508
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
2509
+ const data = await res.json();
2510
+ const classification = (data.classification || 'neutral').replace(/_/g, ' ');
2511
+ const value = typeof data.value === 'number' ? data.value : 50;
2512
+ const description = data.description || 'Market sentiment indicator based on aggregated crypto signals.';
2513
+ let color = 'var(--text-muted)';
2514
+ if (classification.includes('greed')) {
2515
+ color = 'var(--success)';
2516
+ } else if (classification.includes('fear')) {
2517
+ color = 'var(--danger)';
2518
+ }
2519
+ marketSentimentOutput.innerHTML = `
2520
+ <div style="display:flex;flex-direction:column;gap:8px;">
2521
+ <div style="font-weight:600;">Sentiment: <span style="color:${color};text-transform:capitalize;">${classification}</span></div>
2522
+ <div>Index value: <strong>${value}</strong> / 100</div>
2523
+ <div style="font-size:0.8125rem;color:var(--text-muted);">${description}</div>
2524
+ </div>
2525
+ `;
2526
+ } catch (error) {
2527
+ marketSentimentOutput.innerHTML = `<div style="color: var(--danger);">Failed to load market sentiment: ${error.message}</div>`;
2528
+ } finally {
2529
+ marketSentimentBtn.disabled = false;
2530
+ marketSentimentBtn.textContent = originalLabel;
2531
+ }
2532
+ });
2533
+ }
2534
+
2535
+ });
2536
  </script>
2537
 
2538
  <!-- Load App JS as ES6 Module -->