Operator-flagged 2026-05-28, two asks.
**1. Provenance posts ate the panel.** Each post card rendered its full
description (180px scroll box) inline, so a few posts pushed everything
else off-screen. ProvenancePanel now collapses the description by
default behind a per-post "Show description ▾ / Hide ▴" toggle (state
keyed by provenance_id, reset when the viewed image changes). Cards stay
compact — platform/date/title/meta/actions — and the operator expands
only the descriptions they want.
**2. Purge tags of retired/system kinds.** The IR migration left
`archive`/`post`/`artist`-kind tags (e.g. `BlenderKnight:Hannah_BJ_Loops`)
that FC no longer creates — the tag input only makes
character/fandom/series/general, and provenance + artists are their own
systems now. (meta/rating were already hard-deleted by alembic 0023.)
- cleanup_service.purge_tags_by_kind(kinds=PURGEABLE_TAG_KINDS) — counts
(dry_run) or deletes tags whose kind ∈ (archive, post, artist).
CASCADE clears the image_tag / alias / allowlist / etc. rows.
- POST /api/admin/tags/purge-retired-kinds (Tier-A, dry-run preview
returns per-kind counts + sample names — the preview IS the
verification of exactly what'll be deleted before committing).
- Tag Maintenance card gets a second section: "Preview retired-kind
tags" → per-kind breakdown + sample → "Delete N retired-kind tag(s)".
Tests: dry-run counts by kind (general survives), commit deletes only
the retired kinds (general + character survive, retired count → 0).
NOTE: a dry-run preview will show exactly which kinds/counts are
present. If the operator's noisy tags turn out to be `general` (e.g. an
IR `source:patreon` that fell back to general during migration), they
won't be caught by the kind purge — the preview makes that visible so
we can decide on a name-based pass separately.
Operator-flagged 2026-05-27: walk the whole project for the same shape
as the min-dim Delete-button silent failure (crypto.subtle TypeError
on plain HTTP). FC runs over plain HTTP per the homelab posture;
Secure-Context-gated browser APIs are undefined on the production
origin.
**Audit results across `frontend/src/`:**
crypto.subtle.digest — 2 sites:
- MinDimensionCard (fixed 2026-05-27)
- BulkEditorPanel (THIS FIX)
navigator.clipboard — 1 site, already guarded:
- utils/clipboard.js writeText with execCommand fallback
serviceWorker / mediaDevices / Push / Web USB|HID|Bluetooth|Serial /
cookieStore / queryLocalFonts / WebAuthn / geolocation
— NOT USED, nothing to fix
Extension scripts (background.js) use crypto.subtle but run from
moz-extension:// which IS a Secure Context — left as-is.
**BulkEditorPanel double bug**
The bulk-delete UI on the gallery selection had been broken since
FC-3k shipped, in two ways:
1. `crypto.subtle.digest` swallowed TypeError on plain HTTP — modal
never opened. Same symptom as min-dim.
2. Even on HTTPS, the modal's `kind="images-selection"` produced
`delete-images-selection-<sha8>` while the backend expected
`delete-images-<sha8>`. The two would never match.
Fix:
- Backend `/api/admin/images/bulk-delete` dry-run response now returns
`confirm_token` (the canonical `delete-images-<sha8>` string).
Integration test `test_bulk_delete_dry_run_returns_counts` pinned to
assert the new field.
- DestructiveConfirmModal gains an `expectedTokenOverride` prop. When
set, it bypasses the `${action}-${kind}-${runId}` formula and uses
the explicit string. This decouples the UI label (`kind`) from the
wire-format token (server-provided), so future endpoints can use a
kind-specific label without their kind name leaking into the token.
- BulkEditorPanel passes `:expected-token-override="bulkProjected?.confirm_token"`
— no client-side crypto, no kind-prefix mismatch.
- MinDimensionCard refactored to the same explicit pattern (was
slicing the 8-char suffix off the backend's token and passing it
through `runId`; now passes the full backend token via
`expected-token-override` directly). Cleaner; one source of truth.
**Banked memory**
`feedback_no_secure_context_apis.md` documents the full table of
Secure-Context-gated APIs, which ones FC currently uses, and how each
is handled. Indexed in MEMORY.md. Sites for the audit also listed in
the memory for future drift-checking.
No other Secure-Context-gated APIs found in `frontend/src/`. The same
shape won't recur unless someone adds a new dependency on one — at
which point the banked memory should fire.
TagPanel now renders SuggestionsPanel below the current-tags area and
adds a per-chip kebab → Rename… (TagRenameDialog). modal store's
removeTag now also POSTs suggestions/dismiss after the delete so removing
an auto-applied tag records a per-image rejection and the allowlist
maintenance sweep won't re-apply it (closes the remove→re-apply loop
identified in the spec).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AliasPicker: debounced autocomplete scoped to the prediction's category
(artist/character map to tag kinds; copyright searches unscoped),
returns the chosen canonical tag id. RenameDialog: PATCH /api/tags/<id>,
surfaces 409 collisions with the FC-2c merge hint.
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>
AppSnackbar mounts once at the app root and exposes a window.__fcToast
function the stores call from error paths. This avoids prop-drilling a
toast emitter through every component while keeping the snackbar
component itself testable in isolation.
GalleryGrid renders shimmer-skeleton placeholders on initial load so the
first paint isn't empty. Skeleton uses the same auto-fill grid columns
as the real items so layout doesn't shift when content arrives.
Modal arrow nav now ignores text-entry elements so typing in the tag
autocomplete doesn't navigate prev/next. Small-screen breakpoint
(<600px) tightens gallery thumbnails to 120px min.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Teleport mounts the modal under <body> so it can break out of the
gallery container's stacking context. Body overflow is locked while open
so background scroll doesn't compete with pan/zoom.
Keyboard: left/right arrows navigate prev/next (preventDefault so they
don't also scroll the body), Esc closes, Tab is trapped within the
modal by browser default since the modal is focused on mount.
TagPanel renders kind-colored chips with the close (×) affordance for
removal, and hosts TagAutocomplete + FandomPicker for additions. Below
900px the panel drops below the media area instead of beside it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TagAutocomplete: kind selector + debounced (200ms) search + keyboard nav
(up/down/enter/esc). When the operator types a character that doesn't
exist, the create flow first opens FandomPicker so the new character tag
lands with a valid fandom_id (otherwise the API rejects it).
FandomPicker lists existing fandoms (cached after first load) with an
inline create-new affordance, so the operator never has to leave the modal
to manage fandom hierarchies.
Tag store also exposes a kind→icon→color mapping that the TagPanel reuses
in Task 22 — keeping chip colors visually distinct per kind without per-app
accent collisions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ImageCanvas mounts an <img> wrapped in the usePanZoom composable —
spreading its handlers onto the container element. Zoom resets when src
changes so prev/next nav doesn't carry zoom state into the next image.
VideoCanvas plays MP4/WebM/Ogg/MOV natively. Non-playable formats render
a friendly explainer with a download fallback, naming the actual MIME so
operators know what they're dealing with, and pointing forward at FC-2e
where transcoding lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GalleryView wires the grid + timeline + modal together. The URL is the
source of truth for which image (if any) is open — clicking an item pushes
?image=N; closing the modal pops it. This makes deep links shareable and
the back button work intuitively.
Below 900px viewport, the timeline drops below the grid as a horizontal
strip so it doesn't compete with thumbnail space on mobile.
Modal store + ImageViewer are placeholders here; real implementations land
in Batch 5 (Tasks 19–22).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>