feat(gallery): sort by earliest post date across all posts (new default)
CI / backend-lint-and-test (push) Successful in 27s
CI / integration (push) Successful in 3m24s
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 16s

The gallery's newest/oldest sort keys off image_record.effective_date =
COALESCE(primary post's post_date, created_at). The primary post is often the
repost/download the file came from, so the grid led with download dates rather
than when content was first posted (operator-flagged).

Add a second materialized sort key, earliest_post_date = MIN(post_date) across
ALL of an image's provenance posts (every post it appears in), else created_at —
the original publish date. Mirrors the effective_date pattern so the sort stays a
forward index scan.

- alembic 0071: add earliest_post_date + index (DESC, id DESC); backfill
  created_at baseline then MIN over image_provenance ⋈ post.
- importer: recompute earliest_post_date whenever a dated post is linked (MIN over
  the image's provenance, which now includes the just-added row).
- gallery_service: new sorts posted_new / posted_old key off earliest_post_date;
  cursor + year/month grouping follow the active column transparently.
- api: accept posted_new|posted_old; DEFAULT is now posted_new so the grid leads
  with original publish date. newest/oldest (effective_date) still available.
- frontend: sort dropdown gains "Newest/Oldest post date" (default Newest post
  date); existing effective-date sorts relabelled "Newest/Oldest added".
- tests: service test asserts posted_new/posted_old key off earliest_post_date;
  frontend default-sort omission test updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ttrj5P7upUTueSfoJcxEqa
This commit is contained in:
2026-07-01 10:46:09 -04:00
parent ccbb5cbc9e
commit c22f37d64d
9 changed files with 176 additions and 15 deletions
+7 -2
View File
@@ -44,7 +44,8 @@ def _parse_filters():
the image must match at least one tag from EACH group (groups ANDed).
- `tag_not` is a comma-separated exclude list (image must carry none).
`media` is image|video; `sort` is newest|oldest; `platform` selects one
`media` is image|video; `sort` is newest|oldest|posted_new|posted_old
(default posted_new); `platform` selects one
platform (or the UNSOURCED_PLATFORM sentinel); `untagged`/`no_artist` are
boolean flags; `date_from`/`date_to` are inclusive calendar-day bounds
(date_to is widened by a day so the whole day is covered by the service's
@@ -67,8 +68,12 @@ def _parse_filters():
artist_id = int(artist_id_raw) if artist_id_raw else None
media = request.args.get("media")
media_type = media if media in ("image", "video") else None
# newest/oldest key off effective_date (primary post / download); posted_new/
# posted_old off earliest_post_date (original publish across all posts). The
# default is posted_new so the grid leads with original publish date, not the
# download/repost the primary post points at (operator-flagged 2026-07-01).
sort = request.args.get("sort")
sort = sort if sort in ("newest", "oldest") else "newest"
sort = sort if sort in ("newest", "oldest", "posted_new", "posted_old") else "posted_new"
platform = request.args.get("platform") or None
untagged = request.args.get("untagged") in ("1", "true", "yes")
no_artist = request.args.get("no_artist") in ("1", "true", "yes")