Fix sidebar username, Signals filter clarity and layout
- JWT now carries a username claim; sidebar shows "Signed in as <name>" instead of the bare user id (sub). Re-login required for the new claim. - Signals: Min R:R / Min Confidence inputs reflect the effective filter — auto-filled from the activation gate when "Qualified only" is on, reset to 0 when off (no more misleading 0 while the gate is active). - Signals layout: Run Scanner moved to its own action row (it's a job trigger, not a filter); qualified toggle grouped with the refinement filters under one Filters panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -70,7 +70,7 @@ export default function Sidebar() {
|
||||
</span>
|
||||
</div>
|
||||
{username && (
|
||||
<p className="text-xs text-gray-500 truncate px-1">{username}</p>
|
||||
<p className="text-xs text-gray-500 truncate px-1">Signed in as {username}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={logout}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useActivation } from '../../hooks/useActivation';
|
||||
import { useTrades } from '../../hooks/useTrades';
|
||||
@@ -101,8 +101,8 @@ export function SetupsPanel() {
|
||||
const queryClient = useQueryClient();
|
||||
const toast = useToast();
|
||||
|
||||
// "Qualified only" applies the admin activation gate; the manual filters
|
||||
// below refine within whatever is shown.
|
||||
// "Qualified only" applies the admin activation gate; the refinement filters
|
||||
// can raise the bar further.
|
||||
const [qualifiedOnly, setQualifiedOnly] = useState(true);
|
||||
const [minRR, setMinRR] = useState(0);
|
||||
const [minConfidence, setMinConfidence] = useState(0);
|
||||
@@ -111,6 +111,19 @@ export function SetupsPanel() {
|
||||
const [sortColumn, setSortColumn] = useState<SortColumn>('rr_ratio');
|
||||
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
|
||||
|
||||
// Keep the Min R:R / Min Confidence inputs showing the *effective* floor: when
|
||||
// qualified-only is on they reflect the activation gate (so they're never a
|
||||
// misleading 0); off, they reset to 0 (no minimum).
|
||||
useEffect(() => {
|
||||
if (qualifiedOnly && activation.data) {
|
||||
setMinRR(activation.data.min_rr);
|
||||
setMinConfidence(activation.data.min_confidence);
|
||||
} else if (!qualifiedOnly) {
|
||||
setMinRR(0);
|
||||
setMinConfidence(0);
|
||||
}
|
||||
}, [qualifiedOnly, activation.data]);
|
||||
|
||||
const scanMutation = useMutation({
|
||||
mutationFn: () => triggerJob('rr_scanner'),
|
||||
onSuccess: () => {
|
||||
@@ -142,9 +155,19 @@ export function SetupsPanel() {
|
||||
}, [trades, qualifiedOnly, activation.data, minRR, directionFilter, minConfidence, actionFilter, sortColumn, sortDirection]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Qualified gate toggle */}
|
||||
<div className="glass-sm flex flex-wrap items-center justify-between gap-3 px-4 py-3">
|
||||
<div className="space-y-4">
|
||||
{/* Action row — Run Scanner is a job trigger, kept apart from the filters */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-xs text-gray-500">
|
||||
Setups from the latest scan. Re-run to refresh against current prices.
|
||||
</p>
|
||||
<Button onClick={() => scanMutation.mutate()} loading={scanMutation.isPending}>
|
||||
{scanMutation.isPending ? 'Scanning…' : 'Run Scanner'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Filters — qualified gate on top, refinements below */}
|
||||
<div className="glass-sm space-y-4 p-4">
|
||||
<label className="flex cursor-pointer items-center gap-2.5 text-sm text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -159,70 +182,63 @@ export function SetupsPanel() {
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<span className="text-xs text-gray-500">Manual filters below refine within this.</span>
|
||||
</div>
|
||||
|
||||
{/* Filter toolbar */}
|
||||
<div className="glass-sm flex flex-wrap items-end gap-4 p-4">
|
||||
<Field label="Min Risk:Reward" htmlFor="min-rr">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm text-gray-400">1 :</span>
|
||||
<div className="flex flex-wrap items-end gap-4 border-t border-white/[0.06] pt-4">
|
||||
<Field label="Direction" htmlFor="direction">
|
||||
<Dropdown
|
||||
id="direction"
|
||||
value={directionFilter}
|
||||
onChange={(v) => setDirectionFilter(v as DirectionFilter)}
|
||||
className="w-32"
|
||||
options={[
|
||||
{ value: 'both', label: 'Both' },
|
||||
{ value: 'long', label: 'Long' },
|
||||
{ value: 'short', label: 'Short' },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Min Risk:Reward" htmlFor="min-rr">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm text-gray-400">1 :</span>
|
||||
<Input
|
||||
id="min-rr"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={minRR}
|
||||
onChange={(e) => setMinRR(Number(e.target.value) || 0)}
|
||||
className="w-20"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Min Confidence" htmlFor="min-confidence">
|
||||
<Input
|
||||
id="min-rr"
|
||||
id="min-confidence"
|
||||
type="number"
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={minRR}
|
||||
onChange={(e) => setMinRR(Number(e.target.value) || 0)}
|
||||
className="w-20"
|
||||
max={100}
|
||||
step={1}
|
||||
value={minConfidence}
|
||||
onChange={(e) => setMinConfidence(Number(e.target.value) || 0)}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
<Field label="Direction" htmlFor="direction">
|
||||
<Dropdown
|
||||
id="direction"
|
||||
value={directionFilter}
|
||||
onChange={(v) => setDirectionFilter(v as DirectionFilter)}
|
||||
className="w-32"
|
||||
options={[
|
||||
{ value: 'both', label: 'Both' },
|
||||
{ value: 'long', label: 'Long' },
|
||||
{ value: 'short', label: 'Short' },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Min Confidence" htmlFor="min-confidence">
|
||||
<Input
|
||||
id="min-confidence"
|
||||
type="number"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={minConfidence}
|
||||
onChange={(e) => setMinConfidence(Number(e.target.value) || 0)}
|
||||
className="w-24"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Recommended Action" htmlFor="action">
|
||||
<Dropdown
|
||||
id="action"
|
||||
value={actionFilter}
|
||||
onChange={(v) => setActionFilter(v as ActionFilter)}
|
||||
className="w-56"
|
||||
options={[
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'LONG_HIGH', label: RECOMMENDATION_ACTION_LABELS.LONG_HIGH },
|
||||
{ value: 'LONG_MODERATE', label: RECOMMENDATION_ACTION_LABELS.LONG_MODERATE },
|
||||
{ value: 'SHORT_HIGH', label: RECOMMENDATION_ACTION_LABELS.SHORT_HIGH },
|
||||
{ value: 'SHORT_MODERATE', label: RECOMMENDATION_ACTION_LABELS.SHORT_MODERATE },
|
||||
{ value: 'NEUTRAL', label: RECOMMENDATION_ACTION_LABELS.NEUTRAL },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<div className="ml-auto">
|
||||
<Button onClick={() => scanMutation.mutate()} loading={scanMutation.isPending}>
|
||||
{scanMutation.isPending ? 'Scanning…' : 'Run Scanner'}
|
||||
</Button>
|
||||
</Field>
|
||||
<Field label="Recommended Action" htmlFor="action">
|
||||
<Dropdown
|
||||
id="action"
|
||||
value={actionFilter}
|
||||
onChange={(v) => setActionFilter(v as ActionFilter)}
|
||||
className="w-56"
|
||||
options={[
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'LONG_HIGH', label: RECOMMENDATION_ACTION_LABELS.LONG_HIGH },
|
||||
{ value: 'LONG_MODERATE', label: RECOMMENDATION_ACTION_LABELS.LONG_MODERATE },
|
||||
{ value: 'SHORT_HIGH', label: RECOMMENDATION_ACTION_LABELS.SHORT_HIGH },
|
||||
{ value: 'SHORT_MODERATE', label: RECOMMENDATION_ACTION_LABELS.SHORT_MODERATE },
|
||||
{ value: 'NEUTRAL', label: RECOMMENDATION_ACTION_LABELS.NEUTRAL },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -60,8 +60,14 @@ function TargetTable({ setup }: { setup: TradeSetup }) {
|
||||
</thead>
|
||||
<tbody>
|
||||
{setup.targets.map((target) => (
|
||||
<tr key={`${setup.id}-${target.sr_level_id}-${target.price}`} className="border-b border-white/[0.04]">
|
||||
<td className="py-2 pr-3 text-gray-300">{target.classification}</td>
|
||||
<tr
|
||||
key={`${setup.id}-${target.sr_level_id}-${target.price}`}
|
||||
className={`border-b border-white/[0.04] ${target.is_primary ? 'bg-blue-400/10' : ''}`}
|
||||
>
|
||||
<td className="py-2 pr-3 text-gray-300">
|
||||
{target.is_primary && <span className="mr-1 text-blue-300">★</span>}
|
||||
{target.classification}
|
||||
</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPrice(target.price)}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{formatPercent((target.distance_from_entry / setup.entry_price) * 100)}</td>
|
||||
<td className="py-2 pr-3 font-mono text-gray-200">{target.rr_ratio.toFixed(2)}</td>
|
||||
|
||||
@@ -195,6 +195,7 @@ export interface TradeTarget {
|
||||
classification: 'Conservative' | 'Moderate' | 'Aggressive';
|
||||
sr_level_id: number;
|
||||
sr_strength: number;
|
||||
is_primary?: boolean;
|
||||
}
|
||||
|
||||
export interface RecommendationSummary {
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface AuthState {
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
function decodeJwtPayload(token: string): { sub?: string; role?: string } {
|
||||
function decodeJwtPayload(token: string): { sub?: string; username?: string; role?: string } {
|
||||
try {
|
||||
const base64 = token.split('.')[1];
|
||||
const json = atob(base64);
|
||||
@@ -23,7 +23,8 @@ export const useAuthStore = create<AuthState>()((set) => ({
|
||||
username: (() => {
|
||||
const t = localStorage.getItem('token');
|
||||
if (!t) return null;
|
||||
return decodeJwtPayload(t).sub ?? null;
|
||||
const p = decodeJwtPayload(t);
|
||||
return p.username ?? p.sub ?? null;
|
||||
})(),
|
||||
role: (() => {
|
||||
const t = localStorage.getItem('token');
|
||||
@@ -37,7 +38,7 @@ export const useAuthStore = create<AuthState>()((set) => ({
|
||||
localStorage.setItem('token', token);
|
||||
set({
|
||||
token,
|
||||
username: payload.sub ?? null,
|
||||
username: payload.username ?? payload.sub ?? null,
|
||||
role: payload.role === 'admin' ? 'admin' : 'user',
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user