Add a "date added" sort wherever it makes sense #46

Open
opened 2026-08-18 05:57:03 +00:00 by logan · 2 comments
Collaborator

Report

There should be a "date added" sort option on the views where it is relevant.

Findings

  • Sorting is driven by <page-header>'s sortOptions per host, so this is one option added to several hosts rather than one change.
  • The column has to exist and be populated: check that audio_files carries a created/added timestamp and that album/artist rollups can derive one (an album's date added = the earliest/latest of its files — pick one and write it down).
  • The Artists view currently has no keys but name at all (separate issue) and library.Artist carries no counts or dates, so it needs the query extending either way.

Direction

Add it to Tracks, Albums, Artists and Genres, plus the playlist views where a track's "added to playlist" date is a different thing worth distinguishing in the label. Default direction should be newest-first, since "what did I just add" is the reason anyone wants it.

**Report** There should be a "date added" sort option on the views where it is relevant. **Findings** - Sorting is driven by `<page-header>`'s `sortOptions` per host, so this is one option added to several hosts rather than one change. - The column has to exist and be populated: check that `audio_files` carries a created/added timestamp and that album/artist rollups can derive one (an album's date added = the earliest/latest of its files — pick one and write it down). - The Artists view currently has no keys but name at all (separate issue) and `library.Artist` carries no counts or dates, so it needs the query extending either way. **Direction** Add it to Tracks, Albums, Artists and Genres, plus the playlist views where a track's "added to playlist" date is a *different* thing worth distinguishing in the label. Default direction should be newest-first, since "what did I just add" is the reason anyone wants it.
logan self-assigned this 2026-09-04 15:05:06 +00:00
logan added the
Status
In Progress
label 2026-09-04 15:05:07 +00:00
Author
Collaborator

⟦loop⟧ claim for #46, paired with #32 per #73 Phase 5 ("do together, both need the same backend columns").

Branch feat/46-32-date-added-and-artist-sort. Approach: add a created/added timestamp to audio_files plus album/artist rollups, widen the albums/artists queries to return it, add the date-added sort option to Tracks/Albums/Artists/Genres (newest-first default, since "what did I just add" is the use), and add #32's artist sort keys in the same pass. Plan follows as a comment; no push until the implementation leg.

⟦loop⟧ claim for #46, paired with #32 per #73 Phase 5 ("do together, both need the same backend columns"). Branch `feat/46-32-date-added-and-artist-sort`. Approach: add a created/added timestamp to `audio_files` plus album/artist rollups, widen the albums/artists queries to return it, add the date-added sort option to Tracks/Albums/Artists/Genres (newest-first default, since "what did I just add" is the use), and add #32's artist sort keys in the same pass. Plan follows as a comment; no push until the implementation leg.
Author
Collaborator

⟦loop⟧ Implementation plan for #46 + #32 — one pass on feat/46-32-date-added-and-artist-sort, per #73 Phase 5 and the scope agreed on #32 (2026-08-21).

Change, one sentence. Add an import timestamp to audio_files, widen the album/artist/genre/track projections with it plus the count/duration/play rollups, and give every library view real comparator-based sort options on cover-grid's existing SortOption & { comparator } model — date-added (newest-first) everywhere (#46) and the Artists view's missing keys (#32).

Backend (schema change → follow references/schema-change.md)

  • sql/schemas/audio_files.sql: new added_at DATETIME NOT NULL, no default — a missed insert path should fail loudly, not silently get epoch. One edit + make generate. audio_files is Kind: Owned, so staleshape drops+rescans existing DBs — the designed path; no datamap change (the MIXED KIND note already covers the authored neighbours).
  • sql/queries/audio_files.sql: CreateAudioFile sets added_at at insert only. No update path may touch it — a rescan must not reset date-added; check UpdateAudioFileTags/UpdateAudioFileStat stay silent on it.
  • track_metadata view + library.Track.AddedAt (string via trackFromRow, mirroring LastPlayed).
  • GetAlbumArtists (artists.sql), GetAlbums (albums.sql), GetAllGenresWithCounts (genres.sql): EXISTS/DISTINCT → JOIN audio_files + GROUP BY, gaining track/album counts, duration (ms int64), play count, last played, added-at. The library filter must apply inside the aggregates.
  • GetPlaylistTracksWithMetadata (playlists.sql): add tm.added_at — the join already exists.
  • home.sql HomeRecentlyAddedAlbums: MAX(af.id) → MAX(af.added_at); semantics agree by construction.
  • Rollup definitions in comments beside the queries (per the scope agreement): date added = MAX over the entity's files (matches the MAX(af.id) stand-in — completing an album re-surfaces it as recent); last played = MAX; artist play count = SUM ("who do I listen to most").
  • backend/tracklist: AllColumnIDs + DefaultColumns gain dateAdded (default-visible, so the sort exists out of the box). database/testhelper.go: TestTrack gets a settable AddedAt.

Frontend

  • page-header: SortOption gains optional defaultDirection, emitted on field change — that is "newest-first by default".
  • cover-grid-types.ts: extend ALBUM_SORT_OPTIONS (+date added, track count, duration, play count, last played).
  • artists-view: ARTIST_SORT_OPTIONS becomes a comparator array on the same model; add sortField to recomputeArtistCaches' memo key and keep the per-render arrow functions — card-grid-repaint.test.ts is the guard.
  • genres-view: inline ternary → comparator array. track-list/columns.ts: dateAdded column (renderer + comparator, configurable: true); both column lists must agree (#197's two-list trap; settings-column-list.test.ts sweeps). Not in PHONE_COLUMN_IDS.
  • playlist-details: a sort control in its header row (select + direction, page-header's pattern) — "Date added" (library) and "Added to playlist" keyed on playlist.Track.ID, which is already over the IPC and is insertion order. Order-only, no displayable date — see risk 7. This is the largest single frontend piece: the view has no sort at all today, which is materially more than #46's one-line report suggests.

Verification tiers — all of these

  1. make generate, then go test ./backend/database/ ./backend/datamap/ and make test (3 build passes) — schema + query change.
  2. make bindings then make ui-test — new bound struct fields (incl. a ≥2-library Go test proving the aggregates respect the library filter).
  3. npx tsc --noEmit from frontend/ — nothing else runs it over the test tree.
  4. make e2e (SEED=default) — a spec per surface, staging distinct added_at via /__test/sql first: the seed's single scan gives every file the same timestamp, so an un-staged order assertion only tests the name tie-break.
  5. make perf before/after on the bulk seed — the widened projections ride every hot list query; "cheap" is a claim until measured (view-first-open, scroll).
  6. The gate: make lint, make bindings-check, make css-check, make commit-check.
  7. Rebuild every seed after the change (make sandbox-seed NAME=default, bulk too if used): staleshape drops audio_files in old seeds and their playlists empty out (audio_file_id → NULL) — e2e against a stale seed fails in ways that read as regressions. CI builds its own seed.
  8. make ui-visual — no baseline covers the changed surfaces (track-list has none; page-header's own shots are option-agnostic), so expected not required; instead read a PNG of each re-sorted view in the real app (make dev-headless SEED=default + screenshot) — a component test passing is not the app rendering.

Deliberately out of scope. Album-completeness key (#184), recency-weighted stats (#185), tag-status key (rejected — duplicates autotag in the hot projection), Release Year option (marginal per the scope note), smart-playlist sort (rule-evaluated; "added" is meaningless there), queue sort, phone column-set changes, the sort-state one-module extraction (#138 closed via the setup.ts clear — four more localStorage keys are safe on that fix), any migration mechanism for authored tables.

Risks I actually see

  1. Existing dev DBs lose play counts, last-played and tag status, and playlist entries empty — staleshape drops the now-stale audio_files and the authored columns on it ride along (documented MIXED KIND cost). Pre-1.0 accepted; the seed rebuild in tier 7 is mandatory, not hygiene.
  2. The EXISTS→JOIN rewrite can silently change library-scope semantics — aggregates counting files outside the selected library show wrong counts only with ≥2 libraries, which is why tier 2 names that test.
  3. Widening is on every hot list query — tier 5 exists because the scope note's "one projection change, cheap" is unmeasured.
  4. Seed timestamps collide — comparators need name tie-breaks regardless (two files can share a timestamp in real libraries too).
  5. Two-list drift is #197's shape again — dateAdded must land in Go's AllColumnIDs and TS's COLUMN_DEFS in the same commit.
  6. Virtualizer repaint — the artists-view memo-key and arrow-function rules are load-bearing; if card-grid-repaint.test.ts goes red, the change broke it, not the test.
  7. A real "added to playlist" date needs a column on playlist_tracks, which is Authored — staleshape never repairs authored tables, so an existing database would fail every new query with no such column and the only manual fix destroys the user's playlists. Not taken; the id-ordering sort is the honest 90%. File a follow-up if a displayed date is wanted.
⟦loop⟧ **Implementation plan for #46 + #32** — one pass on `feat/46-32-date-added-and-artist-sort`, per #73 Phase 5 and the scope agreed on #32 (2026-08-21). **Change, one sentence.** Add an import timestamp to `audio_files`, widen the album/artist/genre/track projections with it plus the count/duration/play rollups, and give every library view real comparator-based sort options on `cover-grid`'s existing `SortOption & { comparator }` model — date-added (newest-first) everywhere (#46) and the Artists view's missing keys (#32). **Backend** (schema change → follow `references/schema-change.md`) - `sql/schemas/audio_files.sql`: new `added_at DATETIME NOT NULL`, **no default** — a missed insert path should fail loudly, not silently get epoch. One edit + `make generate`. `audio_files` is `Kind: Owned`, so staleshape drops+rescans existing DBs — the designed path; no datamap change (the MIXED KIND note already covers the authored neighbours). - `sql/queries/audio_files.sql`: `CreateAudioFile` sets `added_at` at insert only. **No update path may touch it** — a rescan must not reset date-added; check `UpdateAudioFileTags`/`UpdateAudioFileStat` stay silent on it. - `track_metadata` view + `library.Track.AddedAt` (string via `trackFromRow`, mirroring `LastPlayed`). - `GetAlbumArtists` (artists.sql), `GetAlbums` (albums.sql), `GetAllGenresWithCounts` (genres.sql): EXISTS/DISTINCT → JOIN `audio_files` + GROUP BY, gaining track/album counts, duration (ms int64), play count, last played, added-at. **The library filter must apply inside the aggregates.** - `GetPlaylistTracksWithMetadata` (playlists.sql): add `tm.added_at` — the join already exists. - `home.sql` `HomeRecentlyAddedAlbums`: `MAX(af.id)` → `MAX(af.added_at)`; semantics agree by construction. - Rollup definitions **in comments beside the queries** (per the scope agreement): date added = `MAX` over the entity's files (matches the `MAX(af.id)` stand-in — completing an album re-surfaces it as recent); last played = `MAX`; artist play count = `SUM` ("who do I listen to most"). - `backend/tracklist`: `AllColumnIDs` + `DefaultColumns` gain `dateAdded` (default-visible, so the sort exists out of the box). `database/testhelper.go`: `TestTrack` gets a settable `AddedAt`. **Frontend** - `page-header`: `SortOption` gains optional `defaultDirection`, emitted on field change — that *is* "newest-first by default". - `cover-grid-types.ts`: extend `ALBUM_SORT_OPTIONS` (+date added, track count, duration, play count, last played). - `artists-view`: `ARTIST_SORT_OPTIONS` becomes a comparator array on the same model; **add `sortField` to `recomputeArtistCaches`' memo key** and keep the per-render arrow functions — `card-grid-repaint.test.ts` is the guard. - `genres-view`: inline ternary → comparator array. `track-list/columns.ts`: `dateAdded` column (renderer + comparator, `configurable: true`); **both column lists must agree** (#197's two-list trap; `settings-column-list.test.ts` sweeps). Not in `PHONE_COLUMN_IDS`. - `playlist-details`: a sort control in its header row (select + direction, page-header's pattern) — "Date added" (library) and "Added to playlist" keyed on `playlist.Track.ID`, which is already over the IPC and *is* insertion order. **Order-only, no displayable date** — see risk 7. This is the largest single frontend piece: the view has no sort at all today, which is materially more than #46's one-line report suggests. **Verification tiers — all of these** 1. `make generate`, then `go test ./backend/database/ ./backend/datamap/` and `make test` (3 build passes) — schema + query change. 2. `make bindings` then `make ui-test` — new bound struct fields (incl. a ≥2-library Go test proving the aggregates respect the library filter). 3. `npx tsc --noEmit` from `frontend/` — nothing else runs it over the test tree. 4. `make e2e` (`SEED=default`) — a spec per surface, **staging distinct `added_at` via `/__test/sql` first**: the seed's single scan gives every file the same timestamp, so an un-staged order assertion only tests the name tie-break. 5. `make perf` before/after on the bulk seed — the widened projections ride every hot list query; "cheap" is a claim until measured (view-first-open, scroll). 6. The gate: `make lint`, `make bindings-check`, `make css-check`, `make commit-check`. 7. **Rebuild every seed after the change** (`make sandbox-seed NAME=default`, bulk too if used): staleshape drops `audio_files` in old seeds and their playlists empty out (`audio_file_id` → NULL) — e2e against a stale seed fails in ways that read as regressions. CI builds its own seed. 8. `make ui-visual` — no baseline covers the changed surfaces (track-list has none; page-header's own shots are option-agnostic), so expected **not** required; instead read a PNG of each re-sorted view in the real app (`make dev-headless SEED=default` + screenshot) — a component test passing is not the app rendering. **Deliberately out of scope.** Album-completeness key (#184), recency-weighted stats (#185), tag-status key (rejected — duplicates autotag in the hot projection), Release Year option (marginal per the scope note), smart-playlist sort (rule-evaluated; "added" is meaningless there), queue sort, phone column-set changes, the sort-state one-module extraction (#138 closed via the `setup.ts` clear — four more localStorage keys are safe on that fix), any migration mechanism for authored tables. **Risks I actually see** 1. **Existing dev DBs lose play counts, last-played and tag status, and playlist entries empty** — staleshape drops the now-stale `audio_files` and the authored columns on it ride along (documented MIXED KIND cost). Pre-1.0 accepted; the seed rebuild in tier 7 is mandatory, not hygiene. 2. **The EXISTS→JOIN rewrite can silently change library-scope semantics** — aggregates counting files outside the selected library show wrong counts only with ≥2 libraries, which is why tier 2 names that test. 3. **Widening is on every hot list query** — tier 5 exists because the scope note's "one projection change, cheap" is unmeasured. 4. **Seed timestamps collide** — comparators need name tie-breaks regardless (two files can share a timestamp in real libraries too). 5. **Two-list drift** is #197's shape again — `dateAdded` must land in Go's `AllColumnIDs` and TS's `COLUMN_DEFS` in the same commit. 6. **Virtualizer repaint** — the artists-view memo-key and arrow-function rules are load-bearing; if `card-grid-repaint.test.ts` goes red, the change broke it, not the test. 7. **A real "added to playlist" date needs a column on `playlist_tracks`, which is `Authored`** — staleshape never repairs authored tables, so an existing database would fail every new query with `no such column` and the only manual fix destroys the user's playlists. Not taken; the id-ordering sort is the honest 90%. File a follow-up if a displayed date is wanted.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: yonlu/yellowjacket#46