Spaces:
Runtime error
Runtime error
| <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 ; } | |
| .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">×</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> |