The tracked report had not moved since May and still ended at section 12, the April update — five months and six sections behind. Revisions 13 to 18 were written but only ever existed as -v(n) snapshots, which .gitignore excludes by design: the rule added in 9b57ff9e keeps interim snapshots out of the repository and says the current report stays tracked. That second half had stopped happening, so the repository's copy was quietly stale while the real document moved on outside it. This replaces the tracked file with the current revision, which now runs to section 18 (September 2026) and covers the period April to September. Section 18 records this period's work: the drawing toolbar rebuilt as a docked left rail, four rounds of correction against a real iPhone, the two ways of adding a point reduced to one, the Markers-layer defect that had been present since the beginning, the Web Mercator correction to circle measurements, and the first part of the user manual alongside the schema summary. Two things it states plainly rather than glossing. The typed circle radius and the movable analysis popup are recorded as under test on a branch, not as shipped — LUSPA confirmed them on a desktop, but the remote server was down and the touch behaviour is unverified. And 18.2 lists the four iPhone failures with their causes, because the pattern is more useful for planning than the individual faults: desktop testing at a phone-sized window verifies layout arithmetic, but not stylesheet load order in the built application, nor how iOS paints. The -v(n) files stay ignored. If those snapshots should be versioned too, the ignore rule is the thing to change, not this commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LUPMIS PWA with Offline SQLite and Maps
A Progressive Web App with:
- OpenLayers map with ol-ext LayerSwitcher for base map selection
- SQLocal for SQLite database in the browser (via OPFS)
- BroadcastChannel for cross-tab synchronization
- Service Worker for asset caching and offline support
- Vite for development and building
Features
- 🗺️ Interactive map with 5 base layers (OSM, Satellite, Topo, Carto Light/Dark)
- 📍 Click map to set coordinates, markers colored by category
- 💾 Offline SQLite database (data persists in browser)
- 🔄 Cross-tab sync via BroadcastChannel
- 📴 Works offline (cached assets + up to 500 map tiles)
- 📱 Installable as PWA on mobile and desktop
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Tab 1 │ │ Tab 2 │ │ Tab 3 │ │
│ │ main.js │ │ main.js │ │ main.js │ │
│ │ SQLocal │ │ SQLocal │ │ SQLocal │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └───────────────┼───────────────┘ │
│ │ │
│ BroadcastChannel │
│ (notifies other tabs of changes) │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ OPFS │ ← Single database file │
│ │ (lupmis.db) │ shared by all tabs │
│ └────────────────┘ │
│ │
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
│ │
│ ┌────────────────┐ │
│ │ Service Worker │ ← Caches assets for offline │
│ │ (sw.js) │ │
│ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Why This Architecture?
Initially we considered using a SharedWorker to manage a single database connection. However:
- SQLocal already uses its own internal worker - It handles OPFS access internally
- OPFS handles file coordination - Multiple SQLocal instances can access the same database file
- Simpler is better - BroadcastChannel provides easy cross-tab notification without the complexity of SharedWorker bundling issues in Vite
The result is simpler code that works reliably with Vite's build system.
File Structure
project/
├── index.html # Entry HTML with map container
├── main.js # App entry point
├── vite.config.js # Vite configuration
├── package.json
│
├── src/
│ ├── components/
│ │ └── MapView.js # OpenLayers map with ol-ext LayerSwitcher
│ ├── database.js # SQLocal + BroadcastChannel
│ └── pwa.js # PWA utilities (install, offline)
│
└── public/
├── sw.js # Service Worker (caching)
├── manifest.json # PWA manifest
├── offline.html # Offline fallback page
└── icons/ # PWA icons
Setup
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
Usage
Basic Database Operations
import { sql, dbReady, addLocation, getLocations } from './src/database.js';
// Wait for database to be ready
await dbReady;
// Add a location
await addLocation('Water Point', -1.5234, 7.4567, {
description: 'Main village well',
category: 'water'
});
// Get all locations
const locations = await getLocations();
// Direct SQL queries using tagged templates
const results = await sql`SELECT * FROM locations WHERE category = ${'water'}`;
MapView Component
import { MapView } from './src/components/MapView.js';
// Create map centered on Ghana
const map = new MapView('map-container', {
center: [-1.5, 7.5], // [longitude, latitude]
zoom: 7,
basemap: 'osm' // 'osm' | 'satellite' | 'topo' | 'carto-light' | 'carto-dark'
});
// Set category-based marker colors
map.setCategoryStyles({
'water': { color: '#3b82f6' },
'school': { color: '#f59e0b' },
'health': { color: '#ef4444' },
});
// Add markers from database
const locations = await getLocations();
map.addMarkers(locations);
// Handle map clicks
map.onClick((lon, lat, feature) => {
if (feature) {
// Clicked on existing marker
console.log('Selected:', feature.get('name'));
} else {
// Clicked on empty space - use coordinates
document.getElementById('longitude').value = lon.toFixed(6);
document.getElementById('latitude').value = lat.toFixed(6);
}
});
// Zoom to a location
map.zoomTo(-1.5, 7.5, 14);
// Fit view to show all markers
map.fitToMarkers();
// Select a marker by ID
map.selectMarker(locationId);
Available Base Maps
| Name | Key | Source |
|---|---|---|
| OpenStreetMap | osm |
OpenStreetMap |
| Satellite | satellite |
Esri World Imagery |
| Topographic | topo |
OpenTopoMap |
| Carto Light | carto-light |
CARTO |
| Carto Dark | carto-dark |
CARTO |
Cross-Tab Synchronization
import { onDatabaseChange } from './src/database.js';
// Listen for changes from other tabs
onDatabaseChange((change) => {
console.log('Database changed:', change);
// { table: 'locations', action: 'INSERT', id: 5, timestamp: 1234567890 }
if (change.table === 'locations') {
refreshLocationsList();
}
});
PWA Features
import { initPWA, isOnline, onOfflineChange } from './src/pwa.js';
// Initialize PWA
await initPWA();
// Check online status
if (isOnline()) {
syncWithServer();
}
// React to offline/online changes
onOfflineChange((offline) => {
if (offline) {
showOfflineBanner();
} else {
hideOfflineBanner();
syncWithServer();
}
});
Deployment
Required Headers
Your web server must send these headers for OPFS to work:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Nginx Configuration
server {
listen 443 ssl http2;
server_name your-domain.com;
root /var/www/dist;
index index.html;
# Required for OPFS/SQLite
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
# Cache static assets
location ~* \.(js|css|wasm|png|jpg|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
Apache (.htaccess)
<IfModule mod_headers.c>
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
</IfModule>
OpenResty (Docker)
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
Browser Support
- Chrome/Edge 89+ - Full support
- Firefox 111+ - Full support
- Safari 15.2+ - OPFS supported
- Mobile - Chrome Android, Safari iOS 15.2+
Troubleshooting
"SecurityError" or "NotAllowedError"
The COOP/COEP headers are missing. Check your server configuration.
Database not persisting
- Check that you're using HTTPS (or localhost)
- Verify COOP/COEP headers are present (DevTools → Network → check response headers)
- Check browser DevTools → Application → Storage → OPFS
Changes not syncing between tabs
The BroadcastChannel should handle this automatically. Check the browser console for any errors.
Vite HMR WebSocket errors
The cross-origin isolation can break Vite's hot reload. Options:
- Use
vite-plugin-cross-origin-isolation(add to vite.config.js) - Or manually refresh the browser after changes
License
MIT
Description
Languages
JavaScript
75.2%
HTML
10.6%
CSS
8.5%
PHP
2.7%
Python
1.1%
Other
1.9%