Sync was push-on-demand only: a deck frozen on the laptop stayed in dirty
until someone opened Settings and pressed Sync now. The two stores whose
writes mean "the user just made something" now schedule one debounced
background syncNow("auto") through lib/sync-auto: debounced 2.5s so a burst
of writes is one push, one run at a time, silent on failure (the data stays
dirty and Settings still offers the manual sync), retried on the browser's
online event, and imported dynamically so the sync code stays out of the
first load with no import cycle.
markSyncDirty() now reports whether it actually marked, which keeps pull and
backup-import paths quiet while a real import pushes like any other local
change. Answers and chat keep the existing manual behaviour on purpose —
they change every few seconds while studying.
Tests: phase-1 (a deck write pushes itself, then Sync now is a no-op — 50
checks) and phase-3 (a check run pushes the new homework record — 30 checks).
README: decision 59. SW -> freezmo-v46.
A generated matching card arrived on a phone with three identical
definitions (the generation guards now drop those), but there was no way
to get past a bad card from inside a session. The feed chrome (deck
study, review, mistakes and exam) now has the red "!" control: one tap
records the question, skips to the next card and shows an Undo toast.
- lib/question-reports.ts: the local list (freezmo.reports.v1, zod on
read, capped 200) is the skip state — reported cards leave every
session type, count as "not attempted" in the results instead of
wrong, and Undo takes a report back. A deck where everything is
reported stays studiable so it can be regenerated.
- POST /api/reports appends one JSON line per report to
REPORTS_DIR/reports.jsonl (shared/freezmo/reports in production, the
dev data dir in dev). Deliberately write-only: the question text is
readable on the server (that is the point — prompt tuning) but never
over HTTP, since the app has no auth. Best effort: offline reports stay
local.
- The button: a red outlined rounded square with "!", the chrome's
control size, in both feed headers (hidden on the results slide).
- config: REPORTS_DIR + RATE_LIMIT_REPORTS (20); the deploy creates the
shared reports dir; the dev unit and AGENTS.md note the new server
data location.
- Suites: phase-1 (append + validation + no GET), phase-2 (button, toast,
server POST, local copy, skip counter, hidden next session, Undo, exam
pool). SW -> freezmo-v38.
A generated Czech literature deck reached a phone with three identical
matching definitions — the card could not be solved. The pipeline only
deduped matching terms (and ordering items), so repeated definitions and
options sailed through.
- parseDeck now rejects any card whose options, items, terms or definitions
repeat (case-insensitive), through the same drop-or-repair path as the
ordering giveaway guard: the card is dropped, or a deck left with too few
cards gets one repair round with a clear reason.
- The matching dedupe covers definitions too, and both it and the ordering
one only trim when the card still meets the schema minimum afterwards —
a shorter card would fail validation on the next read and take the whole
deck down with it (latent data-loss bug).
- The generation prompt states the uniqueness rule.
- phase-1 suite: a stub Ollama returns a deck with a repeated-definition
matching card (the reported shape), a repeated-option card and a
salvageable one; asserts the first two are dropped, the third is trimmed
to three pairs and the good cards ship (43 checks).
- README: decision 51.
Server-side only (lib/ai.ts, lib/prompts.ts) — no client bundle change, no
SW bump.
The browser harness lived in /tmp/opencode and an overnight reboot
wiped it. It now lives in tests/ and survives reboots, is versioned
and runs with one command.
- tests/lib.mjs: shared helpers (Chromium auto-detect, seeding,
IndexedDB fixtures, stub/extra instance spawn with process-group
cleanup, check/finish).
- tests/run-all.mjs + npm run test:e2e [filter]: runs the ten suites
in order with a summary; suites that need a stub Ollama or VAPID
keys start their own next start instance on 3208/3209, verify the
port is free and kill the group on exit. Test scratch in
tests/.tmp/ (gitignored).
- Suites rebuilt and green against the current build: regression 38,
mobile 94 (tab matrix, ... sheet, settings sheet, axe), wizard 83,
native 31 (PWA/offline/share/back/undo/haptics/draft), i18n 13,
freeze-loading 6, review 15, phase1 36 (backup/sync/usage limits),
phase2 42 (learning engine/reminders), phase3 21 (homework check).
379 checks, 10/10 suites in one full run.
- playwright-core + axe-core added as devDependencies; CHROME env or
the newest ~/.cache/ms-playwright chromium is used.
- README: new decision 45 documenting the harness; decision 44's
verification note updated.
The older suites' exact sources were lost with the wipe, so the
rebuilt versions reconstruct the same feature areas rather than
being byte-identical.