Skip to content

Releases

v0.10.4

Choose a tag to compare

@github-actions github-actions released this 19 Sep 15:40

Date: 2026-09-19

Fixed

  • The provider endpoint could not accept the value its documentation gives. The client requested an absolute /v1/chat/completions, but httpx concatenates a base URL with the request path rather than resolving the path against it. A base URL carrying /v1 — which is what OpenAI's own documentation tells you to use — therefore produced /v1/v1/chat/completions. The most natural value a user could enter was the one guaranteed to fail, and only the old default of http://localhost:11434 happened to avoid it. The request path is now relative, and the version segment is carried by the configured base URL.
    • /v1 is not an OpenAI-wide convention; it is OpenAI's own version segment. Ollama exposes its compatibility API under /v1, Zhipu uses /api/paas/v4, and DeepSeek documents a base URL of https://api.deepseek.com and serves /chat/completions with no version at all. What is common is base URL plus /chat/completions, which is also the OpenAI SDK's semantics: its default base URL already contains /v1 and it appends only the endpoint path.
    • The endpoint field now accepts either a base URL or a full endpoint URL, because provider documentation puts the two side by side. Normalization is idempotent, so a URL that already worked is unchanged.
    • Existing configurations may need updating. A base URL that worked before because it lacked a version segment still works, but http://localhost:11434 without /v1 does not reach Ollama's compatibility API. The default is now http://localhost:11434/v1.
  • A service that would not survive a reboot was never reported. Starting at boot comes from systemctl enable, a one-time step during installation. A deployment restarted the service and never enabled it, so a missed installation step left a service that worked perfectly until the machine next restarted — possibly weeks later, with no recent change to suspect and nothing in any log to connect it to. Deployments now check and warn. enabled-runtime and static count as failures despite both exiting zero: the first writes its symlink into /run, which a reboot clears, and the second means the unit cannot be enabled at all.

Changed

  • The endpoint hint and the bundle setup instructions show each provider's real form. The hint previously said to enter the base URL only and gave http://localhost:11434 as its example, which is now the one value that does not work for Ollama. The bundle templates listed https://api.openai.com, https://openrouter.ai/api and https://api.siliconflow.cn without version segments — examples that would have been wrong under the new semantics. They now show each provider's actual URL, including DeepSeek's, which has no version segment.

Added

  • AC-GEN-003-08 — The request path is appended to the base URL, path segments in the base URL are preserved, and a configured value that already contains the endpoint path is normalized rather than doubled. The spec also records why the version segment belongs to the provider rather than the client.
  • Deployment documentation on boot persistence — How to check it without rebooting, which systemctl is-enabled answers look like success but are not, and why a deployment is not a reboot: a restart inherits the running service's environment while a boot builds it from scratch, so a service can survive restarts for months and fail on the first boot.

Changed Files

  • backend/src/integrations/openai_compat.py — ENDPOINT_PATH, normalize_base_url(), relative request path.
  • backend/src/main.py — Endpoint normalization at the write boundary; default carries Ollama's /v1.
  • backend/tests/test_openai_compat.py — URL-shape matrix through a real transport, streaming parity, normalization and idempotence.
  • backend/tests/test_provider_config.py — Write-boundary normalization and version-segment preservation.
  • frontend/src/api/settings.ts, frontend/src/views/SettingsView.vue, frontend/src/composables/useI18n.ts — Default endpoint and hint text.
  • scripts/templates/README.*.txt — Provider examples with their version segments.
  • docs/specs/provider-generation.md — AC-GEN-003-01 reworded, AC-GEN-003-08 added.
  • scripts/deploy.py — boot_start_state() and report_boot_start().
  • docs/DEPLOY.md — Boot persistence section and troubleshooting entry.
  • backend/tests/test_deployment_atomicity.py — Boot-state reporting across every is-enabled answer.
  • VERSION, frontend/package.json, backend/pyproject.toml — Version bumped.

v0.10.3

Choose a tag to compare

@github-actions github-actions released this 19 Sep 10:21

Date: 2026-09-19

Added

  • Self-hosting on a machine you own -- scripts/deploy.py moves a deployment to a different revision of the repository in one command: it exports the revision, installs its dependencies, builds its interface, snapshots the database, checks the new revision against that snapshot, repoints a current symlink and restarts the service. Deployment was the step that made every other improvement hard to use, because the cost of redeploying accumulates until you stop bothering to update.
  • /api/health reports the running revision -- The response gained a revision field, supplied through OPENVOCA_REVISION by whatever started the process. After a deployment, "is it up?" and "is the new code live?" are the same question, and answering them with two calls invites checking only the first.
  • deploy/openvoca.service -- A systemd unit that runs the service from a symlink, so switching revisions never touches it. Restarts are bounded: a revision that refuses to start enters the failed state after a few attempts rather than looping and burying the reason. ProtectSystem=strict makes the code tree read-only for the service, so an accidental write fails immediately instead of surviving until the next deployment replaces it.
  • backend/src/preflight.py -- Runs a revision's own startup checks without starting it, so a revision that would refuse to serve is never switched to.
  • docs/DEPLOY.md -- The full procedure, written for whoever is on the deployment machine. Includes why the commands pass PATH through sudo explicitly.
  • docs/specs/private-deployment.md -- 27 acceptance criteria covering the distribution contract, startup checks, deployment atomicity, process supervision, and the boundary between requesting and executing an update.

Fixed

  • The portable bundle overrode the caller's data directory -- The launcher built its child environment with the bundle's own data/ last, so OPENVOCA_DATA_DIR set from outside was silently discarded. Because an update replaces the bundle directory, this made every update destructive: it would take the database with it. The data directory is now a default rather than an assignment, and the directory that actually gets used is the one created. This was the single obstacle that made any update path unsafe.
  • A database older than the models silently served broken queries -- create_all() creates missing tables but never alters existing ones, so a model that gained a column kept running against a table that never received it. The application started, the health check answered 200, static pages loaded, and the first query touching that column raised no such column. Every automated probe reported the service as healthy. Startup now compares the model's declared columns against the database and refuses to start, naming the table and the missing columns. The check is deliberately one-directional: a surplus column is what a rollback looks like, and refusing to start then would block the recovery path an operator reaches for precisely when a deployment has gone wrong.
  • The interface path was only reported when it existed -- Startup printed the frontend directory only when a build was present, and otherwise mentioned it inside a warning on stderr. The output therefore differed by environment and omitted the one line an operator reads when the interface is blank. All three paths are now always printed.
  • deploy.py failed with a traceback when a tool was unreachable -- A missing executable surfaces from subprocess as FileNotFoundError, which escaped the deployment's error handler. The operator got a stack trace instead of being told which revision was still serving and how to return to it — the one thing a failed deployment must say. The toolchain is now checked before any work begins, and the message names the PATH that was searched, because the usual cause is not an absent tool: sudo replaces PATH with its own secure_path, which contains /usr/bin but not where user-installed tools live. git is found and uv is not, which is why a deployment gets past the export and fails at the dependency install.
  • The SPA fallback test asserted nothing in CI -- The static routes were defined inside a module-level if dist exists block, so in an environment without a frontend build they did not exist as attributes, and the test's hasattr guard meant it reported success without executing an assertion. AC-SHELL-001-02 was green and untested. Registration moved into register_spa(app, dist), and the test now drives real requests through a TestClient.

Changed

  • Startup now refuses to serve a database it cannot query -- See Fixed above. This is a behaviour change rather than a cosmetic one: a revision will decline to start where it previously started and failed later. A revision that is deployed and refuses to start leaves the previous one serving.
  • The VPS deployment specification was removed -- docs/specs/deployment.md described a public deployment with a reverse proxy, TLS termination, an image registry and session-cookie authentication. It assumed a target that may never exist. Specifications should describe the path actually being taken, so it is gone rather than shelved, and three criteria from it that were environment-independent (static resources resolved from the application file, version injection, and skipping the update check when the version is unset) were carried into the private deployment specification.

Changed Files

  • scripts/deploy.py -- New; one-command deployment with an atomic symlink switch.
  • scripts/bundle.py -- OPENVOCA_DATA_DIR is a default; the directory used is the one created.
  • deploy/openvoca.service, deploy/openvoca.conf.example -- New; systemd unit and optional overrides.
  • docs/DEPLOY.md -- New; the deployment procedure.
  • docs/specs/private-deployment.md -- New; 27 criteria. docs/specs/deployment.md removed.
  • backend/src/preflight.py, backend/src/services/schema_guard.py -- New; startup checks.
  • backend/src/main.py -- Startup path reporting, schema check, register_spa(), revision in health.
  • backend/src/services/word_store.py -- database_path().
  • backend/tests/ -- test_private_deployment.py, test_deployment_atomicity.py, test_process_supervision.py added; test_static.py rewritten to exercise the routes; test_main.py updated for the health response.
  • README.md -- Self-hosting section.
  • VERSION, frontend/package.json, backend/pyproject.toml -- Version bumped.

v0.10.2

Choose a tag to compare

@github-actions github-actions released this 17 Sep 15:21

Date: 2026-09-17

Added

  • A capability layer for touch and pointer input -- The interface was designed for a mouse with a keyboard nearby, which made several controls unusable on a phone. main.css now defines the shared pieces once instead of leaving each component to reinvent them: .tap-target for controls that need a finger-sized box at every width, .tap-finger for controls that only need it under @media (hover: none) so pointer layouts keep their compact density, .pointer-only for affordances that have no meaning without a cursor, .sheet-rise for the bottom sheet animation, and .dock-divide to drop a dock's top border once the layout is wide enough that the dock no longer floats over content.
  • Bottom docks on the reading, settings and stats views -- Primary navigation and page actions now live in a fixed bottom bar rather than a header. On a phone the top of the screen is the hardest place to reach and a header scrolls away, so every page keeps its controls under the thumb and within reach at all times.

Fixed

  • Controls that were invisible or untappable on touch -- The settings and vocabulary buttons used opacity: 0 with a group-hover:opacity-100 reveal. Touch devices never hover, so those controls were present but permanently invisible. The capability layer forces them visible when no pointer is available. Alongside that, nineteen controls across the composer, reading dock and definition panel were below the 44px minimum touch target; the reading dock and definition panel sized themselves for a cursor and were tapped without aiming.
  • Vocabulary statistics were unreadable on narrow screens -- The table depended on a lemma column that collapsed to zero width at 390px, taking the word itself with it. The table is now a card list: the lemma sits on its own row with its action beside it, and the familiarity and cooldown figures wrap underneath. Cards also removed the horizontal scrolling the table needed. statsLemma is gone from the string table because the column header it labelled no longer exists.
  • The model settings overflowed on narrow screens -- Three rows exceeded the viewport (the API key row, the custom header rows, and the zoom selector) because flex items will not shrink below their intrinsic width unless explicitly allowed to. Grid tracks and flex children now use minmax(0, 1fr) and min-w-0, the API key and header rows stack below 640px, and the header row's remove action becomes an icon button rather than forcing the row wider than the screen.
  • Definition lines ran together into a single line -- The previous release introduced a wrapper element around the definition lines whose display: flex and flex-direction: column were declared inside @media (hover: none). On desktop the wrapper was therefore not a flex container at all, so its inline children flowed together: several senses rendered as one unbroken string with no separation between the Chinese and English text. The stacking rules are now unconditional and only the height cap and scrolling remain touch-specific.
  • The reading settings panel covered the text it configured -- A top panel that pushed the sentence down was replaced by a bottom sheet, so changing the font size or spacing no longer moves the text being adjusted.

Changed

  • The reading view was redesigned for touch -- The header is gone and the brand moved into the normal content flow, which returns that vertical space to the sentence. Reading controls, previously a row of icons in the header, are now a dock with the primary action separated from the secondary ones.
  • The composer controls gathered around the primary button -- Settings and stats sit on one side, about and GitHub on the other, with the generate button between them, instead of the actions appearing above the button and the metadata below it. On wide screens the arrangement is a single row; below 640px it becomes two rows with the primary action on top. ComposerCard exposes actions-before and actions-after slots so the view can place them without the component knowing which controls exist.
  • The reading settings bar became a bottom sheet -- Its three rows (font size, line spacing, theme) keep the label-left, control-right arrangement, and its buttons use the shared touch target.

Changed Files

  • frontend/src/main.css -- Capability layer: touch targets, sheet animation, dock divider.
  • frontend/src/views/HomeView.vue -- Header removed, reading dock, brand in flow, bottom sheet wiring.
  • frontend/src/views/SettingsView.vue -- Bottom dock, narrow-screen overflow fixes, icon remove action.
  • frontend/src/views/StatsView.vue -- Table replaced by a card list, bottom dock.
  • frontend/src/components/ComposerCard.vue -- actions-before / actions-after slots, touch sizing.
  • frontend/src/components/DefinitionToast.vue -- Unconditional line stacking, touch sizing for its controls.
  • frontend/src/components/ReadingSettingsBar.vue -- Top panel converted to a bottom sheet.
  • frontend/src/composables/useI18n.ts -- statsLemma removed with the column header.
  • frontend/tests/StatsView.spec.ts, frontend/tests/App.spec.ts -- Updated for the card list and dock layout.
  • VERSION, frontend/package.json, backend/pyproject.toml -- Version bumped.

v0.10.1

Choose a tag to compare

@github-actions github-actions released this 14 Sep 14:24

Date: 2026-09-14

Fixed

  • Repository links pointed at the old name -- The repository moved to Maribbit/openvoca, so every reference to the previous casing went through a redirect. The update check paid that hop on each startup, and package metadata advertised the stale name. All references and the git remote now use the canonical lowercase form.
  • Changelog extraction is now a script -- Replaced the inline shell extraction with scripts/extract_changelog.py. Passing a tag whose section is missing now fails the job instead of publishing empty notes; the previous guard was added in v0.10.0, but the extraction itself still relied on an awk range whose end pattern also matched its start line, and on head -n -1, which is GNU-only and errors on macOS. Published notes no longer carry surrounding blank lines.

Added

  • Release tooling test coverage -- The release scripts ran without any tests, which is how nine consecutive releases came to be published with empty notes while CI reported success. backend/tests/test_release_process.py now covers changelog extraction, the repair tool's decision rules, and version consistency across the three version sources.
  • scripts/sync_release_notes.py -- Repairs releases already published with empty notes, filling only empty bodies unless --all is passed. Its decision rules are factored out so they can be tested without a network.
  • docs/specs/release-process.md -- Version consistency, release note extraction, and the repair tool as acceptance criteria.

Changed

  • Version consistency is enforced -- RELEASE.md required the three version sources to stay in sync, but nothing checked it, so a bump that missed one file would ship silently mislabelled artifacts. A test now fails when they disagree.
  • CI imports the release scripts via pythonpath -- backend/pyproject.toml adds ../scripts so the suite can import tooling that operates on the repository as a whole.

Changed Files

  • scripts/extract_changelog.py -- New; section extraction with explicit failure semantics.
  • scripts/sync_release_notes.py -- New; plan_updates() factored out of main() for testability.
  • backend/tests/test_release_process.py -- New; 25 tests covering both scripts and version consistency.
  • backend/pyproject.toml -- pythonpath includes ../scripts; version bumped.
  • docs/specs/release-process.md, docs/specs/README.md -- New spec and index entry.
  • .github/workflows/ci.yml -- Release job calls the extraction script.
  • VERSION, frontend/package.json -- Version bumped.
  • backend/src/main.py, frontend/src/views/HomeView.vue, frontend/package.json, scripts/templates/, SECURITY.md -- Canonical repository URL.

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 13 Sep 11:35

Date: 2026-09-13

Breaking Changes

  • Provider API reshaped around key independence. The API key is no longer part of the endpoint/model payload, because a single field carrying both a masked read value and a writable secret caused real data loss (see Fixed below). Clients must migrate:
    • PUT /api/provider now accepts {endpoint, model, headers} only.
    • PUT /api/provider/key sets the key; empty values are rejected with 422.
    • DELETE /api/provider/key clears it. This is the only way to unset.
    • GET /api/provider returns apiKeySet and apiKeyHint instead of apiKey.
    • PUT /api/settings/provider and PUT /api/settings/provider/{key} now return 403: provider configuration has a single write path that also rebuilds the active client.
    • GET /api/settings and GET /api/settings/{namespace} exclude apiKey and headers entirely.
    • DELETE /api/settings no longer clears the provider namespace, so resetting preferences cannot discard the model configuration.

Added

  • Custom request headers for the model provider -- Some OpenAI-compatible endpoints require extra headers to route requests. OpenCode Zen, for example, returns 400 MissingSessionID without x-opencode-session, which reads like a credential or model problem rather than a missing header. Headers are configurable in a collapsible "Advanced" section, merged over the built-in ones so an explicitly configured header wins (enabling non-Bearer auth schemes), and applied to both streaming and non-streaming requests. Invalid names or values are rejected at write time: newline characters in a value would otherwise allow injecting additional headers.
  • Dedicated API key lifecycle -- The key is now independent of the endpoint and model. When set, the field shows a read-only status with an irreversible hint; changing it requires clearing first. The hint is rendered as text rather than bound to an input, so a masked value can never be saved back as the real key.
  • docs/specs/ -- Specifications became the entry point for development, with stable acceptance criteria IDs and inline test traceability.
  • Deferred criteria tracking -- docs/specs/deferred.txt records criteria that are specified but not yet implemented. The traceability gate distinguishes "planned, not built" from "built, but the test went missing", and rejects entries that already have tests so the list cannot rot.

Fixed

  • Streaming generation silently truncated -- generate_completion_stream indexed choices[0] without checking for an empty array. Providers that emit a usage-only tail chunk ({"choices": [], ...}) before [DONE] triggered an IndexError that was not caught, so the response ended with neither a complete nor an error event and the interface waited forever. The frontend showed a stalled word count with no error. Ollama never emitted such a chunk, which is why this went unnoticed.
  • Failed streams now always report -- An unexpected exception mid-stream produced no terminal event at all. Any failure now yields an error event, so a stalled generation can no longer pass for a slow one.
  • API key lost when editing the model -- Changing the endpoint or model wiped the in-memory key while the database kept it, so requests failed until a restart appeared to restore it. An empty value meant "do not change" to one code path and "set to empty" to another.
  • Database files were world-readable -- The database stores the API key and custom headers, which may carry session credentials. The file was created with the default umask, leaving it readable by other users on the same machine, and its SQLite sidecars (-journal, -wal, -shm) shared the exposure. Files are now restricted to the owner, and databases created by earlier versions are tightened on startup.
  • Frontend caches could hold session headers -- The settings cache, export and import skipped only the API key. Custom headers, which can carry session credentials, reached localStorage and exported JSON files. The sensitive-key list is now defined once and shared by all three paths.
  • Cross-platform launch configuration -- The documented dev command (fastapi dev src/main.py) failed outside Windows with ModuleNotFoundError: No module named 'src', and the VS Code launch configuration hard-coded a Windows interpreter path. The package now has explicit __init__.py files, the interpreter is resolved per platform, and development tasks are available for both servers.
  • Release notes were published empty -- The release job extracted the changelog with an awk range pattern whose start and end both matched the version heading, so the range covered only that one line and the following head -n -1 removed it, leaving a zero-byte file. Nine consecutive releases shipped with no release notes. The extraction now captures the section body explicitly and fails the job when a section is missing, instead of publishing empty notes.

Changed

  • Specs-first workflow enforced -- AI coding guidelines now require reading acceptance criteria before writing tests or code, and mandate inline Covers: annotations on new tests.
  • Traceability checker rewritten -- Reports deferred criteria separately, validates that every deferred entry exists and is still unimplemented, and prints actionable guidance.
  • CI now runs the full local check set -- ruff format --check, ruff check and the traceability gate previously ran only on developer machines, so lint drift and untraced work could reach main unflagged. Both are now enforced in CI, and the bundle job depends on the traceability result.

Changed Files

  • backend/src/integrations/openai_compat.py -- extra_headers support, header merge order, empty-choices tolerance.
  • backend/src/main.py -- Provider write paths, _reload_provider(), header validation and persistence, stream error reporting.
  • backend/src/services/settings_store.py -- PROTECTED_NAMESPACES, delete_setting(), protected-namespace-aware clear.
  • backend/src/services/word_store.py -- Restrictive umask and _restrict_db_permissions().
  • backend/tests/test_provider_config.py, backend/tests/test_openai_compat.py, backend/tests/test_main.py, backend/tests/test_word_store.py -- Coverage for the above.
  • frontend/src/api/settings.ts, frontend/src/views/SettingsView.vue, frontend/src/composables/useI18n.ts -- Key lifecycle, header editing.
  • frontend/src/composables/useSettings.ts -- Shared sensitive-key list.
  • frontend/tests/SettingsView.spec.ts, frontend/tests/useSettings.spec.ts -- Coverage for the above.
  • docs/specs/ -- New specs, deferred list, and index corrections.
  • scripts/check_traceability.py -- Deferred-aware rewrite.
  • .vscode/, backend/README.md -- Cross-platform interpreter, launch and task configuration.

v0.9.9

Choose a tag to compare

@github-actions github-actions released this 09 May 10:27

Date: 2026-05-09

Fixed

  • Reading route persistence -- Navigating from Reading to Settings or Stats and back now preserves the current generated sentence instead of resetting to the Composer.
  • Live language/settings refresh -- Cached Reading state now refreshes visible copy and reading display settings when returning from Settings, without accidentally saving route-activation defaults.
  • Stats editing clarity -- The destructive delete action has moved out of the active edit action cluster and into the expanded row details as a labeled danger action.
  • Stats numeric alignment -- Familiarity and cooldown columns now use fixed centered columns and text-mode numeric inputs so values stay aligned with headers and do not shift during editing.

Changed

  • Settings and Stats navigation -- The top headers are now sticky, slimmer, and separated from scrolled content with a subtle divider so Back to Reading remains available on long pages.

Changed Files

  • frontend/src/App.vue -- Cached HomeView with Vue KeepAlive.
  • frontend/src/views/HomeView.vue -- Paused route-specific global listeners while cached/inactive and refreshed reading UI state on activation.
  • frontend/src/composables/useI18n.ts -- Made locale resolution reactive to the shared settings store.
  • frontend/src/composables/useSettings.ts -- Preloaded the local settings cache before first render so cached views initialize from persisted settings.
  • frontend/src/views/SettingsView.vue -- Added the compact sticky header treatment.
  • frontend/src/views/StatsView.vue -- Added the compact sticky header, moved delete into expanded details, and aligned numeric columns/inputs.
  • frontend/tests/App.spec.ts -- Added route persistence and language refresh coverage.
  • frontend/tests/StatsView.spec.ts -- Added Stats editing and numeric alignment coverage.

v0.9.8

Choose a tag to compare

@github-actions github-actions released this 09 May 02:55

Date: 2026-05-09

Added

  • Current-sentence lemma correction fallback -- Users can correct the lemma used for dictionary lookup and feedback before submitting progress. The correction is available from the Definition Toast and Progress Summary Modal, applies only to the current sentence, and is not persisted as a global alias.
  • Color system guidance -- Added frontend color-token guidance to reduce light/dark and palette regressions.

Changed

  • Definition Toast lemma display polished -- The lemma label is localized (lemma / 词元), hyphenated lemmas such as state-of-the-art are accepted, and duplicate word/lemma rows are avoided when both values match.
  • Progress Summary Modal theme colors fixed -- Level-delta badges and modal borders now use theme-aware ink/surface tokens instead of hard-coded dark gray colors.
  • Review Progress button refined -- The reading-page summary button now sits closer to the sentence, uses a subtler theme-tinted neutral fill, and only shows shadow on hover.

Changed Files

  • frontend/src/composables/useLemmaOverrides.ts -- Added the current-sentence lemma override mechanism.
  • frontend/src/views/HomeView.vue -- Routed dictionary lookup, draft feedback, and final submit through corrected lemmas; refined Review Progress spacing and button styling.
  • frontend/src/components/DefinitionToast.vue -- Added lemma editing, localized lemma display, and duplicate display suppression.
  • frontend/src/components/ProgressSummaryModal.vue -- Added lemma editing and theme-aware level-delta styling.
  • frontend/src/composables/useI18n.ts -- Added lemma correction labels in English and Chinese.
  • frontend/tests/ -- Added and expanded coverage for lemma overrides, toast editing, modal editing, and corrected feedback submission.
  • frontend/README.md -- Documented frontend color-system usage rules.
  • design/Implementation.md, design/UI_Design.md, design/Algorithm.md -- Documented the simplified non-persistent lemma correction fallback design.

v0.9.7

Choose a tag to compare

@github-actions github-actions released this 02 May 12:34

Date: 2026-05-02

Added

  • POST /api/feedback/draft endpoint -- Simulates user feedback to calculate accurate vocabulary level changes without mutating the database.

Changed

  • Replaced Hold-to-Submit with Progress Summary Modal -- Reading interaction has been simplified. Instead of holding the "Continue" button, tapping "Continue" (or Spacebar) now opens a modal showing precise vocabulary transitions.
  • Feedback processing refactored -- The backend apply_feedback logic now completely wraps around the simulation output of draft_feedback, ensuring mathematical consistency.
  • Empty state improved -- If a sentence has no vocabulary updates during the feedback draft, the Progress Summary modal cleanly handles the empty state and asks for user confirmation.
  • Loading UI for Async Events -- Every single asynchronous interaction on the frontend (Sentence generating, Target Word suggestion loading, definition fetching, Progress Summary calculating, and File parsing on the Stats View) now comes with responsive animate-spin Tailwind loaders to significantly improve perceived usability overhead during delays.
  • Stats View Edit Workflow Overhaul -- Eliminated chaotic direct-input lists making numbers too easy to misclick. Added row-level specific toggle workflows with precise "Edit Row/Done Editing" and better icon layouts.
  • Deleted Icon overhaul -- Replaced vague "X" interface delete marker across Stats View with a standard clear trash-can icon specifically bound to active edit rows. Visibiliy on dark mode updated to pass contrast limits.
  • Legacy copy removed -- Renamed nextSentenceHint to reviewProgressBtn and updated translations for "Review Progress". Fixed 'new' word UI display state.

Changed Files

  • backend/src/services/word_store.py -- draft_feedback added; apply_feedback refactored to reuse it.
  • backend/src/main.py -- Added submit_feedback_draft route.
  • backend/tests/test_word_store.py -- Added explicit tests for untouched and manipulated word targets.
  • frontend/src/api/reading.ts -- Upgraded to support LevelDelta payload fetching.
  • frontend/src/views/HomeView.vue -- Refactored manual level-up prediction into strict backend draft mapping. Remove manual logic skip filters.
  • frontend/src/components/ProgressSummaryModal.vue -- Exact transitions, robust NEW state, and added empty state.
  • frontend/src/composables/useI18n.ts -- Added progressEmpty translation. Removed legacy releaseHint.
  • frontend/src/components/HoldButton.vue -- Removed.

v0.9.6

Choose a tag to compare

@github-actions github-actions released this 19 Apr 07:40

Date: 2026-04-19

Breaking Changes

  • POS removed from word store -- The pos field has been completely removed from WordRecord. The primary key is now the lemma alone. All API routes, request/response models, CSV export/import, and the frontend have been updated accordingly. Legacy CSV files with a pos column are still accepted on import (the column is silently ignored).

Changed

  • API routes simplified -- PATCH /api/vocabulary/{lemma}/{pos} → PATCH /api/vocabulary/{lemma}; DELETE /api/vocabulary/{lemma}/{pos} → DELETE /api/vocabulary/{lemma}.
  • Feedback payload simplified -- FeedbackRequest.targetWords and markedWords changed from WordPosEntry[] objects (lemma + pos) to plain string[] of lemmas.
  • CSV export -- The pos column is no longer exported. New column order: lemma,level,cooldown,first_seen,last_seen,last_context,seen_count.
  • CSV import -- Only lemma is required. The pos column is accepted but ignored for backward compatibility with v0.9.5 exports.
  • Stats view -- POS column removed from vocabulary table.
  • pick_target_words -- With lemma as the sole PK, the deduplication loop is replaced by a direct .limit() query.

Changed Files

  • backend/src/services/word_store.py -- WordRecord.pos removed; _find_record() removed; apply_feedback(), update_word_record(), delete_word_record(), import_vocabulary() simplified to lemma-only.
  • backend/src/main.py -- WordPosEntry model removed; FeedbackRequest uses list[str]; WordRecordOut drops pos; routes use /{lemma} instead of /{lemma}/{pos}; CSV export drops pos column.
  • frontend/src/api/reading.ts -- WordPosEntry removed; FeedbackRequest and WordRecordOut simplified.
  • frontend/src/views/HomeView.vue -- Feedback construction simplified to lemma strings.
  • frontend/src/views/StatsView.vue -- POS column and wordKey() helper removed.
  • frontend/src/composables/useI18n.ts -- pos removed from i18n type and all translation maps.
  • backend/tests/test_algorithm.py -- All tests updated to use lemma strings instead of (lemma, pos) tuples. POS-specific tests removed.
  • backend/tests/test_word_store.py -- All tests updated: apply_feedback calls use list[str]; update_word_record/delete_word_record drop pos; import tests use lemma-only rows with legacy pos backward-compat test.
  • backend/tests/test_main.py -- API tests updated: feedback payloads use string[]; routes use /{lemma}; CSV assertions match new column layout; legacy pos CSV import test added.

v0.9.5

Choose a tag to compare

@github-actions github-actions released this 19 Apr 05:07

Date: 2026-04-19

Bug Fixes

  • Lazy-load spaCy model -- The spaCy NLP model (en_core_web_sm) is now loaded on first sentence generation instead of at module import time. This allows the /api/health endpoint to respond immediately on startup, preventing the bundled launcher from timing out on cold first launches.
  • Favicon served in bundled mode -- The SPA catch-all route now checks whether a requested file exists in the frontend dist directory before falling back to index.html. Fixes the missing favicon in bundled browser tabs.
  • Target words lemma mismatch safety net -- When the LLM uses a derived form (e.g. "analysis" instead of the target "analyze"), the tokenizer may not match it back to the original DB record, causing the word to never advance and be picked indefinitely. Feedback now includes originalTargets (the raw lemmas from pick_target_words), and apply_feedback ensures every originally-picked word advances unless the user explicitly marked it unknown.

Changed

  • Test file split -- Extracted SRS algorithm tests (level progression, cooldown queue, POS matching, pick logic, full simulation, and original_targets safety net) from test_word_store.py into a dedicated test_algorithm.py.

Changed Files

  • backend/src/services/tokenizer.py -- spacy.load() moved from module scope to lazy _get_nlp() helper.
  • backend/src/main.py -- SPA fallback checks for real files before serving index.html; FeedbackRequest model gains optional originalTargets field; submit_feedback passes it through.
  • backend/src/services/word_store.py -- apply_feedback() accepts original_targets: list[str]; safety-net loop ensures unmatched picked words still advance.
  • frontend/src/api/reading.ts -- FeedbackRequest interface gains optional originalTargets field.
  • frontend/src/views/HomeView.vue -- Stores response.words and sends them as originalTargets in feedback.
  • backend/tests/test_algorithm.py -- New file: 15 algorithm-focused tests extracted from test_word_store.py plus 3 new original_targets tests.
  • backend/tests/test_word_store.py -- Trimmed to 33 CRUD / metadata / import tests.