Compare commits

..

10 commits

Author SHA1 Message Date
Claude Code
a8f2078e5b Add server update scripts and fix repository URLs
All checks were successful
CI / Lint Code (push) Successful in 1m39s
CI / Security Checks (push) Successful in 1m30s
CI / Validate i18n Files (push) Successful in 9s
CI / TypeScript Type Check (push) Successful in 1m49s
CI / Build Project (push) Successful in 1m59s
CI / Run Tests (Node 18) (push) Successful in 2m8s
Auto Tag Release / Create Auto Tag (push) Successful in 2m22s
CI / Run Tests (Node 20) (push) Successful in 2m18s
CI / Test Coverage (push) Successful in 3m13s
Deploy Scripts to S3 / deploy-scripts (push) Successful in 34s
- Add scripts/server-update.sh for production deployments
- Add scripts/dev-update.sh for development updates
- Update repository URLs to signal-works/icewatch
- Document update scripts in CLAUDE.md

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-08 21:27:11 -04:00
047456b1d8 Merge pull request 'Fix double dots in privacy policy disclaimer' (#25) from feature/fix-privacy-policy-double-dots into main
Reviewed-on: deco/ice#25
2025-07-08 06:01:06 +03:00
98d5bda641 Merge branch 'main' into feature/fix-privacy-policy-double-dots 2025-07-08 05:55:24 +03:00
5c3cb388cd Merge pull request 'Add critical git workflow rules to CLAUDE.md' (#24) from feature/add-git-workflow-rules into main
Reviewed-on: deco/ice#24
2025-07-08 05:55:03 +03:00
Claude Code
7325a30f94 Fix double dots in privacy policy disclaimer
Removed extra bullet point from footer disclaimer text in both SharedFooter.ts and en.json translation file.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 22:54:26 -04:00
Claude Code
543ad96109 Add critical git workflow rules to CLAUDE.md
Added prominent warning section at top of file with:
- Mandatory branching workflow rules
- Step-by-step git commands
- Pre-commit checklist
- Strong language to prevent main branch commits

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 22:51:01 -04:00
Claude Code
4610796392 Add rounded corners to footer
Updated footer border-radius to include all corners instead of just top corners for improved visual appearance.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 22:45:34 -04:00
Claude Code
ec60d6bd2a 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>
2025-07-07 22:37:20 -04:00
87fef8309d Merge pull request 'Fix CI test job naming to distinguish Node.js versions' (#23) from fix/ci-test-job-naming into main
Reviewed-on: deco/ice#23
2025-07-08 05:23:11 +03:00
Claude Code
c7a2bb6848 Fix language selector disappearing on navigation
- Remove missing theme-utils.js references from all HTML files
- Fix double initialization in i18n.js causing language selector conflicts
- Update service worker cache version to force fresh file loading
- Language selector now persists when navigating via snowflake icon

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-07 22:21:11 -04:00
17 changed files with 407 additions and 14 deletions

View file

@ -2,8 +2,49 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## ⚠️ CRITICAL RULES - READ FIRST
### Git Workflow - ABSOLUTELY MANDATORY
- **NEVER COMMIT DIRECTLY TO MAIN/MASTER BRANCH**
- **ALWAYS CREATE FEATURE BRANCHES FOR ANY CHANGES**
- **USE TEA CLI FOR GIT.DECO.SH REPOSITORY**
### Required Git Workflow
```bash
# STEP 1: ALWAYS create a feature branch first
git checkout -b feature/brief-description
# STEP 2: Make your changes and commit
git add .
git commit -m "Your commit message"
# STEP 3: Push branch and create PR using tea cli
git push -u origin feature/brief-description
tea pr create
```
### Pre-Commit Checklist
- [ ] Confirm you are NOT on main/master branch: `git branch --show-current`
- [ ] Run tests: `npm test`
- [ ] Run linting: `npm run lint`
- [ ] Build succeeds: `npm run build`
**VIOLATION OF THESE RULES IS UNACCEPTABLE**
## Development Commands
### Quick Update Scripts
For easier development and deployment updates:
```bash
# Development update (pull, install, build, test)
./scripts/dev-update.sh
# Production server update (requires sudo, handles service restart)
sudo ./scripts/server-update.sh
```
### Running the Application
```bash
# Install dependencies

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

@ -188,8 +188,8 @@
</footer>
<!-- Load shared theme utility -->
<script src="theme-utils.js"></script>
<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

@ -9,7 +9,6 @@ class I18nService {
this.defaultLocale = 'en';
this.availableLocales = ['en', 'es-MX'];
this.currentLocale = this.getStoredLocale() || this.detectBrowserLocale();
this.loadTranslations();
}
/**

View file

@ -165,18 +165,17 @@ 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>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Load shared theme utility -->
<script src="theme-utils.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,7 +139,7 @@
</footer>
<!-- Load shared theme utility -->
<script src="theme-utils.js"></script>
<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-v1';
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();
}

40
scripts/dev-update.sh Executable file
View file

@ -0,0 +1,40 @@
#!/bin/bash
# Development Update Script for Ice Watch Application
# Quick script for updating during development
set -e
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}
log "Starting development update..."
# Pull latest changes
log "Pulling latest changes from git..."
git pull origin main
# Install dependencies
log "Installing/updating dependencies..."
npm install
# Build the application
log "Building application..."
npm run build
# Run tests
log "Running tests..."
npm test
log "✅ Development update completed successfully!"
log "You can now run 'npm start' or 'npm run dev:full' to start the application"

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/signal-works/icewatch'
};
// 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/signal-works/icewatch'
};
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');
}

86
scripts/server-update.sh Executable file
View file

@ -0,0 +1,86 @@
#!/bin/bash
# Server Update Script for Ice Watch Application
# This script handles updating the production server with proper user permissions
set -e
# Configuration
APP_DIR="/opt/icewatch"
APP_USER="icewatch"
SERVICE_NAME="icewatch"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Functions
log() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}"
}
warn() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}"
exit 1
}
# Check if running as root
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (use sudo)"
fi
# Check if app directory exists
if [[ ! -d "$APP_DIR" ]]; then
error "Application directory $APP_DIR does not exist"
fi
# Check if service exists
if ! systemctl list-unit-files | grep -q "$SERVICE_NAME.service"; then
error "Service $SERVICE_NAME does not exist"
fi
log "Starting server update process..."
# Stop the service
log "Stopping $SERVICE_NAME service..."
systemctl stop "$SERVICE_NAME"
# Navigate to app directory as app user and update
log "Updating application code..."
sudo -u "$APP_USER" bash -c "
cd '$APP_DIR' && \
git fetch origin && \
git reset --hard origin/main && \
npm install --production && \
npm run build
"
# Check if build was successful
if [[ $? -ne 0 ]]; then
error "Build failed! Check the logs above."
fi
# Start the service
log "Starting $SERVICE_NAME service..."
systemctl start "$SERVICE_NAME"
# Check service status
sleep 2
if systemctl is-active --quiet "$SERVICE_NAME"; then
log "✅ Service $SERVICE_NAME is running successfully"
else
error "❌ Service $SERVICE_NAME failed to start"
fi
# Show recent logs
log "Recent service logs:"
journalctl -u "$SERVICE_NAME" -n 10 --no-pager
log "🎉 Server update completed successfully!"
log "Application is now running the latest version from main branch"

View file

@ -42,7 +42,7 @@ export class SharedFooter {
${this.config.showDisclaimer ? `
<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>
<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>

View file

@ -193,7 +193,7 @@
"footer": {
"safetyNotice": "Safety Notice: This is a community tool for awareness. Stay safe and",
"knowRights": "know your rights",
"disclaimer": "This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours."
"disclaimer": "This website is for informational purposes only. Verify information independently. Reports are automatically deleted after 48 hours."
},
"nojs": {
"notice": "JavaScript is disabled. For the best experience with interactive maps, please enable JavaScript.",

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

@ -8,7 +8,7 @@ footer {
padding: $spacing-lg;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
border-radius: $border-radius-md $border-radius-md 0 0;
border-radius: $border-radius-md;
p {
margin: $spacing-sm 0;

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 => {