Skip to content

LWT 3.6.0: Imported terms you can read, and forms that became an API

Latest

Choose a tag to compare

@HugoFara HugoFara released this 30 Aug 20:12
· 17 commits to main since this release
4f7d745

A dictionary import no longer leaves your texts unreadable, the feed wizard stops reloading the page four times over, and checking a text keeps whatever you were editing. Most of the work underneath is the move off same-origin form POSTs.

A dictionary import no longer leaves every word unmarked

Importing a dictionary created one term per entry and never linked them to the words in your texts. The reader decides a word is unknown from that link rather than from whether the term exists, so imported terms stayed invisible: the word kept its unmarked colour, and marking it known failed outright — HTTP 500 from the term form, and silently nothing from the status shortcut. Because an import covers essentially the whole language, this touched more or less every word in every text.

The import links what it creates now, and a migration repairs the terms already imported, so an install that has been through an import comes back correct on upgrade rather than needing the import run again. Adding a term that turns out to already exist updates it instead of failing. Imported terms also get the word count they were missing, without which every text added after an import came back unmarked all over again. (#283)

The feed wizard is one page

Four steps, four POSTs, and between them the parsed feed, the fetched article and your picked selectors lived in $_SESSION — the selectors as <li> markup that the next page parsed back out of the previous page's DOM.

The four steps are panels of one page now. The article is fetched once and shared between the steps that need it rather than re-fetched on every hop, and reopening a saved feed is a plain link: /feeds/wizard?edit_feed={id}.

The wizard also stops asking for the language it already knows. The navbar has been asked, and the wizard's own curated and manual paths had always saved under it without asking; the last step, the manager's feed form and the feed list all follow it now. A feed you are editing keeps whichever language it was saved with.

Checking a text no longer costs you your edits

The check page and the editor's Check button both posted a form and got back a whole server-rendered document, so checking from the editor threw away everything else in it. The report is fetched and rendered in place now — neither leaves the page it was on.

The forms became an API

Underneath all of the above, the forms that posted back to their own page are gone. Five endpoints take their place:

POST /api/v1/texts/check              report on a parsed text without saving it
POST /api/v1/feeds/wizard/preview     read an RSS feed's article list
POST /api/v1/feeds/wizard/article     fetch one of those articles
PUT  /api/v1/texts/archived/{id}      save an archived text
POST /api/v1/local-dictionaries       create a local dictionary

This matters past tidiness: the interface works against a configurable API base URL now rather than the page origin, which is what a separate frontend needs.

A failed write no longer reports success

API handlers signal failure by returning an error payload rather than by throwing, and the routers handed that straight to Response::success(). The request came back HTTP 200, and the interface — seeing a success — did nothing at all: no message, no retry. Such a payload is recognised and sent as 400 now.

The client had the matching half of the bug: it only ever looked for message on a failed request, while the API sends error, so even a well-formed error surfaced as a bare "HTTP 400: Bad Request". It reads both. (#284)

A repair that could not repair

addMissingForeignKeys() adds a constraint with a plain ALTER TABLE, and InnoDB validates the existing rows when it does. That contradicts the premise of the repair: an install is missing a constraint precisely because it spent time without one, so the rows the constraint would have prevented are already there. InnoDB answered errno 1452 and refused exactly the keys most worth putting back — and the failure was only logged, so the constraint was lost with nothing saying so.

It worked wherever it was actually reached, because every caller happened to set FOREIGN_KEY_CHECKS = 0 around its own migration run. Nothing established that; the method inherited it from whichever caller was on the stack. It establishes it itself now.

Security

Creating a local dictionary from the index's quick-create form passed the URL's language id straight through, while the API path sitting beside it checked that the language belonged to you. LdLgID carries no foreign key, so on a multi-user install a POST naming another user's language id planted a dictionary row pinned to their language. Create goes through the API now, which removes the unguarded path rather than duplicating the guard. Single-user installs — the default — were never affected.

npm audit also flagged nanoid below 3.3.18 (GHSA-2v37-7h3g-55p8). It reaches the project through the Vite build chain rather than anything LWT serves, so nothing was exposed at runtime, but the build no longer pulls a flagged version.

Dependencies

Refreshed within the ranges already declared — Alpine 3.15 to 3.17, Lucide 1.28 to 1.37 and Vite 8.2.0 to 8.2.2 on the frontend; Guzzle 7.15.2 to 7.15.5 and Symfony Console 7.4.15 to 7.4.17 on the backend. Three overrides the ecosystem has outgrown are dropped, and the three that remain are load-bearing rather than leftovers.


Upgrading is the usual docker compose pull && docker compose up -d --force-recreate. This release carries a migration that repairs dictionary-imported terms, so take a dump first, as always. Full detail in CHANGELOG.md.