|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Enhanced Crypto Data Tracker</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1600px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
header { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 15px; |
|
|
padding: 20px 30px; |
|
|
margin-bottom: 20px; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
color: white; |
|
|
font-size: 28px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.connection-status { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
color: white; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.status-indicator { |
|
|
width: 10px; |
|
|
height: 10px; |
|
|
border-radius: 50%; |
|
|
background: #10b981; |
|
|
animation: pulse 2s infinite; |
|
|
} |
|
|
|
|
|
.status-indicator.disconnected { |
|
|
background: #ef4444; |
|
|
animation: none; |
|
|
} |
|
|
|
|
|
@keyframes pulse { |
|
|
0%, 100% { opacity: 1; } |
|
|
50% { opacity: 0.5; } |
|
|
} |
|
|
|
|
|
.controls { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.controls-row { |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
flex-wrap: wrap; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: 10px 20px; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
cursor: pointer; |
|
|
font-size: 14px; |
|
|
font-weight: 600; |
|
|
transition: all 0.3s ease; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: white; |
|
|
color: #667eea; |
|
|
} |
|
|
|
|
|
.btn-primary:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
.btn-success { |
|
|
background: #10b981; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-danger { |
|
|
background: #ef4444; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-info { |
|
|
background: #3b82f6; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn:disabled { |
|
|
opacity: 0.5; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); |
|
|
gap: 20px; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.card { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
backdrop-filter: blur(10px); |
|
|
border-radius: 15px; |
|
|
padding: 20px; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.card h2 { |
|
|
font-size: 18px; |
|
|
margin-bottom: 15px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.stat-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(2, 1fr); |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.stat-item { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
padding: 15px; |
|
|
border-radius: 10px; |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
font-size: 12px; |
|
|
opacity: 0.8; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.stat-value { |
|
|
font-size: 24px; |
|
|
font-weight: 700; |
|
|
} |
|
|
|
|
|
.api-list { |
|
|
max-height: 400px; |
|
|
overflow-y: auto; |
|
|
} |
|
|
|
|
|
.api-item { |
|
|
background: rgba(255, 255, 255, 0.05); |
|
|
padding: 15px; |
|
|
border-radius: 10px; |
|
|
margin-bottom: 10px; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.api-info { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.api-name { |
|
|
font-weight: 600; |
|
|
margin-bottom: 5px; |
|
|
} |
|
|
|
|
|
.api-meta { |
|
|
font-size: 12px; |
|
|
opacity: 0.7; |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.api-controls { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.small-btn { |
|
|
padding: 5px 12px; |
|
|
font-size: 12px; |
|
|
border-radius: 5px; |
|
|
border: none; |
|
|
cursor: pointer; |
|
|
background: rgba(255, 255, 255, 0.2); |
|
|
color: white; |
|
|
transition: all 0.2s; |
|
|
} |
|
|
|
|
|
.small-btn:hover { |
|
|
background: rgba(255, 255, 255, 0.3); |
|
|
} |
|
|
|
|
|
.status-badge { |
|
|
display: inline-block; |
|
|
padding: 4px 10px; |
|
|
border-radius: 12px; |
|
|
font-size: 11px; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.status-success { |
|
|
background: #10b981; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.status-pending { |
|
|
background: #f59e0b; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.status-failed { |
|
|
background: #ef4444; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.log-container { |
|
|
background: rgba(0, 0, 0, 0.3); |
|
|
border-radius: 10px; |
|
|
padding: 15px; |
|
|
max-height: 300px; |
|
|
overflow-y: auto; |
|
|
font-family: 'Courier New', monospace; |
|
|
font-size: 12px; |
|
|
} |
|
|
|
|
|
.log-entry { |
|
|
margin-bottom: 8px; |
|
|
padding: 5px; |
|
|
border-left: 3px solid #667eea; |
|
|
padding-left: 10px; |
|
|
} |
|
|
|
|
|
.log-time { |
|
|
opacity: 0.6; |
|
|
margin-right: 10px; |
|
|
} |
|
|
|
|
|
.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.active { |
|
|
display: flex; |
|
|
} |
|
|
|
|
|
.modal-content { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 30px; |
|
|
max-width: 500px; |
|
|
width: 90%; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.modal-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.modal-close { |
|
|
background: none; |
|
|
border: none; |
|
|
font-size: 24px; |
|
|
cursor: pointer; |
|
|
color: #666; |
|
|
} |
|
|
|
|
|
.form-group { |
|
|
margin-bottom: 15px; |
|
|
} |
|
|
|
|
|
.form-label { |
|
|
display: block; |
|
|
margin-bottom: 5px; |
|
|
font-weight: 600; |
|
|
color: #333; |
|
|
} |
|
|
|
|
|
.form-input { |
|
|
width: 100%; |
|
|
padding: 10px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 8px; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
.form-select { |
|
|
width: 100%; |
|
|
padding: 10px; |
|
|
border: 1px solid #ddd; |
|
|
border-radius: 8px; |
|
|
font-size: 14px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-track { |
|
|
background: rgba(255, 255, 255, 0.1); |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb { |
|
|
background: rgba(255, 255, 255, 0.3); |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
|
background: rgba(255, 255, 255, 0.5); |
|
|
} |
|
|
|
|
|
.toast { |
|
|
position: fixed; |
|
|
bottom: 20px; |
|
|
right: 20px; |
|
|
background: white; |
|
|
color: #333; |
|
|
padding: 15px 20px; |
|
|
border-radius: 10px; |
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|
|
opacity: 0; |
|
|
transform: translateY(20px); |
|
|
transition: all 0.3s ease; |
|
|
z-index: 2000; |
|
|
} |
|
|
|
|
|
.toast.show { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<header> |
|
|
<h1> |
|
|
<span>π</span> |
|
|
Enhanced Crypto Data Tracker |
|
|
</h1> |
|
|
<div class="connection-status"> |
|
|
<div class="status-indicator" id="wsStatus"></div> |
|
|
<span id="wsStatusText">Connecting...</span> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
<div class="controls"> |
|
|
<div class="controls-row"> |
|
|
<button class="btn btn-primary" onclick="exportJSON()"> |
|
|
πΎ Export JSON |
|
|
</button> |
|
|
<button class="btn btn-primary" onclick="exportCSV()"> |
|
|
π Export CSV |
|
|
</button> |
|
|
<button class="btn btn-success" onclick="createBackup()"> |
|
|
π Create Backup |
|
|
</button> |
|
|
<button class="btn btn-info" onclick="showScheduleModal()"> |
|
|
β° Configure Schedule |
|
|
</button> |
|
|
<button class="btn btn-info" onclick="forceUpdateAll()"> |
|
|
π Force Update All |
|
|
</button> |
|
|
<button class="btn btn-danger" onclick="clearCache()"> |
|
|
ποΈ Clear Cache |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="grid"> |
|
|
<div class="card"> |
|
|
<h2>π System Statistics</h2> |
|
|
<div class="stat-grid"> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-label">Total APIs</div> |
|
|
<div class="stat-value" id="totalApis">0</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-label">Active Tasks</div> |
|
|
<div class="stat-value" id="activeTasks">0</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-label">Cached Data</div> |
|
|
<div class="stat-value" id="cachedData">0</div> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<div class="stat-label">WS Connections</div> |
|
|
<div class="stat-value" id="wsConnections">0</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="card"> |
|
|
<h2>π Recent Activity</h2> |
|
|
<div class="log-container" id="activityLog"> |
|
|
<div class="log-entry"> |
|
|
<span class="log-time">--:--:--</span> |
|
|
Waiting for updates... |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="card"> |
|
|
<h2>π API Sources</h2> |
|
|
<div class="api-list" id="apiList"> |
|
|
Loading... |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="modal" id="scheduleModal"> |
|
|
<div class="modal-content"> |
|
|
<div class="modal-header"> |
|
|
<h2>β° Configure Schedule</h2> |
|
|
<button class="modal-close" onclick="closeScheduleModal()">Γ</button> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">API Source</label> |
|
|
<select class="form-select" id="scheduleApiSelect"></select> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">Interval (seconds)</label> |
|
|
<input type="number" class="form-input" id="scheduleInterval" value="60" min="10"> |
|
|
</div> |
|
|
<div class="form-group"> |
|
|
<label class="form-label">Enabled</label> |
|
|
<input type="checkbox" id="scheduleEnabled" checked> |
|
|
</div> |
|
|
<button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="toast" id="toast"></div> |
|
|
|
|
|
<script> |
|
|
let ws = null; |
|
|
let reconnectAttempts = 0; |
|
|
const maxReconnectAttempts = 5; |
|
|
const reconnectDelay = 3000; |
|
|
|
|
|
|
|
|
function initWebSocket() { |
|
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
|
|
const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`; |
|
|
|
|
|
ws = new WebSocket(wsUrl); |
|
|
|
|
|
ws.onopen = () => { |
|
|
console.log('WebSocket connected'); |
|
|
updateWSStatus(true); |
|
|
reconnectAttempts = 0; |
|
|
|
|
|
|
|
|
ws.send(JSON.stringify({ type: 'subscribe_all' })); |
|
|
|
|
|
|
|
|
startHeartbeat(); |
|
|
}; |
|
|
|
|
|
ws.onmessage = (event) => { |
|
|
const message = JSON.parse(event.data); |
|
|
handleWSMessage(message); |
|
|
}; |
|
|
|
|
|
ws.onerror = (error) => { |
|
|
console.error('WebSocket error:', error); |
|
|
}; |
|
|
|
|
|
ws.onclose = () => { |
|
|
console.log('WebSocket disconnected'); |
|
|
updateWSStatus(false); |
|
|
attemptReconnect(); |
|
|
}; |
|
|
} |
|
|
|
|
|
function attemptReconnect() { |
|
|
if (reconnectAttempts < maxReconnectAttempts) { |
|
|
reconnectAttempts++; |
|
|
console.log(`Reconnecting... Attempt ${reconnectAttempts}`); |
|
|
setTimeout(initWebSocket, reconnectDelay); |
|
|
} |
|
|
} |
|
|
|
|
|
let heartbeatInterval; |
|
|
function startHeartbeat() { |
|
|
clearInterval(heartbeatInterval); |
|
|
heartbeatInterval = setInterval(() => { |
|
|
if (ws && ws.readyState === WebSocket.OPEN) { |
|
|
ws.send(JSON.stringify({ type: 'ping' })); |
|
|
} |
|
|
}, 30000); |
|
|
} |
|
|
|
|
|
function updateWSStatus(connected) { |
|
|
const indicator = document.getElementById('wsStatus'); |
|
|
const text = document.getElementById('wsStatusText'); |
|
|
|
|
|
if (connected) { |
|
|
indicator.classList.remove('disconnected'); |
|
|
text.textContent = 'Connected'; |
|
|
} else { |
|
|
indicator.classList.add('disconnected'); |
|
|
text.textContent = 'Disconnected'; |
|
|
} |
|
|
} |
|
|
|
|
|
function handleWSMessage(message) { |
|
|
console.log('Received:', message); |
|
|
|
|
|
switch (message.type) { |
|
|
case 'api_update': |
|
|
handleApiUpdate(message); |
|
|
break; |
|
|
case 'status_update': |
|
|
handleStatusUpdate(message); |
|
|
break; |
|
|
case 'schedule_update': |
|
|
handleScheduleUpdate(message); |
|
|
break; |
|
|
case 'subscribed': |
|
|
addLog(`Subscribed to ${message.api_id || 'all updates'}`); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
function handleApiUpdate(message) { |
|
|
addLog(`Updated: ${message.api_id}`, 'success'); |
|
|
loadSystemStatus(); |
|
|
} |
|
|
|
|
|
function handleStatusUpdate(message) { |
|
|
addLog('System status updated'); |
|
|
loadSystemStatus(); |
|
|
} |
|
|
|
|
|
function handleScheduleUpdate(message) { |
|
|
addLog(`Schedule updated for ${message.schedule.api_id}`); |
|
|
loadAPIs(); |
|
|
} |
|
|
|
|
|
function addLog(text, type = 'info') { |
|
|
const logContainer = document.getElementById('activityLog'); |
|
|
const time = new Date().toLocaleTimeString(); |
|
|
|
|
|
const entry = document.createElement('div'); |
|
|
entry.className = 'log-entry'; |
|
|
entry.innerHTML = `<span class="log-time">${time}</span>${text}`; |
|
|
|
|
|
logContainer.insertBefore(entry, logContainer.firstChild); |
|
|
|
|
|
|
|
|
while (logContainer.children.length > 50) { |
|
|
logContainer.removeChild(logContainer.lastChild); |
|
|
} |
|
|
} |
|
|
|
|
|
function showToast(message, duration = 3000) { |
|
|
const toast = document.getElementById('toast'); |
|
|
toast.textContent = message; |
|
|
toast.classList.add('show'); |
|
|
|
|
|
setTimeout(() => { |
|
|
toast.classList.remove('show'); |
|
|
}, duration); |
|
|
} |
|
|
|
|
|
|
|
|
async function loadSystemStatus() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/status'); |
|
|
const data = await response.json(); |
|
|
|
|
|
document.getElementById('totalApis').textContent = |
|
|
data.services.config_loader.apis_loaded; |
|
|
document.getElementById('activeTasks').textContent = |
|
|
data.services.scheduler.total_tasks; |
|
|
document.getElementById('cachedData').textContent = |
|
|
data.services.persistence.cached_apis; |
|
|
document.getElementById('wsConnections').textContent = |
|
|
data.services.websocket.total_connections; |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error loading status:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function loadAPIs() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/config/apis'); |
|
|
const data = await response.json(); |
|
|
|
|
|
const scheduleResponse = await fetch('/api/v2/schedule/tasks'); |
|
|
const schedules = await scheduleResponse.json(); |
|
|
|
|
|
displayAPIs(data.apis, schedules); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error loading APIs:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
function displayAPIs(apis, schedules) { |
|
|
const listElement = document.getElementById('apiList'); |
|
|
listElement.innerHTML = ''; |
|
|
|
|
|
for (const [apiId, api] of Object.entries(apis)) { |
|
|
const schedule = schedules[apiId] || {}; |
|
|
|
|
|
const item = document.createElement('div'); |
|
|
item.className = 'api-item'; |
|
|
item.innerHTML = ` |
|
|
<div class="api-info"> |
|
|
<div class="api-name">${api.name}</div> |
|
|
<div class="api-meta"> |
|
|
<span>π ${api.category}</span> |
|
|
<span>β±οΈ ${schedule.interval || 300}s</span> |
|
|
<span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}"> |
|
|
${schedule.last_status || 'pending'} |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="api-controls"> |
|
|
<button class="small-btn" onclick="forceUpdate('${apiId}')">π Update</button> |
|
|
<button class="small-btn" onclick="showScheduleModalFor('${apiId}')">βοΈ Schedule</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
listElement.appendChild(item); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function exportJSON() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/export/json', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ include_history: true }) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
showToast('β
JSON export created!'); |
|
|
addLog(`Exported to JSON: ${data.filepath}`); |
|
|
|
|
|
|
|
|
window.open(data.download_url, '_blank'); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Export failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function exportCSV() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/export/csv', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ flatten: true }) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
showToast('β
CSV export created!'); |
|
|
addLog(`Exported to CSV: ${data.filepath}`); |
|
|
|
|
|
|
|
|
window.open(data.download_url, '_blank'); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Export failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function createBackup() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/backup', { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
showToast('β
Backup created!'); |
|
|
addLog(`Backup created: ${data.backup_file}`); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Backup failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function forceUpdate(apiId) { |
|
|
try { |
|
|
const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
showToast(`β
${apiId} updated!`); |
|
|
addLog(`Forced update: ${apiId}`); |
|
|
loadAPIs(); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Update failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function forceUpdateAll() { |
|
|
showToast('π Updating all APIs...'); |
|
|
addLog('Forcing update for all APIs'); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/v2/config/apis'); |
|
|
const data = await response.json(); |
|
|
|
|
|
for (const apiId of Object.keys(data.apis)) { |
|
|
await forceUpdate(apiId); |
|
|
await new Promise(resolve => setTimeout(resolve, 100)); |
|
|
} |
|
|
|
|
|
showToast('β
All APIs updated!'); |
|
|
} catch (error) { |
|
|
showToast('β Update failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function clearCache() { |
|
|
if (!confirm('Clear all cached data?')) return; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/v2/cleanup/cache', { |
|
|
method: 'POST' |
|
|
}); |
|
|
|
|
|
showToast('β
Cache cleared!'); |
|
|
addLog('Cache cleared'); |
|
|
loadSystemStatus(); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Failed to clear cache'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function showScheduleModal() { |
|
|
loadAPISelectOptions(); |
|
|
document.getElementById('scheduleModal').classList.add('active'); |
|
|
} |
|
|
|
|
|
function closeScheduleModal() { |
|
|
document.getElementById('scheduleModal').classList.remove('active'); |
|
|
} |
|
|
|
|
|
async function showScheduleModalFor(apiId) { |
|
|
await loadAPISelectOptions(); |
|
|
document.getElementById('scheduleApiSelect').value = apiId; |
|
|
|
|
|
|
|
|
try { |
|
|
const response = await fetch(`/api/v2/schedule/tasks/${apiId}`); |
|
|
const schedule = await response.json(); |
|
|
|
|
|
document.getElementById('scheduleInterval').value = schedule.interval; |
|
|
document.getElementById('scheduleEnabled').checked = schedule.enabled; |
|
|
|
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
} |
|
|
|
|
|
showScheduleModal(); |
|
|
} |
|
|
|
|
|
async function loadAPISelectOptions() { |
|
|
try { |
|
|
const response = await fetch('/api/v2/config/apis'); |
|
|
const data = await response.json(); |
|
|
|
|
|
const select = document.getElementById('scheduleApiSelect'); |
|
|
select.innerHTML = ''; |
|
|
|
|
|
for (const [apiId, api] of Object.entries(data.apis)) { |
|
|
const option = document.createElement('option'); |
|
|
option.value = apiId; |
|
|
option.textContent = api.name; |
|
|
select.appendChild(option); |
|
|
} |
|
|
|
|
|
} catch (error) { |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function updateSchedule() { |
|
|
const apiId = document.getElementById('scheduleApiSelect').value; |
|
|
const interval = parseInt(document.getElementById('scheduleInterval').value); |
|
|
const enabled = document.getElementById('scheduleEnabled').checked; |
|
|
|
|
|
try { |
|
|
const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, { |
|
|
method: 'PUT' |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
showToast('β
Schedule updated!'); |
|
|
addLog(`Updated schedule for ${apiId}`); |
|
|
closeScheduleModal(); |
|
|
loadAPIs(); |
|
|
|
|
|
} catch (error) { |
|
|
showToast('β Schedule update failed'); |
|
|
console.error(error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
window.addEventListener('load', () => { |
|
|
initWebSocket(); |
|
|
loadSystemStatus(); |
|
|
loadAPIs(); |
|
|
|
|
|
|
|
|
setInterval(loadSystemStatus, 30000); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|