const { createApp, ref, computed, watch, onMounted, onUnmounted } = Vue; const defaultPrefs = { base_tf: '1m', enabled: { ma: { '1d': [10, 20, 50, 100, 200], '4h': [], '1h': [] }, manual: true, auto: false }, hidden_levels_score: false, }; createApp({ setup() { const status = ref({ stream: 'disconnected', bars_held: {} }); const price = ref(null); const storedPrefs = localStorage.getItem('chart-layer-prefs'); const prefs = ref(storedPrefs ? JSON.parse(storedPrefs) : structuredClone(defaultPrefs)); const timeframe = ref(prefs.value.base_tf || '1m'); const levels = ref([]); const timeframes = ['1m', '2m', '5m', '15m', '30m', '1h', '4h', '1d']; const now = ref(Date.now()); let chartApi = null; let socket = null; let timer = null; const barAge = computed(() => { if (!status.value.last_bar_t) return '—'; const seconds = Math.max(0, Math.floor(now.value / 1000 - status.value.last_bar_t)); return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`; }); async function refreshStatus() { const response = await fetch('/api/status'); if (response.ok) status.value = await response.json(); } function connect() { const protocol = location.protocol === 'https:' ? 'wss' : 'ws'; socket = new WebSocket(`${protocol}://${location.host}/ws`); socket.onopen = () => { socket.send(JSON.stringify({ type: 'subscribe', tf: timeframe.value })); sendPrefs(); }; socket.onmessage = ({ data }) => { const message = JSON.parse(data); if (message.type === 'snapshot') { chartApi.setBars(message.bars); levels.value = message.levels || []; syncVisibleLevels(); price.value = message.price; } else if (message.type === 'bar') { chartApi.updateBar(message.bar); price.value = message.bar.c; status.value.last_bar_t = message.bar.t; } else if (message.type === 'levels') { levels.value = message.levels; syncVisibleLevels(); } }; socket.onclose = () => { status.value.stream = 'disconnected'; setTimeout(connect, 2000); }; } function selectTimeframe(tf) { timeframe.value = tf; prefs.value.base_tf = tf; if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'subscribe', tf })); } } function enabled(level) { if (level.kind === 'ma') return (prefs.value.enabled.ma[level.tf] || []).includes(level.period); if (level.kind === 'manual') return prefs.value.enabled.manual; return prefs.value.enabled.auto; } function syncVisibleLevels() { if (chartApi) chartApi.syncLevels(levels.value.filter(enabled)); } function sendPrefs() { if (socket?.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: 'prefs', ...prefs.value })); } } function allEnabled(tf) { const available = tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21]; return available.every(period => (prefs.value.enabled.ma[tf] || []).includes(period)); } function toggleGroup(tf, checked) { prefs.value.enabled.ma[tf] = checked ? (tf === '1d' ? [10, 20, 50, 100, 200] : [9, 21]) : []; } watch(prefs, () => { localStorage.setItem('chart-layer-prefs', JSON.stringify(prefs.value)); syncVisibleLevels(); sendPrefs(); }, { deep: true }); onMounted(() => { chartApi = new ConfluenceChart(); chartApi.create(document.getElementById('chart')); refreshStatus(); connect(); timer = setInterval(() => { now.value = Date.now(); refreshStatus(); }, 5000); }); onUnmounted(() => { clearInterval(timer); if (socket) socket.close(); if (chartApi) chartApi.destroy(); }); return { status, price, barAge, timeframe, timeframes, prefs, selectTimeframe, allEnabled, toggleGroup }; }, }).mount('#app');