// Student portal · Signals feed (read-only). window.SignalsFeed // Shown at the top of the dashboard to PAID students (basic/inter/adv). // Polls every 15s; chimes + toast on a fresh signal. Same signal rows the // coaches see; students cannot edit — they just read and (if pushed) also // get the Telegram DM from n8n. (function(){ const { useState, useEffect, useRef } = React; const SOUND_KEY='cg_signal_sound'; function soundOn(){ return localStorage.getItem(SOUND_KEY)!=='0'; } function chime(){ if(!soundOn()) return; try{ const AC=window.AudioContext||window.webkitAudioContext; if(!AC) return; const ac=chime._ac||(chime._ac=new AC()); if(ac.state==='suspended') ac.resume(); const now=ac.currentTime; [[784,0],[1046.5,0.1],[1318.5,0.2]].forEach(p=>{ const o=ac.createOscillator(),g=ac.createGain(); o.type='sine'; o.frequency.value=p[0]; o.connect(g); g.connect(ac.destination); const t=now+p[1]; g.gain.setValueAtTime(0.0001,t); g.gain.exponentialRampToValueAtTime(0.13,t+0.02); g.gain.exponentialRampToValueAtTime(0.0001,t+0.24); o.start(t); o.stop(t+0.26); }); }catch(e){} } function toast(text){ const el=document.createElement('div'); el.textContent=text; el.style.cssText='position:fixed;left:50%;bottom:26px;transform:translateX(-50%);z-index:9999;background:var(--navy-900);color:var(--ink);border:1px solid var(--border-gold);border-radius:999px;padding:11px 20px;font-family:var(--font-mono);font-size:.82rem;box-shadow:0 12px 40px rgba(0,0,0,.5)'; document.body.appendChild(el); setTimeout(()=>{ el.style.transition='opacity .5s'; el.style.opacity='0'; setTimeout(()=>el.remove(),500); },4200); } const fmtWhen = d => d ? new Date(d).toLocaleString(undefined,{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}) : ''; const STATUS_META={ open:{label:'Open',color:'var(--gold-2)',bg:'var(--gold-soft)'}, tp1_hit:{label:'TP1 hit — running',color:'#4a9eda',bg:'rgba(74,158,218,.12)'}, tp2_hit:{label:'TP2 hit — full win ✓',color:'var(--green)',bg:'rgba(46,160,90,.12)'}, sl_hit:{label:'SL hit ✕',color:'var(--red)',bg:'rgba(214,69,69,.12)'}, cancelled:{label:'Cancelled',color:'var(--ink-faint)',bg:'var(--surface-2)'}, closed:{label:'Closed',color:'#7aa2f7',bg:'rgba(122,162,247,.12)'} }; // Signal price columns are text — a coach may type "3412.50", "3412.5 (zone)" // or "3410 - 3414". Take the first number and ignore the rest; anything we // cannot read comes through as null so the student fills it in themselves. function firstNum(v){ if(v===null||v===undefined) return null; const m = String(v).replace(/,/g,'').match(/-?\d+(\.\d+)?/); return m ? Number(m[0]) : null; } // PH-time date of the signal, so the journal entry is dated the trading day the // student actually saw it, not the viewer's local calendar day. function phDateOf(iso){ const t = iso ? Date.parse(iso) : Date.now(); return new Date((isNaN(t)?Date.now():t) + 8*3600000).toISOString().slice(0,10); } function signalToEntry(r){ return { trade_date: phDateOf(r.created_at), pair: r.pair || '', direction: r.direction === 'sell' ? 'short' : 'long', entry_price: firstNum(r.entry), stop_loss: firstNum(r.stop_loss), take_profit: firstNum(r.take_profit_1 || r.take_profit), signal_id: r.id, notes: 'From coach signal' + (r.coach_name ? ' · ' + r.coach_name : '') + (r.timeframe ? ' · ' + r.timeframe : ''), }; } function Row({r, onImg, onLog}){ const sm=STATUS_META[r.status]||STATUS_META.open; const dc=r.direction==='buy'?'var(--green)':'var(--red)'; const faded = r.status==='cancelled'; return