fix: display only real sync errors in error count and list

- Added CountErroredChanges() method to count only pending changes with LastError
- Previously, error count included all pending changes, not just failed ones
- Added /api/sync/info endpoint with proper error count and error list
- Added sync info modal to display sync status, error count, and error details
- Made sync status indicators clickable to open the modal
- Fixed disconnect between "Error count: 4" and "No errors" in the list

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-02-02 17:19:52 +03:00
parent e0404186ad
commit 0bde12a39d
5 changed files with 202 additions and 99 deletions

View File

@@ -44,6 +44,52 @@
<div id="toast" class="fixed bottom-4 right-4 z-50"></div>
<!-- Sync Info Modal -->
<div id="sync-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">Информация о синхронизации</h3>
<button onclick="closeSyncModal()" class="text-gray-400 hover:text-gray-500">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-4">
<div>
<h4 class="font-medium text-gray-900">Статус БД</h4>
<p id="modal-db-status" class="text-sm text-gray-600">Проверка...</p>
</div>
<div>
<h4 class="font-medium text-gray-900">Количество ошибок</h4>
<p id="modal-error-count" class="text-sm text-gray-600">0</p>
</div>
<div>
<h4 class="font-medium text-gray-900">Последняя синхронизация</h4>
<p id="modal-last-sync" class="text-sm text-gray-600">-</p>
</div>
<div>
<h4 class="font-medium text-gray-900">Список ошибок</h4>
<div id="modal-errors-list" class="mt-2 text-sm text-gray-600 max-h-40 overflow-y-auto">
<p>Нет ошибок</p>
</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button onclick="closeSyncModal()" class="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700">
Закрыть
</button>
</div>
</div>
</div>
</div>
<footer class="fixed bottom-0 left-0 right-0 bg-gray-800 text-gray-300 text-xs py-1 px-4">
<div class="max-w-7xl mx-auto flex justify-between">
<span id="db-status">БД: проверка...</span>
@@ -59,68 +105,78 @@
setTimeout(() => el.innerHTML = '', 3000);
}
// Open sync modal
function openSyncModal() {
const modal = document.getElementById('sync-modal');
if (modal) {
modal.classList.remove('hidden');
// Load sync info when modal opens
loadSyncInfo();
}
}
// Close sync modal
function closeSyncModal() {
const modal = document.getElementById('sync-modal');
if (modal) {
modal.classList.add('hidden');
}
}
// Load sync info for modal
async function loadSyncInfo() {
try {
const resp = await fetch('/api/sync/info');
const data = await resp.json();
document.getElementById('modal-db-status').textContent = data.is_online ? 'Подключено' : 'Отключено';
document.getElementById('modal-error-count').textContent = data.error_count;
if (data.last_sync_at) {
const date = new Date(data.last_sync_at);
document.getElementById('modal-last-sync').textContent = date.toLocaleString('ru-RU');
} else {
document.getElementById('modal-last-sync').textContent = 'Нет данных';
}
// Load error list
const errorsList = document.getElementById('modal-errors-list');
if (data.errors && data.errors.length > 0) {
errorsList.innerHTML = data.errors.map(error =>
`<div class="mb-1"><span class="font-medium">${error.timestamp}</span>: ${error.message}</div>`
).join('');
} else {
errorsList.innerHTML = '<p>Нет ошибок</p>';
}
} catch(e) {
console.error('Failed to load sync info:', e);
document.getElementById('modal-db-status').textContent = 'Ошибка загрузки';
document.getElementById('modal-error-count').textContent = '0';
document.getElementById('modal-last-sync').textContent = '-';
document.getElementById('modal-errors-list').innerHTML = '<p>Ошибка загрузки данных</p>';
}
}
// Event delegation for sync dropdown and actions
document.addEventListener('DOMContentLoaded', function() {
checkDbStatus();
checkWritePermission();
});
// Handle keyboard navigation for dropdown
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const dropdownMenu = document.getElementById('sync-dropdown-menu');
if (dropdownMenu) {
dropdownMenu.classList.add('hidden');
}
}
});
// Event delegation for all sync actions
// Event delegation for sync actions
document.body.addEventListener('click', function(e) {
// Handle dropdown toggle
const dropdownButton = e.target.closest('#sync-dropdown-button');
if (dropdownButton) {
// Handle sync button click (full sync only)
const syncButton = e.target.closest('#sync-button');
if (syncButton) {
e.stopPropagation();
const dropdownMenu = document.getElementById('sync-dropdown-menu');
if (dropdownMenu) {
dropdownMenu.classList.toggle('hidden');
// Update aria-expanded
const isExpanded = dropdownMenu.classList.contains('hidden');
dropdownButton.setAttribute('aria-expanded', !isExpanded);
}
}
// Handle sync actions
const actionButton = e.target.closest('[data-action]');
if (actionButton) {
const action = actionButton.dataset.action;
const button = actionButton; // Keep reference to original button
const button = syncButton;
// Add loading state
const originalHTML = button.innerHTML;
button.disabled = true;
button.innerHTML = '<svg class="animate-spin w-4 h-4 inline mr-2" 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> Синхронизация...';
button.innerHTML = '<svg class="animate-spin w-4 h-4 inline mr-2" 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>';
if (action === 'push-changes') {
pushPendingChanges(button, originalHTML);
} else if (action === 'full-sync') {
fullSync(button, originalHTML);
}
}
});
// Close dropdown when clicking outside
document.body.addEventListener('click', function(e) {
const dropdownButton = document.getElementById('sync-dropdown-button');
const dropdownMenu = document.getElementById('sync-dropdown-menu');
if (dropdownButton && dropdownMenu &&
!dropdownButton.contains(e.target) &&
!dropdownMenu.contains(e.target)) {
dropdownMenu.classList.add('hidden');
if (dropdownButton) {
dropdownButton.setAttribute('aria-expanded', 'false');
}
fullSync(button, originalHTML);
}
});
@@ -132,8 +188,8 @@
if (data.success) {
showToast(successMessage, 'success');
// Update last sync time
loadLastSyncTime();
// Update last sync time - removed since dropdown is gone
// loadLastSyncTime();
} else {
showToast('Ошибка: ' + (data.error || 'неизвестная ошибка'), 'error');
}
@@ -194,29 +250,29 @@
}
}
// Load last sync time for dropdown
async function loadLastSyncTime() {
try {
const resp = await fetch('/api/sync/status');
const data = await resp.json();
if (data.last_pricelist_sync) {
const date = new Date(data.last_pricelist_sync);
document.getElementById('last-sync-time').textContent = date.toLocaleString('ru-RU');
} else {
document.getElementById('last-sync-time').textContent = 'Нет данных';
}
} catch(e) {
console.error('Failed to load last sync time:', e);
}
}
// Load last sync time for dropdown (removed since dropdown is gone)
// async function loadLastSyncTime() {
// try {
// const resp = await fetch('/api/sync/status');
// const data = await resp.json();
// if (data.last_pricelist_sync) {
// const date = new Date(data.last_pricelist_sync);
// document.getElementById('last-sync-time').textContent = date.toLocaleString('ru-RU');
// } else {
// document.getElementById('last-sync-time').textContent = 'Нет данных';
// }
// } catch(e) {
// console.error('Failed to load last sync time:', e);
// }
// }
// Call functions immediately to ensure they run even before DOMContentLoaded
// This ensures username and admin link are visible ASAP
checkDbStatus();
checkWritePermission();
// Load last sync time when page loads
document.addEventListener('DOMContentLoaded', loadLastSyncTime);
// Load last sync time - removed since dropdown is gone
// document.addEventListener('DOMContentLoaded', loadLastSyncTime);
</script>
</body>
</html>