Commit graph

59 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
a4c85ed77a Frutiger Aero: the theme now plays its soundtrack
Selecting Aero starts a random track from the Frutiger Aero playlist
(Bigblamboii's set on SoundCloud); every following track is drawn at random
too, so the set shuffles itself. The player is the credit bar from the mock:
a 44px glass circle with a note by default, expanding into pause + the
track's title + "SoundCloud - artist" (both reported by the widget).

- Only Aero ever touches SoundCloud: the embed and the widget API are created
  while the theme is on and the listener has not turned it off; light/dark
  request nothing (pinned by a suite check).
- The off choice persists (freezmo.aero-music.v1); re-selecting Aero in
  Settings is fresh consent (applyTheme clears it).
- Autoplay rides the theme-selection gesture; after a reload the circle comes
  up paused (browsers require a gesture) and one tap starts it.
- Volume 45; the iframe is 1px and inert, only the audio engine; the credit
  area collapses back to the circle without stopping the music.
- Geometry: clears the phone tab bar, lifts above the chat composer and the
  wizard bar, steps aside under immersive overlays and while the cookie
  notice shares the corner. 44px targets on phones.
- Cookie policy gained the third-party-embed paragraph it was owed.

tests/aero-music-test.mjs (16th suite, stubbed widget API, 21 checks). All
suites green, contrast scan clean. Version 0.7.0, SW v92.
2026-10-15 22:47:44 +00:00
71c56ad06f Review breakdown: show each deck's own activity total
Follow-up to the smart-review breakdown: "I only have 2 decks" turned out to
be three deck cards (one clipped at the window edge) and 15 + 13 + 6 = 34
due. The line now carries each deck's activity total next to its due count -
"Due by deck: Sky deck - 15 of 15 - Humanismus - 13 of 15 - ... - 6 of 13" -
so the arithmetic is checkable inline without opening a deck.

groupByDeck returns {label, count, total}; new i18n key review.dueOfTotal
(en + cs). review-test pins the format. Version 0.6.9, SW v91.
2026-10-15 21:20:53 +00:00
1e8606a95b Smart review: name the source deck on every card, break the counts down
"It contains mistakes I either did more than once or from decks that I've
removed - there's no way there's 34 to repeat with 2 decks." The counts were
honest (dueItems/mistakeItems only resolve live deck+activity pairs; a seeded
repro with 37 SRS rows and 30 live shows "30 due"), but nothing in the UI said
where an item came from: a deck created twice from the same notes shows every
question twice, and a deck that survived a delete or a sync looks like a ghost.
Both read as a broken list.

- SessionFeed gains showDeck (review + mistakes): a deck chip above every
  card, so two identical questions are visibly two decks.
- The Smart review card breaks its numbers down under the buttons:
  "Due by deck: Sky deck - 17 - Sky deck (2) - 17", capped at three decks
  plus "+n more" (groupByDeck in lib/learning).
- New i18n keys (en + cs, plural forms for "+n more").

review-test pins it: a twin-deck seed renders the breakdown and both deck
chips in the feed. Version 0.6.8, SW v90.
2026-10-15 19:27:04 +00:00
98537cee64 Skipping a card is not a mistake
Scrolling through a deck without answering recorded a 0% attempt and listed
every card under "Missed items" with its answer revealed. Only answered cards
count now: the score is correct/answered, the attempt stores the answered
count, an empty run records nothing, and the cards scrolled past wait under
"Not answered (n)" - listed as prompts with "not counted as mistakes" - and
"Answer the rest (n)" starts a catch-up run of just those cards (same restart
path as retry-missed, so history and the SRS still see the answers). "Aced
all" requires nothing left waiting too.

phase2 pins the flow: all-skipped records no attempt and no mistake list;
one-of-two reads "You got 1 of 1 right" + "Not answered (1)" with attempts:1,
bestScore:100; the catch-up run records the remaining answer. Exam mode keeps
its own rule: in a timed exam an unanswered question is a miss.

Version 0.6.5, SW v87.
2026-10-15 13:59:55 +00:00
796e2e9fe9 Study cards: from-your-notes, read aloud, timed runs
From your notes: the generator now stores a verbatim source quote with every
activity (schema + prompt + a server-side clamp so a chatty model cannot drop a
card), and the feedback panel shows the sentence that carries it with the quote
highlighted green. Decks made before the change, and quotes the model trimmed,
fall back to a local IDF-weighted match over the saved notes text
(lib/notes-source.ts): nothing is shown unless at least two distinct terms
land. The passage hides in exam mode with the other reveals.

Read aloud: a Web Speech API speaker control on the text a learner reads
(flashcard faces, questions, statements, sentences, instructions) via
lib/speech.ts and components/SpeakButton.tsx — one utterance app-wide, a voice
matched to the deck's language, and no button at all where the browser has no
speech synthesis. It used to sit inside the flashcard's flip button (nested
buttons, tapping it flipped the card); it now lives next to the hint row.

Timed runs: the feed chrome carries a timer chip (off by default) that gives
each card 30 seconds; a card that runs out is recorded as a miss through the
same path as a wrong answer (history + SRS agree), shows a toast, and the feed
moves straight on with a fresh budget. handleAnswered ignores a second result
per card, so a timeout plus a manual answer cannot record twice.

The generator's type spec now names what each type suits (language decks lean
on flashcards/matching/fill blank, ordering needs a real sequence, short answer
reasoning), so the deck-aware guidance covers all seven types.

New activities-test suite (24 checks: quote path, local path, the green mark,
the speech stub, the timed run on Playwright's fake clock), registered in
run-all; phase2 pins that the notes passage stays hidden in exam mode; en+cs
keys for every new string; no new dependencies. Version 0.6.1, SW v83.
2026-10-15 05:53:47 +00:00
236ad9d552 just changed up some of he translations to make more sense 2026-10-14 22:13:08 +02: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
7e42b03f7e Custom Frutiger Aero 404
app/not-found.tsx renders a standalone screen: the root layout's ice
scene is the backdrop, the glass card/rim/bubbles come from the shared
recipes (full Aero with that theme on, frosted glass in light/dark),
and the mascot is the scene's glossy ice orb at full strength with a
snowflake and a "404" chip. Two ways out - back to the library and
into the chat - both 44px targets, en + cs.

The regression suite pins the 404 status, both links and the Czech
copy; the pixel-contrast scan covers the screen in all three themes.
The service worker no longer caches non-OK navigations as the offline
shell (a typo'd URL used to write the error page into the shell
cache). Version 0.5.6, SW -> freezmo-v78.
2026-10-14 15:27:47 +00:00
a5d30a171c Chat chrome docks into the rail; drop the notes-off banner
The chat header and composer are full-bleed bands inside the main
pane, which starts right at the sidebar on desktop - so both ended in
a hard vertical edge beside the floating rail card (the 12px gutter
showed the raw scene and the bands' fill + backdrop blur read as a
color step at the seam), with their hairlines starting in mid-air at
that edge. Both bands now reach the card's right edge (the rail's own
12px padding, expanded or collapsed) and their hairlines are inset to
the content box; phones keep the plain full-width borders. The chat
suite pins the dock. The "Chat is set to ignore notes text" banner
went away - the scope chip already says what the chat may read.

Version 0.5.5, SW -> freezmo-v77. Chat 104/104 (2 new), mobile
126/126, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13,
native 33/33, regression 44/44, contrast 0 findings, axe clean.
Full e2e: 14/14 suites.
2026-10-14 11:46:34 +00:00
7da308fa1e Aero button glass + homework report control; reduced-motion sheet glitch
Buttons: the recipe's thin veils let btn-secondary/btn-ghost/glass-pill
blend into the grass and sky - they now carry a stronger pale-glass veil
(the Ask-pill look) over the blurred scene, ghosts plus a rim and
full-contrast text. Homework make/check results gained the same red "!"
report control as the feeds and the guide (a synthetic short-answer
activity maps into the report flow; pinned in phase3).

Reduced motion: the manual toggle only fed framer's MotionConfig, which
strips transform animations - while components branched on the OS-only
useReducedMotion - so translucent sheets still spring-faded their opacity
in over the page (the "ugly visual glitch with the settings panel"). All
16 components now read useAppReducedMotion() (OS or toggle), and the
settings sheet/scrim and the library's phone sheet go instant. Verified
frame by frame: opacity 1.00 from the first frame.

Contrast 0 findings, axe clean; mobile 126/126, chat 102/102, providers
20/20, wizard 95/95, phase3 31/31, bubbles 34/34, i18n 13/13, native
33/33. Version 0.5.4, SW -> freezmo-v76.
2026-10-14 09:19:54 +00:00
8651674cf5 First-run gate offers free Ollama vs paid providers; add Google Gemini
The gate (user-key installs) now headlines Ollama with a "Free" badge and
adds an "or - Use a paid provider instead" option that opens the same
provider rows as Settings, framed as billed by the provider directly.
Saving a provider key there lists its models and switches study
generation to it automatically, so the gate steps aside with no Ollama
key at all.

Gemini joins the registry via its OpenAI-compatible endpoint
(generativelanguage.googleapis.com/v1beta/openai); the privacy policy
names it (en+cs).

Providers suite grows to 20 checks: the gate scenario runs against a
second user-key-mode app instance (free badge, paid option, provider
rows, pass-through, generation switch). All other gates green:
contrast 0 findings, mobile 126/126, chat 102/102, wizard 95/95,
bubbles 34/34, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.9, SW -> freezmo-v73.
2026-10-14 07:09:05 +00:00
4372423167 Bring-your-own keys for cloud providers (OpenAI-compatible)
Settings -> Providers takes keys for OpenAI, Anthropic (their official
OpenAI-compatibility layer), DeepSeek, Groq, OpenRouter and any custom
OpenAI-compatible endpoint. One adapter (lib/openai-compat) covers
listing, streaming chat, JSON generation and vision, and every model
flow can run on a provider: the chat picker lists each enabled
provider's models (labelled, remembered), and a "Study generation"
choice moves decks, homework, grading and attachment transcription onto
a provider model.

Keys stay in this browser exactly like the Ollama one
(freezmo.providers.v1): never stored server-side, never in a backup or
the sync payload, forwarded only with the request that needs them. The
provider test asserts the key reaches the provider while the routing
header stays on the app side.

New suite tests/providers-test.mjs (13 checks) stubs both protocols:
model listing, chat streaming, study generation and the Settings flow.
Privacy policy updated (en+cs). Contrast 0 findings, axe clean, mobile
126/126, chat 102/102, wizard 95/95, bubbles 34/34, regression 44/44,
i18n 13/13, native 33/33. Version 0.4.7, SW -> freezmo-v72.
2026-10-14 06:29:39 +00:00
6635b5d405 Design pass (all themes + chat UI), model picker, reduce-motion toggle
- Aero: blue gradients back on the active segment and every primary
  (Review due, the card's Study button, the composer's send button); a
  frosted backing behind the page heading and the All decks label; a
  bottom-weighted rail tint; chat header/composer/welcome panels; calmer
  orbs; stronger card veil + a stronger blur bake.
- Dark/Light: a dedicated --track token makes progress bars read (the
  first attempt shifted --surface-sunken and the pixel scan caught the
  sheet-contrast regression); brighter dark secondary text; darker light
  segmented labels and difficulty tints; stronger light card border and
  shadow.
- Chat UI: friendly model chip ("Gemma 4 31B") with the raw name in a
  tooltip, larger chips, plain centered disclaimer, vertically centered
  welcome panel, distinct chat-plus icon for new chat.
- Chat models: every chat-capable model from the server's /api/tags is
  selectable (favourites first, search field when long, live-list
  validation in the route); default CHAT_MODEL_DEFAULT or gemma4:31b.
- Reduce motion: Settings toggle (default off) that parks the scene,
  collapses CSS animations and drives framer-motion's calm mode.
- Folders hint, "New" badge on fresh decks.

Contrast: 0 findings, axe clean, mobile 126/126, chat 102/102, wizard
95/95, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.6, SW -> freezmo-v71.
2026-10-14 05:06:02 +00:00
b70b0e83cf A dropped connection says so, instead of the generic error card
A user's run failed with the wizard's generic "check your Ollama
settings" card and could not be replicated with the same key - because
the key and model were never the problem: a raw error (a mid-run socket
drop, a wifi/LTE switch, the app backgrounded by the phone OS) leaked
out of the NDJSON reader as a DOMException, and the wizard can only
show the generic card for unknown errors.

The reader now classifies everything: abort -> "cancelled", stall ->
"the model stopped responding", anything else on the wire -> "the
connection dropped mid-run, try again". Pinned with a fetch stub that
kills the stream mid-run in the wizard suite (95 checks).

Contrast 0 findings, mobile 124/124, chat 101/101, regression 44/44,
i18n 13/13, native 33/33. Version 0.4.1, SW -> freezmo-v70.
2026-10-13 18:51:24 +00:00
20bb08b0e7 Infinite loading guard: heartbeats + stall watchdogs on the model runs
A user got stuck on the wizard's loading screen: a hung model meant
silence, and silence was indistinguishable from work - the server's own
hard deadline is 2x OLLAMA_TIMEOUT_MS (ten minutes by default), and the
client waited on a dead stream indefinitely.

Both generate routes (deck, homework) now send an NDJSON heartbeat
every 15s, and the client guards the two quiet phases: no response at
all for 45s, and no bytes for 90s mid-stream, both surfacing the
regular error state with a Retry. The caller's abort still maps to the
friendly "cancelled" state.

Verified with a stalled route and Playwright's fake clock in the wizard
suite (93 checks), plus contrast 0 findings, mobile 124/124, chat
101/101, regression 44/44, i18n 13/13, native 33/33.
Version 0.4.0, SW -> freezmo-v69.
2026-10-13 18:06:25 +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
9b0354fb11 Fix the clipped answer field + the doubled AI note; ordering only where it fits
UI bug from a phone screenshot (short answer, Czech):
- The textarea was flex-shrunk inside the slide's column (58px instead of
  ~82px), clipping the typed answer mid-line. Every direct child of a
  study/review/guide slide is now shrink-0 and the slide scrolls instead.
- Two AI disclaimers were visible at once: the activity's own after the
  grading box plus the feed's row. The activity-level copy is gone; the
  feed/guide row (which also carries the report control) is the single one.
  The phase-2 check now asserts the note sits below the graded feedback.
- The mobile suite's backdrop-tap check is deterministic too (the spring
  animation made the measured box mid-flight).

Ordering activities: the deck prompt now allows them only when the notes
hold a real sequence (chronological events, process stages, procedure steps,
a lifecycle, a ranked scale) and points at matching / multiple choice /
true-false when they do not - "sort these" was landing on decks with no
order at all.

Version 0.2.4, SW -> freezmo-v57.
2026-10-13 07:27:39 +00:00
f1c4a89adc Aero uses the real wallpaper + liquid glass buttons
The wallpaper is the one from the mockup: "The S Aesthetic That You
Definitely Recognize Frutiger Aero" via WallpaperSafari
(https://wallpapersafari.com/w/IlWuz7) — the URL in the request pointed at a
different (teal abstract) wallpaper, this is the grass/sky/water one that
matches the screenshot. Re-encoded to a 237 kB WebP in public/aero/, used as
the scene in aero only (light/dark never download it), and credited in
Settings next to the theme picker.

The drawn CSS scene (rays, reeds, skyline, caustics) is off over the photo;
the ten rising bubbles stay, and the orbs became extra bubbles. Buttons and
segments are liquid glass now — translucent, glossed top half, bright rim,
backdrop blur — instead of the solid aqua fill. Helper lines that sit
straight on the wallpaper (the keyboard tip, the AI note) get a small glass
chip: dark text on grass measured 2.5:1.

Also carries: the underwater scene + bubbles work, the floating cookie
notice, the Czech title (Tvůj mrazák) and the review-suite fix that made the
last deploy fail its gate.

Contrast: 0 below-minimum findings, 0 axe violations across all three
themes. Version 0.2.2, SW -> freezmo-v56.
2026-10-13 06:21:43 +00:00
67e2cefcfe Aero goes underwater with rising bubbles; floating cookie notice; Czech title
Frutiger Aero, closer to the mock:
- The scene is water now: sun shafts through a bright surface, caustic
  patches, reeds on the left, a city silhouette on the right, and ten soap
  bubbles that actually rise (own keyframes, varied sizes/speeds/sway; the
  spans are display:none outside aero so they cost nothing elsewhere, and
  they sit still under reduced motion).
- Pale blue glass at 0.8: still reads as glass, but dark navy text clears
  4.5:1 over the water. The phone tab bar mixes the sheet glass instead of
  the page — in aero the page *is* the water, which left its labels at 3.9:1.
- The aero page-level text (muted/subtle) is much darker for the same reason.
- The PWA status bar follows the new water blue (#2fa7e6).

The cookie notice is floating glass now (blurred backdrop, rim, shadow): as a
plain card it read as part of the page background instead of a notice.

Czech: "Tvoje mraznička" -> "Tvůj mrazák" (library title), test updated.

Contrast: 0 below-minimum findings and 0 axe violations across every screen,
all three themes. Version 0.2.1, SW -> freezmo-v55.
2026-10-13 05:46:53 +00:00
e56653dae1 Frutiger Aero theme, pixel-measured contrast fixes, sheet surfaces
Frutiger Aero (Settings -> Theme): a third theme, not a dark/light variant.
A sky gradient with a strip of grass, the glacier blobs become clouds, the
orbs soap bubbles, glass gets the classic horizon gloss and the buttons a
glossy aqua. The light/dark toggles (header, rail, chat header) hide while it
is on and the picker says why; the pre-paint script applies the class before
first paint and the PWA status bar follows (#3fa3e0). Its muted/frost text is
darker than Light's because those labels sit directly on the sky.

Contrast, measured on real pixels (tests/contrast.mjs, now covering aero):
- The light settings sheet was translucent over a dark scrim, so its muted
  text measured ~2.8:1. Sheets and dialogs now use --glass-sheet-bg (a more
  opaque surface), and Light's --text-muted is a touch darker.
- Result: 0 below-minimum findings and 0 axe violations across every screen,
  all three themes, phone and desktop.

Also: the suites now seed the cookie notice as already dismissed (they run as
a returning user); the regression suite opts out to test the notice itself —
the previous deploy failed its gate because the notice blocked a chat chip
click. The notice also hides during immersive sessions (overlay-open), so it
can never cover a study feed.

Version 0.1.8, SW -> freezmo-v54.
2026-10-12 22:10:16 +00:00
464781a7fd Privacy policy, terms of service, cookie policy + a cookie notice
Three plain-language documents in both app languages (lib/legal.ts, rendered
by components/LegalPage.tsx): /privacy describes the local-first storage, the
key never leaving the browser and the three opt-in server copies (sync,
reminders, reports); /terms covers the GPL-3.0-only software, your own Ollama
key, fair use and liability; /cookies states what is true — no cookies at all
(no Set-Cookie, no document.cookie), strictly-necessary local storage instead,
no tracking.

A cookie notice tells every new browser the same thing once and remembers the
dismissal (freezmo.consent.v1). It sits at z-44: below the immersive study
feeds so it can never cover a session, above the phone tab bar. Settings links
all three documents. The regression suite checks the pages render with real
sections and that dismissing the notice sticks.

Version 0.1.7, SW -> freezmo-v53.
2026-10-12 21:26:55 +00:00
df34337b4a Accessibility pass, report control for homework exercises, clearer rail actions
- axe is clean across every screen in both themes, phone and desktop. Fixed:
  the hidden file inputs (aria-hidden + tabIndex -1 — they are opened by
  labelled buttons, so they should not be in the accessibility tree), the
  heading order (deck/homework/smart-review cards are h2 under the page h1;
  LearningSection takes a level so it stays h2 on the library and h3 inside
  the settings dialog) and landmarks around the study and review feeds.
- tests/contrast.mjs (new diagnostic): samples rendered pixels, because axe
  cannot judge text on blurred glass — it reports those nodes as incomplete.
  It skips disabled controls (1.4.3 exempts them) and content behind an open
  sheet. Result: no below-minimum text on any scanned screen.
- The report control exists in the homework guide too: under each "Try it
  yourself" exercise, labelled "Report exercise", without a skip (there is no
  queue to skip). lib/question-reports now takes deckId/deckTitle directly.
- The rail's two creation actions are buttons now (.nav-action) rather than
  quiet nav rows — once the library is full they were the only way to start
  something on desktop and read as just more navigation. "Make my homework"
  is "New homework" wherever it is an action (rail, empty state, share
  chooser, manifest shortcut); the page title and mode switch keep their
  wording.

Version 0.1.6 (three task bumps), SW -> freezmo-v52.
2026-10-12 21:15:56 +00:00
8822baf4b7 Security: model budget + concurrency caps, SSRF guards, spoof-proof limits
Audit pass with the threats that actually apply to a self-hosted, account-less
app: getting the Ollama key throttled or banned, and someone grinding the app
down. Per-route windows already existed; these are the missing layers.

- lib/model-guard.ts: every route that spends a model call now takes a slot —
  60 calls per window across all clients (RATE_LIMIT_MODEL_TOTAL), 3 in flight
  per IP, 6 in total — and the slot is released when the call finishes, on
  errors and on client disconnects. Over the concurrency cap answers 429
  kind=busy, not a punishment. Slots are taken after every cheap rejection so
  a malformed request cannot hold one (found by the new suite: early 400s
  were leaking slots).
- Images must be base64 data URLs (dataUrlSchema in lib/schemas): a plain URL
  in an images field made Ollama fetch an arbitrary address on our behalf.
- Push endpoints must be a real push service (PUSH_ALLOWED_HOSTS, defaulting
  to FCM/Mozilla/Apple/WNS): the server POSTs to whatever is stored.
- The rate-limit identity is the LAST x-forwarded-for hop. Reading the first
  let a client spoof its own IP and dodge every per-IP limit.
- Disk guards: reports.jsonl stops at REPORTS_MAX_BYTES (8 MB), reminder
  devices cap at PUSH_MAX_SUBSCRIPTIONS (50).
- Security headers: frame-ancestors 'none' + X-Frame-Options DENY, nosniff, a
  referrer policy and a Permissions-Policy without camera/mic/geo/payment.

New tests/security-test.mjs (13 checks, registered in run-all) proves each
guard against a slow stub Ollama with deliberately tiny limits. README:
security notes + rate-limit tables updated. Version 0.1.3, SW -> v51.
2026-10-12 20:40:57 +00:00
aac6befb0d App versioning + the create page's missing top padding
Versioning: package.json carries the app version (0.1.2 here; one bump per
task with `npm run version:bump`, patch 0-9 rolling into the minor at 10),
baked into the build via lib/version.ts, reported by /api/healthz and shown
in Settings next to the running commit. AGENTS.md documents the rule; the
native suite checks both the API and the Settings row.

Fix: on desktop the app header is hidden, so every page owns its top padding —
/create was the only one without `lg:pt-10`, which slammed the Study deck /
Make my homework switch against the top of the window while /homework had a
comfortable gap. Both now sit at 40px; the wizard suite asserts the clearance
and that the two pages stay in step (90 checks).

SW -> freezmo-v50.
2026-10-12 20:04:26 +00:00
2d4d4ee776 One loading screen everywhere (the deck wizard's)
The deck wizard had the good one — app icon, rotating stage message as the
headline, progress bar, stage checklist, tip, Cancel — while homework showed
a spinner with a title and route loads a pulsing icon with a smaller line.
All of them now render components/LoadingState:

- the wizard passes its vision/generation stages and keeps its deck tips;
- homework passes the same shape with homework wording (no tips: they are
  deck-specific);
- route screens (review/stats/exam loading boundaries + the link overlay)
  pass a single message, no stages or progress.

Cancel stays a 44px touch target, the icon animation respects
prefers-reduced-motion, and the unused homework.loading.title string is
gone from both languages.

Suites: freeze-loading 6/6, review 18/18, wizard 88/88, mobile 105/105,
regression 38/38, native 31/31, phase-2 62/62, phase-3 30/30, i18n 13/13.
README: decision 60. SW -> freezmo-v47.
2026-10-12 18:43:53 +00:00
abacfb6018 Deck/homework writes sync themselves when sync is on
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.
2026-10-12 16:40:49 +00:00
d559865a09 Check-my-work attachments, smart review follows deletions, GPL-3.0-only
- "Check my work" now takes photos and documents of the attempt: documents
  are read in the browser into text, photos go to /api/homework as
  solutionImages and are transcribed by the vision model before the check
  ("Reading your solution..."). The finished attempt comes back in
  meta.solutionText and is stored as the record's attempt, so "Redo"
  re-checks the same work without keeping the files.
- Deleting a deck now drops its due schedules and mistakes from the library's
  smart review immediately (the counts resolve against the live deck list);
  Undo brings them back, because the learning data itself is never erased.
- The phone override for the glacier blobs was still at 0.1/0.2: it is now a
  quarter of the original value there too (0.025/0.05), matching the desktop.
- License: GPL-3.0-only (LICENSE, package.json, README section).

Tests: phase-3 photos-only API + desktop attachment checks (28/28), review
suite deck-delete checks (18/18), i18n/mobile/phase-2 green. README:
decisions 57-58. SW -> freezmo-v45.
2026-10-12 16:04:53 +00:00
9d204b85be One-pill study chrome, toggling chat pickers, readable homework steps
UI batch from feedback:

- The study/review/exam/homework chrome is now a single 58px pill: round
  close button, title + position over the progress bar, chips (or the exam
  timer) at the far end — everything on the centre line, so nothing crowds
  the rounded corners any more. Homework guide gets the same recipe.
- The immersive feeds carry z-[45] so they paint over the app chrome (on
  /exam the header used to cover the top of the chrome), and the page chrome
  behind an open overlay (.app-header, .segmented, .tabbar) is hidden while
  the transparent overlay is up, so it can no longer ghost through the glass.
  The guide's disclaimer moved inside the overlay with it.
- Chat: the context and model chips toggle their own popover (pressed state,
  aria-expanded), opening one closes the other, a press outside closes it —
  no more being forced to pick something to get out.
- Homework: the model's plain-text step explanations/hints/verdicts keep
  their line breaks (whitespace-pre-wrap) — a step used to collapse into one
  run-on paragraph with the whole solution inside it; the guide prompt now
  says the explanation is coaching, 2-4 sentences.
- The four glacier blobs are at 25% of their old opacity (0.045 light,
  0.1 dark).

Tests: phase-2 chrome geometry + exam layering checks (62), chat desktop
picker checks (101), the standing suites all green. README: decisions 54-56,
decision 23 updated. SW -> freezmo-v44.
2026-10-12 14:21:11 +00:00
9cea13496a Report question: skip a broken card and tell the server
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.
2026-10-12 07:12:55 +00:00
f202653017 Generation guards: cards with repeated wording never ship
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.
2026-10-12 05:37:03 +00:00
37a84df462 AI disclaimer: one shared component, shown where model output appears
"Freezmo uses AI and can make mistakes. Double-check important info."
(plus a natural informal Czech translation), rendered by a shared
AiDisclaimer so the copy, style and translations live in one place.

Placements — only where the model's words reach the screen:
- Chat: under the composer, below the context/model chips — always (chat
  is pure model output, so it also shows in the empty state).
- Make my homework: under the start CTA (desktop) and the mode cards'
  helper line (phone), plus once under the generated result for all three
  modes (make / check / guide).
- New deck wizard: under the summary helper (phone step 3) and the desktop
  CTA, next to Freeze my deck.
- AI grading (short answers): under the graded feedback; study, review and
  exam all share it.

Stored, already-generated content (deck page, study cards, explanations)
deliberately carries no note: the disclaimer belongs to the moment of
generation, not to browsing. Style: --text-subtle (documented at 5.6:1
light / 8.6:1 dark), 11.5px, centred, wraps to two lines at 360px, not
clickable, no icon.

Suites: chat (composer, phone + desktop), wizard (deck step 3 across
360/390 x en/cs x dark/light, homework start cards), phase3 (homework
trigger + result, phone + desktop), phase2 (stubbed grading feedback).
SW -> freezmo-v37. README: decision 50 + chat composer note.

Also fixes the deploy script's test instance: it now execs the server so
the cleanup kill reaps the real PID (the wrapper subshell left orphans
squatting on the test port — three from earlier sessions were found while
verifying this task), and the readiness poll checks the port serves the
NEW commit rather than just answering, so a leftover instance can never
be tested silently.
2026-10-11 22:51:16 +00:00
8a3bef0add Chat layout: history in the main sidebar, consistent header, cleaner icon
- Desktop: on /chat the main sidebar shows the chat list (search, New chat,
  items with rename/delete, active highlight) in place of the deck
  navigation; the separate second column is gone and the chat uses the full
  width. The expand/collapse chevron sits outside the swappable section and
  a collapsed rail stays clean (the highlighted Chat row already opens a
  new chat). Active conversation travels via a tiny store (lib/chat-active)
  so the sidebar needs no useSearchParams and pages stay static.
- Header (phones): history, theme and New chat are now identical chips -
  same 44px hit area, rounded-2xl, border/surface tokens, size-5 icons,
  even spacing; the title block truncates cleanly. ThemeToggle gained a
  'plain' variant so the header styles it without touching other pages.
- ChatIcon redrawn (clean message-circle, no inner lines) and the empty
  state uses the app's icon-chip pattern (size-12 chip, accent-600/200)
  instead of the bare oversized glyph; the model chip icon follows.
- Also fixed a pre-existing dev-only breakage found while verifying: the
  tab-bar bounce animated three keyframes with a spring, which framer
  motion's dev build throws on - breaking client updates in the dev
  instance (library tab switching, sheet Escape). It now uses a tween.
- Dev metadata keeps the Apple icons in both branches.
- SW -> freezmo-v35.
2026-10-11 20:11:11 +00:00
3f52ab47c2 Infra: split dev and production (releases, services, deploy script)
- Production now runs from ~/freezmo-prod/current (release symlink) as a
  standalone server on :3207; releases are exported by scripts/deploy.sh,
  built and e2e-verified on a temp port, switched atomically, health-
  checked via the new /api/healthz (commit + build time) and rolled back
  automatically on failure. Last 5 releases kept; scripts/rollback.sh for
  manual rollback. Shared env + data live in ~/freezmo-prod/shared/ and
  are symlinked into every release (existing sync/push data migrated and
  verified identical).
- Dev runs from ~/freezmo via freezmo-dev.service: next dev on :3217
  (LAN only), its own build folder (.next-dev via NEXT_DIST_DIR), its own
  data (~/freezmo/.freezmo-dev), DEV badge, dev favicon/theme colour,
  "Freezmo Dev" PWA identity and never the production service worker.
- /api/healthz + the Settings Details version row (short commit + build
  time). output: standalone in next.config (next start still works).
- AGENTS.md documents the workflow rules: only dev updates during a task,
  temporary dirs/ports for test builds, and deploy-at-the-end as the only
  production path. README gains an Environments and deploy section.
- Fix found by the rehearsal: lib/dev-mode.ts must not be a client module
  (a server component importing a constant from a "use client" module
  gets a truthy proxy, which baked dev metadata into production builds).
- SW -> freezmo-v34.
2026-10-11 19:25:29 +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
756a6257a2 Chat mode, phase 1: grounded streaming chat over your notes
Entry points (no sixth tab): Ask in the Library header (desktop button,
phone icon), Ask about this deck in the deck menu, Ask a question on
study/homework results (compact result travels as context), and a Chat
row in the desktop sidebar. Phones get a full-screen route with back +
New chat; the tab bar steps aside.

- Client-side retrieval: chunk scoped decks (~800 tokens), BM25 against
  the question + recent turns, top chunks under ~7k tokens, each
  labelled with deck + section. Notes come from the deck's stored
  sourceText (already kept for Regenerate); decks without it fall back
  to generated content and the UI says so.
- Stateless server: /api/chat validates against the CHAT_MODELS
  allowlist, trims the history to ~6k tokens, caps context at ~9k and
  streams token/thinking/done/error NDJSON. /api/chat/extract reduces
  attachments to text through the existing vision step.
- UI: user bubbles + full-width assistant text, streaming with stop,
  auto-scroll that pauses with a jump-to-bottom button, safe markdown
  (React nodes, no HTML injection; KaTeX dynamically imported only when
  a message has math), Thinking row for reasoning, copy/regenerate/
  edit-and-resend, model + context pickers (last model remembered),
  composer with visualViewport keyboard handling, safe-area, 16px
  textarea, Enter-to-send on desktop.
- Usage-limit failures keep the draft and show the shared message with
  Settings/Ollama actions; stopping maps the reader abort to a clean
  stopped state; offline disables sending with a reason.
- Settings: Keep notes text for chat toggle; deck menu: Delete notes
  text. RATE_LIMIT_CHAT + CHAT_MODELS in .env.example and README.
- Tests: chat suite (63 checks, stub Ollama) wired into run-all.
  Full set: 12/12 suites, 487 checks. SW -> freezmo-v32.

Also: tests/model-compare.mjs (diagnostic) benchmarks two models on the
same notes (latency, repair rounds, activity types) so the gpt-oss vs
gemma default can be decided on numbers; defaults unchanged.
2026-10-11 18:32:11 +00:00
2103f9fc69 Smart Review: instant loading feedback, no more dead-feeling tap
Reported: tapping Review due could sit for many seconds with no loading
screen. The flow itself is local and fast (counts are hydrated on the
library; <1.5s even at 6x CPU throttle with 3,000 answers), so the
visible fault was missing feedback: /review is a dynamic route and
until its payload arrived nothing on screen changed.

- LinkLoadingScreen: the review card links render a full-screen
  loading screen from the link's own useLinkStatus pending state, the
  instant the tap happens (measured 58 ms with the route request held
  open). The links also prefetch their payload.
- Route loading boundaries (loading.tsx) for /review, /stats and
  /exam, sharing a RouteLoading screen (app icon + spinner +
  localized message); ReviewClient's bare skeleton replaced with it.
- Health probes: per-key 30s cache + in-flight sharing in use-health.ts
  (every page mounts its own gate; a session now fires one probe
  instead of one per navigation). Manual re-check still forces.
- Czech fix: exam heading 'Zkouškové režim' -> 'Zkouškový režim'.

Verified by the rebuilt suites (the /tmp harness was wiped overnight):
review-loading 15/15 (stalled-request loading screen at 58 ms, session
arrival after release, fast normal path, mistakes path, end-to-end
grading, probe cache) and navigation 78/78 (tab bar, labels 360/390
EN/CS, routing, deep links, wizard loading lock). Lint + build clean.
SW freezmo-v30.
2026-10-11 14:35:16 +00:00
c662fb11f7 Lock the wizard mode switch while a run is generating
Reported: during a loading screen (freezing a deck) the Study deck /
Make my homework switch stayed live, so the run could be swapped out
mid-flight; a request that finished afterwards could even save a deck
the user never saw and navigate them into it.

- New lib/wizard-busy.ts: a tiny store both wizards publish to while
  phase === 'loading' (UploadScreen) or view.kind === 'loading'
  (HomeworkScreen); WizardModeSwitch hides itself while busy.
- Both wizards abort the in-flight request on unmount, so leaving via
  the header logo or the Back gesture stops the run instead of letting
  it finish invisibly and save/navigate later.
- Deck Cancel keeps its long-standing behaviour (wizard + cancelled
  notice + Retry), matching the freeze-loading suite.

Verified: wizard suite 92 checks (mode switch hidden during deck and
homework runs, it returns after Cancel, and a delayed reply after
leaving mid-run saves nothing and navigates nowhere) plus mobile
134/134 (also de-flaked the sheet's Escape close by waiting for
detach), regression 74/74, declutter 116/116, animation 8/8, native
35/35, i18n 25/25, phase1 38/38, phase2 43/43, phase3 20/20,
freeze-loading 6/6. Lint + build clean. SW freezmo-v29.
2026-10-10 17:47:05 +00:00
8557b04621 Navigation restructure: five-tab bottom bar, no phone drawer
Drawer removed (phones only; desktop rail untouched)
- MobileSidebar, sidebarOpen/useSidebarOpen, MenuIcon and the drawer's
  back-gesture layer are gone; the shared nav content now serves only
  the desktop rail (dead mobile branches deleted). No drawer-specific
  CSS existed; the three drawer strings (header.openMenu,
  sidebar.closeMenu, common.menu) and library.importAria are removed.
- The header drops the hamburger: logo + name on the left, the single
  theme chip on the right.

Five-tab bottom bar
- Library / Progress / New deck (prominent centre +) / Exam / Settings,
  in that order; Progress and Exam reuse the existing /stats and /exam
  screens, Settings keeps opening the dialog. Sliding pill, icon bounce,
  press feedback, re-tap-to-top and safe-area padding kept; every item
  is >=44px (72px wide at 360px).
- New short tab.* keys (EN + CS) so all five labels stay on one line at
  360px — 'Zkouškový režim' became 'Zkouška'. No ellipsis anywhere.
- The bar is mounted once in AppHeader so library, progress, exam and
  share get it; the wizards keep their own bottom action bar and the
  tab bar steps aside there, exactly as before.

Re-homed drawer destinations
- Smart Review card loses the Exam link (it is a tab now); Review due
  and Mistakes stay.
- New deck screen (and /homework) get a Study deck / Make my homework
  segmented switch; the library's homework empty-state button stays.
- The phone import icon becomes a ... button opening a sheet with
  Import JSON and New folder (back-gesture and Escape close it); folder
  chips still appear only once at least one folder exists, and folder
  creation stays in the New deck options step.

Verified: mobile suite 134 checks (no hamburger/drawer, tab order and
44px targets, label matrix at 360/390 x EN/CS x dark/light x reduced
motion, per-tab routing, deep links, Back from every tab, the ... sheet
with folder creation) plus regression 74/74, wizard 83/83, declutter
116/116, animation 8/8, native 35/35, i18n 25/25, phase1 38/38, phase2
43/43, phase3 20/20, freeze-loading 6/6. Key parity 740/740. Lint +
build clean. SW freezmo-v28.
2026-10-10 16:53:55 +00:00
4513364019 Phase 3: Check my work, follow-ups and turn-into-deck
Check my work (third homework mode)
- One model call with its own system prompt marks the student's own
  attempt: verdict, what's right, each issue with why + fix, and the
  corrected version that keeps everything the student got right.
  Unasked-for extras are never errors. The attempt is typed/pasted
  (photos of handwriting are documented as unsupported) and stored in
  the record so Redo re-checks the same work.
- Server: mode "check" + solution on POST /api/homework (zod, 400
  server.needSolution without one) and the same NDJSON stream; new
  checkHomework() pipeline with repair-once validation.

Follow-ups
- New POST /api/homework/refine (plain JSON, RATE_LIMIT_REFINE, zod,
  auth before body): shorter / more detail / explain, plain text back,
  language preserved.
- Shorter + More detail + Original rewrite the finished answer and the
  corrected version in place (FollowUpBar); every guide step has
  Explain step N differently; failed calls say so without losing text.

Turn into a study deck
- Any result hands its content (answer, corrected version or assembled
  guide) to New deck through a one-shot sessionStorage seed with the
  options step pre-filled.

UI
- Phone step 2 gains the third card: tapping it reveals the solution
  field + submit; desktop has three cards and the solution field shows
  with the mode. Homework history labels and share/card previews cover
  the new mode.

Verified: new phase-3 suite 20/20 against a stub Ollama (check payload,
missing-solution 400, refine validation, phone + desktop check flows,
shorter follow-up with restore, guide explain, deck hand-off) plus
regression 74/74, mobile 52/52, declutter 116/116, wizard 80/80,
animation 8/8, native 35/35, phase1 38/38, phase2 43/43, i18n 25/25.
Key parity 734/734. Lint + build clean. SW freezmo-v27.
2026-10-10 14:36:04 +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
4369ba1a17 Native-feel pass: PWA integration, system back, offline, haptics, undo
Mobile only (<=639px / coarse pointers); desktop keeps its behaviour.

System integration
- Manifest: id, display_override, monochrome icon, Make my homework
  shortcut and a share target (text/URL via GET, files via POST).
  iOS startup images for the common iPhone sizes. Pinch-zoom is allowed
  again (double-tap zoom is disabled via touch-action instead).
- Share target: /share chooser with New deck / Make my homework; POSTed
  files wait in a 10-minute in-memory store, reach the wizard through
  sessionStorage and never touch disk.
- Web Share: Share rows for decks (JSON file where supported) and the
  homework result, with a clipboard fallback and a toast.
- Offline: a quiet pill appears and Freeze / mode cards are disabled
  with a clear reason; input is never lost (error cards keep Retry).
- State: wizard drafts (text, options, photos up to a size budget)
  survive reloads and OS kills and are cleared on success; re-tapping
  the active tab scrolls to the top; the deck page shows a skeleton
  instead of a spinner.

Navigation and gestures
- System Back closes the topmost layer first: a small history-entry
  stack (lib/use-back-close.ts) covers the drawer, Settings sheet,
  install sheet, deck actions panel and one wizard step; navigations
  are never rewound and stale entries are skipped.
- Destructive-but-reversible actions use Undo toasts: deleting a deck
  (no confirm) and removing a file in the wizard. Irreversible ones
  keep their confirm.

Input and feedback
- Every phone input got inputmode / enterkeyhint / autocomplete /
  autocapitalize / spellcheck; Enter on the last field advances the
  wizard; a clipboard-paste button appears where the API exists.
- Haptics behind a Settings toggle (on by default): tab switches,
  toggles, correct/wrong, completion; Vibration API + the iOS hidden
  switch tick, throttled and silent everywhere else.
- Wake lock during the study feed and homework guide, released on
  leave and re-acquired when the tab returns.

Verified: new native suite 35/35 (manifest/head/viewport, offline,
share target GET + POST, back-gesture layers, undo, share fallback,
tab re-tap, haptics persistence, draft restore), regression 74/74,
mobile 52/52, wizard 80/80, declutter 116/116, animation 8/8,
i18n 25/25. Lint + build clean. SW freezmo-v23.

Needs a real device (documented in the README): iOS haptic tick,
system share sheet entry, wake-lock grant, iPhone startup images,
iOS edge-swipe back, install prompts. Pull-to-refresh and the app
badge were skipped on purpose.
2026-10-10 12:16:03 +00:00
e66141e9a0 Wizard polish: step headlines, gated Next, real summary, portal action bar
Mobile only (<=639px); desktop markup untouched.

Bug fix: the Back/Next bar was fixed-positioned inside the screen
entrance animation's transformed wrapper, so it painted under the content
and jumped to the viewport bottom about a second in. It is now portalled
to <body> and only fades in, pinned to the bottom from the first frame.
The other fixed/sticky elements (tab bar, drawer, sheets, install sheet)
were checked and live outside the animated wrappers.

- The step name is now the large headline with a one-line description;
  the repeated page title is dropped on phones. Thin progress bar + 1/3.
- Step 1: formats/limits hint, one big dashed Add photos or files area
  (plus icon), Camera and Paste text instead as secondaries, and a
  'nothing added yet' placeholder that becomes the thumbnail/chip list.
- Next is disabled until a photo, file or pasted text exists; step 3 can
  never be reached empty.
- Step 1 shows Cancel (back to the Library) instead of a dead Back.
- Step 3 is a real summary: thumbnail/chip strip plus Files, Difficulty,
  Activities and Folder rows, each with an Edit link to its step; Freeze
  is the single primary action.
- Step 2 got a visible Difficulty label and a helper line under every
  group, defaults still preselected.
- Bottom chrome: flat full-width Back/Next bar above the safe area, and
  the tab bar hides inside both wizards. The lower-right orb is hidden on
  phones so it can never glow behind the bar.
- Make my homework matches: headline, hint, big add area, placeholder,
  Cancel, gated Next, flat bar.

Verified: new wizard suite 80/80 (bar position sampled at 5 frames during
the entrance from tab bar/drawer/empty state; 360 + 390, dark + light,
EN + CS, reduced motion on/off; gating, summary Edit links, hidden tab
bar and orb), declutter 116/116, regression 74/74, mobile 52/52,
animation 8/8, i18n 25/25. Lint + build clean. SW freezmo-v22.
2026-10-10 11:35:45 +00:00
374752c59e Animation pass: one shared motion language for phones
Mobile only (<=639px / coarse pointers); desktop keeps its behaviour.

Foundation
- lib/motion.ts owns the spring presets (snappy/gentle/sheet), durations,
  easings and variants (screens, tabs, wizard steps, list stagger).
- Native-feel CSS: press feedback scales every button, card, chip, tab,
  row and summary to 0.96 with a spring-like cubic-bezier on transform
  (scoped to phones/coarse pointers), tap-highlight off, touch-action
  manipulation, chrome text non-selectable, reduced motion drops it.
- App shell fades in once on launch; the study feed fades in on phones.

Applied
- Tab bar: sliding active pill (shared layoutId) + icon bounce.
- Study Decks/Homework, language toggle, difficulty and size pickers:
  the same gliding highlight, gated so desktop keeps the old styling.
- Route content enters direction-aware (deeper = from the right, back =
  from the left, same-depth = crossfade) via RouteTransition on the
  library/create/homework content columns; library tab switches
  crossfade; wizard steps slide forward/backward (reduced motion safe).
- Library: cards fade up in a capped stagger on first load, glide with
  layout animations and shrink away on delete (AnimatePresence).
- Study feed: correct feedback pulses, wrong shakes, the progress bar
  settles with a spring curve, score and XP count up on the results.
- Settings sheet uses the sheet spring; the drawer now follows the
  finger and closes on release velocity, like the sheet.
- Generation progress: the stage message fades/slides between stages;
  the icon keeps its gentle wobble; the feed fades in on hand-off.

Skipped, on purpose: LazyMotion (the sliding highlights need layout
animations = domMax, same features the current import already has);
route exit animations (the App Router unmounts the outgoing page —
exits are handled by in-page AnimatePresence); replacing the study
feed's native scroll-snap with finger-follow drag (would fight the
platform scroller); finger-follow height animation for native <details>
expanders.

Verified: animation suite 8/8 (reduced motion + 4x-CPU-throttled trace:
no long tasks, CLS 0.0000), regression 74/74, mobile 52/52, declutter
116/116, i18n 25/25. Lint + build clean. SW freezmo-v21.
2026-10-10 11:01:29 +00:00
6564e88138 Mobile declutter pass: step flows, compact cards, simplified settings
Phones only (<=639px); desktop markup is untouched.

- New deck is a 3-step flow: progress (1/3 + bar) and a fixed Back/Next
  bar above the tab bar. Step 1 = Add photos or files / Camera / Paste
  text instead (no drop zone or paste copy on touch), step 2 = folder,
  difficulty and count with defaults plus one collapsed focus/notes row,
  step 3 = one summary line (photos/docs/text · difficulty · count ·
  folder) with Freeze. Going Back keeps every input; step transitions
  respect prefers-reduced-motion.
- Make my homework is a 2-step flow sharing the same pattern; tapping a
  mode card starts it (no separate CTA on phones), with a short preview
  of the assignment on step 2. The hero is now 'Make my homework' /
  'Udělej mi úkol' everywhere ('Solve your homework' is gone) and the
  intro is one line.
- Library: stats line removed, folder row and 'Unorganized' heading
  hidden until folders exist (New folder lives in the drawer), Import is
  a small icon button beside the tabs. Deck and homework cards are
  compact on phones: title, one meta line (activities · difficulty ·
  best), a one-line description that expands on tap, the whole card
  tappable and a small Study/Open pill instead of the full-width button.
- Settings sheet on phones: no .env.local subtitle; Language stays first;
  one Connection card (status dot + 'Connected · 2 models ready', key
  field with the eye inside, Save only when edited, auto re-check on
  save, refresh icon instead of Test connection); privacy one-liner with
  a More expander (daemon tip inside); raw health rows behind one
  collapsed Details list; Remove key as a small danger button at the very
  bottom (same confirmation).
- Decorative orbs on phones moved further off the screen edges so they
  never glow under cards or primary buttons.
- Shared markup for folder/difficulty/count and mode cards keeps one
  source of state; new lib/use-phone.ts gates the layouts.

Verified: new declutter suite 116/116 (360 + 390, EN + CS, photo + PDF +
pasted text, Back keeps input, summaries, settings flow), regression
74/74, mobile 52/52, i18n 25/25. Lint + build clean. SW freezmo-v20.
2026-10-10 10:43:32 +00:00
6fb11fa405 Grade against the question: stop requiring details the question never asked for
A short-answer case: "Why was Kryštof Harant executed?" with the student
answering "because he took part in the anti-Habsburg uprising" was failed
for not naming the year 1621 — a detail only present in the reference
answer, never asked by the question. The generator had put the date into
keyPoints, and the grader treated every key point as mandatory.

- Grading prompt: the QUESTION defines what is required. A key point
  counts only when the question asks for it or the answer is wrong or
  incomplete without it; extra detail in the reference answer (dates,
  places, names, numbers) is a bonus — never listed as missing, never a
  reason to fail. An accepted answer must return an empty "missing"
  array.
- Generation spec: short_answer keyPoints must contain only the ideas
  the question asks for (a "why" question requires the reason, not the
  year).
- gradeShortAnswer drops any stray "missing" entries when correct is
  true, so the UI can never show a gap on an accepted answer.

Verified: mock-Ollama integration test 9/9 through the real /api/grade
route (new rules sent upstream, incorrect answers keep their missing
point, accepted answers normalise to missing: []), regression 74/74,
mobile 51/51, i18n 27/27. Lint + build clean. SW freezmo-v18.
2026-10-09 18:13:29 +00:00
9d6ef90f02 Keep the library header steady across tabs; simplify the empty states
Header & action bar
- Import JSON no longer disappears on the Homework tab: it sits directly
  under the "Your icebox" header/subtitle, above the tab switcher, on every
  tab — so switching pools no longer shifts the layout or hides actions. It
  is one secondary button at all sizes now, not a quiet link when empty.
- The decks-only "Homework Solver" header action is gone; the homework tab's
  empty-state CTA and the drawer cover starting a new solve.

Empty states
- Dropped the "Using your own Ollama models — check the connection in
  Settings." line (and its two dictionary keys) everywhere.
- Both tabs share one structure: ice icon -> heading -> short description ->
  primary CTA button, starting at the same height:
    Study Decks: "No decks yet" -> "Freeze a page of notes into flashcards
    and quizzes." -> "Create study deck"
    Homework: "No homework yet" -> "Solved assignments and step-by-step
    guides show up here." -> "Solve homework"
- CTA labels read as actions on the standardized nouns; removed the now
  unused actionStudyDeck/actionHomework keys.

Tabs
- Link segments are 44px tall at every breakpoint (min-height in the base
  .segmented > a rule).

Service worker: freezmo-v11.

Verified: library-consistency suite 49/49 (Import visible with a fixed
position on both tabs, no Ollama text, empty states at the same y, primary
ice CTA buttons >=44px, deep links, deletion, redirects, desktop, 360px, axe
en+cs light+dark), mobile 51/51, i18n 27/27, desktop regression 74/74.
Lint + build clean.
2026-10-08 21:50:18 +00:00
8c44f47881 Separate Study Decks and Homework in the library; drop the /create mode switch
Library ("Your icebox")
- Two tabs, never mixed: Study Decks (the folder + card library) and Homework
  (saved solutions and step-by-step guides). The active tab lives in the URL
  (/?tab=homework), so the sections are linkable and survive a reload.
- Decks tab keeps Import, the folder bar, stats and its own empty state — now
  a single decks-only CTA.
- Homework tab lists saved results as cards (mode icon, date, step count,
  preview) with Open and delete-with-confirm; its empty state has its own CTA
  ("Homework Solver") that opens the solver directly. Opening a card
  deep-links to /homework?open=<id>, which the screen opens on arrival.
- .segmented now also styles link segments (same control as settings).

/create
- The redundant "Study Deck | Homework Solver" toggle is gone: /create is the
  deck uploader only, and the bottom bar's + goes straight to it. Homework has
  its own screen, reached from the library tab, the drawer and CTAs.

Copy
- New keys (en + cs): tabDecks, tabHomework, homeworkHeroTitle, plus homework
  empty-state/open/step-count strings. The homework hero is now "Solve your
  homework" / "Vyřeš svůj úkol" — no study-deck text in the homework flow.
- Removed the now-unused ModeSwitch component and homework.title key.

Service worker: freezmo-v10.

Verified: library-tabs suite 37/37 (tab switching, context empty states,
deep-link opening, deletion, no switch on /create, heroes, redirects, desktop
grid, 360px + 44px targets, axe contrast en+cs light+dark), mobile 49/49,
i18n 27/27, desktop regression 74/74. Lint + build clean.
2026-10-08 21:26:08 +00:00
461dd2ffcf Fix the mode-choice redundancy: standardized names, deep links, compact switch
Terminology (lib/i18n-dict.ts)
- One name per mode everywhere: "Study Deck" / "Studijní balíček" and
  "Homework Solver" / "Řešitel úkolů". The library empty-state cards and the
  /create switcher now read the same, so the choice no longer changes name
  between screens.

Deep links
- Library quick actions carry the mode: /create?mode=deck and
  /homework?mode=homework.
- Both pages canonicalize: /create?mode=homework redirects to the homework
  flow and /homework?mode=deck redirects to /create, so a mode link always
  opens that mode directly.

/create
- The two bulky square cards (106px tall, wrapping/clipping "Study Deck &
  Qu…" on phones) are replaced by a compact segmented control (52px) placed
  directly above "1. Add your notes": ice pill, snowflake/sparkles icons,
  44px segments and one-line labels at 360px. The pill slides to the chosen
  segment and the route follows ~190ms later, so toggling reads as one smooth
  transition (immediate under prefers-reduced-motion).

Service worker: freezmo-v9.

Verified: mode-switch suite 26/26 (deep links, cross-mode redirects, 52px
control, 44px segments, no clipping/wrapping at 360px, "1. Add your notes" at
y=394 of 740 with the upload controls above the fold, pill slide, axe contrast,
en+cs, phone+desktop), mobile 49/49, i18n 27/27, desktop regression 73/73.
Lint + build clean.
2026-10-08 19:38:02 +00:00