70d4017cf60f46415d5ae1fc666ff9ff22b1dd05
95 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
70d4017cf6 |
feat(activity): search/filter on both Activity-tab panes
Recent failures gains a client-side search over the already-loaded 24h rows (task/queue/target/error), shown as a filtered/total count alongside the existing error-type chips. All recent activity gains a debounced server-side task-name search (new `task` ILIKE param on /runs) so it spans the full history, not just the loaded page. LIKE wildcards are escaped so task names' literal underscores match literally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c999c64cbe |
feat(suggestions): tag-input dropdown searches the full prediction set
The typed dropdown sourced the threshold-filtered panel list (>= 0.70 general), so low-confidence actions/features the model DID predict never appeared — forcing hand-typed custom tags instead of accepting the model's canonical formatting. Add a threshold override: SuggestionService.for_image(threshold_override=) and GET /images/<id>/suggestions?min=<f> surface EVERY stored prediction (down to the 0.05 store floor), alias-resolved and normalized, still excluding applied/rejected and unsurfaced categories. The suggestions store gains allByCategory + loadAll (min=0); the dropdown searches that full set (cap 20), while the Suggestions panel stays curated at the configured threshold. Accept/dismiss drop from both lists. Operator-asked 2026-06-09. Test: a 0.30 general prediction is hidden by default but surfaced with threshold_override=0.0; unsurfaced categories still excluded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e4cebf70d1 |
feat(series): browse search + per-card kebab (rename/delete)
The Series browse tab had no way to find a series in a long grid and no per-series actions. Add a search field (instant client-side name/artist filter over the already-loaded list) and a kebab on each card with Rename (reuses TagRenameDialog → PATCH /api/tags/<id>, with its collision-merge flow) and Delete (confirm dialog → DELETE /api/admin/tags/<id>; series_page/chapter/ suggestion cascade, images kept). Gap badge moved to the cover's top-left so the kebab can sit top-right. Operator-asked 2026-06-09. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a8f624a0f1 |
fix(posts): link duplicate items to every post + prune bare shells
The native Patreon backfill flooded the feed with bare 'Post <id>' shells (1589 for Anduo). Root cause: PostAttachment.sha256 was GLOBALLY unique, so a non-art file reused across posts only ever linked to the first one, and _capture_attachment created the Post before that dedup check — leaving later posts with no image and no attachment. Duplicate IMAGES had the mirror gap: attach_in_place returned duplicate_hash/duplicate_phash before _apply_sidecar, so the second post got no provenance row, and the feed only rendered via primary_post_id (one post per image). Operator requirement: a duplicate item must show on EVERY post it appears in. Unify the fix as link-not-suppress: - importer: on duplicate_hash / duplicate_phash(larger_exists), append an image_provenance row for the new post (keep primary on the first). Both the download path (attach_in_place) and the filesystem path (_import_media). - post_feed_service: render thumbnails by image_provenance UNION primary_post_id, so a cross-posted image shows on every post (and legacy primary-only images still show). - PostAttachment: per-post uniqueness — drop UNIQUE(sha256), add partial UNIQUE(post_id, sha256) + partial UNIQUE(sha256) WHERE post_id IS NULL (migration 0043); _capture_attachment dedups per-(post,sha) over the shared sha-addressed blob, so no post is left bare. - cleanup: new prune-bare-posts maintenance action (cleanup_service _bare_post_conditions shared by preview/count/delete per preview/apply parity; admin endpoint; PostMaintenanceCard). Deletes posts with zero image links (primary or provenance) AND zero attachments. Run after the feed fix so a hidden provenance link spares the post instead of deleting it. Tests: dup image shows on both posts; dup attachment shows on both posts; feed renders provenance-linked duplicates; prune-bare delete-path == preview. Operator redeploys (migration 0043) then runs the prune to clear the shells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
978959bdc4 |
feat(series): manage-view redesign — big pages, editable Part #, slide-over picker (FC-6.4)
Operator feedback: thumbnails too small to judge order, no obvious way to mark
'this installment is Part 2', and the permanent two-pane picker was busy and
competed with the ordering work.
- Full-width parts, each a card with a big page grid (150px, contain so whole
pages are visible) and drag-to-reorder; positional page number as a badge.
- Editable Part # (hero field) backed by new series_chapter.stated_part —
separate from the auto-managed chapter_number, mirroring the page_number vs
stated_page split so reorder/delete renumbering can't wipe a hand-set part.
Missing-Part hints when consecutive parts' stated_part jump >1.
- Each part labels its source post (derived from pages' primary_post_id) and
shows the printed-page range with clear labels.
- Picker demoted to an on-demand right slide-over ('Add pages') with a target-
part selector; part actions (move/merge/delete) collapsed into an overflow ⋮.
alembic 0042 adds series_chapter.stated_part (nullable int).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
19a91a1641 |
feat(series): Suggestions tab + matcher controls — frontend (FC-6.3)
Completes FC-6.3 with the UI. - SeriesView gains tabs: Browse (the existing grid) + Suggestions. - Suggestions tab: pending matches as rows (post → series, per-signal strength chips, score), Add (→ chapter) / Skip (→ dismiss); a "Matching on" toggle and a threshold field (both DB-backed via /settings/import), and a Rescan button that enqueues the background matcher. - seriesSuggestions store wires load / accept / dismiss / rescan / settings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9e262cc5f0 |
feat(series): Add-to-series control + Series browse view + nav (FC-6.2)
Completes FC-6.2 with the UI. - PostSeriesMenu: a "Series ▾" control on each post card — "New series from this post" (promote → navigates to manage) and "Add to existing series…" (dialog with a browsable picker loaded from GET /api/series, client-side filtered — avoids the empty-autocomplete #712 trap). - SeriesView (/series): a top-level Series browse grid — cover, name, artist, chapter/page counts, gap badge; sort recent|name|size; cards → manage/read. meta.title adds it to the nav automatically (peer of Posts). - seriesBrowse store for the list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8ad40da145 |
feat(series): chapter-aware manage view + reader — frontend (FC-6.1)
Completes FC-6.1: the series management UI now works in chapters. - SeriesManageView: chapters as cards (inline-rename, stated-page range inputs, move up/down, merge-into-previous, delete, pick-as-add-target), pages drag-reorder WITHIN a chapter, a "gap: N-M missing" badge between chapters with a stated-page hole, and Add chapter / Add placeholder. The picker adds the selection into the targeted chapter. - seriesManage store: chapter CRUD + reorderChapters/moveChapter/mergeChapter/ reorderPages actions; consumes chapters[]/gaps[]; addSelected targets a chapter. - Reader: page_number is now within-chapter, so anchors switched to a global `seq` (reading-order position) — fixes scroll/jump/active collisions across chapters — plus chapter-title dividers at each chapter boundary. - Updated seriesManage.spec to the chaptered store shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b7d07324ee |
fix(subs): stop the lock/reload on source actions + regroup the row buttons
Lock/reload: every inline source action (check / backfill / recover / toggle / remove) ended by refetching the WHOLE subscription list (store.loadAll / refresh), which blocked the UI and re-rendered the table — collapsing the expanded row, which read as "locks then resets." The action APIs already return the updated source, so the store now patches that one row in place (_patchSource / _dropSource); the post-action loadAll/refresh calls are gone. Toggling enabled, starting/stopping a backfill, recovering, and removing are now instant and leave the expansion intact. Button regroup (operator-flagged: tiny, mis-clickable, not grouped by function): - New shared SourceActions.vue used by desktop SourceRow + mobile SourceCard. - Frequent actions stay as size="small" buttons: Check, Backfill/Stop. - Low-frequency / destructive actions move into a labelled overflow (⋮) menu — Preview backfill, Recover dropped near-duplicates, Remove source — so they can't be fat-fingered, and the labels spell out recover-vs-backfill. - Edit moves next to the source URL (its identity), out of the action cluster where it sat beside Remove. - Single-source Remove now confirms (it had no guard before). Tests: store patches/drops in place without dropping the cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3c89223dcb |
feat(tags): retro-normalize existing tags to Title Case + merge case-collisions (plan #714)
Follow-up to #701: new tags are saved canonical, but the back-catalog keeps whatever casing it was created with. This adds a maintenance action that Title-Cases every existing tag (collapsing whitespace) and merges case/whitespace-variant duplicates into one. Backend: - tag_service.normalize_existing_tags(session, *, dry_run): groups all tags by (kind, coalesce(fandom_id,-1), canonical_name). Per group it picks a survivor (prefer an already-canonical member → no rename/self-alias; else the best-connected tag → fewest FK repoints; else lowest id), merges the variants INTO it via the tested TagService._do_merge (image_tag/allowlist/embedding/ aliases/series_page repoints + protective ML aliases), then renames the survivor to canonical. Losers are deleted before the rename so there's no transient unique-index clash; commits per group and isolates failures per group. Idempotent — an already-canonical lone tag is a no-op. - normalize_tags_task (maintenance queue, asyncio.run + per-task NullPool async engine, soft 1800/hard 2400) — recovery/timeout/duration covered by FC-3i. - POST /api/admin/tags/normalize: dry_run=true returns a projection inline (group/collision/rename counts + sample); dry_run=false enqueues the task. Frontend: a "Standardize tag casing" section in TagMaintenanceCard (Cleanup tab) — preview → apply (polls the activity dashboard to terminal status), behind a back-up-first warning. admin store gains normalizeTags(). Tests: tests/test_tag_normalize.py — dry-run counts, live merge + image-tag dedup/repoint, idempotency, same-name-different-fandom and -different-kind kept separate, ML-known loser keeps a protective alias. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4fe53cdf6b |
fix(modal/tags): fandom list, modal kebab, ESC-after-accept — #712 #711 #700
#712 — fandom picker showed no existing fandoms: loadFandoms enumerated via /tags/autocomplete with q=' ', which the backend strips to empty → returns []. Switch to the cursor-paged /tags/directory?kind=fandom (loop all pages); drop the load-once guard so the dialog reflects fandoms created elsewhere. #711 — modal tag-chip kebab never opened: the kebab + menu were nested INSIDE the v-chip, which swallowed the click / mis-anchored the teleported menu. Un-nest it as a sibling v-btn using the standard v-menu activator slot (Vuetify wires the click and stacks the overlay above the modal natively). Removes the openTagId workaround. #700 — ESC didn't close the modal after accepting a suggested tag: the guard suppressed close while ANY .v-overlay--active existed, which includes tooltips — a lingering tooltip blocked the close. Exclude .v-tooltip from the guard so only real interactive overlays (menus/dialogs) keep ESC from closing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
911d535f56 |
fix(artists): card preview dead-space + empty-state flicker on first load
Two operator-reported Artists-view bugs. Layout: ArtistCard previews used aspect-ratio: 3/1 + max-height: 220px — when a lone grid column got wide (>~660px), the max-height made the aspect-ratio box shrink its own WIDTH to keep the ratio, leaving dead space beside the 3 thumbnails. Dropped max-height (kept the min-height floor) so the strip fills the card width; lowered the grid min 440→360px so a lone column stays <732px (strip ≲244px) and desktop gets 2+ columns. Flicker: isEmpty checked !loading, but on the first render loading is still false (the initial loadMore fires in onMounted, after paint) so "No artists match" flashed for a frame before the spinner/data. Added a reactive `loaded` flag (true after the first load attempt, reset on reset()); isEmpty now also requires it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e82c2ee57b |
feat(subscriptions): dry-run backfill preview — B4 preview (plan #708)
Owning the walk lets an operator gauge "is this source worth a backfill?" before
arming one. ingest_core.Ingester.preview walks the first few feed pages and
counts media NOT already in the seen/dead ledgers, downloading nothing
(read-only). download_backends.preview_source resolves the campaign id + runs it
(native-only, mirrors verify_source_credential / run_download); POST
/api/sources/{id}/preview returns {total_new, posts_scanned, has_more, sample[]}
(409 on auth/drift/unresolvable, 400 for gallery-dl platforms). PatreonClient
gains post_meta(post) for the sample's title/date.
UI: a Patreon-only Preview button (mdi-eye-outline) on SourceRow + SourceCard
opens PreviewDialog — self-fetches with loading / error / empty / result states
and a "Start backfill" shortcut. Store action previewSource.
Tests: preview counts new media without downloading + samples only posts with
new items; page_limit caps the walk + flags has_more.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ec43e823e1 |
feat(patreon): recovery UI + gallery-dl cutover — build step 5 (plan #697)
Final step of the native Patreon ingester: a first-class Recovery action, and removal of the now-dead gallery-dl Patreon path. Recovery (rules #23/#24/#27 — full product, with UI): - source_service.start_recovery arms the #693 backfill state machine PLUS `_backfill_bypass_seen`, flipping download mode to recovery (bypass the seen-ledger to re-fetch dropped-and-deleted near-dups and re-evaluate under the current pHash threshold). Stop via the shared stop_backfill. - SourceRecord exposes backfill_bypass_seen; POST /sources/{id}/backfill gains action="recover". - Frontend: Recovery button (Patreon-only, mdi-backup-restore) on SourceRow + SourceCard; the running badge labels "Recovering (N)" vs "Backfilling (N)"; the Stop tooltip says "Stop recovery". sources.js recoverSource + SubscriptionsTab onRecover. Cutover (rule #22 — no legacy): - gallery_dl: removed PLATFORM_DEFAULTS["patreon"], the patreon files/cursor branch in _build_config_for_source, and the patreon/Mux yt-dlp Referer/Origin block (was patreon-specific and wrongly tagged the other platforms' yt-dlp fetches; native ingester owns it now). - download_service: removed the dead campaign-id-retry helpers (_looks_like_campaign_id_failure / _CAMPAIGN_ID_FAILURE_PATTERN) and _effective_url. Vanity→campaign resolution + resume_cursor + the cursor/PARTIAL lifecycle stay — they serve the native ingester. Tests: removed the two obsolete patreon-gallery-dl config tests (yt-dlp Referer, resume-cursor); repointed the generic skip-value config tests to subscribestar; added start_recovery + recover-endpoint coverage (backfill_bypass_seen). gallery-dl stays for the other 5 platforms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
618dafde85 |
feat(subscriptions): smarter-backfill UI — Start/Stop + state badge
Plan #693 (frontend). Backend landed in
|
||
|
|
509a7958cf |
feat(posts): images open the modal; only text expands in place
Post cards no longer expand the whole card on click (the old two-click path
to the images). The card is compact-only now:
- Hero / rail thumbs / the +N tile are buttons that open the post-scoped image
modal (modal.open(id, { postImageIds })) so you view big + arrow through ALL
the post's images. The feed caps thumbnails at 6, so for posts with more we
lazily getPostFull to get the complete id list; +N opens at the first hidden
image.
- The description is the ONLY in-place expansion: a Show more / Show less toggle
shown only when the text is actually truncated (server description_truncated
flag OR a measured CSS-clamp overflow, ResizeObserver-guarded). Expanding
loads description_full when server-truncated and renders it unclamped.
- Attachments: download chips now render inline in the compact card (the feed
already carries download_url), since the expanded view is gone.
Removes PostImageGrid.vue (the mosaic, now unused). Tests cover show-more
visibility + image-click opening the scoped modal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
5a6a95682d |
fix(cleanup): library scans survive navigation, reconnect on return
The transparency / single-color audit cards held the run + poll timer in local component state, so navigating away destroyed both and onMounted never reconnected — the Celery scan kept running and writing LibraryAuditRun, but the UI forgot it. Now each card, on mount, fetches its rule's latest run (GET /api/cleanup/audit?rule=<rule>&limit=1) and rehydrates: shows progress + resumes polling if still running, or shows the completed result (ready/applied/ error) so the operator can act on it after returning. Adds the ?rule= filter to the audit-history endpoint + cleanup store latestAuditForRule(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
91b0145bc8 |
feat(tags): 'Reset content tagging' admin action
Wipe every general + character tag so the operator can re-tag from scratch via
the Camie auto-suggest, while PRESERVING fandoms, series (+ series_page order),
and each image's stored tagger_predictions (so suggestions repopulate
immediately). One set-based DELETE FROM tag WHERE kind IN ('general','character')
— the five tag-referencing tables all cascade, so applications + aliases +
allowlist + rejections + centroids clear automatically; series tags aren't
deleted so series survive; Tag.fandom_id is SET NULL so fandoms are untouched.
Reuses the established dry-run-preview -> confirm pattern: cleanup_service.
reset_content_tagging() + POST /api/admin/tags/reset-content +
TagMaintenanceCard section with a backup-first warning and a red confirm
showing exact counts (tags by kind + image applications). Irreversible except
via DB backup restore; the wipe only fires when the operator confirms.
Tests: service dry-run counts + live delete preserves fandom/series/series_page
while content tags + their image_tag cascade away; API dry-run wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0497394710 |
feat(ui): double showcase cadence + filter bar matches TopNav frost
- Showcase reveal cadence 80ms -> 160ms (slower, more deliberate one-at-a-time cascade per operator). Bump showcase.spec timer advances to cover 60x160ms. - Gallery filter bar now uses the EXACT gradiated-obsidian frost + blur as TopNav (was a flat rgba(...,0.55) block), so the two read as one continuous piece of chrome with images visibly scrolling under both; the nav's transparent bottom edge against the bar's opaque top leaves a faint seam that separates them at the very top of scroll. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
21a73cd1dc |
feat(gallery): visual 'more like this' UI (Phase 3 frontend)
Modal 'Related' strip (RelatedStrip.vue) — top-12 similar thumbs, fetched on its own DEFERRED, single-flighted path (200ms after the modal is up) so it never blocks or slows the modal; collapses silently on empty/slow/error and is hidden when the image has no embedding (has_embedding flag). 'See all similar' closes the modal and navigates the gallery to ?similar_to=<id>. Gallery store: similar_to filter field + loadSimilar() (ranked, hasMore=false, no timeline); applyFilterFromQuery routes similar-mode to /similar with the scope filters composed; cloneFilter/filterToQuery carry similar_to. Filter bar: clearable 'Similar to #id' chip, sort hidden in similar-mode; timeline sidebar hidden too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a4270e6be |
fix(showcase): reveal each tile only once its image is fully decoded
The buffered cascade revealed tiles on an 80ms timer regardless of image load, so the flip-in animation played on a gray placeholder and the thumbnail popped in afterward. Worse, MasonryGrid ALSO applied a per-index animation-delay (index×70ms) that compounded on top of the insert cadence, so the cascade visibly dragged and desynced as it grew. Now the producer preloads each queued thumbnail (decode pipelined ahead) and the consumer awaits that decode before pushing the item — every tile animates in fully loaded, strictly one at a time. Drop the compounding CSS stagger; the store's one-item-at-a-time push is the sole pacer, so each tile animates the instant it mounts. New utils/preloadImage.js (load+decode+timeout gate). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1adc47f59c |
feat(gallery): faceted refine panel UI (Phase 2 frontend)
Add a 'Refine ▾' toggle to the gallery filter bar that expands a full-width GalleryFacetPanel below it, inside the same sticky hazey chrome. The panel offers platform chips (with live counts + a 'No platform' unsourced bucket), two count-badged curation-flag toggles (Untagged / No artist), and a from/to date range bounded by the facet min/max. Store gains the platform/untagged/no_artist/date_from/date_to filter params (URL-mirrored, AND-composed) and a panel-gated, single-flighted loadFacets() that fetches /api/gallery/facets scoped to the active filter. Shared cloneFilter/filterToQuery helpers keep the bar and panel writing one URL format. The panel auto-opens on deep-link when refine filters are present and refetches counts (debounced) on every filter change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef3ee5aceb |
feat(maintenance): DB maintenance UI card + fix ruff I001
- Settings → Maintenance gains a "Database maintenance" card: a "Run VACUUM ANALYZE now" button (enqueues the maintenance task) plus a per-table bloat readout (live/dead/dead%/last vacuum) from /api/admin/maintenance/db-stats. - dbMaintenance store (loadStats / runVacuum) + test. - Fix ruff I001: combine the two _sync_engine imports onto one line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
76d8ad42a8 |
fix(showcase): buffered producer/consumer for a steady cascade
The cascade "burped" — chunks appeared unevenly — because the old pipeline coupled display to fetch timing: it trickled each batch right after its fetch and assumed the next round-trip would land inside the ~240ms trickle window. When a fetch ran long (TABLESAMPLE hits random, sometimes-cold blocks; RTT jitter) the animation starved, then a clump burst in. Decouple the two: - Producer (_fill) races ahead fetching batches into a buffer up to a target depth, refilling when it dips below BUFFER_MIN. - Consumer (_drain) reveals one item every CADENCE_MS regardless of when fetches land; it only waits if the buffer genuinely starves. A small PRIME buffer precedes the drain so it doesn't starve at the front; the buffer (BUFFER_MIN×CADENCE runway) absorbs per-fetch jitter so images appear at an even pace. Public store API (loadInitial/shuffle/fetchPage/ images/loading/hasMore/isEmpty) unchanged — ShowcaseView/MasonryGrid need no change. Test (fake timers): fire-order + dedup, one-item-per-cadence rate limit, empty-library flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
6d630d13d6 |
feat(gallery): pinned filter bar (Phase 1)
Gallery now has in-view filtering, styled like the app's sticky v-tabs
chrome (pinned at top:64px under TopNav).
- GalleryFilterBar: combined tag+artist autocomplete (searches
/api/tags + /api/artists), closable filter chips (multi-tag AND),
media toggle (All/Images/Videos), Newest/Oldest sort, Clear. Writes all
state to the URL via router.push.
- gallery store: filter is now { tag_ids, artist_id, media_type, sort,
post_id }; applyFilterFromQuery makes the URL the single source of truth
(deep-linkable, back-button works); chip labels resolved by id or
pre-noted on pick. Replaces the standalone tag chip + setTag/PostFilter.
- GalleryView: renders the bar (hidden in post-detail), syncs route.query
→ store on mount + every query change.
Also untracks the transient .claude/scheduled_tasks.lock committed in
|
||
|
|
4f9464d215 |
feat(gallery,tags): clear active filters
Two gaps where a filter couldn't be removed:
- Gallery: a tag_id filter (from clicking a tag) had no indicator or clear
control — only post_id did (PostInfoHeader). Add an "Tag: <name> ✕" chip
that clears the filter by dropping tag_id from the URL. New lightweight
GET /api/tags/<id> resolves the name; the store fetches it on filter set.
- Tags view: the kind chip-group used mandatory="false" — a STRING ("false"
is truthy in JS), which made the group mandatory so the active kind chip
couldn't be deselected. Fixed to :mandatory="false" so the filter clears.
Tests: GET /tags/<id> shape + 404; gallery store resolves filterTagName.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
e678d1dfdf |
feat(tags): fandom-edit UI in tags directory + image modal
Adds the missing UI to change a character tag's fandom, in both places:
- FandomSetDialog (shared): pick an existing fandom, create a new one, or
clear it; on a name collision in the target fandom it surfaces a merge
confirmation and resolves via setFandom(merge:true). Reuses the tags
store's fandom cache.
- TagCard kebab gains "Set fandom…" for character tags (→ TagsView opens
the dialog, reloads on success).
- TagPanel chip kebab gains "Set fandom…" for character tags (→ reloads the
modal's tag list on success).
- tags store: setFandom(tagId, fandomId, {merge}) action + test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
56cc253009 |
feat(gallery): reveal tiles on image load + single initial fetch
The 5×10 metadata batching only staggered the cheap layer (JSON); thumbnails load as independent <img> requests and clustered, so tiles "popped in together" after a wait. Two changes: - GalleryItem reveals each tile when ITS OWN thumbnail fires @load (with an onMounted complete-check for cached thumbs), playing a showcase-style flip-up entrance. Tiles now cascade in natural load order instead of all at once. Honors prefers-reduced-motion. - gallery store does ONE initial fetch (limit=50) instead of 10 serial /scroll round-trips. Fewer RTTs, faster first paint; the reveal-on-load is what makes appearance progressive now. Infinite scroll pulls 25/trigger. Tests: GalleryItem gains is-loaded only after @load; loadInitial issues exactly one scroll request at the initial limit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
98673d4dca |
fix(audit-g5a): small architectural cleanups bundle
Five small G5 findings from the 2026-06-02 audit. Each is local and
follows an established FC pattern.
- download_service: replace hardcoded ('discord','pixiv') tuple with
auth_type_for(platform) == 'token'. A 7th token-platform now picks
up the right credential path without touching this site.
- /api/tags/<source_id>/merge enqueues recompute_centroid.delay after
merge so the target's centroid reflects its new image set
immediately. Daily list_drifted catches it within 24h, but eager
recompute closes the suggestion-quality dip in the meantime.
- backfill_thumbnails added to beat_schedule (daily). The task
docstring claimed periodic Beat but the entry was never registered,
so the library got no self-healing thumbnail repair; only the
manual admin-UI button fired it.
- modal.createAndAdd pushes a kind='fandom' tag into
tagsStore.fandomCache so FandomPicker sees the new fandom on next
open. Was: cache-gated load (length===0) skipped refetch, new
fandom invisible until full page reload.
- cleanup cluster:
- Drop .webp from cleanup_service.unlink — thumbnailer only writes
.jpg/.png; the third tuple member was dead code.
- Drop effective_date from /api/gallery/scroll response — no FE
consumer reads it. Service still computes the attribute for
timeline ordering; this just trims the JSON.
- Rename store.recentMinute → store.recentRuns across the
systemActivity store + three consumers (SystemActivitySummary,
QueuesTable, SystemActivityTab). The data is the last 200 runs
(not actually "last minute"), so the name lied.
NOT in this bundle: the duplicate tag-merge endpoint
(/api/tags vs /api/admin/tags) is harder — has 1 FE caller and 3 tests
on the admin variant; consolidation is its own change.
|
||
|
|
e66987f092 |
fix(audit-g2): async race / state-leak across eight stores
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. |
||
|
|
3898ce7be4 |
fix(audit-g1): six one-liner drift fixes from 2026-06-02 audit
- BACKFILL_TIMEOUT_SECONDS 1800→1170: keep the subprocess timeout 30s below Celery's hard time_limit=1200 so SIGKILL doesn't beat TimeoutExpired (matched the tick 870s/900s rationale). Backfill runs that hit the cap let the next tick continue via the archive. - recover_interrupted_tasks orphan UPDATE now stamps finished_at; without it cleanup_old_tasks' WHERE finished_at<cutoff never reaped orphan-swept rows. recover_stalled_task_runs also now sets duration_ms (matches celery_signals.finalize's millisecond math). - ExtensionService.quick_add_source arms NEW_SOURCE_BACKFILL_RUNS=3 on Source creation, mirroring SourceService.create. Without it, Firefox quick-add on a creator with >20 unsynced posts walked the full feed until subprocess timeout. Renamed the constant from _NEW_SOURCE_BACKFILL_RUNS so it can be imported cross-module. - gallery_dl.verify() accepts TIER_LIMITED as auth-success alongside NO_NEW_CONTENT — the download path (line 712) already does, and TIER_LIMITED proves auth reached the post and was told it was tier-gated. Verify endpoint previously showed red on this and prompted operators to rotate working cookies. - prune_unused_tags now runs a single DELETE with the NOT-IN predicate find_unused_tags uses, instead of SELECT-ids → DELETE-WHERE-IN. Removes the psycopg 65535-param cliff that would have surfaced on a tag explosion (>65k unused tags). - credentials.upload() reflects the returned record into the store cache (`.set(platform, rec)`) instead of evicting it; previously the card briefly rendered "no credential" between upload and loadAll(). |
||
|
|
412edec028 |
fix(showcase): don't exhaust on all-dupe batches; retry up to 8x
/api/showcase returns a random sample; after enough scrolling, an unlucky 3-item batch can be entirely in the `seen` Set, which was flipping `exhausted=true` and surfacing "End." mid-scroll. The showcase is endless by intent — only a genuinely empty API response (library has 0 images) should mark it exhausted. Tiny-library fallback: cap retries at 8 to avoid spinning forever. |
||
|
|
9cbdb70e13 |
fix(thumbnails): surface backfill results + tighten validity check
Two coupled problems, operator-flagged 2026-06-01: "missing thumbnails
but triggering backfill found nothing."
1. **Backfill UI was a black box.** `POST /api/thumbnails/backfill`
returned just `{celery_task_id}` and the admin card said
"Enqueued." with no counts. There was no way to tell whether the
scan found 0 candidates, 5000 candidates, or whether the worker
even picked up the task. "Found nothing" was indistinguishable
from a broken queue.
Fix: refactor the scan into a sync helper (`_run_backfill_scan`)
shared by the Celery task and the API endpoint. The API now runs
the scan in an executor and returns `{scanned, enqueued, ok,
regenerated}`. The actual thumbnail generation work still goes
to the thumbnail Celery queue per row via
`generate_thumbnail.delay()` — the scan itself is fast
(SELECT id+thumbnail_path + a file.stat() per row).
2. **`_thumb_is_valid` accepted header-only corrupt files.** The
magic-byte check passed for any 8-byte file starting with a JPEG
or PNG header, including empty/truncated/zero-pad files that
browsers render as broken. Backfill counted these as `ok` and
never regenerated.
Fix: also require file size ≥ MIN_THUMB_BYTES (256). Real
thumbnails are minimum ~2KB even on solid-color sources; header-
only corrupt files top out around 12 bytes. 256 is well above
the corrupt floor and well below any legitimate thumbnail.
Plus the admin card now shows the per-run counts instead of
"Enqueued.":
Scanned 5,432 · enqueued 3 (2 regenerated) · 5,429 ok
|
||
|
|
19aece1fc4 |
feat(download): tick/backfill modes + partial-success classifier (plan #544)
Routine subscription polls walked the entire post history every tick
even when nothing had changed, because gallery-dl's default `skip: True`
continues iterating archived posts. A creator with ~550 archived posts
(Knuxy patreon) saturates the 870s wall-clock cap before completing,
even with zero downloads needed. Plus, a tier-limited run that
downloaded hundreds of files but ran out the clock should be a
warning, not an error.
Two coupled changes, both operator-flagged 2026-06-01:
* **Tick mode (default, cron polls).** New `TICK_SKIP_VALUE = "exit:20"`
asks gallery-dl to exit after 20 contiguous archived items. Fresh
subscriptions + new-content cases still walk normally; established
subscription with zero new content exits in ~30s of HEAD requests
instead of pegging the timeout. 20 (not 5) gives headroom against
paywall warnings interleaving with archived items.
* **Backfill mode (explicit, operator-triggered).** Sticky for N runs
via new `Source.backfill_runs_remaining` (alembic 0031). While > 0,
downloads use `skip: True` + 1800s timeout. Auto-decrements per run
with early-reset to 0 when a clean run finds zero files (queue
drained). N defaults to 3 — multiple runs give the system enough
budget to finish a deep walk across timeout boundaries. New
`POST /api/sources/{id}/backfill` arms the source; "Deep scan"
button on each SourceRow (chip shows remaining count) wires it.
Plus partial-success classifier: non-zero gallery-dl exit + ≥1 file
downloaded + no source-level error fires `ErrorType.PARTIAL`, which
download_service maps to `status=\"ok\"`. The run did real work; the
next tick continues via gallery-dl's archive. No more red events for
"timed out mid-walk after downloading 300 files."
Retires `SourceConfig.skip_existing` — skip value is now derived from
the source state and passed as a separate `skip_value` parameter
through download() / _build_config_for_source(). `GD_DEFAULTS` drops
the now-dead key (was inert data after this refactor).
Tests cover:
* tick + backfill skip-value emission in _build_config_for_source
* PARTIAL classifier branch + TIER_LIMITED-wins-over-PARTIAL ordering
* SourceService.set_backfill_runs validation + persistence
* /api/sources/{id}/backfill 200/400/404 paths
* download_service auto-decrement / auto-reset / tick-mode-no-touch
* PARTIAL → status=ok in the orchestrator (no consecutive_failures bump)
|
||
|
|
94e7d20792 |
feat(downloads): Logs button on failing-sources rollup rows
Operator-asked 2026-06-01: each row in the "X sources are failing" rollup needs a button to surface the last run's stdout/stderr/error without leaving the Downloads tab to find the matching event row. Wired: - `downloadsStore.loadLastForSource(sourceId)` two-steps via `GET /api/downloads?source_id=N&limit=1` (most recent event) then the existing `loadOne(id)` for the full detail payload. Returns null if no events exist (toast warns). - `FailingSourcesCard` row: new text button `Logs` with `mdi-text-box-search-outline`, per-row spinner via `logLoadingIds`, emits `view-logs` with the source record. - `DownloadsTab.onViewFailingLogs` is the handler — same `DownloadDetailModal` instance the event-row clicks use. |
||
|
|
fb605af959 |
fix(posts): anchored view scroll inherits artist_id/platform filters
Operator-flagged 2026-06-01: clicking a Provenance post title from the
view modal opens the post in the posts feed scoped to that artist
(`/posts?post_id=N&artist_id=A`), but the older/newer infinite-scroll
loaded UNFILTERED global posts instead of staying in the artist's
stream. Root cause: the posts store's loadAround/loadOlder/loadNewer
sent only `{around, cursor, direction}` — never the `artist_id` /
`platform` filters. Backend `/api/posts` accepts them on every path
(post_feed_service.scroll filters via `Source.artist_id`); the
frontend just wasn't passing them.
Fix:
- `posts.js` `loadAround(postId, filters)` now takes the filter
snapshot and stores it. `_aroundParams(extra)` mixes the active
filters into around/cursor calls.
- `PostsView.vue` `loadAroundAndAnchor` passes `artist_id` +
`platform` from the route query.
|
||
|
|
af7b5c95e9 |
feat(modal): autofocus tag input, expand general suggestions, retire copyright/artist categories
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. |
||
|
|
a5101494b6 |
feat(downloads): bulk retry respects cooldown; single-source RETRY overrides
Today's platform-cooldown commit (
|
||
|
|
e3a7aff7a3 |
ux(showcase): pipeline fetches in-order, chunk size 3, keep the trickle
Operator-flagged 2026-05-30 (round 3): the all-parallel fetch was fast but could let later chunks arrive ahead of earlier ones — even when each chunk is a random sample, that "later chunk loads first" risk made the load order non-deterministic. And the original goal behind asking for batching was a faster first-image-on-screen, which neither sequen- tial nor parallel really addressed cleanly. Switched the loadInitial flow to a PIPELINE: - Only one fetch in flight at any moment (in-order arrival, no race). - The NEXT fetch kicks off as soon as the current one resolves (NOT after its trickle finishes), so the next RTT overlaps the visible trickle window — round-trips are hidden behind the animation cadence. - PAGE 5 → 3 + INITIAL_BATCHES 12 → 20 (total still 60). Smaller chunk → first chunk's items appear sooner (a chunk of 3 trickles in 240ms, well within one RTT, so by the time chunk 2 is in-hand the first trickle is just finishing). Trickle, sequence-token guard, and infinite-scroll behaviour unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9cd6d09e60 |
ux(showcase): smooth one-at-a-time cadence + earlier infinite-scroll trigger
Operator-flagged 2026-05-30 (round 2): the batched-loads change improved consistency but still felt "chunky" — 5 items appeared together, then a ~200 ms API round-trip pause, then another 5. And infinite-scroll fired too late when a single tall image (long manga page) made one masonry column much taller than its siblings. **Cadence (showcase store):** - Fire all INITIAL_BATCHES fetches in PARALLEL — collapses the per- batch round-trip gap so all the data arrives in ~1 RTT instead of 12 sequential RTTs. - Trickle each response's items into images.value one at a time with APPEND_DELAY_MS = 80ms between each (≈ the MasonryGrid stagger animation, 70ms). User sees a smooth steady stream. - fetchPage (the infinite-scroll path) uses the same trickle so its 5-item appends also cascade one-by-one instead of popping together. - Sequence token guards against a fast shuffle / mount-then-shuffle interleaving two trickles into the same images.value. - Dropped useAsyncAction here — the parallel-fetch-then-trickle flow doesn't fit its single-wrap-call shape cleanly; inline loading/error state is clearer. **Infinite-scroll trigger (MasonryGrid):** - Pass `rootMargin: '2400px'` to useInfiniteScroll (was the 600px default). The masonry sentinel sits at the bottom of the container, whose height = MAX(column heights). A tall image in one column pushes the sentinel ~2× viewport below where the user is actually reading (the bottom of the SHORTER columns). 2400px ≈ 2-3 screen-heights of pre-emptive trigger, comfortable for typical tall manga heights. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1eefed9ab3 |
fix(ui): finish showcase store batching (PAGE 60 → 5, INITIAL_BATCHES 12)
The showcase-store change in
|
||
|
|
adeee64a2d |
feat(ui): batch initial showcase/gallery loads into smaller chunks
Showcase and gallery were fetching the full initial batch (60 / 50 items) in a single API call. Operator-flagged 2026-05-30: items should stream in as small batches so they render progressively rather than blocking on one big response. - showcase store: PAGE 60 → 5, INITIAL_BATCHES = 12 (5 × 12 = 60, same total). loadInitial() fetches PAGE chunks in sequence; shuffle() delegates. fetchPage() still returns one PAGE-sized chunk so infinite-scroll also pulls 5 per trigger. - gallery store: loadMore() limit 50 → PAGE (5), INITIAL_BATCHES = 10. loadInitial() loops loadMore() up to INITIAL_BATCHES times, stopping early when nextCursor becomes null (end of data). - ShowcaseView: onMounted calls loadInitial() instead of fetchPage(). Gallery/setTagFilter/setPostFilter already routed through loadInitial. Net visual effect: each ~5-item batch lands and renders independently; combined with MasonryGrid's animateFromIndex logic, the showcase now cascades batches in as they arrive instead of one big pop-in. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
77e9859da3 |
fix(downloads): rewire MaintenanceMenu to the downloads pipeline
The maintenance dropdown in Subscriptions → Downloads was wired to the
filesystem-import pipeline (POST /api/import/retry-failed +
POST /api/import/clear-stuck) — the subtitles even said so ("Re-enqueue
every failed import task"), but it was contextually misplaced. From the
Downloads view "Retry failed" queued nothing the operator could see
because the action operated on import_task rows, not download_event
rows. Import-pipeline maintenance is already reachable from Settings →
Imports (ImportTaskList.vue), so removing the import wiring loses
nothing.
Rewired:
- "Retry failed" → bulk-retries the failing-sources list, same loop as
FailingSourcesCard's RETRY ALL (sourcesStore.checkNow per source).
Subtitle now matches: "Re-queue every currently failing source".
- "Force recovery sweep" → triggers recover_stalled_download_events on
demand via a new POST /api/downloads/recover-stalled endpoint. The
sweep also runs every 5 min on Beat; this is the manual fallback so
the operator doesn't have to wait for the next tick to clear newly
stranded events.
MaintenanceMenu is now stateless — emits retry-failed and recover-
stalled. DownloadsTab owns the handlers (reuses the existing
onRetryAll; new onRecoverStalled with a delayed refresh so swept rows
land in the failing rollup).
Operator-flagged 2026-05-29 — "the retry failed button in the
maintenance dropdown doesn't appear to queue anything but manual
requeues works."
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8649a13118 |
refactor(I5): remove one-and-done GS/IR migration tooling
The GS/IR migration cutover is complete, so the runbook tooling is dead weight. Removed: - services/migrators/ (gs_ingest, ir_ingest, tag_apply, ml_queue, verify, cleanup), tasks/migration.py, api/migrate.py (+ blueprint registration) - MigrationRun model; alembic 0027 drops the migration_run table - frontend LegacyMigrationCard + migration store (+ MaintenancePanel ref) - celery include + task route + celery_signals queue mapping for migration.* - the 1 GB MAX_CONTENT_LENGTH / MAX_FORM_MEMORY override (added solely for the ir_ingest upload) - migration-surface tests (test_api_migrate, test_migration_verify, test_ir_ingest, test_gs_ingest, test_tag_apply) Kept: the alembic schema-migration tests (test_migration_00XX — unrelated) and cleanup_service.py (the permanent artist-cascade/unlink home). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
00e2608ba1 |
feat(I3): always-on pipeline status indicator in the top nav
New /api/system/activity/summary aggregates scheduler health + per-queue pending depths + running count + 24h failure count in one cached call (safe to poll app-wide). PipelineStatusChip lives in the TopNav on every page: a compact running/queued/failing chip with a scheduler-health dot that expands to a popover (scheduler, busy queues, counts, link to downloads). Polls the summary every 8s, paused when backgrounded. Reuses the queue-cache read via _queues_cached(). + API test for the summary shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c95b760294 |
feat(posts): in-context anchored feed with bidirectional infinite scroll
Provenance "View post" deep-links to /posts?post_id=X, which now opens the feed centered on that post with infinite load in BOTH directions. Backend: PostFeedService.scroll gains a direction (older|newer); new around(post_id) returns a window of newer + the post + older with a cursor for each end. /api/posts accepts ?around= and ?direction=. + API tests. Frontend: posts store gains loadAround/loadOlder/loadNewer (older appends, newer prepends) with per-end cursors; PostsView's anchored mode scrolls to the post, observes top + bottom sentinels, and preserves scroll position on upward prepend so the page doesn't jump. Normal feed mode unchanged. Closes the remaining half of the post-navigation work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9e74c80e2f |
feat(downloads): front-and-center "active now" panel with live elapsed timers
New ActiveDownloadsPanel pinned at the top of the Downloads tab shows what's running (pulsing dot + live mm:ss timer counting from started_at) and what's queued, so the operator can see activity at a glance without the running filter. Backed by store.loadActive() which fetches running + pending independent of the feed filter; refreshed every 4s by the existing live poll. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a37dad33c7 |
refactor(dry-F1): shared useAsyncAction lifecycle helper for stores
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> |
||
|
|
eebc8e2413 |
refactor(dry-F2): centralize shared UI primitives (relative-time, toast, download-status)
- utils/date.js: add formatRelative(iso, {future,nullText}); migrate 6 sites
(SourceRow, SubscriptionsTab, SourceHealthDot, SchedulerStatusBar +
thin adapters in BackupRunsTable/SystemActivityTab for their '—' null text).
PostCard (30d->absolute) and CredentialCard (mo/y buckets) intentionally
keep bespoke formatters.
- utils/toast.js: toast(opts) wraps the globalThis.window?.__fcToast?.(...)
incantation; migrate 63 call sites across 24 files.
- utils/downloadStatus.js: single source for the download-event status enum
-> label/color/icon; collapse the 3 duplicate maps (DownloadStatChips,
DownloadsFilterPopover, DownloadEventRow).
Net -33 lines. Platform metadata was already centralized in platformColor.js.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2358cedf3e |
feat(dashboards): scheduler health strip, failing-source rollup, 24h activity sparkline, credential staleness nudge
D1 scheduler visibility: AppSetting last-tick stamp on every Beat tick + GET /api/sources/schedule-status (last_tick_at/next_due_at/due_now/auto_sources) + SchedulerStatusBar on the Subscriptions tab (re-polled every 30s). D2 failing-source rollup: ?failing=true on the sources list + FailingSourcesCard on Downloads with per-source and bulk "retry" (re-runs the feed via /check). D3 activity sparkline: GET /api/downloads/activity hourly buckets + CSS bar chart by the stat chips (failures stacked in error color); refreshes on live poll. D4 credential staleness: surface last_verified age + "re-verify recommended" warning past 30d; also fixes the dead last_verified_at field-name mismatch so the verification row renders at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |