Spaces:
Running
fix: card hover cascade, nav/row tap targets, and t4 dark-mode contrast
Browse files- Remove duplicate flatter .card:hover rule (was overriding the richer
translateY(-5px) scale(1.008) + elaborate shadow lift with a plain
translateY(-2px), no-scale version later in cascade order). Only one
.card:hover rule remains; @media (hover:none) touch override untouched.
- Raise nav .chip input/select min-height 20px -> 36px, add matching
min-height:36px to .state-filter-toggle, and add min-height:40px to
.row-summary (ranked-row click target), addressing sub-24px tap
targets flagged in the interaction/motion audit without disrupting
the compact row/chip layouts.
- Lighten dark-theme --t4 from #5b667e (3.46:1 on --bg, fails WCAG AA)
to #707c98 (4.77:1 on --bg, 4.59:1 on --bg2), verified via a
relative-luminance/contrast script. Affects USDT quote-suffix,
confidence /10 denominator, scan-step elapsed labels, and backtest
per-state trade counts.
Also verified (already fixed by prior uncommitted session, logic
confirmed sound on re-read, no changes needed):
- flipReorder() FLIP re-rank now keys the before/after position map by
el.dataset.sym instead of DOM element reference, so the transform
correctly applies across refreshScanQuietly()'s replaceWith() swaps.
- Scan-loader step 4 ("Ranking setups") now rotates reassurance copy
every 5s via startStep4Wait()/STEP4_WAIT_MESSAGES instead of sitting
static during slow 40-90s cold-start scans; timers cleared correctly
in both startLoaderSteps() reset and stopLoaderSteps() cleanup.
Validation: python brace/paren/bracket balance check on full file OK;
node --check (v20.20.2) on largest inline <script> OK; local uvicorn
smoke test on :8954 returned 200 for / and /api/status with no
Python errors, and served output confirmed all edits present.
- static/index.html +52 -17
|
@@ -44,7 +44,7 @@
|
|
| 44 |
--t1: var(--tx);
|
| 45 |
--t2: var(--tx2);
|
| 46 |
--t3: var(--tx3);
|
| 47 |
-
--t4: #
|
| 48 |
--muted: var(--tx3);
|
| 49 |
|
| 50 |
/* ── Palette ── */
|
|
@@ -1531,7 +1531,7 @@ nav {
|
|
| 1531 |
.state-filter-toggle {
|
| 1532 |
display:inline-flex; align-items:center; gap:5px;
|
| 1533 |
background:var(--glass2); border:1px solid var(--gb);
|
| 1534 |
-
border-radius:8px; padding:4px 10px;
|
| 1535 |
font-size:11px; font-weight:700; color:var(--t2); cursor:pointer;
|
| 1536 |
transition:all 0.2s;
|
| 1537 |
}
|
|
@@ -1584,7 +1584,7 @@ nav {
|
|
| 1584 |
|
| 1585 |
.row-summary { display:none; }
|
| 1586 |
.card.row-mode .row-summary {
|
| 1587 |
-
display:grid; cursor:pointer;
|
| 1588 |
grid-template-columns:26px 1.2fr 0.8fr 0.65fr 0.65fr 18px;
|
| 1589 |
align-items:center; gap:10px;
|
| 1590 |
}
|
|
@@ -1671,7 +1671,7 @@ nav {
|
|
| 1671 |
|
| 1672 |
/* ── Touch targets — minimum 44px per WCAG 2.5.5 ── */
|
| 1673 |
.chip { min-height:40px; }
|
| 1674 |
-
.chip input, .chip select { min-height:
|
| 1675 |
.bt-run-btn { min-height:36px; padding:6px 12px; }
|
| 1676 |
.live-acc-resolve-btn { min-height:36px; padding:6px 10px; }
|
| 1677 |
.signals-refresh-btn { width:36px !important; height:36px !important; }
|
|
@@ -1751,11 +1751,6 @@ nav {
|
|
| 1751 |
backdrop-filter:blur(14px) saturate(160%);
|
| 1752 |
-webkit-backdrop-filter:blur(14px) saturate(160%);
|
| 1753 |
}
|
| 1754 |
-
.card:hover {
|
| 1755 |
-
transform:translateY(-2px);
|
| 1756 |
-
border-color:var(--gb);
|
| 1757 |
-
box-shadow:var(--shadow), inset 0 1px 0 var(--rim), 0 0 0 1px rgba(76,201,240,0.22);
|
| 1758 |
-
}
|
| 1759 |
#cycle-section { position:relative; }
|
| 1760 |
#grid { position:relative; }
|
| 1761 |
|
|
@@ -2471,10 +2466,22 @@ function stopSymbolStream() { clearInterval(symStreamTimer); }
|
|
| 2471 |
/* ═══ Loader step animator ═══ */
|
| 2472 |
let stepTimer=null, elapsedTimer=null, countTimer=null, currentStep=0;
|
| 2473 |
let scanStartTime=0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2474 |
|
| 2475 |
function startLoaderSteps() {
|
| 2476 |
currentStep=0; scanStartTime=Date.now();
|
| 2477 |
-
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer);
|
|
|
|
| 2478 |
|
| 2479 |
// Reset all steps
|
| 2480 |
for(let i=1;i<=4;i++){
|
|
@@ -2483,6 +2490,8 @@ function startLoaderSteps() {
|
|
| 2483 |
const num=el.querySelector('.step-num'); if(num) num.textContent=i;
|
| 2484 |
const t=$('st'+i); if(t) t.textContent='';
|
| 2485 |
}
|
|
|
|
|
|
|
| 2486 |
const sc=$('scan-count'); if(sc) sc.textContent='0';
|
| 2487 |
const se=$('scan-elapsed'); if(se) se.textContent='0s';
|
| 2488 |
|
|
@@ -2490,12 +2499,18 @@ function startLoaderSteps() {
|
|
| 2490 |
activateStep(1);
|
| 2491 |
startSymbolStream();
|
| 2492 |
|
| 2493 |
-
// Progress through steps: 1→2 at 3s, 2→3 at 7s, 3→4 at 11s
|
|
|
|
|
|
|
| 2494 |
const timings=[3000,4000,4000];
|
| 2495 |
let cumulative=0;
|
| 2496 |
timings.forEach((delay,i)=>{
|
| 2497 |
cumulative+=delay;
|
| 2498 |
-
setTimeout(()=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2499 |
});
|
| 2500 |
|
| 2501 |
// Elapsed counter
|
|
@@ -2504,7 +2519,8 @@ function startLoaderSteps() {
|
|
| 2504 |
const se=$('scan-elapsed'); if(se) se.textContent=s+'s';
|
| 2505 |
},500);
|
| 2506 |
|
| 2507 |
-
// Fake count-up: 0→280 over ~12s (pace slows toward end)
|
|
|
|
| 2508 |
let count=0;
|
| 2509 |
countTimer=setInterval(()=>{
|
| 2510 |
const target=280, elapsed=(Date.now()-scanStartTime)/1000;
|
|
@@ -2515,6 +2531,17 @@ function startLoaderSteps() {
|
|
| 2515 |
},120);
|
| 2516 |
}
|
| 2517 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2518 |
function activateStep(n) {
|
| 2519 |
if(n>4) return;
|
| 2520 |
const el=$('sd'+n); if(!el) return;
|
|
@@ -2530,7 +2557,9 @@ function markDone(n) {
|
|
| 2530 |
}
|
| 2531 |
|
| 2532 |
function stopLoaderSteps() {
|
| 2533 |
-
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer);
|
|
|
|
|
|
|
| 2534 |
for(let i=1;i<=4;i++) markDone(i);
|
| 2535 |
const sc=$('scan-count'); if(sc) sc.textContent='280+';
|
| 2536 |
stopSymbolStream();
|
|
@@ -2937,13 +2966,19 @@ function setLoading(on) {
|
|
| 2937 |
|
| 2938 |
/* ═══ FLIP re-rank + never-blank auto-refresh ═══ */
|
| 2939 |
function flipReorder(container, mutation) {
|
| 2940 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2941 |
mutation();
|
| 2942 |
const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 2943 |
if (reduced) return;
|
| 2944 |
[...container.children].forEach(el => {
|
| 2945 |
-
const
|
| 2946 |
-
|
|
|
|
| 2947 |
const l = el.getBoundingClientRect();
|
| 2948 |
const dx = f.left - l.left, dy = f.top - l.top;
|
| 2949 |
if (dx || dy) {
|
|
|
|
| 44 |
--t1: var(--tx);
|
| 45 |
--t2: var(--tx2);
|
| 46 |
--t3: var(--tx3);
|
| 47 |
+
--t4: #707c98;
|
| 48 |
--muted: var(--tx3);
|
| 49 |
|
| 50 |
/* ── Palette ── */
|
|
|
|
| 1531 |
.state-filter-toggle {
|
| 1532 |
display:inline-flex; align-items:center; gap:5px;
|
| 1533 |
background:var(--glass2); border:1px solid var(--gb);
|
| 1534 |
+
border-radius:8px; padding:4px 10px; min-height:36px; box-sizing:border-box;
|
| 1535 |
font-size:11px; font-weight:700; color:var(--t2); cursor:pointer;
|
| 1536 |
transition:all 0.2s;
|
| 1537 |
}
|
|
|
|
| 1584 |
|
| 1585 |
.row-summary { display:none; }
|
| 1586 |
.card.row-mode .row-summary {
|
| 1587 |
+
display:grid; cursor:pointer; min-height:40px;
|
| 1588 |
grid-template-columns:26px 1.2fr 0.8fr 0.65fr 0.65fr 18px;
|
| 1589 |
align-items:center; gap:10px;
|
| 1590 |
}
|
|
|
|
| 1671 |
|
| 1672 |
/* ── Touch targets — minimum 44px per WCAG 2.5.5 ── */
|
| 1673 |
.chip { min-height:40px; }
|
| 1674 |
+
.chip input, .chip select { min-height:36px; }
|
| 1675 |
.bt-run-btn { min-height:36px; padding:6px 12px; }
|
| 1676 |
.live-acc-resolve-btn { min-height:36px; padding:6px 10px; }
|
| 1677 |
.signals-refresh-btn { width:36px !important; height:36px !important; }
|
|
|
|
| 1751 |
backdrop-filter:blur(14px) saturate(160%);
|
| 1752 |
-webkit-backdrop-filter:blur(14px) saturate(160%);
|
| 1753 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1754 |
#cycle-section { position:relative; }
|
| 1755 |
#grid { position:relative; }
|
| 1756 |
|
|
|
|
| 2466 |
/* ═══ Loader step animator ═══ */
|
| 2467 |
let stepTimer=null, elapsedTimer=null, countTimer=null, currentStep=0;
|
| 2468 |
let scanStartTime=0;
|
| 2469 |
+
let step4WaitTimer=null, step4MsgIdx=0;
|
| 2470 |
+
// Step 4 ("Ranking setups") is the last step and has no further server signal to advance on —
|
| 2471 |
+
// on a slow cold-start scan (40-90s) it would otherwise sit frozen on "running…" for a long
|
| 2472 |
+
// time. Rotate its sub-label through reassurance copy every ~5s instead of appearing stuck,
|
| 2473 |
+
// until stopLoaderSteps() is called when the real fetch resolves.
|
| 2474 |
+
const STEP4_WAIT_MESSAGES = [
|
| 2475 |
+
'35/20/15/15/15 scoring · sorting by confidence',
|
| 2476 |
+
'Still ranking — large markets can take up to 90s',
|
| 2477 |
+
'Cross-checking confidence bands…',
|
| 2478 |
+
'Almost there — finalizing setup order…'
|
| 2479 |
+
];
|
| 2480 |
|
| 2481 |
function startLoaderSteps() {
|
| 2482 |
currentStep=0; scanStartTime=Date.now();
|
| 2483 |
+
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer); clearInterval(step4WaitTimer);
|
| 2484 |
+
step4MsgIdx=0;
|
| 2485 |
|
| 2486 |
// Reset all steps
|
| 2487 |
for(let i=1;i<=4;i++){
|
|
|
|
| 2490 |
const num=el.querySelector('.step-num'); if(num) num.textContent=i;
|
| 2491 |
const t=$('st'+i); if(t) t.textContent='';
|
| 2492 |
}
|
| 2493 |
+
const sd4sub=document.querySelector('#sd4 .step-sub');
|
| 2494 |
+
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[0];
|
| 2495 |
const sc=$('scan-count'); if(sc) sc.textContent='0';
|
| 2496 |
const se=$('scan-elapsed'); if(se) se.textContent='0s';
|
| 2497 |
|
|
|
|
| 2499 |
activateStep(1);
|
| 2500 |
startSymbolStream();
|
| 2501 |
|
| 2502 |
+
// Progress through steps: 1→2 at 3s, 2→3 at 7s, 3→4 at 11s. These are illustrative pacing
|
| 2503 |
+
// for the first 3 steps only — step 4 has its own indefinite-wait handling below since the
|
| 2504 |
+
// real scan duration varies 40-90s and isn't streamed to the client.
|
| 2505 |
const timings=[3000,4000,4000];
|
| 2506 |
let cumulative=0;
|
| 2507 |
timings.forEach((delay,i)=>{
|
| 2508 |
cumulative+=delay;
|
| 2509 |
+
setTimeout(()=>{
|
| 2510 |
+
markDone(i+1);
|
| 2511 |
+
activateStep(i+2);
|
| 2512 |
+
if(i+2===4) startStep4Wait();
|
| 2513 |
+
}, cumulative);
|
| 2514 |
});
|
| 2515 |
|
| 2516 |
// Elapsed counter
|
|
|
|
| 2519 |
const se=$('scan-elapsed'); if(se) se.textContent=s+'s';
|
| 2520 |
},500);
|
| 2521 |
|
| 2522 |
+
// Fake count-up: 0→280 over ~12s (pace slows toward end), then holds at 280 gracefully
|
| 2523 |
+
// (rather than looking frozen mid-count) for the remainder of a slow scan.
|
| 2524 |
let count=0;
|
| 2525 |
countTimer=setInterval(()=>{
|
| 2526 |
const target=280, elapsed=(Date.now()-scanStartTime)/1000;
|
|
|
|
| 2531 |
},120);
|
| 2532 |
}
|
| 2533 |
|
| 2534 |
+
function startStep4Wait() {
|
| 2535 |
+
clearInterval(step4WaitTimer);
|
| 2536 |
+
step4MsgIdx=0;
|
| 2537 |
+
step4WaitTimer=setInterval(()=>{
|
| 2538 |
+
step4MsgIdx=(step4MsgIdx+1)%STEP4_WAIT_MESSAGES.length;
|
| 2539 |
+
const sd4sub=document.querySelector('#sd4 .step-sub');
|
| 2540 |
+
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[step4MsgIdx];
|
| 2541 |
+
const t=$('st4'); if(t) t.textContent=Math.floor((Date.now()-scanStartTime)/1000)+'s';
|
| 2542 |
+
},5000);
|
| 2543 |
+
}
|
| 2544 |
+
|
| 2545 |
function activateStep(n) {
|
| 2546 |
if(n>4) return;
|
| 2547 |
const el=$('sd'+n); if(!el) return;
|
|
|
|
| 2557 |
}
|
| 2558 |
|
| 2559 |
function stopLoaderSteps() {
|
| 2560 |
+
clearInterval(stepTimer); clearInterval(elapsedTimer); clearInterval(countTimer); clearInterval(step4WaitTimer);
|
| 2561 |
+
const sd4sub=document.querySelector('#sd4 .step-sub');
|
| 2562 |
+
if(sd4sub) sd4sub.textContent=STEP4_WAIT_MESSAGES[0];
|
| 2563 |
for(let i=1;i<=4;i++) markDone(i);
|
| 2564 |
const sc=$('scan-count'); if(sc) sc.textContent='280+';
|
| 2565 |
stopSymbolStream();
|
|
|
|
| 2966 |
|
| 2967 |
/* ═══ FLIP re-rank + never-blank auto-refresh ═══ */
|
| 2968 |
function flipReorder(container, mutation) {
|
| 2969 |
+
// Keyed by data-sym (stable identity) rather than DOM element reference: refreshScanQuietly
|
| 2970 |
+
// replaces unchanged cards with brand-new nodes via replaceWith(), so an element-reference
|
| 2971 |
+
// keyed map would never match post-mutation and the FLIP transform would always be skipped.
|
| 2972 |
+
const before = new Map([...container.children]
|
| 2973 |
+
.filter(el => el.dataset && el.dataset.sym)
|
| 2974 |
+
.map(el => [el.dataset.sym, el.getBoundingClientRect()]));
|
| 2975 |
mutation();
|
| 2976 |
const reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
| 2977 |
if (reduced) return;
|
| 2978 |
[...container.children].forEach(el => {
|
| 2979 |
+
const key = el.dataset && el.dataset.sym;
|
| 2980 |
+
const f = key ? before.get(key) : null;
|
| 2981 |
+
if (!f) return; // brand-new/unkeyed element — handled by its own reveal animation
|
| 2982 |
const l = el.getBoundingClientRect();
|
| 2983 |
const dx = f.left - l.left, dy = f.top - l.top;
|
| 2984 |
if (dx || dy) {
|