A card row whose stored JSON no longer satisfies the current models made
GET /cards return 500 and hid the owner's entire library. `Card.blocks` is
a discriminated union over a fixed vocabulary, so a block with an
unrecognised `type` raised ValidationError inside `to_card()`; because
`list_cards` built every row in one comprehension, one stale row took down
the whole response. Blocks are LLM-authored and rows written by earlier
schema versions live in the database indefinitely, so this is expected data,
not corruption.
`to_card()` now degrades past unreadable values and logs each one with the
card id:
- blocks are validated individually (new `sanitize_blocks`); invalid ones
are dropped and valid siblings survive, so a partly-stale card still reads
- unknown state / failure_reason / content_type fall back instead of raising
- primary_action, action_items and extraction fall back to defaults;
insight, being optional by design, drops to None
New `to_card_or_none()` backs …
upload_file + to_media_url now emit owner-checked proxy paths; legacy absolute HF URLs and local scratch paths are normalised to the same scheme so pre-migration cards still resolve after the repo goes private. ApiClient.mediaHeaders attaches the cached bearer token to proxy URLs only; wired into CardFace.
Anonymous users get a backup nudge (Google sign-in + optional legacy-library claim); Google users get an account row; quota meter under Library; logout now signs out of Firebase too.
feat: bearer-token transport with single 401 refresh-retry
LocalAiService interface + Gemma impl via flutter_gemma (MediaPipe LLM
Inference, Android-only, feature-gated elsewhere). Download managed by the
plugin (progress, retries, foreground service); model is HF license-gated so
the download dialog collects an HF token. Degrade path: quota_degraded card
-> fetch /bundle -> structure on-device -> POST /structure -> card upgrades
in place, with a 'Generating on your phone' share state. Profile gains an
Offline AI section (download / enable / delete, honest copy). All failures
silent — the paragraph card is never made worse.
Rework Present mode so every screen change is preceded by a visible cursor
landing and tap on the real widget, and follow the guided narrative
(home → card → concepts → catalog → nav bar → graph/chat → add).
- PresenterController: point→tap→act gesture choreography; navigation routed
through a visible tap on the nav control; rewritten kTour script; speech
watchdog so a missing web 'end' event can no longer hang the tour.
- PresenterSpotlight: hold the target through the 150ms geometry-poll jitter
(fixes the snapping glide); always render the cursor; add a tap ripple.
- AgentBus: onLibraryTab / onOpenCatalogItem hooks.
- Screens register real tap targets (nav tabs, top icons, plus, first card,
reader blocks/insight/references/action-bar); Concepts & Catalog now flip
the real library tab instead of a full-screen swap.
- .gitignore: scope the bare 'presenter' rule to /presenter/ so the Flutter
feature dir is tracked (it never was) while the root prototype stays ignored.
- Switch google-generativeai -> google-genai so Gemini preprocessing
works (import was broken, raw noisy bundles were hitting Cerebras)
- Add structuring prompt rule to discard OCR garbage
- Add Dockerfile (HF Spaces Docker SDK)
- Various backend API and Flutter app updates