Compact request activity into a live summary and modal history

This commit is contained in:
2026-09-06 22:20:33 +12:00
parent 009bb35032
commit 74c49fad5b
10 changed files with 223 additions and 71 deletions
+20 -45
View File
@@ -1,6 +1,7 @@
'use client'
import PageHeading from '../../ui/PageHeading'
import LatestActivity from './LatestActivity'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
@@ -200,12 +201,6 @@ const torrentProgress = (torrent: Record<string, any>) => {
const formatProgress = (progress: number) => `${progress.toFixed(1).replace(/\.0$/, '')}% complete`
const formatDuration = (duration?: number | null) => {
if (typeof duration !== 'number' || Number.isNaN(duration)) return null
if (duration < 1000) return `${Math.max(0, Math.round(duration))}ms`
return `${(duration / 1000).toFixed(1)}s`
}
const mergeLiveDownload = (current: Snapshot, live: LiveDownloadProgress): Snapshot => {
if (String(current.request_id) !== String(live.request_id)) return current
if ((current.presentation?.repairCycle ?? null) !== (live.repairCycle ?? null)) return current
@@ -583,7 +578,10 @@ export default function RequestTimelinePage() {
})
if (!stopped && progressResponse.ok) {
const progress = await progressResponse.json()
if (Array.isArray(progress?.events)) setOperationProgress(progress)
if (Array.isArray(progress?.events)) {
setOperationProgress(progress)
return progress as OperationProgress
}
}
} catch (error) {
if (!stopped) console.error(error)
@@ -594,8 +592,21 @@ export default function RequestTimelinePage() {
const timer = window.setInterval(() => void refreshProgress(), 650)
try {
const response = await request
await refreshProgress()
const finalProgress = await refreshProgress()
if (!finalProgress || finalProgress.status === 'running') {
setOperationProgress((current) => current?.id === operationId ? {
...current, status: response.ok ? 'complete' : 'error',
events: [...current.events, { id: 'result', service: 'Magent', state: response.ok ? 'complete' : 'error',
message: response.ok ? 'This action has finished. Check the request status for what happens next.' : 'This action could not be completed. Check the message beside the request controls.' }],
} : current)
}
return response
} catch (error) {
setOperationProgress((current) => current?.id === operationId ? {
...current, status: 'error', events: [...current.events, { id: 'connection-error', service: 'Magent', state: 'error',
message: 'The connection was interrupted. Recheck the request before trying the action again—it may already have started.' }],
} : current)
throw error
} finally {
stopped = true
window.clearInterval(timer)
@@ -753,6 +764,7 @@ export default function RequestTimelinePage() {
{download?.lastSeenAt && !download?.torrents?.length && <small>Last observed {formatWhen(download.lastSeenAt)}</small>}
</div>
)}
{operationProgress && <LatestActivity operation={operationProgress} besideDownload={downloadVisible} onDismiss={() => setOperationProgress(null)} />}
<div className="request-overview-block request-next-step">
<div className="request-next-step-main">
<div className="request-next-step-copy">
@@ -793,43 +805,6 @@ export default function RequestTimelinePage() {
{actionError ?? actionMessage}
</div>
)}
{operationProgress && (
<div className={`request-operation-progress is-${operationProgress.status}`} aria-live="polite">
<div className="request-operation-heading">
<div>
<span className="request-overview-label">Remote activity</span>
<strong>{operationProgress.label}</strong>
</div>
<div className="request-operation-heading-actions">
{formatDuration(operationProgress.duration_ms) && (
<span>{formatDuration(operationProgress.duration_ms)}</span>
)}
<span className={`request-operation-status is-${operationProgress.status}`}>
{operationProgress.status === 'running' ? 'In progress' : operationProgress.status}
</span>
{operationProgress.status !== 'running' && (
<button type="button" onClick={() => setOperationProgress(null)}>Dismiss</button>
)}
</div>
</div>
<div className="request-operation-events">
{operationProgress.events.map((event) => (
<div className={`request-operation-event is-${event.state}`} key={event.id}>
<i aria-hidden="true" />
<div>
<strong>{event.service}</strong>
<span>{event.message}</span>
</div>
<small>
{event.state === 'active'
? 'Waiting…'
: formatDuration(event.duration_ms) ?? (event.status_code ? `HTTP ${event.status_code}` : 'Done')}
</small>
</div>
))}
</div>
</div>
)}
</section>
{repairActivity?.visible && (