Add progressive web app companion for cross-platform access

Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

56
pwa/vite.config.ts Normal file
View file

@ -0,0 +1,56 @@
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
define: {
// Injected as a global — changes every build, breaking any stale cache
__BUILD_REVISION__: JSON.stringify(
`${new Date().toISOString().slice(0, 16)}`,
),
},
plugins: [
VitePWA({
registerType: 'autoUpdate',
workbox: {
// Precache all built assets (JS/CSS get content hashes from Vite;
// files in public/ like shelters.json get a Workbox revision hash
// computed from their content, so any change triggers re-fetch).
globPatterns: ['**/*.{js,css,html,json,png,svg,ico,webmanifest}'],
// Remove old precache entries from previous builds
cleanupOutdatedCaches: true,
// SPA: serve index.html for all navigation requests
navigateFallback: '/index.html',
// Vite already hashes JS/CSS filenames — skip Workbox's
// cache-bust query parameter for those
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
// Runtime caching for map tiles (not precached — cached as viewed)
runtimeCaching: [
{
urlPattern: /^https:\/\/[abc]\.tile\.openstreetmap\.org\/.*/,
handler: 'CacheFirst',
options: {
cacheName: 'osm-tiles',
expiration: {
maxEntries: 5000,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
},
cacheableResponse: {
statuses: [0, 200],
},
},
},
],
},
manifest: false, // We provide our own manifest.webmanifest
}),
],
resolve: {
alias: {
'@': '/src',
},
},
});