From 8734f461bb8d9bf0b27b7fcd491d754d5944b28a Mon Sep 17 00:00:00 2001 From: Zak Bearman Date: Fri, 11 Sep 2026 23:06:14 +1200 Subject: [PATCH] Replace request activity feed with simple current progress --- frontend/app/requests/[id]/LatestActivity.tsx | 51 +++++++++++++------ .../app/requests/[id]/latest-activity.css | 18 ++++--- scripts/review_manual_selection_ui.cjs | 4 +- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/frontend/app/requests/[id]/LatestActivity.tsx b/frontend/app/requests/[id]/LatestActivity.tsx index a7915ab..01c6bad 100644 --- a/frontend/app/requests/[id]/LatestActivity.tsx +++ b/frontend/app/requests/[id]/LatestActivity.tsx @@ -14,10 +14,32 @@ export default function LatestActivity({ operation, besideDownload, onDismiss }: const trigger = useRef(null) const [open, setOpen] = useState(true) useEffect(() => { setOpen(true) }, [operation.id]) - const latest = [...operation.events].sort((a, b) => - (a.finished_at ?? a.started_at ?? '').localeCompare(b.finished_at ?? b.started_at ?? '') - ).at(-1) - const status = operation.status === 'running' ? 'Working' : operation.status === 'complete' ? 'Finished' : operation.status === 'searching' ? 'Search still running' : 'Needs attention' + // Events are appended in order. Client result events have no timestamp. + const latest = operation.events.at(-1) + const working = operation.status === 'running' || operation.status === 'searching' + const message = latest?.message ?? '' + const choosing = /[1-9]\d* releases? (found|shown)/i.test(message) + const sent = operation.status === 'complete' && /sent|accepted.*release/i.test(message) && /download|release|Sonarr|Radarr/i.test(message) + const interrupted = latest?.id === 'connection-error' + const status = working ? 'Working on it' : choosing ? 'Choose a download' : operation.status === 'complete' ? 'Done' : 'Needs your attention' + const currentStep = working + ? /send release/i.test(operation.label) ? 'Sending your download...' + : /search/i.test(operation.label) ? 'Looking for a download...' + : latest?.service === 'Jellyfin' ? 'Checking if it is ready to watch...' + : latest?.service === 'qBittorrent' ? 'Checking your download...' + : 'Checking your request...' + : interrupted ? 'The connection was lost.' + : choosing ? 'The search is finished. Choose a version to download.' + : sent ? 'Your download has been sent.' + : operation.status === 'error' ? 'We could not finish this step.' + : 'This check is finished.' + const nextStep = working ? 'Please wait. You can close this box while we work.' + : interrupted ? 'Close this box and check the request before trying again.' + : choosing ? 'Close this box to see the available downloads.' + : sent ? 'You can close this box. The request will update when the download starts.' + : operation.status === 'error' ? 'Close this box to review the request and its available options.' + : 'Close this box to see the updated request status.' + const progress =
useEffect(() => { if (!open) return @@ -35,23 +57,22 @@ export default function LatestActivity({ operation, besideDownload, onDismiss }:
setOpen(false)} onClose={() => setOpen(false)}>
-
Activity details

{operation.label}

{status} · {operation.events.length} updates
+
Request progress

{status}

- {operation.status === 'running' &&

Working on your request. This can take a minute while the media services search.

} -
    - {operation.events.map((event) =>
  1. - {event.state === 'active' ? 'Working' : event.state === 'error' ? 'Needs attention' : 'Done'} -
    {event.service}

    {event.message}

    -
  2. )} -
- {operation.status !== 'running' &&
} +
+

{currentStep}

+ {progress} +

{nextStep}

+
+ {!working && }
diff --git a/frontend/app/requests/[id]/latest-activity.css b/frontend/app/requests/[id]/latest-activity.css index e43ae98..23354f2 100644 --- a/frontend/app/requests/[id]/latest-activity.css +++ b/frontend/app/requests/[id]/latest-activity.css @@ -14,15 +14,17 @@ .activity-dialog header { display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; } .activity-dialog h2 { font-size: 1.2rem; margin: .4rem 0; } .activity-dialog small { color: var(--ops-muted, #bbb); } -.activity-dialog-events { list-style: none; padding: 0; margin: 1.25rem 0 0; display: grid; gap: .65rem; } -.activity-dialog-events li { display: grid; grid-template-columns: 85px minmax(0, 1fr); gap: .8rem; padding: .9rem; border: 1px solid var(--ops-border, #444); border-radius: 10px; } -.activity-dialog-events strong { font-size: .8rem; } -.activity-dialog-events p { margin: .3rem 0 0; font-size: .875rem; line-height: 1.5; overflow-wrap: anywhere; } -.activity-event-state { font-size: .7rem; color: #55dec0; } -.is-error > .activity-event-state { color: #ff9b9b; } -.is-active > .activity-event-state { color: #83d7f7; } .activity-dialog footer { display: flex; justify-content: flex-end; margin-top: 1rem; } @media (max-width: 720px) { .latest-activity.beside-download { grid-column: 1 / -1; grid-row: auto; } - .activity-dialog-events li { grid-template-columns: 1fr; gap: .4rem; } } + +.activity-current { padding: 1rem 0 .25rem; } +.activity-current-step { font-size: 1.05rem; font-weight: 600; margin: 0 0 1rem; } +.activity-next-step { color: var(--ops-muted, #bbb); font-size: .875rem; line-height: 1.6; margin: 1rem 0 0; } +.activity-process { height: 6px; width: 100%; overflow: hidden; border-radius: 999px; background: #ffffff14; } +.activity-process > span { display: block; height: 100%; width: 100%; border-radius: inherit; background: #55dec0; } +.activity-process.is-working > span { width: 35%; background: var(--ops-accent, #83d7f7); animation: activity-process-slide 1.6s ease-in-out infinite alternate; } +.activity-process.is-error > span { background: #e6b86c; } +@keyframes activity-process-slide { from { transform: translateX(0); } to { transform: translateX(185%); } } +@media (prefers-reduced-motion: reduce) { .activity-process.is-working > span { animation: none; width: 100%; opacity: .65; } } diff --git a/scripts/review_manual_selection_ui.cjs b/scripts/review_manual_selection_ui.cjs index 3efd6eb..9730eb1 100644 --- a/scripts/review_manual_selection_ui.cjs +++ b/scripts/review_manual_selection_ui.cjs @@ -47,7 +47,9 @@ const base = process.env.REVIEW_BASE || 'http://localhost:3114'; assert.equal(writes.length, before); page.once('dialog', dialog => dialog.accept()); await picker.getByRole('button', { name: 'Download outside profile' }).click(); - await page.getByText('Selected release sent to Sonarr.', { exact: true }).first().waitFor(); + await page.getByRole('dialog', { name: 'Done', exact: true }).getByText('Your download has been sent.', { exact: true }).waitFor(); + assert.equal(await page.locator('.activity-dialog-events').count(), 0); + assert.equal(await page.getByRole('dialog', { name: 'Done', exact: true }).getByRole('progressbar').getAttribute('aria-valuenow'), '100'); assert.equal(writes.at(-1).ignoreProfileLimits, true); assert.equal(writes.at(-1).selectionToken, 'signed-fixture'); }