feat: calendar eco
This commit is contained in:
@@ -165,6 +165,7 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
|
||||
const [calStatus, setCalStatus] = useState<CalSyncStatus>({ running: false, last_run: null, last_result: null, source: null, next_run: null })
|
||||
const [syncMsg, setSyncMsg] = useState<string | null>(null)
|
||||
const [showSyncDebug, setShowSyncDebug] = useState(false)
|
||||
const calPollRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const [ffPageMsg, setFfPageMsg] = useState<string | null>(null)
|
||||
@@ -211,10 +212,18 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
if (r.error) {
|
||||
setSyncMsg(`Erreur: ${r.error}`)
|
||||
} else {
|
||||
const tw = r.live?.thisweek?.events ?? r.live?.events ?? '?'
|
||||
const nw = r.live?.nextweek?.events ?? '?'
|
||||
const twStatus = r.live?.thisweek?.status
|
||||
const nwStatus = r.live?.nextweek?.status
|
||||
const tw = twStatus === 'ok' ? r.live.thisweek.events : (r.live?.thisweek?.error ? `err` : (twStatus ?? '?'))
|
||||
const nw = nwStatus === 'ok' ? r.live.nextweek.events : (r.live?.nextweek?.error ? `err` : (nwStatus ?? '?'))
|
||||
const sc = r.scrape?.total_upserted ?? 0
|
||||
setSyncMsg(`✓ FF: ${tw}+${nw} live, ${sc} upcoming`)
|
||||
let msg = `✓ FF: ${tw}+${nw} live, ${sc} upcoming`
|
||||
if (r.fmp_fallback) {
|
||||
if (r.fmp_fallback.skipped === 'no_key') msg += ' · FMP fallback skipped (no key)'
|
||||
else if (r.fmp_fallback.error) msg += ` · FMP error: ${r.fmp_fallback.error}`
|
||||
else msg += ` · FMP +${r.fmp_fallback.total_upserted ?? 0}`
|
||||
}
|
||||
setSyncMsg(msg)
|
||||
}
|
||||
onImported()
|
||||
}
|
||||
@@ -250,7 +259,7 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
|
||||
const hasData = stats.total > 0
|
||||
const res = importStatus.last_result
|
||||
const srcLabel = calStatus.source === 'ff' ? 'ForexFactory' : calStatus.source ?? null
|
||||
const srcLabel = calStatus.source === 'ff' ? 'ForexFactory' : calStatus.source === 'ff+fmp' ? 'ForexFactory+FMP' : calStatus.source ?? null
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-4 mb-4 flex flex-wrap items-center gap-4 text-sm">
|
||||
@@ -277,7 +286,7 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
onClick={startCalendarSync}
|
||||
disabled={calStatus.running}
|
||||
className="px-3 py-1.5 rounded bg-indigo-700 hover:bg-indigo-600 disabled:opacity-50 flex items-center gap-1.5 text-white"
|
||||
title="Sync economic calendar (FXStreet primary, FF fallback if blocked)"
|
||||
title="Sync economic calendar (FF live JSON + HTML scraper; FMP fallback for weeks 3+ if FF scraper is blocked)"
|
||||
>
|
||||
<RefreshCw size={13} className={clsx(calStatus.running && 'animate-spin')} />
|
||||
{calStatus.running ? 'Syncing…' : 'Sync Now'}
|
||||
@@ -316,6 +325,16 @@ function ImportPanel({ stats, onImported }: { stats: FFStats; onImported: () =>
|
||||
{syncMsg}
|
||||
</span>
|
||||
)}
|
||||
{calStatus.last_result && (
|
||||
<button onClick={() => setShowSyncDebug(v => !v)} className="text-[10px] text-slate-500 hover:text-slate-300 underline">
|
||||
{showSyncDebug ? 'hide details' : 'details'}
|
||||
</button>
|
||||
)}
|
||||
{showSyncDebug && calStatus.last_result && (
|
||||
<pre className="basis-full text-[10px] text-slate-400 bg-dark-900 border border-slate-700/50 rounded p-2 overflow-x-auto max-h-64 overflow-y-auto">
|
||||
{JSON.stringify(calStatus.last_result, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
{ffPageMsg && (
|
||||
<span className={clsx('text-xs', ffPageMsg.startsWith('✓') ? 'text-emerald-400' : ffPageMsg.startsWith('Error') ? 'text-red-400' : 'text-yellow-400')}>
|
||||
{ffPageMsg}
|
||||
|
||||
@@ -1641,13 +1641,20 @@ export default function Config() {
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org' },
|
||||
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov' },
|
||||
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org' },
|
||||
{ label: 'FMP API Key', value: fmpKey, setter: setFmpKey, placeholder: 'Calendar fallback — financialmodelingprep.com' },
|
||||
].map(({ label, value, setter, placeholder }) => (
|
||||
{ label: 'NewsAPI Key', value: newsapiKey, setter: setNewsapiKey, placeholder: 'Get at newsapi.org', configKey: 'newsapi_key' },
|
||||
{ label: 'EIA API Key', value: eiaKey, setter: setEiaKey, placeholder: 'Get at eia.gov', configKey: 'eia_api_key' },
|
||||
{ label: 'FRED API Key', value: fredKey, setter: setFredKey, placeholder: 'Get at fred.stlouisfed.org', configKey: 'fred_api_key' },
|
||||
{ label: 'FMP API Key', value: fmpKey, setter: setFmpKey, placeholder: 'Calendar fallback — financialmodelingprep.com', configKey: 'fmp_api_key' },
|
||||
].map(({ label, value, setter, placeholder, configKey }) => (
|
||||
<div key={label}>
|
||||
<label className="text-xs text-slate-500 mb-1 block">{label}</label>
|
||||
<label className="text-xs text-slate-500 mb-1 flex items-center justify-between">
|
||||
<span>{label}</span>
|
||||
{config?.[`${configKey}_set`] ? (
|
||||
<span className="text-emerald-400 text-[10px] font-semibold">✓ active</span>
|
||||
) : (
|
||||
<span className="text-slate-600 text-[10px]">not set</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
type={showKeys ? 'text' : 'password'}
|
||||
value={value}
|
||||
|
||||
Reference in New Issue
Block a user