Commit 2eac208c authored by Vũ Hoàng Anh's avatar Vũ Hoàng Anh

Add : limit features

parent 41ee4c0a
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Quản lý Message Limit</title>
<style>
:root {
--bg: #f8fafc;
--surface: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--primary: #3b82f6;
--primary-hover: #2563eb;
--success: #10b981;
--error: #ef4444;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 40px 20px;
display: flex;
justify-content: center;
}
.container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 30px;
width: 100%;
max-width: 500px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
h2 {
margin-top: 0;
margin-bottom: 8px;
}
p {
color: var(--muted);
margin-top: 0;
margin-bottom: 24px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
}
input[type="text"] {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
outline: none;
transition: border-color 0.2s;
}
input[type="text"]:focus {
border-color: var(--primary);
}
.btn {
display: inline-block;
width: 100%;
padding: 10px 16px;
background: var(--primary);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
text-align: center;
transition: background 0.2s;
}
.btn:hover {
background: var(--primary-hover);
}
.btn:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.btn-secondary {
background: #e2e8f0;
color: #0f172a;
margin-top: 10px;
}
.btn-secondary:hover {
background: #cbd5e1;
}
#resultBox {
display: none;
margin-top: 24px;
padding: 16px;
border-radius: 8px;
background: #f1f5f9;
border: 1px solid #cbd5e1;
font-size: 14px;
}
#resultStatus {
font-weight: bold;
margin-bottom: 8px;
}
.success { color: var(--success); }
.error { color: var(--error); }
.info-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px dashed #cbd5e1;
}
.info-row:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
</style>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rate Limit Manager — Canifa AI</title>
<link rel="stylesheet" href="/static/css/theme.css">
<link rel="stylesheet" href="/static/css/components.css">
<script src="/static/js/frame-detect.js"></script>
<style>
body { margin:0; min-height:100vh; background:var(--background); font-family:var(--font-sans); color:var(--foreground); }
.page { max-width:900px; margin:0 auto; padding:20px 20px 60px; }
/* Header */
.page-hdr { display:flex; align-items:center; justify-content:space-between; margin-bottom:20px; flex-wrap:wrap; gap:12px; }
.page-hdr h1 { font-size:20px; font-weight:700; margin:0; }
.page-hdr p { font-size:13px; color:var(--muted-fg); margin:2px 0 0; }
/* Filter Bar for Inputs */
.filter-bar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; padding:20px; background:var(--card); border:1px solid var(--border); border-radius:10px; margin-bottom:24px; box-shadow:0 1px 3px rgba(0,0,0,0.05); }
.filter-bar input {
flex:1; padding:10px 14px; border:1px solid var(--border); border-radius:8px; font:inherit; font-size:14px; background:var(--background); color:var(--foreground); outline:none;
}
.filter-bar input:focus { border-color:var(--primary); box-shadow:0 0 0 3px rgba(59,89,152,0.08); }
.filter-actions { display:flex; gap:10px; }
/* Stats Cards */
.stats-row { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:24px; display:none; }
.stat-card { background:var(--card); border:1px solid var(--border); border-radius:10px; padding:18px 20px; box-shadow:0 1px 3px rgba(0,0,0,0.05); }
.stat-card .label { font-size:11px; font-weight:600; text-transform:uppercase; letter-spacing:.04em; color:var(--muted-fg); margin-bottom:8px; }
.stat-card .value { font-size:28px; font-weight:700; line-height:1.1; }
.stat-card .sub { font-size:12px; color:var(--muted-fg); margin-top:6px; }
.stat-card .value.success { color:var(--success); }
.stat-card .value.warn { color:var(--warn); }
.stat-card .value.error { color:var(--error); }
.stat-card .value.primary { color:var(--primary); }
/* Progress bar */
.progress-bar-wrap { background:var(--muted); border-radius:6px; height:8px; overflow:hidden; margin-top:12px; }
.progress-bar-fill { height:100%; border-radius:6px; background:var(--primary); transition:width .5s ease; }
.progress-bar-fill.danger { background:var(--error); }
.progress-bar-fill.warn { background:var(--warn); }
/* Status message */
.status-msg { padding:14px 16px; border-radius:8px; margin-bottom:20px; font-size:14px; font-weight:500; display:none; }
.status-msg.success { background:var(--success-light); color:var(--success); border:1px solid rgba(16, 185, 129, 0.2); }
.status-msg.error { background:var(--warn-light); color:var(--error); border:1px solid rgba(239, 68, 68, 0.2); }
@keyframes spin { to { transform:rotate(360deg); } }
.spinner-sm { display:inline-block; width:18px; height:18px; border:2px solid var(--border); border-top-color:var(--primary); border-radius:50%; animation:spin .6s linear infinite; }
@media (max-width:768px) {
.stats-row { grid-template-columns:repeat(2,1fr); }
.filter-bar { flex-direction:column; align-items:stretch; }
.filter-actions { justify-content:flex-end; }
}
</style>
</head>
<body>
<div class="page">
<!-- Header -->
<div class="page-hdr">
<div>
<h1>✦ Rate Limit & Quota Manager</h1>
<p>Quản lý số luợng tin nhắn cho phép của người dùng (user) và thiết bị (device)</p>
</div>
</div>
<div class="container">
<h2>Thêm Limit / Reset Quota</h2>
<p>Nhập Identity Key (ví dụ: <code>user:123</code> hoặc <code>device:abc</code>) để xem thông tin hoặc reset/thêm limit gửi tin nhắn trong ngày.</p>
<div id="msgBox" class="status-msg"></div>
<div class="form-group">
<label for="identityKey">Identity Key</label>
<input type="text" id="identityKey" placeholder="user:..." autocomplete="off">
<!-- Main Input Form -->
<div class="filter-bar">
<div style="flex:1;">
<label style="display:block; font-size:12px; font-weight:600; color:var(--muted-fg); margin-bottom:6px; text-transform:uppercase; letter-spacing:0.04em;">Identity Key</label>
<input type="text" id="identityKey" placeholder="VD: user:12345 hoặc device:abcxyz..." autocomplete="off">
</div>
<div class="filter-actions" style="align-self:flex-end;">
<button class="btn btn-outline" id="btnInfo" onclick="checkInfo()">
<span style="margin-right:6px">🔍</span>Kiểm tra
</button>
<button class="btn btn-primary" id="btnReset" onclick="resetLimit()" style="background:var(--success);border-color:var(--success);color:#fff;">
<span style="margin-right:6px"></span>Thêm / Reset Quota
</button>
</div>
</div>
<button class="btn" id="btnInfo" onclick="checkInfo()">Kiểm tra thông tin</button>
<button class="btn btn-secondary" id="btnReset" onclick="resetLimit()" style="background:var(--success); color:white;">Thêm/Reset Limit</button>
<div id="resultBox">
<div id="resultStatus"></div>
<div id="resultDetails"></div>
<!-- Stats display (hidden initially) -->
<div class="stats-row" id="statsRow">
<div class="stat-card">
<div class="label">Quota Trong Ngày</div>
<div class="value primary" id="statTotal">0</div>
<div class="sub">Số tin nhắn tối đa 24h</div>
</div>
<div class="stat-card">
<div class="label">Đã Gửi</div>
<div class="value" id="statUsed">0</div>
<div class="sub" id="statProgressText">0% đã dùng</div>
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="progressFill" style="width:0%"></div></div>
</div>
<div class="stat-card">
<div class="label">Còn Lại</div>
<div class="value success" id="statRemaining">0</div>
<div class="sub">Số tin cho phép gửi</div>
</div>
<div class="stat-card">
<div class="label">Trạng Thái</div>
<div class="value" id="statStatus">OK</div>
<div class="sub" id="statType">Loại: Khách (Guest)</div>
</div>
</div>
</div>
<script>
const API_BASE = '/api/limit';
function showMessage(msg, isError = false) {
const box = document.getElementById('msgBox');
box.textContent = msg;
box.className = 'status-msg ' + (isError ? 'error' : 'success');
box.style.display = 'block';
setTimeout(() => { box.style.display = 'none'; }, 4000);
}
// Handle Enter key
document.getElementById('identityKey').addEventListener('keydown', e => {
if (e.key === 'Enter') checkInfo();
});
async function checkInfo() {
const key = document.getElementById('identityKey').value.trim();
if (!key) { alert('Vui lòng nhập Identity Key'); return; }
setLoading('btnInfo', true);
setLoading('btnInfo', true, '🔍');
try {
const res = await fetch(`${API_BASE}/info?identity_key=${encodeURIComponent(key)}`);
const data = await res.json();
const box = document.getElementById('resultBox');
const statusLabel = document.getElementById('resultStatus');
const details = document.getElementById('resultDetails');
box.style.display = 'block';
const statsRow = document.getElementById('statsRow');
statsRow.style.display = 'grid';
if (data.status === 'success') {
statusLabel.className = 'success';
statusLabel.textContent = 'Thông tin Limit';
const info = data.info || {};
details.innerHTML = `
<div class="info-row"><span>Đã dùng:</span> <strong>${info.used || 0} / ${info.limit || 0}</strong></div>
<div class="info-row"><span>Còn lại:</span> <strong>${info.remaining || 0}</strong></div>
<div class="info-row"><span>Loại:</span> <strong>${info.is_authenticated ? 'User' : 'Guest'}</strong></div>
<div class="info-row"><span>Đã Block:</span> <strong>${info.remaining <= 0 ? 'Có' : 'Không'}</strong></div>
`;
const limit = Number(info.limit || 0);
const used = Number(info.used || 0);
const remaining = Number(info.remaining || 0);
const isAuth = info.is_authenticated;
const isBlocked = remaining <= 0 && limit > 0;
document.getElementById('statTotal').textContent = limit;
document.getElementById('statUsed').textContent = used;
const remainingEl = document.getElementById('statRemaining');
remainingEl.textContent = remaining;
remainingEl.className = 'value ' + (remaining <= 0 ? 'error' : (remaining <= 2 ? 'warn' : 'success'));
const statusEl = document.getElementById('statStatus');
statusEl.textContent = isBlocked ? 'BỊ CHẶN' : 'BÌNH THƯỜNG';
statusEl.className = 'value ' + (isBlocked ? 'error' : 'success');
document.getElementById('statType').textContent = isAuth ? 'Loại: Đăng nhập (User)' : 'Loại: Khách (Guest/Device)';
// Progress
const pct = limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;
document.getElementById('statProgressText').textContent = pct + '% đã dùng';
const pFill = document.getElementById('progressFill');
pFill.style.width = pct + '%';
pFill.className = 'progress-bar-fill ' + (pct >= 100 ? 'danger' : (pct >= 80 ? 'warn' : ''));
} else {
statusLabel.className = 'error';
statusLabel.textContent = 'Lỗi';
details.innerHTML = `<div>${data.message || 'Không thể lấy thông tin'}</div>`;
showMessage(data.message || 'Không thể lấy thông tin', true);
statsRow.style.display = 'none';
}
} catch (e) {
alert('Lỗi: ' + e.message);
showMessage('Lỗi: ' + e.message, true);
} finally {
setLoading('btnInfo', false);
setLoading('btnInfo', false, '🔍');
}
}
......@@ -189,49 +184,36 @@
const key = document.getElementById('identityKey').value.trim();
if (!key) { alert('Vui lòng nhập Identity Key'); return; }
if (!confirm(`Bạn có chắc chắn muốn reset (thêm) limit cho ${key}? Dữ liệu số tin nhắn đã gửi hôm nay sẽ về 0.`)) return;
if (!confirm(`Bạn có chắc chắn muốn reset (thêm) limit cho ${key}?\nDữ liệu số tin nhắn đã gửi hôm nay sẽ tự động về 0.`)) return;
setLoading('btnReset', true);
setLoading('btnReset', true, '⚡');
try {
const res = await fetch(`${API_BASE}/reset`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity_key: key })
});
const data = await res.json();
const box = document.getElementById('resultBox');
const statusLabel = document.getElementById('resultStatus');
const details = document.getElementById('resultDetails');
box.style.display = 'block';
details.innerHTML = '';
if (data.status === 'success') {
statusLabel.className = 'success';
statusLabel.textContent = data.message;
// Tự động tải lại thông tin
setTimeout(checkInfo, 500);
showMessage('✅ ' + data.message, false);
// Tự động tải lại thông tin sau khi reset
setTimeout(checkInfo, 300);
} else {
statusLabel.className = 'error';
statusLabel.textContent = 'Lỗi';
details.innerHTML = `<div>${data.message || 'Không thể reset'}</div>`;
showMessage('❌ ' + (data.message || 'Không thể reset'), true);
}
} catch (e) {
alert('Lỗi: ' + e.message);
showMessage('Lỗi: ' + e.message, true);
} finally {
setLoading('btnReset', false);
setLoading('btnReset', false, '⚡');
}
}
function setLoading(btnId, isLoading) {
function setLoading(btnId, isLoading, icon) {
const btn = document.getElementById(btnId);
if (isLoading) {
btn.dataset.og = btn.innerHTML;
btn.innerHTML = 'Đang xử lý...';
btn.innerHTML = `<span class="spinner-sm" style="margin-right:6px;width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;border-top-color:transparent;"></span> Đang tải...`;
btn.disabled = true;
} else {
btn.innerHTML = btn.dataset.og || btn.innerHTML;
......@@ -239,6 +221,5 @@
}
}
</script>
</body>
</html>
......@@ -139,6 +139,11 @@ body{margin:0;display:flex;min-height:100vh}
<span>User Insight</span>
<span class="nav-badge badge-beta">NEW</span>
</a>
<a data-page="limit.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon" style="font-size:10px;font-weight:800;letter-spacing:.04em">RL</span>
<span>Rate Limit</span>
<span class="nav-badge badge-beta">NEW</span>
</a>
<a data-page="regression-test.html" class="nav-item" onclick="navigateTo(this)">
<span class="nav-icon" style="font-size:10px;font-weight:800;letter-spacing:.04em">RT</span>
<span>Regression Test</span>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment