Next
All checks were successful
Deploy / deploy-staging (push) Successful in 24s
Deploy / deploy-production (push) Has been skipped

adas
This commit is contained in:
2026-06-22 01:44:06 +02:00
parent f83a64b854
commit f9e41380b5
29 changed files with 3433 additions and 17 deletions

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
use ModulesCore\ModuleHttp;
session_start();
$projectRoot = dirname(__DIR__, 3);
ModuleHttp::requireDesktopAccess($projectRoot, true);
require $projectRoot . '/modules/fx-rates/api/index.php';

View File

@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
require dirname(__DIR__, 3) . '/modules/fx-rates/pages/index.php';

View File

@@ -286,6 +286,9 @@ h1 {
.widget-card-actions {
margin-top: 14px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.widget-card-action {
@@ -306,6 +309,30 @@ h1 {
background: #0f172a;
}
.widget-card-action--secondary {
background: rgba(15, 23, 42, 0.16);
color: #0f172a;
}
.widget-card-action--secondary:hover {
background: rgba(15, 23, 42, 0.24);
}
.widget-card-status,
.widget-card-message {
margin: 0;
font-size: 14px;
line-height: 1.5;
}
.widget-card-status {
font-weight: 600;
}
.widget-card-message {
margin-top: 10px;
}
.widget-badge {
display: inline-grid;
place-items: center;

View File

@@ -481,18 +481,11 @@ if (payloadNode) {
`;
}
if (definition.content_mode === 'native-module' && definition.app_id === 'mining-checker') {
if (definition.content_mode === 'native-module') {
const hostId = `${String(definition.app_id || 'module').replace(/[^a-z0-9_-]/gi, '-')}-app`;
return `
<div class="window-content window-content--embedded window-content--module">
<div id="mining-checker-app" class="window-module-host" data-module-app="mining-checker"></div>
</div>
`;
}
if (definition.content_mode === 'native-module' && definition.app_id === 'user-self-management') {
return `
<div class="window-content window-content--embedded window-content--module">
<div id="user-self-management-app" class="window-module-host" data-module-app="user-self-management"></div>
<div id="${hostId}" class="window-module-host" data-module-app="${escapeHtml(definition.app_id || 'module')}"></div>
</div>
`;
}
@@ -1434,6 +1427,82 @@ if (payloadNode) {
});
};
const renderFxRatesWidget = async (card, widget) => {
const statusNode = card.querySelector('[data-widget-status]');
const messageNode = card.querySelector('[data-widget-message]');
const refreshButton = card.querySelector('[data-widget-refresh]');
const forceButton = card.querySelector('[data-widget-force-refresh]');
const loadStatus = async () => {
if (!(statusNode instanceof HTMLElement)) {
return;
}
statusNode.textContent = 'Kursstatus wird geladen ...';
try {
const response = await fetch(widget.status_api, {
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;
statusNode.textContent = latest
? `Letzter Abruf: ${latest.fetched_at_display || latest.fetched_at} · ${latest.base_currency} · ${latest.provider}`
: 'Noch keine gespeicherten Kurse vorhanden.';
} catch (_error) {
statusNode.textContent = 'Kursstatus konnte nicht geladen werden.';
}
};
const runRefresh = async (force) => {
if (!(messageNode instanceof HTMLElement)) {
return;
}
messageNode.textContent = 'Aktualisierung laeuft ...';
try {
const response = await fetch(widget.refresh_api, {
method: 'POST',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
force: force === true,
trigger_source: 'widget',
}),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.context?.message || payload?.error || `HTTP ${response.status}`);
}
const result = payload?.data || {};
messageNode.textContent = result.reused
? 'Kein neuer API-Abruf. Der letzte gespeicherte Snapshot ist noch jung genug.'
: `Kurse aktualisiert. ${result.updated_count || 0} Werte gespeichert.`;
await loadStatus();
} catch (error) {
messageNode.textContent = error instanceof Error ? error.message : 'Aktualisierung fehlgeschlagen.';
}
};
refreshButton?.addEventListener('click', () => runRefresh(false));
forceButton?.addEventListener('click', () => {
const confirmed = window.confirm('Der Kursabruf wird jetzt trotz jungem Snapshot erzwungen. Wirklich fortfahren?');
if (confirmed) {
runRefresh(true);
}
});
await loadStatus();
};
const renderWidgets = () => {
if (!widgetZoneRoot) {
return;
@@ -1455,6 +1524,17 @@ if (payloadNode) {
</div>
`
: '';
const widgetBodyMarkup = widget.widget_type === 'fx-rates-refresh'
? `
<div class="widget-card-status" data-widget-status>Wird geladen ...</div>
<p class="widget-card-message" data-widget-message>${widget.content || ''}</p>
<div class="widget-card-actions">
<button class="widget-card-action" type="button" data-widget-refresh>Kurse aktualisieren</button>
<button class="widget-card-action widget-card-action--secondary" type="button" data-widget-force-refresh>Force</button>
${widget.launch_app_id ? `<button class="widget-card-action widget-card-action--secondary" type="button" data-app-id="${escapeHtml(widget.launch_app_id)}">${escapeHtml(widget.action_label || 'Oeffnen')}</button>` : ''}
</div>
`
: `<p>${widget.content || ''}</p>${actionMarkup}`;
card.innerHTML = `
<div class="widget-card-header">
<div>
@@ -1463,21 +1543,23 @@ if (payloadNode) {
</div>
<span class="widget-badge">${widget.icon || 'WG'}</span>
</div>
<p>${widget.content || ''}</p>
${actionMarkup}
${widgetBodyMarkup}
`;
const actionButton = card.querySelector('.widget-card-action');
if (actionButton instanceof HTMLButtonElement && widget.launch_app_id) {
actionButton.addEventListener('click', () => {
const app = findAppById(widget.launch_app_id);
card.querySelectorAll('[data-app-id]').forEach((button) => {
button.addEventListener('click', () => {
const appId = button.getAttribute('data-app-id') || '';
const app = findAppById(appId);
if (app) {
openApp(app);
}
});
}
});
widgetZoneRoot.appendChild(card);
if (widget.widget_type === 'fx-rates-refresh') {
renderFxRatesWidget(card, widget);
}
});
};
@@ -1758,6 +1840,18 @@ if (payloadNode) {
}
});
window.addEventListener('desktop:open-app', (event) => {
const appId = typeof event.detail?.appId === 'string' ? event.detail.appId : '';
if (!appId) {
return;
}
const app = findAppById(appId);
if (app) {
openApp(app);
}
});
renderWidgets();
renderStartMenu();
updateDebugToggleUi();