Files
signal-platform/frontend/src/components/admin/AlertSettings.tsx
T
dennisthiessenandClaude Opus 5 5ea0785be6 refactor(ui): name the two regime jobs for what they actually do
"Market Regime" and "Regime Monitor" sat next to each other in Admin -> Jobs
(pipeline steps 4 and 5) reading as the same job. They are unrelated, and the
names had it backwards: "Market Regime" is the SPY 50/200 guard that drives the
TopBar trend dot and the counter-trend warning on setups, so it changes what a
setup shows; "Regime Monitor" is the observational AI/Tech thermometer that
explicitly feeds no trades. The more consequential job had the vaguer name.

  market_regime   "Market Regime"   -> "Market Trend (SPY)"
  regime_monitor  "Regime Monitor"  -> "AI/Tech Risk Monitor"

Display strings only. The job *ids* are persisted -- they key the pipeline step
list, cron config, runtime tracking and run history -- so they are untouched,
as is the /regime route, which keeps existing links working.

The label the admin UI renders comes from JOB_LABELS in admin_service (via
routers/jobs.py), not from the scheduler's APScheduler `name=`. Both are updated;
only the former is user-visible.

Carries the vocabulary through the rest of the surface so it does not half-land:
page title, nav ("Regime" -> "Risk"), the empty-state instruction that names the
job to run, the quadrant alert toggle, the morning-pipeline hint, and the
Telegram alert headline ("Regime quadrant change" -> "AI/Tech risk quadrant
change"). No test asserts any of these strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:48:15 +02:00

187 lines
7.0 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 { useAlertSettings, useUpdateAlertSettings, useTestAlert } from '../../hooks/useAdmin';
import { SkeletonTable } from '../ui/Skeleton';
const SOURCE_LABEL: Record<string, string> = {
database: 'configured here',
environment: 'from environment (.env)',
none: 'not configured',
};
type TriggerKey =
| 'qualified_enabled'
| 'sr_proximity_enabled'
| 'score_drop_enabled'
| 'digest_enabled'
| 'regime_quadrant_enabled'
| 'trade_closed_enabled';
const TRIGGERS: { key: TriggerKey; label: string; hint: string }[] = [
{ key: 'qualified_enabled', label: 'Qualified setups', hint: 'a setup newly clears the activation gate' },
{ key: 'sr_proximity_enabled', label: 'Watchlist S/R proximity', hint: 'a watched ticker nears a strong support/resistance' },
{ key: 'score_drop_enabled', label: 'Score deterioration', hint: 'a watched tickers composite drops sharply' },
{ key: 'digest_enabled', label: 'Daily digest', hint: 'end-of-day summary incl. open trades + trailing stops' },
{ key: 'regime_quadrant_enabled', label: 'Risk quadrant change', hint: 'the AI/Tech risk monitor shifts quadrant (hysteresis + cooldown)' },
{ key: 'trade_closed_enabled', label: 'Trade closed', hint: 'a paper trade auto-closes (trailing/target/stop) — incl. losses' },
];
function Toggle({ checked, onChange, label, hint }: {
checked: boolean;
onChange: (v: boolean) => void;
label: string;
hint: string;
}) {
return (
<label className="flex items-start gap-2.5 cursor-pointer">
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="mt-0.5 h-4 w-4 cursor-pointer accent-blue-400"
/>
<span>
<span className="text-sm text-gray-200">{label}</span>
<span className="block text-[11px] text-gray-500">{hint}</span>
</span>
</label>
);
}
export function AlertSettings() {
const { data, isLoading, isError, error } = useAlertSettings();
const update = useUpdateAlertSettings();
const test = useTestAlert();
const [enabled, setEnabled] = useState(false);
const [chatId, setChatId] = useState('');
const [botToken, setBotToken] = useState('');
const [triggers, setTriggers] = useState<Record<TriggerKey, boolean>>({
qualified_enabled: true,
sr_proximity_enabled: true,
score_drop_enabled: true,
digest_enabled: true,
regime_quadrant_enabled: true,
trade_closed_enabled: true,
});
useEffect(() => {
if (data) {
setEnabled(data.enabled);
setChatId(data.telegram_chat_id ?? '');
setTriggers({
qualified_enabled: data.qualified_enabled,
sr_proximity_enabled: data.sr_proximity_enabled,
score_drop_enabled: data.score_drop_enabled,
digest_enabled: data.digest_enabled,
regime_quadrant_enabled: data.regime_quadrant_enabled,
trade_closed_enabled: data.trade_closed_enabled,
});
}
}, [data]);
if (isLoading) return <SkeletonTable rows={4} cols={2} />;
if (isError) return <p className="text-sm text-red-400">{(error as Error)?.message || 'Failed to load alert settings'}</p>;
if (!data) return null;
const onSave = () => {
update.mutate({
enabled,
telegram_chat_id: chatId,
...triggers,
...(botToken ? { bot_token: botToken } : {}),
});
setBotToken('');
};
const tokenConfigured = data.bot_token_configured;
return (
<div className="glass p-5 space-y-4">
<div>
<h3 className="text-sm font-semibold text-gray-200">Telegram Alerts</h3>
<p className="mt-1 text-xs text-gray-500">
Push actionable signals to Telegram so you dont have to keep checking the dashboard.
The dispatcher runs hourly; each trigger respects a cooldown so youre not spammed.
</p>
</div>
<label className="flex items-center gap-2.5 cursor-pointer">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
className="h-4 w-4 cursor-pointer accent-blue-400"
/>
<span className="text-sm text-gray-200">Alerts enabled</span>
</label>
<div className="grid gap-4 md:grid-cols-2">
<label className="block space-y-1">
<span className="text-xs text-gray-400">Bot Token</span>
<input
type="password"
value={botToken}
onChange={(e) => setBotToken(e.target.value)}
autoComplete="new-password"
placeholder={tokenConfigured ? '•••••••••• (leave blank to keep current)' : 'Paste bot token from @BotFather…'}
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-500">
Status:{' '}
<span className={tokenConfigured ? 'text-emerald-400' : 'text-amber-400'}>
{SOURCE_LABEL[data.bot_token_source] ?? data.bot_token_source}
</span>
{' '}· write-only, never displayed
</span>
</label>
<label className="block space-y-1">
<span className="text-xs text-gray-400">Chat ID</span>
<input
type="text"
value={chatId}
onChange={(e) => setChatId(e.target.value)}
placeholder="e.g. 123456789"
className="w-full input-glass px-3 py-2 text-sm"
/>
<span className="text-[11px] text-gray-600">Your numeric chat ID (message @userinfobot to find it).</span>
</label>
</div>
<div className="space-y-2">
<p className="text-xs text-gray-400">Triggers</p>
<div className="grid gap-3 md:grid-cols-2">
{TRIGGERS.map((t) => (
<Toggle
key={t.key}
label={t.label}
hint={t.hint}
checked={triggers[t.key]}
onChange={(v) => setTriggers((prev) => ({ ...prev, [t.key]: v }))}
/>
))}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<button className="btn-primary px-4 py-2 text-sm" onClick={onSave} disabled={update.isPending}>
{update.isPending ? 'Saving…' : 'Save Alerts'}
</button>
<button
className="px-4 py-2 text-sm rounded border border-white/[0.1] text-gray-300 hover:text-white disabled:opacity-50"
onClick={() => test.mutate()}
disabled={test.isPending}
>
{test.isPending ? 'Sending…' : 'Send Test'}
</button>
<span className="text-[11px] text-gray-500">Save first, then Send Test to verify the bot reaches you.</span>
</div>
<div className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-4 py-2.5 text-[11px] text-gray-500">
Setup: 1) message <span className="text-gray-300">@BotFather</span>, send <span className="text-gray-300">/newbot</span>, copy the token.
2) send your new bot any message. 3) get your chat ID from <span className="text-gray-300">@userinfobot</span>. Paste both above.
</div>
</div>
);
}