Full PWA mobile version added, bug fixes on deployment status, navigation links added
This commit is contained in:
588
backend/static/mobile.js
Normal file
588
backend/static/mobile.js
Normal file
@@ -0,0 +1,588 @@
|
||||
/* Mobile JavaScript for Seismo Fleet Manager */
|
||||
/* Handles hamburger menu, modals, offline sync, and mobile interactions */
|
||||
|
||||
// ===== GLOBAL STATE =====
|
||||
let currentUnitData = null;
|
||||
let isOnline = navigator.onLine;
|
||||
|
||||
// ===== HAMBURGER MENU TOGGLE =====
|
||||
function toggleMenu() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const backdrop = document.getElementById('backdrop');
|
||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||
|
||||
if (sidebar && backdrop) {
|
||||
const isOpen = sidebar.classList.contains('open');
|
||||
|
||||
if (isOpen) {
|
||||
// Close menu
|
||||
sidebar.classList.remove('open');
|
||||
backdrop.classList.remove('show');
|
||||
hamburgerBtn?.classList.remove('menu-open');
|
||||
document.body.style.overflow = '';
|
||||
} else {
|
||||
// Open menu
|
||||
sidebar.classList.add('open');
|
||||
backdrop.classList.add('show');
|
||||
hamburgerBtn?.classList.add('menu-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close menu when clicking backdrop
|
||||
function closeMenuFromBackdrop() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const backdrop = document.getElementById('backdrop');
|
||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||
|
||||
if (sidebar && backdrop) {
|
||||
sidebar.classList.remove('open');
|
||||
backdrop.classList.remove('show');
|
||||
hamburgerBtn?.classList.remove('menu-open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Close menu when window is resized to desktop
|
||||
function handleResize() {
|
||||
if (window.innerWidth >= 768) {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const backdrop = document.getElementById('backdrop');
|
||||
const hamburgerBtn = document.getElementById('hamburgerBtn');
|
||||
|
||||
if (sidebar && backdrop) {
|
||||
sidebar.classList.remove('open');
|
||||
backdrop.classList.remove('show');
|
||||
hamburgerBtn?.classList.remove('menu-open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== UNIT DETAIL MODAL =====
|
||||
function openUnitModal(unitId, status = null, age = null) {
|
||||
const modal = document.getElementById('unitModal');
|
||||
if (!modal) return;
|
||||
|
||||
// Store the status info passed from the card
|
||||
// Accept status if it's a non-empty string, use age if provided or default to '--'
|
||||
const cardStatusInfo = (status && status !== '') ? {
|
||||
status: status,
|
||||
age: age || '--'
|
||||
} : null;
|
||||
|
||||
console.log('openUnitModal:', { unitId, status, age, cardStatusInfo });
|
||||
|
||||
// Fetch unit data and populate modal
|
||||
fetchUnitDetails(unitId).then(unit => {
|
||||
if (unit) {
|
||||
currentUnitData = unit;
|
||||
// Pass the card status info to the populate function
|
||||
populateUnitModal(unit, cardStatusInfo);
|
||||
modal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeUnitModal(event) {
|
||||
// Only close if clicking backdrop or close button
|
||||
if (event && event.target.closest('.unit-modal-content') && !event.target.closest('[data-close-modal]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById('unitModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('show');
|
||||
document.body.style.overflow = '';
|
||||
currentUnitData = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUnitDetails(unitId) {
|
||||
try {
|
||||
// Try to fetch from network first
|
||||
const response = await fetch(`/api/roster/${unitId}`);
|
||||
if (response.ok) {
|
||||
const unit = await response.json();
|
||||
|
||||
// Save to IndexedDB if offline support is available
|
||||
if (window.offlineDB) {
|
||||
await window.offlineDB.saveUnit(unit);
|
||||
}
|
||||
|
||||
return unit;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Network fetch failed, trying offline storage:', error);
|
||||
|
||||
// Fall back to offline storage
|
||||
if (window.offlineDB) {
|
||||
return await window.offlineDB.getUnit(unitId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function populateUnitModal(unit, cardStatusInfo = null) {
|
||||
// Set unit ID in header
|
||||
const modalUnitId = document.getElementById('modalUnitId');
|
||||
if (modalUnitId) {
|
||||
modalUnitId.textContent = unit.id;
|
||||
}
|
||||
|
||||
// Populate modal content
|
||||
const modalContent = document.getElementById('modalContent');
|
||||
if (!modalContent) return;
|
||||
|
||||
// Use status from card if provided, otherwise get from snapshot or derive from unit
|
||||
let statusInfo = cardStatusInfo || getUnitStatus(unit.id, unit);
|
||||
console.log('populateUnitModal:', { unit, cardStatusInfo, statusInfo });
|
||||
|
||||
const statusColor = statusInfo.status === 'OK' ? 'green' :
|
||||
statusInfo.status === 'Pending' ? 'yellow' :
|
||||
statusInfo.status === 'Missing' ? 'red' : 'gray';
|
||||
|
||||
const statusTextColor = statusInfo.status === 'OK' ? 'text-green-600 dark:text-green-400' :
|
||||
statusInfo.status === 'Pending' ? 'text-yellow-600 dark:text-yellow-400' :
|
||||
statusInfo.status === 'Missing' ? 'text-red-600 dark:text-red-400' :
|
||||
'text-gray-600 dark:text-gray-400';
|
||||
|
||||
// Determine status label (show "Benched" instead of "Unknown" for non-deployed units)
|
||||
let statusLabel = statusInfo.status;
|
||||
if ((statusInfo.status === 'Unknown' || statusInfo.status === 'N/A') && !unit.deployed) {
|
||||
statusLabel = 'Benched';
|
||||
}
|
||||
|
||||
// Create navigation URL for location
|
||||
const createNavUrl = (address, coordinates) => {
|
||||
if (address) {
|
||||
// Use address for navigation
|
||||
const encodedAddress = encodeURIComponent(address);
|
||||
// Universal link that works on iOS and Android
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
|
||||
} else if (coordinates) {
|
||||
// Use coordinates for navigation (format: lat,lon)
|
||||
const encodedCoords = encodeURIComponent(coordinates);
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodedCoords}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const navUrl = createNavUrl(unit.address, unit.coordinates);
|
||||
|
||||
modalContent.innerHTML = `
|
||||
<!-- Status Section -->
|
||||
<div class="flex items-center justify-between pb-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-4 h-4 rounded-full bg-${statusColor}-500"></span>
|
||||
<span class="font-semibold ${statusTextColor}">${statusLabel}</span>
|
||||
</div>
|
||||
<span class="text-sm text-gray-500">${statusInfo.age || '--'}</span>
|
||||
</div>
|
||||
|
||||
<!-- Device Info -->
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Device Type</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.device_type || '--'}</p>
|
||||
</div>
|
||||
|
||||
${unit.unit_type ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Unit Type</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.unit_type}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.project_id ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Project ID</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.project_id}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.address ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Address</label>
|
||||
${navUrl ? `
|
||||
<a href="${navUrl}" target="_blank" class="mt-1 flex items-center gap-2 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span class="underline">${unit.address}</span>
|
||||
</a>
|
||||
` : `
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.address}</p>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.coordinates && !unit.address ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Coordinates</label>
|
||||
${navUrl ? `
|
||||
<a href="${navUrl}" target="_blank" class="mt-1 flex items-center gap-2 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
|
||||
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span class="font-mono text-sm underline">${unit.coordinates}</span>
|
||||
</a>
|
||||
` : `
|
||||
<p class="mt-1 text-gray-900 dark:text-white font-mono text-sm">${unit.coordinates}</p>
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Seismograph-specific fields -->
|
||||
${unit.device_type === 'seismograph' ? `
|
||||
${unit.last_calibrated ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Last Calibrated</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.last_calibrated}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.next_calibration_due ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Next Calibration Due</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.next_calibration_due}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.deployed_with_modem_id ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Deployed With Modem</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.deployed_with_modem_id}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
|
||||
<!-- Modem-specific fields -->
|
||||
${unit.device_type === 'modem' ? `
|
||||
${unit.ip_address ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">IP Address</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.ip_address}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.phone_number ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Phone Number</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.phone_number}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${unit.hardware_model ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Hardware Model</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.hardware_model}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
` : ''}
|
||||
|
||||
${unit.note ? `
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Notes</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white whitespace-pre-wrap">${unit.note}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 pt-2">
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Deployed</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.deployed ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-medium text-gray-500 dark:text-gray-400">Retired</label>
|
||||
<p class="mt-1 text-gray-900 dark:text-white">${unit.retired ? 'Yes' : 'No'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update action buttons
|
||||
const editBtn = document.getElementById('modalEditBtn');
|
||||
const deployBtn = document.getElementById('modalDeployBtn');
|
||||
const deleteBtn = document.getElementById('modalDeleteBtn');
|
||||
|
||||
if (editBtn) {
|
||||
editBtn.onclick = () => {
|
||||
window.location.href = `/unit/${unit.id}`;
|
||||
};
|
||||
}
|
||||
|
||||
if (deployBtn) {
|
||||
deployBtn.textContent = unit.deployed ? 'Bench Unit' : 'Deploy Unit';
|
||||
deployBtn.onclick = () => toggleDeployStatus(unit.id, !unit.deployed);
|
||||
}
|
||||
|
||||
if (deleteBtn) {
|
||||
deleteBtn.onclick = () => deleteUnit(unit.id);
|
||||
}
|
||||
}
|
||||
|
||||
function getUnitStatus(unitId, unit = null) {
|
||||
// Try to get status from dashboard snapshot if it exists
|
||||
if (window.lastStatusSnapshot && window.lastStatusSnapshot.units && window.lastStatusSnapshot.units[unitId]) {
|
||||
const unitStatus = window.lastStatusSnapshot.units[unitId];
|
||||
return {
|
||||
status: unitStatus.status,
|
||||
age: unitStatus.age,
|
||||
last: unitStatus.last
|
||||
};
|
||||
}
|
||||
|
||||
// Fallback: if unit data is provided, derive status from deployment state
|
||||
if (unit) {
|
||||
if (unit.deployed) {
|
||||
// For deployed units without status data, default to "Unknown"
|
||||
return { status: 'Unknown', age: '--', last: '--' };
|
||||
} else {
|
||||
// For benched units, use "N/A" which will be displayed as "Benched"
|
||||
return { status: 'N/A', age: '--', last: '--' };
|
||||
}
|
||||
}
|
||||
|
||||
return { status: 'Unknown', age: '--', last: '--' };
|
||||
}
|
||||
|
||||
async function toggleDeployStatus(unitId, deployed) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('deployed', deployed ? 'true' : 'false');
|
||||
|
||||
const response = await fetch(`/api/roster/edit/${unitId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✓ Unit updated successfully');
|
||||
closeUnitModal();
|
||||
|
||||
// Trigger HTMX refresh if on roster page
|
||||
const rosterTable = document.querySelector('[hx-get*="roster"]');
|
||||
if (rosterTable) {
|
||||
htmx.trigger(rosterTable, 'refresh');
|
||||
}
|
||||
} else {
|
||||
showToast('❌ Failed to update unit', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling deploy status:', error);
|
||||
showToast('❌ Failed to update unit', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteUnit(unitId) {
|
||||
if (!confirm(`Are you sure you want to delete unit ${unitId}?\n\nThis action cannot be undone!`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/roster/${unitId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast('✓ Unit deleted successfully');
|
||||
closeUnitModal();
|
||||
|
||||
// Refresh roster page if present
|
||||
const rosterTable = document.querySelector('[hx-get*="roster"]');
|
||||
if (rosterTable) {
|
||||
htmx.trigger(rosterTable, 'refresh');
|
||||
}
|
||||
} else {
|
||||
showToast('❌ Failed to delete unit', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting unit:', error);
|
||||
showToast('❌ Failed to delete unit', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ONLINE/OFFLINE STATUS =====
|
||||
function updateOnlineStatus() {
|
||||
isOnline = navigator.onLine;
|
||||
const offlineIndicator = document.getElementById('offlineIndicator');
|
||||
|
||||
if (offlineIndicator) {
|
||||
if (isOnline) {
|
||||
offlineIndicator.classList.remove('show');
|
||||
// Trigger sync when coming back online
|
||||
if (window.offlineDB) {
|
||||
syncPendingEdits();
|
||||
}
|
||||
} else {
|
||||
offlineIndicator.classList.add('show');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('online', updateOnlineStatus);
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
|
||||
// ===== SYNC FUNCTIONALITY =====
|
||||
async function syncPendingEdits() {
|
||||
if (!window.offlineDB) return;
|
||||
|
||||
try {
|
||||
const pendingEdits = await window.offlineDB.getPendingEdits();
|
||||
|
||||
if (pendingEdits.length === 0) return;
|
||||
|
||||
console.log(`Syncing ${pendingEdits.length} pending edits...`);
|
||||
|
||||
for (const edit of pendingEdits) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(edit.changes)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/roster/edit/${edit.unitId}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await window.offlineDB.clearEdit(edit.id);
|
||||
console.log(`Synced edit ${edit.id} for unit ${edit.unitId}`);
|
||||
} else {
|
||||
console.error(`Failed to sync edit ${edit.id}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error syncing edit ${edit.id}:`, error);
|
||||
// Keep in queue for next sync attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Show success toast
|
||||
showToast('✓ Synced successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in syncPendingEdits:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Manual sync button
|
||||
function manualSync() {
|
||||
if (!isOnline) {
|
||||
showToast('⚠️ Cannot sync while offline', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
syncPendingEdits();
|
||||
}
|
||||
|
||||
// ===== TOAST NOTIFICATIONS =====
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.getElementById('syncToast');
|
||||
if (!toast) return;
|
||||
|
||||
// Update toast appearance based on type
|
||||
toast.classList.remove('bg-green-500', 'bg-red-500', 'bg-yellow-500');
|
||||
|
||||
if (type === 'success') {
|
||||
toast.classList.add('bg-green-500');
|
||||
} else if (type === 'error') {
|
||||
toast.classList.add('bg-red-500');
|
||||
} else if (type === 'warning') {
|
||||
toast.classList.add('bg-yellow-500');
|
||||
}
|
||||
|
||||
toast.textContent = message;
|
||||
toast.classList.add('show');
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// ===== BOTTOM NAV ACTIVE STATE =====
|
||||
function updateBottomNavActiveState() {
|
||||
const currentPath = window.location.pathname;
|
||||
const navButtons = document.querySelectorAll('.bottom-nav-btn');
|
||||
|
||||
navButtons.forEach(btn => {
|
||||
const href = btn.getAttribute('data-href');
|
||||
if (href && (currentPath === href || (href !== '/' && currentPath.startsWith(href)))) {
|
||||
btn.classList.add('active');
|
||||
} else {
|
||||
btn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== INITIALIZATION =====
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize online/offline status
|
||||
updateOnlineStatus();
|
||||
|
||||
// Update bottom nav active state
|
||||
updateBottomNavActiveState();
|
||||
|
||||
// Add resize listener
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Close menu on navigation (for mobile)
|
||||
document.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a');
|
||||
if (link && link.closest('#sidebar')) {
|
||||
// Delay to allow navigation to start
|
||||
setTimeout(() => {
|
||||
if (window.innerWidth < 768) {
|
||||
closeMenuFromBackdrop();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Prevent scroll when modals are open (iOS fix)
|
||||
document.addEventListener('touchmove', (e) => {
|
||||
const modal = document.querySelector('.unit-modal.show, #sidebar.open');
|
||||
if (modal && !e.target.closest('.unit-modal-content, #sidebar')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}, { passive: false });
|
||||
|
||||
console.log('Mobile.js initialized');
|
||||
});
|
||||
|
||||
// ===== SERVICE WORKER REGISTRATION =====
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered:', registration);
|
||||
|
||||
// Check for updates periodically
|
||||
setInterval(() => {
|
||||
registration.update();
|
||||
}, 60 * 60 * 1000); // Check every hour
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker registration failed:', error);
|
||||
});
|
||||
|
||||
// Listen for service worker updates
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.log('Service Worker updated, reloading page...');
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// Export functions for global use
|
||||
window.toggleMenu = toggleMenu;
|
||||
window.closeMenuFromBackdrop = closeMenuFromBackdrop;
|
||||
window.openUnitModal = openUnitModal;
|
||||
window.closeUnitModal = closeUnitModal;
|
||||
window.manualSync = manualSync;
|
||||
window.showToast = showToast;
|
||||
Reference in New Issue
Block a user