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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
- "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.
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.
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.
"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.
- 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.
- 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.
- 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Library empty state ("Your icebox")
- Replace the single "Make your first deck" button with two quick-action
cards: "Create interactive test" (primary, ice-gradient icon + subtle
accent border) and "Help me with my homework" (glass card, sparkles
icon). Stacked on phones, side by side from 640px up; both >=44px tall.
/create
- Replace the "Got homework instead of notes?" banner with a two-card mode
switcher above "1. Add your notes": "Study Deck & Quizzes" (the current
view — aria-current, accent tint, ice icon) and "Homework Solver" (a
shortcut to /homework). Icon-on-top tiles on phones, inline rows from
640px up; glass tokens + backdrop blur in both themes.
Strings
- New keys in both dictionaries: emptyStateCreateDeck, emptyStateHomework,
actionStudyDeck, actionHomework. Drop the now-unused homework.entry and
library.makeFirst keys.
Drive-by AA fix (light mode, /create only)
- The "New folder" dashed button and the difficulty blurbs move from
frost-500 to frost-600 (4.0:1 -> ~6:1). Dark mode is unchanged.
Service worker: freezmo-v8.
Verified: redesign suite 32/32 (stacked/side-by-side layouts, 360/390/1280,
dark+light, en+cs, >=44px targets, both cards navigate, no overflow, axe
contrast), mobile 49/49, i18n 27/27, desktop regression 73/73. Lint + build
clean.
Homework (/homework — entry points in the drawer and on the create screen)
- Assignment intake reuses the study pipeline: pasted text, photos
(camera/picker, compressed in the browser) and documents (PDF/DOCX/PPTX/
ODT/text) through lib/image + lib/documents, the same vision step and the
same language guard. Raw files never leave the device — only text and JPEG
data URLs reach the server.
- Two large choice cards: “Just make it” returns the finished homework
(answer only, plain text, copy + make it again); “Explain how to make this
homework” builds an interactive guide.
- The guide is one step per screen in the study-feed snap layout:
explanation, three progressive hints (nudge -> hint -> the answer) and an
optional checkpoint rendered by the existing activity components (graded
through /api/grade). The last screen assembles the solution from the step
answers, with a copy button. Checkpoints carry xp 0 so the guide stays free
of XP/streak chrome.
- Results live in freezmo.homework.v1 with a small history (open/delete);
“Make it again” re-runs from the stored assignment text (the vision
transcription when photos were used) — no original files are stored.
- The answer and the guide are written in the assignment's language, never
translated into the UI language.
Server
- POST /api/homework: NDJSON stage events like /api/generate, the same
http-guard, auth checked before parsing, and its own RATE_LIMIT_HOMEWORK
(documented in .env.example + the README table).
- New prompts in lib/prompts.ts and zod schemas in lib/schemas.ts with the
same single repair retry; TEXT_MODEL for content, VISION_MODEL only for
photos. New shared client pieces: useNotesIntake, lib/homework store,
lib/clipboard, and a shared NDJSON reader in lib/client-api.
Also finishing the mobile polish pass from the previous request
- Phone ice scene: the top-left orb moved behind the glass header (it sat
behind the page heading/subtitle). The settings sheet's scroll cue is now a
bare chevron inside a content fade (mask) instead of a chip over the status
banner, and the API key card's note/disclosure/actions are tightened.
- theme-color now follows the in-app theme (SSR metas still fall back to the
OS preference); apple-mobile-web-app-status-bar-style stays “default”
because iOS fixes the status-bar text colour at launch.
Verified: 78/78 homework checks (text/photo/PDF, both modes, EN+CS, dark+light,
360/390px, copy, history persistence, storage privacy, API language, axe), plus
the existing suites re-run (mobile 47/47, pixel contrast 0 below AA, i18n 27/27,
desktop regression 72/72). Lint + build clean; service worker freezmo-v6.
Phones (<=639px) only for layout/gestures; the theme-level fixes apply
everywhere, as they are token/design changes.
Settings sheet
- Swipe-to-dismiss reusing the InstallSheet gesture: the sheet follows the
finger, closes past ~90px or on a fast flick, snaps back otherwise. Drag
starts only from the handle/header and only while the body is scrolled to
the top. Backdrop tap still closes.
- Fixed header + scrollable body: pb-safe + spacer, so nothing is clipped;
a scroll cue (chevron) fades in while more content is below and out at
the end.
- Show/Save stack under the field at 44px on phones; close, Test connection
and Remove key are 44px too. Remove key is now a rose destructive action
with a confirmation that the full-screen key setup comes back.
- API key note split into a short privacy line plus a collapsible
'Using the local daemon?' tip (OLLAMA_AUTH=local / .env.local / signin).
- Fixed the missing space in the English tip ('ollama signin instead.').
- Health: kept the auto-check and banner; added a loading row and a
'Checked just now' / 'Checked at HH:MM' freshness label (also for errors).
Ice scene / readability
- Verified the orbs were never above content (fixed z-index:-1 layer,
pointer-events:none); they were just shining through the glass. Phones get
lower blob/orb opacity and orbs tucked to the edges; the bottom-right orb
is moved out from under the tab bar and the tab bar paints a more opaque
surface so its labels hold contrast.
Contrast / themes
- Dark --text-subtle raised to #9db1d0 (8.6:1 page, 7.2:1 glass); helper
text now uses the tokens.
- Disabled buttons are a visible muted chip (.btn-disabled) instead of a
40%-opacity ghost.
- One ice primary gradient in both themes; danger/success now actually show
their colour (bg-none strips the primary background-image that used to
paint over !bg-rose-600).
- Light mode: cool card rims instead of white-on-white, stronger shadow.
Redundancy / touch
- Removed only the header gear; the hamburger drawer (folders, Settings,
theme) and the tab-bar Settings stay.
- Theme toggle is a bordered chip with an action aria-label (drawer row too).
- Empty library hides the folder pills, demotes Import to a quiet link and
keeps a single primary CTA. 44px targets for nav rows, segmented control
and folder pills on phones.
- Fixed a pre-existing 16px horizontal overflow (header -mx-4 vs a
padding-less parent) and let info values wrap instead of truncating.
Verified: mobile suite 47/47 (EN+CS, 360/390px), pixel-sampled contrast
audit 0 runs below WCAG AA (8 runs, both themes/locales; axe marks the glass
'incomplete'), i18n 27/27, desktop regression 72/72, lint + build clean.
Service worker cache bumped to freezmo-v5.
English stays the default; Czech is a full translation. Only the app chrome
is translated - notes and generated decks keep their own language, which is a
core feature, not a setting.
i18n
- lib/i18n-dict.ts: flat en/cs dictionaries (441 keys each), translate() and
translatePlural() with real Czech plural rules (1 sada / 2-4 sady / 5+ sad).
English is the default and the fallback for a missing key.
- lib/i18n.ts: browser store (freezmo.locale.v1), useI18n() hook, and the
x-freezmo-locale header. The hook's t/plural are bound to the locale React
is rendering with, so the first client render matches the server HTML and
then re-renders in the stored language; module-level t is only for event
handlers and non-React helpers.
- <html lang> follows the choice (inline script + setLocale).
UI
- Settings -> Language segmented control (English / Cestina), plus an EN/CS
button on the key gate for first run.
- Every screen translated: sidebar, header, tab bar, library, deck cards,
folder bar, create flow, all seven activities, study chrome, results,
settings + health panel, API-key gate and help, install sheet, offline and
the deck-not-found page.
- Plural-aware counts everywhere (decks, folders, activities, runs, photos,
documents, pages, words, wrong attempts).
Server
- API routes and guards (generate, grade, health, http-guard, rate-limit,
ollama error mapping) answer in the request's language, so error cards and
the health panel never fall back to English mid-sentence.
Fixes
- A hydration mismatch when a non-default locale was stored silently dropped
the .dark class after hydration; render-time t() is now bound to the
hook's locale (UploadScreen.describeExtraction, ResultsScreen.answerFor,
DeckCard's folder fallback).
- Bumped the service worker cache to freezmo-v4 for this deploy.
Tests: i18n 27/27 (gate toggle, persistence, html lang, Czech plurals,
settings switch) and a rebuilt regression suite 72/72 (English default,
library, create, all 7 activity types, results, settings, install sheet,
offline, PWA, security, Czech smoke).
Ordering cards
- Generation prompt now forbids dates, years, times, step numbers and
ordinals in ordering items (spec + hard rules), and the server-side
validation drops any ordering card whose items still contain them
instead of shipping a giveaway.
True/False
- The 'false ✕' swipe label was permanently visible on the card: its
opacity curve was inverted, so the clamp at rest was 1. Flipped the
range (and tightened both ranges for the elastic drag) — each label now
only fades in while swiping in that direction.
Regenerate deck
- Results screen gains a Regenerate deck button: rebuilds the activities
from the deck's stored source notes, replaces the deck in storage
(attempts/best score reset) and starts the fresh run from slide 1.
- StudyFeed tracks the current deck so the header, export and messages use
the regenerated title/activities.
- Fixed a spurious 0% attempt recorded when a regenerated deck swapped in:
scroll anchoring held the old results position, so the feed briefly
reported 5/5 with zero answers. The scroll now resets before the new
slides render, overflow-anchor is disabled on the feed, and a run with
no answers is never recorded.
Tests: new deck-fixes suite 15/15 (TF overlay at rest/drag, regenerate
with loading state + storage reset, ordering guard drop/keep); retry
14/14, deck chrome 15/15, pwa 16/16, final-ux 12/12, security 8/8,
contrast 20/20.
Steps
- iOS now uses the exact flow: Safari users get 4 steps (⋯ → Share/More →
Add to Home Screen → Add); non-Safari (CriOS/FxiOS/EdgiOS/in-app) get a
first 'Open this page in Safari' step and 5 total. The vague 'in some
browsers it's under…' wording is gone.
Chips and icons
- The ⋯ chip renders a single three-dot icon (icon-only pill) instead of
the icon plus an '⋯' character (which showed two sets of dots).
- The More chip uses a chevron-down icon instead of the rounded-square.
- The accent pill uses real iOS blue: #0A84FF dark / #007AFF light, still
the only coloured element.
- Steps and chips share one 16px type scale; chip labels are medium weight.
Typography and layout
- Numbers are small muted circles in a fixed 24px column, aligned with the
first text line via matching line-height.
- Removed trailing periods after chips; steps tightened.
- Bottom padding is calc(24px + env(safe-area-inset-bottom)) and a
secondary 'Maybe later' button sits below the steps.
- max-height 90dvh with inner scrolling; 36×4px low-contrast handle with
real swipe-down-to-close (momentum off, spring back).
- App card shows the hostname only (no port), truncated.
Themes
- Dark (default): layered surfaces #1c1c1e sheet / #2c2c2e card / #3a3a3c
chips with 8% white borders; subtle logo tile instead of solid white;
backdrop is 50% black with 4px blur.
- Light only when the app theme was explicitly set to light, ignoring the
system preference; borders #e0e0e0, chip fills #f2f2f2.
Also fixes a crash the test pressure exposed: a 429 (rate-limited)
/api/health response crashed Settings at health.vision.available.
fetchHealth now rejects non-OK/malformed responses so the dialog shows its
error banner instead of the app error screen.
Install-sheet suite now 64 checks; pwa/final-ux/security/contrast green.
- New InstallSheet mounted in the root layout: appears ~1s after load on
iOS/Android phones and tablets, over any screen including the key gate,
with a dimmed backdrop. Never on desktop, never in standalone mode, and
at most once per 7 days after a dismissal (localStorage).
- Platform/browser detection (lib/install.ts): iPhone/iPad incl. iPadOS
reporting as Mac with touch, Android, Safari vs Chrome vs other vs
in-app browsers, with step 1 adapted accordingly.
- iOS: numbered steps with chips (Safari, Share, ... , More, Add to Home
Screen) and the accent Add pill. Android: captured beforeinstallprompt
turns into a native Install button; fallback steps use Install app /
Add to Home screen chips and an accent Install pill.
- Close via X, backdrop, Escape or swipe-down on the handle; the site is
untouched afterwards. Anchored to 100dvh so browser toolbars never hide
the last step; pb-safe respects the home indicator.
- Dark sheet by default; light only when the app theme was explicitly set
to light. Settings gains a mobile-only Install app row that reopens it.
- The inline head script captures beforeinstallprompt before hydration and
records appinstalled. Old InstallHint banner removed.
- Tests: new install-sheet suite (45 checks across iPhone Safari/Chrome/
in-app, Android Chrome/Samsung, desktop, prompt path, dismissal, 7-day
expiry, standalone, Settings row, both themes); pwa suite updated.
In user-key mode a browser with no stored Ollama key now gets a polished
gate instead of the app shell: one masked input with a show/hide toggle,
a single Continue button, inline validation errors, and a "?" help sheet
with the steps to a free key.
- Validates the draft key through /api/health (real 1-token probe) before
saving; invalid-key and network failures read differently.
- Key still lives only in this browser's localStorage; nothing server-side.
- Gate wraps /, /create and deck study; server-key and local-daemon modes
skip it; removing the key in Settings brings it back.
- Removes the now-redundant dashboard/create API-key banner.
- Monochrome corner glows (dark near-black / light near-white), 24px card,
48px field and button, gentle fade/slide-in, reduced-motion aware.
- Help modal: centred dialog on desktop, bottom sheet on mobile, closes via
X, Escape or backdrop.
- README and browser suites updated (34 gate checks, 15 key-flow checks,
12 shipped-default checks; ui/folders/docs/pwa/security/contrast green).
Next.js 15 app that turns photos, PDFs, DOCX/PPTX and text into a swipeable
feed of AI-generated activities (flashcards, multiple choice, true/false,
fill-in-the-blank, matching, ordering, short answer) using the user's own
Ollama models.
Highlights:
- All AI calls run in server route handlers; the browser never talks to Ollama.
- Bring-your-own-key mode (default): the key lives only in the browser and is
sent per-request; server, local-daemon and shared-key modes also supported.
- Client-side document extraction (pdf.js, mammoth, jszip) with scanned-PDF
page rendering for the vision model; notes stay in their original language.
- ChatGPT-style shell: collapsible sidebar / mobile drawer, neutral token-based
light + dark themes, folder organisation, PWA (manifest, SW, offline page).
- Strict JSON validation with zod + auto-repair, rate limiting, and request
guards (cross-origin, body-size, API-key sanitising).
- setup.sh bootstraps deps, .env.local and a production build.
Gitignore excludes .env*, the generated pdf.js worker and local scratch files.