| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'none';"> | |
| <title>Ω Admin Dashboard — API Management & Revenue</title> | |
| <style> | |
| :root { | |
| --primary: #00d4ff; --secondary: #ff00ff; --accent: #ffd700; | |
| --success: #00ff88; --danger: #ff0044; --warning: #ffaa00; | |
| --dark: #0a0a1a; --darker: #050510; --light: #e0e8ff; | |
| --glass: rgba(10, 10, 30, 0.85); --glass-border: rgba(0, 212, 255, 0.15); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| body { font-family: 'Segoe UI', Tahoma, sans-serif; background: var(--darker); color: var(--light); } | |
| .bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; | |
| background: radial-gradient(ellipse at 30% 70%, rgba(0,212,255,0.08) 0%, transparent 50%), | |
| radial-gradient(ellipse at 70% 30%, rgba(255,0,255,0.06) 0%, transparent 50%), var(--darker); } | |
| .container { max-width: 1400px; margin: 0 auto; padding: 20px; } | |
| /* Header */ | |
| .header { text-align: center; padding: 30px 20px; background: var(--glass); | |
| border: 1px solid var(--glass-border); border-radius: 16px; margin-bottom: 20px; } | |
| .header h1 { font-size: 2rem; background: linear-gradient(135deg, var(--primary), var(--accent)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; } | |
| .header .subtitle { color: rgba(224,232,255,0.6); margin-top: 8px; } | |
| /* Stats Cards */ | |
| .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | |
| gap: 15px; margin-bottom: 20px; } | |
| .stat-card { background: var(--glass); border: 1px solid var(--glass-border); | |
| border-radius: 12px; padding: 20px; position: relative; overflow: hidden; } | |
| .stat-card::before { content: ''; position: absolute; top: 0; left: 0; width: 4px; height: 100%; } | |
| .stat-card.revenue::before { background: var(--success); } | |
| .stat-card.keys::before { background: var(--primary); } | |
| .stat-card.calls::before { background: var(--accent); } | |
| .stat-card.usage::before { background: var(--secondary); } | |
| .stat-card h3 { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; | |
| color: rgba(224,232,255,0.5); margin-bottom: 8px; } | |
| .stat-card .value { font-size: 2rem; font-weight: 900; font-family: 'Consolas', monospace; } | |
| .stat-card.revenue .value { color: var(--success); } | |
| .stat-card.keys .value { color: var(--primary); } | |
| .stat-card.calls .value { color: var(--accent); } | |
| .stat-card.usage .value { color: var(--secondary); } | |
| .stat-card .change { font-size: 0.8rem; margin-top: 5px; } | |
| .stat-card .change.up { color: var(--success); } | |
| .stat-card .change.down { color: var(--danger); } | |
| /* Tabs */ | |
| .tabs { display: flex; gap: 5px; margin-bottom: 20px; background: var(--glass); | |
| border: 1px solid var(--glass-border); border-radius: 12px; padding: 5px; } | |
| .tab { flex: 1; padding: 12px; text-align: center; border-radius: 8px; cursor: pointer; | |
| font-weight: 600; font-size: 0.85rem; transition: all 0.3s; color: rgba(224,232,255,0.5); } | |
| .tab:hover { color: var(--light); } | |
| .tab.active { background: linear-gradient(135deg, var(--primary), #0088cc); color: white; } | |
| /* Panels */ | |
| .panel { display: none; background: var(--glass); border: 1px solid var(--glass-border); | |
| border-radius: 12px; padding: 20px; margin-bottom: 20px; } | |
| .panel.active { display: block; } | |
| .panel h2 { font-size: 1.2rem; color: var(--primary); margin-bottom: 15px; | |
| padding-bottom: 10px; border-bottom: 1px solid var(--glass-border); } | |
| /* Table */ | |
| table { width: 100%; border-collapse: collapse; font-size: 0.85rem; } | |
| th { background: rgba(0,212,255,0.1); color: var(--primary); padding: 12px; | |
| text-align: left; border-bottom: 2px solid var(--glass-border); } | |
| td { padding: 12px; border-bottom: 1px solid rgba(255,255,255,0.05); } | |
| tr:hover { background: rgba(0,212,255,0.03); } | |
| .badge { padding: 3px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; } | |
| .badge.starter { background: rgba(0,212,255,0.2); color: var(--primary); } | |
| .badge.pro { background: rgba(255,0,255,0.2); color: var(--secondary); } | |
| .badge.business { background: rgba(255,215,0,0.2); color: var(--accent); } | |
| .badge.enterprise { background: rgba(0,255,136,0.2); color: var(--success); } | |
| .badge.active { background: rgba(0,255,136,0.2); color: var(--success); } | |
| .badge.expired { background: rgba(255,0,68,0.2); color: var(--danger); } | |
| /* Buttons */ | |
| .btn { padding: 10px 20px; border: none; border-radius: 8px; font-weight: 600; | |
| cursor: pointer; font-size: 0.85rem; transition: all 0.3s; } | |
| .btn-primary { background: linear-gradient(135deg, var(--primary), #0088cc); color: white; } | |
| .btn-success { background: linear-gradient(135deg, var(--success), #00cc66); color: var(--dark); } | |
| .btn-danger { background: linear-gradient(135deg, var(--danger), #cc0033); color: white; } | |
| .btn:hover { opacity: 0.85; transform: translateY(-1px); } | |
| /* Form */ | |
| .form-group { margin-bottom: 15px; } | |
| .form-group label { display: block; font-size: 0.85rem; color: var(--primary); | |
| margin-bottom: 5px; font-weight: 600; } | |
| .form-group input, .form-group select { width: 100%; padding: 10px; | |
| background: rgba(0,0,0,0.3); border: 1px solid var(--glass-border); | |
| color: var(--light); border-radius: 8px; font-size: 0.85rem; } | |
| .form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; } | |
| /* Chart */ | |
| .chart-container { background: rgba(0,0,0,0.2); border-radius: 8px; padding: 15px; margin: 15px 0; } | |
| canvas { width: 100%; height: 250px; } | |
| /* Modal */ | |
| .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; | |
| background: rgba(0,0,0,0.7); z-index: 1000; justify-content: center; align-items: center; } | |
| .modal.show { display: flex; } | |
| .modal-content { background: var(--dark); border: 1px solid var(--glass-border); | |
| border-radius: 16px; padding: 30px; max-width: 500px; width: 90%; } | |
| .modal-content h3 { color: var(--primary); margin-bottom: 15px; } | |
| .modal-content .key-display { background: rgba(0,0,0,0.5); padding: 15px; | |
| border-radius: 8px; font-family: 'Consolas', monospace; font-size: 0.9rem; | |
| color: var(--accent); word-break: break-all; margin: 10px 0; } | |
| /* Log */ | |
| .log-entry { padding: 8px 12px; border-left: 3px solid var(--primary); | |
| margin-bottom: 5px; font-size: 0.8rem; background: rgba(0,0,0,0.2); border-radius: 0 4px 4px 0; } | |
| .log-entry .time { color: rgba(224,232,255,0.4); } | |
| .log-entry .action { color: var(--primary); } | |
| @media (max-width: 768px) { | |
| .stats-grid { grid-template-columns: repeat(2, 1fr); } | |
| .container { padding: 10px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg"></div> | |
| <div class="container"> | |
| <!-- HEADER --> | |
| <div class="header"> | |
| <h1>Ω Admin Dashboard</h1> | |
| <p class="subtitle">จัดการ API Keys — ดูรายได้ — วิเคราะห์การใช้งาน</p> | |
| </div> | |
| <!-- STATS --> | |
| <div class="stats-grid"> | |
| <div class="stat-card revenue"> | |
| <h3>💰 รายได้รวม</h3> | |
| <div class="value" id="totalRevenue">฿0</div> | |
| <div class="change up" id="revenueChange">↑ +0% เดือนนี้</div> | |
| </div> | |
| <div class="stat-card keys"> | |
| <h3>🔑 API Keys ทั้งหมด</h3> | |
| <div class="value" id="totalKeys">0</div> | |
| <div class="change up" id="keysChange">↑ +0 ใหม่เดือนนี้</div> | |
| </div> | |
| <div class="stat-card calls"> | |
| <h3>📞 API Calls ทั้งหมด</h3> | |
| <div class="value" id="totalCalls">0</div> | |
| <div class="change up" id="callsChange">↑ +0% จากเดือนก่อน</div> | |
| </div> | |
| <div class="stat-card usage"> | |
| <h3>📊 ค่าเฉลี่ย/Key</h3> | |
| <div class="value" id="avgUsage">0</div> | |
| <div class="change" id="avgChange">calls/key</div> | |
| </div> | |
| </div> | |
| <!-- TABS --> | |
| <div class="tabs"> | |
| <div class="tab active" onclick="switchTab('keys')">🔑 API Keys</div> | |
| <div class="tab" onclick="switchTab('revenue')">💰 รายได้</div> | |
| <div class="tab" onclick="switchTab('usage')">📊 การใช้งาน</div> | |
| <div class="tab" onclick="switchTab('logs')">📋 Logs</div> | |
| <div class="tab" onclick="switchTab('settings')">⚙️ ตั้งค่า</div> | |
| </div> | |
| <!-- PANEL: API Keys --> | |
| <div class="panel active" id="panel-keys"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;"> | |
| <h2>🔑 จัดการ API Keys</h2> | |
| <button class="btn btn-success" onclick="showCreateKeyModal()">+ สร้าง Key ใหม่</button> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>API Key</th> | |
| <th>Plan</th> | |
| <th>Credits</th> | |
| <th>Usage</th> | |
| <th>Created</th> | |
| <th>Status</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody id="keysTableBody"> | |
| <!-- Dynamic --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- PANEL: Revenue --> | |
| <div class="panel" id="panel-revenue"> | |
| <h2>💰 รายงานรายได้</h2> | |
| <div class="form-row" style="margin-bottom:20px;"> | |
| <div class="stat-card revenue"> | |
| <h3>รายได้วันนี้</h3> | |
| <div class="value" id="todayRevenue">฿0</div> | |
| </div> | |
| <div class="stat-card revenue"> | |
| <h3>รายได้เดือนนี้</h3> | |
| <div class="value" id="monthRevenue">฿0</div> | |
| </div> | |
| <div class="stat-card revenue"> | |
| <h3>รายได้ทั้งหมด</h3> | |
| <div class="value" id="allRevenue">฿0</div> | |
| </div> | |
| </div> | |
| <div class="chart-container"> | |
| <canvas id="revenueChart"></canvas> | |
| </div> | |
| <h3 style="margin-top:20px;color:var(--accent);">รายได้แยกตาม Plan</h3> | |
| <table> | |
| <thead><tr><th>Plan</th><th>ราคา/Key</th><th>จำนวน Key</th><th>รายได้รวม</th></tr></thead> | |
| <tbody id="revenueByPlan"></tbody> | |
| </table> | |
| </div> | |
| <!-- PANEL: Usage --> | |
| <div class="panel" id="panel-usage"> | |
| <h2>📊 สถิติการใช้งาน</h2> | |
| <div class="chart-container"> | |
| <canvas id="usageChart"></canvas> | |
| </div> | |
| <h3 style="margin-top:20px;color:var(--accent);">Top 10 Keys ที่ใช้มากที่สุด</h3> | |
| <table> | |
| <thead><tr><th>#</th><th>API Key</th><th>Plan</th><th>Calls</th><th>Credits Left</th></tr></thead> | |
| <tbody id="topUsageTable"></tbody> | |
| </table> | |
| </div> | |
| <!-- PANEL: Logs --> | |
| <div class="panel" id="panel-logs"> | |
| <h2>📋 Activity Logs</h2> | |
| <div id="logsContainer" style="max-height:500px;overflow-y:auto;"> | |
| <!-- Dynamic --> | |
| </div> | |
| </div> | |
| <!-- PANEL: Settings --> | |
| <div class="panel" id="panel-settings"> | |
| <h2>⚙️ ตั้งค่าระบบ</h2> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Admin Key</label> | |
| <input type="text" id="adminKeyInput" value="omega_admin_secret"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Rate Limit (calls/min)</label> | |
| <input type="number" id="rateLimitInput" value="60"> | |
| </div> | |
| </div> | |
| <div class="form-row"> | |
| <div class="form-group"> | |
| <label>Starter Price (THB)</label> | |
| <input type="number" id="starterPrice" value="99"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Pro Price (THB)</label> | |
| <input type="number" id="proPrice" value="499"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Business Price (THB)</label> | |
| <input type="number" id="businessPrice" value="2999"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Enterprise Price (THB)</label> | |
| <input type="number" id="enterprisePrice" value="19999"> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" onclick="saveSettings()">💾 บันทึกการตั้งค่า</button> | |
| </div> | |
| </div> | |
| <!-- MODAL: Create Key --> | |
| <div class="modal" id="createKeyModal"> | |
| <div class="modal-content"> | |
| <h3>🔑 สร้าง API Key ใหม่</h3> | |
| <div class="form-group"> | |
| <label>Plan</label> | |
| <select id="newKeyPlan"> | |
| <option value="Starter">Starter — 100 credits — ฿99</option> | |
| <option value="Pro">Pro — 1,000 credits — ฿499</option> | |
| <option value="Business">Business — 10,000 credits — ฿2,999</option> | |
| <option value="Enterprise">Enterprise — 100,000 credits — ฿19,999</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Email (optional)</label> | |
| <input type="email" id="newKeyEmail" placeholder="customer@email.com"> | |
| </div> | |
| <div style="display:flex;gap:10px;"> | |
| <button class="btn btn-success" onclick="createNewKey()">สร้าง Key</button> | |
| <button class="btn btn-danger" onclick="closeModal()">ยกเลิก</button> | |
| </div> | |
| <div id="newKeyResult" style="display:none;margin-top:15px;"> | |
| <label style="color:var(--accent);font-size:0.85rem;">API Key ที่สร้าง:</label> | |
| <div class="key-display" id="newKeyDisplay"></div> | |
| <p style="font-size:0.75rem;color:rgba(224,232,255,0.5);">⚠️ บันทึก key นี้ทันที! จะไม่แสดงอีกครั้ง</p> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // OFFLINE ENFORCEMENT | |
| // ═══════════════════════════════════════════════════════════════════ | |
| (function() { | |
| window.fetch = function() { return Promise.reject(new Error('Blocked: Offline only')); }; | |
| window.XMLHttpRequest = function() { throw new Error('Blocked: Offline only'); }; | |
| document.addEventListener('click', function(e) { | |
| if (e.target.tagName === 'A' && e.target.href && e.target.href.startsWith('http')) e.preventDefault(); | |
| }); | |
| })(); | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // SIMULATED DATABASE | |
| // ═══════════════════════════════════════════════════════════════════ | |
| const PLANS = { | |
| Starter: { credits: 100, price: 99 }, | |
| Pro: { credits: 1000, price: 499 }, | |
| Business: { credits: 10000, price: 2999 }, | |
| Enterprise: { credits: 100000, price: 19999 } | |
| }; | |
| let db = { | |
| apiKeys: [], | |
| revenue: 0, | |
| totalCalls: 0, | |
| usageLog: [], | |
| settings: { adminKey: 'omega_admin_secret', rateLimit: 60 } | |
| }; | |
| // Generate sample data | |
| function initSampleData() { | |
| const plans = ['Starter', 'Pro', 'Business', 'Enterprise']; | |
| const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; | |
| for (let i = 0; i < 25; i++) { | |
| const plan = plans[random(0, 3)]; | |
| const usage = random(0, PLANS[plan].credits); | |
| const daysAgo = random(1, 90); | |
| const created = new Date(Date.now() - daysAgo * 86400000); | |
| db.apiKeys.push({ | |
| key: 'omega_' + Math.random().toString(36).substr(2, 32), | |
| plan, | |
| credits: PLANS[plan].credits - usage, | |
| totalCredits: PLANS[plan].credits, | |
| usage, | |
| created: created.toISOString(), | |
| email: `user${i}@example.com` | |
| }); | |
| db.revenue += PLANS[plan].price; | |
| } | |
| db.totalCalls = db.apiKeys.reduce((sum, k) => sum + k.usage, 0); | |
| // Generate usage logs | |
| for (let i = 0; i < 100; i++) { | |
| const key = db.apiKeys[random(0, db.apiKeys.length - 1)]; | |
| db.usageLog.push({ | |
| key: key.key, | |
| text: 'Sample analysis text...', | |
| timestamp: new Date(Date.now() - random(0, 86400000 * 7)).toISOString(), | |
| credits_left: key.credits | |
| }); | |
| } | |
| db.usageLog.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // UI FUNCTIONS | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); | |
| document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| document.getElementById('panel-' + tab).classList.add('active'); | |
| } | |
| function formatKey(key) { return key.substring(0, 12) + '...' + key.substring(key.length - 4); } | |
| function updateStats() { | |
| document.getElementById('totalRevenue').textContent = '฿' + db.revenue.toLocaleString(); | |
| document.getElementById('totalKeys').textContent = db.apiKeys.length; | |
| document.getElementById('totalCalls').textContent = db.totalCalls.toLocaleString(); | |
| const avg = db.apiKeys.length > 0 ? Math.round(db.totalCalls / db.apiKeys.length) : 0; | |
| document.getElementById('avgUsage').textContent = avg.toLocaleString(); | |
| } | |
| function renderKeysTable() { | |
| const tbody = document.getElementById('keysTableBody'); | |
| tbody.innerHTML = db.apiKeys.map(k => { | |
| const status = k.credits > 0 ? 'active' : 'expired'; | |
| const statusText = k.credits > 0 ? 'Active' : 'Expired'; | |
| return `<tr> | |
| <td style="font-family:monospace;font-size:0.8rem;">${formatKey(k.key)}</td> | |
| <td><span class="badge ${k.plan.toLowerCase()}">${k.plan}</span></td> | |
| <td>${k.credits.toLocaleString()} / ${k.totalCredits.toLocaleString()}</td> | |
| <td>${k.usage.toLocaleString()}</td> | |
| <td>${new Date(k.created).toLocaleDateString('th-TH')}</td> | |
| <td><span class="badge ${status}">${statusText}</span></td> | |
| <td> | |
| <button class="btn btn-primary" style="padding:5px 10px;font-size:0.75rem;" | |
| onclick="topUpKey('${k.key}')">+ Top Up</button> | |
| <button class="btn btn-danger" style="padding:5px 10px;font-size:0.75rem;" | |
| onclick="deleteKey('${k.key}')">🗑️</button> | |
| </td> | |
| </tr>`; | |
| }).join(''); | |
| } | |
| function renderRevenueByPlan() { | |
| const tbody = document.getElementById('revenueByPlan'); | |
| const planCounts = {}; | |
| db.apiKeys.forEach(k => { | |
| if (!planCounts[k.plan]) planCounts[k.plan] = { count: 0, revenue: 0 }; | |
| planCounts[k.plan].count++; | |
| planCounts[k.plan].revenue += PLANS[k.plan].price; | |
| }); | |
| tbody.innerHTML = Object.entries(planCounts).map(([plan, data]) => | |
| `<tr> | |
| <td><span class="badge ${plan.toLowerCase()}">${plan}</span></td> | |
| <td>฿${PLANS[plan].price.toLocaleString()}</td> | |
| <td>${data.count}</td> | |
| <td style="color:var(--success);font-weight:700;">฿${data.revenue.toLocaleString()}</td> | |
| </tr>` | |
| ).join(''); | |
| } | |
| function renderTopUsage() { | |
| const tbody = document.getElementById('topUsageTable'); | |
| const sorted = [...db.apiKeys].sort((a, b) => b.usage - a.usage).slice(0, 10); | |
| tbody.innerHTML = sorted.map((k, i) => | |
| `<tr> | |
| <td>${i + 1}</td> | |
| <td style="font-family:monospace;font-size:0.8rem;">${formatKey(k.key)}</td> | |
| <td><span class="badge ${k.plan.toLowerCase()}">${k.plan}</span></td> | |
| <td>${k.usage.toLocaleString()}</td> | |
| <td>${k.credits.toLocaleString()}</td> | |
| </tr>` | |
| ).join(''); | |
| } | |
| function renderLogs() { | |
| const container = document.getElementById('logsContainer'); | |
| container.innerHTML = db.usageLog.slice(0, 50).map(log => | |
| `<div class="log-entry"> | |
| <span class="time">${new Date(log.timestamp).toLocaleString('th-TH')}</span> — | |
| <span class="action">API Call</span> — | |
| Key: ${formatKey(log.key)} — | |
| Credits left: ${log.credits_left} | |
| </div>` | |
| ).join(''); | |
| } | |
| function drawRevenueChart() { | |
| const canvas = document.getElementById('revenueChart'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = canvas.offsetHeight; | |
| // Generate last 30 days revenue data | |
| const data = []; | |
| for (let i = 29; i >= 0; i--) { | |
| const day = new Date(Date.now() - i * 86400000); | |
| const dayKeys = db.apiKeys.filter(k => { | |
| const created = new Date(k.created); | |
| return created.toDateString() === day.toDateString(); | |
| }); | |
| data.push(dayKeys.reduce((sum, k) => sum + PLANS[k.plan].price, 0)); | |
| } | |
| const max = Math.max(...data, 1); | |
| const w = canvas.width; | |
| const h = canvas.height; | |
| const barWidth = (w - 40) / data.length; | |
| ctx.clearRect(0, 0, w, h); | |
| // Grid | |
| ctx.strokeStyle = 'rgba(0,212,255,0.1)'; | |
| for (let i = 0; i < 5; i++) { | |
| const y = (h / 5) * i + 20; | |
| ctx.beginPath(); ctx.moveTo(30, y); ctx.lineTo(w, y); ctx.stroke(); | |
| } | |
| // Bars | |
| data.forEach((val, i) => { | |
| const barH = (val / max) * (h - 40); | |
| const x = 30 + i * barWidth; | |
| const y = h - barH - 20; | |
| const gradient = ctx.createLinearGradient(x, y, x, h - 20); | |
| gradient.addColorStop(0, '#00ff88'); | |
| gradient.addColorStop(1, 'rgba(0,255,136,0.2)'); | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(x + 1, y, barWidth - 2, barH); | |
| }); | |
| // Labels | |
| ctx.fillStyle = 'rgba(224,232,255,0.5)'; | |
| ctx.font = '10px sans-serif'; | |
| ctx.fillText('฿' + max.toLocaleString(), 0, 15); | |
| ctx.fillText('30 days ago', 30, h - 5); | |
| ctx.fillText('Today', w - 40, h - 5); | |
| } | |
| function drawUsageChart() { | |
| const canvas = document.getElementById('usageChart'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = canvas.offsetWidth; | |
| canvas.height = canvas.offsetHeight; | |
| // Generate last 7 days usage | |
| const data = []; | |
| for (let i = 6; i >= 0; i--) { | |
| const day = new Date(Date.now() - i * 86400000); | |
| const dayLogs = db.usageLog.filter(l => | |
| new Date(l.timestamp).toDateString() === day.toDateString() | |
| ); | |
| data.push({ day: day.toLocaleDateString('th-TH', { weekday: 'short' }), count: dayLogs.length }); | |
| } | |
| const max = Math.max(...data.map(d => d.count), 1); | |
| const w = canvas.width; | |
| const h = canvas.height; | |
| ctx.clearRect(0, 0, w, h); | |
| // Line chart | |
| ctx.strokeStyle = '#00d4ff'; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| data.forEach((d, i) => { | |
| const x = 40 + (i / (data.length - 1)) * (w - 80); | |
| const y = h - 40 - (d.count / max) * (h - 60); | |
| if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); | |
| }); | |
| ctx.stroke(); | |
| // Points + labels | |
| data.forEach((d, i) => { | |
| const x = 40 + (i / (data.length - 1)) * (w - 80); | |
| const y = h - 40 - (d.count / max) * (h - 60); | |
| ctx.fillStyle = '#00d4ff'; | |
| ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = 'rgba(224,232,255,0.5)'; | |
| ctx.font = '10px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(d.day, x, h - 20); | |
| ctx.fillText(d.count, x, y - 10); | |
| }); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // ACTIONS | |
| // ═══════════════════════════════════════════════════════════════════ | |
| function showCreateKeyModal() { | |
| document.getElementById('createKeyModal').classList.add('show'); | |
| document.getElementById('newKeyResult').style.display = 'none'; | |
| } | |
| function closeModal() { | |
| document.getElementById('createKeyModal').classList.remove('show'); | |
| } | |
| function createNewKey() { | |
| const plan = document.getElementById('newKeyPlan').value; | |
| const email = document.getElementById('newKeyEmail').value; | |
| const newKey = 'omega_' + Math.random().toString(36).substr(2, 32); | |
| db.apiKeys.push({ | |
| key: newKey, plan, | |
| credits: PLANS[plan].credits, | |
| totalCredits: PLANS[plan].credits, | |
| usage: 0, | |
| created: new Date().toISOString(), | |
| }); | |
| db.revenue += PLANS[plan].price; | |
| document.getElementById('newKeyResult').style.display = 'block'; | |
| document.getElementById('newKeyDisplay').textContent = newKey; | |
| updateStats(); | |
| renderKeysTable(); | |
| renderRevenueByPlan(); | |
| } | |
| function topUpKey(key) { | |
| const k = db.apiKeys.find(x => x.key === key); | |
| if (k) { | |
| k.credits += PLANS[k.plan].credits; | |
| db.revenue += PLANS[k.plan].price; | |
| updateStats(); | |
| renderKeysTable(); | |
| alert(`Top Up สำเร็จ! ${k.plan} — Credits: ${k.credits.toLocaleString()}`); | |
| } | |
| } | |
| function deleteKey(key) { | |
| if (confirm('ต้องการลบ Key นี้?')) { | |
| db.apiKeys = db.apiKeys.filter(k => k.key !== key); | |
| updateStats(); | |
| renderKeysTable(); | |
| } | |
| } | |
| function saveSettings() { | |
| db.settings.adminKey = document.getElementById('adminKeyInput').value; | |
| db.settings.rateLimit = parseInt(document.getElementById('rateLimitInput').value); | |
| alert('บันทึกการตั้งค่าเรียบร้อย!'); | |
| } | |
| // ═══════════════════════════════════════════════════════════════════ | |
| // INIT | |
| // ═══════════════════════════════════════════════════════════════════ | |
| initSampleData(); | |
| updateStats(); | |
| renderKeysTable(); | |
| renderRevenueByPlan(); | |
| renderTopUsage(); | |
| renderLogs(); | |
| drawRevenueChart(); | |
| drawUsageChart(); | |
| </script> | |
| </body> | |
| </html> | |
Xet Storage Details
- Size:
- 31.3 kB
- Xet hash:
- d38fc28b366f41f50fb10b894cd23e71b82ee9c1021ce3481b1f7275b0005d32
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.