diff --git a/src/server.ts b/src/server.ts index 554e962..a112ed7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -214,11 +214,19 @@ function setupRoutes(): void { return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; + const escapeHtml = (text: string): string => { + return text.replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + const tableRows = locations.map((location, index) => ` ${index + 1} - ${location.address} - ${location.description || 'No additional details'} + ${escapeHtml(location.address)} + ${escapeHtml(location.description || 'No additional details')} ${location.created_at ? formatDate(location.created_at) : 'Unknown'} ${location.persistent ? 'Persistent' : (location.created_at ? formatTimeRemaining(location.created_at) : 'Unknown')} diff --git a/src/services/MapImageService.ts b/src/services/MapImageService.ts index 1197c62..1bf0fea 100644 --- a/src/services/MapImageService.ts +++ b/src/services/MapImageService.ts @@ -19,9 +19,9 @@ export class MapImageService { async generateMapImage(locations: Location[], options: Partial = {}): Promise { const opts = { ...this.defaultOptions, ...options }; - console.log('Generating Mapbox static map focused on location data'); - console.log('Canvas size:', opts.width, 'x', opts.height); - console.log('Number of locations:', locations.length); + console.info('Generating Mapbox static map focused on location data'); + console.info('Canvas size:', opts.width, 'x', opts.height); + console.info('Number of locations:', locations.length); const mapboxBuffer = await this.fetchMapboxStaticMapAutoFit(opts, locations); @@ -47,7 +47,7 @@ export class MapImageService { let overlays = ''; locations.forEach((location, index) => { if (location.latitude && location.longitude) { - console.log(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`); + console.info(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`); // Correct format: pin-s-label+color(lng,lat) const color = location.persistent ? 'ff9800' : 'ff0000'; // Orange for persistent, red for regular const label = (index + 1).toString(); @@ -58,7 +58,7 @@ export class MapImageService { // Remove trailing comma overlays = overlays.replace(/,$/, ''); - console.log('Generated overlays string:', overlays); + console.info('Generated overlays string:', overlays); // Build Mapbox Static Maps URL with auto-fit let mapboxUrl; @@ -72,8 +72,8 @@ export class MapImageService { mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v12/static/${fallbackLng},${fallbackLat},10/${options.width}x${options.height}?access_token=${mapboxToken}`; } - console.log('Fetching Mapbox static map with auto-fit...'); - console.log('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN')); + console.info('Fetching Mapbox static map with auto-fit...'); + console.info('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN')); return new Promise((resolve) => { const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => { @@ -81,7 +81,7 @@ export class MapImageService { const chunks: Buffer[] = []; response.on('data', (chunk) => chunks.push(chunk)); response.on('end', () => { - console.log('Mapbox static map fetched successfully'); + console.info('Mapbox static map fetched successfully'); resolve(Buffer.concat(chunks)); }); } else { @@ -103,76 +103,27 @@ export class MapImageService { }); } - /** - * Legacy method - keeping for potential fallback - */ - private async fetchMapboxStaticMap(centerLat: number, centerLng: number, zoom: number, options: MapOptions, locations: Location[]): Promise { - const mapboxToken = process.env.MAPBOX_ACCESS_TOKEN; - if (!mapboxToken) { - console.error('No Mapbox token available'); - return null; - } - - // Build overlay string for location markers - let overlays = ''; - locations.forEach((location, index) => { - if (location.latitude && location.longitude) { - console.log(`Location ${index + 1}: ${location.latitude}, ${location.longitude} (${location.address})`); - const color = location.persistent ? 'orange' : 'red'; - const label = (index + 1).toString(); - overlays += `pin-s-${label}+${color}(${location.longitude},${location.latitude}),`; - } - }); - - // Remove trailing comma - overlays = overlays.replace(/,$/, ''); - - // Build Mapbox Static Maps URL - let mapboxUrl; - if (overlays) { - mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${overlays}/${centerLng},${centerLat},${zoom}/${options.width}x${options.height}?access_token=${mapboxToken}`; - } else { - mapboxUrl = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/${centerLng},${centerLat},${zoom}/${options.width}x${options.height}?access_token=${mapboxToken}`; - } - - console.log('Fetching Mapbox static map...'); - console.log('URL:', mapboxUrl.replace(mapboxToken, 'TOKEN_HIDDEN')); - - return new Promise((resolve) => { - const request = https.get(mapboxUrl, { timeout: 10000 }, (response) => { - if (response.statusCode === 200) { - const chunks: Buffer[] = []; - response.on('data', (chunk) => chunks.push(chunk)); - response.on('end', () => { - console.log('Mapbox static map fetched successfully'); - resolve(Buffer.concat(chunks)); - }); - } else { - console.error('Mapbox API error:', response.statusCode); - resolve(null); - } - }); - - request.on('error', (err) => { - console.error('Error fetching Mapbox map:', err.message); - resolve(null); - }); - - request.on('timeout', () => { - console.error('Mapbox request timeout'); - request.destroy(); - resolve(null); - }); - }); - } /** * Generate a simple error image when Mapbox fails */ private generateErrorImage(options: MapOptions): Buffer { - // Return a simple text-based error - in a real implementation you'd create a proper error image - const errorText = `Map generation failed - Mapbox API error\nSize: ${options.width}x${options.height}`; - return Buffer.from(errorText, 'utf8'); + // Generate a simple 1x1 transparent PNG as fallback + // This is a valid PNG header + IHDR + IDAT + IEND for a 1x1 transparent pixel + const transparentPng = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions + 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, // RGBA, no compression + 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk + 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, // Compressed data + 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, // (transparent pixel) + 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk + 0x42, 0x60, 0x82 + ]); + + console.info('Generated transparent PNG fallback due to Mapbox failure'); + return transparentPng; } }