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 = ``;
+ }
+ };
+
+ 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 = `
+
+ `;
+
+ 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 = `
-
- `;
-
- 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 {
= module_shell_footer() ?>
+
+ 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());
- }
- ?>
-
+ = $pageContent ?>
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();
});