Sources: per-source paywall override (3-state) — fix domain-rule mis-flags
The Articles inspector revealed paywall is domain-coarse: nytimes.com is flagged, so NY Times Learning's free Word-of-the-Day inherits 🔒 — and that flag isn't cosmetic, it deprioritizes the content in feed sort + lead selection. Add a per-source override so admins can correct it after inspecting. - sources.paywall_override: NULL (domain rule) | 'free' | 'paywalled'. - paywall.py: keep low-level is_paywalled(url) (domain); add is_paywalled_for_source (url, override) for the EFFECTIVE decision — never patched the domain helper globally (per Codex), so "domain says X" stays distinguishable from "overridden". - Threaded everywhere ranking/UI touches paywall, via src.paywall_override on the shared _ARTICLE_COLUMNS + the source-aware helper: feed sort, /api/since, replace, lead selection, Article badge, brief composition (briefs.py), digest, source_health (table 🔒), the Articles inspector, and the review/attention check — so ranking and UI always agree. - Endpoint POST /api/admin/sources/{id}/paywall {override}; admin UI: a select in the inspector header (Use domain rule / Treat as free / Treat as paywalled) + the basis ("ON (domain)" / "OFF (override)"), optimistic so the panel stays open. Test: domain rule → paywalled in table+inspector+feed badge; 'free' → off in all three; validation 422 + 404. 242 pytest + 11 vitest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,24 @@
|
||||
finally { s._artBusy = false; }
|
||||
}
|
||||
const ART_FILTERS = [['all', 'All'], ['accepted', 'Accepted'], ['rejected', 'Rejected'], ['no_image', 'No image'], ['duplicates', 'Duplicates']];
|
||||
function pwBasis(sum) {
|
||||
if (!sum) return '';
|
||||
if (sum.paywall_override === 'free') return 'OFF (override)';
|
||||
if (sum.paywall_override === 'paywalled') return 'ON (override)';
|
||||
return sum.paywalled ? 'ON (domain)' : 'off';
|
||||
}
|
||||
// Per-source paywall override — corrects a domain-rule false positive/negative
|
||||
// and flows into feed/lead/brief ranking + the table's 🔒. Optimistic in place
|
||||
// so the inspector panel stays open.
|
||||
async function setPaywall(s, override) {
|
||||
const ov = override || null;
|
||||
try {
|
||||
await postJSON(`/api/admin/sources/${s.id}/paywall`, { override: ov });
|
||||
const eff = ov === 'free' ? false : ov === 'paywalled' ? true : (s._artSummary?.paywall_domain ?? s.paywalled);
|
||||
s.paywall_override = ov; s.paywalled = eff; // updates the Media-column 🔒
|
||||
if (s._artSummary) { s._artSummary.paywall_override = ov; s._artSummary.paywalled = eff; }
|
||||
} catch (e) { s._artErr = e?.message || 'Could not set the override.'; }
|
||||
}
|
||||
|
||||
// --- Source candidates: supervised "add a source" pipeline ---
|
||||
let candidates = $state([]);
|
||||
@@ -726,9 +744,15 @@
|
||||
{#if s._artSummary}
|
||||
<div class="artsum">
|
||||
<strong>{s._artSummary.total}</strong> ingested · {s._artSummary.accepted} accepted ·
|
||||
{s._artSummary.rejected} rejected · {s._artSummary.no_image} no image ·
|
||||
{s._artSummary.duplicates} dup ·
|
||||
paywall rule: <span class="pwrule" class:on={s._artSummary.paywalled}>{s._artSummary.paywalled ? 'ON (domain)' : 'off'}</span>
|
||||
{s._artSummary.rejected} rejected · {s._artSummary.no_image} no image · {s._artSummary.duplicates} dup
|
||||
</div>
|
||||
<div class="pwctl">
|
||||
<span>Paywall: <span class="pwrule" class:on={s._artSummary.paywalled}>{pwBasis(s._artSummary)}</span></span>
|
||||
<select class="pwsel" onchange={(e) => setPaywall(s, e.currentTarget.value)}>
|
||||
<option value="" selected={!s._artSummary.paywall_override}>Use domain rule ({s._artSummary.paywall_domain ? 'on' : 'off'})</option>
|
||||
<option value="free" selected={s._artSummary.paywall_override === 'free'}>Treat as free</option>
|
||||
<option value="paywalled" selected={s._artSummary.paywall_override === 'paywalled'}>Treat as paywalled</option>
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="artfilters">
|
||||
@@ -1259,8 +1283,10 @@
|
||||
/* Source article inspector */
|
||||
.srctable tr.artrow td { background: var(--bg); font-size: 0.84rem; padding: 10px 12px; }
|
||||
.artsum { color: var(--ink); margin-bottom: 8px; }
|
||||
.artsum .pwrule { color: var(--muted); font-weight: 600; }
|
||||
.artsum .pwrule.on { color: #9a3b3b; }
|
||||
.pwrule { color: var(--muted); font-weight: 600; }
|
||||
.pwrule.on { color: #9a3b3b; }
|
||||
.pwctl { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; font-size: 0.8rem; color: var(--muted); }
|
||||
.pwsel { font: inherit; font-size: 0.78rem; padding: 3px 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--surface); color: var(--ink); }
|
||||
.artfilters { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-bottom: 8px; }
|
||||
.chip.sm { font-size: 0.74rem; padding: 3px 10px; }
|
||||
.artlist { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 7px; max-height: 360px; overflow-y: auto; }
|
||||
|
||||
Reference in New Issue
Block a user