Changelog

Every release of Kerokero Reader, with its date — 33 releases and counting. The app ships frequently, and every change is recorded here.

v0.10.6

Released 2026-08-05
Bug Fixes
  • Spawn local run_processor when Cloud Run Jobs aren't available (#15, 03569e4) _trigger_processor_job() was silently failing when VERTEX_PROJECT_ID wasn't set (local dev, CI): google.auth.default() couldn't find credentials and the Cloud Run Jobs API call 404'd. The function just printed the error and returned — the queued ProcessingJob was never picked up, so text processing hung forever. Now when VERTEX_PROJECT_ID is absent, _trigger_processor_job() spawns a local subprocess running instead. Same job-based architecture (ProcessingJob row → background worker), just a local worker instead of a Cloud Run one. Co-authored-by: Claude <[email protected]>

v0.10.5

Released 2026-08-05
Bug Fixes
  • Wrap ProcessingJob.objects.create with sync_to_async in async views (#14, e4aff04) Three bare synchronous ORM calls in async def functions were causing 'SynchronousOnlyOperation: You cannot call this from an async context' errors when users submitted text input, link-source URLs, or navigated pages. Each is now wrapped with sync_to_async() matching the pattern already used for every other ORM call in these same functions. Co-authored-by: Claude <[email protected]>
Continuous Integration
  • Run system checks against PostgreSQL, tests against SQLite (#13, 063a2f1) * fix: make sidebar logo clickable and rename brand-indigo to brand-primary
  • Wrap sidebar logo and brand name in anchor tag linking to index page (respects ja/zh theme via zh_theme flag) - Rename CSS variable --color-brand-indigo to --color-brand-primary throughout codebase, since the color is parameterized per language (indigo #2D2A60 for Japanese, jade #0E6B4F for Chinese) and 'indigo' was misleading - Update all template class references: text-brand-indigo → text-brand-primary, bg-brand-indigo → bg-brand-primary, etc. - Rename legacy alias --color-text-on-indigo → --color-text-on-primary - Rebuild compiled tailwind CSS - Update comments in tailwind.css, pdf_export.py, and templates * fix: repair staff user in system check instead of failing * ci: run manage.py migrate in CI to catch migration and system-check errors The previous deploy failure (E002 system check blocking migrations in production) passed CI because manage.py check runs without a database and the check gracefully passes when the DB/table doesn't exist. Adding manage.py migrate --noinput to CI means: - Django system checks fire against a real database - Migration errors (broken FK, missing deps) are caught before deploy - Any system check that depends on DB state gets validated Uses SQLite via DATABASE_URL env var (same as local/dev) so no PostgreSQL service container needed. * ci: use PostgreSQL 18 service instead of SQLite for migrate check Production runs on PostgreSQL, so SQLite can mask PG-specific issues (constraints, index types, transaction behavior). Spin up postgres:18 as a service container and run migrate against it. * ci: use PostgreSQL 18 service for tests Django's test runner already creates a PG test database and runs all migrations — no separate migrate step needed. The separate step was failing because PostgreSQL can't CREATE DATABASE from a template that still has idle connections from the migrate step. Running tests against PostgreSQL instead of SQLite catches PG-specific issues (constraint types, index behavior, transaction semantics) that the production deploy would otherwise hit first. * fix: make conn_max_age configurable, set to 0 in CI * ci: drop separate check step, test runner already validates * fix: use template1 for test DB creation in CI PostgreSQL refuses CREATE DATABASE ... TEMPLATE <db> when there are open connections to <db>. In CI, Django's test runner connects to the configured DB for introspection before creating the test copy, causing 'database is being accessed by other users' errors. With TEST.TEMPLATE=template1, Django copies the built-in PostgreSQL template instead of the configured DB. template1 has no connections and is always available. * fix: use template0 (pristine, no connections) for CI test DB * ci: try postgres:17 instead of :18 * ci: PG system check + SQLite tests to avoid TEMPLATE deadlock GitHub Actions PostgreSQL service container prevents Django from creating test databases via CREATE DATABASE TEMPLATE due to idle connections from the container's own health-check/bootstrap processes. Split approach: - System checks run against PostgreSQL to catch data-dependent checks - Tests run against SQLite (default fallback when DATABASE_URL is unset) Also cleaned up the template0/conn_max_age settings.py additions. * chore: clean up unused TEMPLATE config, keep CONN_MAX_AGE env var --------- Co-authored-by: Claude <[email protected]>

v0.10.4

Released 2026-08-05
Bug Fixes
  • Repair staff user in system check instead of hard-failing (E002) (#12, 00014e2) * fix: make sidebar logo clickable and rename brand-indigo to brand-primary
  • Wrap sidebar logo and brand name in anchor tag linking to index page (respects ja/zh theme via zh_theme flag) - Rename CSS variable --color-brand-indigo to --color-brand-primary throughout codebase, since the color is parameterized per language (indigo #2D2A60 for Japanese, jade #0E6B4F for Chinese) and 'indigo' was misleading - Update all template class references: text-brand-indigo → text-brand-primary, bg-brand-indigo → bg-brand-primary, etc. - Rename legacy alias --color-text-on-indigo → --color-text-on-primary - Rebuild compiled tailwind CSS - Update comments in tailwind.css, pdf_export.py, and templates * fix: repair staff user in system check instead of failing --------- Co-authored-by: Claude <[email protected]>

v0.10.3

Released 2026-08-05
Bug Fixes
  • Make sidebar logo clickable and rename brand-indigo to brand-primary (#11, a3a549a) 🤖 Generated with Claude Code

v0.10.2

Released 2026-08-05
Bug Fixes
  • Critical privilege escalation via spoofable staff email (7e56fbb) The self-healing staff signal added in #3 (models.py _enforce_single_staff) granted is_staff/is_superuser to ANY User row on save if its email matched STAFF_EMAIL -- including brand-new rows. Since ACCOUNT_EMAIL_VERIFICATION='none' and register_view auto-logs in immediately after signup, anyone could register at /register/ with [email protected] (their own chosen password, zero verification) and get an instant, attacker-controlled superuser account -- a separate DB row from the legitimate post_migrate-created account, since EmailUserCreationForm's duplicate check compares against `username`, not the post_migrate account's literal username='kerokeroreader'. Fix: the signal now only self-heals (re-grants) on updates to an *existing* row (instance.pk is not None) -- never on creation. The real designated account already exists via the post_migrate handler; self-healing only needs to cover that row losing its flags later. Also reject the reserved email at the registration form level as defense in depth. Added tests/test_staff_security.py covering both the attack path and the legitimate self-heal case.

v0.10.1

Released 2026-08-05
Bug Fixes
  • Proactively grant staff to designated email, fix XSS in changelog (#3, 1707841)
  • pre_save signal now grants is_staff+is_superuser when email matches STAFF_EMAIL, not just stripping it from others (self-healing on save) - post_migrate handler ensures staff user exists and has correct flags on every deploy, catching existing users who signed up before this code - System check now verifies the designated email IS staff (E002), not just that others aren't (E001) - Fix XSS in changelog.py _linkify: validate URL scheme before inserting into href attribute (only http/https allowed) Co-authored-by: Claude <[email protected]>

v0.10.0

Released 2026-08-05
Features
  • Parameterized frog logo SVG with dynamic coloring (#10, 8b4a262) Replace fixed PNG logos with a single inline SVG that uses currentColor, inheriting the text color from its container. This means the logo automatically adapts to the page theme:
  • Sidebar (admin/staff dark backgrounds): white logo - Sidebar (regular / zh_theme): brand-indigo logo, auto-resolves to jade on Chinese pages via CSS custom property overrides - Login, Register, About, Index pages: brand-indigo logo The SVG is inlined via a reusable include template (templates/partials/logo_svg.html) so every page picks up the same parameterized asset. The SEO schema logo and PDF export keep the PNG fallback since they require raster formats. Co-authored-by: Claude <[email protected]>
Refactoring
  • Separate landing page from input tool (#7, 0928e04) * refactor: separate landing page from input tool Move the SEO-rich marketing content (hero, features, screenshots, pricing, FAQ) from the index page to dedicated landing pages at /welcome/ and /zh/welcome/. The base path / is now the reading tool input form directly — visitors land on the tool, not a wall of marketing content.
  • New: landing.html template with all marketing/SEO content - New: landing() and landing_zh() views - New: /welcome/ and /zh/welcome/ URL routes - Simplify: index.html stripped to input form + quick reads only - Remove: index_redirect() — / renders index() directly - Update: sitemap, robots.txt, lang-switch pairs for new routes - Update: all SEO tests point at /welcome/ instead of /ja/ * fix: remove leftover palette-preview route with no template /palette-preview/ referenced text_processor/palette-preview.html, which doesn't exist anywhere in this branch -- hitting the route would 500 with TemplateDoesNotExist. Unrelated to this PR's actual scope (landing/input separation); looks like cruft carried over from sibling exploratory work on the Chinese theme palette. --------- Co-authored-by: Claude <[email protected]>

v0.9.3

Released 2026-08-05
Bug Fixes
  • Enable BuildKit for Cloud Build deploy (15f4b48) gcloud builds submit --tag uses Cloud Build's legacy docker builder, which doesn't support the --mount=type=cache syntax the multi-stage Dockerfile (#6) relies on -- every deploy since has failed at the Build image step with "the --mount option requires BuildKit". Switch to --config=cloudbuild.yaml, which sets DOCKER_BUILDKIT=1 on the docker build step. Verified locally with `docker buildx build .`.

v0.9.2

Released 2026-08-05
Performance Improvements
  • Multi-stage Dockerfile with BuildKit cache mounts (#6, 4d1ce6f) * perf: multi-stage Dockerfile with BuildKit cache mounts
  • Multi-stage build keeps build-essential (~200MB) out of the runtime image. - BuildKit cache mounts for apt and uv — near-instant rebuilds when deps don't change. - Single COPY --from=build --chown=app:app eliminates the 1.7GB layer bloat that a separate chown -R pass produces on an 890MB .venv. - Export time 33% faster (88s vs 132s) thanks to fewer layers. * perf: multi-stage Dockerfile, lazy NLP loads, drop nagisa, gunicorn --preload
  • Multi-stage build: build-essential (~200MB) only in builder stage, not runtime. - BuildKit cache mounts for apt and uv — near-instant rebuilds when deps don't change. - Single COPY --from=build --chown=app:app avoids 1.7GB chown layer bloat. - Export 33% faster (88s vs 132s). Image: 1.61GB → 1.41GB. - Removed nagisa (unused, 46MB) — cascaded removal of numpy, cython, dynet38 (~122MB additional savings). - Sudachi, jamdict, and jieba imports deferred to first use — Django startup stays fast (no 40s dict load at import time). - gunicorn --preload: master loads once, workers share dict memory via COW. - Executors lazy-initialised per-process to survive gunicorn --preload fork. * fix: add --preload to gunicorn CMD * fix: restore version 0.9.1 clobbered by stale branch base This branch was cut before the 0.9.0/0.9.1 version bumps landed on main, so pyproject.toml/uv.lock still pointed at 0.8.3. --------- Co-authored-by: Claude <[email protected]>

v0.9.1

Released 2026-08-05
Bug Fixes
  • Wrap ORM calls in async _process_async/navigate_page with sync_to_async (#4, bdc953d) ProcessingJob.objects.create() and StagedUpload queries were called directly from async context, triggering Django's SynchronousOnlyOperation guard. Wrapped with sync_to_async / asyncio.to_thread respectively.

v0.9.0

Released 2026-08-05
Chores
  • **seo**: Allow crawlers to read sitemap and public pricing/auth endpoints (e3e5558) robots.txt allowlist gains /sitemap.xml (explicit, alongside the Sitemap: directive), /upgrade/ (the public pricing page, linked from the homepage and About) and /login/ + /register/ (public auth entry points). Per-user/session routes remain blanket-disallowed, now asserted by test. Full suite 60 green.
Features
  • **seo**: Add content-bearing homepage with FAQ, pricing and screenshots (a1c6e25) Keyword-bearing H1s (Read Real Japanese/Chinese with Instant Translations) with the upload tool still above the fold and functional. Below the tool: how-it-works (3 steps), features (6 cards), real reading-screen screenshots, pricing (3 tiers -> /upgrade/) and a FAQ rendered from the same Python list that feeds FAQPage JSON-LD, so visible copy and structured data cannot drift. Word counts: /ja/ 643, /zh/ 608 (audit target >=600). See .specs/features/seo-homepage-content/.
  • **seo**: Add public Quick Reads catalog and 301 redirect for broken sample links (6d478d3) Broken /quick-reads/<id>/ links (un-flagged, deleted, or flagged with zero pages) now 301 to the new /quick-reads/ catalog instead of 404ing — the one stable URL for the rotating sample set, sitemap'd, with an empty state and side-effect-free GET. The public page's back link points at the catalog. 301 (not 302) so Google consolidates dead sample signals onto the catalog; noted in a comment to switch to 302 if re-flagging the same document becomes a normal rotation move. 9 new tests; full suite 59 green. See spec follow-up R9-R11.
  • **seo**: Add public read-only Quick Reads catalog and changelog page (47c9172) Read-only /quick-reads/<id>/ pages: a plain GET with zero side effects (no Document clone — verified; the interactive clone lives behind the explicit 'Open in reader' CTA), server-rendered text, translation and vocabulary with JLPT/HSK level badges (level_label_for), correct per-document <html lang>, and unique meta descriptions. Homepage cards link to the crawlable pages; QuickReadSitemap registered; robots.txt allows /quick-reads/. Public /changelog/ page renders the semantic-release CHANGELOG.md as dated, versioned entries (parser escapes-then-linkifies commit links). The index/index_zh views pass SoftwareApplication + FAQPage JSON-LD, and public_quick_read uses level_label_for instead of the nonexistent Document.level_label. See .specs/features/public-quick-reads/ and seo-trust-content/.
  • **seo**: Add technical indexation basics — meta, canonical, JSON-LD, OG, lang (7ba7d4c) Every public page now carries search identity: unique meta descriptions, absolute self-referencing canonicals, JSON-LD (Organization + WebSite everywhere; SoftwareApplication on the index pages), full OG/Twitter tags with a branded 1200x630 og-image, and a correct per-route <html lang> (ja/zh/en) that preserves the existing translate-prompt suppression. SEO head context lives in text_processor/seo.py (JSON built in Python, never by hand in templates) behind a context processor. See .specs/features/seo-indexation-basics/.
  • **seo**: Expand About page for trust and E-E-A-T (80b0caa) About grows from ~68 to 441 words: what the product is, how it works, language support (furigana + JLPT N5-N1, pinyin + HSK 3.0), plans, and a Resources section with the first real outbound links on the site (Aozora Bunko, NHK News Web Easy, jlpt.jp, jisho.org, Japanese Wikipedia — all genuinely used by the product). Links to the changelog. Testimonials deliberately deferred until real user quotes exist. See .specs/features/seo-trust-content/.
Testing
  • **seo**: Add SEO test suite, verification screenshots and project state (e776519) 30 SEO tests covering M1-M4: meta/canonical/JSON-LD/OG/lang on every public page, FAQPage schema matching rendered copy, word-count targets, public quick reads (no-clone on GET, 404 for unlisted, sitemap/robots entries, card links) and the changelog/About pages. Verification screenshots in screenshots/ (m1-m4 captures). STATE.md documents the four milestones, deviations and external follow-ups.

v0.8.3

Released 2026-08-05
Bug Fixes
  • Add missing cancel_at_period_end migration to resolve 500s for logged-in users (65b80f5)

v0.8.2

Released 2026-08-05
Bug Fixes
  • Correct paid plan names from premium/enterprise to credits/unlimited (7f742d1)

v0.8.1

Released 2026-08-05
Bug Fixes
  • Use getattr for cancel_at_period_end to avoid crash on missing column (3698237)

v0.8.0

Released 2026-08-04
Bug Fixes
  • Correct migration dependency (0026 doesn't exist on main) (50c0405)
  • Remove broken SEO imports, fix CI (ba476c3)
Documentation
  • Add SXO audit report and SEO remediation roadmap (59a884c) SXO audit of kerokeroreader.com (Gap Score 17/100, domain not indexed) plus the four-milestone remediation plan (M1 indexation basics, M2 content-bearing homepage, M3 public quick reads, M4 trust/E-E-A-T) as spec-driven feature docs.
Features
  • Add ProcessingJob model + migration for Cloud Run Jobs (4163b84) ProcessingJob replaces daemon threads as the async processing mechanism. The web service creates a queued row and triggers a Cloud Run Job; the job container runs manage.py run_processor, picks up the oldest queued row, processes it, and writes the result back. The client polls /progress/<task_id>/ which reads the existing progress_tracker (DatabaseCache) unchanged.
  • Cloud Run Jobs for async processing (c8aeccc)

v0.7.1

Released 2026-08-04
Bug Fixes
  • Prevent path traversal when restoring StagedUpload to disk (0e16973) The StagedUpload fallback was writing to a path constructed from the client-supplied file_path JSON field. A malicious client could send file_path=../../../etc/cron.d/evil and the write would land outside MEDIA_ROOT. Now the restore path is reconstructed from MEDIA_ROOT + the stored filename, ignoring the client-supplied directory.

v0.7.0

Released 2026-08-04
Bug Fixes
  • Add Gemini timeout, LLM concurrency cap, and Google Translate length guard (ead2fd9)
  • Gemini client now has a 120s per-call timeout (was unbounded). - format_components() fans out one LLM call per sentence; capped at 5 concurrent via asyncio.Semaphore to avoid saturating the thread pool and hitting Vertex rate limits. - _translate_paragraph_async() now guards against inputs >5000 chars that would cause easygoogletranslate to call exit(0), killing the gunicorn worker.
  • Add StagedUpload model for cross-instance file transfer, fix system check crash (56d83d8) StagedUpload bridges the gap between Cloud Run instances: /upload/ saves the file to both local disk and this table; /process/ falls back to the DB copy when the local file is missing (different instance). Also fixes _check_single_staff to gracefully handle missing auth_user table during before migrations have run (CI).
  • Configure gunicorn workers, threads, and timeout for Cloud Run (766d716) Defaults (1 sync worker, 30s timeout) were a poor match for a Cloud Run service that runs LLM calls and background processing threads. Bumped to 2 workers x 4 threads to handle concurrent requests without queuing behind slow I/O, and 600s timeout to match Cloud Run's own request deadline.
  • Enable no-cpu-throttling, set concurrency and timeout on Cloud Run (bca0a0d) --no-cpu-throttling keeps the CPU allocated between HTTP requests so background processing threads (run_async_in_thread) don't stall. --concurrency=8 matches the new gunicorn capacity (2 workers x 4 threads). --timeout=600 matches gunicorn's timeout.
  • Fall back to StagedUpload when uploaded file is missing from local disk (1f64c5b) When Cloud Run routes /upload/ and /process/ to different instances, the local file path from upload_file doesn't exist on the processing instance. Now _process_async retrieves the file from the StagedUpload table and recreates it locally, then cleans up the DB row.
  • Read app version from pyproject.toml at request time on about page (a30ed13) settings.APP_VERSION is a module-level constant read once at Django startup, so it goes stale if pyproject.toml is bumped without a server restart (e.g. by a release process). Read the version fresh from disk on every about-page request, falling back to settings.APP_VERSION if the file is unavailable.
Chores
  • Remove Japanese subtitle from My Account heading (432c99d)
  • Sync uv.lock version to 0.6.0 (7477fde)
Features
  • Add grammar page-range filter synced with vocab source state (815b1ec) Client-side grammar filtering now mirrors the vocabulary source filter: - Adds page-range From/To inputs to the grammar popover (disabled until a document is selected, same as vocab's source popover). - Adds data-grammar-page attributes to grammar-row-item elements so applyGrammarFilter() can show/hide rows by page range. - syncGrammarSourceInputs() keeps both vocab and grammar page inputs in lockstep whenever either side changes the document or page range. - Removes the legacy dual-thumb slider label (vocab-page-range-label); the From/To number inputs are now the primary page-range UI. Vocab page-range inputs now use total document pages (from #vocab-meta) instead of the list of pages that actually contain vocabulary, so the bounds stay fixed per document regardless of search/filter narrowing.
  • Add pre_save signal and system check to enforce single staff/superuser (d78698a) Only [email protected] may hold is_staff / is_superuser. The pre_save signal catches any .save() call (admin, shell, API); the system check catches .update() queries that bypass signals by failing hard on every manage.py invocation if an unauthorized user somehow ended up with elevated privileges.
  • Pass document total page count to vocab results fragment (a70831d) Adds doc_total_pages (the selected document's total page count) to the AJAX vocab-results context and renders it in the #vocab-meta element so the client-side JS can rebuild page-range inputs with the full document range even when the result set is empty (previously the meta div was inside the {% if entries_page %} block and disappeared with zero results). Also moves the #vocab-meta div outside the conditional block so page-range bounds and total counts remain accurate on empty result sets.

v0.6.0

Released 2026-08-04
Bug Fixes
  • Jlpt level lookup mismatching same-spelling words with different senses (e8a30fa) _load_index() built one flat dict keyed by both kanji spelling and reading via setdefault, so a kanji spelling with multiple dictionary senses at different levels silently collapsed to whichever sense's entry was iterated first (N1 files before N5) -- e.g. every lookup for 前 came back N1 (its ぜん/prefix sense) regardless of context, even though its far more basic まえ/"before" sense is N5. The reading-scoped keys had the same problem one level further removed: two unrelated words sharing a reading (e.g. 地殻/近く, both ちかく) could "donate" a level to each other. Now keys primarily on the exact (word, reading) pair, with a word-only fallback that only reports a level when every reading of that word agrees on one. Added refresh_jlpt_levels to recompute already-stored VocabularyEntry.jlpt values using the fixed lookup, since they're set once from the first occurrence and never overwritten.
  • Replace source-page dual-thumb slider with from/to number inputs (1ba43f1) The range control's JS still referenced the old two-thumb <input type=range> implementation, but the markup had already moved to plain number inputs (id=vocab-page-from/-to) -- so the range control was non-functional. Rewrites rebuildPageRangeSliders/onRangeInput into rebuildPageRangeInputs/onPageRangeChange to match the number-input markup: clamps out-of-range values, swaps from/to if reversed, and falls back to the document's full page span when either is empty.
  • Show credit balance on My Account for users with paid credits (7650df4) Previously anyone not on the Unlimited plan was labeled "Free", even if they had purchased credits. Now shows "Credits - N credits" when nav_credits > 0, and reserves "Free" for the true zero-credit case.
Chores
  • Add markdown table export helper for vocabulary entries (49fda89) _vocab_md formats entries as a markdown table (word/reading/definition/ level/sources) for a future markdown export option; not yet wired to the export view.
Features
  • Add branded PDF export for My Learning (b113155) Adds a reportlab-based PDF export alongside the existing txt/csv formats, with embedded Noto Sans JP/SC fonts (so CJK text renders and subsets correctly), a branded header/footer with the Kerokero Reader logo and a clickable kerokeroreader.com link, and formatted vocabulary/grammar tables matching the current filter selection.
  • Add Chinese (HSK) vocabulary support (8ddcd8b) Extends VocabularyEntry with an hsk level field and a language field (ja/zh, backfilled from each entry's own occurrences for rows that predate the field) so My Learning can split into separate ja/zh decks, plus a per-account "hide vocabulary below this level" default threshold for both JLPT and HSK.
  • Add Chinese-theme logo variant (0b35eeb) Sidebar, About, and the homepage now swap in a zh-specific logo mark when zh_theme is active, matching the existing Japanese branding.
  • Add My Vocabulary & Grammar (word/grammar tracking + My Learning page) (fa0850f) New VocabularyEntry/VocabularyOccurrence models sync from Page.components at write time (vocabulary_sync.sync_vocabulary_for_page, called from every place a page's components get persisted), aggregating every word and grammar note a user has ever encountered across all their documents. Surfaced on a new My Learning page, linked from a teaser card on My Account, with deep links back into the reader (results.js gained applyDeepLinkFromQueryString to open the right tab and flash the right word/grammar note from a ?word=&paragraph= or ?grammar=1&paragraph= URL). See .specs/features/my-vocabulary-grammar/ for the full design writeup and .specs/project/STATE.md for the two dedup/deep-link bugs caught and fixed during implementation.
  • Add SEO basics and Google sign-in (3650a42) robots.txt/sitemap.xml (via django.contrib.sitemaps, keyed off the real kerokeroreader.com domain rather than the default Sites row) and Google OAuth sign-in through allauth, with a login-redirect adapter that lands users on their preferred language home instead of always the same LOGIN_REDIRECT_URL.
  • Brand export filenames and add site link to markdown export header (a29f273) Downloaded exports are now named kerokero-<language>-vocabulary-<date>.<ext> instead of learning-<lang>.<ext>, and the markdown export's header links to kerokeroreader.com like the other export formats already do.
  • Track per-occurrence word senses to surface divergent definitions (6d5d27d) A word can mean different things in different sources (heteronyms, distinct senses under the same dictionary form) -- previously the first occurrence's reading/definition silently won everywhere, so a later, genuinely different sense was lost the moment the entry already existed. VocabularyOccurrence now records its own reading/definition as resolved for that specific paragraph, populated going forward by sync_vocabulary_for_page and backfilled for historical rows from their entry's own values (migration 0022, best available approximation). My Learning's "+N sources" popover and CSV export surface these per-occurrence values when they diverge from the entry's own.
  • Wire up markdown export for My Learning (14599f1) Adds 'md' as a valid export format alongside txt/csv/pdf and an Export: MD button next to the others; compacts the export button labels (PDF/TXT/CSV/MD under a single "Export:" label) since there are now four of them.

v0.5.1

Released 2026-08-03
Bug Fixes
  • Narrow My Account's default-language preference to the root redirect only (f6b4874) Corrected scope, per direct user feedback after the previous commit shipped: "default language should mostly just tell which language the redirect goes from the root route" -- the previous implementation had instead fed the preference into sidebar.html's shared nav_is_zh signal, affecting New Reading/My Library/About link destinations and the lang-switcher pill sitewide, which was broader than intended. Reverted context_processors.py/sidebar.html back to their original, purely path-based nav_is_zh (identical to before the My Account feature). Implemented the actual, previously-missing behavior instead: index_redirect (views.py) unconditionally redirected '/' to Japanese regardless of any preference -- it now checks the logged-in user's UserSubscription.default_language and redirects to index_zh only for that specific case. Anonymous visitors and 'ja'-preference users see byte-identical behavior to before this feature existed. Verified live via the Django test client: anonymous and 'ja'-preference users still land on /ja/, a 'zh'-preference user lands on /zh/, and the sidebar's New Reading link on a neutral page (About) is confirmed unaffected by the preference. Full test suite green throughout (11 tests, OK).

v0.5.0

Released 2026-08-03
Documentation
  • Record the Cloud Run outage investigation and fixes in STATE.md (8c93fff) Two real, currently-live production bugs found and fixed this session (memory limit + broken progress tracking, see the two preceding commits) plus the Cloudflare domain-mapping issue investigated and handed back to the user. Recorded per this project's existing spec-driven state-tracking convention.
Features
  • Add My Account page (password, preferences, usage history, delete) (9a6576a) New logged-in-only "My Account" section, reachable from a new sidebar nav item. Specified via two rounds of clarifying questions (see .specs/features/my-account/context.md) since the original ask was too vague to build directly.
  • Account status: identity (username/email/joined), Google-link status (read-only), plan/credits snapshot, link out to the existing Subscription page (kept separate, not folded in). - Change password (Django's PasswordChangeForm), hidden in favor of a "You sign in with Google" note for accounts with no usable password (has_usable_password() False). update_session_auth_hash keeps the session valid across the change. - Reading preferences: default language (ja/zh) and default page size, new fields on UserSubscription (reused rather than a new model). Page size is threaded into chunk_text_into_pages in process_document_async; language is folded into sidebar_status()'s existing nav_is_zh signal as a fallback ahead of the path-based one, so every template usage picks it up for free. - Usage history: read-only list from the existing LLMUsageLog model. - Delete account: full cascade delete via Django's existing CASCADE FKs (UserSubscription, every Document -> every Page) -- confirmed explicitly with the user this should be a real wipe, not an orphan-the-documents soft delete. Real bug caught by the existing test suite during implementation: the planned sidebar template change used `{% if ... (nav_default_is_zh or nav_is_zh) %}` -- Django's {% if %} doesn't support parentheses grouping, which broke every page (test_index_view/test_root_redirects_to_ja both failed immediately). Fixed by combining both signals directly in sidebar_status() instead, so nav_is_zh itself already carries the preference -- zero template logic changes needed beyond a clarifying comment. Verified live via the Django test client, including the destructive path checked directly against the DB (not just the HTTP response): password change survives session/works on next login, preferences persist and clamp out-of-range page sizes, a custom page size actually changes real Page counts during processing, the sidebar nav item is login-gated and the language preference correctly redirects "New Reading" with no other language signal present, and account deletion removes the user/subscription/documents/pages from the DB and ends the session. Full test suite green throughout (11 tests, OK).

v0.4.0

Released 2026-08-03
Bug Fixes
  • Make progress tracking actually work with the async pipeline (02c2400) This morning's switch to a DB-backed cache (34f8a76) for progress tracking broke text processing in two compounding ways, confirmed live via a real end-to-end run through the Django test client: 1. views.py still poked the old in-memory dict directly (progress_tracker._progress_store[task_id] = {...} / .update({...}) in a dozen places) instead of going through progress.py's public API -- that attribute no longer exists, so every processing request crashed immediately with AttributeError: 'ProcessingProgress' object has no attribute '_progress_store'. This is the exact error reported in production. 2. Once that's fixed, a second bug surfaces: the DB-backed cache does a real synchronous DB query on every read/write, but every one of these calls (in views.py's async views and every async method on TextProcessor in text_processing.py) was unwrapped, tripping Django's SynchronousOnlyOperation guard. Fixed (2) by adding async-safe wrapper methods to ProcessingProgress (start_processing_async/update_progress_async/complete_paragraph_async/ set_error_async/get_progress_async, each just sync_to_async-wrapping the existing sync method) and switching every async-context call site in both files to use them with await. check_progress (the one plain sync view that touches progress_tracker) is untouched -- it's already safe to call the sync methods directly there. Verified live end-to-end via the Django test client: POST /ja/process/ now returns task_id instead of a 500, and polling /progress/<task_id>/ reaches status=complete/percentage=100 (translation and page creation both completed). Full test suite still green (11 tests, OK).
Features
  • Show the actual release version on the About page (2e99fc1) "Version 1.0" was hardcoded in about.html, unrelated to the real release. pyproject.toml's project.version is what python-semantic-release bumps and tags on GitHub as v{version} on every push to main, so it's the actual version -- read it once at startup via stdlib tomllib into settings.APP_VERSION and pass it through about()/about_zh() into the template. Verified: renders "Version 0.3.2" (current pyproject.toml value), full test suite still green (11 tests, OK). Also includes an incidental uv.lock sync (0.3.1 -> 0.3.2) that was already stale from the last semantic-release bump.

v0.3.3

Released 2026-08-03
Bug Fixes
  • Raise Cloud Run memory limit to 1Gi (c5bbc41) The service ran at Cloud Run's 512Mi default, but jieba + sudachipy dictionary loading pushes a single worker to 512-559 MiB in production -- Cloud Run logs show OOM kills mid-request ("Memory limit of 512 MiB exceeded", followed by "container instance was found to be using too much memory and was terminated") repeatedly today across every revision, which is what's breaking text processing for users. Checked actual usage via Cloud Monitoring: ~429s total billable instance-time over the last 30 days, so doubling memory keeps GB-seconds around 429 vs the 360,000/month free tier -- no cost/tier impact.

v0.3.2

Released 2026-08-03
Bug Fixes
  • Back progress tracking with the database instead of process memory (34f8a76) progress_tracker was a plain in-process dict. Cloud Run runs multiple instances, so the request that starts a text-processing job and the requests polling /progress/<task_id>/ for it can land on different instances -- the poller's instance has never heard of the task, so it 404s indefinitely even though the job is actually running fine on the instance that started it. This is what broke Japanese text processing in production (confirmed in Cloud Run logs: process_text ran, then six minutes of 404s on /progress/<task_id>/ until the client gave up). Swaps the in-memory dict for Django's cache framework backed by the existing Postgres connection (CACHES + a migration creating the cache table) -- no new infra, and progress.py's public API is unchanged so views.py/text_processing.py needed no changes. Verified the fix directly: progress written by one Python process is now readable from a completely separate process, which is not true of the old version.
Continuous Integration
  • Serialize deploy runs per branch (2672746) Two deploys just raced (a vulnerable-settings commit and its immediate follow-up fix ran concurrently), with no guarantee the newer commit's deploy would be the one left live. cancel-in-progress on a per-branch group means only the latest push ever wins.

v0.3.1

Released 2026-08-03
Bug Fixes
  • Disable social account auto-connect-by-email (47c273c) SOCIALACCOUNT_EMAIL_AUTHENTICATION(_AUTO_CONNECT) let a Google login silently attach to (and log into) any local account with a matching email. Since local signup (auth_forms.py) never verifies email ownership, an attacker could pre-register someone else's address, then inherit that person's real account the first time they used "Continue with Google" -- full account takeover, and the attacker keeps password access afterward since it's a permanent connection. Left at allauth's secure default instead: a Google login whose email collides with an unconnected local account is now rejected rather than merged. Verified via pre_social_login() directly -- the incoming login no longer resolves to the pre-existing account (is_existing=False, user object unchanged).
Chores

v0.3.0

Released 2026-08-03
Features
  • Add Google sign-in alongside email/password auth (be5f89f) Wires django-allauth's Google OAuth flow into the existing login and register pages (the "Continue with Google" button was already there as a placeholder). Additive, not a replacement -- the existing auth_views.py/auth_forms.py email+password flow is untouched. Accounts are matched by email, so a Google sign-in for an address that already has a password account logs into that same account instead of erroring or duplicating it. Needs a Google Cloud OAuth client (Console-only setup, documented in DEPLOYING.md) before the button actually completes -- degrades to a non-functional link until GOOGLE_OAUTH_CLIENT_ID/_SECRET are set, same pattern as the other optional integrations.

v0.2.4

Released 2026-08-03
Bug Fixes
  • Use custom delimiter for --set-env-vars (23f1668) DJANGO_ALLOWED_HOSTS and DJANGO_CSRF_TRUSTED_ORIGINS now hold comma-separated host lists, which collided with --set-env-vars' own comma-as-separator syntax. ^@^ switches the between-vars delimiter to @, freeing commas for use inside values.
Chores
  • Rename Cloud Run service to kerokero-reader-app (950bc69)

v0.2.3

Released 2026-08-03
Bug Fixes
  • Run migrate job's command through uv run (899ba62) The image's CMD invokes gunicorn via `uv run`, which puts uv's venv on PATH -- the migrate job's --command called bare `python` instead, which resolved to the system interpreter with no Django installed. Also apply --command on the update path, not just create, so an existing job gets corrected too.

v0.2.2

Released 2026-08-02
Bug Fixes
  • Poll Cloud Build status directly instead of streaming logs (db4075c) --suppress-logs didn't help -- gcloud builds submit still blocks on the same log-based polling internally even when not printing it, and that poll needs Cloud Logging read access the deploy SA doesn't have. Submit async and poll the build resource's status field instead, which only needs cloudbuild.builds.get.

v0.2.1

Released 2026-08-02
Bug Fixes
  • Suppress Cloud Build log streaming in deploy workflow (59e0b6f) gcloud builds submit was exiting non-zero purely because the deploy service account can't stream Cloud Build's logs, even though the underlying build succeeds. --suppress-logs skips streaming and lets the command wait on the actual build status instead.

v0.2.0

Released 2026-08-02
Features
  • Production deploy pipeline -- Vertex AI, Gemini Flash-Lite, Sentry, Langfuse, GitHub Actions CD (3654d17)
  • Split Japanese pipeline under /ja/, symmetric with /zh/, instead of silently owning the root - Route Claude calls through Vertex AI in prod (IAM auth, no API key), direct Anthropic API as local-dev fallback - Move vocab refinement and translation to Gemini Flash-Lite (~4x cheaper than Haiku for these high-volume structured-output calls); grammar explanations stay on Claude Sonnet 5 - Wire in Sentry (error tracking) and Langfuse (LLM call tracing -- both Anthropic and Google GenAI SDKs instrumented); both optional, degrade gracefully like ANTHROPIC_API_KEY - Add .github/workflows/deploy.yml: test, build, migrate (Cloud Run Job), deploy on every push to main - Add Google Analytics tag, gated behind GA_MEASUREMENT_ID so local dev traffic is never tracked

v0.1.2

Released 2026-07-29
Bug Fixes
  • Library storage limit is per-language, not combined (165a1d8) Follows directly from making My Library two separate lists (results_list/results_list_zh): the storage cap now matches, scoped to whichever language you're looking at, instead of counting both languages together against one shared cap. UserSubscription.enforce_document_limit() takes an optional `language` and filters/prunes within it; both call sites (open_quick_read, process_document_async) now pass the relevant document's language. results_list's at_library_limit check is back to a plain per-language count (matching the already-filtered `documents` queryset) instead of a separate combined-total query. Real bug caught while wiring this up: open_quick_read's clone never copied source.language, so cloning a Chinese Quick Read into your library silently produced a Document defaulting to language='ja' -- wrong pipeline/theme on open, and would have broken per-language counting here too. Fixed by passing language=source.language into the clone. Verified live: free-tier account (cap 3) with 3 ja docs + 1 zh doc shows "Library limit" only on /results/, not /zh/results/; manually adding a 4th ja doc and calling enforce_document_limit(language='ja') evicted only the oldest ja doc, left the zh doc untouched.

v0.1.1

Released 2026-07-29
Bug Fixes
  • My Library filters by language instead of showing both (11a59ba) results_list()/results_list_zh() previously showed every document regardless of language on both /results/ and /zh/results/ -- a deliberate earlier call, reversed per explicit request: each endpoint should only list its own language's documents. at_library_limit still checks the account's combined document count across both languages, since UserSubscription.enforce_document_limit() enforces one shared storage cap, not a per-language one -- filtering that check the same way as the display list would have let a Chinese document slip in past an already-full account (or vice versa). Verified live: seeded one ja + one zh document for the same user, /results/ shows only the ja title, /zh/results/ shows only the zh title.
Chores
  • Remove dead files and duplicate stale AI-assistant docs (6460ae0)
  • celery_worker.py, start_dev.sh: tracked but always empty (0 bytes) - debug_result.html: dead debug template, zero references anywhere - management/commands/run_amain.py: unreachable -- root-level management/ has no __init__.py files, isn't inside any INSTALLED_APPS app, so Django never discovers it as a command; also calls a text_processor.parser.TextProcessor API and Document(source_type=, is_epub=) kwargs that no longer exist (same stale pattern just fixed in tests/test_models.py) - tests/contract/*, tests/integration/*: every file was an empty (0 byte) stub, never implemented, not collected by any test run - tests/run_all_tests.py: hardcoded absolute path (/home/caiod/parallel-text-creator), broken on any other machine; superseded by `manage.py test tests`, now the documented and CI-driven path - GEMINI.md, .github/instructions/instructions.md: byte-identical duplicates describing the pre-Claude-migration stack (Vertex AI Gemini for vocab/translation), no longer accurate and read by neither Claude Code (that's CLAUDE.md, which doesn't exist here) nor any CI step Also removed untracked Playwright verification artifacts (.playwright-mcp/, assorted root-level *.png screenshots) left over from manual UI verification during earlier sessions.
Documentation
  • Make README actually describe the app (d909e3d) Was two sentences plus the deploy-readiness quickstart from the CI/ release work. Adds a real feature overview (two-language pipeline architecture, input formats, translation/vocab/grammar via Claude, library, subscription tiers) and a project-layout map, grounded in the actual code (text_processor/*.py, urls.py route names, models.py plan choices) rather than guessed.

v0.1.0

Released 2026-07-29
Bug Fixes
  • "current" badge placement and remove redundant Current Plan buttons (3fcecdc)
  • "Current" ribbon now reflects actual usable capacity, not just UserSubscription.plan: shown on Free only when credits are exhausted and not unlimited, on Credit Packs when credits > 0 and not unlimited, and on Unlimited when the plan actually is unlimited - Unlimited's "Current" ribbon is now gold (matching its card's accent) instead of the generic indigo used on the other two cards - Removed the disabled "Current Plan" button under whichever card is current — the ribbon already says so, the button was redundant filler - Gray out both credit-pack purchase buttons when already unlimited, since buying credits is meaningless at that point
  • "process Text" crashed with a 500 (SynchronousOnlyOperation) (1f25dcb) _process_async is an async view, but get_user_limits() does real synchronous ORM work (get_or_create, reset_monthly_usage's .save()) and was being called directly instead of through sync_to_async - the first synchronous DB query anywhere in that call chain raised django.core.exceptions.SynchronousOnlyOperation, which then hit a second bug on the way out: the except block referenced is_ajax before it was ever assigned (UnboundLocalError), masking the real error. Fixes: - Force request.user's lazy resolution in the sync `process()` wrapper before entering the async context, and await sync_to_async(get_user_limits)(...) inside it - Compute is_ajax before the try block so the exception handler can always reference it Also fixed a title regression from the earlier source_url change: for plain-text documents, `metadata.get('title') if metadata else ...` only checked whether metadata was truthy, not whether it actually had a 'title' key - since metadata is now always a dict (holding source_url), every text document's title silently became None. Model's get_title() fallback then rendered the literal string "None" since uuid was also unset for text documents.
  • Add Noto Sans JP fallback so Japanese text renders (3a755cc) The font stack was Inter-only with no CJK fallback, so browsers without a system Japanese font (e.g. minimal Linux installs) rendered all Japanese text as tofu boxes. Add Noto Sans JP after Inter in the stack; the browser falls back to it per-glyph for characters Inter lacks.
  • Align text when no translation (e1cb247)
  • Change upgrade page route from /subscription/ to /upgrade/ (a8f6a96) The URL name stays 'subscription' internally (no template changes needed, every {% url 'text_processor:subscription' %} now resolves to /upgrade/ automatically), but the path itself moves. Removed the old /upgrade/ -> /subscription/ redirect view now that /upgrade/ is the page's real address, and the now-dead redirect import along with it.
  • Correct python-semantic-release config, invalid on first CI run (3b6ae91) build_command = false doesn't satisfy semantic-release's schema (expects a string); pydantic rejected it and the Release workflow failed before doing anything. Also moved changelog_file to the non-deprecated changelog.default_templates path (config.py warned it's moving there in v10). Verified via `uvx --from python-semantic-release semantic-release version --print` locally -- config now parses and computes 1.0.0 as the next version.
  • Expand quick-read tag to "N days ago" without re-truncating titles (ca4eb9b) Went back from the shortened "Nd ago" to the fuller "N days ago"/"1 day ago" wording. Since that's wider, let the title wrap to two lines (items-start instead of items-center, drop truncate) instead of re-clipping long titles like "Train Announcement".
  • Fix EPUB paragraph separation and improve navigation UI (a55266d)
  • Fix EPUB paragraph extraction to properly separate content from HTML structure - Improve paragraph detection in _extract_paragraphs_from_html method - Enhance text processing to handle EPUB content with double newlines - Update JavaScript paragraph splitting to handle both EPUB and regular text - Fix navigation button positioning with indigo color scheme - Add navigation buttons on left/right sides of text boxes instead of overlapping - Improve responsive design for mobile navigation layout
  • Fix page loading overlay (13597f9)
  • Increase furigana/pinyin reading text size (4fd6130) .vocab-word-item rt (Vocabulary panel readings) 0.72em -> 0.9em, .japanese-paragraph rt (Text panel furigana/pinyin, applied to every reading paragraph regardless of language -- shared by both, not a Japanese-only class despite the name) 0.6em -> 0.8em. Rebuilt compiled CSS via npm run build:css. Verified live: pinyin reading "gōng yuán" clearly larger and more legible in the Vocabulary panel.
  • Pin content-with-navigation to full width, the actual source of the shift (c88edec) .content-with-navigation had no width set. Its parent (.container. results-container) uses align-items:center rather than stretch, so a flex/block child with no explicit width shrink-wraps to its content's size instead of filling the available space. That made the *entire* two-panel block's width follow whatever the Vocabulary/Grammar content needed, not just the 2:1 split between them (which the previous min-width:0 fix addressed but couldn't fully solve on its own, since the outer box itself was still undefined-width). Added width:100% to .content-with-navigation, and min-width:0 to .content-with-navigation .results-section (the remaining link in the flex chain lacking it). Verified against a minimal isolated repro (same nesting: flex column parent with align-items:center > width-less flex row > flex:1 row with 2:1 children): without the fix the outer box measured 84px with short content and 704px with long content; with width:100%+min-width:0 it stayed pinned at 1280px in both cases. Confirmed the same stability on the live reading screen with realistic injected vocab/grammar content.
  • Preserve title link styling and complete-state background coverage (dfd1c7d)
  • results-list.js: cancelEdit/updateTitle rebuilt the title <a> with only class="title-link", dropping the Tailwind utility classes that give it its font/size/color. Any edit (even a cancelled one) silently reset the title to default browser styling. Now the original className is captured before swapping to the input and reused when swapping back.
  • tailwind.css: #result-container has its own opaque white background (for its border/shadow box), which sat on top of and hid the green "marked complete" tint applied to its parent .translation-section, so only a thin border strip looked green instead of the whole panel.
  • Prevent panel width shift when switching Vocabulary/Grammar tabs (c373dc1) #panel-body (holds both tab contents) used overflow-y:auto without reserving scrollbar space, unlike the other scrollable panels in this app which already do. Vocabulary and Grammar content lengths differ, so whichever tab's content was short enough to not need a scrollbar got extra width versus the one that did -- a visible shift on browsers with non-overlay scrollbars. scrollbar-gutter:stable reserves that space unconditionally so the content width stays constant across both tabs.
  • Reading screen layout/buttons didn't match the reference (730820e) Two deviations from TextFlow.dc.html's reading screen:
  • .results-section still had the pre-redesign flex-direction: column, so Text and Vocabulary/Grammar were stacked (Text on top, Vocabulary below) instead of side-by-side with Vocabulary on the right (resultsWrapStyle: row, translationSection flex:2, componentsSection flex:1). Scoped the row layout to .content-with-navigation so the separate AJAX inline-results flow in index.html is unaffected.
  • The translation toggle, prev/next page buttons, and page-select were styled with the app's general thick-border/brutal-shadow button language instead of the reference's actual spec for these controls: thin 1px borders, pill/rounded shapes, transparent backgrounds, no shadow (prevBtnStyle/nextBtnStyle/pageSelectStyle/ translationToggleBtnCornerStyle). Added .reader-nav-btn/ .reader-page-select and rebuilt .toggle-icon-btn to match, including the last-page green "Mark as Completed" pill state.
  • Rebuild My Library cards to match the reference faithfully (d09a250) Root cause of the uneven card heights: a stale, unscoped .title-container rule from the pre-redesign table layout still set display:flex/flex-direction:row (with no override anywhere), so each card's title/meta/progress-bar were laying out as a horizontal row instead of a vertical stack - the actual reason heights varied so wildly, not just "optional content".
  • Removed the entire dead results-list.css-era block (results-table, result-row, title-column, date-column, actions-cell, view-link, duplicate title-container, epub-indicator/epub-author, and the old delete-btn/read-status rules) - none of it was reachable from the current card-based template - Rebuilt the card purely with Tailwind utilities (matching the reference's cardStyle/iconStyle/titleRowStyle/metaRowStyle/ progressTrackStyle/progressLabelStyle/deleteBtnStyle almost exactly), dropped the optional author line so every card has the same content shape and therefore the same natural height - the whole card is now clickable to open (matching the reference's onClick on the row), guarded so clicks on the title/edit/delete controls don't also trigger navigation - Removed the standalone read/unread toggle from the list entirely - completion now only comes from "Mark as Completed" on the reading screen (already wired to the same is_read field/endpoint) - Added the reference's wireframe "ghost" placeholder cards (dashed border, diagonal-stripe fill) below the library-limit divider
  • Remove Back link from the About page (cc717b0)
  • Remove plan usage banner from the input screen (727abaa)
  • Remove stray margin-left gap between sidebar and main content (2bfb94a) .main-content had margin-left:70px ("account for half of sidebar width to center better"), but the sidebar is already a normal flex sibling taking its own 140px, so this just opened a 70px dead gap between them. Content centering is already handled by .container's own margin:0 auto, so the extra offset was redundant. That gap always showed body's plain cream background, but it wasn't noticeable until the reading screen's "marked complete" mint tint on .main-content made the mismatched beige strip obvious.
  • Restore sidebar toggle functionality and improve visibility (c19c5d3) Sidebar improvements: - Remove duplicate hamburger button (☰) from templates - Fix sidebar toggle button positioning conflicts with general button CSS - Update arrow toggle color from cream to white for better visibility - Ensure proper CSS specificity to prevent style conflicts - Maintain existing arrow rotation and hover effects CSS fixes: - Separate general button styles from sidebar toggle styles - Add proper overflow handling for box shadows while maintaining sidebar visibility - Fix z-index layering for proper element stacking - Preserve existing toggle functionality with localStorage state management The sidebar now has a single, properly visible arrow toggle button that rotates on state change and maintains consistent styling across the application.
  • Scope leftover bare h2/button selectors breaking the redesign (4e90876) Two legacy global rules survived the Tailwind migration unscoped:
  • A bare `h2` rule forced a full-width indigo banner + centered white text on every <h2>, clobbering the About page's plain uppercase section labels and, on cards with no background override, making text unreadable (indigo-on-indigo). Only translation-section/ components-section headers actually want that banner treatment, so fold the properties into those two selectors and drop the bare rule.
  • Bare `button:hover`/`:active`/`:disabled`/`:not(.sidebar-toggle)` rules (left unscoped when a sibling `.input-section button` rule was fixed earlier) still applied `transform: translateX(-50%)` to every button on :disabled/:hover/:active, and had higher specificity than the `static` utility class templates use specifically to defeat this. This visibly broke the Subscription screen: the disabled "Current Plan" button was shifted left by half its own width, overlapping the neighboring card's button. Scope all of them to `.input-section`, matching the already-fixed sibling rule (that class no longer exists in any template, so this makes them fully inert, like the others).
  • Set page title on the input/home screen (84240d9) It was the only screen not overriding {% block title %}, so it fell back to base.html's generic "Japanese Text Processor" instead of "Kerokero Reader" like every other page.
  • Shorten quick-read tag labels to stop title truncation (366d52e) "ADDED 14 DAYS AGO" as a flex-shrink-0 pill left almost no room for the title in the narrow Quick Reads cards, truncating "Train Announcement" down to "Train An...". Shorten to "14d ago" / "New".
  • Sidebar collapse resize, reading-screen scroll, and pagination/header parity with reference (554e3d9)
  • Sidebar collapse used transform:translateX to slide off-screen, which leaves its 140px flex slot reserved and empty. Nothing repaints that freed strip, so it always showed plain body background (visible as a persistent beige gap once the "marked complete" mint tint made it obvious). Switched to collapsing width/padding/border/shadow instead, so main-content's flex:1 actually reclaims the space and recenters.
  • Reading screen: added overflow-y:hidden on the results-container's main-content so it can never require page-level scroll; the text and vocabulary panels already scroll internally.
  • Pagination (prev/select/next) and the header's "Page X of Y" were hidden entirely for single-page documents. Reference always renders them, with Previous just disabled/grayed. Removed the has_navigation conditionals; existing disabled-state CSS already matches reference colors, so single-page docs now show a grayed Previous instead of no nav at all.
  • Reader nav button/select border color was black instead of the reference's brand-border brown.
  • Translation toggle button never actually changed its label/icon on click (always showed the static "Translation" text) -- reference swaps between "Hide Translation 翻訳を隠す" (🙈) and "Show Translation 翻訳を表示" (👁) depending on state. Also added the header's bottom divider to match the reference's pageNavStyle.
  • Sidebar New Reading + language switcher follow content, not just path (ae963c8) "New Reading" was hardcoded to / regardless of language context -- on /zh/ (or anywhere else) it silently sent you back to the Japanese input page. Its active-highlight also only checked nav_active == 'index', so it never lit up while actually on /zh/. Fixed with a content-aware condition (result.language == 'zh' or language == 'zh', falling back to the path-based nav_is_zh only when neither var exists) applied to both "New Reading"'s href/label and the switcher pills' active-highlight -- not just nav_is_zh alone, since a Chinese document's reading screen lives at the shared /result/<id>/, not /zh/result/<id>/, so path alone would still get it wrong there. Real bug caught during verification: Django's {% if %} tag doesn't support parenthesized grouping -- an early version wrapped part of the condition in parens for clarity and every page using the sidebar (i.e. all of them) 500'd with TemplateSyntaxError. Fixed by removing the parens (harmless given how the and/or chain was structured) and verifying the fix against a real request, not just re-reading the diff. Verified live across all four cases: a Chinese document's reading screen (content-aware fix engaged: New Reading -> /zh/, 中文 highlighted, even though the URL isn't under /zh/), a Japanese document's reading screen (New Reading -> /, 日本語 highlighted), My Library (shared page with no result/language context, correctly falls back to path -> Japanese default), and /zh/ itself.
  • Sidebar toggle button clipped its own "Menu →" label (9f76ffb) button.sidebar-toggle hardcoded width:44px/height:44px/padding:10px, sized for an older icon-only version of this button. The current "Menu <arrow>" text content needs more room than that, so the arrow was rendering outside the button's visible bounds. The rule's positioning (fixed/top/left) was already redundant with Tailwind utility classes on the element, and its sizing is redundant with h-10/px-3.5/gap-1.5 there too, so drop the whole rule and let the button size itself to its content.
  • Simplify subscription screen and match reference's Unlimited card (038f6bf)
  • Remove the demo-mode disclaimer banner and the monthly usage tracker (progress bar / "X of Y documents" / credits line) — usage_percentage is now dead in the view too, removed - Remove the Back link - Retitle the page "Upgrade" instead of "Subscription" - Restyle the Unlimited card to match the reference's actual paid-tier cardStyle: 5px gold border, #FFFAF0 background, 8px/12px brutal shadow — it was still using the same plain white/brown treatment as the other cards
  • Split_text() only split on 。, silently ignoring ! and ? (00dfda1) Found while verifying paragraph splitting for the Chinese pipeline: split_text()'s sentence-boundary regex was `(?<=。)` despite its own comment claiming to use "the same technique as views.py's _SENTENCE_BOUNDARY_RE" -- which actually matches `[。!?\n]`. A sentence ending in ! or ? silently stayed merged with whatever text followed until the next 。, sometimes swallowing the entire paragraph into one chunk (confirmed: "你今天怎么样?我很好!谢谢你。", three real sentences, came back as a single unsplit string). This is a pre-existing bug in a function shared by both pipelines, not something introduced by the Chinese feature -- confirmed by testing the equivalent Japanese case ("元気ですか?私は元気です!ありがとう。"), which had the identical bug and is now equally fixed. Affects paragraph granularity for format_components/translate_text, i.e. how finely text gets batched for vocab lookup and translation -- not a correctness bug (no text was ever lost), but a real granularity regression for any exclamation/question-heavy input in either language. Known pre-existing minor limitation, not addressed: a closing quote immediately after 。!? ends up attached to the next chunk rather than the current one (_SENTENCE_BOUNDARY_RE has the same gap) -- cosmetic chunk-boundary quirk, not a text-loss issue, not worth the added regex complexity for what's a rare edge case in either language.
  • Stop text/vocab panel widths from shifting with content (b8b6ddb) .content-with-navigation .translation-section and .components-section are flex:2/flex:1 row items, but neither had min-width:0. A flex item's default min-width is auto (its content's min-content size), so once either panel's content needed more room than its ratio share, it would force the row to redistribute -- typically showing up as the vocabulary column getting compressed when its content changed. .paragraph-row's grid columns had the same default-auto-min-width gotcha (grid tracks also floor at min-content unless given minmax(0, ...)), so fixed both declarations of it too. With min-width:0 in place, both panels now strictly hold their 2:1 ratio regardless of content -- verified by forcing unusually long English text and full realistic vocab-word markup into both panels and confirming their measured widths never move.
  • Subscription page to exactly 3 columns, matching the reference (3bf29ec) Collapsed Premium+Enterprise into a single truly-unlimited "Unlimited" tier (monthly_processing_limit/max_stored_documents -1, all file types), matching the design's Free / Credit Packs / Unlimited layout and copy verbatim. free/premium remain the only two selectable UserSubscription.plan values surfaced anywhere; 'enterprise' stays a valid but no-longer-offered choice, so no migration was needed. Also fixed the plan-change success message using the internal "premium" key instead of its "Unlimited" display name.
Chores
  • Add ebooklib dependency and Docker dev environment (e08a901) EPUB parsing already relied on ebooklib but it was missing from pyproject.toml; also add a Dockerfile/docker-compose setup for local dev, running as a non-root user with pinned base-image tags.
  • Migrate stylesheet to Tailwind CSS v4 (6aa52b5) Replace the hand-written style.css/results-list.css with a Tailwind v4 build (static_src/tailwind.css -> static/css/tailwind.css via `npm run build:css`), and drop unused leftover templates from an earlier Tailwind spike (base_tailwind.html, index.html, input.html).
Documentation
  • Close out Chinese-language-support feature (T13) (b8c6c0e) All 13 tasks and acceptance criteria checked off; STATE.md moved from "in-progress" to "completed" with the full writeup -- decisions, every real bug found and fixed during implementation, and what was verified live vs. structurally (blocked on ANTHROPIC_API_KEY for the three Claude prompts specifically, everything else exercised for real).
  • Record Chinese-language-support progress in STATE.md (f1b880f)
Features
  • /zh/ routes for Chinese document creation (T7) (cc4650b) New process_zh/index_zh views + /zh/, /zh/process/ routes -- the actual "separate route" entry point per the original request. language threads through process_document_async, process_link_source_async, and every TextProcessor(...)/Document.objects.create(...) call site, including the lazy page-reprocessing path in navigate_page (reads document.language from the existing Document rather than defaulting to 'ja', so page 2+ of a multi-page Chinese document reprocesses correctly). Viewing routes (/result/<id>/, navigate_page, get_page_content, results_list) are deliberately NOT duplicated -- a scope refinement found during implementation: since Document now carries its own .language, these can fork internally (T8, template-level) instead of needing a parallel /zh/result/<id>/ namespace just to view an already-created document. Documented here rather than blindly following design.md's original per-route mirroring, consistent with this project's practice of noting implementation-time deviations. Verified live end-to-end against a real running server (not mocked): posted real Chinese text to /zh/process/, confirmed the created Document has language='zh' and -- notably -- got a real, correct English translation back ("I went for a walk in the park. The weather is nice today.") via the live easygoogletranslate path with source_language correctly set to 'zh-CN'. Vocab components empty as expected (no ANTHROPIC_API_KEY in this environment), falling back gracefully exactly like the Japanese path already does. Re-verified the existing Japanese /process/ path is unaffected (language='ja', translation still correct).
  • Add document progress tracking and card-based library view (13b2f07) Add Document.progress_percent/progress_label/page_count_label and a library-limit check in the results_list view, and rebuild My Library as a card list (was a table) with progress bars, updating results-list.js's selectors to match the new markup.
  • Add JLPT filter and furigana readings to the vocabulary panel (08784a3) Render ruby/furigana annotations over kanji words, a JLPT-level tag per word, and a filter popover (All/N4+/N3+/N2+/N1+) to hide words at or below a chosen difficulty, in both the results.html panel and the AJAX inline-results flow (main.js). Both gracefully handle the older flat "(reading) - definition" string format already stored on existing documents, falling back to no JLPT tag / no filter match for those.
  • Add page endpoint (915c3a7)
  • Add pay-as-you-go credits alongside the monthly subscription plans (2fc27db) The design's Subscription screen offered Free / Credit Packs / Unlimited as mutually-exclusive tiers, but this app already has a working Free/Premium/Enterprise monthly-quota subscription system — so add credits as a genuine second, coexisting mechanism rather than replacing the plan tiers: UserSubscription.credits is a pool (starts at 5, never expires) that only gets spent once a user's monthly plan quota runs out (record_processing() consumes quota first, falls back to a credit), and can be topped up via a new demo-mode buy-credits endpoint (+50 for $5, +200 for $15, matching the existing no-real-payment plan-change pattern). The Subscription screen gets a fourth "Credit Packs" card between Free and Premium, the sidebar/index banners now factor purchased credits into the "credits available" figure, and re-reading already-created documents was already free under this architecture (quota/credits are only spent once, at Document creation) so no change was needed there.
  • Add per-user LLM usage tracking, grammar-note logging, and Claude translation path (6ff6291) Adds LLMUsageLog (tokens/estimated cost per Claude call, across vocab refinement, grammar explanations, and translation) and GrammarNote (every grammar explanation as a queryable row, independent of the display-only Page.grammar_history cache) -- neither existed before, so there was no way to see actual per-user cost or grammar activity. Also threads `user` through TextProcessor so vocab/translation calls can be attributed, and adds translate_text_claude() as a ready-but-not-yet-wired alternative to the current easygoogletranslate path. Admin: expose UserSubscription.last_reset_date (previously invisible -- auto_now_add silently excludes it from the form), add a lifetime document-count column, and date_hierarchy drilldowns.
  • Add preset options (b81f912)
  • Add quick-reads sample pool and about page (3bf8934) Add a day-of-year-rotated pool of sample Japanese texts for the input screen's Quick Reads section, and a new About page.
  • Add sidebar status context processor and get_item filter (d34a64a) Expose login/subscription/credits state and the active nav item to every template via a context processor, so the sidebar can highlight the current page and show upgrade/credits state without each view recomputing it. Also add a get_item template filter for dict lookups.
  • Add source link for documents imported from a recognized link (f91088f) Add Document.source_url, capture it server-side (http/https only) when processing text, and thread the URL detected by the existing Wikipedia/NHK link-recognition UI through to /process/ via window.detectedSourceUrl. The reading screen's top bar now shows a "Source" link back to the original article when one is on record.
  • Add user support (e58cb08)
  • Begin Chinese-language reading support (spec-driven, T1-T2 of 13) (79b8a9f) Full spec/design/tasks under .specs/features/chinese-language-support/ for a parallel Chinese reading pipeline alongside the existing Japanese one, on separate routes, sharing Document/Page/admin/usage-tracking via a new Document.language field rather than duplicating models. T1: add jieba (MIT) as the Chinese tokenizer; vendor a CC-CEDICT-derived simplified-Chinese dictionary (CC BY-SA, attribution in cc-cedict-vocab/README.md, numbered pinyin converted to tone marks) and an HSK 3.0 level dataset (MIT, from drkameleon/complete-hsk-vocabulary) -- the Chinese equivalents of jamdict and yomitan-jlpt-vocab respectively. Found and fixed a real bug in the tone-mark conversion during verification (bare "o" finals like gong/dong weren't being marked). T2: Document.language field (ja/zh, default 'ja'). Verified all 36 existing documents backfill to 'ja' with zero behavior change. Remaining: vocab_pipeline_zh.py, hsk_lookup.py, Chinese-reworded prompts, new /zh/ routes, HSK badge UI, Chinese Wikipedia import source -- tracked in tasks.md T3-T13.
  • Chinese vocab pipeline + HSK lookup (T3-T4) (47eae81) hsk_lookup.py mirrors jlpt_lookup.py's shape against the vendored HSK 3.0 data. vocab_pipeline_zh.py mirrors vocab_pipeline.py's shape: jieba tokenizes + POS-filters (particles/punctuation/numerals/classifiers dropped, single-char HSK-1 words dropped), CC-CEDICT supplies ranked candidates, tag_paragraph_words does hover-highlight HTML reconstruction. No JMnedict-equivalent split needed -- CC-CEDICT already covers proper nouns/place names in one unified lookup, and Chinese has no conjugation so surface form == dictionary form (jaconv's role has no Chinese analog). Verified against real sentences: POS filtering, tone-marked pinyin candidates, elementary-word filtering (我/你 correctly dropped as single-char HSK-1), lossless token reconstruction, hover-highlight HTML. Known limitation found and accepted (same posture as the Japanese pipeline's documented JMdict gaps): jieba's default dictionary sometimes merges compounds that CC-CEDICT doesn't have entries for (e.g. 今天天气 merges "today"+"weather" into one non-standard token) -- the existing LLM-fallback-on-empty-candidates design absorbs this, same as it already does for Japanese dictionary misses.
  • Chinese Wikipedia import source (T10) (330d5a9) Generalizes wikipedia_import.py's hardcoded ja.wikipedia.org host into a shared _fetch_article(url, host, matches, error_class) helper -- the existing fetch_article()/is_ja_wikipedia_url() public functions keep their exact prior names/behavior (zero risk to the Japanese path), with new fetch_article_zh()/is_zh_wikipedia_url() siblings registered as a second LinkSource (wikipedia_zh_url field) in the already-pluggable registry, auto-picked-up via apps.py's existing ready() hook. index.html's client-side LINK_SOURCES detection is gated by route/language -- a zh.wikipedia.org link pasted on the Japanese page (or vice versa) would otherwise get detected and processed through the wrong pipeline, since language is decided by which URL the form submits to (process vs process_zh), not by which link source matched the pasted text. Aozora Bunko (Japan-only content) stays unconditional, Japanese-route-only. Verified live against the real MediaWiki API (not mocked): fetched a real Chinese Wikipedia article (大熊猫/Giant Panda), confirmed it correctly rejects a mismatched Japanese Wikipedia link with a clear error, confirmed the Japanese path is completely unaffected (fetched a real ja.wikipedia.org article successfully), and did a full end-to-end submission through the live /zh/process/ endpoint -- the created Document has language='zh', the correct title (大熊猫), and the real fetched article text, not a stub.
  • Chinese-language templates, HSK badge UI, and Noto Sans SC font (T8-T9) (04ff1d4) Real finding that simplified the plan: JLPT badges have no per-level styling at all (uniform .jlpt-tag regardless of N5 vs N1), so the HSK badges reuse that same class rather than needing a whole new 9-tier visual system as design.md assumed. results.js: IS_ZH_DOC (set once from resultData.language) gates the vocab filter system -- HSK's 9 levels double as their own weight, but the comparison direction is opposite JLPT's inverted N-numbering (HSK: higher number = harder, so "3+" is `hsk >= 3`; JLPT: lower N = harder, so "N3+" is `JLPT_WEIGHT <= weight(N3)`), plus the popover label, filter chip list, level badge, and dictionary link (MDBG instead of Jisho) all branch on it. Ruby/pinyin rendering needed zero changes -- confirmed CJK Unified Ideographs (KANJI_RE) already matches hanzi identically to kanji. results.html/index.html: {% if language == 'zh' %} forks for the ~10 Japanese-hardcoded copy spots (panel labels, nav buttons, hint text, Mark Complete -- found one the initial code scan missed), and index's form now points at process_zh via a data-process-url attribute main.js reads instead of hardcoding '/process/'. Real bug found and fixed during live verification: Chinese text rendered as tofu boxes for some characters -- turned out Noto Sans JP was never actually loaded via any @font-face/link, only referenced in the CSS fallback chain hoping the OS had it. Added a real Google Fonts link for Noto Sans SC on Chinese routes, which is the actual, verifiable fix (confirmed live: 测试文档 renders correctly after, vs. two tofu boxes before). Verified end-to-end via Playwright against a seeded real Document: pinyin ruby rendering, HSK 2/3 badges, MDBG dictionary links (opened a real page), HSK filter popover with all 9 levels, and the 3+ filter correctly hiding the easier HSK-2 word while keeping the HSK-3 one -- confirming the inverted-vs-direct weight comparison is right, not just plausible-looking.
  • Ci, semantic-release, and easy local dev; prod-ready settings for Cloud Run + Neon (2a43ab5) Adds GitHub Actions CI (test on push/PR) and a Conventional-Commits-driven semantic-release workflow (version bump + CHANGELOG.md + tag on merge to main). Makes settings.py env-driven (DEBUG/SECRET_KEY/ALLOWED_HOSTS/ DATABASE_URL) with unchanged zero-config local defaults -- prod-only behavior (required secret, HSTS, secure cookies, WhiteNoise manifest static storage) only activates under DJANGO_DEBUG=0. Dockerfile's default CMD is now gunicorn with collectstatic baked in; docker-compose.yml keeps the dev server for local use and now loads .env. Adds .env.example, a README quickstart for both the uv-native and Docker Compose paths, and python-dotenv loading in settings.py so .env works under plain `uv run` too (it isn't auto-loaded otherwise). No infra provisioned or deployed. Also fixes .gitignore (plain .env was never actually ignored) and tests/test_models.py (stale source_type/is_epub kwargs from before the document_type refactor, which errored on every CI run).
  • Color-code sidebar by account tier (staff/admin/unlimited) (534de2d) Staff, superusers, and subscribed users now get a distinct sidebar background (green/black/gold) plus a matching badge, instead of just a plain-text label, so account tier is visible at a glance from any page.
  • Complete grammar bubble system and optimize vocabulary layout (4c3644e) Grammar bubble improvements: - Make entire grammar bubble clickable, not just the button text - Add conditional hover effect that only applies when button is visible - Remove translation/lift effects for cleaner interaction - Implement proper button styling with intense orange hover gradient - Set button display to 'contents' and explanation display to 'contents' Vocabulary section optimization: - Reduce individual vocabulary row sizes (smaller padding, margins, font) - Restore vocabulary section window to full height (36vh desktop, 30vh mobile) - Implement left-column-first layout with column-fill: auto - Fix box shadow visibility by changing overflow from hidden to visible - Add proper bottom margin (4vh) and container padding for shadow display UI refinements: - More compact vocabulary rows allow more items to fit in same space - Improved visual hierarchy with properly sized grammar elements - Enhanced user experience with clickable grammar bubble areas
  • Django migration and other improvements (6ae3b88)
  • Enhance EPUB parser with HTML structure-based extraction (a3207ca)
  • Replace text-based splitting with HTML structure parsing - Extract paragraphs directly from HTML tags (p, div, h1-h6, blockquote, etc.) - Implement item-based page separation (each EPUB document = one page) - Preserve document order and handle nested elements correctly - Add different minimum length rules for headings vs content - Maintain fallback to text-based extraction when no HTML structure - Remove legacy TextProcessingResult model and migrate to Document/Page - Update text splitting to handle double spaces as paragraph separators - Fix translation alignment between original and translated text
  • Extract JLPT level and structured reading for vocabulary words (623d304) Change the vocab-extraction prompt to return {word: {reading, definition}} instead of a single "(reading) - definition" string, then look up each word's JLPT level from the vendored yomitan-jlpt-vocab data (was sitting unused in the repo) rather than asking the AI to guess it.
  • Finalize viewport layout and improve text interaction (421951b) Layout centering and spacing: - Fix horizontal centering for results container with align-items: center - Add responsive lateral margins (2vw default, 3vw portrait, 10px minimum) - Implement viewport-aware spacing that adapts to screen orientation - Remove main-content padding for results pages with :has() selector fallback - Ensure text boxes never touch screen edges on any device UI improvements: - Change section title from 'Translation' to 'Text' for clarity - Update both results.html and index.html templates consistently Enhanced text interaction: - Protect text selection from accidental paragraph deselection - Only simple clicks (without text selection) toggle paragraph state - Preserve user text selection when clicking within selected paragraphs - Maintain copy-friendly behavior for Japanese text and translations - Add intelligent click detection using window.getSelection() The interface now provides optimal spacing across all screen orientations while preserving user text selections and maintaining intuitive paragraph navigation.
  • Fix paragraph alignment and implement concurrent translation (89ff1ed)
  • Fix paragraph alignment issue between Japanese and English text - Modified translate_text() to process paragraphs individually instead of entire text - Implement concurrent translation processing using asyncio.gather() - Add _translate_paragraph_async() method for parallel paragraph translation - Maintain 1:1 paragraph alignment by preserving original text structure - Improve translation performance through concurrent processing - Add progress tracking integration for real-time translation updates - Ensure translated paragraphs rejoin with same structure using 。separators Technical improvements: - Concurrent processing reduces translation time for multi-paragraph text - Thread pool execution prevents blocking during translation calls - Proper error handling for failed paragraph translations - Progress tracking shows real-time translation status updates
  • Global Quick Reads become precomputed, admin-curated Documents (2a1074c) Quick Reads no longer go through TextProcessor/LLM reprocessing on every click. They're now real Document+Page records an admin flags from My Library (is_staff-gated), processed once like any other document.
  • Document gains is_quick_read + quick_read_added_at. - text_processor/quick_reads.py replaces sample_pool.py: same daily rotation/tag ("New"/"N days ago") logic, sourced from Document.objects.filter(is_quick_read=True) instead of a hardcoded list. - New toggle_quick_read view/endpoint (staff + owner only, mirrors the update_title/delete_result pattern) lets an admin flag/unflag one of their own documents from My Library. - New open_quick_read view: clicking a Quick Read on the index page instantly clones the flagged Document's title/pages (translation, components already computed) into a new Document owned by the viewer (or anonymous, with the existing session-history tracking), then redirects to it. Zero TextProcessor/LLM calls. Respects the viewer's storage-limit eviction but doesn't spend processing quota/credits, since nothing was actually processed. - index.html: quick-read-card/sample-row become plain links instead of textarea-fill-then-submit; removed the now-dead selectSample/ sample-badge JS and markup. Section hides gracefully when zero Quick Reads are flagged. - Dropped the unused get_presets_for_template() call from index() while touching this code -- index.html never rendered {{ presets }}; PresetText/presets_manager.py were already fully dead, just not previously noticed (documented in .specs/project/STATE.md, left alone). Full spec/design/task breakdown in .specs/features/global-quick-reads/, via the tlc-spec-driven skill (first use in this project, so also added minimal .specs/project/ scaffolding). Verified end-to-end via Playwright: staff flag/unflag with correct 403/404 permission checks, anonymous + authenticated clone flows (clone ownership, no processing-pipeline calls in server logs), clone independence (deleting a clone leaves the source untouched), and clean degradation to zero Quick Reads.
  • Implement comprehensive results list with inline editing and management (53ec06a)
  • Add complete results list screen with sortable table - Implement inline title editing with edit icon trigger - Add read status toggle with visual feedback - Include delete functionality with confirmation dialog - Add scroll bar to results container for better UX - Update TextProcessingResult model with new fields (title, last_opened, pages_count, is_read) - Create Django views for CRUD operations (toggle read, update title, delete result) - Add responsive design with neobrutalist styling - Include proper error handling and AJAX functionality - Fix button positioning and layout issues Features: - Click title to navigate to result - Click edit icon (✏️) to edit title inline - Click read status circle to toggle read/unread - Click trash icon (🗑️) to delete with confirmation - Scrollable table for large result sets - Real-time updates without page refresh
  • Implement screen separation with dual processing and loading states (081dd2f)
  • Separate input and results screens with different URLs - Add dual processing approach: AJAX (same page) vs form submission (new page) - Center input section and move Process New Text to sidebar navigation - Implement loading overlays with spinner animations for both processing methods - Create dedicated results.js for results page functionality - Fix form submission redirects with separate form architecture - Add comprehensive debug information and template improvements - Update UI styling with proper centering and responsive design
  • Improvements (3b77172)
  • Merge upgrade flow into unified subscription screen (1546410) Replace the separate plan-picker page with a single Subscription screen showing all three tiers as brutal-shadow cards; subscription_upgrade now just redirects to it, and the old upgrade.html template is removed.
  • Multiple improvements (b7e15f2)
  • Multiple improvements (536e025)
  • Multiple improvements (4fc8d0f)
  • Real Wikipedia + Aozora Bunko link import, mobile-responsive reading screen, and several correctness fixes (851b1ab) New pluggable link-source registry (text_processor/link_sources.py) so Wikipedia and Aozora Bunko both fetch real content on paste instead of the old canned-text demo, and a future third source can plug in without touching views.py's branching. Wikipedia uses MediaWiki's own extracts API; Aozora Bunko parses its XHTML card/file pages (Shift_JIS-aware), preserving real furigana as offset-tracked ruby spans that render inline in the Text panel without leaking into vocabulary generation, translation, or the grammar-explain prompt (results.js's context-building had a real leak here, now fixed). Also: - Sidebar is now a proper off-canvas drawer on mobile, and the reading screen's Text/Vocabulary panels stack instead of squeezing side by side on small screens. - Fixed a real race condition where long documents could fail to redirect to the reading screen after processing (TextProcessor was signaling task-level completion before the caller had finished creating all of a document's pages). - Fixed Wikipedia extraction leaving bare section headings inline in the reading text, and split_text() dropping Japanese periods when breaking text into paragraphs. - Fixed a client/server paragraph-count mismatch (a separate JS paragraph-splitter disagreed with the Python one on certain inputs, producing an extra untranslated row). - BOOK_PAGE_CHAR_COUNT raised from 400 to 700 characters per page.
  • Rebuild reading screen to match the design reference (46d2aa3) The initial pass only reskinned the old two-panel Text/Vocabulary layout; it didn't match TextFlow.dc.html's actual reading-screen interaction model. Rebuild results.html/results.js to match:
  • Hint bar ("Tap a sentence... select Japanese text to explain grammar") - The right panel is now tabbed Vocabulary/Grammar (bottom tab row), with the vertical header label switching to match the active tab - Vocabulary tab shows only the tapped paragraph's words under a "Paragraph N" badge, instead of a scrolling list of every paragraph with hide/show separators - Grammar tab replaces the floating grammar-bubble popup: selecting Japanese text switches to this tab and shows the quote + an Explain Sentence button docked in the panel, reusing the existing /explain-grammar/ endpoint; explained sentences collect into a session-only collapsible history (click to expand/collapse, remove with the x) since there's no backend model to persist it - Last page now offers "Mark as Completed" (wired to the existing toggle-read-status endpoint, logged-in owners only), which tints the reading panels and page background green and shows a brief celebration overlay, matching the design's markedComplete state - Single-page documents now reach the bottom nav/mark-complete controls too; previously that whole row only rendered when has_navigation was true Not implemented (design calls for data this app's model doesn't have): JLPT vocab filter chips, furigana/ruby readings on vocab words, and the "Source" link (no source_url field on Document).
  • Redesign input screen with quick reads and link detection (9dac3b3) Add the bilingual Quick Reads grid with a "More reads" modal, sample and link badges, Wikipedia/NHK link auto-detection, and live character count / page estimate, while keeping all element IDs the AJAX flow in main.js depends on.
  • Redesign login and register screens (30d1fce) Rebuild both as tabbed, bilingual cards in the neobrutalist style with a Google sign-in stub, without changing the underlying username/password auth flow.
  • Redesign results reading screen (a0f72d6) Move the translation toggle into a new bilingual top bar and replace the side-arrow page nav with bottom pill buttons, keeping the results.js-facing structure and element IDs unchanged.
  • Redesign sidebar navigation (0520fd1) Rebuild the sidebar in the neobrutalist Kerokero Reader style: bilingual EN/JP nav labels, active-state highlighting, a status pill, and a reserved-space upgrade link so the layout doesn't shift for logged-out or already-premium users.
  • Route My Library and About to Chinese-forked versions (aa1df22) Adds results_list_zh/about_zh views and /zh/results//zh/about/ routes, following the same content-language-first sidebar detection already used for New Reading. About gets translated copy since it makes Japanese-specific claims; My Library's list stays a single shared, unfiltered list across languages -- only its chrome/CTA copy forks.
  • Show page count on Quick Reads cards (2945134)
  • Sidebar language switcher + jade/gold theme for Chinese routes (534200a) Switcher: two pill links (日本語/中文) in the sidebar, always pointing at each language's landing page ("start a new reading in X"), not tied to the language of whatever document is currently being viewed -- driven by a new nav_is_zh context var (path-based: request.path.startswith('/zh/')), deliberately not content-based, since viewing routes are shared (/result/<id>/ isn't under /zh/ even for a Chinese document). Theme: body[data-lang="zh"] (set in base.html from whichever of `language`/`result.language` the current page has) scopes a full custom-property override -- jade green (#0E6B4F) replaces indigo as the primary UI color, a dark jade-charcoal (#2C3B33) replaces the warm brown border/shadow color, background shifts to a pale jade-tinted ivory. Gold stays the same hero color on both themes deliberately, tying them together. Confirmed via the compiled CSS that every existing bg-brand-indigo/text-brand-indigo/border-brand-border/bg-cream utility (and --shadow-brutal-*, which references --color-brand-border via var()) re-resolves automatically under the scope -- zero template/markup changes needed beyond the one data-attribute, since Tailwind v4's @theme tokens compile to real runtime CSS custom properties, not build-time-baked literals. Verified live: Japanese index/results pages completely unchanged: the Chinese index page and a Chinese document's results page both render in the new jade/gold theme; the switcher correctly highlights the active language on index pages and correctly does NOT highlight either language when viewing a document's reading screen (since that path is shared, matching the documented "switcher reflects path, not content" design).
  • Switch vocabulary refinement and grammar explanations from Vertex AI to Claude (d87f7f3) Replaces the Gemini/Vertex AI calls in the vocabulary-refinement pipeline (Claude Haiku 4.5) and the grammar-explanation view (Claude Sonnet 5) with the Anthropic SDK, mainly to get prepaid billing instead of Vertex's postpaid/invoiced model. Also closes a gap where grammar explanations had no quota/credit check at all, unlike document processing. Drops the now-unused google-cloud-aiplatform/google-generativeai dependencies and GOOGLE_CLOUD_PROJECT/VERTEX_AI_LOCATION env vars in favor of ANTHROPIC_API_KEY.
  • Use email as the login/signup identifier, per the design (860f1c4) The design's login/signup screens ask for an Email field, not a username. Rather than adding a custom auth backend, use the email address itself as the Django username: EmailUserCreationForm collects and validates it as an email, stores it in both User.username and User.email, and EmailAuthenticationForm just relabels the existing username field as Email (the field name stays "username" so auth_views.py's authenticate() call didn't need to change).
  • Wire Chinese pipeline into TextProcessor + grammar-explain (T5-T6) (84874d6) TextProcessor gains a language='ja' param; format_components and the Claude translation path branch once on self.language to pick the Japanese or Chinese vocab pipeline/prompt. New _process_paragraph_async_zh mirrors _process_paragraph_async: jieba+ CC-CEDICT candidates in, Claude-refined JSON out, HSK level attached via get_hsk_level (the hsk key, parallel to the existing jlpt key). Chinese translation prompt added to _translate_paragraph_claude_async. EasyGoogleTranslate's source_language is now derived from `language` ('zh-CN' vs 'ja') instead of hardcoded. explain_grammar (views.py) resolves document.language from the page and picks _build_grammar_prompt_zh (new, same prompt-injection defenses as the Japanese version, reworded for Chinese grammar terms) or the existing Japanese builder accordingly. Verified structurally with mocked Claude responses (no ANTHROPIC_API_KEY in this environment): correct pipeline branch taken, correct prompt content built (Chinese wording, pinyin instructions, no Japanese leakage), correct HSK levels attached from the real vendored dataset, and the grammar prompt's injection-defense wording confirmed present for the Chinese path exactly as it already was for Japanese.
Refactoring
  • Organize results list into separate CSS and JS files (9c15003)
  • Extract inline styles from results_list.html to static/css/results-list.css - Extract inline JavaScript from results_list.html to static/js/results-list.js - Update template to use external files via {% block extra_css %} and {% block extra_js %} - Improve maintainability by separating concerns (structure, styling, functionality) - Enable better caching and code reusability - Reduce template complexity from ~790 lines to ~70 lines - Fix delete button centering and read status icon alignment - Remove duplicate positioning styles for cleaner CSS - Maintain all existing functionality: inline editing, read status toggling, delete operations