v0.2.0
This commit is contained in:
214
src/static/index.html
Normal file
214
src/static/index.html
Normal file
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>AE Anons Control Panel</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||
.container { max-width: 1400px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
||||
h1 { color: #333; margin-top: 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
||||
th, td { border: 1px solid #ddd; padding: 10px; text-align: left; }
|
||||
th { background-color: #4CAF50; color: white; }
|
||||
tr:nth-child(even) { background-color: #f9f9f9; }
|
||||
tr:hover { background-color: #f1f1f1; }
|
||||
.state-finished { color: #4CAF50; font-weight: bold; }
|
||||
.state-error { color: #f44336; font-weight: bold; }
|
||||
.state-started { color: #2196F3; font-weight: bold; }
|
||||
.state-queued { color: #ff9800; font-weight: bold; }
|
||||
button {
|
||||
margin: 5px;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.btn-primary { background: #4CAF50; color: white; }
|
||||
.btn-primary:hover { background: #45a049; }
|
||||
.btn-secondary { background: #2196F3; color: white; }
|
||||
.btn-secondary:hover { background: #0b7dda; }
|
||||
.btn-danger { background: #f44336; color: white; }
|
||||
.btn-danger:hover { background: #da190b; }
|
||||
.status { margin-bottom: 20px; padding: 10px; background: #e7f3fe; border-left: 4px solid #2196F3; }
|
||||
.status span { margin-left: 20px; color: #666; }
|
||||
.filter-bar { margin: 10px 0; }
|
||||
.filter-bar input { padding: 8px; width: 300px; border: 1px solid #ddd; border-radius: 4px; }
|
||||
.stats { margin: 10px 0; font-size: 14px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🎬 AE Anons - Nexrender Job Manager</h1>
|
||||
|
||||
<div class="status">
|
||||
<button class="btn-primary" onclick="generateJobs()">🔄 Generate New Jobs</button>
|
||||
<button class="btn-danger" onclick="cleanupJobs()">🧹 Cleanup Finished Jobs</button>
|
||||
<button class="btn-secondary" onclick="refreshJobs()">↻ Refresh</button>
|
||||
<span id="statusMessage"></span>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="filterInput" placeholder="🔍 Filter by output file name..." onkeyup="filterTable()">
|
||||
</div>
|
||||
|
||||
<div class="stats" id="stats"></div>
|
||||
|
||||
<table id="jobsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Output File</th>
|
||||
<th>State</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>UID</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobsTableBody">
|
||||
<tr><td colspan="5" style="text-align: center;">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let allJobs = [];
|
||||
|
||||
async function refreshJobs() {
|
||||
try {
|
||||
setStatus('Loading jobs...');
|
||||
const response = await fetch('/api/jobs');
|
||||
allJobs = await response.json();
|
||||
renderJobs(allJobs);
|
||||
updateStats();
|
||||
setStatus(`Loaded ${allJobs.length} jobs`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStatus('Error loading jobs: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderJobs(jobs) {
|
||||
const tbody = document.getElementById('jobsTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (jobs.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" style="text-align: center;">No jobs found</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
jobs.sort((a, b) => {
|
||||
// Сортируем по дате создания (новые сверху)
|
||||
const dateA = a.created_at ? new Date(a.created_at) : new Date(0);
|
||||
const dateB = b.created_at ? new Date(b.created_at) : new Date(0);
|
||||
return dateB - dateA;
|
||||
});
|
||||
|
||||
jobs.forEach(job => {
|
||||
const row = tbody.insertRow();
|
||||
|
||||
// Output file (выделяем жирным)
|
||||
const fileCell = row.insertCell();
|
||||
fileCell.textContent = job.outfile_name;
|
||||
fileCell.style.fontWeight = 'bold';
|
||||
|
||||
// State с цветовой кодировкой
|
||||
const stateCell = row.insertCell();
|
||||
stateCell.textContent = job.state;
|
||||
stateCell.className = `state-${job.state}`;
|
||||
|
||||
// Форматируем даты
|
||||
row.insertCell().textContent = formatDate(job.created_at);
|
||||
row.insertCell().textContent = formatDate(job.updated_at);
|
||||
|
||||
// UID (обрезаем для компактности)
|
||||
const uidCell = row.insertCell();
|
||||
uidCell.textContent = job.uid.substring(0, 8) + '...';
|
||||
uidCell.title = job.uid; // полный UID при наведении
|
||||
});
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('ru-RU', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
function filterTable() {
|
||||
const filter = document.getElementById('filterInput').value.toLowerCase();
|
||||
const filtered = allJobs.filter(job =>
|
||||
job.outfile_name.toLowerCase().includes(filter) ||
|
||||
job.uid.toLowerCase().includes(filter)
|
||||
);
|
||||
renderJobs(filtered);
|
||||
updateStats(filtered.length);
|
||||
}
|
||||
|
||||
function updateStats(filteredCount = null) {
|
||||
const total = allJobs.length;
|
||||
const shown = filteredCount !== null ? filteredCount : total;
|
||||
const states = allJobs.reduce((acc, job) => {
|
||||
acc[job.state] = (acc[job.state] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const statsDiv = document.getElementById('stats');
|
||||
const stateText = Object.entries(states)
|
||||
.map(([state, count]) => `${state}: ${count}`)
|
||||
.join(' | ');
|
||||
|
||||
statsDiv.innerHTML = `Total: ${total} jobs${filteredCount !== null ? ` | Showing: ${shown}` : ''} | ${stateText}`;
|
||||
}
|
||||
|
||||
async function generateJobs() {
|
||||
setStatus('⏳ Generating jobs...');
|
||||
try {
|
||||
const response = await fetch('/api/generate', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
setStatus('✅ Job generation started. Will refresh in 10 seconds...');
|
||||
setTimeout(() => refreshJobs(), 10000);
|
||||
} else {
|
||||
const text = await response.text();
|
||||
setStatus('❌ Error: ' + text);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('❌ Error: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanupJobs() {
|
||||
setStatus('🧹 Cleaning up finished jobs...');
|
||||
try {
|
||||
const response = await fetch('/api/cleanup', { method: 'POST' });
|
||||
if (response.ok) {
|
||||
setStatus('✅ Cleanup completed. Refreshing...');
|
||||
await refreshJobs();
|
||||
} else {
|
||||
const text = await response.text();
|
||||
setStatus('❌ Error: ' + text);
|
||||
}
|
||||
} catch (err) {
|
||||
setStatus('❌ Error: ' + err);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
document.getElementById('statusMessage').textContent = msg;
|
||||
}
|
||||
|
||||
// Initial load
|
||||
refreshJobs();
|
||||
// Auto-refresh every 10 seconds
|
||||
setInterval(refreshJobs, 10000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user