Buckets:

Sinningai/asitheboy / admin-dashboard.html
boylnwzav1's picture
download
raw
31.3 kB
<!DOCTYPE html>
<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(),
email
});
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.