feat: v1.1.5 — UPID task tracking, Task Drawer clear button, auto-expire, pve CLI fix
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -4,6 +4,23 @@ All notable changes to PVEmanager will be documented in this file.
|
||||
|
||||
---
|
||||
|
||||
## [v1.1.5] - 2026-03-15
|
||||
|
||||
### 🗂️ Task Drawer — UPID Tracking & Clear Completed
|
||||
|
||||
- **UPID-based task tracking** — VM/LXC control methods (`start`, `stop`, `restart`) in `proxmox_client.py` now return the Proxmox task UPID instead of a boolean; each action automatically registers a `ProxmoxTask` entry for real-time Proxmox-side progress polling
|
||||
- **`DELETE /proxmox/api/all-tasks/completed`** — new API endpoint: removes all `completed` / `failed` / `cancelled` tasks (both `ProxmoxTask` and `TaskQueue`) for the current user in one request
|
||||
- **Clear button in Task Drawer** — trash icon appears in the drawer header whenever there are finished tasks; clicking it calls the new endpoint and updates the drawer instantly
|
||||
- **Auto-expire old tasks in Drawer** — tasks older than 24 h are hidden from the Task Drawer on load and every 5 minutes; they remain accessible on the `/tasks` page
|
||||
- **`openTaskDrawer()` global helper** — bulk VM operations now open the Task Drawer instead of showing a bottom progress toast, providing a unified task-tracking experience
|
||||
- **Human-readable bulk task descriptions** — `TaskQueue.to_dict()` now maps internal `task_type` keys (`bulk_start`, `bulk_stop`, etc.) to localised Russian descriptions
|
||||
|
||||
### 🔧 Fixes
|
||||
|
||||
- **`pve` CLI nginx detection** — mode resolution now checks for the nginx config file (`nginx/conf.d/serverpanel.conf`) in addition to the running `serverpanel-nginx` container, fixing prod-mode detection on already-deployed hosts after container restarts
|
||||
|
||||
---
|
||||
|
||||
## [v1.1.4] - 2026-03-15
|
||||
|
||||
### 🗂️ Workspaces — Scoped Server Groups
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
**Modern web panel for managing Proxmox servers, virtual machines and LXC containers**
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](LICENSE)
|
||||
[](compose.yml)
|
||||
[](backend/)
|
||||
|
||||
25
WIKI.md
25
WIKI.md
@@ -1,6 +1,6 @@
|
||||
# 📖 PVEmanager - Documentation
|
||||
|
||||
> Complete guide for installation, configuration and usage of PVEmanager v1.1.4
|
||||
> Complete guide for installation, configuration and usage of PVEmanager v1.1.5
|
||||
|
||||
---
|
||||
|
||||
@@ -255,6 +255,7 @@ This section summarizes key API endpoints. All require JWT authentication.
|
||||
### Misc
|
||||
|
||||
- `POST /api/sync-vms` — force immediate VM sync from all Proxmox servers
|
||||
- `DELETE /proxmox/api/all-tasks/completed` — delete all completed/failed/cancelled tasks for the current user (both ProxmoxTask and TaskQueue)
|
||||
|
||||
---
|
||||
|
||||
@@ -511,24 +512,18 @@ Bulk operations allow you to perform the same action on multiple VMs or containe
|
||||
4. **Click desired action** button
|
||||
5. **Confirm** if prompted (for delete operations)
|
||||
|
||||
### Task Queue
|
||||
### Task Drawer
|
||||
|
||||
When you initiate a bulk operation:
|
||||
When you initiate a bulk operation the **Task Drawer** opens automatically in the sidebar. It shows real-time progress for each task:
|
||||
|
||||
1. **Task is created** and added to the queue
|
||||
2. **Background processing** starts automatically
|
||||
3. **Progress is tracked** (completed/failed items)
|
||||
4. **Results are saved** for each item
|
||||
- **Running** tasks show a spinner and current progress (`completed / total`)
|
||||
- **Completed** tasks are shown in green; **Failed** in red
|
||||
- Tasks older than **24 hours** are automatically hidden from the drawer (they remain accessible on the `/tasks` page)
|
||||
- Use the **trash icon** (🗑) in the drawer header to remove all finished/failed/cancelled tasks in one click
|
||||
|
||||
### Viewing Task Status
|
||||
### Proxmox UPID Tracking
|
||||
|
||||
Currently, tasks can be viewed via API:
|
||||
|
||||
```bash
|
||||
# Get your recent tasks
|
||||
curl -X GET "http://localhost:8000/api/tasks" \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
Starting from v1.1.5, individual VM/LXC control actions (start / stop / restart) are linked to the real Proxmox task UPID. The UPID is registered as a `ProxmoxTask` entry and polled in real time, so the Task Drawer reflects the actual Proxmox-side task state.
|
||||
|
||||
### Limitations
|
||||
|
||||
|
||||
@@ -279,3 +279,22 @@ def get_proxmox_task_log_db(
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
return {"log_text": task.log_text or "", "status": task.status, "exit_status": task.exit_status}
|
||||
|
||||
|
||||
@router.delete("/api/all-tasks/completed")
|
||||
def clear_completed_tasks(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Delete all completed/failed/cancelled tasks for the current user."""
|
||||
done_statuses = ["completed", "failed", "cancelled"]
|
||||
bulk_deleted = db.query(TaskQueue).filter(
|
||||
TaskQueue.user_id == current_user.id,
|
||||
TaskQueue.status.in_(done_statuses),
|
||||
).delete(synchronize_session=False)
|
||||
proxmox_deleted = db.query(ProxmoxTask).filter(
|
||||
ProxmoxTask.user_id == current_user.id,
|
||||
ProxmoxTask.status.in_(done_statuses),
|
||||
).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return {"deleted": bulk_deleted + proxmox_deleted}
|
||||
|
||||
|
||||
@@ -339,18 +339,38 @@ def control_vm(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
upid = None
|
||||
if action == 'start':
|
||||
success = client.start_vm(node, vmid)
|
||||
upid = client.start_vm(node, vmid)
|
||||
success = bool(upid)
|
||||
elif action == 'stop':
|
||||
if force:
|
||||
success = client.force_stop_vm(node, vmid)
|
||||
else:
|
||||
success = client.stop_vm(node, vmid, force=False)
|
||||
upid = client.stop_vm(node, vmid, force=False)
|
||||
success = bool(upid)
|
||||
else: # restart
|
||||
success = client.restart_vm(node, vmid)
|
||||
|
||||
upid = client.restart_vm(node, vmid)
|
||||
success = bool(upid)
|
||||
|
||||
action_name = 'kill' if action == 'stop' and force else action
|
||||
if success:
|
||||
# Register ProxmoxTask for UPID-based tracking
|
||||
if upid:
|
||||
_desc_map = {
|
||||
'start': f"Запуск VM {vm_name or vmid}",
|
||||
'stop': f"Остановка VM {vm_name or vmid}",
|
||||
'restart': f"Перезапуск VM {vm_name or vmid}",
|
||||
}
|
||||
try:
|
||||
ProxmoxTaskService.register(
|
||||
db=db, upid=upid, server_id=server_id,
|
||||
user_id=current_user.id, action=action_name,
|
||||
node=node, vmid=vmid, vm_type='qemu',
|
||||
description=_desc_map.get(action, action_name),
|
||||
)
|
||||
except Exception as _te:
|
||||
logger.warning(f"Failed to register ProxmoxTask for VM {vmid} {action_name}: {_te}")
|
||||
# Log successful action
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
@@ -366,7 +386,7 @@ def control_vm(
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} executed {action_name} on VM {vmid} at {server.name}")
|
||||
return JSONResponse(content={"status": "success", "action": action_name, "vmid": vmid, "node": node})
|
||||
return JSONResponse(content={"status": "success", "action": action_name, "vmid": vmid, "node": node, "upid": upid})
|
||||
else:
|
||||
# Log failed action
|
||||
LoggingService.log_proxmox_action(
|
||||
@@ -1163,20 +1183,40 @@ def control_container(
|
||||
if not client.is_connected():
|
||||
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
|
||||
|
||||
upid = None
|
||||
if action == 'start':
|
||||
success = client.start_container(node, vmid)
|
||||
upid = client.start_container(node, vmid)
|
||||
success = bool(upid)
|
||||
elif action == 'stop':
|
||||
if force:
|
||||
success = client.force_stop_container(node, vmid)
|
||||
else:
|
||||
success = client.stop_container(node, vmid, force=False)
|
||||
upid = client.stop_container(node, vmid, force=False)
|
||||
success = bool(upid)
|
||||
else: # restart
|
||||
success = client.restart_container(node, vmid)
|
||||
|
||||
upid = client.restart_container(node, vmid)
|
||||
success = bool(upid)
|
||||
|
||||
action_name = 'kill' if action == 'stop' and force else action
|
||||
if success:
|
||||
# Register ProxmoxTask for UPID-based tracking
|
||||
if upid:
|
||||
_desc_map = {
|
||||
'start': f"Запуск LXC {vmid}",
|
||||
'stop': f"Остановка LXC {vmid}",
|
||||
'restart': f"Перезапуск LXC {vmid}",
|
||||
}
|
||||
try:
|
||||
ProxmoxTaskService.register(
|
||||
db=db, upid=upid, server_id=server_id,
|
||||
user_id=current_user.id, action=action_name,
|
||||
node=node, vmid=vmid, vm_type='lxc',
|
||||
description=_desc_map.get(action, action_name),
|
||||
)
|
||||
except Exception as _te:
|
||||
logger.warning(f"Failed to register ProxmoxTask for LXC {vmid} {action_name}: {_te}")
|
||||
logger.info(f"User {current_user.username} executed {action_name} on container {vmid} at {server.name}")
|
||||
return JSONResponse(content={"status": "success", "action": action_name, "vmid": vmid, "node": node})
|
||||
return JSONResponse(content={"status": "success", "action": action_name, "vmid": vmid, "node": node, "upid": upid})
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Failed to execute action")
|
||||
except Exception as e:
|
||||
|
||||
@@ -788,12 +788,19 @@ class TaskQueue(Base):
|
||||
return int((self.completed_items + self.failed_items) / self.total_items * 100)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
_desc_map = {
|
||||
'bulk_start': 'Массовый запуск ВМ/LXC',
|
||||
'bulk_stop': 'Массовая остановка ВМ/LXC',
|
||||
'bulk_restart': 'Массовый перезапуск ВМ/LXC',
|
||||
'bulk_delete': 'Массовое удаление ВМ/LXC',
|
||||
'bulk_shutdown': 'Массовое выключение ВМ/LXC',
|
||||
}
|
||||
return {
|
||||
'id': self.id,
|
||||
'kind': 'bulk',
|
||||
'user_id': self.user_id,
|
||||
'task_type': self.task_type,
|
||||
'description': self.task_type,
|
||||
'description': _desc_map.get(self.task_type, self.task_type),
|
||||
'status': self.status,
|
||||
'total_items': self.total_items,
|
||||
'completed_items': self.completed_items,
|
||||
|
||||
@@ -271,81 +271,77 @@ class ProxmoxClient:
|
||||
"""Получить статистику LXC контейнера (CPU, память, диск)"""
|
||||
return self.get_container_status(node, vmid)
|
||||
|
||||
def start_vm(self, node: str, vmid: int) -> bool:
|
||||
"""Запустить VM"""
|
||||
def start_vm(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Запустить VM. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
self.proxmox.nodes(node).qemu(vmid).status.start.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).qemu(vmid).status.start.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска VM {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def stop_vm(self, node: str, vmid: int, force: bool = False) -> bool:
|
||||
"""Остановить VM"""
|
||||
def stop_vm(self, node: str, vmid: int, force: bool = False) -> Optional[str]:
|
||||
"""Остановить VM. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
# Proxmox не поддерживает force параметр через stop endpoint
|
||||
# Используем обычную остановку
|
||||
self.proxmox.nodes(node).qemu(vmid).status.stop.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).qemu(vmid).status.stop.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки VM {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def restart_vm(self, node: str, vmid: int) -> bool:
|
||||
"""Перезапустить VM"""
|
||||
def restart_vm(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Перезапустить VM. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
self.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка перезапуска VM {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def start_container(self, node: str, vmid: int) -> bool:
|
||||
"""Запустить LXC контейнер"""
|
||||
def start_container(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Запустить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
self.proxmox.nodes(node).lxc(vmid).status.start.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.start.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка запуска LXC {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def stop_container(self, node: str, vmid: int, force: bool = False) -> bool:
|
||||
"""Остановить LXC контейнер"""
|
||||
def stop_container(self, node: str, vmid: int, force: bool = False) -> Optional[str]:
|
||||
"""Остановить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
# Proxmox не поддерживает force параметр через stop endpoint
|
||||
# Используем обычную остановку
|
||||
self.proxmox.nodes(node).lxc(vmid).status.stop.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.stop.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка остановки LXC {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def restart_container(self, node: str, vmid: int) -> bool:
|
||||
"""Перезапустить LXC контейнер"""
|
||||
def restart_container(self, node: str, vmid: int) -> Optional[str]:
|
||||
"""Перезапустить LXC контейнер. Возвращает UPID задачи или None при ошибке."""
|
||||
if not self.proxmox:
|
||||
return False
|
||||
return None
|
||||
|
||||
try:
|
||||
self.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
return True
|
||||
upid = self.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
return upid if isinstance(upid, str) else None
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка перезапуска LXC {vmid} на {node}: {e}")
|
||||
return False
|
||||
return None
|
||||
|
||||
def force_stop_vm(self, node: str, vmid: int) -> bool:
|
||||
"""Принудительно остановить ВМ (через SSH - аналог kill -9)"""
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
5
pve
5
pve
@@ -37,11 +37,12 @@ step() { echo -e "\n${BOLD}▶ $*${NC}"; }
|
||||
declare -a _CF_ARGS
|
||||
|
||||
# Авто-определение режима: prod (nginx) vs standalone
|
||||
# Логика: если контейнер serverpanel-nginx существует — prod-режим
|
||||
# Логика: compose.prod.yml + (контейнер nginx существует ИЛИ nginx сконфигурирован deploy.sh)
|
||||
_resolve_compose() {
|
||||
_CF_ARGS=("-f" "compose.yml")
|
||||
if [ -f "$PVE_DIR/compose.prod.yml" ] && \
|
||||
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "serverpanel-nginx"; then
|
||||
{ docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "serverpanel-nginx" || \
|
||||
[ -f "$PVE_DIR/nginx/conf.d/serverpanel.conf" ]; }; then
|
||||
_CF_ARGS=("-f" "compose.yml" "-f" "compose.prod.yml")
|
||||
fi
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user