mirror of
https://github.com/thegeneralist01/archivr
synced 2026-07-21 18:55:36 +02:00
* http: realistic UA; fall back to WebPage on probe failure Replace bare 'archivr/0.1' user agent with a full Chrome 131 UA string (archivr/0.1 token retained at the end) in both probe_url_kind and download. More importantly, stop hard-failing when probe_url_kind returns an error. Sites behind Cloudflare's managed JS challenge (e.g. Medium) return 403 before any header tuning can help — a plain HTTP client cannot pass the challenge. Instead, log a warning and fall back to Source::WebPage so that the SingleFile/Chromium path gets a chance; a real browser can solve the challenge transparently. * capture: close dialog on Archive; rich per-URL and batch toast notifications UX changes: - Pressing Archive closes the capture dialog immediately; jobs continue polling in the background (component stays mounted). - Probe failures (e.g. Cloudflare JS challenge, HTTP 403) now fall back to Source::WebPage so SingleFile/Chromium can attempt the capture instead of hard-failing at the probe stage. Toast notifications: - Single URL: green 'Archived' on success, amber 'Archived with warnings' (with expandable detail) when uBlock/cookie-ext was skipped, red 'Capture failed' with expandable error text on failure. Per-item warning/error toasts include the locator so the user knows which URL was affected. - Multi-URL batch: per-item failure and warning toasts still fire with locators; per-item success toasts are suppressed. Once all jobs settle a single summary toast fires: 'N archived', 'N archived (M with warnings)', 'N archived (M with warnings), F failed', or 'N failed'. The summary Detail section lists the exact URLs that failed or warned, so the user retains that information after per-item toasts auto-dismiss. - Batch summary color: green = all clean; amber = any warnings or failures present; red = all failed. - handleIgnoreUblock now only removes per-item warning toasts (those with a locator) and persists the ignore flag; batch summary warnings (locator=null) are not swept. ToastStack improvements: - All three branches (success/warning/error) support a toast.headline field so batch summaries can set their own copy. - Warning branch: Details button conditional on toast.text; Ignore button conditional on toast.locator (uBlock-specific, not shown on batch summaries). - Locator display uses hostname/…/last-segment for URLs (preserves domain for context, tail for identity) and tail-truncation for shorthands; full locator in title attribute for hover. - Icon colors: green for success (✓), amber for warning (⚠), red for error (✕).
145 lines
5.1 KiB
JavaScript
145 lines
5.1 KiB
JavaScript
import { useState, useEffect } from 'react'
|
||
|
||
const TOAST_TTL = 7000 // ms before auto-dismiss; paused when detail is expanded
|
||
|
||
export default function ToastStack({ toasts, onDismiss, onIgnoreUblock }) {
|
||
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} onIgnoreUblock={onIgnoreUblock} />
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// For URLs: show hostname/…/tail (drops protocol + middle path, keeps domain + identifier).
|
||
// For shorthands (yt:, x:, etc.): keep the tail since it's the whole identifier.
|
||
// CSS text-overflow:ellipsis on .toast-locator handles any remaining overflow.
|
||
function shortLocator(locator) {
|
||
if (!locator) return null
|
||
if (locator.length <= 52) return locator
|
||
try {
|
||
const { hostname, pathname, search } = new URL(locator)
|
||
const segments = pathname.split('/').filter(Boolean)
|
||
const tail = (segments[segments.length - 1] ?? '') + search
|
||
const candidate = tail ? `${hostname}/\u2026/${tail}` : hostname
|
||
return candidate.length <= 56 ? candidate : candidate.slice(0, 53) + '\u2026'
|
||
} catch {
|
||
return '\u2026' + locator.slice(-51)
|
||
}
|
||
}
|
||
|
||
function Toast({ toast, onDismiss, onIgnoreUblock }) {
|
||
const [expanded, setExpanded] = useState(false)
|
||
const isWarning = toast.type === 'warning'
|
||
const isSuccess = toast.type === 'success'
|
||
|
||
// Auto-dismiss after TTL; paused while detail is expanded
|
||
useEffect(() => {
|
||
if (expanded) return
|
||
const timer = setTimeout(() => onDismiss(toast.id), TOAST_TTL)
|
||
return () => clearTimeout(timer)
|
||
}, [expanded, toast.id, onDismiss])
|
||
|
||
const short = shortLocator(toast.locator)
|
||
|
||
if (isSuccess) {
|
||
return (
|
||
<div className="toast toast--success" 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">{toast.headline || 'Archived'}</span>
|
||
{short && <span className="toast-locator" title={toast.locator}>{short}</span>}
|
||
</div>
|
||
<div className="toast-btns">
|
||
<button type="button" className="toast-dismiss" onClick={() => onDismiss(toast.id)} aria-label="Dismiss">×</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (isWarning) {
|
||
return (
|
||
<div className="toast toast--warning" 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">{toast.headline || 'Archived with warnings'}</span>
|
||
{short && <span className="toast-locator" title={toast.locator}>{short}</span>}
|
||
</div>
|
||
<div className="toast-btns">
|
||
{toast.text && (
|
||
<button
|
||
type="button"
|
||
className="toast-view-btn"
|
||
onClick={() => setExpanded(v => !v)}
|
||
aria-expanded={expanded}
|
||
>
|
||
{expanded ? 'Hide' : 'Details'}
|
||
</button>
|
||
)}
|
||
{toast.locator && (
|
||
<button
|
||
type="button"
|
||
className="toast-view-btn toast-ignore-btn"
|
||
onClick={() => { onIgnoreUblock?.(); onDismiss(toast.id) }}
|
||
>
|
||
Ignore
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="toast-dismiss"
|
||
onClick={() => onDismiss(toast.id)}
|
||
aria-label="Dismiss"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{expanded && (
|
||
<p className="toast-warning-detail">
|
||
{toast.text || 'The page was captured but one or more browser extensions were unavailable (ad-blocking or cookie-consent). Check ARCHIVR_UBLOCK_EXT and ARCHIVR_COOKIE_EXT in your server config.'}
|
||
</p>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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">{toast.headline || 'Capture failed'}</span>
|
||
{short && <span className="toast-locator" title={toast.locator}>{short}</span>}
|
||
</div>
|
||
<div className="toast-btns">
|
||
{toast.text && (
|
||
<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.text && (
|
||
<pre className="toast-error-detail">{toast.text}</pre>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|