Initial commit: X-Plane G1000 web cockpit + bridge + Tauri desktop app

- 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>
This commit is contained in:
2026-06-01 15:07:03 +02:00
commit ebc33a78b7
110 changed files with 14671 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import React, { useEffect, useState } from 'react';
import { num } from '../api/useXplane.js';
// G1000 PROC dialog. Pick a destination/airport, a category (Departure / Arrival
// / Approach), then a procedure + transition; LOAD inserts the procedure's leg
// fixes into the active flight plan. Procedures come from X-Plane's own CIFP data
// via /api/nav/procs and /api/nav/proc (resolved to coordinates server-side).
const CATS = [
{ id: 'approach', label: 'APPROACH', key: 'approaches', t: 'approach' },
{ id: 'arrival', label: 'ARRIVAL', key: 'stars', t: 'star' },
{ id: 'departure', label: 'DEPARTURE', key: 'sids', t: 'sid' },
];
export default function Proc({ xp, onClose }) {
const { flightPlan, fp, values } = xp;
// Default airport: the plan's destination if it's an airport, else blank.
const wps = flightPlan?.waypoints || [];
const destGuess = [...wps].reverse().find((w) => w.type === 'APT')?.id || '';
const [icao, setIcao] = useState(destGuess);
const [query, setQuery] = useState(destGuess);
const [procs, setProcs] = useState(null);
const [err, setErr] = useState('');
const [cat, setCat] = useState('approach');
const [selProc, setSelProc] = useState(null); // { name, transitions }
const [selTrans, setSelTrans] = useState('');
const [legs, setLegs] = useState([]);
// Fetch the procedure summary whenever the airport changes.
useEffect(() => {
const id = icao.trim().toUpperCase();
if (id.length < 3) { setProcs(null); return; }
let alive = true;
setErr(''); setProcs(null); setSelProc(null); setSelTrans(''); setLegs([]);
fetch(`/api/nav/procs?icao=${id}`).then((r) => r.ok ? r.json() : Promise.reject(r.status))
.then((d) => { if (alive) setProcs(d); })
.catch(() => { if (alive) setErr(`keine Prozeduren für ${id}`); });
return () => { alive = false; };
}, [icao]);
// Preview the resolved legs when a procedure+transition is chosen.
useEffect(() => {
if (!procs || !selProc) { setLegs([]); return; }
const c = CATS.find((c) => c.id === cat);
let alive = true;
const t = encodeURIComponent(selTrans || '');
fetch(`/api/nav/proc?icao=${procs.icao}&type=${c.t}&name=${encodeURIComponent(selProc.name)}&trans=${t}`)
.then((r) => r.ok ? r.json() : []).then((d) => { if (alive) setLegs(d); });
return () => { alive = false; };
}, [procs, cat, selProc, selTrans]);
const catList = procs ? (procs[CATS.find((c) => c.id === cat).key] || []) : [];
const load = () => {
if (!legs.length) return;
const existing = wps.slice();
// Departures go to the front, arrivals/approaches to the end.
const merged = cat === 'departure' ? [...legs, ...existing] : [...existing, ...legs];
fp.set({ name: 'ACTIVE', waypoints: merged, activeLeg: cat === 'departure' ? 1 : existing.length || 1 });
onClose();
};
return (
<div className="dlg-backdrop" onClick={onClose}>
<div className="dlg proc" onClick={(e) => e.stopPropagation()}>
<div className="dlg-head">PROCEDURES</div>
<div className="proc-body">
<div className="proc-apt">
<label>APT</label>
<input value={query} onChange={(e) => setQuery(e.target.value.toUpperCase())}
onKeyDown={(e) => e.key === 'Enter' && setIcao(query)}
placeholder="ICAO (z.B. KSEA)" autoCapitalize="characters" autoCorrect="off" spellCheck="false" />
<button className="fbtn" onClick={() => setIcao(query)}>LOAD</button>
</div>
{err && <div className="proc-err">{err}</div>}
<div className="proc-tabs">
{CATS.map((c) => (
<button key={c.id} className={cat === c.id ? 'on' : ''}
onClick={() => { setCat(c.id); setSelProc(null); setSelTrans(''); }}>{c.label}</button>
))}
</div>
<div className="proc-cols">
<div className="proc-list">
<div className="proc-coltitle">{procs ? `${catList.length}` : '—'} PROC</div>
{catList.map((p) => (
<button key={p.name} className={selProc?.name === p.name ? 'on' : ''}
onClick={() => { setSelProc(p); setSelTrans(p.transitions[0] || ''); }}>{p.name}</button>
))}
{procs && catList.length === 0 && <div className="proc-empty">keine</div>}
</div>
<div className="proc-list">
<div className="proc-coltitle">TRANS</div>
{selProc?.transitions.map((t) => (
<button key={t} className={selTrans === t ? 'on' : ''} onClick={() => setSelTrans(t)}>{t}</button>
))}
{selProc && selProc.transitions.length === 0 && <div className="proc-empty"></div>}
</div>
<div className="proc-preview">
<div className="proc-coltitle">{legs.length} FIXES</div>
{legs.map((l, i) => (
<div key={l.id + i} className="proc-leg">
<b>{l.id}</b>{l.alt ? <u>{l.alt}ft</u> : null}
</div>
))}
</div>
</div>
</div>
<div className="dlg-actions">
<button className="fbtn" onClick={onClose}>CANCEL</button>
<button className="fbtn add" disabled={!legs.length} onClick={load}>LOAD FPL</button>
</div>
</div>
</div>
);
}