mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
CaptureDialog:
- Replace single textarea with multi-row inputs; + button adds rows
- Submit fires all pending rows in parallel, dialog stays open/usable
- Polling intervals live on a persistent ref (not cleared on close) so
toasts fire even after the dialog is dismissed
- archiveId stored per item at submit time; page-refresh reconnect uses
it.archiveId instead of the possibly-null prop
- Completed rows flash green then self-remove; failed rows show inline
error + retry button
- Cancel becomes Close while jobs are in flight
ToastStack (new component):
- Fixed bottom-right overlay with spring-in animation
- Error toast: truncated locator, View error / Hide toggle expanding
full error_text in a monospace pre block
- Auto-dismisses after 7 s; timer pauses while detail is expanded
RunsView:
- Failed rows are clickable and expand a full-width detail row showing
error_summary in a scrollable monospace block
capture.rs (archivr-core):
- Staging dir is now "{millis}-{uuid}" — parallel captures in the same
millisecond can no longer collide on temp paths
- create_archive_run moved before URL Content-Type probe so every
attempt appears in /runs regardless of outcome
- Probe failures now call create_archive_run_item with source_metadata
fallback then fail_run, recording error_text on the item and
error_summary on the run with correct failed_count
styles.css:
- Capture dialog: header row, multi-row layout, status dots, spinner,
add-row dashed button, per-row error text
- Toast stack: fixed overlay, error card with coloured left border,
monospace detail expansion
- Run error rows: clickable hover tint, expand hint chevron, detail pre
63 lines
1.9 KiB
JavaScript
63 lines
1.9 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
|
||
const TOAST_TTL = 7000 // ms before auto-dismiss; paused when error is expanded
|
||
|
||
export default function ToastStack({ toasts, onDismiss }) {
|
||
if (!toasts.length) return null
|
||
return (
|
||
<div className="toast-stack" role="log" aria-live="polite" aria-label="Notifications">
|
||
{toasts.map(t => (
|
||
<Toast key={t.id} toast={t} onDismiss={onDismiss} />
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function Toast({ toast, onDismiss }) {
|
||
const [expanded, setExpanded] = useState(false)
|
||
|
||
// Auto-dismiss after TTL; paused while error detail is expanded
|
||
useEffect(() => {
|
||
if (expanded) return
|
||
const timer = setTimeout(() => onDismiss(toast.id), TOAST_TTL)
|
||
return () => clearTimeout(timer)
|
||
}, [expanded, toast.id, onDismiss])
|
||
|
||
const short = toast.locator
|
||
? (toast.locator.length > 48 ? toast.locator.slice(0, 45) + '\u2026' : toast.locator)
|
||
: null
|
||
|
||
return (
|
||
<div className="toast toast--error" role="alert" aria-atomic="true">
|
||
<div className="toast-top">
|
||
<span className="toast-icon" aria-hidden="true">✕</span>
|
||
<div className="toast-body">
|
||
<span className="toast-headline">Capture failed</span>
|
||
{short && <span className="toast-locator">{short}</span>}
|
||
</div>
|
||
<div className="toast-btns">
|
||
{toast.errorText && (
|
||
<button
|
||
type="button"
|
||
className="toast-view-btn"
|
||
onClick={() => setExpanded(v => !v)}
|
||
>
|
||
{expanded ? 'Hide' : 'View error'}
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="toast-dismiss"
|
||
onClick={() => onDismiss(toast.id)}
|
||
aria-label="Dismiss"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{expanded && toast.errorText && (
|
||
<pre className="toast-error-detail">{toast.errorText}</pre>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|