The TopNav and each sticky sub-header pinned beneath it (Gallery's filter bar,
the Browse/Series/Settings/Subscriptions tabs bars) each painted their OWN
dark-to-transparent gradient (Gallery) or a solid surface band (the rest), so
the fade read as happening twice — dark, fade out, then dark again — instead of
one gradient flowing from the nav down through the sub-nav.
Operator asked to treat the sub-nav as part of the nav with a single gradient.
New shared .fc-chrome-continues primitive (app.css): the nav fades from opaque
to a shared --fc-chrome-seam alpha (on views flagged meta.stickyChrome), and the
sub-header continues from that exact seam alpha to transparent over its own
height. Both reference the same var so the alphas meet at the 64px boundary — no
re-darkening, no doubling. Percentage stops keep it spanning the filter bar's
expanding refine panel; the primitive's blur keeps tabs/controls legible where
the old solid bars had none. --fc-chrome-seam is the single tuning knob.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vuetify 4's MD3 v-tab slider underline rendered wider than the tab and floated
below it (operator-flagged in the v4 review). The active tab's text is already
accent-coloured, so drop the slider and mark the active tab with a subtle accent
fill + rounded top — a clean highlight, app-wide across all tabbed views.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every view showed a large empty band at the top (operator-flagged during the
Vuetify-4 review; pre-existing). Cause: .fc-content had padding-top:64px to clear
the navbar, but TopNav is position:sticky and already reserves its own space in
the v-app flex column — the 64px was a fixed-navbar leftover that double-counted
the offset. Removed it; content now flows directly below the sticky nav (and the
full-height calc(100vh-64px) views no longer overflow).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vuetify 4 is a Material-Design-3 styling refresh with revert snippets, not a
component-API overhaul. FC's exposure was small:
- Bump vuetify ^4, vite-plugin-vuetify ^2.1.0, vue ^3.5, engines node>=24.
- Restore the dropped global CSS reset (minimal reset from the upgrade guide) in
Vuetify's own low-precedence reset layer, so FC's margin-zeroing assumptions hold.
- v-row prop→utility: 'dense' → density=compact (×3), align=center → class=align-center.
- v-snackbar: multi-line removed → min-height=68.
- v-autocomplete #item slot: item→internalItem (item now aliases raw) in TagPicker
+ GalleryFilterBar (item.raw.* → internalItem.raw.*).
ACCEPTED (cosmetic, operator reviews live per plan #158): MD3 typography
(text-body-2 ×73), non-uppercase buttons (v4 dropped the uppercase default),
MD3 elevation. CI verifies BUILD only — the LOOK is the live-review pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Both are Vue-3-compatible majors with no code impact for FC:
- vue-router 5: no breaking changes when not using file-based routing (FC uses a
plain createRouter in router.js).
- pinia 3: drops Vue 2 + deprecated APIs; FC uses string-first setup-syntax
defineStore + no custom pinia plugins, so nothing to change.
Phase A of the Vuetify-4 UI framework migration (milestone #158); Vuetify 4 lands
in phase B.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Explore walk got stuck in dense signatures — neighbours all too similar, so
forward-arrow couldn't escape and Random was the only exit. Root cause: MMR only
diversifies WITHIN the nearest ~400 pool; in a dense cluster that whole pool is
near-identical, so there's no escape route in it.
- gallery_service.similar(reach=0.0, exclude_ids=None): reach>0 widens the pool
(cap 400→1000) and _reach_sample strides across an outward-growing distance span
so the set handed to MMR spans near→mid-far (guaranteed escape routes), not just
the tight cluster. exclude_ids drops already-walked images. Gallery 'more like
this' (reach=0) is unchanged.
- api/gallery similar: parse reach + exclude_ids.
- explore store: default reach 0.4 (auto-diversifies without touching the dial),
pass the breadcrumb as exclude_ids, setReach action.
- ExploreView: a Near↔Far reach slider in the trail.
- tests: _reach_sample math (deeper ranks with higher reach, near kept); similar
exclude_ids drops walked + reach path runs clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends WIP title-tagging to lower-precision cues (sketch/doodle/scribble) safely.
- wip_title.py: soft matcher (word-anchored; sketchbook/kadoodle don't trip it);
WIP_TITLE_SOFT_SOURCE + soft SQL prefilter; apply_wip_image_tags takes a source arg.
- training_data._AUTO_SOURCES += 'wip_title_soft' → the soft tier is PROVISIONAL and
never trains the wip head (a finished "sketch" can't pollute it). Only the hard
tier (wip_title) + manual train.
- ImportSettings.wip_soft_title_tagging_enabled (OFF by default, opt-in). Migration 0087.
- importer: hard tier wins, soft is the fallback (source wip_title_soft).
- backfill: refactored into a shared _backfill_wip_tier; hard always, soft when enabled.
- heads.soft_wip_conflict_audit + daily beat: score soft-tagged images against content
heads, flag ring-loud ones (PresentationReview mode=process) for the review strip —
the operator's "measure if they got falsely tagged" safety.
- api settings toggle; ImportFiltersForm soft toggle.
- tests: soft matcher pos/neg; soft source not a training positive; audit flags
ring-loud + spares quiet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frontend for the system-tag refactor (milestone #157 step 6).
- HeadsCard: new 'Auto-tag work-in-progress' section (enable + tag-confidence +
conflict knobs) for wip/editor process auto-apply, mirroring the chrome card;
copy notes they stay VISIBLE and the head only learns from titles/manual (no
runaway). Presentation copy narrowed to banner-only.
- HiddenReviewStrip: mode-aware — chrome flags read 'hidden as X / Keep hidden /
Un-hide'; process flags read 'auto-tagged X / Keep tag / Remove tag'. Same
endpoints (the backend returns mode), different words.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend for the system-tag behavior refactor (milestone #157). editor screenshot
moves from chrome (hidden) to the PROCESS group (shown, like wip); wip+editor gain
provisional auto-apply so they stop needing endless manual identification —
without a runaway loop.
- tag.py: split PRESENTATION_SYSTEM_TAGS → CHROME_SYSTEM_TAGS (banner) +
PROCESS_SYSTEM_TAGS (wip, editor screenshot).
- heads.py: generalize presentation_auto_apply_sweep → system_tag_auto_apply_sweep
(mode chrome|process). Same Guard 1 (skip human/confirmed) + Guard 2 (ring-loud
conflict → PresentationReview). process mode uses source 'process_auto' and does
NOT hide (hide is a gallery-query effect of group membership).
- training_data._AUTO_SOURCES += 'process_auto' → the head never trains on its own
auto-applied output; only wip_title/manual train it (the runaway break).
- ml_settings: process_auto_apply_enabled (OFF, opt-in) + threshold + conflict
threshold. presentation_review.mode ('chrome'|'process'). Migration 0086.
- gallery_service: default-hide reads CHROME only (editor now shows); Explore
neighbors exclude the whole PROCESS group.
- tasks/ml + celery beat: scheduled_process_auto_apply (daily, opt-in); prune
covers both modes.
- api: ml_admin process_* CRUD+validation; hidden-review returns mode.
- tests: rename chrome sweep calls; new test_process_auto_apply (apply, guards,
mode flag, no-self-train); gallery test asserts editor now visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apply_wip_image_tags relied on result.rowcount, but psycopg reports -1 for a
multi-row INSERT ... ON CONFLICT DO NOTHING (executemany path), so the return
count (and the backfill's reported total) was wrong. Compute the count from a
pre-SELECT of already-tagged ids within the same transaction; keep ON CONFLICT
DO NOTHING as a race-safety belt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ruff I001 fixup for 5719387 — the new .wip_title import belongs after
.thumbnailer (alphabetical), not after .archive_extractor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-apply the `wip` system tag to posts whose TITLE explicitly declares
work-in-progress ("WIP" / "work in progress") — a deterministic, high-precision
complement to the image-based ML `wip` head. WIP images are excluded from the
Explore/gallery browse, so honouring the artist's own label keeps unfinished
pieces out of the main browse.
- services/wip_title.py: precision-first token-anchored matcher (swipe/wiped
never trip it) + sync apply helpers (source='wip_title', ON CONFLICT DO
NOTHING, chunked under the psycopg param ceiling).
- importer: live hook on FRESH import only (never on deep-scan/supersede), so a
manually-removed WIP tag is never re-applied by a routine re-scan.
- maintenance.backfill_wip_title_tags: operator-triggered back-catalogue sweep
(coarse SQL prefilter + regex confirm, keyset-paginated). Deliberately NOT a
beat — a periodic re-run would silently undo manual removals.
- ImportSettings.wip_title_tagging_enabled (default ON, migration 0085) gating
the live hook; GET/PATCH + POST /settings/wip-title/scan.
- Settings UI: toggle + "Scan existing posts" button.
- Tests: pure matcher unit tests + integration (apply idempotency, backfill
precision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Frontend is pure JS (no .ts/JSDoc); CI never ran vue-tsc. Removed the devDep and its orphaned `check` script instead of bumping to v3, and updated the ci.yml comment to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ci.yml: add pull_request trigger (base dev) so renovate/* PRs get validated before merge; dev→main flow unchanged (base main), no duplicate runs. renovate.json: group vite/vitest/plugin-vue/happy-dom/@vue-test-utils into one PR (version-coupled).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The gate at a fixed 0.80 couldn't catch the real pain: Interpreter (fresh ==
cached, verified by probe) confidently mis-detects short ASCII English like
"... WIP Part 1" as German at 0.86 — above the floor — so it was accepted and a
re-translate reproduced it. Confidence alone can't separate the 0.86 collision
(genuine German lands there too), and single-word mis-flags sit at a confident
1.0 no floor catches.
Two operator-approved levers:
- Acceptance floor is now a live Settings value (ImportSettings.
translation_min_confidence, default 0.90; surfaced in the Translation card), so
it's tunable without a redeploy. _accept takes the threshold as a parameter.
- Per-post sticky override (Post.translation_override: auto/force/original).
'force' stores a translation even below the floor (rescue a skipped
legit-foreign title); 'original' keeps the original and clears any stored
translation (kill a confident mis-flag no floor catches). The sweep honors it
on every run and _reset_translations skips 'original', so the choice survives a
Re-translate-all. POST /api/posts/<id>/translation-override applies it
immediately (translate now when the service is up, else queue for the sweep).
UI: PostTranslationControl on the posts-feed card.
Migration 0084 (both columns + a CHECK on the override). The feed + provenance
serializers expose translation_override.
With a stricter floor the rollback finally works: raise it -> Re-translate all ->
the 0.86 mis-flags are rejected and restored to the original; force /
keep-original handle the residual either way.
Tests: gate thresholds against the param (0.86 rejected at 0.90, explicit-floor
cases); sweep force/original + re-translate-skips-original; override endpoint
(validation, original clears, force queues when disabled, feed exposes it);
settings min_confidence default/save/validate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
Every tag suggestion is a canonical DB tag now (tagging-v2 #114: heads + CCIP
score EXISTING concept tags). The pre-heads apparatus for model-predicted tags
that didn't exist in the DB — creates_new_tag / raw_name / via_alias, the
/suggestions/alias endpoint + add_alias_and_accept, AliasPickerDialog, and the
store's aliasAccept/removeAlias — was dead and is removed.
The type-to-add dropdown was TWO row sources (server autocomplete + the image's
ML suggestions) merged with a dedup that dropped the %-bearing suggestion row
when the debounced server hit landed — the operator's "confidence % flickers
then vanishes". Now it's ONE list of DB-tag matches, each annotated with the
model's confidence (join by canonical_tag_id) when the tag was scored for this
image. No dedup, no flicker; picking a suggested tag still records acceptance
via TagPanel.findPending.
Single per-image fetch: score_image now reports above_threshold per row
(computed vs the head's own suggest cut, separate from the inclusion floor), so
the rail makes ONE min=0 request and derives the panel (above_threshold) and the
dropdown (all, text-filtered) client-side — the two /suggestions calls collapse
to one. Manual "Create 'X' as <kind>" (novel typed names) is unchanged; the
alias table + tag-side alias admin + auto-apply alias matching are untouched.
Tests: gate/serializer assertions updated (above_threshold; dropped dead-field
+ alias-endpoint checks); frontend spec seeds via the single load and covers the
byCategory/aboveByCategory split.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
Calibrated against fresh probes once Interpreter returned real langdetect
confidence: genuine German detected at 1.0, a correctly-detected but ambiguous
latin string at 0.86. Set _MIN_LATIN_CONFIDENCE to 0.80 (below that band) so
legitimate ambiguous non-English still translates while genuinely-unsure guesses
are rejected. Real langdetect also fixed the original mis-flag at the source, so
this floor is a safety net, not the primary fix. Pin 0.86-accepted in the gate
test to guard against bumping the floor back up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
Interpreter now returns a real per-detection confidence (source stays "auto"),
so curator can reject the mis-detections it was blindly storing — e.g. a short
English title mis-labelled as German and rewritten into the archive.
The gate consumes ONLY Interpreter's own reported detection — curator does no
language detection of its own (Scribe rule 133): a field is stored when the
engine actually translated it AND either the detected language is CJK
(script-detected, reliably high — ja/ko/zh trusted outright, incl. pure-kanji
Japanese that lands as zh ~0.75) or the reported confidence clears a
latin-script floor (_MIN_LATIN_CONFIDENCE = 0.90). A latin detection below the
floor keeps the original and marks the post handled; a missing confidence fails
open. The client already sent source="auto" and parsed confidence, so this is
purely the gate + tests.
Tests: pinned interpreter-client test now asserts source stays "auto"; new
pure-unit gate tests (CJK trusted / latin floor / case-insensitive / fail-open)
in the fast lane; end-to-end reject-low-latin, accept-high-latin,
accept-low-cjk sweeps.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
Throughput: translate_posts now runs every 8h (was daily) as the
steady-state cadence for newly-imported posts, and the Settings
"Translate now" button runs it in drain mode (run-until-done, no reset)
so one press clears the whole untranslated backlog instead of a single
300-post chunk. The interrupt/backoff re-enqueue now preserves the drain
flag so a bulk drain resumes cleanly after an Interpreter restart.
Misdetection groundwork: surface the detector's confidence from the
Interpreter client (it was in the detectedLanguage payload but discarded)
and add a read-only "Test translation" box — POST /settings/translation/
probe + TranslationCard UI — that shows detected language + confidence +
engine + result for pasted text, without saving. Lets the operator see
why a short/abbreviation-heavy English title gets mis-detected so the
detection guard (min-length + confidence floor) can be tuned from real
numbers. The guard itself follows once the mis-detected cases are probed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgZP9v2otxVJymiYsnVuMy
The tonal fill (esp. character = info) is intentionally faint and blends into
the dark tag rail. Add a thin border in each chip's own kind colour via
color-mix on currentColor (the tonal chip's themed foreground), defining the
edge without changing the fill. Theme-aware in both light and dark.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The larger size=default chip widened Vuetify's negative start-margin on the
leading kind-icon, placing it left of the .v-chip__content box whose
overflow:hidden (the name-truncation guard) then clipped its left edge.
Zero the icon's negative inline-start margin so it sits inside the clip box;
the chip's 12px padding keeps a comfortable left inset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>