diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 02f62df..23c5864 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,4 +1,4 @@
-import { BrowserRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom'
+import { BrowserRouter, Routes, Route, useLocation, useNavigate, useParams, Navigate } from 'react-router-dom'
import { useEffect, ComponentType } from 'react'
import Sidebar from './components/layout/Sidebar'
import TabBar from './components/layout/TabBar'
@@ -96,18 +96,41 @@ function KeepAlivePage({ path, component: Component }: { path: string; component
)
}
-// ── NormalRoutes — only InstrumentDashboard (uses :id param) + redirects ─────
+// ── Instrument keep-alive tabs ────────────────────────────────────────────────
+
+// Registers the instrument tab on navigation (renders nothing itself)
+function InstrumentRoute() {
+ const { id = 'EURUSD' } = useParams<{ id: string }>()
+ const { openInstrument } = useTabs()
+ useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument])
+ return null
+}
+
+// Keeps one InstrumentDashboard mounted per open instrument tab
+function InstrumentKeepAlive({ id }: { id: string }) {
+ const location = useLocation()
+ const isActive = location.pathname.toLowerCase() === `/instruments/${id.toLowerCase()}`
+ return (
+
+
+
+ )
+}
+
+// ── NormalRoutes — legacy redirects + instrument tab registration ─────────────
function NormalRoutes() {
const location = useLocation()
if (KEEP_ALIVE_PATHS.has(location.pathname)) return null
+ // On instrument paths the keep-alive layer renders the UI; this Routes just registers the tab
+ const isInstrument = /^\/instruments\/\w/.test(location.pathname)
+
return (
-
+
- {/* InstrumentDashboard kept here: useParams() reads :id from URL */}
- } />
- } />
+ } />
+ } />
{/* Legacy redirects */}
} />
} />
@@ -125,23 +148,33 @@ function GlobalWatcher() {
// ── App ───────────────────────────────────────────────────────────────────────
+function AppInner() {
+ const { instrumentIds } = useTabs()
+ return (
+
+
+
+
+
+
+ {KEEP_ALIVE_DEFS.map(def => (
+
+ ))}
+ {instrumentIds.map(id => (
+
+ ))}
+
+
+
+
+ )
+}
+
export default function App() {
return (
-
-
-
-
-
-
- {KEEP_ALIVE_DEFS.map(def => (
-
- ))}
-
-
-
-
+
)
diff --git a/frontend/src/components/layout/TabBar.tsx b/frontend/src/components/layout/TabBar.tsx
index 3b290cf..c91d6d7 100644
--- a/frontend/src/components/layout/TabBar.tsx
+++ b/frontend/src/components/layout/TabBar.tsx
@@ -1,22 +1,27 @@
import clsx from 'clsx'
-import { X, ExternalLink } from 'lucide-react'
+import { X, ExternalLink, TrendingUp } from 'lucide-react'
import { useLocation, useNavigate } from 'react-router-dom'
import { useTabs } from '../../context/TabsContext'
import { KEEP_ALIVE_META } from '../../config/keepAliveMeta'
export default function TabBar() {
- const { kept, forgetRoute } = useTabs()
+ const { kept, forgetRoute, instrumentIds, closeInstrument } = useTabs()
const location = useLocation()
const navigate = useNavigate()
- const visible = KEEP_ALIVE_META.filter(m => kept.has(m.path))
- if (visible.length === 0) return null
+ const staticVisible = KEEP_ALIVE_META.filter(m => kept.has(m.path))
+ const hasAny = staticVisible.length > 0 || instrumentIds.length > 0
+ if (!hasAny) return null
+
+ const activePath = location.pathname
return (
- {visible.map(({ path, label, icon: Icon }) => {
- const isActive = location.pathname === path || location.pathname.startsWith(path + '/')
+
+ {/* Static keep-alive tabs */}
+ {staticVisible.map(({ path, label, icon: Icon }) => {
+ const isActive = activePath === path || activePath.startsWith(path + '/')
return (
{label}
{
- e.stopPropagation()
- window.open(path, '_blank')
- }}
+ onClick={e => { e.stopPropagation(); window.open(path, '_blank') }}
className="ml-1 opacity-0 group-hover:opacity-100 text-slate-600 hover:text-slate-300 rounded hover:bg-slate-600/40 p-0.5 transition-opacity leading-none"
>
{
e.stopPropagation()
- const remaining = visible.filter(m => m.path !== path)
+ const remaining = staticVisible.filter(m => m.path !== path)
forgetRoute(path)
- if (isActive && remaining.length > 0) {
- navigate(remaining[remaining.length - 1].path)
+ if (isActive && remaining.length > 0) navigate(remaining[remaining.length - 1].path)
+ else if (isActive && instrumentIds.length > 0) navigate(`/instruments/${instrumentIds[instrumentIds.length - 1]}`)
+ }}
+ className="opacity-0 group-hover:opacity-100 text-slate-600 hover:text-white rounded hover:bg-slate-600/40 p-0.5 transition-opacity leading-none"
+ >
+
+
+
+ )
+ })}
+
+ {/* Separator between static and instrument tabs */}
+ {staticVisible.length > 0 && instrumentIds.length > 0 && (
+
+ )}
+
+ {/* Dynamic instrument tabs */}
+ {instrumentIds.map(id => {
+ const path = `/instruments/${id}`
+ const isActive = activePath.toLowerCase() === path.toLowerCase()
+ return (
+