diff --git a/main.go b/main.go index 96f08aa..b44319e 100644 --- a/main.go +++ b/main.go @@ -132,6 +132,7 @@ func main() { // {{{ http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) http.HandleFunc("/sync", pageSync) + http.HandleFunc("/offline", pageOffline) http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) @@ -226,6 +227,15 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ 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) { // {{{ page := NewPage("login") diff --git a/static/js/node.mjs b/static/js/node.mjs index 339016d..c4b2be0 100644 --- a/static/js/node.mjs +++ b/static/js/node.mjs @@ -85,7 +85,7 @@ export class N2NodeUI extends CustomHTMLElement { // 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 // 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) { case true: diff --git a/static/service_worker.js b/static/service_worker.js index c48c162..6c2e8e2 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -2,27 +2,41 @@ const CACHE_NAME = 'notes2-{{ .VERSION }}' const CACHED_ASSETS = [ '/', '/notes2', + '/offline', '/css/{{ .VERSION }}/main.css', + '/css/{{ .VERSION }}/markdown.css', '/css/{{ .VERSION }}/notes2.css', + '/css/{{ .VERSION }}/theme.css', - '/js/{{ .VERSION }}/lib/fullcalendar.min.js', - '/js/{{ .VERSION }}/lib/node_modules/marked/marked.min.js', - '/js/{{ .VERSION }}/lib/sjcl.js', + '/images/{{ .VERSION }}/collapsed.svg', + '/images/{{ .VERSION }}/expanded.svg', + '/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 }}/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_store.mjs', '/js/{{ .VERSION }}/notes2.mjs', '/js/{{ .VERSION }}/sync.mjs', - '/js/{{ .VERSION }}/key.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', + '/js/{{ .VERSION }}/tree.mjs', ] async function precache() { @@ -32,13 +46,50 @@ async function precache() { async function fetchAsset(event) { try { - return await fetch(event.request) - } catch (e) { 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() { const keys = await caches.keys() const keysToDelete = keys.map(key => { @@ -61,6 +112,6 @@ self.addEventListener('activate', event => { }) self.addEventListener('fetch', event => { - //console.log('SERVICE WORKER: fetch') + //console.log('SERVICE WORKER: fetch', event.request.url) event.respondWith(fetchAsset(event)) }) diff --git a/views/pages/offline.gotmpl b/views/pages/offline.gotmpl new file mode 100644 index 0000000..0c283f9 --- /dev/null +++ b/views/pages/offline.gotmpl @@ -0,0 +1,4 @@ +{{ define "page" }} +