Download analytics.html from suzmen/watch: direct link, hf CLI and curl.
- Browser
- Download file 27.2 kB
-
https://huggingface.co/spaces/suzmen/watch/resolve/main/analytics.html
- Command line
-
hf download hf://spaces/suzmen/watch/analytics.html
-
curl -L -o analytics.html https://huggingface.co/spaces/suzmen/watch/resolve/main/analytics.html
27.2 kB
| <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> | |