Files
signal-platform/frontend/src/components/admin/ScheduleSettings.tsx
T
dennisthiessenandClaude Opus 5 083c9dbf7c refactor(jobs): derive job topology from one catalog, make next-run coherent
Groundwork for the Admin -> Jobs cleanup. Three sources of truth collapse into
app/job_catalog.py, which imports nothing from app so both the scheduler and
admin_service can import it at module level (admin_service otherwise has to
import the scheduler inside functions to dodge a cycle).

PIPELINE_MEMBERS is now DERIVED from the four pipeline step lists instead of
being a literal set in admin_service duplicating four lists in scheduler.py with
nothing asserting they agreed. A test pins that the derivation reproduces the
previous hand-maintained 9 names exactly, so this is behaviour-preserving.

Deletes the private _JOB_NAMES list, which held 16 of the 19 jobs:
benchmark_collector, outcome_evaluator and shadow_book had no runtime row, and
so no "last run" line in the panel, until their first run in a given process.
_job_runtime is now seeded from the catalog, and a test pins the invariant.

Next-run is decided by category rather than by reading a timestamp. A pipeline
step has no schedule of its own, so it reports its parent's ("next via Morning
Pipeline in 3h") instead of nothing; a manual job says manual_only rather than
rendering a date. This also fixes a real bug: triggering a paused job set
next_run_time=now, APScheduler re-armed the 520-week backstop behind it, and the
panel displayed "next run in ~87600h". Two independent guards -- the category
rule, plus _visible_next_run dropping anything past a year -- and an APScheduler
listener that re-pauses steps and manual jobs once their run finishes. The
listener is registered at module level because configure_scheduler is called
more than once and add_listener does not deduplicate.

Migrates backtest and ticker_universe_sync from interval to cron (Sun 03:00 ET
and 01:00 ET). configure_scheduler calls remove_all_jobs() on every startup, so
an interval countdown restarts each deploy -- a 168h backtest needed a week of
uninterrupted uptime to fire even once. The codebase already documented this
pitfall as the reason cron was adopted; these two were never migrated. Both are
now editable in Admin -> Schedule.

Also: list_jobs went from one settings query per job (19) to one for all of
them, and data_backfill is hidden from the listing while staying registered and
API-triggerable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 12:08:01 +02:00

136 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import type { ScheduleConfig } from '../../lib/types';
import { useScheduleSettings, useUpdateScheduleSettings } from '../../hooks/useAdmin';
import { SkeletonTable } from '../ui/Skeleton';
const DEFAULTS: ScheduleConfig = {
schedule_timezone: 'America/New_York',
schedule_daily_pipeline_cron: '0 2 * * *',
schedule_dolt_earnings_cron: '30 2 * * *',
schedule_sec_fundamentals_cron: '0 4 * * *',
schedule_near_close_pipeline_cron: '30 15 * * mon-fri',
schedule_after_close_pipeline_cron: '45 16 * * mon-fri',
schedule_intraday_pipeline_cron: '0 10-15 * * mon-fri',
schedule_backtest_cron: '0 3 * * sun',
schedule_ticker_universe_cron: '0 1 * * *',
};
const FIELDS: { key: keyof ScheduleConfig; label: string; hint: string; mono?: boolean }[] = [
{
key: 'schedule_timezone',
label: 'Timezone',
hint: 'IANA name. Prefer America/New_York so the near-close scan tracks the US cash close through DST.',
},
{
key: 'schedule_daily_pipeline_cron',
label: 'Morning pipeline',
hint: 'OHLCV → benchmark → sentiment → trend/risk → alerts (no R:R scan). Default 02:00 ET so risk-quadrant changes hit Telegram in the morning.',
mono: true,
},
{
key: 'schedule_dolt_earnings_cron',
label: 'Dolt earnings',
hint: 'Pull and import earnings dates/results daily at 02:30 ET. The fundamentals cache refresh uses these local events.',
mono: true,
},
{
key: 'schedule_sec_fundamentals_cron',
label: 'SEC fundamentals',
hint: 'Import tracked-universe SEC facts daily at 04:00 ET, then refresh the fundamentals cache scoring reads. Disabling the job stops the SEC fetch only — the local cache refresh still runs.',
mono: true,
},
{
key: 'schedule_near_close_pipeline_cron',
label: 'Near-close pipeline (scan + alert)',
hint: 'OHLCV fetch → R:R scan → Telegram. Default 15:30 ET MonFri so manual MOC fills can still hit ~15:50/15:55.',
mono: true,
},
{
key: 'schedule_after_close_pipeline_cron',
label: 'After-close pipeline (outcome)',
hint: 'OHLCV fetch (final bar) → outcome eval. Default 16:45 ET MonFri — not chained to the partial near-close bar.',
mono: true,
},
{
key: 'schedule_intraday_pipeline_cron',
label: 'Intraday pipeline (light)',
hint: 'Refresh prices + resolve outcomes mid-session. Default hourly 10:0015:00 ET weekdays.',
mono: true,
},
{
key: 'schedule_backtest_cron',
label: 'Backtest',
hint: 'Replay history and refresh the Track Record report. Default Sunday 03:00 ET. Was a 168h interval, which restarted on every deploy and so could defer indefinitely.',
mono: true,
},
{
key: 'schedule_ticker_universe_cron',
label: 'Ticker universe sync',
hint: 'Refresh the tracked-symbol universe. Default 01:00 ET daily, before the morning pipeline.',
mono: true,
},
];
export function ScheduleSettings() {
const { data, isLoading, isError, error } = useScheduleSettings();
const update = useUpdateScheduleSettings();
const [form, setForm] = useState<ScheduleConfig>(DEFAULTS);
useEffect(() => {
if (data) setForm({ ...DEFAULTS, ...data });
}, [data]);
if (isLoading) return <SkeletonTable rows={2} cols={2} />;
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load schedule'}</p>;
return (
<div className="glass p-5 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-200">Pipeline Schedule</h3>
<p className="mt-1 text-xs text-gray-500">
When the jobs run, as 5-field cron (<span className="num">min hour day month weekday</span>).
Saved changes apply to the running scheduler immediately no redeploy.
One qualifying R:R scan per day is the near-close job; alerts fire immediately after that scan.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{FIELDS.map((f) => (
<label key={f.key} className="block space-y-1">
<span className="text-xs text-gray-400">{f.label}</span>
<input
type="text"
value={form[f.key] ?? ''}
spellCheck={false}
onChange={(e) => setForm((prev) => ({ ...prev, [f.key]: e.target.value }))}
className={`w-full input-glass px-3 py-2 text-sm ${f.mono ? 'num' : ''}`}
/>
<span className="block text-[11px] text-gray-600">{f.hint}</span>
</label>
))}
</div>
<div className="flex items-center gap-2">
<button
className="btn-primary px-4 py-2 text-sm"
onClick={() => update.mutate(form)}
disabled={update.isPending}
>
{update.isPending ? 'Saving…' : 'Save Schedule'}
</button>
<button
className="px-4 py-2 text-sm rounded border border-white/[0.1] text-gray-300 hover:text-white"
onClick={() => {
setForm(DEFAULTS);
update.mutate(DEFAULTS);
}}
disabled={update.isPending}
>
Reset to Defaults
</button>
</div>
</div>
);
}