feat(highlights): edit notes from the web — closes the round-trip#142
Merged
Conversation
- The tooltip resolves the day under the cursor (date + that day's minutes, or "no reading" inside a gap) below the book totals — the per-day data was only legible as tick opacity before. - Phones (<480px container): the rail narrows 200->96px so the ribbon keeps the viewport; coarse-pointer taps inspect first (tooltip), navigate on the second tap on the same bar. Scroll or empty-space tap dismisses. Book-page activity sparkline (planned as part of this PR) was investigated and dropped: the per-book stats hero already charts per-day activity for both live sessions and imported page-stats (sessions are synthesized from page-stats), so the strip would duplicate an adjacent chart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/meta/whats-new parses CHANGELOG.md for the running version's section (Unreleased fallback for dev builds); the frontend shows a one-time dismissible panel when the server version differs from the last one this browser saw. Fresh browsers baseline silently — first login never opens with a modal. Panel portals to <body>: the header that mounts it has backdrop-blur, which would otherwise contain the fixed overlay. GET /api/meta/update-check (admin-only) compares against the latest GitHub release, cached in memory 24h (1h after failures), gated by TOME_UPDATE_CHECK (default true). Settings -> About shows a quiet "vX.Y.Z available" link only when something is newer. CHANGELOG.md is now copied into the Docker image for the panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upload dialog: files are sha256-hashed in the browser (sequential, 512MB cap) and checked via the existing POST /books/check-hashes before upload; exact duplicates show "already in your library" with a link and are skipped. The server already deduped silently — this saves the transfer and makes it visible. Hash-check failures degrade to the old behavior. Admin -> Covers (GET /admin/covers/audit): flags missing, unreadable, and low-res covers (PIL header reads only). The floor is 300px — the standard Google Books cover is 329px and fine in practice; the audit exists for the 98-128px thumbnails. Missing covers offer one-click auto-fix via the existing cover-candidates + set-cover endpoints (nothing to downgrade); low-res rows deep-link to the book's cover picker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Global palette mounted from the shared header: full-text book search via GET /books?q= (debounced, covers in rows), series/authors ranked from the facets list client-side (startsWith > includes > shorter), and plain navigation actions (admin sees Admin). Arrow keys + Enter, Esc closes, portaled past the header's backdrop-blur containing block. Registered in the "?" shortcuts help. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New position_history table (additive, created by the startup create_all
like every other table): upsert_position — the single choke point all
position writers go through — appends an entry whenever the position moves
meaningfully (>=0.2% or locator change; the idle heartbeat can't spam it),
pruned to the newest 40 per user+book.
GET /books/{id}/position-history lists the log + live position;
POST .../{hid}/restore sets the live position back. Restore is an explicit
override of the sticky-completion rule: recovering from a false 100% also
un-finishes the book (status/finished_at), or the position would come back
while the book stayed "read". The restore itself is logged, so it can be
undone the same way. Devices converge on their next pull.
UI: history button on the book page's Reading Stats header opens the log
with per-entry Restore; refreshes stats + status pill after.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /books/{id}/reader-pacing returns the chapter fraction boundaries,
book word count, and the user's true WPM (same rule as the stats tile:
finished word-counted books with >=5min reconciled read-time; null without
history). The reader computes words-left from the current relocate
fraction against the chapter's end_fraction — no pagination involved —
and shows "~N min left" in the footer beside the chapter name (250wpm
default, tooltip says which pace is in use). Recomputes when pacing
arrives, since the initial relocate races the fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-user notification_channels table (additive) + fanout hooked at the ORM layer: an after_insert listener collects fresh Notification rows on the session, after_commit hands them to a daemon thread that loads the owners' enabled channels and POSTs each (5s timeout, best-effort, no retries). Zero changes at the six Notification creation sites; a rolled-back transaction never sends. Settings -> Notifications: add/pause/delete channels per kind (ntfy topic URL + optional bearer token; Gotify base URL + app token; bare webhook), with a synchronous test button that surfaces delivery errors. Tokens are never echoed back (has_token only). TOME_OUTBOUND_NOTIFY=false is the operator kill-switch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stateless preview + apply: POST /import/reading-csv detects the dialect (header sets), parses read/currently-reading rows (Goodreads ="ISBN" unwrapping, both date formats, half-star StoryGraph ratings), matches against the caller's visible library (ISBN -> exact normalized title+author-overlap -> fuzzy title >=0.88), and annotates exactly what applying would fill. POST /import/reading-csv/apply is fill-gaps-only: status only onto unread, rating/review/finished_at only when absent — an import can never clobber live sync state or curation (same philosophy as the TomeSync sweep). To-read shelves are skipped by design. Settings -> Import reading history: upload, per-row preview with match provenance and no-op dimming, checkbox selection, applied-counts summary. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backup: GET /admin/backup/download streams a tarball with a consistent SQLite snapshot (sqlite3 backup API — never a raw copy of a live WAL db), the cover cache, and a manifest. Library files stay on disk by design. Restore is two-phase: the upload is validated (tar shape, manifest, integrity_check, table counts) and STAGED; the swap happens at the next server start inside create_db_engine() — before any engine or connection exists — with the current DB kept as tome.db.pre-restore-<ts>. A corrupt staging file is parked as .failed and startup proceeds on the current DB. Typing RESTORE is required to arm it; staging can be cancelled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend round-trip already existed end to end (PUT /annotations/{id}
supports device-synced highlights with LWW mtime bumping; DELETE writes
tombstones; the plugin's _applyForeign applies newer note edits) — but the
web only exposed delete. The Highlights page and the book page's
Highlights & Notes section now edit/add notes inline; saves land on
devices at their next sync like any cross-device edit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overnight run PR 11/12 — stacked on #141; merge in order. Frontend-only — no plugin build needed after all.
The finding
The idea-sheet item was "highlight round-trip editing (server + plugin work)". Investigating first paid off: the entire backend round-trip already exists —
PUT /annotations/{id}explicitly supports device-synced highlights (LWW mtime bumped past current so the edit wins on devices),DELETEwrites tombstones with the clock-frame guard, and the plugin's_applyForeignalready applies newer foreign note edits (L.item.note = s.note). All shipped with the web-annotations work. The only gap was UI: outside the reader, the web could delete but not edit.What
Verification
_applyForeignpath that already ships in build 35 — no new plugin code, but worth including a note-edit in the next device validation pass alongside the sync-on-suspend checks.