commit fc758d2a91f6aae62f03cb3c2a61565627de7ab9 Author: handfly Date: Sat May 2 21:24:40 2026 -0400 Initial Commit diff --git a/.directory b/.directory new file mode 100644 index 0000000..9dd830b --- /dev/null +++ b/.directory @@ -0,0 +1,2 @@ +[Desktop Entry] +Icon=orange-folder-git diff --git a/architecture-overview.md b/architecture-overview.md new file mode 100644 index 0000000..acef8ee --- /dev/null +++ b/architecture-overview.md @@ -0,0 +1,514 @@ +# Pilot Logbook — Architecture Overview + +## Project Summary + +A self-hostable, web-based pilot logbook system. FAA-compliant (14 CFR 61.51) by default, with optional EASA compliance as a configurable profile. Designed for low-resource deployment (Raspberry Pi / Docker Swarm). PostgreSQL backend, Go API server, lightweight web frontend. + +**Licensing model:** Free open-source tier for non-commercial pilots (Part 91, recreational, student). Paid tier for professional pilots (Part 135, 121) with crew management, duty/rest calculations, and advanced compliance features. + +--- + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────┐ +│ Browser │ +│ (React + Tailwind or HTMX) │ +└──────────────────────┬──────────────────────────┘ + │ HTTPS (JSON API) + ▼ +┌─────────────────────────────────────────────────┐ +│ Go API Server │ +│ │ +│ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ Auth/RBAC │ │ Logbook │ │ Reporting │ │ +│ │ Middleware │ │ Service │ │ Engine │ │ +│ └───────────┘ └──────────┘ └───────────────┘ │ +│ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │ +│ │ Aircraft │ │ Airport │ │ Audit Trail │ │ +│ │ Service │ │ Service │ │ Service │ │ +│ └───────────┘ └──────────┘ └───────────────┘ │ +│ ┌───────────┐ ┌──────────┐ │ +│ │ Currency │ │ Medical │ │ +│ │ Tracker │ │ Reporter │ │ +│ └───────────┘ └──────────┘ │ +│ │ +│ DB Interface (database/sql) │ +└──────────────────────┬──────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ PostgreSQL │ + │ │ + │ Audit triggers │ + │ installed on │ + │ tracked tables │ + └─────────────────┘ +``` + +### Key Design Decisions + +- **Go standard library router (net/http) or Chi** — Chi is minimal and well-proven. No heavy frameworks. Keeps binary small. +- **database/sql with pgx driver** — Direct SQL, no ORM. Migrations handled by golang-migrate or a simple embedded migration system. +- **Audit trail via PostgreSQL triggers** — Changes to logbook entries, aircraft, and user data are captured automatically at the DB level. The application doesn't need to remember to log; the trigger handles it. +- **Frontend TBD** — Two reasonable paths: (1) React + Tailwind (like jetlog) for a modern SPA, or (2) Go templates + HTMX for a server-rendered approach that eliminates the Node.js build step entirely. The HTMX path is simpler to deploy, lighter, and arguably better suited to a Pi. Worth discussing. + +--- + +## Database Schema (Core Tables) + +### Users & Auth + +```sql +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username VARCHAR(100) UNIQUE NOT NULL, + email VARCHAR(255) UNIQUE, + password_hash TEXT NOT NULL, + role VARCHAR(20) NOT NULL DEFAULT 'pilot', -- pilot, admin + pilot_cert_num VARCHAR(50), -- FAA certificate number + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +### Aircraft + +```sql +CREATE TABLE aircraft ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + tail_number VARCHAR(20) NOT NULL, + type_code VARCHAR(10), -- ICAO type designator (e.g., C172) + make_model VARCHAR(100) NOT NULL, -- Cessna 172S Skyhawk SP + category_class VARCHAR(50) NOT NULL, -- ASEL, AMEL, RH, Glider, etc. + is_complex BOOLEAN DEFAULT FALSE, + is_high_perf BOOLEAN DEFAULT FALSE, + is_tailwheel BOOLEAN DEFAULT FALSE, + is_taa BOOLEAN DEFAULT FALSE, -- Technologically Advanced Aircraft + gear_type VARCHAR(20), -- fixed_tri, retract, conventional, float, etc. + engine_type VARCHAR(20), -- piston, turboprop, jet, electric + num_engines SMALLINT DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE(user_id, tail_number) +); +``` + +### Airports + +```sql +-- Seeded from OurAirports or similar open dataset +-- Users can also add custom/private strips +CREATE TABLE airports ( + id SERIAL PRIMARY KEY, + icao_code VARCHAR(4), + faa_code VARCHAR(4), -- FAA LID (e.g., JFK, 7B2) + iata_code VARCHAR(3), + name VARCHAR(200) NOT NULL, + city VARCHAR(100), + state_region VARCHAR(100), + country VARCHAR(2), + latitude DECIMAL(10, 7), + longitude DECIMAL(10, 7), + elevation_ft INTEGER, + type VARCHAR(30), -- large_airport, medium_airport, small_airport, heliport, seaplane_base, closed + is_user_defined BOOLEAN DEFAULT FALSE, + user_id UUID REFERENCES users(id), -- NULL for system airports + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_airports_icao ON airports(icao_code); +CREATE INDEX idx_airports_faa ON airports(faa_code); +``` + +### Flight Log Entries (the core table) + +This is the big one. Columns are driven by FAA 14 CFR 61.51 requirements plus common practical fields. + +```sql +CREATE TABLE flights ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + + -- Date & Route + flight_date DATE NOT NULL, + departure_airport VARCHAR(10) NOT NULL, -- ICAO or FAA LID + arrival_airport VARCHAR(10) NOT NULL, + route TEXT, -- free-text route of flight + + -- Aircraft + aircraft_id UUID REFERENCES aircraft(id), + tail_number VARCHAR(20) NOT NULL, -- denormalized for historical accuracy + + -- Times (stored in minutes for easy math; displayed as HH:MM) + total_time INTEGER NOT NULL DEFAULT 0, -- total flight time + pic_time INTEGER DEFAULT 0, + sic_time INTEGER DEFAULT 0, + solo_time INTEGER DEFAULT 0, + dual_received INTEGER DEFAULT 0, + dual_given INTEGER DEFAULT 0, -- CFI logging instruction given + cross_country INTEGER DEFAULT 0, + night_time INTEGER DEFAULT 0, + + -- Instrument + actual_instrument INTEGER DEFAULT 0, + simulated_instrument INTEGER DEFAULT 0, + holds SMALLINT DEFAULT 0, + + -- Instrument Approaches (stored as JSONB for flexibility) + -- e.g., [{"type": "ILS", "airport": "KTPA", "runway": "19L"}, ...] + approaches JSONB DEFAULT '[]', + + -- Landings + day_landings SMALLINT DEFAULT 0, + night_landings SMALLINT DEFAULT 0, + day_full_stop SMALLINT DEFAULT 0, -- needed for tailwheel currency + night_full_stop SMALLINT DEFAULT 0, -- needed for night currency + + -- Simulator / Training Device + simulator_time INTEGER DEFAULT 0, + sim_type VARCHAR(20), -- FTD, AATD, BATD, FFS + + -- Ground Training + ground_training INTEGER DEFAULT 0, + + -- People + persons_on_board TEXT, -- free text or JSONB + instructor_name VARCHAR(100), + instructor_cert_num VARCHAR(50), + + -- Remarks & Endorsements + remarks TEXT, + + -- Metadata + is_checkride BOOLEAN DEFAULT FALSE, + is_ipc BOOLEAN DEFAULT FALSE, -- instrument proficiency check + is_bfr BOOLEAN DEFAULT FALSE, -- biennial flight review (now "flight review") + flight_review BOOLEAN DEFAULT FALSE, -- alias for BFR per 61.56 + + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_flights_user_date ON flights(user_id, flight_date DESC); +CREATE INDEX idx_flights_user_aircraft ON flights(user_id, aircraft_id); +``` + +### Audit Log + +```sql +CREATE TABLE audit_log ( + id BIGSERIAL PRIMARY KEY, + table_name VARCHAR(50) NOT NULL, + record_id UUID NOT NULL, + action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE + field_name VARCHAR(100), -- NULL for INSERT/DELETE + old_value TEXT, + new_value TEXT, + changed_by UUID REFERENCES users(id), + changed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_audit_record ON audit_log(table_name, record_id); +CREATE INDEX idx_audit_time ON audit_log(changed_at); + +-- Trigger function (simplified; real version would iterate over changed columns) +CREATE OR REPLACE FUNCTION audit_trigger_fn() +RETURNS TRIGGER AS $$ +BEGIN + IF TG_OP = 'INSERT' THEN + INSERT INTO audit_log(table_name, record_id, action, new_value, changed_at) + VALUES (TG_TABLE_NAME, NEW.id, 'INSERT', row_to_json(NEW)::TEXT, now()); + RETURN NEW; + ELSIF TG_OP = 'UPDATE' THEN + INSERT INTO audit_log(table_name, record_id, action, old_value, new_value, changed_at) + VALUES (TG_TABLE_NAME, NEW.id, 'UPDATE', row_to_json(OLD)::TEXT, row_to_json(NEW)::TEXT, now()); + RETURN NEW; + ELSIF TG_OP = 'DELETE' THEN + INSERT INTO audit_log(table_name, record_id, action, old_value, changed_at) + VALUES (TG_TABLE_NAME, OLD.id, 'DELETE', row_to_json(OLD)::TEXT, now()); + RETURN OLD; + END IF; +END; +$$ LANGUAGE plpgsql; + +-- Attach to flights table +CREATE TRIGGER flights_audit + AFTER INSERT OR UPDATE OR DELETE ON flights + FOR EACH ROW EXECUTE FUNCTION audit_trigger_fn(); +``` + +### Saved Reports + +```sql +CREATE TABLE saved_reports ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + name VARCHAR(100) NOT NULL, -- "8710 Application", "Insurance Renewal", "Annual Totals" + description TEXT, + report_type VARCHAR(30) NOT NULL, -- summary, detailed, 8710, medical, currency, custom + config JSONB NOT NULL, -- filters, date ranges, grouping, columns to include + is_quick_access BOOLEAN DEFAULT FALSE, -- pin to dashboard for one-click access + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +### Currency Tracking + +```sql +-- Predefine standard FAA currency rules; users can add custom ones +CREATE TABLE currency_rules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), -- NULL = system-defined + name VARCHAR(100) NOT NULL, -- "Night Currency (61.57a)", "IFR Currency (61.57c)" + regulation_ref VARCHAR(50), -- "61.57(a)", "61.57(c)", etc. + rule_config JSONB NOT NULL, -- defines the lookback window, required counts, etc. + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Example rule_config for night passenger currency: +-- { +-- "lookback_days": 90, +-- "required": { +-- "night_full_stop": 3 +-- }, +-- "applies_to": { +-- "category_class": ["ASEL", "AMEL"] +-- } +-- } +``` + +### Medical Certificates (for the one-click medical report) + +```sql +CREATE TABLE medical_certificates ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + class SMALLINT NOT NULL, -- 1, 2, 3 + issue_date DATE NOT NULL, + expiry_date DATE NOT NULL, -- calculated per 61.23 based on age + class + examiner_name VARCHAR(100), + notes TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +``` + +--- + +## Reporting Engine Design + +The goal is to make reporting maximally flexible without requiring users to write SQL. + +### Built-in Report Types + +| Report | Description | Key Fields | +|--------|-------------|------------| +| **8710** | FAA Airman Certificate application totals | Total time, PIC, SIC, XC, night, instrument (actual/sim), category/class breakdowns | +| **Medical** | Total time + last 6 months | One-click: total all-time hours, total hours in last 6 calendar months | +| **Currency** | Current status of all active currency rules | Green/yellow/red status per rule, days remaining, what's needed to regain currency | +| **Insurance** | Configurable — typically total time, time in type, retract time, last 90 days | User defines which fields matter | +| **Period Summary** | Totals for any date range | All time categories summed for the range | +| **By Aircraft** | Breakdown by tail number or type | Time per aircraft/type with sub-totals | + +### Custom Report Builder + +Users can create a saved report by selecting: +1. **Date range** — fixed dates, rolling window (last N days/months), or all-time +2. **Filters** — by aircraft, category/class, airport, tags +3. **Grouping** — by month, year, aircraft type, category/class +4. **Columns** — which time fields to include +5. **Output format** — on-screen table, PDF, CSV + +The `saved_reports.config` JSONB column stores all of this, so reconstructing the report is just loading the config and re-running the query. + +### One-Click Reports + +Certain reports (8710 totals, medical totals, currency status) are flagged as `is_quick_access = true` and rendered as dashboard widgets or a single button press. The medical report specifically: + +- **Total time (all time)** — sum of `total_time` across all flights +- **Last 6 calendar months** — sum of `total_time` where `flight_date >= first day of (current month - 6)` + +No configuration needed, no date pickers. One click, two numbers. + +--- + +## API Structure (Go) + +``` +/api/v1/ +├── auth/ +│ ├── POST /login +│ ├── POST /logout +│ └── POST /refresh +│ +├── flights/ +│ ├── GET / -- list (paginated, filterable) +│ ├── POST / -- create +│ ├── GET /:id -- get one +│ ├── PUT /:id -- update +│ ├── DELETE /:id -- delete +│ └── POST /import -- CSV/bulk import +│ +├── aircraft/ +│ ├── GET / +│ ├── POST / +│ ├── GET /:id +│ ├── PUT /:id +│ └── DELETE /:id +│ +├── airports/ +│ ├── GET / -- search/autocomplete +│ ├── GET /:code -- lookup by ICAO or FAA LID +│ └── POST / -- add custom airport +│ +├── reports/ +│ ├── GET /8710 -- 8710 totals +│ ├── GET /medical -- medical totals (one-click) +│ ├── GET /currency -- all currency statuses +│ ├── GET /summary?from=&to= -- period summary +│ ├── POST /custom -- run a custom report from config +│ ├── GET /saved -- list saved reports +│ ├── POST /saved -- save a report config +│ ├── PUT /saved/:id -- update saved report +│ └── DELETE /saved/:id -- delete saved report +│ +├── audit/ +│ ├── GET / -- paginated audit log +│ └── GET /record/:table/:id -- audit history for a specific record +│ +├── medical/ +│ ├── GET / +│ ├── POST / +│ └── GET /status -- current medical validity +│ +└── export/ + ├── GET /csv -- full logbook CSV export + └── GET /pdf -- PDF export (FAA format) +``` + +--- + +## Go Project Layout + +``` +/ +├── cmd/ +│ └── server/ +│ └── main.go -- entry point +├── internal/ +│ ├── config/ -- env var parsing, app config +│ ├── database/ +│ │ ├── migrations/ -- SQL migration files +│ │ ├── postgres.go -- connection setup +│ │ └── migrate.go -- migration runner +│ ├── models/ -- struct definitions +│ │ ├── flight.go +│ │ ├── aircraft.go +│ │ ├── airport.go +│ │ ├── user.go +│ │ └── report.go +│ ├── handlers/ -- HTTP handlers (one file per resource) +│ │ ├── flights.go +│ │ ├── aircraft.go +│ │ ├── reports.go +│ │ └── auth.go +│ ├── services/ -- business logic +│ │ ├── flights.go +│ │ ├── currency.go +│ │ ├── reporting.go +│ │ └── audit.go +│ ├── middleware/ +│ │ ├── auth.go +│ │ ├── logging.go +│ │ └── cors.go +│ └── seed/ -- airport data seeding +│ └── airports.go +├── web/ -- frontend assets (if using HTMX/templates) +│ ├── templates/ +│ └── static/ +├── Dockerfile +├── docker-compose.yml +├── go.mod +└── go.sum +``` + +--- + +## Deployment (Docker Compose for Swarm) + +```yaml +version: "3.8" + +services: + logbook: + image: yourproject/logbook:latest + ports: + - "8080:8080" + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: logbook + DB_USER: logbook + DB_PASSWORD_FILE: /run/secrets/db_password + JWT_SECRET_FILE: /run/secrets/jwt_secret + LOG_LEVEL: info + secrets: + - db_password + - jwt_secret + deploy: + replicas: 1 + resources: + limits: + memory: 128M # Go binary + serving requests + cpus: "0.5" + depends_on: + - postgres + + postgres: + image: postgres:16-alpine + volumes: + - pgdata:/var/lib/postgresql/data + environment: + POSTGRES_DB: logbook + POSTGRES_USER: logbook + POSTGRES_PASSWORD_FILE: /run/secrets/db_password + secrets: + - db_password + deploy: + resources: + limits: + memory: 256M + cpus: "0.5" + +volumes: + pgdata: + +secrets: + db_password: + external: true + jwt_secret: + external: true +``` + +Estimated total memory footprint: **~200-300MB** for both containers on a Pi. The Go binary itself will likely be 15-30MB and use ~20-50MB of RAM under normal load. + +--- + +## Open Questions / Next Steps + +1. **Frontend approach** — React SPA vs. Go templates + HTMX? HTMX eliminates the Node build pipeline entirely, keeps the Docker image smaller, and is arguably simpler. React gives richer interactivity for things like the report builder and map visualizations. A hybrid is possible (HTMX for most pages, a small JS bundle for the map/charts). + +2. **Project name** — Needed before creating the repo. + +3. **Map/visualization library** — OpenLayers (heavier, more capable) vs. Leaflet (lighter) vs. something React-based like react-simple-maps. Depends on frontend choice. + +4. **EASA profile** — How deep to go in v1? Could be as simple as alternative column labels and a different PDF export layout, or a full parallel schema with EASA-specific fields (multi-pilot time, SPSE/SPME columns, etc.). + +5. **Import formats** — ForeFlight CSV is probably the most common for FAA pilots. LogTen Pro is another big one. Both have documented export formats. + +6. **Authentication** — JWT for API, but also support OAuth2 (Google, etc.) for the hosted/multi-user scenario? Or just username/password for v1? + +7. **License choice** — Apache 2.0, MIT, or AGPL? AGPL would prevent someone from running a competing hosted service without open-sourcing their changes, which aligns with the free/paid tier model.