Offline first

This commit is contained in:
Magnus Åhall 2026-05-20 19:58:34 +02:00
parent 84181858c8
commit f37ebc1c41
4 changed files with 81 additions and 16 deletions

10
main.go
View file

@ -132,6 +132,7 @@ func main() { // {{{
http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/notes2", pageNotes2)
http.HandleFunc("/login", pageLogin) http.HandleFunc("/login", pageLogin)
http.HandleFunc("/sync", pageSync) http.HandleFunc("/sync", pageSync)
http.HandleFunc("/offline", pageOffline)
http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler)
@ -226,6 +227,15 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
} // }}} } // }}}
func pageOffline(w http.ResponseWriter, r *http.Request) { // {{{
page := NewPage("offline")
err := Webengine.Render(page, w, r)
if err != nil {
w.Write([]byte(err.Error()))
return
}
} // }}}
func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{
page := NewPage("login") page := NewPage("login")

View file

@ -85,7 +85,7 @@ export class N2NodeUI extends CustomHTMLElement {
// No point in showing markdown if there is no data. // No point in showing markdown if there is no data.
// If there is no data, it will show a blank page regardless, and the user will most // If there is no data, it will show a blank page regardless, and the user will most
// likely want to edit content, which can't be done in markdown. // likely want to edit content, which can't be done in markdown.
const show = this.node.content().trim() !== '' && state const show = this.node?.content().trim() !== '' && state
switch (show) { switch (show) {
case true: case true:

View file

@ -2,27 +2,41 @@ const CACHE_NAME = 'notes2-{{ .VERSION }}'
const CACHED_ASSETS = [ const CACHED_ASSETS = [
'/', '/',
'/notes2', '/notes2',
'/offline',
'/css/{{ .VERSION }}/main.css', '/css/{{ .VERSION }}/main.css',
'/css/{{ .VERSION }}/markdown.css',
'/css/{{ .VERSION }}/notes2.css', '/css/{{ .VERSION }}/notes2.css',
'/css/{{ .VERSION }}/theme.css',
'/js/{{ .VERSION }}/lib/fullcalendar.min.js', '/images/{{ .VERSION }}/collapsed.svg',
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', '/images/{{ .VERSION }}/expanded.svg',
'/js/{{ .VERSION }}/lib/sjcl.js', '/images/{{ .VERSION }}/icon_markdown_hollow.svg',
'/images/{{ .VERSION }}/icon_markdown.svg',
'/images/{{ .VERSION }}/icon_refresh.svg',
'/images/{{ .VERSION }}/icon_save_disabled.svg',
'/images/{{ .VERSION }}/icon_search.svg',
'/images/{{ .VERSION }}/leaf.svg',
'/images/{{ .VERSION }}/logo.svg',
'/js/{{ .VERSION }}/api.mjs', '/js/{{ .VERSION }}/api.mjs',
'/js/{{ .VERSION }}/app.mjs',
'/js/{{ .VERSION }}/checklist.mjs',
'/js/{{ .VERSION }}/crypto.mjs',
'/js/{{ .VERSION }}/key.mjs',
'/js/{{ .VERSION }}/lib/custom_html_element.mjs',
'/js/{{ .VERSION }}/lib/fullcalendar.min.js',
'/js/{{ .VERSION }}/lib/node_modules/marked/lib/marked.esm.js',
'/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js',
'/js/{{ .VERSION }}/lib/node_modules/marked-token-position/lib/index.esm.js',
'/js/{{ .VERSION }}/lib/sjcl.js',
'/js/{{ .VERSION }}/marked_position.mjs',
'/js/{{ .VERSION }}/mbus.mjs',
'/js/{{ .VERSION }}/node.mjs', '/js/{{ .VERSION }}/node.mjs',
'/js/{{ .VERSION }}/node_store.mjs', '/js/{{ .VERSION }}/node_store.mjs',
'/js/{{ .VERSION }}/notes2.mjs', '/js/{{ .VERSION }}/notes2.mjs',
'/js/{{ .VERSION }}/sync.mjs', '/js/{{ .VERSION }}/sync.mjs',
'/js/{{ .VERSION }}/key.mjs', '/js/{{ .VERSION }}/tree.mjs',
'/js/{{ .VERSION }}/crypto.mjs',
'/js/{{ .VERSION }}/checklist.mjs',
'/images/{{ .VERSION }}/logo.svg',
'/images/{{ .VERSION }}/leaf.svg',
'/images/{{ .VERSION }}/collapsed.svg',
'/images/{{ .VERSION }}/expanded.svg',
] ]
async function precache() { async function precache() {
@ -32,11 +46,48 @@ async function precache() {
async function fetchAsset(event) { async function fetchAsset(event) {
try { try {
return await fetch(event.request)
} catch (e) {
const cache = await caches.open(CACHE_NAME) const cache = await caches.open(CACHE_NAME)
return cache.match(event.request) const match = await cache.match(event.request)
if (match !== undefined) {
// -----------------------------------------------
// This page is precached - return it immediately.
// -----------------------------------------------
//console.log('From cache', event.request.url)
return match
} else {
// ---------------------------------------------------------------
// Not in cache - send it for an online request/browser cache hit.
// ---------------------------------------------------------------
console.log('From network', event.request.url)
const resp = await fetch(event.request)
// This will trigger on an HTTP error such as 502.
if (!resp.ok) {
console.log('HTTP error', resp.status)
// When JSON is expected, return that instead of the offline HTML page.
return await offline(event, `${resp.status} ${resp.statusText}`)
} }
return resp
}
} catch (e) {
// An error here is something like a DNS problem, not a regular HTTP problem.
console.log('Network error', e, event.request.url)
return await offline(event, e)
}
}
async function offline(event, errText) {
if (event.request.headers.get('X-JSON')) {
return new Response('{ "OK": false, "Error": "Network is offline"}', { headers: { 'Content-Type': 'application/json' } })
}
const cache = await caches.open(CACHE_NAME)
const offline = await cache.match('/offline')
let body = await offline.text()
body = body.replace('||ERROR||', errText)
return new Response(body, { headers: { 'Content-Type': 'text/html' } })
} }
async function cleanupCache() { async function cleanupCache() {
@ -61,6 +112,6 @@ self.addEventListener('activate', event => {
}) })
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {
//console.log('SERVICE WORKER: fetch') //console.log('SERVICE WORKER: fetch', event.request.url)
event.respondWith(fetchAsset(event)) event.respondWith(fetchAsset(event))
}) })

View file

@ -0,0 +1,4 @@
{{ define "page" }}
<div>Site is offline.</div>
<div>||ERROR||</div>
{{ end }}