This commit is contained in:
2026-04-17 11:58:16 +03:00
parent 4f1c6d1ab7
commit 8721353b8d
6 changed files with 1093 additions and 463 deletions

214
src/static/index.html Normal file
View 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>