1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-22 03:05:32 +02:00

feat: uBlock Origin Lite integration for ad-blocking during WebPage captures

- singlefile.rs: when ARCHIVR_UBLOCK=true and ARCHIVR_UBLOCK_EXT is set,
  archivr owns Chrome's lifecycle (--headless=new, --remote-debugging-port,
  --load-extension); single-file connects via --browser-server instead of
  launching its own Chrome. Falls back to old behaviour with ublock_skipped=true
  when the ext path is missing or invalid.
- capture.rs: thread ublock_skipped through CaptureResult
- database.rs: add notes_json TEXT column to capture_jobs (DDL + idempotent
  ALTER TABLE migration); update_capture_job_status gains notes_json param
- archive.rs: expose notes_json in CaptureJobSummary
- routes.rs: store {"ublock_skipped":true} in notes_json on completed captures
- ToastStack.jsx: warning toast variant (toast--warning) with Details expander
  and Ignore button
- CaptureDialog.jsx: fire warning toast when poll result has ublock_skipped
- App.jsx: sessionStorage-backed Ignore suppression for ublock warnings
- styles.css: .toast--warning (amber left border) + .toast-warning-detail
- flake.nix: ublockLite derivation fetches uBOLite_2026.705.2152.chromium.zip
  (pinned SHA256) from uBlockOrigin/uBOL-home; sets ARCHIVR_UBLOCK_EXT in both
  archivr and archivr-server wrappers

Env vars:
  ARCHIVR_UBLOCK=true (default) — enable uBlock during WebPage captures
  ARCHIVR_UBLOCK_EXT — path to unpacked uBOL extension dir (set by Nix)
This commit is contained in:
TheGeneralist 2026-07-07 17:18:55 +02:00
parent dae61e585d
commit 8a90259a76
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
14 changed files with 546 additions and 226 deletions

View file

@ -85,6 +85,9 @@ export default function App() {
const [toasts, setToasts] = useState([])
const toastIdRef = useRef(0)
const [ublockWarningIgnored, setUblockWarningIgnored] = useState(
() => sessionStorage.getItem('ublockWarningIgnored') === 'true'
)
const humanizeTags = currentUser?.humanize_slugs ?? false;
@ -238,15 +241,22 @@ export default function App() {
])
}, [archiveId, searchQuery, tagFilter, loadEntries])
const handleToast = useCallback((errorText, locator) => {
const handleToast = useCallback((text, locator, type = 'error') => {
if (type === 'warning' && ublockWarningIgnored) return
const id = ++toastIdRef.current
setToasts(prev => [...prev, { id, errorText, locator }])
}, [])
setToasts(prev => [...prev, { id, text, locator, type }])
}, [ublockWarningIgnored])
const handleDismissToast = useCallback((id) => {
setToasts(prev => prev.filter(t => t.id !== id))
}, [])
const handleIgnoreUblock = useCallback(() => {
sessionStorage.setItem('ublockWarningIgnored', 'true')
setUblockWarningIgnored(true)
setToasts(prev => prev.filter(t => t.type !== 'warning'))
}, [])
if (authState === 'loading') return <div className="auth-loading">Loading\u2026</div>;
if (authState === 'setup') return <SetupPage onComplete={() => setAuthState('login')} />;
if (authState === 'login') return <LoginPage onLogin={user => { setCurrentUser(user); setAuthState('authenticated'); }} />;
@ -347,7 +357,7 @@ export default function App() {
onCaptured={handleCaptured}
onToast={handleToast}
/>
<ToastStack toasts={toasts} onDismiss={handleDismissToast} />
<ToastStack toasts={toasts} onDismiss={handleDismissToast} onIgnoreUblock={handleIgnoreUblock} />
</>
</AuthContext.Provider>
)

View file

@ -186,6 +186,13 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
})
}, 1400)
onCapturedRef.current()
// Warn if uBlock was requested but the extension wasn't available
try {
const notes = updated.notes_json ? JSON.parse(updated.notes_json) : null
if (notes?.ublock_skipped) {
onToastRef.current(null, locator, 'warning')
}
} catch {}
} else if (updated.status === 'failed') {
clearInterval(pollIntervals.current.get(jobUid))
pollIntervals.current.delete(jobUid)

View file

@ -1,22 +1,23 @@
import { useState, useEffect } from 'react'
const TOAST_TTL = 7000 // ms before auto-dismiss; paused when error is expanded
const TOAST_TTL = 7000 // ms before auto-dismiss; paused when detail is expanded
export default function ToastStack({ toasts, onDismiss }) {
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} />
<Toast key={t.id} toast={t} onDismiss={onDismiss} onIgnoreUblock={onIgnoreUblock} />
))}
</div>
)
}
function Toast({ toast, onDismiss }) {
function Toast({ toast, onDismiss, onIgnoreUblock }) {
const [expanded, setExpanded] = useState(false)
const isWarning = toast.type === 'warning'
// Auto-dismiss after TTL; paused while error detail is expanded
// Auto-dismiss after TTL; paused while detail is expanded
useEffect(() => {
if (expanded) return
const timer = setTimeout(() => onDismiss(toast.id), TOAST_TTL)
@ -27,6 +28,50 @@ function Toast({ toast, onDismiss }) {
? (toast.locator.length > 48 ? toast.locator.slice(0, 45) + '\u2026' : toast.locator)
: null
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">Ad-blocking unavailable</span>
{short && <span className="toast-locator">{short}</span>}
</div>
<div className="toast-btns">
<button
type="button"
className="toast-view-btn"
onClick={() => setExpanded(v => !v)}
aria-expanded={expanded}
>
{expanded ? 'Hide' : 'Details'}
</button>
<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 || 'ARCHIVR_UBLOCK=true but ARCHIVR_UBLOCK_EXT is not set or the path is invalid. The page was captured without ad-blocking. Set ARCHIVR_UBLOCK_EXT to the unpacked uBlock Origin Lite extension directory to enable ad-blocking, or set ARCHIVR_UBLOCK=false to silence this warning.'}
</p>
)}
</div>
)
}
return (
<div className="toast toast--error" role="alert" aria-atomic="true">
<div className="toast-top">
@ -36,7 +81,7 @@ function Toast({ toast, onDismiss }) {
{short && <span className="toast-locator">{short}</span>}
</div>
<div className="toast-btns">
{toast.errorText && (
{toast.text && (
<button
type="button"
className="toast-view-btn"
@ -55,8 +100,8 @@ function Toast({ toast, onDismiss }) {
</button>
</div>
</div>
{expanded && toast.errorText && (
<pre className="toast-error-detail">{toast.errorText}</pre>
{expanded && toast.text && (
<pre className="toast-error-detail">{toast.text}</pre>
)}
</div>
)

View file

@ -812,6 +812,7 @@ select {
to { opacity: 1; transform: translateY(0) scale(1); }
}
.toast--error { border-left: 3px solid var(--accent); }
.toast--warning { border-left: 3px solid #e8a000; }
.toast-top {
display: flex;
@ -892,6 +893,18 @@ select {
max-height: 200px;
overflow-y: auto;
}
.toast-warning-detail {
margin: 0;
padding: 10px 14px 12px;
font-size: 12px;
line-height: 1.55;
color: var(--muted);
background: var(--paper-2);
border-top: 1px solid var(--line);
white-space: pre-wrap;
word-break: break-word;
}
.toast-ignore-btn { color: var(--muted); }
/* ── Utility ─────────────────────────────────────────────────────────────── */
.muted { color: var(--muted); }