Skip to content

Remember the last sort for each top collection (#415) - #831

Merged
ajslater merged 7 commits into
developfrom
claude/codex-feature-planning-qkzikc
Aug 30, 2026
Merged

Remember the last sort for each top collection (#415)#831
ajslater merged 7 commits into
developfrom
claude/codex-feature-planning-qkzikc

Conversation

@ajslater

Copy link
Copy Markdown
Owner

Closes #415.

Sorting is one global setting today, so sorting Issues by added time leaves the
Publishers list in added-time order too. Each top collection now keeps the sort
it was last browsed with.

Per the decisions on the plan: a collection with nothing remembered keeps
whatever sort arrives with it (so nothing changes until you sort something),
clearing a search restores the sort the search replaced, the field is spelled
out as collection_order_memory / collectionOrderMemory, and there is no off
switch — a settings reset clears it, and the map only ever holds sorts you
actually used.

How it works

SettingsBrowser.collection_order_memory is a map of top collection to the sort
it was left in. It follows table_columns, the other per-top-collection map: a
JSON column in DIRECT_KEYS, so loading, saving, resetting and saved-view
cloning carry it with no further wiring. It is empty for every existing row,
which reads as "nothing customized yet".

The browser store files the outgoing sort and restores the incoming one when the
user changes collections, and does the same pair around a search. The memory
rides the settings the browse page request already echoes, so there is no new
request to persist it.

Two things the store cannot see needed a server-side counterpart: a url whose
collection forces a different top (a /browse/folders bookmark, returning from
the reader) and a redirect after folder view is turned off. Both go through the
same file-and-restore helper — without it, one collection's sort leaked into
another and would later be filed under the wrong collection, overwriting a real
memory.

search_score is never stored, since relevance ordering only means anything
while its search runs, and a restore never overrules a sort the payload is
already asking for.

Also here:

  • The settings save request has been a no-op: it passed the settings as a
    params wrapper, so the body was {"params": {...}} and every top-level key
    was unknown. Nothing was lost — the browse page request that follows persists
    the same settings — but the save request itself did nothing. Fixed first, on
    its own commit.
  • _get_browser_order_defaults had no callers; set_order_by_default is the
    copy that runs.

OPDS is untouched: it has its own settings row and its serializer never gains
the field.

Verification

  • Full backend suite: 1020 passed. New tests/test_collection_order_memory.py
    covers the field wiring, the lenient validator, the file-and-restore helper,
    and the HTTP round trips — PATCH, the browse page query params that actually
    persist the map, reset, saved views, and a url that forces a different top
    collection. tests/test_user_data_restore.py gains sidecar round-trip and
    older-backup cases.
  • Frontend: frontend/tests/unit/browser-store-order-memory.test.js (11 tests)
    passes alongside the existing search-clear tests. The rest of the suite passes
    except stats-tab.test.js, which fails identically on a clean develop
    checkout and is unrelated to this change.
  • ruff check, ruff format --check, eslint and prettier are clean.

One caveat on the frontend run: this sandbox has Node 22 while the project
builds on Node 26, and src/datetime.js constructs Intl.DurationFormat at
import, which Node 22 lacks. I stubbed it locally to run the suite and reverted
the stub; nothing in the diff touches it, and CI's Node 26 has the real thing.


Generated by Claude Code

claude added 7 commits August 30, 2026 07:22
updateSettings passed the settings as a `params` wrapper, which xior sends
as the request body `{"params": {...}}`. The endpoint validates the body's
top level keys, so every one of them was unknown, the update validated as
empty and the request saved nothing.

Nothing was actually lost: the browse page request that follows a settings
change carries the same settings as query params and persists them. The
save request itself has just been doing nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
Sorting is one global setting today, so sorting issues by added time
leaves the publisher list in added-time order too. Issue #415 asks each
top collection to keep the sort it was last browsed with.

Add SettingsBrowser.collection_order_memory, a map of top collection to
the sort it was left in. It follows table_columns, the other per top
collection map: a JSON column in DIRECT_KEYS, so loading, saving,
resetting and saved-view cloning all carry it without further wiring.

The map round-trips out of stored settings, so the validator cleans
rather than rejects: unknown collections and sorts that no longer exist
are dropped with a warning instead of failing the whole browse page.
search_score is never stored, since relevance ordering only means
anything while its search is running.

Empty for every existing row, which reads as "nothing customized yet"
and keeps today's carry-the-sort-over behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
Switching top collections now files the sort the old one is left in and
hands back the sort the new one was last browsed with. A collection with
nothing filed keeps whatever sort arrives with it, so nothing changes
until you sort something.

Searching does the same pair: entering a search files the sort it
displaces and clearing the search puts it back, instead of always
landing on sort_name.

Whether a payload is the user changing collections is decided before the
validators run, since they inject an orderBy of their own that would
otherwise look like a payload that already carries a sort. Payloads that
really do carry one -- settings loads, saved views, redirects -- keep it.

The memory rides the settings the browse page request already echoes, so
there is no new request to persist it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
The browser store handles collection switches the user makes, but the
server makes some of its own: a url whose collection forces a different
top, or a redirect after folder view is turned off. Those bypass the
store, so the old collection's sort leaked into the new one and would
later be filed under the wrong collection, overwriting a real memory.

Apply the same file-and-restore in the settings GET when validation
rewrites the top collection, and in raise_redirect, which is the one
place every settings-mask redirect passes through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
The sidecar adds columns it is missing when it opens, so a backup written
before this column restores an empty memory rather than failing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
Backend: the field's wiring, the lenient validator, the file-and-restore
helper, and the HTTP round trips -- PATCH, the browse page query params
that actually persist the map, reset, saved views, and a url that forces
a different top collection.

Frontend: switching collections, coming back to one, a collection with
nothing filed, payloads that carry their own sort, and the sort a search
displaces and gives back.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
_get_browser_order_defaults has no callers; set_order_by_default is the
copy that runs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
@ajslater
ajslater marked this pull request as ready for review August 30, 2026 07:25
@ajslater
ajslater merged commit c571bc9 into develop Aug 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants