Commit Graph

95 Commits

Author SHA1 Message Date
bvandeusen 56970fb66d feat(credentials+downloads): real credential Verify button + live download-activity polling
Operator-flagged 2026-05-28, two asks.

**1. Credential Verify (was missing vs GS — and now actually verifies).**
GS's Verify was a stub (`TODO: implement actual verification` — just
stamped last_verified). FC does a real check, which matters given the
recent auth pain (subscribestar age cookie, HF host-only PHPSESSID):

- GalleryDLService.verify(url, platform, cookies_path, auth_token) runs
  gallery-dl in `--simulate --range 1-1` mode (no download) against the
  URL with the materialized credentials, then reuses _categorize_error:
  returncode 0 / NO_NEW_CONTENT → valid; AUTH_ERROR → invalid; other →
  inconclusive (reason surfaced). 45s timeout.
- POST /api/credentials/<platform>/verify picks an enabled Source for
  the platform to probe, runs verify, and on success stamps
  credential.last_verified (new CredentialService.mark_verified).
  Returns {valid: bool|null, reason, last_verified?}. valid=null means
  untestable (no credential, or no enabled source to point at).
- CredentialCard gains a Verify button (on credentialed cards) + a
  result chip (Verified ✓ / Failed / Untestable) and a toast with the
  reason. SettingsTab reloads on @verified so last_verified refreshes.

**2. Live download-activity feedback.** The Downloads tab was static —
no way to tell if downloads were succeeding without manually hitting
Refresh. It now auto-polls: stats every 4s, and the event list too
while anything is queued/running. Polling pauses when the tab is
backgrounded (document.hidden) and the list reload is skipped on idle
ticks to stay light. A pulsing "● live" indicator next to the stat
chips shows when auto-refresh is active (queued+running > 0); honors
prefers-reduced-motion.

Tests: verify endpoint — untestable with no credential, untestable with
no enabled source, valid+stamped on success (gallery-dl mocked), and
auth-failure reported without stamping.
2026-05-28 07:52:20 -04:00
bvandeusen bf8eb4468f feat(tags): legacy-tag purge also catches source:* general tags
Operator-confirmed 2026-05-28: the BlenderKnight:* tags are `archive`
kind (caught by the kind purge), but the `source:patreon`-style tags
are IR's old `source` kind that fell back to `general` during migration
(FC's enum has no `source` kind) — so they can't be matched by kind.

Broadened the purge to a two-rule match and renamed it for accuracy
(all dev-only, unreleased):
- cleanup_service.purge_tags_by_kind → purge_legacy_tags. Predicate is
  now `kind IN (archive, post, artist) OR name LIKE 'source:%'`
  (LEGACY_NAME_PREFIXES). Preview classifies each row into by_kind OR
  by_prefix (source:* counts once under the prefix bucket regardless of
  its general kind).
- endpoint /tags/purge-retired-kinds → /tags/purge-legacy. dry-run
  returns by_kind + by_prefix + count + sample.
- store purgeRetiredKindTags → purgeLegacyTags.
- Tag Maintenance card copy + breakdown updated to show both buckets;
  button reads "Preview/Delete legacy tags".

Tests updated + extended: dry-run reports by_kind {archive,post,artist}
AND by_prefix {source:*}, plain general/character tags survive; commit
deletes both the kind-matched and source:*-matched rows and leaves the
rest.
2026-05-28 01:20:41 -04:00
bvandeusen e1fc65bd1b feat(provenance+tags): collapse provenance descriptions; purge retired-kind tags
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.
2026-05-28 01:12:20 -04:00
bvandeusen dcfe55d731 feat(import-resilience L2): one-shot re-download for corrupt downloaded files
Layer 2 — remediate a corrupt file by re-fetching a fresh copy from its
source, bounded to a single attempt. Operator-requested 2026-05-28.

New backend/app/services/refetch_service.py:
- resolve_refetch_source: parse the failed file's sidecar → platform,
  derive the artist from the import path, find an ENABLED Source with a
  real feed URL for (artist, platform). Returns None for filesystem-only
  imports, missing sidecars, or `sidecar:<platform>:<slug>` synthetic
  anchors (not pollable).
- attempt_refetch: if not already refetched AND a Source resolves,
  delete the corrupt file (so gallery-dl's skip_existing re-fetches it),
  set ImportTask.refetched=True, and trigger ONE download_source
  re-check. Bounded by `refetched` so source-side corruption can't loop.

Wiring:
- Manual endpoint POST /api/import/tasks/<id>/refetch (only on 'failed'
  tasks). Returns refetch_queued / no_source / already_refetched /
  not_found / not_failed.
- Auto path in recover_interrupted_tasks: for each poison-pill row, if
  env FC_AUTO_REFETCH_CORRUPT=1, attempt_refetch (default OFF — the
  manual button is the primary path; auto is opt-in since re-fetch
  deletes a file + re-runs the downloader).
- Frontend: a cloud-refresh icon button on failed rows in ImportTaskList
  → stores.import.refetchTask → toast keyed on the result status.

Filesystem imports with no upstream return no_source — the operator's
only remediation there is replacing the file on disk, surfaced clearly
in the toast.

Tests: 404 unknown task, 400 non-failed task, no_source when
unresolvable, and the full resolvable-source path (file deleted,
refetched flag set, one download_source dispatched, second call is a
no-op). The resolvable test repoints the migration-seeded
import_settings(id=1) scan path rather than inserting a conflicting row.
2026-05-28 00:08:03 -04:00
bvandeusen 4d2c464045 feat(post-card): absorb PostModal into PostCard with click-to-expand
PostCard and PostModal competed for the same data and rendered redundant
chrome (header twice, image grid twice, attachment list twice). The wider
PostCard layout we shipped 2026-05-27 has enough real estate to be the
canonical post surface, so collapse the two into one.

Compact (default) state is unchanged: hero + 3-cell rail + truncated
title + 3/5-line description + attachment count badge. Whole-card click
expands in place. Expanded state shows: full title, mosaic of ALL post
images via PostImageGrid (uncapped, lazy-loaded via getPostFull), full
sanitized-HTML description with paragraph wrapping, attachments as
downloadable pill links. Click the chevron in the header to collapse;
mosaic image clicks open ImageViewer scoped to the post (modalStore's
postImageIds path is preserved — only the comment changed).

Per-card local state — no global modal store. Each PostCard owns its
own expanded ref and lazy-loaded detail; collapsing a card discards
neither (so re-expand is instant after the first fetch).

Deleted: PostModal.vue, postModal.js store. Removed the App.vue mount.
2026-05-27 14:30:04 -04:00
bvandeusen 9322c984fd feat(subs-hub): collapse /credentials + /downloads into /subscriptions hub with three GS-style subtabs
Replaces the three top-level routes with a single `/subscriptions` parent
owning the whole download-pipeline domain. Internal tab state via `?tab=`
query param, mirroring ArtistView's pattern. TopNav auto-drops the two
removed entries (route-driven via meta.title). Bookmark-safe redirects
from `/credentials` and `/downloads` route into the appropriate subtab.

**Subtab 1 — Subscriptions (default).** Carries over the existing
artist-grouped expandable table; adds (a) status filter dropdown, (b)
bulk-select column with Enable/Disable/Delete-all actions, (c) GS-style
color-coded `PlatformChip` per distinct platform in the collapsed row.
Reuses SourceRow, SourceHealthDot, SourceFormDialog, ArtistCreateDialog.

**Subtab 2 — Downloads.** Full GS dashboard. Five colored stat chips up
top (Queued/Running/Completed/Failed/Skipped, sourced from new
`GET /api/downloads/stats?window_hours=`). Popover-style filter UI
(Status/Source/FromDate/ToDate) with active-filter pills below.
Maintenance menu wraps existing /api/import/retry-failed and
/api/import/clear-stuck endpoints; Export-failed-logs item disabled with
a "v2" tooltip. Per-row Retry preserved via existing DownloadEventRow.

**Subtab 3 — Settings.** Four sections: ExtensionKeyBar (top), GS-style
per-platform CredentialCard grid (md=6 v-row/v-col, dashed border if
unset / accent border if set, expandable how-to panel), Downloader card
(rate limit, validate_files), Schedule defaults card (default interval,
event retention, failure warning threshold). The Downloader and Schedule
sections were extracted out of components/settings/ImportFiltersForm.vue
— SettingsView's Import tab now owns only image-import filters.

**Backend:** new `GET /api/downloads/stats` returns
{pending, running, ok, error, skipped} count grouped by status over the
configurable window. Status keys stay raw from the ENUM; UI does the
display-label mapping. Two integration tests pin the response shape +
window_hours validation.

**Util:** `frontend/src/utils/platformColor.js` — single source of truth
for the six platforms' color + icon + label, mirroring GS's palette
(patreon=red mdi-patreon, subscribestar=amber mdi-star,
hentaifoundry=purple mdi-palette, discord=indigo mdi-discord,
pixiv=blue mdi-alpha-p-box, deviantart=green mdi-deviantart). Unknown
platform falls back to grey + mdi-web.

Deferred (explicit non-goals): subscription import/export, "Trigger Due
Now" scheduler-tick button (needs new backend endpoint), Export Failed
Logs CSV dump.
2026-05-27 13:02:24 -04:00
bvandeusen 3838f04c16 feat(tag-kinds): drop meta + rating entirely — alembic 0023 deletes existing meta/rating tags (CASCADE clears related image_tag / alias / allowlist / suggestion_rejection / reference_embedding / series_page rows) then recreates the tag_kind ENUM without those values. Python TagKind enum trimmed; KIND_OPTIONS + KIND_COLOR + KIND_ICONS maps + TagsView KINDS array all updated. Operator confirmed they have no use for the data.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:07:31 -04:00
bvandeusen 90c176b195 feat(postmodal-store): Pinia store driving the app-level PostModal — open(post) fetches full detail via posts store; close() clears
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:48:49 -04:00
bvandeusen b8d89b9f2a feat(modal-store): post-scoped cycle — open(id, { postImageIds }) pins prev/next to the array; canPrev/canNext + goPrev/goNext check the array index instead of current.value.neighbors when set. Gallery-context callers unchanged (default args clear scope)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:48:30 -04:00
bvandeusen 7d84990f6d feat(artist-view): ArtistView rewrite — sticky frosted ArtistHeader (name + stats + tabs) replaces the in-body h1; three lazy tabs (Posts default, Gallery fallback, Management); ?tab= URL state; cross-artist store reset; document.title set on slug change
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:01:14 -04:00
bvandeusen ecd0199799 feat(thumb-backfill): Pinia store — triggerBackfill() POSTs /api/thumbnails/backfill
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:36:45 -04:00
bvandeusen 2505b197ae feat(fc-cleanup): Pinia store + 3 cards + CleanupView + SettingsView tab + TagMaintenanceCard moved from Maintenance + ruff lint fixes — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 2026-05-26 08:16:46 -04:00
bvandeusen e6ededbe8e feat(deep-scan): IR-parity port — refreshed status + counter, re-queue completed paths in deep mode, honest UX — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 2026-05-25 22:16:26 -04:00
bvandeusen f657582f30 feat(import-ui): deep scan button, sticky settings tabs, tasks-above-filters, fix Scanning-undefined source_path — Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> 2026-05-25 17:37:10 -04:00
bvandeusen 5666fd5ca5 fix(ui): scan trigger immediate-feedback toast + delayed status re-poll — operator-flagged 'click does nothing' was actually scan_directory's skip-set finalizing the batch in <100ms when every file already had an ImportTask row, before refreshStatus could ever see the active state. Now the click always produces visible feedback (immediate 'Scan triggered' + 2s 'no new files' if it quick-finalizes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:06:11 -04:00
bvandeusen b6a917ac81 feat(import): /api/import/clear-stuck endpoint + Clear stuck UI button — escape hatch for the autoretry-loop case the automatic sweep can't break
Operator hit 3 large PNGs stuck in 'processing' for 2 days 2026-05-25:
the existing recover_interrupted_tasks flips processing > 5min back to
queued + .delay(), but if the underlying file is unfixably broken (e.g.,
PIL OSError, also patched in 68cffce), the loop never terminates and the
'Scanning...' banner sticks at 0/0 forever blocking new scans.

/api/import/clear-stuck:
- Flips every task in pending/queued/processing to 'failed' with a clear
  marker error message
- Finalizes any 'running' ImportBatch that has no remaining active children
- Idempotent + non-destructive: rows survive, can be retried once the
  underlying cause is resolved

UI button 'Clear stuck...' sits next to 'Retry failed' / 'Clear completed'
with a warning-tonal alert in the confirm dialog explaining what it does
and recommending Retry failed once the cause is fixed.

Tests: clears mixed non-terminal states, untouches complete rows,
finalizes orphan batch, no-op when nothing stuck.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:37:07 -04:00
bvandeusen e41ab1cca5 fc3k(ui): Pinia admin store — six endpoints + task_run polling for Tier-C dispatched ops
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:47:09 -04:00
bvandeusen 102c21feaa fc3h(ui): Pinia backup store — runs, triggers, restore, delete, tag, settings
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 23:04:37 -04:00
bvandeusen 89d0cb2124 fc3i(ui): systemActivity Pinia store — queues, workers, runs, failures
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:07:28 -04:00
bvandeusen b600cf6e86 fc5: migration Pinia store — trigger + FormData upload + 2s poll + history list
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:03:38 -04:00
bvandeusen f2f74efff1 fc3f: artistDirectory Pinia store — cursor scroll + q + platform filters
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:14:53 -04:00
bvandeusen 581f4aea79 fc3e: posts Pinia store — cursor scroll + filters + detail fetch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:55:29 -04:00
bvandeusen 397c21c88e feat(fc3c): downloads Pinia store + sources.checkNow action + vitests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:45:45 -04:00
bvandeusen 408971a798 fix(fc3b): SourceFormDialog + SubscriptionsView read platforms store; drop sources.loadPlatforms
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:39:44 -04:00
bvandeusen e83747b085 feat(fc3b): platforms + credentials Pinia stores + vitests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 18:37:45 -04:00
bvandeusen ded5e5f642 feat(fc3a): sources Pinia store + vitest
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:56:22 -04:00
bvandeusen cba662a900 feat(attachments): downloadable attachments in ProvenancePanel + PostInfoHeader
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 11:17:14 -04:00
bvandeusen b33253429a feat(provenance): gallery store post_id filter (mutually exclusive with tag_id)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:16:09 -04:00
bvandeusen 7bfd700671 feat(provenance): provenance store (cached image/post lookups)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:15:42 -04:00
bvandeusen 4fb176faf6 feat(reader): seriesReader store + pure scroll/progress/clamp helpers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:29:57 -04:00
bvandeusen b19aff198e feat(series): seriesManage Pinia store + pure moveItem reorder helper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:36:44 -04:00
bvandeusen 35e26d6226 fix(bulk): node-safe toast (globalThis.window) + common-tags test matches non-empty spec guard
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:15:23 -04:00
bvandeusen 349d8e50ef feat(bulk): gallerySelection Pinia store with order, mode, bulk actions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 18:03:54 -04:00
bvandeusen 1d1574e586 feat(frontend): tagDirectory rename and merge store actions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:41:20 -04:00
bvandeusen 861e8baeb8 feat(fc2c-i): pinia stores for showcase, tag directory, artist 2026-05-15 21:12:19 -04:00
bvandeusen 6cf82970a1 feat(fc2b): ml / allowlist / aliases Pinia stores
Thin wrappers over the FC-2b API: ml (settings GET/PATCH, backfill +
recompute triggers), allowlist (list, threshold patch, remove with
optimistic local update), aliases (list, remove with optimistic update).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:56:45 -04:00
bvandeusen 2a21ede912 feat(fc2b): wire SuggestionsPanel + rename into TagPanel; reject-on-remove
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>
2026-05-15 07:56:22 -04:00
bvandeusen a8cc6a27dc feat(fc2b): suggestions store + modal panel components
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>
2026-05-15 07:55:10 -04:00
bvandeusen 90686e6deb chore(fc2a): polish pass — toasts, loading skeletons, focus guards, mobile breakpoints
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>
2026-05-14 12:36:25 -04:00
bvandeusen 89fee260a6 feat(fc2a): add tag store, TagAutocomplete, and FandomPicker
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>
2026-05-14 12:34:17 -04:00
bvandeusen 8358d09348 feat(fc2a): real modal store + usePanZoom composable
modal store loads image detail (with neighbors) on open, exposes goPrev/
goNext that re-fetch the next image, and implements optimistic tag
remove with rollback on API failure. createAndAdd is the bridge between
the autocomplete UI and a brand-new tag.

usePanZoom is a small composable: click-to-toggle 2x zoom centered on
click point, wheel-to-zoom up to 4x, drag-to-pan when zoomed. Pointer
events are used (touch + mouse + pen unified). Caller spreads handlers
onto the element it wants pannable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:33:14 -04:00
bvandeusen 899b193085 feat(fc2a): add TimelineSidebar and assemble GalleryView
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>
2026-05-14 12:32:50 -04:00
bvandeusen da89fe6be2 feat(fc2a): add gallery Pinia store, GalleryItem, and EmptyState
Gallery store handles cursor pagination, date-group merging across pages,
tag filter, timeline buckets, and jump-to-month. Stale response detection
via an inflightId counter prevents out-of-order page additions when the
user toggles filters quickly.

GalleryItem is a square thumbnail card with hover/focus accent outline,
keyboard activation (Enter/Space → open), video badge for video MIME types,
and a broken-image fallback if the thumbnail URL fails to load.

EmptyState points operators to /settings?tab=import so the empty path is
self-guiding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:31:53 -04:00
bvandeusen af9f42ef68 feat(fc2a): add useApi composable, import store, extend system store with stats
useApi centralizes fetch error handling — every API call throws ApiError
on non-2xx with the parsed body attached, so components don't repeat the
'check response.ok' boilerplate.

import store wraps import settings, batch status, task list (with cursor
pagination), and the trigger/retry/clear admin actions. system store
gains a refreshStats() that powers the Settings overview tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:29:49 -04:00
bvandeusen bcd9a23501 feat: add SPA app shell, router, system store, and route placeholders
Nav lists every FC-2 and FC-3 surface with a placeholder view that names
the sub-project that will fill it. System store polls /api/health on mount
and renders a status pip in the nav footer.

Favicon uses Curator's aged amber accent (#A87338).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 07:36:01 -04:00