Sadeep Sachintha commited on
Commit
370fc07
·
1 Parent(s): 868f534

Implement premium real-time glassmorphic dashboard served directly on root

Browse files
Files changed (5) hide show
  1. README.md +5 -3
  2. main.py +53 -1
  3. static/app.js +156 -0
  4. static/index.html +187 -0
  5. static/style.css +515 -0
README.md CHANGED
@@ -13,6 +13,7 @@ pinned: false
13
  FlyRates is a high-availability Telegram bot designed for real-time currency exchange rate monitoring. Built with an asynchronous architecture, it efficiently handles on-demand queries, automated subscription updates, and proactive threshold-based notifications.
14
 
15
  ## Features ✨
 
16
  - **Real-Time Queries:** Get the latest exchange rates instantly.
17
  - **Subscriptions:** Subscribe to daily or hourly rate updates.
18
  - **Threshold Alerts:** Set custom threshold values and get notified when a currency hits your target rate.
@@ -21,11 +22,12 @@ FlyRates is a high-availability Telegram bot designed for real-time currency exc
21
  - **Database Flexibility:** Uses SQLAlchemy (supports both SQLite and PostgreSQL).
22
 
23
  ## Tech Stack 🛠️
24
- - **Web Framework:** [FastAPI](https://fastapi.tiangolo.com/)
25
  - **Telegram Bot API:** [Aiogram 3.x](https://aiogram.dev/)
26
- - **Database:** SQLAlchemy, aiosqlite, asyncpg
27
  - **Task Scheduling:** APScheduler
28
- - **Deployment:** Docker, Hugging Face Spaces
 
29
 
30
  ## Environment Setup 🔧
31
 
 
13
  FlyRates is a high-availability Telegram bot designed for real-time currency exchange rate monitoring. Built with an asynchronous architecture, it efficiently handles on-demand queries, automated subscription updates, and proactive threshold-based notifications.
14
 
15
  ## Features ✨
16
+ - **Premium Web Dashboard:** A stunning, glassmorphic UI displaying real-time LKR exchange rates, automated conversion calculator, database connection status, and user statistics.
17
  - **Real-Time Queries:** Get the latest exchange rates instantly.
18
  - **Subscriptions:** Subscribe to daily or hourly rate updates.
19
  - **Threshold Alerts:** Set custom threshold values and get notified when a currency hits your target rate.
 
22
  - **Database Flexibility:** Uses SQLAlchemy (supports both SQLite and PostgreSQL).
23
 
24
  ## Tech Stack 🛠️
25
+ - **Web & API Framework:** [FastAPI](https://fastapi.tiangolo.com/)
26
  - **Telegram Bot API:** [Aiogram 3.x](https://aiogram.dev/)
27
+ - **Database:** SQLAlchemy, aiosqlite, asyncpg (Supabase PgBouncer optimized)
28
  - **Task Scheduling:** APScheduler
29
+ - **Frontend Dashboard:** Premium Glassmorphic Web App (HTML5/Vanilla CSS/Modern JS)
30
+ - **Deployment:** Docker, Hugging Face Spaces (24/7 Webhook & Scheduler)
31
 
32
  ## Environment Setup 🔧
33
 
main.py CHANGED
@@ -1,13 +1,18 @@
1
  import logging
2
  from contextlib import asynccontextmanager
3
  from fastapi import FastAPI, Request, Response
 
 
4
  from aiogram import Bot, Dispatcher, types
5
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
 
6
 
7
  from core.config import settings
8
- from db.session import init_db
 
9
  from bot.handlers import router as bot_router
10
  from bot.scheduler import process_subscriptions, process_thresholds
 
11
 
12
  # Configure Logging
13
  logging.basicConfig(level=getattr(logging, settings.log_level))
@@ -62,3 +67,50 @@ async def telegram_webhook(request: Request):
62
  async def health_check():
63
  """Health check endpoint required by Hugging Face Spaces."""
64
  return {"status": "ok"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import logging
2
  from contextlib import asynccontextmanager
3
  from fastapi import FastAPI, Request, Response
4
+ from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse
6
  from aiogram import Bot, Dispatcher, types
7
  from apscheduler.schedulers.asyncio import AsyncIOScheduler
8
+ from sqlalchemy import select, func
9
 
10
  from core.config import settings
11
+ from db.session import init_db, async_session
12
+ from db.models import User, Subscription, Threshold
13
  from bot.handlers import router as bot_router
14
  from bot.scheduler import process_subscriptions, process_thresholds
15
+ from services.fx_service import fx_service
16
 
17
  # Configure Logging
18
  logging.basicConfig(level=getattr(logging, settings.log_level))
 
67
  async def health_check():
68
  """Health check endpoint required by Hugging Face Spaces."""
69
  return {"status": "ok"}
70
+
71
+ @app.get("/api/stats")
72
+ async def get_system_stats():
73
+ """Retrieve database metrics and live LKR exchange rates."""
74
+ stats = {}
75
+ try:
76
+ async with async_session() as session:
77
+ # Gather subscriber (user) counts, subscriptions, and active thresholds
78
+ users_count = await session.scalar(select(func.count(User.chat_id)))
79
+ subs_count = await session.scalar(select(func.count(Subscription.id)))
80
+ thresholds_count = await session.scalar(select(func.count(Threshold.id).where(Threshold.is_active == True)))
81
+
82
+ stats["subscribers"] = users_count or 0
83
+ stats["active_subscriptions"] = subs_count or 0
84
+ stats["active_thresholds"] = thresholds_count or 0
85
+ stats["db_status"] = "connected"
86
+ except Exception as e:
87
+ logger.error(f"Error fetching stats from DB: {e}")
88
+ stats["subscribers"] = 0
89
+ stats["active_subscriptions"] = 0
90
+ stats["active_thresholds"] = 0
91
+ stats["db_status"] = "error"
92
+
93
+ # Fetch live exchange rates to LKR
94
+ rates = {}
95
+ currencies = ["USD", "EUR", "GBP", "AUD", "JPY"]
96
+ for cur in currencies:
97
+ try:
98
+ rate = await fx_service.get_rate(cur, "LKR")
99
+ rates[cur] = rate or 0.0
100
+ except Exception as e:
101
+ logger.error(f"Error fetching live rate for {cur}: {e}")
102
+ rates[cur] = 0.0
103
+
104
+ stats["rates"] = rates
105
+ stats["system_status"] = "online"
106
+
107
+ return stats
108
+
109
+ # Serve static dashboard
110
+ app.mount("/static", StaticFiles(directory="static"), name="static")
111
+
112
+ @app.get("/")
113
+ async def get_dashboard():
114
+ """Serves the premium dashboard index file."""
115
+ return FileResponse("static/index.html")
116
+
static/app.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Local cache for rates to enable instant client-side calculation
2
+ let cachedRates = {
3
+ USD: 0,
4
+ EUR: 0,
5
+ GBP: 0,
6
+ AUD: 0,
7
+ JPY: 0
8
+ };
9
+
10
+ // Formats number values with commas
11
+ function formatNumber(num) {
12
+ return num.toString().replace(/\B(?=(\d{3})+(?!\n))/g, ",");
13
+ }
14
+
15
+ // Simple digit counter animation
16
+ function animateValue(id, start, end, duration) {
17
+ const obj = document.getElementById(id);
18
+ if (!obj) return;
19
+
20
+ if (start === end) {
21
+ obj.textContent = formatNumber(end);
22
+ return;
23
+ }
24
+
25
+ let startTimestamp = null;
26
+ const step = (timestamp) => {
27
+ if (!startTimestamp) startTimestamp = timestamp;
28
+ const progress = Math.min((timestamp - startTimestamp) / duration, 1);
29
+ obj.textContent = formatNumber(Math.floor(progress * (end - start) + start));
30
+ if (progress < 1) {
31
+ window.requestAnimationFrame(step);
32
+ }
33
+ };
34
+ window.requestAnimationFrame(step);
35
+ }
36
+
37
+ // Fetch dashboard data
38
+ async function fetchStats() {
39
+ try {
40
+ const response = await fetch('/api/stats');
41
+ if (!response.ok) throw new Error('API down');
42
+
43
+ const data = await response.json();
44
+
45
+ // Update stats with nice animations
46
+ const subCount = data.subscribers || 0;
47
+ const activeSubCount = data.active_subscriptions || 0;
48
+ const thresholdCount = data.active_thresholds || 0;
49
+
50
+ const currentSubs = parseInt(document.getElementById('stats-subscribers').textContent) || 0;
51
+ const currentActive = parseInt(document.getElementById('stats-subscriptions').textContent) || 0;
52
+ const currentThresholds = parseInt(document.getElementById('stats-thresholds').textContent) || 0;
53
+
54
+ animateValue('stats-subscribers', currentSubs, subCount, 800);
55
+ animateValue('stats-subscriptions', currentActive, activeSubCount, 800);
56
+ animateValue('stats-thresholds', currentThresholds, thresholdCount, 800);
57
+
58
+ // Update exchange rates
59
+ if (data.rates) {
60
+ Object.keys(data.rates).forEach(cur => {
61
+ const rate = data.rates[cur] || 0;
62
+ cachedRates[cur] = rate;
63
+
64
+ const valEl = document.getElementById(`val-${cur.toLowerCase()}`);
65
+ if (valEl) {
66
+ valEl.innerHTML = `${rate.toFixed(2)} <span class="tag">LKR</span>`;
67
+ }
68
+ });
69
+ // Re-trigger calculator logic
70
+ calculateConversion();
71
+ }
72
+
73
+ // Update System Status indicators
74
+ const statusTextEl = document.getElementById('system-status-text');
75
+ const pulseEl = document.querySelector('.pulse-dot');
76
+
77
+ if (data.system_status === 'online' && data.db_status === 'connected') {
78
+ statusTextEl.textContent = 'SYSTEM ACTIVE';
79
+ pulseEl.style.backgroundColor = 'var(--accent-green)';
80
+ pulseEl.style.boxShadow = '0 0 10px var(--accent-green)';
81
+ } else if (data.db_status === 'error') {
82
+ statusTextEl.textContent = 'DATABASE ERROR';
83
+ pulseEl.style.backgroundColor = 'var(--accent-pink)';
84
+ pulseEl.style.boxShadow = '0 0 10px var(--accent-pink)';
85
+ } else {
86
+ statusTextEl.textContent = 'SYSTEM MAINTENANCE';
87
+ pulseEl.style.backgroundColor = 'var(--accent-purple)';
88
+ pulseEl.style.boxShadow = '0 0 10px var(--accent-purple)';
89
+ }
90
+
91
+ } catch (error) {
92
+ console.error('Error fetching dashboard stats:', error);
93
+
94
+ // Display offline/error states
95
+ const statusTextEl = document.getElementById('system-status-text');
96
+ const pulseEl = document.querySelector('.pulse-dot');
97
+ if (statusTextEl && pulseEl) {
98
+ statusTextEl.textContent = 'SYSTEM OFFLINE';
99
+ pulseEl.style.backgroundColor = 'var(--accent-pink)';
100
+ pulseEl.style.boxShadow = '0 0 10px var(--accent-pink)';
101
+ }
102
+ }
103
+ }
104
+
105
+ // Handle currency conversion calculations
106
+ function calculateConversion() {
107
+ const amountInput = document.getElementById('convert-amount');
108
+ const currencySelect = document.getElementById('convert-from');
109
+ const resultOutput = document.getElementById('conversion-output');
110
+
111
+ if (!amountInput || !currencySelect || !resultOutput) return;
112
+
113
+ const amount = parseFloat(amountInput.value) || 0;
114
+ const fromCur = currencySelect.value;
115
+ const rate = cachedRates[fromCur] || 0;
116
+
117
+ // Apply visual prefixes
118
+ const prefixEl = document.querySelector('.converter-form .prefix');
119
+ if (prefixEl) {
120
+ if (fromCur === 'USD' || fromCur === 'AUD') {
121
+ prefixEl.textContent = '$';
122
+ } else if (fromCur === 'GBP') {
123
+ prefixEl.textContent = '£';
124
+ } else if (fromCur === 'EUR') {
125
+ prefixEl.textContent = '€';
126
+ } else if (fromCur === 'JPY') {
127
+ prefixEl.textContent = '¥';
128
+ } else {
129
+ prefixEl.textContent = '';
130
+ }
131
+ }
132
+
133
+ if (amount <= 0 || rate === 0) {
134
+ resultOutput.innerHTML = `0.00 <span class="currency-tag">LKR</span>`;
135
+ return;
136
+ }
137
+
138
+ const total = amount * rate;
139
+ resultOutput.innerHTML = `${total.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <span class="currency-tag">LKR</span>`;
140
+ }
141
+
142
+ // Event Listeners initialization
143
+ document.addEventListener('DOMContentLoaded', () => {
144
+ // Initial fetch
145
+ fetchStats();
146
+
147
+ // Setup inputs
148
+ const amountInput = document.getElementById('convert-amount');
149
+ const currencySelect = document.getElementById('convert-from');
150
+
151
+ if (amountInput) amountInput.addEventListener('input', calculateConversion);
152
+ if (currencySelect) currencySelect.addEventListener('change', calculateConversion);
153
+
154
+ // Auto-update dashboard metrics and rates every 60 seconds
155
+ setInterval(fetchStats, 60000);
156
+ });
static/index.html ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>FlyRates — Real-time Forex Bot Dashboard</title>
7
+ <meta name="description" content="Premium real-time analytics and exchange rates tracking dashboard for FlyRates Telegram Bot.">
8
+ <!-- Google Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+ <!-- FontAwesome Icons -->
13
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
14
+ <!-- Custom Style Sheet -->
15
+ <link rel="stylesheet" href="/static/style.css">
16
+ </head>
17
+ <body>
18
+ <!-- Background glows -->
19
+ <div class="glow glow-1"></div>
20
+ <div class="glow glow-2"></div>
21
+
22
+ <div class="app-container">
23
+ <!-- Header -->
24
+ <header class="main-header">
25
+ <div class="logo-area">
26
+ <div class="logo-icon"><i class="fa-solid fa-money-bill-transfer"></i></div>
27
+ <div class="logo-text">
28
+ <h1>FlyRates</h1>
29
+ <p>Live Bot Analytics & exchange Rates</p>
30
+ </div>
31
+ </div>
32
+ <div class="status-indicator">
33
+ <span class="pulse-dot"></span>
34
+ <span id="system-status-text">Connecting...</span>
35
+ </div>
36
+ </header>
37
+
38
+ <!-- Main Dashboard Content -->
39
+ <main class="dashboard-grid">
40
+ <!-- Left Side: System Metrics & Converter -->
41
+ <section class="left-panel">
42
+ <!-- System Metrics Cards -->
43
+ <div class="metrics-grid">
44
+ <div class="metric-card" id="metric-subscribers">
45
+ <div class="card-icon"><i class="fa-solid fa-users"></i></div>
46
+ <div class="card-info">
47
+ <h3>Total Subscribers</h3>
48
+ <div class="value" id="stats-subscribers">-</div>
49
+ </div>
50
+ </div>
51
+ <div class="metric-card" id="metric-subs">
52
+ <div class="card-icon"><i class="fa-solid fa-bell"></i></div>
53
+ <div class="card-info">
54
+ <h3>Active Subscriptions</h3>
55
+ <div class="value" id="stats-subscriptions">-</div>
56
+ </div>
57
+ </div>
58
+ <div class="metric-card" id="metric-thresholds">
59
+ <div class="card-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
60
+ <div class="card-info">
61
+ <h3>Active Alerts</h3>
62
+ <div class="value" id="stats-thresholds">-</div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Currency Converter Widget -->
68
+ <div class="glass-card converter-card">
69
+ <div class="card-header">
70
+ <h2><i class="fa-solid fa-calculator"></i> Quick Currency Converter</h2>
71
+ <p>Real-time conversion to Sri Lankan Rupee (LKR)</p>
72
+ </div>
73
+ <div class="converter-form">
74
+ <div class="input-group">
75
+ <label for="convert-amount">Amount</label>
76
+ <div class="input-wrapper">
77
+ <span class="prefix">$</span>
78
+ <input type="number" id="convert-amount" value="1" min="1">
79
+ </div>
80
+ </div>
81
+ <div class="input-group">
82
+ <label for="convert-from">From Currency</label>
83
+ <select id="convert-from">
84
+ <option value="USD">USD - US Dollar</option>
85
+ <option value="EUR">EUR - Euro</option>
86
+ <option value="GBP">GBP - British Pound</option>
87
+ <option value="AUD">AUD - Australian Dollar</option>
88
+ <option value="JPY">JPY - Japanese Yen</option>
89
+ </select>
90
+ </div>
91
+ <div class="conversion-result">
92
+ <div class="label">Equivalent in LKR</div>
93
+ <div class="result-value" id="conversion-output">0.00 <span class="currency-tag">LKR</span></div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Telegram Promotion -->
99
+ <div class="glass-card telegram-promotion">
100
+ <div class="promo-content">
101
+ <h3>Get rate updates directly in Telegram!</h3>
102
+ <p>Subscribe to automated updates and custom threshold alerts instantly.</p>
103
+ <a href="https://t.me/FlyRatesBot" target="_blank" class="btn btn-primary">
104
+ <i class="fa-brands fa-telegram"></i> Launch Telegram Bot
105
+ </a>
106
+ </div>
107
+ </div>
108
+ </section>
109
+
110
+ <!-- Right Side: Live Rates Board -->
111
+ <section class="right-panel">
112
+ <div class="glass-card rates-card">
113
+ <div class="card-header">
114
+ <h2><i class="fa-solid fa-chart-line"></i> Live Exchange Rates (LKR)</h2>
115
+ <p>Current value against 1 Sri Lankan Rupee (LKR)</p>
116
+ </div>
117
+ <div class="rates-list" id="rates-container">
118
+ <!-- USD Card -->
119
+ <div class="rate-row" id="rate-usd">
120
+ <div class="currency-info">
121
+ <div class="flag">🇺🇸</div>
122
+ <div>
123
+ <span class="code">USD</span>
124
+ <span class="name">US Dollar</span>
125
+ </div>
126
+ </div>
127
+ <div class="rate-value" id="val-usd">0.00 <span class="tag">LKR</span></div>
128
+ </div>
129
+ <!-- EUR Card -->
130
+ <div class="rate-row" id="rate-eur">
131
+ <div class="currency-info">
132
+ <div class="flag">🇪🇺</div>
133
+ <div>
134
+ <span class="code">EUR</span>
135
+ <span class="name">Euro</span>
136
+ </div>
137
+ </div>
138
+ <div class="rate-value" id="val-eur">0.00 <span class="tag">LKR</span></div>
139
+ </div>
140
+ <!-- GBP Card -->
141
+ <div class="rate-row" id="rate-gbp">
142
+ <div class="currency-info">
143
+ <div class="flag">🇬🇧</div>
144
+ <div>
145
+ <span class="code">GBP</span>
146
+ <span class="name">British Pound</span>
147
+ </div>
148
+ </div>
149
+ <div class="rate-value" id="val-gbp">0.00 <span class="tag">LKR</span></div>
150
+ </div>
151
+ <!-- AUD Card -->
152
+ <div class="rate-row" id="rate-aud">
153
+ <div class="currency-info">
154
+ <div class="flag">🇦🇺</div>
155
+ <div>
156
+ <span class="code">AUD</span>
157
+ <span class="name">Australian Dollar</span>
158
+ </div>
159
+ </div>
160
+ <div class="rate-value" id="val-aud">0.00 <span class="tag">LKR</span></div>
161
+ </div>
162
+ <!-- JPY Card -->
163
+ <div class="rate-row" id="rate-jpy">
164
+ <div class="currency-info">
165
+ <div class="flag">🇯🇵</div>
166
+ <div>
167
+ <span class="code">JPY</span>
168
+ <span class="name">Japanese Yen</span>
169
+ </div>
170
+ </div>
171
+ <div class="rate-value" id="val-jpy">0.00 <span class="tag">LKR</span></div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+ </section>
176
+ </main>
177
+
178
+ <!-- Footer -->
179
+ <footer class="app-footer">
180
+ <p>&copy; 2026 FlyRates System. Powered by FastAPI & Supabase. Fully high-availability.</p>
181
+ </footer>
182
+ </div>
183
+
184
+ <!-- Custom Logic Script -->
185
+ <script src="/static/app.js"></script>
186
+ </body>
187
+ </html>
static/style.css ADDED
@@ -0,0 +1,515 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Core Design Settings */
2
+ :root {
3
+ --bg-dark: #0B0F19;
4
+ --bg-card: rgba(17, 24, 39, 0.55);
5
+ --border-color: rgba(255, 255, 255, 0.07);
6
+ --border-hover: rgba(255, 255, 255, 0.15);
7
+ --text-primary: #F3F4F6;
8
+ --text-secondary: #9CA3AF;
9
+
10
+ /* Neon Glow Accents */
11
+ --accent-cyan: #06B6D4;
12
+ --accent-purple: #8B5CF6;
13
+ --accent-green: #10B981;
14
+ --accent-pink: #EC4899;
15
+ }
16
+
17
+ /* Reset and Globals */
18
+ * {
19
+ margin: 0;
20
+ padding: 0;
21
+ box-sizing: border-box;
22
+ }
23
+
24
+ body {
25
+ background-color: var(--bg-dark);
26
+ font-family: 'Plus Jakarta Sans', sans-serif;
27
+ color: var(--text-primary);
28
+ overflow-x: hidden;
29
+ min-height: 100vh;
30
+ position: relative;
31
+ }
32
+
33
+ /* Background floating glow blobs */
34
+ .glow {
35
+ position: absolute;
36
+ border-radius: 50%;
37
+ filter: blur(120px);
38
+ z-index: 0;
39
+ opacity: 0.18;
40
+ }
41
+
42
+ .glow-1 {
43
+ width: 400px;
44
+ height: 400px;
45
+ background: radial-gradient(circle, var(--accent-cyan), transparent);
46
+ top: -50px;
47
+ left: -100px;
48
+ }
49
+
50
+ .glow-2 {
51
+ width: 500px;
52
+ height: 500px;
53
+ background: radial-gradient(circle, var(--accent-purple), transparent);
54
+ bottom: -100px;
55
+ right: -100px;
56
+ }
57
+
58
+ /* Main Layout Wrapper */
59
+ .app-container {
60
+ max-width: 1200px;
61
+ margin: 0 auto;
62
+ padding: 2.5rem 1.5rem;
63
+ position: relative;
64
+ z-index: 1;
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 2rem;
68
+ min-height: 100vh;
69
+ }
70
+
71
+ /* Header styling */
72
+ .main-header {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ align-items: center;
76
+ padding: 1.25rem 2rem;
77
+ background: var(--bg-card);
78
+ backdrop-filter: blur(16px);
79
+ -webkit-backdrop-filter: blur(16px);
80
+ border: 1px solid var(--border-color);
81
+ border-radius: 20px;
82
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
83
+ }
84
+
85
+ .logo-area {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 1rem;
89
+ }
90
+
91
+ .logo-icon {
92
+ font-size: 2rem;
93
+ color: transparent;
94
+ background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
95
+ -webkit-background-clip: text;
96
+ background-clip: text;
97
+ }
98
+
99
+ .logo-text h1 {
100
+ font-family: 'Outfit', sans-serif;
101
+ font-size: 1.75rem;
102
+ font-weight: 700;
103
+ letter-spacing: -0.5px;
104
+ line-height: 1.2;
105
+ }
106
+
107
+ .logo-text p {
108
+ font-size: 0.8rem;
109
+ color: var(--text-secondary);
110
+ text-transform: uppercase;
111
+ letter-spacing: 1px;
112
+ }
113
+
114
+ /* Glowing system status indicator */
115
+ .status-indicator {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 0.5rem;
119
+ padding: 0.5rem 1rem;
120
+ background: rgba(255, 255, 255, 0.05);
121
+ border-radius: 30px;
122
+ border: 1px solid var(--border-color);
123
+ font-size: 0.85rem;
124
+ font-weight: 500;
125
+ }
126
+
127
+ .pulse-dot {
128
+ width: 8px;
129
+ height: 8px;
130
+ background-color: var(--accent-green);
131
+ border-radius: 50%;
132
+ box-shadow: 0 0 10px var(--accent-green);
133
+ animation: pulse 1.8s infinite;
134
+ }
135
+
136
+ @keyframes pulse {
137
+ 0% {
138
+ transform: scale(0.95);
139
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
140
+ }
141
+ 70% {
142
+ transform: scale(1);
143
+ box-shadow: 0 0 0 8px rgba(16, 185, 129, 0);
144
+ }
145
+ 100% {
146
+ transform: scale(0.95);
147
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
148
+ }
149
+ }
150
+
151
+ /* Dashboard Grid Layout */
152
+ .dashboard-grid {
153
+ display: grid;
154
+ grid-template-columns: 1.2fr 1fr;
155
+ gap: 2rem;
156
+ }
157
+
158
+ /* Left & Right side panels */
159
+ .left-panel {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: 2rem;
163
+ }
164
+
165
+ .right-panel {
166
+ display: flex;
167
+ flex-direction: column;
168
+ }
169
+
170
+ /* Card General Glassmorphic Styles */
171
+ .glass-card {
172
+ background: var(--bg-card);
173
+ backdrop-filter: blur(16px);
174
+ -webkit-backdrop-filter: blur(16px);
175
+ border: 1px solid var(--border-color);
176
+ border-radius: 24px;
177
+ padding: 2rem;
178
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
179
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), border-color 0.3s;
180
+ }
181
+
182
+ .glass-card:hover {
183
+ border-color: var(--border-hover);
184
+ }
185
+
186
+ .card-header {
187
+ margin-bottom: 1.75rem;
188
+ }
189
+
190
+ .card-header h2 {
191
+ font-family: 'Outfit', sans-serif;
192
+ font-size: 1.4rem;
193
+ font-weight: 600;
194
+ margin-bottom: 0.25rem;
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 0.75rem;
198
+ }
199
+
200
+ .card-header h2 i {
201
+ color: var(--accent-cyan);
202
+ }
203
+
204
+ .card-header p {
205
+ font-size: 0.85rem;
206
+ color: var(--text-secondary);
207
+ }
208
+
209
+ /* Metrics Cards (3-column layout) */
210
+ .metrics-grid {
211
+ display: grid;
212
+ grid-template-columns: repeat(3, 1fr);
213
+ gap: 1rem;
214
+ }
215
+
216
+ .metric-card {
217
+ background: var(--bg-card);
218
+ backdrop-filter: blur(16px);
219
+ -webkit-backdrop-filter: blur(16px);
220
+ border: 1px solid var(--border-color);
221
+ border-radius: 20px;
222
+ padding: 1.25rem;
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 1rem;
226
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
227
+ transition: transform 0.3s;
228
+ }
229
+
230
+ .metric-card:hover {
231
+ transform: translateY(-4px);
232
+ }
233
+
234
+ .metric-card .card-icon {
235
+ width: 48px;
236
+ height: 48px;
237
+ border-radius: 14px;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ font-size: 1.25rem;
242
+ }
243
+
244
+ #metric-subscribers .card-icon {
245
+ background: rgba(6, 182, 212, 0.15);
246
+ color: var(--accent-cyan);
247
+ }
248
+
249
+ #metric-subs .card-icon {
250
+ background: rgba(139, 92, 246, 0.15);
251
+ color: var(--accent-purple);
252
+ }
253
+
254
+ #metric-thresholds .card-icon {
255
+ background: rgba(236, 72, 153, 0.15);
256
+ color: var(--accent-pink);
257
+ }
258
+
259
+ .card-info h3 {
260
+ font-size: 0.75rem;
261
+ color: var(--text-secondary);
262
+ text-transform: uppercase;
263
+ letter-spacing: 0.5px;
264
+ margin-bottom: 0.15rem;
265
+ }
266
+
267
+ .card-info .value {
268
+ font-family: 'Outfit', sans-serif;
269
+ font-size: 1.5rem;
270
+ font-weight: 700;
271
+ }
272
+
273
+ /* Quick Currency Converter Form */
274
+ .converter-form {
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 1.25rem;
278
+ }
279
+
280
+ .input-group {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 0.5rem;
284
+ }
285
+
286
+ .input-group label {
287
+ font-size: 0.8rem;
288
+ font-weight: 600;
289
+ color: var(--text-secondary);
290
+ text-transform: uppercase;
291
+ letter-spacing: 0.5px;
292
+ }
293
+
294
+ .input-wrapper {
295
+ position: relative;
296
+ display: flex;
297
+ align-items: center;
298
+ }
299
+
300
+ .input-wrapper .prefix {
301
+ position: absolute;
302
+ left: 1.25rem;
303
+ color: var(--text-secondary);
304
+ font-weight: 500;
305
+ }
306
+
307
+ .converter-form input,
308
+ .converter-form select {
309
+ width: 100%;
310
+ background: rgba(255, 255, 255, 0.05);
311
+ border: 1px solid var(--border-color);
312
+ border-radius: 12px;
313
+ padding: 0.85rem 1.25rem;
314
+ color: var(--text-primary);
315
+ font-family: inherit;
316
+ font-size: 1rem;
317
+ outline: none;
318
+ transition: border-color 0.25s, box-shadow 0.25s;
319
+ }
320
+
321
+ .converter-form input[type="number"] {
322
+ padding-left: 2.25rem;
323
+ }
324
+
325
+ .converter-form input:focus,
326
+ .converter-form select:focus {
327
+ border-color: var(--accent-cyan);
328
+ box-shadow: 0 0 10px rgba(6, 182, 212, 0.2);
329
+ }
330
+
331
+ .converter-form select option {
332
+ background-color: #111827;
333
+ color: var(--text-primary);
334
+ }
335
+
336
+ /* Calculator Result Block */
337
+ .conversion-result {
338
+ margin-top: 0.5rem;
339
+ background: rgba(6, 182, 212, 0.08);
340
+ border: 1px dashed rgba(6, 182, 212, 0.25);
341
+ border-radius: 16px;
342
+ padding: 1.25rem 1.5rem;
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 0.25rem;
346
+ }
347
+
348
+ .conversion-result .label {
349
+ font-size: 0.8rem;
350
+ color: var(--text-secondary);
351
+ text-transform: uppercase;
352
+ }
353
+
354
+ .conversion-result .result-value {
355
+ font-family: 'Outfit', sans-serif;
356
+ font-size: 1.85rem;
357
+ font-weight: 700;
358
+ color: var(--accent-cyan);
359
+ }
360
+
361
+ .conversion-result .currency-tag {
362
+ font-size: 1.25rem;
363
+ font-weight: 500;
364
+ color: var(--text-secondary);
365
+ }
366
+
367
+ /* Telegram Promo Card */
368
+ .telegram-promotion {
369
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.6) 0%, rgba(17, 24, 39, 0.8) 100%);
370
+ position: relative;
371
+ overflow: hidden;
372
+ }
373
+
374
+ .telegram-promotion::after {
375
+ content: '\f3fe';
376
+ font-family: 'Font Awesome 6 Brands';
377
+ position: absolute;
378
+ right: -20px;
379
+ bottom: -40px;
380
+ font-size: 10rem;
381
+ color: rgba(2, 132, 199, 0.05);
382
+ pointer-events: none;
383
+ }
384
+
385
+ .promo-content h3 {
386
+ font-family: 'Outfit', sans-serif;
387
+ font-size: 1.25rem;
388
+ font-weight: 600;
389
+ margin-bottom: 0.5rem;
390
+ }
391
+
392
+ .promo-content p {
393
+ font-size: 0.85rem;
394
+ color: var(--text-secondary);
395
+ margin-bottom: 1.5rem;
396
+ line-height: 1.5;
397
+ }
398
+
399
+ .btn {
400
+ display: inline-flex;
401
+ align-items: center;
402
+ gap: 0.75rem;
403
+ padding: 0.85rem 1.75rem;
404
+ border-radius: 12px;
405
+ font-size: 0.9rem;
406
+ font-weight: 600;
407
+ text-decoration: none;
408
+ cursor: pointer;
409
+ transition: transform 0.2s, box-shadow 0.2s;
410
+ outline: none;
411
+ border: none;
412
+ }
413
+
414
+ .btn-primary {
415
+ background: linear-gradient(135deg, #0284C7, #0369A1);
416
+ color: white;
417
+ box-shadow: 0 4px 15px rgba(2, 132, 199, 0.3);
418
+ }
419
+
420
+ .btn-primary:hover {
421
+ transform: translateY(-2px);
422
+ box-shadow: 0 6px 20px rgba(2, 132, 199, 0.45);
423
+ }
424
+
425
+ /* Live Forex Rates List */
426
+ .rates-list {
427
+ display: flex;
428
+ flex-direction: column;
429
+ gap: 1rem;
430
+ }
431
+
432
+ .rate-row {
433
+ display: flex;
434
+ justify-content: space-between;
435
+ align-items: center;
436
+ padding: 1.15rem 1.5rem;
437
+ background: rgba(255, 255, 255, 0.03);
438
+ border: 1px solid var(--border-color);
439
+ border-radius: 16px;
440
+ transition: background 0.25s, border-color 0.25s, transform 0.25s;
441
+ }
442
+
443
+ .rate-row:hover {
444
+ background: rgba(255, 255, 255, 0.06);
445
+ border-color: var(--border-hover);
446
+ transform: translateX(4px);
447
+ }
448
+
449
+ .currency-info {
450
+ display: flex;
451
+ align-items: center;
452
+ gap: 1.25rem;
453
+ }
454
+
455
+ .currency-info .flag {
456
+ font-size: 2.25rem;
457
+ line-height: 1;
458
+ }
459
+
460
+ .currency-info .code {
461
+ display: block;
462
+ font-family: 'Outfit', sans-serif;
463
+ font-size: 1.1rem;
464
+ font-weight: 700;
465
+ }
466
+
467
+ .currency-info .name {
468
+ display: block;
469
+ font-size: 0.8rem;
470
+ color: var(--text-secondary);
471
+ }
472
+
473
+ .rate-value {
474
+ font-family: 'Outfit', sans-serif;
475
+ font-size: 1.3rem;
476
+ font-weight: 700;
477
+ color: var(--text-primary);
478
+ }
479
+
480
+ .rate-value .tag {
481
+ font-size: 0.85rem;
482
+ font-weight: 500;
483
+ color: var(--text-secondary);
484
+ margin-left: 0.25rem;
485
+ }
486
+
487
+ /* Footer Styling */
488
+ .app-footer {
489
+ text-align: center;
490
+ padding: 2rem 0;
491
+ color: var(--text-secondary);
492
+ font-size: 0.8rem;
493
+ border-top: 1px solid var(--border-color);
494
+ margin-top: auto;
495
+ }
496
+
497
+ /* Responsive Rules */
498
+ @media (max-width: 968px) {
499
+ .dashboard-grid {
500
+ grid-template-columns: 1fr;
501
+ gap: 2rem;
502
+ }
503
+ }
504
+
505
+ @media (max-width: 580px) {
506
+ .main-header {
507
+ flex-direction: column;
508
+ gap: 1rem;
509
+ align-items: flex-start;
510
+ padding: 1.5rem;
511
+ }
512
+ .metrics-grid {
513
+ grid-template-columns: 1fr;
514
+ }
515
+ }