Browse hub — Posts / Artists / Tags consolidated into one tabbed nav item; old paths redirect into tabs (deep links preserved). Subscriptions stays standalone. (d5d23a9)
Rename a series from the management view — pencil next to the title (reuses the rename→merge dialog). (409bbd4)
Tag-input dropdown searches the full prediction set — surfaces every stored prediction (down to the 0.05 store floor) so low-confidence tags can be accepted in canonical formatting; the Suggestions panel stays curated. (already on dev, in this range)
Process #594 gained the incomplete-consolidation guards (all-or-nothing, consumer enumeration, §8b exhaustiveness, per-consumer testing) and an app-wide visual/behavioral-pattern dimension. Then 7 sweeps, each in the pattern catalog:
<KebabMenu> — 7 hand-rolled kebabs → one modal-safe primitive (folds in a latent #711-class fix). (c774042)
.fc-muted — token defined in 12 style blocks → one global utility. (4e83b42)
<SampleNameGrid> — 5 preview grids → one primitive. (4854d74)
<CardHeading> — icon+title heading in 13 cards/dialogs → one primitive. (9deebfa)
get_or_create — race-safe find-or-create in 4 async sites → one helper (root-causes the recurring duplicate-row/race bugs). (7b2a205, f1a664e)
pagination cursor — encode/decode_cursor defined twice → one shared module. (074c586)
tag_query — fandom self-join + {id,name,kind,fandom_id,fandom_name} serialization in 5 spots → one module. (14c244b)
The over-DRY guard left genuine variants alone (typed-token confirm modal, chip-led dialog title, SourceService reject-on-conflict, TagDirectory's count-join, the importer's sync find-or-create).
Rolls up dev since the last main merge (#89).
## Features / UX
- **Browse hub** — Posts / Artists / Tags consolidated into one tabbed nav item; old paths redirect into tabs (deep links preserved). Subscriptions stays standalone. (`d5d23a9`)
- **Rename a series from the management view** — pencil next to the title (reuses the rename→merge dialog). (`409bbd4`)
- **Tag-input dropdown searches the full prediction set** — surfaces every stored prediction (down to the 0.05 store floor) so low-confidence tags can be accepted in canonical formatting; the Suggestions panel stays curated. (already on dev, in this range)
## DRY pass (process #594 hardened + 7 consolidation sweeps)
Process #594 gained the incomplete-consolidation guards (all-or-nothing, consumer enumeration, §8b exhaustiveness, per-consumer testing) and an app-wide visual/behavioral-pattern dimension. Then 7 sweeps, each in the pattern catalog:
- **`<KebabMenu>`** — 7 hand-rolled kebabs → one modal-safe primitive (folds in a latent #711-class fix). (`c774042`)
- **`.fc-muted`** — token defined in 12 style blocks → one global utility. (`4e83b42`)
- **`<SampleNameGrid>`** — 5 preview grids → one primitive. (`4854d74`)
- **`<CardHeading>`** — icon+title heading in 13 cards/dialogs → one primitive. (`9deebfa`)
- **`get_or_create`** — race-safe find-or-create in 4 async sites → one helper (root-causes the recurring duplicate-row/race bugs). (`7b2a205`, `f1a664e`)
- **`pagination` cursor** — `encode/decode_cursor` defined twice → one shared module. (`074c586`)
- **`tag_query`** — fandom self-join + `{id,name,kind,fandom_id,fandom_name}` serialization in 5 spots → one module. (`14c244b`)
The over-DRY guard left genuine variants alone (typed-token confirm modal, chip-led dialog title, SourceService reject-on-conflict, TagDirectory's count-join, the importer's sync find-or-create).
All commits CI-green on their dev head SHA.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Posts, Artists, and Tags are the three 'browse the library by an axis'
surfaces; Subscriptions stays purely management (operator-asked 2026-06-09).
New BrowseView renders them as tabs (?tab=posts|artists|tags); only the active
tab mounts. The old standalone paths become redirects into the matching tab,
preserving deep-link query (/posts?post_id=N → /browse?tab=posts&post_id=N) and
keeping the route names so existing { name: 'posts'|'artists'|'tags' } links and
path pushes still resolve. Nav now reads Showcase · Gallery · Browse · Series ·
Subscriptions, with Settings pinned right.
Test: /browse resolves; /tags and /artists redirect into their tabs; a posts
deep link survives the redirect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First pattern-consistency DRY pass (process #594). The overflow kebab was
hand-rolled 7 ways in two divergent activator strategies — Pattern A
(#activator + v-bind) which silently breaks inside the teleported image modal
(#711), and Pattern B (manual v-model + activator=parent + open-on-click=false +
z-index 2400) the modal kebabs needed as a workaround.
New <KebabMenu> (components/common) bakes in the modal-safe strategy
UNIVERSALLY, so every kebab works in modal and non-modal contexts — folding the
latent #711-class bug fix into all five Pattern-A sites. Menu items go in the
default slot; variations (size/variant/location/label/min-width) are props.
Adopted across all 7: TagChip, SuggestionItem, TagCard, SeriesView card,
SeriesManageView, BackupRunsTable, SourceActions. Exhaustiveness (§8b):
mdi-dots-vertical now lives only in KebabMenu. Labeled dropdowns / nav menus /
filter popovers are a different concept and left alone. Seeded the pattern
catalog so new code reuses the primitive. Test: KebabMenu renders slot items +
trigger label/glyph + presentational props.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The muted-text token was redefined identically in 12 component <style scoped>
blocks. Consolidate to one global utility in styles/app.css; remove the 12
copies. Keeps the explicit on-surface-variant (vellum) token, NOT Vuetify's
opacity-based text-medium-emphasis (per the muted-text-token rule). Behavior-
preserving: every class=fc-muted usage now resolves to the single source.
§8b exhaustiveness caught (and I fixed) my own sed clobbering the new app.css
rule — now exactly one .fc-muted definition exists, zero component-local.
Catalog updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The management view showed the series name but had no way to change it post-
creation (rename was only on the browse-card kebab). Add a pencil next to the
title that opens TagRenameDialog (reuses the canonical rename → PATCH
/api/tags/<id> with its collision→merge flow, since a series IS a
Tag(kind=series)); the new name reflects in place. Operator-asked 2026-06-09.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preview sample-name grid (scrollable monospace chip grid) was hand-rolled
5 times with verbatim-duplicated markup + CSS — TagMaintenanceCard (×4) and
PostMaintenanceCard. Consolidate to <SampleNameGrid> (components/common): pass
:names for the plain case, default slot for the normalize from→to chips
(styled via :slotted .fc-name). Removed the duplicated .fc-name-grid/.fc-name
CSS from both cards.
Over-DRY guard: only the verbatim-duplicated grid is merged — each card's
preview/commit logic and result-count lines genuinely differ and stay put;
MinDimensionCard's typed-token confirm is a separate variant, untouched.
§8b: fc-name-grid now lives only in SampleNameGrid. Catalog updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The icon+title v-card-title heading (d-flex align-center + gap + <v-icon size=small> +
<span>) was hand-rolled identically in 13 cards/dialogs (15 heading instances).
Consolidate to <CardHeading icon title> (components/common) with an iconColor
prop (error headings) and a default slot for trailing content (spacer+actions,
inline status chip). Adopted everywhere the pattern appears — all-or-nothing per
the hardened DRY process.
Over-DRY guard: plain text-only <v-card-title> one-liners are NOT this pattern
and stay; DownloadDetailModal leads with a status CHIP (not an icon), a different
concept, left alone. §8b: the only remaining d-flex align-center v-card-title is
that intentional variant. Catalog updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The find-or-create dance — SELECT, then a SAVEPOINT INSERT that recovers (not a
full rollback) on IntegrityError when a concurrent worker inserted first — was
hand-rolled identically in 4 async sites: ArtistService.find_or_create,
TagService.find_or_create, ExtensionService._find_or_create_artist and
._find_or_create_source. Divergent copies of exactly this pattern are how the
duplicate-row/race bugs in reference_scalar_one_or_none_duplicates crept in, so
it now lives once in services/db_helpers.get_or_create (returns (row, created);
factory adds+flushes+returns the row; caller owns the outer commit).
Over-DRY guard: SourceService's IntegrityError sites RAISE DuplicateSourceError
(reject-on-conflict, a different concept) — left alone. Importer._get_or_create
is the lone SYNC consumer (already shared by 2 callers) — stays separate, can't
cross the sync/async boundary. §8b: no hand-rolled async find-or-create remains.
Test: get_or_create creates then returns existing without re-invoking the factory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI lint flagged UP047 — use the native generic syntax def get_or_create[T](...)
instead of typing.TypeVar on Python 3.14.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
encode_cursor/decode_cursor (base64 <iso8601>|<id>) were defined identically in
gallery_service AND post_feed_service, with artist_service importing gallery's
copy. Two implementations of one cursor format silently break pagination in
whichever feed drifts. Extract to services/pagination.py; gallery/post_feed/
artist all import it. Dropped now-unused base64/datetime imports.
§8b: encode_cursor/decode_cursor now defined only in pagination.py. Existing
cursor round-trip tests still cover it via the re-export. Catalog updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The fandom self-join (resolve a character's fandom NAME via Tag.fandom_id->Tag)
and the {id,name,kind,fandom_id,fandom_name} dict were hand-written in
TagService.autocomplete/.list_for_image, GalleryService.get_image_with_tags and
the api/tags handlers — the last few grown by this session's fandom-on-chip
feature. Consolidate to services/tag_query: fandom_join_alias() + tag_columns()
build the select; serialize_tag(row) builds the dict. Now a new tag field is
added in one place.
Over-DRY guard: TagDirectoryService selects the full Tag ORM + an image-count
aggregate (a different select shape) — left as its own variant. §8b: the
fandom_lookup alias lives only in tag_query; gallery + both api/tags handlers
serialize via serialize_tag. Test: serialize_tag handles enum + string kind.
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.
Rolls up dev since the last main merge (#89).
Features / UX
d5d23a9)409bbd4)DRY pass (process #594 hardened + 7 consolidation sweeps)
Process #594 gained the incomplete-consolidation guards (all-or-nothing, consumer enumeration, §8b exhaustiveness, per-consumer testing) and an app-wide visual/behavioral-pattern dimension. Then 7 sweeps, each in the pattern catalog:
<KebabMenu>— 7 hand-rolled kebabs → one modal-safe primitive (folds in a latent #711-class fix). (c774042).fc-muted— token defined in 12 style blocks → one global utility. (4e83b42)<SampleNameGrid>— 5 preview grids → one primitive. (4854d74)<CardHeading>— icon+title heading in 13 cards/dialogs → one primitive. (9deebfa)get_or_create— race-safe find-or-create in 4 async sites → one helper (root-causes the recurring duplicate-row/race bugs). (7b2a205,f1a664e)paginationcursor —encode/decode_cursordefined twice → one shared module. (074c586)tag_query— fandom self-join +{id,name,kind,fandom_id,fandom_name}serialization in 5 spots → one module. (14c244b)The over-DRY guard left genuine variants alone (typed-token confirm modal, chip-led dialog title, SourceService reject-on-conflict, TagDirectory's count-join, the importer's sync find-or-create).
All commits CI-green on their dev head SHA.
🤖 Generated with Claude Code
Posts, Artists, and Tags are the three 'browse the library by an axis' surfaces; Subscriptions stays purely management (operator-asked 2026-06-09). New BrowseView renders them as tabs (?tab=posts|artists|tags); only the active tab mounts. The old standalone paths become redirects into the matching tab, preserving deep-link query (/posts?post_id=N → /browse?tab=posts&post_id=N) and keeping the route names so existing { name: 'posts'|'artists'|'tags' } links and path pushes still resolve. Nav now reads Showcase · Gallery · Browse · Series · Subscriptions, with Settings pinned right. Test: /browse resolves; /tags and /artists redirect into their tabs; a posts deep link survives the redirect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>The fandom self-join (resolve a character's fandom NAME via Tag.fandom_id->Tag) and the {id,name,kind,fandom_id,fandom_name} dict were hand-written in TagService.autocomplete/.list_for_image, GalleryService.get_image_with_tags and the api/tags handlers — the last few grown by this session's fandom-on-chip feature. Consolidate to services/tag_query: fandom_join_alias() + tag_columns() build the select; serialize_tag(row) builds the dict. Now a new tag field is added in one place. Over-DRY guard: TagDirectoryService selects the full Tag ORM + an image-count aggregate (a different select shape) — left as its own variant. §8b: the fandom_lookup alias lives only in tag_query; gallery + both api/tags handlers serialize via serialize_tag. Test: serialize_tag handles enum + string kind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>