- 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.
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.
- Orbs rise from below the bottom edge to above the top edge, each with
its own 36-68s pass (slower on phones), a negative delay (scene already
populated on load), own horizontal position, a gentle sway and a slight
scale drift; opacity fades in/out at the edges so the loop never pops.
- Only transform + opacity animate, each orb on its own composited layer
(will-change: transform); no filter/backdrop-filter, nothing animates
size/offsets/shadow. The glacier blobs are untouched.
- Opacity lowered ~38% in both themes: 0.55->0.34 light, 0.8->0.5 dark,
phone overrides 0.32->0.2 / 0.42->0.26.
- prefers-reduced-motion: rise removed, orbs return to the old static
placements (a paused-from-start CSS animation is not applied in Chrome
and would park them below the fold); tab hidden: a visibility listener
parks the whole scene (.ice-paused), blob drift included.
- Measured with a CDP perf trace on a 4x throttled phone profile: p95
33.4ms, 0 frames over 34ms, no long tasks - identical to the pre-change
baseline; ~0.6% extra main-thread CPU. Desktop stays compositor-bound
from the pre-existing blob drift, as before. tests/bubble-perf.mjs
keeps it repeatable.
- New bubbles suite (34 checks) in the repo harness; the axe contrast
matrix is now 16 runs (360/390/1280 x dark/light x EN/CS) and passes.
- SW cache -> freezmo-v31; README decision 46.
Full suite: 11/11 suites, 424 checks.
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.
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.
The last visible difference between the two CTAs was the container: the
deck button lived in a full-bleed sticky strip (translucent background,
hairline top border, its own padding), which also tinted the disabled chip
differently. The homework button sits plainly in the page flow.
- The CTA block is now a plain space-y-2 stack at the end of the form:
full-width button + centred helper line, exactly like /homework.
- /create's page padding switches from the sticky-era .pb-cta (9rem, phone
override 4.6rem) to .pb-tabbar, and lg:pb-20 to lg:pb-16 — identical to
the homework page. The now-unused .pb-cta rules are gone.
- Result: same button width (358px @390 / 688px @1280), height, radius,
helper placement and trailing space on both pages; neither has a sticky
ancestor any more.
Service worker: freezmo-v16.
Verified: CTA-flow suite 26/26 (below-the-fold at rest, no sticky ancestor,
full-width, helper centred underneath, width/height/radius/trailing-space
parity at 390px and 1280px), desktop regression 74/74, mobile 51/51,
i18n 27/27. Lint + build clean.
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.
On phones the "Freeze my deck" strip floats one tab-bar height (4.6rem)
above the viewport bottom, but the page reserved env + 9rem of trailing
room — so at the end of the scroll the strip docked ~70px higher than where
it had been floating, leaving a visible cavity above the tab bar.
Phones now get exactly the sticky offset (env + 4.6rem) as trailing space:
the strip settles where it was floating and the bar is never double-counted.
Tablet/desktop (>=640px) keep the roomier 9rem — unchanged.
Verified at 360/390px: gap CTA-to-bar 70.3px -> 0.3px, ~70px less dead
scroll; tablet 800px and desktop 1280px measurements identical to before.
Service worker: freezmo-v7.
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).
Rebuilds the look around the spec: an ice scene, one shared glass
recipe, icy accents and glass on every large surface. Visual only —
layout, behaviour, copy and functionality are untouched.
Background
- .ice-scene: fixed, z-index -1, pointer-events none. Navy base
(#070b16 to #0c1426) in dark, pale (#f4f9ff to #e6f1fb) in light,
four drifting glacier blobs (#38bdf8/#60a5fa/#818cf8, 30-60s
transform-only loops), five glossy ice orbs (40-220px radial
gradients with a highlight at 30% 25% and a blue shadow) placed so the
glass blurs them, and an SVG feTurbulence grain at 4-5%.
- prefers-reduced-motion stops the drift; phones get two blobs and two
orbs fewer.
Glass
- One shared recipe (.glass-panel, .card, .sidebar-surface,
.study-chrome, .tabbar, .glass, .gate-card): rgba(255,255,255,.06)
dark / .55 light, blur(24px) saturate(140%) (16px on phones), a bright
1px rim, an inset top highlight and a blue-tinted outer shadow.
- Nested glass (chips, inputs, secondary buttons) is more opaque and
never adds a second blur layer. @supports fallback for no
backdrop-filter. Radii: 24-28px cards/sheets, 14px inputs/buttons,
pills for chips. Cards hover brighter + cyan rim + 2px lift, 180ms.
Colour and type
- Accent is glacier cyan (#38bdf8) to ice blue (#0ea5e9); progress bars
and selected states use the gradient; the active sidebar item is a
frosted highlight with a cyan rim.
- Primary CTA: icy gradient — white to #bfe3ff with ink text in dark,
deep blue in light (biased so the label sits on the darker half and
keeps 5.6:1).
- Leftover slate/black greys replaced with navy-tinted frost tokens.
- Headings are semibold with tighter tracking.
Screens
- Dashboard: the sidebar is one tall inset rounded glass panel; deck
cards are glass.
- Gate: one centred glass card over the scene, orbs visible behind it.
- Sheets/modals: glass over the dimmed, lightly blurred backdrop.
- Study view: question card more opaque (.glass-solid); top bar and
pager are slim glass pills.
Perf: measured on a phone viewport with 4x CPU throttling. The animated
blobs' filter: blur() was the whole cost (150ms vs 33ms per frame), so
they are soft radial gradients with no filter now; idle 60fps, ~17ms
median while scrolling.
Tests updated for the new tokens: contrast 18/18 AA, deck chrome 15/15,
key gate 49/49, install sheet 64/64, deck fixes 15/15, retry 14/14,
zoom 6/6, final ux 12/12, security 8/8, pwa 16/16.
The colourful blob wash (92fe685) and the flat "straight ice" pass
(5d05592) both missed. Reverting both to get back to the icon-driven
navy palette before rebuilding the look as a proper frosted ice glass
design.
The colourful multi-blob wash was too much and still didn't read as
frosted glass. Replaced with a cleaner material approach:
- Backdrop is now flat with a single faint top highlight (the only
gradient left in it); the animated blobs and the four colour radials
are gone.
- Panels are crisper and more translucent: cards 62%, panels 66%,
sidebar 64% (now flat, no vertical gradient), study chrome 68%, tab
bar 70% — with a bright 1px edge (--glass-edge), a light-from-above
sheen (--glass-sheen) and the inset top highlight.
- Light mode is lighter: page #eef3fa pale ice, sidebar and cards
frosted white; dark mode is a flatter, slightly lighter navy
(#121a2c page, #1a2337 surfaces).
- Key-gate glows toned right down to match.
Contrast re-audited against the effective frosted backgrounds: 21/21 AA.
Suites: deck chrome 15/15, deck fixes 15/15, final ux 12/12, key gate
49/49, install sheet 64/64, retry 14/14, zoom 6/6, security 8/8, pwa
16/16.
The wash was far too faint for backdrop-blur to register, so the glass
read as flat navy. Now:
- The backdrop is a real colour field: four saturated blue/periwinkle/
cyan radial blobs over the navy base, plus two slow animated blobs at
higher opacity and less pre-blur, placed behind the content column.
- Surfaces are properly translucent instead of near-opaque: cards 62%,
glass panels 68%, sidebar 68%, list groups 70%, study chrome 72%,
tab bar 74%, mobile header 62%.
- Glass gets a specular top edge (inset 0 1px 0 --glass-top) so the
surface catches light like real glass.
- Light --text-subtle darkened to #55647f so text still clears AA on the
tinted glass (4.84:1 on the most saturated sidebar area).
Audit updated to the tinted backgrounds: contrast 21/21. Suites: deck
chrome 15/15, deck fixes 15/15, final ux 12/12, key gate 49/49, install
sheet 64/64, retry 14/14, zoom 6/6, security 8/8, pwa 16/16.
The icon's artwork is now the source of truth for the palette:
- frost-* is a navy-tinted neutral scale and accent-* is the icon's
snowflake blue; light page #f4f7fd / sidebar #e9eff9 / border
#dce4f1, dark page #0e1424 / sidebar #0a0f1c / surfaces #171f33.
- Surfaces are frosted glass: .card, .glass-panel, .list-group and
.sidebar-surface are translucent with backdrop-blur/saturate, and the
page wrapper no longer paints an opaque --page, so the fixed navy/blue
backdrop reads through the sidebar, cards, chrome and sheets.
- The primary CTA is the icon's blue (#1d61ab in light, #59a8f7 in dark
with ink text).
- The gate stage, fields and glows are navy/blue; viewport theme-color
and the manifest background/theme colour are #0e1424.
- The old monochrome snowflake tile is gone from the brand: sidebar,
header, key gate and empty state now show the real app icon (the
install sheet already did). Decorative snowflakes stay.
- --surface-strong is finally defined (it was referenced but never
declared, so those inputs had no fill).
- Icon URLs carry ?v=2 so browser favicon caches refetch.
Updated the contrast audit (21 pairs) and the deck-chrome token
assertions to the new theme. Verified: contrast 21/21, deck chrome
15/15, key gate 49/49, install sheet 64/64, final ux 12/12, retry
14/14, zoom 6/6, security 8/8, pwa 16/16, deck fixes 15/15.
Source artwork committed as assets/app-icon-source.jpeg (1024x1024 navy
rounded square) and scripts/make-icons.mjs rewritten to render every icon
from it with headless Chromium:
- The master is masked at full resolution first (a rounded rect 4px inside
the artwork's own radius), so the JPEG's anti-aliased white edge never
survives and small sizes downscale with clean alpha.
- PWA icons (192/512) and favicon-32 keep the rounded square with
transparent corners; apple-touch 180/167/152 are full-bleed (iOS paints
transparency black) with the artwork zoomed 20% to crop its corners;
maskable-512 keeps the artwork inside the 80% safe circle on a matching
navy field.
- iOS splash screens regenerated with the new mark.
- app/favicon.ico regenerated (PNG payload in an ICO container); the old
app/icon.svg removed so the browser tab can't fall back to it.
- Install sheet's app card now shows the real icon, so the home-screen
icon is recognisable.
- Service worker CACHE_VERSION bumped to freezmo-v3 so installed clients
pick up the new assets.
- README updated (source artwork + script description).
Verified: pwa 16/16, install sheet 64/64, final ux 12/12, key gate 49/49,
zoom 6/6; every icon route serves 200 and the manifest/head reference the
new files.
Layout (desktop / lg+ only):
- 40px top padding above the page title.
- Deck grid uses minmax(320px, 1fr), so two cards fill the content width
and share a right edge with the header (same max-width container).
- Group headings only render once real folders exist; with none, a single
quiet "All decks" label replaces "Unorganized".
- Import JSON becomes a plain text button (mobile keeps the pill).
Deck cards (desktop):
- The whole card is a link; Study is a compact secondary button instead
of a full-width solid pill.
- The ... control is a 32px ghost button with a real ellipsis icon,
aligned to the title's centre.
- Meta line always shows the same fields (activities · runs / Not played
yet); the score is a separate quiet "Best 53%" stat.
- 12px rhythm between title/meta/tags/summary, 16px before the actions.
- Hover: lighter surface, stronger border, soft shadow and a 2px lift in
light mode (pointer devices only, so touch has no sticky hover).
Sidebar (desktop rail):
- All decks + Unorganized are views above the FOLDERS label; only
user-created folders sit under it.
- Count only on All decks. The mobile drawer is unchanged.
Theme:
- Dark: sidebar is a step darker (#181818 to #141414 vs #212121 page).
- Light: cards get a slightly stronger border (#e2e2e2) and a soft
shadow; the sidebar divider and edge shadow are stronger too.
- Medium difficulty darkened to #7a5300 (6.2:1 on the light chip).
Mobile is untouched: layout changes are behind lg: (or the desktop prop);
theme tweaks are shared tokens.
Verified: key-gate 49/49, install sheet 64/64, retry 14/14, zoom 6/6,
deck chrome 15/15, deck fixes 15/15, final ux 12/12, contrast 20/20,
security 8/8, pwa 16/16.
- API-key gate is now a fixed, out-of-flow overlay (inset-0, overflow
hidden, overscroll-none) with its own internal scroller, so the document
has no scrollable height on phones. It can't be panned or bounced.
- Home/create mains are flex columns with min-h-dvh and a flex-1 content
area, and the empty state's mobile padding was trimmed, so a short/empty
library is exactly viewport height instead of overflowing by ~76px.
Long pages (the create form, decks) still scroll normally.
- html/body get overscroll-behavior: none to stop pull-to-refresh and
rubber-banding on fitting screens.
Verified with a scroll probe (gate and empty library: scrollHeight ==
viewport; create still scrolls) plus zoom 6/6, install sheet 64/64,
pwa 16/16, final-ux 12/12, retry 14/14, security 8/8, contrast 20/20.
- Viewport: maximum-scale=1 and user-scalable=no (app-like, no pinch or
double-tap zoom on phones).
- Mobile-only rule keeps every input/textarea/select at 16px so iOS
Safari's focus auto-zoom never triggers (it ignores user-scalable=no for
that). Deliberately unlayered so it beats the per-component Tailwind
sizes; desktop keeps the designed 12-14px fields.
New zoom suite (6 checks): viewport meta, gate input, settings key input,
notes textarea and every study-feed field at >= 16px on a phone, desktop
sizes unchanged. Install sheet 64/64, pwa 16/16, final-ux 12/12,
security 8/8, contrast 20/20.
- The study feed's progress header and bottom pager no longer use the old
navy glass: new .study-chrome class uses the neutral surface token
(same gray as the cards, #2f2f2f in dark), the neutral border
(8% white in dark, #e5e5e5 in light) and a neutral shadow.
- Close tile, streak pill and XP pill now use --surface-sunken /
--text-muted, matching the app's other buttons and chips; the close
glyph is the shared XIcon.
- Bottom pager: counter in --text, arrows in --text-muted.
- .glass (mobile header) no longer has the hardcoded #101720 navy; it is
now color-mix of the surface token. Removed the unused --color-night-*
tokens and the last night- usage (create screen sticky bar).
- Hidden the 'Swipe up' hint on desktop, where it collided with the
bottom pager.
New deck-chrome suite: 15/15 (computed surfaces/borders/text in both
themes, no navy); 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.
- Disabled Continue is now the primary style at 45% opacity with
cursor: not-allowed; full opacity while validating and once text is
entered.
- Move "How do I get a key?" below the Continue button (small ? icon,
muted, underline on hover) and tighten input+button spacing.
- Shorten the subtitle to "One step to freeze your first deck." and
balance it.
- Replace the faint gradient with two real blurred blobs (top-right and
bottom-left, 120px blur, pointer-events: none, behind the card).
- Dark: page #0f0f0f, card #1a1a1a, 8% white border.
- Phone: 24px heading, 15px subtitle, wider card padding, 40px corner
buttons with safe-area-aware offsets, robust 100dvh centering.
- Modal: 480px wide with text-pretty steps, stronger tinted free-tier
callout, handle removed from the phone sheet.
- Tests: gate suite now 49 checks incl. disabled opacity, glow pseudo,
card separation and help-link placement; new state screenshots for
invalid/offline/validating in both themes.
- Add a theme toggle to the gate (sun/moon ghost button next to the help
button); it switches instantly and persists to localStorage.
- Strengthen the corner glows so the stage reads atmospheric in both themes
(bigger, slightly stronger radial gradients behind the card).
- Center the logo, heading, subtitle and footnote inside the card.
- Dark mode: replace the solid white logo tile with a subtle raised tile
(thin border, light snowflake); make the card only slightly lighter than
the near-black page with a low-opacity border and soft shadow.
- Disabled Continue keeps the primary fill at ~45% opacity with a crisp
label instead of a faded muddy one; fully solid once text is entered.
- Bigger 36px corner buttons and a visible "How do I get a key?" text link
under the input that opens the same modal.
- Shorten and balance the reassurance line.
- Help modal: step circles aligned to the first text line, emphasised
free-tier callout with a check icon, underlined links with an
external-link icon, muted medium-weight UI labels, 36px close button
aligned to the title.
- Gate test extended to 44 checks (theme toggle, text link, modal polish).
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.