From 104cac5dca42dc505e4f6a4c01d83a5fe10bd74c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 00:56:47 -0400 Subject: [PATCH 01/18] =?UTF-8?q?fix(ui):=20tooltip=20readability=20?= =?UTF-8?q?=E2=80=94=20dark=20bg=20+=20parchment=20text=20(was=20light-on-?= =?UTF-8?q?light)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-flagged 2026-05-28: tooltips (e.g. the action buttons in Subscriptions → Subscriptions) render near-white-on-near-white, unreadable. Cause: Vuetify's default v-tooltip pairs `on-surface-variant` text with an `surface-variant` background. FC's theme deliberately maps `on-surface-variant` to vellum (#C2BFB4 — a light cream, the correct muted-text token for captions/hints on the dark page) but never defines `surface-variant`, so Vuetify auto-generates a light-ish tooltip background. Light text on light bg. Fix is tooltip-specific so it doesn't disturb the (correctly light) muted-text token elsewhere. New app-global stylesheet frontend/src/styles/app.css, imported in main.js AFTER vuetify/styles (equal-specificity rule wins by source order), overrides `.v-tooltip > .v-overlay__content` to a dark elevated panel (surface-bright = slate #2C313A) with high-contrast parchment text (#E8E4D8) + a subtle border + shadow. Applies to every tooltip in the app, so the fix is consistent rather than per-component. --- frontend/src/main.js | 3 +++ frontend/src/styles/app.css | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 frontend/src/styles/app.css diff --git a/frontend/src/main.js b/frontend/src/main.js index 0882de9..d4ce856 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -3,6 +3,9 @@ import { createPinia } from 'pinia' import { createVuetify } from 'vuetify' import 'vuetify/styles' import '@mdi/font/css/materialdesignicons.css' +// App-global CSS overrides — imported AFTER vuetify/styles so equal- +// specificity rules (e.g. the tooltip readability fix) win by source order. +import './styles/app.css' import App from './App.vue' import router from './router.js' diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css new file mode 100644 index 0000000..55b13a4 --- /dev/null +++ b/frontend/src/styles/app.css @@ -0,0 +1,21 @@ +/* App-global overrides. Imported in main.js AFTER 'vuetify/styles' so + these win on equal selector specificity by source order. */ + +/* Tooltip readability fix (operator-flagged 2026-05-28). + Vuetify's default v-tooltip pairs `on-surface-variant` TEXT with an + `surface-variant` BACKGROUND. FC's theme deliberately maps + `on-surface-variant` to vellum (#C2BFB4 — a light cream, correct for + muted captions/hints on the dark page) but never defines + `surface-variant`, so Vuetify auto-generates a light-ish background: + light text on light bg → near-white-on-near-white, unreadable. + + Tooltips want the inverse of muted body text — a dark, slightly + elevated panel with the HIGH-contrast parchment text. Fixing it here + (not by changing on-surface-variant) keeps captions/empty-states + correct while making every tooltip in the app legible. */ +.v-tooltip > .v-overlay__content { + background: rgb(var(--v-theme-surface-bright)); /* slate #2C313A */ + color: rgb(var(--v-theme-on-surface)); /* parchment #E8E4D8 */ + border: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.25); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); +} From e1fc65bd1bdbcb84fdb48716097cc1048bdf81ff Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 01:12:20 -0400 Subject: [PATCH 02/18] feat(provenance+tags): collapse provenance descriptions; purge retired-kind tags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- backend/app/api/admin.py | 19 ++++++ backend/app/services/cleanup_service.py | 42 +++++++++++++ .../src/components/modal/ProvenancePanel.vue | 18 +++++- .../settings/TagMaintenanceCard.vue | 62 +++++++++++++++++++ frontend/src/stores/admin.js | 14 +++++ tests/test_api_admin.py | 50 +++++++++++++++ 6 files changed, 203 insertions(+), 2 deletions(-) diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py index fd3c5ce..e6011b3 100644 --- a/backend/app/api/admin.py +++ b/backend/app/api/admin.py @@ -206,3 +206,22 @@ async def tags_prune_unused(): ) ) return jsonify(result) + + +@admin_bp.route("/tags/purge-retired-kinds", methods=["POST"]) +async def tags_purge_retired_kinds(): + """Tier-A: delete tags whose kind is a system/legacy kind FC no + longer creates (archive / post / artist — the IR-migration leftovers + like `BlenderKnight:Hannah_BJ_Loops`). dry-run preview returns + per-kind counts + a sample so the UI can show exactly what'll go + before the operator confirms with dry_run=false.""" + from ..services.cleanup_service import purge_tags_by_kind + + body = await request.get_json(silent=True) or {} + dry_run = bool(body.get("dry_run", False)) + + async with get_session() as session: + result = await session.run_sync( + lambda sync_sess: purge_tags_by_kind(sync_sess, dry_run=dry_run) + ) + return jsonify(result) diff --git a/backend/app/services/cleanup_service.py b/backend/app/services/cleanup_service.py index 7025b8b..4470d3f 100644 --- a/backend/app/services/cleanup_service.py +++ b/backend/app/services/cleanup_service.py @@ -369,6 +369,48 @@ def prune_unused_tags(session: Session, *, dry_run: bool = False) -> dict: return {"deleted": len(ids), "sample_names": sample} +# System/legacy tag kinds that FC no longer creates: the tag input only +# makes character/fandom/series/general; provenance (post grouping) and +# archive membership are their own systems now, and artists are +# first-class Artist/Source rows. These linger only on IR-migrated rows. +# meta/rating were hard-deleted by alembic 0023, so they're not listed. +PURGEABLE_TAG_KINDS = ("archive", "post", "artist") + + +def purge_tags_by_kind( + session: Session, *, kinds: tuple[str, ...] = PURGEABLE_TAG_KINDS, + dry_run: bool = False, +) -> dict: + """Count (dry_run) or delete all tags whose kind is in `kinds`. + + CASCADE on image_tag / tag_alias / tag_allowlist / + tag_reference_embedding / tag_suggestion_rejection / series_page + clears the related rows on the parent DELETE. + + Returns: + {"by_kind": {kind: count, ...}, "count": total, + "sample_names": [first 50], and on live runs "deleted": total} + """ + rows = session.execute( + select(Tag.id, Tag.name, Tag.kind).where(Tag.kind.in_(kinds)) + ).all() + by_kind: dict[str, int] = {} + for _id, _name, kind in rows: + key = kind.value if hasattr(kind, "value") else str(kind) + by_kind[key] = by_kind.get(key, 0) + 1 + sample = [name for _id, name, _kind in rows[:50]] + total = len(rows) + if dry_run: + return {"by_kind": by_kind, "count": total, "sample_names": sample} + if total: + session.execute(Tag.__table__.delete().where(Tag.kind.in_(kinds))) + session.commit() + return { + "by_kind": by_kind, "count": total, "deleted": total, + "sample_names": sample, + } + + # --------------------------------------------------------------------------- # FC-Cleanup additions (2026-05-26): retroactive audit of import-filter rules. # --------------------------------------------------------------------------- diff --git a/frontend/src/components/modal/ProvenancePanel.vue b/frontend/src/components/modal/ProvenancePanel.vue index 178c6af..507a9e6 100644 --- a/frontend/src/components/modal/ProvenancePanel.vue +++ b/frontend/src/components/modal/ProvenancePanel.vue @@ -37,9 +37,13 @@ View images from this post + {{ expanded[e.provenance_id] ? 'Hide description ▴' : 'Show description ▾' }}
@@ -74,7 +78,7 @@ From a459d21a6510ba0c91fd9598ea576d846bc52686 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 08:10:04 -0400 Subject: [PATCH 06/18] feat(dashboards): 1600px max-width, richer Downloads filters, needs-attention + sticky headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator-flagged 2026-05-28: the Subscriptions/Downloads dashboards were full-bleed and thin on filtering. Chosen via AskUserQuestion. **Layout** - SubscriptionsView capped at a centered max-width 1600px (covers all three subtabs) so rows aren't a mile wide on ultrawide monitors. - Sticky control headers on both tabs (top: 48px, below the top nav, opaque bg) so filters/stat-chips stay reachable while scrolling a long list. **Downloads filters (all four requested)** - Stat chips are now clickable filters: click Queued/Running/Completed/ Failed/Skipped to filter the list to that status; the active chip is outlined + elevated; re-click clears. - Free-text search box over the loaded events (artist / platform / error substring). - Artist filter: the filter popover's numeric "Source ID" field is replaced with an artist autocomplete (sources.autocompleteArtist → artist_id, which /api/downloads already supports). Pill shows the artist name. - "Show no-change scans" toggle (default OFF): hides status=ok/skipped rows with 0 files (the scheduled scans that found nothing) so real downloads + failures stand out. **Subscriptions** - "Needs attention" quick-filter chip: one click to show only artists with sources that have errors OR have never been checked; chip shows the count and disables the status dropdown while active. Frontend-only — backend filter params (status/artist_id/date) and the /api/downloads endpoint already supported everything. --- .../subscriptions/DownloadStatChips.vue | 13 ++- .../subscriptions/DownloadsFilterPopover.vue | 64 ++++++++++--- .../components/subscriptions/DownloadsTab.vue | 95 +++++++++++++++---- .../subscriptions/SubscriptionsTab.vue | 36 ++++++- frontend/src/views/SubscriptionsView.vue | 8 +- 5 files changed, 184 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/subscriptions/DownloadStatChips.vue b/frontend/src/components/subscriptions/DownloadStatChips.vue index ce5c975..e2e3ab1 100644 --- a/frontend/src/components/subscriptions/DownloadStatChips.vue +++ b/frontend/src/components/subscriptions/DownloadStatChips.vue @@ -3,9 +3,11 @@ {{ s.label }} {{ stats[s.key] ?? 0 }} @@ -16,7 +18,11 @@ diff --git a/frontend/src/components/subscriptions/DownloadsTab.vue b/frontend/src/components/subscriptions/DownloadsTab.vue index 750f9f6..98dce0d 100644 --- a/frontend/src/components/subscriptions/DownloadsTab.vue +++ b/frontend/src/components/subscriptions/DownloadsTab.vue @@ -1,20 +1,41 @@