proxycf / dashboard.html
arfandi7322's picture
fix(proxy): architectural overhaul for timeouts, 9k limits, and infinite hang prevention
6149ef7
Raw
History Blame Contribute Delete
24.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CF Proxy Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
[x-cloak] { display: none !important; }
.tab-active { border-bottom: 2px solid #3b82f6; color: #3b82f6; font-weight: bold; }
</style>
</head>
<body class="bg-gray-100 text-gray-800 font-sans" x-data="dashboard()">
<!-- Navbar -->
<nav class="bg-white shadow-md border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<svg class="w-8 h-8 text-blue-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path></svg>
<span class="font-bold text-xl tracking-tight">CF Proxy Manager</span>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm font-medium" :class="data.settings?.thinking_enabled ? 'text-green-600' : 'text-gray-500'">
Thinking: <span x-text="data.settings?.thinking_enabled ? 'ON' : 'OFF'"></span>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Tabs -->
<div class="flex space-x-6 border-b border-gray-200 mb-6">
<button @click="tab = 'overview'" :class="tab === 'overview' ? 'tab-active' : 'text-gray-500 hover:text-gray-700'" class="pb-3 text-sm font-medium px-2">Overview</button>
<button @click="tab = 'accounts'" :class="tab === 'accounts' ? 'tab-active' : 'text-gray-500 hover:text-gray-700'" class="pb-3 text-sm font-medium px-2">Accounts</button>
<button @click="tab = 'logs'" :class="tab === 'logs' ? 'tab-active' : 'text-gray-500 hover:text-gray-700'" class="pb-3 text-sm font-medium px-2">Logs</button>
<button @click="tab = 'settings'" :class="tab === 'settings' ? 'tab-active' : 'text-gray-500 hover:text-gray-700'" class="pb-3 text-sm font-medium px-2">Settings</button>
</div>
<!-- Alerts -->
<div x-show="alert.show" x-cloak class="mb-4 p-4 rounded-md shadow-sm" :class="alert.type === 'error' ? 'bg-red-50 text-red-800' : 'bg-green-50 text-green-800'">
<div class="flex justify-between">
<span x-text="alert.message"></span>
<button @click="alert.show = false" class="font-bold">&times;</button>
</div>
</div>
<!-- TAB: Overview -->
<div x-show="tab === 'overview'" x-cloak>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 text-sm font-medium mb-1">Total Accounts</div>
<div class="text-3xl font-bold text-gray-800" x-text="data.accounts?.length || 0"></div>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 text-sm font-medium mb-1">Accounts on Cooldown</div>
<div class="text-3xl font-bold text-red-600" x-text="cooldownCount"></div>
</div>
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<div class="text-gray-500 text-sm font-medium mb-1">Total Neurons Today</div>
<div class="text-3xl font-bold text-blue-600" x-text="totalNeuronsToday.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})"></div>
</div>
</div>
<!-- Chart placeholder (requires Chart.js) -->
<div class="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
<h3 class="text-lg font-medium text-gray-800 mb-4">Neuron Usage (Today)</h3>
<canvas id="usageChart" height="100"></canvas>
</div>
</div>
<!-- TAB: Accounts -->
<div x-show="tab === 'accounts'" x-cloak>
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-800">Cloudflare Accounts</h2>
<button @click="showAddAccount = true" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium shadow-sm transition-colors">
+ Add Account
</button>
</div>
<!-- Add Account Modal -->
<div x-show="showAddAccount" x-cloak class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50 flex items-center justify-center">
<div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-md">
<h3 class="text-lg font-bold mb-4">Add CF Account</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700">Account ID</label>
<input type="text" x-model="newAcc.account_id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">API Key</label>
<input type="password" x-model="newAcc.api_key" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Admin Password</label>
<input type="password" x-model="newAcc.password" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div class="mt-6 flex justify-end space-x-3">
<button @click="showAddAccount = false" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300">Cancel</button>
<button @click="addAccount()" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Save</button>
</div>
</div>
</div>
<!-- Accounts Table -->
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Source</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Daily Usage</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<template x-for="acc in accountList" :key="acc.account_id">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
<span x-text="acc.account_id.substring(0, 8) + '...'"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span x-show="acc.is_env" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">.env</span>
<span x-show="!acc.is_env" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800">JSON</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-1">
<div class="h-2.5 rounded-full" :class="acc.usage >= 9000 ? 'bg-red-600' : 'bg-blue-600'" :style="`width: ${Math.min((acc.usage / 9000) * 100, 100)}%`"></div>
</div>
<div class="text-xs text-gray-500">
<span x-text="Number(acc.usage).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})"></span> / 9,000 Neurons
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span x-show="acc.usage < 9000" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Active</span>
<span x-show="acc.usage >= 9000" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800">Cooldown</span>
<div x-show="acc.usage >= 9000" class="text-xs text-red-500 mt-1" x-text="timeToMidnight"></div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button x-show="!acc.is_env" @click="deleteAccount(acc.account_id)" class="text-red-600 hover:text-red-900">Delete</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- TAB: Logs -->
<div x-show="tab === 'logs'" x-cloak>
<div class="bg-white shadow-sm rounded-lg border border-gray-200 overflow-hidden">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time (UTC)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Neurons</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<template x-for="log in (data.logs || []).slice().reverse().slice(0, 50)" :key="log.timestamp">
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500" x-text="new Date(log.timestamp * 1000).toLocaleString()"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900" x-text="log.account_id ? log.account_id.substring(0,8)+'...' : '-'"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500" x-text="log.model"></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span x-text="Number(log.neurons || 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2})"></span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span x-show="log.status === 200" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">Success</span>
<span x-show="log.status !== 200" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 text-red-800" x-text="'HTTP ' + log.status"></span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- TAB: Settings -->
<div x-show="tab === 'settings'" x-cloak class="max-w-2xl">
<div class="bg-white shadow-sm rounded-lg border border-gray-200 p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Proxy Configuration</h3>
<div class="flex items-center justify-between py-4 border-b border-gray-200">
<div>
<h4 class="text-sm font-medium text-gray-900">Thinking (Reasoning Tokens)</h4>
<p class="text-sm text-gray-500">Enable or disable reasoning output from models (e.g., DeepSeek/Llama 3 thinking process).</p>
</div>
<div>
<!-- Toggle switch -->
<button type="button" @click="toggleThinking()" class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" :class="data.settings?.thinking_enabled ? 'bg-blue-600' : 'bg-gray-200'">
<span class="sr-only">Use setting</span>
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200" :class="data.settings?.thinking_enabled ? 'translate-x-5' : 'translate-x-0'"></span>
</button>
</div>
</div>
<div class="mt-6">
<label class="block text-sm font-medium text-gray-700">Admin Password (Required for changes)</label>
<input type="password" x-model="adminPassword" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm p-2 focus:ring-blue-500 focus:border-blue-500 max-w-sm" placeholder="Enter password...">
</div>
</div>
</div>
</div>
<script>
function dashboard() {
return {
tab: 'overview',
data: { accounts: [], usage: {}, settings: {}, logs: [] },
envAccounts: [], // Loaded from initial status
showAddAccount: false,
adminPassword: '',
newAcc: { account_id: '', api_key: '', password: '' },
alert: { show: false, message: '', type: 'success' },
timeToMidnight: '',
chartInstance: null,
async init() {
await this.fetchData();
this.startMidnightTimer();
setInterval(() => this.fetchData(), 10000); // refresh every 10s
},
async fetchData() {
try {
const res = await fetch('/api/dashboard_data');
const json = await res.json();
this.data = json;
const statusRes = await fetch('/api/status');
const statusJson = await statusRes.json();
this.envAccounts = statusJson.env_accounts || [];
if (this.tab === 'overview') this.updateChart();
} catch (e) {
console.error('Error fetching data:', e);
}
},
get todayUTC() {
return new Date().toISOString().split('T')[0];
},
get accountList() {
const allMap = new Map();
// Merge env and json accounts
this.envAccounts.forEach(kid => {
allMap.set(kid, { account_id: kid, is_env: true, usage: 0 });
});
(this.data.accounts || []).forEach(acc => {
allMap.set(acc.account_id, { account_id: acc.account_id, is_env: false, usage: 0 });
});
// Merge usage
const todayUsage = this.data.usage[this.todayUTC] || {};
for (const [kid, usageObj] of Object.entries(todayUsage)) {
if (allMap.has(kid)) {
allMap.get(kid).usage = usageObj.neurons || 0;
} else {
// usage exists but account doesn't, still track it
allMap.set(kid, { account_id: kid, is_env: false, usage: usageObj.neurons || 0 });
}
}
return Array.from(allMap.values()).sort((a, b) => b.usage - a.usage);
},
get cooldownCount() {
return this.accountList.filter(a => a.usage >= 9000).length;
},
get totalNeuronsToday() {
return this.accountList.reduce((sum, a) => sum + a.usage, 0);
},
startMidnightTimer() {
setInterval(() => {
const now = new Date();
const tomorrowUTC = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1, 0, 0, 0));
let diffMs = tomorrowUTC - now;
if (diffMs < 0) diffMs = 0;
const h = Math.floor(diffMs / 3600000);
const m = Math.floor((diffMs % 3600000) / 60000);
const s = Math.floor((diffMs % 60000) / 1000);
this.timeToMidnight = `${h}h ${m}m ${s}s to reset`;
}, 1000);
},
async addAccount() {
if (!this.newAcc.account_id || !this.newAcc.api_key || !this.newAcc.password) {
this.showAlert('Please fill all fields', 'error');
return;
}
try {
const res = await fetch('/api/accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.newAcc)
});
const data = await res.json();
if (res.ok) {
this.showAlert('Account added successfully', 'success');
this.showAddAccount = false;
this.newAcc = { account_id: '', api_key: '', password: '' };
await this.fetchData();
} else {
this.showAlert(data.error || 'Failed to add account', 'error');
}
} catch (e) {
this.showAlert('Network error', 'error');
}
},
async deleteAccount(account_id) {
if (!this.adminPassword) {
this.showAlert('Admin Password required in Settings tab', 'error');
this.tab = 'settings';
return;
}
if (!confirm('Delete this account?')) return;
try {
const res = await fetch(`/api/accounts?account_id=${account_id}&password=${this.adminPassword}`, {
method: 'DELETE'
});
const data = await res.json();
if (res.ok) {
this.showAlert('Account deleted', 'success');
await this.fetchData();
} else {
this.showAlert(data.error || 'Failed to delete account', 'error');
}
} catch (e) {
this.showAlert('Network error', 'error');
}
},
async toggleThinking() {
if (!this.adminPassword) {
this.showAlert('Admin Password required', 'error');
return;
}
const newState = !this.data.settings?.thinking_enabled;
try {
const res = await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
password: this.adminPassword,
settings: { thinking_enabled: newState }
})
});
const data = await res.json();
if (res.ok) {
this.data.settings.thinking_enabled = newState;
this.showAlert(`Thinking mode ${newState ? 'enabled' : 'disabled'}`, 'success');
} else {
this.showAlert(data.error || 'Failed to update setting', 'error');
}
} catch (e) {
this.showAlert('Network error', 'error');
}
},
showAlert(msg, type) {
this.alert = { show: true, message: msg, type: type };
setTimeout(() => this.alert.show = false, 3000);
},
updateChart() {
const ctx = document.getElementById('usageChart');
if (!ctx) return;
const labels = this.accountList.map(a => a.account_id.substring(0,8));
const data = this.accountList.map(a => Math.floor(a.usage));
const bgColors = this.accountList.map(a => a.usage >= 9000 ? 'rgba(239, 68, 68, 0.5)' : 'rgba(59, 130, 246, 0.5)');
const borderColors = this.accountList.map(a => a.usage >= 9000 ? 'rgb(239, 68, 68)' : 'rgb(59, 130, 246)');
if (this.chartInstance) {
this.chartInstance.data.labels = labels;
this.chartInstance.data.datasets[0].data = data;
this.chartInstance.data.datasets[0].backgroundColor = bgColors;
this.chartInstance.data.datasets[0].borderColor = borderColors;
this.chartInstance.update();
} else {
this.chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Neurons Used Today',
data: data,
backgroundColor: bgColors,
borderColor: borderColors,
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, max: 10000 }
}
}
});
}
}
}
}
</script>
</body>
</html>