A sequenced schedule of everything not yet addressed, drawn from the Functional Specifications v4, the Open Issues and Estimates handout of 18 September 2026, Development Status Report v9, and this month's review of the running system. The estimates from September are carried over unchanged; a Source column marks which figures are the project's own, which come from the status report, and which are mine. Twelve items are new from the October review and were not in either document: the Save action in the drawing toolbar has no listener registered, so edits are never written back; overlay analysis holds the main thread for about nine seconds at 8,000 features; spatial.lu_parcels carries 3.1 million rows with zero indexes; the parcel geometry column is declared bare so catalogues report SRID 0 while the data is EPSG:4326; collector zones sit in a text column as WKT; and the Applicant Portal's only dependency on this module, the parcel-map embed endpoint, is not built. The Seq column is a recommended start order rather than a serial plan, since fourteen of the sixty-one belong to infrastructure, the database team, LUSPA or the permitting team. It opens with the container's AllowOverride setting, because that leaves an already-deployed fix inert, and raises the four LUSPA inputs and the MoU process early only because they have the longest lead times. Foundations follow — index and repair the parcel and zone data before building on it — then the pilot-workshop feedback, then the remaining specification scope. Role-based access control and the server audit trail appear once rather than twice, and a third sheet lists what has closed since September so nothing is counted in both places. Co-Authored-By: Claude Opus 5 <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%