cm_bot_v2/cm_web_view.py
Wong Yiek Heng 0d7f152317 First Commit
2025-10-04 10:16:41 +08:00

745 lines
24 KiB
Python

from flask import Flask, render_template_string, request, jsonify
from flask_cors import CORS
import requests
import json
app = Flask(__name__)
CORS(app)
# API base URL - use environment variable for Docker Compose
import os
API_BASE_URL = os.getenv('API_BASE_URL', 'http://localhost:3000')
print("API: ", API_BASE_URL)
# Beautiful HTML template with modern styling
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CM Bot Database Viewer</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.tab {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
padding: 15px 30px;
margin: 0 10px;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.tab:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.tab.active {
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
}
.content {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.table-container {
overflow-x: auto;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 15px;
overflow: hidden;
}
th {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
text-align: left;
font-weight: 600;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
td {
padding: 18px 20px;
border-bottom: 1px solid #f0f0f0;
font-size: 0.95rem;
color: #333;
}
tr:hover {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f2ff 100%);
transform: scale(1.01);
transition: all 0.2s ease;
}
tr:last-child td {
border-bottom: none;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background: #d4edda;
color: #155724;
}
.status-inactive {
background: #f8d7da;
color: #721c24;
}
.editable {
cursor: pointer;
position: relative;
transition: all 0.2s ease;
}
.editable:hover {
background: #e3f2fd !important;
border-radius: 4px;
}
.editable.editing {
background: #fff3e0 !important;
border: 2px solid #ff9800;
border-radius: 4px;
}
.edit-input {
width: 100%;
border: none;
background: transparent;
padding: 4px 8px;
font-size: inherit;
font-family: inherit;
outline: none;
}
.edit-buttons {
display: inline-flex;
gap: 5px;
margin-left: 10px;
}
.edit-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
font-weight: 600;
transition: all 0.2s ease;
}
.save-btn {
background: #4caf50;
color: white;
}
.save-btn:hover {
background: #45a049;
}
.cancel-btn {
background: #f44336;
color: white;
}
.cancel-btn:hover {
background: #da190b;
}
.edit-icon {
opacity: 0;
transition: opacity 0.2s ease;
margin-left: 5px;
color: #666;
}
.editable:hover .edit-icon {
opacity: 1;
}
.sort-indicator {
margin-left: 5px;
font-size: 0.8rem;
}
.sortable {
cursor: pointer;
user-select: none;
}
.sortable:hover {
background: rgba(255, 255, 255, 0.1);
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
.loading i {
font-size: 2rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
background: #f8d7da;
color: #721c24;
padding: 20px;
border-radius: 10px;
text-align: center;
margin: 20px 0;
}
.refresh-btn {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
border: none;
padding: 12px 25px;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
margin-bottom: 20px;
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.stat-card h3 {
font-size: 2rem;
margin-bottom: 10px;
}
.stat-card p {
opacity: 0.9;
font-size: 1rem;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;
}
.tabs {
flex-direction: column;
align-items: center;
}
.tab {
margin: 5px 0;
width: 200px;
}
.content {
padding: 20px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1><i class="fas fa-database"></i> CM Bot Database Viewer</h1>
<p>Real-time view of accounts and users data</p>
</div>
<div class="tabs">
<button class="tab active" onclick="showTab('acc')">
<i class="fas fa-user-circle"></i> Accounts
</button>
<button class="tab" onclick="showTab('user')">
<i class="fas fa-users"></i> Users
</button>
</div>
<div class="content">
<button class="refresh-btn" onclick="refreshData()">
<i class="fas fa-sync-alt"></i> Refresh Data
</button>
<div id="stats" class="stats" style="display: none;">
<div class="stat-card">
<h3 id="acc-count">0</h3>
<p>Total Accounts</p>
</div>
<div class="stat-card">
<h3 id="user-count">0</h3>
<p>Total Users</p>
</div>
</div>
<div id="acc-content" class="tab-content">
<div class="loading">
<i class="fas fa-spinner"></i>
<p>Loading accounts...</p>
</div>
</div>
<div id="user-content" class="tab-content" style="display: none;">
<div class="loading">
<i class="fas fa-spinner"></i>
<p>Loading users...</p>
</div>
</div>
</div>
</div>
<script>
let currentTab = 'acc';
let accData = [];
let userData = [];
let editingCell = null;
let originalValue = null;
function showTab(tab) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
// Show/hide content
document.getElementById('acc-content').style.display = tab === 'acc' ? 'block' : 'none';
document.getElementById('user-content').style.display = tab === 'user' ? 'block' : 'none';
currentTab = tab;
// Load data if not already loaded
if (tab === 'acc' && accData.length === 0) {
loadAccData();
} else if (tab === 'user' && userData.length === 0) {
loadUserData();
}
}
async function loadAccData() {
try {
const response = await fetch('/api/acc/');
if (!response.ok) throw new Error('Failed to fetch accounts');
accData = await response.json();
displayAccData();
updateStats();
} catch (error) {
document.getElementById('acc-content').innerHTML =
`<div class="error">Error loading accounts: ${error.message}</div>`;
}
}
async function loadUserData() {
try {
const response = await fetch('/api/user/');
if (!response.ok) throw new Error('Failed to fetch users');
userData = await response.json();
displayUserData();
updateStats();
} catch (error) {
document.getElementById('user-content').innerHTML =
`<div class="error">Error loading users: ${error.message}</div>`;
}
}
function displayAccData() {
const container = document.getElementById('acc-content');
if (accData.length === 0) {
container.innerHTML = '<div class="error">No accounts found</div>';
return;
}
// Sort data with 13c prefix priority
const sortedData = sortData([...accData], 'acc');
const table = `
<div class="table-container">
<table>
<thead>
<tr>
<th class="sortable"><i class="fas fa-user"></i> Username</th>
<th><i class="fas fa-lock"></i> Password <i class="fas fa-edit edit-icon"></i></th>
<th><i class="fas fa-info-circle"></i> Status <i class="fas fa-edit edit-icon"></i></th>
<th><i class="fas fa-link"></i> Link <i class="fas fa-edit edit-icon"></i></th>
</tr>
</thead>
<tbody>
${sortedData.map((acc, index) => `
<tr>
<td><strong>${acc.username || 'N/A'}</strong></td>
<td class="editable" onclick="startEdit(this, 'password', 'acc', ${accData.findIndex(a => a.username === acc.username)})">
${acc.password || 'N/A'}
</td>
<td class="editable" onclick="startEdit(this, 'status', 'acc', ${accData.findIndex(a => a.username === acc.username)})">
<span class="status-badge ${acc.status === 'active' ? 'status-active' : 'status-inactive'}">
${acc.status || 'N/A'}
</span>
</td>
<td class="editable" onclick="startEdit(this, 'link', 'acc', ${accData.findIndex(a => a.username === acc.username)})">
${acc.link || 'N/A'}
</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = table;
}
function displayUserData() {
const container = document.getElementById('user-content');
if (userData.length === 0) {
container.innerHTML = '<div class="error">No users found</div>';
return;
}
// Sort data with 13c prefix priority and by update time
const sortedData = sortData([...userData], 'user');
const table = `
<div class="table-container">
<table>
<thead>
<tr>
<th class="sortable"><i class="fas fa-user"></i> From Username</th>
<th><i class="fas fa-lock"></i> From Password <i class="fas fa-edit edit-icon"></i></th>
<th><i class="fas fa-user"></i> To Username <i class="fas fa-edit edit-icon"></i></th>
<th><i class="fas fa-lock"></i> To Password <i class="fas fa-edit edit-icon"></i></th>
<th class="sortable"><i class="fas fa-clock"></i> Last Update</th>
</tr>
</thead>
<tbody>
${sortedData.map((user, index) => `
<tr>
<td><strong>${user.f_username || 'N/A'}</strong></td>
<td class="editable" onclick="startEdit(this, 'f_password', 'user', ${userData.findIndex(u => u.f_username === user.f_username)})">
${user.f_password || 'N/A'}
</td>
<td class="editable" onclick="startEdit(this, 't_username', 'user', ${userData.findIndex(u => u.f_username === user.f_username)})">
<strong>${user.t_username || 'N/A'}</strong>
</td>
<td class="editable" onclick="startEdit(this, 't_password', 'user', ${userData.findIndex(u => u.f_username === user.f_username)})">
${user.t_password || 'N/A'}
</td>
<td>${user.last_update_time || 'N/A'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
container.innerHTML = table;
}
function updateStats() {
document.getElementById('acc-count').textContent = accData.length;
document.getElementById('user-count').textContent = userData.length;
document.getElementById('stats').style.display = 'grid';
}
function refreshData() {
if (currentTab === 'acc') {
loadAccData();
} else {
loadUserData();
}
}
// Sorting functions
function sortData(data, type) {
if (type === 'acc') {
return data.sort((a, b) => {
// 13c prefix always on top
const aIs13c = a.username && a.username.startsWith('13c');
const bIs13c = b.username && b.username.startsWith('13c');
if (aIs13c && !bIs13c) return -1;
if (!aIs13c && bIs13c) return 1;
// If both are 13c or both are not 13c, sort by username in descending order
if (aIs13c && bIs13c) {
return (b.username || '').localeCompare(a.username || '');
} else {
return (b.username || '').localeCompare(a.username || '');
}
});
} else if (type === 'user') {
return data.sort((a, b) => {
// 13c prefix always on top
const aIs13c = a.f_username && a.f_username.startsWith('13c');
const bIs13c = b.f_username && b.f_username.startsWith('13c');
if (aIs13c && !bIs13c) return -1;
if (!aIs13c && bIs13c) return 1;
// Then sort by last_update_time (newest first)
const aTime = new Date(a.last_update_time || 0);
const bTime = new Date(b.last_update_time || 0);
return bTime - aTime;
});
}
return data;
}
// Editing functions
function startEdit(cell, field, type, index) {
if (editingCell) {
cancelEdit();
}
editingCell = cell;
originalValue = cell.textContent.trim();
const input = document.createElement('input');
input.type = 'text';
input.className = 'edit-input';
input.value = originalValue;
const buttons = document.createElement('div');
buttons.className = 'edit-buttons';
buttons.innerHTML = `
<button class="edit-btn save-btn" onclick="saveEdit('${field}', '${type}', ${index})">
<i class="fas fa-check"></i>
</button>
<button class="edit-btn cancel-btn" onclick="cancelEdit()">
<i class="fas fa-times"></i>
</button>
`;
cell.innerHTML = '';
cell.appendChild(input);
cell.appendChild(buttons);
cell.classList.add('editing');
input.focus();
input.select();
}
function cancelEdit() {
if (editingCell) {
editingCell.textContent = originalValue;
editingCell.classList.remove('editing');
editingCell = null;
originalValue = null;
}
}
async function saveEdit(field, type, index) {
if (!editingCell) return;
const input = editingCell.querySelector('.edit-input');
const newValue = input.value.trim();
try {
if (type === 'acc') {
const data = {
username: accData[index].username,
password: field === 'password' ? newValue : accData[index].password,
status: field === 'status' ? newValue : accData[index].status,
link: field === 'link' ? newValue : accData[index].link
};
const response = await fetch('/api/update-acc-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
accData[index][field] = newValue;
editingCell.textContent = newValue;
editingCell.classList.remove('editing');
editingCell = null;
originalValue = null;
} else {
throw new Error('Failed to update account data');
}
} else if (type === 'user') {
const data = {
f_username: userData[index].f_username,
f_password: field === 'f_password' ? newValue : userData[index].f_password,
t_username: field === 't_username' ? newValue : userData[index].t_username,
t_password: field === 't_password' ? newValue : userData[index].t_password
};
const response = await fetch('/api/update-user-data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
if (response.ok) {
userData[index][field] = newValue;
userData[index].last_update_time = new Date().toUTCString();
editingCell.textContent = newValue;
editingCell.classList.remove('editing');
editingCell = null;
originalValue = null;
} else {
throw new Error('Failed to update user data');
}
}
} catch (error) {
alert('Error updating data: ' + error.message);
cancelEdit();
}
}
// Load initial data
loadAccData();
// Auto-refresh every 30 seconds
setInterval(() => {
if (currentTab === 'acc') {
loadAccData();
} else {
loadUserData();
}
}, 30000);
</script>
</body>
</html>
"""
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/api/acc/')
def proxy_acc():
try:
response = requests.get(f"{API_BASE_URL}/acc/")
return jsonify(response.json())
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/user/')
def proxy_user():
try:
response = requests.get(f"{API_BASE_URL}/user/")
return jsonify(response.json())
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/update-acc-data', methods=['POST'])
def proxy_update_acc():
try:
data = request.get_json()
response = requests.post(f"{API_BASE_URL}/update-acc-data", json=data)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/api/update-user-data', methods=['POST'])
def proxy_update_user():
try:
data = request.get_json()
response = requests.post(f"{API_BASE_URL}/update-user-data", json=data)
return jsonify(response.json()), response.status_code
except Exception as e:
return jsonify({"error": str(e)}), 500
if __name__ == '__main__':
print("Starting CM Web View...")
print("Web interface will be available at: http://localhost:8000")
print("Make sure the API server is running on port 3000")
app.run(host='0.0.0.0', port=8000, debug=True)