Unify Trader Mode workbench shell

This commit is contained in:
Codex
2026-05-20 15:04:22 +08:00
parent 1e012a7d1c
commit 729eb80c1f
9 changed files with 236 additions and 14 deletions
+3 -1
View File
@@ -10,7 +10,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
## Last Updated
2026-05-18 UTC
2026-05-20 UTC
## Automation Status
@@ -118,6 +118,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- latest_decision_assistant_workspace_refinement_slice: continuous_workspace_summary_navigation
- latest_decision_assistant_digest_browser_slice: digest_history_browser_review_sequence
- latest_decision_assistant_digest_preference_slice: digest_preference_profile_local_mode
- latest_trader_mode_shell_slice: unified_workbench_left_tree_route
- latest_decision_assistant_workflow_queue_slice: assistant_loop_queue
- latest_decision_assistant_workspace_loop_slice: workflow_section_navigation_loop_queue
- latest_decision_assistant_learning_focus_slice: learning_focus_queue
@@ -800,6 +801,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
- Workbench UX + read-only live usefulness recovery passed on 2026-05-20 after real intraday testing showed the root workbench was too long, hard to use, and runtime-dependent panels could appear ineffective. `/` now uses a left-menu/right-workspace layout with hash/localStorage section switching, default `Decision Assistant`, grouped `Readiness` and `Alerts` pages, and `/trader-mode` remains the fast-read entry. Frontend `fetchJson` now has request timeout fallback so slow review/readiness endpoints no longer keep the entire workbench stuck on Loading. Remote blockers were also recovered: PostgreSQL `pg_hba` accepted the Docker network, compose now maps `host.docker.internal`, non-root deploy env backup stays inside the remote app path, DB object ownership was reassigned to the migration/app user, the local proxy was relaunched, remote runtime env was restored with `TRADING_RUNTIME_ENABLED=1` / market and broker proxy URLs / `LIVE_TRADING_ENABLED=0`, and read-only launch readiness now treats fresh REST market data plus gap-clear state as usable while surfacing stream/news/account issues as warnings. Validation passed focused workbench/readiness/maintenance/deploy tests, secret/static script checks, Playwright screenshots at 1366x768 and 390x844 plus an Alerts hash view, full unit discovery, deploy smoke over 11 endpoints, standard remote ops smoke including `unattended_harness`, and remote `run_readonly_live_analysis_smoke.py` with `ready_for_readonly_live_analysis=true`, `must_not_trade=true`, `trade_permission_required=false`, and `submit_cancel_or_live_execution_called=false`. Current warning state: market stream may still be connecting while REST candles are fresh, news provider may be stale, and OKX account context may be unavailable; these warnings do not change `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, or operator confirmation protocol.
- Trader Mode usefulness compression passed on 2026-05-20: `/trader-mode` and the embedded Decision Assistant watchlist now prioritize trader-language first-screen answers: `Why No Trade`, `Watch Next`, `Focus Reason`, `Data State`, and `No Candidate`. Watchlist queue cards now say why not to trade, what to watch next, and why the unique focus deserves attention instead of exposing internal reason buckets first. The stale/freshness gate no longer freezes the page just because an old candidate timestamp exists when fresh market/review timestamps are available, and read-only runtime fallback now treats market stream / broker sync degradation as warnings for analysis-only mode while strict execution/readiness gates stay unchanged. Profile filtering now preserves the server-generated readout when the current focus already matches the trader profile, avoiding fallback text that degraded `FVG retrace` into generic `confirmation`. State-change alerts now show readable trader messages instead of raw internal signatures. Validation passed 23 focused operator/static tests, `node --check`, Python compile checks, full unit discovery (`787` tests), secret static checks, script static checks, `git diff --check` with only LF/CRLF warnings, deploy smoke over 11 endpoints, standard remote ops smoke including `unattended_harness`, remote `run_readonly_live_analysis_smoke.py` with `ready_for_readonly_live_analysis=true` and `must_not_trade=true`, and gstack browser screenshots at `output/playwright/trader-mode-usefulness-readout-remote-final3-390x844.png` plus `output/playwright/trader-mode-usefulness-readout-remote-final3-1366x768.png`.
- First real Trader Mode R2 use loop executed on 2026-05-20: used remote `/trader-mode` as the primary screen, observed `Observe Only: Watch Candidate #4610: FVG retrace`, and recorded a real `No Trade` action from the UI because Candidate `4610` remained `blocked` / `executable=false`. This correctly updated `shadow_paper_observations` instead of creating a fake paper ticket: `operator_feedback_count` increased from `1` to `2`, `shadow_observation_count` increased from `14` to `15`, while `paper_ticket_count=0` and `paper_execution_count=0` stayed unchanged. Saved after-close draft note `operator_notes.id=3` with scope `weekly_review_draft`; `/trading/journal/generate` returned `created=0`, so `weekly_review_entry_count` remains `0` until a real execution-derived journal entry exists. Evidence screenshot saved at `output/playwright/trader-mode-r2-real-use-loop-390x844.png`. Current R2 gate remains `paper_ticket_sample_gate`; do not mark R2 completed.
- Trader Mode unified shell closure passed on 2026-05-20: `/trader-mode` now resolves to the same left-menu/right-workspace `index.html` shell instead of the standalone page, with the Trader Mode section active from the route and the sidebar `Trader Mode` tree item selected. The embedded Trader Mode surface keeps the fast-read status, live readout, fast queue, freshness, focus, action buttons, after-close draft, R2 evidence, and read-only boundary controls, while the generic workbench filter bar is hidden only for this section. `trader_mode.js` is scoped so it can run inside the workbench without leaking globals into `app.js`; keyboard shortcuts and auto-refresh are active only while the Trader Mode section is visible. Remote validation passed deploy smoke over 11 endpoints, gstack browser checks on `http://192.168.3.90:8000/trader-mode` (`bodySection=trader-mode`, active nav=`Trader Mode`, sidebar present, generic controls hidden, loaded status=`Observe Only`), `run_readonly_live_analysis_smoke.py`, and `run_remote_ops_smoke.py`. Evidence screenshot saved at `output/playwright/trader-mode-unified-shell-1366x768-loaded.png`. No change was made to `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, or operator confirmation protocol.
- Current assisted-analysis next gate: keep R1/R2 focused on `decision_support_ready` trust/sample/quality evidence and shadow/paper/operator feedback collection. OKX trade-permission sandbox smoke is deferred until the project explicitly re-enters execution adapter or real-trading preparation.
## Exact Next Step
+2 -1
View File
@@ -150,7 +150,7 @@
- [x] R2 shadow / paper continuous validation ledger starter slice implemented and deployed
- [x] R2 sample collection closure slice implemented and deployed: persistent daily observation ledger, runtime/API/cockpit observation actions, validation count fields, and remote sample writeback
- [x] R2 evidence backfill audit and guarded shadow-observation backfill complete: remote dry-run found 10 eligible historical blocked-candidate days, remote apply created observations `1449` through `1458`, and the live gate moved from `shadow_candidate_day_coverage` to `paper_ticket_sample_gate`
- [ ] R2 evidence window still collecting: live state has `observed_shadow_day_count=12`, `shadow_observation_count=13`, `shadow_observation_day_count=11`, `paper_ticket_count=0`, `paper_execution_count=0`, `operator_feedback_count=1`, `weekly_review_entry_count=0`, and Trader Mode now surfaces these missing evidence types without marking `decision_support_ready=true`
- [ ] R2 evidence window still collecting: live state has `observed_shadow_day_count=12`, `shadow_observation_count=15`, `shadow_observation_day_count=11`, `paper_ticket_count=0`, `paper_execution_count=0`, `operator_feedback_count=2`, `weekly_review_entry_count=0`, and Trader Mode now surfaces these missing evidence types without marking `decision_support_ready=true`
- [x] Recover remote API responsiveness blocker: no-proxy `/health` returned 200, SSH command execution recovered, and standard remote ops smoke passed all 9 checks including `unattended_harness`
- [x] Restore OKX read-only market/broker proxy before continuing R2 sampling: remote no-proxy readiness now has `market_truth_ready`, `broker_state_ready`, and `runtime_ready`; standard remote ops smoke passed
- [x] Read-only live analysis launch closure complete: `live_analysis_assistant_ready=true`, cockpit boundary/health summary visible, `decision_support_ready=false`, `execution_assist_ready=false`, `real_live_ready=false`, deploy smoke passed, `run_readonly_live_analysis_smoke.py` passed
@@ -162,6 +162,7 @@
- [x] Low-risk sensitive credential cleanup complete: removed tracked expect scripts with embedded password sends, switched rollback docs/checker to `deploy_with_password.py` + runtime env, added redacted `run_secret_static_checks.py`, wired it into script static checks and Trading Delivery Checklist, and kept all real remote/runtime credentials unchanged
- [x] Workbench frontend split-navigation closure: converted the long root workbench into a left-menu/right-workspace layout, added hash/localStorage section switching, added frontend request timeout fallback so slow review/readiness APIs no longer freeze the page, and validated desktop/narrow rendering
- [x] Live usefulness blocker recovery: root workbench split-navigation is usable, slow frontend requests time out instead of freezing the page, remote DB/compose/deploy/migration/proxy/runtime blockers were recovered, standard remote ops smoke including `unattended_harness` passed, and readonly live analysis smoke passed without enabling order submission
- [x] Trader Mode unified workbench shell closure: `/trader-mode` now opens inside the same left-tree/right-workspace framework with the Trader Mode tree item active, hides the generic workbench filters only for this section, scopes Trader Mode JS to avoid app globals/hidden-section shortcuts, deploy smoke passed, remote gstack browser verification passed, readonly live analysis smoke passed, and remote ops smoke passed
- [ ] Next trader-usefulness refinement: compress Trader Mode / watchlist language around why no trade, what to watch next, unique focus quality, stale data, and no-candidate states so the first screen answers live operator questions faster
- [ ] R2 next collection action after readonly launch: wait for or surface a real executable candidate, then build shadow paper ticket / confirm paper execution / record operator feedback / add weekly review evidence without changing execution/live gates
- [x] R3 OKX sandbox preflight starter implemented and deployed: signed dry-run request previews, idempotency key preview, error normalization catalog, `/trading/okx-sandbox-preflight`, live-readiness summary, cockpit surface, deploy smoke, remote ops smoke, and remote field/UI smoke
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

+1 -1
View File
@@ -1299,7 +1299,7 @@ def _resolve_static_file(path: str) -> Optional[Path]:
if path in {"/training", "/training.html"}:
return STATIC_DIR / "training.html"
if path in {"/trader-mode", "/trader-mode.html"}:
return STATIC_DIR / "trader_mode.html"
return STATIC_DIR / "index.html"
relative = path.removeprefix("/static/")
resolved = (STATIC_DIR / relative).resolve()
try:
+11 -1
View File
@@ -170,6 +170,14 @@ function getWorkbenchSection(sectionId) {
return workbenchSections.find((section) => section.dataset.workbenchSection === sectionId) || null;
}
function getPathWorkbenchSection() {
const normalizedPath = window.location.pathname.replace(/\/+$/, '') || '/';
if (normalizedPath === '/trader-mode' || normalizedPath === '/trader-mode.html') {
return 'trader-mode';
}
return '';
}
function activateWorkbenchSection(sectionId, options = {}) {
if (!workbenchSections.length) return;
const fallback = getWorkbenchSection('decision-assistant') || workbenchSections[0];
@@ -180,6 +188,7 @@ function activateWorkbenchSection(sectionId, options = {}) {
section.classList.toggle('is-active', isActive);
section.toggleAttribute('hidden', !isActive);
});
document.body.dataset.workbenchSection = activeId;
workbenchNavButtons.forEach((button) => {
const isActive = button.dataset.workbenchTarget === activeId;
button.classList.toggle('is-active', isActive);
@@ -195,6 +204,7 @@ function activateWorkbenchSection(sectionId, options = {}) {
if (options.scrollToTop && workbenchContent) {
workbenchContent.scrollTo({ top: 0, behavior: options.smooth ? 'smooth' : 'auto' });
}
window.dispatchEvent(new CustomEvent('workbench-section-change', { detail: { sectionId: activeId } }));
}
function initializeWorkbenchNavigation() {
@@ -209,7 +219,7 @@ function initializeWorkbenchNavigation() {
});
});
const hashSection = window.location.hash ? window.location.hash.replace(/^#\/?/, '') : '';
activateWorkbenchSection(hashSection || getStoredWorkbenchSection() || 'decision-assistant', {
activateWorkbenchSection(hashSection || getPathWorkbenchSection() || getStoredWorkbenchSection() || 'decision-assistant', {
persist: false,
scrollToTop: false,
});
+136 -2
View File
@@ -13,7 +13,7 @@
<p>Live plan, candidate, ticket, position, and review flow.</p>
</div>
<div class="topbar-actions">
<a class="nav-link" href="/trader-mode">Trader Mode</a>
<a class="nav-link" href="/trader-mode">Open Trader Mode</a>
<div id="healthStatus" class="health-card">Loading workspace...</div>
</div>
</header>
@@ -25,7 +25,7 @@
<div id="workbenchViewLabel" class="workbench-view-label">Decision Assistant</div>
</div>
<nav id="workbenchNav" class="workbench-nav">
<a class="workbench-nav-link workbench-nav-primary" href="/trader-mode">Trader Mode</a>
<button class="workbench-nav-primary" type="button" data-workbench-target="trader-mode">Trader Mode</button>
<button type="button" data-workbench-target="overview">Overview</button>
<button type="button" data-workbench-target="decision-assistant">Decision Assistant</button>
<button type="button" data-workbench-target="trading-status">Trading Status</button>
@@ -98,6 +98,139 @@
</section>
<main id="workbenchMain" class="grid workbench-main">
<section class="workspace-section trader-mode-workspace-section" data-workbench-section="trader-mode" data-workbench-label="Trader Mode">
<div class="panel trader-mode-workspace-header">
<div>
<h2>Trader Mode</h2>
<p>Read-only live brief for fast intraday review.</p>
</div>
<div id="traderModeHealth" class="health-card">Loading Trader Mode...</div>
</div>
<div class="trader-mode-shell trader-mode-shell-embedded">
<section id="traderChangeAlert" class="trader-change-alert hidden"></section>
<section id="traderStatusPanel" class="trader-status-panel">
<div>
<div class="metric">Status</div>
<div id="traderStatusLabel" class="trader-status-label">Loading</div>
</div>
<div>
<div class="metric">Decision Now</div>
<div id="traderConclusion" class="trader-conclusion">Waiting for live brief.</div>
</div>
<div>
<div class="metric">Next Action</div>
<div id="traderNextAction" class="trader-next-action">n/a</div>
</div>
</section>
<section class="trader-mode-band trader-readout-band">
<h2>Live Readout</h2>
<div id="traderReadoutPanel" class="trader-readout-grid"></div>
</section>
<section class="trader-mode-band">
<h2>Fast Queue</h2>
<div id="traderQueuePanel" class="trader-queue-panel"></div>
</section>
<section class="controls trader-mode-controls">
<label>Profile
<select id="traderProfileSelect"></select>
</label>
<label>Profile Name <input id="traderProfileName" type="text" value="BTC London" /></label>
<label>Instrument ID <input id="traderInstrumentId" type="number" value="1" min="1" /></label>
<label>Timeframe <input id="traderTimeframe" type="text" value="1m" /></label>
<label>Sessions <input id="traderSessionFilter" type="text" value="LONDON,NY_AM" /></label>
<label>Setups <input id="traderSetupFilter" type="text" value="SWEEP,MSS,FVG" /></label>
<label>Auto Refresh
<select id="traderAutoRefreshSeconds">
<option value="0">Off</option>
<option value="5" selected>5s</option>
<option value="10">10s</option>
<option value="30">30s</option>
</select>
</label>
<button id="traderRefreshBtn" type="button">Refresh</button>
<button id="traderSaveProfileBtn" type="button">Save Profile</button>
<button id="traderResetProfileBtn" type="button">Reset Profile</button>
<button id="traderAlertBtn" type="button">Enable Alerts</button>
<div id="traderLastUpdated" class="last-updated">Waiting for first refresh</div>
<div id="traderKeyboardStatus" class="last-updated">Auto refresh active</div>
</section>
<section class="trader-mode-band">
<div>
<h2>Freshness</h2>
<div id="traderFreshnessGrid" class="trader-metric-grid">
<div class="trader-metric"><div class="metric">Market Last Updated</div></div>
<div class="trader-metric"><div class="metric">Review Packet Updated</div></div>
<div class="trader-metric"><div class="metric">Runtime Online</div></div>
<div class="trader-metric"><div class="metric">Stale Seconds</div></div>
</div>
</div>
</section>
<section class="trader-mode-band trader-mode-two-col">
<div>
<h2>Chart Bridge</h2>
<div id="traderChartBridgePanel" class="trader-metric-grid"></div>
</div>
<div>
<h2>Duty Panel</h2>
<div id="traderOpsDutyPanel" class="trader-metric-grid"></div>
</div>
</section>
<section class="trader-mode-band trader-mode-two-col">
<div>
<h2>Focus</h2>
<div id="traderFocusPanel" class="trader-focus-panel"></div>
</div>
<div>
<h2>Three Reasons</h2>
<ol id="traderReasonList" class="trader-reason-list"></ol>
</div>
</section>
<section class="trader-mode-band">
<h2>Recovery</h2>
<div id="traderRecoveryPanel" class="trader-recovery-panel"></div>
</section>
<section class="trader-mode-band">
<h2>Record Action</h2>
<div id="traderActionBar" class="trader-action-bar">
<button data-trader-action="agree" type="button">Agree</button>
<button data-trader-action="disagree" type="button">Disagree</button>
<button data-trader-action="no_trade" type="button">No Trade</button>
<button data-trader-action="need_more_evidence" type="button">Need More Evidence</button>
<button data-trader-action="write_quick_note" type="button">Write Quick Note</button>
</div>
<textarea id="traderQuickNote" class="trader-quick-note" rows="3" placeholder="Optional quick note for this live brief."></textarea>
<div id="traderActionStatus" class="metric">No action recorded in this browser session.</div>
</section>
<section class="trader-mode-band">
<h2>After-Close Draft</h2>
<div id="traderPostMarketDraftPanel" class="trader-post-market-draft"></div>
<button id="traderSaveDraftBtn" type="button">Save Draft Note</button>
</section>
<section class="trader-mode-band trader-mode-two-col">
<div>
<h2>R2 Evidence Window</h2>
<div id="traderEvidenceGrid" class="trader-metric-grid"></div>
</div>
<div>
<h2>Boundary</h2>
<div id="traderBoundaryBadges" class="trader-boundary-badges"></div>
</div>
</section>
</div>
</section>
<section class="panel panel-wide workspace-section" data-workbench-section="overview" data-workbench-label="Overview">
<h2>Overview</h2>
<div id="overviewCards" class="cards overview-cards"></div>
@@ -188,5 +321,6 @@
</div>
<script src="/static/app.js"></script>
<script src="/static/trader_mode.js"></script>
</body>
</html>
+28
View File
@@ -156,6 +156,10 @@ body {
.workbench-content {
min-width: 0;
}
body[data-workbench-section="trader-mode"] #workbenchContent > .controls,
body[data-workbench-section="trader-mode"] #workbenchContent > .filters-bar {
display: none;
}
.workbench-content .controls,
.workbench-content .filters-bar {
margin-left: 0;
@@ -404,6 +408,29 @@ th { color: #9db0d3; }
min-width: 0;
max-width: 100%;
}
.trader-mode-workspace-section {
min-width: 0;
}
.trader-mode-workspace-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 12px;
}
.trader-mode-workspace-header h2 {
margin: 0 0 6px;
}
.trader-mode-workspace-header p {
margin: 0;
color: #93a4c3;
}
.trader-mode-shell-embedded {
padding: 0;
}
.trader-mode-shell .trader-mode-controls {
margin: 0;
}
.trader-change-alert {
padding: 12px 14px;
border: 1px solid #38bdf8;
@@ -639,6 +666,7 @@ th { color: #9db0d3; }
}
@media (max-width: 760px) {
.topbar,
.trader-mode-workspace-header,
.trader-status-panel,
.trader-mode-two-col {
grid-template-columns: 1fr;
+42
View File
@@ -1,3 +1,4 @@
(() => {
const traderHealth = document.getElementById('traderModeHealth');
const traderProfileSelect = document.getElementById('traderProfileSelect');
const traderProfileName = document.getElementById('traderProfileName');
@@ -33,6 +34,10 @@ const traderSaveDraftBtn = document.getElementById('traderSaveDraftBtn');
const traderEvidenceGrid = document.getElementById('traderEvidenceGrid');
const traderBoundaryBadges = document.getElementById('traderBoundaryBadges');
if (!traderHealth || !traderStatusPanel || !traderActionBar || !traderProfileSelect) {
return;
}
const TRADER_SIGNATURE_KEY = 'ai_ict_trader_mode_signature_v1';
const TRADER_ALERTS_KEY = 'ai_ict_trader_mode_alerts_enabled_v1';
const TRADER_PROFILE_KEY = 'ai_ict_trader_mode_profile_key_v1';
@@ -88,6 +93,7 @@ const DEFAULT_TRADER_PROFILES = [
let traderAutoRefreshTimer = null;
let traderAutoRefreshPaused = false;
let traderModeInitialized = false;
let latestTraderPayload = null;
let traderAlertsEnabled = localStorage.getItem(TRADER_ALERTS_KEY) === '1';
@@ -108,6 +114,11 @@ function createElement(tag, className, text) {
return element;
}
function isTraderModeActive() {
const section = traderStatusPanel.closest('[data-workbench-section]');
return !section || !section.hidden || section.classList.contains('is-active');
}
function queryString(params) {
const search = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
@@ -868,12 +879,22 @@ function configureTraderAutoRefresh() {
if (traderAutoRefreshTimer) clearInterval(traderAutoRefreshTimer);
traderAutoRefreshTimer = null;
const seconds = Number(traderAutoRefreshSeconds.value || 0);
if (!isTraderModeActive()) {
setText(traderKeyboardStatus, 'Auto refresh waiting for Trader Mode');
return;
}
if (!traderAutoRefreshPaused && seconds > 0) {
traderAutoRefreshTimer = setInterval(refreshTraderMode, seconds * 1000);
}
setText(traderKeyboardStatus, traderAutoRefreshPaused ? 'Auto refresh paused' : 'Auto refresh active');
}
function stopTraderAutoRefresh() {
if (traderAutoRefreshTimer) clearInterval(traderAutoRefreshTimer);
traderAutoRefreshTimer = null;
setText(traderKeyboardStatus, 'Auto refresh waiting for Trader Mode');
}
function toggleTraderAutoRefresh() {
traderAutoRefreshPaused = !traderAutoRefreshPaused;
const shortcutAction = 'pause_or_resume_auto_refresh';
@@ -917,6 +938,7 @@ function configureKeyboardShortcuts() {
}
document.addEventListener('keydown', async (event) => {
if (event.ctrlKey || event.altKey || event.metaKey || isTypingTarget(event.target)) return;
if (!isTraderModeActive()) return;
const key = event.key.toLowerCase();
try {
if (key === 'r') {
@@ -973,8 +995,28 @@ traderSessionFilter.addEventListener('change', refreshTraderMode);
traderSetupFilter.addEventListener('change', refreshTraderMode);
traderAutoRefreshSeconds.addEventListener('change', configureTraderAutoRefresh);
function initializeTraderMode() {
if (!isTraderModeActive()) {
stopTraderAutoRefresh();
return;
}
if (!traderModeInitialized) {
traderModeInitialized = true;
setText(traderAlertBtn, traderAlertsEnabled ? 'Disable Alerts' : 'Enable Alerts');
renderProfileOptions();
configureKeyboardShortcuts();
}
configureTraderAutoRefresh();
refreshTraderMode();
}
window.addEventListener('workbench-section-change', (event) => {
if (event.detail?.sectionId === 'trader-mode') {
initializeTraderMode();
} else {
stopTraderAutoRefresh();
}
});
initializeTraderMode();
})();
+8 -3
View File
@@ -22,7 +22,7 @@ class ApiStaticServerTests(unittest.TestCase):
def test_trader_mode_route_is_treated_as_static(self) -> None:
self.assertTrue(_should_serve_static("/trader-mode"))
self.assertTrue(str(_resolve_static_file("/trader-mode")).replace("\\", "/").endswith("src/web/trader_mode.html"))
self.assertTrue(str(_resolve_static_file("/trader-mode")).replace("\\", "/").endswith("src/web/index.html"))
def test_static_asset_path_is_resolved(self) -> None:
self.assertTrue(_should_serve_static("/static/app.js"))
@@ -36,7 +36,7 @@ class ApiStaticServerTests(unittest.TestCase):
self.assertIsNotNone(index_file)
index_html = index_file.read_text(encoding="utf-8")
for asset in ("/static/styles.css", "/static/app.js"):
for asset in ("/static/styles.css", "/static/app.js", "/static/trader_mode.js"):
with self.subTest(asset=asset):
self.assertIn(asset, index_html)
asset_file = _resolve_static_file(asset)
@@ -60,7 +60,7 @@ class ApiStaticServerTests(unittest.TestCase):
self.assertIsNotNone(trader_mode_file)
trader_mode_html = trader_mode_file.read_text(encoding="utf-8")
for asset in ("/static/styles.css", "/static/trader_mode.js"):
for asset in ("/static/styles.css", "/static/app.js", "/static/trader_mode.js"):
with self.subTest(asset=asset):
self.assertIn(asset, trader_mode_html)
asset_file = _resolve_static_file(asset)
@@ -131,6 +131,9 @@ class ApiStaticServerTests(unittest.TestCase):
for expected in (
'class="workbench-layout"',
'id="workbenchNav"',
'data-workbench-target="trader-mode"',
'data-workbench-section="trader-mode"',
'id="traderStatusPanel"',
'data-workbench-target="decision-assistant"',
'data-workbench-section="decision-assistant"',
'data-workbench-section="readiness"',
@@ -141,6 +144,8 @@ class ApiStaticServerTests(unittest.TestCase):
for expected in (
"activateWorkbenchSection",
"initializeWorkbenchNavigation",
"getPathWorkbenchSection",
"workbench-section-change",
"aiIctActiveWorkbenchSection",
"request_timeout",
"AbortController",