Desktop: first-run setup wizard + FlyWithLua/Web-API/Lua-status guidance
Adds the four onboarding pieces that were missing:
- flywithlua_present Tauri command + wizard step that checks the plugin and
links the FlyWithLua NG+ download when it's absent.
- Wizard step explaining how to enable X-Plane's Web/REST API (Settings>Network).
- FlyWithLua-Sync status row in the live diagnostics, from /api/health.lua
('N Skripte aktiv' / 'FlyWithLua fehlt' / 'kein X-Plane').
- 4-step guided wizard (X-Plane folder → FlyWithLua → Web-API → install+start)
that auto-opens on first launch and is reachable via the header Einrichten
button; the final step hands off to the normal server start (auto-installs Lua).
Verified the wizard DOM flow + the dLua status against a live bridge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,19 @@ fn server_running(state: State<ServerState>) -> bool {
|
|||||||
state.child.lock().unwrap().is_some()
|
state.child.lock().unwrap().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Is FlyWithLua NG+ installed in this X-Plane? It's the prerequisite for the
|
||||||
|
// FMS/terrain sync — the bridge auto-installs OUR scripts into its Scripts
|
||||||
|
// folder, but only if the plugin itself is present. Checked by the setup wizard.
|
||||||
|
#[tauri::command]
|
||||||
|
fn flywithlua_present(path: String) -> bool {
|
||||||
|
!path.is_empty()
|
||||||
|
&& PathBuf::from(&path)
|
||||||
|
.join("Resources")
|
||||||
|
.join("plugins")
|
||||||
|
.join("FlyWithLua")
|
||||||
|
.is_dir()
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn start_server(
|
async fn start_server(
|
||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
@@ -203,6 +216,7 @@ pub fn run() {
|
|||||||
suggest_port,
|
suggest_port,
|
||||||
default_xplane_path,
|
default_xplane_path,
|
||||||
valid_xplane_path,
|
valid_xplane_path,
|
||||||
|
flywithlua_present,
|
||||||
server_running,
|
server_running,
|
||||||
start_server,
|
start_server,
|
||||||
stop_server
|
stop_server
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<div class="panel">
|
<div class="panel">
|
||||||
<header class="hd">
|
<header class="hd">
|
||||||
<div class="brand">G1000<span>·web</span></div>
|
<div class="brand">G1000<span>·web</span></div>
|
||||||
|
<button id="setupBtn" class="link" title="Einrichtungs-Assistent">⚙ Einrichten</button>
|
||||||
<div id="status" class="status off"><span class="dot"></span><span id="statusText">Gestoppt</span></div>
|
<div id="status" class="status off"><span class="dot"></span><span id="statusText">Gestoppt</span></div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
<div class="diag-row"><span>Verbundene Geräte</span><b id="dClients">—</b></div>
|
<div class="diag-row"><span>Verbundene Geräte</span><b id="dClients">—</b></div>
|
||||||
<div class="diag-row"><span>Navdata</span><b id="dNav">—</b></div>
|
<div class="diag-row"><span>Navdata</span><b id="dNav">—</b></div>
|
||||||
<div class="diag-row"><span>Datarefs</span><b id="dRefs">—</b></div>
|
<div class="diag-row"><span>Datarefs</span><b id="dRefs">—</b></div>
|
||||||
|
<div class="diag-row"><span>FlyWithLua-Sync</span><b id="dLua">—</b></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="asp-wrap">
|
<details class="asp-wrap">
|
||||||
@@ -88,6 +90,66 @@
|
|||||||
<button id="updateBtn" class="link">Nach Updates suchen</button>
|
<button id="updateBtn" class="link">Nach Updates suchen</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- First-run setup wizard: links X-Plane, checks FlyWithLua, installs the Lua
|
||||||
|
scripts, explains the Web-API, then starts. -->
|
||||||
|
<div id="wizard" class="wiz hidden">
|
||||||
|
<div class="wiz-box">
|
||||||
|
<div class="wiz-head">
|
||||||
|
<b>Einrichtung</b>
|
||||||
|
<div class="wiz-steps">
|
||||||
|
<span data-s="1" class="on">1</span><span data-s="2">2</span><span data-s="3">3</span><span data-s="4">4</span>
|
||||||
|
</div>
|
||||||
|
<button id="wizClose" class="wiz-x" title="Schließen">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 1: X-Plane folder -->
|
||||||
|
<div class="wiz-step" data-step="1">
|
||||||
|
<h3>1 · X-Plane 12 Ordner</h3>
|
||||||
|
<p>Wähle deinen X-Plane-12-Ordner. (Demo ohne X-Plane: einfach überspringen.)</p>
|
||||||
|
<div class="row">
|
||||||
|
<input id="wizPath" type="text" placeholder="z.B. /Users/du/X-Plane 12" spellcheck="false" />
|
||||||
|
<button id="wizBrowse" class="btn ghost">Suchen…</button>
|
||||||
|
</div>
|
||||||
|
<div id="wizPathHint" class="hint"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 2: FlyWithLua -->
|
||||||
|
<div class="wiz-step hidden" data-step="2">
|
||||||
|
<h3>2 · FlyWithLua NG+</h3>
|
||||||
|
<p>Für die zweiseitige FMS- und Terrain-Synchronisierung braucht X-Plane das kostenlose Plugin <b>FlyWithLua NG+</b>.</p>
|
||||||
|
<div id="wizFwl" class="wiz-status">—</div>
|
||||||
|
<p class="wiz-sub">Nicht installiert? <a href="#" id="wizFwlLink">FlyWithLua NG+ herunterladen</a>, in <code>X-Plane/Resources/plugins/</code> entpacken, X-Plane neu starten.</p>
|
||||||
|
<button id="wizFwlCheck" class="btn ghost sm">Erneut prüfen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 3: Web API -->
|
||||||
|
<div class="wiz-step hidden" data-step="3">
|
||||||
|
<h3>3 · X-Plane Web-API aktivieren</h3>
|
||||||
|
<p>Damit das Cockpit Daten bekommt, muss X-Planes Web-Server an sein:</p>
|
||||||
|
<ol class="wiz-ol">
|
||||||
|
<li>X-Plane → <b>Settings</b> → <b>Network</b></li>
|
||||||
|
<li>Bereich <b>„Web/REST API"</b> (X-Plane 12.1.1+)</li>
|
||||||
|
<li><b>„Enable web server"</b> / API einschalten</li>
|
||||||
|
</ol>
|
||||||
|
<p class="wiz-sub">Ob's klappt, siehst du nach dem Start am Status „X-Plane: verbunden".</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Step 4: install + start -->
|
||||||
|
<div class="wiz-step hidden" data-step="4">
|
||||||
|
<h3>4 · Lua installieren & starten</h3>
|
||||||
|
<p>Beim Start kopiert der Server die Begleit-Skripte automatisch nach <code>FlyWithLua/Scripts/</code> und verbindet sich mit X-Plane.</p>
|
||||||
|
<label class="toggle"><input id="wizDemo" type="checkbox" /><span>Demo-Modus (ohne X-Plane)</span></label>
|
||||||
|
<div id="wizResult" class="wiz-status">Bereit.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wiz-foot">
|
||||||
|
<button id="wizBack" class="btn ghost" disabled>Zurück</button>
|
||||||
|
<button id="wizNext" class="btn primary">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="main.js"></script>
|
<script src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+74
-1
@@ -107,9 +107,19 @@ function resetUi() {
|
|||||||
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
if (healthTimer) { clearInterval(healthTimer); healthTimer = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Human-readable FlyWithLua-install status from /api/health.lua.
|
||||||
|
function luaText(lua) {
|
||||||
|
if (!lua) return { t: '—', cls: '' };
|
||||||
|
if (lua.reason === 'no-xplane') return { t: 'kein X-Plane', cls: 'warn' };
|
||||||
|
if (lua.reason === 'no-flywithlua') return { t: 'FlyWithLua fehlt', cls: 'bad' };
|
||||||
|
if (lua.reason === 'no-source') return { t: 'Skripte fehlen', cls: 'bad' };
|
||||||
|
const n = (lua.installed?.length || 0) + (lua.updated?.length || 0) + (lua.unchanged?.length || 0);
|
||||||
|
return { t: `${n} Skripte aktiv`, cls: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
function pollHealth(port) {
|
function pollHealth(port) {
|
||||||
if (healthTimer) clearInterval(healthTimer);
|
if (healthTimer) clearInterval(healthTimer);
|
||||||
const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs');
|
const dXp = $('dXp'), dClients = $('dClients'), dNav = $('dNav'), dRefs = $('dRefs'), dLua = $('dLua');
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
try {
|
try {
|
||||||
const r = await fetch(`http://127.0.0.1:${port}/api/health`, { cache: 'no-store' });
|
const r = await fetch(`http://127.0.0.1:${port}/api/health`, { cache: 'no-store' });
|
||||||
@@ -123,6 +133,7 @@ function pollHealth(port) {
|
|||||||
const n = d.nav || {};
|
const n = d.nav || {};
|
||||||
dNav.textContent = n.loaded ? `${n.airports ?? 0} APT · ${n.navaids ?? 0} Navaids` : 'lädt…';
|
dNav.textContent = n.loaded ? `${n.airports ?? 0} APT · ${n.navaids ?? 0} Navaids` : 'lädt…';
|
||||||
dRefs.textContent = d.datarefs ?? 0;
|
dRefs.textContent = d.datarefs ?? 0;
|
||||||
|
if (dLua) { const l = luaText(d.lua); dLua.textContent = l.t; dLua.className = l.cls; }
|
||||||
} catch { setStatus('warn', 'Server läuft'); }
|
} catch { setStatus('warn', 'Server läuft'); }
|
||||||
};
|
};
|
||||||
check();
|
check();
|
||||||
@@ -242,4 +253,66 @@ $('updateBtn').addEventListener('click', () => checkUpdate(false));
|
|||||||
$('ubInstall').addEventListener('click', installUpdate);
|
$('ubInstall').addEventListener('click', installUpdate);
|
||||||
$('ubDismiss').addEventListener('click', () => $('updateBanner').classList.add('hidden'));
|
$('ubDismiss').addEventListener('click', () => $('updateBanner').classList.add('hidden'));
|
||||||
|
|
||||||
|
/* ---------------- first-run setup wizard ---------------- */
|
||||||
|
const FWL_URL = 'https://github.com/X-Friese/FlyWithLua/releases';
|
||||||
|
let wizStep = 1;
|
||||||
|
const wiz = $('wizard');
|
||||||
|
|
||||||
|
function showStep(n) {
|
||||||
|
wizStep = Math.max(1, Math.min(4, n));
|
||||||
|
document.querySelectorAll('.wiz-step').forEach((s) => s.classList.toggle('hidden', +s.dataset.step !== wizStep));
|
||||||
|
document.querySelectorAll('.wiz-steps span').forEach((s) => s.classList.toggle('on', +s.dataset.s <= wizStep));
|
||||||
|
$('wizBack').disabled = wizStep === 1;
|
||||||
|
$('wizNext').textContent = wizStep === 4 ? (demoEl.checked || $('wizDemo').checked ? 'Demo starten' : 'Lua installieren & starten') : 'Weiter';
|
||||||
|
if (wizStep === 2) checkFwl();
|
||||||
|
}
|
||||||
|
function openWizard() {
|
||||||
|
wiz.classList.remove('hidden');
|
||||||
|
$('wizPath').value = xpPath.value || '';
|
||||||
|
$('wizDemo').checked = demoEl.checked;
|
||||||
|
validateWizPath();
|
||||||
|
showStep(1);
|
||||||
|
}
|
||||||
|
function closeWizard() { wiz.classList.add('hidden'); localStorage.setItem('setupDone', '1'); }
|
||||||
|
|
||||||
|
async function validateWizPath() {
|
||||||
|
const p = $('wizPath').value.trim();
|
||||||
|
const h = $('wizPathHint');
|
||||||
|
if (!p) { h.textContent = 'Leer = Demo-Modus möglich.'; h.className = 'hint'; return; }
|
||||||
|
const ok = await invoke('valid_xplane_path', { path: p });
|
||||||
|
h.textContent = ok ? '✓ X-Plane erkannt' : '⚠ kein „Resources/default data" — Pfad prüfen';
|
||||||
|
h.className = 'hint ' + (ok ? 'ok' : 'bad');
|
||||||
|
}
|
||||||
|
async function checkFwl() {
|
||||||
|
const el = $('wizFwl'); const p = $('wizPath').value.trim();
|
||||||
|
if (!p) { el.textContent = '— (kein X-Plane gewählt; im Demo-Modus nicht nötig)'; el.className = 'wiz-status'; return; }
|
||||||
|
const present = await invoke('flywithlua_present', { path: p });
|
||||||
|
el.textContent = present ? '✓ FlyWithLua ist installiert' : '✗ FlyWithLua nicht gefunden';
|
||||||
|
el.className = 'wiz-status ' + (present ? 'ok' : 'bad');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('setupBtn').addEventListener('click', openWizard);
|
||||||
|
$('wizClose').addEventListener('click', closeWizard);
|
||||||
|
$('wizBrowse').addEventListener('click', async () => {
|
||||||
|
try { const dir = await T.dialog.open({ directory: true, multiple: false, title: 'X-Plane 12 Ordner wählen' });
|
||||||
|
if (dir) { $('wizPath').value = dir; validateWizPath(); } } catch (e) { appendLog('dialog: ' + e); }
|
||||||
|
});
|
||||||
|
$('wizPath').addEventListener('input', validateWizPath);
|
||||||
|
$('wizFwlCheck').addEventListener('click', checkFwl);
|
||||||
|
$('wizFwlLink').addEventListener('click', (e) => { e.preventDefault(); openUrl(FWL_URL); });
|
||||||
|
$('wizDemo').addEventListener('change', () => { demoEl.checked = $('wizDemo').checked; showStep(wizStep); });
|
||||||
|
$('wizBack').addEventListener('click', () => showStep(wizStep - 1));
|
||||||
|
$('wizNext').addEventListener('click', async () => {
|
||||||
|
if (wizStep < 4) { showStep(wizStep + 1); return; }
|
||||||
|
// final step: carry the chosen path/demo to the main controls and start
|
||||||
|
xpPath.value = $('wizPath').value.trim();
|
||||||
|
demoEl.checked = $('wizDemo').checked;
|
||||||
|
await validatePath();
|
||||||
|
$('wizResult').textContent = 'Starte Server …'; $('wizResult').className = 'wiz-status';
|
||||||
|
closeWizard();
|
||||||
|
if (!running) startBtn.click();
|
||||||
|
});
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
// Offer the wizard automatically on the very first launch.
|
||||||
|
if (!localStorage.getItem('setupDone')) setTimeout(openWizard, 400);
|
||||||
|
|||||||
+28
-1
@@ -76,7 +76,7 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r
|
|||||||
.diag { margin-top: 10px; border-top: 1px solid var(--line-soft); padding-top: 8px; display: flex; flex-direction: column; gap: 5px; }
|
.diag { margin-top: 10px; border-top: 1px solid var(--line-soft); padding-top: 8px; display: flex; flex-direction: column; gap: 5px; }
|
||||||
.diag-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--mut); }
|
.diag-row { display: flex; justify-content: space-between; font-size: 12px; color: var(--mut); }
|
||||||
.diag-row b { color: var(--txt2); font-weight: 600; }
|
.diag-row b { color: var(--txt2); font-weight: 600; }
|
||||||
.diag-row b.ok { color: var(--green); } .diag-row b.warn { color: var(--amber); }
|
.diag-row b.ok { color: var(--green); } .diag-row b.warn { color: var(--amber); } .diag-row b.bad { color: #ff6b6b; }
|
||||||
|
|
||||||
.log-wrap { background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 12px; padding: 6px 12px; }
|
.log-wrap { background: var(--bg2); border: 1px solid var(--line-soft); border-radius: 12px; padding: 6px 12px; }
|
||||||
.log-wrap summary { color: var(--mut); font-size: 12px; cursor: pointer; padding: 4px 0; }
|
.log-wrap summary { color: var(--mut); font-size: 12px; cursor: pointer; padding: 4px 0; }
|
||||||
@@ -104,3 +104,30 @@ input:focus { outline: none; border-color: var(--green); box-shadow: 0 0 0 3px r
|
|||||||
.asp-name { color: var(--txt2); font-size: 13px; }
|
.asp-name { color: var(--txt2); font-size: 13px; }
|
||||||
.asp-name em { color: var(--mut); font-style: normal; font-size: 11px; }
|
.asp-name em { color: var(--mut); font-style: normal; font-size: 11px; }
|
||||||
.asp-count { color: var(--mut); font-size: 12px; min-width: 56px; text-align: right; }
|
.asp-count { color: var(--mut); font-size: 12px; min-width: 56px; text-align: right; }
|
||||||
|
|
||||||
|
/* header setup button sits between brand and status */
|
||||||
|
.hd { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.hd #setupBtn { margin-left: auto; }
|
||||||
|
.hd #status { margin-left: 8px; }
|
||||||
|
|
||||||
|
/* first-run setup wizard */
|
||||||
|
.wiz { position: fixed; inset: 0; background: rgba(0,0,0,.55); display: flex; align-items: center; justify-content: center; z-index: 50; }
|
||||||
|
.wiz.hidden { display: none; }
|
||||||
|
.wiz-box { width: min(440px, 92vw); background: var(--bg2); border: 1px solid var(--line); border-radius: 14px; padding: 18px; box-shadow: 0 20px 60px rgba(0,0,0,.5); }
|
||||||
|
.wiz-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
|
.wiz-head b { font-size: 15px; }
|
||||||
|
.wiz-steps { display: flex; gap: 6px; margin-left: auto; }
|
||||||
|
.wiz-steps span { width: 22px; height: 22px; border-radius: 50%; display: grid; place-items: center; font-size: 11px; background: var(--bg3); color: var(--mut); }
|
||||||
|
.wiz-steps span.on { background: var(--green); color: #042b10; font-weight: 700; }
|
||||||
|
.wiz-x { background: none; border: none; color: var(--mut); font-size: 16px; cursor: pointer; }
|
||||||
|
.wiz-step h3 { margin: 0 0 8px; font-size: 14px; color: var(--txt); }
|
||||||
|
.wiz-step p { margin: 0 0 10px; color: var(--txt2); font-size: 13px; line-height: 1.45; }
|
||||||
|
.wiz-step .wiz-sub { color: var(--mut); font-size: 12px; }
|
||||||
|
.wiz-step a { color: var(--green); }
|
||||||
|
.wiz-step code { background: var(--bg3); padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
||||||
|
.wiz-ol { margin: 0 0 10px; padding-left: 20px; color: var(--txt2); font-size: 13px; line-height: 1.6; }
|
||||||
|
.wiz-status { background: var(--bg3); border: 1px solid var(--line-soft); border-radius: 8px; padding: 8px 10px; font-size: 13px; margin: 8px 0; }
|
||||||
|
.wiz-status.ok { color: var(--green); border-color: #1d4a2c; }
|
||||||
|
.wiz-status.bad { color: #ff6b6b; border-color: #5a2424; }
|
||||||
|
.wiz-foot { display: flex; justify-content: space-between; gap: 10px; margin-top: 16px; }
|
||||||
|
.wiz-foot .btn { flex: 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user