Skip to main content

DASHBOARD_HTML

Constant DASHBOARD_HTML 

Source
pub const DASHBOARD_HTML: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Analytics - Rush Sync Server</title>
<link rel="icon" href="/.rss/favicon.svg" type="image/svg+xml">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a0f;color:#e4e4ef;min-height:100vh}
.container{max-width:1200px;margin:0 auto;padding:24px}
.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:32px;flex-wrap:wrap;gap:12px}
.header h1{font-size:26px;font-weight:700;letter-spacing:-0.5px}
.header h1 span{color:#6c63ff}
.header-right{display:flex;align-items:center;gap:16px}
.live-dot{width:8px;height:8px;border-radius:50%;background:#4ade80;animation:pulse 2s ease-in-out infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(74,222,128,0.4)}50%{box-shadow:0 0 0 6px rgba(74,222,128,0)}}
.live-label{font-size:12px;color:#4ade80;display:flex;align-items:center;gap:6px}
.back{color:#6c63ff;text-decoration:none;font-size:14px}
.back:hover{text-decoration:underline}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:28px}
.card{background:#14141f;border:1px solid #2a2a3a;border-radius:12px;padding:20px;transition:border-color 0.2s}
.card:hover{border-color:#3a3a4a}
.card .lbl{font-size:11px;color:#8888a0;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}
.card .val{font-size:32px;font-weight:700;line-height:1.1}
.card .sub{font-size:12px;color:#555;margin-top:6px}
.card .val.purple{color:#6c63ff}
.card .val.green{color:#4ade80}
.card .val.blue{color:#22d3ee}
.card .val.orange{color:#fb923c}
.tabs{display:flex;gap:8px;margin-bottom:20px}
.tab{padding:8px 18px;border-radius:8px;background:#14141f;border:1px solid #2a2a3a;color:#8888a0;cursor:pointer;font-size:13px;font-weight:500;transition:all 0.2s}
.tab:hover{border-color:#6c63ff;color:#c0c0d0}
.tab.active{background:#6c63ff;color:#fff;border-color:#6c63ff}
.section{background:#14141f;border:1px solid #2a2a3a;border-radius:12px;padding:24px;margin-bottom:16px}
.section h2{font-size:14px;margin-bottom:16px;font-weight:600;color:#8888a0;text-transform:uppercase;letter-spacing:0.5px}
.chart{display:flex;align-items:flex-end;gap:3px;height:160px;padding:0 0 28px 0;position:relative}
.bar-w{flex:1;display:flex;flex-direction:column;align-items:center;position:relative;min-width:0}
.bar{width:100%;background:linear-gradient(180deg,#6c63ff,#4a43cc);border-radius:4px 4px 0 0;min-height:1px;transition:height 0.4s ease;cursor:pointer}
.bar:hover{background:linear-gradient(180deg,#8b83ff,#6c63ff)}
.bar-lbl{font-size:9px;color:#555566;position:absolute;bottom:-22px;white-space:nowrap}
.tooltip{display:none;position:absolute;top:-44px;left:50%;transform:translateX(-50%);background:#2a2a3a;color:#e4e4ef;padding:6px 10px;border-radius:6px;font-size:11px;white-space:nowrap;z-index:10;box-shadow:0 4px 12px rgba(0,0,0,0.3)}
.tooltip::after{content:'';position:absolute;bottom:-4px;left:50%;transform:translateX(-50%);border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #2a2a3a}
.bar-w:hover .tooltip{display:block}
.chart-empty{display:flex;align-items:flex-end;gap:3px;height:160px;padding-bottom:28px}
.chart-empty .bar-w{flex:1}.chart-empty .bar{background:#1a1a2a;height:20%;border-radius:4px 4px 0 0}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}
@media(max-width:768px){.grid{grid-template-columns:1fr}.cards{grid-template-columns:1fr 1fr}.chart{height:120px}}
table{width:100%;border-collapse:collapse}
th{text-align:left;font-size:10px;color:#555566;text-transform:uppercase;letter-spacing:1px;padding:8px 0;border-bottom:1px solid #2a2a3a}
td{padding:10px 0;font-size:13px;border-bottom:1px solid #1a1a2a}
td:last-child{text-align:right;font-weight:600;color:#6c63ff;font-family:'SF Mono',monospace;font-size:12px}
.path-cell{font-family:'SF Mono',monospace;font-size:12px;color:#c0c0d0;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sub-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.sub-card{background:#1a1a2a;border:1px solid #252535;border-radius:10px;padding:16px;transition:border-color 0.2s}
.sub-card:hover{border-color:#3a3a4a}
.sub-card .name{font-weight:600;margin-bottom:6px;color:#e4e4ef;font-size:14px}
.sub-card .stats{font-size:12px;color:#8888a0;line-height:1.5}
.sub-card .stats span{color:#6c63ff;font-weight:600}
.empty{color:#555;font-style:italic;font-size:13px;padding:20px;text-align:center}
.footer{text-align:center;font-size:11px;color:#444;padding:20px;display:flex;justify-content:center;align-items:center;gap:8px}
.footer .refresh-dot{width:6px;height:6px;border-radius:50%;background:#333;animation:none}
.footer .refresh-dot.active{background:#4ade80;animation:pulse 2s ease-in-out infinite}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Analytics <span>Dashboard</span></h1>
<div class="header-right">
<div class="live-label"><div class="live-dot"></div>Live</div>
<a href="/" class="back">&larr; Back to site</a>
</div>
</div>
<div class="cards" id="cards"></div>
<div class="tabs" id="tabs">
<div class="tab active" data-p="today">Today</div>
<div class="tab" data-p="last_7_days">7 Days</div>
<div class="tab" data-p="last_30_days">30 Days</div>
</div>
<div class="section"><h2>Hourly Traffic (Last 24h)</h2><div class="chart" id="chart"></div></div>
<div class="grid">
<div class="section"><h2>Top Pages</h2><div id="pages"></div></div>
<div class="section"><h2>Downloads</h2><div id="downloads"></div></div>
</div>
<div class="section"><h2>Traffic by Subdomain</h2><div class="sub-grid" id="subs"></div></div>
<div class="footer"><div class="refresh-dot active" id="rdot"></div><span id="foot">Loading...</span></div>
</div>
<script>
var D=__ANALYTICS_DATA__;
var P='today';
var refreshTimer=30;
document.querySelectorAll('.tab').forEach(function(t){t.addEventListener('click',function(){document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active')});t.classList.add('active');P=t.dataset.p;render()})});
function render(){var p=D[P]||D.today||{};
var views=p.page_views||0;var uniq=p.unique_visitors||0;var dls=p.downloads||0;
var vpu=uniq>0?Math.round(views/uniq):0;
document.getElementById('cards').innerHTML=
'<div class="card"><div class="lbl">Page Views</div><div class="val purple">'+fmt(views)+'</div><div class="sub">Total tracked requests</div></div>'+
'<div class="card"><div class="lbl">Unique Visitors</div><div class="val green">'+fmt(uniq)+'</div><div class="sub">By unique IP address</div></div>'+
'<div class="card"><div class="lbl">Downloads</div><div class="val blue">'+fmt(dls)+'</div><div class="sub">Binary downloads</div></div>'+
'<div class="card"><div class="lbl">Views / Visitor</div><div class="val orange">'+fmt(vpu)+'</div><div class="sub">Avg. engagement</div></div>';
renderChart();renderPages(p);renderDownloads(p);renderSubs()}
function renderChart(){var h=D.hourly_traffic||[];
var el=document.getElementById('chart');
if(h.length===0){el.innerHTML='<div class="empty">No hourly data yet</div>';return}
var allHours=[];var hmap={};
h.forEach(function(x){var hr=x.hour;hmap[hr]=x});
var now=new Date();for(var i=23;i>=0;i--){var d=new Date(now.getTime()-i*3600000);var key=d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'T'+pad(d.getHours())+':00';allHours.push(key)}
var mx=1;allHours.forEach(function(k){var v=hmap[k];if(v&&v.views>mx)mx=v.views});
el.innerHTML=allHours.map(function(k){var v=hmap[k]||{views:0,unique:0};var pct=mx>0?(v.views/mx)*100:0;var hr=k.split('T')[1].replace(':00','h');var barH=v.views>0?Math.max(pct,3):0;return '<div class="bar-w"><div class="bar" style="height:'+barH+'%"></div><div class="tooltip">'+fmt(v.views)+' views &middot; '+fmt(v.unique)+' unique</div><div class="bar-lbl">'+hr+'</div></div>'}).join('')}
function renderPages(p){var pg=p.top_pages||[];var el=document.getElementById('pages');
if(pg.length===0){el.innerHTML='<div class="empty">No page views yet</div>';return}
el.innerHTML='<table><tr><th>Path</th><th>Views</th></tr>'+pg.map(function(x){return '<tr><td class="path-cell" title="'+esc(x.path)+'">'+esc(x.path)+'</td><td>'+fmt(x.views)+'</td></tr>'}).join('')+'</table>'}
function renderDownloads(p){var dl=p.top_downloads||[];var el=document.getElementById('downloads');
if(dl.length===0){el.innerHTML='<div class="empty">No downloads tracked yet</div>';return}
el.innerHTML='<table><tr><th>File</th><th>Count</th></tr>'+dl.map(function(x){var name=x.file.split('/').pop();return '<tr><td class="path-cell" title="'+esc(x.file)+'">'+esc(name)+'</td><td>'+fmt(x.count)+'</td></tr>'}).join('')+'</table>'}
function renderSubs(){var sb=D.by_subdomain||{};var sk=Object.keys(sb);var el=document.getElementById('subs');
if(sk.length===0){el.innerHTML='<div class="empty">No subdomain data</div>';return}
sk.sort(function(a,b){return (sb[b].views||0)-(sb[a].views||0)});
el.innerHTML=sk.map(function(s){var d=sb[s];return '<div class="sub-card"><div class="name">'+esc(s)+'</div><div class="stats"><span>'+fmt(d.views)+'</span> views &middot; <span>'+fmt(d.unique)+'</span> unique visitors</div></div>'}).join('')}
function fmt(n){return (n||0).toLocaleString()}
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
function pad(n){return n<10?'0'+n:''+n}
render();
setInterval(function(){refreshTimer--;if(refreshTimer<=0){location.reload()}document.getElementById('foot').textContent='Updated '+new Date().toLocaleTimeString()+' \u00b7 refresh in '+refreshTimer+'s'},1000);
document.getElementById('foot').textContent='Updated '+new Date().toLocaleTimeString()+' \u00b7 refresh in 30s';
</script>
</body></html>"#;
Expand description

Dashboard HTML template. The placeholder __ANALYTICS_DATA__ is replaced with the current analytics JSON at render time.