The app becomes a product: EFB, MPL-2.0 relicense, weight and balance, and an end-to-end audit - #2
Draft
allenmcghan wants to merge 15 commits into
Draft
The app becomes a product: EFB, MPL-2.0 relicense, weight and balance, and an end-to-end audit#2allenmcghan wants to merge 15 commits into
allenmcghan wants to merge 15 commits into
Conversation
Three problems with the first cut, all fair. **The mounting compensation was wrong.** The old "cage" button zeroed pitch and roll, which silently assumes the phone's axes line up with the airframe's. They do not, and cannot be made to by two offsets: a phone tilted on a dash or lying on a kneeboard has a completely different mapping from device axes to aircraft axes, and zeroing two numbers just relabels the phone's attitude as the aircraft's. SET LEVEL now captures the full three-axis mounting rotation. With the aircraft level, gravity in the device frame gives the true down axis through the airframe; combined with the screen direction nominated as the right wing, that fixes all three aircraft axes in device coordinates, and everything after is a change of basis. Any mount angle works: vertical on the dash, flat on a kneeboard, or anything between. The calibration persists across launches. Display rotation is adjustable in 90 degree steps and 5 degree trim, for a phone mounted rotated or slightly skewed. **It scrolled.** The page is now fixed and full bleed with no scroll at all. Everything that was prose below the display moved into an overlay behind a single small button, out of the scan. **It was a toy.** It now runs as an instrument: fullscreen, landscape lock, wake lock, and calibration in local storage, so it can go on the aircraft as-is. Also added, since the mounting work made it nearly free: an angle of attack estimate from pitch minus flight path angle, with flight path angle derived from GNSS vertical speed against ground speed, plus a flight path marker on the attitude indicator. Both are marked as estimates. True AoA needs a vane or a differential probe; this is attitude minus trajectory and it is honest about that. The uncalibrated state is annunciated in red rather than quietly showing phone attitude as though it were the aircraft's.
Four questions, answered in the order they actually depend on each other. **Choosing an aircraft.** A library of profiles lives on the phone, with three presets to start: the PM-2 twin, a generic single, and a four-cylinder experimental. Selecting one reconfigures the display immediately. PRD section 11 still holds: the node stays authoritative for the aircraft it is installed in. The phone holds the collection, which is the part section 11 never had to describe because it only ever considered one aircraft at a time. **Different widgets per aircraft.** The panel is generated FROM the profile rather than configured beside it. A twin gets two RPM rows and a sync bar; a single gets one and neither. A four-cylinder gets four CHT and four EGT rows and the strip re-sizes to fit. V-speeds drive the airspeed tape bands and its scale, limits drive the redlines. Nobody edits a layout, they edit an aircraft. That is design rule 5 applied to the panel. Units are display-only and every value moves in SI internally, which is the same rule ble-telemetry.md enforces on the wire. Switching a profile from mph to knots or Fahrenheit to Celsius changes the glass and nothing else. **Saving several.** Duplicate, edit, delete, all persisted locally, with the same validation the node would apply: usable fuel over capacity, stall at or above never-exceed, an engine with no CHT and no EGT. **Sync, backup, sharing.** Files, not a service. A profile exports as TOML matching spec/aircraft-profile.md, with copy, share sheet, and download, and imports from a file or pasted text. Schema version is checked and a profile from a newer build is refused rather than guessed at. No cloud, deliberately. Design rule 4 says the owner owns the data, section 17 rules out anything requiring renewal, and MAINTAINERS.md exists because the project has to outlive its maintainer. A project-run sync service is exactly the dependency that dies when nobody pays for it, and a profile carries a registration and a home field. A file in the owner's own cloud is rule 4 done properly and costs the project nothing to keep running.
Three fixes and two new capabilities. The fixes first, because they were
blocking use rather than missing from it.
**It did not fill the screen.** A fixed 1000x580 viewBox letterboxes badly
on a Fold: the inner screen is nearly square and the outer is very tall,
and neither is 1.7:1. The viewBox now tracks the window, and the
instrument cluster is re-centred and stretched into it on load and on
every resize, which also covers the fold and unfold transition. Safe-area
insets are honoured so the controls clear the cutout.
**Sensors needed hunting for.** They now start on load. Where a gesture is
required the first tap anywhere completes it, rather than requiring the
pilot to find the right menu item.
**Calibration was buried.** CAL is on the main screen, next to a
SIM/LIVE master switch, both reachable without opening anything.
**LIVE stops simulating what the phone actually knows.** In LIVE the
altitude and speed tapes show GNSS altitude and ground speed, and the
source labels switch to magenta PHONE GNSS. Showing invented numbers on
an aircraft was the worst thing this app did, because they looked exactly
like real ones. Simulated node channels remain for the engine strip,
which the phone genuinely cannot know.
**Traffic and weather, both real.** Investigated first, because the two
sources behave completely differently:
api.airplanes.live sends access-control-allow-origin: *, so it works
from a plain browser. Verified live: 57 aircraft
with position, altitude, ground speed and track.
aviationweather.gov sends no CORS header at all, so no browser can read
it. The APK reaches it through a native bridge with
an explicit host allowlist, since
addJavascriptInterface without one would be an open
proxy inside the app.
Traffic is displayed as a ranked list with distance, bearing and relative
altitude, and per design rule 9 it raises no alert. The warning is in the
UI rather than the commit log: coverage is crowdsourced ground receivers
on line of sight, so below 1000 to 2000 ft AGL there is often nothing,
which is exactly where this aircraft flies.
Weather is labelled METAR and not ATIS, deliberately. ATIS also carries
the active runway and approach in use and there is no free feed for it.
METAR carries the same weather and does not carry the runway, so calling
it ATIS would be a lie a pilot might act on. Density altitude is computed
from the observation rather than guessed from a profile limit.
Still to come, and not started: pre-flight planning, the checklists, and
the logbook.
Looked at what ForeFlight, Garmin Pilot, FltPlan Go, Avare and the rest actually give a pilot, and took the parts that matter to an aircraft with no panel and often no cell signal. **Navigation, which was the real gap.** Heading and attitude without "where am I relative to anywhere" is not much use. Distance, bearing and time to run now appear on the main screen and as a green pointer on the heading tape. HOME is captured automatically at takeoff, so "how do I get back to the field I just left" needs no setup at all — the question an ultralight pilot actually asks, answered by default. Destinations are entered as lat/lon or grabbed from the current position. Time to run uses closure along the course rather than raw ground speed. Crabbing 40 degrees off does not get you there at ground speed and an ETE that pretends otherwise is worse than none. Verified: 3.0 nm at 12.5 m/s reads 445 s straight and 890 s at 60 degrees off. **A map with no basemap, deliberately.** Breadcrumb, home, destination, traffic, range rings, track-up or north-up. Tiles need a network at 800 feet over a field, which is exactly where there is not one, so a track and a bearing that always work beat a grey square that sometimes does. **Checklists per aircraft, editable, five phases** from preflight to after landing. Stored against the profile id, so switching aircraft switches lists. Ticks are deliberately not persisted: a checklist that remembers being complete from last week is worse than no checklist. **Logbook with takeoff and landing detected from GNSS** ground speed against the aircraft's own stall speed, with hysteresis so a GPS glitch does not log a flight. Records duration, landings, distance, maximum altitude and the track. Exports CSV and GPX by copy, share sheet or download. Per section 13 these are proposals and the file says so in its own header, because 14 CFR 61.51 puts accuracy on the pilot. Not done, and worth being straight about: photographing a paper checklist and having it transcribed needs OCR the app cannot do on-device, and sending photos to a cloud model is exactly the dependency section 17 rules out. Downloading a checklist for a given type has no free source to download from. Both need a real answer rather than a plausible one.
…tion Checked the earlier claim that no standard exists. It was too strong, and the real picture is more useful. Six formats exist and are in daily use: Garmin .ace, Garmin Pilot .gplt, ForeFlight .fmd, Advanced Flight Systems, Dynon SkyView, Grand Rapids. efis-editor already reads and writes all six under Apache 2.0. The rule from the DroneCAN spec applies unchanged: standard types wherever one exists, and a new definition only for the genuine gap. Three gaps are genuine. Every one of those formats is a vendor artifact, binary or semi-binary, tied to a panel, and at least one Garmin Pilot variant is encrypted and cannot be read at all. None is something a builder opens in an editor, diffs, or reviews a change to. A format nobody can read by eye is a format nobody can check, and a checklist is a safety document. The free repositories that exist are PDFs. Useful to a human with a printer, useless to software: nothing can load one into a panel, diff two revisions, or tell you the copy in your hand is three years stale. And none of it covers this aircraft class. Every format targets a certified or experimental panel. A Part 103 ultralight is not in scope for any of them, and it is the aircraft most likely to be flown with no checklist at all. So this defines a human-editable TOML format matching the aircraft profile, and a repository convention that is a git tree and nothing else. Git already supplies what a checklist repository needs and a web form does not: history, attribution, diffs, review before merge, forking a file you disagree with, and working with the origin server gone. Provenance is a required block rather than metadata. A checklist off the internet is a starting point for the pilot's own, and verified_on_aircraft = false is the honest default for a transcription nobody has flown behind. Cautions and warnings are separate item types that cannot be ticked, because a warning is not a task. Export goes through efis-editor rather than reimplementing six parsers. That is Apache 2.0, which is incompatible with GPL-2.0-only but fine for GPL-2.0-or-later via GPLv3. First time the "or later" has paid for itself, and worth recording that it was not decorative.
Reverses an earlier call. The map had no basemap on the argument that tiles need a network you will not have at 800 feet over a field. That solved the wrong half of the problem: tiles need a network when they are fetched, not when they are drawn. Fetch them on the ground and the objection disappears, and ground reference is worth a great deal to a pilot who is low, slow, and navigating by looking outside. Low zoom is the point rather than a compromise. At zoom 11 a tile is about 12 nm across and 13 kB, so a 7x7 block covers roughly 80 nm for under a megabyte. Field boundaries, roads, rivers, towns and coastline are all legible and that is the whole content of a ground reference. Verified zoom selection across the range: 1 nm picks z14, 10 nm picks z11, 100 nm picks z8. PULL AREA downloads the current area before flight. The service worker keeps tiles in their own cache that a shell update never evicts, because a tile pulled on the ground is the entire reason the map works in the air. The basemap is knocked back 42% behind the symbology. A basemap that competes with the traffic and the course line is a hazard rather than a help. On being a good guest with donated infrastructure. tile.openstreetmap.org has a published usage policy and this tries to honour it: the WebView now sends a User-Agent naming the app and its repository, pre-caching is user-initiated, bounded to the visible area and hard-capped at 240 tiles rather than being a crawler, and the ODbL attribution is drawn on the map rather than buried in an about box. If this ever sees real use the right answer is a self-hosted basemap rather than leaning harder on someone else's donated servers. Protomaps pmtiles gives a whole region as a single file and suits an offline instrument better than any tile server does. Recorded in tiles.js rather than left to be rediscovered.
The FAA publishes its own VFR Sectional and Terminal Area charts as 256 px Web Mercator tiles with CORS enabled, zoom 0 to 23, from tiles.arcgis.com. That is the same scheme OSM uses, so the tile path built for the basemap serves them unchanged and one cache holds all three layers. Tried vector airspace first and rejected it. The FAA ArcGIS Class_Airspace feature service works and returns GeoJSON, but a two degree box came back at 10 MB for 53 features, and 1.7 MB for 13 even after filtering to the classes that matter and asking for generalisation. Rendered tiles are two orders of magnitude smaller and carry the symbology a pilot already reads. Filtering that vector data did surface the right rule though, and it is now recorded in the source: 14 CFR 103.17 forbids operating an ultralight in Class A, B, C, D, or the surface area of Class E designated for an airport without prior ATC authorisation. Class E4 and E5 are not surface areas and do not require it. For a Part 103 pilot the airspace boundary is a legal line rather than a convenience, which is why this is worth carrying at all. Charts are dimmed 12% against the basemap's 42%. A sectional has to stay readable; a road map only has to stay out of the way. Staleness is annunciated rather than hidden. Sectionals run a 56-day cycle and a tile cached before a boundary looks exactly like a current one, so the map shows when the layer was last pulled and warns past 27 days. The FAA attribution reads "not for navigation" because that is true, and it is drawn on the map rather than buried.
**The map was blank and the cause was mine.** The app defaults to SIM, SIM only ever simulated attitude, and the tile layer returns early when there is no position. So it never requested a single tile. Simulating half the state made half the app untestable from an armchair, which is the opposite of what SIM is for. SIM now flies the demo circuit over real ground and publishes a position, so charts, moving map, breadcrumb and direct-to all work sitting at a desk. The origin is the last real fix if there was one, else HOME, else KOSH as an obvious placeholder. The map annunciates SIMULATED POSITION so it can never be mistaken for a fix, and switching to LIVE discards the invented position and trail rather than letting them leak into real data. The logbook still only records in LIVE, so simulating never pollutes it. **A real tab bar.** PFD, MAP, CHK, LOG, WX as persistent tabs across the bottom with CAL and SIM/LIVE alongside, instead of buttons that opened overlays with no sense of where you were. **The checklist editor is per item rather than a raw textarea.** Add, edit in place, reorder up and down, delete, with item counts on each phase tab. Paste-many is still there for bulk entry, but typing your own preflight no longer means editing a wall of text. **Custom phases per aircraft.** The five standard phases cover the normal flight; anything else is the owner's to add. Emergency and abnormal procedures are the obvious case and they differ by airframe. Deleting a custom phase removes its items rather than orphaning them, and standard phases cannot be deleted.
**The HUD was hidden behind the tab bar.** Adding tabs last commit put a z-40 bar over a HUD living inside a z-29 page, and a 46px offset that a gesture-nav phone exceeds. So every map control existed and none was reachable. The HUD is now fixed at z-41 with its offset driven by the tab bar's measured height, which cannot drift out of agreement again. **Charts were upside down because of track-up.** The tile scheme is standard XYZ with the correct origin, so addressing was never wrong; the map was rotating to the demo heading and taking the sectional's text with it. A chart rotated past south is unreadable, which is a good argument that north-up is the right default for chart layers. Track-up is now a toggle rather than the assumption. **Pan and pinch zoom.** Drag to move, pinch to zoom from 0.4 to 240 nm, CENTRE to snap back to own position. The map now anchors to own position when it has one and the last fix, HOME, or a placeholder when it does not, so it always draws something. Own ship is drawn where it actually is rather than pinned to the middle, so panning away shows you leaving the frame instead of teleporting. **Route plotting.** ROUTE mode turns taps into waypoints, with legs, per-leg distance and bearing, and a running total on the status line. Routes persist. A tap is distinguished from a drag by movement so panning never drops a waypoint by accident. **LIVE is the default and demo is a choice.** Real data unless the pilot asks otherwise, which is the right way round for something meant to be flown with. Switching to SIM clears the trail so invented track never mixes with real. Not doing: streaming ATC audio. LiveATC's terms state plainly that audio streams may not be used in any third-party product, so this will not ship it. It also would not help much — internet ATC runs 20 to 30 seconds behind and needs a signal, while the handheld already in the cockpit is live and works with none.
**Airports and frequencies, bundled rather than fetched.** 25,101 US airports with 13,012 frequencies from OurAirports, public domain, about 600 kB compressed inside the APK. The moment you need a nearest field is the moment you have neither signal nor time, so it ships with the app. A NRST tab lists nearest fields with distance, bearing, elevation and every published frequency, direct-to on a tap, and airports draw on the map inside 40 nm. Tried the FAA services first. US_Airport is spatial and fine, but Frequencies is a non-spatial table keyed by SERVICE_ID that needs a three-way join to reach an airport, which is the wrong shape for a phone. OurAirports joins on airport_ident in one step and is more complete for small private strips, at the cost of being community maintained. The UI says so and says to confirm against the Chart Supplement before keying the mic at a towered field. **This stopped being a mockup, so it stopped being called one.** app/mockup is now app/junco, the package is com.keylinkit.junco rather than org.junco.mockup, and the version is 0.1.0. **A real release path.** Play requires an App Bundle, which the raw-SDK build cannot produce, so there is now a Gradle build alongside build.sh. build.sh keeps the ten-second debug loop; Gradle produces the signed AAB. Signing comes from the environment and .gitignore refuses keystores, because an upload key in git is an upload key in every fork. **A first-run safety acknowledgement**, shown once. Advisory only, not a certified product, attitude comes from phone sensors, charts go stale, traffic is badly incomplete below 2000 ft, logbook entries are drafts, the pilot is responsible. This is a flight instrument for an aircraft with no panel and the limits belong in front of the pilot before the first flight rather than in grey text nobody reads. **RELEASE.md** records what is done, what Allen must supply, and the two decisions that cannot be undone: the package name, and the upload key. It also raises two things worth deciding before publishing rather than after — product liability for an instrument people will over-trust, and that GPL plus Play is fine for a sole copyright holder and stops being simple at the first outside contribution.
**Launch acknowledgement, every time.** Was once and remembered; now it appears at every launch, which is what every marine and aviation GPS does. An acknowledgement is only worth something at the start of the flight it applies to — a box ticked eight months ago says nothing about whether this pilot, today, understands what they are looking at. **The PRD now describes what exists.** Junco is two products on two schedules: an Android EFB that is useful today with no hardware at all, and the sensor node that was the original project and is still unbuilt. The app began as a layout study for hardware that did not exist, and it turned out the half of the instrument picture a phone can supply on its own is worth shipping by itself to someone flying with no panel. Continuing to call it a mockup was becoming false, and a document that describes something other than what exists is worse than no document. New section 25 covers the app as a product: what it does with no hardware, how each design rule shows up in it, and the two limits that must never be quietly designed away — phone attitude is not an AHRS, and a cached chart carries no expiry the app can see. Section 20 splits the roadmap so the app is not blocked behind hardware. Section 4's "anything sold to anyone" is intact: free, no advertising, no analytics, no accounts, nothing collected. **On the licence, correcting myself.** I said GPL sat awkwardly with Play. That was overstated. The famous GPL-versus-app-store problem is Apple's, not Google's — Play imposes no equivalent usage restrictions, GPL apps ship there routinely, and the obligation is only that source stays available, which it does. Outside contributions do not change that either: a contributor licensing under GPL-2.0-or-later has already granted what redistribution needs, so no contributor licence agreement is required. GPL-2.0-or-later already does what was asked for — free, and anyone may build on it. The one thing it adds is that a fork stays open too, which was the deliberate choice in revision 4. No reason to revisit it, so it stands. CONTRIBUTING.md now asks for a Developer Certificate of Origin sign-off, which is not a copyright assignment and costs a contributor nothing but records that they had the right to submit.
…f the app
Two unrelated things that both had to happen before this can go on a store.
Relicense
---------
Code moves from GPL-3.0 to MPL-2.0. Hardware stays CERN-OHL-S-2.0 and
documentation stays CC-BY-4.0; neither is affected.
The reason is distribution, not ideology. GPL cannot ship on Apple's App
Store: the DRM Apple applies imposes exactly the further restrictions GPL
section 6 forbids, which is why VLC was pulled in 2011. This project is meant
to outlive its author, and an app that structurally cannot exist on half the
phones in the world is not going to. MPL-2.0 is what Firefox for iOS ships
under.
Nothing about the openness intent changes. MPL is still copyleft, just per
file rather than per work: change a Junco file and you publish that file.
Combine it with whatever you like, including closed code, and charge for it.
Forks are fine. The author has said plainly he would rather this lived on as
a product than ever made him money, and this is the licence that matches that.
MPL is explicitly GPL-compatible through its Secondary License clause, so the
two-way code flow with MakerPlane's FIX-Gateway and pyEFIS that motivated the
GPL choice in revision 4 survives the change intact.
No CLA. Sign-off (DCO) only, as before.
The dead handler
----------------
An end-to-end audit driving 65 clicks through every control in the app found
one genuine bug, and it was a bad one.
$("mapUp").onclick = ...
was still bound to a button that had been removed when the north-up/track-up
toggle replaced it. `$()` returned null, the assignment threw, and every
handler defined after that line never got bound. One dead line silently took
out the checklist page, the nearest-airport page and destination entry.
Removed the line. Then hardened `$()` so this class of bug can never cascade
again: a missing element now logs a warning and returns an inert stub, so one
stale reference costs you one control instead of the rest of the app.
Chasing the audit's flakiness surfaced a second real bug: a page opened before
the first GPS fix stayed empty forever, because nothing re-rendered when the
fix arrived. The geolocation callback now re-renders the nearest and weather
pages on first fix, and the empty state says so instead of looking broken.
…ound
Weight and balance
------------------
New W&B page, per aircraft. Empty weight and arm, max gross, CG envelope, a
named station per seat and baggage area, fuel arm and density. Live ramp weight,
CG, a graphical envelope, and a 14 CFR 103.1 check underneath it.
This is the one EFB feature that matters MORE here than in a certified
aircraft. Part 103's 254 lb empty weight is not guidance; over it you are not
flying an ultralight, you are flying an unregistered aircraft with no
certificate and no licence.
Three decisions inside it are load bearing:
Presets ship it OFF and empty. Every other preset field is a plausible starting
point a builder corrects later. A weight is not. There is no plausible empty
weight for an aircraft nobody has weighed, and a fabricated one that happens to
close the envelope is worse than a blank page because it looks like an answer.
The page will not compute until the numbers come off a real weighing, and the
spec now forbids an implementation from supplying a default.
Fuel on board is declared as typed, not measured. There is no sender in this
build, so endurance, time to reserve, and fuel remaining at the destination are
arithmetic on a dipstick reading. Every readout says how long ago it was
entered. When a node exists this becomes the fallback for an invalid sender,
which is what design rule 8 asks for.
The 103 check reports what it cannot check. Two of the four numeric limits are
calibrated airspeeds no phone can measure, so they are listed as unverified
rather than omitted, and the page says the 254 lb limit excludes safety devices
so a ballistic 'chute comes out before you type the number.
Profile schema goes to 2: [weight_balance] and [station.N], with lb/in or kg/cm
as a display choice over SI storage. Old profiles forward-migrate to a blank
block, never a guessed one.
Also new
--------
Tap an airport on the chart to identify it — ident, name, elevation, distance,
bearing and every published frequency — then go direct or add it to the route.
The nearest page answers "what is close by"; this answers "what is that field
out the left window", which a list sorted by distance cannot.
Chart cache accounting with a purge. PULL AREA is what makes the map work with
no signal and also what quietly fills a phone. Purge removes tiles and nothing
else, unlike clearing app data, which would also take the aircraft profiles and
the logbook.
Copy diagnostics. No crash reporter, nothing phones home; the state goes to the
clipboard so it can be pasted into an issue, with a warning to read it first
because it contains the aircraft and the last known position.
Five bugs, four of them invisible from the code
-----------------------------------------------
Eighty-eight scripted interactions across two audit passes.
1. The first tap anywhere on the screen threw away the GPS fix and erased the
breadcrumb trail. The gesture that re-asks for orientation permission on iOS
was re-running the whole of setMode("live"), which resets position state. It
only bit where the orientation sensor is slow or absent — exactly the
population that needs the GPS most. Split out startOrient(): asking for a
permission must not have side effects on unrelated state.
This is also what the "flaky headless geolocation" in the previous audit
actually was. It was never the harness.
2. A padding shorthand overrode the tab-bar clearance on four pages, leaving
each one's Close button underneath the tab bar and untappable.
3. The settings button sat at z-20, below the overlay pages, so settings was
unreachable from the map, the charts, the checklist and the logbook.
4. setPointerCapture was called unguarded at the top of the map's pointerdown
handler. When it throws, no gesture state is recorded and the map stops
responding to touch entirely.
5. Adding a station redrew the setup form from the profile, wiping numbers that
were half typed — so you would fill the form, add the pilot, save, and be
told the empty weight was not set. Reading a form and committing a form are
now separate operations.
Both audits are green: 88 clicks, 0 bugs, 0 page errors.
Pasting an empty clipboard into "Load from text" produced an aircraft. Every field fell back to a default, so validation passed and the owner got a profile named "Imported aircraft" carrying someone else's stall speed, CHT limit and fuel capacity, with nothing on screen to say so. A document containing none of the tables a profile is made of is not an empty profile. It is not a profile. fromTOML now says so instead of inventing one. Found by a fuzz pass over the import path rather than by reading it.
The guard added in the previous commit checked `out[t] && Object.keys(out[t])`,
which a bare string satisfies: strip the section headers off a profile and
`fuel = "gal"` from [units] lands at the top level, where Object.keys("gal")
is three characters long and the file sails through as an aircraft.
Require an actual table. Caught by the same fuzz pass one round later.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This branch started as a competitive comparison against the Levil AIR system and turned into the app.
What is in it
An electronic flight bag that is useful today with no hardware at all.
app/junco/is a Garmin-style PFD driven by the phone's own sensors, plus FAA sectional and terminal charts, direct-to navigation, 25,101 bundled US airports with 13,012 frequencies, editable per-aircraft checklists, an automatic logbook, METAR, and advisory traffic. Offline by design, no account, nothing phones home.Relicensed to MPL-2.0. GPL cannot ship on Apple's App Store — Apple's DRM imposes exactly the further restrictions GPL section 6 forbids, and VLC was pulled over it in 2011. An iOS client was already a stated future, so GPL forecloses it. MPL is what Firefox for iOS ships under, is still copyleft per file, and stays GPL-compatible through its Secondary License clause so code still moves both ways with MakerPlane. Hardware stays CERN-OHL-S-2.0, docs stay CC-BY-4.0. No CLA; DCO sign-off only.
Weight and balance with a 14 CFR 103.1 check. The one EFB feature that matters more here than in a certified aircraft, because Part 103's 254 lb empty weight is the boundary of the category rather than guidance. It ships off and empty and stays that way until numbers come off a real weighing — a fabricated empty weight that happens to close the envelope is worse than a blank page, because it looks like an answer. Profile schema goes to 2 with
[weight_balance]and[station.N]; old profiles forward-migrate to a blank block, never a guessed one.Fuel endurance and fuel remaining at the destination, from a figure the pilot types. Declared as typed rather than measured, with its age shown everywhere it appears, because there is no sender in this build.
Tap an airport on the chart to identify it and go direct or add it to a route. Chart cache accounting with a purge that removes tiles and leaves profiles and logbooks alone. Copy diagnostics to the clipboard in place of a crash reporter.
Acknowledgement at every launch, the way every marine and aviation GPS does it, because an acknowledgement is only worth something at the start of the flight it applies to.
The audit
Eighty-eight scripted interactions across two Playwright passes, driving every control in the app. Both are green: 0 bugs, 0 page errors. It found five real defects, four of which were not visible from the code:
setMode("live"), which resets position state. It only bit on devices where the orientation sensor is slow or absent — precisely the population that needs the GPS most. This is also what the previous round's "flaky headless geolocation" actually was; it was never the harness.setPointerCapturewas called unguarded at the top of the map's pointerdown handler; when it throws, the map stops responding to touch entirely.Documentation
PRD is at revision 8. Section 25 covers the app as a product and now the weight and balance reasoning; section 26 records revisions 6, 7 and 8 with their rationale.
spec/aircraft-profile.mdspecifies the weight and balance block, including the rule that an implementation must not supply a default empty weight and must distinguish "outside the envelope" from "no envelope entered".Not done here
The node still does not exist. Nothing has been built and nothing has flown — see PRD section 16.