Commit graph

6 commits

Author SHA1 Message Date
d95c5c7151 Podcast mode: notes read aloud with a lyric-style word highlight
The third segment of the note-powered wizards (Study deck / Homework /
Podcast) takes the same intake - photos, documents, pasted text, the share
target - and turns it into a spoken episode.

- prompts: PODCAST_SYSTEM_PROMPT writes for the ear (plain prose, no markdown,
  no visual references), chaptered into 3-8 sections around a length target.
- /api/podcast mirrors /api/generate's plumbing (rate limit, model gate,
  provider keys, heartbeats, the 2x deadline, the optional vision pass).
- storage: frezmo.podcasts.v1, zod-validated, included in the backup and the
  encrypted sync payload; a new library tab lists episodes with listen /
  rename / delete-with-undo, and the wizard switch deep-links in.
- player: the transcript is word-wrapped spans and the word under the voice
  rides a lyric-style highlight that keeps itself centred. Two clocks: exact
  onboundary events where the browser reports them, a per-word length timer
  where it does not (iOS Safari); the first boundary retires the timer.
  Pause = cancel + re-speak from the word, which makes resume, chapter jumps
  and tap-any-word-to-seek the same operation; the position saves on pause,
  page-hide and unmount, so reopening resumes. Speed rebuilds the engine and
  keeps playing; the Aero soundtrack pauses itself when the voice starts; a
  browser with no voice gets an honest note and a readable transcript.
- the script sits on a glass pane (plain paragraphs over Aero's grass failed
  contrast; the scan now covers the player too).

tests/podcast-test.mjs (17th suite) stubs the model and the speech engine and
drives the highlight by hand: 26 checks. All suites green, contrast clean.
Version 0.7.3, SW v95.
2026-10-16 11:42:15 +00:00
982391ca45 Codebase audit: fix bugs, performance issues, dead code, and edge cases
1. Bugs fixed:
- Backup schema now accepts theme: "aero" so backups exported under
  Frutiger Aero pass parseBackup on import, and applyImport in "replace"
  mode replaces SRS schedules, answer history, and chats instead of
  merging into existing SRS or skipping empty arrays.
- Typed FillBlank cards bind the input value to chosen ?? value instead
  of chosen ?? "", and normalise() preserves Unicode letters (\p{L}\p{N})
  when grading non-ASCII answers.
- OpenAI-compatible vision calls (contentFor in lib/openai-compat.ts)
  prefix data:image/jpeg;base64, when routes pass stripped base64.
- Streaming calls (ollamaChatStream, providerChatStream) keep their
  AbortController active across the entire body stream and API routes
  forward request.signal so stopping a chat or cancelling a run aborts
  the upstream request immediately.
- HomeworkCard uses homeworkModeLabelKey so "check" records display the
  Checked label instead of Step-by-step guide.
- Desktop dropzones on /create and /homework accept dropped documents as
  well as images.
- LibraryScreen filters reported questions out of its due and mistake
  counts to match ReviewClient.
- Clearing a provider key resets generation if it pointed at that
  provider; chat conversations persist and restore provider; ModelPicker
  keys items by provider:name.
- RouteTransition no longer mutates module-level lastPath during render.
- Service worker asset handler falls back to Response.error() when an
  uncached asset fetch fails offline.
- Confetti and .ice-orb / .ice-bubble respect the in-app reduce-motion
  toggle.

2. Performance & cleanup:
- Deduplicate window storage listeners across all six localStorage
  stores (one listener while subscribed instead of N^2 notifications).
- Use todayAnswerCount in LibraryScreen and pass sorted folders to
  DeckCard.
- Memoize MessageRow with stable callbacks in ChatScreen and compute
  last message IDs in a single pass.
- Close ImageBitmap after drawing in lib/image.ts and destroy pdf.js
  documents in finally in lib/documents.ts.
- Avoid re-parsing serialized sync payloads in POST /api/sync.
- Deduplicate copyText (lib/share.ts -> lib/clipboard.ts), chatMessageId,
  describeExtraction, ALL_FOLDERS, useIsPhone, and remove unused helpers
  and boilerplate public/*.svg files.

Version 0.5.8, SW -> freezmo-v80.
2026-10-14 19:00:17 +00:00
ff58fdd71d New Aero background (Gemini-made), old credit removed
The scene is the user's own Frutiger Aero image now: green hills, a river,
an eco-city, bubbles and daisies. Re-encoded to public/aero/background.webp
(152 kB) with a fresh 3 kB pre-blurred variant for the glass, and the
WallpaperSafari credit line, its i18n keys and the README credit are gone
(the image is made by the user with Gemini).

Contrast: 0 below-minimum findings and 0 axe violations, all three themes.
Version 0.2.8, SW -> freezmo-v60.
2026-10-13 08:10:05 +00:00
5ca0de920c Chat mode, phase 2: local history, answer actions, motion polish
- Conversations in a versioned IndexedDB store (freezmo-chat v1, zod
  validated with the same schema backup/sync use, capped at 100 x 300).
  Settled turns persist (question always; answer when produced, partial
  stops included); ?c=<id> keeps reloads on the conversation via
  history.replaceState (no router round-trip mid-chat).
- History UI: desktop column, phone sheet (spring, backdrop, Escape),
  auto titles from the first question, inline rename, delete with an
  Undo toast, diacritic-folding search over titles and message text.
  Scope and model are remembered per conversation; history is readable
  offline.
- Assistant actions: Turn into flashcards (New deck with the answer as
  notes, landing on the options step - deck seed gained step) and Save
  to notes (appends to the single scoped deck, trimmed at 40k).
- Chats travel in the backup export (preview counts them) and in the
  encrypted sync payload; still never the key.
- Motion from lib/motion.ts: message fade-up, sheet springs for the
  model/context/history sheets, 150ms composer height transition, all
  skipped under reduced motion.
- Tests: chat suite 63 -> 89 checks (history, reload, rename/search/
  delete/undo, actions, export, two-device sync round-trip, offline,
  desktop panel). Full set: 12/12 suites, 513 checks. SW -> freezmo-v33.

Also: tests/model-compare.mjs benchmark (diagnostic) for deciding the
default text model on numbers; defaults unchanged.
2026-10-11 19:03:48 +00:00
e4a8e0ba01 Phase 2: learning engine — history, SRS review, mistakes, stats, exam, reminders
Answer history and schedules
- Every answer is recorded (deck, activity, topic, correct/wrong, time)
  in a versioned IndexedDB log (freezmo-learning v1, stores answers +
  srs), mirrored in memory for synchronous React reads and capped at
  20k answers (oldest pruned). StudyFeed records as it grades.
- Light SM-2 schedule per activity: 1 day, 3 days, then interval x
  ease; wrong answers return in ten minutes with the ease nudged down.

Review, mistakes, stats
- Library 'Smart review' card: due count, mistakes count (latest
  answer wrong — clears itself when answered correctly), today's goal
  progress. Review sessions freeze their item list at start.
- New /review (due across decks, ?mistakes=1 global or ?deck= per
  deck) built on a shared SessionFeed with the study feed's snap/keyboard
  mechanics, wake lock and a results slide.
- New /stats: day streak, five-week calendar, accuracy per subject and
  per deck, weakest topics (>=3 answers), daily-goal progress.
- Daily goal configurable in Settings (10/20/30/50).

Exam mode
- New /exam: pick decks, question count, optional timer; no hints
  (a NoHints context hides hint rows and the self-check escape);
  results with score, per-deck and per-topic breakdown; wrong answers
  land in the mistakes set through the normal history.

Reminders (conditional)
- VAPID-gated: GET/POST/DELETE /api/push and POST /api/push/test with
  the standard route pattern, RATE_LIMIT_PUSH, zod, auth before body.
  Subscriptions stored server-side as endpoint + keys only (PUSH_DIR).
  UI hidden entirely without keys; iOS install note; service worker
  push/notificationclick handlers; scripts/send-reminders.mjs for a
  cron/systemd timer; web-push added as the one server dependency.

Verified: new phase-2 suite 42/42 (history/SRS shapes from a real study
run, mistakes clearing, review session, stats math, exam timer and
breakdowns, VAPID-less hidden state plus the configured
subscribe/validate/test/delete path against a second instance) plus
regression 74/74, mobile 52/52, wizard 80/80, declutter 116/116,
animation 8/8, native 35/35, phase1 38/38, i18n 25/25. Key parity
709/709. Lint + build clean. SW freezmo-v26.
2026-10-10 14:15:21 +00:00
bb1f4e2018 Phase 1: backup, encrypted sync and monthly-usage handling
Safety net and errors, mobile-first, desktop unchanged in behaviour.

Backup and persistence
- Settings -> Data & backup: 'Export all data' writes one zod-validated
  JSON (decks, folders, homework, progress, safe settings; the API key
  is not part of the shape and never appears in the file).
- 'Import backup' validates, shows a preview with added/replaced
  counts and offers merge or replace; invalid files get a friendly
  error. Storage persistence (navigator.storage.persist) is shown and
  requestable, with the result reported.
- A dismissible nudge appears after 5 new decks or 14 days without a
  backup and stays snoozed for 14 days.

Opt-in encrypted sync (off by default)
- New lib/sync.ts: PBKDF2-SHA256 (310k) -> AES-GCM-256, derived key
  kept as a non-extractable CryptoKey in IndexedDB (lib/sync-key.ts);
  the passphrase and key never leave the browser.
- New POST/GET/DELETE /api/sync following the route pattern (locale,
  cross-origin guard, body cap, rate limit RATE_LIMIT_SYNC, auth
  before body parsing, zod validation). One atomic JSON file per
  random 128-bit sync ID under SYNC_DIR; server-stamped updatedAt is
  the conflict clock; SYNC_MAX_BYTES and SYNC_TOTAL_BYTES enforced
  (413 / 507). The server only ever sees an opaque envelope.
- Settings -> Sync: enable with a passphrase, copy the sync ID, sync
  now, last-synced time, explicit keep-this-device / use-synced-copy
  conflict choice, join from another device (ID + passphrase, wrong
  passphrase rejected), unlock after the key is gone, delete the
  synced copy (works even on a locked device). Every data store marks
  sync dirty on write; pulled payloads are applied without marking
  dirty again.

Monthly usage vs short-term throttling
- lib/ollama.ts classifies the real 429 body (monthly/quota/upgrade
  vs burst) into new 'usage_limit' and 'throttled' kinds, mapped in
  every route and in the health probe.
- The client renders the same localized message in the deck flow,
  homework, grading and the Settings banner: the required
  upgrade/new-key copy with 'Open Settings' (jumps to and focuses the
  key field) and 'Open Ollama'. Input and wizard drafts are kept so
  Retry works right after swapping the key. No model fallback.

Verified: new phase-1 suite 38/38 (backup content + no key, import
preview/merge/replace, junk file, persistence row, nudge + snooze,
sync enable/upload/opaque blob/push/join/wrong passphrase/delete,
server validation 400/401, usage-limit copy and key focus, and the
real route mapping against a stub Ollama for both 429 variants) plus
regression 74/74, mobile 52/52, wizard 80/80, declutter 116/116,
animation 8/8, native 35/35, i18n 25/25. Key parity 638/638. Lint +
build clean. SW freezmo-v25.
2026-10-10 13:43:42 +00:00