From 0d3a273e2266880079319e1a436df060c3512cb4 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 6 Aug 2025 16:04:41 -0600 Subject: [PATCH] fixed cut overlays and solved the docker duplication thing --- map/app/Dockerfile | 7 +- map/app/package.json | 1 - map/app/public/css/modules/leaflet-custom.css | 14 +- .../css/modules/start-location-marker.css | 6 +- map/app/public/js/cut-manager.js | 199 +- map/app/public/js/map.js.backup | 1031 --------- map/app/server copy.js | 2051 ----------------- map/app/server.js | 196 +- map/app/utils/logger.js | 17 +- 9 files changed, 248 insertions(+), 3274 deletions(-) delete mode 100644 map/app/public/js/map.js.backup delete mode 100644 map/app/server copy.js diff --git a/map/app/Dockerfile b/map/app/Dockerfile index 4965d2f..bbd45ee 100644 --- a/map/app/Dockerfile +++ b/map/app/Dockerfile @@ -1,7 +1,7 @@ FROM node:18-alpine -# Install wget and dumb-init for proper signal handling -RUN apk add --no-cache wget dumb-init +# Install wget for health checks +RUN apk add --no-cache wget WORKDIR /app @@ -33,6 +33,5 @@ USER nodejs EXPOSE 3000 -# Use dumb-init to handle signals properly and prevent zombie processes -ENTRYPOINT ["dumb-init", "--"] +# Use exec form to ensure PID 1 is node process CMD ["node", "server.js"] \ No newline at end of file diff --git a/map/app/package.json b/map/app/package.json index f515133..30de2af 100644 --- a/map/app/package.json +++ b/map/app/package.json @@ -5,7 +5,6 @@ "main": "server.js", "scripts": { "start": "node server.js", - "dev": "nodemon server.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ diff --git a/map/app/public/css/modules/leaflet-custom.css b/map/app/public/css/modules/leaflet-custom.css index 95f6e56..662943a 100644 --- a/map/app/public/css/modules/leaflet-custom.css +++ b/map/app/public/css/modules/leaflet-custom.css @@ -54,8 +54,8 @@ cursor: pointer; } -/* Ensure circle markers are visible */ -path.leaflet-interactive { +/* Ensure circle markers are visible - but NOT cut polygons */ +path.leaflet-interactive:not(.cut-polygon) { stroke: #fff; stroke-opacity: 1; stroke-width: 2; @@ -107,7 +107,15 @@ path.leaflet-interactive { /* Cut polygons - allow dynamic opacity (higher specificity to override) */ .leaflet-container path.leaflet-interactive.cut-polygon { stroke-width: 2px !important; - /* Allow JavaScript to control fill-opacity - remove !important */ + /* Allow JavaScript to control fill-opacity - explicitly do NOT override it */ + stroke: currentColor !important; + stroke-opacity: 0.8 !important; +} + +/* Additional specificity rule for cut polygons to ensure JS opacity takes precedence */ +path.leaflet-interactive.cut-polygon { + /* Do not set fill-opacity here - let JavaScript control it */ + stroke-width: 2px; } /* Marker being moved */ diff --git a/map/app/public/css/modules/start-location-marker.css b/map/app/public/css/modules/start-location-marker.css index d4f38b0..c6593af 100644 --- a/map/app/public/css/modules/start-location-marker.css +++ b/map/app/public/css/modules/start-location-marker.css @@ -37,6 +37,10 @@ border-radius: 50%; background: rgba(255, 68, 68, 0.3); animation: pulse-ring 2s ease-out infinite; + /* Position the pulse at the tip of the marker */ + top: 24px; + left: 0; + transform-origin: center center; } @keyframes bounce-marker { @@ -69,4 +73,4 @@ .start-location-popup-enhanced .leaflet-popup-content { margin: 0; -} +} \ No newline at end of file diff --git a/map/app/public/js/cut-manager.js b/map/app/public/js/cut-manager.js index f0455de..53a9a34 100644 --- a/map/app/public/js/cut-manager.js +++ b/map/app/public/js/cut-manager.js @@ -183,92 +183,123 @@ export class CutManager { return false; } } - - /** - * Display a cut on the map (enhanced to support multiple cuts) - */ - /** - * Display a cut on the map (enhanced to support multiple cuts) - */ - displayCut(cutData, autoDisplayed = false) { - if (!this.map) { - console.error('Map not initialized'); - return false; - } - - // Normalize field names for consistent access - const normalizedCut = { - ...cutData, - id: cutData.id || cutData.Id || cutData.ID, - name: cutData.name || cutData.Name, - description: cutData.description || cutData.Description, - color: cutData.color || cutData.Color, - opacity: cutData.opacity || cutData.Opacity, - category: cutData.category || cutData.Category, - geojson: cutData.geojson || cutData.GeoJSON || cutData['GeoJSON Data'], - is_public: cutData.is_public || cutData['Public Visibility'], - is_official: cutData.is_official || cutData['Official Cut'], - autoDisplayed: autoDisplayed // Track if this was auto-displayed - }; - - // Check if already displayed - if (this.cutLayers.has(normalizedCut.id)) { - console.log(`Cut already displayed: ${normalizedCut.name}`); - return true; - } - - if (!normalizedCut.geojson) { - console.error('Cut has no GeoJSON data'); - return false; - } - - try { - const geojsonData = typeof normalizedCut.geojson === 'string' ? - JSON.parse(normalizedCut.geojson) : normalizedCut.geojson; - - const cutLayer = L.geoJSON(geojsonData, { - style: { - color: normalizedCut.color || '#3388ff', - fillColor: normalizedCut.color || '#3388ff', - fillOpacity: parseFloat(normalizedCut.opacity) || 0.3, - weight: 2, - opacity: 1, - className: 'cut-polygon' - } - }); - - // Add popup with cut info - cutLayer.bindPopup(` -
-

${normalizedCut.name}

- ${normalizedCut.description ? `

${normalizedCut.description}

` : ''} - ${normalizedCut.category ? `

Category: ${normalizedCut.category}

` : ''} - ${normalizedCut.is_official ? 'Official Cut' : ''} -
- `); - - cutLayer.addTo(this.map); - - // Store in both tracking systems - this.cutLayers.set(normalizedCut.id, cutLayer); - this.displayedCuts.set(normalizedCut.id, normalizedCut); - - // Update current cut reference (for legacy compatibility) - this.currentCut = normalizedCut; - this.currentCutLayer = cutLayer; - - console.log(`Displayed cut: ${normalizedCut.name} (ID: ${normalizedCut.id})`); - return true; - - } catch (error) { - console.error('Error displaying cut:', error); - return false; - } + + // ...existing code... + +displayCut(cutData, autoDisplayed = false) { + if (!this.map) { + console.error('Map not initialized'); + return false; } - /** - * Hide the currently displayed cut (legacy method - now hides all cuts) - */ + // Normalize field names for consistent access + const normalizedCut = { + ...cutData, + id: cutData.id || cutData.Id || cutData.ID, + name: cutData.name || cutData.Name, + description: cutData.description || cutData.Description, + color: cutData.color || cutData.Color, + opacity: cutData.opacity || cutData.Opacity, + category: cutData.category || cutData.Category, + geojson: cutData.geojson || cutData.GeoJSON || cutData['GeoJSON Data'], + is_public: cutData.is_public || cutData['Public Visibility'], + is_official: cutData.is_official || cutData['Official Cut'], + autoDisplayed: autoDisplayed // Track if this was auto-displayed + }; + + // Check if already displayed + if (this.cutLayers.has(normalizedCut.id)) { + console.log(`Cut already displayed: ${normalizedCut.name}`); + return true; + } + + if (!normalizedCut.geojson) { + console.error('Cut has no GeoJSON data'); + return false; + } + + try { + const geojsonData = typeof normalizedCut.geojson === 'string' ? + JSON.parse(normalizedCut.geojson) : normalizedCut.geojson; + + // Parse opacity value - ensure it's a number between 0 and 1 + let opacityValue = parseFloat(normalizedCut.opacity); + + // Validate opacity is within range + if (isNaN(opacityValue) || opacityValue < 0 || opacityValue > 1) { + opacityValue = 0.3; // Default fallback + console.log(`Invalid opacity value (${normalizedCut.opacity}), using default: ${opacityValue}`); + } + + const cutLayer = L.geoJSON(geojsonData, { + style: { + color: normalizedCut.color || '#3388ff', + fillColor: normalizedCut.color || '#3388ff', + fillOpacity: opacityValue, + weight: 2, + opacity: 0.8, // Stroke opacity - keeping this slightly transparent for better visibility + className: 'cut-polygon' + }, + // Add onEachFeature to apply styles to each individual feature + onEachFeature: function (feature, layer) { + // Apply styles directly to the layer to ensure they override CSS + if (layer.setStyle) { + layer.setStyle({ + fillOpacity: opacityValue, + color: normalizedCut.color || '#3388ff', + fillColor: normalizedCut.color || '#3388ff', + weight: 2, + opacity: 0.8 + }); + } + + // Add cut-polygon class to the path element + if (layer._path) { + layer._path.classList.add('cut-polygon'); + } + } + }); + + // Add popup with cut info + cutLayer.bindPopup(` +
+

${normalizedCut.name}

+ ${normalizedCut.description ? `

${normalizedCut.description}

` : ''} + ${normalizedCut.category ? `

Category: ${normalizedCut.category}

` : ''} + ${normalizedCut.is_official ? 'Official Cut' : ''} +
+ `); + + cutLayer.addTo(this.map); + + // Ensure cut-polygon class is applied to all path elements after adding to map + cutLayer.eachLayer(function(layer) { + if (layer._path) { + layer._path.classList.add('cut-polygon'); + // Force the fill-opacity style to ensure it overrides CSS + layer._path.style.fillOpacity = opacityValue; + } + }); + + // Store in both tracking systems + this.cutLayers.set(normalizedCut.id, cutLayer); + this.displayedCuts.set(normalizedCut.id, normalizedCut); + + // Update current cut reference (for legacy compatibility) + this.currentCut = normalizedCut; + this.currentCutLayer = cutLayer; + + console.log(`Displayed cut: ${normalizedCut.name} (ID: ${normalizedCut.id}) with opacity: ${opacityValue} (raw: ${normalizedCut.opacity})`); + return true; + + } catch (error) { + console.error('Error displaying cut:', error); + return false; + } +} + +// ...existing code... + hideCut() { this.hideAllCuts(); } diff --git a/map/app/public/js/map.js.backup b/map/app/public/js/map.js.backup deleted file mode 100644 index 712ce64..0000000 --- a/map/app/public/js/map.js.backup +++ /dev/null @@ -1,1031 +0,0 @@ -// Global configuration -const CONFIG = { - DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461, - DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938, - DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11, - REFRESH_INTERVAL: 30000, // 30 seconds - MAX_ZOOM: 19, - MIN_ZOOM: 2 -}; - -// Application state -let map = null; -let markers = []; -let userLocationMarker = null; -let isAddingLocation = false; -let refreshInterval = null; -let currentEditingLocation = null; -let currentUser = null; -let startLocationMarker = null; -let isStartLocationVisible = true; - -// Initialize the application -document.addEventListener('DOMContentLoaded', async () => { - console.log('DOM loaded, initializing application...'); - - try { - // First check authentication - await checkAuth(); - - // Then initialize the map - await initializeMap(); - - // Only load locations after map is ready - await loadLocations(); - - // Setup other features - setupEventListeners(); - setupAutoRefresh(); - - } catch (error) { - console.error('Initialization error:', error); - showStatus('Failed to initialize application', 'error'); - } finally { - hideLoading(); - } -}); - -// Check authentication -async function checkAuth() { - try { - const response = await fetch('/api/auth/check'); - const data = await response.json(); - - if (!data.authenticated) { - window.location.href = '/login.html'; - // Throw error to stop further initialization - throw new Error('Not authenticated'); - } - - currentUser = data.user; - updateUserInterface(); - - } catch (error) { - console.error('Auth check failed:', error); - window.location.href = '/login.html'; - // Re-throw to stop initialization chain - throw error; - } -} - -// Update UI based on user -function updateUserInterface() { - if (!currentUser) return; - - // Update user email in both desktop and mobile - const userEmailElement = document.getElementById('user-email'); - const mobileUserEmailElement = document.getElementById('mobile-user-email'); - - if (userEmailElement) { - userEmailElement.textContent = currentUser.email; - } - if (mobileUserEmailElement) { - mobileUserEmailElement.textContent = currentUser.email; - } - - // Add admin link if user is admin - if (currentUser.isAdmin) { - // Add admin link to desktop header - const headerActions = document.querySelector('.header-actions'); - if (headerActions) { - const adminLink = document.createElement('a'); - adminLink.href = '/admin.html'; - adminLink.className = 'btn btn-secondary'; - adminLink.textContent = '⚙️ Admin'; - headerActions.insertBefore(adminLink, headerActions.firstChild); - } - - // Add admin link to mobile dropdown - const mobileDropdownContent = document.getElementById('mobile-dropdown-content'); - if (mobileDropdownContent) { - // Check if admin link already exists - if (!mobileDropdownContent.querySelector('.admin-link-mobile')) { - const adminItem = document.createElement('div'); - adminItem.className = 'mobile-dropdown-item admin-link-mobile'; - - const adminLink = document.createElement('a'); - adminLink.href = '/admin.html'; - adminLink.style.color = 'inherit'; - adminLink.style.textDecoration = 'none'; - adminLink.textContent = '⚙️ Admin Panel'; - - adminItem.appendChild(adminLink); - - // Insert admin link at the top of the dropdown - if (mobileDropdownContent.firstChild) { - mobileDropdownContent.insertBefore(adminItem, mobileDropdownContent.firstChild); - } else { - mobileDropdownContent.appendChild(adminItem); - } - } - } - } -} - -// Initialize the map -async function initializeMap() { - try { - // Get start location from server - const response = await fetch('/api/admin/start-location'); - const data = await response.json(); - - let startLat = CONFIG.DEFAULT_LAT; - let startLng = CONFIG.DEFAULT_LNG; - let startZoom = CONFIG.DEFAULT_ZOOM; - - if (data.success && data.location) { - startLat = data.location.latitude; - startLng = data.location.longitude; - startZoom = data.location.zoom; - } - - // Initialize map - map = L.map('map').setView([startLat, startLng], startZoom); - - // Add tile layer - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - maxZoom: CONFIG.MAX_ZOOM, - minZoom: CONFIG.MIN_ZOOM - }).addTo(map); - - // Add start location marker - addStartLocationMarker(startLat, startLng); - - console.log('Map initialized successfully'); - - } catch (error) { - console.error('Failed to initialize map:', error); - showStatus('Failed to initialize map', 'error'); - } -} - -// Add start location marker function -function addStartLocationMarker(lat, lng) { - console.log(`Adding start location marker at: ${lat}, ${lng}`); - - // Remove existing start location marker if it exists - if (startLocationMarker) { - map.removeLayer(startLocationMarker); - } - - // Create a very distinctive custom icon - const startIcon = L.divIcon({ - html: ` -
-
-
- - - -
-
-
-
- `, - className: 'start-location-custom-marker', - iconSize: [48, 48], - iconAnchor: [24, 48], - popupAnchor: [0, -48] - }); - - // Create the marker - startLocationMarker = L.marker([lat, lng], { - icon: startIcon, - zIndexOffset: 1000 - }).addTo(map); - - // Add popup - startLocationMarker.bindPopup(` - - `); -} - -// Toggle start location visibility -function toggleStartLocationVisibility() { - if (!startLocationMarker) return; - - isStartLocationVisible = !isStartLocationVisible; - - if (isStartLocationVisible) { - map.addLayer(startLocationMarker); - // Update both desktop and mobile button text - const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text'); - if (desktopBtn) desktopBtn.textContent = 'Hide Start Location'; - } else { - map.removeLayer(startLocationMarker); - // Update both desktop and mobile button text - const desktopBtn = document.querySelector('#toggle-start-location-btn .btn-text'); - if (desktopBtn) desktopBtn.textContent = 'Show Start Location'; - } -} - -// Load locations from the API -async function loadLocations() { - try { - const response = await fetch('/api/locations'); - const data = await response.json(); - - if (data.success) { - displayLocations(data.locations); - updateLocationCount(data.locations.length); - } else { - throw new Error(data.error || 'Failed to load locations'); - } - } catch (error) { - console.error('Error loading locations:', error); - showStatus('Failed to load locations', 'error'); - } -} - -// Display locations on the map -function displayLocations(locations) { - // Clear existing markers - markers.forEach(marker => { - if (marker && map) { - map.removeLayer(marker); - } - }); - markers = []; - - // Add new markers - locations.forEach(location => { - if (location.latitude && location.longitude) { - const marker = createLocationMarker(location); - if (marker) { // Only add if marker was successfully created - markers.push(marker); - } - } - }); - - console.log(`Displayed ${markers.length} locations`); -} - -// Create a location marker -function createLocationMarker(location) { - // Safety check - ensure map exists - if (!map) { - console.warn('Map not initialized, skipping marker creation'); - return null; - } - - const lat = parseFloat(location.latitude); - const lng = parseFloat(location.longitude); - - // Determine marker color based on support level - let markerColor = 'blue'; - if (location['Support Level']) { - const level = parseInt(location['Support Level']); - switch(level) { - case 1: markerColor = 'green'; break; - case 2: markerColor = 'yellow'; break; - case 3: markerColor = 'orange'; break; - case 4: markerColor = 'red'; break; - } - } - - const marker = L.circleMarker([lat, lng], { - radius: 8, - fillColor: markerColor, - color: '#fff', - weight: 2, - opacity: 1, - fillOpacity: 0.8 - }).addTo(map); - - // Create popup content - const popupContent = createPopupContent(location); - marker.bindPopup(popupContent); - - // Store location data on marker for later use - marker._locationData = location; - - return marker; -} - -// Create popup content -function createPopupContent(location) { - // Try to find the ID field - const locationId = location.Id || location.id || location.ID || location._id; - - const name = [location['First Name'], location['Last Name']] - .filter(Boolean).join(' ') || 'Unknown'; - const address = location.Address || 'No address'; - const supportLevel = location['Support Level'] ? - `Level ${location['Support Level']}` : 'Not specified'; - - return ` - - `; -} - -// Setup event listeners -function setupEventListeners() { - // Desktop controls - document.getElementById('refresh-btn')?.addEventListener('click', () => { - loadLocations(); - showStatus('Locations refreshed', 'success'); - }); - - document.getElementById('geolocate-btn')?.addEventListener('click', getUserLocation); - document.getElementById('toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility); - document.getElementById('add-location-btn')?.addEventListener('click', toggleAddLocationMode); - document.getElementById('fullscreen-btn')?.addEventListener('click', toggleFullscreen); - - // Mobile controls - document.getElementById('mobile-refresh-btn')?.addEventListener('click', () => { - loadLocations(); - showStatus('Locations refreshed', 'success'); - }); - - document.getElementById('mobile-geolocate-btn')?.addEventListener('click', getUserLocation); - document.getElementById('mobile-toggle-start-location-btn')?.addEventListener('click', toggleStartLocationVisibility); - document.getElementById('mobile-add-location-btn')?.addEventListener('click', toggleAddLocationMode); - document.getElementById('mobile-fullscreen-btn')?.addEventListener('click', toggleFullscreen); - - // Mobile dropdown toggle - document.getElementById('mobile-dropdown-toggle')?.addEventListener('click', (e) => { - e.stopPropagation(); - const dropdown = document.getElementById('mobile-dropdown'); - dropdown.classList.toggle('active'); - }); - - // Close mobile dropdown when clicking outside - document.addEventListener('click', (e) => { - const dropdown = document.getElementById('mobile-dropdown'); - if (!dropdown.contains(e.target)) { - dropdown.classList.remove('active'); - } - }); - - // Modal controls - document.getElementById('close-modal-btn')?.addEventListener('click', closeAddModal); - document.getElementById('cancel-modal-btn')?.addEventListener('click', closeAddModal); - - // Edit footer controls - document.getElementById('close-edit-footer-btn')?.addEventListener('click', closeEditForm); - - // Forms - document.getElementById('location-form')?.addEventListener('submit', handleAddLocation); - document.getElementById('edit-location-form')?.addEventListener('submit', handleEditLocation); - - // Delete button - document.getElementById('delete-location-btn')?.addEventListener('click', handleDeleteLocation); - - // Address lookup buttons - document.getElementById('lookup-address-add-btn')?.addEventListener('click', () => { - lookupAddress('add'); - }); - - document.getElementById('lookup-address-edit-btn')?.addEventListener('click', () => { - lookupAddress('edit'); - }); - - // Geo-location field sync - setupGeoLocationSync(); - - // Add event delegation for popup edit buttons - document.addEventListener('click', (e) => { - if (e.target.classList.contains('edit-location-popup-btn')) { - e.preventDefault(); - try { - const locationData = JSON.parse(e.target.getAttribute('data-location')); - openEditForm(locationData); - } catch (error) { - console.error('Error parsing location data:', error); - showStatus('Error opening edit form', 'error'); - } - } - }); -} - -// Setup geo-location field synchronization -function setupGeoLocationSync() { - // For add form - const addLatInput = document.getElementById('location-lat'); - const addLngInput = document.getElementById('location-lng'); - const addGeoInput = document.getElementById('geo-location'); - - if (addLatInput && addLngInput && addGeoInput) { - [addLatInput, addLngInput].forEach(input => { - input.addEventListener('input', () => { - const lat = addLatInput.value; - const lng = addLngInput.value; - if (lat && lng) { - addGeoInput.value = `${lat};${lng}`; - } - }); - }); - - addGeoInput.addEventListener('input', () => { - const coords = parseGeoLocation(addGeoInput.value); - if (coords) { - addLatInput.value = coords.lat; - addLngInput.value = coords.lng; - } - }); - } - - // For edit form - const editLatInput = document.getElementById('edit-location-lat'); - const editLngInput = document.getElementById('edit-location-lng'); - const editGeoInput = document.getElementById('edit-geo-location'); - - if (editLatInput && editLngInput && editGeoInput) { - [editLatInput, editLngInput].forEach(input => { - input.addEventListener('input', () => { - const lat = editLatInput.value; - const lng = editLngInput.value; - if (lat && lng) { - editGeoInput.value = `${lat};${lng}`; - } - }); - }); - - editGeoInput.addEventListener('input', () => { - const coords = parseGeoLocation(editGeoInput.value); - if (coords) { - editLatInput.value = coords.lat; - editLngInput.value = coords.lng; - } - }); - } -} - -// Parse geo-location string -function parseGeoLocation(value) { - if (!value) return null; - - // Try semicolon separator first - let parts = value.split(';'); - if (parts.length !== 2) { - // Try comma separator - parts = value.split(','); - } - - if (parts.length === 2) { - const lat = parseFloat(parts[0].trim()); - const lng = parseFloat(parts[1].trim()); - - if (!isNaN(lat) && !isNaN(lng)) { - return { lat, lng }; - } - } - - return null; -} - -// Get user location -function getUserLocation() { - if (!navigator.geolocation) { - showStatus('Geolocation is not supported by your browser', 'error'); - return; - } - - showStatus('Getting your location...', 'info'); - - navigator.geolocation.getCurrentPosition( - (position) => { - const lat = position.coords.latitude; - const lng = position.coords.longitude; - - // Center map on user location - map.setView([lat, lng], 15); - - // Add or update user location marker - if (userLocationMarker) { - userLocationMarker.setLatLng([lat, lng]); - } else { - userLocationMarker = L.circleMarker([lat, lng], { - radius: 10, - fillColor: '#2196F3', - color: '#fff', - weight: 3, - opacity: 1, - fillOpacity: 0.8 - }).addTo(map); - - userLocationMarker.bindPopup('Your Location'); - } - - showStatus('Location found!', 'success'); - }, - (error) => { - let message = 'Unable to get your location'; - switch(error.code) { - case error.PERMISSION_DENIED: - message = 'Location permission denied'; - break; - case error.POSITION_UNAVAILABLE: - message = 'Location information unavailable'; - break; - case error.TIMEOUT: - message = 'Location request timed out'; - break; - } - showStatus(message, 'error'); - }, - { - enableHighAccuracy: true, - timeout: 10000, - maximumAge: 0 - } - ); -} - -// Toggle add location mode -function toggleAddLocationMode() { - isAddingLocation = !isAddingLocation; - - const crosshair = document.getElementById('crosshair'); - const addBtn = document.getElementById('add-location-btn'); - const mobileAddBtn = document.getElementById('mobile-add-location-btn'); - - if (isAddingLocation) { - crosshair.classList.remove('hidden'); - - // Update desktop button - if (addBtn) { - addBtn.classList.add('active'); - addBtn.innerHTML = 'Cancel'; - } - - // Update mobile button - if (mobileAddBtn) { - mobileAddBtn.classList.add('active'); - mobileAddBtn.innerHTML = '✕'; - mobileAddBtn.title = 'Cancel'; - } - - map.on('click', handleMapClick); - } else { - crosshair.classList.add('hidden'); - - // Update desktop button - if (addBtn) { - addBtn.classList.remove('active'); - addBtn.innerHTML = 'Add Location Here'; - } - - // Update mobile button - if (mobileAddBtn) { - mobileAddBtn.classList.remove('active'); - mobileAddBtn.innerHTML = '➕'; - mobileAddBtn.title = 'Add Location'; - } - - map.off('click', handleMapClick); - } -} - -// Handle map click in add mode -function handleMapClick(e) { - if (!isAddingLocation) return; - - const { lat, lng } = e.latlng; - openAddModal(lat, lng); - toggleAddLocationMode(); -} - -// Open add location modal -function openAddModal(lat, lng) { - const modal = document.getElementById('add-modal'); - const latInput = document.getElementById('location-lat'); - const lngInput = document.getElementById('location-lng'); - const geoInput = document.getElementById('geo-location'); - - // Set coordinates - latInput.value = lat.toFixed(8); - lngInput.value = lng.toFixed(8); - geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; - - // Clear other fields - document.getElementById('location-form').reset(); - latInput.value = lat.toFixed(8); - lngInput.value = lng.toFixed(8); - geoInput.value = `${lat.toFixed(8)};${lng.toFixed(8)}`; - - // Show modal - modal.classList.remove('hidden'); -} - -// Close add modal -function closeAddModal() { - const modal = document.getElementById('add-modal'); - modal.classList.add('hidden'); - document.getElementById('location-form').reset(); -} - -// Handle add location form submission -async function handleAddLocation(e) { - e.preventDefault(); - - const formData = new FormData(e.target); - const data = {}; - - // Convert form data to object - for (let [key, value] of formData.entries()) { - // Map form field names to NocoDB column names - if (key === 'latitude') data.latitude = value.trim(); - else if (key === 'longitude') data.longitude = value.trim(); - else if (key === 'Geo-Location') data['Geo-Location'] = value.trim(); - else if (value.trim() !== '') { - data[key] = value.trim(); - } - } - - // Ensure geo-location is set - if (data.latitude && data.longitude) { - data['Geo-Location'] = `${data.latitude};${data.longitude}`; - } - - // Handle checkbox - data.Sign = document.getElementById('sign').checked; - - try { - const response = await fetch('/api/locations', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - - const result = await response.json(); - - if (result.success) { - showStatus('Location added successfully!', 'success'); - closeAddModal(); - loadLocations(); - } else { - throw new Error(result.error || 'Failed to add location'); - } - } catch (error) { - console.error('Error adding location:', error); - showStatus(error.message || 'Failed to add location', 'error'); - } -} - -// Open edit form -function openEditForm(location) { - currentEditingLocation = location; - - // Debug: Log all possible ID fields - console.log('Opening edit form for location:', { - 'Id': location.Id, - 'id': location.id, - 'ID': location.ID, - '_id': location._id, - 'all_keys': Object.keys(location) - }); - - // Extract ID - check multiple possible field names - const locationId = location.Id || location.id || location.ID || location._id; - - if (!locationId) { - console.error('No ID found in location object. Available fields:', Object.keys(location)); - showStatus('Error: Location ID not found. Check console for details.', 'error'); - return; - } - - // Store the ID in a data attribute for later use - document.getElementById('edit-location-id').value = locationId; - document.getElementById('edit-location-id').setAttribute('data-location-id', locationId); - - // Populate form fields - document.getElementById('edit-first-name').value = location['First Name'] || ''; - document.getElementById('edit-last-name').value = location['Last Name'] || ''; - document.getElementById('edit-location-email').value = location.Email || ''; - document.getElementById('edit-location-phone').value = location.Phone || ''; - document.getElementById('edit-location-unit').value = location['Unit Number'] || ''; - document.getElementById('edit-support-level').value = location['Support Level'] || ''; - document.getElementById('edit-location-address').value = location.Address || ''; - document.getElementById('edit-sign').checked = location.Sign === true || location.Sign === 'true' || location.Sign === 1; - document.getElementById('edit-sign-size').value = location['Sign Size'] || ''; - document.getElementById('edit-location-notes').value = location.Notes || ''; - document.getElementById('edit-location-lat').value = location.latitude || ''; - document.getElementById('edit-location-lng').value = location.longitude || ''; - document.getElementById('edit-geo-location').value = location['Geo-Location'] || ''; - - // Show edit footer - document.getElementById('edit-footer').classList.remove('hidden'); -} - -// Close edit form -function closeEditForm() { - document.getElementById('edit-footer').classList.add('hidden'); - currentEditingLocation = null; -} - -// Handle edit location form submission -async function handleEditLocation(e) { - e.preventDefault(); - - if (!currentEditingLocation) return; - - // Get the stored location ID - const locationIdElement = document.getElementById('edit-location-id'); - const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value; - - if (!locationId || locationId === 'undefined') { - showStatus('Error: Location ID not found', 'error'); - return; - } - - const formData = new FormData(e.target); - const data = {}; - - // Convert form data to object - for (let [key, value] of formData.entries()) { - // Skip the ID field - if (key === 'id' || key === 'Id' || key === 'ID') continue; - - if (value !== null && value !== undefined) { - // Don't skip empty strings - they may be intentional field clearing - data[key] = value.trim(); - } - } - - // Ensure geo-location is set - if (data.latitude && data.longitude) { - data['Geo-Location'] = `${data.latitude};${data.longitude}`; - } - - // Handle checkbox - data.Sign = document.getElementById('edit-sign').checked; - - // Add debugging - console.log('Sending update data for ID:', locationId); - console.log('Update data:', data); - - try { - const response = await fetch(`/api/locations/${locationId}`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - - const responseText = await response.text(); - let result; - - try { - result = JSON.parse(responseText); - } catch (e) { - console.error('Failed to parse response:', responseText); - throw new Error(`Server response error: ${response.status} ${response.statusText}`); - } - - if (result.success) { - showStatus('Location updated successfully!', 'success'); - closeEditForm(); - loadLocations(); - } else { - throw new Error(result.error || 'Failed to update location'); - } - } catch (error) { - console.error('Error updating location:', error); - showStatus(`Update failed: ${error.message}`, 'error'); - } -} - -// Handle delete location -async function handleDeleteLocation() { - if (!currentEditingLocation) return; - - // Get the stored location ID - const locationIdElement = document.getElementById('edit-location-id'); - const locationId = locationIdElement.getAttribute('data-location-id') || locationIdElement.value; - - if (!locationId || locationId === 'undefined') { - showStatus('Error: Location ID not found', 'error'); - return; - } - - if (!confirm('Are you sure you want to delete this location?')) { - return; - } - - try { - const response = await fetch(`/api/locations/${locationId}`, { - method: 'DELETE' - }); - - const result = await response.json(); - - if (result.success) { - showStatus('Location deleted successfully!', 'success'); - closeEditForm(); - loadLocations(); - } else { - throw new Error(result.error || 'Failed to delete location'); - } - } catch (error) { - console.error('Error deleting location:', error); - showStatus(error.message || 'Failed to delete location', 'error'); - } -} - -// Lookup address based on current coordinates -async function lookupAddress(mode) { - let latInput, lngInput, addressInput; - - if (mode === 'add') { - latInput = document.getElementById('location-lat'); - lngInput = document.getElementById('location-lng'); - addressInput = document.getElementById('location-address'); - } else if (mode === 'edit') { - latInput = document.getElementById('edit-location-lat'); - lngInput = document.getElementById('edit-location-lng'); - addressInput = document.getElementById('edit-location-address'); - } else { - console.error('Invalid lookup mode:', mode); - return; - } - - if (!latInput || !lngInput || !addressInput) { - showStatus('Form elements not found', 'error'); - return; - } - - const lat = parseFloat(latInput.value); - const lng = parseFloat(lngInput.value); - - if (isNaN(lat) || isNaN(lng)) { - showStatus('Please enter valid coordinates first', 'warning'); - return; - } - - // Show loading state - const button = mode === 'add' ? - document.getElementById('lookup-address-add-btn') : - document.getElementById('lookup-address-edit-btn'); - - const originalText = button ? button.textContent : ''; - if (button) { - button.disabled = true; - button.textContent = 'Looking up...'; - } - - try { - console.log(`Looking up address for: ${lat}, ${lng}`); - const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Geocoding failed: ${response.status} ${errorText}`); - } - - const data = await response.json(); - - if (data.success && data.data) { - // Use the formatted address or full address - const address = data.data.formattedAddress || data.data.fullAddress; - if (address) { - addressInput.value = address; - showStatus('Address found!', 'success'); - } else { - showStatus('No address found for these coordinates', 'warning'); - } - } else { - showStatus('Address lookup failed', 'warning'); - } - - } catch (error) { - console.error('Address lookup error:', error); - showStatus(`Address lookup failed: ${error.message}`, 'error'); - } finally { - // Restore button state - if (button) { - button.disabled = false; - button.textContent = originalText; - } - } -} - -// Toggle fullscreen -function toggleFullscreen() { - const app = document.getElementById('app'); - const btn = document.getElementById('fullscreen-btn'); - const mobileBtn = document.getElementById('mobile-fullscreen-btn'); - - if (!document.fullscreenElement) { - app.requestFullscreen().then(() => { - app.classList.add('fullscreen'); - - // Update desktop button - if (btn) { - btn.innerHTML = 'Exit Fullscreen'; - } - - // Update mobile button - if (mobileBtn) { - mobileBtn.innerHTML = '◱'; - mobileBtn.title = 'Exit Fullscreen'; - } - }).catch(err => { - console.error('Error entering fullscreen:', err); - showStatus('Unable to enter fullscreen', 'error'); - }); - } else { - document.exitFullscreen().then(() => { - app.classList.remove('fullscreen'); - - // Update desktop button - if (btn) { - btn.innerHTML = 'Fullscreen'; - } - - // Update mobile button - if (mobileBtn) { - mobileBtn.innerHTML = '⛶'; - mobileBtn.title = 'Fullscreen'; - } - }); - } -} - -// Update location count -function updateLocationCount(count) { - const countElement = document.getElementById('location-count'); - const mobileCountElement = document.getElementById('mobile-location-count'); - - const countText = `${count} location${count !== 1 ? 's' : ''}`; - - if (countElement) { - countElement.textContent = countText; - } - if (mobileCountElement) { - mobileCountElement.textContent = countText; - } -} - -// Setup auto-refresh -function setupAutoRefresh() { - refreshInterval = setInterval(() => { - loadLocations(); - }, CONFIG.REFRESH_INTERVAL); -} - -// Show status message -function showStatus(message, type = 'info') { - const container = document.getElementById('status-container'); - - const messageDiv = document.createElement('div'); - messageDiv.className = `status-message ${type}`; - messageDiv.textContent = message; - - container.appendChild(messageDiv); - - // Auto-remove after 5 seconds - setTimeout(() => { - messageDiv.remove(); - }, 5000); -} - -// Hide loading overlay -function hideLoading() { - const loading = document.getElementById('loading'); - if (loading) { - loading.classList.add('hidden'); - } -} - -// Escape HTML for security -function escapeHtml(text) { - if (text === null || text === undefined) { - return ''; - } - const div = document.createElement('div'); - div.textContent = String(text); - return div.innerHTML; -} - -// Clean up on page unload -window.addEventListener('beforeunload', () => { - if (refreshInterval) { - clearInterval(refreshInterval); - } -}); diff --git a/map/app/server copy.js b/map/app/server copy.js deleted file mode 100644 index 0c4e7a1..0000000 --- a/map/app/server copy.js +++ /dev/null @@ -1,2051 +0,0 @@ -const express = require('express'); -const axios = require('axios'); -const cors = require('cors'); -const helmet = require('helmet'); -const rateLimit = require('express-rate-limit'); -const winston = require('winston'); -const path = require('path'); -const session = require('express-session'); -const cookieParser = require('cookie-parser'); -require('dotenv').config(); - -// Import geocoding routes -const geocodingRoutes = require('./routes/geocoding'); - -// Import QR code service (only for local generation, no upload) -const { generateQRCode } = require('./services/qrcode'); - -// Parse project and table IDs from view URL -function parseNocoDBUrl(url) { - if (!url) return { projectId: null, tableId: null }; - - // Pattern to match NocoDB URLs - const patterns = [ - /#\/nc\/([^\/]+)\/([^\/\?#]+)/, // matches #/nc/PROJECT_ID/TABLE_ID (dashboard URLs) - /\/nc\/([^\/]+)\/([^\/\?#]+)/, // matches /nc/PROJECT_ID/TABLE_ID - /project\/([^\/]+)\/table\/([^\/\?#]+)/, // alternative pattern - ]; - - for (const pattern of patterns) { - const match = url.match(pattern); - if (match) { - return { - projectId: match[1], - tableId: match[2] - }; - } - } - - return { projectId: null, tableId: null }; -} - -// Add this helper function near the top of the file after the parseNocoDBUrl function -function syncGeoFields(data) { - // If we have latitude and longitude but no Geo-Location, create it - if (data.latitude && data.longitude && !data['Geo-Location']) { - const lat = parseFloat(data.latitude); - const lng = parseFloat(data.longitude); - if (!isNaN(lat) && !isNaN(lng)) { - data['Geo-Location'] = `${lat};${lng}`; data.geodata = `${lat};${lng}`; } - } - - // If we have Geo-Location but no lat/lng, parse it - else if (data['Geo-Location'] && (!data.latitude || !data.longitude)) { - const geoLocation = data['Geo-Location'].toString(); - - // Try semicolon-separated first - let parts = geoLocation.split(';'); - if (parts.length === 2) { - const lat = parseFloat(parts[0].trim()); - const lng = parseFloat(parts[1].trim()); - if (!isNaN(lat) && !isNaN(lng)) { - data.latitude = lat; - data.longitude = lng; - data.geodata = `${lat};${lng}`; - return data; - } - } - - // Try comma-separated - parts = geoLocation.split(','); - if (parts.length === 2) { - const lat = parseFloat(parts[0].trim()); - const lng = parseFloat(parts[1].trim()); - if (!isNaN(lat) && !isNaN(lng)) { - data.latitude = lat; - data.longitude = lng; - data.geodata = `${lat};${lng}`; - // Normalize Geo-Location to semicolon format for NocoDB GeoData - data['Geo-Location'] = `${lat};${lng}`; - } - } - } - - return data; -} - -// Auto-parse IDs if view URL is provided -if (process.env.NOCODB_VIEW_URL && (!process.env.NOCODB_PROJECT_ID || !process.env.NOCODB_TABLE_ID)) { - const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_VIEW_URL); - - if (projectId && tableId) { - process.env.NOCODB_PROJECT_ID = projectId; - process.env.NOCODB_TABLE_ID = tableId; - console.log(`Auto-parsed from URL - Project ID: ${projectId}, Table ID: ${tableId}`); - } -} - -// Auto-parse login sheet ID if URL is provided -let LOGIN_SHEET_ID = null; -if (process.env.NOCODB_LOGIN_SHEET) { - // Check if it's a URL or just an ID - if (process.env.NOCODB_LOGIN_SHEET.startsWith('http')) { - const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_LOGIN_SHEET); - if (projectId && tableId) { - LOGIN_SHEET_ID = tableId; - console.log(`Auto-parsed login sheet ID from URL: ${LOGIN_SHEET_ID}`); - } else { - console.error('Could not parse login sheet URL'); - } - } else { - // Assume it's already just the ID - LOGIN_SHEET_ID = process.env.NOCODB_LOGIN_SHEET; - console.log(`Using login sheet ID: ${LOGIN_SHEET_ID}`); - } -} - -// Auto-parse settings sheet ID if URL is provided -let SETTINGS_SHEET_ID = null; -if (process.env.NOCODB_SETTINGS_SHEET) { - // Check if it's a URL or just an ID - if (process.env.NOCODB_SETTINGS_SHEET.startsWith('http')) { - const { projectId, tableId } = parseNocoDBUrl(process.env.NOCODB_SETTINGS_SHEET); - if (projectId && tableId) { - SETTINGS_SHEET_ID = tableId; - console.log(`Auto-parsed settings sheet ID from URL: ${SETTINGS_SHEET_ID}`); - } else { - console.error('Could not parse settings sheet URL'); - } - } else { - // Assume it's already just the ID - SETTINGS_SHEET_ID = process.env.NOCODB_SETTINGS_SHEET; - console.log(`Using settings sheet ID: ${SETTINGS_SHEET_ID}`); - } -} - -// Configure logger -const logger = winston.createLogger({ - level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ), - transports: [ - new winston.transports.Console({ - format: winston.format.simple() - }) - ] -}); - -// Initialize Express app -const app = express(); -const PORT = process.env.PORT || 3000; - -// Session configuration -app.use(cookieParser()); - -// Determine if we should use secure cookies based on environment and request -const isProduction = process.env.NODE_ENV === 'production'; - -// Cookie configuration function -const getCookieConfig = (req) => { - const host = req?.get('host') || ''; - const isLocalhost = host.includes('localhost') || host.includes('127.0.0.1') || host.match(/^\d+\.\d+\.\d+\.\d+/); - - const config = { - httpOnly: true, - maxAge: 24 * 60 * 60 * 1000, // 24 hours - sameSite: 'lax', - secure: false, // Default to false - domain: undefined // Default to no domain restriction - }; - - // Only set domain and secure for production non-localhost access - if (isProduction && !isLocalhost && process.env.COOKIE_DOMAIN) { - // Check if the request is coming from a subdomain of COOKIE_DOMAIN - const cookieDomain = process.env.COOKIE_DOMAIN.replace(/^\./, ''); if (host.includes(cookieDomain)) { - config.domain = process.env.COOKIE_DOMAIN; - config.secure = true; // Enable secure cookies for production - } - } - - return config; -}; - -app.use(session({ - secret: process.env.SESSION_SECRET || 'your-secret-key-change-in-production', - resave: false, - saveUninitialized: false, - cookie: getCookieConfig(), - name: 'nocodb-map-session', - genid: (req) => { - // Use a custom session ID generator to avoid conflicts - return require('crypto').randomBytes(16).toString('hex'); - } -})); - -// Security middleware -app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com"], - scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.jsdelivr.net"], - imgSrc: ["'self'", "data:", "https://*.tile.openstreetmap.org", "https://unpkg.com"], - connectSrc: ["'self'"] - } - } -})); - -// CORS configuration -app.use(cors({ - origin: function(origin, callback) { - // Allow requests with no origin (like mobile apps or curl requests) - if (!origin) return callback(null, true); - - const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || []; - if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) { - callback(null, true); - } else { - callback(new Error('Not allowed by CORS')); - } - }, - credentials: true, - methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], - allowedHeaders: ['Content-Type', 'Authorization'] -})); - -// Trust proxy for Cloudflare -app.set('trust proxy', true); - -// Rate limiting with Cloudflare support -const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: 100, - keyGenerator: (req) => { - // Use CF-Connecting-IP header if available (Cloudflare) - return req.headers['cf-connecting-ip'] || - req.headers['x-forwarded-for']?.split(',')[0] || - req.ip; - }, - standardHeaders: true, - legacyHeaders: false, -}); - -const strictLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: 20, - keyGenerator: (req) => { - return req.headers['cf-connecting-ip'] || - req.headers['x-forwarded-for']?.split(',')[0] || - req.ip; - } -}); - -const authLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, - max: process.env.NODE_ENV === 'production' ? 10 : 50, // Increase limit slightly - message: 'Too many login attempts, please try again later.', - keyGenerator: (req) => { - return req.headers['cf-connecting-ip'] || - req.headers['x-forwarded-for']?.split(',')[0] || - req.ip; - }, - standardHeaders: true, - legacyHeaders: false, -}); - -// Middleware -app.use(express.json({ limit: '10mb' })); - -// Authentication middleware -const requireAuth = (req, res, next) => { - if (req.session && req.session.authenticated) { - next(); - } else { - if (req.xhr || req.headers.accept?.indexOf('json') > -1) { - res.status(401).json({ success: false, error: 'Authentication required' }); - } else { - res.redirect('/login.html'); - } - } -}; - -// Admin middleware -const requireAdmin = (req, res, next) => { - if (req.session && req.session.authenticated && req.session.isAdmin) { - next(); - } else { - if (req.xhr || req.headers.accept?.indexOf('json') > -1) { - res.status(403).json({ success: false, error: 'Admin access required' }); - } else { - res.redirect('/login.html'); - } - } -}; - -// Serve login page without authentication -app.get('/login.html', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'login.html')); -}); - -// Auth routes (no authentication required) -app.post('/api/auth/login', authLimiter, async (req, res) => { - try { - // Log request details for debugging - logger.info('Login attempt:', { - email: req.body.email, - ip: req.ip, - cfIp: req.headers['cf-connecting-ip'], - forwardedFor: req.headers['x-forwarded-for'], - userAgent: req.headers['user-agent'] - }); - - const { email, password } = req.body; - - if (!email || !password) { - return res.status(400).json({ - success: false, - error: 'Email and password are required' - }); - } - - // Validate email format - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(email)) { - return res.status(400).json({ - success: false, - error: 'Invalid email format' - }); - } - - // Check if login sheet is configured - if (!LOGIN_SHEET_ID) { - logger.error('NOCODB_LOGIN_SHEET not configured or could not be parsed'); - return res.status(500).json({ - success: false, - error: 'Authentication system not properly configured' - }); - } - - // Fetch user from NocoDB - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`; - - logger.info(`Checking authentication for email: ${email}`); - logger.debug(`Using login sheet API: ${url}`); - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - where: `(Email,eq,${email})`, - limit: 1 - } - }); - - const users = response.data.list || []; - - if (users.length === 0) { - logger.warn(`No user found with email: ${email}`); - return res.status(401).json({ - success: false, - error: 'Invalid email or password' - }); - } - - const user = users[0]; - - // Check password (plain text comparison for now) - if (user.Password !== password && user.password !== password) { - logger.warn(`Invalid password for email: ${email}`); - return res.status(401).json({ - success: false, - error: 'Invalid email or password' - }); - } - - // Update last login time - try { - const updateUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}/${user.Id || user.id || user.ID}`; - await axios.patch(updateUrl, { - 'Last Login': new Date().toISOString(), - last_login: new Date().toISOString() - }, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - }); - } catch (updateError) { - logger.warn('Failed to update last login time:', updateError.message); - // Don't fail the login if we can't update last login time - } - - // Set session including admin status - req.session.authenticated = true; - req.session.userEmail = email; - req.session.userName = user.Name || user.name || email; - req.session.isAdmin = user.Admin === true || user.Admin === 1 || user.admin === true || user.admin === 1; - req.session.userId = user.Id || user.id || user.ID; - - // Force session save before sending response - req.session.save((err) => { - if (err) { - logger.error('Session save error:', err); - return res.status(500).json({ - success: false, - error: 'Session error. Please try again.' - }); - } - - logger.info(`User authenticated: ${email}, Admin: ${req.session.isAdmin}`); - - res.json({ - success: true, - message: 'Login successful', - user: { - email: email, - name: req.session.userName, - isAdmin: req.session.isAdmin - } - }); - }); - - } catch (error) { - logger.error('Login error:', error.message); - res.status(500).json({ - success: false, - error: 'Authentication service error. Please try again later.' - }); - } -}); - -app.get('/api/auth/check', (req, res) => { - res.json({ - authenticated: req.session?.authenticated || false, - user: req.session?.authenticated ? { - email: req.session.userEmail, - name: req.session.userName, - isAdmin: req.session.isAdmin || false - } : null - }); -}); - -app.post('/api/auth/logout', (req, res) => { - req.session.destroy((err) => { - if (err) { - logger.error('Logout error:', err); - return res.status(500).json({ - success: false, - error: 'Logout failed' - }); - } - res.json({ - success: true, - message: 'Logged out successfully' - }); - }); -}); - -// Admin routes -// Serve admin page (protected) -app.get('/admin.html', requireAdmin, (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'admin.html')); -}); - -// Add admin API endpoint to update start location -app.post('/api/admin/start-location', requireAdmin, async (req, res) => { - try { - const { latitude, longitude, zoom } = req.body; - - // Validate input - if (!latitude || !longitude) { - return res.status(400).json({ - success: false, - error: 'Latitude and longitude are required' - }); - } - - const lat = parseFloat(latitude); - const lng = parseFloat(longitude); - const mapZoom = parseInt(zoom) || 11; - - if (isNaN(lat) || isNaN(lng) || lat < -90 || lat > 90 || lng < -180 || lng > 180) { - return res.status(400).json({ - success: false, - error: 'Invalid coordinates' - }); - } - - if (!SETTINGS_SHEET_ID) { - return res.status(500).json({ - success: false, - error: 'Settings sheet not configured' - }); - } - - // Get the most recent settings to preserve ALL fields - let currentConfig = {}; - try { - const response = await axios.get( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - sort: '-created_at', - limit: 1 - } - } - ); - - if (response.data?.list && response.data.list.length > 0) { - currentConfig = response.data.list[0]; - logger.info('Loaded existing settings for preservation'); - } - } catch (e) { - logger.warn('Could not load existing settings, using defaults:', e.message); - } - - // Create new settings row with updated location but preserve everything else - const settingData = { - created_at: new Date().toISOString(), - created_by: req.session.userEmail, - // Map location fields (what we're updating) - 'Geo-Location': `${lat};${lng}`, - latitude: lat, - longitude: lng, - zoom: mapZoom, - // Preserve all walk sheet fields - walk_sheet_title: currentConfig.walk_sheet_title || 'Campaign Walk Sheet', - walk_sheet_subtitle: currentConfig.walk_sheet_subtitle || 'Door-to-Door Canvassing Form', - walk_sheet_footer: currentConfig.walk_sheet_footer || 'Thank you for your support!', - qr_code_1_url: currentConfig.qr_code_1_url || '', - qr_code_1_label: currentConfig.qr_code_1_label || '', - qr_code_2_url: currentConfig.qr_code_2_url || '', - qr_code_2_label: currentConfig.qr_code_2_label || '', - qr_code_3_url: currentConfig.qr_code_3_url || '', - qr_code_3_label: currentConfig.qr_code_3_label || '' - }; - - const createUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; - - const createResponse = await axios.post(createUrl, settingData, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - }); - - logger.info('Created new settings row with start location'); - - res.json({ - success: true, - message: 'Start location saved successfully', - location: { latitude: lat, longitude: lng, zoom: mapZoom }, - settingsId: createResponse.data.id || createResponse.data.Id || createResponse.data.ID - }); - - } catch (error) { - logger.error('Error updating start location:', error); - res.status(500).json({ - success: false, - error: error.message || 'Failed to update start location' - }); - } -}); - -// Get current start location (fetch most recent) -app.get('/api/admin/start-location', requireAdmin, async (req, res) => { - try { - // First try to get from database - if (SETTINGS_SHEET_ID) { - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - }, - params: { - sort: '-created_at', // Get most recent - limit: 1 - } - }); - - const settings = response.data.list || []; - - if (settings.length > 0) { - const setting = settings[0]; - - // Try to extract coordinates - let lat, lng, zoom; - - if (setting['Geo-Location']) { - const parts = setting['Geo-Location'].split(';'); - if (parts.length === 2) { - lat = parseFloat(parts[0]); - lng = parseFloat(parts[1]); - } - } else if (setting.latitude && setting.longitude) { - lat = parseFloat(setting.latitude); - lng = parseFloat(setting.longitude); - } - - zoom = parseInt(setting.zoom) || 11; - - if (lat && lng && !isNaN(lat) && !isNaN(lng)) { - return res.json({ - success: true, - location: { - latitude: lat, - longitude: lng, - zoom: zoom - }, - source: 'database', - settingsId: setting.id || setting.Id || setting.ID, - lastUpdated: setting.created_at - }); - } - } - } - - // Fallback to environment variables - res.json({ - success: true, - location: { - latitude: 53.5461, - longitude: -113.4938, - zoom: 11 - }, - source: 'defaults' - }); - - } catch (error) { - logger.error('Error fetching start location:', error); - - // Return defaults on error - res.json({ - success: true, - location: { - latitude: 53.5461, - longitude: -113.4938, - zoom: 11 - }, - source: 'defaults' - }); - } -}); - -// Update the public config endpoint similarly -app.get('/api/config/start-location', async (req, res) => { - try { - // Try to get from database first - if (SETTINGS_SHEET_ID) { - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; - - logger.info(`Fetching start location from settings sheet: ${SETTINGS_SHEET_ID}`); - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - }, - params: { - sort: '-created_at', // Get most recent - limit: 1 - } - }); - - const settings = response.data.list || []; - - if (settings.length > 0) { - const setting = settings[0]; - logger.info('Found settings row:', { - id: setting.id || setting.Id || setting.ID, - hasGeoLocation: !!setting['Geo-Location'], - hasLatLng: !!(setting.latitude && setting.longitude) - }); - - // Try to extract coordinates - let lat, lng, zoom; - - if (setting['Geo-Location']) { - const parts = setting['Geo-Location'].split(';'); - if (parts.length === 2) { - lat = parseFloat(parts[0]); - lng = parseFloat(parts[1]); - } - } else if (setting.latitude && setting.longitude) { - lat = parseFloat(setting.latitude); - lng = parseFloat(setting.longitude); - } - - zoom = parseInt(setting.zoom) || 11; - - if (lat && lng && !isNaN(lat) && !isNaN(lng)) { - logger.info(`Returning location from database: ${lat}, ${lng}, zoom: ${zoom}`); - return res.json({ - latitude: lat, - longitude: lng, - zoom: zoom - }); - } - } else { - logger.info('No settings found in database'); - } - } else { - logger.info('Settings sheet not configured, using defaults'); - } - } catch (error) { - logger.error('Error fetching config start location:', error); - } - - // Return defaults - const defaultLat = parseFloat(process.env.DEFAULT_LAT) || 53.5461; - const defaultLng = parseFloat(process.env.DEFAULT_LNG) || -113.4938; - const defaultZoom = parseInt(process.env.DEFAULT_ZOOM) || 11; - - logger.info(`Using default start location: ${defaultLat}, ${defaultLng}, zoom: ${defaultZoom}`); - - res.json({ - latitude: defaultLat, - longitude: defaultLng, - zoom: defaultZoom - }); -}); - -// Get walk sheet configuration (load most recent) -app.get('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { - try { - // Default configuration - const defaultConfig = { - walk_sheet_title: 'Campaign Walk Sheet', - walk_sheet_subtitle: 'Door-to-Door Canvassing Form', - walk_sheet_footer: 'Thank you for your support!', - qr_code_1_url: '', - qr_code_1_label: '', - qr_code_2_url: '', - qr_code_2_label: '', - qr_code_3_url: '', - qr_code_3_label: '' - }; - - if (!SETTINGS_SHEET_ID) { - logger.warn('SETTINGS_SHEET_ID not configured, returning defaults'); - return res.json({ - success: true, - config: defaultConfig, - source: 'defaults', - message: 'Settings sheet not configured, using defaults' - }); - } - - // Get ALL settings rows and find the most recent one with walk sheet data - const response = await axios.get( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - sort: '-created_at', // Sort by created_at descending - limit: 20 // Get more records to find one with walk sheet data - } - } - ); - - logger.debug('GET Settings response structure:', JSON.stringify(response.data, null, 2)); - - if (!response.data?.list || response.data.list.length === 0) { - logger.info('No settings found in database, returning defaults'); - return res.json({ - success: true, - config: defaultConfig, - source: 'defaults', - message: 'No settings found in database' - }); - } - - // Find the first row that has walk sheet configuration (not just location data) - const settingsRow = response.data.list.find(row => - row.walk_sheet_title || - row.walk_sheet_subtitle || - row.walk_sheet_footer || - row.qr_code_1_url || - row.qr_code_2_url || - row.qr_code_3_url - ) || response.data.list[0]; // Fallback to most recent if none have walk sheet data - - const walkSheetConfig = { - walk_sheet_title: settingsRow.walk_sheet_title || settingsRow['Walk Sheet Title'] || defaultConfig.walk_sheet_title, - walk_sheet_subtitle: settingsRow.walk_sheet_subtitle || settingsRow['Walk Sheet Subtitle'] || defaultConfig.walk_sheet_subtitle, - walk_sheet_footer: settingsRow.walk_sheet_footer || settingsRow['Walk Sheet Footer'] || defaultConfig.walk_sheet_footer, - qr_code_1_url: settingsRow.qr_code_1_url || settingsRow['QR Code 1 URL'] || defaultConfig.qr_code_1_url, - qr_code_1_label: settingsRow.qr_code_1_label || settingsRow['QR Code 1 Label'] || defaultConfig.qr_code_1_label, - qr_code_2_url: settingsRow.qr_code_2_url || settingsRow['QR Code 2 URL'] || defaultConfig.qr_code_2_url, - qr_code_2_label: settingsRow.qr_code_2_label || settingsRow['QR Code 2 Label'] || defaultConfig.qr_code_2_label, - qr_code_3_url: settingsRow.qr_code_3_url || settingsRow['QR Code 3 URL'] || defaultConfig.qr_code_3_url, - qr_code_3_label: settingsRow.qr_code_3_label || settingsRow['QR Code 3 Label'] || defaultConfig.qr_code_3_label - }; - - logger.info(`Retrieved walk sheet config from database (ID: ${settingsRow.Id || settingsRow.id})`); - res.json({ - success: true, - config: walkSheetConfig, - source: 'database', - settingsId: settingsRow.id || settingsRow.Id || settingsRow.ID, - lastUpdated: settingsRow.created_at || settingsRow.updated_at - }); - - } catch (error) { - logger.error('Failed to get walk sheet config:', error); - logger.error('Error details:', error.response?.data || error.message); - - // Return defaults if there's an error - res.json({ - success: true, - config: { - walk_sheet_title: 'Campaign Walk Sheet', - walk_sheet_subtitle: 'Door-to-Door Canvassing Form', - walk_sheet_footer: 'Thank you for your support!', - qr_code_1_url: '', - qr_code_1_label: '', - qr_code_2_url: '', - qr_code_2_label: '', - qr_code_3_url: '', - qr_code_3_label: '' - }, - source: 'defaults', - message: 'Error retrieving from database, using defaults', - error: error.message - }); - } -}); - -// Save walk sheet configuration (always create new row) -app.post('/api/admin/walk-sheet-config', requireAdmin, async (req, res) => { - try { - if (!SETTINGS_SHEET_ID) { - return res.status(500).json({ - success: false, - error: 'Settings sheet not configured' - }); - } - - logger.info('Using SETTINGS_SHEET_ID:', SETTINGS_SHEET_ID); - - const config = req.body; - logger.info('Received walk sheet config:', JSON.stringify(config, null, 2)); - - // Validate input - if (!config || typeof config !== 'object') { - return res.status(400).json({ - success: false, - error: 'Invalid configuration data' - }); - } - - // Get the most recent settings to preserve ALL fields - let currentConfig = {}; - try { - const response = await axios.get( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - sort: '-created_at', - limit: 1 - } - } - ); - - if (response.data?.list && response.data.list.length > 0) { - currentConfig = response.data.list[0]; - logger.info('Loaded existing settings for preservation'); - } - } catch (e) { - logger.warn('Could not load existing settings, using defaults:', e.message); - } - - const userEmail = req.session.userEmail; - const timestamp = new Date().toISOString(); - - // Prepare data for saving - include ALL fields - const walkSheetData = { - created_at: timestamp, - created_by: userEmail, - // Preserve map location fields from last saved config - 'Geo-Location': currentConfig['Geo-Location'] || currentConfig.geodata || '53.5461;-113.4938', - latitude: currentConfig.latitude || 53.5461, - longitude: currentConfig.longitude || -113.4938, - zoom: currentConfig.zoom || 11, - // Walk sheet fields (what we're updating) - walk_sheet_title: (config.walk_sheet_title || '').toString().trim(), - walk_sheet_subtitle: (config.walk_sheet_subtitle || '').toString().trim(), - walk_sheet_footer: (config.walk_sheet_footer || '').toString().trim(), - 'Walk Sheet Title': (config.walk_sheet_title || '').toString().trim(), - 'Walk Sheet Subtitle': (config.walk_sheet_subtitle || '').toString().trim(), - 'Walk Sheet Footer': (config.walk_sheet_footer || '').toString().trim(), - qr_code_1_url: validateUrl(config.qr_code_1_url), - qr_code_1_label: (config.qr_code_1_label || '').toString().trim(), - qr_code_2_url: validateUrl(config.qr_code_2_url), - qr_code_2_label: (config.qr_code_2_label || '').toString().trim(), - qr_code_3_url: validateUrl(config.qr_code_3_url), - qr_code_3_label: (config.qr_code_3_label || '').toString().trim(), - 'QR Code 1 URL': validateUrl(config.qr_code_1_url), - 'QR Code 1 Label': (config.qr_code_1_label || '').toString().trim(), - 'QR Code 2 URL': validateUrl(config.qr_code_2_url), - 'QR Code 2 Label': (config.qr_code_2_label || '').toString().trim(), - 'QR Code 3 URL': validateUrl(config.qr_code_3_url), - 'QR Code 3 Label': (config.qr_code_3_label || '').toString().trim() - }; - - logger.info('Prepared walk sheet data for saving:', JSON.stringify(walkSheetData, null, 2)); - - // Create new settings row - const response = await axios.post( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - walkSheetData, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - } - ); - - logger.info('NocoDB create response:', JSON.stringify(response.data, null, 2)); - - const newId = response.data.id || response.data.Id || response.data.ID; - - res.json({ - success: true, - message: 'Walk sheet configuration saved successfully', - config: walkSheetData, - settingsId: newId, - timestamp: timestamp - }); - - } catch (error) { - logger.error('Failed to save walk sheet config:', error); - logger.error('Error response:', error.response?.data); - logger.error('Request URL:', error.config?.url); - - // Provide more detailed error information - let errorMessage = 'Failed to save walk sheet configuration'; - let errorDetails = null; - - if (error.response?.data) { - if (error.response.data.message) { - errorMessage = error.response.data.message; - } - if (error.response.data.errors) { - errorDetails = error.response.data.errors; - } - } - - res.status(500).json({ - success: false, - error: errorMessage, - details: errorDetails, - timestamp: new Date().toISOString() - }); - } -}); - -// Helper function to validate URLs -function validateUrl(url) { - if (!url || typeof url !== 'string') { - return ''; - } - - const trimmed = url.trim(); - if (!trimmed) { - return ''; - } - - // Basic URL validation - try { - new URL(trimmed); - return trimmed; - } catch (e) { - // If not a valid URL, check if it's a relative path or missing protocol - if (trimmed.startsWith('/') || !trimmed.includes('://')) { - // For relative paths or missing protocol, return as-is - return trimmed; - } - logger.warn('Invalid URL provided:', trimmed); - return ''; - } -} - -// Debug session endpoint -app.get('/api/debug/session', (req, res) => { - res.json({ - sessionID: req.sessionID, - session: req.session, - cookies: req.cookies, - authenticated: req.session?.authenticated || false - }); -}); - -// Serve static files with authentication for main app -app.use(express.static(path.join(__dirname, 'public'), { - index: false // Don't serve index.html automatically -})); - -// Protect main app routes -app.get('/', requireAuth, (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); -}); - -// Add geocoding routes (protected) -app.use('/api/geocode', requireAuth, geocodingRoutes); - -// Apply rate limiting to API routes -app.use('/api/', limiter); - -// Health check endpoint (no auth required) -app.get('/health', (req, res) => { - res.json({ - status: 'healthy', - timestamp: new Date().toISOString(), - version: process.env.npm_package_version || '1.0.0' - }); -}); - -// Configuration validation endpoint (protected) -app.get('/api/config-check', requireAuth, (req, res) => { - const config = { - hasApiUrl: !!process.env.NOCODB_API_URL, - hasApiToken: !!process.env.NOCODB_API_TOKEN, - hasProjectId: !!process.env.NOCODB_PROJECT_ID, - hasTableId: !!process.env.NOCODB_TABLE_ID, - hasLoginSheet: !!LOGIN_SHEET_ID, - hasSettingsSheet: !!SETTINGS_SHEET_ID, - projectId: process.env.NOCODB_PROJECT_ID, - tableId: process.env.NOCODB_TABLE_ID, - loginSheet: LOGIN_SHEET_ID, - loginSheetConfigured: process.env.NOCODB_LOGIN_SHEET, - settingsSheet: SETTINGS_SHEET_ID, - settingsSheetConfigured: process.env.NOCODB_SETTINGS_SHEET, - nodeEnv: process.env.NODE_ENV - }; - - const isConfigured = config.hasApiUrl && config.hasApiToken && config.hasProjectId && config.hasTableId; - - res.json({ - configured: isConfigured, - ...config - }); -}); - -// All other API routes require authentication -app.use('/api/*', requireAuth); - -// Get all locations from NocoDB -app.get('/api/locations', async (req, res) => { - try { - const { limit = 1000, offset = 0, where } = req.query; - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`; - - const params = new URLSearchParams({ - limit, - offset - }); - - if (where) { - params.append('where', where); - } - - logger.info(`Fetching locations from NocoDB: ${url}`); - - const response = await axios.get(`${url}?${params}`, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - timeout: 10000 // 10 second timeout - }); - - // Process locations to ensure they have required fields - const locations = response.data.list || []; - - // Log the structure of the first location to debug ID field name - if (locations.length > 0) { - const sampleLocation = locations[0]; - logger.info('Sample location structure:', { - keys: Object.keys(sampleLocation), - idFields: { - 'Id': sampleLocation.Id, - 'id': sampleLocation.id, - 'ID': sampleLocation.ID, - '_id': sampleLocation._id - } - }); - } - - const validLocations = locations.filter(loc => { - // Apply geo field synchronization to each location - loc = syncGeoFields(loc); - - // Check if location has valid coordinates - if (loc.latitude && loc.longitude) { - return true; - } - - // Try to parse from geodata column (semicolon-separated) - if (loc.geodata && typeof loc.geodata === 'string') { - const parts = loc.geodata.split(';'); - if (parts.length === 2) { - loc.latitude = parseFloat(parts[0]); - loc.longitude = parseFloat(parts[1]); - return !isNaN(loc.latitude) && !isNaN(loc.longitude); - } - } - - return false; - }); - - logger.info(`Retrieved ${validLocations.length} valid locations out of ${locations.length} total`); - - res.json({ - success: true, - count: validLocations.length, - total: response.data.pageInfo?.totalRows || validLocations.length, - locations: validLocations - }); - - } catch (error) { - logger.error('Error fetching locations:', error.message); - - if (error.response) { - // NocoDB API error - res.status(error.response.status).json({ - success: false, - error: 'Failed to fetch data from NocoDB', - details: error.response.data - }); - } else if (error.code === 'ECONNABORTED') { - // Timeout - res.status(504).json({ - success: false, - error: 'Request timeout' - }); - } else { - // Other errors - res.status(500).json({ - success: false, - error: 'Internal server error' - }); - } - } -}); - -// Get single location by ID -app.get('/api/locations/:id', async (req, res) => { - try { - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${req.params.id}`; - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - } - }); - - res.json({ - success: true, - location: response.data - }); - - } catch (error) { - logger.error(`Error fetching location ${req.params.id}:`, error.message); - res.status(error.response?.status || 500).json({ - success: false, - error: 'Failed to fetch location' - }); - } -}); - -// Create new location -app.post('/api/locations', strictLimiter, async (req, res) => { - try { - let locationData = { ...req.body }; - - // Sync geo fields before validation - locationData = syncGeoFields(locationData); - - const { latitude, longitude, ...additionalData } = locationData; - - // Validate coordinates - if (!latitude || !longitude) { - return res.status(400).json({ - success: false, - error: 'Latitude and longitude are required' - }); - } - - const lat = parseFloat(latitude); - const lng = parseFloat(longitude); - - if (isNaN(lat) || isNaN(lng)) { - return res.status(400).json({ - success: false, - error: 'Invalid coordinate values' - }); - } - - if (lat < -90 || lat > 90) { - return res.status(400).json({ - success: false, - error: 'Latitude must be between -90 and 90' - }); - } - - if (lng < -180 || lng > 180) { - return res.status(400).json({ - success: false, - error: 'Longitude must be between -180 and 180' - }); - } - - // Check bounds if configured - if (process.env.BOUND_NORTH) { - const bounds = { - north: parseFloat(process.env.BOUND_NORTH), - south: parseFloat(process.env.BOUND_SOUTH), - east: parseFloat(process.env.BOUND_EAST), - west: parseFloat(process.env.BOUND_WEST) - }; - - if (lat > bounds.north || lat < bounds.south || - lng > bounds.east || lng < bounds.west) { - return res.status(400).json({ - success: false, - error: 'Location is outside allowed bounds' - }); - } - } - - // Format geodata in both formats for compatibility - const geodata = `${lat};${lng}`; - const geoLocation = `${lat};${lng}`; // Use semicolon format for NocoDB GeoData column - - // Prepare data for NocoDB - const finalData = { - geodata, - 'Geo-Location': geoLocation, - latitude: lat, - longitude: lng, - ...additionalData, - created_at: new Date().toISOString(), - created_by: req.session.userEmail // Track who created the location - }; - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`; - - logger.info('Creating new location:', { lat, lng }); - - const response = await axios.post(url, finalData, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - }); - - logger.info('Location created successfully:', response.data.id); - - res.status(201).json({ - success: true, - location: response.data - }); - - } catch (error) { - logger.error('Error creating location:', error.message); - - if (error.response) { - res.status(error.response.status).json({ - success: false, - error: 'Failed to save location to NocoDB', - details: error.response.data - }); - } else { - res.status(500).json({ - success: false, - error: 'Internal server error' - }); - } - } -}); - -// Update location -app.put('/api/locations/:id', strictLimiter, async (req, res) => { - try { - const locationId = req.params.id; - - // Validate ID - if (!locationId || locationId === 'undefined' || locationId === 'null') { - return res.status(400).json({ - success: false, - error: 'Invalid location ID' - }); - } - - let updateData = { ...req.body }; - - // Remove ID from update data to avoid conflicts - use the correct field name - delete updateData.ID; - delete updateData.Id; - delete updateData.id; - delete updateData._id; - - // Sync geo fields - updateData = syncGeoFields(updateData); - - updateData.last_updated_at = new Date().toISOString(); - updateData.last_updated_by = req.session.userEmail; // Track who updated - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${locationId}`; - - logger.info(`Updating location ${locationId} by ${req.session.userEmail}`); - logger.debug('Update data:', updateData); - - const response = await axios.patch(url, updateData, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - }); - - res.json({ - success: true, - location: response.data - }); - - } catch (error) { - logger.error(`Error updating location ${req.params.id}:`, error.message); - if (error.response) { - logger.error('Error response:', error.response.data); - } - - res.status(error.response?.status || 500).json({ - success: false, - error: 'Failed to update location', - details: error.response?.data?.message || error.message - }); - } -}); - -// Delete location -app.delete('/api/locations/:id', strictLimiter, async (req, res) => { - try { - const locationId = req.params.id; - - // Validate ID - if (!locationId || locationId === 'undefined' || locationId === 'null') { - return res.status(400).json({ - success: false, - error: 'Invalid location ID' - }); - } - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}/${locationId}`; - - await axios.delete(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - } - }); - - logger.info(`Location ${locationId} deleted by ${req.session.userEmail}`); - - res.json({ - success: true, - message: 'Location deleted successfully' - }); - - } catch (error) { - logger.error(`Error deleting location ${req.params.id}:`, error.message); - res.status(error.response?.status || 500).json({ - success: false, - error: 'Failed to delete location' - }); - } -}); - -// Add a debug endpoint to check table structure -app.get('/api/debug/table-structure', requireAdmin, async (req, res) => { - try { - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${process.env.NOCODB_TABLE_ID}`; - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - }, - params: { - limit: 1 - } - }); - - const sample = response.data.list?.[0] || {}; - - res.json({ - success: true, - fields: Object.keys(sample), - sampleRecord: sample, - idField: sample.ID ? 'ID' : (sample.Id ? 'Id' : (sample.id ? 'id' : 'unknown')) - }); - - } catch (error) { - logger.error('Error checking table structure:', error); - res.status(500).json({ - success: false, - error: 'Failed to check table structure' - }); - } -}); - -// QR code generation test endpoint (local only, no upload) -app.get('/api/debug/test-qr', requireAdmin, async (req, res) => { - try { - const testUrl = req.query.url || 'https://example.com/test'; - const testSize = parseInt(req.query.size) || 200; - - logger.info('Testing local QR code generation...'); - - const qrOptions = { - type: 'png', - width: testSize, - margin: 1, - color: { - dark: '#000000', - light: '#FFFFFF' - }, - errorCorrectionLevel: 'M' - }; - - const buffer = await generateQRCode(testUrl, qrOptions); - - res.set({ - 'Content-Type': 'image/png', - 'Content-Length': buffer.length - }); - - res.send(buffer); - - } catch (error) { - logger.error('QR code test failed:', error); - res.status(500).json({ - success: false, - error: error.message - }); - } -}); - -// Local QR code generation endpoint -app.get('/api/qr', async (req, res) => { - try { - const { text, size = 200 } = req.query; - - if (!text) { - return res.status(400).json({ - success: false, - error: 'Text parameter is required' - }); - } - - const { generateQRCode } = require('./services/qrcode'); - - const qrOptions = { - type: 'png', - width: parseInt(size), - margin: 1, - color: { - dark: '#000000', - light: '#FFFFFF' - }, - errorCorrectionLevel: 'M' - }; - - const buffer = await generateQRCode(text, qrOptions); - - res.set({ - 'Content-Type': 'image/png', - 'Content-Length': buffer.length, - 'Cache-Control': 'public, max-age=3600' // Cache for 1 hour - }); - - res.send(buffer); - - } catch (error) { - logger.error('QR code generation error:', error); - res.status(500).json({ - success: false, - error: 'Failed to generate QR code' - }); - } -}); - -// Simple QR test page -app.get('/test-qr', (req, res) => { - res.send(` - - - - QR Code Test - - - -

QR Code Generation Test

- -
-

Test 1: Direct API Call

-

Try accessing these URLs directly:

- -
- -
-

Test 2: Dynamic Generation

- - -
-
- -
-

Test 3: Using QRCode Library (like admin panel)

- - -
-
- - - - - `); -}); - -// Debug walk sheet configuration endpoint -app.get('/api/debug/walk-sheet-config', requireAdmin, async (req, res) => { - try { - const debugInfo = { - settingsSheetId: SETTINGS_SHEET_ID, - settingsSheetConfigured: process.env.NOCODB_SETTINGS_SHEET, - hasSettingsSheet: !!SETTINGS_SHEET_ID, - timestamp: new Date().toISOString() - }; - - if (!SETTINGS_SHEET_ID) { - return res.json({ - success: true, - debug: debugInfo, - message: 'Settings sheet not configured' - }); - } - - // Test connection to settings sheet - const testUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`; - - const response = await axios.get(testUrl, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - limit: 5, - sort: '-created_at' - } - }); - - const records = response.data.list || []; - const sampleRecord = records[0] || {}; - - res.json({ - success: true, - debug: { - ...debugInfo, - connectionTest: 'success', - recordCount: records.length, - availableFields: Object.keys(sampleRecord), - sampleRecord: sampleRecord, - recentRecords: records.slice(0, 3).map(r => ({ - id: r.id || r.Id || r.ID, - created_at: r.created_at, - walk_sheet_title: r.walk_sheet_title, - hasQrCodes: !!(r.qr_code_1_url || r.qr_code_2_url || r.qr_code_3_url) - })) - } - }); - - } catch (error) { - logger.error('Error debugging walk sheet config:', error); - res.json({ - success: false, - debug: { - settingsSheetId: SETTINGS_SHEET_ID, - settingsSheetConfigured: process.env.NOCODB_SETTINGS_SHEET, - hasSettingsSheet: !!SETTINGS_SHEET_ID, - timestamp: new Date().toISOString(), - error: error.message, - errorDetails: error.response?.data - } - }); - } -}); - -// Test walk sheet configuration endpoint -app.post('/api/debug/test-walk-sheet-save', requireAdmin, async (req, res) => { - try { - const testConfig = { - walk_sheet_title: 'Test Walk Sheet', - walk_sheet_subtitle: 'Test Subtitle', - walk_sheet_footer: 'Test Footer', - qr_code_1_url: 'https://example.com/test1', - qr_code_1_label: 'Test QR 1', - qr_code_2_url: 'https://example.com/test2', - qr_code_2_label: 'Test QR 2', - qr_code_3_url: 'https://example.com/test3', - qr_code_3_label: 'Test QR 3' - }; - - logger.info('Testing walk sheet configuration save...'); - - // Create a test request object - const testReq = { - body: testConfig, - session: { - userEmail: req.session.userEmail, - authenticated: true, - isAdmin: true - } - }; - - // Create a test response object - let testResult = null; - let testError = null; - - const testRes = { - json: (data) => { testResult = data; }, - status: (code) => ({ - json: (data) => { - testResult = data; - testResult.statusCode = code; - } - }) - }; - - // Test the save functionality - if (!SETTINGS_SHEET_ID) { - return res.json({ - success: false, - test: 'failed', - error: 'Settings sheet not configured', - config: testConfig - }); - } - - const userEmail = req.session.userEmail; - const timestamp = new Date().toISOString(); - - const walkSheetData = { - created_at: timestamp, - created_by: userEmail, - walk_sheet_title: testConfig.walk_sheet_title, - walk_sheet_subtitle: testConfig.walk_sheet_subtitle, - walk_sheet_footer: testConfig.walk_sheet_footer, - qr_code_1_url: testConfig.qr_code_1_url, - qr_code_1_label: testConfig.qr_code_1_label, - qr_code_2_url: testConfig.qr_code_2_url, - qr_code_2_label: testConfig.qr_code_2_label, - qr_code_3_url: testConfig.qr_code_3_url, - qr_code_3_label: testConfig.qr_code_3_label - }; - - const response = await axios.post( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - walkSheetData, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - } - ); - - res.json({ - success: true, - test: 'passed', - message: 'Test walk sheet configuration saved successfully', - testData: walkSheetData, - saveResponse: response.data, - settingsId: response.data.id || response.data.Id || response.data.ID - }); - - } catch (error) { - logger.error('Test walk sheet save failed:', error); - res.json({ - success: false, - test: 'failed', - error: error.message, - errorDetails: error.response?.data, - timestamp: new Date().toISOString() - }); - } -}); - -// Debug endpoint to see raw walk sheet data -app.get('/api/debug/walk-sheet-raw', requireAdmin, async (req, res) => { - try { - if (!SETTINGS_SHEET_ID) { - return res.json({ error: 'No settings sheet ID configured' }); - } - - const response = await axios.get( - `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${SETTINGS_SHEET_ID}`, - { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - }, - params: { - sort: '-created_at', - limit: 5 - } - } - ); - - return res.json({ - success: true, - tableId: SETTINGS_SHEET_ID, - records: response.data?.list || [], - count: response.data?.list?.length || 0 - }); - } catch (error) { - logger.error('Error fetching raw walk sheet data:', error); - return res.status(500).json({ - success: false, - error: error.message - }); - } -}); - -// Admin user management endpoints -app.get('/api/admin/users', requireAdmin, async (req, res) => { - try { - if (!LOGIN_SHEET_ID) { - return res.status(500).json({ - success: false, - error: 'Login sheet not configured' - }); - } - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`; - - const response = await axios.get(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - limit: 100, - sort: '-created_at' - } - }); - - const users = response.data.list || []; - - // Remove password field from response for security - const safeUsers = users.map(user => { - const { Password, password, ...safeUser } = user; - return safeUser; - }); - - res.json({ - success: true, - users: safeUsers - }); - - } catch (error) { - logger.error('Error fetching users:', error); - res.status(500).json({ - success: false, - error: 'Failed to fetch users' - }); - } -}); - -app.post('/api/admin/users', requireAdmin, async (req, res) => { - try { - const { email, password, name, admin } = req.body; - - if (!email || !password) { - return res.status(400).json({ - success: false, - error: 'Email and password are required' - }); - } - - if (!LOGIN_SHEET_ID) { - return res.status(500).json({ - success: false, - error: 'Login sheet not configured' - }); - } - - // Check if user already exists - const checkUrl = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}`; - - const checkResponse = await axios.get(checkUrl, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - }, - params: { - where: `(Email,eq,${email})`, - limit: 1 - } - }); - - if (checkResponse.data.list && checkResponse.data.list.length > 0) { - return res.status(400).json({ - success: false, - error: 'User with this email already exists' - }); - } - - // Create new user - const userData = { - Email: email, - email: email, - Password: password, - password: password, - Name: name || '', - name: name || '', - Admin: admin === true, - admin: admin === true, - 'Created At': new Date().toISOString(), - created_at: new Date().toISOString() - }; - - const response = await axios.post(checkUrl, userData, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN, - 'Content-Type': 'application/json' - } - }); - - res.status(201).json({ - success: true, - message: 'User created successfully', - user: { - id: response.data.Id || response.data.id || response.data.ID, - email: email, - name: name, - admin: admin - } - }); - - } catch (error) { - logger.error('Error creating user:', error); - res.status(500).json({ - success: false, - error: 'Failed to create user' - }); - } -}); - -app.delete('/api/admin/users/:id', requireAdmin, async (req, res) => { - try { - const userId = req.params.id; - - if (!LOGIN_SHEET_ID) { - return res.status(500).json({ - success: false, - error: 'Login sheet not configured' - }); - } - - // Don't allow admins to delete themselves - if (userId === req.session.userId) { - return res.status(400).json({ - success: false, - error: 'Cannot delete your own account' - }); - } - - const url = `${process.env.NOCODB_API_URL}/db/data/v1/${process.env.NOCODB_PROJECT_ID}/${LOGIN_SHEET_ID}/${userId}`; - - await axios.delete(url, { - headers: { - 'xc-token': process.env.NOCODB_API_TOKEN - } - }); - - res.json({ - success: true, - message: 'User deleted successfully' - }); - - } catch (error) { - logger.error('Error deleting user:', error); - res.status(500).json({ - success: false, - error: 'Failed to delete user' - }); - } -}); - -// Error handling middleware -app.use((err, req, res, next) => { - logger.error('Unhandled error:', err); - res.status(500).json({ - success: false, - error: 'Internal server error' - }); -}); - -// Start server -app.listen(PORT, () => { - logger.info(` - ╔════════════════════════════════════════╗ - ║ BNKops Map Server ║ - ╠════════════════════════════════════════╣ - ║ Status: Running ║ - ║ Port: ${PORT} ║ - ║ Environment: ${process.env.NODE_ENV || 'development'} ║ - ║ Project ID: ${process.env.NOCODB_PROJECT_ID} ║ - ║ Table ID: ${process.env.NOCODB_TABLE_ID} ║ - ║ Login Sheet: ${LOGIN_SHEET_ID || 'Not Configured'} ║ - ║ Time: ${new Date().toISOString()} ║ - ╚════════════════════════════════════════╝ - `); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - logger.info('SIGTERM signal received: closing HTTP server'); - app.close(() => { - logger.info('HTTP server closed'); - process.exit(0); - }); -}); - diff --git a/map/app/server.js b/map/app/server.js index a22ff30..9995646 100644 --- a/map/app/server.js +++ b/map/app/server.js @@ -1,6 +1,16 @@ +// At the very top of the file, before any requires +const startTime = Date.now(); + +// Use a more robust check for duplicate execution +if (global.__serverInitialized) { + console.log(`[INIT] Server already initialized - EXITING`); + return; +} +global.__serverInitialized = true; + // Prevent duplicate execution if (require.main !== module) { - console.log('Server.js being imported, not executed directly'); + console.log('[INIT] Server.js being imported, not executed directly - EXITING'); return; } @@ -12,10 +22,6 @@ const cookieParser = require('cookie-parser'); const crypto = require('crypto'); const fetch = require('node-fetch'); -// Debug: Check if server.js is being loaded multiple times -const serverInstanceId = Math.random().toString(36).substr(2, 9); -console.log(`[DEBUG] Server.js PID:${process.pid} instance ${serverInstanceId} loading at ${new Date().toISOString()}`); - // Import configuration and utilities const config = require('./config'); const logger = require('./utils/logger'); @@ -24,8 +30,14 @@ const { apiLimiter } = require('./middleware/rateLimiter'); const { cacheBusting } = require('./utils/cacheBusting'); const { initializeEmailService } = require('./services/email'); -// Initialize Express app +// Initialize Express app - only create once +if (global.__expressApp) { + console.log('[INIT] Express app already created - EXITING'); + return; +} + const app = express(); +global.__expressApp = app; // Trust proxy for Cloudflare app.set('trust proxy', true); @@ -33,8 +45,8 @@ app.set('trust proxy', true); // Cookie parser app.use(cookieParser()); -// Session configuration -app.use(session({ +// Session configuration - only initialize once +const sessionMiddleware = session({ secret: config.session.secret, resave: false, saveUninitialized: false, @@ -44,36 +56,34 @@ app.use(session({ // Use a custom session ID generator to avoid conflicts return crypto.randomBytes(16).toString('hex'); } -})); +}); + +app.use(sessionMiddleware); // Build dynamic CSP configuration const buildConnectSrc = () => { const sources = ["'self'"]; - // Add MkDocs URLs from config - if (config.mkdocs?.url) { - sources.push(config.mkdocs.url); + // Add NocoDB API URL + if (config.nocodb.apiUrl) { + try { + const nocodbUrl = new URL(config.nocodb.apiUrl); + sources.push(`${nocodbUrl.protocol}//${nocodbUrl.host}`); + } catch (e) { + // Invalid URL, skip + } } - // Add localhost ports from environment - const mkdocsPort = process.env.MKDOCS_PORT || '4000'; - const mkdocsSitePort = process.env.MKDOCS_SITE_SERVER_PORT || '4002'; - - sources.push(`http://localhost:${mkdocsPort}`); - sources.push(`http://localhost:${mkdocsSitePort}`); - - // Add City of Edmonton Socrata API + // Add Edmonton Open Data Portal sources.push('https://data.edmonton.ca'); - // Add Stadia Maps for better tile coverage - sources.push('https://tiles.stadiamaps.com'); + // Add Nominatim for geocoding + sources.push('https://nominatim.openstreetmap.org'); - // Add production domains if in production - if (config.isProduction || process.env.NODE_ENV === 'production') { - // Add the main domain from environment - const mainDomain = process.env.DOMAIN || 'cmlite.org'; - sources.push(`https://${mainDomain}`); - sources.push('https://cmlite.org'); // Fallback + // Add localhost for development + if (!config.isProduction) { + sources.push('http://localhost:*'); + sources.push('ws://localhost:*'); } return sources; @@ -95,14 +105,26 @@ app.use(helmet({ // CORS configuration app.use(cors({ origin: function(origin, callback) { - // Allow requests with no origin (like mobile apps or curl requests) + // Allow requests with no origin (like Postman or server-to-server) if (!origin) return callback(null, true); - const allowedOrigins = config.cors.allowedOrigins; - if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) { - callback(null, true); + // In production, be more restrictive + if (config.isProduction) { + const allowedOrigins = [ + `https://${config.domain}`, + `https://map.${config.domain}`, + `https://docs.${config.domain}`, + `https://admin.${config.domain}` + ]; + + if (allowedOrigins.includes(origin)) { + callback(null, true); + } else { + callback(new Error('Not allowed by CORS')); + } } else { - callback(new Error('Not allowed by CORS')); + // In development, allow localhost + callback(null, true); } }, credentials: true, @@ -127,7 +149,7 @@ app.use('/api/', apiLimiter); // Cache busting version endpoint app.get('/api/version', (req, res) => { - res.json({ + res.json({ version: cacheBusting.getVersion(), timestamp: new Date().toISOString() }); @@ -136,25 +158,24 @@ app.get('/api/version', (req, res) => { // Proxy endpoint for MkDocs search app.get('/api/docs-search', async (req, res) => { try { - const mkdocsUrl = config.mkdocs?.url || `http://localhost:${process.env.MKDOCS_SITE_SERVER_PORT || '4002'}`; - logger.info(`Fetching search index from: ${mkdocsUrl}/search/search_index.json`); - - const response = await fetch(`${mkdocsUrl}/search/search_index.json`); - - if (!response.ok) { - throw new Error(`Failed to fetch search index: ${response.status}`); - } + const docsUrl = config.isProduction ? + `https://docs.${config.domain}/search/search_index.json` : + 'http://localhost:8000/search/search_index.json'; + const response = await fetch(docsUrl); const data = await response.json(); res.json(data); } catch (error) { - logger.error('Error fetching search index:', error); + logger.error('Failed to fetch docs search index:', error); res.status(500).json({ error: 'Failed to fetch search index' }); } }); -// Initialize email service -initializeEmailService(); +// Initialize email service - only once +if (!global.__emailInitialized) { + initializeEmailService(); + global.__emailInitialized = true; +} // Import and setup routes require('./routes')(app); @@ -162,63 +183,70 @@ require('./routes')(app); // Error handling middleware app.use((err, req, res, next) => { logger.error('Unhandled error:', err); - - // Don't leak error details in production - const message = config.isProduction ? - 'Internal server error' : - err.message || 'Internal server error'; - - res.status(err.status || 500).json({ - success: false, - error: message + res.status(500).json({ + error: 'Internal server error', + message: config.isProduction ? 'An error occurred' : err.message }); }); -// Start server -const server = app.listen(config.port, () => { - logger.info(` +// Add a simple health check endpoint early in the middleware stack (before other middlewares) +app.get('/health', (req, res) => { + res.status(200).json({ + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime() + }); +}); + +// Only start server if not already started +if (!global.__serverStarted) { + global.__serverStarted = true; + + const server = app.listen(config.port, () => { + logger.info(` ╔════════════════════════════════════════╗ ║ BNKops Map Server ║ ╠════════════════════════════════════════╣ ║ Status: Running ║ ║ Port: ${config.port} ║ - ║ Environment: ${config.nodeEnv} ║ + ║ Environment: ${config.isProduction ? 'production' : 'development'} ║ ║ Project ID: ${config.nocodb.projectId} ║ ║ Table ID: ${config.nocodb.tableId} ║ - ║ Login Sheet: ${config.nocodb.loginSheetId || 'Not Configured'} ║ + ║ Login Sheet: ${config.nocodb.loginSheetId} ║ ║ PID: ${process.pid} ║ ║ Time: ${new Date().toISOString()} ║ ╚════════════════════════════════════════╝ `); -}); - -// Graceful shutdown -process.on('SIGTERM', () => { - logger.info('SIGTERM signal received: closing HTTP server'); - server.close(() => { - logger.info('HTTP server closed'); - process.exit(0); }); -}); -process.on('SIGINT', () => { - logger.info('SIGINT signal received: closing HTTP server'); - server.close(() => { - logger.info('HTTP server closed'); - process.exit(0); + // Graceful shutdown + process.on('SIGTERM', () => { + logger.info('SIGTERM signal received: closing HTTP server'); + server.close(() => { + logger.info('HTTP server closed'); + process.exit(0); + }); }); -}); -// Handle uncaught exceptions -process.on('uncaughtException', (err) => { - logger.error('Uncaught exception:', err); - process.exit(1); -}); + process.on('SIGINT', () => { + logger.info('SIGINT signal received: closing HTTP server'); + server.close(() => { + logger.info('HTTP server closed'); + process.exit(0); + }); + }); -// Handle unhandled promise rejections -process.on('unhandledRejection', (reason, promise) => { - logger.error('Unhandled Rejection at:', promise, 'reason:', reason); - process.exit(1); -}); + // Handle uncaught exceptions + process.on('uncaughtException', (err) => { + logger.error('Uncaught Exception:', err); + process.exit(1); + }); + + // Handle unhandled promise rejections + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); + process.exit(1); + }); +} module.exports = app; \ No newline at end of file diff --git a/map/app/utils/logger.js b/map/app/utils/logger.js index 100313e..aca3772 100644 --- a/map/app/utils/logger.js +++ b/map/app/utils/logger.js @@ -1,20 +1,10 @@ const winston = require('winston'); const config = require('../config'); -// Debug: Check if logger is being created multiple times -const instanceId = Math.random().toString(36).substr(2, 9); -console.log(`[DEBUG] Creating logger instance ${instanceId} at ${new Date().toISOString()}`); - -// Ensure we only create one logger instance -if (global.appLogger) { - console.log(`[DEBUG] Reusing existing logger instance`); - module.exports = global.appLogger; - return; -} - +// Create the logger only once const logger = winston.createLogger({ level: config.isProduction ? 'info' : 'debug', - defaultMeta: { service: 'bnkops-map', instanceId }, + defaultMeta: { service: 'bnkops-map' }, transports: [ new winston.transports.Console({ format: winston.format.combine( @@ -50,7 +40,4 @@ if (config.isProduction) { })); } -// Store logger globally to prevent multiple instances -global.appLogger = logger; - module.exports = logger; \ No newline at end of file