The schedule showed a lecturer only where the session notes named one (Globalizacja), so the student plan had nobody under the other courses. The session JSON now carries the course lecturer in its course ref and an optional per-session lecturer (new nullable column, admin field, seed key) for courses shared between two people. The student app shows the session's own lecturer or, failing that, the course's.
84 lines
3.0 KiB
JavaScript
84 lines
3.0 KiB
JavaScript
const BUILD = '3394fd5feb39'
|
|
// The service worker of the BM student app. The Vite plugin bm-service-worker
|
|
// (vite.config.ts) emits this file as /sw.js and replaces the build id above,
|
|
// so every build that changes an asset gets a fresh cache and an update prompt.
|
|
//
|
|
// It is deliberately minimal: hashed /assets/* files are cached, navigations
|
|
// go to the network with an offline fallback page, and everything else is
|
|
// left to the browser untouched.
|
|
|
|
const CACHE = 'bm-' + BUILD
|
|
const OFFLINE_URL = '/offline.html'
|
|
|
|
// API, realtime, upload and admin paths are never intercepted. The speaker
|
|
// deck (/_presentations) holds notes and text meant only for its owner, and
|
|
// no response from these prefixes may ever land in a device cache. The list
|
|
// mirrors reservedPrefixes in plugins/jz/bm/static.go plus /ws and the admin.
|
|
const SKIP_PREFIXES = ['/_user', '/_bm', '/_quizzes', '/_presentations', '/api', '/ws', '/storage', '/bmadmin']
|
|
|
|
// swRoute classifies a request: 'skip' leaves it to the browser, 'page' is a
|
|
// network-only navigation with an offline fallback, 'asset' is cache-first.
|
|
function swRoute(method, href, mode, origin) {
|
|
if (method !== 'GET') return 'skip'
|
|
let url
|
|
try {
|
|
url = new URL(href)
|
|
} catch {
|
|
return 'skip'
|
|
}
|
|
if (url.origin !== origin) return 'skip'
|
|
const path = url.pathname
|
|
// The reserved check runs before the navigation check, so even a top-level
|
|
// navigation to /storage/... or /bmadmin is never touched.
|
|
for (const prefix of SKIP_PREFIXES) {
|
|
if (path === prefix || path.startsWith(prefix + '/')) return 'skip'
|
|
}
|
|
if (mode === 'navigate') return 'page'
|
|
if (path.startsWith('/assets/')) return 'asset'
|
|
return 'skip'
|
|
}
|
|
|
|
self.addEventListener('install', (event) => {
|
|
// No skipWaiting here: a new worker waits until the user asks to refresh.
|
|
event.waitUntil(caches.open(CACHE).then((cache) => cache.add(new Request(OFFLINE_URL, { cache: 'reload' }))))
|
|
})
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(
|
|
caches
|
|
.keys()
|
|
.then((keys) => Promise.all(keys.filter((key) => key.startsWith('bm-') && key !== CACHE).map((key) => caches.delete(key))))
|
|
.then(() => self.clients.claim()),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const req = event.request
|
|
const route = swRoute(req.method, req.url, req.mode, self.location.origin)
|
|
if (route === 'skip') return
|
|
|
|
if (route === 'page') {
|
|
// Navigations are never stored: an HTML response could carry user state.
|
|
event.respondWith(
|
|
fetch(req).catch(() => caches.match(OFFLINE_URL).then((res) => res || Response.error())),
|
|
)
|
|
return
|
|
}
|
|
|
|
event.respondWith(
|
|
caches.open(CACHE).then((cache) =>
|
|
cache.match(req).then((hit) => {
|
|
if (hit) return hit
|
|
return fetch(req).then((res) => {
|
|
if (res.status === 200 && res.type === 'basic') cache.put(req, res.clone())
|
|
return res
|
|
})
|
|
}),
|
|
),
|
|
)
|
|
})
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting()
|
|
})
|