54 Commits

Author SHA1 Message Date
bvandeusen 4cf77aac83 fix(suggestions): make ambiguous-character picker buttons clickable
The chip sets pointer-events:none while accept is in flight to prevent
double-clicks. When the server returns 409 ambiguous and the picker is
appended as a child of the chip, the picker's buttons inherit the
disabled pointer events and silently refuse clicks.

Set pointer-events:auto on the picker container so its children can
receive clicks regardless of the parent chip's state. Picker buttons
already manage their own disabled state during the second-stage POST,
so re-enabling the whole picker is safe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 10:56:19 -04:00
bvandeusen 3b472cf519 fix(gallery): infinite scroll + timeline + jump use tag_id
All three API calls (/api/gallery/scroll, /api/gallery/timeline,
/api/gallery/jump) were still sending the legacy ?tag=<name> param.
Task 10's backend switch silently ignored it, so subsequent scroll
loads and timeline navigation fetched unfiltered content on a
tag-filtered gallery page.

Read tag_id from window.location.search at init and cache it in
state.activeTagId. Use it as ?tag_id=<int> on every subsequent
fetch. state.activeTag (the display string) stays for human-facing
uses elsewhere.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 08:49:44 -04:00
bvandeusen 7df508d65e fix(modal): scroll highlighted autocomplete row into view on arrow keys
selectAutocompleteItem (main tag input) and highlightFandomItem (fandom
picker) call scrollIntoView({block:'nearest'}) on the newly-highlighted
row. Previously arrowing past the visible slice would move the .selected
class off-screen and leave the user with no visible feedback about
where they were in the list.

block:'nearest' is a no-op when the row is already visible, so in the
common case (first few arrow presses) nothing scrolls. Only scrolls the
minimum distance when the highlighted row has moved outside the
container's viewport.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:52:10 -04:00
bvandeusen 435586388e fix(modal): blocklist offer persists until user acts or image changes
Drops the 8s auto-fade on the post-removal blocklist offer. User
complained it closed too fast to be useful. The offer is already
bounded by image switch (hideBlocklistOffer in updateImage) and
modal close (hideBlocklistOffer in closeModal), so time-based fade
was belt-and-suspenders. Keeps the 2.5s delay after a successful
blocklist so the confirmation text shows before hiding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 21:41:57 -04:00
bvandeusen e76e94a635 feat(modal): inline blocklist offer after pill × removal
When the user clicks the × on a tag pill, the tag is removed from the
image and a small '/Removed 'X' — [⊘ Blocklist] [×]/' control surfaces
below the tag input. One click on ⊘ Blocklist escalates the removal
to a global blocklist add (which also enqueues the retroactive sweep
Celery task, removing the tag from every other image that has it).
Fades after 8s, hides on image switch or modal close.

Targets the composition-descriptor case: WD14 tags like '1girl',
'breasts', 'simple background' that show up on many images. Without
this, after removing the pill the user has to navigate to Settings →
Maintenance → Suggestion blocklist to add the name, or wait for the
next suggestion chip to appear and use its ⊘. Now it's one click at
the point of removal.

Misclick recovery unchanged: the pill × alone only rejects per-image.
The inline offer is opt-in; ignore it or click × to dismiss and the
removal stays scoped to the current image only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 20:54:19 -04:00
bvandeusen 2c486b898c fix(modal): provenance chips use tag_id, not legacy tag=<name>
The provenance section's chip hrefs (artist, post) still built
'/gallery?tag=${name}', which the gallery route silently ignores
since Task 10 switched to ?tag_id=<int>. So clicking the artist or
post chip from the modal rendered an unfiltered gallery.

Tag objects from /image/<id>/tags already include 'id', so the fix
is one line: chip.href = '/gallery?tag_id=${t.id}'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 18:41:13 -04:00
bvandeusen 5b7e033d0c feat(suggestions): auto-accept threshold + retroactive blocklist sweep
Two capabilities that work together to turn the suggestion system from
a 'review every noisy chip' UX into a 'curated tags only, configurable
auto-apply' UX for a solo-user library.

Auto-accept threshold
  Service (tag_suggestions.py):
    - New default auto_accept_general_threshold = 0.95 in _DEFAULTS
    - get_suggestions splits general-category hits at/above the threshold
      into a separate auto_accept_candidates list; the core 'character'/
      'copyright'/'general' keys stay the same shape for existing callers
    - get_bulk_suggestions ignores the new key (no side effects in bulk)
  Route (main.py GET /image/<id>/suggestions):
    - For each candidate: find-or-create Tag(kind='user', name=name), attach
      to image, log SuggestionFeedback(source='wd14'|etc, decision='accepted')
    - Response adds 'auto_accepted: [{id, name, display_name, kind,
      confidence, source}, ...]' so the modal can review + undo
  Config endpoints (main.py):
    GET  /api/suggestions/config/auto-accept-threshold
    POST /api/suggestions/config/auto-accept-threshold {threshold}
    Values > 1.0 effectively disable the feature
  UI (view-modal.js):
    - New renderAutoAccepted block at top of suggestions section with
      green-tinted chips carrying ✕ (undo for this image, logs rejection)
      and ⊘ (blocklist + remove from all images)
    - loadSuggestions refreshes the tag pill list when auto_accepted has
      items so the user sees them in the Tags section too
  Settings:
    - Maintenance tab gains a number input + save button backed by
      /api/suggestions/config/auto-accept-threshold

Retroactive blocklist sweep
  New Celery task app.tasks.maintenance.sweep_blocklisted_tag_from_images
  on the maintenance queue. Enqueued automatically when a name is added
  via POST /api/suggestions/blocklist or appears new in the bulk replace.
  Task body: finds kind='user' Tag matching the name, deletes its
  image_tags rows, deletes the Tag itself. Scope limited to kind='user'
  so deliberate character/fandom/artist tags sharing a blocklisted name
  aren't silently destroyed.

Verification (local dev):
  - Threshold GET/POST round-trip correct (default 0.95, persisted in
    tag_suggestion_config)
  - Image 633 at threshold=0.9: 4 general tags auto-applied, each with
    a SuggestionFeedback row, returned in auto_accepted
  - Blocklist add of 'sweeptestonly' with an attached test tag: Redis
    maintenance queue depth = 1, task body removes 1 image_tags row +
    the Tag row
  - get_bulk_suggestions still works (auto_accept_candidates key skipped)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 17:23:25 -04:00
bvandeusen a510665a17 feat(suggestions): user-managed blocklist for noisy auto-tags
Adds a tag_suggestion_blocklist table + service-layer filter so the
user can permanently suppress specific canonical tag names from the
suggestion stream (WD14 anatomy/composition tags like '1girl',
'breasts', 'nipples' that aren't useful for the discoverability use
case this app targets).

Data model
  - migration k26042201: tag_suggestion_blocklist(name TEXT PK, created_at)
  - model TagSuggestionBlocklistEntry

Service
  - tag_suggestions._blocklisted_names() snapshots the current set
  - get_suggestions filters merged results before grouping, so both
    WD14- and embedding-sourced suggestions respect the blocklist
  - get_bulk_suggestions inherits the filter via its per-image call
    to get_suggestions

API
  - GET  /api/suggestions/blocklist           -> {ok, names}
  - POST /api/suggestions/blocklist           -> add one
  - POST /api/suggestions/blocklist/delete    -> remove one
  - POST /api/suggestions/blocklist/bulk      -> replace the whole list
    (backs the settings textarea save button)

UI
  - modal suggestion chip gets a third action button (⊘) alongside
    accept (✓) / reject (✕). Clicking it adds the name to the
    blocklist, logs a rejection for ML feedback on this image, and
    sweeps every chip on the page carrying that same name.
  - Settings -> Maintenance -> Suggestion blocklist section with a
    monospaced textarea (one name per line) + Save. Loads current
    entries on mount.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 08:41:44 -04:00
bvandeusen 3f11dcdc6c fix(modal,tags): five UX follow-ups
1. Hide 'archive' kind from /tags default view. Archive tags only
   appear when the user explicitly requests them via ?kind=archive.
   _get_tags_with_previews gains a default_excluded_kinds tuple that
   applies when no explicit kind filter is set.

2. Hide the 'Add' submit button on desktop (>=768px). Enter in the
   tag input (and in the fandom picker when no row is highlighted)
   already submits; the button is only needed for mobile virtual
   keyboards where Enter-to-submit is unreliable.

3. Fandom picker keyboard parity: ArrowUp/Down navigate rows, Enter
   picks a highlighted row or (if no row is highlighted) submits the
   whole add-tag form. Escape clears the highlight. Mirrors the main
   tagInput's keyboard block.

4. Tag editor merge bug: the edit modal populated nameInput.value
   with data.tag.display_name — for a character with a fandom, that
   value is 'Name (Fandom)'. Saving without stripping the suffix
   routed to a rename that never collided with the canonical bare
   name, so the merge offer never fired. Now uses data.tag.name (the
   bare form). The (Fandom) display is still shown in the fandom
   input which is rendered separately.

5. Fandom picker styling: match the main tag-form input — same
   padding, border, background, focus state. Drop the separate
   'Fandom' label; the placeholder 'Fandom (optional)' now carries
   the label role.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 08:28:18 -04:00
bvandeusen 03e89416a1 fix(modal): autocomplete + pill list show display_name (fandom suffix)
Two sites in view-modal.js were still calling getTagDisplayName(t.name)
which is a post-refactor passthrough returning the bare name. Both
callers should render display_name so a character with a fandom shows
as 'Ruby Rose (RWBY)' — the whole point of the server sending
display_name alongside name.

Fixes both the autocomplete reveal under the add-tag input and the
sidebar pill list rendered after a successful add/load. bulk-select.js
was already correct (t.display_name || t.name pattern).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 02:38:37 -04:00
bvandeusen 3d6413c639 fix(modal): autocomplete picks resolve by tag_id, not just by name
Clicking an autocomplete row (or pressing Enter on a highlighted row)
now sends tag_id in the add-tag POST. Server attaches that exact tag,
skipping parse_kind_prefix. Same-name ambiguity — e.g. a character
tag 'Yidhari' and a user tag 'Yidhari' both matching the autocomplete
query 'yid' — now resolves by whichever row the user picked, instead
of silently routing to the user-kind fallback.

Bare free-text input (no click) still goes through parse_kind_prefix
with its kind: shortcut and kind='user' default. The fandom picker
path is untouched.

The click handler also stashes tag_id on the dataset so the keyboard
Enter path on a highlighted row picks up the same resolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 18:57:37 -04:00
bvandeusen 27609043e8 refactor(js): tag-utils uses display_name and tag_id
getTagDisplayName is now a passthrough; post-refactor names are bare
and callers that need the character+fandom suffix pass tag.display_name
from the server. createTagChipHtml builds ?tag_id=<int> URLs and uses
display_name for the label. bulk-select.js callers pass display_name
directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:19:52 -04:00
bvandeusen c9fce48a39 refactor(js): use display_name from API instead of client-side split
Every user-facing tag rendering now reads tag.display_name from the
server JSON. The client-side split-on-colon fallback is removed.
Series-info link switches from ?tag=<name> to ?tag_id=<int>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:16:38 -04:00
bvandeusen 54c3d96e45 feat(modal): ambiguous-candidate picker + accept payload rename
Accept suggestion requests now send 'name' (matches Task 8 backend)
and preserve source/confidence for feedback logging. New 409 handler
renders an inline picker with each candidate's display_name; user
clicks one, which re-POSTs with explicit tag_id.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:14:57 -04:00
bvandeusen 3cd6aa2a6a feat(modal): fandom picker wiring
Reveals the picker on 'character:' prefix; autocomplete via
/api/tags/search?kind=fandom; submits fandom_id (preferred) or
fandom_name alongside the character name. Clears on image switch
and modal close.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 16:11:47 -04:00
bvandeusen 03184e7f1c refactor(modal): reserved inline area for add-tag autocomplete
Replace the floating autocomplete popup with a reserved ~5-row inline
area below the add-tag input. It never covers provenance or suggestion
chips — they stay visible side-by-side with the autocomplete list while
the user types. The area scrolls when more matches are loaded, and is
prefilled with top tags on every image load so it's never empty.

The base .tag-autocomplete class (still used by bulk-select and the
fandom picker) keeps its floating behavior; the modal opts in to the
new inline layout via an added .tag-autocomplete-inline modifier.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 19:03:48 -04:00
bvandeusen 40010202b2 fix(modal): reposition tag autocomplete when suggestions load
Suggestions arrive after the modal opens, so the autocomplete dropdown
positioned itself downward before the suggestions section was visible
and stayed there — covering the chips until the input lost and regained
focus. Re-run positionAutocomplete() after renderSuggestions so it flips
above the input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 08:21:52 -04:00
bvandeusen 3944605d33 feat(bulk): consensus ML suggestions + frontend polish
Bulk editor now loads consensus ML suggestions across the current
selection via a new POST /api/suggestions/bulk endpoint, powered by
get_bulk_suggestions() in tag_suggestions.py. A tag is surfaced only if
it was suggested for or already applied to >= 80% of the selection;
coverage counts include images that already have the tag so a near-
universal tag isn't penalized for dropping out of suggestion lists.
Accept-only chips (no reject) match the rest of the suggestions UX.

Frontend polish in the same commit:
- Showcase session dedup (exclude seen ids, reset on second lap) and
  aspect-ratio-aware placeholder heights for better column balancing
- Modal touch-swipe navigation on the image wrapper, auto-focus of the
  tag input on desktop (>=768px), and autocomplete flip-up when the
  dropdown would cover the Suggestions section
- Settings template inline styles replaced with utility classes
- Inline series-editor script extracted to gallery-series-editor.js

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 18:39:47 -04:00
bvandeusen ac9b5cbc06 revert: remove modal tag-input auto-focus
The restore in fb925f5 was based on an observation of an older running
image, not a real regression against current main. User doesn't want the
auto-focus behavior; removing it so the modal opens without moving focus.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:13:37 -04:00
bvandeusen fb925f5e6a feat: ml-worker self-heals models; restore modal tag-input auto-focus
ml-worker now fetches WD14 + SigLIP on container start via an entrypoint
script that calls huggingface_hub with a populated local_dir. HF does a
content-based integrity check, so subsequent starts are a no-op. Transient
HF outages warn and continue when a local copy is already present. This
drops the baked-in model layer (~4 GB) from the image so it pushes cleanly
through registry proxies and no longer bloats every tagged release.

view-modal.js restores the desktop-only auto-focus of the tag input on
openModal (with touch-device guard). The block existed in an earlier
main commit (0bf1961) but was absent from the post-corruption
consolidation commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 21:12:43 -04:00
bvandeusen 0f35a0c484 feat: ML tag suggestions, character/fandom integrity, underscores, modal polish
Consolidated merge of feat/tag-suggestions branch. Original 64-commit history
was lost to git-object corruption in a Nextcloud-synced checkout; this single
commit captures the equivalent diff.

Includes:
- pgvector-backed tag suggestion infra (WD14 + SigLIP centroids, ml-worker
  container, Celery tasks, suggestion service, accept/reject endpoints + modal
  UI with green/red chip buttons)
- Character/fandom integrity: title-case normalization on every write path,
  fandom-id backfill, maintenance task + settings button, migrations g26041901
  + h26041901 to canonicalize legacy rows with case-only duplicate merging
- Tag-underscores + modal polish: WD14 name canonicalization at emit + accept
  + add/bulk-add paths, migration i26041901 for legacy-row rename-or-merge
  across character/fandom/NULL kinds, suggestion-accept refresh parity via
  awaited loadTags, persistent chip tint
2026-04-19 19:50:58 -04:00
bvandeusen 1f33ca6cdf fix: three post-review corrections
- Filter source tags from gallery overlay refresh in view-modal.js and bulk-select.js
- Fix split(':', 1) bug causing series name to render as "undefined"
- Offset reader quick-nav above floating page indicator to prevent overlap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 13:33:28 -04:00
bvandeusen bf7d528917 fix: reader header auto-hide, keyboard null guard, active thumb box-shadow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 12:15:17 -04:00
bvandeusen 1e96208533 feat: add progress bar, auto-hide header, active thumb tracking to series reader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:59:29 -04:00
bvandeusen 8599ffb37c fix: scope tag-chip x button opacity to modal, cache tagActionFeedback ref
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:56:15 -04:00
bvandeusen 7cae463fe8 feat: improve modal sidebar hierarchy, collapse add-to-series, in-context feedback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:51:03 -04:00
bvandeusen 5e675f683f feat: add stagger entry animation to showcase on load and shuffle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:44:13 -04:00
bvandeusen d4258d2ff1 feat: replace showcase tag overlays with ghost count badge, refine nav hint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 08:35:42 -04:00
bvandeusen f65e40b711 feat: filter source tags from gallery items, refine date/video overlays
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 23:57:11 -04:00
bvandeusen bf20eeab5c character-fandom tag association system
Link character tags to fandom tags via fandom_id FK with auto-apply
behavior. Adding character:Saber (Fate) auto-creates fandom:Fate and
applies it to the image. Renaming a fandom cascades to all linked
characters. Includes set-fandom endpoint for retroactive association
and fandom selector dropdown in the tag editor UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 19:53:02 -05:00
bvandeusen ac9a8d145f DRYing the project 2026-02-05 21:23:59 -05:00
bvandeusen c95896693f import filters for pixiv sidecars, tag filtering, and favicon generation 2026-02-04 19:08:50 -05:00
Bryan Van Deusen 042a69f9c3 Optimize scan process with quick/deep modes and archive tag merging
Quick scan (default):
- Pre-load all ImportTask records for O(1) duplicate checks
- Batch task creation commits (50 at a time)
- Cache import settings (5-min TTL)
- Skip pHash comparison for speed

Deep scan (on-demand):
- Full reprocessing of all files
- pHash similarity detection
- Optional thumbnail regeneration
- Optional sidecar re-application

Archive tag merging:
- Add archive tags to existing images when duplicates found
- Works for both hash and pHash duplicate detection

Also fixes:
- Add missing source/post tag icons to gallery templates
2026-01-31 15:18:43 -05:00
Bryan Van Deusen d0fcde38e8 ui polish and importer logging tuning 2026-01-28 09:36:59 -05:00
Bryan Van Deusen 0bf1961175 updated view-modal generating js and references, corrected logging issue with archive import. 2026-01-28 08:36:12 -05:00
Bryan Van Deusen 5af1dcbd4f ui change to improve tag list load 2026-01-24 12:14:27 -05:00
Bryan Van Deusen 9ebeeed133 series management tuning and import archive extraction troubleshooting 2026-01-23 23:05:25 -05:00
Bryan Van Deusen d3e73b9533 changes to series order system. add artist retagging tool 2026-01-23 08:38:07 -05:00
Bryan Van Deusen 82cf3b782b removed trial preview for numbering 2026-01-22 23:29:00 -05:00
Bryan Van Deusen c620c2e041 tweaking series order tools 2026-01-22 23:19:06 -05:00
Bryan Van Deusen a05aee5a07 implement post metadata import and views. implemented series paging and reader view. 2026-01-22 22:19:11 -05:00
Bryan Van Deusen f41acb40f6 correct mass tag edit menu overlap 2026-01-21 22:52:20 -05:00
Bryan Van Deusen fa9b89eca2 updates to transparency filtering, fixing mass tag edit autocomplete 2026-01-21 08:36:46 -05:00
Bryan Van Deusen ede1457abc added dedup tools to settings. first pass of mass tag editting to gallery view. 2026-01-20 23:45:54 -05:00
Bryan Van Deusen cb3897c0b0 changes to mobile styling in modal view, complete reword of backend worker system 2026-01-20 13:14:13 -05:00
Bryan Van Deusen 96f72718bd moving styling and views to be more consistent and for gallery view to be less cumbersome. 2026-01-19 23:41:36 -05:00
Bryan Van Deusen 482c5a8ead import filter work and cleanup 2026-01-18 21:57:08 -05:00
Bryan Van Deusen b3331bad76 additional tag edditing functionality. and delete by tag. added import tuning options to settings 2026-01-18 16:52:32 -05:00
Bryan Van Deusen 4c56b73bc9 correct issue with modal tag management submit function. 2026-01-18 12:32:05 -05:00
Bryan Van Deusen 46144ccc76 major updates to theming, creation of showcase view and polish of existing systems including tagging editting. 2026-01-18 11:32:21 -05:00