Better visual sync

This commit is contained in:
Magnus Åhall 2025-11-29 16:45:56 +01:00
parent 40a68d6ad0
commit d9c82868ab
6 changed files with 130 additions and 106 deletions

View file

@ -25,7 +25,7 @@ import (
const VERSION = "v1" const VERSION = "v1"
const CONTEXT_USER = 1 const CONTEXT_USER = 1
const SYNC_PAGINATION = 100 const SYNC_PAGINATION = 500
var ( var (
FlagGenerate bool FlagGenerate bool
@ -269,9 +269,11 @@ func actionSyncFromServer(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
/*
Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq) Log.Debug("/sync/from_server", "num_nodes", len(nodes), "maxSeq", maxSeq)
foo, _ := json.Marshal(nodes) foo, _ := json.Marshal(nodes)
os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644) os.WriteFile(fmt.Sprintf("/tmp/nodes-%d.json", offset), foo, 0644)
*/
j, _ := json.Marshal(struct { j, _ := json.Marshal(struct {
OK bool OK bool
@ -288,7 +290,6 @@ func actionSyncFromServerCount(w http.ResponseWriter, r *http.Request) { // {{{
user := getUser(r) user := getUser(r)
changedFrom, _ := strconv.Atoi(r.PathValue("sequence")) changedFrom, _ := strconv.Atoi(r.PathValue("sequence"))
Log.Debug("FOO", "UUID", user.ClientUUID, "changedFrom", changedFrom)
count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID) count, err := NodesCount(user.UserID, uint64(changedFrom), user.ClientUUID)
if err != nil { if err != nil {
Log.Error("/sync/from_server/count", "error", err) Log.Error("/sync/from_server/count", "error", err)
@ -341,7 +342,6 @@ func actionSyncToServer(w http.ResponseWriter, r *http.Request) { // {{{
return return
} }
responseData(w, map[string]any{ responseData(w, map[string]any{
"OK": true, "OK": true,
}) })

View file

@ -19,14 +19,10 @@ html {
"tree files" "tree files"
*/ */
"tree blank" "tree blank"
; ;
grid-template-columns: min-content 1fr; grid-template-columns: min-content 1fr;
grid-template-rows: grid-template-rows:
48px 48px 56px 48px min-content 1fr;
56px
48px
min-content
1fr;
@media only screen and (max-width: 600px) { @media only screen and (max-width: 600px) {
@ -41,7 +37,7 @@ html {
"files" "files"
*/ */
"blank" "blank"
; ;
grid-template-columns: 1fr; grid-template-columns: 1fr;
#tree { #tree {
@ -55,7 +51,8 @@ html {
display: grid; display: grid;
padding: 16px 0px 16px 16px; padding: 16px 0px 16px 16px;
color: #ddd; color: #ddd;
z-index: 100; /* Over crumbs shadow */ z-index: 100;
/* Over crumbs shadow */
border-left: 2px solid #333; border-left: 2px solid #333;
&:focus { &:focus {
@ -90,13 +87,13 @@ html {
display: grid; display: grid;
grid-template-columns: 24px min-content; grid-template-columns: 24px min-content;
grid-template-rows: grid-template-rows:
min-content min-content 1fr;
1fr;
margin-top: 12px; margin-top: 12px;
.expand-toggle { .expand-toggle {
user-select: none; user-select: none;
img { img {
width: 16px; width: 16px;
height: 16px; height: 16px;
@ -111,6 +108,7 @@ html {
&:hover { &:hover {
color: var(--color1); color: var(--color1);
} }
&.selected { &.selected {
color: var(--color1); color: var(--color1);
font-weight: bold; font-weight: bold;
@ -135,7 +133,7 @@ html {
padding: 16px 32px; padding: 16px 32px;
background-color: #333; background-color: #333;
border-radius: 8px; border-radius: 8px;
box-shadow: 5px 5px 10px -5px rgba(0,0,0,0.75); box-shadow: 5px 5px 10px -5px rgba(0, 0, 0, 0.75);
} }
#crumbs { #crumbs {
@ -158,6 +156,7 @@ html {
&.node-modified { &.node-modified {
background-color: var(--color1); background-color: var(--color1);
color: var(--color2); color: var(--color2);
.crumb:after { .crumb:after {
color: var(--color2); color: var(--color2);
} }
@ -184,6 +183,7 @@ html {
.crumb:last-child { .crumb:last-child {
margin-right: 0; margin-right: 0;
} }
.crumb:last-child:after { .crumb:last-child:after {
content: ''; content: '';
margin-left: 0px; margin-left: 0px;
@ -194,55 +194,64 @@ html {
} }
#sync-progress { #sync-progress {
--radius: 8px;
grid-area: sync; grid-area: sync;
display: grid; display: grid;
justify-items: center; justify-items: center;
align-items: center;
width: 100%; width: 100%;
height: 56px; height: 56px;
position: relative;
progress { .container {
width: 100%; position: relative;
padding: 0 7px;
max-width: 900px; progress {
height: 16px; width: 900px;
border-radius: 4px; padding: 0 7px;
max-width: 900px;
height: 24px;
border-radius: 8px;
}
.count {
position: absolute;
top: 5px;
width: 100%;
white-space: nowrap;
color: #888;
text-align: center;
font-size: 12pt;
font-weight: bold;
}
progress[value]::-webkit-progress-bar {
background-color: #eee;
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
border-radius: var(--radius);
}
progress[value]::-moz-progress-bar {
background-color: #eee;
box-shadow: 0 2px var(--radius) rgba(0, 0, 0, 0.25) inset;
border-radius: var(--radius);
}
progress[value]::-webkit-progress-value {
background: rgb(186, 95, 89);
background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
border-radius: var(--radius);
}
progress[value]::-moz-progress-value {
background: rgb(186, 95, 89);
background: linear-gradient(180deg, rgba(186, 95, 89, 1) 0%, rgba(254, 95, 85, 1) 50%, rgba(186, 95, 89, 1) 100%);
border-radius: var(--radius);
}
} }
progress[value]::-webkit-progress-bar {
background-color: #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
border-radius: 4px;
}
progress[value]::-moz-progress-bar {
background-color: #eee;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25) inset;
border-radius: 4px;
}
progress[value]::-webkit-progress-value {
background: rgb(186,95,89);
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
border-radius: 4px;
}
/* TODO: style the progress value for Firefox */
progress[value]::-moz-progress-value {
background: rgb(186,95,89);
background: linear-gradient(180deg, rgba(186,95,89,1) 0%, rgba(254,95,85,1) 50%, rgba(186,95,89,1) 100%);
border-radius: 4px;
}
.count {
width: min-content;
white-space: nowrap;
margin-top: 0px;
color: #888;
position: absolute;
top: 22px;
}
&.hidden { &.hidden {
visibility: hidden; visibility: hidden;
@ -270,6 +279,7 @@ html {
grid-area: content; grid-area: content;
font-size: 1.0em; font-size: 1.0em;
} }
.grow-wrap::after { .grow-wrap::after {
/* Note the weird space! Needed to preventy jumpy behavior */ /* Note the weird space! Needed to preventy jumpy behavior */
content: attr(data-replicated-value) " "; content: attr(data-replicated-value) " ";
@ -285,14 +295,16 @@ html {
/* Hidden from view, clicks, and screen readers */ /* Hidden from view, clicks, and screen readers */
visibility: hidden; visibility: hidden;
} }
.grow-wrap > textarea {
.grow-wrap>textarea {
/* You could leave this, but after a user resizes, then it ruins the auto sizing */ /* You could leave this, but after a user resizes, then it ruins the auto sizing */
resize: none; resize: none;
/* Firefox shows scrollbar on growth, you can hide like this. */ /* Firefox shows scrollbar on growth, you can hide like this. */
overflow: hidden; overflow: hidden;
} }
.grow-wrap > textarea,
.grow-wrap>textarea,
.grow-wrap::after { .grow-wrap::after {
/* Identical styling required!! */ /* Identical styling required!! */
padding: 0.5rem; padding: 0.5rem;
@ -301,6 +313,7 @@ html {
/* Place on top of each other */ /* Place on top of each other */
grid-area: 1 / 1 / 2 / 2; grid-area: 1 / 1 / 2 / 2;
} }
/* ============================================================= */ /* ============================================================= */
#node-content { #node-content {

View file

@ -1,6 +1,7 @@
import { ROOT_NODE } from 'node_store' import { ROOT_NODE } from 'node_store'
import { TreeNative } from 'tree' import { TreeNative } from 'tree'
import { NodeUINative, Node } from 'node' import { NodeUINative, Node } from 'node'
import { SyncProgress } from 'sync'
export class App { export class App {
constructor() {// {{{ constructor() {// {{{
@ -34,6 +35,9 @@ export class App {
document.getElementById('node-content')?.focus() document.getElementById('node-content')?.focus()
}) })
const syncProgress = document.getElementById('sync-progress')
new SyncProgress(syncProgress)
window._sync = new Sync() window._sync = new Sync()
window._sync.run() window._sync.run()
}// }}} }// }}}

View file

@ -54,7 +54,7 @@ export class NodeUI extends Component {
${crumbDivs} ${crumbDivs}
</div> </div>
</div> </div>
<${SyncProgress} ref=${this.syncProgress} /> <div id="sync-progress"></div>
<div id="name">${node.get('Name')}</div> <div id="name">${node.get('Name')}</div>
<${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} /> <${NodeContent} key=${node.UUID} node=${node} ref=${this.nodeContent} />
<div id="blank"></div> <div id="blank"></div>
@ -156,8 +156,12 @@ export class NodeUI extends Component {
` `
}//}}} }//}}}
async componentDidMount() {//{{{ async componentDidMount() {//{{{
console.log('hum')
_notes2.current.goToNode(this.props.startNode.UUID, true) _notes2.current.goToNode(this.props.startNode.UUID, true)
_notes2.current.tree.expandToTrunk(this.props.startNode) _notes2.current.tree.expandToTrunk(this.props.startNode)
const syncProgressEl = document.getElementById('#sync-progress')
console.log(syncProgressEl)
}//}}} }//}}}
setNode(node) {//{{{ setNode(node) {//{{{
this.nodeModified.value = false this.nodeModified.value = false

View file

@ -1,31 +1,11 @@
import { API } from 'api' import { API } from 'api'
import { Node } from 'node' import { Node } from 'node'
import { h, Component } from 'preact'
import htm from 'htm'
const html = htm.bind(h)
const SYNC_COUNT = 1
const SYNC_HANDLED = 2
const SYNC_DONE = 3
export class Sync { export class Sync {
constructor() {//{{{ constructor() {//{{{
this.listeners = [] this.listeners = []
this.messagesReceived = [] this.messagesReceived = []
}//}}} }//}}}
addListener(fn, runMessageQueue) {//{{{
// Some handlers won't be added until a time after sync messages have been added to the queue.
// This is an opportunity for the handler to receive the old messages in order.
if (runMessageQueue)
for (const msg of this.messagesReceived)
fn(msg)
this.listeners.push(fn)
}//}}}
pushMessage(msg) {//{{{
this.messagesReceived.push(msg)
for (const fn of this.listeners)
fn(msg)
}//}}}
async run() {//{{{ async run() {//{{{
try { try {
@ -38,8 +18,8 @@ export class Sync {
let nodeCount = await this.getNodeCount(oldMax) let nodeCount = await this.getNodeCount(oldMax)
nodeCount += await nodeStore.sendQueue.count() nodeCount += await nodeStore.sendQueue.count()
const msg = { op: SYNC_COUNT, count: nodeCount }
this.pushMessage(msg) _mbus.dispatch('SYNC_COUNT', { count: nodeCount })
await this.nodesFromServer(oldMax) await this.nodesFromServer(oldMax)
.then(durationNodes => { .then(durationNodes => {
@ -49,7 +29,7 @@ export class Sync {
await this.nodesToServer() await this.nodesToServer()
} finally { } finally {
this.pushMessage({ op: SYNC_DONE }) _mbus.dispatch('SYNC_DONE')
} }
}//}}} }//}}}
async getNodeCount(oldMax) {//{{{ async getNodeCount(oldMax) {//{{{
@ -60,6 +40,7 @@ export class Sync {
async nodesFromServer(oldMax) {//{{{ async nodesFromServer(oldMax) {//{{{
const syncStart = Date.now() const syncStart = Date.now()
let syncEnd let syncEnd
let handled = 0
try { try {
let currMax = oldMax let currMax = oldMax
let offset = 0 let offset = 0
@ -86,9 +67,14 @@ export class Sync {
for (const i in res.Nodes) { for (const i in res.Nodes) {
backendNode = new Node(res.Nodes[i], -1) backendNode = new Node(res.Nodes[i], -1)
await window._sync.handleNode(backendNode) await window._sync.handleNode(backendNode)
handled++
if (handled % 100 === 0)
_mbus.dispatch('SYNC_HANDLED', { handled })
} }
} while (res.Continue) } while (res.Continue)
_mbus.dispatch('SYNC_HANDLED', { handled })
nodeStore.setAppState('latest_sync_node', currMax) nodeStore.setAppState('latest_sync_node', currMax)
} catch (e) { } catch (e) {
@ -130,7 +116,7 @@ export class Sync {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
this.pushMessage({ op: SYNC_HANDLED, count: 1 }) //_mbus.dispatch('SYNC_HANDLED', { count: 1 })
} }
}//}}} }//}}}
async nodesToServer() {//{{{ async nodesToServer() {//{{{
@ -157,7 +143,7 @@ export class Sync {
// Nodes are archived on server and can now be deleted from the send queue. // Nodes are archived on server and can now be deleted from the send queue.
const keys = nodesToSend.map(node => node.ClientSequence) const keys = nodesToSend.map(node => node.ClientSequence)
await nodeStore.sendQueue.delete(keys) await nodeStore.sendQueue.delete(keys)
this.pushMessage({ op: SYNC_HANDLED, count: nodesToSend.length }) _mbus.dispatch('SYNC_UPLOADED', { count: nodesToSend.length })
} catch (e) { } catch (e) {
console.trace(e) console.trace(e)
@ -168,11 +154,28 @@ export class Sync {
}//}}} }//}}}
} }
export class SyncProgress extends Component { export class SyncProgress {
constructor() {//{{{ constructor(parentEl) {//{{{
super()
this.reset() this.reset()
_mbus.subscribe('SYNC_COUNT', event => this.progressHandler(event))
_mbus.subscribe('SYNC_HANDLED', event => this.progressHandler(event))
_mbus.subscribe('SYNC_DONE', event => this.progressHandler(event))
this.el = this.createElements()
parentEl.replaceChildren(this.el)
}//}}} }//}}}
createElements() {
const div = document.createElement('div')
div.classList.add('container')
div.innerHTML = `
<progress min=0 max=137 value=0></progress>
<div class="count">0 / 0</div>
`
this.elProgress = div.querySelector('progress')
this.elCount = div.querySelector('.count')
return div
}
reset() {//{{{ reset() {//{{{
this.forceUpdateRequest = null this.forceUpdateRequest = null
this.state = { this.state = {
@ -182,9 +185,6 @@ export class SyncProgress extends Component {
} }
document.getElementById('sync-progress')?.classList.remove('hidden') document.getElementById('sync-progress')?.classList.remove('hidden')
}//}}} }//}}}
componentDidMount() {//{{{
window._sync.addListener(msg => this.progressHandler(msg), true)
}//}}}
getSnapshotBeforeUpdate(_, prevState) {//{{{ getSnapshotBeforeUpdate(_, prevState) {//{{{
if (!prevState.syncedDone && this.state.syncedDone) if (!prevState.syncedDone && this.state.syncedDone)
setTimeout(() => document.getElementById('sync-progress')?.classList.add('hidden'), 750) setTimeout(() => document.getElementById('sync-progress')?.classList.add('hidden'), 750)
@ -202,19 +202,22 @@ export class SyncProgress extends Component {
) )
} }
}//}}} }//}}}
progressHandler(msg) {//{{{ progressHandler(event) {//{{{
switch (msg.op) { const eventData = event.detail.data
case SYNC_COUNT: switch (event.type) {
this.setState({ nodesToSync: msg.count }) case 'SYNC_COUNT':
console.log(eventData.count)
this.state.nodesToSync = eventData.count
break break
case SYNC_HANDLED: case 'SYNC_HANDLED':
this.state.nodesSynced += msg.count console.log('sync handled')
this.state.nodesSynced = eventData.handled
break break
case SYNC_DONE: case 'SYNC_DONE':
// Hides the progress bar. // Hides the progress bar.
this.setState({ syncedDone: true }) this.state.syncedDone = true
// Don't update anything if nothing was synced. // Don't update anything if nothing was synced.
if (this.state.nodesSynced === 0) if (this.state.nodesSynced === 0)
@ -227,17 +230,19 @@ export class SyncProgress extends Component {
} }
break break
} }
this.render()
}//}}} }//}}}
render(_, { nodesToSync, nodesSynced }) {//{{{ render() {//{{{
console.log('render', this.state.nodesToSync)
this.elProgress.max = this.state.nodesToSync
this.elProgress.value = this.state.nodesSynced
this.elCount.innerText = `${this.state.nodesSynced} / ${this.state.nodesToSync}`
/*
if (nodesToSync === 0) if (nodesToSync === 0)
return html`<div id="sync-progress"></div>` return html`<div id="sync-progress"></div>`
*/
return html`
<div id="sync-progress">
<progress min=0 max=${nodesToSync} value=${nodesSynced}></progress>
<div class="count">${nodesSynced} / ${nodesToSync}</div>
</div>
`
}//}}} }//}}}
} }

View file

@ -3,10 +3,8 @@
<div id="tree" tabindex=0></div> <div id="tree" tabindex=0></div>
<div id="crumbs"></div> <div id="crumbs"></div>
<div id="sync-progress"> <div id="sync-progress">
<!--
<progress min=0 max=1 value=0></progress> <progress min=0 max=1 value=0></progress>
<div class="count">0 / 1</div> <div class="count">0 / 1</div>
-->
</div> </div>
<div id="note"></div> <div id="note"></div>