diff --git a/modules/boersenchecker/assets/boersenchecker-native.js b/modules/boersenchecker/assets/boersenchecker-native.js new file mode 100644 index 00000000..545a4450 --- /dev/null +++ b/modules/boersenchecker/assets/boersenchecker-native.js @@ -0,0 +1,114 @@ +(function () { + 'use strict'; + + function toPartialUrl(input, fallbackView) { + const url = new URL(input, window.location.origin); + if (!url.searchParams.has('view') && fallbackView) { + url.searchParams.set('view', fallbackView); + } + url.searchParams.set('partial', '1'); + return url; + } + + async function fetchHtml(url, options) { + const response = await fetch(url.toString(), { + credentials: 'same-origin', + headers: { + Accept: 'text/html', + 'X-Requested-With': 'XMLHttpRequest' + }, + ...options + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return response.text(); + } + + function enhanceHost(host, state) { + host.querySelectorAll('a[href]').forEach((link) => { + const href = link.getAttribute('href') || ''; + if (!href.startsWith('/apps/boersenchecker')) { + return; + } + + link.addEventListener('click', (event) => { + event.preventDefault(); + state.load(href); + }); + }); + + host.querySelectorAll('form').forEach((form) => { + form.addEventListener('submit', async (event) => { + event.preventDefault(); + + const method = String(form.getAttribute('method') || 'get').toUpperCase(); + const action = form.getAttribute('action') || state.currentUrl.toString(); + const formData = new FormData(form); + const url = toPartialUrl(action, state.defaultView); + + try { + if (method === 'GET') { + const next = new URL(url.toString()); + next.search = ''; + next.searchParams.set('partial', '1'); + for (const [key, value] of formData.entries()) { + next.searchParams.append(key, String(value)); + } + await state.load(next.toString()); + return; + } + + const html = await fetchHtml(url, { + method, + body: formData + }); + state.render(html, url); + } catch (error) { + state.renderError(error); + } + }); + }); + } + + window.initBoersencheckerApp = function initBoersencheckerApp(host, options) { + if (!(host instanceof HTMLElement)) { + return; + } + + const entryRoute = String(options?.entryRoute || '/apps/boersenchecker'); + const defaultView = String(options?.defaultView || 'overview'); + + const state = { + defaultView, + currentUrl: toPartialUrl(entryRoute, defaultView), + async load(targetUrl) { + const url = toPartialUrl(targetUrl, defaultView); + this.currentUrl = url; + host.innerHTML = '

Börsenchecker wird geladen...

'; + try { + const html = await fetchHtml(url); + this.render(html, url); + } catch (error) { + this.renderError(error); + } + }, + render(html, url) { + this.currentUrl = url; + host.innerHTML = html; + enhanceHost(host, this); + if (typeof window.initBoersencheckerCharts === 'function') { + window.initBoersencheckerCharts(host); + } + }, + renderError(error) { + const message = error instanceof Error ? error.message : 'Börsenchecker konnte nicht geladen werden.'; + host.innerHTML = `

${message}

`; + } + }; + + state.load(entryRoute); + }; +})(); diff --git a/modules/boersenchecker/assets/boersenchecker.js b/modules/boersenchecker/assets/boersenchecker.js index b53063f2..2e57edaf 100644 --- a/modules/boersenchecker/assets/boersenchecker.js +++ b/modules/boersenchecker/assets/boersenchecker.js @@ -1,152 +1,168 @@ (function () { - const app = document.querySelector('[data-bc-home]'); - if (!app) return; + 'use strict'; - const chartShell = app.querySelector('[data-bc-chart]'); - const instrumentSelect = app.querySelector('[data-bc-instrument]'); - const instrumentNameNode = app.querySelector('[data-bc-instrument-name]'); - const instrumentMetaNode = app.querySelector('[data-bc-instrument-meta]'); - const rangeButtons = Array.from(app.querySelectorAll('[data-range]')); - const statusNode = app.querySelector('[data-bc-chart-status]'); - const summaryNode = app.querySelector('[data-bc-chart-summary]'); - const endpoint = app.getAttribute('data-chart-endpoint') || ''; - const instrumentsScript = app.querySelector('[data-bc-instruments-json]'); - const instrumentMap = new Map(); - if (instrumentsScript?.textContent) { - try { - const items = JSON.parse(instrumentsScript.textContent); - if (Array.isArray(items)) { - items.forEach((item) => instrumentMap.set(String(item.instrument_id), item)); + function initBoersencheckerCharts(root) { + const scope = root instanceof Element ? root : document; + const app = scope.matches?.('[data-bc-home]') ? scope : scope.querySelector('[data-bc-home]'); + if (!app || app.dataset.bcChartBound === '1') return; + app.dataset.bcChartBound = '1'; + + const chartShell = app.querySelector('[data-bc-chart]'); + const instrumentSelect = app.querySelector('[data-bc-instrument]'); + const instrumentNameNode = app.querySelector('[data-bc-instrument-name]'); + const instrumentMetaNode = app.querySelector('[data-bc-instrument-meta]'); + const rangeButtons = Array.from(app.querySelectorAll('[data-range]')); + const statusNode = app.querySelector('[data-bc-chart-status]'); + const summaryNode = app.querySelector('[data-bc-chart-summary]'); + const endpoint = app.getAttribute('data-bc-chart-endpoint') || app.getAttribute('data-chart-endpoint') || ''; + const instrumentsScript = app.querySelector('[data-bc-instruments-json]'); + const instrumentMap = new Map(); + + if (instrumentsScript?.textContent) { + try { + const items = JSON.parse(instrumentsScript.textContent); + if (Array.isArray(items)) { + items.forEach((item) => instrumentMap.set(String(item.instrument_id), item)); + } + } catch (_error) {} + } + + let activeRange = '1m'; + let currentPayload = null; + + function pointsForRange(payload, range) { + if (!payload) return []; + const daily = payload.daily || []; + const weekly = payload.weekly || []; + const monthly = payload.monthly || []; + switch (range) { + case '1d': return daily.slice(-2); + case '5d': return daily.slice(-5); + case '1m': return daily.slice(-22); + case '3m': return daily.slice(-66); + case '6m': return weekly.slice(-26); + case '1y': return weekly.slice(-52); + case '5y': return monthly.slice(-60); + default: return daily.slice(-22); } - } catch (_error) {} - } - let activeRange = '1m'; - let currentPayload = null; - - function pointsForRange(payload, range) { - if (!payload) return []; - const daily = payload.daily || []; - const weekly = payload.weekly || []; - const monthly = payload.monthly || []; - switch (range) { - case '1d': return daily.slice(-2); - case '5d': return daily.slice(-5); - case '1m': return daily.slice(-22); - case '3m': return daily.slice(-66); - case '6m': return weekly.slice(-26); - case '1y': return weekly.slice(-52); - case '5y': return monthly.slice(-60); - default: return daily.slice(-22); - } - } - - function renderChart(points) { - if (!chartShell) return; - chartShell.classList.remove('bc-panel-fade'); - void chartShell.offsetWidth; - chartShell.classList.add('bc-panel-fade'); - - if (!points || points.length === 0) { - chartShell.innerHTML = '
Keine Chartdaten verfuegbar.
'; - return; } - const values = points.map((point) => Number(point.close || 0)); - const min = Math.min(...values); - const max = Math.max(...values); - const width = 920; - const height = 340; - const paddingX = 24; - const paddingY = 28; - const usableWidth = width - paddingX * 2; - const usableHeight = height - paddingY * 2; - const spread = max - min || 1; + function renderChart(points) { + if (!chartShell) return; + chartShell.classList.remove('bc-panel-fade'); + void chartShell.offsetWidth; + chartShell.classList.add('bc-panel-fade'); - const coords = points.map((point, index) => { - const x = paddingX + (usableWidth * index / Math.max(points.length - 1, 1)); - const y = paddingY + usableHeight - ((Number(point.close || 0) - min) / spread) * usableHeight; - return { x, y }; + if (!points || points.length === 0) { + chartShell.innerHTML = '
Keine Chartdaten verfuegbar.
'; + return; + } + + const values = points.map((point) => Number(point.close || 0)); + const min = Math.min(...values); + const max = Math.max(...values); + const width = 920; + const height = 340; + const paddingX = 24; + const paddingY = 28; + const usableWidth = width - paddingX * 2; + const usableHeight = height - paddingY * 2; + const spread = max - min || 1; + + const coords = points.map((point, index) => { + const x = paddingX + (usableWidth * index / Math.max(points.length - 1, 1)); + const y = paddingY + usableHeight - ((Number(point.close || 0) - min) / spread) * usableHeight; + return { x, y }; + }); + + const path = coords.map((coord, index) => `${index === 0 ? 'M' : 'L'}${coord.x.toFixed(2)},${coord.y.toFixed(2)}`).join(' '); + const area = `${path} L${coords[coords.length - 1].x.toFixed(2)},${height - paddingY} L${coords[0].x.toFixed(2)},${height - paddingY} Z`; + const grid = [0, 1, 2, 3].map((step) => { + const y = paddingY + (usableHeight * step / 3); + return ``; + }).join(''); + + chartShell.innerHTML = ` + + + + + + + + ${grid} + + + + `; + + const first = values[0]; + const last = values[values.length - 1]; + const delta = last - first; + const percent = first !== 0 ? (delta / first) * 100 : 0; + if (summaryNode) { + summaryNode.textContent = `${last.toFixed(2)} | ${delta >= 0 ? '+' : ''}${delta.toFixed(2)} (${percent.toFixed(2)}%)`; + } + } + + async function loadChart() { + const instrumentId = instrumentSelect ? instrumentSelect.value : ''; + if (!instrumentId || !endpoint) { + if (statusNode) statusNode.textContent = 'Keine Aktie fuer den Chart ausgewaehlt.'; + if (summaryNode) summaryNode.textContent = '-'; + if (chartShell) chartShell.innerHTML = '
Keine Chartdaten verfuegbar.
'; + return; + } + if (statusNode) statusNode.textContent = 'Chartdaten werden geladen...'; + try { + const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, { + credentials: 'same-origin', + headers: { Accept: 'application/json' } + }); + const payload = await response.json(); + if (!payload.ok) { + throw new Error(payload.message || 'Chartdaten konnten nicht geladen werden.'); + } + currentPayload = payload; + renderChart(pointsForRange(payload, activeRange)); + if (statusNode) { + const sourceLabel = payload.source_label || payload.source || 'Lokale Kurshistorie'; + const instrumentRef = payload.symbol || payload.isin || ''; + statusNode.textContent = instrumentRef + ? `Quelle: ${sourceLabel} | ${instrumentRef}` + : `Quelle: ${sourceLabel}`; + } + } catch (error) { + currentPayload = null; + if (chartShell) { + chartShell.innerHTML = `
${error instanceof Error ? error.message : 'Chartdaten konnten nicht geladen werden.'}
`; + } + if (statusNode) statusNode.textContent = 'Fehler beim Laden der Chartdaten.'; + } + } + + rangeButtons.forEach((button) => { + button.addEventListener('click', () => { + activeRange = button.getAttribute('data-range') || '1m'; + rangeButtons.forEach((item) => item.setAttribute('aria-pressed', item === button ? 'true' : 'false')); + renderChart(pointsForRange(currentPayload, activeRange)); + }); }); - const path = coords.map((coord, index) => `${index === 0 ? 'M' : 'L'}${coord.x.toFixed(2)},${coord.y.toFixed(2)}`).join(' '); - const area = `${path} L${coords[coords.length - 1].x.toFixed(2)},${height - paddingY} L${coords[0].x.toFixed(2)},${height - paddingY} Z`; - const grid = [0, 1, 2, 3].map((step) => { - const y = paddingY + (usableHeight * step / 3); - return ``; - }).join(''); - - chartShell.innerHTML = ` - - - - - - - - ${grid} - - - - `; - - const first = values[0]; - const last = values[values.length - 1]; - const delta = last - first; - const percent = first !== 0 ? (delta / first) * 100 : 0; - if (summaryNode) { - summaryNode.textContent = `${last.toFixed(2)} | ${delta >= 0 ? '+' : ''}${delta.toFixed(2)} (${percent.toFixed(2)}%)`; + if (instrumentSelect) { + instrumentSelect.addEventListener('change', () => { + const meta = instrumentMap.get(String(instrumentSelect.value)); + if (meta) { + if (instrumentNameNode) instrumentNameNode.textContent = meta.instrument_name || 'Keine Aktie ausgewaehlt'; + if (instrumentMetaNode) instrumentMetaNode.textContent = `${meta.symbol || ''} · ${meta.isin || '-'}`; + } + loadChart(); + }); } + + loadChart(); } - async function loadChart() { - const instrumentId = instrumentSelect ? instrumentSelect.value : ''; - if (!instrumentId || !endpoint) { - if (statusNode) statusNode.textContent = 'Keine Aktie fuer den Chart ausgewaehlt.'; - if (summaryNode) summaryNode.textContent = '-'; - if (chartShell) chartShell.innerHTML = '
Keine Chartdaten verfuegbar.
'; - return; - } - if (statusNode) statusNode.textContent = 'Chartdaten werden geladen...'; - try { - const response = await fetch(`${endpoint}${endpoint.includes('?') ? '&' : '?'}instrument_id=${encodeURIComponent(instrumentId)}`, { headers: { Accept: 'application/json' } }); - const payload = await response.json(); - if (!payload.ok) { - throw new Error(payload.message || 'Chartdaten konnten nicht geladen werden.'); - } - currentPayload = payload; - renderChart(pointsForRange(payload, activeRange)); - if (statusNode) { - const sourceLabel = payload.source_label || payload.source || 'Lokale Kurshistorie'; - const instrumentRef = payload.symbol || payload.isin || ''; - statusNode.textContent = instrumentRef - ? `Quelle: ${sourceLabel} | ${instrumentRef}` - : `Quelle: ${sourceLabel}`; - } - } catch (error) { - currentPayload = null; - chartShell.innerHTML = `
${error.message}
`; - if (statusNode) statusNode.textContent = 'Fehler beim Laden der Chartdaten.'; - } - } - - rangeButtons.forEach((button) => { - button.addEventListener('click', () => { - activeRange = button.getAttribute('data-range') || '1m'; - rangeButtons.forEach((item) => item.setAttribute('aria-pressed', item === button ? 'true' : 'false')); - renderChart(pointsForRange(currentPayload, activeRange)); - }); - }); - - if (instrumentSelect) { - instrumentSelect.addEventListener('change', () => { - const meta = instrumentMap.get(String(instrumentSelect.value)); - if (meta) { - if (instrumentNameNode) instrumentNameNode.textContent = meta.instrument_name || 'Keine Aktie ausgewaehlt'; - if (instrumentMetaNode) instrumentMetaNode.textContent = `${meta.symbol || ''} · ${meta.isin || '-'}`; - } - loadChart(); - }); - } - - loadChart(); + window.initBoersencheckerCharts = initBoersencheckerCharts; + initBoersencheckerCharts(document); })(); diff --git a/modules/boersenchecker/desktop.php b/modules/boersenchecker/desktop.php index feba66df..1b0b1967 100644 --- a/modules/boersenchecker/desktop.php +++ b/modules/boersenchecker/desktop.php @@ -15,11 +15,27 @@ return [ 'icon' => 'BC', 'entry_route' => '/apps/boersenchecker', 'window_mode' => 'window', - 'content_mode' => 'iframe', + 'content_mode' => 'native-module', 'default_width' => 1220, 'default_height' => 860, 'supports_widget' => false, 'supports_tray' => false, 'module_name' => 'finance', 'summary' => (string) ($manifest['description'] ?? 'Depotverwaltung fuer Aktien, Kaufdaten, Kursverlauf und Waehrungsumrechnung.'), + 'native_module' => [ + 'initializer' => 'initBoersencheckerApp', + 'options' => [ + 'entryRoute' => '/apps/boersenchecker', + 'defaultView' => 'overview', + ], + 'assets' => [ + 'styles' => [ + ['href' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.css&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker.css') ? filemtime(__DIR__ . '/assets/boersenchecker.css') : '0'))], + ], + 'scripts' => [ + ['src' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker.js') ? filemtime(__DIR__ . '/assets/boersenchecker.js') : '0'))], + ['src' => '/module-assets/index.php?module=boersenchecker&path=assets/boersenchecker-native.js&v=' . rawurlencode((string) (is_file(__DIR__ . '/assets/boersenchecker-native.js') ? filemtime(__DIR__ . '/assets/boersenchecker-native.js') : '0'))], + ], + ], + ], ]; diff --git a/modules/boersenchecker/pages/index.php b/modules/boersenchecker/pages/index.php index 95c8dac7..6f99f7c6 100644 --- a/modules/boersenchecker/pages/index.php +++ b/modules/boersenchecker/pages/index.php @@ -11,6 +11,7 @@ session_start(); $projectRoot = dirname(__DIR__, 3); ModuleHttp::requireDesktopAccess($projectRoot); +$isPartial = !empty($_GET['partial']); $view = trim((string) ($_GET['view'] ?? 'overview')); $allowedViews = ['overview', 'depotverwaltung', 'aktienverwaltung', 'setup']; @@ -149,6 +150,36 @@ $renderSetup = static function (): void { +
+ handle()); + } elseif ($view === 'aktienverwaltung') { + $page = new \Modules\Boersenchecker\Support\InstrumentPage(); + module_tpl('boersenchecker', 'instruments', $page->handle()); + } else { + $page = new \Modules\Boersenchecker\Support\HomePage(); + module_tpl('boersenchecker', 'home', $page->handle()); + } + ?> +
+ @@ -250,22 +281,7 @@ $renderSetup = static function (): void { -
- handle()); - } elseif ($view === 'aktienverwaltung') { - $page = new \Modules\Boersenchecker\Support\InstrumentPage(); - module_tpl('boersenchecker', 'instruments', $page->handle()); - } else { - $page = new \Modules\Boersenchecker\Support\HomePage(); - module_tpl('boersenchecker', 'home', $page->handle()); - } - ?> -
+ diff --git a/partials/desktop/shell.php b/partials/desktop/shell.php index 31aed278..2807671a 100644 --- a/partials/desktop/shell.php +++ b/partials/desktop/shell.php @@ -178,6 +178,11 @@ $renderStartIcon = static function (array $profile): string { + diff --git a/public/assets/desktop/desktop.css b/public/assets/desktop/desktop.css index f617b9db..1e20a94a 100644 --- a/public/assets/desktop/desktop.css +++ b/public/assets/desktop/desktop.css @@ -1405,6 +1405,14 @@ h1 { background: rgba(255, 255, 255, 0.12); } +.tray-menu-copy { + margin: 0; + padding: 6px 4px 2px; + color: #cbd5e1; + font-size: 12px; + line-height: 1.45; +} + .window-content p:last-child { margin-bottom: 0; } diff --git a/public/assets/desktop/desktop.js b/public/assets/desktop/desktop.js index 7c62ec72..0f33bb2a 100644 --- a/public/assets/desktop/desktop.js +++ b/public/assets/desktop/desktop.js @@ -17,6 +17,8 @@ if (payloadNode) { const startMenuUserAvatar = document.getElementById('start-menu-user-avatar'); const startMenuSessionAction = document.getElementById('start-menu-session-action'); const accountMenu = document.getElementById('tray-account-menu'); + const fxTrayMenu = document.getElementById('tray-fx-menu'); + const fxTrayStatusNode = document.getElementById('tray-fx-status'); const clockNode = document.getElementById('clock'); const clockTopNode = document.getElementById('clock-top'); const debugToggleNode = document.getElementById('debug-toggle'); @@ -389,6 +391,88 @@ if (payloadNode) { accountButton.setAttribute('aria-expanded', String(isOpen)); }; + const setFxTrayMenuOpen = (isOpen) => { + const fxButton = document.querySelector('.tray-pill[data-tray-app-id="fx-rates"]'); + + if (!(fxTrayMenu instanceof HTMLElement) || !(fxButton instanceof HTMLElement)) { + return; + } + + fxTrayMenu.hidden = !isOpen; + fxButton.setAttribute('aria-expanded', String(isOpen)); + }; + + const loadFxTrayStatus = async () => { + const fxButton = document.querySelector('.tray-pill[data-tray-app-id="fx-rates"]'); + + if (!(fxTrayStatusNode instanceof HTMLElement) || !(fxButton instanceof HTMLElement)) { + return null; + } + + fxTrayStatusNode.textContent = 'Kursstatus wird geladen ...'; + fxButton.setAttribute('title', 'Kursstatus wird geladen ...'); + + try { + const response = await fetch('/api/fx-rates/index.php?path=v1/status', { + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + const payload = await response.json().catch(() => ({})); + const rows = Array.isArray(payload?.data) ? payload.data : []; + const latest = rows.slice().sort((left, right) => { + const leftTs = new Date(left?.fetched_at || 0).getTime(); + const rightTs = new Date(right?.fetched_at || 0).getTime(); + return rightTs - leftTs; + })[0] || null; + const statusText = latest + ? `Letzter Abruf: ${latest.fetched_at_display || latest.fetched_at} · ${latest.base_currency} · ${latest.provider}` + : 'Noch keine gespeicherten Kurse vorhanden.'; + + fxTrayStatusNode.textContent = statusText; + fxButton.setAttribute('title', statusText); + return latest; + } catch (_error) { + const errorText = 'Kursstatus konnte nicht geladen werden.'; + fxTrayStatusNode.textContent = errorText; + fxButton.setAttribute('title', errorText); + return null; + } + }; + + const runFxTrayRefresh = async (force) => { + if (!(fxTrayStatusNode instanceof HTMLElement)) { + return; + } + + fxTrayStatusNode.textContent = 'Aktualisierung laeuft ...'; + try { + const response = await fetch('/api/fx-rates/index.php?path=v1/refresh', { + method: 'POST', + credentials: 'same-origin', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + force: force === true, + trigger_source: 'tray', + }), + }); + const payload = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(payload?.context?.message || payload?.error || `HTTP ${response.status}`); + } + + const result = payload?.data || {}; + fxTrayStatusNode.textContent = result.reused + ? 'Kein neuer API-Abruf. Snapshot ist noch jung genug.' + : `Kurse aktualisiert. ${result.updated_count || 0} Werte gespeichert.`; + await loadFxTrayStatus(); + } catch (error) { + fxTrayStatusNode.textContent = error instanceof Error ? error.message : 'Aktualisierung fehlgeschlagen.'; + } + }; + const findAppById = (appId) => payload.apps.find((app) => app.app_id === appId) || null; const dispatchUserSettingsUpdate = (detail) => { @@ -1711,12 +1795,17 @@ if (payloadNode) { if (!(event.target instanceof Element) || !event.target.closest('.tray-pill[data-tray-id="account"], #tray-account-menu')) { setAccountMenuOpen(false); } + + if (!(event.target instanceof Element) || !event.target.closest('.tray-pill[data-tray-app-id="fx-rates"], #tray-fx-menu')) { + setFxTrayMenuOpen(false); + } }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closeDesktopContextMenu(); setAccountMenuOpen(false); + setFxTrayMenuOpen(false); } }); @@ -1729,6 +1818,7 @@ if (payloadNode) { const isClosed = startMenu.hasAttribute('hidden'); setStartMenuOpen(isClosed); setAccountMenuOpen(false); + setFxTrayMenuOpen(false); }); document.querySelector('.tray-pill[data-tray-id="account"]')?.addEventListener('click', () => { @@ -1738,20 +1828,50 @@ if (payloadNode) { setStartMenuOpen(false); setAccountMenuOpen(accountMenu.hidden); + setFxTrayMenuOpen(false); }); document.querySelectorAll('.tray-pill[data-tray-app-id]').forEach((button) => { button.addEventListener('click', () => { const appId = button.getAttribute('data-tray-app-id') || ''; + if (appId === 'fx-rates') { + setStartMenuOpen(false); + setAccountMenuOpen(false); + setFxTrayMenuOpen(fxTrayMenu instanceof HTMLElement ? fxTrayMenu.hidden : false); + void loadFxTrayStatus(); + return; + } const app = findAppById(appId); if (app) { openApp(app); setStartMenuOpen(false); setAccountMenuOpen(false); + setFxTrayMenuOpen(false); } }); }); + fxTrayMenu?.addEventListener('click', (event) => { + const target = event.target instanceof Element ? event.target.closest('[data-fx-action]') : null; + + if (!target) { + return; + } + + const action = target.getAttribute('data-fx-action'); + + if (action === 'refresh') { + void runFxTrayRefresh(false); + return; + } + + if (action === 'force-refresh') { + void runFxTrayRefresh(true); + } + }); + + void loadFxTrayStatus(); + debugToggleNode?.addEventListener('click', () => { toggleDebugWindow(); });