Repository navigation
Releases
Releases · Maribbit/openvoca
v0.10.4
Date: 2026-09-19
Fixed
- The provider endpoint could not accept the value its documentation gives. The client requested an absolute
/v1/chat/completions, buthttpxconcatenates 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 ofhttp://localhost:11434happened to avoid it. The request path is now relative, and the version segment is carried by the configured base URL./v1is 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 ofhttps://api.deepseek.comand serves/chat/completionswith 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/v1and 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:11434without/v1does not reach Ollama's compatibility API. The default is nowhttp://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-runtimeandstaticcount 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:11434as its example, which is now the one value that does not work for Ollama. The bundle templates listedhttps://api.openai.com,https://openrouter.ai/apiandhttps://api.siliconflow.cnwithout 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-enabledanswers 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-01reworded,AC-GEN-003-08added.scripts/deploy.py—boot_start_state()andreport_boot_start().docs/DEPLOY.md— Boot persistence section and troubleshooting entry.backend/tests/test_deployment_atomicity.py— Boot-state reporting across everyis-enabledanswer.VERSION,frontend/package.json,backend/pyproject.toml— Version bumped.
v0.10.3
Date: 2026-09-19
Added
- Self-hosting on a machine you own --
scripts/deploy.pymoves 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 acurrentsymlink 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/healthreports the running revision -- The response gained arevisionfield, supplied throughOPENVOCA_REVISIONby 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 thefailedstate after a few attempts rather than looping and burying the reason.ProtectSystem=strictmakes 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 passPATHthroughsudoexplicitly.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, soOPENVOCA_DATA_DIRset 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 answered200, static pages loaded, and the first query touching that column raisedno 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.pyfailed with a traceback when a tool was unreachable -- A missing executable surfaces fromsubprocessasFileNotFoundError, 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 thePATHthat was searched, because the usual cause is not an absent tool:sudoreplacesPATHwith its ownsecure_path, which contains/usr/binbut not where user-installed tools live.gitis found anduvis 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 existsblock, so in an environment without a frontend build they did not exist as attributes, and the test'shasattrguard meant it reported success without executing an assertion.AC-SHELL-001-02was green and untested. Registration moved intoregister_spa(app, dist), and the test now drives real requests through aTestClient.
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.mddescribed 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_DIRis 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.mdremoved.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.pyadded;test_static.pyrewritten to exercise the routes;test_main.pyupdated for the health response.README.md-- Self-hosting section.VERSION,frontend/package.json,backend/pyproject.toml-- Version bumped.
v0.10.2
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.cssnow defines the shared pieces once instead of leaving each component to reinvent them:.tap-targetfor controls that need a finger-sized box at every width,.tap-fingerfor controls that only need it under@media (hover: none)so pointer layouts keep their compact density,.pointer-onlyfor affordances that have no meaning without a cursor,.sheet-risefor the bottom sheet animation, and.dock-divideto 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: 0with agroup-hover:opacity-100reveal. 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.
statsLemmais 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)andmin-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: flexandflex-direction: columnwere 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.
ComposerCardexposesactions-beforeandactions-afterslots 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-afterslots, 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--statsLemmaremoved 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
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 anawkrange whose end pattern also matched its start line, and onhead -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.pynow 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--allis 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.mdrequired 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.tomladds../scriptsso 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 ofmain()for testability.backend/tests/test_release_process.py-- New; 25 tests covering both scripts and version consistency.backend/pyproject.toml--pythonpathincludes../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
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/providernow accepts{endpoint, model, headers}only.PUT /api/provider/keysets the key; empty values are rejected with 422.DELETE /api/provider/keyclears it. This is the only way to unset.GET /api/providerreturnsapiKeySetandapiKeyHintinstead ofapiKey.PUT /api/settings/providerandPUT /api/settings/provider/{key}now return 403: provider configuration has a single write path that also rebuilds the active client.GET /api/settingsandGET /api/settings/{namespace}excludeapiKeyandheadersentirely.DELETE /api/settingsno 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 MissingSessionIDwithoutx-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.txtrecords 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_streamindexedchoices[0]without checking for an empty array. Providers that emit a usage-only tail chunk ({"choices": [], ...}) before[DONE]triggered anIndexErrorthat was not caught, so the response ended with neither acompletenor anerrorevent 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
errorevent, 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
localStorageand 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 withModuleNotFoundError: No module named 'src', and the VS Code launch configuration hard-coded a Windows interpreter path. The package now has explicit__init__.pyfiles, 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
awkrange pattern whose start and end both matched the version heading, so the range covered only that one line and the followinghead -n -1removed 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 checkand the traceability gate previously ran only on developer machines, so lint drift and untraced work could reachmainunflagged. Both are now enforced in CI, and the bundle job depends on the traceability result.
Changed Files
backend/src/integrations/openai_compat.py--extra_headerssupport, header merge order, empty-choicestolerance.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
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-- CachedHomeViewwith VueKeepAlive.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
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 asstate-of-the-artare 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
Date: 2026-05-02
Added
POST /api/feedback/draftendpoint -- 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_feedbacklogic now completely wraps around the simulation output ofdraft_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-spinTailwind 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
nextSentenceHinttoreviewProgressBtnand updated translations for "Review Progress". Fixed 'new' word UI display state.
Changed Files
backend/src/services/word_store.py--draft_feedbackadded;apply_feedbackrefactored to reuse it.backend/src/main.py-- Addedsubmit_feedback_draftroute.backend/tests/test_word_store.py-- Added explicit tests for untouched and manipulated word targets.frontend/src/api/reading.ts-- Upgraded to supportLevelDeltapayload 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, robustNEWstate, and added empty state.frontend/src/composables/useI18n.ts-- AddedprogressEmptytranslation. Removed legacyreleaseHint.frontend/src/components/HoldButton.vue-- Removed.
v0.9.6
Date: 2026-04-19
Breaking Changes
- POS removed from word store -- The
posfield has been completely removed fromWordRecord. 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 aposcolumn 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.targetWordsandmarkedWordschanged fromWordPosEntry[]objects (lemma + pos) to plainstring[]of lemmas. - CSV export -- The
poscolumn is no longer exported. New column order:lemma,level,cooldown,first_seen,last_seen,last_context,seen_count. - CSV import -- Only
lemmais required. Theposcolumn 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.posremoved;_find_record()removed;apply_feedback(),update_word_record(),delete_word_record(),import_vocabulary()simplified to lemma-only.backend/src/main.py--WordPosEntrymodel removed;FeedbackRequestuseslist[str];WordRecordOutdropspos; routes use/{lemma}instead of/{lemma}/{pos}; CSV export dropsposcolumn.frontend/src/api/reading.ts--WordPosEntryremoved;FeedbackRequestandWordRecordOutsimplified.frontend/src/views/HomeView.vue-- Feedback construction simplified to lemma strings.frontend/src/views/StatsView.vue-- POS column andwordKey()helper removed.frontend/src/composables/useI18n.ts--posremoved 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_feedbackcalls uselist[str];update_word_record/delete_word_recorddroppos; import tests use lemma-only rows with legacy pos backward-compat test.backend/tests/test_main.py-- API tests updated: feedback payloads usestring[]; routes use/{lemma}; CSV assertions match new column layout; legacy pos CSV import test added.
v0.9.5
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/healthendpoint 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 frompick_target_words), andapply_feedbackensures 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.pyinto a dedicatedtest_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 servingindex.html;FeedbackRequestmodel gains optionaloriginalTargetsfield;submit_feedbackpasses it through.backend/src/services/word_store.py--apply_feedback()acceptsoriginal_targets: list[str]; safety-net loop ensures unmatched picked words still advance.frontend/src/api/reading.ts--FeedbackRequestinterface gains optionaloriginalTargetsfield.frontend/src/views/HomeView.vue-- Storesresponse.wordsand sends them asoriginalTargetsin feedback.backend/tests/test_algorithm.py-- New file: 15 algorithm-focused tests extracted fromtest_word_store.pyplus 3 neworiginal_targetstests.backend/tests/test_word_store.py-- Trimmed to 33 CRUD / metadata / import tests.