Spaces:
Sleeping
Sleeping
| ADMIN_HTML_CONTENT = """<!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>LocalRadar - Admin Analytics Dashboard</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- FontAwesome Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| body { | |
| font-family: 'Outfit', sans-serif; | |
| background-color: #0f172a; | |
| color: #f1f5f9; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- 1. Password Gate Screen --> | |
| <div id="loginGate" class="flex-1 flex items-center justify-center p-6"> | |
| <div class="bg-slate-800 rounded-3xl p-8 max-w-sm w-full shadow-2xl border border-slate-700/50 text-center"> | |
| <div class="w-16 h-16 bg-indigo-500/10 rounded-full flex items-center justify-center mx-auto mb-6"> | |
| <i class="fa-solid fa-lock text-indigo-400 text-3xl"></i> | |
| </div> | |
| <h1 class="text-2xl font-bold mb-2">Admin Portal</h1> | |
| <p class="text-slate-400 text-sm mb-6">๊ด๋ฆฌ์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํด์ฃผ์ธ์.</p> | |
| <div class="mb-4 text-left"> | |
| <label class="block text-slate-300 text-xs font-semibold mb-2" for="password">Password</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 flex items-center pl-3 text-slate-500"> | |
| <i class="fa-solid fa-key"></i> | |
| </span> | |
| <input type="password" id="password" class="w-full bg-slate-900 border border-slate-700 rounded-xl py-3 pl-10 pr-4 text-sm focus:outline-none focus:border-indigo-500 transition-colors" placeholder="๋น๋ฐ๋ฒํธ ์ ๋ ฅ" onkeydown="if(event.key === 'Enter') login()"> | |
| </div> | |
| <p id="errorMsg" class="text-rose-500 text-xs mt-2 hidden">๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.</p> | |
| </div> | |
| <button onclick="login()" class="w-full bg-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 text-white font-semibold py-3 rounded-xl transition-all shadow-lg shadow-indigo-600/30"> | |
| ๋ก๊ทธ์ธ | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 2. Main Dashboard (Initially Hidden) --> | |
| <div id="dashboard" class="hidden flex-1 flex flex-col"> | |
| <!-- Header --> | |
| <header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur-md sticky top-0 z-10 px-6 py-4 flex items-center justify-between"> | |
| <div class="flex items-center space-x-3"> | |
| <div class="w-10 h-10 bg-indigo-600/10 rounded-xl flex items-center justify-center"> | |
| <i class="fa-solid fa-chart-line text-indigo-400 text-xl"></i> | |
| </div> | |
| <h1 class="text-xl font-bold">LocalRadar Admin</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button onclick="refreshData()" class="p-2.5 bg-slate-800 hover:bg-slate-700 active:bg-slate-900 rounded-xl text-slate-300 hover:text-white transition-all"> | |
| <i class="fa-solid fa-rotate"></i> | |
| </button> | |
| <button onclick="logout()" class="px-4 py-2 bg-rose-600/10 hover:bg-rose-600 text-rose-400 hover:text-white rounded-xl text-sm font-semibold transition-all"> | |
| <i class="fa-solid fa-right-from-bracket mr-1.5"></i> ๋ก๊ทธ์์ | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Content --> | |
| <main class="flex-1 p-6 max-w-6xl w-full mx-auto space-y-8"> | |
| <!-- Cards Grid --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <!-- Card 1 --> | |
| <div class="bg-slate-800 rounded-2xl p-6 shadow-xl border border-slate-700/30 flex items-center space-x-4"> | |
| <div class="p-4 bg-emerald-500/10 rounded-xl"> | |
| <i class="fa-solid fa-users text-emerald-400 text-2xl"></i> | |
| </div> | |
| <div> | |
| <div class="flex items-center space-x-2 text-slate-400 text-xs"> | |
| <span>Active Users (5m)</span> | |
| <span class="w-2.5 h-2.5 bg-emerald-500 rounded-full animate-pulse"></span> | |
| </div> | |
| <h2 id="activeUsers" class="text-3xl font-bold mt-1">0</h2> | |
| </div> | |
| </div> | |
| <!-- Card 2 --> | |
| <div class="bg-slate-800 rounded-2xl p-6 shadow-xl border border-slate-700/30 flex items-center space-x-4"> | |
| <div class="p-4 bg-indigo-500/10 rounded-xl"> | |
| <i class="fa-solid fa-calendar-day text-indigo-400 text-2xl"></i> | |
| </div> | |
| <div> | |
| <p class="text-slate-400 text-xs">Today's Views</p> | |
| <h2 id="todayViews" class="text-3xl font-bold mt-1">0</h2> | |
| </div> | |
| </div> | |
| <!-- Card 3 --> | |
| <div class="bg-slate-800 rounded-2xl p-6 shadow-xl border border-slate-700/30 flex items-center space-x-4"> | |
| <div class="p-4 bg-amber-500/10 rounded-xl"> | |
| <i class="fa-solid fa-chart-simple text-amber-400 text-2xl"></i> | |
| </div> | |
| <div> | |
| <p class="text-slate-400 text-xs">Total Views</p> | |
| <h2 id="totalViews" class="text-3xl font-bold mt-1">0</h2> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Detailed Stats --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> | |
| <!-- Languages Distribution --> | |
| <div class="bg-slate-800 rounded-3xl p-6 shadow-xl border border-slate-700/30"> | |
| <h3 class="text-lg font-bold mb-6 flex items-center"> | |
| <i class="fa-solid fa-globe text-indigo-400 mr-2.5"></i> User Languages | |
| </h3> | |
| <div id="languagesList" class="space-y-4"> | |
| <!-- Filled dynamically --> | |
| </div> | |
| </div> | |
| <!-- Top Visited Events --> | |
| <div class="bg-slate-800 rounded-3xl p-6 shadow-xl border border-slate-700/30"> | |
| <h3 class="text-lg font-bold mb-6 flex items-center"> | |
| <i class="fa-solid fa-fire text-amber-400 mr-2.5"></i> Top Visited Events | |
| </h3> | |
| <div id="topEventsList" class="space-y-3"> | |
| <!-- Filled dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Funnel Section --> | |
| <div class="bg-slate-800 rounded-3xl p-6 shadow-xl border border-slate-700/30"> | |
| <h3 class="text-lg font-bold mb-6 flex items-center"> | |
| <i class="fa-solid fa-filter text-indigo-400 mr-2.5"></i> SEO & App Acquisition Funnel | |
| </h3> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-left border-collapse"> | |
| <thead> | |
| <tr class="border-b border-slate-700 text-slate-400 text-sm font-semibold"> | |
| <th class="py-3 px-4">Language</th> | |
| <th class="py-3 px-4">SEO Page Views</th> | |
| <th class="py-3 px-4">SEO Clicks (CTA)</th> | |
| <th class="py-3 px-4">App Conversions</th> | |
| <th class="py-3 px-4">Conversion Rate</th> | |
| </tr> | |
| </thead> | |
| <tbody id="funnelRows" class="text-sm divide-y divide-slate-800"> | |
| <!-- Filled dynamically --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Scripts --> | |
| <script> | |
| const flagMap = { | |
| 'ko': '๐ฐ๐ท KO', | |
| 'en': '๐บ๐ธ EN', | |
| 'es': '๐ช๐ธ ES', | |
| 'zh': '๐จ๐ณ ZH', | |
| 'ja': '๐ฏ๐ต JA', | |
| 'ru': '๐ท๐บ RU', | |
| 'fr': '๐ซ๐ท FR' | |
| }; | |
| // Check if already authenticated on load | |
| window.onload = function() { | |
| const token = localStorage.getItem('admin_token'); | |
| if (token === 'localradar2026') { | |
| showDashboard(); | |
| } | |
| }; | |
| function login() { | |
| const pwd = document.getElementById('password').value; | |
| const errorMsg = document.getElementById('errorMsg'); | |
| if (pwd === 'localradar2026') { | |
| localStorage.setItem('admin_token', pwd); | |
| errorMsg.classList.add('hidden'); | |
| showDashboard(); | |
| } else { | |
| errorMsg.classList.remove('hidden'); | |
| } | |
| } | |
| function logout() { | |
| localStorage.removeItem('admin_token'); | |
| document.getElementById('password').value = ''; | |
| document.getElementById('loginGate').classList.remove('hidden'); | |
| document.getElementById('dashboard').classList.add('hidden'); | |
| } | |
| function showDashboard() { | |
| document.getElementById('loginGate').classList.add('hidden'); | |
| document.getElementById('dashboard').classList.remove('hidden'); | |
| refreshData(); | |
| // Auto refresh every 8 seconds | |
| setInterval(refreshData, 8000); | |
| } | |
| function refreshData() { | |
| fetch('/analytics/stats') | |
| .then(res => res.json()) | |
| .then(data => { | |
| // Update main cards | |
| document.getElementById('activeUsers').innerText = data.active_users || 0; | |
| document.getElementById('todayViews').innerText = data.today_views || 0; | |
| document.getElementById('totalViews').innerText = data.total_views || 0; | |
| // Update Languages | |
| const langsDiv = document.getElementById('languagesList'); | |
| langsDiv.innerHTML = ''; | |
| const totalLangs = Object.values(data.languages || {}).reduce((a, b) => a + b, 0); | |
| const sortedLangs = Object.entries(data.languages || {}).sort((a, b) => b[1] - a[1]); | |
| if (sortedLangs.length === 0) { | |
| langsDiv.innerHTML = '<p class="text-slate-500 text-sm">๊ธฐ๋ก๋ ์ธ์ด ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.</p>'; | |
| } else { | |
| sortedLangs.forEach(([lang, count]) => { | |
| const pct = totalLangs > 0 ? (count / totalLangs * 100).toFixed(1) : 0; | |
| const flag = flagMap[lang.toLowerCase()] || lang.toUpperCase(); | |
| langsDiv.innerHTML += ` | |
| <div> | |
| <div class="flex justify-between text-sm mb-1.5 font-semibold"> | |
| <span>${flag}</span> | |
| <span class="text-slate-400">${count} (${pct}%)</span> | |
| </div> | |
| <div class="w-full bg-slate-900 rounded-full h-2.5"> | |
| <div class="bg-indigo-500 h-2.5 rounded-full" style="width: ${pct}%"></div> | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| } | |
| // Update Funnel Table | |
| const funnelRows = document.getElementById('funnelRows'); | |
| funnelRows.innerHTML = ''; | |
| const funnelData = data.funnel || {}; | |
| const sortedFunnel = Object.entries(funnelData).sort((a, b) => b[1].seo_views - a[1].seo_views); | |
| if (sortedFunnel.length === 0) { | |
| funnelRows.innerHTML = '<tr><td colspan="5" class="py-4 text-center text-slate-500">๊ธฐ๋ก๋ ํผ๋ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.</td></tr>'; | |
| } else { | |
| sortedFunnel.forEach(([lang, stats]) => { | |
| const views = stats.seo_views || 0; | |
| const clicks = stats.seo_clicks || 0; | |
| const convs = stats.app_conversions || 0; | |
| const rate = views > 0 ? ((convs / views) * 100).toFixed(1) : '0.0'; | |
| const flag = flagMap[lang.toLowerCase()] || lang.toUpperCase(); | |
| funnelRows.innerHTML += ` | |
| <tr class="hover:bg-slate-900/30 transition-colors border-b border-slate-800"> | |
| <td class="py-3 px-4 font-semibold text-white">${flag}</td> | |
| <td class="py-3 px-4 text-slate-300">${views.toLocaleString()}</td> | |
| <td class="py-3 px-4 text-slate-300">${clicks.toLocaleString()}</td> | |
| <td class="py-3 px-4 text-emerald-400 font-semibold">${convs.toLocaleString()}</td> | |
| <td class="py-3 px-4"> | |
| <div class="flex items-center space-x-2"> | |
| <span class="font-bold text-indigo-400">${rate}%</span> | |
| <div class="w-20 bg-slate-900 rounded-full h-1.5 shrink-0 hidden sm:block"> | |
| <div class="bg-indigo-500 h-1.5 rounded-full" style="width: ${Math.min(parseFloat(rate), 100)}%"></div> | |
| </div> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| } | |
| // Update Top Events | |
| const eventsDiv = document.getElementById('topEventsList'); | |
| eventsDiv.innerHTML = ''; | |
| const topEvents = data.top_events || []; | |
| if (topEvents.length === 0) { | |
| eventsDiv.innerHTML = '<p class="text-slate-500 text-sm">๊ธฐ๋ก๋ ์ด๋ฒคํธ ์กฐํ ๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.</p>'; | |
| } else { | |
| topEvents.forEach((item, idx) => { | |
| eventsDiv.innerHTML += ` | |
| <div class="flex items-center justify-between p-3.5 bg-slate-900/40 border border-slate-700/20 rounded-xl"> | |
| <div class="flex items-center space-x-3 truncate"> | |
| <span class="w-7 h-7 bg-indigo-500/10 rounded-full flex items-center justify-center text-xs font-bold text-indigo-400"> | |
| ${idx + 1} | |
| </span> | |
| <div class="truncate"> | |
| <div class="font-semibold text-sm truncate" title="${item.title}">${item.title}</div> | |
| <div class="text-slate-500 text-[10px] truncate">${item.languages || ''}</div> | |
| </div> | |
| </div> | |
| <span class="px-2.5 py-1 bg-indigo-500/15 text-indigo-400 text-xs font-bold rounded-lg shrink-0"> | |
| ${item.count} views | |
| </span> | |
| </div> | |
| `; | |
| }); | |
| } | |
| }) | |
| .catch(err => { | |
| console.error("๋ฐ์ดํฐ ๋ก๋ ์ค๋ฅ: ", err); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |