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' })
+ }
+}