Extracts gallery.js's hand-rolled inflightId pattern into a new
useInflightToken composable; adopts in every store that previously
had no guard against late-response overwrites or wrong-image URL
interpolation.
Two operator-impacting bugs the audit (workflow wf_bbe3fdb1-e62)
flagged:
- modal.removeTag rolled back the chip rail unconditionally even
when only the secondary dismiss POST had failed — UI lied until
refresh. And all tag-mutation URLs interpolated currentImageId
AFTER an await, so a fast prev/next could route DELETE/POST to
the wrong image. Both fixed: split try/catch (dismiss failure
surfaces a warning, doesn't roll back the delete); imageId
captured at call-time and used in URLs throughout.
- suggestions.accept dereferenced currentImageId after the awaited
POST /api/tags, so the subsequent /suggestions/accept could
apply A's chosen tag to image B AND push it to B's allowlist.
Fixed by capturing imageId at click-time + inflight guard on
load().
Same shape across artist / downloads / artistDirectory /
tagDirectory / posts stores: rapid filter/nav changes used to
interleave responses (last-writer-wins). Now the late response is
discarded and the most-recent request wins. Filter-change-during-
search no longer drops the second fetch because the loading flag
was still true from the first.
gallery.js's inflightId removed in favor of the shared composable
so the pattern stays consistent.
Four coupled operator-asked changes to the view modal (Scribe plan #509):
1. **Autofocus tag entry on modal open** — TagAutocomplete grabs focus
in onMounted/nextTick so the caret is in the input the moment the
modal renders. No click needed to start typing.
2. **General suggestions expanded by default** — SuggestionsPanel's
general-category group now mounts with `:default-open="true"`.
Operator can collapse if too noisy, but the v1 frame shows them.
3. **Lower general threshold default 0.95 → 0.50** — MLSettings.
suggestion_threshold_general default matches character. Alembic
0029 also bumps the existing singleton row's value if it's still
at the old 0.95. Operator can re-tune from Settings → ML.
4. **Retire `copyright` + `artist` as ML suggestion categories** —
neither feeds a Tag.kind (`artist` retired in FC-2d-vii-c, never
really existed as a copyright tag-kind). They were surfaced in the
suggestions pipeline + threshold settings UI but had no follow-
through. Drop from SURFACED_CATEGORIES, suggestions._threshold_for,
ml_admin GET/PATCH allowlist, MLSettings columns (alembic 0029
drops the two columns), frontend CATEGORY_ORDER + CATEGORY_LABELS,
SuggestionsPanel.peopleCats, AliasPickerDialog kind-check, and
MLThresholdSliders rows.
Out of scope (intentional): `tag_kind` Postgres enum still includes
`artist` for historic Tag row queryability (per the model comment);
no operator pain reported, no enum-shrink needed.
Tests:
- test_surfaced_categories asserts {character, general}, excludes
artist + copyright.
- test_threshold_for_artist_is_unsurfaced extended to cover copyright.
- test_get_and_patch_settings asserts new 0.50 default and the absent
artist + copyright keys in the GET payload.
New composables/useAsyncAction.js owns the loading/error/try-finally
lifecycle. Migrated 11 stores: credentials, downloads, sources, posts
(error=raw) + ml, artistDirectory, tagDirectory, showcase, suggestions,
seriesReader, modal (error=message). The errorAs option preserves each
store's existing error shape so store.error keeps the same type for
components (~50 consumption sites unchanged). Stores whose catch also
reset data (suggestions/seriesReader/modal) clear it upfront instead.
Deliberately NOT migrated (special control flow, would change behavior):
artist (conditional 404 catch + dual loading states), migration (rethrows),
gallery (inflight-id stale-response guard), and the Shape-B no-catch /
loaded-guard / keyed-cache stores.
Net -77 lines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Store: load per-image suggestions, accept (creates the tag first for
raw/creates_new_tag suggestions, then accepts by id), aliasAccept,
dismiss (client-side hide for raw tags), success toasts folded in.
Panel: people/sources groups always open, General collapsed by default;
alias picker dialog wired; shimmer skeleton while loading. SuggestionItem
shows score %, +new badge for raw tags, kebab menu. AliasPickerDialog
referenced (lands in Task 15).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>