Add version footer and disable map scroll wheel zoom

- Add build-time version generation script that captures git commit info
- Create /api/version endpoint to serve version data
- Add version footer component showing commit SHA, date, and branch
- Link version SHA to commit on git.deco.sh for easy navigation
- Fix footer text duplication issue with i18n translations
- Disable mouse wheel zoom on map, require +/- buttons for better UX
- Update service worker cache to v4 with new version-footer.js

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code 2025-07-07 22:37:20 -04:00
parent 87fef8309d
commit ec60d6bd2a
10 changed files with 237 additions and 6 deletions

View file

@ -13,10 +13,11 @@
"watch-css": "sass src/scss/main.scss public/style.css --watch --style=expanded --source-map",
"dev-with-css": "concurrently \"npm run watch-css\" \"npm run dev\"",
"dev-with-css:ts": "concurrently \"npm run watch-css\" \"npm run dev:ts\"",
"build": "npm run build:ts && npm run build-css && npm run build:frontend && npm run copy-i18n",
"build": "npm run generate-version && npm run build:ts && npm run build-css && npm run build:frontend && npm run copy-i18n",
"build:ts": "tsc",
"copy-i18n": "mkdir -p dist/i18n/locales && cp -r src/i18n/locales/* dist/i18n/locales/",
"test": "jest --runInBand --forceExit",
"generate-version": "node scripts/generate-version.js",
"test": "jest --runInBand --forceExit --detectOpenHandles",
"test:coverage": "jest --coverage",
"lint": "eslint src/ tests/",
"lint:fix": "eslint src/ tests/ --fix",

View file

@ -190,5 +190,6 @@
<!-- Load shared theme utility -->
<script src="utils.js"></script>
<script src="admin.js"></script>
<script src="version-footer.js"></script>
</body>
</html>

View file

@ -1,5 +1,7 @@
document.addEventListener('DOMContentLoaded', async () => {
const map = L.map('map').setView([42.9634, -85.6681], 10);
const map = L.map('map', {
scrollWheelZoom: false
}).setView([42.9634, -85.6681], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,

View file

@ -165,9 +165,9 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
</div>
<footer>
<p><strong data-i18n="footer.safetyNotice">Safety Notice:</strong> This is a community tool for awareness. Stay safe and <a href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener noreferrer" style="color: #007bff; text-decoration: underline;" data-i18n="footer.knowRights">know your rights</a>.</p>
<p><span data-i18n="footer.safetyNotice">Safety Notice: This is a community tool for awareness. Stay safe and</span> <a href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener noreferrer" style="color: #007bff; text-decoration: underline;" data-i18n="footer.knowRights">know your rights</a>.</p>
<div class="disclaimer">
<small><span data-i18n="footer.disclaimer">This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours.</span> <a href="/privacy" style="color: #007bff; text-decoration: underline;" data-i18n="common.privacyPolicy">Privacy Policy</a></small>
<small><span data-i18n="footer.disclaimer">This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours.</span> <a href="/privacy" style="color: #007bff; text-decoration: underline;" data-i18n="common.privacyPolicy">Privacy Policy</a></small>
</div>
</footer>
</div>
@ -175,6 +175,7 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="utils.js"></script>
<script src="app-mapbox.js"></script>
<script src="version-footer.js"></script>
<!-- PWA Service Worker Registration -->
<script>

View file

@ -139,6 +139,7 @@
</footer>
<!-- Load shared theme utility -->
<script src="version-footer.js"></script>
<script>
// Initialize theme when page loads
document.addEventListener('DOMContentLoaded', initializeTheme);

View file

@ -1,5 +1,5 @@
// Service Worker for Great Lakes Ice Report PWA
const CACHE_NAME = 'ice-report-v2';
const CACHE_NAME = 'ice-report-v4';
const OFFLINE_URL = '/offline.html';
// Files to cache for offline functionality
@ -12,6 +12,7 @@ const CACHE_FILES = [
'/app.js',
'/admin.js',
'/utils.js',
'/version-footer.js',
'/manifest.json',
OFFLINE_URL
];

101
public/version-footer.js Normal file
View file

@ -0,0 +1,101 @@
/**
* Version Footer Utility
* Fetches version information and adds it to page footers
*/
class VersionFooter {
constructor() {
this.versionData = null;
this.init();
}
async init() {
try {
await this.fetchVersionData();
this.addVersionToFooter();
} catch (error) {
console.warn('Could not load version information:', error);
this.addFallbackVersion();
}
}
async fetchVersionData() {
const response = await fetch('/api/version');
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
this.versionData = await response.json();
}
addVersionToFooter() {
const footers = document.querySelectorAll('footer');
footers.forEach(footer => {
// Check if version info already exists
if (footer.querySelector('.version-info')) {
return;
}
const versionDiv = document.createElement('div');
versionDiv.className = 'version-info';
versionDiv.style.cssText = `
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid #e0e0e0;
font-size: 11px;
color: #666;
text-align: center;
`;
if (this.versionData) {
const commitUrl = `${this.versionData.gitUrl}/commit/${this.versionData.sha}`;
const commitDate = new Date(this.versionData.commitDate).toLocaleDateString();
versionDiv.innerHTML = `
<span>Version: <a href="${commitUrl}" target="_blank" rel="noopener noreferrer"
style="color: #666; text-decoration: none; font-family: monospace;"
title="View commit: ${this.versionData.commitMessage}">${this.versionData.shortSha}</a></span>
<span style="margin-left: 8px;"> ${commitDate}</span>
<span style="margin-left: 8px;"> Branch: ${this.versionData.branch}</span>
`;
} else {
versionDiv.innerHTML = '<span>Version information unavailable</span>';
}
footer.appendChild(versionDiv);
});
}
addFallbackVersion() {
const footers = document.querySelectorAll('footer');
footers.forEach(footer => {
if (footer.querySelector('.version-info')) {
return;
}
const versionDiv = document.createElement('div');
versionDiv.className = 'version-info';
versionDiv.style.cssText = `
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid #e0e0e0;
font-size: 11px;
color: #666;
text-align: center;
`;
versionDiv.innerHTML = '<span>Version information unavailable</span>';
footer.appendChild(versionDiv);
});
}
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
new VersionFooter();
});
} else {
new VersionFooter();
}

View file

@ -0,0 +1,72 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
try {
// Get the current commit SHA (short version)
const fullSha = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
const shortSha = fullSha.substring(0, 7);
// Get the current branch name
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
// Get the commit date
const commitDate = execSync('git log -1 --format=%cI', { encoding: 'utf8' }).trim();
// Get the commit message (first line only)
const commitMessage = execSync('git log -1 --format=%s', { encoding: 'utf8' }).trim();
const versionInfo = {
sha: fullSha,
shortSha: shortSha,
branch: branch,
commitDate: commitDate,
commitMessage: commitMessage,
buildDate: new Date().toISOString(),
gitUrl: 'https://git.deco.sh/deco/ice'
};
// Ensure dist directory exists
const distDir = path.join(__dirname, '..', 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Write version info to dist directory
fs.writeFileSync(
path.join(distDir, 'version.json'),
JSON.stringify(versionInfo, null, 2)
);
console.log(`✅ Generated version info: ${shortSha} (${branch})`);
console.log(`📅 Commit date: ${commitDate}`);
console.log(`📝 Message: ${commitMessage}`);
} catch (error) {
console.warn('⚠️ Could not generate version info (not in git repository?):', error.message);
// Create fallback version info
const fallbackVersion = {
sha: 'unknown',
shortSha: 'unknown',
branch: 'unknown',
commitDate: new Date().toISOString(),
commitMessage: 'No git information available',
buildDate: new Date().toISOString(),
gitUrl: 'https://git.deco.sh/deco/ice'
};
const distDir = path.join(__dirname, '..', 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
fs.writeFileSync(
path.join(distDir, 'version.json'),
JSON.stringify(fallbackVersion, null, 2)
);
console.log('📦 Created fallback version info');
}

49
src/routes/version.ts Normal file
View file

@ -0,0 +1,49 @@
import { Router, Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
/**
* Version API routes
* Provides build and git version information
*/
export default function createVersionRoutes(): Router {
const router = Router();
/**
* GET /api/version
* Returns version information including git commit SHA and build details
*/
router.get('/', (req: Request, res: Response): void => {
try {
const versionPath = path.join(__dirname, '../version.json');
if (!fs.existsSync(versionPath)) {
res.status(404).json({
error: 'Version information not available',
message: 'Version file not found. Run build process to generate version info.'
});
return;
}
const versionData = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
// Add some additional runtime info
const response = {
...versionData,
serverStartTime: process.env.SERVER_START_TIME || new Date().toISOString(),
nodeVersion: process.version,
environment: process.env.NODE_ENV || 'development'
};
res.json(response);
} catch (error) {
console.error('Error reading version information:', error);
res.status(500).json({
error: 'Failed to read version information',
message: 'Internal server error while reading version data'
});
}
});
return router;
}

View file

@ -23,6 +23,7 @@ import configRoutes from './routes/config';
import locationRoutes from './routes/locations';
import adminRoutes from './routes/admin';
import i18nRoutes from './routes/i18n';
import versionRoutes from './routes/version';
const app: Application = express();
@ -186,6 +187,7 @@ function setupRoutes(): void {
app.use('/api/locations', locationRoutes(locationModel, profanityFilter));
app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, profanityFilter, authenticateAdmin));
app.use('/api/i18n', i18nRoutes());
app.use('/api/version', versionRoutes());
// Static page routes
app.get('/', (req: Request, res: Response): void => {