ekke 5967b99a73 Keep the placed point on screen until it becomes the marker
Placing a point with Draw > Point removed it immediately and opened the Add
Location form, so the officer filled in a form with nothing at the spot they had
just tapped, and a marker then appeared from nowhere. Nothing visibly turned
into anything, which is what made it read as broken.

The sketch now stays until the form closes. On save the marker has already been
added by then, so the point is replaced by it; on cancel, or on leaving the
drawing tools with the form still open, it is simply discarded. Placing a second
point while a form is open replaces the pending sketch rather than leaving the
first one behind.

Marker loading is also taken out of the undo history. Entering edit mode calls
UndoRedo._watchSources(), which picks up every vector source including the
markers — so reloading the Locations list registered as two undoable edits, and
Undo after saving a location stripped the markers off the map while leaving the
row in the database. That is the same "my point disappeared" symptom by another
route. clearMarkers() uses _silentClearSource for the reason already documented
on the vertex overlay: ol-ext patches VectorSource.clear to fire block events
that the record flag does not gate.

Verified all three endings — save, cancel, and abandoning by leaving Digitise —
plus placing a second point mid-form, and that the undo stack stays empty
through the whole sequence with recording switched back on afterwards.

Service Worker v21 -> v22.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-21 09:45:58 +02:00
2026-03-04 12:59:40 +01:00
2026-03-04 12:59:40 +01:00
2026-03-04 12:59:40 +01:00

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:

  1. SQLocal already uses its own internal worker - It handles OPFS access internally
  2. OPFS handles file coordination - Multiple SQLocal instances can access the same database file
  3. 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

  1. Check that you're using HTTPS (or localhost)
  2. Verify COOP/COEP headers are present (DevTools → Network → check response headers)
  3. 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:

  1. Use vite-plugin-cross-origin-isolation (add to vite.config.js)
  2. Or manually refresh the browser after changes

License

MIT

Description
progressive web app with sqlocal as a mobile app
Readme 49 MiB
Languages
JavaScript 75.4%
HTML 11%
CSS 9.1%
PHP 2.3%
Python 1.2%
Other 1%