feat: implement background task system with notifications
- Added background task manager with goroutine execution and panic recovery - Replaced SSE streaming with background task execution for: * Price recalculation (RecalculateAll) * Stock import (ImportStockLog) * Pricelist creation (CreateWithProgress) - Implemented unified polling for task status and DB connection in frontend - Added task indicator in top bar showing running tasks count - Added toast notifications for task completion/error - Tasks automatically cleaned up after 10 minutes - Tasks show progress (0-100%) with descriptive messages - Updated handler constructors to receive task manager - Added API endpoints for task status (/api/tasks, /api/tasks/:id) Fixes issue with SSE disconnection on slow connections during long-running operations
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div id="task-indicator" class="hidden items-center gap-1 text-sm"></div>
|
||||
<span id="db-connection-status" class="inline-flex items-center gap-1 text-xs text-gray-600">
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-gray-400"></span>
|
||||
DB: checking
|
||||
@@ -102,22 +103,79 @@
|
||||
el.title = errorText || 'Database not connected';
|
||||
}
|
||||
|
||||
async function refreshDBConnectionStatus() {
|
||||
const knownTasks = new Map(); // taskID -> {status, notified}
|
||||
|
||||
async function refreshSystemStatus() {
|
||||
try {
|
||||
const resp = await fetch('/api/db-status');
|
||||
const data = await resp.json();
|
||||
renderDBStatus(data.connected === true, data.error || '');
|
||||
// Poll DB status
|
||||
const dbResp = await fetch('/api/db-status');
|
||||
const dbData = await dbResp.json();
|
||||
renderDBStatus(dbData.connected === true, dbData.error || '');
|
||||
|
||||
const userEl = document.getElementById('db-user');
|
||||
if (data.connected && data.db_user) {
|
||||
userEl.textContent = '@' + String(data.db_user);
|
||||
if (dbData.connected && dbData.db_user) {
|
||||
userEl.textContent = '@' + String(dbData.db_user);
|
||||
} else if (userEl) {
|
||||
userEl.textContent = '';
|
||||
}
|
||||
|
||||
// Poll tasks
|
||||
const tasksResp = await fetch('/api/tasks');
|
||||
const tasksData = await tasksResp.json();
|
||||
updateTaskIndicator(tasksData.tasks || []);
|
||||
} catch (e) {
|
||||
renderDBStatus(false, 'Status request failed');
|
||||
}
|
||||
}
|
||||
|
||||
function updateTaskIndicator(tasks) {
|
||||
const indicator = document.getElementById('task-indicator');
|
||||
if (!indicator) return;
|
||||
|
||||
const runningTasks = tasks.filter(t => t.status === 'running');
|
||||
|
||||
if (runningTasks.length > 0) {
|
||||
indicator.classList.remove('hidden');
|
||||
indicator.classList.add('flex');
|
||||
indicator.innerHTML = `
|
||||
<svg class="animate-spin h-4 w-4 text-orange-600" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-xs">${runningTasks.length}</span>
|
||||
`;
|
||||
indicator.title = runningTasks.map(t => t.message).join(', ');
|
||||
} else {
|
||||
indicator.classList.add('hidden');
|
||||
indicator.classList.remove('flex');
|
||||
indicator.innerHTML = '';
|
||||
}
|
||||
|
||||
// Check for newly completed/errored tasks
|
||||
tasks.forEach(task => {
|
||||
const known = knownTasks.get(task.id);
|
||||
|
||||
if (!known) {
|
||||
knownTasks.set(task.id, {status: task.status, notified: false});
|
||||
} else if (known.status === 'running' && task.status !== 'running' && !known.notified) {
|
||||
// Task transitioned from running to completed/error
|
||||
if (task.status === 'completed') {
|
||||
showToast(task.message || 'Задача завершена', 'success');
|
||||
} else if (task.status === 'error') {
|
||||
showToast(task.error || 'Ошибка выполнения задачи', 'error');
|
||||
}
|
||||
knownTasks.set(task.id, {status: task.status, notified: true});
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup old tasks from knownTasks
|
||||
for (const [taskID, info] of knownTasks.entries()) {
|
||||
if (!tasks.find(t => t.id === taskID)) {
|
||||
knownTasks.delete(taskID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function closeConnectionSettingsModal() {
|
||||
const modal = document.getElementById('connection-settings-modal');
|
||||
if (!modal) return;
|
||||
@@ -217,8 +275,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
refreshDBConnectionStatus();
|
||||
setInterval(refreshDBConnectionStatus, 10000);
|
||||
refreshSystemStatus();
|
||||
setInterval(refreshSystemStatus, 5000); // 5 second polling
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user