ebc33a78b7
- server/: Node bridge (datarefs/commands, navdata, CIFP procedures, flight plan) - web/: React cockpit (PFD/MFD/Map, VFR six-pack, AFCS, FMS CDU), PWA, collapsible sidebar - desktop/: Tauri 2 launcher (Bun sidecar, system tray, updater) + Linux build via Docker - scripts/: prep-desktop, build-linux, Gitea release + latest.json Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
7.3 KiB
JavaScript
195 lines
7.3 KiB
JavaScript
// Control-panel logic. Uses the global Tauri API (withGlobalTauri).
|
||
const T = window.__TAURI__ || {};
|
||
const invoke = T.core.invoke;
|
||
const listen = T.event.listen;
|
||
|
||
const $ = (id) => document.getElementById(id);
|
||
const xpPath = $('xpPath'), portEl = $('port'), demoEl = $('demo');
|
||
const startBtn = $('startBtn'), liveCard = $('liveCard'), urlEl = $('url');
|
||
const statusEl = $('status'), statusText = $('statusText'), logEl = $('log');
|
||
|
||
let running = false, healthTimer = null;
|
||
|
||
function setStatus(kind, text) {
|
||
statusEl.className = 'status ' + kind;
|
||
statusText.textContent = text;
|
||
}
|
||
|
||
async function validatePath() {
|
||
const p = xpPath.value.trim();
|
||
const hint = $('xpHint');
|
||
if (!p) { hint.textContent = ''; hint.className = 'hint'; return false; }
|
||
const ok = await invoke('valid_xplane_path', { path: p });
|
||
hint.textContent = ok ? '✓ X-Plane erkannt' : '⚠ kein „Resources/default data“ — Demo-Modus nutzen oder Pfad prüfen';
|
||
hint.className = 'hint ' + (ok ? 'ok' : 'bad');
|
||
return ok;
|
||
}
|
||
|
||
// Is the chosen port free? If not, offer the next free one.
|
||
async function validatePort() {
|
||
const hint = $('portHint');
|
||
const port = parseInt(portEl.value, 10) || 0;
|
||
if (port < 1024 || port > 65535) { hint.textContent = '⚠ Port 1024–65535'; hint.className = 'hint bad'; return false; }
|
||
const free = await invoke('port_free', { port });
|
||
if (free) { hint.textContent = '✓ Port frei'; hint.className = 'hint ok'; return true; }
|
||
const alt = await invoke('suggest_port', { start: port + 1 });
|
||
hint.innerHTML = `⚠ Port ${port} belegt — <a href="#" id="usePort">${alt} verwenden</a>`;
|
||
hint.className = 'hint bad';
|
||
const a = $('usePort');
|
||
if (a) a.onclick = (e) => { e.preventDefault(); portEl.value = alt; validatePort(); };
|
||
return false;
|
||
}
|
||
|
||
async function init() {
|
||
try { $('ver').textContent = 'v' + (await T.app.getVersion()); } catch {}
|
||
try {
|
||
const def = await invoke('default_xplane_path');
|
||
if (def) { xpPath.value = def; validatePath(); }
|
||
} catch {}
|
||
validatePort();
|
||
checkUpdate(true); // silent on launch
|
||
}
|
||
|
||
xpPath.addEventListener('change', validatePath);
|
||
xpPath.addEventListener('blur', validatePath);
|
||
portEl.addEventListener('change', validatePort);
|
||
portEl.addEventListener('blur', validatePort);
|
||
|
||
$('browse').addEventListener('click', async () => {
|
||
try {
|
||
const dir = await T.dialog.open({ directory: true, multiple: false, title: 'X-Plane 12 Ordner wählen' });
|
||
if (dir) { xpPath.value = dir; validatePath(); }
|
||
} catch (e) { appendLog('dialog: ' + e); }
|
||
});
|
||
|
||
startBtn.addEventListener('click', async () => {
|
||
if (running) return stop();
|
||
if (!(await validatePort())) return; // refuse a busy port up front
|
||
startBtn.disabled = true;
|
||
try {
|
||
const info = await invoke('start_server', {
|
||
xplanePath: xpPath.value.trim(),
|
||
port: parseInt(portEl.value, 10) || 8080,
|
||
demo: demoEl.checked,
|
||
});
|
||
running = true;
|
||
urlEl.textContent = info.url;
|
||
liveCard.classList.remove('hidden');
|
||
startBtn.textContent = 'Server stoppen';
|
||
startBtn.classList.add('stop');
|
||
setStatus('warn', 'Server läuft · warte auf Sim');
|
||
pollHealth(info.port);
|
||
} catch (e) {
|
||
appendLog('Fehler: ' + e);
|
||
setStatus('off', 'Fehler');
|
||
} finally {
|
||
startBtn.disabled = false;
|
||
}
|
||
});
|
||
|
||
async function stop() {
|
||
startBtn.disabled = true;
|
||
try { await invoke('stop_server'); } catch (e) { appendLog('stop: ' + e); }
|
||
resetUi();
|
||
startBtn.disabled = false;
|
||
}
|
||
|
||
function resetUi() {
|
||
running = false;
|
||
liveCard.classList.add('hidden');
|
||
startBtn.textContent = 'Server starten';
|
||
startBtn.classList.remove('stop');
|
||
setStatus('off', 'Gestoppt');
|
||
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
||
}
|
||
|
||
function pollHealth(port) {
|
||
if (healthTimer) clearInterval(healthTimer);
|
||
const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs');
|
||
const check = async () => {
|
||
try {
|
||
const r = await fetch(`http://127.0.0.1:${port}/api/health`, { cache: 'no-store' });
|
||
const d = await r.json();
|
||
const sim = d.xpConnected;
|
||
if (sim) setStatus('run', demoEl.checked ? 'Demo läuft' : 'X-Plane verbunden');
|
||
else setStatus('warn', 'Server läuft · kein Sim');
|
||
dXp.textContent = sim ? (demoEl.checked ? 'Demo' : 'verbunden') : 'kein Sim';
|
||
dXp.className = sim ? 'ok' : 'warn';
|
||
dClients.textContent = d.clients ?? 0;
|
||
const n = d.nav || {};
|
||
dNav.textContent = n.loaded ? `${n.airports ?? 0} APT · ${n.navaids ?? 0} Navaids` : 'lädt…';
|
||
dRefs.textContent = d.datarefs ?? 0;
|
||
} catch { setStatus('warn', 'Server läuft'); }
|
||
};
|
||
check();
|
||
healthTimer = setInterval(check, 3000);
|
||
}
|
||
|
||
$('copy').addEventListener('click', async () => {
|
||
try { await navigator.clipboard.writeText(urlEl.textContent); $('copy').textContent = '✓'; setTimeout(() => ($('copy').textContent = '⧉'), 1200); } catch {}
|
||
});
|
||
|
||
const openUrl = (u) => { try { T.opener.openUrl(u); } catch (e) { appendLog('open: ' + e); } };
|
||
$('openBtn').addEventListener('click', () => openUrl(urlEl.textContent));
|
||
document.querySelectorAll('.quick .btn').forEach((b) =>
|
||
b.addEventListener('click', () => openUrl(urlEl.textContent + '/#' + b.dataset.page)));
|
||
|
||
function appendLog(line) {
|
||
logEl.textContent += line;
|
||
if (logEl.textContent.length > 8000) logEl.textContent = logEl.textContent.slice(-6000);
|
||
logEl.scrollTop = logEl.scrollHeight;
|
||
}
|
||
listen('server-log', (e) => appendLog(e.payload));
|
||
listen('server-exited', () => { appendLog('\n[Server beendet]\n'); resetUi(); });
|
||
|
||
// Tray actions routed to the panel (which holds the current URL + start logic).
|
||
listen('tray-open', () => { if (urlEl.textContent && urlEl.textContent !== '—') openUrl(urlEl.textContent); });
|
||
listen('tray-toggle', () => startBtn.click());
|
||
|
||
/* ---------------- updates ---------------- */
|
||
let pendingUpdate = null;
|
||
async function checkUpdate(silent) {
|
||
const btn = $('updateBtn');
|
||
if (!silent) { btn.disabled = true; btn.textContent = 'Suche…'; }
|
||
try {
|
||
const update = await T.updater.check();
|
||
if (update) {
|
||
pendingUpdate = update;
|
||
$('ubTitle').textContent = `Update ${update.version} verfügbar`;
|
||
$('ubNotes').textContent = update.body || '';
|
||
$('updateBanner').classList.remove('hidden');
|
||
if (!document.querySelector('.update-badge')) {
|
||
const dot = document.createElement('span'); dot.className = 'update-badge'; btn.after(dot);
|
||
}
|
||
if (!silent) { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; }
|
||
} else if (!silent) {
|
||
btn.textContent = 'Aktuell ✓';
|
||
setTimeout(() => { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; }, 2500);
|
||
}
|
||
} catch (e) {
|
||
if (!silent) {
|
||
appendLog('update: ' + e);
|
||
btn.textContent = 'Update fehlgeschlagen';
|
||
setTimeout(() => { btn.textContent = 'Nach Updates suchen'; btn.disabled = false; }, 2500);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function installUpdate() {
|
||
if (!pendingUpdate) return;
|
||
$('ubInstall').disabled = true; $('ubInstall').textContent = 'Lädt…';
|
||
try {
|
||
await pendingUpdate.downloadAndInstall();
|
||
await T.process.relaunch();
|
||
} catch (e) {
|
||
appendLog('install: ' + e);
|
||
$('ubInstall').disabled = false; $('ubInstall').textContent = 'Installieren';
|
||
}
|
||
}
|
||
|
||
$('updateBtn').addEventListener('click', () => checkUpdate(false));
|
||
$('ubInstall').addEventListener('click', installUpdate);
|
||
$('ubDismiss').addEventListener('click', () => $('updateBanner').classList.add('hidden'));
|
||
|
||
init();
|