diora-web/docs/progress-sync.md
mrwnslz 60dd4926cf
All checks were successful
Build and push Docker image / build (push) Successful in 15s
Test / test (push) Successful in 8m21s
Lesestand: die zuletzt gelesene Stelle gewinnt, nicht mehr die weiteste; sw v100
docs/progress-sync.md ist der Vertrag für Server, Web, Android und Desktop.

- Server: read_at (Zeit, zu der dort gelesen wurde, Uhrfehler des Geräts per
  sent_at herausgerechnet, nie nach "jetzt"), Gerät, weiteste Stelle. Ältere
  Schreibvorgänge (offline nachgereicht, liegengelassener Tab) gewinnen nicht.
  Apps ohne read_at: nur strikt vorwärts. Antwort = Zustand nach dem Schreiben.
  /books/ mit X-Server-Now, /api/sync/ mit server_now.
- Web-Reader: Zeit einer Stelle ist der Moment, in dem man durch eigenes Tun
  hinkam (Scrollen, Blättern, Tasten, Vorlesen); unverändert Liegendes
  schickt nichts bzw. nur seine alte Zeit. Vor dem Wiederherstellen und
  während eines Blicks (Fußnote, "Weit gescrollt") wird nicht gespeichert.
  Eigene Stellen in einem Ausgang (localStorage) bis der Server sie hat.
  Angebote "Auf <Gerät> weitergelesen" und "Weiteste Stelle".
- Tests: books/tests.py (Reihenfolge, Uhrfehler, Zukunft, alte Apps, weiteste
  Stelle, Liste und Snapshot).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:31:36 +02:00

71 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.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/`
```json
{"scroll_fraction": 0.42, "position_anchor": "187:0.62", "read_at": 1760270000000,
"sent_at": 1760270004000, "device": "diora Desktop"}
```
* `read_at` is **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's `read_at` back unchanged.
* `sent_at` is 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)`, then `t = 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, or `force`).
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_fraction` when an anchor is missing).
* `force: true` (the explicit "Diese Stelle als Fortschritt setzen") stores regardless, with `t = 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), with `t = 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_at` with 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 later `read_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,
for `furthest`).
* **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_at` are 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 <device> 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".