Replace request activity feed with simple current progress
Magent CI/CD / verify (push) Successful in 1m57s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-11 23:06:14 +12:00
parent 835d9c8de3
commit 8734f461bb
3 changed files with 49 additions and 24 deletions
+36 -15
View File
@@ -14,10 +14,32 @@ export default function LatestActivity({ operation, besideDownload, onDismiss }:
const trigger = useRef<HTMLButtonElement>(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 = <div className={`activity-process is-${working ? 'working' : operation.status}`} role="progressbar" aria-label={currentStep} aria-valuemin={0} aria-valuemax={100} aria-valuenow={working || operation.status === 'error' ? undefined : 100}><span /></div>
useEffect(() => {
if (!open) return
@@ -35,23 +57,22 @@ export default function LatestActivity({ operation, besideDownload, onDismiss }:
<div className={`request-overview-block latest-activity ${besideDownload ? 'beside-download' : 'full-row'}`}>
<button ref={trigger} type="button" className="latest-activity-trigger" onClick={() => setOpen(true)} aria-haspopup="dialog" aria-expanded={open}>
<span className="latest-activity-heading"><span className="request-overview-label">Latest activity</span><span className={`latest-activity-badge is-${operation.status}`}>{status}</span></span>
<span className="latest-activity-message" role="status">{latest?.message || 'Getting ready to check your request…'}</span>
<span className="latest-activity-more">View all activity ({operation.events.length}) <span aria-hidden="true"></span></span>
<span className="latest-activity-message" role="status">{currentStep}</span>
{progress}
<span className="latest-activity-more">View progress</span>
</button>
<dialog ref={dialog} className="activity-dialog" aria-labelledby="activity-dialog-title" onCancel={() => setOpen(false)} onClose={() => setOpen(false)}>
<div className="activity-dialog-content">
<header>
<div><span className="request-overview-label">Activity details</span><h2 id="activity-dialog-title">{operation.label}</h2><small>{status} · {operation.events.length} updates</small></div>
<div><span className="request-overview-label">Request progress</span><h2 id="activity-dialog-title">{status}</h2></div>
<button type="button" onClick={() => setOpen(false)}>Close</button>
</header>
{operation.status === 'running' && <p role="status" className="activity-working">Working on your request. This can take a minute while the media services search.</p>}
<ol aria-live="polite" className="activity-dialog-events" aria-label="All activity, oldest first">
{operation.events.map((event) => <li key={event.id} className={`is-${event.state}`}>
<span className="activity-event-state">{event.state === 'active' ? 'Working' : event.state === 'error' ? 'Needs attention' : 'Done'}</span>
<div><strong>{event.service}</strong><p>{event.message}</p></div>
</li>)}
</ol>
{operation.status !== 'running' && <footer><button type="button" onClick={() => { setOpen(false); onDismiss() }}>Dismiss activity</button></footer>}
<div className="activity-current" role="status" aria-live="polite" aria-atomic="true">
<p className="activity-current-step">{currentStep}</p>
{progress}
<p className="activity-next-step">{nextStep}</p>
</div>
{!working && <footer><button type="button" onClick={() => { setOpen(false); onDismiss() }}>Dismiss activity</button></footer>}
</div>
</dialog>
</div>
+10 -8
View File
@@ -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; } }