steelforesight/live_dashboard.html

682 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🪙 داشبورد زنده قیمت سکه — اندیشکده فولاد آینده</title>
<!-- Modern Persian & English Typography -->
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazir-font@v30.1.0/dist/font-face.css" rel="stylesheet" type="text/css" />
<style>
:root {
--bg-gradient: linear-gradient(135deg, #0b0f19 0%, #111827 100%);
--card-bg: rgba(17, 24, 39, 0.7);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-muted: #9ca3af;
--accent-gold: #f59e0b;
--accent-gold-glow: rgba(245, 158, 11, 0.15);
--color-up: #10b981;
--color-up-glow: rgba(16, 185, 129, 0.2);
--color-down: #ef4444;
--color-down-glow: rgba(239, 68, 68, 0.2);
--color-neutral: #6b7280;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Vazir', 'Inter', -apple-system, sans-serif;
}
body {
background: var(--bg-gradient);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
padding: 2rem 1.5rem;
}
/* Container */
.dashboard-container {
max-width: 1200px;
margin: 0 auto;
width: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 2rem;
}
/* Header */
header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1.5rem;
border-bottom: 1px solid var(--card-border);
padding-bottom: 1.5rem;
}
.brand-section {
display: flex;
align-items: center;
gap: 1rem;
}
.brand-logo {
font-size: 2.5rem;
animation: float 4s ease-in-out infinite;
}
h1 {
font-size: 1.8rem;
font-weight: 800;
background: linear-gradient(to left, #ffffff, #f59e0b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.2rem;
}
.brand-sub {
font-size: 0.85rem;
color: var(--text-muted);
letter-spacing: 0.5px;
}
/* Status Pill and Countdown */
.status-panel {
display: flex;
align-items: center;
gap: 1.2rem;
background: rgba(255, 255, 255, 0.03);
padding: 0.6rem 1.2rem;
border-radius: 50px;
border: 1px solid var(--card-border);
backdrop-filter: blur(10px);
}
.status-pill {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
font-weight: bold;
color: var(--color-up);
}
.pulse-dot {
width: 8px;
height: 8px;
background: var(--color-up);
border-radius: 50%;
box-shadow: 0 0 0 0 var(--color-up-glow);
animation: pulse 1.5s infinite;
}
.timer-section {
font-size: 0.85rem;
color: var(--text-muted);
border-right: 1px solid var(--card-border);
padding-right: 1.2rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.timer-val {
font-weight: 900;
color: var(--accent-gold);
font-variant-numeric: tabular-nums;
}
/* main price grid */
.price-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
}
/* Card styling - Glassmorphism */
.coin-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 1.8rem 1.5rem;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 1.2rem;
position: relative;
overflow: hidden;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.3s, box-shadow 0.3s;
}
.coin-card:hover {
transform: translateY(-5px);
border-color: rgba(245, 158, 11, 0.3);
box-shadow: 0 12px 40px 0 rgba(245, 158, 11, 0.08);
}
/* Price Flashes */
@keyframes up-flash {
0% { border-color: var(--color-up); box-shadow: 0 0 25px var(--color-up-glow); }
100% { border-color: var(--card-border); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); }
}
@keyframes down-flash {
0% { border-color: var(--color-down); box-shadow: 0 0 25px var(--color-down-glow); }
100% { border-color: var(--card-border); box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3); }
}
.up-flash {
animation: up-flash 1.5s ease-out;
}
.down-flash {
animation: down-flash 1.5s ease-out;
}
/* Card Header */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.coin-title {
font-size: 1.1rem;
font-weight: 800;
color: #ffffff;
}
.coin-badge {
font-size: 1.5rem;
background: var(--accent-gold-glow);
padding: 0.4rem;
border-radius: 12px;
color: var(--accent-gold);
border: 1px solid rgba(245, 158, 11, 0.2);
}
/* Card Price Body */
.price-section {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.price-toman {
font-size: 1.8rem;
font-weight: 900;
color: #ffffff;
letter-spacing: -0.5px;
}
.price-toman span {
font-size: 0.85rem;
font-weight: normal;
color: var(--text-muted);
margin-right: 0.3rem;
}
.price-rial {
font-size: 0.8rem;
color: var(--text-muted);
}
/* Change badge indicators */
.change-badge {
align-self: flex-start;
font-size: 0.78rem;
font-weight: bold;
padding: 0.3rem 0.7rem;
border-radius: 50px;
display: flex;
align-items: center;
gap: 0.3rem;
direction: ltr; /* English layout inside pill */
}
.change-up {
background: rgba(16, 185, 129, 0.1);
color: var(--color-up);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.change-down {
background: rgba(239, 68, 68, 0.1);
color: var(--color-down);
border: 1px solid rgba(239, 68, 68, 0.2);
}
.change-neutral {
background: rgba(107, 114, 128, 0.1);
color: var(--color-neutral);
border: 1px solid rgba(107, 114, 128, 0.2);
}
/* Card Footer Info */
.card-footer {
border-top: 1px solid rgba(255, 255, 255, 0.04);
padding-top: 0.8rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
font-size: 0.78rem;
color: var(--text-muted);
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-val {
font-weight: bold;
color: #ffffff;
}
/* History table area */
.history-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 1.8rem;
backdrop-filter: blur(12px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.history-title {
font-size: 1.2rem;
font-weight: 800;
color: #ffffff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.table-container {
overflow-x: auto;
border-radius: 12px;
border: 1px solid var(--card-border);
}
table {
width: 100%;
border-collapse: collapse;
text-align: right;
font-size: 0.88rem;
}
th {
background: rgba(255, 255, 255, 0.02);
color: var(--text-muted);
font-weight: bold;
padding: 1rem;
border-bottom: 1px solid var(--card-border);
}
td {
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
color: #e5e7eb;
}
tr:last-child td {
border-bottom: none;
}
tr:hover td {
background: rgba(255, 255, 255, 0.01);
}
/* Animations */
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 10px rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
footer {
text-align: center;
color: var(--text-muted);
font-size: 0.8rem;
margin-top: 3rem;
border-top: 1px solid var(--card-border);
padding-top: 1.5rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
body {
padding: 1rem;
}
header {
flex-direction: column;
align-items: flex-start;
}
.status-panel {
width: 100%;
justify-content: space-between;
}
}
</style>
</head>
<body>
<div class="dashboard-container">
<!-- Brand Header -->
<header>
<div class="brand-section">
<div class="brand-logo">🪙</div>
<div>
<h1>داشبورد زنده قیمت سکه</h1>
<div class="brand-sub">اندیشکده فولاد آینده — مرکز تحلیل راهبردی بازار طلا و صنعت</div>
</div>
</div>
<!-- Real-time updates panel -->
<div class="status-panel">
<div class="status-pill">
<div class="pulse-dot"></div>
<span>بروزرسانی زنده فعال</span>
</div>
<div class="timer-section">
<span>داده جدید در: </span>
<span class="timer-val" id="countdown-val">۶۰ ثانیه</span>
</div>
</div>
</header>
<!-- 5 Live Coin Cards -->
<section class="price-grid" id="cards-grid">
<!-- Cards will be dynamically rendered here -->
</section>
<!-- Recent History Log Table -->
<section class="history-card">
<div class="history-header">
<h2 class="history-title">⏳ تاریخچه نوسانات اخیر (ثبت خودکار هر ۱ دقیقه)</h2>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>زمان ثبت سیستم</th>
<th>نام سکه</th>
<th>قیمت فعلی (تومان)</th>
<th>میزان و درصد تغییر</th>
<th>کمترین قیمت روز</th>
<th>بیشترین قیمت روز</th>
<th>زمان بروزرسانی سایت</th>
</tr>
</thead>
<tbody id="history-tbody">
<tr>
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 2rem;">در حال لود اطلاعات از سرور...</td>
</tr>
</tbody>
</table>
</div>
</section>
<footer>
مرکز مستقل تحلیل راهبردی و سیاست‌گذاری بازار و صنایع — تمامی نرخ‌ها به صورت مستقیم و بدون کش از بازار تهران دریافت می‌شوند.
</footer>
</div>
<!-- Real-time Dynamic script re-loader and state manager -->
<script>
let lastFetchedTime = null;
let priceHistory = [];
let previousPrices = {};
let countdownSecs = 60;
// Format numbers helper
function fmtNum(val) {
if (typeof val === 'string') {
val = parseInt(val.replace(/[^\d]/g, ''));
}
return val ? val.toLocaleString('fa-IR') : '۰';
}
// Convert numbers to Persian characters
const persianDigits = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
function toPersianDigits(str) {
if (str === null || str === undefined) return '';
return str.toString().replace(/[0-9]/g, w => persianDigits[+w]);
}
// Load data from live_prices.js dynamically to bypass CORS file:// issues
function reloadPriceScript() {
// Remove any previously injected script
const oldScript = document.getElementById('live-data-script');
if (oldScript) {
oldScript.remove();
}
// Create a fresh script element with cache buster
const script = document.createElement('script');
script.id = 'live-data-script';
script.src = 'live_prices.js?_t=' + Date.now();
script.onload = function() {
if (window.LIVE_COIN_PRICES) {
updateUI(window.LIVE_COIN_PRICES);
}
};
document.body.appendChild(script);
}
function updateUI(data) {
// If no new data fetched since last load, do nothing
if (lastFetchedTime === data.last_fetch) {
return;
}
lastFetchedTime = data.last_fetch;
countdownSecs = 60; // Reset countdown on fresh fetch
const grid = document.getElementById('cards-grid');
grid.innerHTML = '';
// Target emojis
const coinEmojis = {
"سکه امامی": "👑",
"سکه بهار آزادی": "🕌",
"نیم سکه": "🌓",
"ربع سکه": "🌗",
"سکه گرمی": "⚡"
};
data.prices.forEach(coin => {
// Compare price with previous to determine change flashes
const oldPrice = previousPrices[coin.name];
let flashClass = '';
if (oldPrice !== undefined) {
if (coin.price_toman > oldPrice) flashClass = 'up-flash';
else if (coin.price_toman < oldPrice) flashClass = 'down-flash';
}
previousPrices[coin.name] = coin.price_toman;
// Parse change percentage and sign
const isUp = coin.change.includes('+') || (!coin.change.includes('-') && !coin.change.startsWith('0') && !coin.change.includes('(0%'));
const isZero = coin.change.startsWith('0') || coin.change.includes('(0%') || coin.change === '0';
let changeClass = 'change-neutral';
let changeIcon = '●';
if (!isZero) {
if (isUp) {
changeClass = 'change-up';
changeIcon = '▲';
} else {
changeClass = 'change-down';
changeIcon = '▼';
}
}
// Render card html
const emoji = coinEmojis[coin.name] || "🪙";
const cardHtml = `
<div class="coin-card ${flashClass}">
<div class="card-header">
<span class="coin-title">${coin.name}</span>
<span class="coin-badge">${emoji}</span>
</div>
<div class="price-section">
<div class="price-toman">
${fmtNum(coin.price_toman)} <span>تومان</span>
</div>
<div class="price-rial">
${fmtNum(coin.price_rial)} ریال
</div>
</div>
<div class="change-badge ${changeClass}">
<span>${toPersianDigits(coin.change)}</span>
<span>${changeIcon}</span>
</div>
<div class="card-footer">
<div class="info-row">
<span>کمترین قیمت روز:</span>
<span class="info-val">${fmtNum(coin.min_toman)} ت</span>
</div>
<div class="info-row">
<span>بیشترین قیمت روز:</span>
<span class="info-val">${fmtNum(coin.max_toman)} ت</span>
</div>
<div class="info-row" style="margin-top: 0.4rem; border-top: 1px dashed rgba(255,255,255,0.03); padding-top: 0.4rem;">
<span>بروزرسانی سایت:</span>
<span class="info-val" style="color: var(--accent-gold); font-weight: bold;">${toPersianDigits(coin.last_update)}</span>
</div>
</div>
</div>
`;
grid.insertAdjacentHTML('beforeend', cardHtml);
// Push record to local rolling history
pushToRollingHistory(data.last_fetch, coin);
});
// Update history table view
renderHistoryTable();
}
// Keep the last 15 ticks in local storage rolling history
function pushToRollingHistory(fetchTime, coin) {
// Avoid duplicate logs for the exact same fetch time
const duplicate = priceHistory.some(h => h.time === fetchTime && h.name === coin.name);
if (duplicate) return;
priceHistory.push({
time: fetchTime,
name: coin.name,
price_toman: coin.price_toman,
change: coin.change,
min_toman: coin.min_toman,
max_toman: coin.max_toman,
last_update: coin.last_update
});
// Limit size to last 50 entries
if (priceHistory.length > 50) {
priceHistory.shift();
}
localStorage.setItem('coin_price_history_v2', JSON.stringify(priceHistory));
}
function renderHistoryTable() {
const tbody = document.getElementById('history-tbody');
tbody.innerHTML = '';
// Display logs in reverse order (newest first)
const reversedLogs = [...priceHistory].reverse();
if (reversedLogs.length === 0) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; color: var(--text-muted);">هیچ رکوردی در تاریخچه ثبت نشده است.</td></tr>`;
return;
}
reversedLogs.forEach(row => {
const isUp = row.change.includes('+') || (!row.change.includes('-') && !row.change.startsWith('0') && !row.change.includes('(0%'));
const isZero = row.change.startsWith('0') || row.change.includes('(0%') || row.change === '0';
let color = 'var(--text-primary)';
if (!isZero) {
color = isUp ? 'var(--color-up)' : 'var(--color-down)';
}
const trHtml = `
<tr>
<td style="font-weight: bold; color: var(--accent-gold);">${toPersianDigits(row.time)}</td>
<td style="font-weight: bold;">${row.name}</td>
<td style="font-weight: 900;">${fmtNum(row.price_toman)} تومان</td>
<td style="color: ${color}; font-weight: bold; direction: ltr;">${toPersianDigits(row.change)}</td>
<td>${fmtNum(row.min_toman)} تومان</td>
<td>${fmtNum(row.max_toman)} تومان</td>
<td style="color: var(--text-muted);">${toPersianDigits(row.last_update)}</td>
</tr>
`;
tbody.insertAdjacentHTML('beforeend', trHtml);
});
}
// Initialize state
if (localStorage.getItem('coin_price_history_v2')) {
try {
priceHistory = JSON.parse(localStorage.getItem('coin_price_history_v2'));
} catch(e) {
priceHistory = [];
}
}
// Loop timer
setInterval(() => {
if (countdownSecs > 1) {
countdownSecs--;
document.getElementById('countdown-val').innerText = toPersianDigits(countdownSecs) + ' ثانیه';
} else {
document.getElementById('countdown-val').innerText = 'در حال دریافت...';
}
}, 1000);
// Initial loading
reloadPriceScript();
// Poll the JS file every 2 seconds for ultra-responsive updates
setInterval(reloadPriceScript, 2000);
</script>
</body>
</html>