Fix sidebar username, Signals filter clarity and layout
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s

- 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:
2026-06-14 12:11:39 +02:00
parent 33f6baca6b
commit 5a0e8c8258
11 changed files with 178 additions and 125 deletions
+1 -1
View File
@@ -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}
+81 -65
View File
@@ -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>
+1
View File
@@ -195,6 +195,7 @@ export interface TradeTarget {
classification: 'Conservative' | 'Moderate' | 'Aggressive';
sr_level_id: number;
sr_strength: number;
is_primary?: boolean;
}
export interface RecommendationSummary {
+4 -3
View File
@@ -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',
});
},