Files
my-NexusTradingForexBot/Web/ux_conn.js
T
gavindiazandClaude Code 3a005ddcc6
Docs / Validate documentation (push) Canceled after 0s
Docs / Deploy GitHub Pages (push) Canceled after 0s
CI Final Summary (AI) / AI Final Summary (push) Canceled after 0s
CI Final Summary (AI) / Push/PR event feed (push) Canceled after 0s
CI / CI Integrity and Change Classification (push) Canceled after 0s
CI / Code Quality & Tests (push) Canceled after 0s
Dependency Lock & Migration Safety / Dependency drift (lock vs pyproject) (push) Canceled after 0s
Dependency Lock & Migration Safety / Migration safety (fail-loud + version postconditions) (push) Canceled after 0s
JS Tests / Frontend JS Unit Tests (push) Canceled after 0s
Security / CodeQL Analysis (push) Canceled after 0s
Security / Trivy Vulnerability Scan (push) Canceled after 0s
Tests (OS Matrix) / Py Tests (macos-latest) (push) Canceled after 0s
Tests (OS Matrix) / Py Tests (windows-latest) (push) Canceled after 0s
CI / Extended Validation (Tier 2) (push) Canceled after 0s
CI / Heavy CI (database-provider) (push) Canceled after 0s
CI / Heavy CI (e2e) (push) Canceled after 0s
CI / Heavy CI (integration) (push) Canceled after 0s
CI / Heavy CI (model-validation) (push) Canceled after 0s
CI / Heavy CI (research-backtest) (push) Canceled after 0s
CI / Aggregate CI Results (push) Canceled after 0s
chore(repo): initial import of Nexus Trading Forex Bot into self-hosted hosting
Full project import for the self-hosted code hosting
(https://code.aifunny.ltd/gavindiaz/my-NexusTradingForexBot.git).

Includes the live-freshness + scaler-hash fixes for the permanent
STALE dashboard (tick_pipeline now_utc stamps; infer_probabilities
computes model_input_hash on the actual scaled 70D tensor), the
data-fetch default-path fix, the g29 web-auth test fixes, and a
Chinese user manual (使用手册.md) for the web dashboard.

Scratch diagnostic scripts (check*.py, contain a live web-auth token)
and the .kilo/ agent-worktree directory are gitignored, not committed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 23:24:54 +08:00

128 lines
5.5 KiB
JavaScript

/* ==========================================================================
* Nexus Scalp Engine — Connectivity Banner Controller (NXConn) · CHG-0048
* --------------------------------------------------------------------------
* Agent: Nexus-Main (UX). Presentation-layer ONLY.
*
* One authoritative banner for the whole client (no per-widget guessing):
* UP — banner hidden.
* DEGRADED — amber warning: stream stale; data on screen may be old.
* DOWN — red alert: connection lost; last-update time + Retry now.
*
* Truth rules (NO UX FAKERY):
* - The banner only appears from real signal loss (SSE error, fetch
* failure, stale heartbeat) and disappears only on a real event.
* - "Last update" shows the real timestamp of the last live event.
* - Never starts DOWN on page load: before the first event the header
* badge CONNECTING state is the truthful signal.
* ========================================================================== */
(function (global) {
'use strict';
var state = 'UP'; // UP | DEGRADED | DOWN
var lastEventAt = null; // ms timestamp of the last REAL live event
var retryBinded = false;
var banner = null;
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
function t(key, fallback, vars) {
var i = global.NX_I18N;
var s = i ? i.t(key, fallback) : fallback;
if (vars) Object.keys(vars).forEach(function (k) { s = s.split('{' + k + '}').join(vars[k]); });
return s;
}
function ensure() {
banner = document.getElementById('conn-lost-banner');
if (!banner || retryBinded) return;
retryBinded = true;
var btn = document.getElementById('conn-retry-btn');
if (btn) {
btn.addEventListener('click', function () {
try { fetchSystemSnapshot(); } catch (e) { /* app.js not ready */ }
try { startSSE(); } catch (e) { /* not initialized yet */ }
});
}
}
function fmtClock(ms) {
var d = new Date(ms);
function p(n) { return (n < 10 ? '0' : '') + n; }
return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
}
function render() {
ensure();
if (!banner) return; // index.html not loaded (embedded pages) — stay silent
var detail = document.getElementById('conn-lost-detail');
var lastEl = document.getElementById('conn-lost-last');
var titleEl = document.getElementById('conn-lost-title');
if (state === 'UP') {
banner.classList.add('hidden');
return;
}
banner.classList.remove('hidden');
if (state === 'DEGRADED') {
banner.className = banner.className.replace(/bg-rose-[^\s"]+/g, 'bg-amber-500/15')
.replace(/border-rose-[^\s"]+/g, 'border-amber-500/40')
.replace(/text-rose-[^\s"]+/g, 'text-amber-100');
banner.setAttribute('role', 'status');
banner.setAttribute('aria-live', 'polite');
if (titleEl) titleEl.textContent = t('ux.conn.stale_title', 'DATA MAY BE STALE');
if (detail) detail.textContent = t('ux.conn.stale_detail', 'No live updates for a while. Values shown are the last known.');
} else {
banner.className = banner.className.replace(/bg-amber-[^\s"]+/g, 'bg-rose-500/15')
.replace(/border-amber-[^\s"]+/g, 'border-rose-500/40')
.replace(/text-amber-[^\s"]+/g, 'text-rose-100');
banner.setAttribute('role', 'alert');
banner.setAttribute('aria-live', 'assertive');
if (titleEl) titleEl.textContent = t('ux.conn.title', 'CONNECTION LOST');
if (detail) detail.textContent = t('ux.conn.detail', 'Live updates stopped. Data on screen may be outdated.');
}
if (lastEl) {
if (lastEventAt) {
var ageSec = Math.max(0, Math.round((Date.now() - lastEventAt) / 1000));
lastEl.textContent = t('ux.conn.last', 'Last update: {t} ({s}s ago)', { t: fmtClock(lastEventAt), s: ageSec });
} else {
lastEl.textContent = t('ux.conn.never', 'No live data received yet.');
}
}
}
global.NXConn = {
/** Called on ANY real live event (SSE tick/state/heartbeat, snapshot OK). */
setUp: function () {
lastEventAt = Date.now();
if (state === 'UP') return;
state = 'UP';
render();
},
/** Hard failure: SSE error, fetch exception, HTTP 5xx. */
setDown: function (reason) {
if (state === 'DOWN') { render(); return; } // refresh last-update while down
state = 'DOWN';
render();
if (reason) console.warn('[UI_CONN] DOWN: ' + reason);
},
/** Soft failure: stream open but no events (stale), engine paused, etc. */
setDegraded: function () {
if (state === 'DOWN') { render(); return; }
state = 'DEGRADED';
render();
},
state: function () { return state; },
lastEventAt: function () { return lastEventAt; },
/** Internal: let the stale timer refresh the age label. */
refresh: render
};
// Keep the "last update (Ns ago)" label honest while the banner shows.
setInterval(function () { if (state !== 'UP') render(); }, 5000);
})(window);