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

feat: cookie consent extension support (ARCHIVR_COOKIE_EXT)

Mirrors the uBlock Origin Lite integration exactly:

Backend:
- singlefile.rs: resolve_cookie_ext_config() reads ARCHIVR_COOKIE_CONSENT
  (default true) + ARCHIVR_COOKIE_EXT path; extension paths comma-joined
  into --load-extension / --disable-extensions-except so uBlock and cookie
  ext can coexist; SaveResult.cookie_ext_skipped tracks miss
- database.rs: cookie_ext_enabled column on instance_settings (DEFAULT 1);
  idempotent ALTER TABLE migration; get/update wired through
- capture.rs: CaptureConfig.cookie_ext_enabled: Option<bool>; threaded to
  singlefile::save(); cookie_ext_skipped surfaced in CaptureResult
- routes.rs: CaptureBody + UpdateInstanceSettingsBody get cookie_ext_enabled;
  capture handler resolves effective value (body overrides global); notes_json
  only includes skipped fields that are true; GET instance-settings includes
  cookie_ext_available from env path check

Frontend:
- api.js: submitCapture forwards cookie_ext_enabled
- SettingsView.jsx: 'I Still Don't Care About Cookies' card in Extensions
  tab; always-active toggle (user can disable even when ext not installed);
  amber 'Not configured' hint + ARCHIVR_COOKIE_EXT guidance when unavailable
- CaptureDialog.jsx: 'Block cookie banners' toggle in Advanced options;
  always shown with amber hint when ext not configured; defaults from
  global setting

Operator setup: download + unzip the extension from GitHub releases, set
ARCHIVR_COOKIE_EXT=/path/to/unpacked/ext. No Node daemon needed.
This commit is contained in:
TheGeneralist 2026-07-08 11:25:37 +02:00
parent a6ca24b846
commit 52effb60f6
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4
10 changed files with 260 additions and 70 deletions

View file

@ -102,6 +102,7 @@ export async function submitCapture(archiveId, locator, quality = null, extensio
if (extensions) {
if (typeof extensions.ublock_enabled === 'boolean') payload.ublock_enabled = extensions.ublock_enabled
if (typeof extensions.reader_mode === 'boolean') payload.reader_mode = extensions.reader_mode
if (typeof extensions.cookie_ext_enabled === 'boolean') payload.cookie_ext_enabled = extensions.cookie_ext_enabled
}
const res = await fetch(`/api/archives/${archiveId}/captures`, {
method: "POST",

View file

@ -116,18 +116,24 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
const [advancedOpen, setAdvancedOpen] = useState(false)
// null = use server default; true/false = per-session override
const [ublockOverride, setUblockOverride] = useState(null)
// Server-side global default (loaded on mount, null until loaded)
const [globalUblock, setGlobalUblock] = useState(null)
// Server-side global settings (loaded on mount, null until loaded)
const [globalSettings, setGlobalSettings] = useState(null)
// Cookie consent: session-level only, initialized from server default
const [cookieExtEnabled, setCookieExtEnabled] = useState(true)
// Load global uBlock setting from server once on mount
// Load global settings from server once on mount
useEffect(() => {
getInstanceSettings()
.then(s => setGlobalUblock(s.ublock_enabled ?? true))
.catch(() => setGlobalUblock(true)) // safe default
.then(s => {
setGlobalSettings(s)
setCookieExtEnabled(s.cookie_ext_enabled ?? true)
})
.catch(() => setGlobalSettings({}))
}, [])
// Effective uBlock for this session
const ublockEnabled = ublockOverride !== null ? ublockOverride : (globalUblock ?? true)
const ublockEnabled = ublockOverride !== null ? ublockOverride : (globalSettings?.ublock_enabled ?? true)
// Reader mode: off by default, per-session only
const [readerMode, setReaderMode] = useState(false)
@ -242,7 +248,7 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
const qual = item.quality || 'best'
setItems(prev => prev.map(it => it.id === item.id ? { ...it, status: 'submitting', error: null } : it))
try {
const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode }
const extensions = { ublock_enabled: ublockEnabled, reader_mode: readerMode, cookie_ext_enabled: cookieExtEnabled }
const job = await submitCapture(aid, loc, qual, extensions)
setItems(prev => prev.map(it =>
it.id === item.id ? { ...it, status: 'running', jobUid: job.job_uid, archiveId: aid } : it
@ -397,6 +403,25 @@ export default function CaptureDialog({ open, archiveId, onClose, onCaptured, on
<span className="ext-toggle-knob" />
</button>
</label>
<label className="capture-ext-row" style={{ marginTop: 8 }}>
<span className="capture-ext-label">
<span className="capture-ext-name">Block cookie banners</span>
<span className="capture-ext-desc">Dismiss cookie consent banners during this capture</span>
{!globalSettings?.cookie_ext_available && (
<span className="capture-ext-hint">Not configured &mdash; set <code>ARCHIVR_COOKIE_EXT</code></span>
)}
</span>
<button
type="button"
role="switch"
aria-checked={cookieExtEnabled}
className={`ext-toggle ext-toggle--sm${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => setCookieExtEnabled(v => !v)}
aria-label="Toggle cookie banner blocking for this capture"
>
<span className="ext-toggle-knob" />
</button>
</label>
<label className="capture-ext-row" style={{ marginTop: 8 }}>
<span className="capture-ext-label">
<span className="capture-ext-name">Reader mode</span>

View file

@ -669,10 +669,26 @@ function ExtensionsTab() {
}
}
async function toggleCookieExt(val) {
setSaving(true)
setMsg(null)
try {
await updateInstanceSettings({ cookie_ext_enabled: val })
setSettings(s => ({ ...s, cookie_ext_enabled: val }))
setMsg({ ok: true, text: 'Saved.' })
} catch (e) {
setMsg({ ok: false, text: e.message })
} finally {
setSaving(false)
}
}
if (loading) return <div className="muted">Loading\u2026</div>
const extAvailable = settings?.ublock_ext_available ?? false
const extEnabled = settings?.ublock_enabled ?? true
const cookieExtAvailable = settings?.cookie_ext_available ?? false
const cookieExtEnabled = settings?.cookie_ext_enabled ?? true
return (
<div style={{ maxWidth: 560 }}>
@ -712,6 +728,34 @@ function ExtensionsTab() {
</div>
</div>
<div className="ext-card">
<div className="ext-card-header">
<div className="ext-card-info">
<span className="ext-card-name">I Still Don&rsquo;t Care About Cookies</span>
<span className="ext-card-desc">
Dismiss cookie consent banners during archiving.
</span>
{!cookieExtAvailable && (
<span className="ext-card-hint">
Not configured &mdash; set <code>ARCHIVR_COOKIE_EXT</code> to the
unpacked extension directory to enable.
</span>
)}
</div>
<button
type="button"
role="switch"
aria-checked={cookieExtEnabled}
className={`ext-toggle${cookieExtEnabled ? ' ext-toggle--on' : ''}`}
onClick={() => toggleCookieExt(!cookieExtEnabled)}
disabled={saving}
aria-label="Toggle I Still Don't Care About Cookies"
>
<span className="ext-toggle-knob" />
</button>
</div>
</div>
{msg && <div className={`form-msg form-msg--${msg.ok ? 'ok' : 'err'}`}>{msg.text}</div>}
</div>
</div>