mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-03 21:08:13 +02:00
0fee36e8f7
Wikimedia's User-Agent policy asks API clients to identify themselves with a stable, contactable identifier so their operators can rate-limit or coordinate. Before this change, ShadowBroker was sending: - Backend (region_dossier.py): generic project default UA only; no Api-User-Agent. - Frontend (useRegionDossier.ts, WikiImage.tsx, NewsFeed.tsx): zero identifying header at all; three separate copy-pasted anonymous fetches with their own module-local caches. Three separate components doing the same broken thing meant policy fixes had to happen in three places, with no shared cache or kill switch. Fix (no UX change, zero hostility): == Backend == `backend/services/region_dossier.py` now sets explicit `User-Agent` + `Api-User-Agent` headers on every outbound Wikidata and Wikipedia request via a new `_WIKIMEDIA_REQUEST_HEADERS` constant. The identifier includes a contact path (issues page on the public GitHub repo). == Frontend == New shared helper `frontend/src/lib/wikimediaClient.ts`: - `fetchWikipediaSummary(title)` — single source of truth for Wikipedia REST summary lookups, with one shared LRU cache (in-flight requests deduplicated, 512-entry cap), `Api-User-Agent` on every fetch. - `fetchWikidataSparql(query)` — same shape for Wikidata SPARQL. - `WIKIMEDIA_API_USER_AGENT` — exported constant; one place to update if Wikimedia ever asks us to back off. Refactored three components to use the shared client: - `frontend/src/hooks/useRegionDossier.ts` — fetchLeader() and fetchLocalWikiSummary() now route through the shared helpers. - `frontend/src/components/WikiImage.tsx` — uses fetchWikipediaSummary, proper React state instead of module-mutation + forceUpdate trick. - `frontend/src/components/NewsFeed.tsx` — same shape. UX: byte-for-byte identical. Same thumbnails, same dossier content, same load behavior. The only observable difference is the outgoing request header. Note on #239 (route duplication): an audit-grade inventory shows 166 main.py routes are shadowed by router modules. That cleanup is too large to land safely in this PR; it will be staged as a separate ladder of small PRs grouped by router module. Tests: - `backend/tests/test_region_dossier_wikimedia_ua.py` — 3 tests asserting backend headers are present. - `frontend/src/__tests__/utils/wikimediaClient.test.ts` — 9 tests covering Api-User-Agent presence, shared cache, concurrent deduplication, disambiguation/HTTP-error/network-error fallthroughs, empty-input safety. Local: backend 76/76 security suite green, frontend 716/716 vitest suite green. Credit: tg12 (external security audit).
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
'use client';
|
|
import React, { useState, useEffect } from 'react';
|
|
import ExternalImage from '@/components/ExternalImage';
|
|
import { fetchWikipediaSummary } from '@/lib/wikimediaClient';
|
|
|
|
/**
|
|
* WikiImage — displays a Wikipedia thumbnail for a given article URL.
|
|
*
|
|
* Issue #220 (tg12): this component previously had its own
|
|
* module-local Wikipedia fetch + cache. It now delegates to
|
|
* `lib/wikimediaClient`, which sends the policy-compliant
|
|
* `Api-User-Agent` header and shares one cache across every UI
|
|
* component that asks Wikipedia for an article summary (WikiImage,
|
|
* NewsFeed, useRegionDossier).
|
|
*
|
|
* Props:
|
|
* wikiUrl: Full Wikipedia URL, e.g. "https://en.wikipedia.org/wiki/Boeing_787_Dreamliner"
|
|
* label: Alt text / label for the image link
|
|
* maxH: Max height class (default "max-h-32")
|
|
* accent: Border hover color class (default "hover:border-cyan-500/50")
|
|
*/
|
|
export default function WikiImage({
|
|
wikiUrl,
|
|
label,
|
|
maxH = 'max-h-52',
|
|
accent = 'hover:border-cyan-500/50',
|
|
}: {
|
|
wikiUrl: string;
|
|
label?: string;
|
|
maxH?: string;
|
|
accent?: string;
|
|
}) {
|
|
const [imgUrl, setImgUrl] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// Extract article title from URL
|
|
const title = wikiUrl.replace(/^https?:\/\/[^/]+\/wiki\//, '');
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
if (!title) {
|
|
setImgUrl(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
fetchWikipediaSummary(title).then((summary) => {
|
|
if (cancelled) return;
|
|
setImgUrl(summary?.thumbnail || null);
|
|
setLoading(false);
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [title]);
|
|
|
|
return (
|
|
<div className="pb-2">
|
|
{loading && (
|
|
<div className={`w-full h-20 rounded bg-[var(--bg-tertiary)]/60 animate-pulse`} />
|
|
)}
|
|
{imgUrl && (
|
|
<a href={wikiUrl} target="_blank" rel="noopener noreferrer" className="block">
|
|
<ExternalImage
|
|
src={imgUrl}
|
|
alt={label || title.replace(/_/g, ' ')}
|
|
width={640}
|
|
height={360}
|
|
className={`w-full h-auto ${maxH} object-contain rounded border border-[var(--border-primary)]/50 ${accent} transition-colors`}
|
|
style={{ width: '100%', height: 'auto' }}
|
|
/>
|
|
</a>
|
|
)}
|
|
<a
|
|
href={wikiUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-[10px] text-cyan-400 hover:text-cyan-300 underline mt-1 inline-block font-mono"
|
|
>
|
|
📖 {label || title.replace(/_/g, ' ')} — Wikipedia →
|
|
</a>
|
|
</div>
|
|
);
|
|
}
|