diff --git a/design.drawio b/design.drawio index 6118e3e..bf766ef 100644 --- a/design.drawio +++ b/design.drawio @@ -1,156 +1,153 @@ - + - + - - - + + + - - + + - + - + - + - + - + - - + + - + - - + + - + - + - - + + - + - + - - + + - - + + - + - - + + - + - + - + - + - + - - - - + - + - + - + - - + + - + - + - - + + - + - + - + - + @@ -159,49 +156,49 @@ - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/file.go b/file.go index c5bca8c..51a09fc 100644 --- a/file.go +++ b/file.go @@ -1,6 +1,9 @@ package main import ( + // External + "github.com/jmoiron/sqlx" + // Standard "time" ) @@ -15,3 +18,63 @@ type File struct { MD5 string Uploaded time.Time } + +func AddFile(userID int, file *File) (err error) { // {{{ + file.UserID = userID + + var rows *sqlx.Rows + rows, err = db.Queryx(` + INSERT INTO file(user_id, node_id, filename, size, mime, md5) + VALUES($1, $2, $3, $4, $5, $6) + RETURNING id + `, + file.UserID, + file.NodeID, + file.Filename, + file.Size, + file.MIME, + file.MD5, + ) + if err != nil { + return + } + defer rows.Close() + + rows.Next() + err = rows.Scan(&file.ID) + return +} // }}} +func Files(userID, nodeID, fileID int) (files []File, err error) { // {{{ + var rows *sqlx.Rows + rows, err = db.Queryx( + `SELECT * + FROM file + WHERE + user_id = $1 AND + node_id = $2 AND + CASE $3::int + WHEN 0 THEN true + ELSE id = $3 + END`, + userID, + nodeID, + fileID, + ) + if err != nil { + return + } + defer rows.Close() + + files = []File{} + for rows.Next() { + file := File{} + if err = rows.StructScan(&file); err != nil { + return + } + files = append(files, file) + } + + return +} // }}} + +// vim: foldmethod=marker diff --git a/main.go b/main.go index f789a30..e5063d8 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "net/http" "path" "regexp" + "strconv" "strings" "text/template" ) @@ -109,6 +110,7 @@ func main() { // {{{ http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) http.HandleFunc("/node/tree", authenticated(actionNodeTree)) + http.HandleFunc("/node/retrieve/{id}", authenticated(actionNodeRetrieve)) http.HandleFunc("/service_worker.js", pageServiceWorker) @@ -163,10 +165,13 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ Webengine.StaticResource(w, r) } // }}} -func httpError(w http.ResponseWriter, err error) {// {{{ - j, _ := json.Marshal(struct { OK bool; Error string }{false, err.Error()}) +func httpError(w http.ResponseWriter, err error) { // {{{ + j, _ := json.Marshal(struct { + OK bool + Error string + }{false, err.Error()}) w.Write(j) -}// }}} +} // }}} func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ w.Header().Add("Content-Type", "text/javascript; charset=utf-8") @@ -183,18 +188,14 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ return } - err = tmpl.Execute(w, struct{ VERSION string }{VERSION}) + err = tmpl.Execute(w, struct{ VERSION string; DevMode bool }{VERSION, FlagDev}) if err != nil { w.Write([]byte(err.Error())) return } } // }}} func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ - page := HTMLTemplate.SimplePage{ - Layout: "main", - Page: "login", - Version: VERSION, - } + page := NewPage("login") err := Webengine.Render(page, w, r) if err != nil { @@ -222,12 +223,30 @@ func actionNodeTree(w http.ResponseWriter, r *http.Request) { // {{{ } j, _ := json.Marshal(struct { - OK bool - Nodes []Node + OK bool + Nodes []Node }{true, nodes}) Log.Debug("tree", "nodes", nodes) w.Write(j) } // }}} +func actionNodeRetrieve(w http.ResponseWriter, r *http.Request) { // {{{ + user := getUser(r) + var err error + + idStr := r.PathValue("id") + id, _ := strconv.Atoi(idStr) + + node, err := RetrieveNode(user.ID, id) + if err != nil { + responseError(w, err) + return + } + + responseData(w, map[string]interface{}{ + "OK": true, + "Node": node, + }) +} // }}} func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) diff --git a/node.go b/node.go index e1e815c..5808971 100644 --- a/node.go +++ b/node.go @@ -109,3 +109,695 @@ func NodeTree(userID, startNodeID int) (nodes []Node, err error) { // {{{ return } // }}} +func RetrieveNode(userID, nodeID int) (node Node, err error) { // {{{ + if nodeID == 0 { + return RootNode(userID) + } + + var rows *sqlx.Rows + rows, err = db.Queryx(` + WITH RECURSIVE recurse AS ( + SELECT + id, + user_id, + COALESCE(parent_id, 0) AS parent_id, + COALESCE(crypto_key_id, 0) AS crypto_key_id, + name, + content, + content_encrypted, + markdown, + 0 AS level + FROM node + WHERE + user_id = $1 AND + id = $2 + + UNION + + SELECT + n.id, + n.user_id, + n.parent_id, + COALESCE(n.crypto_key_id, 0) AS crypto_key_id, + n.name, + '' AS content, + '' AS content_encrypted, + false AS markdown, + r.level + 1 AS level + FROM node n + INNER JOIN recurse r ON n.parent_id = r.id AND r.level = 0 + WHERE + n.user_id = $1 + ) + + SELECT * FROM recurse ORDER BY level ASC + `, + userID, + nodeID, + ) + if err != nil { + return + } + defer rows.Close() + + type resultRow struct { + Node + Level int + } + + node = Node{} + node.Children = []Node{} + for rows.Next() { + row := resultRow{} + if err = rows.StructScan(&row); err != nil { + return + } + + if row.Level == 0 { + node.ID = row.ID + node.UserID = row.UserID + node.ParentID = row.ParentID + node.CryptoKeyID = row.CryptoKeyID + node.Name = row.Name + node.Complete = true + node.Markdown = row.Markdown + + if node.CryptoKeyID > 0 { + node.Content = row.ContentEncrypted + } else { + node.Content = row.Content + } + + node.retrieveChecklist() + } + + if row.Level == 1 { + node.Children = append(node.Children, Node{ + ID: row.ID, + UserID: row.UserID, + ParentID: row.ParentID, + CryptoKeyID: row.CryptoKeyID, + Name: row.Name, + }) + } + } + + node.Crumbs, err = NodeCrumbs(node.ID) + node.Files, err = Files(userID, node.ID, 0) + + return +} // }}} +func RootNode(userID int) (node Node, err error) { // {{{ + var rows *sqlx.Rows + rows, err = db.Queryx(` + SELECT + id, + user_id, + 0 AS parent_id, + name + FROM node + WHERE + user_id = $1 AND + parent_id IS NULL + `, + userID, + ) + if err != nil { + return + } + defer rows.Close() + + node.Name = "Start" + node.UserID = userID + node.Complete = true + node.Children = []Node{} + node.Crumbs = []Node{} + node.Files = []File{} + for rows.Next() { + row := Node{} + if err = rows.StructScan(&row); err != nil { + return + } + + node.Children = append(node.Children, Node{ + ID: row.ID, + UserID: row.UserID, + ParentID: row.ParentID, + Name: row.Name, + }) + } + + return +} // }}} +func (node *Node) retrieveChecklist() (err error) { // {{{ + var rows *sqlx.Rows + rows, err = db.Queryx(` + SELECT + g.id AS group_id, + g.order AS group_order, + g.label AS group_label, + + COALESCE(i.id, 0) AS item_id, + COALESCE(i.order, 0) AS item_order, + COALESCE(i.label, '') AS item_label, + COALESCE(i.checked, false) AS checked + + FROM public.checklist_group g + LEFT JOIN public.checklist_item i ON i.checklist_group_id = g.id + WHERE + g.node_id = $1 + ORDER BY + g.order DESC, + i.order DESC + `, node.ID) + if err != nil { + return + } + defer rows.Close() + + groups := make(map[int]*ChecklistGroup) + var found bool + var group *ChecklistGroup + var item ChecklistItem + for rows.Next() { + row := struct { + GroupID int `db:"group_id"` + GroupOrder int `db:"group_order"` + GroupLabel string `db:"group_label"` + + ItemID int `db:"item_id"` + ItemOrder int `db:"item_order"` + ItemLabel string `db:"item_label"` + Checked bool + }{} + err = rows.StructScan(&row) + if err != nil { + return + } + + if group, found = groups[row.GroupID]; !found { + group = new(ChecklistGroup) + group.ID = row.GroupID + group.NodeID = node.ID + group.Order = row.GroupOrder + group.Label = row.GroupLabel + group.Items = []ChecklistItem{} + groups[group.ID] = group + } + + item = ChecklistItem{} + item.ID = row.ItemID + item.GroupID = row.GroupID + item.Order = row.ItemOrder + item.Label = row.ItemLabel + item.Checked = row.Checked + + if item.ID > 0 { + group.Items = append(group.Items, item) + } + } + + node.ChecklistGroups = []ChecklistGroup{} + for _, group := range groups { + node.ChecklistGroups = append(node.ChecklistGroups, *group) + } + + return +} // }}} +func NodeCrumbs(nodeID int) (nodes []Node, err error) { // {{{ + var rows *sqlx.Rows + rows, err = db.Queryx(` + WITH RECURSIVE nodes AS ( + SELECT + id, + COALESCE(parent_id, 0) AS parent_id, + name + FROM node + WHERE + id = $1 + + UNION + + SELECT + n.id, + COALESCE(n.parent_id, 0) AS parent_id, + n.name + FROM node n + INNER JOIN nodes nr ON n.id = nr.parent_id + ) + SELECT * FROM nodes + `, nodeID) + if err != nil { + return + } + defer rows.Close() + + nodes = []Node{} + for rows.Next() { + node := Node{} + if err = rows.StructScan(&node); err != nil { + return + } + nodes = append(nodes, node) + } + return +} // }}} +/* +func CreateNode(userID, parentID int, name string) (node Node, err error) { // {{{ + var rows *sqlx.Rows + + rows, err = service.Db.Conn.Queryx(` + INSERT INTO node(user_id, parent_id, name) + VALUES($1, NULLIF($2, 0)::integer, $3) + RETURNING + id, + user_id, + COALESCE(parent_id, 0) AS parent_id, + name, + content + `, + userID, + parentID, + name, + ) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + node = Node{} + if err = rows.StructScan(&node); err != nil { + return + } + node.Children = []Node{} + node.Files = []File{} + node.Complete = true + } + + node.Crumbs, err = NodeCrumbs(node.ID) + return +} // }}} +func UpdateNode(userID, nodeID, timeOffset int, content string, cryptoKeyID int, markdown bool) (err error) { // {{{ + if nodeID == 0 { + return + } + + var timezone string + row := service.Db.Conn.QueryRow(`SELECT timezone FROM _webservice.user WHERE id=$1`, userID) + err = row.Scan(&timezone) + if err != nil { + err = werr.Wrap(err).WithCode("002-000F") + return + } + + var scannedSchedules, dbSchedules, add, remove []Schedule + scannedSchedules = ScanForSchedules(timezone, content) + for i := range scannedSchedules { + scannedSchedules[i].Node.ID = nodeID + scannedSchedules[i].UserID = userID + } + + var tsx *sql.Tx + tsx, err = service.Db.Conn.Begin() + if err != nil { + return + } + + dbSchedules, err = RetrieveSchedules(userID, nodeID) + if err != nil { + tsx.Rollback() + return + } + + for _, scanned := range scannedSchedules { + found := false + for _, db := range dbSchedules { + if scanned.IsEqual(db) { + found = true + break + } + } + if !found { + add = append(add, scanned) + } + } + + for _, db := range dbSchedules { + found := false + for _, scanned := range scannedSchedules { + if db.IsEqual(scanned) { + found = true + break + } + } + if !found { + remove = append(remove, db) + } + } + + for _, event := range remove { + err = event.Delete(tsx) + if err != nil { + tsx.Rollback() + return + } + } + + for _, event := range add { + err = event.Insert(tsx) + if err != nil { + tsx.Rollback() + return + } + } + + if cryptoKeyID > 0 { + _, err = tsx.Exec(` + UPDATE node + SET + content = '', + content_encrypted = $1, + markdown = $5, + crypto_key_id = CASE $2::int + WHEN 0 THEN NULL + ELSE $2 + END + WHERE + id = $3 AND + user_id = $4 + `, + content, + cryptoKeyID, + nodeID, + userID, + markdown, + ) + } else { + _, err = tsx.Exec(` + UPDATE node + SET + content = $1, + content_encrypted = '', + markdown = $5, + crypto_key_id = CASE $2::int + WHEN 0 THEN NULL + ELSE $2 + END + WHERE + id = $3 AND + user_id = $4 + `, + content, + cryptoKeyID, + nodeID, + userID, + markdown, + ) + } + if err != nil { + tsx.Rollback() + return + } + + err = tsx.Commit() + + return +} // }}} +func RenameNode(userID, nodeID int, name string) (err error) { // {{{ + _, err = service.Db.Conn.Exec(` + UPDATE node SET name = $1 WHERE user_id = $2 AND id = $3 + `, + name, + userID, + nodeID, + ) + return +} // }}} +func DeleteNode(userID, nodeID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec(` + WITH RECURSIVE nodetree AS ( + SELECT + id, parent_id + FROM node + WHERE + user_id = $1 AND id = $2 + + UNION + + SELECT + n.id, n.parent_id + FROM node n + INNER JOIN nodetree nt ON n.parent_id = nt.id + ) + + DELETE FROM node WHERE id IN ( + SELECT id FROM nodetree + )`, + userID, + nodeID, + ) + return +} // }}} +func SearchNodes(userID int, search string) (nodes []Node, err error) { // {{{ + nodes = []Node{} + var rows *sqlx.Rows + rows, err = service.Db.Conn.Queryx(` + SELECT + id, + user_id, + COALESCE(parent_id, 0) AS parent_id, + name, + updated + FROM node + WHERE + user_id = $1 AND + crypto_key_id IS NULL AND + ( + content ~* $2 OR + name ~* $2 + ) + ORDER BY + updated DESC + `, userID, search) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + node := Node{} + node.Complete = false + if err = rows.StructScan(&node); err != nil { + return + } + nodes = append(nodes, node) + } + + return +} // }}} + +func ChecklistGroupAdd(userID, nodeID int, label string) (item ChecklistGroup, err error) { // {{{ + var row *sqlx.Row + row = service.Db.Conn.QueryRowx( + ` + INSERT INTO checklist_group(node_id, "order", "label") + ( + SELECT + $1, + MAX("order")+1 AS "order", + $2 AS "label" + FROM checklist_group g + INNER JOIN node n ON g.node_id = n.id + WHERE + user_id = $3 AND + node_id = $1 + GROUP BY + node_id + ) UNION ( + SELECT + node.id AS node_id, + 0 AS "order", + $2 AS "label" + FROM node + WHERE + user_id = $3 AND + node.id = $1 + ) + ORDER BY "order" DESC + LIMIT 1 + RETURNING + * + `, + nodeID, + label, + userID, + ) + err = row.StructScan(&item) + return +} // }}} +func ChecklistGroupLabel(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + UPDATE checklist_group g + SET label = $3 + FROM node n + WHERE + g.node_id = n.id AND + n.user_id = $1 AND + g.id = $2; + `, + userID, + checklistGroupID, + label, + ) + return +} // }}} +func ChecklistGroupItemAdd(userID, checklistGroupID int, label string) (item ChecklistItem, err error) { // {{{ + var row *sqlx.Row + row = service.Db.Conn.QueryRowx( + ` + INSERT INTO checklist_item(checklist_group_id, "order", "label") + ( + SELECT + checklist_group_id, + MAX("order")+1 AS "order", + $1 AS "label" + FROM checklist_item + WHERE + checklist_group_id = $2 + GROUP BY + checklist_group_id + ) UNION ( + SELECT $2 AS checklist_group_id, 0 AS "order", $1 AS "label" + ) + ORDER BY "order" DESC + LIMIT 1 + RETURNING + * + `, + label, + checklistGroupID, + ) + err = row.StructScan(&item) + return +} // }}} +func ChecklistGroupDelete(userID, checklistGroupID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + DELETE + FROM checklist_group g + USING + node n + WHERE + g.id = $2 AND + g.node_id = n.id AND + n.user_id = $1 + `, + userID, + checklistGroupID, + ) + return +} // }}} + +func ChecklistItemState(userID, checklistItemID int, state bool) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + UPDATE checklist_item i + SET checked = $3 + FROM checklist_group g, node n + WHERE + i.checklist_group_id = g.id AND + g.node_id = n.id AND + n.user_id = $1 AND + i.id = $2; + `, + userID, + checklistItemID, + state, + ) + return +} // }}} +func ChecklistItemLabel(userID, checklistItemID int, label string) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + UPDATE checklist_item i + SET label = $3 + FROM checklist_group g, node n + WHERE + i.checklist_group_id = g.id AND + g.node_id = n.id AND + n.user_id = $1 AND + i.id = $2; + `, + userID, + checklistItemID, + label, + ) + return +} // }}} +func ChecklistItemDelete(userID, checklistItemID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + DELETE + FROM checklist_item i + USING + checklist_group g, + node n + WHERE + i.id = $2 AND + i.checklist_group_id = g.id AND + g.node_id = n.id AND + n.user_id = $1 + `, + userID, + checklistItemID, + ) + return +} // }}} +func ChecklistItemMove(userID, checklistItemID, afterItemID int) (err error) { // {{{ + _, err = service.Db.Conn.Exec( + ` + WITH + "to" AS ( + SELECT + i.checklist_group_id AS group_id, + i."order" + FROM checklist_item i + INNER JOIN checklist_group g ON i.checklist_group_id = g.id + INNER JOIN node n ON g.node_id = n.id + WHERE + n.user_id = $1 AND + i.id = $3 + ), + + update_order AS ( + UPDATE checklist_item + SET + "order" = + CASE + WHEN checklist_item."order" <= "to"."order" THEN checklist_item."order" - 1 + WHEN checklist_item."order" > "to"."order" THEN checklist_item."order" + 1 + END + FROM "to" + WHERE + checklist_item.id != $2 AND + checklist_item.checklist_group_id = "to".group_id + ) + + UPDATE checklist_item + SET + checklist_group_id = "to".group_id, + "order" = "to"."order" + FROM "to" + WHERE + checklist_item.id = $2 + `, + userID, + checklistItemID, + afterItemID, + ) + return +} // }}} + +*/ diff --git a/request_response.go b/request_response.go new file mode 100644 index 0000000..5eaea5b --- /dev/null +++ b/request_response.go @@ -0,0 +1,41 @@ +package main + +import ( + // External + werr "git.gibonuddevalla.se/go/wrappederror" + + // Standard + "encoding/json" + "io" + "net/http" +) + +type Request struct { + ID string +} + +func responseError(w http.ResponseWriter, err error) { + res := map[string]interface{}{ + "OK": false, + "Error": err.Error(), + } + resJSON, _ := json.Marshal(res) + + + werr.Wrap(err).Log() + w.Header().Add("Content-Type", "application/json") + w.Write(resJSON) +} + +func responseData(w http.ResponseWriter, data interface{}) { + resJSON, _ := json.Marshal(data) + w.Header().Add("Content-Type", "application/json") + w.Write(resJSON) +} + +func parseRequest(r *http.Request, data interface{}) (err error) { + body, _ := io.ReadAll(r.Body) + defer r.Body.Close() + err = json.Unmarshal(body, data) + return +} diff --git a/static/js/app.mjs b/static/js/app.mjs index a93cf11..da73bcb 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -23,7 +23,7 @@ export class Notes2 { this.startNode = new Node(this, nodeID ? parseInt(nodeID) : 0) }//}}} - treeGet() { + treeGet() {//{{{ const req = {} API.query('POST', '/node/tree', req) .then(response => { @@ -31,7 +31,7 @@ export class Notes2 { nodeStore.add(response.Nodes) }) .catch(e => console.log(e.type, e.error)) - } + }//}}} } class Tree extends Component { diff --git a/static/js/node_store.mjs b/static/js/node_store.mjs index 0a24dbe..1054c1f 100644 --- a/static/js/node_store.mjs +++ b/static/js/node_store.mjs @@ -1,3 +1,5 @@ +import { API } from 'api' + export class NodeStore { constructor() { if (!('indexedDB' in window)) { @@ -6,7 +8,6 @@ export class NodeStore { this.db = null } - async initializeDB() { return new Promise((resolve, reject) => { let req = indexedDB.open('notes', 2) @@ -60,7 +61,7 @@ export class NodeStore { } records.forEach(record => { - let addReq = nodeStore.add(record) + let addReq = nodeStore.put(record) addReq.onsuccess = (event) => { console.log('OK!', record.ID, record.Name) } @@ -74,14 +75,42 @@ export class NodeStore { } }) } + async get(id) { + return new Promise((resolve, reject) => { + // Node is always returned from IndexedDB if existing there. + // Otherwise an attempt to get it from backend is executed. + const trx = this.db.transaction('nodes', 'readonly') + const nodeStore = trx.objectStore('nodes') + const getRequest = nodeStore.get(id) + getRequest.onsuccess = (event) => { + // Node found in IndexedDB and returned. + if (event.target.result !== undefined) { + resolve(event.target.result) + return + } + // Node not found and a request to the backend is made. + API.query("POST", `/node/retrieve/${id}`, {}) + .then(res => { + const trx = this.db.transaction('nodes', 'readwrite') + const nodeStore = trx.objectStore('nodes') + const putRequest = nodeStore.put(res.Node) + putRequest.onsuccess = () => resolve(res.Node) + putRequest.onerror = (event) => { + reject(event.target.error) + } + }) + .catch(e => reject(e)) + } + }) + } async getTreeNodes() { - return new Promise((resolve, reject)=>{ + return new Promise((resolve, reject) => { let trx = this.db.transaction('nodes', 'readonly') let nodeStore = trx.objectStore('nodes') let req = nodeStore.getAll() - req.onsuccess = (event)=>resolve(event.target.result) - req.onerror = (event)=>reject(event.target.error) + req.onsuccess = (event) => resolve(event.target.result) + req.onerror = (event) => reject(event.target.error) }) } } diff --git a/static/service_worker.js b/static/service_worker.js index 420db04..35a8efc 100644 --- a/static/service_worker.js +++ b/static/service_worker.js @@ -1,7 +1,28 @@ const CACHE_NAME = 'notes2-{{ .VERSION }}' const CACHED_ASSETS = [ '/', + '/notes2', + + '/css/{{ .VERSION }}/main.css', + '/css/{{ .VERSION }}/notes2.css', + + '/js/{{ .VERSION }}/lib/preact/preact.mjs', + '/js/{{ .VERSION }}/lib/htm/htm.mjs', + '/js/{{ .VERSION }}/lib/preact/devtools.mjs', + '/js/{{ .VERSION }}/lib/signals/signals.mjs', + '/js/{{ .VERSION }}/lib/signals/signals-core.mjs', + '/js/{{ .VERSION }}/lib/preact/hooks.mjs', + + '/js/{{ .VERSION }}/api.mjs', + '/js/{{ .VERSION }}/node_store.mjs', '/js/{{ .VERSION }}/app.mjs', + '/js/{{ .VERSION }}/key.mjs', + '/js/{{ .VERSION }}/crypto.mjs', + '/js/{{ .VERSION }}/checklist.mjs', + + '/images/{{ .VERSION }}/leaf.svg', + '/images/{{ .VERSION }}/collapsed.svg', + '/images/{{ .VERSION }}/expanded.svg', ] async function precache() { @@ -40,6 +61,6 @@ self.addEventListener('activate', event => { }) self.addEventListener('fetch', event => { - //console.log('SERVICE WORKER: fetch') + console.log('SERVICE WORKER: fetch') event.respondWith(fetchAsset(event)) })