diff --git a/frontend/src/components/modal/ImageViewer.vue b/frontend/src/components/modal/ImageViewer.vue index 0ea01bc..84cb495 100644 --- a/frontend/src/components/modal/ImageViewer.vue +++ b/frontend/src/components/modal/ImageViewer.vue @@ -92,7 +92,16 @@ let prevBodyOverflow = null // own keystrokes. function onKeyDown(ev) { if (ev.key === 'Escape') { - if (isTextEntry(ev.target)) return + // Escape closes the modal even from inside a text input — that's + // the universal "get me out of here" expectation, and the + // autofocused tag-entry field would otherwise trap focus with no + // visible escape (operator-flagged 2026-06-01). EXCEPTION: when a + // nested Vuetify overlay is open (v-menu autocomplete dropdown, + // FandomPicker v-dialog, per-suggestion 3-dot menu), let that + // overlay's own Esc handling fire instead of closing the whole + // modal mid-interaction. Vuetify marks open overlays with + // `.v-overlay--active`. + if (document.querySelector('.v-overlay--active')) return ev.preventDefault() emit('close') } else if (ev.key === 'ArrowLeft') { diff --git a/frontend/src/components/modal/ProvenancePanel.vue b/frontend/src/components/modal/ProvenancePanel.vue index 1d8e5ee..5c36fd4 100644 --- a/frontend/src/components/modal/ProvenancePanel.vue +++ b/frontend/src/components/modal/ProvenancePanel.vue @@ -10,7 +10,11 @@ density="compact" >{{ state.error }} - +

Attachments

@@ -159,6 +163,18 @@ function openPost(postId, artistId) { color: rgb(var(--v-theme-on-surface)); margin-bottom: 12px; } +.fc-prov__cards { + /* 2.5 cards-worth at the typical collapsed card height (~108px each + incl. 10px gap). Slightly under to ensure the third card's bottom + edge is clipped — the visual cue that there's more below. */ + max-height: 270px; + overflow-y: auto; + /* Hairline scrollbar that doesn't compete with content. */ + scrollbar-width: thin; + scrollbar-color: rgb(var(--v-theme-surface-light)) transparent; + /* Pad-right so the scrollbar gutter doesn't squeeze card borders. */ + padding-right: 4px; +} .fc-prov__card { border: 1px solid rgb(var(--v-theme-surface-light)); border-radius: 6px; padding: 10px 12px; margin-bottom: 10px; diff --git a/frontend/src/components/subscriptions/DownloadsTab.vue b/frontend/src/components/subscriptions/DownloadsTab.vue index 9a48144..5c26b07 100644 --- a/frontend/src/components/subscriptions/DownloadsTab.vue +++ b/frontend/src/components/subscriptions/DownloadsTab.vue @@ -58,6 +58,7 @@ :retrying-all="retryingAll" @retry="onRetrySource" @retry-all="onRetryAll" + @view-logs="onViewFailingLogs" />
@@ -364,6 +365,23 @@ watch(filterModel, async (m) => { async function openDetail(id) { await store.loadOne(id) } + +async function onViewFailingLogs(source) { + // Find and open the most recent DownloadEvent for this source. + // Reuses the existing DownloadDetailModal — same stdout/stderr/error + // surface the row-click in the events feed shows. + try { + const ev = await store.loadLastForSource(source.id) + if (!ev) { + toast({ + text: `No download events recorded for ${source.artist_name || source.platform} yet.`, + type: 'warning', + }) + } + } catch (e) { + toast({ text: `Failed to load logs: ${e.message}`, type: 'error' }) + } +}