1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat: non-blocking batch capture dialog, toasts on failure, fix /runs errors

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
This commit is contained in:
TheGeneralist 2026-07-05 13:56:00 +02:00
parent d83faab8e6
commit fb1115a409
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
11 changed files with 698 additions and 223 deletions

View file

@ -0,0 +1,63 @@
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>
)
}