Batch of dev work since the last main cut. All CI-green on dev HEAD 6d630d1.
Gallery load (speed + feel)
Reveal each tile on its own thumbnail @load (cascade in load order) instead of popping in together; single 50-item initial fetch instead of 10 serial round-trips.
Materialized + indexed image_record.effective_date (alembic 0035) so /scroll is an index range scan instead of a COALESCE-across-join sort per page.
Fandom editing
TagService.set_fandom (set / change / clear) with cross-fandom merge on name collisions; PATCH /tags/<id> extended; UI in both the tags-directory card kebab and the image-modal tag-chip kebab.
Filter clearing
Gallery tag-filter chip with a clear; tags-view kind chip :mandatory bug fix so it deselects.
Batch of dev work since the last main cut. All CI-green on dev HEAD `6d630d1`.
## Gallery load (speed + feel)
- Reveal each tile on its own thumbnail `@load` (cascade in load order) instead of popping in together; single 50-item initial fetch instead of 10 serial round-trips.
- Materialized + indexed `image_record.effective_date` (alembic 0035) so `/scroll` is an index range scan instead of a COALESCE-across-join sort per page.
## Fandom editing
- `TagService.set_fandom` (set / change / clear) with cross-fandom merge on name collisions; `PATCH /tags/<id>` extended; UI in both the tags-directory card kebab and the image-modal tag-chip kebab.
## Filter clearing
- Gallery tag-filter chip with a clear; tags-view kind chip `:mandatory` bug fix so it deselects.
## Pinned gallery filter bar (Phase 1)
- Composable `/scroll` + `/timeline` + `/jump`: multi-tag AND, artist, media type, newest/oldest sort (post_id stays exclusive).
- Sticky `top:64px` filter bar matching the app's `v-tabs` chrome: tag+artist autocomplete, closable chips, media toggle, sort, clear. URL-driven (deep-linkable, back-button).
Also untracks the transient `.claude/scheduled_tasks.lock`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The 5×10 metadata batching only staggered the cheap layer (JSON);
thumbnails load as independent <img> requests and clustered, so tiles
"popped in together" after a wait. Two changes:
- GalleryItem reveals each tile when ITS OWN thumbnail fires @load (with
an onMounted complete-check for cached thumbs), playing a showcase-style
flip-up entrance. Tiles now cascade in natural load order instead of all
at once. Honors prefers-reduced-motion.
- gallery store does ONE initial fetch (limit=50) instead of 10 serial
/scroll round-trips. Fewer RTTs, faster first paint; the reveal-on-load
is what makes appearance progressive now. Infinite scroll pulls 25/trigger.
Tests: GalleryItem gains is-loaded only after @load; loadInitial issues
exactly one scroll request at the initial limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gallery cursored on COALESCE(post.post_date, image_record.created_at)
across the Post outer join — an expression spanning two tables that no
index can serve, so every /scroll sorted a large slice of the library
(and the old frontend fired ten serially). Materialize it:
- image_record.effective_date column + ix_image_record_effective_date
(effective_date DESC, id DESC); alembic 0035 backfills
COALESCE(primary post's post_date, created_at) for existing rows.
- gallery_service._effective_date_col() now returns the column, so scroll
/ timeline / jump / neighbors all order off the index instead of
re-deriving the COALESCE. _neighbors reads record.effective_date
directly (drops an extra Post lookup).
- importer._apply_sidecar maintains it: when a primary post with a date is
linked, effective_date = post.post_date; plain inserts keep the
created_at-equivalent server default.
Tests: sidecar import asserts effective_date == post.post_date; gallery
ordering/timeline/jump test seeds set effective_date alongside created_at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
No way existed to change which fandom a character tag belongs to after
creation — PATCH /tags/<id> only renamed.
- TagService.set_fandom(tag_id, fandom_id, merge=False): set / change /
clear (fandom_id=None) a character's fandom, with the same validation as
find_or_create. On a name collision in the target fandom it raises
TagMergeConflict (→ 409, same shape as rename); merge=True resolves it by
merging this tag INTO the existing character.
- Extract _do_merge(source, target) from merge() so set_fandom can perform
the deliberate CROSS-fandom merge the public merge() validation forbids.
- PATCH /tags/<id> now accepts optional fandom_id (+ merge flag) alongside
name, and returns fandom_id.
Tests: set/change/clear, non-character + bad-ref rejection, collision
raises, merge resolves; API set/clear + collision→merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the missing UI to change a character tag's fandom, in both places:
- FandomSetDialog (shared): pick an existing fandom, create a new one, or
clear it; on a name collision in the target fandom it surfaces a merge
confirmation and resolves via setFandom(merge:true). Reuses the tags
store's fandom cache.
- TagCard kebab gains "Set fandom…" for character tags (→ TagsView opens
the dialog, reloads on success).
- TagPanel chip kebab gains "Set fandom…" for character tags (→ reloads the
modal's tag list on success).
- tags store: setFandom(tagId, fandomId, {merge}) action + test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two gaps where a filter couldn't be removed:
- Gallery: a tag_id filter (from clicking a tag) had no indicator or clear
control — only post_id did (PostInfoHeader). Add an "Tag: <name> ✕" chip
that clears the filter by dropping tag_id from the URL. New lightweight
GET /api/tags/<id> resolves the name; the store fetches it on filter set.
- Tags view: the kind chip-group used mandatory="false" — a STRING ("false"
is truthy in JS), which made the group mandatory so the active kind chip
couldn't be deselected. Fixed to :mandatory="false" so the filter clears.
Tests: GET /tags/<id> shape + 404; gallery store resolves filterTagName.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 backend for the gallery filter bar. Extends scroll/timeline/jump
from a single mutually-exclusive filter to a composable one:
- tag_ids: image must carry ALL of them (one correlated EXISTS per tag —
AND, no row multiplication), replacing the single-tag JOIN.
- artist_id composes with tags; media_type ('image'|'video') narrows by
mime; post_id stays the exclusive post-detail path.
- sort ('newest'|'oldest') flips the effective_date/id cursor comparison
and ordering; the cursor value is unchanged (direction comes from the
request). jump_cursor honors sort too.
- Shared _apply_scope helper applied across scroll/timeline/jump so the
timeline sidebar reflects the filtered set. API _parse_filters parses
tag_id (comma list), artist_id, media, sort.
Tests: multi-tag AND, media filter, sort reversal (service + API);
post_id-excludes-others; single tag_id back-compat.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gallery now has in-view filtering, styled like the app's sticky v-tabs
chrome (pinned at top:64px under TopNav).
- GalleryFilterBar: combined tag+artist autocomplete (searches
/api/tags + /api/artists), closable filter chips (multi-tag AND),
media toggle (All/Images/Videos), Newest/Oldest sort, Clear. Writes all
state to the URL via router.push.
- gallery store: filter is now { tag_ids, artist_id, media_type, sort,
post_id }; applyFilterFromQuery makes the URL the single source of truth
(deep-linkable, back-button works); chip labels resolved by id or
pre-noted on pick. Replaces the standalone tag chip + setTag/PostFilter.
- GalleryView: renders the bar (hidden in post-detail), syncs route.query
→ store on mount + every query change.
Also untracks the transient .claude/scheduled_tasks.lock committed in
3f30327 and gitignores it.
Tests: store parses query → composable scroll params, post_id exclusivity,
newest-sort omitted, label pre-seed, single initial fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Batch of dev work since the last main cut. All CI-green on dev HEAD
6d630d1.Gallery load (speed + feel)
@load(cascade in load order) instead of popping in together; single 50-item initial fetch instead of 10 serial round-trips.image_record.effective_date(alembic 0035) so/scrollis an index range scan instead of a COALESCE-across-join sort per page.Fandom editing
TagService.set_fandom(set / change / clear) with cross-fandom merge on name collisions;PATCH /tags/<id>extended; UI in both the tags-directory card kebab and the image-modal tag-chip kebab.Filter clearing
:mandatorybug fix so it deselects.Pinned gallery filter bar (Phase 1)
/scroll+/timeline+/jump: multi-tag AND, artist, media type, newest/oldest sort (post_id stays exclusive).top:64pxfilter bar matching the app'sv-tabschrome: tag+artist autocomplete, closable chips, media toggle, sort, clear. URL-driven (deep-linkable, back-button).Also untracks the transient
.claude/scheduled_tasks.lock.🤖 Generated with Claude Code
Adds the missing UI to change a character tag's fandom, in both places: - FandomSetDialog (shared): pick an existing fandom, create a new one, or clear it; on a name collision in the target fandom it surfaces a merge confirmation and resolves via setFandom(merge:true). Reuses the tags store's fandom cache. - TagCard kebab gains "Set fandom…" for character tags (→ TagsView opens the dialog, reloads on success). - TagPanel chip kebab gains "Set fandom…" for character tags (→ reloads the modal's tag list on success). - tags store: setFandom(tagId, fandomId, {merge}) action + test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Two gaps where a filter couldn't be removed: - Gallery: a tag_id filter (from clicking a tag) had no indicator or clear control — only post_id did (PostInfoHeader). Add an "Tag: <name> ✕" chip that clears the filter by dropping tag_id from the URL. New lightweight GET /api/tags/<id> resolves the name; the store fetches it on filter set. - Tags view: the kind chip-group used mandatory="false" — a STRING ("false" is truthy in JS), which made the group mandatory so the active kind chip couldn't be deselected. Fixed to :mandatory="false" so the filter clears. Tests: GET /tags/<id> shape + 404; gallery store resolves filterTagName. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Phase 1 backend for the gallery filter bar. Extends scroll/timeline/jump from a single mutually-exclusive filter to a composable one: - tag_ids: image must carry ALL of them (one correlated EXISTS per tag — AND, no row multiplication), replacing the single-tag JOIN. - artist_id composes with tags; media_type ('image'|'video') narrows by mime; post_id stays the exclusive post-detail path. - sort ('newest'|'oldest') flips the effective_date/id cursor comparison and ordering; the cursor value is unchanged (direction comes from the request). jump_cursor honors sort too. - Shared _apply_scope helper applied across scroll/timeline/jump so the timeline sidebar reflects the filtered set. API _parse_filters parses tag_id (comma list), artist_id, media, sort. Tests: multi-tag AND, media filter, sort reversal (service + API); post_id-excludes-others; single tag_id back-compat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Gallery now has in-view filtering, styled like the app's sticky v-tabs chrome (pinned at top:64px under TopNav). - GalleryFilterBar: combined tag+artist autocomplete (searches /api/tags + /api/artists), closable filter chips (multi-tag AND), media toggle (All/Images/Videos), Newest/Oldest sort, Clear. Writes all state to the URL via router.push. - gallery store: filter is now { tag_ids, artist_id, media_type, sort, post_id }; applyFilterFromQuery makes the URL the single source of truth (deep-linkable, back-button works); chip labels resolved by id or pre-noted on pick. Replaces the standalone tag chip + setTag/PostFilter. - GalleryView: renders the bar (hidden in post-detail), syncs route.query → store on mount + every query change. Also untracks the transient .claude/scheduled_tasks.lock committed in3f30327and gitignores it. Tests: store parses query → composable scroll params, post_id exclusivity, newest-sort omitted, label pre-seed, single initial fetch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>