Go web application for filling USB drives with media files. Runs in Docker on Unraid with /media, /mnt/usb, /config volumes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
138 lines
5.2 KiB
HTML
138 lines
5.2 KiB
HTML
{{define "content"}}
|
||
|
||
<section class="panel">
|
||
<h2>Накопитель</h2>
|
||
<table class="kv-table">
|
||
<tbody>
|
||
<tr>
|
||
<th>Статус</th>
|
||
<td id="diskState"><span class="badge badge-unknown">Не подключён</span></td>
|
||
</tr>
|
||
<tr id="rowDiskID" class="hidden">
|
||
<th>ID диска</th>
|
||
<td><span class="mono" id="valDiskID"></span></td>
|
||
</tr>
|
||
<tr id="rowTotal" class="hidden">
|
||
<th>Всего на диске</th>
|
||
<td id="valTotal"></td>
|
||
</tr>
|
||
<tr id="rowFree" class="hidden">
|
||
<th>Свободно</th>
|
||
<td id="valFree"></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<section class="panel hidden" id="progressPanel">
|
||
<h2>Копирование</h2>
|
||
<div style="padding:14px 16px">
|
||
<div class="progress-bar-bg">
|
||
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
|
||
</div>
|
||
<div class="progress-label" id="progressMsg">Подготовка…</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="btn-row" style="background:transparent;border:none;padding:0;margin-bottom:24px">
|
||
<button class="button-primary" id="btnStart" onclick="startCopy()" disabled>▶ Запустить копирование</button>
|
||
<button class="button-danger hidden" id="btnCancel" onclick="cancelCopy()">✕ Отменить</button>
|
||
</div>
|
||
|
||
<script>
|
||
let pollInterval = null;
|
||
let activeTaskId = null;
|
||
|
||
async function refreshDisk() {
|
||
try {
|
||
const r = await fetch('/api/disk');
|
||
if (!r.ok) return;
|
||
const d = await r.json();
|
||
|
||
const labels = { absent: 'Не подключён', foreign: 'Незнакомый диск', known: 'Диск подключён' };
|
||
const cls = { absent: 'badge-unknown', foreign: 'badge-warn', known: 'badge-ok' };
|
||
document.getElementById('diskState').innerHTML =
|
||
`<span class="badge ${cls[d.state]||'badge-unknown'}">${labels[d.state]||'—'}</span>`;
|
||
|
||
const known = d.state === 'known';
|
||
['rowDiskID','rowTotal','rowFree'].forEach(id =>
|
||
document.getElementById(id).classList.toggle('hidden', !known));
|
||
if (known) {
|
||
document.getElementById('valDiskID').textContent = d.disk_id;
|
||
document.getElementById('valTotal').textContent = fmtBytes(d.total_bytes);
|
||
document.getElementById('valFree').textContent = fmtBytes(d.free_bytes);
|
||
}
|
||
|
||
const hasTask = !!d.active_task_id;
|
||
document.getElementById('btnStart').disabled = !known || hasTask;
|
||
document.getElementById('btnStart').classList.toggle('hidden', hasTask);
|
||
document.getElementById('btnCancel').classList.toggle('hidden', !hasTask);
|
||
document.getElementById('progressPanel').classList.toggle('hidden', !hasTask);
|
||
|
||
if (d.active_task_id && d.active_task_id !== activeTaskId) {
|
||
activeTaskId = d.active_task_id;
|
||
startTaskPoll(activeTaskId);
|
||
}
|
||
if (!d.active_task_id && activeTaskId) {
|
||
activeTaskId = null; stopTaskPoll();
|
||
document.getElementById('progressPanel').classList.add('hidden');
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function startTaskPoll(id) { stopTaskPoll(); pollInterval = setInterval(() => pollTask(id), 1500); }
|
||
function stopTaskPoll() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }
|
||
|
||
async function pollTask(id) {
|
||
try {
|
||
const r = await fetch('/api/tasks/' + id);
|
||
if (!r.ok) return;
|
||
const t = await r.json();
|
||
document.getElementById('progressFill').style.width = t.progress + '%';
|
||
document.getElementById('progressMsg').textContent = t.message || '…';
|
||
if (['success','failed','canceled'].includes(t.status)) {
|
||
stopTaskPoll(); activeTaskId = null;
|
||
document.getElementById('btnStart').disabled = false;
|
||
document.getElementById('btnStart').classList.remove('hidden');
|
||
document.getElementById('btnCancel').classList.add('hidden');
|
||
document.getElementById('progressPanel').classList.add('hidden');
|
||
if (t.status === 'success') toast(t.message || 'Готово', 'ok');
|
||
if (t.status === 'failed') toast('Ошибка: ' + t.error, 'error');
|
||
if (t.status === 'canceled') toast('Копирование отменено', 'error');
|
||
refreshDisk();
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
async function startCopy() {
|
||
document.getElementById('btnStart').disabled = true;
|
||
try {
|
||
const r = await fetch('/api/copy/start', { method: 'POST' });
|
||
const d = await r.json();
|
||
if (!r.ok) {
|
||
toast(d.error || 'Ошибка запуска', 'error');
|
||
document.getElementById('btnStart').disabled = false;
|
||
return;
|
||
}
|
||
activeTaskId = d.task_id;
|
||
document.getElementById('btnStart').classList.add('hidden');
|
||
document.getElementById('btnCancel').classList.remove('hidden');
|
||
document.getElementById('progressPanel').classList.remove('hidden');
|
||
document.getElementById('progressFill').style.width = '0%';
|
||
document.getElementById('progressMsg').textContent = 'Подготовка…';
|
||
startTaskPoll(activeTaskId);
|
||
} catch(e) {
|
||
toast('Ошибка связи', 'error');
|
||
document.getElementById('btnStart').disabled = false;
|
||
}
|
||
}
|
||
|
||
async function cancelCopy() {
|
||
try { await fetch('/api/copy/cancel', { method: 'POST' }); toast('Отмена…', 'ok'); } catch(e) {}
|
||
}
|
||
|
||
refreshDisk();
|
||
setInterval(refreshDisk, 5000);
|
||
</script>
|
||
{{end}}
|