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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user