Skip to content

feat(anki): pull reviews from Anki over AnkiConnect (#264) - #327

Merged
HugoFara merged 1 commit into
developfrom
feat/264-anki-review-pull
Nov 3, 2026
Merged

HugoFara merged 1 commit into
developfrom
feat/264-anki-review-pull

Conversation

@HugoFara

@HugoFara HugoFara commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Step 1 of the build order in the sync spike (docs-src/developer/anki-sync-conflicts.md, #326): reviews done in Anki come into LWT without a file. Steps 2 (push LWT reviews to Anki) and 3 (fields, tags, flags, existence) are not in this PR, so #264 stays open.

What the learner gets

A page at /vocabulary/anki/sync, linked from the .apkg import page:

  1. Its script calls AnkiConnect on 127.0.0.1:8765, starting with requestPermission, so Anki asks once whether to trust the page. Nobody has to edit webCorsOriginList.
  2. It reads every LWT Term note (findNotes + notesInfo), maps cards to terms through the LwtId field, and reads their reviews (getReviewsOfCards).
  3. It posts them to POST /api/v1/anki/reviews in batches of up to 5,000, then shows what was merged, already known, recomputed or skipped.

Anki is only read. Pulling twice is harmless.

How reviews merge: MergeReviewHistory (Review module)

  • Duplicates: reviews LWT already has (same second and grade, which is how an exported review comes back) are dropped.
  • Newer reviews: appended, starting from the stored state, so a due date set by hand is respected.
  • Older reviews: a review older than LWT's latest is fitted in where it happened. The later history is recomputed on top through the real scheduler, in one transaction (replaceHistoryFrom). A test checks that the result equals doing all the reviews in one app.
  • Rebuild start: the starting state is reconstructed exactly from review_log, which stores stability, difficulty and interval after each row; reps, lapses and state follow from the grades. The one approximation: a review older than a seeded term's entire history starts from a fresh seed of its current status (seedFor, anchored at WoCreated), because the original seed isn't stored.

The .apkg importer (ScheduleReplay) now merges the same way. That fixes its documented limit of skipping an Anki review that predates LWT's last one.

Other changes

  • CSP: connect-src gains http://127.0.0.1:8765 and http://localhost:8765 (loopback, AnkiConnect's port only).
  • Translations: 27 strings in all 11 locales, with plural forms per language. check-locales reports 100%.
  • Docs and CHANGELOG: the spike doc marks step 1 implemented, the FSRS doc's description of the import policy is updated, and the CHANGELOG has an Added and a Fixed entry.

Limits

  • Hosted LWT: on an HTTPS server, the browser may ask for local network access to reach Anki on the user's machine, or block it. The page says so and points to the .apkg round trip. A local install is unaffected.
  • Every pull reads every review: the browser fetches all reviews of LWT's notes each time and the server drops the known ones. That's simple and needs no stored cursor; cardReviews from a high-water mark is the optimisation if large collections turn out slow.
  • Not run against real AnkiConnect: Anki isn't installed in this environment. The request shapes follow yanki-connect 4.0.6's types, since the official README was unreachable. Worth one manual run before release.

Tests

  • PHP:
    • MergeReviewHistoryTest (9): duplicates, milliseconds, same-second grades, append from the stored state, rebuild, starting from nothing or from a seed, repository refusal.
    • ScheduleReplayTest: interleaving, plus an equality check against a single history.
    • AnkiReviewPullTest (6) and AnkiApiHandlerTest (5).
    • Two MySQL integration tests: a real rebuild and seedFor.
  • Frontend: anki_sync.test.ts (6) covers the client, card-to-term mapping, chunking, unreachable versus refused, and batching.
  • E2E: 21-anki-import gains four tests: the page loads translated, it's linked, the unreachable message shows, and stubbed AnkiConnect data is posted and summarised. 22, 23, 06 and 09 still pass.
  • Gates: composer test:no-coverage (9213), npm test (4437), Psalm (no errors), phpcs, typecheck, lint, build:all and docs:build all pass.

🤖 Generated with Claude Code

Step 1 of the build order in the sync spike. A page at
/vocabulary/anki/sync reads every review of LWT's notes from Anki through
AnkiConnect on the learner's machine, maps cards to terms by the LwtId
field, and posts them to POST /api/v1/anki/reviews in batches. Anki is
only read; it asks once whether to trust the page (requestPermission).

The server merges each term's reviews with MergeReviewHistory: reviews
LWT already has (same second and grade) are dropped, so pulling twice is
harmless; newer ones are appended from the stored state; and a review
older than LWT's latest is fitted in where it happened, with the later
history recomputed in one transaction. The .apkg importer now merges
the same way, which fixes its old limit of skipping such reviews.

The state a rebuild starts from is reconstructed exactly from the log;
only a review predating a seeded term's whole history falls back to a
fresh seed of its current status (seedFor). connect-src gains AnkiConnect
on loopback. Strings in all 11 locales.

Co-Authored-By: Claude <noreply@anthropic.com>
@HugoFara
HugoFara merged commit 5bdffb2 into develop Nov 3, 2026
14 checks passed
@HugoFara
HugoFara deleted the feat/264-anki-review-pull branch November 3, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant