diff --git a/.env.example b/.env.example index a0227ff..aab3afa 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# ICE Watch Environment Variables +# Great Lakes Ice Report Environment Variables # Copy this file to .env and fill in your actual values # MapBox API Configuration (Required for fast geocoding) diff --git a/README.md b/README.md index 5ca8b31..dff7b64 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# ICE Watch Michigan +# Great Lakes Ice Report -A community-driven web application for tracking ICE activity locations in Michigan. Reports automatically expire after 24 hours to maintain current information. +A community-driven web application for tracking winter road conditions and icy hazards in the Great Lakes region. Reports automatically expire after 48 hours to maintain current information. ## Features @@ -21,8 +21,8 @@ A community-driven web application for tracking ICE activity locations in Michig 1. **Clone the repository:** ```bash - git clone git@github.com:deco/ice.git - cd icewatch + git clone git@github.com:deco/great-lakes-ice-report.git + cd great-lakes-ice-report ``` 2. **Install dependencies:** @@ -65,13 +65,13 @@ PORT=3000 1. **Run the deployment script on your server:** ```bash - curl -sSL https://ice-puremichigan-lol.s3.amazonaws.com/scripts/deploy.sh | bash + curl -sSL https://ice.puremichigan.lol/scripts/deploy.sh | bash ``` 2. **Deploy your application:** ```bash - git clone git@github.com:deco/ice.git /opt/icewatch - cd /opt/icewatch + git clone git@github.com:deco/great-lakes-ice-report.git /opt/great-lakes-ice-report + cd /opt/great-lakes-ice-report npm install ``` @@ -83,8 +83,8 @@ PORT=3000 4. **Start services:** ```bash - sudo systemctl enable icewatch - sudo systemctl start icewatch + sudo systemctl enable great-lakes-ice-report + sudo systemctl start great-lakes-ice-report sudo systemctl enable caddy sudo systemctl start caddy ``` diff --git a/package.json b/package.json index 9479a43..26b954f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "icewatch", + "name": "great-lakes-ice-report", "version": "1.0.0", - "description": "ICE location tracking website for Michigan", + "description": "Great Lakes Ice Report - Community-driven winter road conditions tracker for Michigan", "main": "server.js", "scripts": { "start": "node server.js", @@ -19,8 +19,12 @@ }, "keywords": [ "ice", - "tracking", + "winter", + "road conditions", "michigan", + "great lakes", + "weather", + "tracking", "map" ], "author": "Your Name", diff --git a/public/admin.html b/public/admin.html index e5858e6..b993aa8 100644 --- a/public/admin.html +++ b/public/admin.html @@ -3,8 +3,10 @@ - ICE Watch Admin - + Great Lakes Ice Report Admin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/index.html b/public/index.html index 259d77b..6a0f713 100644 --- a/public/index.html +++ b/public/index.html @@ -3,20 +3,29 @@ - ICE Watch Michigan + Great Lakes Ice Report + + - +
-

🚨 ICE Watch Michigan

-

Community-reported ICE activity locations (auto-expire after 24 hours)

+
+
+

❄️ Great Lakes Ice Report

+

Community-reported icy road conditions and winter hazards (auto-expire after 48 hours)

+
+ +
-

Report ICE Activity

+

Report Icy Conditions

@@ -56,8 +65,8 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
-

🔴 Red markers: ICE activity reported

-

⏰ Auto-cleanup: Reports disappear after 24 hours

+

🔴 Red markers: Icy conditions reported

+

⏰ Auto-cleanup: Reports disappear after 48 hours

Loading locations...

@@ -90,15 +99,15 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
-

Safety Notice: This is a community tool for awareness. Stay safe and know your rights.

+

Safety Notice: This is a community tool for awareness. Stay safe and know your rights.

This website is for informational purposes only. Verify information independently. - Reports are automatically deleted after 24 hours. + Reports are automatically deleted after 48 hours. • Privacy Policy
- + diff --git a/public/privacy.html b/public/privacy.html new file mode 100644 index 0000000..e52aa6d --- /dev/null +++ b/public/privacy.html @@ -0,0 +1,252 @@ + + + + + + Privacy Policy - Great Lakes Ice Report + + + + + + +
+
+
+
+

❄️ Privacy Policy

+

Great Lakes Ice Report

+
+ +
+
Effective Date: January 2025
+
+ +
+

Our Commitment to Privacy

+

Great Lakes Ice Report is a community safety tool designed to help residents share winter road conditions. We believe in transparency and minimal data collection.

+ +

Information We Collect

+
    +
  • Location Reports: Address/location descriptions and coordinates you submit
  • +
  • Optional Details: Any additional description you provide with reports
  • +
  • Technical Data: IP address (for preventing abuse), browser type, and basic usage statistics
  • +
  • Theme Preference: Your dark/light mode choice (stored locally on your device)
  • +
+ +

How We Use Your Information

+
    +
  • Display Reports: Show icy conditions on the public map for community awareness
  • +
  • Auto-Cleanup: Reports automatically delete after 48 hours
  • +
  • Prevent Abuse: Rate limiting and basic spam protection
  • +
  • Service Improvement: Understanding usage patterns to improve the tool
  • +
+ +

Information Sharing

+

We do not sell, rent, or share your personal information with third parties.

+
    +
  • Location reports are displayed publicly on the map (this is the service's purpose)
  • +
  • We may disclose information if required by law or to protect safety
  • +
  • Anonymous usage statistics may be shared to improve community safety tools
  • +
+ +

Data Retention

+
    +
  • Reports: Automatically deleted after 48 hours (unless marked persistent by admin)
  • +
  • Technical Logs: Retained for up to 30 days for security purposes
  • +
  • Theme Preferences: Stored locally on your device only
  • +
+ +

Your Rights

+
    +
  • Anonymous Use: No account required - you can use the service anonymously
  • +
  • Voluntary Participation: All report submissions are voluntary
  • +
  • Contact Us: Questions about your data or this policy (see contact info below)
  • +
+ +

Security

+

We implement reasonable security measures including:

+
    +
  • HTTPS encryption for all communications
  • +
  • Rate limiting to prevent abuse
  • +
  • Regular security updates and monitoring
  • +
  • Minimal data collection principle
  • +
+ +

Third-Party Services

+

We use the following external services:

+
    +
  • MapBox API: For fast address geocoding (see MapBox Privacy Policy)
  • +
  • OpenStreetMap: For map tiles and fallback geocoding
  • +
  • Nominatim: Backup geocoding service
  • +
+ +

Changes to This Policy

+

We may update this privacy policy occasionally. Significant changes will be indicated by updating the effective date above. Continued use of the service constitutes acceptance of any changes.

+ +

Community Safety Focus

+

This tool exists to help community members stay safe during winter conditions. We encourage responsible use and respect for everyone's safety and privacy.

+ +
+

Contact Information

+

Questions about this privacy policy or your data?

+ +
+ + ← Back to Great Lakes Ice Report +
+
+ + + + diff --git a/public/style.css b/public/style.css index f100ba2..c63c8d0 100644 --- a/public/style.css +++ b/public/style.css @@ -1,9 +1,94 @@ +/* CSS Variables for theming */ +:root { + --bg-color: #f4f4f9; + --text-color: #333; + --card-bg: white; + --border-color: #ddd; + --input-bg: white; + --input-border: #ddd; + --button-bg: #007bff; + --button-hover: #0056b3; + --header-bg: transparent; + --footer-border: #ddd; + --table-header-bg: #f8f9fa; + --table-hover: #f8f9fa; + --toggle-bg: #f8f9fa; + --toggle-border: #dee2e6; + --toggle-active-bg: #007bff; + --shadow: rgba(0,0,0,0.1); +} + +/* Dark mode variables */ +[data-theme="dark"] { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --card-bg: #2d2d2d; + --border-color: #404040; + --input-bg: #333; + --input-border: #555; + --button-bg: #4a90e2; + --button-hover: #357abd; + --header-bg: transparent; + --footer-border: #404040; + --table-header-bg: #3a3a3a; + --table-hover: #3a3a3a; + --toggle-bg: #404040; + --toggle-border: #555; + --toggle-active-bg: #4a90e2; + --shadow: rgba(0,0,0,0.3); +} + +/* Auto system theme detection */ +@media (prefers-color-scheme: dark) { + :root[data-theme="auto"] { + --bg-color: #1a1a1a; + --text-color: #e0e0e0; + --card-bg: #2d2d2d; + --border-color: #404040; + --input-bg: #333; + --input-border: #555; + --button-bg: #4a90e2; + --button-hover: #357abd; + --header-bg: transparent; + --footer-border: #404040; + --table-header-bg: #3a3a3a; + --table-hover: #3a3a3a; + --toggle-bg: #404040; + --toggle-border: #555; + --toggle-active-bg: #4a90e2; + --shadow: rgba(0,0,0,0.3); + } +} + +@media (prefers-color-scheme: light) { + :root[data-theme="auto"] { + --bg-color: #f4f4f9; + --text-color: #333; + --card-bg: white; + --border-color: #ddd; + --input-bg: white; + --input-border: #ddd; + --button-bg: #007bff; + --button-hover: #0056b3; + --header-bg: transparent; + --footer-border: #ddd; + --table-header-bg: #f8f9fa; + --table-hover: #f8f9fa; + --toggle-bg: #f8f9fa; + --toggle-border: #dee2e6; + --toggle-active-bg: #007bff; + --shadow: rgba(0,0,0,0.1); + } +} + body { font-family: Arial, sans-serif; line-height: 1.6; margin: 0; padding: 0; - background-color: #f4f4f9; + background-color: var(--bg-color); + color: var(--text-color); + transition: background-color 0.3s ease, color 0.3s ease; } .container { @@ -20,9 +105,10 @@ header { .map-section, .form-section { margin-bottom: 20px; padding: 15px; - background-color: white; + background-color: var(--card-bg); border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px var(--shadow); + transition: background-color 0.3s ease; } .map-section { @@ -46,8 +132,11 @@ input[type="text"], textarea { width: calc(100% - 20px); padding: 8px; margin-top: 5px; - border: 1px solid #ddd; + border: 1px solid var(--input-border); border-radius: 4px; + background-color: var(--input-bg); + color: var(--text-color); + transition: background-color 0.3s ease, border-color 0.3s ease; } .autocomplete-container { @@ -60,14 +149,15 @@ input[type="text"], textarea { top: 100%; left: 0; right: 0; - background: white; - border: 1px solid #ddd; + background: var(--card-bg); + border: 1px solid var(--border-color); border-top: none; border-radius: 0 0 4px 4px; max-height: 200px; overflow-y: auto; z-index: 1000; display: none; + box-shadow: 0 4px 6px var(--shadow); } .autocomplete-item { @@ -78,7 +168,7 @@ input[type="text"], textarea { .autocomplete-item:hover, .autocomplete-item.selected { - background-color: #f8f9fa; + background-color: var(--table-hover); } .autocomplete-item:last-child { @@ -93,7 +183,7 @@ input[type="text"], textarea { } button[type="submit"] { - background-color: #007bff; + background-color: var(--button-bg); color: white; border: none; padding: 10px 20px; @@ -103,7 +193,7 @@ button[type="submit"] { } button[type="submit"]:hover { - background-color: #0056b3; + background-color: var(--button-hover); } .message { @@ -123,12 +213,55 @@ button[type="submit"]:hover { color: #721c24; } +/* Header layout for theme toggle */ +.header-content { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 20px; +} + +.header-text { + flex: 1; +} + +/* Theme toggle button */ +.theme-toggle { + background: var(--card-bg); + border: 2px solid var(--border-color); + border-radius: 50%; + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.3s ease; + box-shadow: 0 2px 4px var(--shadow); + flex-shrink: 0; +} + +.theme-toggle:hover { + transform: scale(1.1); + box-shadow: 0 4px 8px var(--shadow); +} + +.theme-icon { + font-size: 20px; + transition: transform 0.3s ease; +} + +[data-theme="dark"] .theme-icon { + transform: rotate(180deg); +} + footer { text-align: center; padding: 30px 20px; margin-top: 30px; - border-top: 1px solid #ddd; + border-top: 1px solid var(--footer-border); clear: both; + transition: border-color 0.3s ease; } .disclaimer { @@ -150,9 +283,9 @@ footer { } .toggle-btn { - background-color: #f8f9fa; - border: 2px solid #dee2e6; - color: #495057; + background-color: var(--toggle-bg); + border: 2px solid var(--toggle-border); + color: var(--text-color); padding: 8px 16px; border-radius: 6px; cursor: pointer; @@ -162,19 +295,17 @@ footer { } .toggle-btn:hover { - background-color: #e9ecef; - border-color: #adb5bd; + opacity: 0.8; } .toggle-btn.active { - background-color: #007bff; - border-color: #007bff; + background-color: var(--toggle-active-bg); + border-color: var(--toggle-active-bg); color: white; } .toggle-btn.active:hover { - background-color: #0056b3; - border-color: #0056b3; + opacity: 0.9; } /* View containers */ @@ -192,17 +323,18 @@ footer { .reports-table { width: 100%; border-collapse: collapse; - background: white; + background: var(--card-bg); font-size: 14px; + transition: background-color 0.3s ease; } .reports-table th { - background-color: #f8f9fa; - color: #495057; + background-color: var(--table-header-bg); + color: var(--text-color); font-weight: 600; padding: 12px; text-align: left; - border-bottom: 2px solid #dee2e6; + border-bottom: 2px solid var(--border-color); position: sticky; top: 0; z-index: 10; @@ -210,12 +342,12 @@ footer { .reports-table td { padding: 12px; - border-bottom: 1px solid #dee2e6; + border-bottom: 1px solid var(--border-color); vertical-align: top; } .reports-table tr:hover { - background-color: #f8f9fa; + background-color: var(--table-hover); } .reports-table tr:last-child td { @@ -286,6 +418,20 @@ footer { padding: 10px; } + .header-content { + flex-direction: column; + align-items: center; + gap: 15px; + } + + .header-text { + text-align: center; + } + + .theme-toggle { + align-self: center; + } + header h1 { font-size: 1.5em; } diff --git a/s3-bucket-policy.json b/s3-bucket-policy.json index 7fac330..a7fc249 100644 --- a/s3-bucket-policy.json +++ b/s3-bucket-policy.json @@ -3,10 +3,10 @@ "Statement": [ { "Sid": "PublicReadGetObject", - "Effect": "Allow", - "Principal": "*", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::ice-puremichigan-lol/scripts/*" + "Principal": "*", + "Effect": "Allow", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::greatlakes-conditions/*", } ] } diff --git a/scripts/Caddyfile b/scripts/Caddyfile index ba19513..7c9ea16 100644 --- a/scripts/Caddyfile +++ b/scripts/Caddyfile @@ -1,5 +1,5 @@ -# ICE Watch Caddy Configuration -# Replace yourdomain.com with your actual domain +# Great Lakes Ice Report Caddy Configuration +# Using subdomain on existing puremichigan.lol domain # # This configuration automatically: # - Obtains SSL certificates from Let's Encrypt @@ -7,7 +7,7 @@ # - Serves on ports 80 and 443 # Main site configuration -yourdomain.com { +ice.puremichigan.lol { # Automatic HTTPS (default behavior) # Caddy automatically: # - Listens on :80 and :443 @@ -22,7 +22,7 @@ yourdomain.com { health_timeout 5s } - # Security headers for ICE Watch + # Security headers for Great Lakes Ice Report header { # Enable HSTS (force HTTPS for 1 year) Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" @@ -43,7 +43,7 @@ yourdomain.com { # Logging for monitoring log { - output file /var/log/caddy/icewatch.log { + output file /var/log/caddy/great-lakes-ice-report.log { roll_size 100MB roll_keep 5 } @@ -71,17 +71,17 @@ yourdomain.com { } } -# Redirect www to non-www (with HTTPS) -www.yourdomain.com { - redir https://yourdomain.com{uri} permanent +# Redirect www subdomain (if someone tries it) +www.ice.puremichigan.lol { + redir https://ice.puremichigan.lol{uri} permanent } # HTTP redirect (explicit, though Caddy does this automatically) # This is just for clarity -http://yourdomain.com { - redir https://yourdomain.com{uri} permanent +http://ice.puremichigan.lol { + redir https://ice.puremichigan.lol{uri} permanent } -http://www.yourdomain.com { - redir https://yourdomain.com{uri} permanent +http://www.ice.puremichigan.lol { + redir https://ice.puremichigan.lol{uri} permanent } diff --git a/scripts/deploy.sh b/scripts/deploy.sh index e755727..6b46198 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -1,11 +1,11 @@ #!/bin/bash -# ICE Watch Deployment Script for Debian 12 ARM64 +# Great Lakes Ice Report Deployment Script for Debian 12 ARM64 # Run this script on your server: drone@91.99.139.235 set -e -echo "🚀 Starting ICE Watch deployment..." +echo "🚀 Starting Great Lakes Ice Report deployment..." # Update system echo "📦 Updating system packages..." @@ -77,23 +77,23 @@ echo "✅ Caddy with rate limiting plugin installed successfully!" # Create app directory echo "📁 Setting up app directory..." -sudo mkdir -p /opt/icewatch -sudo chown $USER:$USER /opt/icewatch +sudo mkdir -p /opt/great-lakes-ice-report +sudo chown $USER:$USER /opt/great-lakes-ice-report # Navigate to app directory -cd /opt/icewatch +cd /opt/great-lakes-ice-report -# Create icewatch user for security -echo "👤 Creating icewatch user..." -sudo useradd --system --shell /bin/false --home /opt/icewatch --create-home icewatch +# Create great-lakes-ice-report user for security +echo "👤 Creating great-lakes-ice-report user..." +sudo useradd --system --shell /bin/false --home /opt/great-lakes-ice-report --create-home great-lakes-ice-report # Download additional configuration files from S3 echo "📥 Downloading configuration files..." -S3_BASE_URL="https://ice-puremichigan-lol.s3.amazonaws.com/scripts" +S3_BASE_URL="https://greatlakes-conditions.s3.amazonaws.com/scripts" # Download systemd service file echo "📥 Downloading systemd service..." -curl -sSL "$S3_BASE_URL/icewatch.service" | sudo tee /etc/systemd/system/icewatch.service > /dev/null +curl -sSL "$S3_BASE_URL/great-lakes-ice-report.service" | sudo tee /etc/systemd/system/great-lakes-ice-report.service > /dev/null # Download Caddyfile template echo "📥 Downloading Caddy configuration..." @@ -101,13 +101,13 @@ curl -sSL "$S3_BASE_URL/Caddyfile" | sudo tee /etc/caddy/Caddyfile.template > /d echo "✅ Server setup complete!" echo "" -echo "🚀 Next steps to deploy ICE Watch:" +echo "🚀 Next steps to deploy Great Lakes Ice Report:" echo "" echo "1. Clone your repository:" -echo " git clone git@github.com:deco/ice.git /opt/icewatch" +echo " git clone git@github.com:deco/great-lakes-ice-report.git /opt/great-lakes-ice-report" echo "" echo "2. Set up the application:" -echo " cd /opt/icewatch" +echo " cd /opt/great-lakes-ice-report" echo " npm install" echo " cp .env.example .env" echo " nano .env # Add your MapBox token and admin password" @@ -118,16 +118,16 @@ echo " # Replace 'yourdomain.com' with your actual domain" echo " sudo mv /etc/caddy/Caddyfile.template /etc/caddy/Caddyfile" echo "" echo "4. Set permissions:" -echo " sudo chown -R icewatch:icewatch /opt/icewatch" -echo " sudo chmod 660 /opt/icewatch/.env" +echo " sudo chown -R great-lakes-ice-report:great-lakes-ice-report /opt/great-lakes-ice-report" +echo " sudo chmod 660 /opt/great-lakes-ice-report/.env" echo "" echo "5. Start services:" echo " sudo systemctl daemon-reload" -echo " sudo systemctl enable icewatch caddy" -echo " sudo systemctl start icewatch caddy" +echo " sudo systemctl enable great-lakes-ice-report caddy" +echo " sudo systemctl start great-lakes-ice-report caddy" echo "" echo "6. Check status:" -echo " sudo systemctl status icewatch" +echo " sudo systemctl status great-lakes-ice-report" echo " sudo systemctl status caddy" echo "" -echo "🌐 Your ICE Watch app will be available at: https://yourdomain.com" +echo "🌐 Your Great Lakes Ice Report app will be available at: https://ice.puremichigan.lol" diff --git a/scripts/great-lakes-ice-report.service b/scripts/great-lakes-ice-report.service new file mode 100644 index 0000000..9567edf --- /dev/null +++ b/scripts/great-lakes-ice-report.service @@ -0,0 +1,24 @@ +[Unit] +Description=Great Lakes Ice Report - Community Winter Conditions Tool +After=network.target +Wants=network.target + +[Service] +Type=simple +User=great-lakes-ice-report +Group=great-lakes-ice-report +WorkingDirectory=/opt/great-lakes-ice-report +ExecStart=/usr/bin/node server.js +Restart=always +RestartSec=5 +Environment=NODE_ENV=production + +# Security settings +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=strict +ProtectHome=true +ReadWritePaths=/opt/great-lakes-ice-report + +[Install] +WantedBy=multi-user.target diff --git a/server.js b/server.js index 8ee32f7..ec03429 100644 --- a/server.js +++ b/server.js @@ -47,11 +47,11 @@ db.serialize(() => { }); }); -// Clean up expired locations (older than 24 hours, but not persistent ones) +// Clean up expired locations (older than 48 hours, but not persistent ones) const cleanupExpiredLocations = () => { console.log('Running cleanup of expired locations'); - const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - db.run('DELETE FROM locations WHERE created_at < ? AND persistent = 0', [twentyFourHoursAgo], function(err) { + const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(); + db.run('DELETE FROM locations WHERE created_at < ? AND persistent = 0', [fortyEightHoursAgo], function(err) { if (err) { console.error('Error cleaning up expired locations:', err); } else { @@ -113,14 +113,14 @@ app.post('/api/admin/login', (req, res) => { } }); -// Get all active locations (within 24 hours) +// Get all active locations (within 48 hours) app.get('/api/locations', (req, res) => { console.log('Fetching active locations'); - const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const fortyEightHoursAgo = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(); db.all( 'SELECT * FROM locations WHERE created_at > ? ORDER BY created_at DESC', - [twentyFourHoursAgo], + [fortyEightHoursAgo], (err, rows) => { if (err) { console.error('Error fetching locations:', err); @@ -190,7 +190,7 @@ app.get('/api/admin/locations', authenticateAdmin, (req, res) => { longitude: row.longitude, persistent: !!row.persistent, created_at: row.created_at, - isActive: new Date(row.created_at) > new Date(Date.now() - 24 * 60 * 60 * 1000) + isActive: new Date(row.created_at) > new Date(Date.now() - 48 * 60 * 60 * 1000) })); res.json(locations); @@ -311,13 +311,19 @@ app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin.html')); }); +// Serve the privacy policy page +app.get('/privacy', (req, res) => { + console.log('Serving the privacy policy page'); + res.sendFile(path.join(__dirname, 'public', 'privacy.html')); +}); + // Start server app.listen(PORT, () => { - console.log('======================================='); - console.log('ICE Watch server successfully started'); + console.log('==========================================='); + console.log('Great Lakes Ice Report server started'); console.log(`Listening on port ${PORT}`); console.log(`Visit http://localhost:${PORT} to view the website`); - console.log('======================================='); + console.log('==========================================='); }); // Graceful shutdown