Initial Commit

This commit is contained in:
handfly 2026-05-03 10:48:28 -04:00
commit 22f664b2ec
9 changed files with 1839 additions and 0 deletions

2
.directory Normal file
View file

@ -0,0 +1,2 @@
[Desktop Entry]
Icon=orange-folder-github

68
COMPLETED.md Normal file
View file

@ -0,0 +1,68 @@
# Pilot's Toolkit — Completed Milestones
*Last updated: February 28, 2026*
---
## 🏁 Release Candidate 1 (RC1) — February 28, 2026
Phase I feature set locked. Seven modules, both platforms at parity, all data externalized to versioned JSON (v3 split-file structure with per-aircraft isolation and graceful error handling). No new features planned before Docker deployment (Phase II).
**Modules:** Fuel Order, Pavement Strength, Crosswind, Fuel Buckets, HF Frequencies, Passdown, Flight Level ↔ Meters
**Platforms:** Web (React/Vite), Android (Kotlin/Jetpack Compose)
**Data:** `reference.json` + `aircraft/{G450,G500,G650,G700}.json`
---
## Aircraft Data Status
| Aircraft | Pavement (ACN/PCN) | Pavement (ACR/PCR) | ESWL | Weight Limits | Fuel Buckets |
|----------|-------------------|-------------------|------|---------------|-------------|
| G450 | ⚠ Updated data pending | ⚠ Updated data pending | ✅ Formula | ✅ 42,000 / 74,600 / 75,000 | ✅ Built-in |
| G500 | ❌ Not started | ❌ Not started | ❌ Not started | ❌ Not started | ❌ Not started |
| G650 | ❌ Not started | ❌ Not started | ✅ Formula | ✅ 52,600 / 99,600 / 100,000 | ❌ Not started |
| G700 | ✅ Complete (±2%) | ✅ Complete (±2%) | ✅ Thresholds (S-85 / D-108) | ✅ 56,000 / 107,600 / 108,000 | ❌ Not started |
*Weight limits shown as: BEW / MTOW / Max Ramp*
---
## Phase I — Web & Android Apps
### Web (React/Vite)
- **Fuel Order Calculator** — ASTM D1250 Table 6B density correction replaces previous linear interpolation. Fuel density input at 15°C reference temperature with three-way unit toggle: specific gravity (SG), kg/L, or lb/US gallon (default) — values auto-convert when switching units. Default 0.810 SG / 6.76 lb/gal (typical Jet-A) so calculator works without a ticket in hand. Ambient temperature corrects the reference density using the ASTM thermal expansion coefficient and volume correction factor (K₀=346.4228, K₁=0.4388 for refined products). Results show both reference density (15°C) and corrected density at ambient temp in lb/gal. Jet-A spec range validation (0.7750.840 SG). Outputs in US gallons, imperial gallons, and liters, all rounded up to nearest 10. Primary volume unit configurable in hamburger menu with locale-based auto-detection (US → US gal, UK/Caribbean Commonwealth → imp gal, everyone else → liters). °C/°F toggle with live value conversion on switch (integer rounding). Temperature defaults to °C (aviation standard). Auto-calc from Total Required minus On Board. Density display. Temperature warnings adjusted: danger below 40°C (freeze point), warning above +55°C. Fully supersedes the original `fuelorder.cpp` standalone calculator (retired).
- **Pavement Strength Module** — Full PCN/PCR system selector (PCR default) with ACN/ACR output, rigid and flexible pavement types, four subgrade categories (AD). Dynamic "Airport PCN/PCR" label. Airport comparison banner (green pass / red exceed with percentage). Partial availability support for aircraft with ESWL-only or threshold-only data.
- **Crosswind Calculator** — Combined wind direction/speed input (xxxxx format, auto-slash on blur), separate optional gust field, runway input accepting both runway number (0136) and heading (010360). Headwind/tailwind and crosswind components for sustained and gust winds. Tailwind values in red.
- **Fuel Buckets** — Slider-based stage length lookup with interpolated fuel burn. CSV import for operator-specific data via hamburger menu. Per-aircraft localStorage persistence. Named fuel profiles (see below).
- **Named Fuel Profiles** — Fuel bucket schedules are now named per operator/aircraft (e.g., "FLEXJET G450"). Both web and Android. CSV import prompts for a profile name (web: browser prompt, Android: defaults to filename). Profiles exportable as JSON files (`FUEL_PROFILE_name_aircraft.json`) with format identifier, aircraft ID, name, and bucket data — designed for sharing between crew members and devices. JSON import reads the embedded aircraft ID and name, targeting the correct aircraft automatically. Hamburger menu shows loaded profiles with name, aircraft, entry count, export/share button, and clear button. The FuelBucketsModule displays the active profile name. Backward-compatible: old anonymous bucket arrays auto-migrate to `{ name: "Custom", buckets: [...] }` on load. Android uses SharedPreferences storage (fuel_profiles), web uses localStorage.
- **HF Frequencies** — ARINC Atlantic/Pacific page age check via "Valid from" HTML parsing with fallback URL chain (tries each URL in order, stops at first valid response). Atlantic uses end-of-window time for staleness ("Frequencies Valid Until"). Pacific uses start time ("Frequencies Valid From"). 4-hour staleness warning. Date mismatch detection (accepts today/yesterday UTC to handle timezone boundary). Direct link to open ARINC page in browser (points to whichever URL succeeded).
- **Flight Level ↔ Meters** — Two sub-modes: Meters ↔ Feet lookup (OEM Table 15, 107 entries from 300 m / 1,000 ft through 15,100 m / 49,500 ft) with type-ahead search in either direction and exact match highlighting; and China RVSM FLAS (Figure 5, 50 entries) with dual eastbound/westbound toggle filter (both on = all levels, one off = filtered), RVSM level color coding (blue = eastbound 0°179°, amber = westbound 186°359°), and operational notes. Both platforms. Source: GVIII-G700 Operating Manual 06-10-00, 2024-03-29.
- **JSON Data Extraction** — All data externalized from inline code into JSON files shared across platforms. Version 3 split-file structure: `reference.json` (airframe-agnostic: meters-to-feet table, China FLAS table, pavement subgrade labels) and per-aircraft files (`aircraft/G450.json`, `G500.json`, `G650.json`, `G700.json`) each containing weight limits, ESWL config, PCN/PCR coefficients, fuel buckets, and wind limitations placeholder. Graceful error handling: each aircraft file loads independently — a malformed file is skipped with a log warning rather than crashing the app. Android uses `DataLoader.init(context)` parsing from `assets/`; web uses Vite static JSON imports with per-aircraft `loadAircraft()` wrapper. ESWL uses `type` discriminator (`formula` vs `thresholds`). Both platforms refactored and build-tested.
- **Passdown Module (Round 1)** — Crew passdown form: crew emails (PIC, SIC, Lead CA) at top, registration, date, airport (ICAO, auto-uppercase), FBO, FBO phone (auto-format 10-digit US on blur, clickable tel: link in detail view), maintenance status, narrative, oncoming crew emails (PIC, SIC) at bottom. IndexedDB storage with autocomplete dropdowns (sorted alphabetically) that learn from entries. History view with detail/edit/delete (two-tap confirm). Plain text export (copy to clipboard + email with all crew addresses auto-populated in TO field). Passdown tab in first position, Fuel tab retains default focus. Content card widens for Passdown.
- **Passdown Round 2** — .ptz export/import: gzipped JSON array format (batch-capable by design, single-file workflow for now). Email attachment options: plain text body only, .ptz attachment only, or both. Import: decompress → parse → deduplicate by ID → merge into local DB. Android file association for .ptz. Stale date prompt: if passdown date ≠ today when sending, user chooses "Send Anyway", "Update to Today & Send", or "Cancel". Send tracking: `lastSentAt` timestamp stamped on each send, shown in detail view and as ✉ indicator in history list (not delivery confirmation — tracks intent only). History age styling: today's passdowns get full accent color + left border, yesterday's are muted, older entries are dimmed at 60% opacity — instant visual triage. History search/filter: filter bar at top of history view with registration autocomplete dropdown and From/To date range fields; result count shown when filters active; "Clear" resets all filters. Batch export: "Export (N)" button appears when filters are active with results — exports all matching passdowns as a single .ptz file, sorted by date. Filename convention: `PASSDOWN_BATCH_REG_DATEFROM_DATETO.ptz` (single tail) or `PASSDOWN_BATCH_Nrecords_DATEFROM_DATETO.ptz` (mixed). Maintenance log view eliminated — registration + date range filtering covers the real use case. Narrative search deferred to TBD.
- **Passdown Batch Import Preview** — Import flow now shows a preview screen before committing. Each passdown in the .ptz file is listed with registration, date, location, and PIC. Duplicates (matching ID already in local DB) flagged with "DUPLICATE" badge and unchecked by default. User can select/deselect individual entries, select all, or select none. "Import (N)" commits only checked entries and learns lookups from imported data. Works for both file picker imports and Android file association (tapping .ptz in file manager). Replaces the previous all-or-nothing auto-import flow.
- **Global Aircraft Selector** — Compact chip in top bar, color-coded (teal bold for default, amber semibold for non-default). Full selector in hamburger menu with "Set Default" feature. Persistent via localStorage.
- **Dark Mode** — Toggle in hamburger menu, persisted in localStorage.
- **Hamburger Menu** — Aircraft selector, dark mode toggle, CSV import per aircraft, passdown retention settings (3/6/12/24/36 months or never, auto-purge on app open), About section.
- **UI Polish** — Leading zero stripping on heading inputs, side-by-side wind/gust fields, "Flight Time" label on buckets results, temperature field proportions (2/5 input, 3/5 toggle), disclaimer footer on all modules.
- **User Documentation**`DOCUMENTATION.md` covering all seven modules, data file structure for editors (reference.json and per-aircraft JSON schema with field-level documentation), adding new airframes, .ptz file format (field table, filename conventions, manual inspection command), fuel profile JSON format, fuel bucket CSV format, and app settings.
---
### Android (Kotlin / Jetpack Compose)
- **Status:** Feature parity with web — RC1. All seven modules implemented including Passdown with SharedPreferences/JSON storage, autocomplete, history, detail view with tappable phone (ACTION_DIAL), copy to clipboard, email intent with crew addresses, two-tap delete confirmation. Passdown retention settings in hamburger menu. Fuel profiles: CSV import, JSON import/export, profile management in hamburger menu. Flight Level ↔ Meters with lookup and China FLAS. Scrollable menu. Crosswind input uses focus-loss formatting (no cursor jumping). Data loaded from `assets/` JSON files via `DataLoader.init(context)` with per-aircraft error isolation.
- **Package:** `com.harshmallow.pilottoolkit`
---
## Architecture Notes
- **Web stack:** Single-file React component (toolkit.jsx, ~3400 lines), Vite dev server, no backend, no accounts.
- **Data files:** Split-file JSON structure (v3): `reference.json` (flight levels, subgrade labels) + per-aircraft files in `aircraft/` directory. Web imports via Vite; Android reads from `assets/` via `DataLoader`. Each aircraft file loads independently with graceful error handling.
- **Data storage (web):** localStorage for preferences, dark mode, fuel buckets CSV, retention settings. IndexedDB for passdown records and autocomplete lookups.
- **Data storage (Android):** SharedPreferences for all settings, aircraft defaults, passdown data (JSON serialization), and fuel profiles. Room/SQLite migration planned.
- **Data portability:** CSV import/export for fuel bucket data. Plain text / email export for passdowns. .ptz (gzipped JSON array) for structured import/export, batch-capable.
- **Formulas:** Linear regression fits derived from official aircraft manual charts. R² > 0.99 on all curves. Error estimates noted per aircraft.
- **Disclaimer:** "Not for operational use · Verify all calculations independently" on every module.

333
DOCUMENTATION.md Normal file
View file

@ -0,0 +1,333 @@
# Pilot's Toolkit — Documentation
*RC1 — February 28, 2026*
---
## Overview
Pilot's Toolkit is a collection of aviation utilities for flight crews. It runs as a web app (React/Vite) and an Android app (Kotlin/Jetpack Compose). Both platforms share the same data files and feature set.
All calculations carry the disclaimer: **Not for operational use — verify all calculations independently.**
---
## Modules
### Fuel Order Calculator
Enter total fuel required, fuel on board, and fuel density to get the order quantity in US gallons, imperial gallons, and liters, all rounded up to the nearest 10 units.
Fuel density is entered at the 15°C reference temperature. The three-way toggle switches between specific gravity, kg/L, and lb/US gallon — values auto-convert when you switch units. If you don't have a fuel ticket, the default (0.810 SG / 6.76 lb/gal) is typical Jet-A.
The ambient temperature field corrects density using the ASTM D1250 Table 6B thermal expansion model. Temperature defaults to °C; tap the toggle to switch to °F (the value converts live). The primary volume unit (US gal, imperial gal, or liters) is set in the hamburger menu and defaults based on locale.
### Pavement Strength
Calculates ACN/ACR values for the selected aircraft at a given weight, using linear regression fits from official aircraft manual charts. Supports PCN (legacy) and PCR (ICAO 2024+ transition), rigid and flexible pavement types, and four subgrade categories (A through D).
Enter the airport's published PCN or PCR value in the comparison field to see a pass/exceed result with percentage. Airport pavement values can be found in the Jeppesen airport pages, the national AIP, or airport operator publications.
Some aircraft (G450, G650) show a computed ESWL using a weight-based formula. Others (G700) show fixed ESWL thresholds (single wheel and dual wheel) because the manufacturer publishes specific values rather than a formula.
### Crosswind Calculator
Enter wind as a five-digit string — for example, typing `31015` auto-formats to `310/15` when the field loses focus. Gust goes in the separate field. Runway accepts either a runway number (0136) or a heading (010360).
The output shows headwind/tailwind and crosswind components for both sustained and gust winds. Tailwind values display in red.
### Fuel Buckets
Drag the slider to a stage length and see the interpolated fuel burn rate. Data comes from either the aircraft's built-in bucket schedule (G450 only at RC1) or an operator-specific profile loaded via CSV or JSON import in the hamburger menu.
**CSV format** for import: two columns — stage length (hours, decimal) and fuel burn (lbs/hr). A header row is optional and auto-detected.
```
stage_length,fuel_burn
0.1,650
1.0,548.5
3.0,449
```
Profiles are named and stored per aircraft. See the "Fuel Profile JSON Format" section below for the JSON import/export schema.
### HF Frequencies
Fetches current ARINC HF frequency schedules for Atlantic and Pacific. Shows a staleness indicator — frequencies change on propagation-based windows (typically every few hours), so the 4-hour threshold flags when you should reload. The link opens the ARINC page directly in your browser.
### Passdown
A crew changeover form for sharing operational information: crew emails, registration, date, airport, FBO, FBO phone, maintenance status, and narrative. Records are stored locally (IndexedDB on web, SharedPreferences on Android).
The history view shows all passdowns with age-based color coding: today's entries are highlighted, yesterday's are muted, older entries are dimmed. Filter by registration and/or date range. Batch export produces a `.ptz` file of all matching results. See ".ptz File Format" below for the schema.
### Flight Level ↔ Meters
Two modes. **Meters ↔ Feet Lookup** searches the OEM conversion table (ICAO Table 15, 107 entries). Type in either direction and results filter live. **China FLAS** shows the China RVSM Flight Level Allocation Scheme with dual toggle buttons for eastbound (0°179°) and westbound (186°359°) filtering. RVSM levels are color-coded blue (east) and amber (west). When both buttons are active, all levels display.
ATC clears flight levels in meters; the pilot sets the altimeter to the corresponding feet value from the table. Due to rounding, the onboard metric readout may differ from the cleared value by up to 30 meters.
**Do not use for approach minima.** Chart values are rounded to the nearest 100 feet.
---
## Data File Structure
All aircraft-specific and reference data lives in JSON files shared by both platforms. This is the section to read if you need to edit existing data or add a new airframe.
### Directory Layout
**Web** (Vite `src/` directory):
```
src/
data/
reference.json
aircraft/
G450.json
G500.json
G650.json
G700.json
```
**Android** (`app/src/main/`):
```
assets/
reference.json
aircraft/
G450.json
G500.json
G650.json
G700.json
```
Both platforms read the same JSON — just placed in the appropriate directory. The web app imports via Vite at build time; Android reads from `assets/` at runtime via `DataLoader.init(context)`.
### Error Handling
Each aircraft file loads independently. If `G700.json` has a syntax error, the other three airframes still load normally. The app logs a warning rather than crashing. On Android, `DataLoader.loadErrors` contains a map of failed aircraft IDs and error messages.
### reference.json
Airframe-agnostic data. Not included in per-aircraft export/import.
```json
{
"_meta": {
"format": "pilot-toolkit-data",
"version": 3
},
"pavementSubgrades": {
"A": "A High",
"B": "B Medium",
"C": "C Low",
"D": "D Ultra Low"
},
"metersToFeet": {
"source": "...",
"caution": "Do not use for approach minima.",
"entries": [
[15100, 49500],
[300, 1000]
]
},
"chinaFLAS": {
"source": "...",
"entries": [
[15500, 50900],
[100, 300]
],
"rvsmEastbound": [29100, 31100, 33100, 35100, 37100, 39100, 41100],
"rvsmWestbound": [30100, 32100, 34100, 36100, 38100, 40100]
}
}
```
The `entries` arrays are `[meters, feet]` pairs, sorted descending by meters. RVSM arrays list feet values assigned to each direction.
### Aircraft JSON Files
Each file represents one airframe. The filename (minus `.json`) is the aircraft ID used throughout the app.
#### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| `_meta.format` | string | Must be `"pilot-toolkit-aircraft"` |
| `_meta.version` | number | Schema version (currently `3`) |
| `_meta.aircraftId` | string | Must match the filename |
| `label` | string | Display name (e.g., `"G700"`) |
| `available` | boolean | `true` if the aircraft has enough data for calculations; `false` shows it as a placeholder |
#### Optional Fields (null if not available)
| Field | Type | Description |
|-------|------|-------------|
| `weightLimits` | object | `minBew`, `mtow`, `maxRamp` (all integers, lbs) |
| `eswl` | object | ESWL configuration — see below |
| `pcn` | object | PCN regression coefficients — see below |
| `pcr` | object | PCR regression coefficients — same structure as `pcn` |
| `pcnError` | string | Error margin (e.g., `"±2%"`) |
| `pcrError` | string | Error margin |
| `windLimitations` | object | Reserved for future use, currently `null` |
| `fuelBuckets` | array | Array of `[stageLength, fuelBurn]` pairs |
#### ESWL — Two Types
The `eswl` field uses a `type` discriminator.
**Formula type** (G450, G650): ESWL is calculated as `weight × weightFactor ÷ wheelFactor`.
```json
"eswl": {
"type": "formula",
"weightFactor": 0.45,
"wheelFactor": 1.23
}
```
**Thresholds type** (G700): Manufacturer publishes fixed values.
```json
"eswl": {
"type": "thresholds",
"singleWheel": { "label": "S-85", "value": 85000, "unit": "lbs" },
"dualWheel": { "label": "D-108", "value": 108000, "unit": "lbs" },
"note": "Unrestricted operations at or above these values."
}
```
#### Pavement Coefficients (PCN/PCR)
Linear regression: `result = slope × weight + intercept`. The structure is the same for both `pcn` and `pcr`:
```json
"pcn": {
"rigid": {
"A": { "slope": 0.0003323, "intercept": -2.4247 },
"B": { "slope": 0.0003268, "intercept": -1.2011 },
"C": { "slope": 0.0003521, "intercept": -2.1385 },
"D": { "slope": 0.0003547, "intercept": -1.5244 }
},
"flexible": {
"A": { "slope": 0.000286, "intercept": -2.4517 },
"...": "..."
}
}
```
Each combination of pavement type (rigid/flexible) and subgrade (A/B/C/D) has its own slope and intercept. These are derived from linear regression fits to official aircraft manual charts.
#### Fuel Buckets
Array of `[stageLength, fuelBurn]` pairs sorted by stage length. Stage length is in decimal hours; fuel burn is in lbs/hr.
```json
"fuelBuckets": [
[0.1, 650.0],
[1.0, 548.5],
[3.0, 449.0],
[9.9, 456.6]
]
```
Set to `null` if no built-in data is available for the airframe. Users can still load operator-specific data via CSV or JSON import.
### Adding a New Airframe
1. Create a new JSON file in `aircraft/` (e.g., `G280.json`).
2. Set `_meta.aircraftId` and `label` to match.
3. Set `available` to `false` if the data is incomplete — the app shows the aircraft in the selector but disables calculations.
4. Populate whichever fields you have. Everything except `_meta`, `label`, and `available` is nullable.
5. On Android, the file is auto-discovered from the `aircraft/` directory. On web, add an import line and a `loadAircraft()` call in `toolkit.jsx`.
---
## Exchange Formats
### .ptz File Format
The `.ptz` format is a **gzipped JSON array** of passdown objects. The format is open — third-party tools and operators are free to generate or consume `.ptz` files.
To inspect a `.ptz` file manually: `gunzip < PASSDOWN_N12345_2026-02-28.ptz | python3 -m json.tool`
Each object in the array has the following fields:
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier (UUID-style) |
| `date` | string | Passdown date (`YYYY-MM-DD`) |
| `registration` | string | Aircraft registration (e.g., `N12345`) |
| `airport` | string | ICAO code (e.g., `KPTK`) |
| `fbo` | string | FBO name |
| `fboPhone` | string | FBO phone number |
| `pic` | string | PIC email |
| `sic` | string | SIC email |
| `ca1` | string | Lead cabin attendant email |
| `oncomingPic` | string | Oncoming PIC email |
| `oncomingSic` | string | Oncoming SIC email |
| `maintenance` | string | Maintenance status / squawks |
| `narrative` | string | Free-text operational notes |
| `createdAt` | string | ISO 8601 timestamp |
| `updatedAt` | string | ISO 8601 timestamp |
| `lastSentAt` | string | ISO 8601 timestamp (empty string if never sent) |
Filename conventions:
- Single passdown: `PASSDOWN_REG_DATE.ptz`
- Batch (single tail): `PASSDOWN_BATCH_REG_FROMDATE_TODATE.ptz`
- Batch (mixed tails): `PASSDOWN_BATCH_Nrecords_FROMDATE_TODATE.ptz`
On import, duplicates are detected by `id` and flagged in the preview screen.
### Fuel Profile JSON Format
Exported as `FUEL_PROFILE_name_aircraft.json`. The app validates the `format` field on import.
```json
{
"format": "pilot-toolkit-fuel-profile",
"version": 1,
"aircraft": "G450",
"aircraftLabel": "G450",
"name": "FLEXJET G450",
"entries": 99,
"buckets": [
[0.1, 650.0],
[1.0, 548.5],
[9.9, 456.6]
]
}
```
| Field | Required on Import | Description |
|-------|-------------------|-------------|
| `format` | Yes | Must be `"pilot-toolkit-fuel-profile"` |
| `aircraft` | Yes | Target aircraft ID (e.g., `"G450"`) |
| `name` | No | Profile name (defaults to `"Imported"`) |
| `buckets` | Yes | Array of `[stageLength, fuelBurn]` pairs |
| `version`, `aircraftLabel`, `entries` | No | Informational; ignored on import |
### Fuel Bucket CSV Format
Two columns: stage length (decimal hours) and fuel burn (lbs/hr). Header row is optional and auto-detected.
```
stage_length,fuel_burn
0.1,650
1.0,548.5
3.0,449
9.9,456.6
```
On import, the app prompts for a profile name. The data replaces any existing profile for the selected aircraft.
---
## Settings
Available in the hamburger menu (☰):
- **Aircraft selector** — Sets the active airframe for pavement, fuel order, and fuel bucket modules. "Set Default" persists the choice across sessions.
- **Dark mode** — Toggles light/dark theme.
- **Primary volume unit** — US gallons, imperial gallons, or liters. Affects fuel order output. Auto-detected from locale on first use.
- **Passdown retention** — How long to keep passdown history: 3, 6, 12, 24, or 36 months, or never delete. Auto-purge runs on app open.
- **Fuel profiles** — View, export, and clear loaded profiles per aircraft. CSV and JSON import.

104
ROADMAP.md Normal file
View file

@ -0,0 +1,104 @@
# Pilot's Toolkit — Working Roadmap
*Last updated: March 4, 2026*
*For completed milestones, see COMPLETED.md*
---
> **RC1 locked — February 28, 2026.** Seven modules, both platforms, data externalized.
> **RC2 planning — March 2026.** FBO Contact and Passdown Audit Trail targeted for next release.
---
## Phase I — Web & Android Apps
### RC2 Targets
- **FBO Contact** — New module: outbound communication tab for contacting FBOs ahead of arrival. FBO autocomplete (learned from usage, auto-populates email), request details field with placeholder prompts (purpose of visit, transportation, services, ETA/ETD, catering), saveable/editable signature block (pilot name, registration, contact info, FBO loyalty program numbers). Email compose on send. Rewards numbers excluded from bulk exports.
- **Passdown Audit Trail** — Diffs stored as a `history` array inline in the passdown JSON object. Each save captures timestamp, changed fields, and previous values. Compresses naturally inside existing `.ptz` gzip. Accessible from the passdown detail view only, via a disclosure element — main form and history list show only the final version. Retention policy on snapshots to keep record size bounded.
### Future Revisions
- **Crew Rest Calculator** — Simplified rest period calculator for augmented crews (34 pilots). Equal division of en route rest after 30-minute all-hands blocks at departure and arrival. Potential paid add-on. Timer/alarm deferred (Android foreground services, notification channels). Full Part 117 compliance may not be attainable — regulatory liability is a fundamentally different risk profile than the rest of the toolkit.
- **Crosswind Limits Overlay** — Per-aircraft wind limitations. Placeholder field exists in aircraft JSON. Blocked by data sourcing complexity (OEM vs operator limits, angle-dependent calculations).
- **Room/SQLite Migration (Android)** — Migrate passdown storage from SharedPreferences to Room for better query/filter scalability.
### Removed from Consideration
- ~~Unit Converter~~ — Duplicates existing EFB tools and web searches.
- ~~Pressure Conversion~~ — Marginal standalone value.
- ~~Density Altitude Calculator~~ — Covered by existing fleet performance tools.
---
## Phase II — Docker Deployment
Single Docker container hosting the full stack: nginx (static Vite frontend + API proxy) + Node/Express (thin REST API, session-based auth) + SQLite (single-file database). Designed for homelab or small-group hosting. Doubles as an iOS demonstrator — iOS users access the web app through a browser before native iOS is built.
- **Multi-user requirement:** User accounts with data partitioning. Each user's passdowns, lookups, and settings are isolated — no cross-visibility. Initial auth model: email + provided password, user changes password at first login. Managed by a small trusted group; more sophisticated auth layered on later if the user base grows.
- **Frontend migration:** Storage calls swap from IndexedDB/localStorage to `fetch('/api/...')`. Data model, UI, and module logic stay untouched.
- **API routes:** Auth (login/logout/session), passdowns CRUD, lookups CRUD, settings, .ptz import/export.
- **Deployment:** Single `docker-compose.yml`, one volume for the SQLite database file. Exposed on port 38911 (no registered IANA service, low bot visibility). Runs on homelab HPC server.
- **Security posture (proof of concept):**
- nginx rate limiting on login endpoint (e.g., 5 attempts/min/IP → 429).
- fail2ban monitoring nginx logs for 429s/401s — auto-bans offending IPs via iptables (e.g., 5 failures in 10 min → 1 hour ban, escalating for repeat offenders).
- Session-based auth: user logs in once, stays logged in until explicit logout or token expiry (controlled server-side). No per-login OTP friction.
- High port number provides minor obscurity benefit (not a security measure, but reduces noise from common-port scanners).
- **Security upgrades (post-approval):** Cloudflare Tunnel (no open ports, email OTP gatekeeper, free tier, nothing to install on user devices — runs on server only) or VPN (Tailscale/WireGuard) if users can install software on their devices. Both are future options, not proof-of-concept requirements. Cloudflare session windows are configurable (24h to 30 days) but still require periodic re-auth, which adds friction for frequent daily use.
- **Data security (future — managed device deployment):** Current .ptz files are plain-text gzipped JSON — readable by anyone with `gunzip`. Acceptable for a hand-selected beta group, but operationally sensitive data (maintenance squawks, crew schedules, tail numbers) must be protected on managed devices. Planned approach: when the Docker backend is live, passdown sync goes through the authenticated API — no .ptz files over email as the primary flow. The server controls who sees what; unmanaged/unauthorized devices simply can't authenticate. For offline/disconnected scenarios where .ptz file transfer is still needed, options include symmetric encryption (AES with operator-level shared passphrase) or per-user keys issued by the server. Key management complexity scales with the auth model — keep it simple until the user base demands otherwise. The goal: make it difficult for an individual to exfiltrate company operational data from a managed device.
- **Mobile apps:** Stay local-only unless API sync is added later. Manual .ptz import/export provides a sync path for small groups in the meantime.
---
## Phase III — iOS (SwiftUI)
- **Status:** Planning only. Architecture mapped out (SwiftUI + UserDefaults for persistence).
- **Prerequisite:** Apple Developer Program ($99/year). Docker deployment (Phase II) provides a web-based stopgap for iOS users in the meantime.
- **Approach:** Incremental migration in small, manageable chunks mirroring the Android module structure. Each module ported and tested independently before moving to the next. Allows troubleshooting at each step without large-scale rework.
- **Migration order (tentative):**
1. App shell — navigation, tab bar, hamburger menu, aircraft selector, dark mode, theme
2. Fuel Order Calculator — simplest module, validates the SwiftUI patterns and data flow
3. Crosswind Calculator — standalone, no data dependencies
4. Pavement Strength — aircraft-dependent, tests data model integration
5. Fuel Buckets — slider, interpolation, CSV import, named profiles
6. Flight Level ↔ Meters — static data tables, lookup search, FLAS direction filter
7. HF Frequencies — network fetch, HTML parsing
8. Passdown Module — most complex, all CRUD + storage + export
- **Storage:** Core Data or SwiftData for structured records, UserDefaults for preferences.
- **Notable differences:** iOS will need its own .ptz export mechanism (likely via share sheet using UIActivityViewController), and HF fetch should work natively (no CORS restriction).
---
## Phase IV — Future Enhancements
### Ride Report / Turbulence Sensing
Use phone accelerometer hardware to detect and classify turbulence intensity in real time.
- **v1 (near-term, once core stable):** Simple vertical g-meter with real-time display, min/max peak hold, color-coded intensity gauge (light/moderate/severe/extreme bands).
- **v2 (requires research):** Filtered intensity classification with time history graph. Key challenge is signal processing — low-pass/high-pass filter design, possibly Kalman filter for orientation fusion (accelerometer + gyroscope), windowing functions to separate turbulence from handling noise. This is a signal processing problem, not just linear math.
- **v3 (ambitious, far future):** Crowd-sourced ride reports with GPS location, altitude, and intensity. Essentially a mini Turbulence Aware / PIREP system.
- **APIs:** Web — DeviceMotion API. Android — SensorManager. iOS — CoreMotion.
- **Status:** Wish list. Filtering approach needs dedicated research before implementation.
### App Icon
Ideas discussed: compass rose, PT monogram, stylized wing/chevron, wrench+propeller hybrid, flight instrument silhouette. Compass rose and PT monogram scale best to small sizes (favicon, 48px Android icon).
---
## Aircraft Data (Suspended)
Resumed when source data becomes available.
- **G650** — ESWL formula working, max ramp weight needs confirmation (100,000 vs 104,000 lbs). ACN/ACR formulas not yet started (charts need deciphering).
- **G700 fuel buckets** — Data not available for an indefinite period.
- **G500** — Placeholder only. No pavement, weight, or bucket data yet.
- **G450** — Updated pavement calculations pending integration of corrected data.
---
## Known Issues
- **BlueMail (Android):** Send button uses `ACTION_SEND` with `message/rfc822` (.ptz attachment + plain text body + TO/CC). BlueMail receives the attachment but may not populate recipient fields or body text. Works correctly with Gmail and other standard email clients. BlueMail not presently supported; investigating.

498
aircraft/G450.json Normal file
View file

@ -0,0 +1,498 @@
{
"_meta": {
"format": "pilot-toolkit-aircraft",
"version": 3,
"aircraftId": "G450",
"generated": "2026-02-28"
},
"label": "G450",
"available": true,
"weightLimits": {
"minBew": 42000,
"mtow": 74600,
"maxRamp": 75000
},
"eswl": {
"type": "formula",
"weightFactor": 0.45,
"wheelFactor": 1.23,
"formula": "weight × weightFactor ÷ wheelFactor"
},
"pcn": {
"rigid": {
"A": {
"slope": 0.0003669,
"intercept": -3.0215
},
"B": {
"slope": 0.0003742,
"intercept": -2.777
},
"C": {
"slope": 0.000378,
"intercept": -2.58
},
"D": {
"slope": 0.0003813,
"intercept": -2.3455
}
},
"flexible": {
"A": {
"slope": 0.0003187,
"intercept": -2.9945
},
"B": {
"slope": 0.0003296,
"intercept": -3.036
},
"C": {
"slope": 0.0003467,
"intercept": -3.0745
},
"D": {
"slope": 0.0003416,
"intercept": -1.796
}
}
},
"pcr": {
"rigid": {
"A": {
"slope": 0.003596,
"intercept": -32.304
},
"B": {
"slope": 0.003658,
"intercept": -29.142
},
"C": {
"slope": 0.003692,
"intercept": -26.508
},
"D": {
"slope": 0.003731,
"intercept": -25.019
}
},
"flexible": {
"A": {
"slope": 0.002592,
"intercept": -21.282
},
"B": {
"slope": 0.003723,
"intercept": -65.027
},
"C": {
"slope": 0.004177,
"intercept": -68.073
},
"D": {
"slope": 0.0041,
"intercept": -41
}
}
},
"pcnError": "±3%",
"pcrError": "not calculated",
"windLimitations": null,
"fuelBuckets": [
[
0.1,
650
],
[
0.2,
650
],
[
0.3,
650
],
[
0.4,
650
],
[
0.5,
650
],
[
0.6,
650
],
[
0.7,
650
],
[
0.8,
650
],
[
0.9,
650
],
[
1,
548.5
],
[
1.1,
534
],
[
1.2,
523
],
[
1.3,
513.5
],
[
1.4,
504.5
],
[
1.5,
496.5
],
[
1.6,
489.5
],
[
1.7,
483.5
],
[
1.8,
478
],
[
1.9,
474
],
[
2,
470.5
],
[
2.1,
467.5
],
[
2.2,
464.5
],
[
2.3,
462
],
[
2.4,
459.5
],
[
2.5,
457
],
[
2.6,
455
],
[
2.7,
453
],
[
2.8,
451.5
],
[
2.9,
450
],
[
3,
449
],
[
3.1,
448.3
],
[
3.2,
448.5
],
[
3.3,
448.7
],
[
3.4,
448.9
],
[
3.5,
449.1
],
[
3.6,
449.3
],
[
3.7,
449.5
],
[
3.8,
449.7
],
[
3.9,
449.9
],
[
4,
450.1
],
[
4.1,
450.3
],
[
4.2,
450.5
],
[
4.3,
450.7
],
[
4.4,
450.9
],
[
4.5,
451.1
],
[
4.6,
451.3
],
[
4.7,
451.5
],
[
4.8,
451.7
],
[
4.9,
451.9
],
[
5,
452.1
],
[
5.1,
452.3
],
[
5.2,
452.5
],
[
5.3,
452.7
],
[
5.4,
452.9
],
[
5.5,
453.1
],
[
5.6,
453.3
],
[
5.7,
453.5
],
[
5.8,
453.7
],
[
5.9,
453.9
],
[
6,
454.2
],
[
6.1,
454.5
],
[
6.2,
454.8
],
[
6.3,
455.1
],
[
6.4,
455.4
],
[
6.5,
455.7
],
[
6.6,
456
],
[
6.7,
456.3
],
[
6.8,
456.6
],
[
6.9,
456.6
],
[
7,
456.6
],
[
7.1,
456.6
],
[
7.2,
456.6
],
[
7.3,
456.6
],
[
7.4,
456.6
],
[
7.5,
456.6
],
[
7.6,
456.6
],
[
7.7,
456.6
],
[
7.8,
456.6
],
[
7.9,
456.6
],
[
8,
456.6
],
[
8.1,
456.6
],
[
8.2,
456.6
],
[
8.3,
456.6
],
[
8.4,
456.6
],
[
8.5,
456.6
],
[
8.6,
456.6
],
[
8.7,
456.6
],
[
8.8,
456.6
],
[
8.9,
456.6
],
[
9,
456.6
],
[
9.1,
456.6
],
[
9.2,
456.6
],
[
9.3,
456.6
],
[
9.4,
456.6
],
[
9.5,
456.6
],
[
9.6,
456.6
],
[
9.7,
456.6
],
[
9.8,
456.6
],
[
9.9,
456.6
]
]
}

18
aircraft/G500.json Normal file
View file

@ -0,0 +1,18 @@
{
"_meta": {
"format": "pilot-toolkit-aircraft",
"version": 3,
"aircraftId": "G500",
"generated": "2026-02-28"
},
"label": "G500",
"available": false,
"weightLimits": null,
"eswl": null,
"pcn": null,
"pcr": null,
"pcnError": null,
"pcrError": null,
"windLimitations": null,
"fuelBuckets": null
}

27
aircraft/G650.json Normal file
View file

@ -0,0 +1,27 @@
{
"_meta": {
"format": "pilot-toolkit-aircraft",
"version": 3,
"aircraftId": "G650",
"generated": "2026-02-28"
},
"label": "G650",
"available": true,
"weightLimits": {
"minBew": 52600,
"mtow": 99600,
"maxRamp": 100000
},
"eswl": {
"type": "formula",
"weightFactor": 0.45,
"wheelFactor": 1.28,
"formula": "weight × weightFactor ÷ wheelFactor"
},
"pcn": null,
"pcr": null,
"pcnError": null,
"pcrError": null,
"windLimitations": null,
"fuelBuckets": null
}

109
aircraft/G700.json Normal file
View file

@ -0,0 +1,109 @@
{
"_meta": {
"format": "pilot-toolkit-aircraft",
"version": 3,
"aircraftId": "G700",
"generated": "2026-02-28"
},
"label": "G700",
"available": true,
"weightLimits": {
"minBew": 56000,
"mtow": 107600,
"maxRamp": 108000
},
"eswl": {
"type": "thresholds",
"singleWheel": {
"label": "S-85",
"value": 85000,
"unit": "lbs"
},
"dualWheel": {
"label": "D-108",
"value": 108000,
"unit": "lbs"
},
"note": "Unrestricted operations at or above these values."
},
"pcn": {
"rigid": {
"A": {
"slope": 0.0003323,
"intercept": -2.4247
},
"B": {
"slope": 0.0003268,
"intercept": -1.2011
},
"C": {
"slope": 0.0003521,
"intercept": -2.1385
},
"D": {
"slope": 0.0003547,
"intercept": -1.5244
}
},
"flexible": {
"A": {
"slope": 0.000286,
"intercept": -2.4517
},
"B": {
"slope": 0.0003167,
"intercept": -3.794
},
"C": {
"slope": 0.0003145,
"intercept": -1.7641
},
"D": {
"slope": 0.0003221,
"intercept": -0.2135
}
}
},
"pcr": {
"rigid": {
"A": {
"slope": 0.0033054,
"intercept": -25.3477
},
"B": {
"slope": 0.0033973,
"intercept": -24.1697
},
"C": {
"slope": 0.0034952,
"intercept": -24.4997
},
"D": {
"slope": 0.0035379,
"intercept": -21.5928
}
},
"flexible": {
"A": {
"slope": 0.0022591,
"intercept": -3.1378
},
"B": {
"slope": 0.0027682,
"intercept": -29.7287
},
"C": {
"slope": 0.0034418,
"intercept": -62.6803
},
"D": {
"slope": 0.0038994,
"intercept": -66.621
}
}
},
"pcnError": "±2%",
"pcrError": "±2%",
"windLimitations": null,
"fuelBuckets": null
}

680
reference.json Normal file
View file

@ -0,0 +1,680 @@
{
"_meta": {
"format": "pilot-toolkit-data",
"version": 3,
"generated": "2026-02-28",
"notes": "Reference data is airframe-agnostic. Not included in per-aircraft export/import."
},
"pavementSubgrades": {
"A": "A High",
"B": "B Medium",
"C": "C Low",
"D": "D Ultra Low"
},
"metersToFeet": {
"source": "GVIII-G700 Operating Manual Table 15, 06-10-00, 2024-03-29",
"caution": "Do not use for approach minima. Values rounded to nearest 100 ft.",
"note": "Because of rounding differences, most metric flight levels can be satisfied by two equivalent feet values. Of the two, the closest value in feet is used in this table.",
"entries": [
[
15100,
49500
],
[
14100,
46300
],
[
13100,
43000
],
[
12800,
42000
],
[
12500,
41000
],
[
12200,
40000
],
[
12100,
39700
],
[
11900,
39000
],
[
11600,
38100
],
[
11300,
37100
],
[
11100,
36400
],
[
10900,
35800
],
[
10600,
34800
],
[
10300,
33800
],
[
10100,
33100
],
[
9900,
32500
],
[
9600,
31500
],
[
9300,
30500
],
[
9100,
29900
],
[
8900,
29200
],
[
8600,
28200
],
[
8300,
27200
],
[
8100,
26600
],
[
8000,
26200
],
[
7900,
25900
],
[
7800,
25600
],
[
7700,
25300
],
[
7600,
24900
],
[
7500,
24600
],
[
7400,
24300
],
[
7300,
24000
],
[
7200,
23600
],
[
7100,
23300
],
[
7000,
23000
],
[
6900,
22600
],
[
6800,
22300
],
[
6700,
22000
],
[
6600,
21700
],
[
6500,
21300
],
[
6400,
21000
],
[
6300,
20700
],
[
6200,
20300
],
[
6100,
20000
],
[
6000,
19700
],
[
5900,
19400
],
[
5800,
19000
],
[
5700,
18700
],
[
5600,
18400
],
[
5500,
18000
],
[
5400,
17700
],
[
5300,
17400
],
[
5200,
17100
],
[
5100,
16700
],
[
5000,
16400
],
[
4900,
16100
],
[
4800,
15700
],
[
4700,
15400
],
[
4600,
15100
],
[
4500,
14800
],
[
4400,
14400
],
[
4300,
14100
],
[
4200,
13800
],
[
4100,
13500
],
[
4000,
13100
],
[
3900,
12800
],
[
3800,
12500
],
[
3700,
12100
],
[
3600,
11800
],
[
3500,
11500
],
[
3400,
11200
],
[
3300,
10800
],
[
3200,
10500
],
[
3100,
10200
],
[
3000,
9800
],
[
2900,
9500
],
[
2800,
9200
],
[
2700,
8900
],
[
2600,
8500
],
[
2500,
8200
],
[
2400,
7900
],
[
2300,
7500
],
[
2200,
7200
],
[
2100,
6900
],
[
2000,
6600
],
[
1900,
6200
],
[
1800,
5900
],
[
1700,
5600
],
[
1600,
5200
],
[
1500,
4900
],
[
1400,
4600
],
[
1300,
4300
],
[
1200,
3900
],
[
1100,
3600
],
[
1000,
3300
],
[
900,
3000
],
[
850,
2800
],
[
800,
2600
],
[
750,
2500
],
[
700,
2300
],
[
650,
2100
],
[
600,
2000
],
[
550,
1800
],
[
500,
1600
],
[
450,
1500
],
[
400,
1300
],
[
350,
1100
],
[
300,
1000
]
]
},
"chinaFLAS": {
"source": "GVIII-G700 Operating Manual Figure 5, 06-10-00, 2024-03-29",
"caution": "Do not use for approach minima. Values rounded to nearest 100 ft.",
"notes": [
"ATC will issue the Flight Level clearance in meters.",
"Pilots shall use the China RVSM FLAS table to determine the corresponding flight level in feet.",
"The aircraft shall be flown using the flight level in FEET.",
"Due to rounding differences, the metric readout of the onboard avionics will not necessarily correspond to the cleared Flight Level in meters however the difference will never be more than 30 meters."
],
"entries": [
[
15500,
50900
],
[
14900,
48900
],
[
14300,
46900
],
[
13700,
44900
],
[
13100,
43000
],
[
12500,
41100
],
[
12200,
40100
],
[
11900,
39100
],
[
11600,
38100
],
[
11300,
37100
],
[
11000,
36100
],
[
10700,
35100
],
[
10400,
34100
],
[
10100,
33100
],
[
9800,
32100
],
[
9500,
31100
],
[
9200,
30100
],
[
8900,
29100
],
[
8400,
27600
],
[
8100,
26600
],
[
7800,
25600
],
[
7500,
24600
],
[
7200,
23600
],
[
6900,
22600
],
[
6600,
21700
],
[
6300,
20700
],
[
6000,
19700
],
[
5700,
18700
],
[
5400,
17700
],
[
5100,
16700
],
[
4800,
15700
],
[
4500,
14800
],
[
4200,
13800
],
[
3900,
12800
],
[
3600,
11800
],
[
3300,
10800
],
[
3000,
9800
],
[
2700,
8900
],
[
2400,
7900
],
[
2100,
6900
],
[
1800,
5900
],
[
1500,
4900
],
[
1200,
3900
],
[
900,
3000
],
[
600,
2000
],
[
500,
1600
],
[
400,
1300
],
[
300,
1000
],
[
200,
700
],
[
100,
300
]
],
"rvsmEastbound": [
29100,
31100,
33100,
35100,
37100,
39100,
41100
],
"rvsmWestbound": [
30100,
32100,
34100,
36100,
38100,
40100
],
"eastboundHeading": "0° to 179°",
"westboundHeading": "186° to 359°"
}
}