fix: use FRED official JSON API (bypasses CloudFlare) + API key UI
- fred_bootstrap.py: switch from CSV graph endpoint (CloudFlare-blocked) to api.stlouisfed.org/fred/series/observations JSON API; reads key from DB config 'fred_api_key'; returns clear error if key missing - eco.py: add GET/POST /api/eco/fred-key to check/save the FRED API key - CalendarPage.tsx: BootstrapPanel shows API key section with status, input to paste key + save button, disables Launch if key missing Free key: fred.stlouisfed.org -> My Account -> API Keys Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -142,11 +142,37 @@ function BootstrapPanel({ onDone }: { onDone: () => void }) {
|
||||
const [force, setForce] = useState(false)
|
||||
const [bsStatus, setBsStatus] = useState<BootstrapStatus>({ running: false, last_result: null })
|
||||
const [polling, setPolling] = useState(false)
|
||||
const [apiKey, setApiKey] = useState('')
|
||||
const [keyConfigured, setKeyConfigured] = useState<boolean | null>(null)
|
||||
const [keyPreview, setKeyPreview] = useState('')
|
||||
const [savingKey, setSavingKey] = useState(false)
|
||||
const onDoneRef = useRef(onDone)
|
||||
useEffect(() => { onDoneRef.current = onDone }, [onDone])
|
||||
|
||||
// Check if FRED key is already saved
|
||||
useEffect(() => {
|
||||
fetch('/api/eco/fred-key')
|
||||
.then(r => r.json())
|
||||
.then(d => { setKeyConfigured(d.configured); setKeyPreview(d.preview) })
|
||||
.catch(() => setKeyConfigured(false))
|
||||
}, [])
|
||||
|
||||
const saveKey = async () => {
|
||||
if (!apiKey.trim()) return
|
||||
setSavingKey(true)
|
||||
try {
|
||||
const r = await fetch(`/api/eco/fred-key?key=${encodeURIComponent(apiKey.trim())}`, { method: 'POST' })
|
||||
const d = await r.json()
|
||||
setKeyConfigured(true)
|
||||
setKeyPreview(d.preview)
|
||||
setApiKey('')
|
||||
} finally {
|
||||
setSavingKey(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startBootstrap = async () => {
|
||||
const url = `${API_BASE}/api/eco/bootstrap?from_date=${fromDate}&force=${force}`
|
||||
const url = `/api/eco/bootstrap?from_date=${fromDate}&force=${force}`
|
||||
await fetch(url, { method: 'POST' })
|
||||
setPolling(true)
|
||||
setBsStatus(s => ({ ...s, running: true }))
|
||||
@@ -155,7 +181,7 @@ function BootstrapPanel({ onDone }: { onDone: () => void }) {
|
||||
useEffect(() => {
|
||||
if (!polling) return
|
||||
const iv = setInterval(async () => {
|
||||
const r = await fetch(`${API_BASE}/api/eco/bootstrap/status`)
|
||||
const r = await fetch(`/api/eco/bootstrap/status`)
|
||||
const data: BootstrapStatus = await r.json()
|
||||
setBsStatus(data)
|
||||
if (!data.running) {
|
||||
@@ -168,7 +194,9 @@ function BootstrapPanel({ onDone }: { onDone: () => void }) {
|
||||
|
||||
const rawResult = bsStatus.last_result as Record<string, unknown> | null
|
||||
const bootstrapError: string | null =
|
||||
rawResult && typeof (rawResult as { error?: unknown }).error === 'string'
|
||||
rawResult && typeof (rawResult as { _error?: unknown })._error === 'string'
|
||||
? (rawResult as { _error: string })._error
|
||||
: rawResult && typeof (rawResult as { error?: unknown }).error === 'string'
|
||||
? (rawResult as { error: string }).error
|
||||
: null
|
||||
const bootstrapEntries: [string, BootstrapSeriesResult][] =
|
||||
@@ -181,9 +209,46 @@ function BootstrapPanel({ onDone }: { onDone: () => void }) {
|
||||
<div className="text-xs font-semibold text-blue-400 flex items-center gap-1">
|
||||
<Download className="w-3 h-3" /> Bootstrap FRED
|
||||
</div>
|
||||
<div className="text-xs text-slate-400">
|
||||
Télécharge les données FRED (endpoint CSV public, sans clé API) depuis la date choisie.
|
||||
Séries : NFP, Chômage, CPI, Core CPI, Core PCE, FOMC, Jobless Claims, GDP, HY Spread, T10Y2Y, T10Y3M.
|
||||
|
||||
{/* API Key section */}
|
||||
<div className="bg-dark-600 rounded border border-slate-700/50 p-3 space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-300">Clé API FRED</span>
|
||||
{keyConfigured === true && (
|
||||
<span className="text-xs text-emerald-400 font-mono">{keyPreview} ✓</span>
|
||||
)}
|
||||
{keyConfigured === false && (
|
||||
<span className="text-xs text-orange-400">Non configurée</span>
|
||||
)}
|
||||
</div>
|
||||
{keyConfigured === false && (
|
||||
<div className="text-xs text-slate-500">
|
||||
L'endpoint CSV public est bloqué par CloudFlare sur les IPs serveur.
|
||||
L'API officielle FRED nécessite une clé gratuite :{' '}
|
||||
<span className="text-blue-400 font-mono">fred.stlouisfed.org → My Account → API Keys</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={apiKey}
|
||||
onChange={e => setApiKey(e.target.value)}
|
||||
placeholder={keyConfigured ? 'Nouvelle clé (optionnel)' : 'Colle ta clé FRED ici…'}
|
||||
className="flex-1 text-xs bg-dark-700 border border-slate-700 rounded px-2 py-1 text-white font-mono placeholder:text-slate-600"
|
||||
onKeyDown={e => e.key === 'Enter' && saveKey()}
|
||||
/>
|
||||
<button
|
||||
onClick={saveKey}
|
||||
disabled={!apiKey.trim() || savingKey}
|
||||
className="text-xs px-3 py-1 rounded bg-emerald-700 hover:bg-emerald-600 text-white font-semibold disabled:opacity-40"
|
||||
>
|
||||
{savingKey ? '…' : 'Sauver'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-500">
|
||||
Séries : NFP · Chômage · CPI · Core CPI · Core PCE · FOMC · Jobless Claims · GDP · HY Spread · T10Y2Y · T10Y3M
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
@@ -200,11 +265,14 @@ function BootstrapPanel({ onDone }: { onDone: () => void }) {
|
||||
</label>
|
||||
<button
|
||||
onClick={startBootstrap}
|
||||
disabled={bsStatus.running}
|
||||
disabled={bsStatus.running || keyConfigured === false}
|
||||
title={keyConfigured === false ? 'Configure ta clé FRED d\'abord' : ''}
|
||||
className={clsx(
|
||||
'mt-4 px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
bsStatus.running
|
||||
? 'bg-blue-900/40 text-blue-400 cursor-wait'
|
||||
: keyConfigured === false
|
||||
? 'bg-slate-700 text-slate-500 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-500 text-white',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user