Compare commits

..

No commits in common. "b52a68edb3481e5c990bfc1067257ab2077013b1" and "9f23db6237d7d451b38348221d47f045f3aa292d" have entirely different histories.

2 changed files with 0 additions and 516 deletions

View file

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

View file

@ -1,514 +0,0 @@
# 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.