Browse hub, series rename, full-prediction dropdown + a DRY pass (7 sweeps) #90

Merged
bvandeusen merged 10 commits from dev into main 2026-06-10 00:24:01 -04:00
Owner

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 cursorencode/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

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)
bvandeusen added 10 commits 2026-06-10 00:23:56 -04:00
feat(nav): consolidate Posts/Artists/Tags into a Browse hub
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 37s
CI / integration (push) Successful in 3m13s
d5d23a92f2
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>
refactor(ui): consolidate 7 hand-rolled kebabs into one KebabMenu (DRY pattern sweep)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 40s
CI / integration (push) Successful in 3m18s
c774042a85
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>
refactor(ui): single global .fc-muted token (DRY pattern sweep)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 23s
CI / backend-lint-and-test (push) Successful in 28s
CI / integration (push) Successful in 3m21s
4e83b4225a
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>
feat(series): rename a series from the management view
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m15s
409bbd43db
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>
refactor(ui): SampleNameGrid primitive for maintenance-card previews (DRY pattern sweep)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m9s
4854d74c5a
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>
refactor(ui): CardHeading primitive for icon+title card/dialog headings (DRY pattern sweep)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m10s
9deebfa133
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>
refactor(services): shared race-safe get_or_create helper (DRY backend sweep)
CI / lint (push) Failing after 2s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m19s
7b2a2051e9
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>
fix(services): PEP 695 type params for get_or_create (ruff UP047)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m11s
f1a664e5a7
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>
refactor(services): shared pagination cursor (DRY sweep)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 43s
CI / integration (push) Successful in 3m21s
074c5868fb
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>
refactor(tags): shared tag_query for fandom self-join + serialization (DRY sweep)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 31s
CI / integration (push) Successful in 3m17s
14c244bd3d
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>
bvandeusen merged commit bad37e07b2 into main 2026-06-10 00:24:01 -04:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/FabledCurator#90