Add full-screen API-key gate

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).
This commit is contained in:
Martin Dvořák 2026-10-06 21:20:22 +00:00
commit dfa7b7502a
13 changed files with 505 additions and 94 deletions

View file

@ -84,6 +84,14 @@ no database, no data leaving your machine except the requests you send to Ollama
| `ordering` | Drag with the mouse/finger, or use ▲▼ buttons, then check |
| `short_answer` | Typed answer graded by the AI via `/api/grade`, with a self-check fallback |
**First run**
- A full-screen **API-key gate** replaces the app until a key is saved (in `user-key` mode): one
masked input with a show/hide toggle, a single **Continue**, and a "?" sheet with the four steps
to a free key. The key is validated with a real 1-token probe before it is accepted — invalid
keys and network trouble get different inline messages. Removing the key in Settings brings the
gate back; `server` and `local` installs skip it entirely.
**Library & folders**
- Decks saved locally with title, subject, folder, activity count, date, play count and best score
@ -144,7 +152,7 @@ build too.
Open <http://localhost:3207>. The app runs without any Ollama configuration — you'll just need a
key (or a signed-in daemon) before it can generate a deck. Complete one of the Ollama setups below,
then paste the key in **Settings**.
then paste the key on the gate (or later in **Settings**).
`npm run dev` and `npm run build` also copy the pdf.js worker into `public/` automatically (see
`scripts/copy-pdf-worker.mjs`), which is what lets the browser parse PDFs.
@ -172,7 +180,7 @@ Who provides the API key is controlled by **`OLLAMA_AUTH`**:
| `OLLAMA_AUTH` | Key comes from | Use it when |
| --- | --- | --- |
| **`user-key`** (default) | each browser, pasted in the settings dialog | several people/devices share the app, or you don't want a key on the server |
| **`user-key`** (default) | each browser, pasted on the key gate (or in Settings) | several people/devices share the app, or you don't want a key on the server |
| `server` | `OLLAMA_API_KEY` in `.env.local` | a single-user install |
| `local` | nothing — a daemon signed in with `ollama signin` | you run Ollama locally |
@ -185,9 +193,10 @@ VISION_MODEL=gemma4:31b-cloud
TEXT_MODEL=gpt-oss:120b-cloud
```
1. Open the app → a banner asks for a key → click **Add API key** (or the ⚙️ button).
2. Create a key at <https://ollama.com/settings/keys> and paste it in.
3. **Test connection** shows whether Ollama accepted it, and which models are available.
1. Open the app — the **key gate** asks for a key on first visit.
2. Create one at <https://ollama.com/settings/keys>, paste it in and press **Continue**.
3. The gate validates it with a real 1-token request, then drops you into the icebox.
**Settings → Test connection** re-checks any time and shows the model list.
Where the key lives:
@ -530,7 +539,7 @@ components/
activities/ one component per activity type + shared shell
Flashcard.tsx MultipleChoice.tsx TrueFalse.tsx FillBlank.tsx
Matching.tsx Ordering.tsx ShortAnswer.tsx shared.tsx index.tsx
UploadScreen.tsx StudyFeed.tsx ResultsScreen.tsx DeckCard.tsx ApiKeyBanner.tsx
UploadScreen.tsx StudyFeed.tsx ResultsScreen.tsx DeckCard.tsx ApiKeyGate.tsx
TabBar.tsx InstallHint.tsx ServiceWorkerRegistrar.tsx FolderBar.tsx
LibraryScreen.tsx SettingsDialog.tsx AppHeader.tsx ThemeToggle.tsx icons.tsx ui.tsx
lib/
@ -627,7 +636,7 @@ regeneration and “add more”.
| A document card is stuck on “Reading page …” | The pdf.js worker may be missing: run `npm run dev`/`npm run build` (they copy it to `public/pdf.worker.min.mjs`) or `node scripts/copy-pdf-worker.mjs` manually. |
| PDF uploaded but no activities about its content | The PDF is probably image-only (a scan). Freezmo renders such pages and sends them to the vision model — check the card note (“N scanned pages were turned into images”); if it says “No readable text”, the scan may be too low quality. |
| Deck came back in English even though the notes are not | Make sure both model calls succeed (a failed vision step falls back to raw text). The prompts force the notes' language; if you still see translation, switch `TEXT_MODEL` to a stronger instruction-follower such as `gpt-oss:120b-cloud`. |
| “Add your Ollama API key first” | Bring-your-own-key mode is active. Paste a key from <https://ollama.com/settings/keys> in the ⚙️ dialog — it is stored only in your browser. |
| “Add your Ollama API key first” | Bring-your-own-key mode is active. Paste a key from <https://ollama.com/settings/keys> on the key gate (or in the ⚙️ dialog) — it is stored only in your browser. |
| “Ollama rejected this API key” | The key is invalid or revoked. Create a new one and paste it again; the check uses a real 1-token request, so a green tick means it truly works. |
| “this model is not included in your free usage” | The key works, but the model is outside your plan. Use one of the ✅ models in the table above, or add credits at <https://ollama.com/settings>. |
| “Too many requests — try again in Ns” | You hit a rate limit (12 decks / 40 grades per 5 min by default). Wait, or raise `RATE_LIMIT_*` in `.env.local`. |
@ -745,6 +754,12 @@ Things I chose while building, with the reasoning:
density) flags a mismatch, the text model rewrites the JSON back into the original language
before generation. Verified both ways: it fires on a synthetic English-but-declared-Czech
transcription and never fires on real Czech notes (no extra calls, no added latency).
28. **The first screen is the API-key gate.** In `user-key` mode a browser without a key gets a
full-screen, monochrome gate instead of the dashboard: one masked input, one Continue button,
and a "?" sheet with the steps to a free key. The gate reuses the cached 1-token probe through
`/api/health`, so an invalid key and an unreachable Ollama are worded differently. The old
dashboard banner is gone; key management stays in Settings, and removing the key brings the
gate back. The gate covers `/`, `/create` and deck study, but never `/offline`.
14. **`gpt-oss:120b` does all content work — but it cannot read images** (`this model does not
support image input`), so `VISION_MODEL` has to stay a multimodal model (`gemma4:31b-cloud`).
Text-only inputs — pasted text, PDFs with a text layer, DOCX/PPTX/ODT — never touch the vision