nexo / analytics.html
suzmen's picture
Upload 23 files
781b567 verified
Raw History Blame Contribute Delete
27.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CinéVault Analytics Dashboard | 实时数据统计</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800&family=Roboto+Mono&display=swap" rel="stylesheet">
<style>
:root {
--bg-main: #f8fafc;
--bg-card: #ffffff;
--border-color: rgba(226, 232, 240, 0.8);
--text-base: #0f172a;
--text-muted: #64748b;
--primary: #3b82f6;
--primary-hover: #2563eb;
--primary-light: rgba(59, 130, 246, 0.08);
--success: #10b981;
--success-light: rgba(16, 185, 129, 0.08);
--danger: #ef4444;
--danger-hover: #dc2626;
--danger-light: rgba(239, 68, 68, 0.08);
--font-main: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-mono: 'Roboto Mono', ui-monospace, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background-color: var(--bg-main);
color: var(--text-base);
padding: 24px;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.container {
width: 100%;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 20px;
}
/* CARD STYLE */
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.03);
padding: 30px;
}
/* HEADER SECTION */
.header-card {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.header-left h1 {
font-size: 24px;
font-weight: 800;
letter-spacing: -0.5px;
}
.header-left p {
font-size: 13px;
color: var(--text-muted);
margin-top: 4px;
}
/* LANGUAGE TOGGLE */
.lang-switch {
display: flex;
background-color: var(--bg-main);
border: 1px solid var(--border-color);
border-radius: 100px;
padding: 4px;
cursor: pointer;
user-select: none;
}
.lang-opt {
padding: 6px 14px;
font-size: 12px;
font-weight: 700;
border-radius: 100px;
transition: all 0.2s ease;
color: var(--text-muted);
}
.lang-opt.active {
background-color: var(--bg-card);
color: var(--primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* METRICS ROW */
.metrics-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 25px; /* Spaced out */
}
.metric-card {
border: 1px solid var(--border-color);
background-color: var(--bg-card);
border-radius: 16px;
padding: 25px 30px; /* Spacious padding */
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.2s ease;
}
.metric-card h3 {
font-size: 13px;
font-weight: 700;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: 42px;
font-weight: 800;
font-family: var(--font-mono);
line-height: 1.1;
}
.metric-badge {
font-size: 11px;
font-weight: 700;
padding: 4px 8px;
border-radius: 6px;
align-self: flex-start;
}
.badge-blue { background-color: var(--primary-light); color: var(--primary); }
.badge-green { background-color: var(--success-light); color: var(--success); }
.badge-warning { background-color: var(--warning-light); color: var(--warning); }
.text-warning { color: var(--warning); }
/* PROGRESS SECTION */
.progress-card {
display: flex;
flex-direction: column;
gap: 14px;
}
.progress-header {
display: flex;
justify-content: space-between;
font-weight: 700;
font-size: 14px;
}
.progress-bar-bg {
height: 12px;
background-color: #f1f5f9;
border-radius: 100px;
overflow: hidden;
width: 100%;
}
.progress-bar-fill {
height: 100%;
background-color: var(--success);
border-radius: 100px;
transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
width: 0%;
}
.realtime-badge {
background-color: var(--success-light);
color: var(--success);
font-size: 11px;
font-weight: 800;
padding: 4px 10px;
border-radius: 100px;
letter-spacing: 0.5px;
display: inline-flex;
align-items: center;
gap: 4px;
margin-top: 10px;
align-self: flex-start;
}
.btn-reset {
border: none;
background-color: var(--danger-light);
color: var(--danger);
font-family: var(--font-main);
font-size: 13px;
font-weight: 700;
padding: 10px 20px;
border-radius: 10px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
align-self: flex-end;
margin-top: 10px;
}
.btn-reset:hover {
background-color: var(--danger);
color: #fff;
}
/* TABLE STYLING FOR EXTERNAL METRICS */
.metrics-table-wrapper {
margin-top: 15px;
overflow-x: auto;
}
.metrics-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 13px;
}
.metrics-table th, .metrics-table td {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.metrics-table th {
background-color: var(--bg-main);
color: var(--text-muted);
font-weight: 700;
text-transform: uppercase;
font-size: 11px;
letter-spacing: 0.5px;
}
.metrics-table tr:hover {
background-color: #f8fafc;
}
.table-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-weight: 700;
font-size: 11px;
}
.footer {
text-align: center;
font-size: 12px;
color: var(--text-muted);
margin-top: 10px;
}
.footer a {
color: var(--primary);
text-decoration: none;
font-weight: 600;
}
.footer a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<!-- HEADER -->
<header class="card header-card">
<div class="header-left">
<h1 id="lbl-title">CinéVault Real-time Traffic</h1>
<p id="lbl-subtitle">Live tracking of page visits and app installer downloads</p>
</div>
<div class="lang-switch" onclick="toggleLanguage()">
<div class="lang-opt active" id="btn-lang-en">EN</div>
<div class="lang-opt" id="btn-lang-zh">中文</div>
</div>
</header>
<!-- DATE PICKER ROW -->
<section class="card" style="display: flex; align-items: center; gap: 15px; padding: 15px 30px;">
<span id="lbl-select-date" style="font-weight: 700; font-size: 14px; color: var(--text-base);">Select Date:</span>
<select id="val-date-select" onchange="changeDate(this.value)" style="padding: 8px 16px; border: 1px solid var(--border-color); border-radius: 8px; font-family: var(--font-main); font-size: 13px; font-weight: 600; background-color: var(--bg-main); color: var(--text-base); cursor: pointer; outline: none;"></select>
</section>
<!-- OVERVIEW CARDS -->
<section class="metrics-row">
<!-- Visits -->
<div class="card metric-card">
<h3 id="lbl-visits">Total Visits</h3>
<div class="metric-value text-blue" id="val-visits">0</div>
<div class="metric-badge badge-blue" id="lbl-badge-visits">Page Views (PV)</div>
</div>
<!-- Click Downloads -->
<div class="card metric-card">
<h3 id="lbl-downloads">App Downloads (Clicks)</h3>
<div class="metric-value text-green" id="val-downloads">0</div>
<div class="metric-badge badge-green" id="lbl-badge-downloads">Button Clicks</div>
</div>
<!-- Device Installs / Accessibility On -->
<div class="card metric-card">
<h3 id="lbl-installs">Device Installs (Accessibility)</h3>
<div class="metric-value text-warning" id="val-installs">0</div>
<div class="metric-badge badge-warning" id="lbl-badge-installs">Accessibility On</div>
</div>
</section>
<!-- CONVERSION RATE PROGRESS -->
<section class="card progress-card">
<div class="progress-header">
<span id="lbl-conv-rate">Effective Conversion Rate</span>
<span id="val-conv-rate">0.0%</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="val-conv-bar"></div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; width: 100%;">
<span class="realtime-badge">
<span style="display:inline-block; width:6px; height:6px; background-color:var(--success); border-radius:50%; margin-right:4px; animation: pulse 1.5s infinite;"></span>
<span id="lbl-realtime">Active Real-time Checking</span>
</span>
<button class="btn-reset" onclick="resetStats()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
<span id="lbl-reset">Reset Statistics</span>
</button>
</div>
</section>
<!-- EXTERNAL DEVICE METRICS TABLE -->
<section class="card">
<h2 id="lbl-ext-title" style="font-size: 16px; font-weight: 800; margin-bottom: 15px;">User Device Metrics</h2>
<div class="metrics-table-wrapper">
<table class="metrics-table">
<thead>
<tr>
<th id="lbl-th-uid">User ID</th>
<th id="lbl-th-uname">Username</th>
<th id="lbl-th-devices">Total Devices</th>
<th id="lbl-th-online">Online</th>
<th id="lbl-th-removed">Removed</th>
<th id="lbl-th-accesson">Accessibility On</th>
<th id="lbl-th-accessoff">Accessibility Off</th>
</tr>
</thead>
<tbody id="val-ext-table-body">
<!-- Loaded dynamically -->
</tbody>
</table>
</div>
</section>
<!-- CONFIGURATION SETTINGS CARD -->
<section class="card">
<h2 id="lbl-config-title" style="font-size: 16px; font-weight: 800; margin-bottom: 15px;">Facebook Pixel & App Configuration</h2>
<div style="display: flex; flex-direction: column; gap: 15px;">
<div style="display: flex; flex-direction: column; gap: 5px;">
<label id="lbl-config-pixel" for="val-pixel-id" style="font-size: 12px; font-weight: 700; color: var(--text-muted);">Facebook Pixel ID</label>
<input type="text" id="val-pixel-id" style="padding: 10px 15px; border: 1px solid var(--border-color); border-radius: 8px; font-family: var(--font-main); font-size: 13px; outline: none; background: var(--bg-main); color: var(--text-base);" placeholder="1520209009772747">
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
<label id="lbl-config-token" for="val-capi-token" style="font-size: 12px; font-weight: 700; color: var(--text-muted);">Conversions API (CAPI) Access Token</label>
<textarea id="val-capi-token" style="padding: 10px 15px; border: 1px solid var(--border-color); border-radius: 8px; font-family: var(--font-main); font-size: 13px; outline: none; background: var(--bg-main); color: var(--text-base); resize: vertical; min-height: 80px;" placeholder="EAAWh..."></textarea>
</div>
<div style="display: flex; flex-direction: column; gap: 5px;">
<label id="lbl-config-url" for="val-download-url" style="font-size: 12px; font-weight: 700; color: var(--text-muted);">APK Download Link</label>
<input type="text" id="val-download-url" style="padding: 10px 15px; border: 1px solid var(--border-color); border-radius: 8px; font-family: var(--font-main); font-size: 13px; outline: none; background: var(--bg-main); color: var(--text-base);" placeholder="https://a9ju139y6axn.morvexalink.com/UBKHo7Bot">
</div>
<button class="btn btn-primary" onclick="saveConfiguration()" style="align-self: flex-start;">
<span id="lbl-save-config">Save Settings</span>
</button>
</div>
</section>
<footer class="footer">
<p>CinéVault Landing Page: <a href="/index.html" target="_blank">Open Website (打开网站)</a></p>
</footer>
</div>
<script>
const locales = {
en: {
title: "CinéVault Live Traffic",
subtitle: "Real-time tracking of page visits and app installer downloads",
selectDate: "Select Date:",
visits: "Total Visits",
downloads: "Click Downloads",
installs: "Device Downloads (Accessibility On)",
badgeVisits: "Page Views (PV)",
badgeDownloads: "Button Clicks",
badgeInstalls: "Real Installs",
convRate: "Real Device Conversion Rate (Real Installs / Visits)",
realtime: "Real-time checking (3s poll)",
reset: "Reset Statistics",
confirmReset: "Are you sure you want to clear all statistics?",
extTitle: "User Device Metrics (External Matrix)",
thUid: "User ID",
thUname: "Username",
thDevices: "Total Devices",
thOnline: "Online",
thRemoved: "Removed",
thAccessOn: "Accessibility On",
thAccessOff: "Accessibility Off",
configTitle: "Facebook Pixel & App Configuration",
configPixel: "Facebook Pixel ID",
configToken: "Conversions API (CAPI) Access Token",
configUrl: "APK Download Link",
saveConfig: "Save Settings",
saveSuccess: "Configuration saved successfully!"
},
zh: {
title: "CinéVault 实时流量统计",
subtitle: "实时追踪网页访问量与应用安装包下载次数",
selectDate: "选择日期:",
visits: "访问总人数 (UV/PV)",
downloads: "点击下载次数 (点击数)",
installs: "实际安装设备数 (无障碍开启)",
badgeVisits: "页面浏览量 (PV)",
badgeDownloads: "按钮点击数",
badgeInstalls: "真实设备安装",
convRate: "真实设备转化率 (无障碍开启 / 访问人数)",
realtime: "数据实时更新中 (3秒轮询)",
reset: "重置统计数据",
confirmReset: "您确定要清除所有统计数据吗?",
extTitle: "用户设备指标 (外部矩阵数据)",
thUid: "用户 ID",
thUname: "用户名",
thDevices: "总设备数",
thOnline: "在线设备",
thRemoved: "已移除",
thAccessOn: "无障碍开启",
thAccessOff: "无障碍关闭",
configTitle: "Facebook 像素与应用配置",
configPixel: "Facebook 像素 ID",
configToken: "转化 API (CAPI) 访问 Token",
configUrl: "APK 下载链接",
saveConfig: "保存设置",
saveSuccess: "配置保存成功!"
}
};
let currentLang = 'en';
function setLanguage(lang) {
currentLang = lang;
const t = locales[lang];
document.getElementById('lbl-title').innerText = t.title;
document.getElementById('lbl-subtitle').innerText = t.subtitle;
document.getElementById('lbl-select-date').innerText = t.selectDate;
document.getElementById('lbl-visits').innerText = t.visits;
document.getElementById('lbl-downloads').innerText = t.downloads;
document.getElementById('lbl-installs').innerText = t.installs;
document.getElementById('lbl-badge-visits').innerText = t.badgeVisits;
document.getElementById('lbl-badge-downloads').innerText = t.badgeDownloads;
document.getElementById('lbl-badge-installs').innerText = t.badgeInstalls;
document.getElementById('lbl-conv-rate').innerText = t.convRate;
document.getElementById('lbl-realtime').innerText = t.realtime;
document.getElementById('lbl-reset').innerText = t.reset;
// External table headers
document.getElementById('lbl-ext-title').innerText = t.extTitle;
document.getElementById('lbl-th-uid').innerText = t.thUid;
document.getElementById('lbl-th-uname').innerText = t.thUname;
document.getElementById('lbl-th-devices').innerText = t.thDevices;
document.getElementById('lbl-th-online').innerText = t.thOnline;
document.getElementById('lbl-th-removed').innerText = t.thRemoved;
document.getElementById('lbl-th-accesson').innerText = t.thAccessOn;
document.getElementById('lbl-th-accessoff').innerText = t.thAccessOff;
// Config section
document.getElementById('lbl-config-title').innerText = t.configTitle;
document.getElementById('lbl-config-pixel').innerText = t.configPixel;
document.getElementById('lbl-config-token').innerText = t.configToken;
document.getElementById('lbl-config-url').innerText = t.configUrl;
document.getElementById('lbl-save-config').innerText = t.saveConfig;
}
function toggleLanguage() {
if (currentLang === 'en') {
document.getElementById('btn-lang-zh').classList.add('active');
document.getElementById('btn-lang-en').classList.remove('active');
setLanguage('zh');
} else {
document.getElementById('btn-lang-en').classList.add('active');
document.getElementById('btn-lang-zh').classList.remove('active');
setLanguage('en');
}
}
let localVisits = 0;
let localDownloads = 0;
let extAccessibilitySum = 0;
let selectedDate = new Date().toISOString().split('T')[0];
// Fetch Stats from Local API
async function fetchStats() {
try {
const res = await fetch(`/api/stats?date=${encodeURIComponent(selectedDate)}`);
if (res.ok) {
const data = await res.json();
localVisits = data.stats.visits;
localDownloads = data.stats.downloads;
// Populate Date Select
const select = document.getElementById('val-date-select');
const currentSelected = select.value || selectedDate;
let html = '';
data.availableDates.forEach(date => {
html += `<option value="${date}" ${date === currentSelected ? 'selected' : ''}>${date}</option>`;
});
select.innerHTML = html;
updateUI();
}
} catch (e) {
console.error('Error fetching statistics:', e);
}
}
function updateUI() {
document.getElementById('val-visits').innerText = localVisits;
document.getElementById('val-downloads').innerText = localDownloads;
document.getElementById('val-installs').innerText = extAccessibilitySum;
// Calculate conversion rate: Real Installs / Visits
const rate = localVisits === 0 ? 0 : ((extAccessibilitySum / localVisits) * 100);
document.getElementById('val-conv-rate').innerText = `${rate.toFixed(1)}%`;
document.getElementById('val-conv-bar').style.width = `${Math.min(rate, 100)}%`;
}
function changeDate(dateVal) {
selectedDate = dateVal;
fetchStats();
}
async function resetStats() {
if (confirm(locales[currentLang].confirmReset)) {
try {
const res = await fetch('/api/stats/reset', { method: 'POST' });
if (res.ok) {
const data = await res.json();
localVisits = data.stats.visits;
localDownloads = data.stats.downloads;
updateUI();
}
} catch (e) {
console.error('Error resetting statistics:', e);
}
}
}
// Set initial language
setLanguage('en');
// Fetch external devices table
async function fetchExternalMetrics() {
try {
const res = await fetch('/api/external-metrics');
if (res.ok) {
const list = await res.json();
const tbody = document.getElementById('val-ext-table-body');
let html = '';
let tempSum = 0;
list.forEach(row => {
const onlineBadge = parseInt(row.online) > 0 ? `badge-green` : `badge-blue`;
tempSum += parseInt(row.accessibilityOn || 0);
html += `
<tr>
<td><strong>${row.userId}</strong></td>
<td>${row.username}</td>
<td>${row.totalDevices}</td>
<td><span class="table-badge ${onlineBadge}">${row.online}</span></td>
<td>${row.removed}</td>
<td><span class="table-badge badge-green">${row.accessibilityOn}</span></td>
<td>${row.accessibilityOff}</td>
</tr>
`;
});
tbody.innerHTML = html;
extAccessibilitySum = tempSum;
updateUI();
}
} catch (e) {
console.error('Error fetching external metrics:', e);
}
}
// Fetch Config from API
async function fetchConfig() {
try {
const res = await fetch('/api/config');
if (res.ok) {
const data = await res.json();
document.getElementById('val-pixel-id').value = data.pixelId;
document.getElementById('val-capi-token').value = data.token;
document.getElementById('val-download-url').value = data.downloadUrl;
}
} catch (e) {
console.error('Error fetching config:', e);
}
}
// Save Config to API
async function saveConfiguration() {
const pixelId = document.getElementById('val-pixel-id').value;
const token = document.getElementById('val-capi-token').value;
const downloadUrl = document.getElementById('val-download-url').value;
try {
const res = await fetch('/api/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pixelId, token, downloadUrl })
});
if (res.ok) {
alert(locales[currentLang].saveSuccess);
} else {
alert('Error saving configuration');
}
} catch (e) {
console.error('Error saving config:', e);
}
}
// Initial Load and start polling every 3 seconds
fetchStats();
fetchExternalMetrics();
fetchConfig();
setInterval(() => {
fetchStats();
fetchExternalMetrics();
}, 3000);
</script>
<style>
@keyframes pulse {
0% { opacity: 0.4; }
50% { opacity: 1; }
100% { opacity: 0.4; }
}
</style>
</body>
</html>