feat: v1.1.5 — UPID task tracking, Task Drawer clear button, auto-expire, pve CLI fix

This commit is contained in:
markmorado
2026-03-15 20:34:17 +01:00
parent 284c3dfce0
commit 5a67981f9a
11 changed files with 241 additions and 95 deletions

View File

@@ -774,9 +774,14 @@ body.update-in-progress .layout {
<div id="taskDrawer" class="task-drawer" style="display:none;">
<div class="task-drawer-header">
<h3><i class="fa-solid fa-list-check" style="color:var(--primary); margin-right:.5rem;"></i> Задачи</h3>
<button class="task-drawer-close" onclick="toggleTaskDrawer()">
<i class="fa-solid fa-xmark"></i>
</button>
<div style="display:flex; gap:.4rem; align-items:center; margin-left:auto;">
<button id="taskDrawerClearBtn" onclick="clearCompletedTasks()" title="Очистить завершённые" style="background:none; border:none; color:var(--text-muted); cursor:pointer; padding:.3rem .5rem; border-radius:6px; font-size:.75rem; display:none;">
<i class="fa-solid fa-trash-can"></i>
</button>
<button class="task-drawer-close" onclick="toggleTaskDrawer()">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
</div>
<div class="task-drawer-body" id="taskDrawerBody">
<div class="task-drawer-empty" id="taskDrawerEmpty">
@@ -1634,6 +1639,13 @@ body.update-in-progress .layout {
let taskDrawerReconnectTimer = null;
let taskDrawerReconnectDelay = 2000;
// Task Drawer: задачи старше 24 ч скрываются из панели (в /tasks они остаются)
const DRAWER_TASK_TTL = 24 * 60 * 60 * 1000;
function isDrawerTaskExpired(t) {
if (!t || !t.created_at) return false;
return (Date.now() - new Date(t.created_at).getTime()) > DRAWER_TASK_TTL;
}
function toggleTaskDrawer() {
const drawer = document.getElementById('taskDrawer');
taskDrawerOpen = !taskDrawerOpen;
@@ -1725,9 +1737,13 @@ body.update-in-progress .layout {
const msg = JSON.parse(data);
if (msg.type === 'task_list') {
taskDrawerTasks = {};
(msg.tasks||[]).forEach(t => { taskDrawerTasks[`${t.kind}_${t.id}`] = t; });
(msg.tasks||[]).forEach(t => {
if (!isDrawerTaskExpired(t))
taskDrawerTasks[`${t.kind}_${t.id}`] = t;
});
} else if (msg.type === 'task_update' || msg.type === 'task_created') {
if (msg.task) taskDrawerTasks[`${msg.task.kind}_${msg.task.id}`] = msg.task;
if (msg.task && !isDrawerTaskExpired(msg.task))
taskDrawerTasks[`${msg.task.kind}_${msg.task.id}`] = msg.task;
} else if (msg.type === 'task_removed') {
if (msg.task) delete taskDrawerTasks[`${msg.task.kind}_${msg.task.id}`];
}
@@ -1761,6 +1777,21 @@ body.update-in-progress .layout {
};
}
// Периодически удаляем задачи старше 24 ч из Drawer (каждые 5 минут)
setInterval(() => {
let changed = false;
for (const key of Object.keys(taskDrawerTasks)) {
if (isDrawerTaskExpired(taskDrawerTasks[key])) {
delete taskDrawerTasks[key];
changed = true;
}
}
if (changed) {
updateTaskBadge();
if (taskDrawerOpen) renderTaskDrawer();
}
}, 5 * 60 * 1000);
// ── WebSocket utility API (usable from any page script) ──────────────────
/** Registry of per-message-type handlers. */
@@ -1843,6 +1874,59 @@ body.update-in-progress .layout {
taskDrawerWS.send(JSON.stringify({type: 'request', action, request_id, params}));
});
}
// ── Task Drawer utilities ─────────────────────────────────────────────────
/**
* Открыть Task Drawer и подсветить задачу (используется внешними страницами).
* Вызывается при запуске bulk-операций вместо нижнего тоста.
*/
function openTaskDrawer() {
if (!taskDrawerOpen) {
taskDrawerOpen = true;
document.getElementById('taskDrawer').style.display = 'flex';
renderTaskDrawer();
}
}
/** Обновить видимость кнопки очистки в зависимости от состава задач. */
function _updateClearButton() {
const hasDone = Object.values(taskDrawerTasks).some(t => {
const s = normalizeTaskStatus(t.status);
return s === 'completed' || s === 'failed' || s === 'cancelled';
});
const btn = document.getElementById('taskDrawerClearBtn');
if (btn) btn.style.display = hasDone ? 'flex' : 'none';
}
/** Оригинальная renderTaskDrawer расширяется — вызываем _updateClearButton после. */
const _origRenderTaskDrawer = renderTaskDrawer;
renderTaskDrawer = function() {
_origRenderTaskDrawer();
_updateClearButton();
};
/** Удалить все завершённые/ошибочные/отменённые задачи. */
async function clearCompletedTasks() {
try {
const resp = await fetchWithAuth('/proxmox/api/all-tasks/completed', { method: 'DELETE' });
if (resp && resp.ok) {
const data = await resp.json();
// Remove done tasks from local state
for (const key of Object.keys(taskDrawerTasks)) {
const s = normalizeTaskStatus(taskDrawerTasks[key].status);
if (s === 'completed' || s === 'failed' || s === 'cancelled') {
delete taskDrawerTasks[key];
}
}
renderTaskDrawer();
updateTaskBadge();
if (data.deleted > 0) showToast('success', 'Очищено', `Удалено ${data.deleted} задач`);
}
} catch(e) {
console.error('clearCompletedTasks error', e);
}
}
</script>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>

View File

@@ -1177,7 +1177,8 @@ async function bulkAction(action) {
updateVMStatusUI(item.server_id, item.vmid, 'pending', action);
});
// Show progress toast
// Открыть Task Drawer вместо нижнего тоста
if (typeof openTaskDrawer === 'function') openTaskDrawer();
showTaskProgress(action, items.length);
// Start polling for progress
@@ -1193,27 +1194,13 @@ async function bulkAction(action) {
}
function showTaskProgress(action, totalItems) {
const toast = document.getElementById('taskProgressToast');
const title = document.getElementById('taskProgressTitle');
// Тост заменён на Task Drawer — просто сбрасываем состояние элементов без показа тоста
const bar = document.getElementById('taskProgressBar');
const text = document.getElementById('taskProgressText');
const results = document.getElementById('taskProgressResults');
const actionNames = {
start: '{{ t("starting") }}',
stop: '{{ t("stopping") }}',
restart: '{{ t("restarting") }}',
shutdown: '{{ t("shutting_down") }}',
delete: '{{ t("deleting") }}'
};
title.textContent = `${actionNames[action] || action}...`;
bar.style.width = '0%';
bar.className = 'task-progress-bar';
text.textContent = `0 / ${totalItems}`;
results.innerHTML = '';
toast.classList.remove('hidden');
if (bar) { bar.style.width = '0%'; bar.className = 'task-progress-bar'; }
if (text) text.textContent = `0 / ${totalItems}`;
if (results) results.innerHTML = '';
}
// Track which VMs have already been updated in UI
@@ -1312,7 +1299,7 @@ function updateTaskProgress(task) {
}
function hideTaskProgress() {
document.getElementById('taskProgressToast').classList.add('hidden');
// Toast заменён на Task Drawer — просто останавливаем поллинг
stopTaskPolling();
}