Second route move from the plan in Discussion #1536: the four settings
routes (`GET /api/settings`, `GET` and `PUT /api/settings/<tab>`, and
`POST /api/settings/<tab>/action/<key>`) go from `main.py` to
`core/settings_routes.py`, registered with
`register_settings_routes(app, login_required)`. `main.py` is 143 lines
shorter. These handlers call `settings_registry` directly, so nothing
else has to be passed in. The handler bodies are the same code, and the
only difference I found after de-indenting them is that `ruff format`
wrapped one signature that no longer fits on a line.
Same two-commit layout as the onboarding one (#1594). The first commit
adds 27 tests for these routes and they pass on current `main` before
anything moves. They pin the 404 for an unknown tab, the 400 when a
`PUT` body is not a JSON object (a list, a string, a number, or no body
at all), the "No changes to save" answer for an empty object, the
unsaved form values reaching an action, the result passed through with a
400 on failure, the 500 with the error text on an operational error, a
401 without a session, a 403 for a signed-in non-admin on all four
routes, and open access with no auth. The second commit is the move and
no test changed in it. The admin check for these paths lives in
`login_required`, so the same decorator is passed in.
What I checked:
- The Flask URL map (every rule, its methods and its endpoint name) is
identical before and after, all 77 rules.
- Full suite, ruff and basedpyright are clean.
- I ran the app from this branch and opened Settings in a browser: the
modal lists every tab and the General tab shows its values, which come
from the moved `GET`. Over HTTP, a `PUT` of a new search page title
saved and read back, a list body got the 400 and an unknown tab the 404.
As with #1594, log lines for these routes now come from
`shelfmark.core.settings_routes`, and the error tuple is a local copy
(`_SETTINGS_ERRORS`) so the module does not import `main`. The two PRs
touch neighbouring lines of `main.py` and I checked that they merge
together without a conflict, in either order.