Merge branch 'main' of https://git.bennu.duckdns.org/jshackney/tabulae-aeriae
Initial Commit/Sync
This commit is contained in:
commit
b52a68edb3
457
documentation/administrative/tabulae-aeriae-data-dictionary.md
Normal file
457
documentation/administrative/tabulae-aeriae-data-dictionary.md
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
# Tabulae Aeriae — Data Dictionary
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** March 29, 2026
|
||||
|
||||
This document defines every table and field in the Tabulae Aeriae database schema. All tables use PostgreSQL. All timestamps are stored in UTC. All time durations are stored as INTEGER minutes.
|
||||
|
||||
---
|
||||
|
||||
## Table: `users`
|
||||
|
||||
Pilot accounts, authentication credentials, and profile settings.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `username` | VARCHAR(100) | NO | — | Unique login username |
|
||||
| `email` | VARCHAR(255) | YES | — | Unique email address |
|
||||
| `password_hash` | TEXT | NO | — | Bcrypt-hashed password |
|
||||
| `role` | VARCHAR(20) | NO | `'pilot'` | User role: `pilot`, `admin` |
|
||||
| `tier` | VARCHAR(20) | NO | `'free'` | Feature tier: `free`, `professional` |
|
||||
| `pilot_cert_num` | VARCHAR(50) | YES | — | FAA pilot certificate number |
|
||||
| `regulatory_profile` | VARCHAR(10) | NO | `'faa'` | Active regulatory framework: `faa`, `easa`, `both` |
|
||||
| `auth_provider` | VARCHAR(30) | NO | `'password'` | Auth method: `password`, `google`, `oidc`, `ldap` |
|
||||
| `external_auth_id` | VARCHAR(255) | YES | — | External provider's user ID (for OAuth/LDAP) |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp (UTC) |
|
||||
| `updated_at` | TIMESTAMPTZ | NO | `now()` | Last modification timestamp (UTC) |
|
||||
|
||||
**Indexes:**
|
||||
- `users_username_key` — UNIQUE on `username`
|
||||
- `users_email_key` — UNIQUE on `email`
|
||||
|
||||
**Audit:** Tracked (INSERT/UPDATE/DELETE trigger → `audit_log`)
|
||||
|
||||
---
|
||||
|
||||
## Table: `flights`
|
||||
|
||||
Core logbook entries. Each row represents one flight leg or one simulator/training device session. All time fields are stored as INTEGER minutes. All timestamps are UTC.
|
||||
|
||||
### Identification
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id`. Owner of this flight record |
|
||||
|
||||
### Date, Route, and Block Times
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `flight_date` | DATE | NO | — | Date of flight (UTC date) |
|
||||
| `departure_airport` | VARCHAR(10) | NO | — | Departure airport code (ICAO or FAA LID). Stored as-entered for historical accuracy |
|
||||
| `arrival_airport` | VARCHAR(10) | NO | — | Arrival airport code. Stored as-entered for historical accuracy |
|
||||
| `route` | TEXT | YES | — | Free-text route of flight (intermediate waypoints, airways, etc.) |
|
||||
| `departure_time` | TIMESTAMPTZ | YES | — | Block out / wheels up time (UTC). NULL if pilot logs duration only |
|
||||
| `arrival_time` | TIMESTAMPTZ | YES | — | Block in / wheels down time (UTC). NULL if pilot logs duration only |
|
||||
|
||||
### Aircraft
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `aircraft_id` | UUID | YES | — | FK → `aircraft.id`. Links to aircraft or simulator record |
|
||||
| `tail_number` | VARCHAR(20) | NO | — | Aircraft registration or simulator identifier. Denormalized from aircraft record for historical accuracy (registrations change over time) |
|
||||
|
||||
### Flight Times (INTEGER minutes)
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `total_time` | INTEGER | NO | `0` | Total flight/session time. Authoritative value — may be auto-derived from departure/arrival times but is always manually overridable |
|
||||
| `pic_time` | INTEGER | YES | `0` | Pilot-in-command time |
|
||||
| `sic_time` | INTEGER | YES | `0` | Second-in-command time |
|
||||
| `solo_time` | INTEGER | YES | `0` | Solo flight time |
|
||||
| `dual_received` | INTEGER | YES | `0` | Dual instruction received time |
|
||||
| `dual_given` | INTEGER | YES | `0` | Instruction given time (CFI logging) |
|
||||
| `cross_country` | INTEGER | YES | `0` | Cross-country time. Pilot's determination per 14 CFR 61.1(b) |
|
||||
| `night_time` | INTEGER | YES | `0` | Night time (sunset to sunrise definition per 61.51(b)(3)(i)) |
|
||||
|
||||
### Instrument
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `actual_instrument` | INTEGER | YES | `0` | Time in actual instrument meteorological conditions |
|
||||
| `simulated_instrument` | INTEGER | YES | `0` | Simulated instrument time (hood/foggles/view-limiting device) |
|
||||
| `holds` | SMALLINT | YES | `0` | Number of holding patterns performed |
|
||||
| `approaches` | JSONB | YES | `'[]'` | Instrument approaches. Array of objects: `[{"type": "ILS", "airport": "KTPA", "runway": "19L", "notes": ""}]`. Type validated against approach enum |
|
||||
|
||||
**Approach type enum values:** `ILS`, `LOC`, `LOC-BC`, `VOR`, `VOR/DME`, `NDB`, `RNAV (GPS)`, `RNAV (RNP)`, `LPV`, `LNAV`, `LNAV/VNAV`, `GPS`, `MLS`, `SDF`, `LDA`, `PAR`, `ASR`, `VISUAL`, `CONTACT`
|
||||
|
||||
### Takeoffs and Landings
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `day_takeoffs` | SMALLINT | YES | `0` | Day takeoffs (sun above horizon at departure airport at departure time) |
|
||||
| `night_takeoffs` | SMALLINT | YES | `0` | Night takeoffs (1 hr after sunset to 1 hr before sunrise per 61.57(b) at departure airport) |
|
||||
| `day_landings` | SMALLINT | YES | `0` | Day landings |
|
||||
| `night_landings` | SMALLINT | YES | `0` | Night landings (1 hr after sunset to 1 hr before sunrise per 61.57(b) at arrival airport) |
|
||||
| `day_full_stop` | SMALLINT | YES | `0` | Day full-stop landings (required for tailwheel currency) |
|
||||
| `night_full_stop` | SMALLINT | YES | `0` | Night full-stop landings (required for night currency per 61.57(b)) |
|
||||
|
||||
**Note on day/night classification:** Takeoffs and landings are independently classified as day or night. A single-leg flight may have a day takeoff and a night landing (or vice versa). Auto-suggestion is computed from departure/arrival times and airport solar position, always pilot-overridable.
|
||||
|
||||
### Simulator / Training Device
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `simulator_time` | INTEGER | YES | `0` | Time logged in an FFS, FTD, or ATD |
|
||||
| `sim_type` | VARCHAR(20) | YES | — | Type of device used for this session: `FFS`, `FTD`, `AATD`, `BATD`. NULL for real aircraft flights |
|
||||
| `ground_training` | INTEGER | YES | `0` | Ground instruction time (minutes) |
|
||||
|
||||
### Glider Operations
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `launch_type` | VARCHAR(20) | YES | — | Glider launch method: `aero_tow`, `ground_launch`, `powered_launch`, `self_launch`. NULL for non-glider flights. Required by 8710 for glider time reporting |
|
||||
|
||||
### People
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `persons_on_board` | TEXT | YES | — | Free text or crew listing |
|
||||
| `instructor_name` | VARCHAR(100) | YES | — | Name of instructor (when receiving instruction) |
|
||||
| `instructor_cert_num` | VARCHAR(50) | YES | — | Instructor's CFI certificate number |
|
||||
|
||||
### Remarks
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `remarks` | TEXT | YES | — | Free-text remarks, endorsement references, crew names, etc. |
|
||||
|
||||
### EASA Skeleton Fields
|
||||
|
||||
These columns are nullable and hidden in the UI when `users.regulatory_profile = 'faa'`. They exist to enable future EASA compliance without destructive schema migrations.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `multi_pilot_time` | INTEGER | YES | — | EASA: multi-pilot operations time (minutes) |
|
||||
| `co_pilot_time` | INTEGER | YES | — | EASA: co-pilot time (minutes) |
|
||||
| `spse_time` | INTEGER | YES | — | EASA: single-pilot single-engine time (minutes) |
|
||||
| `spme_time` | INTEGER | YES | — | EASA: single-pilot multi-engine time (minutes) |
|
||||
| `pic_name` | VARCHAR(100) | YES | — | EASA: name of PIC (required for all flights under EASA) |
|
||||
| `function_type` | VARCHAR(20) | YES | — | EASA: pilot function — `PIC`, `co-pilot`, `dual`, `instructor` |
|
||||
|
||||
### Custom and Extension Fields
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `custom_fields` | JSONB | YES | `'{}'` | User-defined fields for company, insurance, or personal tracking. Examples: `{"pf": true, "ioe_time": 120, "company_route_id": "ATL-MIA-142"}` |
|
||||
|
||||
### Metadata
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp (UTC) |
|
||||
| `updated_at` | TIMESTAMPTZ | NO | `now()` | Last modification timestamp (UTC) |
|
||||
|
||||
**Indexes:**
|
||||
- `idx_flights_user_date` — on `(user_id, flight_date DESC)`
|
||||
- `idx_flights_user_aircraft` — on `(user_id, aircraft_id)`
|
||||
|
||||
**Audit:** Tracked (INSERT/UPDATE/DELETE trigger → `audit_log`)
|
||||
|
||||
---
|
||||
|
||||
## Table: `aircraft`
|
||||
|
||||
Aircraft and training device records. Simulators and FTDs are stored here alongside real aircraft, distinguished by `is_simulator = TRUE`.
|
||||
|
||||
### Identification
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id`. Owner of this aircraft record |
|
||||
| `tail_number` | VARCHAR(20) | NO | — | Aircraft registration (e.g., `N453FX`) or simulator identifier (e.g., `SIM-GLF4-01`) |
|
||||
|
||||
### Aircraft Details
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `type_code` | VARCHAR(10) | YES | — | ICAO type designator (e.g., `C172`, `GLF4`, `B738`) |
|
||||
| `make_model` | VARCHAR(100) | NO | — | Full make and model (e.g., `Gulfstream G450`, `Cessna 172S Skyhawk SP`) |
|
||||
| `category_class` | VARCHAR(50) | NO | — | FAA category/class. Values: `ASEL`, `AMEL`, `ASES`, `AMES`, `RH`, `RG`, `GL`, `LTA-B`, `LTA-A`, `PL` |
|
||||
|
||||
### Aircraft Attributes
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `is_complex` | BOOLEAN | YES | `FALSE` | Complex aircraft (retractable gear + flaps + controllable prop) |
|
||||
| `is_high_perf` | BOOLEAN | YES | `FALSE` | High performance (>200 HP) |
|
||||
| `is_tailwheel` | BOOLEAN | YES | `FALSE` | Conventional landing gear (tailwheel/taildragger) |
|
||||
| `is_taa` | BOOLEAN | YES | `FALSE` | Technologically Advanced Aircraft (PFD + MFD + 2-axis autopilot) |
|
||||
| `gear_type` | VARCHAR(20) | YES | — | Landing gear type: `fixed_tri`, `retract`, `conventional`, `float`, `amphibious`, `ski` |
|
||||
| `engine_type` | VARCHAR(20) | YES | — | Engine type: `piston`, `turboprop`, `jet`, `electric` |
|
||||
| `num_engines` | SMALLINT | YES | `1` | Number of engines |
|
||||
|
||||
### Simulator / Training Device Fields
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `is_simulator` | BOOLEAN | YES | `FALSE` | TRUE for FFS, FTD, ATD devices; FALSE for real aircraft |
|
||||
| `device_level` | VARCHAR(20) | YES | — | Qualification level: `FFS_A`, `FFS_B`, `FFS_C`, `FFS_D`, `FTD_4`, `FTD_5`, `FTD_6`, `FTD_7`, `AATD`, `BATD`. NULL for real aircraft |
|
||||
| `device_serial` | VARCHAR(50) | YES | — | Device serial number or regulatory identifier. NULL for real aircraft |
|
||||
| `device_approval` | VARCHAR(100) | YES | — | FAA approval/qualification number. NULL for real aircraft |
|
||||
|
||||
### EASA Skeleton Fields
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `easa_type_designator` | VARCHAR(20) | YES | — | EASA type designator (may differ from ICAO) |
|
||||
| `sp_mp` | VARCHAR(5) | YES | — | Single-pilot or multi-pilot: `SP`, `MP` |
|
||||
|
||||
### Metadata
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp (UTC) |
|
||||
| `updated_at` | TIMESTAMPTZ | NO | `now()` | Last modification timestamp (UTC) |
|
||||
|
||||
**Indexes:**
|
||||
- UNIQUE on `(user_id, tail_number)`
|
||||
|
||||
**Audit:** Tracked (INSERT/UPDATE/DELETE trigger → `audit_log`)
|
||||
|
||||
---
|
||||
|
||||
## Table: `airports`
|
||||
|
||||
Airport reference data. Seeded from OurAirports (`davidmegginson/ourairports-data`). Users may add custom airports.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | SERIAL | NO | auto-increment | Primary key |
|
||||
| `icao_code` | VARCHAR(4) | YES | — | ICAO airport code (e.g., `KJFK`) |
|
||||
| `faa_code` | VARCHAR(4) | YES | — | FAA Location Identifier (e.g., `JFK`, `7B2`). For US airports, may differ from ICAO |
|
||||
| `iata_code` | VARCHAR(3) | YES | — | IATA code (e.g., `JFK`) |
|
||||
| `name` | VARCHAR(200) | NO | — | Airport name |
|
||||
| `city` | VARCHAR(100) | YES | — | Primary municipality served |
|
||||
| `state_region` | VARCHAR(100) | YES | — | State or administrative region |
|
||||
| `country` | VARCHAR(2) | YES | — | ISO 3166-1 alpha-2 country code |
|
||||
| `latitude` | DECIMAL(10,7) | YES | — | Latitude in decimal degrees (positive = north) |
|
||||
| `longitude` | DECIMAL(10,7) | YES | — | Longitude in decimal degrees (positive = east) |
|
||||
| `elevation_ft` | INTEGER | YES | — | Field elevation MSL in feet |
|
||||
| `type` | VARCHAR(30) | YES | — | Airport type: `large_airport`, `medium_airport`, `small_airport`, `heliport`, `seaplane_base`, `closed` |
|
||||
| `is_user_defined` | BOOLEAN | YES | `FALSE` | TRUE for user-added custom airports/strips |
|
||||
| `user_id` | UUID | YES | — | FK → `users.id`. NULL for system (OurAirports) records |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- `idx_airports_icao` — on `icao_code`
|
||||
- `idx_airports_faa` — on `faa_code`
|
||||
- `idx_airports_name` — GIN index on `to_tsvector('english', name)` for full-text search
|
||||
|
||||
---
|
||||
|
||||
## Table: `airport_identifiers`
|
||||
|
||||
Maps current and historical airport codes to physical airport records. Enables lookup of retired codes (e.g., 09G → KTEW) while preserving historical accuracy in flight records.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | SERIAL | NO | auto-increment | Primary key |
|
||||
| `airport_id` | INTEGER | NO | — | FK → `airports.id`. The physical airport this identifier refers to |
|
||||
| `identifier` | VARCHAR(10) | NO | — | The airport code (e.g., `09G`, `KTEW`, `JFK`) |
|
||||
| `identifier_type` | VARCHAR(10) | NO | — | Code type: `icao`, `faa`, `iata`, `former` |
|
||||
| `is_current` | BOOLEAN | YES | `TRUE` | TRUE if this code is currently active for the airport |
|
||||
| `effective_from` | DATE | YES | — | Date this code became active (NULL if unknown) |
|
||||
| `effective_to` | DATE | YES | — | Date this code was retired (NULL if currently active) |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
|
||||
**Indexes:**
|
||||
- UNIQUE on `(identifier, identifier_type)` WHERE `is_current = TRUE`
|
||||
- `idx_airport_ident_all` — on `identifier` (covers former codes)
|
||||
|
||||
**Resolution flow:** User enters airport code → system searches `airport_identifiers` → finds physical airport → resolves lat/long for map display → stores original code in flight record unchanged.
|
||||
|
||||
---
|
||||
|
||||
## Table: `pilot_qualifications`
|
||||
|
||||
Unified table for pilot certificates, ratings, endorsements, and currency events. Each row represents one qualification or one currency event.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id` |
|
||||
| `qual_type` | VARCHAR(30) | NO | — | Qualification type (see enum below) |
|
||||
| `certificate_grade` | VARCHAR(30) | YES | — | For `pilot_certificate`: `student`, `private`, `commercial`, `atp`, `atp_restricted` |
|
||||
| `certificate_number` | VARCHAR(50) | YES | — | FAA certificate number |
|
||||
| `rating_name` | VARCHAR(100) | YES | — | Rating or endorsement name (e.g., `ASEL`, `AMEL`, `CE-500`, `Instrument Airplane`, `Complex`, `High Performance`, `High Altitude`) |
|
||||
| `date_earned` | DATE | NO | — | Date the qualification was earned or event occurred |
|
||||
| `expiry_date` | DATE | YES | — | Expiration date (NULL for non-expiring qualifications) |
|
||||
| `flight_id` | UUID | YES | — | FK → `flights.id`. Links to the flight where this event occurred (NULL for events not tied to a flight record, e.g., ground-only checkrides, sim-only IPCs) |
|
||||
| `issuing_authority` | VARCHAR(100) | YES | — | Issuing body (e.g., `FAA`, `EASA`) |
|
||||
| `instructor_name` | VARCHAR(100) | YES | — | Name of endorsing instructor (for endorsements and currency events) |
|
||||
| `instructor_cert` | VARCHAR(50) | YES | — | Endorsing instructor's certificate number |
|
||||
| `examiner_name` | VARCHAR(100) | YES | — | Name of designated examiner (for checkrides, IPCs) |
|
||||
| `regulation_ref` | VARCHAR(50) | YES | — | Regulatory reference (e.g., `61.31(e)`, `61.56`, `61.57(d)`) |
|
||||
| `document_ref` | TEXT | YES | — | Reference to scan/photo of supporting document (future: attachment support) |
|
||||
| `notes` | TEXT | YES | — | Free-text notes |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
| `updated_at` | TIMESTAMPTZ | NO | `now()` | Last modification timestamp |
|
||||
|
||||
**`qual_type` enum values:**
|
||||
|
||||
| Value | Category | Description |
|
||||
|-------|----------|-------------|
|
||||
| `pilot_certificate` | Certificate | Pilot certificate (student, private, commercial, ATP) |
|
||||
| `flight_instructor` | Certificate | CFI, CFII, MEI certificate |
|
||||
| `ground_instructor` | Certificate | Basic, Advanced, or Instrument ground instructor |
|
||||
| `type_rating` | Rating | Aircraft type rating (e.g., CE-500, GLF4, B737) |
|
||||
| `instrument_rating` | Rating | Instrument rating by category |
|
||||
| `category_class_rating` | Rating | Additional category/class rating |
|
||||
| `endorsement` | Endorsement | Instructor endorsement (complex, high-perf, high-altitude, tailwheel, etc.) |
|
||||
| `flight_review` | Currency event | Flight review per 61.56 |
|
||||
| `ipc` | Currency event | Instrument proficiency check per 61.57(d) |
|
||||
| `checkride` | Currency event | Practical test (also satisfies flight review) |
|
||||
| `wings_phase` | Currency event | WINGS Pilot Proficiency Program phase completion |
|
||||
| `proficiency_check` | Currency event | Part 135/121 proficiency check |
|
||||
| `135_recurrent` | Currency event | Part 135 recurrent training completion |
|
||||
| `121_recurrent` | Currency event | Part 121 recurrent training completion |
|
||||
|
||||
**Indexes:**
|
||||
- `idx_qual_user_type` — on `(user_id, qual_type)`
|
||||
- `idx_qual_user_date` — on `(user_id, date_earned DESC)`
|
||||
|
||||
---
|
||||
|
||||
## Table: `medical_certificates`
|
||||
|
||||
FAA medical certificate records.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id` |
|
||||
| `class` | SMALLINT | NO | — | Medical class: `1`, `2`, `3` |
|
||||
| `issue_date` | DATE | NO | — | Date medical was issued |
|
||||
| `expiry_date` | DATE | NO | — | Calculated expiration date per 14 CFR 61.23 (based on age and class) |
|
||||
| `examiner_name` | VARCHAR(100) | YES | — | Aviation Medical Examiner (AME) name |
|
||||
| `notes` | TEXT | YES | — | Free-text notes (restrictions, limitations, BasicMed status, etc.) |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
|
||||
---
|
||||
|
||||
## Table: `saved_reports`
|
||||
|
||||
User-saved report configurations for one-click report generation.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id` |
|
||||
| `name` | VARCHAR(100) | NO | — | Report name (e.g., `8710 Application`, `Insurance Renewal`, `Annual Totals`) |
|
||||
| `description` | TEXT | YES | — | User-provided description of this report |
|
||||
| `report_type` | VARCHAR(30) | NO | — | Report category: `summary`, `detailed`, `8710`, `medical`, `currency`, `by_aircraft`, `custom` |
|
||||
| `config` | JSONB | NO | — | Full report configuration: date range, filters, grouping, columns, output format |
|
||||
| `is_quick_access` | BOOLEAN | YES | `FALSE` | If TRUE, report appears as a dashboard button for one-click access |
|
||||
| `sort_order` | SMALLINT | YES | `0` | Display order for quick-access buttons |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
| `updated_at` | TIMESTAMPTZ | NO | `now()` | Last modification timestamp |
|
||||
|
||||
**Config JSONB structure example:**
|
||||
```json
|
||||
{
|
||||
"date_range": {"type": "rolling", "months": 6},
|
||||
"filters": {"category_class": ["AMEL"], "aircraft_type": ["GLF4"]},
|
||||
"grouping": "month",
|
||||
"columns": ["total_time", "pic_time", "night_time", "actual_instrument", "approaches"],
|
||||
"output_format": "screen",
|
||||
"rounding": "truncate"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Table: `currency_rules`
|
||||
|
||||
JSON-driven currency rule definitions. System-defined rules (FAA defaults) have `user_id = NULL`. Users can add custom rules for personal minimums or company requirements.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | YES | — | FK → `users.id`. NULL for system-defined rules |
|
||||
| `name` | VARCHAR(100) | NO | — | Rule display name (e.g., `Night Currency — ASEL`, `IFR Currency`) |
|
||||
| `regulation_ref` | VARCHAR(50) | YES | — | Regulatory reference (e.g., `61.57(a)`, `61.57(c)`) |
|
||||
| `rule_config` | JSONB | NO | — | Complete rule configuration (lookback window, requirements, thresholds, grace periods, remediation text) |
|
||||
| `is_active` | BOOLEAN | YES | `TRUE` | Whether this rule is currently being evaluated |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
|
||||
**See Currency Rules Engine document for complete `rule_config` schema and examples.**
|
||||
|
||||
---
|
||||
|
||||
## Table: `audit_log`
|
||||
|
||||
Append-only changelog capturing all modifications to tracked tables. Populated automatically by PostgreSQL triggers on `flights`, `aircraft`, and `users`.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | BIGSERIAL | NO | auto-increment | Primary key |
|
||||
| `table_name` | VARCHAR(50) | NO | — | Name of the modified table |
|
||||
| `record_id` | UUID | NO | — | Primary key of the modified record |
|
||||
| `action` | VARCHAR(10) | NO | — | Operation type: `INSERT`, `UPDATE`, `DELETE` |
|
||||
| `old_value` | JSONB | YES | — | Full row before modification (NULL for INSERT) |
|
||||
| `new_value` | JSONB | YES | — | Full row after modification (NULL for DELETE) |
|
||||
| `changed_by` | UUID | YES | — | FK → `users.id`. User who made the change. Set via PostgreSQL session variable `app.current_user_id` |
|
||||
| `changed_at` | TIMESTAMPTZ | NO | `now()` | Timestamp of the change (UTC) |
|
||||
|
||||
**Indexes:**
|
||||
- `idx_audit_record` — on `(table_name, record_id)`
|
||||
- `idx_audit_time` — on `changed_at`
|
||||
|
||||
**Implementation:** PostgreSQL `AFTER INSERT OR UPDATE OR DELETE` trigger on each tracked table calls `audit_trigger_fn()`. The application middleware sets `SET LOCAL app.current_user_id = $1` on each transaction so the trigger can capture the acting user.
|
||||
|
||||
---
|
||||
|
||||
## Deferred Table: `pilot_status_periods`
|
||||
|
||||
**Not included in v1 schema. Designed for paid tier.**
|
||||
|
||||
Tracks date-range statuses such as high minimums captain periods.
|
||||
|
||||
| Column | Type | Nullable | Default | Description |
|
||||
|--------|------|----------|---------|-------------|
|
||||
| `id` | UUID | NO | `gen_random_uuid()` | Primary key |
|
||||
| `user_id` | UUID | NO | — | FK → `users.id` |
|
||||
| `status_type` | VARCHAR(50) | NO | — | Status type: `high_mins`, `line_check_required`, `probationary`, etc. |
|
||||
| `start_date` | DATE | NO | — | Start of status period |
|
||||
| `end_date` | DATE | YES | — | End of status period (NULL = still active) |
|
||||
| `notes` | TEXT | YES | — | Free-text notes |
|
||||
| `created_at` | TIMESTAMPTZ | NO | `now()` | Record creation timestamp |
|
||||
|
||||
---
|
||||
|
||||
## Foreign Key Reference Map
|
||||
|
||||
```
|
||||
users
|
||||
├── flights.user_id
|
||||
├── aircraft.user_id
|
||||
├── airports.user_id (user-defined airports only)
|
||||
├── pilot_qualifications.user_id
|
||||
├── medical_certificates.user_id
|
||||
├── saved_reports.user_id
|
||||
├── currency_rules.user_id (NULL for system rules)
|
||||
└── audit_log.changed_by
|
||||
|
||||
aircraft
|
||||
└── flights.aircraft_id
|
||||
|
||||
flights
|
||||
└── pilot_qualifications.flight_id
|
||||
|
||||
airports
|
||||
└── airport_identifiers.airport_id
|
||||
```
|
||||
275
documentation/administrative/tabulae-aeriae-scope-statement.md
Normal file
275
documentation/administrative/tabulae-aeriae-scope-statement.md
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# Tabulae Aeriae — Project Scope Statement
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** March 29, 2026
|
||||
**License:** AGPL-3.0 (pending final confirmation)
|
||||
**Repository:** Forgejo (self-hosted)
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
Tabulae Aeriae is a self-hostable, web-based pilot logbook system designed to provide FAA-compliant flight record keeping, robust reporting, currency tracking, and data visualization. The system targets two tiers of users: a free open-source tier for non-commercial pilots (Part 91, recreational, student) and a paid professional tier for commercial operators (Part 135, Part 121).
|
||||
|
||||
The project is motivated by the absence of a quality open-source, self-hostable pilot logbook that provides genuine FAA regulatory compliance, one-click report generation for common FAA forms, and a data-driven currency tracking engine. Existing open-source projects (web-logbook, jetlog, flightlog) are either EASA-focused, passenger flight trackers, or lack the reporting depth that professional pilots require.
|
||||
|
||||
---
|
||||
|
||||
## 2. Objectives
|
||||
|
||||
### Primary Objectives (v1)
|
||||
|
||||
- Provide a complete FAA-compliant logbook meeting 14 CFR 61.51 requirements
|
||||
- Generate FAA Form 8710-1 Section III data with one-click reporting
|
||||
- Generate FAA medical (8500-8) total time and last-6-months data with one-click reporting
|
||||
- Track pilot currency under 14 CFR 61.57 (day, night, instrument, flight review) with real-time status display
|
||||
- Visualize flight data through maps (route visualization) and charts (time breakdowns, trends)
|
||||
- Support self-hosted deployment on low-resource hardware (Raspberry Pi / Docker Swarm)
|
||||
- Maintain a complete audit trail of all logbook changes
|
||||
|
||||
### Secondary Objectives (v1)
|
||||
|
||||
- Provide a skeleton framework for future EASA (FCL.050) compliance
|
||||
- Support configurable, user-saveable custom reports for insurance, company, and personal use
|
||||
- Support CSV import with a pluggable adapter architecture for future format support
|
||||
- Track pilot qualifications, certificates, ratings, endorsements, and currency events in a unified table
|
||||
- Track medical certificate status with expiry countdowns
|
||||
|
||||
### Deferred Objectives (Paid Tier / Future Releases)
|
||||
|
||||
- PF/PNF (Pilot Flying / Pilot Not Flying) as first-class fields
|
||||
- IOE (Initial Operating Experience) tracking
|
||||
- High minimums captain status tracking
|
||||
- Duty/rest tracking (Part 117, Part 135 Subpart F)
|
||||
- Augmented operations crew management
|
||||
- Operator-specific currency rule overrides (61.57(e) exceptions)
|
||||
- GPS track import (KML/GPX) for precise route/night calculation
|
||||
- SQLite backend option for simplified single-user deployment
|
||||
- Full EASA compliance (FCL.050 fields, EASA PDF export formats)
|
||||
- OAuth2/OIDC/LDAP authentication providers
|
||||
|
||||
---
|
||||
|
||||
## 3. Technical Architecture
|
||||
|
||||
### Stack
|
||||
|
||||
| Layer | Technology | Rationale |
|
||||
|-------|-----------|-----------|
|
||||
| Backend | Go with Chi router | Low memory footprint, single binary, fast compilation, strong stdlib |
|
||||
| Database | PostgreSQL 16 | Robust, supports JSONB for flexible fields, trigger-based audit trail, already deployed on target infrastructure |
|
||||
| Frontend | React + Vite + Tailwind CSS | Rich interactivity for report builder, maps, charts; static build embedded in Go binary |
|
||||
| Charts | Recharts | React-native, lightweight, composable |
|
||||
| Maps | react-leaflet | React-native, lighter than OpenLayers, strong community |
|
||||
| Deployment | Docker (multi-stage build) | Single image containing Go binary with embedded frontend assets; no runtime dependencies on Node or Nginx |
|
||||
|
||||
### Deployment Target
|
||||
|
||||
Docker Swarm on Raspberry Pi cluster. Estimated resource consumption:
|
||||
|
||||
| Container | Memory Limit | CPU Limit |
|
||||
|-----------|-------------|-----------|
|
||||
| Tabulae Aeriae (Go + static assets) | 128 MB | 0.5 CPU |
|
||||
| PostgreSQL 16 Alpine | 256 MB | 0.5 CPU |
|
||||
| **Total** | **~384 MB** | **1.0 CPU** |
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
Multi-stage Docker build:
|
||||
1. **Stage 1 (Node):** Build React frontend with Vite → static assets
|
||||
2. **Stage 2 (Go):** Compile Go binary embedding frontend via `embed.FS`
|
||||
3. **Stage 3 (Alpine):** Minimal runtime image with binary + CA certificates + timezone data
|
||||
|
||||
Final image size: estimated 20–30 MB.
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Architecture
|
||||
|
||||
### Database Tables (v1 — 10 tables)
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | Pilot accounts, authentication, regulatory profile, tier |
|
||||
| `flights` | Core logbook entries — all time fields, approaches, custom fields |
|
||||
| `aircraft` | Real aircraft and simulators/FTDs — tail numbers, type, attributes, device info |
|
||||
| `airports` | Seeded from OurAirports; includes user-defined custom airports |
|
||||
| `airport_identifiers` | Maps current and former airport codes to physical airports |
|
||||
| `pilot_qualifications` | Certificates, ratings, endorsements, and currency events (IPC, flight review, checkrides) |
|
||||
| `medical_certificates` | Medical class, issue/expiry dates, examiner info |
|
||||
| `saved_reports` | User-saved report configurations with JSONB config |
|
||||
| `currency_rules` | JSON-driven currency rule definitions (system and user-defined) |
|
||||
| `audit_log` | Append-only changelog triggered by INSERT/UPDATE/DELETE on tracked tables |
|
||||
|
||||
### Key Schema Design Principles
|
||||
|
||||
- **All times stored as INTEGER minutes.** Floating-point hours introduce rounding errors that accumulate over a career. Rounding to display hours (truncate, round-nearest, or round-up) is applied only at the reporting layer, after aggregation.
|
||||
- **All timestamps stored as TIMESTAMPTZ in UTC.** No local time in storage or computation. Local time conversion is a display-layer concern only.
|
||||
- **No pre-aggregated compound categories.** Fields like "cross-country while under instruction" are derived at query time using `LEAST()` intersection logic. Pilots log atomic observations; the system computes intersections.
|
||||
- **JSONB for flexible structures.** Instrument approaches (variable count and type), custom fields (company/insurance-specific), and report configurations all use JSONB for schema flexibility without sacrificing queryability.
|
||||
- **Historical accuracy over normalization.** The `tail_number` is denormalized onto the flight record because aircraft registrations change. The `departure_airport` and `arrival_airport` store the code the pilot used at the time, even if that code has since been retired.
|
||||
- **Simulators and FTDs in the aircraft table.** Training devices are aircraft for logging purposes. A boolean `is_simulator` flag plus device-specific nullable columns (level, serial, approval number) avoid a separate table and dual foreign key complexity.
|
||||
|
||||
### Airport Data
|
||||
|
||||
Source: OurAirports (`davidmegginson/ourairports-data`), Unlicense. Over 78,000 airports worldwide, updated daily, includes closed airports.
|
||||
|
||||
Strategy: Embed a snapshot at build time for offline-capable first boot. Provide an admin command (`--refresh-airports`) or API endpoint to pull updated data on demand.
|
||||
|
||||
Historical code changes (e.g., 09G → KTEW) are handled by the `airport_identifiers` table, which maps multiple codes to a single physical airport with `is_current` and date-range fields.
|
||||
|
||||
---
|
||||
|
||||
## 5. Reporting
|
||||
|
||||
### Built-in Reports (One-Click)
|
||||
|
||||
| Report | Output |
|
||||
|--------|--------|
|
||||
| **FAA 8710-1 Section III** | Complete pilot time matrix — all categories, classes, and time fields. Derived at query time from atomic flight fields using `LEAST()` for compound categories. Rounded to whole hours per user preference. |
|
||||
| **FAA Medical (8500-8)** | Total all-time hours and total hours in last 6 calendar months. Two numbers, one click. |
|
||||
| **Currency Status** | Real-time status of all active currency rules — green/yellow/red with days remaining and remediation guidance. |
|
||||
|
||||
### Configurable Reports
|
||||
|
||||
Users can create and save custom reports by selecting date range (fixed, rolling, or all-time), filters (aircraft, category/class, airport), grouping (month, year, type, category/class), columns to include, and output format (on-screen, PDF, CSV).
|
||||
|
||||
Report configurations are stored as JSONB and can be pinned to the dashboard for one-click access.
|
||||
|
||||
### 8710 Derivation Method
|
||||
|
||||
Compound time categories are derived using `LEAST()` logic at query time:
|
||||
|
||||
- **XC Instruction Received** = `SUM(LEAST(cross_country, dual_received))` on flights where both > 0
|
||||
- **Night PIC/SIC** = `SUM(LEAST(night_time, pic_time + sic_time))` on flights where both > 0
|
||||
|
||||
This prevents the aggregation errors that occur when pilots manually pre-compute compound categories. The pilot logs atomic observations; the system handles the math.
|
||||
|
||||
Rounding is applied after aggregation (sum all minutes across all flights, then round to hours), not per-flight, to prevent cumulative rounding error.
|
||||
|
||||
---
|
||||
|
||||
## 6. Currency Tracking
|
||||
|
||||
### Engine Design
|
||||
|
||||
Currency rules are **data, not code.** Each rule is a JSON configuration stored in the `currency_rules` table. The engine evaluates rules against flight records and qualification events to produce real-time status.
|
||||
|
||||
### FAA Rules (v1)
|
||||
|
||||
| Rule | Regulation | Lookback | Requirements |
|
||||
|------|-----------|----------|-------------|
|
||||
| Day currency (per category/class/type) | 61.57(a) | 90 days rolling | 3 takeoffs + 3 landings |
|
||||
| Night currency (per category/class/type) | 61.57(b) | 90 days rolling | 3 night takeoffs + 3 night full-stop landings |
|
||||
| Instrument currency | 61.57(c) | 6 calendar months | 6 approaches + holding + tracking |
|
||||
| Instrument grace period | 61.57(c)/(d) | Months 7–12 after lapse | Same tasks to self-restore; IPC required after month 12 |
|
||||
| Flight review | 61.56 | 24 calendar months | Flight review, checkride, or WINGS phase |
|
||||
| Medical certificate | 61.23 | Per class/age | Valid medical not expired |
|
||||
|
||||
### Custom Rules
|
||||
|
||||
Users can define personal minimums, insurance requirements, or company-specific rules using the same engine.
|
||||
|
||||
### Night Definition Handling
|
||||
|
||||
The `night_time` field stores sunset-to-sunrise time (the standard logging definition). Night takeoff/landing classification for currency purposes uses the 1-hour-after-sunset to 1-hour-before-sunrise standard, auto-suggested from departure/arrival times and airport coordinates.
|
||||
|
||||
### Night Time Auto-Calculation
|
||||
|
||||
Solar position computed at intervals along the great circle route between departure and arrival airports. Accounts for aircraft movement relative to the terminator — critical for long-haul flights crossing significant longitude. Produces the sunset-to-sunrise value for logging. Always pilot-overridable.
|
||||
|
||||
---
|
||||
|
||||
## 7. Authentication & Authorization
|
||||
|
||||
### Architecture
|
||||
|
||||
Single auth system with a pluggable provider interface. No separate codepaths for free vs. commercial.
|
||||
|
||||
**v1 provider:** Username/password with bcrypt hashing and JWT token issuance.
|
||||
|
||||
**Future providers (same interface):** OAuth2/OIDC (Google, generic), LDAP. Adding a provider means implementing the `AuthProvider` interface — no changes to middleware, handlers, or session management.
|
||||
|
||||
### Feature Gating
|
||||
|
||||
Tier-based authorization middleware reads a `tier` field (free/professional) and gates access to paid-tier features. Auth and authorization are separate concerns — the auth system doesn't know about tiers.
|
||||
|
||||
---
|
||||
|
||||
## 8. Import / Export
|
||||
|
||||
### Import
|
||||
|
||||
Pluggable adapter architecture. Each adapter implements an `ImportAdapter` interface with `Detect()` (auto-format detection) and `Parse()` (conversion to internal model) methods.
|
||||
|
||||
**v1 adapter:** Generic CSV with user-defined column mapping.
|
||||
**Future adapters:** ForeFlight, LogTen Pro, Logbook Pro, AeroLog.
|
||||
|
||||
Import supports partial success (bad rows reported individually, good rows imported), duplicate detection, and detailed per-row error reporting.
|
||||
|
||||
### Export
|
||||
|
||||
- CSV: Full logbook export
|
||||
- PDF: Configurable column layout (user-defined logbook page format)
|
||||
|
||||
---
|
||||
|
||||
## 9. Visualization
|
||||
|
||||
### Flight Map
|
||||
|
||||
Interactive world map (react-leaflet) displaying great circle routes between airports. Filterable by date range, aircraft, and category/class.
|
||||
|
||||
### Statistics Dashboard
|
||||
|
||||
Recharts-based charts showing: time by category/class over time, cumulative hours, flights by month/year, time by aircraft type, airport frequency, and custom breakdowns.
|
||||
|
||||
---
|
||||
|
||||
## 10. Audit Trail
|
||||
|
||||
PostgreSQL trigger-based changelog. Every INSERT, UPDATE, and DELETE on tracked tables (`flights`, `aircraft`, `users`) writes a row to `audit_log` with the full old and new row values as JSONB. The `changed_by` user ID is captured via a PostgreSQL session variable set by the application middleware.
|
||||
|
||||
Enables: full reconstruction of any record's history at any point in time, regulatory compliance documentation, and dispute resolution for commercial operators.
|
||||
|
||||
---
|
||||
|
||||
## 11. EASA Skeleton
|
||||
|
||||
FAA compliance is the v1 target. EASA support is prepared via:
|
||||
|
||||
- A `regulatory_profile` field on the user record (values: `faa`, `easa`, `both`)
|
||||
- Nullable EASA-specific columns on `flights` and `aircraft` tables (multi-pilot time, co-pilot time, SPSE/SPME, function type, etc.)
|
||||
- These columns are hidden in the UI when `regulatory_profile = 'faa'`
|
||||
- Full EASA implementation is a future release — the schema is ready, the UI and reporting are not
|
||||
|
||||
---
|
||||
|
||||
## 12. Out of Scope (v1)
|
||||
|
||||
- Duty/rest tracking (Part 117, Part 135 Subpart F)
|
||||
- Augmented operations / multi-crew scheduling
|
||||
- PF/PNF as first-class schema fields (available via custom_fields JSONB)
|
||||
- IOE tracking as a first-class field
|
||||
- High minimums captain tracking
|
||||
- Operator-specific currency overrides (61.57(e) exceptions)
|
||||
- GPS track import (KML/GPX)
|
||||
- SQLite backend
|
||||
- Full EASA compliance
|
||||
- OAuth2/OIDC/LDAP authentication
|
||||
- Mobile-native applications (responsive web only)
|
||||
- Integration with external services (ForeFlight, Garmin Pilot, etc.)
|
||||
- Electronic endorsement/signature system
|
||||
- Multi-tenant SaaS hosting infrastructure
|
||||
|
||||
---
|
||||
|
||||
## 13. Risks and Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|-----------|
|
||||
| FAA regulatory changes | Currency rules or reporting requirements change | Rules are data-driven JSON configs; updates are database migrations, not code changes |
|
||||
| PostgreSQL resource consumption on Pi | Exceeds memory limits under load | Alpine image, connection pooling, aggressive `work_mem` tuning; schema is simple (10 tables) |
|
||||
| Airport data staleness | New airports or code changes not reflected | Build-time embed with on-demand refresh command; quarterly refresh cadence is sufficient |
|
||||
| Night time calculation accuracy | Solar algorithm produces incorrect results for edge cases | Algorithm uses well-established NOAA equations; pilot always has manual override; auto-calculation is a suggestion, not a mandate |
|
||||
| Scope creep from professional tier features | v1 becomes unshippable | Professional features are explicitly deferred and documented; schema is designed to accommodate them without destructive migrations |
|
||||
Loading…
Reference in a new issue