History fetching from server

This commit is contained in:
Magnus Åhall 2026-06-06 20:21:11 +02:00
parent 9506b89453
commit aeca9d8559
4 changed files with 149 additions and 65 deletions

20
main.go
View file

@ -25,7 +25,7 @@ import (
const VERSION = "v11" const VERSION = "v11"
const CONTEXT_USER = 1 const CONTEXT_USER = 1
const SYNC_PAGINATION = 20 const SYNC_PAGINATION = 200
var ( var (
FlagGenerate bool FlagGenerate bool
@ -141,6 +141,7 @@ func main() { // {{{
http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve)) http.HandleFunc("/node/retrieve/{uuid}", authenticated(actionNodeRetrieve))
http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve)) http.HandleFunc("/node/history/retrieve/{uuid}/{offset}", authenticated(actionNodeHistoryRetrieve))
http.HandleFunc("/node/history/count/{uuid}", authenticated(actionNodeHistoryCount))
http.HandleFunc("/service_worker.js", pageServiceWorker) http.HandleFunc("/service_worker.js", pageServiceWorker)
@ -352,6 +353,23 @@ func actionNodeHistoryRetrieve(w http.ResponseWriter, r *http.Request) { // {{{
"HasMore": hasMore, "HasMore": hasMore,
}) })
} // }}} } // }}}
func actionNodeHistoryCount(w http.ResponseWriter, r *http.Request) { // {{{
user := getUser(r)
var err error
uuid := r.PathValue("uuid")
count, err := RetrieveNodeHistoryCount(user.UserID, uuid)
if err != nil {
responseError(w, err)
return
}
responseData(w, map[string]any{
"OK": true,
"Count": count,
})
} // }}}
func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
user := getUser(r) user := getUser(r)

20
node.go
View file

@ -293,6 +293,26 @@ func RetrieveNodeHistory(userID int, nodeUUID string, offset int) (nodes []Node,
} }
return return
} // }}} } // }}}
func RetrieveNodeHistoryCount(userID int, nodeUUID string) (count int, err error) { // {{{
var row *sql.Row
row = db.QueryRow(`
SELECT
COUNT(*)
FROM node_history
WHERE
user_id = $1 AND
uuid = $2
`,
userID,
nodeUUID,
)
if err = row.Scan(&count); err != nil {
err = werr.Wrap(err)
return
}
return
} // }}}
func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{ func NodeCrumbs(nodeUUID string) (nodes []Node, err error) { // {{{
var rows *sqlx.Rows var rows *sqlx.Rows
rows, err = db.Queryx(` rows, err = db.Queryx(`

View file

@ -20,6 +20,10 @@ html {
filter: var(--colorize); filter: var(--colorize);
} }
button {
font-size: 1e m;
}
/* ------------------------------------- * /* ------------------------------------- *
* Default application grid in wide mode * * Default application grid in wide mode *
* ------------------------------------- */ * ------------------------------------- */
@ -550,6 +554,18 @@ n2-pagehistory {
rgba(0, 0, 0, 0.2) 0px -3px 0px inset; rgba(0, 0, 0, 0.2) 0px -3px 0px inset;
} }
.el-stats {
margin-bottom: 16px;
display: grid;
grid-template-columns: min-content 1fr;
grid-gap: 8px 12px;
white-space: nowrap;
}
.el-fetch-history-progress {
margin-top: 16px;
}
.el-back-image, .el-back-image,
.el-back-text { .el-back-text {
cursor: pointer; cursor: pointer;

View file

@ -3,8 +3,7 @@ import { Node } from './page_node.mjs'
export class N2PageHistory extends CustomHTMLElement { export class N2PageHistory extends CustomHTMLElement {
static PAGESIZE = 15 static PAGESIZE = 15
static {// {{{
static {
this.tmpl = document.createElement('template') this.tmpl = document.createElement('template')
this.tmpl.innerHTML = ` this.tmpl.innerHTML = `
<style> <style>
@ -26,11 +25,20 @@ export class N2PageHistory extends CustomHTMLElement {
<div class="group-label">Actions</div> <div class="group-label">Actions</div>
<div class="group"> <div class="group">
<button data-el="download-history">Fetch all history from server</button> <button data-el="download-history">Fetch all history from server</button>
<div data-el="fetch-history-progress"></div>
</div> </div>
<div class="group-label">History</div> <div class="group-label">History</div>
<div class="group"> <div class="group">
<div data-el="stats">
<div>History on server:</div>
<div data-el="stats-on-server"></div>
<div>History on client:</div>
<div data-el="stats-on-client"></div>
</div>
<div data-el="nodes"></div> <div data-el="nodes"></div>
<div data-el="pagination"> <div data-el="pagination">
@ -40,9 +48,9 @@ export class N2PageHistory extends CustomHTMLElement {
</div> </div>
</div> </div>
` `
} }// }}}
constructor() { constructor() {// {{{
super() super()
this.setAttribute('tabindex', '-1') this.setAttribute('tabindex', '-1')
@ -53,66 +61,23 @@ export class N2PageHistory extends CustomHTMLElement {
this.elBackText.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' })) this.elBackText.addEventListener('click', () => _mbus.dispatch('SHOW_PAGE', { page: 'node' }))
this.elPrev.addEventListener('click', () => this.prevPage()) this.elPrev.addEventListener('click', () => this.prevPage())
this.elNext.addEventListener('click', () => this.nextPage()) this.elNext.addEventListener('click', () => this.nextPage())
this.elDownloadHistory.addEventListener('click', () => this.downloadHistory()) this.elDownloadHistory.addEventListener('click', async () => {
await this.downloadHistory()
await this.useNode(this.node)
this.render(true)
})
_mbus.subscribe('NODE_UI_OPEN', async (event) => { _mbus.subscribe('NODE_UI_OPEN', async (event) => {
await this.useNode(event.detail.data) await this.useNode(event.detail.data)
this.render() this.render()
}) })
} }// }}}
async render(keepFetchHistoryProgress) {// {{{
async useNode(node) {
this.node = node
this.page = 1
this.nodesTotal = await nodeStore.nodesHistory.count(this.node.UUID)
this.pages = Math.ceil(this.nodesTotal / N2PageHistory.PAGESIZE)
}
keyHandler(event) {
switch (event.key) {
case 'ArrowLeft':
this.prevPage()
break
case 'ArrowRight':
this.nextPage()
break
}
}
prevPage() {
if (this.page == 1)
return
this.page--
this.render()
}
nextPage() {
if (this.page >= this.pages)
return
this.page++
this.render()
}
formatSize(s) {
let div = 1
let unit = 'B'
if (s >= 1048576) {
div = 1048576
unit = 'MB'
} else if (s >= 1024) {
div = 1024
unit = 'kB'
}
return new Intl.NumberFormat(undefined, {
maximumFractionDigits: 0
}).format(Math.round(s / div)) + ' ' + unit
}
async render() {
this.elNodeName.innerText = this.node.get('Name') this.elNodeName.innerText = this.node.get('Name')
this.elPage.innerText = `${this.page} / ${this.pages}` this.elPage.innerText = `${this.page} / ${this.pages}`
this.elStatsOnClient.innerText = `${this.nodesTotal}`
this.elStatsOnServer.innerText = `${this.historyOnServerTotal}`
if (this.nodesTotal <= N2PageHistory.PAGESIZE) if (this.nodesTotal <= N2PageHistory.PAGESIZE)
this.elPagination.style.display = 'none' this.elPagination.style.display = 'none'
@ -138,9 +103,59 @@ export class N2PageHistory extends CustomHTMLElement {
return div return div
}) })
this.elNodes.replaceChildren(...divs) this.elNodes.replaceChildren(...divs)
if (!keepFetchHistoryProgress)
this.elFetchHistoryProgress.innerText = ''
}// }}}
async useNode(node) {// {{{
this.node = node
this.page = 1
this.nodesTotal = await nodeStore.nodesHistory.count(this.node.UUID)
this.historyOnServerTotal = await this.getServerTotal()
this.pages = Math.ceil(this.nodesTotal / N2PageHistory.PAGESIZE)
}// }}}
keyHandler(event) {// {{{
switch (event.key) {
case 'ArrowLeft':
this.prevPage()
break
case 'ArrowRight':
this.nextPage()
break
}
}// }}}
prevPage() {// {{{
if (this.page == 1)
return
this.page--
this.render()
}// }}}
nextPage() {// {{{
if (this.page >= this.pages)
return
this.page++
this.render()
}// }}}
async getServerTotal() {// {{{
const res = await fetch(`/node/history/count/${this.node.UUID}`, {
headers: {
"Authorization": 'Bearer ' + localStorage.getItem('token'),
}
})
const json = await res.json()
if (!json.OK) {
alert(json.Error)
return
} }
async downloadHistory() { return json.Count
}// }}}
async downloadHistory() {// {{{
try { try {
const nodes = [] const nodes = []
let offset = 0 let offset = 0
@ -153,6 +168,7 @@ export class N2PageHistory extends CustomHTMLElement {
nodes.push(new Node(nodeData)) nodes.push(new Node(nodeData))
} }
offset = nodes.length offset = nodes.length
this.elFetchHistoryProgress.innerText = `${nodes.length} fetched.`
} }
let num = 0 let num = 0
@ -161,15 +177,14 @@ export class N2PageHistory extends CustomHTMLElement {
if (ok) num++ if (ok) num++
await nodeStore.nodesHistory.add(node) await nodeStore.nodesHistory.add(node)
} }
console.log(num)
this.elFetchHistoryProgress.innerText = `${nodes.length} fetched - all history fetched.`
} catch (e) { } catch (e) {
console.error(e) console.error(e)
alert(e) alert(e)
} }
} }// }}}
async downloadHistoryPage(offset) {// {{{
async downloadHistoryPage(offset) {
const res = await fetch(`/node/history/retrieve/${this.node.UUID}/${offset}`, { const res = await fetch(`/node/history/retrieve/${this.node.UUID}/${offset}`, {
headers: { headers: {
"Authorization": 'Bearer ' + localStorage.getItem('token'), "Authorization": 'Bearer ' + localStorage.getItem('token'),
@ -183,6 +198,21 @@ export class N2PageHistory extends CustomHTMLElement {
} }
return json return json
}// }}}
formatSize(s) {// {{{
let div = 1
let unit = 'B'
if (s >= 1048576) {
div = 1048576
unit = 'MB'
} else if (s >= 1024) {
div = 1024
unit = 'kB'
} }
return new Intl.NumberFormat(undefined, {
maximumFractionDigits: 0
}).format(Math.round(s / div)) + ' ' + unit
}// }}}
} }
customElements.define('n2-pagehistory', N2PageHistory) customElements.define('n2-pagehistory', N2PageHistory)