Skip to content

Commit 6a97491

Browse files
committed
fix offline mode bug
1 parent cf1661c commit 6a97491

File tree

4 files changed

+141
-72
lines changed

4 files changed

+141
-72
lines changed

index.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<head>
44
<meta charset="UTF-8" />
55
<!-- Root path works in dev; Vite rebases on build to /ship-log-map/... -->
6-
<link rel="icon" href="/favicon.ico" />
6+
<link rel="icon" href="favicon.ico" />
77
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
88

99
<!-- PWA: Manifest + meta -->
10-
<link rel="manifest" href="/manifest.webmanifest" />
10+
<link rel="manifest" href="manifest.webmanifest" />
1111
<meta name="theme-color" content="#0f172a" />
1212

1313
<!-- Modern capability hint (keep the Apple tag too if you like) -->
@@ -16,13 +16,13 @@
1616
<meta name="apple-mobile-web-app-capable" content="yes" />
1717
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
1818

19-
<link rel="apple-touch-icon" href="/icon-192.png" />
19+
<link rel="apple-touch-icon" href="icon-192.png" />
2020

2121
<title>Web App Map</title>
2222
</head>
2323
<body>
2424
<div id="root"></div>
2525

26-
<script type="module" src="/src/main.jsx"></script>
26+
<script type="module" src="src/main.jsx"></script>
2727
</body>
2828
</html>

public/manifest.webmanifest

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
{
22
"name": "Web Map App",
33
"short_name": "WebMap",
4-
"id": "/ship-log-map/",
5-
"start_url": "/ship-log-map/?source=pwa",
6-
"scope": "/ship-log-map/",
4+
"start_url": "./?source=pwa",
5+
"scope": "./",
76
"display": "standalone",
87
"display_override": ["standalone", "window-controls-overlay"],
98
"theme_color": "#0f172a",
109
"background_color": "#ffffff",
1110
"description": "Outer Wilds-inspired rumor map with nodes, edges, notes, and images — now offline-capable.",
1211
"icons": [
13-
{ "src": "/ship-log-map/logo192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
14-
{ "src": "/ship-log-map/logo512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
12+
{ "src": "logo192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
13+
{ "src": "logo512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
1514
]
1615
}

public/service-worker.js

Lines changed: 112 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,155 +5,221 @@
55
* It will only control pages under /ship-log-map/ (non-overlapping with other apps).
66
*/
77

8-
const APP_VERSION = '0.0.1'; // <-- CI will inject package.json version (see README/CI step)
8+
/* ship-log-map: Service Worker (offline + update + installable) */
9+
10+
const APP_VERSION = '0.0.1';
911
const CACHE_PREFIX = 'ship-log-map';
1012
const PRECACHE = `${CACHE_PREFIX}-precache-v${APP_VERSION}`;
1113
const RUNTIME = `${CACHE_PREFIX}-runtime-v${APP_VERSION}`;
1214

13-
// Compute BASE_URL from the service worker script URL directory.
14-
// Example: https://avidrucker.github.io/ship-log-map/service-worker.js -> '/ship-log-map/'
15+
// ✅ Compute BASE_URL from service worker location
1516
const BASE_URL = (() => {
1617
const url = new URL(self.location.href);
17-
const path = url.pathname.replace(/[^/]+$/, ''); // drop filename
18+
const path = url.pathname.replace(/[^/]+$/, '');
1819
return path.endsWith('/') ? path : path + '/';
1920
})();
2021

22+
console.log('[SW] BASE_URL detected:', BASE_URL);
23+
2124
const CORE_URLS = [
2225
`${BASE_URL}`,
2326
`${BASE_URL}index.html`,
2427
`${BASE_URL}manifest.webmanifest`,
2528
`${BASE_URL}offline.html`,
26-
// Icons (you will add these soon)
2729
`${BASE_URL}logo192.png`,
2830
`${BASE_URL}logo512.png`,
31+
`${BASE_URL}favicon.ico`,
2932
];
3033

31-
// Message channel (optional): allow page to request immediate activation
3234
self.addEventListener('message', (event) => {
3335
if (event.data && event.data.type === 'SKIP_WAITING') {
3436
self.skipWaiting();
3537
}
3638
});
3739

38-
// Helper: fetch index.html and pull out hashed asset URLs that Vite injects (assets/*.js|css)
40+
// ✅ IMPROVED: Discover build assets more reliably
3941
async function discoverBuildAssets() {
4042
try {
4143
const res = await fetch(`${BASE_URL}index.html`, { cache: 'no-store' });
4244
if (!res.ok) return [];
4345
const html = await res.text();
4446
const urls = new Set();
4547

46-
const assetRegex = new RegExp(`${BASE_URL.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}assets/[^"']+\\.(?:js|css)`, 'g');
47-
for (const m of html.matchAll(assetRegex)) {
48-
urls.add(m[0]);
48+
// Match script src and link href
49+
const scriptRegex = /<script[^>]+src=["']([^"']+)["']/gi;
50+
const linkRegex = /<link[^>]+href=["']([^"']+)["']/gi;
51+
52+
let match;
53+
54+
// Find all script tags
55+
while ((match = scriptRegex.exec(html)) !== null) {
56+
let src = match[1];
57+
// Convert relative paths to absolute
58+
if (!src.startsWith('http')) {
59+
if (src.startsWith('/')) {
60+
src = `${self.location.origin}${src}`;
61+
} else {
62+
src = `${self.location.origin}${BASE_URL}${src}`;
63+
}
64+
}
65+
// Only cache same-origin assets
66+
if (src.startsWith(self.location.origin)) {
67+
urls.add(src);
68+
}
4969
}
50-
51-
// Also cache the Vite SVG and any CSS referenced by link tags
52-
const viteSvg = `${BASE_URL}vite.svg`;
53-
urls.add(viteSvg);
54-
55-
// Look for <link rel="stylesheet" href="..."> too
56-
const linkHrefRegex = /<link\s+[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["']/gi;
57-
let lm;
58-
while ((lm = linkHrefRegex.exec(html)) !== null) {
59-
const href = lm[1].startsWith('http') ? lm[1] : new URL(lm[1], `${location.origin}${BASE_URL}`).pathname.replace(location.origin, '');
60-
// Only cache same-origin & within scope
61-
if (href.startsWith(BASE_URL)) urls.add(href);
70+
71+
// Find all link tags (CSS, icons, etc.)
72+
while ((match = linkRegex.exec(html)) !== null) {
73+
let href = match[1];
74+
// Convert relative paths to absolute
75+
if (!href.startsWith('http')) {
76+
if (href.startsWith('/')) {
77+
href = `${self.location.origin}${href}`;
78+
} else {
79+
href = `${self.location.origin}${BASE_URL}${href}`;
80+
}
81+
}
82+
// Only cache same-origin assets
83+
if (href.startsWith(self.location.origin)) {
84+
urls.add(href);
85+
}
6286
}
6387

88+
console.log('[SW] Discovered assets:', Array.from(urls));
6489
return Array.from(urls);
65-
} catch (_e) {
90+
} catch (err) {
91+
console.error('[SW] Failed to discover assets:', err);
6692
return [];
6793
}
6894
}
6995

70-
// Install: precache the app shell and discovered build assets
96+
// Install: precache
7197
self.addEventListener('install', (event) => {
98+
console.log('[SW] Installing...');
7299
event.waitUntil((async () => {
73100
const cache = await caches.open(PRECACHE);
74101
const assets = await discoverBuildAssets();
75102
const toCache = [...CORE_URLS, ...assets];
76-
await cache.addAll(toCache);
77-
// Take over quickly
103+
104+
console.log('[SW] Caching:', toCache);
105+
106+
// Cache each URL individually to avoid failures blocking install
107+
const results = await Promise.allSettled(
108+
toCache.map(url => cache.add(url).catch(err => {
109+
console.warn('[SW] Failed to cache:', url, err);
110+
}))
111+
);
112+
113+
const failed = results.filter(r => r.status === 'rejected');
114+
if (failed.length > 0) {
115+
console.warn('[SW] Some assets failed to cache:', failed.length);
116+
}
117+
78118
await self.skipWaiting();
119+
console.log('[SW] Install complete');
79120
})());
80121
});
81122

82-
// Activate: clean old caches and claim clients
123+
// Activate: clean old caches
83124
self.addEventListener('activate', (event) => {
125+
console.log('[SW] Activating...');
84126
event.waitUntil((async () => {
85127
const names = await caches.keys();
86128
await Promise.all(
87129
names
88130
.filter((n) => ![PRECACHE, RUNTIME].includes(n) && n.startsWith(CACHE_PREFIX))
89-
.map((n) => caches.delete(n))
131+
.map((n) => {
132+
console.log('[SW] Deleting old cache:', n);
133+
return caches.delete(n);
134+
})
90135
);
91136
await self.clients.claim();
137+
console.log('[SW] Activation complete');
92138
})());
93139
});
94140

95141
// Strategy helpers
96142
async function networkFirst(request) {
97143
try {
144+
console.log('[SW] Network first:', request.url);
98145
const fresh = await fetch(request);
99146
const cache = await caches.open(RUNTIME);
100-
if (fresh && fresh.ok) cache.put(request, fresh.clone());
147+
if (fresh && fresh.ok) {
148+
cache.put(request, fresh.clone());
149+
}
101150
return fresh;
102-
} catch (_e) {
151+
} catch (err) {
152+
console.log('[SW] Network failed, trying cache:', request.url);
103153
const cached = await caches.match(request);
104-
if (cached) return cached;
105-
// If it's a navigation, fall back to cached index or offline page
154+
if (cached) {
155+
console.log('[SW] Serving from cache:', request.url);
156+
return cached;
157+
}
158+
159+
// Navigation fallback
106160
if (request.mode === 'navigate') {
107161
const index = await caches.match(`${BASE_URL}index.html`);
108-
if (index) return index;
162+
if (index) {
163+
console.log('[SW] Serving index.html fallback');
164+
return index;
165+
}
109166
const offline = await caches.match(`${BASE_URL}offline.html`);
110-
if (offline) return offline;
167+
if (offline) {
168+
console.log('[SW] Serving offline.html');
169+
return offline;
170+
}
111171
}
112-
throw _e;
172+
throw err;
113173
}
114174
}
115175

116176
async function cacheFirst(request) {
117177
const cached = await caches.match(request);
118-
if (cached) return cached;
178+
if (cached) {
179+
console.log('[SW] Cache hit:', request.url);
180+
return cached;
181+
}
182+
console.log('[SW] Cache miss, fetching:', request.url);
119183
const res = await fetch(request);
120184
const cache = await caches.open(RUNTIME);
121-
if (res && res.ok) cache.put(request, res.clone());
185+
if (res && res.ok) {
186+
cache.put(request, res.clone());
187+
}
122188
return res;
123189
}
124190

125191
// Fetch handler
126192
self.addEventListener('fetch', (event) => {
127193
const { request } = event;
128194

129-
// Only handle GET requests
130195
if (request.method !== 'GET') return;
131196

132197
const url = new URL(request.url);
133198

134-
// Only handle requests within our scope (same origin + path starts with BASE_URL)
135-
if (url.origin !== location.origin || !url.pathname.startsWith(BASE_URL)) {
199+
// ✅ IMPORTANT: Only handle same-origin requests
200+
if (url.origin !== self.location.origin) {
201+
console.log('[SW] Ignoring cross-origin:', request.url);
136202
return;
137203
}
138204

139-
// Navigations -> Network-first with offline fallback
205+
// Navigation -> Network-first
140206
if (request.mode === 'navigate') {
141207
event.respondWith(networkFirst(request));
142208
return;
143209
}
144210

145-
// HTML files -> Network-first
211+
// HTML -> Network-first
146212
if (url.pathname.endsWith('.html')) {
147213
event.respondWith(networkFirst(request));
148214
return;
149215
}
150216

151-
// Hashed build assets (Vite) -> Cache-first (they are content-hashed)
152-
if (url.pathname.startsWith(`${BASE_URL}assets/`) || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
217+
// Assets (JS/CSS) -> Cache-first
218+
if (url.pathname.includes('/assets/') || url.pathname.endsWith('.js') || url.pathname.endsWith('.css')) {
153219
event.respondWith(cacheFirst(request));
154220
return;
155221
}
156222

157-
// Everything else under scope -> Cache-first as a safe default
223+
// Everything else -> Cache-first
158224
event.respondWith(cacheFirst(request));
159-
});
225+
});

src/swRegistration.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,53 @@
1-
// src/swRegistration.js
2-
// Light-weight Service Worker registration + update prompt hook
3-
41
export function registerSW() {
5-
if (!('serviceWorker' in navigator)) return;
2+
if (!('serviceWorker' in navigator)) {
3+
console.warn('[PWA] Service Worker not supported');
4+
return;
5+
}
66

7-
// ✅ Skip in dev
8-
if (import.meta.env.DEV) return;
7+
// ✅ Skip in dev (unless you want to test)
8+
if (import.meta.env.DEV) {
9+
console.log('[PWA] Skipping SW registration in dev mode');
10+
return;
11+
}
912

10-
// Use BASE_URL so this works in dev and on GitHub Pages subpath
1113
const swUrl = `${import.meta.env.BASE_URL}service-worker.js`;
14+
console.log('[PWA] Registering service worker:', swUrl);
1215

1316
window.addEventListener('load', () => {
1417
navigator.serviceWorker.register(swUrl).then((reg) => {
15-
// Listen for new SW installation
18+
console.log('[PWA] Service worker registered successfully:', reg.scope);
19+
1620
reg.addEventListener('updatefound', () => {
1721
const nw = reg.installing;
1822
if (!nw) return;
23+
24+
console.log('[PWA] New service worker installing...');
25+
1926
nw.addEventListener('statechange', () => {
20-
// When the new SW is installed and we already have a controller, an update is ready
27+
console.log('[PWA] Service worker state:', nw.state);
28+
2129
if (nw.state === 'installed' && navigator.serviceWorker.controller) {
22-
// Option A: simple console notice (replace with your toast UI)
23-
// To apply immediately:
24-
// promptUserToReload(reg);
2530
console.log('[PWA] New version available. Reload to update.');
2631
}
2732
});
2833
});
2934

30-
// Optional: listen for controller changes and reload once the new SW takes control
3135
navigator.serviceWorker.addEventListener('controllerchange', () => {
32-
// Uncomment to auto-reload when activating a new service worker
36+
console.log('[PWA] Controller changed, reloading...');
37+
// Uncomment to auto-reload:
3338
// window.location.reload();
3439
});
3540
}).catch((err) => {
36-
console.warn('[PWA] SW registration failed:', err);
41+
console.error('[PWA] SW registration failed:', err);
3742
});
3843
});
3944
}
4045

41-
// If you build a toast UI, you can call this from it:
4246
export function promptUserToReload(reg) {
4347
if (reg.waiting) {
4448
reg.waiting.postMessage({ type: 'SKIP_WAITING' });
4549
}
4650
navigator.serviceWorker.addEventListener('controllerchange', () => {
4751
window.location.reload();
4852
});
49-
}
53+
}

0 commit comments

Comments
 (0)