4.6 KiB
Reading position: the last place read wins
Since 2026-10-12 the place to continue a book is where it was last read, on any device, not the
furthest place ever reached. The furthest place is still kept, as a hint ("Weiteste Stelle").
This file is the contract between the server (books/views.py save_progress), the web reader
(static/js/app/46-readpos.js, with the reader in 62-reader.js), diora-android (Ops.kt / SyncEngine.kt / native-*.js) and diora-desktop
(src/main/ops.js / sync.js). They must apply the same rules.
The state per book
| field | meaning |
|---|---|
scroll_fraction, position_anchor |
the continue point (anchor "block:inner", empty for PDFs) |
read_at |
when the reader was at that place, ms since epoch, server clock |
device |
who read there ("Browser", "diora Desktop", "Handy", "tolino" …), for "auf X weitergelesen" |
furthest_fraction, furthest_anchor |
the furthest place ever reported (only ever grows) |
last_read in /books/ is read_at as ISO time (it used to be the time of the last save attempt).
Writing: POST /books/<id>/progress/
{"scroll_fraction": 0.42, "position_anchor": "187:0.62", "read_at": 1760270000000,
"sent_at": 1760270004000, "device": "diora Desktop"}
read_atis the moment the reader arrived at this position (the last time the visible position changed because of the reader), in the device's clock, not the moment of saving. Saving an unchanged position again — every 10 s, on hide, on close, on unload — therefore carries the old time and can never beat a newer position from another device. A device that only shows a position it got from the server (just opened, nothing moved) reports the server'sread_atback unchanged.sent_atis the device clock when the request leaves (an outbox sets it at flush time, not when queuing). The server corrects the device's clock error with it:t = read_at + (server_now − sent_at), thent = min(t, server_now). So a tolino whose clock is hours off still orders correctly, and nothing can win "from the future".- The server stores the position if
t >= stored read_at(or nothing is stored, orforce). Older writes (an offline device catching up late) are not stored as the continue point. - Every write — stored or not — raises
furthest_*if it is further (same comparison as before: anchor block, then inner fraction;scroll_fractionwhen an anchor is missing). force: true(the explicit "Diese Stelle als Fortschritt setzen") stores regardless, witht = server_now.- Old clients (no
read_at): stored only if strictly further than the stored position (the old rule, but>so an idle old client re-saving its place changes nothing), witht = server_now. This keeps not-yet-updated apps from pulling the continue point back. - The response is the state after the write:
{"ok": true, "stored": bool, "scroll_fraction", "position_anchor", "read_at", "device", "furthest_fraction", "furthest_anchor", "server_now"}.
Reading
/books/ and /api/sync/ (book_progress) carry all fields of the state, read_at in ms (server clock),
plus server_now (ms) so a client can estimate its clock offset server_now − device_now.
Clients
- Local apply / outbox (Android, desktop, web cache): the same rule as the server, in server time:
a local write converts its
read_atwith the last known offset (0 if none), stores it if newer than or equal to what the local list holds. Two queued writes for the same book: keep the one with the laterread_at(force stays sticky). After a pull, a pending write is reapplied with that rule: newer than the server's → it shows (and is sent); older → the server's position shows (it's still sent, forfurthest). - Never save before the position is restored after opening a book, and never save a position the
reader didn't reach by reading: while a jump is tentative ("Zurück zum Text" after a footnote or link,
"Weit gescrollt — Zurückspringen"), the previous position and its
read_atare what gets saved, until the reader returns (nothing changes) or carries on reading there (then the jump target counts, with its own time). - Opening a book uses the newer of server state and local (not yet sent) state, by
read_at. - Coming back to an open book (window/app in front again, or after a pull): if the server holds a newer position from another device than the one shown, offer it: "Auf weitergelesen — Springen". Do not move the page without asking.
- If
furthest_*is clearly ahead of the continue point when a book opens, offer "Weiteste Stelle: 63 % — Springen".